libstore: allow for multiple daemon sockets with preference

this will let us configure more than one socket to connect/bind to,
which in turn lets us use posix acls on sockets for access control.
we will also need something like this for the final rpc transition.

Change-Id: I9c39f14906e9bf809055ab5c94bf687745b4f69e
This commit is contained in:
eldritch horrors
2026-01-26 18:17:07 +00:00
parent f4458b8e46
commit bdc220b8ec
11 changed files with 170 additions and 43 deletions
+4 -1
View File
@@ -383,7 +383,10 @@ I grepped `lix/` for `get[eE]nv\("` to find the mentions in Lix code.
Overrides compile-time configuration of various locations used by Lix. See `lix/libstore/globals.cc`.
**Expected value**: a directory
- `NIX_DAEMON_SOCKET_PATH` (optional) - Overrides the daemon socket path from `$NIX_STATE_DIR/daemon-socket/socket`.
- `LIX_DAEMON_SOCKET_DIR` (optional) - Overrides the daemon socket directory from `$NIX_STATE_DIR/daemon-socket`.
**Expected value**: a directory
- `NIX_DAEMON_SOCKET_PATH` (optional) - Overrides the daemon socket path from `$NIX_STATE_DIR/daemon-socket/socket`. Ignored if `LIX_DAEMON_SOCKET_DIR` is set.
**Expected value**: path to a socket
- `NIX_LOG_FD` (output) - An FD number for logs in `internal-json` format to be sent to.
+17 -3
View File
@@ -49,13 +49,16 @@ namespace nix {
Nix daemon by setting the mode/ownership of the directory
appropriately. (This wouldn't work on the socket itself since it
must be deleted and recreated on startup.) */
#define DEFAULT_SOCKET_PATH "/daemon-socket/socket"
#define DEFAULT_SOCKET_DIR "/daemon-socket"
#define LEGACY_SOCKET "/socket"
Settings settings;
Settings::Settings()
: nixPrefix(NIX_PREFIX)
, nixStore(canonPath(getEnvNonEmpty("NIX_STORE_DIR").value_or(getEnvNonEmpty("NIX_STORE").value_or(NIX_STORE_DIR))))
, nixStore(canonPath(
getEnvNonEmpty("NIX_STORE_DIR").value_or(getEnvNonEmpty("NIX_STORE").value_or(NIX_STORE_DIR))
))
, nixDataDir(canonPath(getEnvNonEmpty("NIX_DATA_DIR").value_or(NIX_DATA_DIR)))
, nixLogDir(canonPath(getEnvNonEmpty("NIX_LOG_DIR").value_or(NIX_LOG_DIR)))
, nixStateDir(canonPath(getEnvNonEmpty("NIX_STATE_DIR").value_or(NIX_STATE_DIR)))
@@ -63,8 +66,19 @@ Settings::Settings()
, nixUserConfFiles(getUserConfigFiles())
, nixBinDir(canonPath(getEnvNonEmpty("NIX_BIN_DIR").value_or(NIX_BIN_DIR)))
, nixManDir(canonPath(NIX_MAN_DIR))
, nixDaemonSocketFile(canonPath(getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH").value_or(nixStateDir + DEFAULT_SOCKET_PATH)))
{
if (auto socketDirFromEnv = getEnvNonEmpty("LIX_DAEMON_SOCKET_DIR")) {
nixDaemonSockets_ = {{canonPath(*socketDirFromEnv + LEGACY_SOCKET)}};
} else if (auto socketPathFromEnv = getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH")) {
nixDaemonSockets_ = {{canonPath(*socketPathFromEnv)}};
} else {
auto baseDir = nixStateDir + DEFAULT_SOCKET_DIR;
// this should always match the list of sockets created by daemonLoop and the socket units
nixDaemonSockets_ = {
{canonPath(baseDir + LEGACY_SOCKET)},
};
}
buildUsersGroup.setDefault(getuid() == 0 ? "nixbld" : "");
allowSymlinkedStore.setDefault(getEnv("NIX_IGNORE_SYMLINK_STORE") == "1");
+15 -4
View File
@@ -61,7 +61,16 @@ const uint32_t maxIdsPerBuild =
#endif
;
class Settings : public Config {
class Settings : public Config
{
public:
struct DaemonSocketPath
{
Path path;
};
private:
std::list<DaemonSocketPath> nixDaemonSockets_;
unsigned int getDefaultCores();
@@ -117,9 +126,12 @@ public:
Path nixManDir;
/**
* File name of the socket the daemon listens to.
* Socket paths a client should connect to, in order of decreasing preference.
*/
Path nixDaemonSocketFile;
const std::list<DaemonSocketPath> & nixDaemonSockets() const
{
return nixDaemonSockets_;
}
/**
* Whether to show build log output in real time.
@@ -135,7 +147,6 @@ public:
#include "lix/libstore/libstore-settings.gen.inc"
};
// FIXME: don't use a global variable.
extern Settings settings;
+7 -2
View File
@@ -27,6 +27,7 @@
#include "lix/libstore/worker-protocol.hh"
#include "lix/libutil/users.hh"
#include <algorithm>
#include <functional>
#include <kj/async.h>
#include <memory>
@@ -1422,7 +1423,11 @@ openFromNonUri(const std::string & uri, const StoreConfig::Params & params, Allo
{
if (uri == "" || uri == "auto") {
auto stateDir = getOr(params, "state", settings.nixStateDir);
if (allowDaemon == AllowDaemon::Allow && pathExists(settings.nixDaemonSocketFile)) {
if (allowDaemon == AllowDaemon::Allow
&& std::ranges::any_of(
settings.nixDaemonSockets(), [](auto & socket) { return pathExists(socket.path); }
))
{
return make_ref<UDSRemoteStore>(params);
} else if (sys::access(stateDir, R_OK | W_OK) == 0) {
return LocalStore::makeLocalStore(params);
@@ -1453,7 +1458,7 @@ openFromNonUri(const std::string & uri, const StoreConfig::Params & params, Allo
// FIXME? this ignores *all* store parameters passed to this function?
return LocalStore::makeLocalStore(chrootStoreParams);
}
#endif
#endif
else
return LocalStore::makeLocalStore(params);
} else if (uri == "daemon") {
+34 -1
View File
@@ -1,9 +1,15 @@
#include "lix/libstore/uds-remote-store.hh"
#include "globals.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/unix-domain-socket.hh"
#include "lix/libstore/worker-protocol.hh"
#include <cerrno>
#include <ranges>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
@@ -51,6 +57,23 @@ std::string UDSRemoteStore::getUri()
}
}
static void connectToFirstAvailableSocket(AutoCloseFD & sockFD, const std::list<Path> & paths)
{
for (const auto & socket : paths) {
try {
nix::connect(sockFD.get(), socket);
return;
} catch (SysError & e) {
if (e.errNo == EACCES || e.errNo == EPERM || e.errNo == ECONNREFUSED || e.errNo == ENOENT) {
debug("skipping socket %s: %s", socket, strerror(e.errNo));
} else {
throw;
}
}
}
throw Error("could not connect to any lix socket (tried %s)", concatStringsSep(", ", paths));
}
ref<RemoteStore::Connection> UDSRemoteStore::openConnection()
{
auto conn = make_ref<Connection>();
@@ -58,7 +81,17 @@ ref<RemoteStore::Connection> UDSRemoteStore::openConnection()
/* Connect to a daemon that does the privileged work for us. */
conn->fd = createUnixDomainSocket();
nix::connect(conn->fd.get(), path ? *path : settings.nixDaemonSocketFile);
std::list<Path> candidates;
if (path) {
candidates.emplace_back(*path);
} else {
candidates = settings.nixDaemonSockets()
| std::views::transform([](auto & socket) { return socket.path; })
| std::ranges::to<std::list<Path>>();
}
connectToFirstAvailableSocket(conn->fd, candidates);
conn->startTime = std::chrono::steady_clock::now();
+48 -28
View File
@@ -6,6 +6,7 @@
#include "lix/libstore/remote-store.hh"
#include "lix/libstore/remote-store-connection.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libutil/async-collect.hh"
#include "lix/libutil/async-io.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/current-process.hh"
@@ -33,6 +34,7 @@
#include <cstdint>
#include <cstring>
#include <kj/async.h>
#include <string>
#include <unistd.h>
#include <signal.h>
@@ -276,34 +278,13 @@ static std::pair<TrustedFlag, std::string> authPeer(const PeerInfo & peer)
return { trusted, std::move(user) };
}
/**
* Run a server. The loop opens a socket and accepts new connections from that
* socket.
*
* @param forceTrustClientOpt If present, force trusting or not trusted
* the client. Otherwise, decide based on the authentication settings
* and user credentials (from the unix domain socket).
*/
static kj::Promise<Result<void>> daemonLoop(std::optional<TrustedFlag> forceTrustClientOpt)
static kj::Promise<Result<void>> daemonLoopForSocket(
const Path & self,
const Settings::DaemonSocketPath & socket,
AutoCloseFD & fdSocket,
std::optional<TrustedFlag> forceTrustClientOpt
)
try {
if (chdir("/") == -1)
throw SysError("cannot change current directory");
createDirs(dirOf(settings.nixDaemonSocketFile));
auto fdSocket = createUnixDomainSocket(settings.nixDaemonSocketFile, 0666);
// Get rid of children automatically; don't let them become zombies.
setSigChldAction(true);
const auto self = [] {
auto tmp = getSelfExe();
if (!tmp) {
throw Error("can't locate the daemon binary!");
}
return *tmp;
}();
makeNonBlocking(fdSocket.get());
auto observer = kj::UnixEventPort::FdObserver{
AIO().unixEventPort, fdSocket.get(), kj::UnixEventPort::FdObserver::OBSERVE_READ
@@ -377,6 +358,46 @@ try {
co_return result::current_exception();
}
/**
* Run a server. The loop opens a socket and accepts new connections from that
* socket.
*
* @param forceTrustClientOpt If present, force trusting or not trusted
* the client. Otherwise, decide based on the authentication settings
* and user credentials (from the unix domain socket).
*/
static kj::Promise<Result<void>> daemonLoop(std::optional<TrustedFlag> forceTrustClientOpt)
try {
if (chdir("/") == -1) {
throw SysError("cannot change current directory");
}
const auto self = [] {
auto tmp = getSelfExe();
if (!tmp) {
throw Error("can't locate the daemon binary!");
}
return *tmp;
}();
std::list<std::pair<Settings::DaemonSocketPath, AutoCloseFD>> sockets;
for (auto & socket : settings.nixDaemonSockets()) {
createDirs(dirOf(socket.path));
sockets.emplace_back(socket, createUnixDomainSocket(socket.path, 0666));
}
// Get rid of children automatically; don't let them become zombies.
setSigChldAction(true);
TRY_AWAIT(asyncSpread(sockets, [&](auto & socket) {
return daemonLoopForSocket(self, socket.first, socket.second, forceTrustClientOpt);
}));
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
static void
daemonInstance(AsyncIoRoot & aio, std::optional<TrustedFlag> forceTrustClientOpt, char * peerPidArg)
{
@@ -651,5 +672,4 @@ void registerNixDaemon()
{
registerCommand2<CmdDaemon>({"daemon"});
}
}
+39 -1
View File
@@ -1,4 +1,41 @@
foreach config : [ 'nix-daemon.socket', 'nix-daemon.service', 'nix-daemon@.service' ]
lix_daemon_socket_units = []
# legacy sockets are handled specially for graceful fallback if nixos module
# updates are not applied in lockstep with the socket split updates for lix.
lix_daemon_socket_units += [ 'nix-daemon.socket' ]
configure_file(
input : 'daemon.socket.in',
output : lix_daemon_socket_units[-1],
install : true,
install_dir : prefix / 'lib/systemd/system',
install_mode : 'rw-r--r--',
configuration : {
'storedir' : store_dir,
'localstatedir' : state_dir,
'bindir' : bindir,
'protocol' : 'legacy',
'kind' : 'combined',
'path' : 'socket',
'dirmode' : '0755',
},
)
configure_file(
input : 'daemon@.service.in',
output : 'nix-daemon@.service',
install : true,
install_dir : prefix / 'lib/systemd/system',
install_mode : 'rw-r--r--',
configuration : {
'storedir' : store_dir,
'localstatedir' : state_dir,
'bindir' : bindir,
'conflicts' : ' '.join(lix_daemon_socket_units),
'protocol' : 'legacy',
'kind' : 'combined',
},
)
foreach config : [ 'nix-daemon.service' ]
configure_file(
input : config + '.in',
output : config,
@@ -9,6 +46,7 @@ foreach config : [ 'nix-daemon.socket', 'nix-daemon.service', 'nix-daemon@.servi
'storedir' : store_dir,
'localstatedir' : state_dir,
'bindir' : bindir,
'conflicts' : ' '.join(lix_daemon_socket_units),
},
)
endforeach
+1 -1
View File
@@ -1,7 +1,7 @@
[Unit]
Description=Nix Daemon
Documentation=man:nix-daemon https://docs.lix.systems/manual/lix/stable
Conflicts=nix-daemon.socket
Conflicts=@conflicts@
RequiresMountsFor=@storedir@
RequiresMountsFor=@localstatedir@
RequiresMountsFor=@localstatedir@/nix/db
+5 -2
View File
@@ -179,7 +179,8 @@ class Nix:
daemon.settings.store = f"local?root={self.env.dirs.test_root}"
daemon.settings.other_settings |= settings
sockets = [Path(daemon.env.dirs.nix_state_dir) / "daemon-socket/socket"]
sockets_dir = Path(daemon.env.dirs.nix_state_dir) / "daemon-socket"
sockets = [sockets_dir / "socket"]
for p in sockets:
p.unlink(missing_ok=True)
@@ -194,7 +195,9 @@ class Nix:
self.logger.error("daemon exited unexpectedly")
# wait for daemon to come up. this may take a while under load.
while not all(p.exists() for p in sockets):
# we only test the *last* socket in the list because that's the
# last one the daemon creates, once it's there the daemon is up
while not sockets[-1].exists():
if status := proc.wait(0.01):
log_daemon_result(status, logging.ERROR)
raise RuntimeError("daemon exited during startup")