Add ability to keep the CAP_NET_BIND_SERVICE capability on Linux

This feature allows us to bind low ports when starting as root and
switching UIDs.

Based on code by David Goulet.

Implement feature 8195
This commit is contained in:
Nick Mathewson 2015-11-06 13:12:44 -05:00
parent af80d472f7
commit e8cc839e41
7 changed files with 175 additions and 11 deletions

6
changes/feature8195 Normal file
View File

@ -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 KeepCapabilities. Closes ticket 8195.

View File

@ -698,6 +698,19 @@ else
fi fi
AC_SUBST(TOR_ZLIB_LIBS) 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 ---------------------------------------------------------------------
dnl Now that we know about our major libraries, we can check for compiler 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, dnl and linker hardening options. We need to do this with the libraries known,
@ -705,7 +718,7 @@ dnl since sometimes the linker will like an option but not be willing to
dnl use it with a build of a library. dnl use it with a build of a library.
all_ldflags_for_check="$TOR_LDFLAGS_zlib $TOR_LDFLAGS_openssl $TOR_LDFLAGS_libevent" 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([], [ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [
#if !defined(__clang__) #if !defined(__clang__)
@ -898,6 +911,7 @@ AC_CHECK_HEADERS(
fcntl.h \ fcntl.h \
signal.h \ signal.h \
string.h \ string.h \
sys/capability.h \
sys/fcntl.h \ sys/fcntl.h \
sys/stat.h \ sys/stat.h \
sys/time.h \ sys/time.h \

View File

@ -601,6 +601,14 @@ GENERAL OPTIONS
[[User]] **User** __UID__:: [[User]] **User** __UID__::
On startup, setuid to this user and setgid to their primary group. On startup, setuid to this user and setgid to their primary group.
[[KeepCapabilities]] **KeepCapabilities** **0**|**1**|**auto**::
On Linux, when we are started as root and we switch our identity using
the **User** option, the **KeepCapabilities** 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**:: [[HardwareAccel]] **HardwareAccel** **0**|**1**::
If non-zero, try to use built-in (static) crypto hardware acceleration when If non-zero, try to use built-in (static) crypto hardware acceleration when
available. (Default: 0) available. (Default: 0)

View File

@ -71,6 +71,9 @@
#ifdef HAVE_SYS_STATVFS_H #ifdef HAVE_SYS_STATVFS_H
#include <sys/statvfs.h> #include <sys/statvfs.h>
#endif #endif
#ifdef HAVE_SYS_CAPABILITY_H
#include <sys/capability.h>
#endif
#ifdef _WIN32 #ifdef _WIN32
#include <conio.h> #include <conio.h>
@ -1917,17 +1920,95 @@ tor_getpwuid(uid_t uid)
} }
#endif #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 /** 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. * 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 capabilitity
* system to retain the abilitity to bind low ports.
*/ */
int int
switch_id(const char *user) switch_id(const char *user, const unsigned flags)
{ {
#ifndef _WIN32 #ifndef _WIN32
const struct passwd *pw = NULL; const struct passwd *pw = NULL;
uid_t old_uid; uid_t old_uid;
gid_t old_gid; gid_t old_gid;
static int have_already_switched_id = 0; static int have_already_switched_id = 0;
const int keep_bindlow = !!(flags & SWITCH_ID_KEEP_BINDLOW);
tor_assert(user); tor_assert(user);
@ -1951,6 +2032,13 @@ switch_id(const char *user)
return -1; return -1;
} }
#ifdef HAVE_LINUX_CAPABILITIES
if (keep_bindlow) {
if (drop_capabilities(1))
return -1;
}
#endif
/* Properly switch egid,gid,euid,uid here or bail out */ /* Properly switch egid,gid,euid,uid here or bail out */
if (setgroups(1, &pw->pw_gid)) { if (setgroups(1, &pw->pw_gid)) {
log_warn(LD_GENERAL, "Error setting groups to gid %d: \"%s\".", log_warn(LD_GENERAL, "Error setting groups to gid %d: \"%s\".",
@ -2004,6 +2092,12 @@ switch_id(const char *user)
/* We've properly switched egid, gid, euid, uid, and supplementary groups if /* We've properly switched egid, gid, euid, uid, and supplementary groups if
* we're here. */ * we're here. */
#ifdef HAVE_LINUX_CAPABILITIES
if (keep_bindlow) {
if (drop_capabilities(0))
return -1;
}
#endif
#if !defined(CYGWIN) && !defined(__CYGWIN__) #if !defined(CYGWIN) && !defined(__CYGWIN__)
/* If we tried to drop privilege to a group/user other than root, attempt to /* If we tried to drop privilege to a group/user other than root, attempt to
@ -2051,6 +2145,7 @@ switch_id(const char *user)
#else #else
(void)user; (void)user;
(void)flags;
log_warn(LD_CONFIG, log_warn(LD_CONFIG,
"User specified but switching users is unsupported on your OS."); "User specified but switching users is unsupported on your OS.");

View File

@ -625,7 +625,15 @@ typedef unsigned long rlim_t;
int get_max_sockets(void); int get_max_sockets(void);
int set_max_file_descriptors(rlim_t limit, int *max); int set_max_file_descriptors(rlim_t limit, int *max);
int tor_disable_debugger_attach(void); 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);
#define SWITCH_ID_KEEP_BINDLOW 1
int switch_id(const char *user, unsigned flags);
#ifdef HAVE_PWD_H #ifdef HAVE_PWD_H
char *get_user_homedir(const char *username); char *get_user_homedir(const char *username);
#endif #endif

View File

@ -308,6 +308,7 @@ static config_var_t option_vars_[] = {
V(Socks5ProxyUsername, STRING, NULL), V(Socks5ProxyUsername, STRING, NULL),
V(Socks5ProxyPassword, STRING, NULL), V(Socks5ProxyPassword, STRING, NULL),
V(KeepalivePeriod, INTERVAL, "5 minutes"), V(KeepalivePeriod, INTERVAL, "5 minutes"),
V(KeepCapabilities, AUTOBOOL, "auto"),
VAR("Log", LINELIST, Logs, NULL), VAR("Log", LINELIST, Logs, NULL),
V(LogMessageDomains, BOOL, "0"), V(LogMessageDomains, BOOL, "0"),
V(LogTimeGranularity, MSEC_INTERVAL, "1 second"), V(LogTimeGranularity, MSEC_INTERVAL, "1 second"),
@ -567,7 +568,8 @@ static int parse_ports(or_options_t *options, int validate_only,
char **msg_out, int *n_ports_out, char **msg_out, int *n_ports_out,
int *world_writable_control_socket); int *world_writable_control_socket);
static int check_server_ports(const smartlist_t *ports, 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 validate_data_directory(or_options_t *options);
static int write_configuration_file(const char *fname, static int write_configuration_file(const char *fname,
@ -1045,6 +1047,9 @@ consider_adding_dir_servers(const or_options_t *options,
return 0; 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 /** Fetch the active option list, and take actions based on it. All of the
* things we do should survive being done repeatedly. If present, * things we do should survive being done repeatedly. If present,
* <b>old_options</b> contains the previous value of the options. * <b>old_options</b> contains the previous value of the options.
@ -1178,8 +1183,14 @@ options_act_reversible(const or_options_t *old_options, char **msg)
} }
/* Setuid/setgid as appropriate */ /* Setuid/setgid as appropriate */
tor_assert(have_low_ports != -1);
if (options->User) { if (options->User) {
if (switch_id(options->User) != 0) { unsigned switch_id_flags = 0;
if (options->KeepCapabilities == 1 ||
(options->KeepCapabilities == -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. */ /* No need to roll back, since you can't change the value. */
*msg = tor_strdup("Problem with User value. See logs for details."); *msg = tor_strdup("Problem with User value. See logs for details.");
goto done; goto done;
@ -3997,6 +4008,12 @@ options_transition_allowed(const or_options_t *old,
return -1; return -1;
} }
if (old->KeepCapabilities != new_val->KeepCapabilities) {
*msg = tor_strdup("While Tor is running, changing KeepCapabilities is "
"not allowed.");
return -1;
}
if (!opt_streq(old->SyslogIdentityTag, new_val->SyslogIdentityTag)) { if (!opt_streq(old->SyslogIdentityTag, new_val->SyslogIdentityTag)) {
*msg = tor_strdup("While Tor is running, changing " *msg = tor_strdup("While Tor is running, changing "
"SyslogIdentityTag is not allowed."); "SyslogIdentityTag is not allowed.");
@ -6535,10 +6552,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"); *msg = tor_strdup("Misconfigured server ports");
goto err; goto err;
} }
if (have_low_ports < 0)
have_low_ports = (n_low_ports > 0);
*n_ports_out = smartlist_len(ports); *n_ports_out = smartlist_len(ports);
@ -6592,10 +6612,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 /** 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_port</b> to the number
* of sub-1024 ports we will be binding. */
static int static int
check_server_ports(const smartlist_t *ports, 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 = 0;
int n_orport_advertised_ipv4 = 0; int n_orport_advertised_ipv4 = 0;
@ -6658,16 +6680,24 @@ check_server_ports(const smartlist_t *ports,
r = -1; r = -1;
} }
if (n_low_port && options->AccountingMax) { if (n_low_port && options->AccountingMax &&
(!have_capability_support() || options->KeepCapabilities == 0)) {
const char *extra = "";
if (options->KeepCapabilities == 0 && have_capability_support())
extra = ", and you have disabled KeepCapabilities.";
log_warn(LD_CONFIG, log_warn(LD_CONFIG,
"You have set AccountingMax to use hibernation. You have also " "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 " "working when it tries to re-attach the port after a period of "
"hibernation. Please choose a different port or turn off " "hibernation. Please choose a different port or turn off "
"hibernation unless you know this combination will work on your " "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; return r;
} }

View File

@ -4317,6 +4317,9 @@ typedef struct {
int keygen_passphrase_fd; int keygen_passphrase_fd;
int change_key_passphrase; int change_key_passphrase;
char *master_key_fname; char *master_key_fname;
/** Autobool: Do we try to retain capabilities if we can? */
int KeepCapabilities;
} or_options_t; } or_options_t;
/** Persistent state for an onion router, as saved to disk. */ /** Persistent state for an onion router, as saved to disk. */