From 7b1bc210fd0362be56eea410c2601cbf710240f2 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 25 May 2024 09:39:18 +0000 Subject: [PATCH] sanebox: integrate with `pasta` (passt) for better net sandboxing --- hosts/common/programs/assorted.nix | 2 + modules/programs/default.nix | 11 ++-- modules/programs/make-sandbox-args.nix | 4 ++ pkgs/additional/sanebox/default.nix | 4 +- pkgs/additional/sanebox/sanebox | 70 ++++++++++++++++++++++++-- 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/hosts/common/programs/assorted.nix b/hosts/common/programs/assorted.nix index d2b4cd19..0406be2a 100644 --- a/hosts/common/programs/assorted.nix +++ b/hosts/common/programs/assorted.nix @@ -723,6 +723,8 @@ in # settings (electron app) obsidian.persist.byStore.plaintext = [ ".config/obsidian" ]; + passt.sandbox.enable = false; #< sandbox helper (netns specifically) + parted.sandbox.method = "landlock"; parted.sandbox.extraPaths = [ "/dev" diff --git a/modules/programs/default.nix b/modules/programs/default.nix index 8d82c04d..74b8e02a 100644 --- a/modules/programs/default.nix +++ b/modules/programs/default.nix @@ -74,6 +74,10 @@ let vpn.name else sandbox.net; + netGateway = if sandbox.net == "vpn" then + vpn.addrV4 + else + null; dns = if sandbox.net == "vpn" then vpn.dns else @@ -429,9 +433,10 @@ let null else wrapPkg name config config.packageUnwrapped - ; - suggestedPrograms = lib.optionals (config.sandbox.method == "bwrap") [ "bubblewrap" ] - ++ lib.optionals (config.sandbox.method == "firejail") [ "firejail" ]; + ; + suggestedPrograms = lib.optionals (config.sandbox.method == "bwrap") [ + "bubblewrap" "passt" + ] ++ lib.optionals (config.sandbox.method == "firejail") [ "firejail" ]; # declare a fs dependency for each secret, but don't specify how to populate it yet. # can't populate it here because it varies per-user. # this gets the symlink into the sandbox, but not the actual secret. diff --git a/modules/programs/make-sandbox-args.nix b/modules/programs/make-sandbox-args.nix index 05186d33..37a1a192 100644 --- a/modules/programs/make-sandbox-args.nix +++ b/modules/programs/make-sandbox-args.nix @@ -7,6 +7,7 @@ , capabilities ? [] , dns ? null , netDev ? null +, netGateway ? null , whitelistPwd ? false , extraConfig ? [] }: @@ -22,6 +23,9 @@ let netItems = lib.optionals (netDev != null) [ "--sanebox-net-dev" netDev + ] ++ lib.optionals (netGateway != null) [ + "--sanebox-net-gateway" + netGateway ] ++ lib.optionals (dns != null) ( lib.flatten (builtins.map (addr: [ "--sanebox-dns" addr ]) diff --git a/pkgs/additional/sanebox/default.nix b/pkgs/additional/sanebox/default.nix index 0d1ed14d..270ee756 100644 --- a/pkgs/additional/sanebox/default.nix +++ b/pkgs/additional/sanebox/default.nix @@ -1,6 +1,7 @@ { lib, stdenv , bash , bubblewrap +, passt , firejail , landlock-sandboxer , libcap @@ -24,7 +25,8 @@ stdenv.mkDerivation { --replace-fail '@bwrap@' '${lib.getExe bubblewrap}' \ --replace-fail '@firejail@' '${lib.getExe' firejail "firejail"}' \ --replace-fail '@landlockSandboxer@' '${lib.getExe landlock-sandboxer}' \ - --replace-fail '@capsh@' '${lib.getExe' libcap "capsh"}' + --replace-fail '@capsh@' '${lib.getExe' libcap "capsh"}' \ + --replace-fail '@pasta@' '${lib.getExe' passt "pasta"}' runHook postBuild ''; diff --git a/pkgs/additional/sanebox/sanebox b/pkgs/additional/sanebox/sanebox index 2ed22e11..c6f9420f 100755 --- a/pkgs/additional/sanebox/sanebox +++ b/pkgs/additional/sanebox/sanebox @@ -6,6 +6,7 @@ FIREJAIL_FALLBACK='@firejail@' BWRAP_FALLBACK='@bwrap@' LANDLOCK_SANDBOXER_FALLBACK='@landlockSandboxer@' CAPSH_FALLBACK='@capsh@' +PASTA_FALLBACK='@pasta@' ## EARLY DEBUG HOOKS @@ -80,7 +81,10 @@ capabilities=() # - "all": as if all the above were specified keepNamespace=() # name of some network device to make available to the sandbox, if any. +# or "all" to keep all devices available netDev= +# IPv4 address of the default gateway associated with the bridged network device (usually that's just the VPN's IP addr) +netGateway= # list of IP addresses to use for DNS servers inside the sandbox (firejail only) dns=() # list of `VAR=VALUE` environment variables to add to the sandboxed program's environment @@ -121,6 +125,7 @@ usage() { echo ' --sanebox-firejail-arg ' echo ' --sanebox-bwrap-arg ' echo ' --sanebox-net-dev ' + echo ' --sanebox-net-gateway ' echo ' --sanebox-dns ' echo ' --sanebox-keep-namespace ' echo ' do not unshare the provided linux namespace' @@ -516,10 +521,14 @@ parseArgs() { netDev=$1 shift ;; - (--sanebox-dns) - local dns=$1 + (--sanebox-net-gateway) + netGateway=$1 shift - dns+=("$dns") + ;; + (--sanebox-dns) + local dnsServer=$1 + shift + dns+=("$dnsServer") ;; (--sanebox-keep-namespace) local namespace=$1 @@ -578,6 +587,9 @@ firejailIngestNetDev() { # firejail can then spawn a veth from this bridge and namespace it that way. firejailFlags+=("--net=br-$1") } +firejailIngestNetGateway() { + debug "firejailIngestNetGateway: noop" +} firejailIngestDns() { firejailFlags+=("--dns=$1") } @@ -604,6 +616,9 @@ bwrapUnshareUts=(--unshare-uts) bwrapVirtualizeDev=(--dev /dev) bwrapVirtualizeProc=(--proc /proc) bwrapVirtualizeTmp=(--tmpfs /tmp) +# args to invoke `pasta` (user-mode network stack) with +bwrapPastaArgs=() +bwrapNetSetup= bwrapSetup() { debug "bwrapSetup: noop" @@ -652,8 +667,22 @@ bwrapIngestPath() { esac } bwrapIngestNetDev() { - debug "bwrapIngestNetDev: enabling full net access for '$1' because don't know how to restrict it more narrowly" + local dev=$1 bwrapUnshareNet=() + case $dev in + (all) + ;; + (*) + bwrapPastaArgs+=(--outbound-if4 "$dev") + ;; + esac +} +bwrapIngestNetGateway() { + bwrapPastaArgs+=(--gateway "$1") +} +bwrapIngestDns() { + # NAT DNS requests to localhost to the VPN's DNS resolver + bwrapNetSetup="ip addr del 127.0.0.1/8 dev lo; iptables -A OUTPUT -t nat -p udp --dport 53 -m iprange --dst-range 127.0.0.1 -j DNAT --to-destination $1:53; $bwrapNetSetup" } bwrapIngestKeepNamespace() { case $1 in @@ -693,6 +722,21 @@ bwrapGetCli() { "${bwrapFlags[@]}" -- env "${portalEnv[@]}" "${cliArgs[@]}" ) + if [ ${#bwrapPastaArgs} -ne 0 ]; then + # if [ -n "$bwrapNetSetup" ]; then + cliArgs=( + "/bin/sh" "-c" + "$bwrapNetSetup exec"' "$0" "$@"' + "${cliArgs[@]}" + ) + # fi + locate _pasta "pasta" "$PASTA_FALLBACK" + cliArgs=( + "$_pasta" --ipv4-only -U none -T none --config-net + "${bwrapPastaArgs[@]}" -- + "${cliArgs[@]}" + ) + fi } @@ -741,6 +785,12 @@ landlockIngestPath() { landlockIngestNetDev() { debug "landlockIngestNetDev: '$1': stubbed (landlock network is always unrestricted)" } +landlockIngestNetGateway() { + debug "landlockIngestNetGateway: noop" +} +landlockIngestDns() { + debug "landlockIngestDns: noop" +} landlockIngestKeepNamespace() { debug "landlockIngestKeepNamespace: noop" } @@ -776,6 +826,12 @@ capshonlyIngestPath() { capshonlyIngestNetDev() { debug "capshonlyIngestNetDev: '$1': stubbed (capsh network is always unrestricted)" } +capshonlyIngestNetGateway() { + debug "capshonlyIngestNetGateway: '$1': stubbed (capsh network is always unrestricted)" +} +capshonlyIngestDns() { + debug "capshonlyIngestDns: '$1': stubbed (capsh network is always unrestricted)" +} capshonlyIngestKeepNamespace() { debug "capshonlyIngestKeepNamespace: noop" } @@ -944,7 +1000,11 @@ ingestForBackend() { done if [ -n "$netDev" ]; then - "$method"IngestNetDev"$netDev" + "$method"IngestNetDev "$netDev" + fi + + if [ -n "$netGateway" ]; then + "$method"IngestNetGateway "$netGateway" fi for addr in "${dns[@]}"; do