From b86b6fa4b57116ca3aae89d8f1d4a1d87c146d40 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Sat, 3 Oct 2026 00:43:08 -0400 Subject: [PATCH] =?UTF-8?q?NetVM:=201:1=20profile:node=20mapping=20?= =?UTF-8?q?=E2=80=94=20hashed=20iface=20names,=20netvm-enter=20+=20netvm-c?= =?UTF-8?q?hrome=20launcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/netvm-chrome.sh | 18 ++++++++++++++++ bin/netvm-enter-inner.sh | 6 ++++++ bin/netvm-enter.sh | 15 ++++++++++++++ bin/netvm-names.sh | 13 ++++++++++++ bin/netvm-node-down.sh | 16 +++++++-------- bin/netvm-node-up.sh | 41 +++++++++++++++---------------------- bin/netvm-provision-edge.sh | 4 +++- bin/netvm-topology.sh | 17 ++++++--------- 8 files changed, 84 insertions(+), 46 deletions(-) create mode 100755 bin/netvm-chrome.sh create mode 100755 bin/netvm-enter-inner.sh create mode 100755 bin/netvm-enter.sh create mode 100755 bin/netvm-names.sh diff --git a/bin/netvm-chrome.sh b/bin/netvm-chrome.sh new file mode 100755 index 0000000..44d360c --- /dev/null +++ b/bin/netvm-chrome.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# netvm-chrome.sh [url] — launch a chrome-box profile inside its +# dedicated NetVM netns. 1:1: profile = node = warp identity = egress IP. +# Run as your normal user (uses sudo -n only for allowlisted netns ops). +set -euo pipefail +PROFILE="${1:?usage: netvm-chrome.sh [url]}" +URL="${2:-}" +NODE="$PROFILE" +NETVM_BIN="$(cd "$(dirname "$0")" && pwd)" +CHROME_BOX="${CHROME_BOX_BIN:-$HOME/Projects/chrome-box/chrome-box}" +[ -x "$CHROME_BOX" ] || { echo "chrome-box not found at $CHROME_BOX (set CHROME_BOX_BIN)"; exit 1; } +[ -f "/etc/netvm/${NODE}.conf" ] || { echo "no warp identity for '$NODE' — human: netvm-new-identity.sh $NODE"; exit 1; } +sudo -n "$NETVM_BIN/netvm-node-up.sh" "$NODE" | tail -2 +if [ -n "$URL" ]; then + exec sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "$CHROME_BOX" launch "$PROFILE" "$URL" +else + exec sudo -n "$NETVM_BIN/netvm-enter.sh" "$NODE" "$(id -u)" "$(id -g)" "$HOME" -- "$CHROME_BOX" launch "$PROFILE" +fi diff --git a/bin/netvm-enter-inner.sh b/bin/netvm-enter-inner.sh new file mode 100755 index 0000000..bdd47a1 --- /dev/null +++ b/bin/netvm-enter-inner.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# runs as root inside the netns + a private mount ns; drops privs, execs cmd. +set -euo pipefail +mount --bind "$NETVM_RESOLV" /etc/resolv.conf +exec setpriv --reuid="$NETVM_UID" --regid="$NETVM_GID" --clear-groups \ + env HOME="$NETVM_HOME" "$@" diff --git a/bin/netvm-enter.sh b/bin/netvm-enter.sh new file mode 100755 index 0000000..1041e85 --- /dev/null +++ b/bin/netvm-enter.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# netvm-enter.sh -- [args...] +# Enter the node's netns, bind a working resolv.conf (host uses the +# systemd-resolved stub 127.0.0.53, unreachable in the netns), drop +# privileges, exec the command. Run as root (sudo -n via the allowlist). +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/netvm-names.sh" +netvm_names "$1"; TUID="$2"; TGID="$3"; THOME="$4"; shift 4 +[ "${1:-}" = "--" ]; shift +RESOLV=/etc/netvm/resolv-warp.conf +[ -f "$RESOLV" ] || echo "nameserver 1.1.1.1" > "$RESOLV" +ip netns exec "$NETNS" env \ + NETVM_RESOLV="$RESOLV" NETVM_UID="$TUID" NETVM_GID="$TGID" NETVM_HOME="$THOME" \ + unshare --mount "$SCRIPT_DIR/netvm-enter-inner.sh" "$@" diff --git a/bin/netvm-names.sh b/bin/netvm-names.sh new file mode 100755 index 0000000..ceb1652 --- /dev/null +++ b/bin/netvm-names.sh @@ -0,0 +1,13 @@ +# netvm-names.sh — source this. Sets NETNS WG VETH VPEER SUB GW PEER_IP from NODE. +# 1:1 mapping: profile name = node name. Interface names must fit 15 chars, +# so wg/veth use a deterministic hash tag; the netns keeps the full name. +netvm_names() { + NODE="$1" + NETNS="warp-${NODE}" + _tag=$(echo -n "$NODE" | sha256sum | cut -c1-8) + WG="wb-${_tag}" + VETH="ve-${_tag}" + VPEER="vp-${_tag}" + _idx=$(( 0x${_tag:0:3} % 200 + 10 )) + GW="10.201.${_idx}.1"; PEER_IP="10.201.${_idx}.2"; SUB="10.201.${_idx}.0/30" +} diff --git a/bin/netvm-node-down.sh b/bin/netvm-node-down.sh index 4479bad..45db6fa 100755 --- a/bin/netvm-node-down.sh +++ b/bin/netvm-node-down.sh @@ -1,16 +1,14 @@ #!/usr/bin/env bash # netvm-node-down.sh — tear down a node's Warp egress. Run as root. set -euo pipefail -NODE="${1:?usage: netvm-node-down.sh }" -NETNS="warp-${NODE}" -VETH="veth-${NODE}" -IDX=$(( $(echo -n "$NODE" | cksum | cut -d' ' -f1) % 60 + 10 )) -SUB="10.201.${IDX}.0/30" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/netvm-names.sh" +netvm_names "${1:?usage: netvm-node-down.sh }" nsexec() { ip netns exec "$NETNS" "$@"; } -nsexec ip link del "wg-${NODE}" 2>/dev/null || true # inside netns first -ip link del "$VETH" 2>/dev/null || true # also drops the netns-side peer -ip link del "wg-${NODE}" 2>/dev/null || true # stray host-side copy +nsexec ip link del "$WG" 2>/dev/null || true # inside netns first +ip link del "$VETH" 2>/dev/null || true # also drops the netns-side peer +ip link del "$WG" 2>/dev/null || true # stray host-side copy iptables -t nat -D POSTROUTING -s "$SUB" -j MASQUERADE 2>/dev/null || true ip netns del "$NETNS" 2>/dev/null || true -ip link del "wg-${NODE}" 2>/dev/null || true # final sweep post-reap +ip link del "$WG" 2>/dev/null || true # final sweep post-reap echo "node=$NODE down" diff --git a/bin/netvm-node-up.sh b/bin/netvm-node-up.sh index bdfc0c9..b53f69a 100755 --- a/bin/netvm-node-up.sh +++ b/bin/netvm-node-up.sh @@ -1,26 +1,13 @@ #!/usr/bin/env bash # netvm-node-up.sh — bring up a node's Warp egress in its own netns. -# Run as root (operator: sudo -n via the allowlist). -# -# Per-node layout: -# netns warp-; veth pair veth- <-> vpeer- (10.201.X.0/30) -# with host NAT; WireGuard handshake packets route via the veth gateway -# (bypassing the tunnel — else they'd loop into it); everything else -# defaults through the tunnel. +# Run as root (operator: sudo -n via the allowlist). Idempotent. set -euo pipefail -NODE="${1:?usage: netvm-node-up.sh }" -NETNS="warp-${NODE}" -WG="wg-${NODE}" -VETH="veth-${NODE}" -VPEER="vpeer-${NODE}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/netvm-names.sh" +netvm_names "${1:?usage: netvm-node-up.sh }" CONF="/etc/netvm/${NODE}.conf" [ -f "$CONF" ] || { echo "missing $CONF (human: netvm-new-identity.sh $NODE)"; exit 1; } chmod 600 "$CONF" - -# deterministic /30 per node for the veth link -IDX=$(( $(echo -n "$NODE" | cksum | cut -d' ' -f1) % 60 + 10 )) -GW="10.201.${IDX}.1"; PEER_IP="10.201.${IDX}.2"; SUB="10.201.${IDX}.0/30" - nsexec() { ip netns exec "$NETNS" "$@"; } ip netns add "$NETNS" 2>/dev/null || true @@ -48,14 +35,14 @@ for eip in $(getent ahostsv4 "$ENDPOINT" | awk '{print $1}' | sort -u); do nsexec ip route replace "$eip" via "$GW" done -# wireguard interface (wg setconf rejects wg-quick extensions: strip them) +# wireguard interface (wg setconf rejects wg-quick-only keys: strip them) if ! nsexec ip link show "$WG" >/dev/null 2>&1; then ip link del "$WG" 2>/dev/null || true # stale host-side (reaped zombie netns) ip link add "$WG" type wireguard ip link set "$WG" netns "$NETNS" fi STRIPPED=$(mktemp) -grep -vE '^\s*(Address|DNS|MTU|Table|PreUp|PreDown|PostUp|PostDown|SaveConfig)\s*' "$CONF" > "$STRIPPED" +grep -vE '^\s*(Address|DNS|MTU|Table|PreUp|PreDown|PostUp|PostDown|SaveConfig)\s*=' "$CONF" > "$STRIPPED" nsexec wg setconf "$WG" "$STRIPPED" rm -f "$STRIPPED" MTU=$(grep -oP '^\s*MTU\s*=\s*\K\d+' "$CONF" | head -1); MTU=${MTU:-1280} @@ -68,13 +55,17 @@ nsexec ip link set "$WG" up nsexec ip route replace default dev "$WG" nsexec ip -6 route replace default dev "$WG" 2>/dev/null || true -# verify: handshake = tunnel up (definitive); egress IP best-effort (no DNS in netns) -sleep 3 -HS=$(nsexec wg show "$WG" latest-handshakes | awk '{print $2}') +# wait for handshake (first one can take ~10s) +HS="" +for i in $(seq 1 10); do + HS=$(nsexec wg show "$WG" latest-handshakes 2>/dev/null | awk '{print $2}') + if [ -n "$HS" ] && [ "$HS" != "0" ]; then break; fi + sleep 2 +done if [ -z "$HS" ] || [ "$HS" = "0" ]; then - echo "no handshake yet (endpoint=$ENDPOINT) — may still be negotiating" + echo "no handshake yet (endpoint=$ENDPOINT)" else echo "handshake ok" fi -EGRESS=$(nsexec curl -sk --max-time 15 'https://[2606:4700:4700::1111]/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) -echo "node=$NODE netns=$NETNS veth=$SUB egress=${EGRESS:-unknown}" +EGRESS=$(nsexec curl -sk --max-time 15 'https://1.1.1.1/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) +echo "node=$NODE netns=$NETNS ifaces=$WG/$VETH egress=${EGRESS:-unknown}" diff --git a/bin/netvm-provision-edge.sh b/bin/netvm-provision-edge.sh index 2c741d4..a9974e4 100755 --- a/bin/netvm-provision-edge.sh +++ b/bin/netvm-provision-edge.sh @@ -32,7 +32,9 @@ sudo tee "$SUDOERS_FILE" > /dev/null << SUDOERS # NetVM operator lifecycle access — managed by netvm-provision-edge.sh. Named zz- so it sorts last: in sudoers the LAST matching entry wins, and this must beat any blanket (ALL) grant. # Lets the operator user bring node egress up/down and read topology. # WireGuard configs in /etc/netvm stay root-only; these scripts never print them. -$OPERATOR_USER ALL=(root) NOPASSWD: $REPO/bin/netvm-node-up.sh *, $REPO/bin/netvm-node-down.sh *, $REPO/bin/netvm-topology.sh +$OPERATOR_USER ALL=(root) NOPASSWD: $REPO/bin/netvm-node-up.sh *, $REPO/bin/netvm-node-down.sh *, $REPO/bin/netvm-topology.sh, $REPO/bin/netvm-enter.sh * +# let the display env survive sudo for browser launches inside the netns +Defaults!$REPO/bin/netvm-enter.sh env_keep+="DISPLAY WAYLAND_DISPLAY XDG_RUNTIME_DIR XAUTHORITY" SUDOERS sudo chmod 440 "$SUDOERS_FILE" sudo visudo -c -f "$SUDOERS_FILE" > /dev/null && echo "ok: sudoers valid" diff --git a/bin/netvm-topology.sh b/bin/netvm-topology.sh index ecdf620..51b749b 100755 --- a/bin/netvm-topology.sh +++ b/bin/netvm-topology.sh @@ -2,18 +2,13 @@ # netvm-topology.sh — live topology + per-node diagnostics. # Run as root (operator: sudo -n via the allowlist). set -u +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/netvm-names.sh" for ns in $(ip netns list 2>/dev/null | awk '{print $1}' | grep '^warp-'); do - node="${ns#warp-}" - hs=$(ip netns exec "$ns" wg show "wg-${node}" latest-handshakes 2>/dev/null | awk '{print $2}') + netvm_names "${ns#warp-}" + hs=$(ip netns exec "$ns" wg show "$WG" latest-handshakes 2>/dev/null | awk '{print $2}') [ -z "$hs" ] && hs="none" - routes=$(ip netns exec "$ns" ip route 2>/dev/null | tr '\n' '|' ) egress=$(ip netns exec "$ns" curl -sk --max-time 10 'https://1.1.1.1/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) - printf 'node=%s netns=%s handshake=%s egress=%s\n routes: %s\n' "$node" "$ns" "$hs" "${egress:-?}" "${routes:-none}" + printf 'node=%s netns=%s ifaces=%s/%s handshake=%s egress=%s\n' "$NODE" "$ns" "$WG" "$VETH" "$hs" "${egress:-?}" done -# host-side NAT rules for our subnets -iptables -t nat -L POSTROUTING -n 2>/dev/null | grep '10.201\.' || echo "no netvm NAT rules on host" - -echo "--- host links ---" -ip link show 2>/dev/null | grep -E 'veth-|vpeer-|wg-' || echo "(no netvm links on host)" -echo "--- all netns ---" -ip netns list 2>/dev/null || echo "(none)" +iptables -t nat -L POSTROUTING -n 2>/dev/null | grep '10.201\.' || echo "(no netvm NAT rules on host)"