diff --git a/doc/tinc.conf.5.in b/doc/tinc.conf.5.in index f0e765dc2..8ede45b5e 100644 --- a/doc/tinc.conf.5.in +++ b/doc/tinc.conf.5.in @@ -493,7 +493,7 @@ reordering. Setting this to zero will disable replay tracking completely and pass all traffic, but leaves tinc vulnerable to replay-based attacks on your traffic. .It Va Sandbox Li = off | normal | high Po normal Pc -Use process sandbox on some operating systems where it is supported (currently that's OpenBSD). +Use process sandbox on some operating systems where it is supported (currently that's Linux and OpenBSD). Using this directive on other operating systems with levels higher than .Ar off will cause @@ -512,11 +512,7 @@ all functionality works as if this feature did not exist. .It normal The default level which aims to be safe for most users. Adds some level of protection with only minor reductions in functionality. -For example, executables located in non-standard paths may not be available as -.Nm tincd -scripts or -.Ar exec -proxies, and configuration reloading may not work for some variables, forcing you to restart +For example, configuration reloading may not work for some variables, forcing you to restart .Nm tincd to apply new settings. .It high diff --git a/src/bsd/openbsd/tincd.c b/src/bsd/openbsd/tincd.c index 0b49ea9eb..bee7c70d6 100644 --- a/src/bsd/openbsd/tincd.c +++ b/src/bsd/openbsd/tincd.c @@ -1,88 +1,49 @@ #include "../../system.h" -#include #include #include "sandbox.h" #include "../../device.h" #include "../../logger.h" #include "../../names.h" -#include "../../net.h" #include "../../sandbox.h" -#include "../../script.h" -#include "../../xalloc.h" -#include "../../proxy.h" static sandbox_level_t current_level = SANDBOX_NONE; static bool can_use_new_paths = true; static bool entered = false; -static bool chrooted(void) { - return !(confbase && *confbase); -} - -static void create_conf_subdir(const char *name, mode_t mode) { - char path[PATH_MAX]; - snprintf(path, sizeof(path), "%s/%s", confbase, name); - mkdir(path, mode); -} - static void open_conf_subdir(const char *name, const char *privs) { char path[PATH_MAX]; - snprintf(path, sizeof(path), "%s/%s", confbase, name); + conf_subdir(path, name); allow_path(path, privs); } -static void open_common_paths(bool can_exec) { +static void open_common_paths(void) { // Dummy device uses a fake path, skip it const char *dev = strcasecmp(device, DEVICE_DUMMY) ? device : NULL; - // These calls must be done before the first unveil() for two reasons: - // 1. the first unveil() blocks access to all other paths. - // 2. unveil() remembers the exact directory and won't allow access if it's (re)created. - create_conf_subdir("cache", 0777); - create_conf_subdir("hosts", 0777); - create_conf_subdir("invitations", 0700); - const unveil_path_t paths[] = { - {"/dev/random", "r"}, - {"/dev/urandom", "r"}, - {confbase, can_exec ? "rx" : "r"}, - {dev, "rw"}, - {logfilename, "rwc"}, - {pidfilename, "rwc"}, - {unixsocketname, "rwc"}, - {NULL, NULL}, + {"/dev/random", "r"}, + {"/dev/urandom", "r"}, + {"/etc/resolv.conf", "r"}, + {"/etc/hosts", "r"}, + {confbase, "r"}, + {dev, "rw"}, + {logfilename, "rwc"}, + {pidfilename, "rwc"}, + {unixsocketname, "rwc"}, + {NULL, NULL}, }; allow_paths(paths); open_conf_subdir("cache", "rwc"); - open_conf_subdir("hosts", can_exec ? "rwxc" : "rwc"); + open_conf_subdir("hosts", "rwc"); open_conf_subdir("invitations", "rwc"); } -static void open_exec_paths(void) { - // proxyhost was checked previously. If we're here, proxyhost - // contains the path to the executable, and nothing else. - const char *proxy_exec = proxytype == PROXY_EXEC ? proxyhost : NULL; - - const unveil_path_t bin_paths[] = { - {"/bin", "rx"}, - {"/sbin", "rx"}, - {"/usr/bin", "rx"}, - {"/usr/sbin", "rx"}, - {"/usr/local/bin", "rx"}, - {"/usr/local/sbin", "rx"}, - {scriptinterpreter, "rx"}, - {proxy_exec, "rx"}, - {NULL, NULL}, - }; - allow_paths(bin_paths); -} - -static bool sandbox_privs(bool can_exec) { +static bool sandbox_privs(void) { // no mcast since multicasting should be set up by now - char promises[512] = + const char *promises = "stdio" // General I/O, both disk and network " rpath" // Read files and directories " wpath" // Write files and directories @@ -90,39 +51,24 @@ static bool sandbox_privs(bool can_exec) { " dns" // Resolve domain names " inet" // Make network connections " unix"; // Control socket connections from tinc CLI - - if(can_exec) { - // fork() and execve() for scripts and exec proxies - const char *exec = " proc exec"; - size_t n = strlcat(promises, exec, sizeof(promises)); - assert(n < sizeof(promises)); - } - - return restrict_privs(promises, can_exec ? PROMISES_ALL : PROMISES_NONE); + return restrict_privs(promises, PROMISES_NONE); } -static void sandbox_paths(bool can_exec) { +static void sandbox_paths(void) { if(chrooted()) { logger(DEBUG_ALWAYS, LOG_DEBUG, "chroot is used. Disabling path sandbox."); - return; - } - - open_common_paths(can_exec); - can_use_new_paths = false; - - if(can_exec) { - if(proxytype == PROXY_EXEC && !access(proxyhost, X_OK)) { - logger(DEBUG_ALWAYS, LOG_WARNING, "Looks like a shell expression was used for exec proxy. Using weak path sandbox."); - allow_path("/", "rx"); - } else { - open_exec_paths(); - } + } else { + open_common_paths(); + can_use_new_paths = false; } } static bool sandbox_can_after_enter(sandbox_action_t action) { switch(action) { case START_PROCESSES: + return current_level == SANDBOX_NONE; + + case RUN_SCRIPTS: return current_level < SANDBOX_HIGH; case USE_NEW_PATHS: @@ -141,6 +87,14 @@ bool sandbox_can(sandbox_action_t action, sandbox_time_t when) { } } +bool sandbox_enabled(void) { + return current_level > SANDBOX_NONE; +} + +bool sandbox_active(void) { + return sandbox_enabled() && entered; +} + void sandbox_set_level(sandbox_level_t level) { assert(!entered); current_level = level; @@ -155,11 +109,9 @@ bool sandbox_enter() { return true; } - bool can_exec = sandbox_can_after_enter(START_PROCESSES); - - sandbox_paths(can_exec); + sandbox_paths(); - if(sandbox_privs(can_exec)) { + if(sandbox_privs()) { logger(DEBUG_ALWAYS, LOG_DEBUG, "Entered sandbox at level %d", current_level); return true; } diff --git a/src/fsck.c b/src/fsck.c index b44b77595..a213f3125 100644 --- a/src/fsck.c +++ b/src/fsck.c @@ -108,17 +108,6 @@ static void print_new_keys_cmd(key_type_t key_type, const char *message) { } } -static int strtailcmp(const char *str, const char *tail) { - size_t slen = strlen(str); - size_t tlen = strlen(tail); - - if(tlen > slen) { - return -1; - } - - return memcmp(str + slen - tlen, tail, tlen); -} - static void check_conffile(const char *nodename, bool server) { splay_tree_t config; init_configuration(&config); diff --git a/src/have.h b/src/have.h index a155b04b4..96687d615 100644 --- a/src/have.h +++ b/src/have.h @@ -81,6 +81,12 @@ #define ATTR_NONNULL #endif +#ifdef HAVE_ATTR_NORETURN +#define ATTR_NORETURN __attribute__((__noreturn__)) +#else +#define ATTR_NORETURN +#endif + #ifdef HAVE_ATTR_WARN_UNUSED_RESULT #define ATTR_WARN_UNUSED __attribute__((__warn_unused_result__)) #else @@ -294,4 +300,12 @@ #define SLASH "/" #endif +#ifdef __SANITIZE_ADDRESS__ +#define HAVE_SANITIZER_ADDRESS 1 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#define HAVE_SANITIZER_ADDRESS 1 +#endif +#endif + #endif diff --git a/src/linux/landlock.c b/src/linux/landlock.c new file mode 100644 index 000000000..5156103fd --- /dev/null +++ b/src/linux/landlock.c @@ -0,0 +1,123 @@ +#include "../system.h" + +#ifdef HAVE_LINUX_LANDLOCK_H + +#include + +#include "landlock.h" +#include "../logger.h" + +// Full access without any restrictions +static const uint32_t ACCESS_FULL = FS_EXECUTE | + FS_WRITE_FILE | + FS_READ_FILE | + FS_READ_DIR | + FS_REMOVE_DIR | + FS_REMOVE_FILE | + FS_MAKE_CHAR | + FS_MAKE_DIR | + FS_MAKE_REG | + FS_MAKE_SOCK | + FS_MAKE_FIFO | + FS_MAKE_BLOCK | + FS_MAKE_SYM; + +#ifndef landlock_create_ruleset +static inline int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, uint32_t flags) { + return (int)syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, const void *rule_attr, uint32_t flags) { + return (int)syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags); +} +#endif + +#ifndef landlock_restrict_self +static inline int landlock_restrict_self(int ruleset_fd, uint32_t flags) { + return (int)syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif + +static void flags_to_str(char *buf, size_t len, uint64_t flags) { + snprintf(buf, len, "file[%c%c%c%c] dir[%c%c] new[%c%c%c%c%c%c%c]", + flags & FS_READ_FILE ? 'r' : '-', + flags & FS_WRITE_FILE ? 'w' : '-', + flags & FS_EXECUTE ? 'x' : '-', + flags & FS_REMOVE_FILE ? 'd' : '-', + + flags & FS_READ_DIR ? 'r' : '-', + flags & FS_REMOVE_DIR ? 'd' : '-', + + flags & FS_MAKE_CHAR ? 'c' : '-', + flags & FS_MAKE_DIR ? 'd' : '-', + flags & FS_MAKE_REG ? 'r' : '-', + flags & FS_MAKE_SOCK ? 's' : '-', + flags & FS_MAKE_FIFO ? 'f' : '-', + flags & FS_MAKE_BLOCK ? 'b' : '-', + flags & FS_MAKE_SYM ? 'l' : '-'); +} + +static void print_path(const char *path, uint64_t flags) { + char buf[512]; + flags_to_str(buf, sizeof(buf), flags); + logger(DEBUG_ALWAYS, LOG_DEBUG, "Allowing %s with: %s", path, buf); +} + +static bool add_rule(int ruleset, const char *path, uint64_t flags) { + print_path(path, flags); + + const struct landlock_path_beneath_attr attr = { + .allowed_access = flags, + .parent_fd = open(path, O_PATH | O_CLOEXEC), + }; + + if(attr.parent_fd < 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not open path %s: %s", path, strerror(errno)); + return false; + } + + bool success = !landlock_add_rule(ruleset, LANDLOCK_RULE_PATH_BENEATH, &attr, 0); + + if(!success) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not allow path %s: %s", path, strerror(errno)); + } + + close(attr.parent_fd); + return success; +} + +static bool add_rules(int fd, const landlock_path_t paths[]) { + bool added_any = false; + + for(const landlock_path_t *p = paths; p->path || p->flags; ++p) { + if(p->path && p->flags) { + added_any |= add_rule(fd, p->path, p->flags); + } + } + + return added_any; +} + +bool allow_paths(const landlock_path_t paths[]) { + const struct landlock_ruleset_attr ruleset = {.handled_access_fs = ACCESS_FULL}; + int fd = landlock_create_ruleset(&ruleset, sizeof(ruleset), 0); + + if(fd < 0) { + if(errno == ENOSYS || errno == EOPNOTSUPP) { + logger(DEBUG_ALWAYS, LOG_WARNING, "Path protection is not supported by this kernel"); + return true; + } + + return false; + } + + bool success = add_rules(fd, paths) && + !landlock_restrict_self(fd, 0); + close(fd); + + return success; +} + +#endif // HAVE_LINUX_LANDLOCK_H diff --git a/src/linux/landlock.h b/src/linux/landlock.h new file mode 100644 index 000000000..0429ab656 --- /dev/null +++ b/src/linux/landlock.h @@ -0,0 +1,37 @@ +#ifndef TINC_LINUX_LANDLOCK_H +#define TINC_LINUX_LANDLOCK_H + +#include "../system.h" + +#ifdef HAVE_LINUX_LANDLOCK_H + +#include + +typedef struct landlock_path_t { + const char *path; + const uint64_t flags; +} landlock_path_t; + +static const uint32_t FS_EXECUTE = LANDLOCK_ACCESS_FS_EXECUTE; // Execute a file +static const uint32_t FS_WRITE_FILE = LANDLOCK_ACCESS_FS_WRITE_FILE; // Open a file with write access +static const uint32_t FS_READ_FILE = LANDLOCK_ACCESS_FS_READ_FILE; // Open a file with read access +static const uint32_t FS_READ_DIR = LANDLOCK_ACCESS_FS_READ_DIR; // Open a directory or list its content +static const uint32_t FS_REMOVE_DIR = LANDLOCK_ACCESS_FS_REMOVE_DIR; // Remove an empty directory or rename one +static const uint32_t FS_REMOVE_FILE = LANDLOCK_ACCESS_FS_REMOVE_FILE; // Unlink (or rename) a file +static const uint32_t FS_MAKE_CHAR = LANDLOCK_ACCESS_FS_MAKE_CHAR; // Create (or rename or link) a character device +static const uint32_t FS_MAKE_DIR = LANDLOCK_ACCESS_FS_MAKE_DIR; // Create (or rename) a directory +static const uint32_t FS_MAKE_REG = LANDLOCK_ACCESS_FS_MAKE_REG; // Create (or rename or link) a regular file +static const uint32_t FS_MAKE_SOCK = LANDLOCK_ACCESS_FS_MAKE_SOCK; // Create (or rename or link) a UNIX domain socket +static const uint32_t FS_MAKE_FIFO = LANDLOCK_ACCESS_FS_MAKE_FIFO; // Create (or rename or link) a named pipe +static const uint32_t FS_MAKE_BLOCK = LANDLOCK_ACCESS_FS_MAKE_BLOCK; // Create (or rename or link) a block device +static const uint32_t FS_MAKE_SYM = LANDLOCK_ACCESS_FS_MAKE_SYM; // Create (or rename or link) a symbolic link + +// Restrict access to paths using Landlock LSM (kernel 5.13+). +// Filters are inherited by child processes and cannot be removed. +// Paths not passed in the first call to this function will not be available after it returns. +// https://docs.kernel.org/userspace-api/landlock.html +extern bool allow_paths(const landlock_path_t paths[]); + +#endif // HAVE_LINUX_LANDLOCK_H + +#endif // TINC_LINUX_LANDLOCK_H diff --git a/src/linux/meson.build b/src/linux/meson.build index 1b94f95c9..144d889f4 100644 --- a/src/linux/meson.build +++ b/src/linux/meson.build @@ -1,5 +1,6 @@ check_headers += [ 'linux/if_tun.h', + 'linux/landlock.h', 'netpacket/packet.h', 'sys/epoll.h', ] @@ -20,3 +21,21 @@ if opt_uml cdata.set('ENABLE_UML', 1) endif +seccomp_tested = ['x86_64', 'aarch64'] + +if opt_sandbox.enabled() or (opt_sandbox.auto() and cpu_family in seccomp_tested) + dep_seccomp = dependency('libseccomp', required: opt_sandbox) + if dep_seccomp.found() + if cpu_family not in seccomp_tested + warning('seccomp-bpf on ' + cpu_family + ' is untested, use at your own risk') + endif + src_tincd += files('tincd.c') + deps_tincd += dep_seccomp + src_tinc += files('../sandbox.c') + cdata.set('HAVE_SANDBOX', 1) + endif +endif + +if cc.has_header('linux/landlock.h') + src_tincd += files('landlock.c') +endif diff --git a/src/linux/sandbox.h b/src/linux/sandbox.h new file mode 100644 index 000000000..5319376e3 --- /dev/null +++ b/src/linux/sandbox.h @@ -0,0 +1,14 @@ +#ifndef TINC_LINUX_SANDBOX_H +#define TINC_LINUX_SANDBOX_H + +#include "../system.h" + +#include + +#define ALLOW_CALL(call) \ + if(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(call), 0) < 0) return false + +#define DENY_CALL(call) \ + if(seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(call), 0) < 0) return false + +#endif // TINC_LINUX_SANDBOX_H diff --git a/src/linux/tincd.c b/src/linux/tincd.c new file mode 100644 index 000000000..bb99d7384 --- /dev/null +++ b/src/linux/tincd.c @@ -0,0 +1,463 @@ +#include "../system.h" + +#include +#include +#include +#include + +#include "sandbox.h" +#include "../names.h" +#include "../sandbox.h" +#include "../logger.h" +#include "../utils.h" + +#ifdef HAVE_LINUX_LANDLOCK_H +#include "landlock.h" +#endif + +static sandbox_level_t current_level = SANDBOX_NORMAL; +static bool entered = false; +static bool can_use_new_paths = true; + +#define DENY_MEMORY(call, flags) \ + if(seccomp_rule_add(ctx, SCMP_ACT_TRAP, SCMP_SYS(call), 1, SCMP_A2(SCMP_CMP_MASKED_EQ, flags, flags)) < 0) goto exit + +// Block attempts to create (or change) memory regions that are both writable and executable. +static bool add_seccomp_memory_wxe(void) { + bool success = false; + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); + + if(!ctx) { + return false; + } + + logger(DEBUG_ALWAYS, LOG_DEBUG, "Adding memory W^X filter"); + + DENY_MEMORY(mprotect, PROT_EXEC); + DENY_MEMORY(pkey_mprotect, PROT_EXEC); + DENY_MEMORY(shmat, SHM_EXEC); + DENY_MEMORY(mmap, PROT_EXEC | PROT_WRITE); + DENY_MEMORY(mmap2, PROT_EXEC | PROT_WRITE); + + success = !seccomp_load(ctx); +exit: + seccomp_release(ctx); + return success; +} + +#define ALLOW_CALL0(call, arg) \ + if(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(call), 1, SCMP_A0(SCMP_CMP_MASKED_EQ, arg, arg)) < 0) return false + +#define ALLOW_CALL1(call, arg) \ + if(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(call), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, arg, arg)) < 0) return false + +#define ALLOW_SOCKET(domain) ALLOW_CALL0(socket, domain) +#define ALLOW_SETSOCKOPT(level) ALLOW_CALL1(setsockopt, level) +#define ALLOW_GETSOCKOPT(level) ALLOW_CALL1(getsockopt, level) +#define ALLOW_IOCTL(req) ALLOW_CALL1(ioctl, req) +#define ALLOW_FCNTL(cmd) ALLOW_CALL1(fcntl, cmd); ALLOW_CALL1(fcntl64, cmd) + +static bool allow_syscall_list(scmp_filter_ctx ctx) { + const int calls[] = { +#ifdef HAVE_SANITIZER_ADDRESS + SCMP_SYS(clone), + SCMP_SYS(getppid), + SCMP_SYS(prctl), + SCMP_SYS(ptrace), + SCMP_SYS(sched_yield), + SCMP_SYS(sigaltstack), +#endif + + // threading + SCMP_SYS(futex), + SCMP_SYS(set_robust_list), +#ifdef __NR_futex_time64 + SCMP_SYS(futex_time64), +#endif + + // epoll/select + SCMP_SYS(_newselect), + SCMP_SYS(epoll_ctl), + SCMP_SYS(epoll_pwait), + SCMP_SYS(epoll_wait), + SCMP_SYS(poll), + SCMP_SYS(ppoll), + SCMP_SYS(pselect6), +#ifdef __NR_ppoll_time64 + SCMP_SYS(ppoll_time64), +#endif +#ifdef __NR_pselect6_time64 + SCMP_SYS(pselect6_time64), +#endif + + // I/O + SCMP_SYS(close), + SCMP_SYS(open), + SCMP_SYS(openat), + SCMP_SYS(pipe), + SCMP_SYS(pipe2), + SCMP_SYS(read), + SCMP_SYS(readv), + SCMP_SYS(select), + SCMP_SYS(write), + SCMP_SYS(writev), + SCMP_SYS(pread64), +#ifdef __NR_preadv + SCMP_SYS(preadv), +#endif +#ifdef __NR_preadv2 + SCMP_SYS(preadv2), +#endif +#ifdef __NR_pwritev + SCMP_SYS(pwritev), +#endif +#ifdef __NR_pwritev2 + SCMP_SYS(pwritev2), +#endif + + // network + SCMP_SYS(accept), + SCMP_SYS(bind), + SCMP_SYS(connect), + SCMP_SYS(getsockname), + SCMP_SYS(recv), + SCMP_SYS(recvfrom), + SCMP_SYS(recvmmsg), + SCMP_SYS(recvmsg), + SCMP_SYS(send), + SCMP_SYS(sendmmsg), + SCMP_SYS(sendmsg), + SCMP_SYS(sendto), + + // signals + SCMP_SYS(rt_sigaction), + SCMP_SYS(rt_sigprocmask), + SCMP_SYS(rt_sigreturn), + SCMP_SYS(signal), + SCMP_SYS(sigreturn), + + // misc + SCMP_SYS(getrandom), + SCMP_SYS(sysinfo), + SCMP_SYS(uname), + + // time + SCMP_SYS(gettimeofday), + SCMP_SYS(time), + +#ifdef HAVE_WATCHDOG + // users/groups (needed by libsystemd) + SCMP_SYS(getegid), + SCMP_SYS(geteuid), + SCMP_SYS(getgid), + SCMP_SYS(getuid), +#endif + + // process + SCMP_SYS(exit), + SCMP_SYS(exit_group), + SCMP_SYS(getpid), + SCMP_SYS(getrlimit), + SCMP_SYS(gettid), + SCMP_SYS(set_tid_address), + SCMP_SYS(wait4), + SCMP_SYS(waitpid), +#ifdef __NR_rseq + SCMP_SYS(rseq), +#endif + + // memory + SCMP_SYS(brk), + SCMP_SYS(madvise), + SCMP_SYS(mmap), + SCMP_SYS(mmap2), + SCMP_SYS(mprotect), + SCMP_SYS(mremap), + SCMP_SYS(munmap), + + // filesystem + SCMP_SYS(_llseek), + SCMP_SYS(access), + SCMP_SYS(faccessat), + SCMP_SYS(fstat), + SCMP_SYS(fstat64), + SCMP_SYS(fstatat64), + SCMP_SYS(getdents), + SCMP_SYS(getdents64), + SCMP_SYS(lseek), + SCMP_SYS(newfstatat), + SCMP_SYS(rename), + SCMP_SYS(renameat), + SCMP_SYS(renameat2), + SCMP_SYS(stat), + SCMP_SYS(stat64), + SCMP_SYS(unlink), + SCMP_SYS(unlinkat), +#ifdef __NR_statx + SCMP_SYS(statx), +#endif + }; + + // getsockopt() + ALLOW_GETSOCKOPT(SOL_SOCKET); + ALLOW_GETSOCKOPT(IPPROTO_IP); + + // setsockopt() + ALLOW_SETSOCKOPT(IPPROTO_IP); + ALLOW_SETSOCKOPT(IPPROTO_IPV6); + ALLOW_SETSOCKOPT(IPPROTO_TCP); + ALLOW_SETSOCKOPT(SOL_SOCKET); + + // socket() + ALLOW_SOCKET(AF_INET); + ALLOW_SOCKET(AF_INET6); + ALLOW_SOCKET(AF_NETLINK); // libc + ALLOW_SOCKET(AF_PACKET); + ALLOW_SOCKET(AF_UNIX); + + // ioctl() + ALLOW_IOCTL(FIONREAD); // libc + ALLOW_IOCTL(SIOCGIFHWADDR); + ALLOW_IOCTL(SIOCGIFINDEX); + ALLOW_IOCTL(TCGETS); // libc + ALLOW_IOCTL(TIOCGWINSZ); // libc + ALLOW_IOCTL(TUNSETIFF); + + // fcntl() + ALLOW_FCNTL(F_GETFL); + ALLOW_FCNTL(F_SETFD); + ALLOW_FCNTL(F_SETFL); + + for(size_t i = 0; i < sizeof(calls) / sizeof(*calls); ++i) { + if(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, calls[i], 0) < 0) { + return false; + } + } + + return true; +} + +static void handle_sigsys(int signum, siginfo_t *si, void *thread_context) ATTR_NORETURN; +static void handle_sigsys(int signum, siginfo_t *si, void *thread_context) { + (void)signum; + (void)thread_context; + + int call = si->si_syscall; + char msg[] = "Syscall XXX blocked by sandbox (possible attack, or your system is not supported yet)."; + + // The idea is stolen from memcached since formatting functions cannot be safely used here. + // Don't forget to update indexes if template is changed. + msg[8] = (char)('0' + (call / 100) % 10); + msg[9] = (char)('0' + (call / 10) % 10); + msg[10] = (char)('0' + call % 10); + + if(write(STDERR_FILENO, msg, strlen(msg)) < 0) { + // nothing we can do here + } + + _exit(EXIT_FAILURE); +} + +// Allow only syscalls used by tincd. +static bool add_seccomp_used(void) { + bool success = false; + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_TRAP); + + if(ctx) { + logger(DEBUG_ALWAYS, LOG_DEBUG, "Adding used syscalls filter"); + success = allow_syscall_list(ctx) && !seccomp_load(ctx); + seccomp_release(ctx); + } + + if(success) { + const struct sigaction act = { + .sa_sigaction = handle_sigsys, + .sa_flags = SA_SIGINFO, + }; + success = !sigaction(SIGSYS, &act, NULL); + } + + return success; +} + +static bool sandbox_can_after_enter(sandbox_action_t action) { + switch(action) { + case START_PROCESSES: + return current_level == SANDBOX_NONE; + + case RUN_SCRIPTS: + return current_level < SANDBOX_HIGH; + + case USE_NEW_PATHS: + return can_use_new_paths; + + default: + abort(); + } +} + +bool sandbox_can(sandbox_action_t action, sandbox_time_t when) { + if(when == AFTER_SANDBOX || entered) { + return sandbox_can_after_enter(action); + } else { + return true; + } +} + +bool sandbox_enabled(void) { + return current_level > SANDBOX_NONE; +} + +bool sandbox_active(void) { + return sandbox_enabled() && entered; +} + +void sandbox_set_level(sandbox_level_t level) { + assert(!entered); + current_level = level; +} + +static bool disable_escalation(void) { + return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != -1 && + prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) != -1; +} + +#ifdef HAVE_LINUX_LANDLOCK_H +// Sandbox can be circumvented by creating and starting new scripts, or writing to old ones. +// The first sceneario should be prevented by blocking all chmod-related syscalls using seccomp-bpf. +// The second one by removing write access to existing scripts using this function. +// Sadly, Landlock picks the most permissive rule and does not allow creating rules for non-existing files +// (unlike unveil() where the most specific rule wins), so we cannot use Landlock here. +static void hosts_scripts_deny_write(const char *hosts) { + DIR *dir = opendir(hosts); + + if(!dir) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not read directory %s: %s", hosts, strerror(errno)); + return; + } + + uid_t uid = getuid(); + struct dirent *ent; + + while((ent = readdir(dir))) { + if(strtailcmp(ent->d_name, "-up") && strtailcmp(ent->d_name, "-down")) { + continue; + } + + char fname[PATH_MAX]; + int total = snprintf(fname, sizeof(fname), "%s/%s", hosts, ent->d_name); + + if(total < 0 || (size_t)total >= sizeof(fname)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Path %s too long", fname); + continue; + } + + struct stat st; + + if(stat(fname, &st)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not stat file %s: %s", fname, strerror(errno)); + continue; + } + + if(st.st_uid != uid) { + continue; + } + + mode_t nowrite = (st.st_mode & 07777) & ~(S_IWUSR | S_IWGRP | S_IWOTH); + + if(chmod(fname, nowrite)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not chmod file %s: %s", fname, strerror(errno)); + } + } + + closedir(dir); + logger(DEBUG_ALWAYS, LOG_DEBUG, "Removed write access to host scripts"); +} + +static bool add_path_rules(void) { + char cache[PATH_MAX], hosts[PATH_MAX], invitations[PATH_MAX]; + conf_subdir(cache, "cache"); + conf_subdir(hosts, "hosts"); + conf_subdir(invitations, "invitations"); + + hosts_scripts_deny_write(hosts); + + char *logdir = NULL; + + if(logfilename) { + char *logf = alloca(strlen(logfilename) + 1); + strcpy(logf, logfilename); + logdir = dirname(logf); + } + + char *pidf = alloca(strlen(pidfilename) + 1); + strcpy(pidf, pidfilename); + + char *unixf = alloca(strlen(unixsocketname) + 1); + strcpy(unixf, unixsocketname); + + const landlock_path_t paths[] = { +#ifdef HAVE_SANITIZER_ADDRESS + {"/proc", FS_READ_FILE | FS_READ_DIR}, +#endif + {"/dev/random", FS_READ_FILE}, + {"/dev/urandom", FS_READ_FILE}, + {"/etc/hosts", FS_READ_FILE}, + {"/etc/nsswitch.conf", FS_READ_FILE}, + {"/etc/resolv.conf", FS_READ_FILE}, + {logdir, FS_MAKE_REG}, + {logfilename, FS_WRITE_FILE}, + {dirname(pidf), FS_REMOVE_FILE}, + {dirname(unixf), FS_REMOVE_FILE}, + {confbase, FS_READ_FILE | FS_READ_DIR}, + {cache, FS_READ_FILE | FS_WRITE_FILE | FS_REMOVE_FILE | FS_MAKE_REG | FS_READ_DIR}, + {hosts, FS_READ_FILE | FS_WRITE_FILE | FS_REMOVE_FILE | FS_MAKE_REG | FS_READ_DIR}, + {invitations, FS_READ_FILE | FS_WRITE_FILE | FS_REMOVE_FILE | FS_MAKE_REG | FS_READ_DIR}, + {NULL, 0} + }; + return allow_paths(paths); +} +#endif // HAVE_LINUX_LANDLOCK_H + +bool sandbox_enter(void) { + assert(!entered); + entered = true; + + if(current_level == SANDBOX_NONE) { + logger(DEBUG_ALWAYS, LOG_DEBUG, "Sandbox is disabled"); + return true; + } + + if(!disable_escalation()) { + logger(DEBUG_ALWAYS, LOG_ERR, "Failed to disable privilege escalation: %s", strerror(errno)); + return false; + } + +#ifdef HAVE_LINUX_LANDLOCK_H + + if(chrooted()) { + logger(DEBUG_ALWAYS, LOG_NOTICE, "chroot is used, disabling path sandbox."); + } else { + if(!add_path_rules()) { + logger(DEBUG_ALWAYS, LOG_ERR, "Failed to block filesystem access: %s", strerror(errno)); + return false; + } + + can_use_new_paths = false; + } + +#endif + + if(!add_seccomp_memory_wxe()) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not block creating writable & executable memory regions: %s", strerror(errno)); + return false; + } + + if(!add_seccomp_used()) { + logger(DEBUG_ALWAYS, LOG_ERR, "Error setting up seccomp sandbox: %s", strerror(errno)); + return false; + } + + logger(DEBUG_ALWAYS, LOG_DEBUG, "Entered sandbox at level %d", current_level); + return true; +} diff --git a/src/logger.c b/src/logger.c index 8650a1cc3..3ae626968 100644 --- a/src/logger.c +++ b/src/logger.c @@ -31,7 +31,7 @@ #include "console.h" debug_t debug_level = DEBUG_NOTHING; -static logmode_t logmode = LOGMODE_STDERR; +logmode_t logmode = LOGMODE_STDERR; static pid_t logpid; static FILE *logfile = NULL; #ifdef HAVE_WINDOWS @@ -273,6 +273,12 @@ void openlogger(const char *ident, logmode_t mode) { logident = ident; logmode = mode; + // Let sandboxing code close access to one more unneeded path + if(mode != LOGMODE_FILE) { + free(logfilename); + logfilename = NULL; + } + switch(mode) { case LOGMODE_STDERR: logpid = getpid(); diff --git a/src/logger.h b/src/logger.h index 52f8283da..70b3f6ca4 100644 --- a/src/logger.h +++ b/src/logger.h @@ -71,6 +71,7 @@ enum { #include extern debug_t debug_level; +extern logmode_t logmode; extern bool logcontrol; extern int umbilical; extern bool umbilical_colorize; diff --git a/src/meson.build b/src/meson.build index d9f7b14bd..37a77a195 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,7 +11,7 @@ cdata.set_quoted('SBINDIR', dir_sbin) cdata.set('HAVE_' + os_name.to_upper(), 1) -foreach attr : ['malloc', 'nonnull', 'warn_unused_result', 'packed', 'format'] +foreach attr : ['malloc', 'nonnull', 'warn_unused_result', 'packed', 'format', 'noreturn'] if cc.has_function_attribute(attr) cdata.set('HAVE_ATTR_' + attr.to_upper(), 1, description: '__attribute__((__@0@__))'.format(attr)) diff --git a/src/names.c b/src/names.c index fa3574b0e..cb0c5286a 100644 --- a/src/names.c +++ b/src/names.c @@ -181,3 +181,11 @@ void free_names(void) { confdir = NULL; myname = NULL; } + +bool chrooted(void) { + return !(confbase && *confbase); +} + +void conf_subdir(char *buf, const char *name) { + snprintf(buf, PATH_MAX, "%s" SLASH "%s", confbase, name); +} diff --git a/src/names.h b/src/names.h index b3830e0d2..d407bf86a 100644 --- a/src/names.h +++ b/src/names.h @@ -37,4 +37,7 @@ extern char *program_name; extern void make_names(bool daemon); extern void free_names(void); +extern bool chrooted(void); +extern void conf_subdir(char *buf, const char *name); + #endif diff --git a/src/net.h b/src/net.h index 888a70c28..cb1af832b 100644 --- a/src/net.h +++ b/src/net.h @@ -202,6 +202,7 @@ extern void broadcast_packet(const struct node_t *n, vpn_packet_t *packet); extern char *get_name(void) ATTR_MALLOC; extern void device_enable(void); extern void device_disable(void); +extern void setup_script_config(void); extern bool setup_myself_reloadable(void); extern bool setup_network(void); extern void setup_outgoing_connection(struct outgoing_t *outgoing, bool verbose); diff --git a/src/net_setup.c b/src/net_setup.c index 3ac5a6796..8b88d7c29 100644 --- a/src/net_setup.c +++ b/src/net_setup.c @@ -231,31 +231,32 @@ char *get_name(void) { return returned_name; } -static void read_interpreter(void) { +static void read_script_conf(char **target, const char *name, const char *fallback) { char *interpreter = NULL; - get_config_string(lookup_config(&config_tree, "ScriptsInterpreter"), &interpreter); + get_config_string(lookup_config(&config_tree, name), &interpreter); - if(!interpreter || (sandbox_can(START_PROCESSES, AFTER_SANDBOX) && sandbox_can(USE_NEW_PATHS, AFTER_SANDBOX))) { - free(scriptinterpreter); - scriptinterpreter = interpreter; + if(!interpreter || !sandbox_active()) { + free(*target); + *target = interpreter + ? interpreter + : (fallback ? xstrdup(fallback) : NULL); return; } - if(!string_eq(interpreter, scriptinterpreter)) { + if(!string_eq(interpreter, *target)) { logger(DEBUG_ALWAYS, LOG_NOTICE, "Not changing ScriptsInterpreter because of sandbox."); } free(interpreter); } -bool setup_myself_reloadable(void) { - read_interpreter(); - - free(scriptextension); +void setup_script_config(void) { + read_script_conf(&scriptinterpreter, "ScriptsInterpreter", NULL); + read_script_conf(&scriptextension, "ScriptsExtension", ""); +} - if(!get_config_string(lookup_config(&config_tree, "ScriptsExtension"), &scriptextension)) { - scriptextension = xstrdup(""); - } +bool setup_myself_reloadable(void) { + setup_script_config(); char *proxy = NULL; @@ -283,6 +284,7 @@ bool setup_myself_reloadable(void) { proxytype = PROXY_EXEC; } else { logger(DEBUG_ALWAYS, LOG_ERR, "Cannot use exec proxies with current sandbox level."); + free_string(proxy); return false; } } else { diff --git a/src/sandbox.c b/src/sandbox.c index d846e1744..f6ae66cf2 100644 --- a/src/sandbox.c +++ b/src/sandbox.c @@ -14,6 +14,15 @@ void sandbox_set_level(sandbox_level_t level) { (void)level; } +bool sandbox_enabled(void) { + return false; +} + +bool sandbox_active(void) { + return false; +} + bool sandbox_enter(void) { + // No initialization needed, always return success return true; } diff --git a/src/sandbox.h b/src/sandbox.h index 66f204c47..68aea4f45 100644 --- a/src/sandbox.h +++ b/src/sandbox.h @@ -11,6 +11,7 @@ typedef enum sandbox_level_t { typedef enum sandbox_action_t { START_PROCESSES, // Start child processes + RUN_SCRIPTS, // Use tincd scripts USE_NEW_PATHS, // Access to filesystem paths that were not known at the start of the process } sandbox_action_t; @@ -19,6 +20,12 @@ typedef enum sandbox_time_t { RIGHT_NOW, // Check if the action can be performed right now } sandbox_time_t; +// true if sandbox is enabled (but not necessarily activated). +extern bool sandbox_enabled(void); + +// true if sandbox is enabled and activated. +extern bool sandbox_active(void); + // Check if the current process has enough privileges to perform the action extern bool sandbox_can(sandbox_action_t action, sandbox_time_t when); diff --git a/src/script.c b/src/script.c index 2f2c30bf5..f4f71bf31 100644 --- a/src/script.c +++ b/src/script.c @@ -20,6 +20,10 @@ #include "system.h" +#ifdef HAVE_LINUX +#include +#endif + #include "conf.h" #include "device.h" #include "logger.h" @@ -141,15 +145,257 @@ void environment_exit(environment_t *env) { free(env->entries); } +static int run_command(const char *scriptname) { + char buf[8192]; + + if(scriptinterpreter) { + snprintf(buf, sizeof(buf), "%s \"%s\"", scriptinterpreter, scriptname); + } else { + snprintf(buf, sizeof(buf), "\"%s\"", scriptname); + } + + return system(buf); +} + +static bool build_script_name(char *buf, size_t len, const char *name) { + if(!name) { + return false; + } + + // If name contains a forward slash, make sure it is the only one, and + // it forms a separator between the 'hosts' subdirectory and a file inside it. + // This should prevent attackers from using names like `/ysr/bin/python` or `../../python3`. + const char *slash = strchr(name, '/'); + + if(slash) { + if(strncmp("hosts/", name, sizeof("hosts/") - 1) || strchr(slash + 1, '/')) { + return false; + } + } + + int wrote = snprintf(buf, len, "%s" SLASH "%s%s", confbase, name, scriptextension); + return wrote > 0 && (size_t)wrote <= len; +} + +#ifdef HAVE_SANDBOX +struct { + pid_t pid; + int sock; +} worker; + +static void cleanup_script_worker(void) { + if(worker.sock) { + logger(DEBUG_ALWAYS, LOG_INFO, "Waiting for script runner to exit"); + close(worker.sock); + waitpid(worker.pid, NULL, 0); + worker.sock = 0; + } +} + +void set_script_worker(pid_t pid, int sock) { + assert(!worker.pid && !worker.sock); + worker.pid = pid; + worker.sock = sock; + atexit(cleanup_script_worker); +} + +static const char *known_vars[] = { + "DEBUG", + "DEVICE", + "INTERFACE", + "INVITATION_FILE", + "INVITATION_URL", + "NAME", + "NETNAME", + "NODE", + "REMOTEADDRESS", + "REMOTEPORT", + "SUBNET", + "WEIGHT", + NULL, +}; + +static bool is_known_var(const char *var) { + for(const char **known = known_vars; *known; ++known) { + if(!strcmp(*known, var)) { + return true; + } + } + + return false; +} + +static ssize_t read_unix(int fd, void *data, size_t len) { + struct iovec iov = { + .iov_base = data, + .iov_len = len, + }; + struct msghdr hdr = { + .msg_iov = &iov, + .msg_iovlen = 1, + }; + + while(true) { + ssize_t res = recvmsg(fd, &hdr, 0); + + if(res < 1) { + if(errno == EINTR) { + continue; + } + + return res; + } + + if(hdr.msg_flags & MSG_TRUNC) { + logger(DEBUG_ALWAYS, LOG_EMERG, "Script message truncated (possible attack)"); + return -1; + } + + return res; + } +} + +static bool write_unix(int fd, const void *data, size_t len) { + ssize_t res = send(fd, data, len, MSG_EOR); + return res > 0 && (size_t)res == len; +} + +#define CMD_MAGIC 0xDEADBEEF + +typedef struct { + uint32_t magic; + uint32_t current; + int vars; +} cmd_hdr_t; + +typedef struct { + uint32_t magic; + uint32_t current; + int status; +} cmd_result_t; + +// Receive script names + environment variables from tincd through the socket, +// build full commands, run them, and send back the result. +// The worker doesn't trust its input and runs some basic checks +// to prevent broken tincd from using it to run arbitrary binaries. +static void script_worker_loop(int sock) { + char buf[4096]; + errno = 0; + + for(uint32_t current = 0; ; ++current) { + cmd_hdr_t hdr; + + // Read the current iteration number and make sure it matches the one we expect + if(read_unix(sock, &hdr, sizeof(hdr)) <= 0 || hdr.magic != CMD_MAGIC || hdr.current != current || hdr.vars < 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Got empty or invalid header: %s", strerror(errno)); + return; + } + + // Reset all known environment variables + for(const char **known = known_vars; *known; ++known) { + unsetenv(*known); + } + + for(int i = 0; i < hdr.vars; ++i) { + // Read environment variable + ssize_t varlen = read_unix(sock, buf, sizeof(buf) - 1); + + if(varlen <= 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Got incorrect environment variable value: %s", strerror(errno)); + return; + } + + buf[varlen] = '\0'; + + // Check that we received a valid env var expression: NAME=value + char *var_val = strchr(buf, '='); + + if(!var_val || var_val == buf) { + logger(DEBUG_ALWAYS, LOG_EMERG, "Got broken environment variable (possible attack)"); + return; + } + + // Replace '=' with '\0', splitting env var expression into name and value + *var_val = '\0'; + + // Check that tincd didn't pass anything weird or dangerous (like LD_PRELOAD) + if(!is_known_var(buf)) { + logger(DEBUG_ALWAYS, LOG_EMERG, "Got unknown environment variable (possible attack)"); + return; + } + + setenv(buf, var_val + 1, 1); + } + + // Read script name + ssize_t namelen = read_unix(sock, buf, sizeof(buf) - 1); + + if(namelen <= 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Got incorrect command: %s", strerror(errno)); + return; + } + + buf[namelen] = '\0'; + + char scriptname[PATH_MAX]; + + // Check it for signs of possible attack (basically paths to anything other + // than tincd scripts in its configuration directory), and build full command. + if(!build_script_name(scriptname, sizeof(scriptname), buf)) { + logger(DEBUG_ALWAYS, LOG_EMERG, "Got invalid script name (possible attack)"); + return; + } + + const cmd_result_t result = { + .magic = CMD_MAGIC, + .current = hdr.current, + .status = run_command(scriptname), + }; + + // Send current iteration number and command exit status to tincd + if(!write_unix(sock, &result, sizeof(result))) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not write command result: %s", strerror(errno)); + return; + } + } +} + +void run_script_worker(uid_t uid, int sock) { + if(uid && setuid(uid)) { + fprintf(stderr, "Could not set user ID %d\n", uid); + exit(EXIT_FAILURE); + } + +#ifdef HAVE_LINUX + prctl(PR_SET_NAME, "scripts"); +#endif + + script_worker_loop(sock); + close(sock); + + logger(DEBUG_ALWAYS, LOG_NOTICE, "Script worker is terminating"); + exit(EXIT_SUCCESS); +} + +static void script_worker_fatal(const char *msg) ATTR_NORETURN; +static void script_worker_fatal(const char *msg) { + logger(DEBUG_ALWAYS, LOG_EMERG, "Script worker failed: could not %s (%s)", msg, strerror(errno)); + cleanup_script_worker(); + abort(); +} +#endif // HAVE_SANDBOX + bool execute_script(const char *name, environment_t *env) { - if(!sandbox_can(START_PROCESSES, RIGHT_NOW)) { + if(!sandbox_can(RUN_SCRIPTS, RIGHT_NOW)) { return false; } char scriptname[PATH_MAX]; - char *command; - snprintf(scriptname, sizeof(scriptname), "%s" SLASH "%s%s", confbase, name, scriptextension); + if(!build_script_name(scriptname, sizeof(scriptname), name)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Invalid script name '%s'", name); + return false; + } /* First check if there is a script */ @@ -195,7 +441,7 @@ bool execute_script(const char *name, environment_t *env) { return true; } } else -#endif +#endif // HAVE_WINDOWS if(access(scriptname, F_OK)) { return true; @@ -203,26 +449,58 @@ bool execute_script(const char *name, environment_t *env) { logger(DEBUG_STATUS, LOG_INFO, "Executing script %s", name); - /* Set environment */ +#ifdef HAVE_SANDBOX + static cmd_hdr_t hdr = {.magic = CMD_MAGIC}; - for(int i = 0; i < env->n; i++) { - putenv(env->entries[i]); - } + if(worker.sock) { + hdr.vars = env->n; - if(scriptinterpreter) { - xasprintf(&command, "%s \"%s\"", scriptinterpreter, scriptname); - } else { - xasprintf(&command, "\"%s\"", scriptname); + if(!write_unix(worker.sock, &hdr, sizeof(hdr))) { + script_worker_fatal("send header"); + } + + for(int i = 0; i < env->n; i++) { + const char *val = env->entries[i]; + + if(!write_unix(worker.sock, val, strlen(val))) { + script_worker_fatal("send env vars"); + } + } + } else +#endif + { + /* Set environment */ + for(int i = 0; i < env->n; i++) { + putenv(env->entries[i]); + } } - int status = system(command); + int status = 0; - free(command); +#ifdef HAVE_SANDBOX - /* Unset environment */ + if(worker.sock) { + if(!write_unix(worker.sock, name, strlen(name))) { + script_worker_fatal("send name"); + } - for(int i = 0; i < env->n; i++) { - unputenv(env->entries[i]); + cmd_result_t result; + + if(read_unix(worker.sock, &result, sizeof(result)) <= 0 || result.magic != CMD_MAGIC || result.current != hdr.current) { + script_worker_fatal("start script"); + } + + status = result.status; + ++hdr.current; + } else +#endif + { + status = run_command(scriptname); + + /* Unset environment */ + for(int i = 0; i < env->n; i++) { + unputenv(env->entries[i]); + } } if(status != -1) { diff --git a/src/script.h b/src/script.h index 4d10d2e8e..16a5c991e 100644 --- a/src/script.h +++ b/src/script.h @@ -33,7 +33,11 @@ extern int environment_add(environment_t *env, const char *format, ...) ATTR_FOR extern void environment_update(environment_t *env, int pos, const char *format, ...) ATTR_FORMAT(printf, 3, 4); extern void environment_init(environment_t *env); extern void environment_exit(environment_t *env); - extern bool execute_script(const char *name, environment_t *env); +#ifdef HAVE_SANDBOX +extern void set_script_worker(pid_t pid, int sock); +extern void run_script_worker(uid_t uid, int sock) ATTR_NORETURN; +#endif + #endif diff --git a/src/subnet.c b/src/subnet.c index 94000cc0e..795219391 100644 --- a/src/subnet.c +++ b/src/subnet.c @@ -322,7 +322,7 @@ subnet_t *lookup_subnet_ipv6(const ipv6_t *address) { } void subnet_update(node_t *owner, subnet_t *subnet, bool up) { - if(!sandbox_can(START_PROCESSES, RIGHT_NOW)) { + if(!sandbox_can(RUN_SCRIPTS, RIGHT_NOW)) { return; } diff --git a/src/tincd.c b/src/tincd.c index 8a4c1f394..6b600465a 100644 --- a/src/tincd.c +++ b/src/tincd.c @@ -56,6 +56,7 @@ #include "version.h" #include "random.h" #include "sandbox.h" +#include "script.h" #include "watchdog.h" /* If nonzero, display usage information and exit. */ @@ -365,34 +366,62 @@ static bool read_sandbox_level(void) { return true; } -static bool drop_privs(void) { -#ifndef HAVE_WINDOWS - uid_t uid = 0; +static void create_conf_dir(const char *name, mode_t mode) { + assert(confbase); - if(switchuser) { - struct passwd *pw = getpwnam(switchuser); + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s" SLASH "%s", confbase, name); - if(!pw) { - logger(DEBUG_ALWAYS, LOG_ERR, "unknown user `%s'", switchuser); - return false; + if(mkdir(path, mode)) { + if(errno == EEXIST) { + chmod(path, mode); + } else { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not create config subdirectory %s: %s", path, strerror(errno)); } + } +} - uid = pw->pw_uid; +#ifndef HAVE_WINDOWS +static uid_t get_user_id(const char *username, uid_t *uid) { + *uid = 0; - // The second parameter to initgroups on macOS requires int, - // but __gid_t is unsigned int. There's not much we can do here. - if(initgroups(switchuser, pw->pw_gid) != 0 || // NOLINT(bugprone-narrowing-conversions) - setgid(pw->pw_gid) != 0) { - logger(DEBUG_ALWAYS, LOG_ERR, "System call `%s' failed: %s", - "initgroups", strerror(errno)); - return false; - } + if(!username) { + return true; + } + + const struct passwd *pw = getpwnam(username); + + if(!pw) { + logger(DEBUG_ALWAYS, LOG_ERR, "Unknown user %s", username); + return false; + } + + *uid = pw->pw_uid; + + // The second parameter to initgroups on macOS requires int, + // but __gid_t is unsigned int. There's not much we can do here. + if(initgroups(username, pw->pw_gid) != 0 || // NOLINT(bugprone-narrowing-conversions) + setgid(pw->pw_gid) != 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "System call `initgroups' failed: %s", strerror(errno)); + return false; + } #ifndef __ANDROID__ // Not supported in android NDK - endgrent(); - endpwent(); + endgrent(); + endpwent(); #endif + + return true; +} +#endif // HAVE_WINDOWS + +static bool drop_privs(void) { +#ifndef HAVE_WINDOWS + uid_t uid = 0; + + if(!get_user_id(switchuser, &uid)) { + return false; } if(do_chroot) { @@ -408,14 +437,17 @@ static bool drop_privs(void) { confbase = xstrdup(""); } - if(switchuser) - if(setuid(uid) != 0) { - logger(DEBUG_ALWAYS, LOG_ERR, "System call `%s' failed: %s", - "setuid", strerror(errno)); - return false; - } + if(uid && setuid(uid) != 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "System call `%s' failed: %s", + "setuid", strerror(errno)); + return false; + } -#endif +#endif // HAVE_WINDOWS + + create_conf_dir("cache", 0755); + create_conf_dir("hosts", 0755); + create_conf_dir("invitations", 0700); return sandbox_enter(); } @@ -454,6 +486,56 @@ static void cleanup(void) { free_names(); } +#ifdef HAVE_SANDBOX +static bool start_script_worker(void) { + uid_t uid = 0; + + if(!get_user_id(switchuser, &uid)) { + return false; + } + + int fds[2]; + + if(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fds)) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not create socket pair: %s", strerror(errno)); + return false; + } + + pid_t pid = fork(); + + if(pid < 0) { + logger(DEBUG_ALWAYS, LOG_ERR, "Could not create script worker: %s", strerror(errno)); + return false; + } + + if(pid > 0) { + set_script_worker(pid, fds[0]); + close(fds[1]); + return true; + } + + // Use a different logging identifier + if(logmode == LOGMODE_FILE || logmode == LOGMODE_SYSLOG) { + closelogger(); + + // Not a good idea to log from multiple processes to the same file + logmode = LOGMODE_SYSLOG; + + char *oldname = identname; + xasprintf(&identname, "%s.scripts", oldname); + free(oldname); + openlogger(identname, logmode); + } + + // Close unused inherited FDs + close(STDIN_FILENO); + close(umbilical); + close(fds[0]); + + run_script_worker(uid, fds[1]); +} +#endif + int main(int argc, char **argv) { program_name = argv[0]; @@ -632,6 +714,18 @@ int main2(int argc, char **argv) { return 1; } +#ifdef HAVE_SANDBOX + + if(sandbox_enabled() && sandbox_can(RUN_SCRIPTS, AFTER_SANDBOX)) { + setup_script_config(); + + if(!start_script_worker()) { + return 1; + } + } + +#endif + #ifdef HAVE_MLOCKALL /* Lock all pages into memory if requested. diff --git a/src/utils.c b/src/utils.c index 231938dcf..181bba3b9 100644 --- a/src/utils.c +++ b/src/utils.c @@ -377,3 +377,14 @@ bool string_eq(const char *first, const char *second) { return !first == !second && !(first && second && strcmp(first, second)); } + +int strtailcmp(const char *str, const char *tail) { + size_t slen = strlen(str); + size_t tlen = strlen(tail); + + if(tlen > slen) { + return -1; + } + + return memcmp(str + slen - tlen, tail, tlen); +} diff --git a/src/utils.h b/src/utils.h index 487058ae7..d8f276130 100644 --- a/src/utils.h +++ b/src/utils.h @@ -81,4 +81,6 @@ extern FILE *fopenmask(const char *filename, const char *mode, mode_t perms) ATT // NULL-safe wrapper around strcmp(). extern bool string_eq(const char *first, const char *second); +int strtailcmp(const char *str, const char *tail) ATTR_NONNULL; + #endif diff --git a/test/integration/proxy.py b/test/integration/proxy.py index c0e797108..d9dc66ab0 100755 --- a/test/integration/proxy.py +++ b/test/integration/proxy.py @@ -424,6 +424,8 @@ def test_proxy(ctx: Test, handler: T.Type[ProxyServer], user="", passw="") -> No def test_proxy_exec(ctx: Test) -> None: """Test that exec proxies work as expected.""" foo, bar = init(ctx) + for node in foo, bar: + node.cmd("set", "Sandbox", "off") log.info("exec proxy without arguments fails") foo.cmd("set", "Proxy", "exec")