From ef2a4c419a34efc49c41808a4a966ee924795fa2 Mon Sep 17 00:00:00 2001 From: operator Date: Thu, 8 Oct 2026 13:23:06 -0400 Subject: [PATCH] feat(netns): bwrap-contained chrome + proton wrapper - netvm-chrome.sh --contained: bwrap fs jail inside netns (profile home only + vault ro at /tmp/vault, Chromium sandbox stays on) - netvm-proton.sh: run proton-cli as the profile identity in netns + jail (config dir + static binary, PROTON_NO_INPUT=1) --- README.md | 3 ++- bin/netvm-chrome.sh | 50 ++++++++++++++++++++++++++++++++++++++++----- bin/netvm-proton.sh | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100755 bin/netvm-proton.sh diff --git a/README.md b/README.md index eb66cd9..79529de 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,8 @@ veth IPs aren't routable off the host and Warp forwards no inbound traffic. - `bin/netvm-fleet.sh` — operator fleet control over the tailnet (topology/up/down/ssh/exec/cdp per node). - `bin/netvm-exec.sh -- ` — run a command inside the node's netns as the invoking user (the agent-friendly primitive). - `bin/netvm-enter.sh` — root worker behind netvm-exec/netvm-chrome (allowlisted; enters netns, fixes DNS, drops privs). -- `bin/netvm-chrome.sh [url]` — launch a chrome-box profile in its netns (CDP on by default; --headless for agents). +- `bin/netvm-chrome.sh [url]` — launch a chrome-box profile in its netns (CDP on by default; --headless for agents; --contained adds a bwrap fs jail inside the netns: profile home only + vault read-only at /tmp/vault, Chromium sandbox stays on). +- `bin/netvm-proton.sh -- ` — run proton-cli as the profile's Proton identity (1:1:1 profile = node = warp identity = proton-cli profile) inside the netns + bwrap jail (config dir + static binary only, PROTON_NO_INPUT=1). Human creates the session once: `proton-cli -p account login`. - `bin/netvm-cdp.sh ` — print the CDP endpoint + SSH forward. - `bin/netvm-cdp-relay.py` — veth-IP→loopback TCP relay for CDP (pidfile-supervised). - `bin/netvm-names.sh` — shared naming: netns, hashed iface tags, veth subnet, CDP port. diff --git a/bin/netvm-chrome.sh b/bin/netvm-chrome.sh index a9916b0..8c34cee 100755 --- a/bin/netvm-chrome.sh +++ b/bin/netvm-chrome.sh @@ -1,15 +1,18 @@ #!/usr/bin/env bash -# netvm-chrome.sh [--headless] [--cdp-port N|--no-cdp] [url] +# netvm-chrome.sh [--headless] [--cdp-port N|--no-cdp] [--contained] [url] # Launch a chrome-box profile inside its dedicated NetVM netns. # 1:1: profile = node = warp identity = egress IP. # CDP is on by default (deterministic port) so agents can automate the # session via Playwright/Puppeteer; --headless runs without a display. +# --contained adds a bwrap filesystem jail INSIDE the netns (no net unshare): +# the browser sees only its own profile home (+ vault read-only). Chromium's +# own sandbox stays on (we never pass --no-sandbox to it). # Run as your normal user (uses sudo -n only for allowlisted netns ops). set -euo pipefail NETVM_BIN="$(cd "$(dirname "$0")" && pwd)" . "$NETVM_BIN/netvm-names.sh" -WAYPIPE=0; HEADLESS=0; CDP_OVERRIDE=""; NO_CDP=0; PROFILE=""; URL="" -usage() { echo "usage: netvm-chrome.sh [--waypipe] [--headless] [--cdp-port N|--no-cdp] [url]"; } +WAYPIPE=0; HEADLESS=0; CDP_OVERRIDE=""; NO_CDP=0; PROFILE=""; URL=""; CONTAINED=0 +usage() { echo "usage: netvm-chrome.sh [--waypipe] [--headless] [--cdp-port N|--no-cdp] [--contained] [url]"; } while [ $# -gt 0 ]; do case "$1" in --waypipe) WAYPIPE=1; shift;; @@ -17,6 +20,7 @@ while [ $# -gt 0 ]; do --cdp-port) CDP_OVERRIDE="$2"; shift 2;; --cdp-port=*) CDP_OVERRIDE="${1#*=}"; shift;; --no-cdp) NO_CDP=1; shift;; + --contained) CONTAINED=1; shift;; -h|--help) usage; exit 0;; *) if [ -z "$PROFILE" ]; then PROFILE="$1"; elif [ -z "$URL" ]; then URL="$1"; else echo "unexpected: $1"; usage; exit 1; fi; shift;; @@ -51,8 +55,44 @@ LAUNCH_ARGS=(launch "$PROFILE" --no-sandbox) [ -n "$CDP" ] && LAUNCH_ARGS+=(--cdp-port "$CDP") [ -n "$URL" ] && LAUNCH_ARGS+=("$URL") [ -n "$CDP" ] && echo "cdp: http://$PEER_IP:$CDP/json/list (local) | ssh -L $CDP:$PEER_IP:$CDP @ (remote)" +CMD=("$CHROME_BOX" "${LAUNCH_ARGS[@]}") +if [ "$CONTAINED" = 1 ]; then + # Contained mode execs Chromium directly (same flags chrome-box uses for a + # native launch) because chrome-box re-resolves its profile dir from $HOME, + # which the jail replaces. Direct Warp egress: no proxy flags by design. + command -v bwrap >/dev/null 2>&1 || { echo "bwrap not found" >&2; exit 1; } + P_HOME="$HOME/.local/share/chrome-box/profiles/$PROFILE" + mkdir -p "$P_HOME/.config/chromium" + KEYRING="$(python3 -c "import json,sys;print(json.load(open('$P_HOME/config.json')).get('keyring','basic'))" 2>/dev/null || echo basic)" + SB_DIR="$(dirname "$CHROME_BOX")" + BWRAP=(bwrap --unshare-uts --unshare-ipc --die-with-parent + --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp --tmpfs /dev/shm + --bind "$P_HOME" "$HOME" --setenv HOME "$HOME" --setenv PATH /usr/bin:/bin) + [ -d "$HOME/notes" ] && BWRAP+=(--ro-bind "$HOME/notes" /tmp/vault) + [ -f "$SB_DIR/hosts-sandbox.conf" ] && BWRAP+=(--ro-bind "$SB_DIR/hosts-sandbox.conf" /etc/hosts) + [ -f "$SB_DIR/nsswitch-sandbox.conf" ] && BWRAP+=(--ro-bind "$SB_DIR/nsswitch-sandbox.conf" /etc/nsswitch.conf) + CHROMIUM=(/usr/lib/chromium/chromium + "--user-data-dir=$HOME/.config/chromium" + "--password-store=$KEYRING" + --ozone-platform=x11 --disable-gpu --disable-quic + --disable-features=DnsOverHttpsUpgrade,AsyncDns + --built-in-dns-client-enabled=false) + if [ "$HEADLESS" = 1 ]; then + BWRAP+=(--unsetenv DISPLAY --unsetenv WAYLAND_DISPLAY --unsetenv XDG_RUNTIME_DIR) + CHROMIUM+=(--headless=new) + else + [ -d /tmp/.X11-unix ] && BWRAP+=(--ro-bind /tmp/.X11-unix /tmp/.X11-unix) + [ -n "${WAYLAND_DISPLAY:-}" ] && [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ] && \ + BWRAP+=(--ro-bind "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY") + [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR/pulse" ] && BWRAP+=(--bind "$XDG_RUNTIME_DIR/pulse" /run/pulse) + [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -S "$XDG_RUNTIME_DIR/bus" ] && BWRAP+=(--ro-bind "$XDG_RUNTIME_DIR/bus" /run/dbus/system_bus_socket) + fi + [ -n "$CDP" ] && CHROMIUM+=(--remote-debugging-port="$CDP" --remote-allow-origins='*') + [ -n "$URL" ] && CHROMIUM+=("$URL") + CMD=("${BWRAP[@]}" -- "${CHROMIUM[@]}") +fi if [ "$WAYPIPE" = 1 ]; then - exec waypipe --compress=lz4 run -- sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "$CHROME_BOX" "${LAUNCH_ARGS[@]}" + exec waypipe --compress=lz4 run -- sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "${CMD[@]}" else - exec sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "$CHROME_BOX" "${LAUNCH_ARGS[@]}" + exec sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "${CMD[@]}" fi diff --git a/bin/netvm-proton.sh b/bin/netvm-proton.sh new file mode 100755 index 0000000..bc1f78b --- /dev/null +++ b/bin/netvm-proton.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# netvm-proton.sh -- +# Run proton-cli as the profile's Proton identity, inside the profile's netns +# (Warp egress) and inside a bwrap filesystem jail. +# 1:1:1: profile = node = warp identity = proton-cli profile. +# Human creates the session once: proton-cli -p account login. +# (Credential setup itself belongs to pix; see proton-ingest design D6.) +set -euo pipefail +NETVM_BIN="$(cd "$(dirname "$0")" && pwd)" +NODE="${1:?usage: netvm-proton.sh -- }"; shift +[ "${1:-}" = "--" ] && shift +[ $# -gt 0 ] || { echo "usage: netvm-proton.sh -- "; exit 1; } +[ -f "/etc/netvm/${NODE}.conf" ] || { echo "no warp identity for '$NODE' (human: netvm-new-identity.sh $NODE)"; exit 1; } +command -v bwrap >/dev/null 2>&1 || { echo "bwrap not found" >&2; exit 1; } +command -v proton-cli >/dev/null 2>&1 || { echo "proton-cli not found" >&2; exit 1; } + +PCLI_HOME="$HOME/.config/proton-cli" +[ -d "$PCLI_HOME" ] || { echo "no proton-cli config dir (human: proton-cli account login)"; exit 1; } +PCLI_BIN="$(readlink -f "$(command -v proton-cli)")" +[ -x "$PCLI_BIN" ] || { echo "proton-cli binary not executable: $PCLI_BIN"; exit 1; } + +# Jail: whole home is tmpfs except the proton-cli config (rw: sessions refresh, +# logs), the resolved static binary (ro), and a scratch tmp. proton-cli needs +# nothing else on disk. +BWRAP=(bwrap --unshare-uts --unshare-ipc --die-with-parent + --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp --tmpfs /dev/shm + --tmpfs "$HOME" --dir "$HOME/.config" + --bind "$PCLI_HOME" "$HOME/.config/proton-cli" + --ro-bind "$PCLI_BIN" /tmp/proton-cli + --setenv HOME "$HOME" --setenv PATH /usr/bin:/bin + --setenv PROTON_PROFILE "$NODE" + --setenv PROTON_NO_INPUT 1 + --unsetenv DISPLAY --unsetenv WAYLAND_DISPLAY --unsetenv XDG_RUNTIME_DIR) + +exec sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" \ + -- "${BWRAP[@]}" -- /tmp/proton-cli "$@"