From f38006b0cd3a99f3f6b08af3b1b8914547a4cb95 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Thu, 23 Feb 2023 11:50:20 -0500 Subject: [PATCH] Support systemd socket activation support more comprensively We now support: 1. Multiple sockets, per the systemd socket activation spec 2. The sockets not having pid/uid/gid peer info because they might not be Unix domain sockets. The changes are by @edolstra, taken from #5265. This is just that PR *without* the TCP parts, which I gathered are the controversial parts. Hopefully this remainder is not so controversial. --- src/nix/daemon.cc | 231 ++++++++++++++++++++++++++++------------------ src/nix/daemon.md | 14 +++ 2 files changed, 155 insertions(+), 90 deletions(-) diff --git a/src/nix/daemon.cc b/src/nix/daemon.cc index a22bccba113..a585eb81555 100644 --- a/src/nix/daemon.cc +++ b/src/nix/daemon.cc @@ -15,17 +15,19 @@ #include #include -#include +#include +#include +#include +#include +#include +#include #include -#include -#include -#include #include +#include +#include #include -#include -#include -#include -#include +#include +#include #if __APPLE__ || __FreeBSD__ #include @@ -136,27 +138,26 @@ bool matchUser(const std::string & user, const std::string & group, const String struct PeerInfo { - bool pidKnown; - pid_t pid; - bool uidKnown; - uid_t uid; - bool gidKnown; - gid_t gid; + std::optional pid; + std::optional uid; + std::optional gid; }; // Get the identity of the caller, if possible. -static PeerInfo getPeerInfo(int remote) +static PeerInfo getPeerInfo(int fd) { - PeerInfo peer = { false, 0, false, 0, false, 0 }; + PeerInfo peer; #if defined(SO_PEERCRED) ucred cred; socklen_t credLen = sizeof(cred); - if (getsockopt(remote, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == -1) - throw SysError("getting peer credentials"); - peer = { true, cred.pid, true, cred.uid, true, cred.gid }; + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == 0) { + peer.pid = cred.pid; + peer.uid = cred.uid; + peer.gid = cred.gid; + } #elif defined(LOCAL_PEERCRED) @@ -166,9 +167,8 @@ static PeerInfo getPeerInfo(int remote) xucred cred; socklen_t credLen = sizeof(cred); - if (getsockopt(remote, SOL_LOCAL, LOCAL_PEERCRED, &cred, &credLen) == -1) - throw SysError("getting peer credentials"); - peer = { false, 0, true, cred.cr_uid, false, 0 }; + if (getsockopt(fd, SOL_LOCAL, LOCAL_PEERCRED, &cred, &credLen) == 0) + peer.uid = cred.cr_uid; #endif @@ -188,100 +188,133 @@ static ref openUncachedStore() } +static void authConnection(FdSource & from, FdSink & to) +{ + TrustedFlag trusted = NotTrusted; + PeerInfo peer = getPeerInfo(from.fd); + + std::string user, group; + + if (peer.uid) { + auto pw = getpwuid(*peer.uid); + user = pw ? pw->pw_name : std::to_string(*peer.uid); + } + + if (peer.gid) { + auto gr = getgrgid(*peer.gid); + group = gr ? gr->gr_name : std::to_string(*peer.gid); + } + + Strings trustedUsers = authorizationSettings.trustedUsers; + Strings allowedUsers = authorizationSettings.allowedUsers; + + if (matchUser(user, group, trustedUsers)) + trusted = Trusted; + + if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup) + throw Error("user '%1%' is not allowed to connect to the Nix daemon", user); + + printInfo( + "accepted connection from %s%s", + peer.pid && peer.uid + ? fmt("pid %s, user %s", std::to_string(*peer.pid), user) + : "", + trusted ? " (trusted)" : ""); + + // For debugging, stuff the pid into argv[1]. + if (peer.pid && savedArgv[1]) { + std::string processName = std::to_string(*peer.pid); + strncpy(savedArgv[1], processName.c_str(), strlen(savedArgv[1])); + } + + // Handle the connection. + processConnection(openUncachedStore(), from, to, trusted, NotRecursive); +} + + static void daemonLoop() { if (chdir("/") == -1) throw SysError("cannot change current directory"); - AutoCloseFD fdSocket; + std::vector listeningSockets; - // Handle socket-based activation by systemd. + // Handle socket-based activation by systemd. auto listenFds = getEnv("LISTEN_FDS"); if (listenFds) { - if (getEnv("LISTEN_PID") != std::to_string(getpid()) || listenFds != "1") + if (getEnv("LISTEN_PID") != std::to_string(getpid())) throw Error("unexpected systemd environment variables"); - fdSocket = SD_LISTEN_FDS_START; - closeOnExec(fdSocket.get()); + auto count = string2Int(*listenFds); + assert(count); + for (auto i = 0; i < count; ++i) { + AutoCloseFD fdSocket(SD_LISTEN_FDS_START + i); + closeOnExec(fdSocket.get()); + listeningSockets.push_back(std::move(fdSocket)); + } } - // Otherwise, create and bind to a Unix domain socket. + // Otherwise, create and bind to a Unix domain socket. else { createDirs(dirOf(settings.nixDaemonSocketFile)); - fdSocket = createUnixDomainSocket(settings.nixDaemonSocketFile, 0666); + listeningSockets.push_back(createUnixDomainSocket(settings.nixDaemonSocketFile, 0666)); } + std::vector fds; + for (auto & i : listeningSockets) + fds.push_back({.fd = i.get(), .events = POLLIN}); + // Get rid of children automatically; don't let them become zombies. setSigChldAction(true); - // Loop accepting connections. + // Loop accepting connections. while (1) { try { - // Accept a connection. - struct sockaddr_un remoteAddr; - socklen_t remoteAddrLen = sizeof(remoteAddr); - - AutoCloseFD remote = accept(fdSocket.get(), - (struct sockaddr *) &remoteAddr, &remoteAddrLen); checkInterrupt(); - if (!remote) { + + auto count = poll(fds.data(), fds.size(), -1); + if (count == -1) { if (errno == EINTR) continue; - throw SysError("accepting connection"); + throw SysError("poll"); } - closeOnExec(remote.get()); - - TrustedFlag trusted = NotTrusted; - PeerInfo peer = getPeerInfo(remote.get()); - - struct passwd * pw = peer.uidKnown ? getpwuid(peer.uid) : 0; - std::string user = pw ? pw->pw_name : std::to_string(peer.uid); + for (auto & fd : fds) { + if (!fd.revents) continue; - struct group * gr = peer.gidKnown ? getgrgid(peer.gid) : 0; - std::string group = gr ? gr->gr_name : std::to_string(peer.gid); - - Strings trustedUsers = authorizationSettings.trustedUsers; - Strings allowedUsers = authorizationSettings.allowedUsers; - - if (matchUser(user, group, trustedUsers)) - trusted = Trusted; + // Accept a connection. + AutoCloseFD remote = accept(fd.fd, nullptr, nullptr); + checkInterrupt(); + if (!remote) { + if (errno == EINTR) continue; + throw SysError("accepting connection"); + } - if ((!trusted && !matchUser(user, group, allowedUsers)) || group == settings.buildUsersGroup) - throw Error("user '%1%' is not allowed to connect to the Nix daemon", user); + closeOnExec(remote.get()); - printInfo(format((std::string) "accepted connection from pid %1%, user %2%" + (trusted ? " (trusted)" : "")) - % (peer.pidKnown ? std::to_string(peer.pid) : "") - % (peer.uidKnown ? user : "")); + // Fork a child to handle the connection. + ProcessOptions options; + options.errorPrefix = "unexpected Nix daemon error: "; + options.dieWithParent = false; + options.runExitHandlers = true; + options.allowVfork = false; + startProcess([&]() { + listeningSockets.clear(); - // Fork a child to handle the connection. - ProcessOptions options; - options.errorPrefix = "unexpected Nix daemon error: "; - options.dieWithParent = false; - options.runExitHandlers = true; - options.allowVfork = false; - startProcess([&]() { - fdSocket = -1; + // Background the daemon. + if (setsid() == -1) + throw SysError("creating a new session"); - // Background the daemon. - if (setsid() == -1) - throw SysError("creating a new session"); + // Restore normal handling of SIGCHLD. + setSigChldAction(false); - // Restore normal handling of SIGCHLD. - setSigChldAction(false); + FdSource from(remote.get()); + FdSink to(remote.get()); + authConnection(from, to); - // For debugging, stuff the pid into argv[1]. - if (peer.pidKnown && savedArgv[1]) { - auto processName = std::to_string(peer.pid); - strncpy(savedArgv[1], processName.c_str(), strlen(savedArgv[1])); - } + exit(0); + }, options); - // Handle the connection. - FdSource from(remote.get()); - FdSink to(remote.get()); - processConnection(openUncachedStore(), from, to, trusted, NotRecursive); - - exit(0); - }, options); + } } catch (Interrupted & e) { return; @@ -294,7 +327,7 @@ static void daemonLoop() } } -static void runDaemon(bool stdio) +static void runDaemon(bool stdio, bool auth) { if (stdio) { if (auto store = openUncachedStore().dynamic_pointer_cast()) { @@ -328,10 +361,10 @@ static void runDaemon(bool stdio) } else { FdSource from(STDIN_FILENO); FdSink to(STDOUT_FILENO); - /* Auth hook is empty because in this mode we blindly trust the - standard streams. Limiting access to those is explicitly - not `nix-daemon`'s responsibility. */ - processConnection(openUncachedStore(), from, to, Trusted, NotRecursive); + if (auth) + authConnection(from, to); + else + processConnection(openUncachedStore(), from, to, Trusted, NotRecursive); } } else daemonLoop(); @@ -355,7 +388,7 @@ static int main_nix_daemon(int argc, char * * argv) return true; }); - runDaemon(stdio); + runDaemon(stdio, false); return 0; } @@ -365,6 +398,24 @@ static RegisterLegacyCommand r_nix_daemon("nix-daemon", main_nix_daemon); struct CmdDaemon : StoreCommand { + bool stdio = false; + bool auth = true; + + CmdDaemon() + { + addFlag({ + .longName = "stdio", + .description = "Handle a single connection on stdin/stdout.", + .handler = {&stdio, true}, + }); + + addFlag({ + .longName = "no-auth", + .description = "Do not check whether the client is in `allowed-users`.", + .handler = {&auth, false}, + }); + } + std::string description() override { return "daemon to perform store operations on behalf of non-root clients"; @@ -381,7 +432,7 @@ struct CmdDaemon : StoreCommand void run(ref store) override { - runDaemon(false); + runDaemon(stdio, auth); } }; diff --git a/src/nix/daemon.md b/src/nix/daemon.md index d5cdadf08e1..99ec7a73d81 100644 --- a/src/nix/daemon.md +++ b/src/nix/daemon.md @@ -18,4 +18,18 @@ management framework such as `systemd`. Note that this daemon does not fork into the background. +# Systemd socket activation + +`nix daemon` supports systemd socket-based activation using the +`nix-daemon.socket` unit in the Nix distribution. It supports +listening on multiple addresses; for example, the following stanza in +`nix-daemon.socket` makes the daemon listen on two Unix domain +sockets: + +``` +[Socket] +ListenStream=/nix/var/nix/daemon-socket/socket +ListenStream=/nix/var/nix/daemon-socket/socket-2 +``` + )""