diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | changes/feature8195 | 6 | ||||
-rw-r--r-- | configure.ac | 16 | ||||
-rw-r--r-- | doc/tor.1.txt | 8 | ||||
-rw-r--r-- | src/common/compat.c | 108 | ||||
-rw-r--r-- | src/common/compat.h | 13 | ||||
-rw-r--r-- | src/or/config.c | 49 | ||||
-rw-r--r-- | src/or/or.h | 3 | ||||
-rw-r--r-- | src/test/include.am | 17 | ||||
-rw-r--r-- | src/test/test_switch_id.c | 181 | ||||
-rwxr-xr-x | src/test/test_switch_id.sh | 25 |
11 files changed, 414 insertions, 14 deletions
diff --git a/.gitignore b/.gitignore index c1dff8bfec..dad8be1a48 100644 --- a/.gitignore +++ b/.gitignore @@ -172,6 +172,7 @@ cscope.* /src/test/test-child /src/test/test-memwipe /src/test/test-ntor-cl +/src/test/test-switch-id /src/test/test_workqueue /src/test/test.exe /src/test/test-slow.exe @@ -179,6 +180,7 @@ cscope.* /src/test/test-child.exe /src/test/test-ntor-cl.exe /src/test/test-memwipe.exe +/src/test/test-switch-id.exe /src/test/test_workqueue.exe /src/test/test_zero_length_keys.sh /src/test/test_ntor.sh diff --git a/changes/feature8195 b/changes/feature8195 new file mode 100644 index 0000000000..cb81f2e1d0 --- /dev/null +++ b/changes/feature8195 @@ -0,0 +1,6 @@ + o Major features: + - When Tor is started as root on Linux and told to switch user ID, it + can now retain the capabilitity to bind to low ports. By default, + Tor will do this only when it's switching user ID and some low + ports have been configured. You can change this behavior with + the new option KeepBindCapabilities. Closes ticket 8195. diff --git a/configure.ac b/configure.ac index 4dbb3c4fa4..c723257ea3 100644 --- a/configure.ac +++ b/configure.ac @@ -712,6 +712,19 @@ else fi AC_SUBST(TOR_ZLIB_LIBS) +dnl ---------------------------------------------------------------------- +dnl Check if libcap is available for capabilities. + +tor_cap_pkg_debian="libcap2" +tor_cap_pkg_redhat="libcap" +tor_cap_devpkg_debian="libcap-dev" +tor_cap_devpkg_redhat="libcap-devel" + +AC_CHECK_LIB([cap], [cap_init], [], + AC_MSG_NOTICE([Libcap was not found. Capabilities will not be usable.]) +) +AC_CHECK_FUNCS(cap_set_proc) + dnl --------------------------------------------------------------------- dnl Now that we know about our major libraries, we can check for compiler dnl and linker hardening options. We need to do this with the libraries known, @@ -719,7 +732,7 @@ dnl since sometimes the linker will like an option but not be willing to dnl use it with a build of a library. all_ldflags_for_check="$TOR_LDFLAGS_zlib $TOR_LDFLAGS_openssl $TOR_LDFLAGS_libevent" -all_libs_for_check="$TOR_ZLIB_LIBS $TOR_LIB_MATH $TOR_LIBEVENT_LIBS $TOR_OPENSSL_LIBS $TOR_SYSTEMD_LIBS $TOR_LIB_WS32 $TOR_LIB_GDI" +all_libs_for_check="$TOR_ZLIB_LIBS $TOR_LIB_MATH $TOR_LIBEVENT_LIBS $TOR_OPENSSL_LIBS $TOR_SYSTEMD_LIBS $TOR_LIB_WS32 $TOR_LIB_GDI $TOR_CAP_LIBS" AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [ #if !defined(__clang__) @@ -912,6 +925,7 @@ AC_CHECK_HEADERS( fcntl.h \ signal.h \ string.h \ + sys/capability.h \ sys/fcntl.h \ sys/stat.h \ sys/time.h \ diff --git a/doc/tor.1.txt b/doc/tor.1.txt index d8802bf104..7dd8812ff9 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -619,6 +619,14 @@ GENERAL OPTIONS [[User]] **User** __UID__:: On startup, setuid to this user and setgid to their primary group. +[[KeepBindCapabilities]] **KeepBindCapabilities** **0**|**1**|**auto**:: + On Linux, when we are started as root and we switch our identity using + the **User** option, the **KeepBindCapabilities** option tells us whether to + try to retain our ability to bind to low ports. If this value is 1, we + try to keep the capability; if it is 0 we do not; and if it is **auto**, + we keep the capability only if we are configured to listen on a low port. + (Default: auto.) + [[HardwareAccel]] **HardwareAccel** **0**|**1**:: If non-zero, try to use built-in (static) crypto hardware acceleration when available. (Default: 0) diff --git a/src/common/compat.c b/src/common/compat.c index a103e58163..7e8eec189a 100644 --- a/src/common/compat.c +++ b/src/common/compat.c @@ -71,6 +71,9 @@ #ifdef HAVE_SYS_STATVFS_H #include <sys/statvfs.h> #endif +#ifdef HAVE_SYS_CAPABILITY_H +#include <sys/capability.h> +#endif #ifdef _WIN32 #include <conio.h> @@ -1966,17 +1969,99 @@ tor_getpwuid(uid_t uid) } #endif +/** Return true iff we were compiled with capability support, and capabilities + * seem to work. **/ +int +have_capability_support(void) +{ +#ifdef HAVE_LINUX_CAPABILITIES + cap_t caps = cap_get_proc(); + if (caps == NULL) + return 0; + cap_free(caps); + return 1; +#else + return 0; +#endif +} + +#ifdef HAVE_LINUX_CAPABILITIES +/** Helper. Drop all capabilities but a small set, and set PR_KEEPCAPS as + * appropriate. + * + * If pre_setuid, retain only CAP_NET_BIND_SERVICE, CAP_SETUID, and + * CAP_SETGID, and use PR_KEEPCAPS to ensure that capabilities persist across + * setuid(). + * + * If not pre_setuid, retain only CAP_NET_BIND_SERVICE, and disable + * PR_KEEPCAPS. + * + * Return 0 on success, and -1 on failure. + */ +static int +drop_capabilities(int pre_setuid) +{ + /* We keep these three capabilities, and these only, as we setuid. + * After we setuid, we drop all but the first. */ + const cap_value_t caplist[] = { + CAP_NET_BIND_SERVICE, CAP_SETUID, CAP_SETGID + }; + const char *where = pre_setuid ? "pre-setuid" : "post-setuid"; + const int n_effective = pre_setuid ? 3 : 1; + const int n_permitted = pre_setuid ? 3 : 1; + const int n_inheritable = 1; + const int keepcaps = pre_setuid ? 1 : 0; + + /* Sets whether we keep capabilities across a setuid. */ + if (prctl(PR_SET_KEEPCAPS, keepcaps) < 0) { + log_warn(LD_CONFIG, "Unable to call prctl() %s: %s", + where, strerror(errno)); + return -1; + } + + cap_t caps = cap_get_proc(); + if (!caps) { + log_warn(LD_CONFIG, "Unable to call cap_get_proc() %s: %s", + where, strerror(errno)); + return -1; + } + cap_clear(caps); + + cap_set_flag(caps, CAP_EFFECTIVE, n_effective, caplist, CAP_SET); + cap_set_flag(caps, CAP_PERMITTED, n_permitted, caplist, CAP_SET); + cap_set_flag(caps, CAP_INHERITABLE, n_inheritable, caplist, CAP_SET); + + int r = cap_set_proc(caps); + cap_free(caps); + if (r < 0) { + log_warn(LD_CONFIG, "No permission to set capabilities %s: %s", + where, strerror(errno)); + return -1; + } + + return 0; +} +#endif + /** Call setuid and setgid to run as <b>user</b> and switch to their * primary group. Return 0 on success. On failure, log and return -1. + * + * If SWITCH_ID_KEEP_BINDLOW is set in 'flags', try to use the capability + * system to retain the abilitity to bind low ports. + * + * If SWITCH_ID_WARN_IF_NO_CAPS is set in flags, also warn if we have + * don't have capability support. */ int -switch_id(const char *user) +switch_id(const char *user, const unsigned flags) { #ifndef _WIN32 const struct passwd *pw = NULL; uid_t old_uid; gid_t old_gid; static int have_already_switched_id = 0; + const int keep_bindlow = !!(flags & SWITCH_ID_KEEP_BINDLOW); + const int warn_if_no_caps = !!(flags & SWITCH_ID_WARN_IF_NO_CAPS); tor_assert(user); @@ -2000,6 +2085,20 @@ switch_id(const char *user) return -1; } +#ifdef HAVE_LINUX_CAPABILITIES + (void) warn_if_no_caps; + if (keep_bindlow) { + if (drop_capabilities(1)) + return -1; + } +#else + (void) keep_bindlow; + if (warn_if_no_caps) { + log_warn(LD_CONFIG, "KeepBindCapabilities set, but no capability support " + "on this system."); + } +#endif + /* Properly switch egid,gid,euid,uid here or bail out */ if (setgroups(1, &pw->pw_gid)) { log_warn(LD_GENERAL, "Error setting groups to gid %d: \"%s\".", @@ -2053,6 +2152,12 @@ switch_id(const char *user) /* We've properly switched egid, gid, euid, uid, and supplementary groups if * we're here. */ +#ifdef HAVE_LINUX_CAPABILITIES + if (keep_bindlow) { + if (drop_capabilities(0)) + return -1; + } +#endif #if !defined(CYGWIN) && !defined(__CYGWIN__) /* If we tried to drop privilege to a group/user other than root, attempt to @@ -2100,6 +2205,7 @@ switch_id(const char *user) #else (void)user; + (void)flags; log_warn(LD_CONFIG, "User specified but switching users is unsupported on your OS."); diff --git a/src/common/compat.h b/src/common/compat.h index fb177832af..d38adca598 100644 --- a/src/common/compat.h +++ b/src/common/compat.h @@ -625,7 +625,18 @@ typedef unsigned long rlim_t; int get_max_sockets(void); int set_max_file_descriptors(rlim_t limit, int *max); int tor_disable_debugger_attach(void); -int switch_id(const char *user); + +#if defined(HAVE_SYS_CAPABILITY_H) && defined(HAVE_CAP_SET_PROC) +#define HAVE_LINUX_CAPABILITIES +#endif + +int have_capability_support(void); + +/** Flag for switch_id; see switch_id() for documentation */ +#define SWITCH_ID_KEEP_BINDLOW (1<<0) +/** Flag for switch_id; see switch_id() for documentation */ +#define SWITCH_ID_WARN_IF_NO_CAPS (1<<1) +int switch_id(const char *user, unsigned flags); #ifdef HAVE_PWD_H char *get_user_homedir(const char *username); #endif diff --git a/src/or/config.c b/src/or/config.c index 86c3125cbb..30f944fe39 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -310,6 +310,7 @@ static config_var_t option_vars_[] = { V(Socks5ProxyUsername, STRING, NULL), V(Socks5ProxyPassword, STRING, NULL), V(KeepalivePeriod, INTERVAL, "5 minutes"), + V(KeepBindCapabilities, AUTOBOOL, "auto"), VAR("Log", LINELIST, Logs, NULL), V(LogMessageDomains, BOOL, "0"), V(LogTimeGranularity, MSEC_INTERVAL, "1 second"), @@ -606,7 +607,8 @@ static int parse_ports(or_options_t *options, int validate_only, char **msg_out, int *n_ports_out, int *world_writable_control_socket); static int check_server_ports(const smartlist_t *ports, - const or_options_t *options); + const or_options_t *options, + int *num_low_ports_out); static int validate_data_directory(or_options_t *options); static int write_configuration_file(const char *fname, @@ -1085,6 +1087,9 @@ consider_adding_dir_servers(const or_options_t *options, return 0; } +/* Helps determine flags to pass to switch_id. */ +static int have_low_ports = -1; + /** Fetch the active option list, and take actions based on it. All of the * things we do should survive being done repeatedly. If present, * <b>old_options</b> contains the previous value of the options. @@ -1219,7 +1224,16 @@ options_act_reversible(const or_options_t *old_options, char **msg) /* Setuid/setgid as appropriate */ if (options->User) { - if (switch_id(options->User) != 0) { + tor_assert(have_low_ports != -1); + unsigned switch_id_flags = 0; + if (options->KeepBindCapabilities == 1) { + switch_id_flags |= SWITCH_ID_KEEP_BINDLOW; + switch_id_flags |= SWITCH_ID_WARN_IF_NO_CAPS; + } + if (options->KeepBindCapabilities == -1 && have_low_ports) { + switch_id_flags |= SWITCH_ID_KEEP_BINDLOW; + } + if (switch_id(options->User, switch_id_flags) != 0) { /* No need to roll back, since you can't change the value. */ *msg = tor_strdup("Problem with User value. See logs for details."); goto done; @@ -4094,6 +4108,12 @@ options_transition_allowed(const or_options_t *old, return -1; } + if (old->KeepBindCapabilities != new_val->KeepBindCapabilities) { + *msg = tor_strdup("While Tor is running, changing KeepBindCapabilities is " + "not allowed."); + return -1; + } + if (!opt_streq(old->SyslogIdentityTag, new_val->SyslogIdentityTag)) { *msg = tor_strdup("While Tor is running, changing " "SyslogIdentityTag is not allowed."); @@ -6632,10 +6652,13 @@ parse_ports(or_options_t *options, int validate_only, } } - if (check_server_ports(ports, options) < 0) { + int n_low_ports = 0; + if (check_server_ports(ports, options, &n_low_ports) < 0) { *msg = tor_strdup("Misconfigured server ports"); goto err; } + if (have_low_ports < 0) + have_low_ports = (n_low_ports > 0); *n_ports_out = smartlist_len(ports); @@ -6689,10 +6712,12 @@ parse_ports(or_options_t *options, int validate_only, } /** Given a list of <b>port_cfg_t</b> in <b>ports</b>, check them for internal - * consistency and warn as appropriate. */ + * consistency and warn as appropriate. Set *<b>n_low_ports_out</b> to the + * number of sub-1024 ports we will be binding. */ static int check_server_ports(const smartlist_t *ports, - const or_options_t *options) + const or_options_t *options, + int *n_low_ports_out) { int n_orport_advertised = 0; int n_orport_advertised_ipv4 = 0; @@ -6755,16 +6780,24 @@ check_server_ports(const smartlist_t *ports, r = -1; } - if (n_low_port && options->AccountingMax) { + if (n_low_port && options->AccountingMax && + (!have_capability_support() || options->KeepBindCapabilities == 0)) { + const char *extra = ""; + if (options->KeepBindCapabilities == 0 && have_capability_support()) + extra = ", and you have disabled KeepBindCapabilities."; log_warn(LD_CONFIG, "You have set AccountingMax to use hibernation. You have also " - "chosen a low DirPort or OrPort. This combination can make Tor stop " + "chosen a low DirPort or OrPort%s." + "This combination can make Tor stop " "working when it tries to re-attach the port after a period of " "hibernation. Please choose a different port or turn off " "hibernation unless you know this combination will work on your " - "platform."); + "platform.", extra); } + if (n_low_ports_out) + *n_low_ports_out = n_low_port; + return r; } diff --git a/src/or/or.h b/src/or/or.h index 2944eaf330..68811e8e4d 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -4432,6 +4432,9 @@ typedef struct { int keygen_passphrase_fd; int change_key_passphrase; char *master_key_fname; + + /** Autobool: Do we try to retain capabilities if we can? */ + int KeepBindCapabilities; } or_options_t; /** Persistent state for an onion router, as saved to disk. */ diff --git a/src/test/include.am b/src/test/include.am index f593782709..d52867b94e 100644 --- a/src/test/include.am +++ b/src/test/include.am @@ -8,14 +8,16 @@ TESTS_ENVIRONMENT = \ export builddir="$(builddir)"; \ export TESTING_TOR_BINARY="$(TESTING_TOR_BINARY)"; -TESTSCRIPTS = src/test/test_zero_length_keys.sh +TESTSCRIPTS = src/test/test_zero_length_keys.sh \ + src/test/test_switch_id.sh if USEPYTHON TESTSCRIPTS += src/test/test_ntor.sh src/test/test_bt.sh endif TESTS += src/test/test src/test/test-slow src/test/test-memwipe \ - src/test/test_workqueue src/test/test_keygen.sh $(TESTSCRIPTS) + src/test/test_workqueue src/test/test_keygen.sh \ + $(TESTSCRIPTS) # These flavors are run using automake's test-driver and test-network.sh TEST_CHUTNEY_FLAVORS = basic-min bridges-min hs-min bridges+hs @@ -37,7 +39,8 @@ noinst_PROGRAMS+= \ src/test/test-slow \ src/test/test-memwipe \ src/test/test-child \ - src/test/test_workqueue + src/test/test_workqueue \ + src/test/test-switch-id endif src_test_AM_CPPFLAGS = -DSHARE_DATADIR="\"$(datadir)\"" \ @@ -136,6 +139,14 @@ src_test_test_workqueue_SOURCES = \ src_test_test_workqueue_CPPFLAGS= $(src_test_AM_CPPFLAGS) src_test_test_workqueue_CFLAGS = $(AM_CFLAGS) $(TEST_CFLAGS) +src_test_test_switch_id_SOURCES = \ + src/test/test_switch_id.c +src_test_test_switch_id_CPPFLAGS= $(src_test_AM_CPPFLAGS) +src_test_test_switch_id_CFLAGS = $(AM_CFLAGS) $(TEST_CFLAGS) +src_test_test_switch_id_LDADD = \ + src/common/libor-testing.a \ + @TOR_ZLIB_LIBS@ @TOR_LIB_MATH@ + src_test_test_LDFLAGS = @TOR_LDFLAGS_zlib@ @TOR_LDFLAGS_openssl@ \ @TOR_LDFLAGS_libevent@ src_test_test_LDADD = src/or/libtor-testing.a src/common/libor-testing.a \ diff --git a/src/test/test_switch_id.c b/src/test/test_switch_id.c new file mode 100644 index 0000000000..e85025c3b5 --- /dev/null +++ b/src/test/test_switch_id.c @@ -0,0 +1,181 @@ +/* Copyright (c) 2015, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "or.h" + +#ifdef HAVE_SYS_CAPABILITY_H +#include <sys/capability.h> +#endif + +#define TEST_BUILT_WITH_CAPS 0 +#define TEST_HAVE_CAPS 1 +#define TEST_ROOT_CAN_BIND_LOW 2 +#define TEST_SETUID 3 +#define TEST_SETUID_KEEPCAPS 4 +#define TEST_SETUID_STRICT 5 + +static const char *username; + +static const struct { + const char *name; + int test_id; +} which_test[] = { + { "built-with-caps", TEST_BUILT_WITH_CAPS }, + { "have-caps", TEST_HAVE_CAPS }, + { "root-bind-low", TEST_ROOT_CAN_BIND_LOW }, + { "setuid", TEST_SETUID }, + { "setuid-keepcaps", TEST_SETUID_KEEPCAPS }, + { "setuid-strict", TEST_SETUID_STRICT }, + { NULL, 0 } +}; + +/* 0 on no, 1 on yes, -1 on failure. */ +static int +check_can_bind_low_ports(void) +{ + int port; + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + + for (port = 600; port < 1024; ++port) { + sin.sin_port = htons(port); + tor_socket_t fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (! SOCKET_OK(fd)) { + perror("socket"); + return -1; + } + + int one = 1; + if (setsockopt(fd, SOL_SOCKET,SO_REUSEADDR, &one, sizeof(one))) { + perror("setsockopt"); + tor_close_socket_simple(fd); + return -1; + } + + int res = bind(fd, (struct sockaddr *)&sin, sizeof(sin)); + tor_close_socket_simple(fd); + + if (res == 0) { + /* bind was successful */ + return 1; + } else if (errno == EACCES || errno == EPERM) { + /* Got a permission-denied error. */ + return 0; + } else if (errno == EADDRINUSE) { + /* Huh; somebody is using that port. */ + } else { + perror("bind"); + } + } + + return -1; +} + +int +main(int argc, char **argv) +{ + const char *testname; + if (argc != 3) { + fprintf(stderr, "I want 2 arguments: a username and a command.\n"); + return 1; + } + if (getuid() != 0) { + fprintf(stderr, "This test only works when it's run as root.\n"); + return 1; + } + username = argv[1]; + testname = argv[2]; + int test_id = -1; + int i; + for (i = 0; which_test[i].name; ++i) { + if (!strcmp(which_test[i].name, testname)) { + test_id = which_test[i].test_id; + break; + } + } + if (test_id == -1) { + fprintf(stderr, "Unrecognized test '%s'\n", testname); + return 1; + } + +#ifdef HAVE_LINUX_CAPABILITIES + const int have_cap_support = 1; +#else + const int have_cap_support = 0; +#endif + + int okay; + + init_logging(1); + log_severity_list_t sev; + memset(&sev, 0, sizeof(sev)); + set_log_severity_config(LOG_WARN, LOG_ERR, &sev); + add_stream_log(&sev, "", fileno(stderr)); + + switch (test_id) + { + case TEST_BUILT_WITH_CAPS: + /* Succeed if we were built with capability support. */ + okay = have_cap_support; + break; + case TEST_HAVE_CAPS: + /* Succeed if "capabilities work" == "we were built with capability + * support." */ + okay = have_cap_support == have_capability_support(); + break; + case TEST_ROOT_CAN_BIND_LOW: + /* Succeed if root can bind low ports. */ + okay = check_can_bind_low_ports() == 1; + break; + case TEST_SETUID: + /* Succeed if we can do a setuid with no capability retention, and doing + * so makes us lose the ability to bind low ports */ + case TEST_SETUID_KEEPCAPS: + /* Succeed if we can do a setuid with capability retention, and doing so + * does not make us lose the ability to bind low ports */ + { + int keepcaps = (test_id == TEST_SETUID_KEEPCAPS); + okay = switch_id(username, keepcaps ? SWITCH_ID_KEEP_BINDLOW : 0) == 0; + if (okay) { + okay = check_can_bind_low_ports() == keepcaps; + } + break; + } + case TEST_SETUID_STRICT: + /* Succeed if, after a setuid, we cannot setuid back, and we cannot + * re-grab any capabilities. */ + okay = switch_id(username, SWITCH_ID_KEEP_BINDLOW) == 0; + if (okay) { + /* We'd better not be able to setuid back! */ + if (setuid(0) == 0 || errno != EPERM) { + okay = 0; + } + } +#ifdef HAVE_LINUX_CAPABILITIES + if (okay) { + cap_t caps = cap_get_proc(); + const cap_value_t caplist[] = { + CAP_SETUID, + }; + cap_set_flag(caps, CAP_PERMITTED, 1, caplist, CAP_SET); + if (cap_set_proc(caps) == 0 || errno != EPERM) { + okay = 0; + } + cap_free(caps); + } +#endif + break; + default: + fprintf(stderr, "Unsupported test '%s'\n", testname); + okay = 0; + break; + } + + if (!okay) { + fprintf(stderr, "Test %s failed!\n", testname); + } + + return (okay ? 0 : 1); +} + diff --git a/src/test/test_switch_id.sh b/src/test/test_switch_id.sh new file mode 100755 index 0000000000..1b4e0998b5 --- /dev/null +++ b/src/test/test_switch_id.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +if test "`id -u`" != '0'; then + echo "This test only works when run as root. Skipping." >&2 + exit 77 +fi + +if test "`id -u nobody`" = ""; then + echo "This test requires that your system have a 'nobody' user. Sorry." >&2 + exit 1 +fi + +"${builddir:-.}/src/test/test-switch-id" nobody setuid || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody root-bind-low || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody setuid-strict || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody built-with-caps || exit 0 +# ... Go beyond this point only if we were built with capability support. + +"${builddir:-.}/src/test/test-switch-id" nobody have-caps || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody setuid-keepcaps || exit 1 + + +echo "All okay" + +exit 0 |