NetVM: 1:1 profile:node mapping — hashed iface names, netvm-enter + netvm-chrome launcher

This commit is contained in:
Antigravity Agent
2026-10-03 00:43:08 -04:00
parent 218a527623
commit b86b6fa4b5
8 changed files with 84 additions and 46 deletions
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# netvm-chrome.sh <profile> [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 <profile> [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
+6
View File
@@ -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" "$@"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# netvm-enter.sh <node> <uid> <gid> <home> -- <cmd> [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" "$@"
+13
View File
@@ -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"
}
+7 -9
View File
@@ -1,16 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# netvm-node-down.sh <node> — tear down a node's Warp egress. Run as root. # netvm-node-down.sh <node> — tear down a node's Warp egress. Run as root.
set -euo pipefail set -euo pipefail
NODE="${1:?usage: netvm-node-down.sh <node>}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NETNS="warp-${NODE}" . "$SCRIPT_DIR/netvm-names.sh"
VETH="veth-${NODE}" netvm_names "${1:?usage: netvm-node-down.sh <node>}"
IDX=$(( $(echo -n "$NODE" | cksum | cut -d' ' -f1) % 60 + 10 ))
SUB="10.201.${IDX}.0/30"
nsexec() { ip netns exec "$NETNS" "$@"; } nsexec() { ip netns exec "$NETNS" "$@"; }
nsexec ip link del "wg-${NODE}" 2>/dev/null || true # inside netns first 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 "$VETH" 2>/dev/null || true # also drops the netns-side peer
ip link del "wg-${NODE}" 2>/dev/null || true # stray host-side copy 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 iptables -t nat -D POSTROUTING -s "$SUB" -j MASQUERADE 2>/dev/null || true
ip netns del "$NETNS" 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" echo "node=$NODE down"
+16 -25
View File
@@ -1,26 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# netvm-node-up.sh <node> — bring up a node's Warp egress in its own netns. # netvm-node-up.sh <node> — bring up a node's Warp egress in its own netns.
# Run as root (operator: sudo -n via the allowlist). # Run as root (operator: sudo -n via the allowlist). Idempotent.
#
# Per-node layout:
# netns warp-<node>; veth pair veth-<node> <-> vpeer-<node> (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.
set -euo pipefail set -euo pipefail
NODE="${1:?usage: netvm-node-up.sh <node>}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
NETNS="warp-${NODE}" . "$SCRIPT_DIR/netvm-names.sh"
WG="wg-${NODE}" netvm_names "${1:?usage: netvm-node-up.sh <node>}"
VETH="veth-${NODE}"
VPEER="vpeer-${NODE}"
CONF="/etc/netvm/${NODE}.conf" CONF="/etc/netvm/${NODE}.conf"
[ -f "$CONF" ] || { echo "missing $CONF (human: netvm-new-identity.sh $NODE)"; exit 1; } [ -f "$CONF" ] || { echo "missing $CONF (human: netvm-new-identity.sh $NODE)"; exit 1; }
chmod 600 "$CONF" 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" "$@"; } nsexec() { ip netns exec "$NETNS" "$@"; }
ip netns add "$NETNS" 2>/dev/null || true 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" nsexec ip route replace "$eip" via "$GW"
done 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 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 del "$WG" 2>/dev/null || true # stale host-side (reaped zombie netns)
ip link add "$WG" type wireguard ip link add "$WG" type wireguard
ip link set "$WG" netns "$NETNS" ip link set "$WG" netns "$NETNS"
fi fi
STRIPPED=$(mktemp) 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" nsexec wg setconf "$WG" "$STRIPPED"
rm -f "$STRIPPED" rm -f "$STRIPPED"
MTU=$(grep -oP '^\s*MTU\s*=\s*\K\d+' "$CONF" | head -1); MTU=${MTU:-1280} 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 route replace default dev "$WG"
nsexec ip -6 route replace default dev "$WG" 2>/dev/null || true 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) # wait for handshake (first one can take ~10s)
sleep 3 HS=""
HS=$(nsexec wg show "$WG" latest-handshakes | awk '{print $2}') 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 if [ -z "$HS" ] || [ "$HS" = "0" ]; then
echo "no handshake yet (endpoint=$ENDPOINT) — may still be negotiating" echo "no handshake yet (endpoint=$ENDPOINT)"
else else
echo "handshake ok" echo "handshake ok"
fi fi
EGRESS=$(nsexec curl -sk --max-time 15 'https://[2606:4700:4700::1111]/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) 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 veth=$SUB egress=${EGRESS:-unknown}" echo "node=$NODE netns=$NETNS ifaces=$WG/$VETH egress=${EGRESS:-unknown}"
+3 -1
View File
@@ -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. # 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. # 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. # 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 SUDOERS
sudo chmod 440 "$SUDOERS_FILE" sudo chmod 440 "$SUDOERS_FILE"
sudo visudo -c -f "$SUDOERS_FILE" > /dev/null && echo "ok: sudoers valid" sudo visudo -c -f "$SUDOERS_FILE" > /dev/null && echo "ok: sudoers valid"
+6 -11
View File
@@ -2,18 +2,13 @@
# netvm-topology.sh — live topology + per-node diagnostics. # netvm-topology.sh — live topology + per-node diagnostics.
# Run as root (operator: sudo -n via the allowlist). # Run as root (operator: sudo -n via the allowlist).
set -u 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 for ns in $(ip netns list 2>/dev/null | awk '{print $1}' | grep '^warp-'); do
node="${ns#warp-}" netvm_names "${ns#warp-}"
hs=$(ip netns exec "$ns" wg show "wg-${node}" latest-handshakes 2>/dev/null | awk '{print $2}') hs=$(ip netns exec "$ns" wg show "$WG" latest-handshakes 2>/dev/null | awk '{print $2}')
[ -z "$hs" ] && hs="none" [ -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) 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 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)"
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)"