Skip to content

Commit

Permalink
Add seccomp-bpf & landlock sandbox for Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
hg committed May 24, 2022
1 parent 95924a0 commit d160916
Show file tree
Hide file tree
Showing 25 changed files with 1,193 additions and 158 deletions.
8 changes: 2 additions & 6 deletions doc/tinc.conf.5.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
114 changes: 33 additions & 81 deletions src/bsd/openbsd/tincd.c
Original file line number Diff line number Diff line change
@@ -1,128 +1,74 @@
#include "../../system.h"

#include <libgen.h>
#include <assert.h>

#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
" cpath" // Create new ones
" 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:
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
11 changes: 0 additions & 11 deletions src/fsck.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/have.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
123 changes: 123 additions & 0 deletions src/linux/landlock.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#include "../system.h"

#ifdef HAVE_LINUX_LANDLOCK_H

#include <sys/syscall.h>

#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
Loading

0 comments on commit d160916

Please sign in to comment.