From 53a79d349417d63dabd8e237d8415a1bc4a63ad0 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 21:50:52 -0400 Subject: [PATCH] net-tools: fix wg-render apply (set -e abort + dash syncconf), nyc3 endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- bin/wg-render | 18 +++++++++++++----- data/mesh-hosts.json | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bin/wg-render b/bin/wg-render index b289669..a3d216a 100755 --- a/bin/wg-render +++ b/bin/wg-render @@ -166,10 +166,14 @@ render_conf() { | select(.wg_pubkey != null and .wg_pubkey != \"\") | \"# \(.name)\n[Peer]\nPublicKey = \(.wg_pubkey)\nAllowedIPs = \(.wg)/32\n\" " "$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" " ${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 # Single [Peer] = the segment hub. 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 $SUDO systemctl enable "wg-quick@${iface}" >/dev/null 2>&1 || true if $SUDO systemctl is-active "wg-quick@${iface}" >/dev/null 2>&1; then - # Live update without dropping the tunnel. - if $SUDO sh -c "wg syncconf $iface <(wg-quick strip $iface)" 2>/dev/null; then - echo "wg-render: $iface syncconf applied" + # Live update without dropping the tunnel. `wg syncconf` needs a stripped + # conf file; build it with a temp file (POSIX) — NOT bash <() process + # 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 + 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; } fi else diff --git a/data/mesh-hosts.json b/data/mesh-hosts.json index f0ffe6d..dd9a055 100644 --- a/data/mesh-hosts.json +++ b/data/mesh-hosts.json @@ -31,7 +31,7 @@ "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.", "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": {