From 1d34fdbc2add6a2d92455e71a8ad22ff6642c3b4 Mon Sep 17 00:00:00 2001 From: netpleb Date: Thu, 4 Jun 2026 14:59:02 +0000 Subject: [PATCH 1/2] fix: handle absolute WAYLAND_DISPLAY and XAUTHORITY in -d binding WAYLAND_DISPLAY and XAUTHORITY may each be either a bare name (resolved relative to XDG_RUNTIME_DIR and $HOME respectively) or an absolute path. The old code always prepended the base directory, so an absolute value produced a malformed path like /run/user/1001//run/user/1000/wayland-0 or /home/alice//run/user/1000/.mutter-Xwaylandauth.XXXXXX, causing a bwrap "Can't find source path" error and breaking -d desktop access. Detect the absolute form for both and use it as-is, otherwise resolve relative to the base as before. Guard each bind with an existence check so a stale/missing socket or auth file is skipped instead of crashing bwrap. Co-Authored-By: Claude Opus 4.8 (1M context) --- wrap.sh | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/wrap.sh b/wrap.sh index 24530a3..1886e56 100755 --- a/wrap.sh +++ b/wrap.sh @@ -211,8 +211,18 @@ while getopts "r:w:e:abcdfhmnpuv" opt; do # grant desktop access (Wayland or X11) and rendering hardware access d) if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then - # Using Wayland: bind the Wayland display socket - bwrap_opts+=(--bind "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY") + # Using Wayland: bind the Wayland display socket. + # WAYLAND_DISPLAY may be either a bare socket name (resolved relative to + # XDG_RUNTIME_DIR, the common case) or an absolute path (in which case + # XDG_RUNTIME_DIR must NOT be prepended). Handle both. + if [[ "$WAYLAND_DISPLAY" = /* ]]; then + wayland_socket="$WAYLAND_DISPLAY" + else + wayland_socket="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" + fi + if [ -e "$wayland_socket" ]; then + bwrap_opts+=(--bind "$wayland_socket" "$wayland_socket") + fi fi if [ -n "${DISPLAY:-}" ]; then @@ -224,8 +234,17 @@ while getopts "r:w:e:abcdfhmnpuv" opt; do # Bind the .Xauthority file so that the authorization data is available. if [ -n "${XAUTHORITY:-}" ]; then - # Bind a custom path Xauthority file to the standard path in the sandbox - bwrap_opts+=(--ro-bind "${HOME}/${XAUTHORITY}" "$HOME/.Xauthority") + # XAUTHORITY may be an absolute path (e.g. /run/user/1000/.mutter-...) + # or a bare name resolved relative to $HOME. Handle both, then bind it + # to the standard $HOME/.Xauthority path the client expects in sandbox. + if [[ "$XAUTHORITY" = /* ]]; then + xauth_file="$XAUTHORITY" + else + xauth_file="${HOME}/${XAUTHORITY}" + fi + if [ -f "$xauth_file" ]; then + bwrap_opts+=(--ro-bind "$xauth_file" "$HOME/.Xauthority") + fi elif [ -f "$HOME/.Xauthority" ]; then # Bind the standard path Xauthority file to the sandbox bwrap_opts+=(--ro-bind "$HOME/.Xauthority" "$HOME/.Xauthority") From ef25c27bdb0a4b1b0ddc23669b7297b6bdf34690 Mon Sep 17 00:00:00 2001 From: netpleb Date: Thu, 4 Jun 2026 15:15:01 +0000 Subject: [PATCH 2/2] feat: add -N flag to run inside an existing named network namespace Adds a -N NAME option that launches the wrapped program inside a pre-existing Linux network namespace (e.g. one set up by the new netns-sandbox.sh, which routes all traffic through a WireGuard VPN). Unlike -n, which only shares the host net, -N keeps the target namespace's network so the sandboxed process inherits the namespace's routing, firewall, and VPN-provided DNS. To enter it, wrap prefixes the launch with 'sudo ip netns exec NAME sudo -u $USER', dropping back to the invoking user before exec'ing bwrap. The process must live in the namespace's net, so -N keeps the default --unshare-all and appends --share-net after it: per bwrap(1) --share-net "retains the network namespace, overriding an earlier --unshare-all". This yields full isolation minus net without hand-enumerating each namespace, mirroring how -n already works. It also implies the network binds (resolv.conf, ssl) so DNS and TLS work, relying on the kernel exposing /etc/netns/NAME/resolv.conf as /etc/resolv.conf in the namespace. Documents the flag under ADVANCED OPTIONS in usage and README. --- README.md | 5 +++++ wrap.sh | 45 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a760f8..2f841a4 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ By default, Nixwrap will: #### Advanced Options ``` + -N NAME Run inside the existing named network namespace NAME. The namespace + must already exist (e.g. created by netns-sandbox.sh). wrap enters it + via 'sudo ip netns exec NAME' and drops back to the current user + before launching. This keeps the namespace's network instead of + unsharing net, and implies network access (-n) so DNS and TLS work. -p Do not share current working directory. By default wrap will share the current working directory as a write mount and cd into it before running the program. With this option, wrap will not share diff --git a/wrap.sh b/wrap.sh index 1886e56..5103700 100755 --- a/wrap.sh +++ b/wrap.sh @@ -140,8 +140,13 @@ OPTIONS: -v Verbose output for debugging. ADVANCED OPTIONS: - -p Do not share current working directory. By default wrap will share - the current working directory as a write mount and cd into it + -N NAME Run inside the existing named network namespace NAME. The namespace + must already exist (e.g. created by netns-sandbox.sh). wrap enters it + via 'sudo ip netns exec NAME' and drops back to the current user + before launching. This keeps the namespace's network instead of + unsharing net, and implies network access (-n) so DNS and TLS work. + -p Do not share current working directory. By default wrap will share + the current working directory as a write mount and cd into it before running the program. With this option, wrap will not share the directory and leave the current directory untouched. -f Force share current working directory. By default wrap will share @@ -173,8 +178,9 @@ fi unshare_all=1 share_cwd=1 force_share_cwd=0 +netns="" -while getopts "r:w:e:abcdfhmnpuv" opt; do +while getopts "r:w:e:N:abcdfhmnpuv" opt; do case "$opt" in # bind / mount a path readonly in sandbox to the same path as host @@ -279,6 +285,29 @@ while getopts "r:w:e:abcdfhmnpuv" opt; do bwrap_opts+=(--ro-bind /etc/static/ssl /etc/static/ssl) ;; + # run inside an existing named network namespace (see netns-sandbox.sh). + # the namespace must already exist; wrap will enter it via + # `sudo ip netns exec NAME` before launching bwrap, dropping back to the + # current user. this keeps the namespace's network (instead of unsharing + # net) and implies network access (-n) so that DNS and TLS work. + N) + netns="$OPTARG" + + # keep the netns' network instead of unsharing net. we still want the + # full default isolation (--unshare-all), so rather than enumerate every + # namespace by hand we just add --share-net: per bwrap(1) it "retains the + # network namespace, overriding an earlier --unshare-all". it is appended + # to bwrap_opts, which is re-expanded after --unshare-all below, so the + # ordering (--unshare-all ... --share-net) is correct. + bwrap_opts+=(--share-net) + + # imply network access binds so resolv.conf / TLS work inside the netns. + # the kernel exposes /etc/netns/NAME/resolv.conf as /etc/resolv.conf here. + bwrap_opts+=(--ro-bind /etc/resolv.conf /etc/resolv.conf) + bwrap_opts+=(--ro-bind /etc/ssl /etc/ssl) + bwrap_opts+=(--ro-bind /etc/static/ssl /etc/static/ssl) + ;; + # grant audio access a) bwrap_opts+=(--bind-try "$XDG_RUNTIME_DIR/pulse/native" "$XDG_RUNTIME_DIR/pulse/native") @@ -392,7 +421,15 @@ for e in "${env_vars[@]}"; do fi done -exec bwrap \ +# when running inside a named network namespace, enter it via +# `sudo ip netns exec NAME` and drop back to the current user before bwrap. +# otherwise the prefix is empty and bwrap runs directly as today. +netns_prefix=() +if [[ -n "$netns" ]]; then + netns_prefix=(sudo ip netns exec "$netns" sudo -u "$USER") +fi + +exec "${netns_prefix[@]}" bwrap \ --chdir "$bwrap_chdir" \ --clearenv \ --dev /dev \