#!/bin/sh # wg-phone-add — generate (or reuse) a WireGuard peer for a phone/tablet, # add it to the quinn-vps wg1 hub, render the client config + QR. # # Idempotent: re-runs reuse the stored keypair and just re-display the QR. # # Keys live OUTSIDE this repo (private keys must not be committed): # ~/.config/wg-mesh/clients//{private.key,public.key,address} # # Usage: # wg-phone-add # default device "phone-quinn", auto-pick IP # wg-phone-add -d phone-rachel # named device, auto-pick IP # wg-phone-add -d ipad-quinn -i 10.9.0.6 # wg-phone-add -d phone-quinn --show # just re-render QR for an existing client # # Exit codes: # 0 success # 1 missing dep / invalid input # 2 ssh to hub failed # 3 hub config edit / wg syncconf failed set -eu device="phone-quinn" forced_ip="" show_only=0 while [ $# -gt 0 ]; do case $1 in -d) device=$2; shift 2 ;; -i) forced_ip=$2; shift 2 ;; --show) show_only=1; shift ;; -h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done # Validate device name (used in filenames + comment in hub conf). case $device in *[!A-Za-z0-9_-]*|"") echo "invalid device name: $device" >&2; exit 1 ;; esac for cmd in ssh wg qrencode jq; do command -v $cmd >/dev/null || { echo "$cmd not installed" >&2; exit 1; } done hub=quinn-vps hub_endpoint=89.127.233.145:51820 hub_pubkey=t6LT6Ff4AxcGCd9Zug7dxkT7tXhkMV+fd28UCY/h8Xw= mesh_subnet=10.9.0.0/24 lan_subnet=10.0.0.0/24 mesh_dns=10.9.0.2 # apricot, where wg-dns-sync runs client_dir="$HOME/.config/wg-mesh/clients/$device" mkdir -p "$client_dir" chmod 700 "$client_dir" # 1. Reuse or generate keypair. if [ -s "$client_dir/private.key" ] && [ -s "$client_dir/public.key" ]; then echo "wg-phone-add: reusing existing keypair at $client_dir" privkey=$(cat "$client_dir/private.key") pubkey=$(cat "$client_dir/public.key") else [ "$show_only" -eq 1 ] && { echo "wg-phone-add: --show given but no keypair at $client_dir" >&2; exit 1; } echo "wg-phone-add: generating new keypair" privkey=$(wg genkey) pubkey=$(echo "$privkey" | wg pubkey) umask 077 printf %s "$privkey" > "$client_dir/private.key" printf %s "$pubkey" > "$client_dir/public.key" fi # 2. Pick or validate the WG IP slot. if [ -n "$forced_ip" ]; then address=$forced_ip elif [ -s "$client_dir/address" ]; then address=$(cat "$client_dir/address") else # Find lowest free .5..254 in 10.9.0.0/24 by reading hub config over ssh. used=$(ssh -o BatchMode=yes "$hub" "grep -hoE '10\\.9\\.0\\.[0-9]+' /etc/wireguard/wg1.conf | sort -u") address="" for n in $(seq 5 254); do candidate="10.9.0.$n" echo "$used" | grep -qx "$candidate" || { address=$candidate; break; } done [ -n "$address" ] || { echo "no free 10.9.0.x slot in /24" >&2; exit 1; } fi printf %s "$address" > "$client_dir/address" # 3. (skipped if --show) ensure peer block is on hub. if [ "$show_only" -eq 0 ]; then # Probe whether hub already has this pubkey configured. if ssh -o BatchMode=yes "$hub" "grep -qxF 'PublicKey = $pubkey' /etc/wireguard/wg1.conf"; then echo "wg-phone-add: hub already has peer with this pubkey, skipping config edit" else echo "wg-phone-add: appending [Peer] block to $hub:/etc/wireguard/wg1.conf" # Use printf to avoid heredoc quoting headaches over ssh. # NOTE: assumes ssh user on quinn-vps can write /etc/wireguard/wg1.conf. # On quinn-vps the user is root (no sudo), per existing topology. if ! ssh -o BatchMode=yes "$hub" "printf '\n[Peer]\n# %s\nPublicKey = %s\nAllowedIPs = %s/32\nPersistentKeepalive = 25\n' '$device' '$pubkey' '$address' >> /etc/wireguard/wg1.conf"; then echo "ssh write to hub failed" >&2; exit 3 fi # Live-reload without disturbing existing tunnels. if ! ssh -o BatchMode=yes "$hub" "wg syncconf wg1 <(wg-quick strip wg1)"; then echo "wg syncconf on hub failed" >&2; exit 3 fi fi fi # 4. Render the client config. client_conf="$client_dir/$device.conf" umask 077 cat > "$client_conf" <