mirror of
https://github.com/NixOS/nix.git
synced 2025-11-17 07:52:43 +01:00
pasta: wip
TODO: add original authors as commit authors
This commit is contained in:
parent
82315c3807
commit
b2c35b45d2
16 changed files with 653 additions and 3 deletions
|
|
@ -27,3 +27,10 @@ option(
|
||||||
value : false,
|
value : false,
|
||||||
description : 'Build benchmarks (requires gbenchmark)',
|
description : 'Build benchmarks (requires gbenchmark)',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
option(
|
||||||
|
'pasta-path',
|
||||||
|
type : 'string',
|
||||||
|
value : 'pasta',
|
||||||
|
description : 'Path to the location of pasta (provided by passt)',
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,10 @@ Settings::Settings()
|
||||||
sandboxPaths = {{"/bin/sh", {.source = SANDBOX_SHELL}}};
|
sandboxPaths = {{"/bin/sh", {.source = SANDBOX_SHELL}}};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PASTA_PATH)
|
||||||
|
pastaPath.setDefault(PASTA_PATH);
|
||||||
|
#endif
|
||||||
|
|
||||||
/* chroot-like behavior from Apple's sandbox */
|
/* chroot-like behavior from Apple's sandbox */
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
for (PathView p : {
|
for (PathView p : {
|
||||||
|
|
|
||||||
|
|
@ -1372,6 +1372,21 @@ public:
|
||||||
Default is 0, which disables the warning.
|
Default is 0, which disables the warning.
|
||||||
Set it to 1 to warn on all paths.
|
Set it to 1 to warn on all paths.
|
||||||
)"};
|
)"};
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
Setting<Path> pastaPath{
|
||||||
|
this,
|
||||||
|
"",
|
||||||
|
"pasta-path",
|
||||||
|
R"(
|
||||||
|
If set to an absolute path, enables fully sandboxing fixed-output
|
||||||
|
derivations, by using `pasta` to pass network traffic between the
|
||||||
|
private network namespace. This allows for greater levels of isolation
|
||||||
|
of builds to the host.
|
||||||
|
)",
|
||||||
|
{},
|
||||||
|
false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: don't use a global variable.
|
// FIXME: don't use a global variable.
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,15 @@ configdata_priv.set_quoted(
|
||||||
: 'lsof',
|
: 'lsof',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Find pasta for network isolation
|
||||||
|
if host_machine.system() == 'linux'
|
||||||
|
pasta_path = get_option('pasta-path')
|
||||||
|
pasta = find_program(pasta_path, required : false, native : false)
|
||||||
|
if pasta.found()
|
||||||
|
configdata_priv.set_quoted('PASTA_PATH', pasta.full_path())
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
config_priv_h = configure_file(
|
config_priv_h = configure_file(
|
||||||
configuration : configdata_priv,
|
configuration : configdata_priv,
|
||||||
output : 'store-config-private.hh',
|
output : 'store-config-private.hh',
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,10 @@ option(
|
||||||
value : '/nix/var/log/nix',
|
value : '/nix/var/log/nix',
|
||||||
description : 'path to store logs in for Nix',
|
description : 'path to store logs in for Nix',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
option(
|
||||||
|
'pasta-path',
|
||||||
|
type : 'string',
|
||||||
|
value : 'pasta',
|
||||||
|
description : 'Path to the location of pasta (provided by passt)',
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
sqlite,
|
sqlite,
|
||||||
|
|
||||||
busybox-sandbox-shell ? null,
|
busybox-sandbox-shell ? null,
|
||||||
|
passt ? null,
|
||||||
|
|
||||||
# Configuration Options
|
# Configuration Options
|
||||||
|
|
||||||
|
|
@ -77,6 +78,9 @@ mkMesonLibrary (finalAttrs: {
|
||||||
]
|
]
|
||||||
++ lib.optionals stdenv.hostPlatform.isLinux [
|
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
(lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox")
|
(lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox")
|
||||||
|
]
|
||||||
|
++ [
|
||||||
|
(lib.mesonOption "pasta-path" "${passt}/bin/pasta")
|
||||||
];
|
];
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
|
|
||||||
|
# include "nix/store/globals.hh"
|
||||||
# include "nix/store/personality.hh"
|
# include "nix/store/personality.hh"
|
||||||
# include "nix/util/cgroup.hh"
|
# include "nix/util/cgroup.hh"
|
||||||
|
# include "nix/util/file-system.hh"
|
||||||
# include "nix/util/linux-namespaces.hh"
|
# include "nix/util/linux-namespaces.hh"
|
||||||
|
# include "nix/util/strings.hh"
|
||||||
# include "linux/fchmodat2-compat.hh"
|
# include "linux/fchmodat2-compat.hh"
|
||||||
|
# include "pasta.hh"
|
||||||
|
|
||||||
# include <sys/ioctl.h>
|
# include <sys/ioctl.h>
|
||||||
# include <net/if.h>
|
# include <net/if.h>
|
||||||
|
|
@ -190,6 +194,16 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
*/
|
*/
|
||||||
std::optional<Path> cgroup;
|
std::optional<Path> cgroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process ID of pasta, if we're using it for network isolation.
|
||||||
|
*/
|
||||||
|
Pid pastaPid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether pasta was started for this build.
|
||||||
|
*/
|
||||||
|
bool runPasta = false;
|
||||||
|
|
||||||
ChrootLinuxDerivationBuilder(
|
ChrootLinuxDerivationBuilder(
|
||||||
LocalStore & store, std::unique_ptr<DerivationBuilderCallbacks> miscMethods, DerivationBuilderParams params)
|
LocalStore & store, std::unique_ptr<DerivationBuilderCallbacks> miscMethods, DerivationBuilderParams params)
|
||||||
: DerivationBuilderImpl{store, std::move(miscMethods), std::move(params)}
|
: DerivationBuilderImpl{store, std::move(miscMethods), std::move(params)}
|
||||||
|
|
@ -198,6 +212,18 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~ChrootLinuxDerivationBuilder()
|
||||||
|
{
|
||||||
|
// pasta being left around mostly happens when builds are aborted
|
||||||
|
if (pastaPid) {
|
||||||
|
try {
|
||||||
|
pasta::killPasta(pastaPid);
|
||||||
|
} catch (Error & e) {
|
||||||
|
// Ignore errors during cleanup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uid_t sandboxUid()
|
uid_t sandboxUid()
|
||||||
{
|
{
|
||||||
return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID();
|
return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 1000 : 0) : buildUser->getUID();
|
||||||
|
|
@ -433,6 +459,19 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
/* Signal the builder that we've updated its user namespace. */
|
/* Signal the builder that we've updated its user namespace. */
|
||||||
writeFull(userNamespaceSync.writeSide.get(), "1\n");
|
writeFull(userNamespaceSync.writeSide.get(), "1\n");
|
||||||
userNamespaceSyncDone = true;
|
userNamespaceSyncDone = true;
|
||||||
|
|
||||||
|
/* Set up pasta for network isolation if enabled and this is a fixed-output derivation */
|
||||||
|
bool privateNetwork = derivationType.isSandboxed();
|
||||||
|
runPasta = !privateNetwork && settings.pastaPath != "" && pathExists("/dev/net/tun");
|
||||||
|
|
||||||
|
if (runPasta) {
|
||||||
|
pastaPid = pasta::setupPasta(
|
||||||
|
settings.pastaPath,
|
||||||
|
pid,
|
||||||
|
buildUser ? std::optional(buildUser->getUID()) : std::nullopt,
|
||||||
|
buildUser ? std::optional(buildUser->getGID()) : std::nullopt,
|
||||||
|
usingUserNamespace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterChroot() override
|
void enterChroot() override
|
||||||
|
|
@ -444,6 +483,10 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
|
|
||||||
userNamespaceSync.readSide = -1;
|
userNamespaceSync.readSide = -1;
|
||||||
|
|
||||||
|
if (runPasta) {
|
||||||
|
pasta::waitForPastaInterface();
|
||||||
|
}
|
||||||
|
|
||||||
if (derivationType.isSandboxed()) {
|
if (derivationType.isSandboxed()) {
|
||||||
|
|
||||||
/* Initialise the loopback interface. */
|
/* Initialise the loopback interface. */
|
||||||
|
|
@ -532,8 +575,17 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
happens when testing Nix building fixed-output derivations
|
happens when testing Nix building fixed-output derivations
|
||||||
within a pure derivation. */
|
within a pure derivation. */
|
||||||
for (auto & path : {"/etc/resolv.conf", "/etc/services", "/etc/hosts"})
|
for (auto & path : {"/etc/resolv.conf", "/etc/services", "/etc/hosts"})
|
||||||
if (pathExists(path))
|
if (pathExists(path)) {
|
||||||
|
// Special handling for resolv.conf when using pasta
|
||||||
|
if (runPasta && std::string(path) == "/etc/resolv.conf") {
|
||||||
|
// We'll write a modified version instead of bind-mounting
|
||||||
|
auto resolvConf = readFile(std::string(path));
|
||||||
|
auto modifiedResolvConf = pasta::rewriteResolvConf(resolvConf);
|
||||||
|
writeFile(chrootRootDir + "/etc/resolv.conf", modifiedResolvConf);
|
||||||
|
} else {
|
||||||
ss.push_back(path);
|
ss.push_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.caFile != "") {
|
if (settings.caFile != "") {
|
||||||
Path caFile = settings.caFile;
|
Path caFile = settings.caFile;
|
||||||
|
|
@ -691,6 +743,10 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pastaPid) {
|
||||||
|
pasta::killPasta(pastaPid);
|
||||||
|
}
|
||||||
|
|
||||||
DerivationBuilderImpl::killSandbox(getStats);
|
DerivationBuilderImpl::killSandbox(getStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
161
src/libstore/unix/build/pasta.cc
Normal file
161
src/libstore/unix/build/pasta.cc
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
# include "pasta.hh"
|
||||||
|
# include "nix/util/error.hh"
|
||||||
|
# include "nix/util/file-system.hh"
|
||||||
|
# include "nix/util/processes.hh"
|
||||||
|
# include "nix/util/strings.hh"
|
||||||
|
# include "nix/util/util.hh"
|
||||||
|
|
||||||
|
# include "nix/util/fmt.hh"
|
||||||
|
# include <regex>
|
||||||
|
# include <sys/socket.h>
|
||||||
|
# include <net/if.h>
|
||||||
|
# include <sys/ioctl.h>
|
||||||
|
# include <unistd.h>
|
||||||
|
# include <fcntl.h>
|
||||||
|
# include <linux/capability.h>
|
||||||
|
# include <netinet/in.h>
|
||||||
|
# include <sys/types.h>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
namespace pasta {
|
||||||
|
|
||||||
|
Pid setupPasta(
|
||||||
|
const Path & pastaPath,
|
||||||
|
pid_t pid,
|
||||||
|
std::optional<uid_t> buildUserId,
|
||||||
|
std::optional<gid_t> buildGroupId,
|
||||||
|
bool usingUserNamespace)
|
||||||
|
{
|
||||||
|
// Bring up pasta for handling FOD networking. We don't let it daemonize
|
||||||
|
// itself for process management reasons and kill it manually when done.
|
||||||
|
|
||||||
|
Strings args = {
|
||||||
|
"--quiet", "--foreground", "--config-net", "--gateway", PASTA_HOST_IPV4, "--address",
|
||||||
|
PASTA_CHILD_IPV4, "--netmask", PASTA_IPV4_NETMASK, "--dns-forward", PASTA_HOST_IPV4, "--gateway",
|
||||||
|
PASTA_HOST_IPV6, "--address", PASTA_CHILD_IPV6, "--dns-forward", PASTA_HOST_IPV6, "--ns-ifname",
|
||||||
|
PASTA_NS_IFNAME, "--no-netns-quit", "--netns", "/proc/self/fd/0",
|
||||||
|
};
|
||||||
|
|
||||||
|
AutoCloseFD netns(open(fmt("/proc/%i/ns/net", pid).c_str(), O_RDONLY | O_CLOEXEC));
|
||||||
|
if (!netns) {
|
||||||
|
throw SysError("failed to open netns");
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoCloseFD userns;
|
||||||
|
if (usingUserNamespace) {
|
||||||
|
userns = AutoCloseFD(open(fmt("/proc/%i/ns/user", pid).c_str(), O_RDONLY | O_CLOEXEC));
|
||||||
|
if (!userns) {
|
||||||
|
throw SysError("failed to open userns");
|
||||||
|
}
|
||||||
|
args.push_back("--userns");
|
||||||
|
args.push_back("/proc/self/fd/1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME ideally we want a notification when pasta exits, but we cannot do
|
||||||
|
// this at present. Without such support we need to busy-wait for pasta to
|
||||||
|
// set up the namespace completely and time out after a while for the case
|
||||||
|
// of pasta launch failures. Pasta logs go to syslog only for now as well.
|
||||||
|
|
||||||
|
// Use startProcess to launch pasta with proper file descriptor setup
|
||||||
|
return startProcess([&]() {
|
||||||
|
// Set up file descriptor redirections
|
||||||
|
if (dup2(netns.get(), 0) == -1)
|
||||||
|
throw SysError("cannot redirect netns fd to stdin");
|
||||||
|
|
||||||
|
if (userns) {
|
||||||
|
if (dup2(userns.get(), 1) == -1)
|
||||||
|
throw SysError("cannot redirect userns fd to stdout");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user/group if specified
|
||||||
|
if (buildGroupId && setgid(*buildGroupId) == -1)
|
||||||
|
throw SysError("setgid failed");
|
||||||
|
if (buildUserId && setuid(*buildUserId) == -1)
|
||||||
|
throw SysError("setuid failed");
|
||||||
|
|
||||||
|
// Execute pasta
|
||||||
|
Strings allArgs = {pastaPath};
|
||||||
|
allArgs.insert(allArgs.end(), args.begin(), args.end());
|
||||||
|
|
||||||
|
execvp(pastaPath.c_str(), stringsToCharPtrs(allArgs).data());
|
||||||
|
throw SysError("executing pasta");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void waitForPastaInterface()
|
||||||
|
{
|
||||||
|
// Wait for the pasta interface to appear. pasta can't signal us when
|
||||||
|
// it's done setting up the namespace, so we have to wait for a while
|
||||||
|
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||||
|
if (!fd)
|
||||||
|
throw SysError("cannot open IP socket");
|
||||||
|
|
||||||
|
struct ifreq ifr;
|
||||||
|
strcpy(ifr.ifr_name, PASTA_NS_IFNAME);
|
||||||
|
|
||||||
|
// Wait two minutes for the interface to appear. If it does not do so
|
||||||
|
// we are either grossly overloaded, or pasta startup failed somehow.
|
||||||
|
static constexpr int SINGLE_WAIT_US = 1000;
|
||||||
|
static constexpr int TOTAL_WAIT_US = 120'000'000;
|
||||||
|
|
||||||
|
for (unsigned tries = 0;; tries++) {
|
||||||
|
if (tries > TOTAL_WAIT_US / SINGLE_WAIT_US) {
|
||||||
|
throw Error(
|
||||||
|
"sandbox network setup timed out, please check daemon logs for "
|
||||||
|
"possible error output.");
|
||||||
|
} else if (ioctl(fd.get(), SIOCGIFFLAGS, &ifr) == 0) {
|
||||||
|
if ((ifr.ifr_ifru.ifru_flags & IFF_UP) != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (errno == ENODEV) {
|
||||||
|
usleep(SINGLE_WAIT_US);
|
||||||
|
} else {
|
||||||
|
throw SysError("cannot get loopback interface flags");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string rewriteResolvConf(const std::string & fromHost)
|
||||||
|
{
|
||||||
|
static constexpr auto flags = std::regex::ECMAScript | std::regex::multiline;
|
||||||
|
static std::regex lineRegex("^nameserver\\s.*$", flags);
|
||||||
|
static std::regex v4Regex("^nameserver\\s+\\d{1,3}\\.", flags);
|
||||||
|
static std::regex v6Regex("^nameserver.*:", flags);
|
||||||
|
|
||||||
|
std::string nsInSandbox = "\n";
|
||||||
|
if (std::regex_search(fromHost, v4Regex)) {
|
||||||
|
nsInSandbox += nix::fmt("nameserver %s\n", PASTA_HOST_IPV4);
|
||||||
|
}
|
||||||
|
if (std::regex_search(fromHost, v6Regex)) {
|
||||||
|
nsInSandbox += nix::fmt("nameserver %s\n", PASTA_HOST_IPV6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::regex_replace(fromHost, lineRegex, "") + nsInSandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
void killPasta(Pid & pastaPid)
|
||||||
|
{
|
||||||
|
// FIXME we really want to send SIGTERM instead and wait for pasta to exit,
|
||||||
|
// but we do not have the infra for that right now. We send SIGKILL instead
|
||||||
|
// and treat exiting with that as a successful exit code until such a time.
|
||||||
|
// This is not likely to cause problems since pasta runs as the build user,
|
||||||
|
// but not inside the build sandbox. If it's killed it's either due to some
|
||||||
|
// external influence (in which case the sandboxed child will probably fail
|
||||||
|
// due to network errors, if it used the network at all) or some bug in nix
|
||||||
|
if (auto status = pastaPid.kill(); !WIFSIGNALED(status) || WTERMSIG(status) != SIGKILL) {
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
throw Error("pasta killed by signal %i", WTERMSIG(status));
|
||||||
|
} else if (WIFEXITED(status)) {
|
||||||
|
throw Error("pasta exited with code %i", WEXITSTATUS(status));
|
||||||
|
} else {
|
||||||
|
throw Error("pasta exited with status %i", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pasta
|
||||||
|
} // namespace nix
|
||||||
|
|
||||||
|
#endif // __linux__
|
||||||
78
src/libstore/unix/build/pasta.hh
Normal file
78
src/libstore/unix/build/pasta.hh
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
|
||||||
|
# include "nix/util/processes.hh"
|
||||||
|
# include "nix/util/file-descriptor.hh"
|
||||||
|
# include <string>
|
||||||
|
# include <set>
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pasta (Plug A Simple Socket Transport) network isolation constants and utilities.
|
||||||
|
*
|
||||||
|
* Pasta provides network isolation for fixed-output derivations by creating
|
||||||
|
* a Layer-2 to Layer-4 translation without requiring special privileges.
|
||||||
|
*/
|
||||||
|
namespace pasta {
|
||||||
|
|
||||||
|
// Network configuration constants
|
||||||
|
// NOTE: These are all C strings because macOS doesn't have constexpr std::string
|
||||||
|
// constructors, and std::string_view is a pain to turn into std::strings again.
|
||||||
|
static constexpr const char * PASTA_NS_IFNAME = "eth0";
|
||||||
|
static constexpr const char * PASTA_HOST_IPV4 = "169.254.1.1";
|
||||||
|
static constexpr const char * PASTA_CHILD_IPV4 = "169.254.1.2";
|
||||||
|
static constexpr const char * PASTA_IPV4_NETMASK = "16";
|
||||||
|
// Randomly chosen 6to4 prefix, mapping the same ipv4ll as above.
|
||||||
|
// Even if this id is used on the daemon host there should not be
|
||||||
|
// any collisions since ipv4ll should never be addressed by ipv6.
|
||||||
|
static constexpr const char * PASTA_HOST_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0101";
|
||||||
|
static constexpr const char * PASTA_CHILD_IPV6 = "64:ff9b:1:4b8e:472e:a5c8:a9fe:0102";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch pasta for network isolation of a build process.
|
||||||
|
*
|
||||||
|
* @param pastaPath Path to the pasta executable
|
||||||
|
* @param pid Process ID of the build process to isolate
|
||||||
|
* @param buildUserId Optional UID to run pasta as
|
||||||
|
* @param buildGroupId Optional GID to run pasta as
|
||||||
|
* @param usingUserNamespace Whether the build is using a user namespace
|
||||||
|
* @return Process ID of the pasta process
|
||||||
|
*/
|
||||||
|
Pid setupPasta(
|
||||||
|
const Path & pastaPath,
|
||||||
|
pid_t pid,
|
||||||
|
std::optional<uid_t> buildUserId,
|
||||||
|
std::optional<gid_t> buildGroupId,
|
||||||
|
bool usingUserNamespace);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for pasta to set up the network interface.
|
||||||
|
*
|
||||||
|
* @throws Error if the interface doesn't appear within the timeout period
|
||||||
|
*/
|
||||||
|
void waitForPastaInterface();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewrite /etc/resolv.conf for pasta-isolated builds.
|
||||||
|
*
|
||||||
|
* Replaces nameserver entries with pasta's DNS forwarders.
|
||||||
|
*
|
||||||
|
* @param fromHost The original resolv.conf content from the host
|
||||||
|
* @return Modified resolv.conf content for the sandboxed build
|
||||||
|
*/
|
||||||
|
std::string rewriteResolvConf(const std::string & fromHost);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill the pasta process.
|
||||||
|
*
|
||||||
|
* @param pastaPid The pasta process to kill
|
||||||
|
* @throws Error if pasta exits with unexpected status
|
||||||
|
*/
|
||||||
|
void killPasta(Pid & pastaPid);
|
||||||
|
|
||||||
|
} // namespace pasta
|
||||||
|
} // namespace nix
|
||||||
|
|
||||||
|
#endif // __linux__
|
||||||
|
|
@ -6,4 +6,16 @@ sources += files(
|
||||||
'user-lock.cc',
|
'user-lock.cc',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if host_machine.system() == 'linux'
|
||||||
|
sources += files(
|
||||||
|
'build/pasta.cc',
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
|
if host_machine.system() == 'darwin'
|
||||||
|
sources += files(
|
||||||
|
'build/darwin-derivation-builder.cc',
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
||||||
subdir('include/nix/store')
|
subdir('include/nix/store')
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ coreutils=@coreutils@
|
||||||
|
|
||||||
dot=@dot@
|
dot=@dot@
|
||||||
busybox="@sandbox_shell@"
|
busybox="@sandbox_shell@"
|
||||||
|
pasta_path=@pasta_path@
|
||||||
|
|
||||||
version=@PACKAGE_VERSION@
|
version=@PACKAGE_VERSION@
|
||||||
system=@system@
|
system=@system@
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ busybox = find_program('busybox', native : true, required : false)
|
||||||
# guaranteed to exist either.
|
# guaranteed to exist either.
|
||||||
coreutils = find_program('ls', native : true)
|
coreutils = find_program('ls', native : true)
|
||||||
dot = find_program('dot', native : true, required : false)
|
dot = find_program('dot', native : true, required : false)
|
||||||
|
pasta_path = find_program('pasta', native : false, required : false)
|
||||||
|
|
||||||
nix_bin_dir = fs.parent(nix.full_path())
|
nix_bin_dir = fs.parent(nix.full_path())
|
||||||
|
|
||||||
|
|
@ -35,6 +36,15 @@ test_confdata = {
|
||||||
'PACKAGE_VERSION' : meson.project_version(),
|
'PACKAGE_VERSION' : meson.project_version(),
|
||||||
'system' : nix_system_cpu + '-' + host_machine.system(),
|
'system' : nix_system_cpu + '-' + host_machine.system(),
|
||||||
}
|
}
|
||||||
|
if pasta_path.found()
|
||||||
|
test_confdata += {
|
||||||
|
'pasta_path': pasta_path.full_path(),
|
||||||
|
}
|
||||||
|
else
|
||||||
|
test_confdata += {
|
||||||
|
'pasta_path': '',
|
||||||
|
}
|
||||||
|
endif
|
||||||
|
|
||||||
# Just configures `common/vars-and-functions.sh.in`.
|
# Just configures `common/vars-and-functions.sh.in`.
|
||||||
# Done as a subdir() so Meson places it under `common` in the build directory as well.
|
# Done as a subdir() so Meson places it under `common` in the build directory as well.
|
||||||
|
|
@ -127,6 +137,7 @@ suites = [
|
||||||
'misc.sh',
|
'misc.sh',
|
||||||
'dump-db.sh',
|
'dump-db.sh',
|
||||||
'linux-sandbox.sh',
|
'linux-sandbox.sh',
|
||||||
|
'pasta-network.sh',
|
||||||
'supplementary-groups.sh',
|
'supplementary-groups.sh',
|
||||||
'build-dry.sh',
|
'build-dry.sh',
|
||||||
'structured-attrs.sh',
|
'structured-attrs.sh',
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
mercurial,
|
mercurial,
|
||||||
util-linux,
|
util-linux,
|
||||||
unixtools,
|
unixtools,
|
||||||
|
passt,
|
||||||
|
|
||||||
nix-store,
|
nix-store,
|
||||||
nix-expr,
|
nix-expr,
|
||||||
|
|
@ -56,6 +57,7 @@ mkMesonDerivation (
|
||||||
git
|
git
|
||||||
mercurial
|
mercurial
|
||||||
unixtools.script
|
unixtools.script
|
||||||
|
passt
|
||||||
]
|
]
|
||||||
++ lib.optionals stdenv.hostPlatform.isLinux [
|
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
# For various sandboxing tests that needs a statically-linked shell,
|
# For various sandboxing tests that needs a statically-linked shell,
|
||||||
|
|
|
||||||
133
tests/functional/pasta-network.sh
Executable file
133
tests/functional/pasta-network.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
source common.sh
|
||||||
|
|
||||||
|
# This test requires Linux sandbox support and pasta
|
||||||
|
needLocalStore "the sandbox only runs on the builder side"
|
||||||
|
requireSandboxSupport
|
||||||
|
requiresUnprivilegedUserNamespaces
|
||||||
|
|
||||||
|
# Skip test if pasta is not configured or available
|
||||||
|
PASTA_PATH="${pasta_path:-}"
|
||||||
|
if [[ -z "$PASTA_PATH" ]] || [[ "$PASTA_PATH" == "pasta" ]] || [[ ! -x "$PASTA_PATH" ]]; then
|
||||||
|
skipTest "pasta is not available (pasta_path=$PASTA_PATH)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure pasta is in a standard location that Nix can access
|
||||||
|
# If pasta is in a non-standard location, we need to add it to sandbox-paths
|
||||||
|
PASTA_DIR=$(dirname "$PASTA_PATH")
|
||||||
|
export NIX_SANDBOX_PATHS="$PASTA_DIR=$PASTA_DIR"
|
||||||
|
|
||||||
|
# Skip test if /dev/net/tun is not available (required for pasta)
|
||||||
|
if [[ ! -e /dev/net/tun ]]; then
|
||||||
|
skipTest "/dev/net/tun not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
clearStore
|
||||||
|
|
||||||
|
# Test that fixed-output derivations can access the network when pasta is enabled
|
||||||
|
echo 'testing fixed-output derivation with network access...'
|
||||||
|
|
||||||
|
# Create a test derivation that tries to access the network
|
||||||
|
cat > pasta-test.nix <<'EOF'
|
||||||
|
with import ./config.nix;
|
||||||
|
|
||||||
|
{
|
||||||
|
# Test basic network functionality with a fixed-output derivation
|
||||||
|
testNetworkAccess = mkDerivation {
|
||||||
|
name = "test-network-access";
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
${bash}/bin/bash -c '
|
||||||
|
# Test basic network connectivity
|
||||||
|
# Try to resolve a hostname
|
||||||
|
if getent hosts localhost >/dev/null 2>&1; then
|
||||||
|
echo "DNS resolution works"
|
||||||
|
else
|
||||||
|
echo "DNS resolution failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test if we can see network interfaces
|
||||||
|
if ${coreutils}/bin/test -e /sys/class/net/eth0; then
|
||||||
|
echo "Network interface eth0 exists"
|
||||||
|
else
|
||||||
|
echo "Network interface eth0 missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output
|
||||||
|
echo "Network test passed" > $out
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
outputHashMode = "flat";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHash = "sha256-YCa7ssqLHbdFkPJEG4REJJbsZF9g3w1i+Eg21nUYCCk=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test that non-fixed-output derivations cannot access the network
|
||||||
|
testNoNetworkAccess = mkDerivation {
|
||||||
|
name = "test-no-network-access";
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
${bash}/bin/bash -c '
|
||||||
|
# This should fail because non-fixed-output derivations
|
||||||
|
# should not have network access
|
||||||
|
if getent hosts localhost >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: DNS resolution works but should not!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# There should be no network interfaces
|
||||||
|
if ${coreutils}/bin/test -e /sys/class/net/eth0; then
|
||||||
|
echo "ERROR: Network interface exists but should not!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Network properly isolated" > $out
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Test with pasta enabled
|
||||||
|
echo "Setting pasta-path for network isolation..."
|
||||||
|
NIX_CONFIG="pasta-path = $PASTA_PATH
|
||||||
|
sandbox-paths = $NIX_SANDBOX_PATHS" \
|
||||||
|
nix-build pasta-test.nix -A testNetworkAccess --no-out-link
|
||||||
|
|
||||||
|
# Test that non-fixed-output derivations are still isolated
|
||||||
|
echo "Testing non-fixed-output derivation isolation..."
|
||||||
|
nix-build pasta-test.nix -A testNoNetworkAccess --no-out-link
|
||||||
|
|
||||||
|
# Test that pasta process is properly cleaned up
|
||||||
|
echo "Testing pasta process cleanup..."
|
||||||
|
cat > pasta-cleanup-test.nix <<'EOF'
|
||||||
|
with import ./config.nix;
|
||||||
|
|
||||||
|
mkDerivation {
|
||||||
|
name = "pasta-cleanup-test";
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
${bash}/bin/bash -c '
|
||||||
|
# Just create output
|
||||||
|
echo "test" > $out
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
outputHashMode = "flat";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHash = "sha256-n4xS51kG4lw0bKJl5VUkJptBS0EbV8LPHZkFV3RJQBU=";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build with pasta and check that no pasta processes remain
|
||||||
|
PASTA_COUNT_BEFORE=$(pgrep -c pasta || echo 0)
|
||||||
|
NIX_CONFIG="pasta-path = $PASTA_PATH
|
||||||
|
sandbox-paths = $NIX_SANDBOX_PATHS" \
|
||||||
|
nix-build pasta-cleanup-test.nix --no-out-link
|
||||||
|
PASTA_COUNT_AFTER=$(pgrep -c pasta || echo 0)
|
||||||
|
|
||||||
|
if [[ $PASTA_COUNT_AFTER -gt $PASTA_COUNT_BEFORE ]]; then
|
||||||
|
echo "ERROR: pasta process was not cleaned up properly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pasta network isolation tests passed!"
|
||||||
57
tests/functional/pasta-test.nix
Normal file
57
tests/functional/pasta-test.nix
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
with import ./config.nix;
|
||||||
|
|
||||||
|
{
|
||||||
|
# Test basic network functionality with a fixed-output derivation
|
||||||
|
testNetworkAccess = mkDerivation {
|
||||||
|
name = "test-network-access";
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
${bash}/bin/bash -c '
|
||||||
|
# Test basic network connectivity
|
||||||
|
# Try to resolve a hostname
|
||||||
|
if getent hosts localhost >/dev/null 2>&1; then
|
||||||
|
echo "DNS resolution works"
|
||||||
|
else
|
||||||
|
echo "DNS resolution failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test if we can see network interfaces
|
||||||
|
if ${coreutils}/bin/test -e /sys/class/net/eth0; then
|
||||||
|
echo "Network interface eth0 exists"
|
||||||
|
else
|
||||||
|
echo "Network interface eth0 missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create output
|
||||||
|
echo "Network test passed" > $out
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
outputHashMode = "flat";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHash = "sha256-YCa7ssqLHbdFkPJEG4REJJbsZF9g3w1i+Eg21nUYCCk=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Test that non-fixed-output derivations cannot access the network
|
||||||
|
testNoNetworkAccess = mkDerivation {
|
||||||
|
name = "test-no-network-access";
|
||||||
|
builder = builtins.toFile "builder.sh" ''
|
||||||
|
${bash}/bin/bash -c '
|
||||||
|
# This should fail because non-fixed-output derivations
|
||||||
|
# should not have network access
|
||||||
|
if getent hosts localhost >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: DNS resolution works but should not!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# There should be no network interfaces
|
||||||
|
if ${coreutils}/bin/test -e /sys/class/net/eth0; then
|
||||||
|
echo "ERROR: Network interface exists but should not!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Network properly isolated" > $out
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
93
tests/unit/libstore/pasta-test.cc
Normal file
93
tests/unit/libstore/pasta-test.cc
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
|
||||||
|
#include "nix/store/globals.hh"
|
||||||
|
#include "nix/util/processes.hh"
|
||||||
|
#include "nix/util/file-system.hh"
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
#include "nix/unix/build/pasta.hh"
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
TEST(PastaTest, ConstantsAreDefined) {
|
||||||
|
// Test that all pasta constants are properly defined
|
||||||
|
EXPECT_STREQ(pasta::PASTA_NS_IFNAME, "eth0");
|
||||||
|
EXPECT_STREQ(pasta::PASTA_HOST_IPV4, "169.254.1.1");
|
||||||
|
EXPECT_STREQ(pasta::PASTA_CHILD_IPV4, "169.254.1.2");
|
||||||
|
EXPECT_STREQ(pasta::PASTA_PREFIX_IPV4, "30");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PastaTest, RewriteResolvConfBasic) {
|
||||||
|
// Test basic resolv.conf rewriting
|
||||||
|
std::string original = R"(
|
||||||
|
# Generated by NetworkManager
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
search example.com
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string expected = R"(
|
||||||
|
# Generated by NetworkManager
|
||||||
|
nameserver 169.254.1.1
|
||||||
|
nameserver 169.254.1.1
|
||||||
|
search example.com
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string result = pasta::rewriteResolvConf(original);
|
||||||
|
EXPECT_EQ(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PastaTest, RewriteResolvConfEmpty) {
|
||||||
|
// Test empty resolv.conf
|
||||||
|
std::string original = "";
|
||||||
|
std::string result = pasta::rewriteResolvConf(original);
|
||||||
|
EXPECT_EQ(result, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PastaTest, RewriteResolvConfComments) {
|
||||||
|
// Test resolv.conf with only comments
|
||||||
|
std::string original = R"(# This is a comment
|
||||||
|
# Another comment
|
||||||
|
)";
|
||||||
|
std::string result = pasta::rewriteResolvConf(original);
|
||||||
|
EXPECT_EQ(result, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PastaTest, RewriteResolvConfMixed) {
|
||||||
|
// Test resolv.conf with various directives
|
||||||
|
std::string original = R"(
|
||||||
|
nameserver 192.168.1.1
|
||||||
|
domain local.lan
|
||||||
|
search local.lan corp.lan
|
||||||
|
nameserver 192.168.1.2
|
||||||
|
options ndots:1
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string expected = R"(
|
||||||
|
nameserver 169.254.1.1
|
||||||
|
domain local.lan
|
||||||
|
search local.lan corp.lan
|
||||||
|
nameserver 169.254.1.1
|
||||||
|
options ndots:1
|
||||||
|
)";
|
||||||
|
|
||||||
|
std::string result = pasta::rewriteResolvConf(original);
|
||||||
|
EXPECT_EQ(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(PastaTest, SettingIsConfigurable) {
|
||||||
|
// Test that pasta path setting can be configured
|
||||||
|
Settings settings;
|
||||||
|
|
||||||
|
// Default should be empty
|
||||||
|
EXPECT_EQ(settings.pastaPath.get(), "");
|
||||||
|
|
||||||
|
// Should be settable
|
||||||
|
settings.pastaPath = "/usr/bin/pasta";
|
||||||
|
EXPECT_EQ(settings.pastaPath.get(), "/usr/bin/pasta");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nix
|
||||||
|
|
||||||
|
#endif // __linux__
|
||||||
Loading…
Add table
Add a link
Reference in a new issue