mirror of
https://codeberg.org/guix/guix.git
synced 2025-10-02 02:15:12 +00:00
daemon: Use slirp4netns to provide networking to fixed-output derivations.
Previously, the builder of a fixed-output derivation could communicate with an external process via an abstract Unix-domain socket. In particular, it could send an open file descriptor to the store, granting write access to some of its output files in the store provided the derivation build fails—the fix for CVE-2024-27297 did not address this specific case. It could also send an open file descriptor to a setuid program, which could then be executed using execveat to gain the privileges of the build user. With this change, fixed-output derivations other than “builtin:download” and “builtin:git-download” always run in a separate network namespace and have network access provided by a TAP device backed by slirp4netns, thereby closing the abstract Unix-domain socket channel. * nix/libstore/globals.hh (Settings)[useHostLoopback, slirp4netns]: new fields. * config-daemon.ac (SLIRP4NETNS): new C preprocessor definition. * nix/libstore/globals.cc (Settings::Settings): initialize them to defaults. * nix/nix-daemon/guix-daemon.cc (options): add --isolate-host-loopback option. * doc/guix.texi: document it. * nix/libstore/build.cc (DerivationGoal)[slirp]: New field. (setupTap, setupTapAction, waitForSlirpReadyAction, enableRouteLocalnetAction, prepareSlirpChrootAction, spawnSlirp4netns, haveGlobalIPv6Address, remapIdsTo0Action): New functions. (initializeUserNamespace): allow the guest UID and GID to be specified. (DerivationGoal::killChild): When ‘slirp’ is not -1, call ‘kill’. (DerivationGoal::startBuilder): Unconditionally add CLONE_NEWNET to FLAGS. When ‘fixedOutput’ is true, spawn ‘slirp4netns’. When ‘fixedOutput’ and ‘useChroot’ are true, add setupTapAction, waitForSlirpReadyAction, and enableRouteLocalnetAction to builder setup phases. Create a /etc/resolv.conf for fixed-output derivations that directs them to slirp4netns's dns address. When settings.useHostLoopback is true, supply fixed-output derivations with a /etc/hosts that resolves "localhost" to slirp4netns's address for accessing the host loopback. * nix/libutil/util.cc (keepOnExec, decodeOctalEscaped, sendFD, receiveFD, findProgram): New functions. * nix/libutil/util.hh (keepOnExec, decodeOctalEscaped, sendFD, receiveFD, findProgram): New declarations. * gnu/packages/package-management.scm (guix): add slirp4netns input for linux targets. * tests/derivations.scm (builder-network-isolated?): new variable. ("fixed-output derivation, network access, localhost", "fixed-output derivation, network access, external host"): skip test case if fixed output derivations are isolated from the network. Change-Id: Ia3fea2ab7add56df66800071cf15cdafe7bfab96 Signed-off-by: John Kehayias <john.kehayias@protonmail.com>
This commit is contained in:
parent
be8aca0651
commit
fb42611b8f
10 changed files with 704 additions and 21 deletions
|
@ -14,6 +14,7 @@
|
|||
#include <map>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <regex>
|
||||
|
||||
#include <limits.h>
|
||||
#include <time.h>
|
||||
|
@ -73,10 +74,18 @@
|
|||
#endif
|
||||
|
||||
#if CHROOT_ENABLED
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <net/route.h>
|
||||
#include <arpa/inet.h>
|
||||
#if __linux__
|
||||
#include <linux/if_tun.h>
|
||||
/* This header isn't documented in 'man netdevice', but there doesn't seem to
|
||||
be any other way to get 'struct in6_ifreq'... */
|
||||
#include <linux/ipv6.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if __linux__
|
||||
|
@ -661,6 +670,10 @@ private:
|
|||
/* Whether this is a fixed-output derivation. */
|
||||
bool fixedOutput;
|
||||
|
||||
/* PID of the 'slirp4netns' process in case of a fixed-output
|
||||
derivation. */
|
||||
Pid slirp;
|
||||
|
||||
typedef void (DerivationGoal::*GoalState)();
|
||||
GoalState state;
|
||||
|
||||
|
@ -831,6 +844,10 @@ void DerivationGoal::killChild()
|
|||
worker.childTerminated(hook->pid);
|
||||
}
|
||||
hook.reset();
|
||||
|
||||
if (slirp != -1)
|
||||
/* Terminate the 'slirp4netns' process. */
|
||||
slirp.kill();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1611,7 +1628,9 @@ static const gid_t guestGID = 30000;
|
|||
/* Initialize the user namespace of CHILD. */
|
||||
static void initializeUserNamespace(pid_t child,
|
||||
uid_t hostUID = getuid(),
|
||||
gid_t hostGID = getgid())
|
||||
gid_t hostGID = getgid(),
|
||||
uid_t guestUID = guestUID,
|
||||
gid_t guestGID = guestGID)
|
||||
{
|
||||
writeFile("/proc/" + std::to_string(child) + "/uid_map",
|
||||
(format("%d %d 1") % guestUID % hostUID).str());
|
||||
|
@ -1624,12 +1643,427 @@ static void initializeUserNamespace(pid_t child,
|
|||
|
||||
#if CHROOT_ENABLED
|
||||
|
||||
void clearRootWritePermsAction(SpawnContext & sctx)
|
||||
/* Creating TAP device for the fixed-output derivation build environment,
|
||||
based on how slirp4netns does it. send_fd_socket is a unix-domain socket
|
||||
that a file descriptor for the TAP device will be sent on along with a
|
||||
single null byte of regular data. */
|
||||
static void setupTap(int send_fd_socket, bool ipv6Enabled)
|
||||
{
|
||||
AutoCloseFD tapfd;
|
||||
struct ifreq ifr;
|
||||
struct in6_ifreq ifr6;
|
||||
char tapname[] = "tap0";
|
||||
int ifindex;
|
||||
|
||||
tapfd = open("/dev/net/tun", O_RDWR);
|
||||
if(tapfd < 0)
|
||||
throw SysError("opening `/dev/net/tun'");
|
||||
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
|
||||
strncpy(ifr.ifr_name, tapname, sizeof(ifr.ifr_name) - 1);
|
||||
if(ioctl(tapfd, TUNSETIFF, (void*)&ifr) < 0)
|
||||
throw SysError("TUNSETIFF");
|
||||
|
||||
/* DAD is "duplicate address detection". By default the kernel will put
|
||||
any ipv6 addresses that we add into the "tentative" state, and only
|
||||
after several seconds have been spent trying to chat with network
|
||||
neighbors about whether anyone is already using the address will it
|
||||
allow it to be bound to, whether for listening or for connecting.
|
||||
|
||||
This causes tcp connections initiated before then to bind to ::1, which
|
||||
obviously is not a valid address for communication between hosts. Even
|
||||
after the real addresses leave the "tentative" state, the source address
|
||||
used for the already-started connection attempt does not change.
|
||||
|
||||
In our situation we know for a fact nobody else is using the addresses
|
||||
we give, so there's no point in waiting the extra several seconds to
|
||||
perform DAD; disable it entirely instead.
|
||||
|
||||
Note: this needs to use conf/tap0/ instead of conf/all/ */
|
||||
writeFile("/proc/sys/net/ipv6/conf/tap0/accept_dad", "0");
|
||||
|
||||
/* By default tap0 will solicit and receive router advertisements, and
|
||||
* thereby obtain an ipv6 address from slirp4netns. But if the host
|
||||
* doesn't have a working ipv6 connection, this could mess things up for
|
||||
* guest programs (and really the guest network stack itself), as they
|
||||
* have no way of knowing that, and will therefore likely try connecting
|
||||
* to addresses found in AAAA records, which will fail. To prevent this,
|
||||
* ignore router advertisements. */
|
||||
writeFile("/proc/sys/net/ipv6/conf/tap0/accept_ra", "0");
|
||||
|
||||
/* Now set up:
|
||||
1. tap0's active flags (so it's running, up, etc)
|
||||
2. tap0's MTU
|
||||
3. tap0's ip address
|
||||
4. tap0's network mask
|
||||
5. A default route to tap0 */
|
||||
AutoCloseFD sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
|
||||
if(sockfd < 0)
|
||||
throw SysError("creating socket");
|
||||
|
||||
AutoCloseFD sockfd6 = socket(AF_INET6, SOCK_DGRAM, 0);
|
||||
|
||||
if(sockfd6 < 0)
|
||||
throw SysError("creating ipv6 socket");
|
||||
|
||||
if(ioctl(sockfd, SIOCGIFINDEX, &ifr) < 0)
|
||||
throw SysError("getting tap0 ifindex");
|
||||
|
||||
ifindex = ifr.ifr_ifindex;
|
||||
|
||||
ifr.ifr_flags = IFF_UP | IFF_RUNNING;
|
||||
if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0)
|
||||
throw SysError("setting flags for tap0");
|
||||
|
||||
/* slirp4netns default */
|
||||
ifr.ifr_mtu = 1500;
|
||||
if(ioctl(sockfd, SIOCSIFMTU, &ifr) < 0)
|
||||
throw SysError("setting MTU for tap0");
|
||||
|
||||
/* default network CIDR: 10.0.2.0/24, fd00::/64 */
|
||||
/* default recommended_vguest: 10.0.2.100, fd00::??? (we choose to use
|
||||
fd00::80 and fe80::80) */
|
||||
/* default gateway: 10.0.2.2, fd00::2 */
|
||||
struct sockaddr_in *sai = (struct sockaddr_in *) &ifr.ifr_addr;
|
||||
sai->sin_family = AF_INET;
|
||||
sai->sin_port = htonl(0);
|
||||
if(inet_pton(AF_INET, "10.0.2.100", &sai->sin_addr) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
|
||||
if(ioctl(sockfd, SIOCSIFADDR, &ifr) < 0)
|
||||
throw SysError("setting tap0 address");
|
||||
|
||||
if(ipv6Enabled) {
|
||||
if(inet_pton(AF_INET6, "fd00::80", &ifr6.ifr6_addr) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
ifr6.ifr6_prefixlen = 64;
|
||||
ifr6.ifr6_ifindex = ifindex;
|
||||
|
||||
if(ioctl(sockfd6, SIOCSIFADDR, &ifr6) < 0)
|
||||
throw SysError("setting tap0 ipv6 address");
|
||||
}
|
||||
|
||||
/* Always set up the link-local address so that communication with the
|
||||
* host loopback over ipv6 can be possible. */
|
||||
if(inet_pton(AF_INET6, "fe80::80", &ifr6.ifr6_addr) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
ifr6.ifr6_prefixlen = 64;
|
||||
ifr6.ifr6_ifindex = ifindex;
|
||||
|
||||
if(ioctl(sockfd6, SIOCSIFADDR, &ifr6) < 0)
|
||||
throw SysError("setting tap0 link-local ipv6 address");
|
||||
|
||||
if(inet_pton(AF_INET, "255.255.255.0", &sai->sin_addr) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
|
||||
if(ioctl(sockfd, SIOCSIFNETMASK, &ifr) < 0)
|
||||
throw SysError("setting tap0 network mask");
|
||||
|
||||
/* To my knowledge there is no official documentation of SIOCADDRT and
|
||||
struct rtentry for Linux aside from the Linux kernel source code as of
|
||||
the year 2025. This is therefore fully cargo-culted from
|
||||
slirp4netns. */
|
||||
|
||||
struct rtentry route;
|
||||
memset(&route, 0, sizeof(route));
|
||||
sai = (struct sockaddr_in *)&route.rt_gateway;
|
||||
sai->sin_family = AF_INET;
|
||||
if(inet_pton(AF_INET, "10.0.2.2", &sai->sin_addr) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
sai = (struct sockaddr_in *)&route.rt_dst;
|
||||
sai->sin_family = AF_INET;
|
||||
sai->sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
sai = (struct sockaddr_in *)&route.rt_genmask;
|
||||
sai->sin_family = AF_INET;
|
||||
sai->sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
|
||||
route.rt_flags = RTF_UP | RTF_GATEWAY;
|
||||
route.rt_metric = 0;
|
||||
route.rt_dev = tapname;
|
||||
|
||||
if(ioctl(sockfd, SIOCADDRT, &route) < 0)
|
||||
throw SysError("setting tap0 as default route");
|
||||
|
||||
struct in6_rtmsg route6;
|
||||
memset(&route6, 0, sizeof(route6));
|
||||
if(inet_pton(AF_INET6, "fd00::2", &route6.rtmsg_gateway) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
|
||||
if(ipv6Enabled) {
|
||||
/* Set up a default gateway via slirp4netns */
|
||||
route6.rtmsg_dst = IN6ADDR_ANY_INIT;
|
||||
route6.rtmsg_dst_len = 0;
|
||||
route6.rtmsg_flags = RTF_UP | RTF_GATEWAY;
|
||||
} else {
|
||||
/* Set up a route to slirp4netns, but only for talking to the host
|
||||
* loopback */
|
||||
if(inet_pton(AF_INET6, "fd00::2", &route6.rtmsg_dst) != 1)
|
||||
throw Error("inet_pton failed");
|
||||
route6.rtmsg_dst_len = 128;
|
||||
route6.rtmsg_flags = RTF_UP;
|
||||
}
|
||||
route6.rtmsg_src = IN6ADDR_ANY_INIT;
|
||||
route6.rtmsg_src_len = 0;
|
||||
route6.rtmsg_ifindex = ifindex;
|
||||
route6.rtmsg_metric = 1;
|
||||
|
||||
if(ioctl(sockfd6, SIOCADDRT, &route6) < 0)
|
||||
throw SysError("setting tap0 as default ipv6 route");
|
||||
|
||||
sendFD(send_fd_socket, tapfd);
|
||||
}
|
||||
|
||||
struct ChrootBuildSpawnContext : CloneSpawnContext {
|
||||
bool ipv6Enabled = false;
|
||||
};
|
||||
|
||||
static void setupTapAction(SpawnContext & sctx)
|
||||
{
|
||||
ChrootBuildSpawnContext & ctx = (ChrootBuildSpawnContext &) sctx;
|
||||
setupTap(ctx.setupFD, ctx.ipv6Enabled);
|
||||
}
|
||||
|
||||
|
||||
static void waitForSlirpReadyAction(SpawnContext & sctx)
|
||||
{
|
||||
CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
||||
/* Wait for the parent process to get slirp4netns running */
|
||||
waitForMessage(ctx.setupFD, "1");
|
||||
}
|
||||
|
||||
|
||||
static void enableRouteLocalnetAction(SpawnContext & sctx)
|
||||
{
|
||||
/* Don't treat as invalid packets received with loopback source addresses.
|
||||
This allows for packets to be received from the host loopback using its
|
||||
real address, so for example proxy settings referencing 127.0.0.1 will
|
||||
work both for builtin and regular fixed-output derivations. */
|
||||
|
||||
/* Note: this file is treated relative to the network namespace of the
|
||||
process that opens it. We aren't modifying any host settings here,
|
||||
provided we are in a new network namespace. */
|
||||
Path route_localnet4 = "/proc/sys/net/ipv4/conf/all/route_localnet";
|
||||
/* XXX: no such toggle exists for ipv6 */
|
||||
if(pathExists(route_localnet4))
|
||||
writeFile(route_localnet4, "1");
|
||||
}
|
||||
|
||||
|
||||
static void prepareSlirpChrootAction(SpawnContext & sctx)
|
||||
{
|
||||
CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
||||
auto mounts = tokenizeString<Strings>(readFile("/proc/self/mountinfo", true), "\n");
|
||||
set<string> seen;
|
||||
for(auto & i : mounts) {
|
||||
auto fields = tokenizeString<vector<string> >(i, " ");
|
||||
auto fs = decodeOctalEscaped(fields.at(4));
|
||||
if(seen.find(fs) == seen.end()) {
|
||||
/* slirp4netns only does a single umount of the old root ("/old")
|
||||
after pivot_root. Because of this, if there are multiple
|
||||
mounts stacked on top of each other, only the topmost one (the
|
||||
read-only bind mount) will be unmounted, leaving the real root
|
||||
in place and causing the subsequent rmdir to fail. The best we
|
||||
can do is to make everything immediately underneath "/" be
|
||||
read-only, which we do after mounting every non-/ filesystem
|
||||
read-only. */
|
||||
if(fs == "/") continue;
|
||||
/* Don't mount /etc or any of its subdirectories, we're only interested
|
||||
in mounting network stuff from it */
|
||||
if(fs.compare(0, 4, "/etc") == 0) continue;
|
||||
/* We want /run to be empty */
|
||||
if(fs.compare(0, 4, "/run") == 0) continue;
|
||||
/* Don't mount anything from under our chroot directory */
|
||||
if(fs.compare(0, ctx.chrootRootDir.length(), ctx.chrootRootDir) == 0) continue;
|
||||
struct stat st;
|
||||
if(stat(fs.c_str(), &st) != 0) {
|
||||
if(errno == EACCES) continue; /* Not accessible anyway */
|
||||
else throw SysError(format("stat of `%1%'") % fs);
|
||||
}
|
||||
|
||||
ctx.readOnlyFilesInChroot.insert(fs);
|
||||
ctx.filesInChroot[fs] = fs;
|
||||
seen.insert(fs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Limit /etc to containing just /etc/resolv.conf and /etc/hosts, and
|
||||
read-only at that */
|
||||
Strings etcFiles = { "/etc/resolv.conf", "/etc/hosts" };
|
||||
for(auto & i : etcFiles) {
|
||||
if(pathExists(i)) {
|
||||
ctx.filesInChroot[i] = i;
|
||||
ctx.readOnlyFilesInChroot.insert(i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Make everything immediately under "/" read-only, since we can't make /
|
||||
itself read-only. */
|
||||
DirEntries dirs = readDirectory("/");
|
||||
for (auto & i : dirs) {
|
||||
string fs = "/" + i.name;
|
||||
if(fs == "/etc") continue;
|
||||
if(fs == "/run") continue;
|
||||
ctx.filesInChroot[fs] = fs;
|
||||
ctx.readOnlyFilesInChroot.insert(fs);
|
||||
}
|
||||
|
||||
if(mkdir((ctx.chrootRootDir + "/run").c_str(), 0700) == -1)
|
||||
throw SysError("mkdir /run in chroot");
|
||||
}
|
||||
|
||||
|
||||
static void remapIdsTo0Action(SpawnContext & sctx)
|
||||
{
|
||||
CloneSpawnContext & ctx = (CloneSpawnContext &) sctx;
|
||||
string uid = std::to_string(ctx.setuid ? ctx.user : getuid());
|
||||
string gid = std::to_string(ctx.setgid ? ctx.group : getgid());
|
||||
|
||||
/* If uid != getuid(), then the process that writes to uid_map needs
|
||||
* capabilities in the parent user namespace. Fork a child to stay in
|
||||
* the parent namespace and do the write for us. */
|
||||
unshareAndInitUserns(CLONE_NEWUSER,
|
||||
"0 " + uid + " 1",
|
||||
"0 " + gid + " 1",
|
||||
ctx.lockMountsAllowSetgroups);
|
||||
|
||||
ctx.user = 0;
|
||||
ctx.group = 0;
|
||||
}
|
||||
|
||||
|
||||
/* Spawn 'slirp4netns' in separate namespaces as the given user and group;
|
||||
'tapfd' must correspond to a /dev/net/tun connection. Configure it to
|
||||
write to 'notifyReadyFD' once it's up and running. */
|
||||
static pid_t spawnSlirp4netns(int tapfd, int notifyReadyFD,
|
||||
uid_t slirpUser, gid_t slirpGroup)
|
||||
{
|
||||
Pipe slirpSetupPipe;
|
||||
CloneSpawnContext slirpCtx;
|
||||
AutoCloseFD devNullFd;
|
||||
bool amRoot = geteuid() == 0;
|
||||
bool newUserNS = !amRoot;
|
||||
slirpCtx.phases = getCloneSpawnPhases();
|
||||
slirpCtx.cloneFlags =
|
||||
/* slirp4netns will handle the chroot and pivot_root on its own, but
|
||||
we should ensure that whatever filesystem holds the slirp4netns
|
||||
executable is read-only, since otherwise it might be possible for a
|
||||
compromised slirp4netns to overwrite itself using /proc/self/exe,
|
||||
depending on who owns what. */
|
||||
CLONE_NEWNS |
|
||||
/* ptrace disregards user namespaces when the would-be tracing process
|
||||
and the would-be traced process have the same real, effective, and
|
||||
saved user ids. The only way to protect them is to make it
|
||||
impossible to reference them. */
|
||||
CLONE_NEWPID |
|
||||
/* need this when we're not running as root so that we have the
|
||||
* capabilities to create the other namespaces. */
|
||||
(newUserNS ? CLONE_NEWUSER : 0) |
|
||||
/* For good measure */
|
||||
CLONE_NEWIPC |
|
||||
CLONE_NEWUTS |
|
||||
/* Of course, a new network namespace would defeat the
|
||||
purpose. */
|
||||
SIGCHLD;
|
||||
slirpCtx.program = settings.slirp4netns;
|
||||
slirpCtx.args =
|
||||
{ "slirp4netns", "--netns-type=tapfd",
|
||||
"--enable-sandbox",
|
||||
"--enable-ipv6",
|
||||
"--ready-fd=" + std::to_string(notifyReadyFD) };
|
||||
if(!settings.useHostLoopback)
|
||||
slirpCtx.args.push_back("--disable-host-loopback");
|
||||
slirpCtx.args.push_back(std::to_string(tapfd));
|
||||
slirpCtx.inheritEnv = true;
|
||||
if(newUserNS) {
|
||||
slirpSetupPipe.create();
|
||||
slirpCtx.setupFD = slirpSetupPipe.readSide;
|
||||
slirpCtx.earlyCloseFDs.insert(slirpSetupPipe.writeSide);
|
||||
}
|
||||
slirpCtx.closeMostFDs = true;
|
||||
slirpCtx.preserveFDs.insert(notifyReadyFD);
|
||||
slirpCtx.preserveFDs.insert(tapfd);
|
||||
slirpCtx.setStdin = true;
|
||||
slirpCtx.stdinFile = "/dev/null";
|
||||
slirpCtx.setsid = true;
|
||||
slirpCtx.dropAmbientCapabilities = true;
|
||||
slirpCtx.doChroot = true;
|
||||
slirpCtx.mountTmpfsOnChroot = true;
|
||||
slirpCtx.chrootRootDir = getEnv("TMPDIR", "/tmp");
|
||||
slirpCtx.lockMounts = true;
|
||||
slirpCtx.lockMountsMapAll = true; /* So that later setuid will work */
|
||||
slirpCtx.lockMountsAllowSetgroups = amRoot;
|
||||
slirpCtx.mountProc = true;
|
||||
slirpCtx.setuid = true;
|
||||
slirpCtx.user = slirpUser;
|
||||
slirpCtx.setgid = true;
|
||||
slirpCtx.group = slirpGroup;
|
||||
/* Dropping supplementary groups requires capabilities in current user
|
||||
* namespace */
|
||||
if(amRoot) {
|
||||
slirpCtx.supplementaryGroups = {};
|
||||
slirpCtx.setSupplementaryGroups = true;
|
||||
}
|
||||
slirpCtx.seccompFilter = slirpSeccompFilter();
|
||||
slirpCtx.addSeccompFilter = true;
|
||||
|
||||
/* Silence slirp4netns output unless requested */
|
||||
if(verbosity <= lvlInfo) {
|
||||
devNullFd = open("/dev/null", O_WRONLY);
|
||||
if(devNullFd == -1)
|
||||
throw SysError("cannot open `/dev/null'");
|
||||
slirpCtx.logFD = devNullFd;
|
||||
}
|
||||
|
||||
addPhaseAfter(slirpCtx.phases,
|
||||
"makeChrootSeparateFilesystem",
|
||||
"prepareSlirpChroot",
|
||||
prepareSlirpChrootAction);
|
||||
|
||||
/* slirp behaves differently when uid != 0 */
|
||||
addPhaseAfter(slirpCtx.phases,
|
||||
"lockMounts",
|
||||
"remapIdsTo0",
|
||||
remapIdsTo0Action);
|
||||
|
||||
#if 0 /* For debugging networking issues */
|
||||
slirpCtx.env["SLIRP_DEBUG"] = "call,misc,error,tftp,verbose_call";
|
||||
slirpCtx.env["G_MESSAGES_DEBUG"] = "all";
|
||||
#endif
|
||||
|
||||
pid_t slirpPid = cloneChild(slirpCtx);
|
||||
|
||||
if(newUserNS) {
|
||||
slirpSetupPipe.readSide.close();
|
||||
initializeUserNamespace(slirpPid, getuid(), getgid(), getuid(), getgid());
|
||||
writeFull(slirpSetupPipe.writeSide, (unsigned char*)"go\n", 3);
|
||||
}
|
||||
return slirpPid;
|
||||
}
|
||||
|
||||
static void clearRootWritePermsAction(SpawnContext & sctx)
|
||||
{
|
||||
if(chmod("/", 0555) == -1)
|
||||
throw SysError("changing mode of chroot root directory");
|
||||
}
|
||||
|
||||
|
||||
/* Note: linux-only */
|
||||
bool haveGlobalIPv6Address()
|
||||
{
|
||||
if(!pathExists("/proc/net/if_inet6")) return false;
|
||||
|
||||
auto addresses = tokenizeString<Strings>(readFile("/proc/net/if_inet6", true), "\n");
|
||||
for(auto & i : addresses) {
|
||||
auto fields = tokenizeString<vector<string> >(i, " ");
|
||||
auto scopeHex = fields.at(3);
|
||||
/* 0x0 means "Global scope" */
|
||||
if(scopeHex == "00" || scopeHex == "40") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif /* CHROOT_ENABLED */
|
||||
|
||||
/* Return true if the operating system kernel part of SYSTEM1 and SYSTEM2 (the
|
||||
|
@ -1731,10 +2165,10 @@ void DerivationGoal::startBuilder()
|
|||
f.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
|
||||
startNest(nest, lvlInfo, f % showPaths(missingPaths) % curRound % nrRounds);
|
||||
|
||||
/* A CloneSpawnContext reference can be passed to procedures expecting a
|
||||
SpawnContext reference */
|
||||
/* A ChrootBuildSpawnContext reference can be passed to procedures
|
||||
expecting a SpawnContext reference */
|
||||
#if CHROOT_ENABLED
|
||||
CloneSpawnContext ctx;
|
||||
ChrootBuildSpawnContext ctx;
|
||||
#else
|
||||
SpawnContext ctx;
|
||||
#endif
|
||||
|
@ -1945,6 +2379,10 @@ void DerivationGoal::startBuilder()
|
|||
ctx.supplementaryGroups = buildUser.getSupplementaryGIDs();
|
||||
}
|
||||
|
||||
#if CHROOT_ENABLED
|
||||
bool useSlirp4netns = false;
|
||||
#endif
|
||||
|
||||
if (useChroot) {
|
||||
#if CHROOT_ENABLED
|
||||
ctx.phases = getCloneSpawnPhases();
|
||||
|
@ -1960,14 +2398,26 @@ void DerivationGoal::startBuilder()
|
|||
/* Clean up the chroot directory automatically. */
|
||||
autoDelChroot = std::shared_ptr<AutoDelete>(new AutoDelete(chrootRootTop));
|
||||
|
||||
if(fixedOutput) {
|
||||
if(findProgram(settings.slirp4netns) == "")
|
||||
printMsg(lvlError, format("`%1%' can't be found in PATH, network access disabled") % settings.slirp4netns);
|
||||
else {
|
||||
if(!pathExists("/dev/net/tun"))
|
||||
printMsg(lvlError, "`/dev/net/tun' is missing, network access disabled");
|
||||
else {
|
||||
useSlirp4netns = true;
|
||||
ctx.ipv6Enabled = haveGlobalIPv6Address();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.doChroot = true;
|
||||
ctx.chrootRootDir = chrootRootDir;
|
||||
ctx.cloneFlags = CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD;
|
||||
ctx.cloneFlags = CLONE_NEWNS | CLONE_NEWNET | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD;
|
||||
|
||||
if(!fixedOutput) {
|
||||
if(!fixedOutput || /* redundant but shows the cases clearly */
|
||||
(fixedOutput && !settings.useHostLoopback))
|
||||
ctx.initLoopback = true;
|
||||
ctx.cloneFlags |= CLONE_NEWNET;
|
||||
}
|
||||
|
||||
if(!buildUser.enabled())
|
||||
ctx.cloneFlags |= CLONE_NEWUSER;
|
||||
|
@ -2014,18 +2464,42 @@ void DerivationGoal::startBuilder()
|
|||
if (fixedOutput) {
|
||||
/* Fixed-output derivations typically need to access the network,
|
||||
so give them access to /etc/resolv.conf and so on. */
|
||||
auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
|
||||
"/etc/services", "/etc/hosts" };
|
||||
for (auto & file: files) {
|
||||
std::vector<Path> files = { "/etc/services", "/etc/nsswitch.conf" };
|
||||
if (useSlirp4netns) {
|
||||
if (settings.useHostLoopback) {
|
||||
string hosts;
|
||||
if(pathExists("/etc/hosts")) {
|
||||
hosts = readFile("/etc/hosts");
|
||||
hosts = std::regex_replace(hosts, std::regex("127\\.0\\.0\\.1"), "10.0.2.2");
|
||||
hosts = std::regex_replace(hosts, std::regex("::1"), "fd00::2");
|
||||
} else {
|
||||
hosts =
|
||||
"10.0.2.2 localhost\n"
|
||||
"fd00::2 localhost\n";
|
||||
}
|
||||
writeFile(chrootRootDir + "/etc/hosts", hosts);
|
||||
}
|
||||
else {
|
||||
files.push_back("/etc/hosts");
|
||||
}
|
||||
writeFile(chrootRootDir + "/etc/resolv.conf", "nameserver 10.0.2.3");
|
||||
}
|
||||
else {
|
||||
files.push_back("/etc/hosts");
|
||||
files.push_back("/etc/resolv.conf");
|
||||
}
|
||||
for (auto & file : files) {
|
||||
if (pathExists(file)) {
|
||||
ctx.filesInChroot[file] = file;
|
||||
ctx.readOnlyFilesInChroot.insert(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Create /etc/hosts with localhost entry. */
|
||||
writeFile(chrootRootDir + "/etc/hosts", "127.0.0.1 localhost\n");
|
||||
}
|
||||
else
|
||||
/* Create /etc/hosts with localhost entry. */
|
||||
writeFile(chrootRootDir + "/etc/hosts",
|
||||
"127.0.0.1 localhost\n"
|
||||
"::1 localhost\n");
|
||||
|
||||
/* Bind-mount a user-configurable set of directories from the
|
||||
host file system. */
|
||||
|
@ -2175,7 +2649,9 @@ void DerivationGoal::startBuilder()
|
|||
|
||||
- The private network namespace ensures that the builder cannot
|
||||
talk to the outside world (or vice versa). It only has a
|
||||
private loopback interface.
|
||||
private loopback interface. As an exception, fixed-output
|
||||
derivations may talk to the outside world through slirp4netns, but
|
||||
still in a separate network namespace.
|
||||
|
||||
- The IPC namespace prevents the builder from communicating
|
||||
with outside processes using SysV IPC mechanisms (shared
|
||||
|
@ -2191,7 +2667,7 @@ void DerivationGoal::startBuilder()
|
|||
AutoCloseFD parentSetupSocket;
|
||||
AutoCloseFD childSetupSocket;
|
||||
|
||||
if(((ctx.cloneFlags & CLONE_NEWUSER) != 0)) {
|
||||
if(((ctx.cloneFlags & CLONE_NEWUSER) != 0) || useSlirp4netns) {
|
||||
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fds))
|
||||
throw SysError("creating setup socket");
|
||||
parentSetupSocket = fds[0];
|
||||
|
@ -2202,6 +2678,15 @@ void DerivationGoal::startBuilder()
|
|||
ctx.setupFD = childSetupSocket;
|
||||
}
|
||||
|
||||
if(useSlirp4netns) {
|
||||
addPhaseAfter(ctx.phases, "initLoopback", "setupTap", setupTapAction);
|
||||
addPhaseAfter(ctx.phases, "setupTap", "waitForSlirpReady",
|
||||
waitForSlirpReadyAction);
|
||||
if(settings.useHostLoopback)
|
||||
addPhaseAfter(ctx.phases, "waitForSlirpReady", "enableRouteLocalnet",
|
||||
enableRouteLocalnetAction);
|
||||
}
|
||||
|
||||
pid = cloneChild(ctx);
|
||||
|
||||
if(childSetupSocket >= 0) childSetupSocket.close();
|
||||
|
@ -2211,6 +2696,34 @@ void DerivationGoal::startBuilder()
|
|||
initializeUserNamespace(pid);
|
||||
writeFull(parentSetupSocket, (unsigned char*)"go\n", 3);
|
||||
}
|
||||
|
||||
try {
|
||||
if(useSlirp4netns) {
|
||||
AutoCloseFD tapfd = receiveFD(parentSetupSocket);
|
||||
/* Start 'slirp4netns' to provide networking in the child process;
|
||||
running the builder in the global network namespace would give
|
||||
it access to the global namespace of abstract sockets, which
|
||||
could be used to grant write access to the store to an external
|
||||
process. */
|
||||
slirp = spawnSlirp4netns(
|
||||
tapfd,
|
||||
parentSetupSocket,
|
||||
/* Do whatever we can to run slirp4netns as some user
|
||||
other than root - run it as the build user if
|
||||
necessary */
|
||||
buildUser.enabled() ? buildUser.getUID() : getuid(),
|
||||
buildUser.enabled() ? buildUser.getGID() : getgid());
|
||||
}
|
||||
} catch(std::exception & e) {
|
||||
if(slirp != -1) {
|
||||
slirp.kill(true);
|
||||
}
|
||||
if(pid != -1) {
|
||||
pid.kill(true);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
|
|
|
@ -56,6 +56,8 @@ Settings::Settings()
|
|||
envKeepDerivations = false;
|
||||
lockCPU = getEnv("NIX_AFFINITY_HACK", "1") == "1";
|
||||
showTrace = false;
|
||||
useHostLoopback = true;
|
||||
slirp4netns = SLIRP4NETNS;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -206,6 +206,15 @@ struct Settings {
|
|||
/* Whether to show a stack trace if Nix evaluation fails. */
|
||||
bool showTrace;
|
||||
|
||||
/* Whether fixed-output chroot builds should be able to use the host
|
||||
loopback, for example to access a socks proxy. Note that while using
|
||||
"localhost" and 127.0.0.1 to access the host loopback will work, using
|
||||
::1 will not, due to a limitation in Linux. */
|
||||
bool useHostLoopback;
|
||||
|
||||
/* The filename to use for executing slirp4netns when it is needed. */
|
||||
Path slirp4netns;
|
||||
|
||||
private:
|
||||
SettingsMap settings, overrides;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue