summaryrefslogtreecommitdiff
path: root/src/sf.jail/main.c
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 /src/sf.jail/main.c
Import project
Diffstat (limited to 'src/sf.jail/main.c')
-rw-r--r--src/sf.jail/main.c525
1 files changed, 525 insertions, 0 deletions
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();
+}