#!/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). Idempotent. set -euo pipefail 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" # let the operator user stat (not read) identities: 711 dir, 600 files chmod 711 /etc/netvm 2>/dev/null || true nsexec() { ip netns exec "$NETNS" "$@"; } ip netns add "$NETNS" 2>/dev/null || true # veth pair host <-> netns if ! nsexec ip link show "$VPEER" >/dev/null 2>&1; then ip link del "$VETH" 2>/dev/null || true ip link add "$VETH" type veth peer name "$VPEER" ip link set "$VPEER" netns "$NETNS" ip addr add "${GW}/30" dev "$VETH" 2>/dev/null || true ip link set "$VETH" up nsexec ip addr add "${PEER_IP}/30" dev "$VPEER" 2>/dev/null || true nsexec ip link set "$VPEER" up fi nsexec ip link set lo up # host NAT + forwarding for the veth subnet sysctl -qw net.ipv4.ip_forward=1 # NB: never NAT host<->netvm traffic — SNATing the host's own veth IP breaks # host->netns connections (e.g. CDP): the reply would route out the tunnel. iptables -t nat -D POSTROUTING -s "$SUB" -j MASQUERADE 2>/dev/null || true # migrate old broad rule iptables -t nat -C POSTROUTING -s "$SUB" ! -d 10.201.0.0/16 -j MASQUERADE 2>/dev/null || \ iptables -t nat -A POSTROUTING -s "$SUB" ! -d 10.201.0.0/16 -j MASQUERADE # endpoint bypasses the tunnel (else the handshake routes into itself) ENDPOINT=$(grep -oP '^\s*Endpoint\s*=\s*\K[^:;#]+' "$CONF" | head -1) 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-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" nsexec wg setconf "$WG" "$STRIPPED" rm -f "$STRIPPED" # Persistent keepalive: without it, NAT mapping expires after 1-2 min of # inactivity and the tunnel silently dies (page loads, then network fails). PEER_PK=$(grep -oP '^\s*PublicKey\s*=\s*\K\S+' "$CONF" | head -1) if [ -n "$PEER_PK" ]; then nsexec wg set "$WG" peer "$PEER_PK" persistent-keepalive 25 2>/dev/null || true fi MTU=$(grep -oP '^\s*MTU\s*=\s*\K\d+' "$CONF" | head -1); MTU=${MTU:-1280} nsexec ip link set "$WG" mtu "$MTU" for a in $(grep -oP '^\s*Address\s*=\s*\K\S+' "$CONF" | tr ',' ' '); do if [[ "$a" == *:* ]]; then nsexec ip -6 addr add "$a" dev "$WG" 2>/dev/null || true else nsexec ip addr add "$a" dev "$WG" 2>/dev/null || true; fi done 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 # CDP bridge: chromium binds DevTools to loopback only. A tiny TCP relay # listens on the veth IP and forwards to loopback (empirically reliable; # iptables REDIRECT to 127.0.0.1 did not establish). Supervised via pidfile. PIDFILE="/run/netvm-${NODE}-cdp-relay.pid" if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "cdp relay already running" else nsexec python3 "$SCRIPT_DIR/netvm-cdp-relay.py" "$PEER_IP" "$CDP_PORT" 127.0.0.1 "$CDP_PORT" \ >/dev/null 2>&1 & echo $! > "$PIDFILE" echo "cdp relay started ($PEER_IP:$CDP_PORT -> 127.0.0.1:$CDP_PORT)" fi # fast path: if the tunnel already passes traffic, skip the handshake wait. # (WireGuard handshakes go stale without traffic; waiting 20s every launch # is the main reason chrome-box feels slow to start.) EGRESS=$(nsexec curl -sk --max-time 5 'https://1.1.1.1/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) if [ -n "$EGRESS" ]; then echo "tunnel already up (egress=$EGRESS), skipping handshake wait" else # 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)" else echo "handshake ok" fi EGRESS=$(nsexec curl -sk --max-time 15 'https://1.1.1.1/cdn-cgi/trace' 2>/dev/null | grep -oP '^ip=\K.*' || true) fi echo "node=$NODE netns=$NETNS ifaces=$WG/$VETH egress=${EGRESS:-unknown}"