diff --git a/src/libcmd/include/nix/cmd/meson.build b/src/libcmd/include/nix/cmd/meson.build index 119d0814b9f1..c9c3d0dc7de2 100644 --- a/src/libcmd/include/nix/cmd/meson.build +++ b/src/libcmd/include/nix/cmd/meson.build @@ -20,4 +20,5 @@ headers = files( 'network-proxy.hh', 'repl-interacter.hh', 'repl.hh', + 'unix-socket-server.hh', ) diff --git a/src/libcmd/include/nix/cmd/unix-socket-server.hh b/src/libcmd/include/nix/cmd/unix-socket-server.hh new file mode 100644 index 000000000000..c1aed41f94c4 --- /dev/null +++ b/src/libcmd/include/nix/cmd/unix-socket-server.hh @@ -0,0 +1,79 @@ +#pragma once +///@file + +#include "nix/util/file-descriptor.hh" + +#include +#include +#include +#include + +namespace nix::unix { + +/** + * Information about the identity of the peer on a Unix domain socket connection. + */ +struct PeerInfo +{ + std::optional pid; + std::optional uid; + std::optional gid; +}; + +/** + * Get the identity of the caller, if possible. + */ +PeerInfo getPeerInfo(Descriptor remote); + +/** + * Callback type for handling new connections. + * + * The callback receives ownership of the connection and is responsible + * for handling it (e.g., forking a child process, spawning a thread, etc.). + * + * @param socket The accepted connection file descriptor. + * @param closeListeners A callback to close the listening sockets. + * Useful in forked child processes to release the bound sockets. + */ +using UnixSocketHandler = std::function closeListeners)>; + +/** + * Options for the serve loop. + * + * Only used if no systemd socket activation is detected. + */ +struct ServeUnixSocketOptions +{ + /** + * The Unix domain socket path to create and listen on. + */ + std::filesystem::path socketPath; + + /** + * Mode for the created socket file. + */ + mode_t socketMode = 0666; +}; + +/** + * Run a server loop that accepts connections and calls the handler for each. + * + * This function handles: + * - systemd socket activation (via LISTEN_FDS environment variable) + * - Creating and binding a Unix domain socket if no activation is detected + * - Polling for incoming connections + * - Accepting connections + * + * For each accepted connection, the handler is called with the connection + * file descriptor. The handler takes ownership of the file descriptor and + * is responsible for closing it when done. + * + * This function never returns normally. It runs until interrupted + * (e.g., via SIGINT), at which point it throws `Interrupted`. + * + * @param options Configuration for the server. + * @param handler Callback invoked for each accepted connection. + */ +[[noreturn]] void serveUnixSocket(const ServeUnixSocketOptions & options, UnixSocketHandler handler); + +} // namespace nix::unix diff --git a/src/libcmd/meson.build b/src/libcmd/meson.build index f553afa0ba17..aa8c6322f042 100644 --- a/src/libcmd/meson.build +++ b/src/libcmd/meson.build @@ -86,6 +86,12 @@ sources = files( 'repl.cc', ) +if host_machine.system() != 'windows' + sources += files( + 'unix/unix-socket-server.cc', + ) +endif + subdir('include/nix/cmd') subdir('nix-meson-build-support/export-all-symbols') diff --git a/src/libcmd/unix/unix-socket-server.cc b/src/libcmd/unix/unix-socket-server.cc new file mode 100644 index 000000000000..8cc2bd713d07 --- /dev/null +++ b/src/libcmd/unix/unix-socket-server.cc @@ -0,0 +1,126 @@ +///@file + +#include "nix/cmd/unix-socket-server.hh" +#include "nix/util/environment-variables.hh" +#include "nix/util/file-system.hh" +#include "nix/util/logging.hh" +#include "nix/util/signals.hh" +#include "nix/util/strings.hh" +#include "nix/util/unix-domain-socket.hh" +#include "nix/util/util.hh" + +#include +#include +#include + +#if defined(__APPLE__) || defined(__FreeBSD__) +# include +#endif + +namespace nix::unix { + +PeerInfo getPeerInfo(Descriptor remote) +{ + PeerInfo peer; + +#if defined(SO_PEERCRED) + +# if defined(__OpenBSD__) + struct sockpeercred cred; +# else + ucred cred; +# endif + socklen_t credLen = sizeof(cred); + if (getsockopt(remote, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == 0) { + peer.pid = cred.pid; + peer.uid = cred.uid; + peer.gid = cred.gid; + } + +#elif defined(LOCAL_PEERCRED) + +# if !defined(SOL_LOCAL) +# define SOL_LOCAL 0 +# endif + + xucred cred; + socklen_t credLen = sizeof(cred); + if (getsockopt(remote, SOL_LOCAL, LOCAL_PEERCRED, &cred, &credLen) == 0) + peer.uid = cred.cr_uid; + +#endif + + return peer; +} + +[[noreturn]] void serveUnixSocket(const ServeUnixSocketOptions & options, UnixSocketHandler handler) +{ + std::vector listeningSockets; + + static constexpr int SD_LISTEN_FDS_START = 3; + + // Handle socket-based activation by systemd. + auto listenFds = getEnv("LISTEN_FDS"); + if (listenFds) { + if (getEnv("LISTEN_PID") != std::to_string(getpid())) + throw Error("unexpected systemd environment variables"); + auto count = string2Int(*listenFds); + assert(count); + for (unsigned int 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. + else { + createDirs(options.socketPath.parent_path()); + listeningSockets.push_back(createUnixDomainSocket(options.socketPath.string(), options.socketMode)); + } + + std::vector fds; + for (auto & i : listeningSockets) + fds.push_back({.fd = i.get(), .events = POLLIN}); + + // Loop accepting connections. + while (1) { + try { + checkInterrupt(); + + auto count = poll(fds.data(), fds.size(), -1); + if (count == -1) { + if (errno == EINTR) + continue; + throw SysError("polling for incoming connections"); + } + + for (auto & fd : fds) { + if (!fd.revents) + continue; + + // Accept a connection. + struct sockaddr_un remoteAddr; + socklen_t remoteAddrLen = sizeof(remoteAddr); + + AutoCloseFD remote = accept(fd.fd, (struct sockaddr *) &remoteAddr, &remoteAddrLen); + checkInterrupt(); + if (!remote) { + if (errno == EINTR) + continue; + throw SysError("accepting connection"); + } + + handler(std::move(remote), [&]() { listeningSockets.clear(); }); + } + + } catch (Error & error) { + auto ei = error.info(); + // FIXME: add to trace? + ei.msg = HintFmt("while processing connection: %1%", ei.msg.str()); + logError(ei); + } + } +} + +} // namespace nix::unix diff --git a/src/nix/unix/daemon.cc b/src/nix/unix/daemon.cc index 8012f3118b31..cbd0d6aca788 100644 --- a/src/nix/unix/daemon.cc +++ b/src/nix/unix/daemon.cc @@ -15,6 +15,7 @@ #include "nix/store/derivations.hh" #include "nix/util/finally.hh" #include "nix/cmd/legacy.hh" +#include "nix/cmd/unix-socket-server.hh" #include "nix/store/daemon.hh" #include "man-pages.hh" @@ -27,24 +28,16 @@ #include #include #include -#include -#include #include #include #include #include #include -#include -#include #ifdef __linux__ # include "nix/util/cgroup.hh" #endif -#if defined(__APPLE__) || defined(__FreeBSD__) -# include -#endif - using namespace nix; using namespace nix::daemon; @@ -199,52 +192,6 @@ matchUser(const std::optional & user, const std::optional pid; - std::optional uid; - std::optional gid; -}; - -/** - * Get the identity of the caller, if possible. - */ -static PeerInfo getPeerInfo(Descriptor remote) -{ - PeerInfo peer; - -#if defined(SO_PEERCRED) - -# if defined(__OpenBSD__) - struct sockpeercred cred; -# else - ucred cred; -# endif - socklen_t credLen = sizeof(cred); - if (getsockopt(remote, SOL_SOCKET, SO_PEERCRED, &cred, &credLen) == 0) { - peer.pid = cred.pid; - peer.uid = cred.uid; - peer.gid = cred.gid; - } - -#elif defined(LOCAL_PEERCRED) - -# if !defined(SOL_LOCAL) -# define SOL_LOCAL 0 -# endif - - xucred cred; - socklen_t credLen = sizeof(cred); - if (getsockopt(remote, SOL_LOCAL, LOCAL_PEERCRED, &cred, &credLen) == 0) - peer.uid = cred.cr_uid; - -#endif - - return peer; -} - -#define SD_LISTEN_FDS_START 3 - /** * Open a store without a path info cache. */ @@ -267,7 +214,7 @@ static ref openUncachedStore() * * If the potential client is not allowed to talk to us, we throw an `Error`. */ -static std::pair> authPeer(const PeerInfo & peer) +static std::pair> authPeer(const unix::PeerInfo & peer) { TrustedFlag trusted = NotTrusted; @@ -306,32 +253,6 @@ static void daemonLoop(std::optional forceTrustClientOpt) if (chdir("/") == -1) throw SysError("cannot change current directory"); - std::vector listeningSockets; - - // Handle socket-based activation by systemd. - auto listenFds = getEnv("LISTEN_FDS"); - if (listenFds) { - if (getEnv("LISTEN_PID") != std::to_string(getpid())) - throw Error("unexpected systemd environment variables"); - auto count = string2Int(*listenFds); - assert(count); - for (auto i = 0; i < count; ++i) { - AutoCloseFD fdSocket(SD_LISTEN_FDS_START + i); - unix::closeOnExec(fdSocket.get()); - listeningSockets.push_back(std::move(fdSocket)); - } - } - - // Otherwise, create and bind to a Unix domain socket. - else { - createDirs(dirOf(settings.nixDaemonSocketFile)); - 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); @@ -356,45 +277,23 @@ static void daemonLoop(std::optional forceTrustClientOpt) } #endif - // Loop accepting connections. - while (1) { - - try { - checkInterrupt(); - - auto count = poll(fds.data(), fds.size(), -1); - if (count == -1) { - if (errno == EINTR) - continue; - throw SysError("polling for incomming connections"); - } - - for (auto & fd : fds) { - if (!fd.revents) - continue; - - // Accept a connection. - struct sockaddr_un remoteAddr; - socklen_t remoteAddrLen = sizeof(remoteAddr); - - AutoCloseFD remote = accept(fd.fd, (struct sockaddr *) &remoteAddr, &remoteAddrLen); - checkInterrupt(); - if (!remote) { - if (errno == EINTR) - continue; - throw SysError("accepting connection"); - } - + try { + unix::serveUnixSocket( + { + .socketPath = settings.nixDaemonSocketFile, + .socketMode = 0666, + }, + [&](AutoCloseFD remote, std::function closeListeners) { unix::closeOnExec(remote.get()); - PeerInfo peer; + unix::PeerInfo peer; TrustedFlag trusted; std::optional userName; if (forceTrustClientOpt) trusted = *forceTrustClientOpt; else { - peer = getPeerInfo(remote.get()); + peer = unix::getPeerInfo(remote.get()); auto [_trusted, _userName] = authPeer(peer); trusted = _trusted; userName = _userName; @@ -412,8 +311,8 @@ static void daemonLoop(std::optional forceTrustClientOpt) options.runExitHandlers = true; options.allowVfork = false; startProcess( - [&]() { - listeningSockets.clear(); + [&, closeListeners = std::move(closeListeners)]() { + closeListeners(); // Background the daemon. if (setsid() == -1) @@ -435,16 +334,9 @@ static void daemonLoop(std::optional forceTrustClientOpt) exit(0); }, options); - } - - } catch (Interrupted & e) { - return; - } catch (Error & error) { - auto ei = error.info(); - // FIXME: add to trace? - ei.msg = HintFmt("while processing connection: %1%", ei.msg.str()); - logError(ei); - } + }); + } catch (Interrupted & e) { + return; } }