diff --git a/src/libstore/tcp-store.cc b/src/libstore/tcp-store.cc new file mode 100644 index 00000000000..1ce891427a3 --- /dev/null +++ b/src/libstore/tcp-store.cc @@ -0,0 +1,107 @@ +#include "remote-store.hh" +#include "finally.hh" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nix { + +struct TCPStoreConfig : virtual RemoteStoreConfig +{ + using RemoteStoreConfig::RemoteStoreConfig; + + const std::string name() override { return "TCP Store"; } +}; + +struct TCPStore : public virtual TCPStoreConfig, public virtual RemoteStore +{ + std::string host; + uint16_t port = 0; + + TCPStore(const std::string scheme, std::string path, const Params & params) + : StoreConfig(params) + , RemoteStoreConfig(params) + , TCPStoreConfig(params) + , Store(params) + , RemoteStore(params) + { + // FIXME: use parsed URL. + + auto p = path.find(':'); + if (p == std::string::npos) + throw UsageError("tcp:// stores require a port number (e.g. 'tcp://example.org:1234'), in '%s'", path); + + host = std::string(path, 0, p); + if (auto port2 = string2Int(std::string(path, p + 1))) + port = *port2; + else + throw UsageError("invalid TCP port number, in '%s'", path); + } + + std::string getUri() override + { return fmt("tcp://"); } + + static std::set uriSchemes() + { return {"tcp"}; } + + bool sameMachine() override + { return false; } + + ref openConnection() override + { + auto conn = make_ref(); + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = IPPROTO_TCP; + + struct addrinfo * result; + if (auto s = getaddrinfo(host.c_str(), fmt("%d", port).c_str(), &hints, &result)) + throw Error("DNS lookup of '%s' failed: %s", host, gai_strerror(s)); + + Finally cleanup([&]() { freeaddrinfo(result); }); + + std::string err; + + for (auto rp = result; rp; rp = rp->ai_next) { + AutoCloseFD fd = socket( + rp->ai_family, + rp->ai_socktype + #ifdef SOCK_CLOEXEC + | SOCK_CLOEXEC + #endif + , rp->ai_protocol); + if (!fd) { + err = strerror(errno); + continue; + } + closeOnExec(fd.get()); + + if (::connect(fd.get(), rp->ai_addr, rp->ai_addrlen) == -1) { + err = strerror(errno); + continue; + } + + conn->fd = std::move(fd); + conn->from.fd = conn->fd.get(); + conn->to.fd = conn->fd.get(); + conn->startTime = std::chrono::steady_clock::now(); + return conn; + } + + throw Error("could not connect to daemon at '%s': %s", host, err); + } +}; + +static RegisterStoreImplementation regTCPStore; + +} diff --git a/src/nix/daemon.cc b/src/nix/daemon.cc index 2cf2a04c9cb..cfa16bbbcd0 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 @@ -99,27 +101,28 @@ bool matchUser(const string & user, const string & group, const Strings & users) 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; + std::optional ip; + std::optional port; }; // 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) @@ -129,12 +132,24 @@ 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 + sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + if (getpeername(fd, (sockaddr *) &addr, &addrlen) == 0 + && (addr.ss_family == AF_INET || addr.ss_family == AF_INET6)) + { + char host[1024]; + char serv[128]; + if (getnameinfo((sockaddr *) &addr, addrlen, host, sizeof(host), serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV) == 0) { + peer.ip = std::string(host); + peer.port = std::string(serv); + } + } + return peer; } @@ -151,108 +166,138 @@ 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 = settings.trustedUsers; + Strings allowedUsers = settings.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.ip + ? fmt("%s:%s", *peer.ip, *peer.port) + : 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]) { + 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, [&](Store & store) { + if (peer.uid) + store.createUser(user, *peer.uid); + }); +} + + static void daemonLoop() { if (chdir("/") == -1) throw SysError("cannot change current directory"); - // Get rid of children automatically; don't let them become zombies. + // Get rid of children automatically; don't let them become zombies. setSigChldAction(true); - 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)); } - // Loop accepting connections. + std::vector fds; + for (auto & i : listeningSockets) + fds.push_back({.fd = i.get(), .events = POLLIN}); + + // 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; - 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; - string group = gr ? gr->gr_name : std::to_string(peer.gid); - - Strings trustedUsers = settings.trustedUsers; - Strings allowedUsers = settings.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((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]) { - string 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, [&](Store & store) { -#if 0 - /* Prevent users from doing something very dangerous. */ - if (geteuid() == 0 && - querySetting("build-users-group", "") == "") - throw Error("if you run 'nix-daemon' as root, then you MUST set 'build-users-group'!"); -#endif - store.createUser(user, peer.uid); - }); - - exit(0); - }, options); + } } catch (Interrupted & e) { return; @@ -265,7 +310,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()) { @@ -299,10 +344,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, [&](Store & _){}); + if (auth) + authConnection(from, to); + else + processConnection(openUncachedStore(), from, to, Trusted, NotRecursive, [&](Store & _) {}); } } else daemonLoop(); @@ -326,7 +371,7 @@ static int main_nix_daemon(int argc, char * * argv) return true; }); - runDaemon(stdio); + runDaemon(stdio, false); return 0; } @@ -336,6 +381,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"; @@ -352,7 +415,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 e97016a94f4..798538d2cb8 100644 --- a/src/nix/daemon.md +++ b/src/nix/daemon.md @@ -8,6 +8,19 @@ R""( # nix daemon ``` +* The daemon does not have native support for opening a TCP socket to + listen to, but you can do this using `socat`: + + ```console + # socat TCP-LISTEN:3456,reuseaddr,fork EXEC:'nix daemon --stdio',nofork + ``` + + You can then connect to this daemon using the `tcp` store type: + + ```console + # nix store ping --store tcp://example.org:3456 + ``` + # Description This command runs the Nix daemon, which is a required component in @@ -18,4 +31,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 both on a Unix domain +socket and on port 1234: + +``` +[Socket] +ListenStream=/nix/var/nix/daemon-socket/socket +ListenStream=1234 +``` + )""