Files
ai-app/scripts/wg-setup-host.sh
T

153 lines
6.0 KiB
Bash
Executable File

#!/bin/sh
# Sets up the WireGuard tunnel on the BACKEND HOST -- the machine that runs
# ai-server and that the phone dials in to. Run this on the host, not in the
# dev VM (the VM is behind qemu user-mode networking and has no inbound path;
# see AGENTS.md).
#
# sudo WG_ENDPOINT=your-name.duckdns.org scripts/wg-setup-host.sh
#
# What it creates:
# /etc/wireguard/wg0.conf the backend's tunnel: 10.66.0.1, port 51820
# /etc/wireguard/peers/phone.conf the phone's config, shown as a QR to scan
# and brings the interface up with wg-quick. Making it come back after a
# reboot is left to you: that is the one step whose commands differ per init
# system, and this script would only be guessing (the backend host is Gentoo,
# the dev VM is Arch). It prints what to run at the end.
#
# Addressing matches PLAN.md: the phone reaches the backend at 10.66.0.1 from
# everywhere, home or away -- one address in the app, one SAN in the leaf
# certificate, no home/away distinction. The phone's AllowedIPs is only
# 10.66.0.0/24, so this is a split tunnel: the phone's other traffic does not
# route through your house, and nothing here forwards or NATs.
#
# Re-running is safe: existing keys are reused, so the phone's config stays
# valid. Pass WG_NEW_PHONE_KEY=1 to issue a fresh phone keypair, which
# invalidates the old one.
#
# The one thing this cannot do for you: forward UDP 51820 from your router to
# this host. That is the only internet-facing hole, and it is silent to
# unauthenticated packets -- scanners see a closed port.
set -eu
WG_DIR=/etc/wireguard
PEER_DIR="$WG_DIR/peers"
SERVER_IP=10.66.0.1
PHONE_IP=10.66.0.2
SUBNET=10.66.0.0/24
PORT="${WG_PORT:-51820}"
ENDPOINT="${WG_ENDPOINT:-}"
if [ "$(id -u)" -ne 0 ]; then
echo "Run this with sudo -- it writes $WG_DIR and enables a service." >&2
exit 1
fi
for tool in wg wg-quick; do
command -v "$tool" >/dev/null || { echo "$tool not found: install wireguard-tools." >&2; exit 1; }
done
if [ -z "$ENDPOINT" ]; then
echo "Set WG_ENDPOINT to the hostname the phone should dial from outside," >&2
echo "e.g. WG_ENDPOINT=your-name.duckdns.org (a DDNS name, since a home IP" >&2
echo "can change). Then re-run." >&2
exit 1
fi
umask 077
mkdir -p "$PEER_DIR"
# Keys are generated here and never leave, except the phone's -- which is
# what the QR carries. Regenerating the server key would invalidate every
# peer, so it is created once and then reused.
if [ ! -f "$WG_DIR/server.key" ]; then
echo "==> Generating the backend's keypair"
wg genkey > "$WG_DIR/server.key"
wg pubkey < "$WG_DIR/server.key" > "$WG_DIR/server.pub"
else
echo "==> Reusing the backend's existing keypair"
fi
if [ ! -f "$PEER_DIR/phone.key" ] || [ -n "${WG_NEW_PHONE_KEY:-}" ]; then
echo "==> Generating the phone's keypair"
wg genkey > "$PEER_DIR/phone.key"
wg pubkey < "$PEER_DIR/phone.key" > "$PEER_DIR/phone.pub"
else
echo "==> Reusing the phone's existing keypair"
fi
echo "==> Writing $WG_DIR/wg0.conf"
cat > "$WG_DIR/wg0.conf" <<EOF
# Generated by ai-app/scripts/wg-setup-host.sh. The backend binds this interface's
# address and refuses to start without it (see server/src/main.rs).
[Interface]
Address = $SERVER_IP/24
ListenPort = $PORT
PrivateKey = $(cat "$WG_DIR/server.key")
[Peer]
PublicKey = $(cat "$PEER_DIR/phone.pub")
AllowedIPs = $PHONE_IP/32
EOF
echo "==> Writing $PEER_DIR/phone.conf"
cat > "$PEER_DIR/phone.conf" <<EOF
[Interface]
Address = $PHONE_IP/24
PrivateKey = $(cat "$PEER_DIR/phone.key")
[Peer]
PublicKey = $(cat "$WG_DIR/server.pub")
Endpoint = $ENDPOINT:$PORT
# Split tunnel: only the backend's subnet goes over WireGuard.
AllowedIPs = $SUBNET
# Keep the phone's NAT mapping reachable from the server.
PersistentKeepalive = 25
EOF
if wg show wg0 >/dev/null 2>&1; then
# Already up: load the new peers without dropping the interface, so a
# re-run doesn't kill a connected phone mid-session. `wg-quick strip`
# prints the config with the wg-quick-only keys removed, which is what
# `wg syncconf` accepts.
echo "==> wg0 is already up -- reloading its peers in place"
STRIPPED=$(mktemp)
trap 'rm -f "$STRIPPED"' EXIT
wg-quick strip wg0 > "$STRIPPED"
wg syncconf wg0 "$STRIPPED"
else
echo "==> Bringing wg0 up"
wg-quick up wg0
fi
sleep 1
wg show wg0 | sed 's/^/ /'
echo
echo "==> Phone config -- scan this with the WireGuard app (Add > Scan from QR code):"
echo
if command -v qrencode >/dev/null; then
qrencode -t ansiutf8 < "$PEER_DIR/phone.conf"
else
echo " (install qrencode to get a scannable QR; the config is below)"
sed 's/^/ /' "$PEER_DIR/phone.conf"
fi
echo
echo "Still to do, in order:"
echo " 0. Make wg0 come back after a reboot. Left to you rather than"
echo " guessed at, since the command depends on your init system:"
echo " OpenRC: ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.wg0"
echo " rc-update add wg-quick.wg0 default"
echo " systemd: systemctl enable wg-quick@wg0"
echo " (Gentoo with netifrc instead of wg-quick: configure net.wg0 in"
echo " /etc/conf.d/net -- see the WireGuard page on the Gentoo wiki.)"
echo " 1. Forward UDP $PORT on your router to this host. That is the only"
echo " internet-facing port; it stays silent to unauthenticated packets."
echo " 2. Point $ENDPOINT at your home IP (DDNS client on the router, or a"
echo " curl cron here). WireGuard on the phone resolves this once when the"
echo " tunnel comes up, so after a rare IP change, toggle the tunnel."
echo " 3. Check NAT hairpinning works at home: with the tunnel on and the"
echo " phone on your wifi, 'ping $SERVER_IP' from the phone should answer."
echo " If it doesn't, your router can't hairpin -- turn the tunnel off at"
echo " home, or use a split-DNS entry pointing $ENDPOINT at the LAN IP."
echo " 4. Start the backend here (it binds $SERVER_IP only, and refuses to"
echo " start if wg0 is down):"
echo " cd $(dirname "$(dirname "$(readlink -f "$0")")") && ./server/target/release/ai-server"
echo " Add --rotate-token once to print a fresh enrollment QR for the app."