net-tools: fix wg-render apply (set -e abort + dash syncconf), nyc3 endpoint

Two bugs found bringing the nyc3 segment live (citron hub + lime spoke):
- Hub render ended in `[ -n "$miss" ] && echo`, which returns 1 when no spokes
  are unkeyed; under `set -e` that silently aborted `render_conf > tmp` on the
  apply path (spokes were fine — they end in printf). Use an if-block.
- `wg syncconf <(wg-quick strip)` used bash process substitution but the script
  runs under /bin/sh (dash) — replaced with a POSIX temp file.

Also: nyc3 endpoint -> citron's bound public IP (104.248.9.88), not the reserved
IP (143.244.223.5) — DO routes the reserved IP in but WG replies from the
primary, so the reserved IP can't be a WG endpoint without anchor source-routing.

Verified live: lime<->citron handshake, ping 10.9.0.7 0% loss, citron dnsmasq
resolving *.wg on 10.9.0.7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 21:50:52 -04:00
parent de1f7f2dec
commit 53a79d3494
2 changed files with 14 additions and 6 deletions

View file

@ -166,10 +166,14 @@ render_conf() {
| select(.wg_pubkey != null and .wg_pubkey != \"\") | select(.wg_pubkey != null and .wg_pubkey != \"\")
| \"# \(.name)\n[Peer]\nPublicKey = \(.wg_pubkey)\nAllowedIPs = \(.wg)/32\n\" | \"# \(.name)\n[Peer]\nPublicKey = \(.wg_pubkey)\nAllowedIPs = \(.wg)/32\n\"
" "$data_file" " "$data_file"
# Warn (to stderr) about spokes still missing a key. # Warn (to stderr) about spokes still missing a key. Use an if-block, not
# `[ ... ] && echo`: the latter returns 1 when the test is false, which
# under `set -e` (apply path: render_conf > tmp) aborts the whole render.
miss=$(jq -r --arg SEG "${self_seg:-}" --arg SELF "$self" " miss=$(jq -r --arg SEG "${self_seg:-}" --arg SELF "$self" "
${seg_members_filter} | select(.name!=\$SELF) | select((.wg_pubkey//\"\")==\"\") | .name" "$data_file" | tr '\n' ' ') ${seg_members_filter} | select(.name!=\$SELF) | select((.wg_pubkey//\"\")==\"\") | .name" "$data_file" | tr '\n' ' ')
[ -n "$(echo "$miss" | tr -d ' ')" ] && echo "wg-render: NOTE spokes without wg_pubkey (not peered): $miss" >&2 if [ -n "$(echo "$miss" | tr -d ' ')" ]; then
echo "wg-render: NOTE spokes without wg_pubkey (not peered): $miss" >&2
fi
else else
# Single [Peer] = the segment hub. # Single [Peer] = the segment hub.
hub_pub=$(jq -r --arg H "$seg_hub" '.hosts[] | select(.name==$H) | .wg_pubkey // empty' "$data_file") hub_pub=$(jq -r --arg H "$seg_hub" '.hosts[] | select(.name==$H) | .wg_pubkey // empty' "$data_file")
@ -203,10 +207,14 @@ echo "wg-render: wrote $CONF_FILE for $self ($role/${self_seg:-legacy})"
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
$SUDO systemctl enable "wg-quick@${iface}" >/dev/null 2>&1 || true $SUDO systemctl enable "wg-quick@${iface}" >/dev/null 2>&1 || true
if $SUDO systemctl is-active "wg-quick@${iface}" >/dev/null 2>&1; then if $SUDO systemctl is-active "wg-quick@${iface}" >/dev/null 2>&1; then
# Live update without dropping the tunnel. # Live update without dropping the tunnel. `wg syncconf` needs a stripped
if $SUDO sh -c "wg syncconf $iface <(wg-quick strip $iface)" 2>/dev/null; then # conf file; build it with a temp file (POSIX) — NOT bash <() process
echo "wg-render: $iface syncconf applied" # substitution, since this script runs under /bin/sh (dash on Ubuntu).
strip_tmp=$(mktemp "${TMPDIR:-/tmp}/wg1.strip.XXXXXX")
if $SUDO wg-quick strip "$iface" > "$strip_tmp" 2>/dev/null && $SUDO wg syncconf "$iface" "$strip_tmp" 2>/dev/null; then
echo "wg-render: $iface syncconf applied"; rm -f "$strip_tmp"
else else
rm -f "$strip_tmp"
$SUDO systemctl restart "wg-quick@${iface}" || { echo "wg-render: $iface restart failed — rolling back" >&2; [ -f "$CONF_FILE.netbak" ] && $SUDO cp "$CONF_FILE.netbak" "$CONF_FILE"; $SUDO systemctl restart "wg-quick@${iface}" || true; exit 3; } $SUDO systemctl restart "wg-quick@${iface}" || { echo "wg-render: $iface restart failed — rolling back" >&2; [ -f "$CONF_FILE.netbak" ] && $SUDO cp "$CONF_FILE.netbak" "$CONF_FILE"; $SUDO systemctl restart "wg-quick@${iface}" || true; exit 3; }
fi fi
else else

View file

@ -31,7 +31,7 @@
"segments": { "segments": {
"_note": "A segment = a WireGuard hub + its spokes (bin/wg-render). hosts[].segment names the segment a host belongs to; hosts[].wg_pubkey is its public key (never private). yuzu (iceland) and citron (nyc3) are independent stars. Hosts without a `segment` fall back to the legacy single hub (mesh.hub) in wg-render.", "_note": "A segment = a WireGuard hub + its spokes (bin/wg-render). hosts[].segment names the segment a host belongs to; hosts[].wg_pubkey is its public key (never private). yuzu (iceland) and citron (nyc3) are independent stars. Hosts without a `segment` fall back to the legacy single hub (mesh.hub) in wg-render.",
"iceland": { "hub": "yuzu", "endpoint": "89.127.233.145:51820", "dns_host": "apricot", "dns_listen": "127.0.0.1,10.9.0.2" }, "iceland": { "hub": "yuzu", "endpoint": "89.127.233.145:51820", "dns_host": "apricot", "dns_listen": "127.0.0.1,10.9.0.2" },
"nyc3": { "hub": "citron", "endpoint": "143.244.223.5:51820", "dns_host": "citron", "dns_listen": "127.0.0.1,10.9.0.7" } "nyc3": { "hub": "citron", "endpoint": "104.248.9.88:51820", "dns_host": "citron", "dns_listen": "127.0.0.1,10.9.0.7" }
} }
}, },
"lan": { "lan": {