145 lines
4.9 KiB
Text
145 lines
4.9 KiB
Text
|
|
#!/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/<device>/{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" <<EOF
|
||
|
|
# WireGuard config for $device
|
||
|
|
# Generated by session-tools/bin/wg-phone-add at $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||
|
|
# Hub: $hub ($hub_endpoint)
|
||
|
|
|
||
|
|
[Interface]
|
||
|
|
PrivateKey = $privkey
|
||
|
|
Address = $address/32
|
||
|
|
DNS = $mesh_dns
|
||
|
|
|
||
|
|
[Peer]
|
||
|
|
PublicKey = $hub_pubkey
|
||
|
|
Endpoint = $hub_endpoint
|
||
|
|
AllowedIPs = $mesh_subnet, $lan_subnet
|
||
|
|
PersistentKeepalive = 25
|
||
|
|
EOF
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "Client config written: $client_conf"
|
||
|
|
echo
|
||
|
|
echo "===== QR (scan with WireGuard iOS) ====="
|
||
|
|
qrencode -t ansiutf8 < "$client_conf"
|
||
|
|
echo "========================================"
|
||
|
|
echo
|
||
|
|
echo "Device: $device"
|
||
|
|
echo "Address: $address"
|
||
|
|
echo "Pubkey: $pubkey"
|
||
|
|
echo "Hub: $hub_endpoint"
|
||
|
|
echo
|
||
|
|
echo "After scanning: enable the tunnel in WireGuard iOS, then test from phone:"
|
||
|
|
echo " ping 10.0.0.116 (apricot LAN)"
|
||
|
|
echo " open https://quinn.apricot.local/"
|