summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordautor <karlo98.m@gmail.com>2024-11-16 13:22:54 +0100
committerdautor <karlo98.m@gmail.com>2024-11-16 17:54:38 +0100
commit47778ccd67cbb3fb70dda706911d3166038ca010 (patch)
tree906bf0537d14f5ce8e2528736fb89a3499ada214
Import project
-rw-r--r--.gitignore3
-rwxr-xr-xbuild.sh21
-rw-r--r--src/base.h118
-rw-r--r--src/base/main.c797
-rw-r--r--src/base/state.c380
-rw-r--r--src/base/state.h72
-rw-r--r--src/jprint.c176
-rw-r--r--src/jprint.h26
-rw-r--r--src/lamina/main.c583
-rw-r--r--src/module/module.c126
-rw-r--r--src/module/module.h14
-rw-r--r--src/sf.bridge/main.c166
-rw-r--r--src/sf.bridge/state.c58
-rw-r--r--src/sf.bridge/state.h20
-rw-r--r--src/sf.eiface/main.c155
-rw-r--r--src/sf.eiface/state.c63
-rw-r--r--src/sf.eiface/state.h21
-rw-r--r--src/sf.jail/main.c525
-rw-r--r--src/sf.jail/state.c396
-rw-r--r--src/sf.jail/state.h93
-rw-r--r--src/sf.skel/main.c116
-rw-r--r--src/sf.skel/state.c49
-rw-r--r--src/sf.skel/state.h19
-rw-r--r--src/util.c396
-rw-r--r--src/util.h28
-rw-r--r--todo.org113
26 files changed, 4534 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a04fdd5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+getmntopts.o
+sf.*
+!src/sf.*
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..e60f258
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -e
+
+CC=cc
+SAN="$SAN -fsanitize=address -fsanitize=undefined -fsanitize=leak"
+CFLAGS="$CFLAGS -g -O0 -DENABLE_ASSERT=1 -DENABLE_DEBUG=1"
+
+CFLAGS="$CFLAGS -Wall -Wextra -Weverything -pedantic"
+CFLAGS="$CFLAGS -Wno-unsafe-buffer-usage -Wno-gnu-zero-variadic-macro-arguments -Wno-format-non-iso -Wno-declaration-after-statement -Wno-padded -Wno-covered-switch-default -Wno-gnu-empty-struct -Wno-c++-compat -Wno-alloca -Wno-zero-length-array"
+
+CFLAGS="$CFLAGS -I/usr/local/include"
+LDFLAGS="$LDFLAGS -L/usr/local/lib -lucl -lnetgraph -ljail -lutil getmntopts.o"
+
+$CC -c /usr/src/sbin/mount/getmntopts.c -o getmntopts.o
+
+for i in $(ls -d src/sf.*); do
+ $CC $CFLAGS $SAN src/*.c src/module/module.c $i/*.c $LDFLAGS -o $(basename $i)
+done
+
+$CC $CFLAGS $SAN src/*.c src/base/*.c $LDFLAGS $LIBS -o sf.base
+$CC $CFLAGS $SAN src/*.c src/lamina/*.c $LDFLAGS $LIBS -o sf.lamina
diff --git a/src/base.h b/src/base.h
new file mode 100644
index 0000000..83d086c
--- /dev/null
+++ b/src/base.h
@@ -0,0 +1,118 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <sysexits.h>
+#include <errno.h>
+#include <string.h>
+#include <err.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <ctype.h>
+
+typedef uint64_t u64;
+typedef uint32_t u32;
+typedef uint16_t u16;
+typedef uint8_t u8;
+typedef int64_t s64;
+typedef int32_t s32;
+typedef int16_t s16;
+typedef int8_t s8;
+typedef float f32;
+typedef double f64;
+typedef int fd;
+typedef int jid_t;
+
+#define ArrayCount(x) (sizeof(x) / sizeof(*(x)))
+
+#define PARENS ()
+
+#define EVAL3(...) EVAL2(EVAL2(EVAL2(EVAL2(__VA_ARGS__))))
+#define EVAL2(...) EVAL1(EVAL1(EVAL1(EVAL1(__VA_ARGS__))))
+#define EVAL1(...) EVAL0(EVAL0(EVAL0(EVAL0(__VA_ARGS__))))
+#define EVAL0(...) __VA_ARGS__
+
+#define FOR_EACH(f, ...) __VA_OPT__(EVAL3(FOR_EACH_STEP(f, __VA_ARGS__)))
+#define FOR_EACH_STEP(f, x, ...) f(x) __VA_OPT__(FOR_EACH_AGAIN PARENS(f, __VA_ARGS__))
+#define FOR_EACH_AGAIN() FOR_EACH_STEP
+
+#define FOR_EACH_1(f, s, ...) __VA_OPT__(EVAL3(FOR_EACH_1_STEP(f, s __VA_ARGS__)))
+#define FOR_EACH_1_STEP(f, s, x, ...) f(s, x) __VA_OPT__(FOR_EACH_1_AGAIN PARENS(f, s, __VA_ARGS__))
+#define FOR_EACH_1_AGAIN() FOR_EACH_1_STEP
+
+#define DO_PRAGMA(x) _Pragma(#x)
+#define WARNINGS_RESTORE DO_PRAGMA(clang diagnostic pop)
+#define WARNINGS_IGNORE_STEP(x) DO_PRAGMA(clang diagnostic ignored x)
+#define WARNINGS_IGNORE(...) \
+ DO_PRAGMA(clang diagnostic push) \
+ FOR_EACH(WARNINGS_IGNORE_STEP, __VA_ARGS__)
+
+#define UNUSED(...) (void)sizeof(__VA_ARGS__ __VA_OPT__(, ) 0) // NOLINT(bugprone-sizeof-expression)
+
+#define OffsetOf(x, y) __builtin_offsetof(x, y)
+#define ContainerOf(Member, type, member) (type *)(void *)((u8 *)(Member)-OffsetOf(type, member))
+
+WARNINGS_IGNORE("-Wdocumentation")
+#include <ucl.h>
+WARNINGS_RESTORE
+
+#define UCL_CHECK_HELPER_INT "an integer"
+#define UCL_CHECK_HELPER_STRING "a string"
+#define UCL_CHECK_HELPER_OBJECT "an object"
+#define UCL_CHECK_HELPER_ARRAY "an array"
+#define UCL_CHECK_HELPER_BOOLEAN "a boolean"
+#define UCL_CHECK_HELPER_(x) UCL_CHECK_HELPER_##x
+#define UCL_CHECK_HELPER(x) UCL_CHECK_HELPER_(x)
+#define UCL_CHECK_ROOT(type_) \
+ do { \
+ if(root == NULL || root->type != UCL_##type_) \
+ { \
+ if(*Position == '\0') Position = "."; \
+ asprintf(&Error, "%s is not " UCL_CHECK_HELPER(type_), Position); \
+ goto error; \
+ } \
+ } while(0)
+#define UCL_CHECK(x, type_) \
+ do { \
+ if(x == NULL || x->type != UCL_##type_) \
+ { \
+ asprintf(&Error, "%s." #x " is not " UCL_CHECK_HELPER(type_), Position); \
+ goto error; \
+ } \
+ } while(0)
+#define UCL_CHECK_OPTIONAL(x, type_) \
+ do { \
+ if(x != NULL && x->type != UCL_##type_) \
+ { \
+ asprintf(&Error, "%s." #x " is not " UCL_CHECK_HELPER(type_), Position); \
+ goto error; \
+ } \
+ } while(0)
+
+#define CONCAT_ENUM_NAMES(a, b) a##_##b,
+#define STRING_(...) #__VA_ARGS__
+#define STRING(...) STRING_(__VA_ARGS__)
+#define LISTIFY(...) __VA_ARGS__,
+#define STRING_LISTIFY(...) LISTIFY(STRING(__VA_ARGS__))
+#define REQUIRE_SEMICOLON_(x) struct dummy_struct_##x
+#define REQUIRE_SEMICOLON(x) REQUIRE_SEMICOLON_(x)
+#define NAMED_ENUM(x, ...) \
+ enum \
+ { \
+ FOR_EACH_1(CONCAT_ENUM_NAMES, x, __VA_OPT__(, ) __VA_ARGS__) x##_COUNT \
+ } typedef x; \
+ WARNINGS_IGNORE("-Wunused-variable") \
+ static char const *x##_Names[] = { FOR_EACH(STRING_LISTIFY __VA_OPT__(, ) __VA_ARGS__) }; \
+ WARNINGS_RESTORE \
+ REQUIRE_SEMICOLON(x)
+
+static inline bool
+isprints(char const *String)
+{
+ for(char const *At = String; *At; ++At)
+ if(isprint(*At) == false) return false;
+ return true;
+}
diff --git a/src/base/main.c b/src/base/main.c
new file mode 100644
index 0000000..27ef1c2
--- /dev/null
+++ b/src/base/main.c
@@ -0,0 +1,797 @@
+#include "state.h"
+#include <netgraph.h>
+
+static char const *Arg0;
+
+static size_t
+FindNodeIndex(experiment *E, char const *Name)
+{
+ for(size_t i = 0; i < E->NodeCount; ++i)
+ if(strcmp(E->Node[i].Name, Name) == 0) return i;
+ return SIZE_MAX;
+}
+
+static size_t
+FindLinkIndex(experiment *E, char const *Name)
+{
+ for(size_t i = 0; i < E->LinkCount; ++i)
+ if(strcmp(E->Link[i].Name, Name) == 0) return i;
+ return SIZE_MAX;
+}
+
+struct
+{
+ bool Called;
+ int Status;
+ ucl_object_t *Data;
+} typedef call_module_result;
+
+static call_module_result
+CallModule_a(char const *Module, char **Arg)
+{
+ call_module_result Result;
+ bzero(&Result, sizeof(Result));
+ Result.Called = false;
+#define RD 0
+#define WR 1
+ fd PipeI[2];
+ PipeI[RD] = -1;
+ PipeI[WR] = -1;
+
+ pid_t PID = -1;
+ struct ucl_parser *Parser = NULL;
+ if(pipe(PipeI) == -1)
+ {
+ fprintf(stderr, "pipe failed: %s\n", strerror(errno));
+ goto error;
+ }
+
+ PID = fork();
+ if(PID == -1)
+ {
+ fprintf(stderr, "fork failed: %s\n", strerror(errno));
+ goto error;
+ }
+ if(PID == 0)
+ {
+ close(STDIN_FILENO);
+ dup2(PipeI[WR], STDOUT_FILENO);
+ close(PipeI[RD]);
+ close(PipeI[WR]);
+ execvp(Module, Arg);
+ fprintf(stderr, "exec failed: %s\n", strerror(errno));
+ exit(-1);
+ }
+ close(PipeI[WR]);
+
+ // Read
+ Parser = ucl_parser_new(0);
+ unsigned char Buffer[8192]; // XXX: libucl currently does not handle partial objects
+ while(true)
+ {
+ ssize_t Count = read(PipeI[RD], Buffer, sizeof(Buffer));
+ if(Count == -1) break;
+ if(Count == 0) break;
+ if(ucl_parser_add_chunk(Parser, Buffer, (size_t)Count) == false)
+ {
+ fprintf(stderr, "Unable to parse configuration file: %s\n", ucl_parser_get_error(Parser));
+ goto error;
+ }
+ }
+ close(PipeI[RD]);
+ Result.Called = true;
+ Result.Data = ucl_parser_get_object(Parser);
+ ucl_parser_free(Parser);
+ waitpid(PID, &Result.Status, 0);
+ return Result;
+error:
+ if(Parser != NULL) ucl_parser_free(Parser);
+ if(Result.Data != NULL) ucl_object_unref(Result.Data);
+ if(Result.Called == true) waitpid(PID, &Result.Status, 0);
+ if(PipeI[0] != -1) close(PipeI[0]);
+ if(PipeI[1] != -1) close(PipeI[1]);
+ return Result;
+#undef RD
+#undef WR
+}
+
+static call_module_result
+CallModule_v(char const *Module, va_list Arg)
+{
+ va_list Arg_;
+ va_copy(Arg_, Arg);
+ size_t ArgCount = 0;
+ do {
+ char const *_ = va_arg(Arg_, char const *);
+ ++ArgCount;
+ if(_ == NULL) break;
+ } while(true);
+ va_end(Arg_);
+ char **Args = alloca(sizeof(char *) * ArgCount);
+ for(size_t i = 0; i < ArgCount; ++i) Args[i] = va_arg(Arg, char *);
+ return CallModule_a(Module, Args);
+}
+
+static call_module_result
+CallModule(char const *Module, ...)
+{
+ va_list Arg;
+ va_start(Arg, Module);
+ call_module_result Result = CallModule_v(Module, Arg);
+ va_end(Arg);
+ return Result;
+}
+
+static char *
+CallModuleBuffer(ucl_object_t *object)
+{
+ size_t const BufferSize = 1024 * 1024;
+ char *Buffer = calloc(BufferSize, 1);
+ if(Buffer == NULL)
+ {
+ fprintf(stderr, "calloc failed: %s\n", strerror(errno));
+ return NULL;
+ }
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = fmemopen(Buffer, BufferSize, "w");
+ if(S.F == NULL)
+ {
+ fprintf(stderr, "fmemopen failed: %s\n", strerror(errno));
+ free(Buffer);
+ return NULL;
+ }
+ JPrint_ucl_object_t(&S, object);
+ fclose(S.F);
+ return Buffer;
+}
+
+static endpoint
+GetEndpointAddress(experiment *E, endpoint_configuration *Endpoint)
+{
+ endpoint R;
+ bzero(&R, sizeof(R));
+ size_t NodeIndex = FindNodeIndex(E, Endpoint->Node);
+ if(NodeIndex == SIZE_MAX)
+ {
+ fprintf(stderr, "node %s does not exist\n", Endpoint->Node);
+ return R;
+ }
+ node *Node = E->Node + NodeIndex;
+ char const *Module = Node->Configuration.Module;
+ char *Configuration = CallModuleBuffer(Node->Configuration.Configuration);
+ if(Configuration == NULL) return R;
+ char *Data = CallModuleBuffer(Node->Data);
+ if(Data == NULL)
+ {
+ free(Configuration);
+ return R;
+ }
+ call_module_result Result =
+ CallModule(Module, Module, "get-endpoint", Node->Name, Configuration, Data, Endpoint->Interface, NULL);
+ free(Configuration);
+ free(Data);
+ if(Result.Called == false)
+ {
+ fprintf(stderr, "failed to call module %s\n", Module);
+ return R;
+ }
+ if(!WIFEXITED(Result.Status) || (WEXITSTATUS(Result.Status) != 0))
+ {
+ fprintf(stderr, "%s exited with status 0x%x\n", Module, Result.Status);
+ if(Result.Data != NULL)
+ {
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrint_ucl_object_t(&S, Result.Data);
+ ucl_object_unref(Result.Data);
+ }
+ return R;
+ }
+ if(Result.Data == NULL)
+ {
+ fprintf(stderr, "%s returned no data\n", Module);
+ return R;
+ }
+ char *Error = Parse_endpoint(&R, Result.Data, "");
+ ucl_object_unref(Result.Data);
+ if(Error != NULL)
+ {
+ fprintf(stderr, "link format error: %s\n", Error);
+ free(Error);
+ return R;
+ }
+ return R;
+}
+
+static int
+start_link_(link_ *R)
+{
+ fd Control;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ return -1;
+ }
+ struct ngm_connect D;
+ char Path0[NG_PATHSIZ];
+ strncpy(Path0, R->Peer[0].Address, sizeof(Path0));
+ strncpy(D.path, R->Peer[1].Address, sizeof(D.path));
+ strncpy(D.ourhook, R->Peer[0].Hook, sizeof(D.ourhook));
+ strncpy(D.peerhook, R->Peer[1].Hook, sizeof(D.peerhook));
+ if(NgSendMsg(Control, Path0, NGM_GENERIC_COOKIE, NGM_CONNECT, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr,
+ "ngctl connect %s %s %s %s: %s\n",
+ Path0,
+ D.path,
+ D.ourhook,
+ D.peerhook,
+ strerror(errno));
+ close(Control);
+ return -1;
+ }
+ close(Control);
+ return 0;
+}
+
+static int
+stop_link_(link_ *R)
+{
+ fd Control;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ return -1;
+ }
+ struct ngm_rmhook D;
+ char Path0[NG_PATHSIZ];
+ strncpy(Path0, R->Peer[0].Address, sizeof(Path0));
+ strncpy(D.ourhook, R->Peer[0].Hook, sizeof(D.ourhook));
+ if(NgSendMsg(Control, Path0, NGM_GENERIC_COOKIE, NGM_RMHOOK, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr, "ngctl rmhook %s %s: %s\n", Path0, D.ourhook, strerror(errno));
+ close(Control);
+ return -1;
+ }
+ close(Control);
+ return 0;
+}
+
+static int
+start_link(experiment *E,
+ char const *Name,
+ char const *NodeA,
+ char const *InterfaceA,
+ char const *NodeB,
+ char const *InterfaceB)
+{
+ if(FindLinkIndex(E, Name) != SIZE_MAX)
+ {
+ fprintf(stderr, "Link %s already exists\n", Name);
+ return -1;
+ }
+ link_configuration C;
+ {
+ C.Peer[0].Node = strdup(NodeA);
+ C.Peer[0].Interface = strdup(InterfaceA);
+ C.Peer[1].Node = strdup(NodeB);
+ C.Peer[1].Interface = strdup(InterfaceB);
+ }
+ link_ R;
+ { // Start
+ endpoint E0 = GetEndpointAddress(E, C.Peer + 0);
+ if(E0.Address == NULL || E0.Hook == NULL)
+ {
+ fprintf(stderr, "peer0 endpoint is invalid (%s, %s)\n", C.Peer[0].Node, C.Peer[0].Interface);
+ Free_link_configuration(&C);
+ return -1;
+ }
+ endpoint E1 = GetEndpointAddress(E, C.Peer + 1);
+ if(E1.Address == NULL || E1.Hook == NULL)
+ {
+ fprintf(stderr, "peer1 endpoint is invalid (%s, %s)\n", C.Peer[1].Node, C.Peer[1].Interface);
+ Free_link_configuration(&C);
+ Free_endpoint(&E0);
+ return -1;
+ }
+ bzero(&R, sizeof(R));
+ R.Peer[0] = E0;
+ R.Peer[1] = E1;
+ R.Configuration = C;
+ R.Name = strdup(Name);
+ if(start_link_(&R) == -1)
+ {
+ Free_link(&R);
+ return -1;
+ }
+ }
+ { // Insert
+ size_t LinkCount = E->LinkCount + 1;
+ link_ *Link = reallocarray(E->Link, LinkCount, sizeof(*Link));
+ if(Link == NULL)
+ {
+ fprintf(stderr, "Failed to reallcoate links: %s\n", strerror(errno));
+ fprintf(stderr, "Link is running, but its state will be lost... Dumping it to stderr:\n");
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stderr;
+ Save_link(&S, &R);
+ Free_link(&R);
+ return -1;
+ }
+ E->Link = Link;
+ E->LinkCount = LinkCount;
+ Link[LinkCount - 1] = R;
+ }
+ return 0;
+}
+
+static int
+stop_link(experiment *E, char const *Name)
+{
+ size_t LinkIndex = FindLinkIndex(E, Name);
+ if(LinkIndex == SIZE_MAX)
+ {
+ fprintf(stderr, "Link %s does not exists\n", Name);
+ return -1;
+ }
+ link_ *R = E->Link + LinkIndex;
+ if(stop_link_(R) == -1)
+ {
+ fprintf(stderr, "Failed to stop link %s\n", Name);
+ return -1;
+ }
+ Free_link(R);
+ memmove(E->Link + LinkIndex, E->Link + LinkIndex + 1, (E->LinkCount - LinkIndex - 1) * sizeof(link_));
+ --E->LinkCount;
+ return 0;
+}
+
+static void __attribute__((noreturn)) cmd(node *N, size_t ArgCount, char **Arg)
+{
+ char *Configuration = CallModuleBuffer(N->Configuration.Configuration);
+ if(Configuration == NULL) exit(-1);
+ char *Data = CallModuleBuffer(N->Data);
+ if(Data == NULL)
+ {
+ free(Configuration);
+ exit(-1);
+ }
+ size_t ExtraArguments = 5;
+ char **Args = alloca(sizeof(char *) * (ArgCount + ExtraArguments + 1));
+ Args[0] = N->Configuration.Module;
+ Args[1] = "cmd";
+ Args[2] = N->Name;
+ Args[3] = Configuration;
+ Args[4] = Data;
+ memcpy(Args + ExtraArguments, Arg, sizeof(char *) * ArgCount);
+ Args[ArgCount + ExtraArguments] = NULL;
+ execvp(N->Configuration.Module, Args);
+ fprintf(stderr, "execvp failed: %s\n", strerror(errno));
+ exit(-1);
+}
+
+static int
+start_node(experiment *E, char const *Name, ucl_object_t *root)
+{
+ if(FindNodeIndex(E, Name) != SIZE_MAX)
+ {
+ fprintf(stderr, "Node %s already exists\n", Name);
+ return -1;
+ }
+ node_configuration C;
+ { // Load
+ char *Error = Parse_node_configuration(&C, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "Node configuration error: %s\n", Error);
+ free(Error);
+ return -1;
+ }
+ }
+ node R;
+ { // Start
+ bzero(&R, sizeof(R));
+ char *Configuration = CallModuleBuffer(C.Configuration);
+ if(Configuration == NULL)
+ {
+ Free_node_configuration(&C);
+ return -1;
+ }
+ call_module_result Result = CallModule(C.Module, C.Module, "start", Name, Configuration, NULL);
+ free(Configuration);
+ if(Result.Called == false)
+ {
+ fprintf(stderr, "failed to call module %s\n", C.Module);
+ Free_node_configuration(&C);
+ return -1;
+ }
+ if(!WIFEXITED(Result.Status) || (WEXITSTATUS(Result.Status) != 0))
+ {
+ fprintf(stderr, "%s exited with status 0x%x\n", C.Module, Result.Status);
+ if(Result.Data != NULL)
+ {
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrint_ucl_object_t(&S, Result.Data);
+ ucl_object_unref(Result.Data);
+ }
+ Free_node_configuration(&C);
+ return -1;
+ }
+ if(Result.Data == NULL)
+ {
+ fprintf(stderr, "%s returned no data\n", C.Module);
+ Free_node_configuration(&C);
+ return -1;
+ }
+ R.Data = Result.Data;
+ R.Configuration = C;
+ R.Name = strdup(Name);
+ }
+ { // Insert
+ size_t NodeCount = E->NodeCount + 1;
+ node *Node = reallocarray(E->Node, NodeCount, sizeof(*Node));
+ if(Node == NULL)
+ {
+ fprintf(stderr, "Failed to reallcoate nodes: %s\n", strerror(errno));
+ fprintf(stderr, "Node is running, but its state will be lost... Dumping it to stderr:\n");
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stderr;
+ Save_node(&S, &R);
+ Free_node(&R);
+ return -1;
+ }
+ E->Node = Node;
+ E->NodeCount = NodeCount;
+ Node[NodeCount - 1] = R;
+ }
+ return 0;
+}
+
+static int
+stop_node(experiment *E, char const *Name)
+{
+ size_t NodeIndex = FindNodeIndex(E, Name);
+ if(NodeIndex == SIZE_MAX)
+ {
+ fprintf(stderr, "Node %s does not exists\n", Name);
+ return -1;
+ }
+ node *R = E->Node + NodeIndex;
+ { // Stop all links
+ for(size_t i = 0; i < E->LinkCount;)
+ {
+ link_ *L = E->Link + i;
+ if(strcmp(Name, L->Configuration.Peer[0].Node) == 0 ||
+ strcmp(Name, L->Configuration.Peer[1].Node) == 0)
+ {
+ fprintf(stderr, "destroying link '%s'\n", L->Name);
+ if(stop_link(E, L->Name) == -1)
+ {
+ fprintf(stderr, "failed to destroy link '%s'\n", L->Name);
+ return -1;
+ }
+ } else
+ {
+ ++i;
+ }
+ }
+ }
+ { // Stop
+ char *Configuration = CallModuleBuffer(R->Configuration.Configuration);
+ if(Configuration == NULL) return -1;
+ char *Data = CallModuleBuffer(R->Data);
+ if(Data == NULL)
+ {
+ free(Configuration);
+ return -1;
+ }
+ call_module_result Result =
+ CallModule(R->Configuration.Module, R->Configuration.Module, "stop", Name, Configuration, Data, NULL);
+ free(Configuration);
+ free(Data);
+ if(Result.Data != NULL)
+ {
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrint_ucl_object_t(&S, Result.Data);
+ ucl_object_unref(Result.Data);
+ }
+ if(Result.Called == false)
+ {
+ fprintf(stderr, "failed to call module %s\n", R->Configuration.Module);
+ return -1;
+ }
+ if(!WIFEXITED(Result.Status) || (WEXITSTATUS(Result.Status) != 0))
+ {
+ fprintf(stderr, "%s exited with status 0x%x\n", R->Configuration.Module, Result.Status);
+ return -1;
+ }
+ }
+ { // Remove
+ Free_node(R);
+ memmove(E->Node + NodeIndex, E->Node + NodeIndex + 1, (E->NodeCount - NodeIndex - 1) * sizeof(node));
+ --E->NodeCount;
+ }
+ return 0;
+}
+
+static int
+mod(node *N, size_t ArgCount, char **Arg)
+{
+ char *Configuration = CallModuleBuffer(N->Configuration.Configuration);
+ if(Configuration == NULL) return -1;
+ char *Data = CallModuleBuffer(N->Data);
+ if(Data == NULL)
+ {
+ free(Configuration);
+ return -1;
+ }
+ size_t ExtraArguments = 5;
+ char **Args = alloca(sizeof(char *) * (ArgCount + ExtraArguments + 1));
+ Args[0] = N->Configuration.Module;
+ Args[1] = "mod";
+ Args[2] = N->Name;
+ Args[3] = Configuration;
+ Args[4] = Data;
+ memcpy(Args + ExtraArguments, Arg, sizeof(char *) * ArgCount);
+ Args[ArgCount + ExtraArguments] = NULL;
+ char const *Module = N->Configuration.Module;
+ call_module_result Result = CallModule_a(Module, Args);
+ free(Configuration);
+ free(Data);
+ if(Result.Called == false)
+ {
+ fprintf(stderr, "failed to call module %s\n", Module);
+ return -1;
+ }
+ if(!WIFEXITED(Result.Status) || (WEXITSTATUS(Result.Status) != 0))
+ {
+ fprintf(stderr, "%s exited with status 0x%x\n", Module, Result.Status);
+ if(Result.Data != NULL)
+ {
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrint_ucl_object_t(&S, Result.Data);
+ ucl_object_unref(Result.Data);
+ }
+ return -1;
+ }
+ if(Result.Data == NULL)
+ {
+ fprintf(stderr, "%s returned no data\n", Module);
+ return -1;
+ }
+ ucl_object_unref(N->Data);
+ N->Data = Result.Data;
+ return 0;
+}
+
+static int
+LoadState(experiment *E, fd FD)
+{
+ struct ucl_parser *Parser = ucl_parser_new(0);
+ if(ucl_parser_add_fd(Parser, FD) == false)
+ {
+ fprintf(stderr, "Unable to parse state file: %s\n", ucl_parser_get_error(Parser));
+ ucl_parser_free(Parser);
+ exit(EX_CONFIG);
+ }
+ ucl_object_t *root = ucl_parser_get_object(Parser);
+ char *Error = Parse_experiment(E, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "Config error: %s\n", Error);
+ free(Error);
+ ucl_object_unref(root);
+ ucl_parser_free(Parser);
+ exit(EX_DATAERR);
+ }
+ ucl_object_unref(root);
+ ucl_parser_free(Parser);
+ return 0;
+}
+
+static void __attribute__((noreturn)) usage(void)
+{
+ fprintf(
+ stderr,
+ "usage: %1$s show\n"
+ " %1$s node start [node-name] [configuration]\n"
+ " %1$s node stop [node-name]\n"
+ " %1$s link start [link-name] [node-A-name] [interface-A-name] [node-B-name] [interface-B-name]\n"
+ " %1$s link stop [link-name]\n"
+ " %1$s node cmd [node-name] [command...]\n"
+ " %1$s node mod [node-name] [command...]\n",
+ Arg0);
+ exit(EX_USAGE);
+}
+
+static int
+SaveState(experiment *E, fd FD)
+{
+ ftruncate(FD, 0);
+ FILE *F = fdopen(FD, "w");
+ if(F == NULL)
+ {
+ fprintf(stderr, "failed to save state (fdopen: %s)\n", strerror(errno));
+ return -1;
+ }
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = F;
+ Save_experiment(&S, E);
+ fclose(F);
+ return 0;
+}
+
+static int
+Command_node(experiment *E, fd StateFD, int ArgCount, char **Arg)
+{
+ if(ArgCount < 2) usage();
+ char const *Command = Arg[0];
+ char const *NodeName = Arg[1];
+ if(isprints(NodeName) == false)
+ {
+ fprintf(stderr, "Node name is non-printable\n");
+ return -1;
+ }
+ ArgCount -= 2;
+ Arg += 2;
+ if(strcmp(Command, "start") == 0)
+ {
+ if(ArgCount != 1) usage();
+ char const *Configuration = Arg[0];
+ struct ucl_parser *Parser = ucl_parser_new(0);
+ if(ucl_parser_add_string(Parser, Configuration, 0) == false)
+ {
+ fprintf(stderr, "Unable to parse configuration file: %s\n", ucl_parser_get_error(Parser));
+ ucl_parser_free(Parser);
+ return -1;
+ }
+ ucl_object_t *root = ucl_parser_get_object(Parser);
+ if(root == NULL)
+ {
+ fprintf(stderr, "configuration error: %s\n", ucl_parser_get_error(Parser));
+ ucl_parser_free(Parser);
+ return -1;
+ }
+ ucl_parser_free(Parser);
+ int Result = start_node(E, NodeName, root);
+ ucl_object_unref(root);
+ if(Result == 0) SaveState(E, StateFD);
+ return Result;
+ } else if(strcmp(Command, "stop") == 0)
+ {
+ if(ArgCount != 0) usage();
+ stop_node(E, NodeName);
+ return SaveState(E, StateFD);
+ } else if(strcmp(Command, "cmd") == 0)
+ {
+ size_t NodeIndex = FindNodeIndex(E, NodeName);
+ if(NodeIndex == SIZE_MAX)
+ {
+ fprintf(stderr, "Node %s does not exists\n", NodeName);
+ return -1;
+ }
+ node *N = E->Node + NodeIndex;
+ cmd(N, (size_t)ArgCount, Arg);
+ } else if(strcmp(Command, "mod") == 0)
+ {
+ size_t NodeIndex = FindNodeIndex(E, NodeName);
+ if(NodeIndex == SIZE_MAX)
+ {
+ fprintf(stderr, "Node %s does not exists\n", NodeName);
+ return -1;
+ }
+ node *N = E->Node + NodeIndex;
+ if(mod(N, (size_t)ArgCount, Arg) == -1) return -1;
+ return SaveState(E, StateFD);
+ } else
+ {
+ fprintf(stderr, "unknown argument: %s\n", Command);
+ usage();
+ }
+}
+
+static int
+Command_link(experiment *E, fd StateFD, int ArgCount, char **Arg)
+{
+ if(ArgCount < 2) usage();
+ char const *Command = Arg[0];
+ char const *LinkName = Arg[1];
+ if(isprints(LinkName) == false)
+ {
+ fprintf(stderr, "link name is non-printable\n");
+ return -1;
+ }
+ ArgCount -= 2;
+ Arg += 2;
+ int Result;
+ if(strcmp(Command, "start") == 0)
+ {
+ if(ArgCount != 4) usage();
+ Result = start_link(E, LinkName, Arg[0], Arg[1], Arg[2], Arg[3]);
+ } else if(strcmp(Command, "stop") == 0)
+ {
+ if(ArgCount != 0) usage();
+ Result = stop_link(E, LinkName);
+ } else
+ {
+ fprintf(stderr, "unknown argument: %s\n", Command);
+ usage();
+ }
+ if(Result == 0) SaveState(E, StateFD);
+ return Result;
+}
+
+static int
+main_(int ArgCount, char **Arg, experiment *E, fd StateFD)
+{
+ char const *Command = Arg[0];
+ --ArgCount;
+ ++Arg;
+ if(strcmp(Command, "show") == 0)
+ {
+ if(ArgCount != 0) usage();
+ return SaveState(E, STDOUT_FILENO);
+ } else if(strcmp(Command, "node") == 0)
+ {
+ return Command_node(E, StateFD, ArgCount, Arg);
+ } else if(strcmp(Command, "link") == 0)
+ {
+ return Command_link(E, StateFD, ArgCount, Arg);
+ } else
+ {
+ fprintf(stderr, "unknown argument: %s\n", Command);
+ usage();
+ }
+}
+
+int
+main(int ArgCount, char **Arg)
+{
+ Arg0 = Arg[0];
+ int C;
+ while((C = getopt(ArgCount, Arg, "")) != -1)
+ {
+ switch(C)
+ {
+ case '?':
+ default: usage();
+ }
+ }
+ ArgCount -= optind;
+ Arg += optind;
+ if(ArgCount == 0) usage();
+ char const *StatePath = getenv("SF_PATH");
+ if(StatePath == NULL)
+ {
+ fprintf(stderr, "SF_PATH environment variable is unset\n");
+ exit(-1);
+ }
+ fd StateFD = open(StatePath, O_RDWR | O_CREAT | O_EXLOCK | O_CLOEXEC | O_NONBLOCK, 0644);
+ if(StateFD == -1)
+ {
+ fprintf(stderr, "failed to acquire state: %s\n", strerror(errno));
+ exit(-1);
+ }
+ static experiment E_;
+ experiment *E = &E_;
+ if(LoadState(E, StateFD) == -1)
+ {
+ close(StateFD);
+ exit(-1);
+ }
+ int Result = main_(ArgCount, Arg, E, StateFD);
+ Free_experiment(E);
+ close(StateFD);
+ return Result;
+}
diff --git a/src/base/state.c b/src/base/state.c
new file mode 100644
index 0000000..50ebe4e
--- /dev/null
+++ b/src/base/state.c
@@ -0,0 +1,380 @@
+#include "state.h"
+
+// FREE
+
+void
+Free_endpoint(endpoint *E)
+{
+ free(E->Address);
+ free(E->Hook);
+}
+
+void
+Free_endpoint_configuration(endpoint_configuration *E)
+{
+ free(E->Node);
+ free(E->Interface);
+}
+
+void
+Free_link_configuration(link_configuration *E)
+{
+ Free_endpoint_configuration(E->Peer + 0);
+ Free_endpoint_configuration(E->Peer + 1);
+}
+
+void
+Free_link(link_ *E)
+{
+ free(E->Name);
+ Free_link_configuration(&E->Configuration);
+ Free_endpoint(E->Peer + 0);
+ Free_endpoint(E->Peer + 1);
+}
+
+void
+Free_node_configuration(node_configuration *E)
+{
+ free(E->Module);
+ ucl_object_unref(E->Configuration);
+}
+
+void
+Free_node(node *E)
+{
+ free(E->Name);
+ Free_node_configuration(&E->Configuration);
+ ucl_object_unref(E->Data);
+}
+
+void
+Free_experiment(experiment *E)
+{
+ for(size_t i = 0; i < E->NodeCount; ++i) Free_node(E->Node + i);
+ free(E->Node);
+ for(size_t i = 0; i < E->LinkCount; ++i) Free_link(E->Link + i);
+ free(E->Link);
+}
+
+// PARSE
+
+char *
+Parse_endpoint(endpoint *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ if(root == NULL || root->type != UCL_OBJECT)
+ {
+ if(*Position == '\0') Position = ".";
+ asprintf(&Error, "%s is not an object", Position);
+ goto error;
+ }
+ ucl_object_t const *address = ucl_object_lookup(root, "address");
+ if(address == NULL || address->type != UCL_STRING)
+ {
+ asprintf(&Error, "%s.address is not a string", Position);
+ goto error;
+ }
+ ucl_object_t const *hook = ucl_object_lookup(root, "hook");
+ if(hook == NULL || hook->type != UCL_STRING)
+ {
+ asprintf(&Error, "%s.hook is not a string", Position);
+ goto error;
+ }
+ E->Address = strdup(ucl_object_tostring(address));
+ E->Hook = strdup(ucl_object_tostring(hook));
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_endpoint_configuration(endpoint_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *node = ucl_object_lookup(root, "node");
+ UCL_CHECK(node, STRING);
+ ucl_object_t const *interface = ucl_object_lookup(root, "interface");
+ UCL_CHECK(interface, STRING);
+ E->Node = strdup(ucl_object_tostring(node));
+ E->Interface = strdup(ucl_object_tostring(interface));
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_link_configuration(link_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ size_t EndpointAt = 0;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *peer0 = ucl_object_lookup(root, "peer0");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.peer0", Position);
+ Error = Parse_endpoint_configuration(&E->Peer[EndpointAt], peer0, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ ++EndpointAt;
+ }
+ ucl_object_t const *peer1 = ucl_object_lookup(root, "peer1");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.peer1", Position);
+ Error = Parse_endpoint_configuration(&E->Peer[EndpointAt], peer1, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ ++EndpointAt;
+ }
+ return NULL;
+error:
+ for(size_t i = 0; i < EndpointAt; ++i) Free_endpoint_configuration(E->Peer + i);
+ return Error;
+}
+
+char *
+Parse_link(link_ *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ size_t EndpointAt = 0;
+ bool Configuration = false;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *configuration = ucl_object_lookup(root, "configuration");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.configuration", Position);
+ Error = Parse_link_configuration(&E->Configuration, configuration, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ Configuration = true;
+ }
+ ucl_object_t const *peer0 = ucl_object_lookup(root, "peer0");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.peer0", Position);
+ Error = Parse_endpoint(E->Peer + EndpointAt, peer0, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ ++EndpointAt;
+ }
+ ucl_object_t const *peer1 = ucl_object_lookup(root, "peer1");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.peer1", Position);
+ Error = Parse_endpoint(E->Peer + EndpointAt, peer1, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ ++EndpointAt;
+ }
+ return NULL;
+error:
+ for(size_t i = 0; i < EndpointAt; ++i) Free_endpoint(E->Peer + i);
+ if(Configuration == true) Free_link_configuration(&E->Configuration);
+ return Error;
+}
+
+char *
+Parse_node_configuration(node_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *module = ucl_object_lookup(root, "module");
+ UCL_CHECK(module, STRING);
+ ucl_object_t const *configuration = ucl_object_lookup(root, "configuration");
+ if(configuration == NULL)
+ {
+ asprintf(&Error, "%s.configuration does not exist", Position);
+ goto error;
+ }
+ E->Module = strdup(ucl_object_tostring(module));
+ E->Configuration = ucl_object_copy(configuration);
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_node(node *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ bool Configuration = false;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *configuration = ucl_object_lookup(root, "configuration");
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.configuration", Position);
+ Error = Parse_node_configuration(&E->Configuration, configuration, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ Configuration = true;
+ }
+ ucl_object_t const *data = ucl_object_lookup(root, "data");
+ if(data == NULL)
+ {
+ asprintf(&Error, "%s.does not exist", Position);
+ goto error;
+ }
+ E->Data = ucl_object_copy(data);
+ return NULL;
+error:
+ if(Configuration == true) Free_node_configuration(&E->Configuration);
+ return Error;
+}
+
+char *
+Parse_experiment(experiment *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ size_t NodeAt = 0;
+ size_t LinkAt = 0;
+ if(root == NULL) return NULL;
+ UCL_CHECK_ROOT(OBJECT);
+ {
+ ucl_object_t const *nodes = ucl_object_lookup(root, "nodes");
+ UCL_CHECK(nodes, OBJECT);
+ E->NodeCount = nodes->len;
+ E->Node = calloc(sizeof(node), E->NodeCount);
+ ucl_object_t const *v;
+ ucl_object_iter_t it = NULL;
+ while((v = ucl_iterate_object(nodes, &it, true)))
+ {
+ char const *k = ucl_object_key(v);
+ if(k == NULL) continue;
+ node *I = E->Node + NodeAt;
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.nodes['%s']", Position, k);
+ Error = Parse_node(I, v, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ I->Name = strdup(k);
+ NodeAt++;
+ }
+ }
+ {
+ ucl_object_t const *links = ucl_object_lookup(root, "links");
+ UCL_CHECK(links, OBJECT);
+ E->LinkCount = links->len;
+ E->Link = calloc(sizeof(link_), E->LinkCount);
+ ucl_object_t const *v;
+ ucl_object_iter_t it = NULL;
+ while((v = ucl_iterate_object(links, &it, true)))
+ {
+ char const *k = ucl_object_key(v);
+ if(k == NULL) continue;
+ link_ *I = E->Link + LinkAt;
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.links['%s']", Position, k);
+ Error = Parse_link(I, v, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ I->Name = strdup(k);
+ LinkAt++;
+ }
+ }
+ return NULL;
+error:
+ E->NodeCount = NodeAt;
+ E->LinkCount = LinkAt;
+ Free_experiment(E);
+ return Error;
+}
+
+// SAVE
+
+void
+Save_endpoint(jprint_state *S, endpoint const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "address");
+ JPrint_string(S, E->Address);
+ JPrintMember(S, "hook");
+ JPrint_string(S, E->Hook);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_endpoint_configuration(jprint_state *S, endpoint_configuration const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "node");
+ JPrint_string(S, E->Node);
+ JPrintMember(S, "interface");
+ JPrint_string(S, E->Interface);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_link_configuration(jprint_state *S, link_configuration const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "peer0");
+ Save_endpoint_configuration(S, E->Peer + 0);
+ JPrintMember(S, "peer1");
+ Save_endpoint_configuration(S, E->Peer + 1);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_link(jprint_state *S, link_ const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "configuration");
+ Save_link_configuration(S, &E->Configuration);
+ JPrintMember(S, "peer0");
+ Save_endpoint(S, E->Peer + 0);
+ JPrintMember(S, "peer1");
+ Save_endpoint(S, E->Peer + 1);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_node_configuration(jprint_state *S, node_configuration const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "module");
+ JPrint_string(S, E->Module);
+ JPrintMember(S, "configuration");
+ JPrint_ucl_object_t(S, E->Configuration);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_node(jprint_state *S, node const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "configuration");
+ Save_node_configuration(S, &E->Configuration);
+ JPrintMember(S, "data");
+ JPrint_ucl_object_t(S, E->Data);
+ JPrintObjectEnd(S);
+}
+
+void
+Save_experiment(jprint_state *S, experiment const *E)
+{
+ JPrintObjectBegin(S);
+ {
+ JPrintMember(S, "nodes");
+ JPrintObjectBegin(S);
+ for(size_t i = 0; i < E->NodeCount; ++i)
+ {
+ node *I = E->Node + i;
+ JPrintMember(S, I->Name);
+ Save_node(S, I);
+ }
+ JPrintObjectEnd(S);
+ }
+ {
+ JPrintMember(S, "links");
+ JPrintObjectBegin(S);
+ for(size_t i = 0; i < E->LinkCount; ++i)
+ {
+ link_ *I = E->Link + i;
+ JPrintMember(S, I->Name);
+ Save_link(S, E->Link + i);
+ }
+ JPrintObjectEnd(S);
+ }
+ JPrintObjectEnd(S);
+}
diff --git a/src/base/state.h b/src/base/state.h
new file mode 100644
index 0000000..3bbccb5
--- /dev/null
+++ b/src/base/state.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include "../jprint.h"
+
+struct
+{
+ char *Address;
+ char *Hook;
+} typedef endpoint;
+
+struct
+{
+ char *Node;
+ char *Interface;
+} typedef endpoint_configuration;
+
+struct
+{
+ endpoint_configuration Peer[2];
+} typedef link_configuration;
+
+struct
+{
+ char *Name;
+ link_configuration Configuration;
+ endpoint Peer[2];
+} typedef link_;
+
+struct
+{
+ char *Module;
+ ucl_object_t *Configuration;
+} typedef node_configuration;
+
+struct
+{
+ char *Name;
+ node_configuration Configuration;
+ ucl_object_t *Data;
+} typedef node;
+
+struct
+{
+ size_t NodeCount;
+ node *Node;
+ size_t LinkCount;
+ link_ *Link;
+} typedef experiment;
+
+void Free_endpoint(endpoint *);
+void Free_endpoint_configuration(endpoint_configuration *);
+void Free_link_configuration(link_configuration *);
+void Free_link(link_ *);
+void Free_node_configuration(node_configuration *);
+void Free_node(node *);
+void Free_experiment(experiment *);
+
+char *Parse_endpoint(endpoint *, ucl_object_t const *, char const *Position);
+char *Parse_endpoint_configuration(endpoint_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_link_configuration(link_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_link(link_ *, ucl_object_t const *, char const *Position);
+char *Parse_node_configuration(node_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_node(node *, ucl_object_t const *, char const *Position);
+char *Parse_experiment(experiment *, ucl_object_t const *, char const *Position);
+
+void Save_endpoint(jprint_state *, endpoint const *);
+void Save_endpoint_configuration(jprint_state *, endpoint_configuration const *);
+void Save_link_configuration(jprint_state *, link_configuration const *);
+void Save_link(jprint_state *, link_ const *);
+void Save_node_configuration(jprint_state *, node_configuration const *);
+void Save_node(jprint_state *, node const *);
+void Save_experiment(jprint_state *, experiment const *);
diff --git a/src/jprint.c b/src/jprint.c
new file mode 100644
index 0000000..e058ea3
--- /dev/null
+++ b/src/jprint.c
@@ -0,0 +1,176 @@
+#include "jprint.h"
+
+void
+JPrintElement(jprint_state *State)
+{
+ if(State->NeedsComma == true)
+ {
+ fprintf(State->F, ",");
+ } else
+ {
+ State->NeedsComma = true;
+ }
+}
+
+void
+JPrint_null(jprint_state *State)
+{
+ JPrintElement(State);
+ fprintf(State->F, "null");
+}
+
+void
+JPrint_bool(jprint_state *State, bool E)
+{
+ JPrintElement(State);
+ fprintf(State->F, "%s", E ? "true" : "false");
+}
+
+void
+JPrint_ssize_t(jprint_state *State, ssize_t E)
+{
+ JPrintElement(State);
+ fprintf(State->F, "%zd", E);
+}
+
+void
+JPrint_double(jprint_state *State, double E)
+{
+ JPrintElement(State);
+ fprintf(State->F, "%f", E);
+}
+
+void
+JPrint_string(jprint_state *State, char const E[])
+{
+ JPrintElement(State);
+ fprintf(State->F, "\"");
+ for(size_t i = 0; E[i] != '\0'; ++i)
+ {
+ if(__builtin_expect(E[i] == '\\', false))
+ {
+ fputc('\\', State->F);
+ fputc('\\', State->F);
+ } else if(__builtin_expect(E[i] == '"', false))
+ {
+ fputc('\\', State->F);
+ fputc('"', State->F);
+ } else
+ {
+ fputc(E[i], State->F);
+ }
+ }
+ fprintf(State->F, "\"");
+}
+
+void
+JPrint_string_n(jprint_state *State, char const E[], size_t Length)
+{
+ JPrintElement(State);
+ fprintf(State->F, "\"");
+ for(size_t i = 0; i < Length && E[i] != '\0'; ++i)
+ {
+ if(__builtin_expect(E[i] == '\\', false))
+ {
+ fputc('\\', State->F);
+ fputc('\\', State->F);
+ } else if(__builtin_expect(E[i] == '"', false))
+ {
+ fputc('\\', State->F);
+ fputc('"', State->F);
+ } else
+ {
+ fputc(E[i], State->F);
+ }
+ }
+ fprintf(State->F, "\"");
+}
+
+void
+JPrintArrayBegin(jprint_state *State)
+{
+ JPrintElement(State);
+ State->NeedsComma = false;
+ fprintf(State->F, "[");
+}
+
+void
+JPrintArrayEnd(jprint_state *State)
+{
+ fprintf(State->F, "]");
+ State->NeedsComma = true;
+}
+
+void
+JPrintObjectBegin(jprint_state *State)
+{
+ JPrintElement(State);
+ State->NeedsComma = false;
+ fprintf(State->F, "{");
+}
+
+void
+JPrintObjectEnd(jprint_state *State)
+{
+ fprintf(State->F, "}");
+ State->NeedsComma = true;
+}
+
+void
+JPrintMember(jprint_state *State, char const T[])
+{
+ JPrintElement(State);
+ State->NeedsComma = false;
+ fprintf(State->F, "\"%s\":", T);
+}
+
+void
+JPrintMember_n(jprint_state *State, char const T[], size_t Length)
+{
+ JPrintElement(State);
+ State->NeedsComma = false;
+ fprintf(State->F, "\"%.*s\":", (int)Length, T);
+}
+
+void
+JPrint_ucl_object_t(jprint_state *State, ucl_object_t const *E)
+{
+ if(E == NULL)
+ {
+ JPrint_null(State);
+ return;
+ }
+ switch(E->type)
+ {
+ case UCL_OBJECT:
+ {
+ JPrintObjectBegin(State);
+ ucl_object_t const *v;
+ ucl_object_iter_t it = NULL;
+ while((v = ucl_iterate_object(E, &it, true)))
+ {
+ char const *k = ucl_object_key(v);
+ if(k == NULL) continue;
+ JPrintMember(State, k);
+ JPrint_ucl_object_t(State, v);
+ }
+ JPrintObjectEnd(State);
+ break;
+ }
+ case UCL_ARRAY:
+ {
+ JPrintArrayBegin(State);
+ ucl_object_t const *v;
+ ucl_object_iter_t it = NULL;
+ while((v = ucl_iterate_object(E, &it, true))) JPrint_ucl_object_t(State, v);
+ JPrintArrayEnd(State);
+ break;
+ }
+ case UCL_STRING: JPrint_string(State, ucl_object_tostring(E)); break;
+ case UCL_INT: JPrint_ssize_t(State, ucl_object_toint(E)); break;
+ case UCL_FLOAT: JPrint_double(State, ucl_object_todouble(E)); break;
+ case UCL_BOOLEAN: JPrint_bool(State, ucl_object_toboolean(E)); break;
+ case UCL_NULL: JPrint_null(State); break;
+ default: __builtin_unreachable();
+ }
+}
diff --git a/src/jprint.h b/src/jprint.h
new file mode 100644
index 0000000..d9e4b95
--- /dev/null
+++ b/src/jprint.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "base.h"
+
+struct
+{
+ FILE *F;
+ bool NeedsComma;
+} typedef jprint_state;
+
+void JPrint_null(jprint_state *);
+void JPrint_bool(jprint_state *, bool);
+void JPrint_ssize_t(jprint_state *, ssize_t);
+void JPrint_double(jprint_state *, double);
+void JPrint_string(jprint_state *, char const[]);
+void JPrint_string_n(jprint_state *, char const[], size_t);
+void JPrint_ucl_object_t(jprint_state *, ucl_object_t const *);
+
+void JPrintArrayBegin(jprint_state *);
+void JPrintElement(jprint_state *);
+void JPrintArrayEnd(jprint_state *);
+
+void JPrintObjectBegin(jprint_state *);
+void JPrintMember(jprint_state *, char const[]);
+void JPrintMember_n(jprint_state *, char const[], size_t);
+void JPrintObjectEnd(jprint_state *);
diff --git a/src/lamina/main.c b/src/lamina/main.c
new file mode 100644
index 0000000..a17462e
--- /dev/null
+++ b/src/lamina/main.c
@@ -0,0 +1,583 @@
+#include "../util.h"
+
+#include <sys/param.h>
+#include <sys/mount.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <signal.h>
+#include <pwd.h>
+
+static char *Root;
+
+NAMED_ENUM(mount_type, layer, devfs, );
+
+struct mount
+{
+ mount_type Type;
+ union
+ {
+ struct
+ {
+ char *Source;
+ } layer;
+ struct
+ {
+ char *Target;
+ } devfs;
+ };
+};
+
+static struct mount *Mounts;
+static size_t MountsCount;
+static bool MountsCleanup;
+static pid_t SlavePID;
+static fd SlaveSocket;
+
+static char const *Arg0;
+
+static void __attribute__((noreturn)) usage(void)
+{
+ fprintf(stderr, "usage: %1$s [recipe-file] [name]\n", Arg0);
+ exit(EX_USAGE);
+}
+
+NAMED_ENUM(message_type, run, chdir, user, );
+
+#define MAX_MESSAGE_SIZE 8192
+#define MAX_SLAVE_COMMAND_ARGUMENT_COUNT 32
+
+struct
+{
+ message_type Type;
+ union
+ {
+ struct
+ {
+ size_t ArgCount;
+ size_t Arg[MAX_SLAVE_COMMAND_ARGUMENT_COUNT];
+ char ArgData[0];
+ } run;
+ struct
+ {
+ char Path[0];
+ } chdir;
+ struct
+ {
+ char Username[0];
+ } user;
+ };
+} typedef message;
+
+static string_array
+Split(char const *Line, size_t LineLen)
+{
+ string_array E;
+ E._ = NULL;
+ E.Count = 0;
+ bool PartStart = true;
+ size_t Mark = 0;
+ for(size_t i = 0; i < LineLen; ++i)
+ {
+ if(Line[i] == ' ')
+ {
+ if(PartStart == false)
+ {
+ PartStart = true;
+ E._[E.Count - 1] = strndup(Line + Mark, i - Mark);
+ }
+ } else if(PartStart == true)
+ {
+ PartStart = false;
+ ++E.Count;
+ Mark = i;
+ E._ = reallocarray(E._, E.Count, sizeof(*E._));
+ if(E._ == NULL)
+ {
+ fprintf(stderr, "fatal: reallocarray failed\n");
+ exit(-1);
+ }
+ }
+ }
+ if(PartStart == false)
+ {
+ PartStart = true;
+ E._[E.Count - 1] = strndup(Line + Mark, LineLen - Mark);
+ }
+ E._ = reallocarray(E._, E.Count + 1, sizeof(*E._));
+ E._[E.Count] = NULL;
+ return E;
+}
+
+static void
+ExpandMounts(void)
+{
+ struct mount *NewMounts = reallocarray(Mounts, MountsCount + 1, sizeof(*NewMounts));
+ if(NewMounts == NULL)
+ {
+ fprintf(stderr, "reallocarray failed: %s\n", strerror(errno));
+ exit(-1);
+ }
+ Mounts = NewMounts;
+}
+
+static int
+command_layer(string_array E)
+{
+ if(E.Count != 1)
+ {
+ fprintf(stderr, "command expects 1 argument\n");
+ return -1;
+ }
+ char const *Source = E._[0];
+ ExpandMounts();
+ char Target[MAXPATHLEN];
+ snprintf(Target, sizeof(Target), "%s", Root);
+ if(mkdir(Target, 755) == -1)
+ {
+ if(errno != EEXIST)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", Target, strerror(errno));
+ return -1;
+ }
+ }
+ int Result = mount_unionfs(Target, Source, true);
+ if(Result == -1) return -1;
+ Mounts[MountsCount++] = (struct mount){
+ .Type = mount_type_layer,
+ .layer = { strdup(Source) },
+ };
+ return 0;
+}
+
+static int
+command_devfs(string_array E)
+{
+ if(E.Count != 1)
+ {
+ fprintf(stderr, "command expects 1 argument\n");
+ return -1;
+ }
+ char const *TargetRelative = E._[0];
+ ExpandMounts();
+ char Target[MAXPATHLEN];
+ snprintf(Target, sizeof(Target), "%s/%s", Root, E._[0]);
+ int Result = mount_devfs(Target);
+ if(Result == -1) return -1;
+ Mounts[MountsCount++] = (struct mount){
+ .Type = mount_type_devfs,
+ .devfs = { strdup(TargetRelative) },
+ };
+ return 0;
+}
+
+static int
+command_copy(string_array E)
+{
+ if(E.Count != 2)
+ {
+ fprintf(stderr, "command expects 2 arguments\n");
+ return -1;
+ }
+ char const *Source = E._[0];
+ char Target[MAXPATHLEN];
+ snprintf(Target, sizeof(Target), "%s/%s", Root, E._[1]);
+ pid_t PID = fork();
+ if(PID == -1) return -1;
+ if(PID == 0)
+ {
+ MountsCleanup = false;
+ close(STDIN_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ close_all();
+ fprintf(stderr, "rsync --archive --xattrs %s %s\n", Source, Target);
+ execlp("rsync", "rsync", "--archive", "--xattrs", Source, Target, NULL);
+ exit(-1);
+ }
+ int Exit;
+ waitpid(PID, &Exit, 0);
+ if(!WIFEXITED(Exit) || (WEXITSTATUS(Exit) != 0)) return -1;
+ return 0;
+}
+
+static int
+command_run(string_array E)
+{
+ if(E.Count > MAX_SLAVE_COMMAND_ARGUMENT_COUNT)
+ {
+ fprintf(stderr, "too many arguments\n");
+ return -1;
+ }
+ char Buffer[MAX_MESSAGE_SIZE];
+ message *M = (message *)Buffer;
+ M->Type = message_type_run;
+ M->run.ArgCount = E.Count;
+ size_t ArgAt = 0;
+ size_t At = 0;
+ for(size_t i = 0; i < E.Count; ++i)
+ {
+ M->run.Arg[ArgAt] = At;
+ size_t Length = strlen(E._[i]);
+ // XXX: check that we do not go outside
+ memcpy(M->run.ArgData + At, E._[i], Length);
+ M->run.ArgData[At + Length] = 0;
+ At += Length + 1;
+ ++ArgAt;
+ }
+ send(SlaveSocket, M, OffsetOf(message, run.ArgData) + At, 0);
+ int Status;
+ if(recv(SlaveSocket, &Status, sizeof(Status), 0) == -1)
+ {
+ fprintf(stderr, "recv: %s\n", strerror(errno));
+ return -1;
+ }
+ return Status;
+}
+
+static int
+command_chdir(string_array E)
+{
+ if(E.Count != 1)
+ {
+ fprintf(stderr, "requires 1 argument\n");
+ return -1;
+ }
+ char Buffer[MAX_MESSAGE_SIZE];
+ message *M = (message *)Buffer;
+ M->Type = message_type_chdir;
+ size_t Length = strlen(E._[0]);
+ // XXX: check bounds
+ memcpy(M->chdir.Path, E._[0], Length);
+ M->chdir.Path[Length] = 0;
+ send(SlaveSocket, M, OffsetOf(message, chdir.Path) + Length + 1, 0);
+ int Status;
+ if(recv(SlaveSocket, &Status, sizeof(Status), 0) == -1)
+ {
+ fprintf(stderr, "recv: %s\n", strerror(errno));
+ return -1;
+ }
+ return Status;
+}
+
+static int
+command_user(string_array E)
+{
+ if(E.Count != 1)
+ {
+ fprintf(stderr, "requires 1 argument\n");
+ return -1;
+ }
+ char Buffer[MAX_MESSAGE_SIZE];
+ message *M = (message *)Buffer;
+ M->Type = message_type_user;
+ size_t Length = strlen(E._[0]);
+ // XXX: check bounds
+ memcpy(M->user.Username, E._[0], Length);
+ M->user.Username[Length] = 0;
+ send(SlaveSocket, M, OffsetOf(message, user.Username) + Length + 1, 0);
+ int Status;
+ if(recv(SlaveSocket, &Status, sizeof(Status), 0) == -1)
+ {
+ fprintf(stderr, "recv: %s\n", strerror(errno));
+ return -1;
+ }
+ return Status;
+}
+
+static int
+Execute(char const *Line, size_t LineLen)
+{
+ string_array Args = Split(Line, LineLen);
+ if(Args.Count == 0)
+ {
+ Free_string_array(&Args);
+ return 0;
+ }
+ int Result;
+ char *Command = Args._[0];
+ string_array Rest = (string_array){ ._ = Args._ + 1, .Count = Args.Count - 1 };
+ if(strcmp(Command, "RUN") == 0)
+ {
+ Result = command_run(Rest);
+ } else if(strcmp(Command, "LAYER") == 0)
+ {
+ Result = command_layer(Rest);
+ } else if(strcmp(Command, "DEVFS") == 0)
+ {
+ Result = command_devfs(Rest);
+ } else if(strcmp(Command, "CHDIR") == 0)
+ {
+ Result = command_chdir(Rest);
+ } else if(strcmp(Command, "COPY") == 0)
+ {
+ Result = command_copy(Rest);
+ } else if(strcmp(Command, "USER") == 0)
+ {
+ Result = command_user(Rest);
+ } else
+ {
+ fprintf(stderr, "unrecognized command: %s\n", Command);
+ Result = -1;
+ }
+ Free_string_array(&Args);
+ return Result;
+}
+
+static int
+unmount_single(struct mount *E)
+{
+ char const *Target;
+ switch(E->Type)
+ {
+ case mount_type_layer: Target = Root; break;
+ case mount_type_devfs:
+ {
+ char TargetBuffer[MAXPATHLEN];
+ snprintf(TargetBuffer, sizeof(TargetBuffer), "%s/%s", Root, E->devfs.Target);
+ Target = TargetBuffer;
+ break;
+ }
+ case mount_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ while(unmount(Target, MNT_FORCE) == -1)
+ {
+ fprintf(stderr, "unmount %s failed: %s\n", Target, strerror(errno));
+ if(errno != EBUSY) return -1;
+ sleep(1);
+ }
+ return 0;
+}
+
+static void
+unmount_all(void)
+{
+ for(size_t i = MountsCount - 1; i < MountsCount; --i)
+ {
+ unmount_single(Mounts + i);
+ switch(Mounts[i].Type)
+ {
+ case mount_type_layer: free(Mounts[i].layer.Source); break;
+ case mount_type_devfs: free(Mounts[i].devfs.Target); break;
+ case mount_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ }
+ if(Mounts != NULL) free(Mounts);
+}
+
+static int
+slave_command_run(string_array E)
+{
+ if(E.Count == 0)
+ {
+ fprintf(stderr, "run what?\n");
+ return -1;
+ }
+ pid_t PID = fork();
+ if(PID == -1) return -1;
+ if(PID == 0)
+ {
+ MountsCleanup = false;
+ close(STDIN_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ close_all();
+ execvp(E._[0], E._);
+ fprintf(stderr, "failed to exec '%s': %s\n", E._[0], strerror(errno));
+ exit(-1);
+ }
+ int Exit;
+ waitpid(PID, &Exit, 0);
+ if(!WIFEXITED(Exit) || (WEXITSTATUS(Exit) != 0)) return -1;
+ return 0;
+}
+
+static int
+chuser(char *Username)
+{
+ errno = 0;
+ struct passwd *E = getpwnam(Username);
+ if(E == NULL)
+ {
+ if(errno != 0) fprintf(stderr, "getpwnam(%s): %s\n", Username, strerror(errno));
+ else
+ fprintf(stderr, "user %s does not exist\n", Username);
+ return -1;
+ }
+ uid_t UID = E->pw_uid;
+ gid_t GID = E->pw_gid;
+ if(setgid(GID) == -1)
+ {
+ fprintf(stderr, "setgid(%u): %s\n", GID, strerror(errno));
+ return -1;
+ }
+ if(setuid(UID) == -1)
+ {
+ fprintf(stderr, "setuid(%u): %s\n", UID, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+slave(fd FD)
+{
+ char Buffer[MAX_MESSAGE_SIZE];
+ message *M = (message *)Buffer;
+ ssize_t Size;
+ while((Size = recv(FD, Buffer, sizeof(Buffer), 0)))
+ {
+ if(Size == -1)
+ {
+ if(errno == EINTR) continue;
+ fprintf(stderr, "recv: %s\n", strerror(errno));
+ break;
+ }
+ if(Size == 0) break;
+ int Result;
+ switch(M->Type)
+ {
+ case message_type_run:
+ {
+ string_array E;
+ E._ = NULL;
+ E.Count = M->run.ArgCount;
+ E._ = reallocarray(E._, E.Count + 1, sizeof(*E._));
+ E._[E.Count] = NULL;
+ // XXX: add checking
+ for(size_t i = 0; i < M->run.ArgCount; ++i) E._[i] = M->run.ArgData + M->run.Arg[i];
+ Result = slave_command_run(E);
+ free(E._);
+ break;
+ }
+ case message_type_chdir:
+ {
+ Result = chdir(M->chdir.Path);
+ break;
+ }
+ case message_type_user:
+ {
+ Result = chuser(M->user.Username);
+ break;
+ }
+ case message_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ send(FD, &Result, sizeof(Result), 0);
+ }
+ close(FD);
+ return 0;
+}
+
+static void
+StopSlave(void)
+{
+ kill(SlavePID, SIGINT);
+ int Exit;
+ waitpid(SlavePID, &Exit, 0);
+}
+
+static int
+SpawnSlave(void)
+{
+ fd FD[2];
+ if(socketpair(PF_UNIX, SOCK_DGRAM, 0, FD) == -1)
+ {
+ fprintf(stderr, "socketpair %s\n", strerror(errno));
+ return -1;
+ }
+ pid_t PID = fork();
+ if(PID == -1) return -1;
+ if(PID == 0)
+ {
+ close(FD[1]);
+ close(STDIN_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ if(chroot(Root) == -1)
+ {
+ fprintf(stderr, "chroot %s: %s\n", Root, strerror(errno));
+ exit(-1);
+ }
+ if(chdir("/") == -1)
+ {
+ fprintf(stderr, "chdir /: %s\n", strerror(errno));
+ exit(-1);
+ }
+ exit(slave(FD[0]));
+ }
+ close(FD[0]);
+ SlaveSocket = FD[1];
+ SlavePID = PID;
+ return 0;
+}
+
+static void
+Cleanup(void)
+{
+ if(SlavePID > 0) StopSlave();
+ if(MountsCleanup == true) unmount_all();
+}
+
+int
+main(int ArgCount, char **Arg)
+{
+ Arg0 = Arg[0];
+ --ArgCount;
+ ++Arg;
+ if(ArgCount != 2) usage();
+ char const *Path = Arg[0];
+ Root = Arg[1];
+ ArgCount -= 2;
+ Arg += 2;
+ if(mkdir(Root, 0755) == -1)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", Root, strerror(errno));
+ exit(-1);
+ }
+ atexit(Cleanup);
+ if(SpawnSlave() == -1) exit(-1);
+ FILE *F = fopen(Path, "r");
+ if(F == NULL)
+ {
+ fprintf(stderr, "fopen(\"%s\", \"r\")\n", Path);
+ exit(-1);
+ }
+ MountsCleanup = true;
+ char *Line = NULL;
+ size_t LineSize = 0;
+ ssize_t LineLength;
+ size_t LineAt = 0;
+ while((LineLength = getline(&Line, &LineSize, F)))
+ {
+ if(LineLength <= 0) break;
+ ++LineAt;
+ if(Line[LineLength - 1] == '\n') Line[--LineLength] = '\0';
+ if(LineLength == 0) continue;
+ if(Line[0] == '#') continue;
+ if(Execute(Line, (size_t)LineLength) == -1)
+ {
+ fprintf(stderr, "(line %zu)\n", LineAt);
+ break;
+ }
+ }
+ printf("LAYER %s\n", Root);
+ for(size_t i = 0; i < MountsCount; ++i)
+ {
+ struct mount *E = Mounts + i;
+ switch(Mounts[i].Type)
+ {
+ case mount_type_layer:
+ {
+ printf("LAYER %s\n", E->layer.Source);
+ break;
+ }
+ case mount_type_devfs:
+ {
+ printf("DEVFS %s\n", E->devfs.Target);
+ break;
+ }
+ case mount_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ }
+ return 0;
+}
diff --git a/src/module/module.c b/src/module/module.c
new file mode 100644
index 0000000..22f6142
--- /dev/null
+++ b/src/module/module.c
@@ -0,0 +1,126 @@
+#include "module.h"
+
+char const *Arg0;
+
+void __attribute__((noreturn)) usage(void)
+{
+ WARNINGS_IGNORE("-Wformat-nonliteral")
+ fprintf(stderr, Usage, Arg0);
+ WARNINGS_RESTORE
+ exit(EX_USAGE);
+}
+
+static ucl_object_t *
+Parse(char const *Arg)
+{
+ struct ucl_parser *Parser = ucl_parser_new(0);
+ if(ucl_parser_add_string(Parser, Arg, 0) == false)
+ {
+ fprintf(stderr, "Unable to parse state file: %s\n", ucl_parser_get_error(Parser));
+ ucl_parser_free(Parser);
+ exit(EX_CONFIG);
+ }
+ ucl_object_t *root = ucl_parser_get_object(Parser);
+ ucl_parser_free(Parser);
+ return root;
+}
+
+int
+main(int ArgCount, char **Arg)
+{
+ Arg0 = Arg[0];
+ int C;
+ while((C = getopt(ArgCount, Arg, "")) != -1)
+ {
+ switch(C)
+ {
+ case '?':
+ default: usage();
+ }
+ }
+ ArgCount -= optind;
+ Arg += optind;
+ if(ArgCount == 0) usage();
+ char const *Command = Arg[0];
+ --ArgCount;
+ ++Arg;
+ int Result;
+ if(strcmp(Command, "start") == 0)
+ {
+ if(ArgCount != 2) usage();
+ char const *Name = Arg[0];
+ if(isprints(Name) == false)
+ {
+ fprintf(stderr, "Name is non-printable\n");
+ return -1;
+ }
+ ucl_object_t *Configuration = Parse(Arg[1]);
+ Result = start(Name, Configuration);
+ ucl_object_unref(Configuration);
+ } else if(strcmp(Command, "stop") == 0)
+ {
+ if(ArgCount != 3) usage();
+ char const *Name = Arg[0];
+ if(isprints(Name) == false)
+ {
+ fprintf(stderr, "Name is non-printable\n");
+ return -1;
+ }
+ ucl_object_t *Configuration = Parse(Arg[1]);
+ ucl_object_t *Data = Parse(Arg[2]);
+ Result = stop(Name, Configuration, Data);
+ ucl_object_unref(Configuration);
+ ucl_object_unref(Data);
+ } else if(strcmp(Command, "get-endpoint") == 0)
+ {
+ if(ArgCount != 4) usage();
+ char const *Name = Arg[0];
+ if(isprints(Name) == false)
+ {
+ fprintf(stderr, "Name is non-printable\n");
+ return -1;
+ }
+ ucl_object_t *Configuration = Parse(Arg[1]);
+ ucl_object_t *Data = Parse(Arg[2]);
+ char const *Interface = Arg[3];
+ Result = get_endpoint(Name, Configuration, Data, Interface);
+ ucl_object_unref(Configuration);
+ ucl_object_unref(Data);
+ } else if(strcmp(Command, "cmd") == 0)
+ {
+ if(ArgCount < 3) usage();
+ char const *Name = Arg[0];
+ if(isprints(Name) == false)
+ {
+ fprintf(stderr, "Name is non-printable\n");
+ return -1;
+ }
+ ucl_object_t *Configuration = Parse(Arg[1]);
+ ucl_object_t *Data = Parse(Arg[2]);
+ Arg += 3;
+ ArgCount -= 3;
+ Result = cmd(Name, Configuration, Data, (size_t)ArgCount, Arg);
+ ucl_object_unref(Configuration);
+ ucl_object_unref(Data);
+ } else if(strcmp(Command, "mod") == 0)
+ {
+ if(ArgCount < 3) usage();
+ char const *Name = Arg[0];
+ if(isprints(Name) == false)
+ {
+ fprintf(stderr, "Name is non-printable\n");
+ return -1;
+ }
+ ucl_object_t *Configuration = Parse(Arg[1]);
+ ucl_object_t *Data = Parse(Arg[2]);
+ Arg += 3;
+ ArgCount -= 3;
+ Result = mod(Name, Configuration, Data, (size_t)ArgCount, Arg);
+ ucl_object_unref(Configuration);
+ ucl_object_unref(Data);
+ } else
+ {
+ usage();
+ }
+ return Result;
+}
diff --git a/src/module/module.h b/src/module/module.h
new file mode 100644
index 0000000..c357faa
--- /dev/null
+++ b/src/module/module.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "../base.h"
+
+extern char const *Usage;
+extern char const *Arg0;
+
+void __attribute__((noreturn)) usage(void);
+
+int start(char const *Name, ucl_object_t *Configuration);
+int stop(char const *Name, ucl_object_t *Configuration, ucl_object_t *Data);
+int get_endpoint(char const *Name, ucl_object_t *Configuration, ucl_object_t *Data, char const *Interface);
+int cmd(char const *Name, ucl_object_t *Configuration, ucl_object_t *Data, size_t ArgCount, char **Arg);
+int mod(char const *Name, ucl_object_t *Configuration, ucl_object_t *Data, size_t ArgCount, char **Arg);
diff --git a/src/sf.bridge/main.c b/src/sf.bridge/main.c
new file mode 100644
index 0000000..fbfb500
--- /dev/null
+++ b/src/sf.bridge/main.c
@@ -0,0 +1,166 @@
+#include "../module/module.h"
+#include "../util.h"
+#include "state.h"
+
+#include <netgraph.h>
+
+char const *Usage = "usage: %1$s start [name] [configuration]\n"
+ " %1$s stop [name] [configuration] [data]\n"
+ " %1$s get-endpoint [name] [configuration] [data] [interface]\n"
+ "unsupported commands: cmd mod\n";
+
+static configuration Configuration;
+static data Data;
+static bool ConfigurationLoaded;
+static bool DataLoaded;
+
+// HELPERS
+
+static void
+Free_all(void)
+{
+ if(ConfigurationLoaded == true) Free_configuration(&Configuration);
+ if(DataLoaded == true) Free_data(&Data);
+}
+
+static void
+Load_configuration(ucl_object_t *root)
+{
+ char *Error = Parse_configuration(&Configuration, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: configuration error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ ConfigurationLoaded = true;
+}
+
+static void
+Load_data(ucl_object_t *root)
+{
+ char *Error = Parse_data(&Data, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: data error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ DataLoaded = true;
+ // TODO: check that data matches configuration
+}
+
+// COMMANDS
+
+int
+start(char const *Name, ucl_object_t *configuration)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ // Implement
+ Data.ID = 0;
+ fd Control = -1;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(Create_bridge(&Data.ID, Control) == -1) goto error;
+ close(Control);
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ Save_data(&S, &Data);
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+stop(char const *Name, ucl_object_t *configuration, ucl_object_t *data)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Load_data(data);
+ fd Control = -1;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(DestroyNetgraphNode(Data.ID, Control) == -1) goto error;
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+get_endpoint(char const *Name, ucl_object_t *configuration, ucl_object_t *data, char const *Interface)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Load_data(data);
+ size_t linkNum;
+ if(strncmp(Interface, "link", __builtin_strlen("link")) == 0)
+ {
+ char const *N = Interface + __builtin_strlen("link");
+ linkNum = strtoul(N, NULL, 10);
+ if(linkNum == 0 && errno == EINVAL)
+ {
+ fprintf(stderr, "invalid interface %s\n", Interface);
+ goto error;
+ }
+ } else if(strncmp(Interface, "uplink", __builtin_strlen("uplink")) == 0)
+ {
+ char const *N = Interface + __builtin_strlen("uplink");
+ linkNum = strtoul(N, NULL, 10);
+ if(linkNum == 0)
+ {
+ fprintf(stderr, "unknown interface %s\n", Interface);
+ goto error;
+ }
+ } else
+ {
+ fprintf(stderr, "unknown interface %s\n", Interface);
+ }
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrintObjectBegin(&S);
+ JPrintMember(&S, "address");
+ char Address[NG_PATHSIZ];
+ snprintf(Address, sizeof(Address), "[%x]:", Data.ID);
+ JPrint_string(&S, Address);
+ JPrintMember(&S, "hook");
+ JPrint_string(&S, Interface);
+ JPrintObjectEnd(&S);
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+cmd(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ usage();
+}
+
+int
+mod(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ usage();
+}
diff --git a/src/sf.bridge/state.c b/src/sf.bridge/state.c
new file mode 100644
index 0000000..c7780f3
--- /dev/null
+++ b/src/sf.bridge/state.c
@@ -0,0 +1,58 @@
+#include "state.h"
+
+// FREE
+
+void
+Free_configuration(configuration *E)
+{
+ UNUSED(E);
+}
+
+void
+Free_data(data *E)
+{
+ UNUSED(E);
+}
+
+// PARSE
+
+char *
+Parse_configuration(configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ UNUSED(E);
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_data(data *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *id = ucl_object_lookup(root, "id");
+ UCL_CHECK(id, INT);
+ s64 ID = ucl_object_toint(id);
+ if(ID < 0 || ID > UINT32_MAX)
+ {
+ asprintf(&Error, "%s id invalid (%ld)", Position, ID);
+ goto error;
+ }
+ E->ID = (u32)ID;
+ return NULL;
+error:
+ return Error;
+}
+
+// SAVE
+
+void
+Save_data(jprint_state *S, data const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "id");
+ JPrint_ssize_t(S, E->ID);
+ JPrintObjectEnd(S);
+}
diff --git a/src/sf.bridge/state.h b/src/sf.bridge/state.h
new file mode 100644
index 0000000..7ef6243
--- /dev/null
+++ b/src/sf.bridge/state.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "../jprint.h"
+
+struct
+{
+} typedef configuration;
+
+struct
+{
+ u32 ID;
+} typedef data;
+
+void Free_configuration(configuration *);
+void Free_data(data *);
+
+char *Parse_configuration(configuration *, ucl_object_t const *, char const *Position);
+char *Parse_data(data *, ucl_object_t const *, char const *Position);
+
+void Save_data(jprint_state *, data const *);
diff --git a/src/sf.eiface/main.c b/src/sf.eiface/main.c
new file mode 100644
index 0000000..4fafa81
--- /dev/null
+++ b/src/sf.eiface/main.c
@@ -0,0 +1,155 @@
+#include "../module/module.h"
+#include "../util.h"
+#include "state.h"
+
+#include <netgraph.h>
+
+char const *Usage = "usage: %1$s start [name] [configuration]\n"
+ " %1$s stop [name] [configuration] [data]\n"
+ " %1$s get-endpoint [name] [configuration] [data] [interface]\n"
+ "unsupported commands: cmd mod\n";
+
+static configuration Configuration;
+static data Data;
+static bool ConfigurationLoaded;
+static bool DataLoaded;
+
+// HELPERS
+
+static void
+Free_all(void)
+{
+ if(ConfigurationLoaded == true) Free_configuration(&Configuration);
+ if(DataLoaded == true) Free_data(&Data);
+}
+
+static void
+Load_configuration(ucl_object_t *root)
+{
+ char *Error = Parse_configuration(&Configuration, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: configuration error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ ConfigurationLoaded = true;
+}
+
+static void
+Load_data(ucl_object_t *root)
+{
+ char *Error = Parse_data(&Data, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: data error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ DataLoaded = true;
+ // TODO: check that data matches configuration
+}
+
+// COMMANDS
+
+int
+start(char const *Name, ucl_object_t *configuration)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Data.ID = 0;
+ fd Control = -1;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(Create_eiface(&Data.ID, Control) == -1) goto error;
+ char *InterfaceName = getifname(Data.ID, Control);
+ if(InterfaceName == NULL) goto error;
+ int Result = command("ifconfig", "ifconfig", InterfaceName, "name", Name, NULL);
+ free(InterfaceName);
+ if(Result == -1) goto error;
+ close(Control);
+ if(Configuration.inet != NULL) command("ifconfig", "ifconfig", Name, "inet", Configuration.inet, NULL);
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ Save_data(&S, &Data);
+ Free_all();
+ return 0;
+error:
+ if(Data.ID != 0) DestroyNetgraphNode(Data.ID, Control);
+ if(Control != -1) close(Control);
+ Free_all();
+ return -1;
+}
+
+int
+stop(char const *Name, ucl_object_t *configuration, ucl_object_t *data)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Load_data(data);
+ fd Control = -1;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(DestroyNetgraphNode(Data.ID, Control) == -1) goto error;
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+get_endpoint(char const *Name, ucl_object_t *configuration, ucl_object_t *data, char const *Interface)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Load_data(data);
+ if(*Interface != '\0')
+ {
+ fprintf(stderr, "%s provides only empty interface name\n", Arg0);
+ goto error;
+ }
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrintObjectBegin(&S);
+ JPrintMember(&S, "address");
+ char Address[NG_PATHSIZ];
+ snprintf(Address, sizeof(Address), "[%x]:", Data.ID);
+ JPrint_string(&S, Address);
+ JPrintMember(&S, "hook");
+ JPrint_string(&S, "ether");
+ JPrintObjectEnd(&S);
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+cmd(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ usage();
+}
+
+int
+mod(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ usage();
+}
diff --git a/src/sf.eiface/state.c b/src/sf.eiface/state.c
new file mode 100644
index 0000000..7d85056
--- /dev/null
+++ b/src/sf.eiface/state.c
@@ -0,0 +1,63 @@
+#include "state.h"
+
+// FREE
+
+void
+Free_configuration(configuration *E)
+{
+ if(E->inet != NULL) free(E->inet);
+}
+
+void
+Free_data(data *E)
+{
+ UNUSED(E);
+}
+
+// PARSE
+
+char *
+Parse_configuration(configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *inet = ucl_object_lookup(root, "inet");
+ if(inet != NULL)
+ {
+ UCL_CHECK(inet, STRING);
+ E->inet = strdup(ucl_object_tostring(inet));
+ }
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_data(data *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *id = ucl_object_lookup(root, "id");
+ UCL_CHECK(id, INT);
+ s64 ID = ucl_object_toint(id);
+ if(ID < 0 || ID > UINT32_MAX)
+ {
+ asprintf(&Error, "%s id invalid (%ld)", Position, ID);
+ goto error;
+ }
+ E->ID = (u32)ID;
+ return NULL;
+error:
+ return Error;
+}
+
+// SAVE
+
+void
+Save_data(jprint_state *S, data const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "id");
+ JPrint_ssize_t(S, E->ID);
+ JPrintObjectEnd(S);
+}
diff --git a/src/sf.eiface/state.h b/src/sf.eiface/state.h
new file mode 100644
index 0000000..0431fdf
--- /dev/null
+++ b/src/sf.eiface/state.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "../jprint.h"
+
+struct
+{
+ char *inet;
+} typedef configuration;
+
+struct
+{
+ u32 ID;
+} typedef data;
+
+void Free_configuration(configuration *);
+void Free_data(data *);
+
+char *Parse_configuration(configuration *, ucl_object_t const *, char const *Position);
+char *Parse_data(data *, ucl_object_t const *, char const *Position);
+
+void Save_data(jprint_state *, data const *);
diff --git a/src/sf.jail/main.c b/src/sf.jail/main.c
new file mode 100644
index 0000000..d71511b
--- /dev/null
+++ b/src/sf.jail/main.c
@@ -0,0 +1,525 @@
+#include "../module/module.h"
+#include "../util.h"
+#include "state.h"
+
+#include <jail.h>
+#include <sys/param.h>
+#include <sys/jail.h>
+
+#include <netgraph.h>
+#include <netgraph/ng_message.h>
+#include <netgraph/ng_eiface.h>
+
+#include <sys/mount.h>
+#include <sys/ioctl.h>
+#include <fs/devfs/devfs.h>
+
+char const *Usage = "usage: %1$s start [name] [configuration]\n"
+ " %1$s stop [name] [configuration] [data]\n"
+ " %1$s get-endpoint [name] [configuration] [data] [interface]\n"
+ " %1$s cmd [name] [configuration] [data] sh\n"
+ " %1$s cmd [name] [configuration] [data] wireshark [interface]\n"
+ "unsupported commands: mod\n";
+
+static configuration Configuration;
+static data Data;
+static bool ConfigurationLoaded;
+static bool DataLoaded;
+
+// HELPERS
+
+static void
+Free_all(void)
+{
+ if(ConfigurationLoaded == true) Free_configuration(&Configuration);
+ if(DataLoaded == true) Free_data(&Data);
+}
+
+static void
+Load_configuration(ucl_object_t *root)
+{
+ char *Error = Parse_configuration(&Configuration, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: configuration error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ ConfigurationLoaded = true;
+}
+
+static void
+Load_data(ucl_object_t *root)
+{
+ char *Error = Parse_data(&Data, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: data error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ DataLoaded = true;
+ // TODO: check that data matches configuration
+}
+
+static int
+start_interface(interface *E, interface_configuration *C, fd Control, char const *JailName)
+{
+ E->Type = C->Type;
+ switch(C->Type)
+ {
+ case interface_type_eiface:
+ {
+ if(Create_eiface(&E->ID, Control) == -1) return -1;
+ char *InterfaceName = getifname(E->ID, Control);
+ if(InterfaceName == NULL)
+ {
+ DestroyNetgraphNode(E->ID, Control);
+ return -1;
+ }
+ // NOTE: This first renames the interface and only then vnets it.
+ int Result =
+ command("ifconfig", "ifconfig", InterfaceName, "name", C->Name, "vnet", JailName, NULL);
+ free(InterfaceName);
+ return Result;
+ }
+ case interface_type_steal:
+ {
+ return command("ifconfig", "ifconfig", C->steal.Interface, "name", C->Name, "vnet", JailName, NULL);
+ }
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+}
+
+static int
+stop_interface(interface *E, interface_configuration *C, fd Control, char const *JailName)
+{
+ switch(C->Type)
+ {
+ case interface_type_eiface: return DestroyNetgraphNode(E->ID, Control);
+ case interface_type_steal:
+ {
+ return command("ifconfig",
+ "ifconfig",
+ C->Name,
+ "-vnet",
+ JailName,
+ "name",
+ C->steal.Interface,
+ NULL);
+ }
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+}
+
+static int
+stop_interfaces(interfaces *E, interfaces_configuration *C, fd Control, char const *JailName)
+{
+ for(size_t i = 0; i < E->Count; ++i)
+ {
+ if(stop_interface(E->_ + i, C->_ + i, Control, JailName) == -1)
+ {
+ fprintf(stderr, "failed to stop interface '%s'\n", C->_[i].Name);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int
+start_interfaces(interfaces *E, interfaces_configuration *C, fd Control, char const *Name)
+{
+ Data.Interfaces._ = calloc(sizeof(interface), Configuration.Interfaces.Count);
+ for(E->Count = 0; E->Count < C->Count; ++E->Count)
+ {
+ interface *E_ = E->_ + E->Count;
+ interface_configuration *C_ = C->_ + E->Count;
+ if(start_interface(E_, C_, Control, Name) == -1) goto error;
+ }
+ return 0;
+error:
+ stop_interfaces(E, C, Control, Name);
+ Free_interfaces(E);
+ return -1;
+}
+
+static int
+start_jail(jid_t *JID, char const *Name, char const *Path)
+{
+ *JID = jail_setv(JAIL_CREATE, "name", Name, "vnet", "new", "persist", "true", "path", Path, NULL);
+ if(*JID == -1)
+ {
+ fprintf(stderr, "jail creation failed: %s\n", strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int
+stop_jail(jid_t JID)
+{
+ if(jail_remove(JID) == -1)
+ {
+ fprintf(stderr, "jail removal failed: %s\n", strerror(errno));
+ Free_all();
+ return -1;
+ }
+ return 0;
+}
+
+NAMED_ENUM(mapping_type, unionfs_below, unionfs, devfs, nullfs);
+
+static int
+mapping_mount(mapping *M, char const *TargetPrefix, mapping_type Type)
+{
+ char Target[MAXPATHLEN];
+ snprintf(Target, sizeof(Target), "%s/%s", TargetPrefix, M->Target);
+ if(mkdir(Target, 755) == -1)
+ {
+ if(errno != EEXIST)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", Target, strerror(errno));
+ return -1;
+ }
+ }
+ switch(Type)
+ {
+ case mapping_type_unionfs_below: return mount_unionfs(Target, M->Source, true);
+ case mapping_type_unionfs: return mount_unionfs(Target, M->Source, false);
+ case mapping_type_devfs: return mount_devfs(Target);
+ case mapping_type_nullfs: return mount_nullfs(Target, M->Source);
+ case mapping_type_COUNT:
+ default: __builtin_unreachable();
+ }
+}
+
+static int
+mapping_umount(char const *TargetPrefix, char const *Target_)
+{
+ char Target[MAXPATHLEN];
+ snprintf(Target, sizeof(Target), "%s/%s", TargetPrefix, Target_);
+ // NOTE: unmount fails because some processes somehow still hold a reference to the jail FS
+ while(unmount(Target, MNT_FORCE) == -1)
+ {
+ fprintf(stderr, "unmount %s failed: %s\n", Target, strerror(errno));
+ if(errno != EBUSY) return -1;
+ sleep(1);
+ }
+ return 0;
+}
+
+static int
+rm(char const *Path)
+{
+ pid_t PID = fork();
+ if(PID == -1) return -1;
+ if(PID == 0)
+ {
+ close(STDIN_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ close_all();
+ fprintf(stderr, "rm -r %s\n", Path);
+ execlp("rm", "rm", "-r", Path, NULL);
+ exit(-1);
+ }
+ int Exit;
+ waitpid(PID, &Exit, 0);
+ if(!WIFEXITED(Exit) || (WEXITSTATUS(Exit) != 0)) return -1;
+ return 0;
+}
+
+static int
+ApplyDevfsRuleset(char const *Prefix, s64 Ruleset)
+{
+ if(Ruleset == 0) return 0;
+ devfs_rsnum RS = (devfs_rsnum)Ruleset;
+ if(RS != Ruleset)
+ {
+ fprintf(stderr, "Ruleset invalid: %ld\n", Ruleset);
+ return -1;
+ }
+ char Path[MAXPATHLEN];
+ snprintf(Path, sizeof(Path), "%s/dev", Prefix);
+ fd DevFD = open(Path, O_RDONLY);
+ if(DevFD == -1)
+ {
+ fprintf(stderr, "open '%s' failed: %s\n", Path, strerror(errno));
+ return -1;
+ }
+ if(ioctl(DevFD, DEVFSIO_SAPPLY, &RS) == -1)
+ {
+ fprintf(stderr, "ruleset %hu apply failed: %s\n", RS, strerror(errno));
+ close(DevFD);
+ return -1;
+ }
+ close(DevFD);
+ return 0;
+}
+
+static int
+start_fs(filesystem_configuration *E, char const *Path)
+{
+ bool CreatedPath = false;
+ size_t LayerAt = 0;
+ size_t VolumeAt = 0;
+ bool Mounted_devfs = false;
+ bool Mounted_temporary = false;
+ if(mkdir(Path, 0755) == -1)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", Path, strerror(errno));
+ goto error;
+ }
+ CreatedPath = true;
+ for(LayerAt = 0; LayerAt < E->LayerCount; ++LayerAt)
+ {
+ if(mapping_mount(&(mapping){ .Source = E->Layer[LayerAt], .Target = "/" },
+ Path,
+ mapping_type_unionfs_below) == -1)
+ goto error;
+ }
+ if(E->Temporary != NULL)
+ {
+ if(mkdir(E->Temporary, 755) == -1)
+ {
+ if(errno != EEXIST)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", E->Temporary, strerror(errno));
+ goto error;
+ }
+ }
+ if(mapping_mount(&(mapping){ .Source = E->Temporary, .Target = "/" }, Path, mapping_type_unionfs) == -1)
+ goto error;
+ }
+ Mounted_temporary = true;
+ if(mapping_mount(&(mapping){ .Target = "/dev" }, Path, mapping_type_devfs) == -1) goto error;
+ if(ApplyDevfsRuleset(Path, E->DevfsRuleset) == -1) goto error;
+ for(VolumeAt = 0; VolumeAt < E->VolumeCount; ++VolumeAt)
+ {
+ if(mapping_mount(E->Volume + VolumeAt, Path, mapping_type_nullfs) == -1) goto error;
+ }
+ return 0;
+error:
+ for(size_t i = 0; i < VolumeAt; ++i) mapping_umount(Path, E->Volume[E->VolumeCount - 1 - i].Target);
+ if(Mounted_devfs) mapping_umount(Path, "/dev");
+ if(Mounted_temporary) mapping_umount(Path, "/");
+ for(size_t i = 0; i < LayerAt; ++i) mapping_umount(Path, "/");
+ if(CreatedPath) rm(Path);
+ return -1;
+}
+
+static int
+stop_fs(filesystem_configuration *E, char const *Path)
+{
+ for(size_t i = 0; i < E->VolumeCount; ++i) mapping_umount(Path, E->Volume[E->VolumeCount - 1 - i].Target);
+ mapping_umount(Path, "/dev");
+ if(E->Temporary != NULL) mapping_umount(Path, "/");
+ for(size_t i = 0; i < E->LayerCount; ++i) mapping_umount(Path, "/");
+ if(rm(Path) == -1)
+ {
+ fprintf(stderr, "rm -r %s: %s\n", Path, strerror(errno));
+ goto error;
+ }
+ return 0;
+error:
+ return -1;
+}
+
+static bool
+ValidJailName(char const *Name)
+{
+ for(char const *At = Name; *At; ++At)
+ {
+ if(isnumber(*At) == false) return true;
+ }
+ return true;
+}
+
+// COMMANDS
+
+static char const *FilesystemPrefix = "/tmp/sf";
+
+int
+start(char const *Name, ucl_object_t *configuration)
+{
+ if(ValidJailName(Name) == false) return -1;
+ Load_configuration(configuration);
+ fd Control = -1;
+ Data.JID = -1;
+ char Path[PATH_MAX];
+ snprintf(Path, sizeof(Path), "%s/%s", FilesystemPrefix, Name);
+ if(mkdir(FilesystemPrefix, 0755) == -1)
+ {
+ if(errno != EEXIST)
+ {
+ fprintf(stderr, "mkdir %s: %s\n", FilesystemPrefix, strerror(errno));
+ goto error;
+ }
+ }
+ if(start_fs(&Configuration.Filesystem, Path) == -1) goto error;
+ if(start_jail(&Data.JID, Name, Path) == -1) goto error;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(start_interfaces(&Data.Interfaces, &Configuration.Interfaces, Control, Name) == -1)
+ {
+ fprintf(stderr, "failed to start interfaces\n");
+ goto error;
+ }
+ DataLoaded = true;
+ close(Control);
+ exec_string_array(&Configuration.Init, false, Data.JID);
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ Save_data(&S, &Data);
+ Free_all();
+ return 0;
+error:
+ if(Control != -1) close(Control);
+ if(Data.JID != -1) stop_jail(Data.JID);
+ Free_all();
+ return -1;
+}
+
+int
+stop(char const *Name, ucl_object_t *configuration, ucl_object_t *data)
+{
+ if(ValidJailName(Name) == false) return -1;
+ Load_configuration(configuration);
+ Load_data(data);
+ exec_string_array(&Configuration.Shutdown, true, Data.JID);
+ fd Control = -1;
+ if(NgMkSockNode(NULL, &Control, NULL) == -1)
+ {
+ fprintf(stderr, "Failed to create netgraph socket: %s\n", strerror(errno));
+ goto error;
+ }
+ if(stop_interfaces(&Data.Interfaces, &Configuration.Interfaces, Control, Name) == -1) goto error;
+ if(stop_jail(Data.JID) == -1) goto error;
+ char Path[PATH_MAX];
+ snprintf(Path, sizeof(Path), "%s/%s", FilesystemPrefix, Name);
+ if(stop_fs(&Configuration.Filesystem, Path) == -1) goto error;
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+get_endpoint(char const *Name, ucl_object_t *configuration, ucl_object_t *data, char const *Interface)
+{
+ if(ValidJailName(Name) == false) return -1;
+ Load_configuration(configuration);
+ Load_data(data);
+ for(size_t i = 0; i < Configuration.Interfaces.Count; ++i)
+ {
+ interface_configuration *C = Configuration.Interfaces._ + i;
+ if(strcmp(C->Name, Interface) == 0)
+ {
+ interface *E = Data.Interfaces._ + i;
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ JPrintObjectBegin(&S);
+ JPrintMember(&S, "address");
+ char Address[NG_PATHSIZ];
+ snprintf(Address, sizeof(Address), "[%x]:", E->ID);
+ JPrint_string(&S, Address);
+ JPrintMember(&S, "hook");
+ JPrint_string(&S, "ether");
+ JPrintObjectEnd(&S);
+ Free_all();
+ return 0;
+ }
+ }
+ Free_all();
+ return -1;
+}
+
+int
+cmd(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ if(ValidJailName(Name) == false) return -1;
+ Load_configuration(configuration);
+ Load_data(data);
+ if(ArgCount < 1) usage();
+ char const *Command = Arg[0];
+ --ArgCount;
+ ++Arg;
+ if(strcmp(Command, "sh") == 0)
+ {
+ size_t ExtraArguments = 2;
+ char **Args = alloca(sizeof(char *) * (ArgCount + ExtraArguments + 1));
+ Args[0] = "jexec";
+ Args[1] = strdup(Name);
+ memcpy(Args + ExtraArguments, Arg, sizeof(char *) * ArgCount);
+ Args[ArgCount + ExtraArguments] = NULL;
+ execvp("jexec", Args);
+ fprintf(stderr, "execvp failed: %s\n", strerror(errno));
+ exit(-1);
+ } else if(strcmp(Command, "wireshark") == 0)
+ {
+ if(ArgCount < 1) usage();
+ char const *Interface = Arg[0];
+ --ArgCount;
+ ++Arg;
+ fd Pipe[2];
+ if(pipe(Pipe) == -1)
+ {
+ fprintf(stderr, "pipe: %s\n", strerror(errno));
+ return -1;
+ }
+ pid_t PID = fork();
+ if(PID == -1)
+ {
+ fprintf(stderr, "fork: %s\n", strerror(errno));
+ return -1;
+ } else if(PID == 0)
+ {
+ dup2(Pipe[1], STDOUT_FILENO);
+ close(Pipe[0]);
+ close(Pipe[1]);
+ close_all();
+ if(jail_attach(Data.JID))
+ {
+ fprintf(stderr, "jail_attach(%d): %s\n", Data.JID, strerror(errno));
+ exit(-1);
+ }
+ execlp("tcpdump", "tcpdump", "-s", "-0", "-U", "-w", "-", "-i", Interface, NULL);
+ fprintf(stderr, "execlp failed: %s\n", strerror(errno));
+ exit(-1);
+ } else
+ {
+ dup2(Pipe[0], STDIN_FILENO);
+ close(Pipe[0]);
+ close(Pipe[1]);
+ close_all();
+ char Title[64];
+ snprintf(Title, sizeof(Title), "gui.window_title:%s:%s", Name, Interface);
+ execlp("wireshark", "wireshark", "-o", Title, "-k", "-i", "-", NULL);
+ fprintf(stderr, "execlp failed: %s\n", strerror(errno));
+ exit(-1);
+ }
+ } else
+ {
+ fprintf(stderr, "unknown command %s\n", Command);
+ usage();
+ }
+}
+
+int
+mod(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ if(ValidJailName(Name) == false) return -1;
+ Load_configuration(configuration);
+ Load_data(data);
+ UNUSED(ArgCount, Arg);
+ usage();
+}
diff --git a/src/sf.jail/state.c b/src/sf.jail/state.c
new file mode 100644
index 0000000..893961e
--- /dev/null
+++ b/src/sf.jail/state.c
@@ -0,0 +1,396 @@
+#include "state.h"
+
+// FREE
+
+void
+Free_interface_configuration(interface_configuration *E)
+{
+ free(E->Name);
+ switch(E->Type)
+ {
+ case interface_type_eiface: break;
+ case interface_type_steal: free(E->steal.Interface); break;
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+}
+
+void
+Free_interfaces_configuration(interfaces_configuration *E)
+{
+ for(size_t i = 0; i < E->Count; ++i) Free_interface_configuration(E->_ + i);
+ free(E->_);
+}
+
+void
+Free_mapping(mapping *E)
+{
+ free(E->Source);
+ free(E->Target);
+}
+
+void
+Free_filesystem_configuration(filesystem_configuration *E)
+{
+ for(size_t i = 0; i < E->LayerCount; ++i) free(E->Layer[i]);
+ if(E->Layer != NULL) free(E->Layer);
+ if(E->Temporary != NULL) free(E->Temporary);
+ for(size_t i = 0; i < E->VolumeCount; ++i) Free_mapping(E->Volume + i);
+ if(E->Volume != NULL) free(E->Volume);
+}
+
+void
+Free_configuration(configuration *E)
+{
+ Free_filesystem_configuration(&E->Filesystem);
+ Free_interfaces_configuration(&E->Interfaces);
+ Free_string_array(&E->Init);
+ Free_string_array(&E->Shutdown);
+}
+
+void
+Free_interface(interface *E)
+{
+ UNUSED(E);
+}
+
+void
+Free_interfaces(interfaces *E)
+{
+ for(size_t i = 0; i < E->Count; ++i) Free_interface(E->_ + i);
+ free(E->_);
+}
+
+void
+Free_data(data *E)
+{
+ Free_interfaces(&E->Interfaces);
+}
+
+// PARSE
+
+char *
+Parse_interface_type(interface_type *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(STRING);
+ char const *Type = ucl_object_tostring(root);
+ for(interface_type i = 0; i < interface_type_COUNT; ++i)
+ {
+ if(strcmp(interface_type_Names[i], Type) == 0)
+ {
+ *E = i;
+ return NULL;
+ }
+ }
+ asprintf(&Error, "%s invalid interface type '%s'", Position, Type);
+ goto error;
+error:
+ return Error;
+}
+
+char *
+Parse_interface_configuration(interface_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.type", Position);
+ Error = Parse_interface_type(&E->Type, ucl_object_lookup(root, "type"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ switch(E->Type)
+ {
+ case interface_type_eiface: break;
+ case interface_type_steal:
+ {
+ ucl_object_t const *interface = ucl_object_lookup(root, "interface");
+ UCL_CHECK(interface, STRING);
+ E->steal.Interface = strdup(ucl_object_tostring(interface));
+ break;
+ }
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_interfaces_configuration(interfaces_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ E->Count = root->len;
+ E->_ = calloc(sizeof(interface_configuration), E->Count);
+ {
+ ucl_object_iter_t it = NULL;
+ for(E->Count = 0; E->Count < root->len; ++E->Count)
+ {
+ ucl_object_t const *v = ucl_iterate_object(root, &it, true);
+ if(v == NULL) break;
+ char const *k = ucl_object_key(v);
+ if(k == NULL) continue;
+ interface_configuration *I = E->_ + E->Count;
+ char *NewPosition;
+ asprintf(&NewPosition, "%s['%s']", Position, k);
+ Error = Parse_interface_configuration(I, v, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ I->Name = strdup(k);
+ }
+ }
+ return NULL;
+error:
+ Free_interfaces_configuration(E);
+ return Error;
+}
+
+char *
+Parse_mapping(mapping *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *source = ucl_object_lookup(root, "source");
+ UCL_CHECK(source, STRING);
+ ucl_object_t const *target = ucl_object_lookup(root, "target");
+ UCL_CHECK(target, STRING);
+ E->Source = strdup(ucl_object_tostring(source));
+ E->Target = strdup(ucl_object_tostring(target));
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_filesystem_configuration(filesystem_configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *layers = ucl_object_lookup(root, "layers");
+ UCL_CHECK_OPTIONAL(layers, ARRAY);
+ ucl_object_t const *temporary = ucl_object_lookup(root, "temporary");
+ UCL_CHECK_OPTIONAL(temporary, STRING);
+ ucl_object_t const *volumes = ucl_object_lookup(root, "volumes");
+ UCL_CHECK_OPTIONAL(volumes, ARRAY);
+ ucl_object_t const *devfs = ucl_object_lookup(root, "devfs");
+ UCL_CHECK_OPTIONAL(devfs, INT);
+ if(layers != NULL)
+ {
+ E->Layer = calloc(sizeof(char *), layers->len);
+ ucl_object_iter_t it = NULL;
+ for(E->LayerCount = 0; E->LayerCount < layers->len; ++E->LayerCount)
+ {
+ ucl_object_t const *v = ucl_iterate_object(layers, &it, true);
+ if(v == NULL) break;
+ char const *k = ucl_object_key(v);
+ if(k != NULL) continue;
+ if(v->type != UCL_STRING)
+ {
+ asprintf(&Error, "%s.layers[%zu] is not " UCL_CHECK_HELPER(STRING), Position, E->LayerCount);
+ goto error;
+ }
+ E->Layer[E->LayerCount] = strdup(ucl_object_tostring(v));
+ }
+ }
+ if(temporary != NULL) E->Temporary = strdup(ucl_object_tostring(temporary));
+ if(volumes != NULL)
+ {
+ E->Volume = calloc(sizeof(mapping), volumes->len);
+ ucl_object_iter_t it = NULL;
+ for(E->VolumeCount = 0; E->VolumeCount < volumes->len; ++E->VolumeCount)
+ {
+ ucl_object_t const *v = ucl_iterate_object(volumes, &it, true);
+ if(v == NULL) break;
+ char const *k = ucl_object_key(v);
+ if(k != NULL) continue;
+ mapping *I = E->Volume + E->VolumeCount;
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.volumes[%zu]", Position, E->VolumeCount);
+ Error = Parse_mapping(I, v, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ }
+ E->DevfsRuleset = ucl_object_toint(devfs);
+ return NULL;
+error:
+ Free_filesystem_configuration(E);
+ return Error;
+}
+
+char *
+Parse_configuration(configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.interfaces", Position);
+ Error =
+ Parse_interfaces_configuration(&E->Interfaces, ucl_object_lookup(root, "interfaces"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.filesystem", Position);
+ Error =
+ Parse_filesystem_configuration(&E->Filesystem, ucl_object_lookup(root, "filesystem"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.init", Position);
+ Error = Parse_string_array(&E->Init, ucl_object_lookup(root, "init"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.shutdown", Position);
+ Error = Parse_string_array(&E->Shutdown, ucl_object_lookup(root, "shutdown"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_interface(interface *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.type", Position);
+ Error = Parse_interface_type(&E->Type, ucl_object_lookup(root, "type"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ switch(E->Type)
+ {
+ case interface_type_eiface:
+ {
+ ucl_object_t const *id = ucl_object_lookup(root, "id");
+ UCL_CHECK(id, INT);
+ s64 ID = ucl_object_toint(id);
+ if(ID < 0 || ID > INT32_MAX)
+ {
+ asprintf(&Error, "%s id invalid (%ld)", Position, ID);
+ goto error;
+ }
+ E->ID = (u32)ID;
+ break;
+ }
+ case interface_type_steal: break;
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_interfaces(interfaces *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(ARRAY);
+ E->Count = root->len;
+ E->_ = calloc(sizeof(interface_configuration), E->Count);
+ {
+ ucl_object_iter_t it = NULL;
+ for(E->Count = 0; E->Count < root->len; ++E->Count)
+ {
+ ucl_object_t const *v = ucl_iterate_object(root, &it, true);
+ if(v == NULL) break;
+ char const *k = ucl_object_key(v);
+ if(k != NULL) continue;
+ interface *I = E->_ + E->Count;
+ char *NewPosition;
+ asprintf(&NewPosition, "%s['%zu']", Position, E->Count);
+ Error = Parse_interface(I, v, NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ }
+ return NULL;
+error:
+ Free_interfaces(E);
+ return Error;
+}
+
+char *
+Parse_data(data *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ ucl_object_t const *jid = ucl_object_lookup(root, "jid");
+ UCL_CHECK(jid, INT);
+ int64_t JID = ucl_object_toint(jid);
+ if(JID < 0 || JID > INT32_MAX)
+ {
+ asprintf(&Error, "%s jid invalid (%ld)", Position, JID);
+ goto error;
+ }
+ E->JID = (jid_t)JID;
+ {
+ char *NewPosition;
+ asprintf(&NewPosition, "%s.interfaces", Position);
+ Error = Parse_interfaces(&E->Interfaces, ucl_object_lookup(root, "interfaces"), NewPosition);
+ free(NewPosition);
+ if(Error != NULL) goto error;
+ }
+ return NULL;
+error:
+ return Error;
+}
+
+// SAVE
+
+void
+Save_interface(jprint_state *S, interface const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "type");
+ JPrint_string(S, interface_type_Names[E->Type]);
+ switch(E->Type)
+ {
+ case interface_type_eiface:
+ {
+ JPrintMember(S, "id");
+ JPrint_ssize_t(S, E->ID);
+ break;
+ }
+ case interface_type_steal: break;
+ case interface_type_COUNT:
+ default: __builtin_unreachable();
+ }
+ JPrintObjectEnd(S);
+}
+
+void
+Save_interfaces(jprint_state *S, interfaces const *E)
+{
+ JPrintArrayBegin(S);
+ for(size_t i = 0; i < E->Count; ++i) Save_interface(S, E->_ + i);
+ JPrintArrayEnd(S);
+}
+
+void
+Save_data(jprint_state *S, data const *E)
+{
+ JPrintObjectBegin(S);
+ JPrintMember(S, "jid");
+ JPrint_ssize_t(S, E->JID);
+ JPrintMember(S, "interfaces");
+ Save_interfaces(S, &E->Interfaces);
+ JPrintObjectEnd(S);
+}
diff --git a/src/sf.jail/state.h b/src/sf.jail/state.h
new file mode 100644
index 0000000..d22d374
--- /dev/null
+++ b/src/sf.jail/state.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include "../util.h"
+
+NAMED_ENUM(interface_type, eiface, steal);
+
+struct
+{
+ char *Name;
+ interface_type Type;
+ union
+ {
+ struct
+ {
+ } eiface;
+ struct
+ {
+ char *Interface;
+ } steal;
+ };
+} typedef interface_configuration;
+
+struct
+{
+ size_t Count;
+ interface_configuration *_;
+} typedef interfaces_configuration;
+
+struct
+{
+ char *Source;
+ char *Target;
+} typedef mapping;
+
+struct
+{
+ size_t LayerCount;
+ char **Layer;
+ char *Temporary;
+ size_t VolumeCount;
+ mapping *Volume;
+ s64 DevfsRuleset;
+} typedef filesystem_configuration;
+
+struct
+{
+ interfaces_configuration Interfaces;
+ filesystem_configuration Filesystem;
+ string_array Init;
+ string_array Shutdown;
+} typedef configuration;
+
+struct
+{
+ u32 ID;
+ interface_type Type;
+} typedef interface;
+
+struct
+{
+ size_t Count;
+ interface *_;
+} typedef interfaces;
+
+struct
+{
+ interfaces Interfaces;
+ jid_t JID;
+} typedef data;
+
+void Free_interface_configuration(interface_configuration *);
+void Free_interfaces_configuration(interfaces_configuration *);
+void Free_mapping(mapping *);
+void Free_filesystem_configuration(filesystem_configuration *);
+void Free_configuration(configuration *);
+void Free_interface(interface *);
+void Free_interfaces(interfaces *);
+void Free_data(data *);
+
+char *Parse_interface_type(interface_type *, ucl_object_t const *, char const *Position);
+char *Parse_interface_configuration(interface_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_interfaces_configuration(interfaces_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_mapping(mapping *, ucl_object_t const *, char const *Position);
+char *Parse_filesystem_configuration(filesystem_configuration *, ucl_object_t const *, char const *Position);
+char *Parse_configuration(configuration *, ucl_object_t const *, char const *Position);
+char *Parse_interface(interface *, ucl_object_t const *, char const *Position);
+char *Parse_interfaces(interfaces *, ucl_object_t const *, char const *Position);
+char *Parse_data(data *, ucl_object_t const *, char const *Position);
+
+void Save_interface_type(jprint_state *, interface_type const *);
+void Save_interface(jprint_state *, interface const *);
+void Save_interfaces(jprint_state *, interfaces const *);
+void Save_data(jprint_state *, data const *);
diff --git a/src/sf.skel/main.c b/src/sf.skel/main.c
new file mode 100644
index 0000000..5460454
--- /dev/null
+++ b/src/sf.skel/main.c
@@ -0,0 +1,116 @@
+#include "../module/module.h"
+#include "state.h"
+
+char const *Usage = "usage: %1$s start [name] [configuration]\n"
+ " %1$s stop [name] [configuration] [data]\n"
+ " %1$s get-endpoint [name] [configuration] [data] [interface]\n"
+ "unsupported commands: cmd mod\n";
+
+static configuration Configuration;
+static data Data;
+static bool ConfigurationLoaded;
+static bool DataLoaded;
+
+// HELPERS
+
+static void
+Free_all(void)
+{
+ if(ConfigurationLoaded == true) Free_configuration(&Configuration);
+ if(DataLoaded == true) Free_data(&Data);
+}
+
+static void
+Load_configuration(ucl_object_t *root)
+{
+ char *Error = Parse_configuration(&Configuration, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: configuration error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ ConfigurationLoaded = true;
+}
+
+static void
+Load_data(ucl_object_t *root)
+{
+ char *Error = Parse_data(&Data, root, "");
+ if(Error != NULL)
+ {
+ fprintf(stderr, "%s: data error: %s\n", Arg0, Error);
+ free(Error);
+ Free_all();
+ exit(-1);
+ }
+ DataLoaded = true;
+ // TODO: check that data matches configuration
+}
+
+// COMMANDS
+
+int
+start(char const *Name, ucl_object_t *configuration)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ // Implement
+ if((true)) goto error;
+ jprint_state S;
+ bzero(&S, sizeof(S));
+ S.F = stdout;
+ Save_data(&S, &Data);
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+stop(char const *Name, ucl_object_t *configuration, ucl_object_t *data)
+{
+ UNUSED(Name);
+ Load_configuration(configuration);
+ Load_data(data);
+ // Implement
+ if((true)) goto error;
+ Free_all();
+ return 0;
+error:
+ Free_all();
+ return -1;
+}
+
+int
+get_endpoint(char const *Name, ucl_object_t *configuration, ucl_object_t *data, char const *Interface)
+{
+ UNUSED(Name, Interface);
+ Load_configuration(configuration);
+ Load_data(data);
+ // Implement
+ Free_all();
+ return -1;
+}
+
+int
+cmd(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ // Implement
+ usage();
+}
+
+int
+mod(char const *Name, ucl_object_t *configuration, ucl_object_t *data, size_t ArgCount, char **Arg)
+{
+ UNUSED(Name, ArgCount, Arg);
+ Load_configuration(configuration);
+ Load_data(data);
+ // Implement
+ usage();
+}
diff --git a/src/sf.skel/state.c b/src/sf.skel/state.c
new file mode 100644
index 0000000..3a1d297
--- /dev/null
+++ b/src/sf.skel/state.c
@@ -0,0 +1,49 @@
+#include "state.h"
+
+// FREE
+
+void
+Free_configuration(configuration *E)
+{
+ UNUSED(E);
+}
+
+void
+Free_data(data *E)
+{
+ UNUSED(E);
+}
+
+// PARSE
+
+char *
+Parse_configuration(configuration *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ UNUSED(E);
+ return NULL;
+error:
+ return Error;
+}
+
+char *
+Parse_data(data *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(OBJECT);
+ UNUSED(E);
+ return NULL;
+error:
+ return Error;
+}
+
+// SAVE
+
+void
+Save_data(jprint_state *S, data const *E)
+{
+ JPrintObjectBegin(S);
+ UNUSED(E);
+ JPrintObjectEnd(S);
+}
diff --git a/src/sf.skel/state.h b/src/sf.skel/state.h
new file mode 100644
index 0000000..9f52a2b
--- /dev/null
+++ b/src/sf.skel/state.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "../jprint.h"
+
+struct
+{
+} typedef configuration;
+
+struct
+{
+} typedef data;
+
+void Free_configuration(configuration *);
+void Free_data(data *);
+
+char *Parse_configuration(configuration *, ucl_object_t const *, char const *Position);
+char *Parse_data(data *, ucl_object_t const *, char const *Position);
+
+void Save_data(jprint_state *, data const *);
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..f7d0aef
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,396 @@
+#include "util.h"
+
+#include <net/ethernet.h>
+#include <netgraph.h>
+#include <netgraph/ng_message.h>
+#include <netgraph/ng_eiface.h>
+#include <netgraph/ng_bridge.h>
+#include <sys/user.h>
+#include <libutil.h>
+
+#include <sys/jail.h>
+
+#include "/usr/src/sbin/mount/mntopts.h"
+#include <sys/mount.h>
+
+int
+DestroyNetgraphNode(u32 ID, fd Control)
+{
+ char Path[NG_PATHSIZ];
+ snprintf(Path, sizeof(Path), "[%x]:", ID);
+ if(NgSendMsg(Control, Path, NGM_GENERIC_COOKIE, NGM_SHUTDOWN, NULL, 0) == -1)
+ {
+ fprintf(stderr, "ngctl shutdown %s: %s\n", Path, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+int
+Create_eiface(u32 *ID, fd Control)
+{
+ *ID = 0;
+ {
+ struct ngm_mkpeer D;
+ strcpy(D.type, "eiface");
+ strcpy(D.ourhook, "_");
+ strcpy(D.peerhook, "ether");
+ if(NgSendMsg(Control, ".:", NGM_GENERIC_COOKIE, NGM_MKPEER, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr, "ngctl mkpeer .: eiface _ ether: %s\n", strerror(errno));
+ goto error;
+ }
+ }
+ {
+ if(NgSendMsg(Control, ".:_", NGM_GENERIC_COOKIE, NGM_NODEINFO, NULL, 0) == -1)
+ {
+ fprintf(stderr, "ngctl info .:_ : %s\n", strerror(errno));
+ goto error;
+ }
+ struct ng_mesg *Response;
+ if(NgAllocRecvMsg(Control, &Response, NULL) == -1)
+ {
+ fprintf(stderr, "NgAllocRecvMsg: %s\n", strerror(errno));
+ return -1;
+ }
+ struct nodeinfo *Info = (void *)Response->data;
+ *ID = Info->id;
+ free(Response);
+ }
+ {
+ struct ngm_rmhook D;
+ strcpy(D.ourhook, "_");
+ if(NgSendMsg(Control, ".:", NGM_GENERIC_COOKIE, NGM_RMHOOK, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr, "ngctl rmhook .: _: %s\n", strerror(errno));
+ goto error;
+ }
+ }
+ return 0;
+error:
+ if(*ID != 0) DestroyNetgraphNode(*ID, Control);
+ return -1;
+}
+
+int
+Create_bridge(u32 *ID, fd Control)
+{
+ *ID = 0;
+ {
+ struct ngm_mkpeer D;
+ strcpy(D.type, "bridge");
+ strcpy(D.ourhook, "_");
+ strcpy(D.peerhook, "link0");
+ if(NgSendMsg(Control, ".:", NGM_GENERIC_COOKIE, NGM_MKPEER, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr, "ngctl mkpeer .: eiface _ ether: %s\n", strerror(errno));
+ goto error;
+ }
+ }
+ {
+ if(NgSendMsg(Control, ".:_", NGM_BRIDGE_COOKIE, NGM_BRIDGE_SET_PERSISTENT, NULL, 0) == -1)
+ {
+ fprintf(stderr, "ngctl msg .:_ setpersistent: %s\n", strerror(errno));
+ goto error;
+ }
+ }
+ {
+ if(NgSendMsg(Control, ".:_", NGM_GENERIC_COOKIE, NGM_NODEINFO, NULL, 0) == -1)
+ {
+ fprintf(stderr, "ngctl info .:_ : %s\n", strerror(errno));
+ goto error;
+ }
+ struct ng_mesg *Response;
+ if(NgAllocRecvMsg(Control, &Response, NULL) == -1)
+ {
+ fprintf(stderr, "NgAllocRecvMsg: %s\n", strerror(errno));
+ return -1;
+ }
+ struct nodeinfo *Info = (void *)Response->data;
+ *ID = Info->id;
+ free(Response);
+ }
+ {
+ struct ngm_rmhook D;
+ strcpy(D.ourhook, "_");
+ if(NgSendMsg(Control, ".:", NGM_GENERIC_COOKIE, NGM_RMHOOK, &D, sizeof(D)) == -1)
+ {
+ fprintf(stderr, "ngctl rmhook .: _: %s\n", strerror(errno));
+ goto error;
+ }
+ }
+ return 0;
+error:
+ if(*ID != 0) DestroyNetgraphNode(*ID, Control);
+ return -1;
+}
+
+char *
+getifname(u32 ID, fd Control)
+{
+ struct ng_mesg *Response;
+ char Path[NG_PATHSIZ];
+ snprintf(Path, sizeof(Path), "[%x]:", ID);
+ if(NgSendMsg(Control, Path, NGM_EIFACE_COOKIE, NGM_EIFACE_GET_IFNAME, NULL, 0) == -1)
+ {
+ fprintf(stderr, "ngctl msg %s getifname: %s\n", Path, strerror(errno));
+ return NULL;
+ }
+ if(NgAllocRecvMsg(Control, &Response, NULL) == -1)
+ {
+ fprintf(stderr, "NgAllocRecvMsg: %s\n", strerror(errno));
+ return NULL;
+ }
+ char *Result = strdup((void *)Response->data);
+ free(Response);
+ return Result;
+}
+
+void
+close_all(void)
+{
+ int Count;
+ struct kinfo_file *Result = kinfo_getfile(getpid(), &Count);
+ if(Result == NULL) return;
+ for(int i = 0; i < Count; ++i)
+ {
+ if(Result[i].kf_fd > STDERR_FILENO) close(Result[i].kf_fd);
+ }
+ free(Result);
+}
+
+int
+command_a(char const *Program, char **Arg)
+{
+ pid_t PID = -1;
+ PID = fork();
+ if(PID == -1)
+ {
+ fprintf(stderr, "fork failed: %s\n", strerror(errno));
+ goto error;
+ }
+ if(PID == 0)
+ {
+ close(STDIN_FILENO);
+ close(STDOUT_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ close_all();
+ execvp(Program, Arg);
+ fprintf(stderr, "exec failed: %s\n", strerror(errno));
+ exit(-1);
+ }
+ int Status;
+ waitpid(PID, &Status, 0);
+ if(!WIFEXITED(Status) || (WEXITSTATUS(Status) != 0)) return -1;
+ return 0;
+error:
+ return -1;
+}
+
+int
+command_v(char const *Program, va_list Arg)
+{
+ va_list Arg_;
+ va_copy(Arg_, Arg);
+ size_t ArgCount = 0;
+ do {
+ char const *_ = va_arg(Arg_, char const *);
+ ++ArgCount;
+ if(_ == NULL) break;
+ } while(true);
+ va_end(Arg_);
+ char **Args = alloca(sizeof(char *) * ArgCount);
+ for(size_t i = 0; i < ArgCount; ++i) Args[i] = va_arg(Arg, char *);
+ return command_a(Program, Args);
+}
+
+int
+command(char const *Program, ...)
+{
+ va_list Arg;
+ va_start(Arg, Program);
+ int Result = command_v(Program, Arg);
+ va_end(Arg);
+ return Result;
+}
+
+void
+Free_string_array(string_array *E)
+{
+ for(size_t i = 0; i < E->Count; ++i) free(E->_[i]);
+ if(E->_ != NULL) free(E->_);
+}
+
+char *
+Parse_string_array(string_array *E, ucl_object_t const *root, char const *Position)
+{
+ char *Error;
+ UCL_CHECK_ROOT(ARRAY);
+ E->Count = root->len;
+ E->_ = calloc(sizeof(char *), E->Count + 1); // NOTE: we null-terminate it
+ ucl_object_iter_t it = NULL;
+ for(E->Count = 0; E->Count < root->len; ++E->Count)
+ {
+ ucl_object_t const *v = ucl_iterate_object(root, &it, true);
+ if(v == NULL) break;
+ char const *k = ucl_object_key(v);
+ if(k != NULL) continue;
+ if(v->type != UCL_STRING)
+ {
+ asprintf(&Error, "%s[%zu] is not a string", Position, E->Count);
+ goto error;
+ }
+ E->_[E->Count] = strdup(ucl_object_tostring(v));
+ }
+ return NULL;
+error:
+ Free_string_array(E);
+ return Error;
+}
+
+void
+Save_string_array(jprint_state *S, string_array const *E)
+{
+ JPrintArrayBegin(S);
+ for(size_t i = 0; i < E->Count; ++i) JPrint_string(S, E->_[i]);
+ JPrintArrayEnd(S);
+}
+
+int
+exec_string_array(string_array *E, bool Wait, jid_t JID)
+{
+ if(E->Count == 0) return 0;
+ pid_t PID = fork();
+ if(PID == -1) return -1;
+ if(PID == 0)
+ {
+ close(STDIN_FILENO);
+ dup2(STDERR_FILENO, STDOUT_FILENO);
+ close_all();
+ if(JID != 0)
+ {
+ if(jail_attach(JID) == -1)
+ {
+ fprintf(stderr, "failed to attach to jail %d: %s\n", JID, strerror(errno));
+ exit(-1);
+ }
+ }
+ execvp(E->_[0], E->_);
+ fprintf(stderr, "failed exec '%s': %s\n", E->_[0], strerror(errno));
+ exit(-1);
+ }
+ if(Wait == true)
+ {
+ int Exit;
+ waitpid(PID, &Exit, 0);
+ if(!WIFEXITED(Exit) || (WEXITSTATUS(Exit) != 0)) return -1;
+ }
+ return 0;
+}
+
+static int
+subdir(char const *p, char const *dir)
+{
+ size_t l = strlen(dir);
+ if(l <= 1) return 1;
+ if((strncmp(p, dir, l) == 0) && (p[l] == '/' || p[l] == '\0')) return 1;
+ return 0;
+}
+
+int
+mount_unionfs(char const *Target, char const *Source, bool Below)
+{
+ char TargetR[MAXPATHLEN];
+ char SourceR[MAXPATHLEN];
+ if(checkpath(Target, TargetR) != 0)
+ {
+ fprintf(stderr, "checkpath %s\n", TargetR);
+ return -1;
+ }
+ if(checkpath(Source, SourceR) != 0)
+ {
+ fprintf(stderr, "checkpath %s\n", SourceR);
+ return -1;
+ }
+ if(subdir(TargetR, SourceR) || subdir(SourceR, TargetR))
+ {
+ fprintf(stderr, "%s (%s) and %s (%s) are not distinct paths\n", Target, TargetR, Source, SourceR);
+ return -1;
+ }
+ char errmsg[255];
+ bzero(errmsg, sizeof(errmsg));
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ build_iovec(&iov, &iovlen, "fstype", "unionfs", (size_t)-1);
+ if(Below == true) build_iovec(&iov, &iovlen, "below", NULL, 0);
+ build_iovec(&iov, &iovlen, "fspath", TargetR, (size_t)-1);
+ build_iovec(&iov, &iovlen, "from", SourceR, (size_t)-1);
+ build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg));
+ if(nmount(iov, (u32)iovlen, 0))
+ {
+ fprintf(stderr, "nmount unionfs %s: %s\n", SourceR, errmsg);
+ return -1;
+ }
+ return 0;
+}
+
+int
+mount_devfs(char const *Target)
+{
+ char TargetR[MAXPATHLEN];
+ if(checkpath(Target, TargetR) != 0)
+ {
+ fprintf(stderr, "checkpath %s\n", TargetR);
+ return -1;
+ }
+ char errmsg[255];
+ bzero(errmsg, sizeof(errmsg));
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ build_iovec(&iov, &iovlen, "fstype", "devfs", (size_t)-1);
+ build_iovec(&iov, &iovlen, "fspath", TargetR, (size_t)-1);
+ build_iovec(&iov, &iovlen, "from", "devfs", (size_t)-1);
+ build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg));
+ if(nmount(iov, (u32)iovlen, 0))
+ {
+ fprintf(stderr, "nmount devfs: %s\n", errmsg);
+ return -1;
+ }
+ return 0;
+}
+
+int
+mount_nullfs(char const *Target, char const *Source)
+{
+ char TargetR[MAXPATHLEN];
+ char SourceR[MAXPATHLEN];
+ if(checkpath(Target, TargetR) != 0)
+ {
+ fprintf(stderr, "checkpath %s\n", TargetR);
+ return -1;
+ }
+ if(checkpath(Source, SourceR) != 0)
+ {
+ fprintf(stderr, "checkpath %s\n", SourceR);
+ return -1;
+ }
+ if(subdir(TargetR, SourceR) || subdir(SourceR, TargetR))
+ {
+ fprintf(stderr, "%s (%s) and %s (%s) are not distinct paths\n", Target, TargetR, Source, SourceR);
+ return -1;
+ }
+ char errmsg[255];
+ bzero(errmsg, sizeof(errmsg));
+ struct iovec *iov = NULL;
+ int iovlen = 0;
+ build_iovec(&iov, &iovlen, "fstype", "nullfs", (size_t)-1);
+ build_iovec(&iov, &iovlen, "fspath", TargetR, (size_t)-1);
+ build_iovec(&iov, &iovlen, "from", SourceR, (size_t)-1);
+ build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg));
+ if(nmount(iov, (u32)iovlen, 0))
+ {
+ fprintf(stderr, "nmount nullfs %s: %s\n", SourceR, errmsg);
+ return -1;
+ }
+ return 0;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..02ed6ad
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "base.h"
+#include "jprint.h"
+
+int Create_eiface(u32 *ID, fd Control);
+int Create_bridge(u32 *ID, fd Control);
+int DestroyNetgraphNode(u32 ID, fd Control);
+char *getifname(u32 ID, fd Control);
+void close_all(void);
+int command_a(char const *Program, char **Arg);
+int command_v(char const *Program, va_list Arg);
+int command(char const *Program, ...);
+
+struct
+{
+ size_t Count;
+ char **_;
+} typedef string_array;
+
+void Free_string_array(string_array *);
+char *Parse_string_array(string_array *, ucl_object_t const *root, char const *Position);
+void Save_string_array(jprint_state *, string_array const *);
+int exec_string_array(string_array *, bool Wait, jid_t JID);
+
+int mount_unionfs(char const *Target, char const *Source, bool Below);
+int mount_devfs(char const *Target);
+int mount_nullfs(char const *Target, char const *Source);
diff --git a/todo.org b/todo.org
new file mode 100644
index 0000000..676d338
--- /dev/null
+++ b/todo.org
@@ -0,0 +1,113 @@
+
+- [X] escape strings in jprint
+- [X] improve NgSendMsg error messages
+- [X] destroy eiface if ifconfig_vnet fails
+- [X] directly use nmount instead of fork+exec
+- [X] mdkir for layers in lamina
+- [X] remove target directory for layers
+ - because of how unionfs works, union is not done on separate subdirectories with the same name
+ - this confused me at first and to prevent further confusion I should probably remove target
+- [X] escape strings when printing mounts in sf.lamina
+- [X] move examples to a separate repository
+- [ ] use rmslashes
+- [ ] add a bunch of missing checks to sf.lamina
+- [ ] create getting started
+- [ ] create documentation / man pages
+ - [ ] sf system
+ - [ ] sf.base
+ - [ ] modules
+ - [ ] sf.skeleton
+ - [ ] sf.jail
+ - [ ] sf.eiface
+ - [ ] sf.bridge
+ - [ ] sf.bhyve
+ - [ ] UIs
+- [ ] create a more elaborate topology to show of more features
+
+- [-] sf.base
+ - [X] load module specific data
+ - [X] call modules on node start and stop operations
+ - [X] call custom module functions
+ - [X] call modules to get endpoints
+ - [X] node start
+ - [X] node stop
+ - [X] link start
+ - [X] link stop
+ - [X] cmd
+ - [X] mod
+ - [X] make link creation simpler
+ - [X] when stopping nodes, first disconnect their links
+ - [ ] lossy links using ng_pipe
+ - [ ] allow caller to store arbitrary data in state
+
+- [X] sf.skeleton
+
+- [-] sf.jail
+ - [X] parse configuration
+ - [X] parse data
+ - [X] start
+ - [X] fs
+ - [X] jail
+ - [X] interfaces
+ - [X] move interfaces to vnet
+ - [X] stop
+ - [X] fs
+ - [X] jail
+ - [X] interfaces
+ - [X] get-endpoint
+ - [X] cmd
+ - [X] sh
+ - [X] wireshark
+ - [X] interface types
+ - [X] eiface
+ - [X] steal
+ - [X] mount devfs
+ - [X] init system
+ - [X] start
+ - [X] shutdown
+ - [X] custom commands
+ - [X] specify devfs ruleset
+ - [X] remove unmount sleep
+ - [X] we force unmount after jail_remove
+ - [X] check that name is valid
+ - [ ] investigate zfs whiteout support
+ - [ ] mod
+
+- [X] sf.eiface
+ - [X] parse configuration
+ - [X] parse data
+ - [X] start
+ - [X] stop
+ - [X] get-endpoint
+ - [X] allow automatic address assignment
+ - [X] check that name is valid
+
+- [X] sf.bridge
+ - [X] parse configuration
+ - [X] parse data
+ - [X] start
+ - [X] stop
+ - [X] get-endpoint
+
+- [ ] sf.bhyve
+ - [ ] parse configuration
+ - [ ] parse data
+ - [ ] start
+ - [ ] stop
+ - [ ] get-endpoint
+ - [ ] 9p
+ - this will probably have to wait for 15.0-RELEASE
+
+- [X] sf.lamina
+ - [X] parse commands
+ - [X] commands
+ - [X] LAYER
+ - [X] DEVFS
+ - [X] RUN
+ - [X] CHDIR
+ - [X] COPY
+ - [X] USER
+
+- [ ] sf.gui
+ - Web gui would be nice... I should first play with canvas a bit and javascript. I have no previous experience with this.
+ - Considering we are handling UCL/json/yaml data, web client shouldn't be that hard.