WireGuard bring-up: host setup script, in-VM test tunnel, portable repo_root
wg-setup-host.sh sets up the tunnel on the backend host: keys generated there and kept there, wg0.conf, wg-quick enabled, and the phone's config printed as a scannable QR. Split tunnel (AllowedIPs is only the backend subnet), single-address addressing per PLAN.md, and the three things it can't do for you -- router UDP forward, DDNS, hairpin check -- spelled out at the end. test-wg-tunnel.sh stands up a real WireGuard tunnel between two network namespaces inside one machine, so the production posture (bind wg0 and nothing else) is testable with no router, phone, or internet exposure. Verified: real handshake, server listening on 10.66.0.1:8443 only, and an authorized request from inside the tunnel answering 200 over pinned TLS -- the leaf's 10.66.0.1 SAN is what a phone will validate too. repo_root() now resolves from the running executable before falling back to the compiled-in path: the repo is shared host<->VM over virtiofs at different absolute paths with a shared target/, so a binary built on one side and run on the other looked for its config where nothing exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
This commit is contained in:
1 parent
8841effc6a
commit
29f8b31f0b
5 files changed
+324
-4
No files matched your search
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/bin/sh
|
||||
# Stands up a real WireGuard tunnel entirely inside this machine, so the
|
||||
# server's production network posture -- "bind wg0 and nothing else, fail
|
||||
# closed if it's missing" -- can be exercised without a phone, a router
|
||||
# port-forward, or any internet exposure.
|
||||
#
|
||||
# The shape, all in one kernel:
|
||||
#
|
||||
# main netns "phone" netns
|
||||
# wg0 10.66.0.1 <-- encrypted --> wg1 10.66.0.2
|
||||
# | |
|
||||
# veth-srv 10.99.0.1 <--- UDP ---> veth-phone 10.99.0.2
|
||||
#
|
||||
# The veth pair stands in for "the internet" carrying WireGuard's UDP; the
|
||||
# wg interfaces are real, with a real handshake and real keys. 10.66.0.1 is
|
||||
# deliberately the same address the leaf certificate carries a SAN for
|
||||
# (gen-dev-cert.sh), so a client inside the tunnel completes the same
|
||||
# pinned-TLS handshake a phone will.
|
||||
#
|
||||
# ./test-wg-tunnel.sh up create the tunnel (needs sudo)
|
||||
# ./test-wg-tunnel.sh test run the server on wg0 and reach it from "phone"
|
||||
# ./test-wg-tunnel.sh down remove everything it created
|
||||
#
|
||||
# Everything here is torn down by `down`: the netns (taking wg1 and the veth
|
||||
# peer with it), wg0, and the temporary key files.
|
||||
|
||||
set -eu
|
||||
|
||||
NS=phone
|
||||
WG_SERVER=wg0
|
||||
WG_CLIENT=wg1
|
||||
SERVER_WG_IP=10.66.0.1
|
||||
CLIENT_WG_IP=10.66.0.2
|
||||
SERVER_UDP_IP=10.99.0.1
|
||||
CLIENT_UDP_IP=10.99.0.2
|
||||
LISTEN_PORT=51820
|
||||
KEYDIR=/run/ai-app-wg-test
|
||||
REPO=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
up() {
|
||||
echo "==> Generating ephemeral keypairs in $KEYDIR"
|
||||
sudo mkdir -p "$KEYDIR"
|
||||
sudo sh -c "umask 077; wg genkey > $KEYDIR/server.key; wg genkey > $KEYDIR/client.key"
|
||||
sudo sh -c "wg pubkey < $KEYDIR/server.key > $KEYDIR/server.pub"
|
||||
sudo sh -c "wg pubkey < $KEYDIR/client.key > $KEYDIR/client.pub"
|
||||
|
||||
echo "==> Creating netns '$NS' and the veth pair that carries the UDP"
|
||||
sudo ip netns add "$NS"
|
||||
sudo ip link add veth-srv type veth peer name veth-phone
|
||||
sudo ip link set veth-phone netns "$NS"
|
||||
sudo ip addr add "$SERVER_UDP_IP/24" dev veth-srv
|
||||
sudo ip link set veth-srv up
|
||||
sudo ip -n "$NS" addr add "$CLIENT_UDP_IP/24" dev veth-phone
|
||||
sudo ip -n "$NS" link set veth-phone up
|
||||
sudo ip -n "$NS" link set lo up
|
||||
|
||||
echo "==> Creating $WG_SERVER (server side, $SERVER_WG_IP)"
|
||||
sudo ip link add "$WG_SERVER" type wireguard
|
||||
sudo sh -c "wg set $WG_SERVER listen-port $LISTEN_PORT private-key $KEYDIR/server.key \
|
||||
peer \$(cat $KEYDIR/client.pub) allowed-ips $CLIENT_WG_IP/32"
|
||||
sudo ip addr add "$SERVER_WG_IP/24" dev "$WG_SERVER"
|
||||
sudo ip link set "$WG_SERVER" up
|
||||
|
||||
# Created in the main namespace, then moved: a wireguard interface keeps
|
||||
# its UDP socket in the namespace it was born in, which is exactly what
|
||||
# lets the "phone" reach the server's veth address from inside its own.
|
||||
echo "==> Creating $WG_CLIENT (phone side, $CLIENT_WG_IP) in netns '$NS'"
|
||||
sudo ip link add "$WG_CLIENT" type wireguard
|
||||
sudo ip link set "$WG_CLIENT" netns "$NS"
|
||||
sudo ip netns exec "$NS" sh -c "wg set $WG_CLIENT private-key $KEYDIR/client.key \
|
||||
peer \$(cat $KEYDIR/server.pub) allowed-ips $SERVER_WG_IP/32 \
|
||||
endpoint $SERVER_UDP_IP:$LISTEN_PORT persistent-keepalive 5"
|
||||
sudo ip -n "$NS" addr add "$CLIENT_WG_IP/24" dev "$WG_CLIENT"
|
||||
sudo ip -n "$NS" link set "$WG_CLIENT" up
|
||||
|
||||
echo "==> Forcing a handshake"
|
||||
sudo ip netns exec "$NS" ping -c 2 -W 3 "$SERVER_WG_IP" >/dev/null 2>&1 || true
|
||||
sudo wg show "$WG_SERVER" | sed 's/^/ /'
|
||||
echo "==> Up. wg0 is $SERVER_WG_IP; run '$0 test' next."
|
||||
}
|
||||
|
||||
test_tunnel() {
|
||||
if [ ! -f "$REPO/certs/leaf.pem" ]; then
|
||||
echo "No certs/ -- run ./gen-dev-cert.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$REPO/server/target/debug/ai-server" ]; then
|
||||
echo "Build the server first: (cd server && cargo build)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Starting ai-server with NO --bind (production path: wg0 only)"
|
||||
setsid nohup "$REPO/server/target/debug/ai-server" \
|
||||
</dev/null >"$REPO/server/wg-test.log" 2>&1 &
|
||||
sleep 2
|
||||
|
||||
echo "==> Where is it actually listening?"
|
||||
ss -tlnp 2>/dev/null | grep 8443 | sed 's/^/ /' || echo " (nothing on 8443)"
|
||||
|
||||
echo "==> From inside the tunnel: GET /sessions through wg1 -> wg0"
|
||||
TOKEN=$(sudo cat "$REPO/config.json" 2>/dev/null | sed -n 's/.*"sha256": "\(.*\)".*/\1/p' | head -1)
|
||||
if [ -z "${AI_TOKEN:-}" ]; then
|
||||
echo " (set AI_TOKEN=<the enrollment token> to test an authorized call;"
|
||||
echo " without it this only proves reachability + TLS, via a 401)"
|
||||
fi
|
||||
sudo ip netns exec "$NS" curl -s -o /dev/null -w " HTTP %{http_code} (TLS ok, pinned CA)\n" \
|
||||
--cacert "$REPO/certs/ca.pem" \
|
||||
${AI_TOKEN:+-H "Authorization: Bearer $AI_TOKEN"} \
|
||||
"https://$SERVER_WG_IP:8443/sessions" || echo " UNREACHABLE"
|
||||
|
||||
echo "==> Handshake counters (proves the traffic really crossed WireGuard)"
|
||||
sudo wg show "$WG_SERVER" transfer | sed 's/^/ /'
|
||||
|
||||
pkill -f "[a]i-server" || true
|
||||
echo "==> Server stopped."
|
||||
}
|
||||
|
||||
down() {
|
||||
echo "==> Removing tunnel"
|
||||
sudo ip netns del "$NS" 2>/dev/null || true
|
||||
sudo ip link del "$WG_SERVER" 2>/dev/null || true
|
||||
sudo ip link del veth-srv 2>/dev/null || true
|
||||
sudo rm -rf "$KEYDIR"
|
||||
echo "==> Down."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
up) up ;;
|
||||
test) test_tunnel ;;
|
||||
down) down ;;
|
||||
*) echo "usage: $0 up|test|down" >&2; exit 1 ;;
|
||||
esac
|
||||
Reference in new issue
Block a user