From b2c35b45d2f78905f404461c0b5d1151b5019118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 24 Jul 2025 13:17:58 +0200 Subject: [PATCH] pasta: wip TODO: add original authors as commit authors --- meson.options | 7 + src/libstore/globals.cc | 4 + src/libstore/include/nix/store/globals.hh | 15 ++ src/libstore/meson.build | 9 + src/libstore/meson.options | 7 + src/libstore/package.nix | 4 + .../unix/build/linux-derivation-builder.cc | 60 ++++++- src/libstore/unix/build/pasta.cc | 161 ++++++++++++++++++ src/libstore/unix/build/pasta.hh | 78 +++++++++ src/libstore/unix/meson.build | 12 ++ tests/functional/common/subst-vars.sh.in | 1 + tests/functional/meson.build | 11 ++ tests/functional/package.nix | 4 +- tests/functional/pasta-network.sh | 133 +++++++++++++++ tests/functional/pasta-test.nix | 57 +++++++ tests/unit/libstore/pasta-test.cc | 93 ++++++++++ 16 files changed, 653 insertions(+), 3 deletions(-) create mode 100644 src/libstore/unix/build/pasta.cc create mode 100644 src/libstore/unix/build/pasta.hh create mode 100755 tests/functional/pasta-network.sh create mode 100644 tests/functional/pasta-test.nix create mode 100644 tests/unit/libstore/pasta-test.cc diff --git a/meson.options b/meson.options index d2c9fa40c..d82dbecc0 100644 --- a/meson.options +++ b/meson.options @@ -27,3 +27,10 @@ option( value : false, description : 'Build benchmarks (requires gbenchmark)', ) + +option( + 'pasta-path', + type : 'string', + value : 'pasta', + description : 'Path to the location of pasta (provided by passt)', +) diff --git a/src/libstore/globals.cc b/src/libstore/globals.cc index 612e79ab0..4aab27568 100644 --- a/src/libstore/globals.cc +++ b/src/libstore/globals.cc @@ -89,6 +89,10 @@ Settings::Settings() sandboxPaths = {{"/bin/sh", {.source = SANDBOX_SHELL}}}; #endif +#if defined(__linux__) && defined(PASTA_PATH) + pastaPath.setDefault(PASTA_PATH); +#endif + /* chroot-like behavior from Apple's sandbox */ #ifdef __APPLE__ for (PathView p : { diff --git a/src/libstore/include/nix/store/globals.hh b/src/libstore/include/nix/store/globals.hh index 2cd92467c..12254ae00 100644 --- a/src/libstore/include/nix/store/globals.hh +++ b/src/libstore/include/nix/store/globals.hh @@ -1372,6 +1372,21 @@ public: Default is 0, which disables the warning. Set it to 1 to warn on all paths. )"}; + +#ifdef __linux__ + Setting 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. diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 556616181..6964f5368 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -259,6 +259,15 @@ configdata_priv.set_quoted( : '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( configuration : configdata_priv, output : 'store-config-private.hh', diff --git a/src/libstore/meson.options b/src/libstore/meson.options index b8414068d..a5adef04d 100644 --- a/src/libstore/meson.options +++ b/src/libstore/meson.options @@ -33,3 +33,10 @@ option( value : '/nix/var/log/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)', +) diff --git a/src/libstore/package.nix b/src/libstore/package.nix index d890d2256..f719f2aca 100644 --- a/src/libstore/package.nix +++ b/src/libstore/package.nix @@ -15,6 +15,7 @@ sqlite, busybox-sandbox-shell ? null, + passt ? null, # Configuration Options @@ -77,6 +78,9 @@ mkMesonLibrary (finalAttrs: { ] ++ lib.optionals stdenv.hostPlatform.isLinux [ (lib.mesonOption "sandbox-shell" "${busybox-sandbox-shell}/bin/busybox") + ] + ++ [ + (lib.mesonOption "pasta-path" "${passt}/bin/pasta") ]; meta = { diff --git a/src/libstore/unix/build/linux-derivation-builder.cc b/src/libstore/unix/build/linux-derivation-builder.cc index 35730644b..7ee516a82 100644 --- a/src/libstore/unix/build/linux-derivation-builder.cc +++ b/src/libstore/unix/build/linux-derivation-builder.cc @@ -1,9 +1,13 @@ #ifdef __linux__ +# include "nix/store/globals.hh" # include "nix/store/personality.hh" # include "nix/util/cgroup.hh" +# include "nix/util/file-system.hh" # include "nix/util/linux-namespaces.hh" +# include "nix/util/strings.hh" # include "linux/fchmodat2-compat.hh" +# include "pasta.hh" # include # include @@ -190,6 +194,16 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu */ std::optional 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( LocalStore & store, std::unique_ptr miscMethods, DerivationBuilderParams 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() { 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. */ writeFull(userNamespaceSync.writeSide.get(), "1\n"); 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 @@ -444,6 +483,10 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu userNamespaceSync.readSide = -1; + if (runPasta) { + pasta::waitForPastaInterface(); + } + if (derivationType.isSandboxed()) { /* Initialise the loopback interface. */ @@ -532,8 +575,17 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu happens when testing Nix building fixed-output derivations within a pure derivation. */ for (auto & path : {"/etc/resolv.conf", "/etc/services", "/etc/hosts"}) - if (pathExists(path)) - ss.push_back(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); + } + } if (settings.caFile != "") { Path caFile = settings.caFile; @@ -691,6 +743,10 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu return; } + if (pastaPid) { + pasta::killPasta(pastaPid); + } + DerivationBuilderImpl::killSandbox(getStats); } diff --git a/src/libstore/unix/build/pasta.cc b/src/libstore/unix/build/pasta.cc new file mode 100644 index 000000000..0d19e9412 --- /dev/null +++ b/src/libstore/unix/build/pasta.cc @@ -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 +# include +# include +# include +# include +# include +# include +# include +# include + +namespace nix { +namespace pasta { + +Pid setupPasta( + const Path & pastaPath, + pid_t pid, + std::optional buildUserId, + std::optional 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__ diff --git a/src/libstore/unix/build/pasta.hh b/src/libstore/unix/build/pasta.hh new file mode 100644 index 000000000..ba1037793 --- /dev/null +++ b/src/libstore/unix/build/pasta.hh @@ -0,0 +1,78 @@ +#pragma once + +#ifdef __linux__ + +# include "nix/util/processes.hh" +# include "nix/util/file-descriptor.hh" +# include +# include + +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 buildUserId, + std::optional 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__ \ No newline at end of file diff --git a/src/libstore/unix/meson.build b/src/libstore/unix/meson.build index 4b8a6b105..c7321de66 100644 --- a/src/libstore/unix/meson.build +++ b/src/libstore/unix/meson.build @@ -6,4 +6,16 @@ sources += files( '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') diff --git a/tests/functional/common/subst-vars.sh.in b/tests/functional/common/subst-vars.sh.in index df140dec1..1871c73dd 100644 --- a/tests/functional/common/subst-vars.sh.in +++ b/tests/functional/common/subst-vars.sh.in @@ -10,6 +10,7 @@ coreutils=@coreutils@ dot=@dot@ busybox="@sandbox_shell@" +pasta_path=@pasta_path@ version=@PACKAGE_VERSION@ system=@system@ diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 54e13b26d..61c1da17b 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -21,6 +21,7 @@ busybox = find_program('busybox', native : true, required : false) # guaranteed to exist either. coreutils = find_program('ls', native : true) 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()) @@ -35,6 +36,15 @@ test_confdata = { 'PACKAGE_VERSION' : meson.project_version(), '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`. # Done as a subdir() so Meson places it under `common` in the build directory as well. @@ -127,6 +137,7 @@ suites = [ 'misc.sh', 'dump-db.sh', 'linux-sandbox.sh', + 'pasta-network.sh', 'supplementary-groups.sh', 'build-dry.sh', 'structured-attrs.sh', diff --git a/tests/functional/package.nix b/tests/functional/package.nix index 1f1d10ea8..8c49bcf04 100644 --- a/tests/functional/package.nix +++ b/tests/functional/package.nix @@ -12,6 +12,7 @@ mercurial, util-linux, unixtools, + passt, nix-store, nix-expr, @@ -56,7 +57,8 @@ mkMesonDerivation ( git mercurial unixtools.script - ] + passt + ] ++ lib.optionals stdenv.hostPlatform.isLinux [ # For various sandboxing tests that needs a statically-linked shell, # etc. diff --git a/tests/functional/pasta-network.sh b/tests/functional/pasta-network.sh new file mode 100755 index 000000000..3669d9351 --- /dev/null +++ b/tests/functional/pasta-network.sh @@ -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!" diff --git a/tests/functional/pasta-test.nix b/tests/functional/pasta-test.nix new file mode 100644 index 000000000..407e19b14 --- /dev/null +++ b/tests/functional/pasta-test.nix @@ -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 + ' + ''; + }; +} diff --git a/tests/unit/libstore/pasta-test.cc b/tests/unit/libstore/pasta-test.cc new file mode 100644 index 000000000..85ed13be7 --- /dev/null +++ b/tests/unit/libstore/pasta-test.cc @@ -0,0 +1,93 @@ +#include +#include + +#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__ \ No newline at end of file