Skip to content

Commit

Permalink
Support systemd socket activation support more comprensively
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Ericson2314 committed Feb 23, 2023
1 parent 4a921ba commit f38006b
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 90 deletions.
231 changes: 141 additions & 90 deletions src/nix/daemon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
#include <climits>
#include <cstring>

#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <netdb.h>
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <pwd.h>
#include <grp.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>

#if __APPLE__ || __FreeBSD__
#include <sys/ucred.h>
Expand Down Expand Up @@ -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_t> pid;
std::optional<uid_t> uid;
std::optional<gid_t> 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)

Expand All @@ -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

Expand All @@ -188,100 +188,133 @@ static ref<Store> 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)
: "<unknown>",
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<AutoCloseFD> 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<unsigned int>(*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<struct pollfd> 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) : "<unknown>")
% (peer.uidKnown ? user : "<unknown>"));
// 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;
Expand All @@ -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<RemoteStore>()) {
Expand Down Expand Up @@ -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();
Expand All @@ -355,7 +388,7 @@ static int main_nix_daemon(int argc, char * * argv)
return true;
});

runDaemon(stdio);
runDaemon(stdio, false);

return 0;
}
Expand All @@ -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";
Expand All @@ -381,7 +432,7 @@ struct CmdDaemon : StoreCommand

void run(ref<Store> store) override
{
runDaemon(false);
runDaemon(stdio, auth);
}
};

Expand Down
14 changes: 14 additions & 0 deletions src/nix/daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

)""

0 comments on commit f38006b

Please sign in to comment.