feat(wgtunnel): render operator OOB /32 into wg-felhom AllowedIPs (H1 Part 1, [OF-1])

renderConf appends a validated, deterministically-SORTED oob_peer_ip /32 so the
operator peer survives self-heal/restart (a runtime wg set was wiped by self-heal).
WireWireguard gains oob_peer_ip (omitempty — absent = byte-identical pre-H1 conf).
Non-hollow tests: sorted+byte-stable, lower-IP-sorts-first, absent-identical,
invalid/v6 rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-05 22:08:04 +02:00
parent 4b7c5bf128
commit effff53f99
3 changed files with 92 additions and 1 deletions
+35 -1
View File
@@ -14,6 +14,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@@ -226,6 +227,15 @@ func renderConf(block *hub.WireWireguard, privB64, endpointIPv4 string) (string,
if block.Endpoint.WGPort < 1 || block.Endpoint.WGPort > 65535 {
return "", fmt.Errorf("wgtunnel: wg_port %d out of range", block.Endpoint.WGPort)
}
// AllowedIPs: the PBS tunnel IP always; the operator OOB peer /32 too when present (H1 [OF-1]).
// The tunnel carries box→PBS AND (when OOB is on) operator→box SSH — both terminate on the box, so
// the low interface MTU still bounds both directions. RENDERED here (not a runtime `wg set`) so it
// survives the agent's own self-heal. allowedIPs returns a DETERMINISTICALLY SORTED list so the
// conf-hash is stable across ticks (trap 1: nondeterministic order → a restart every 60s).
allowed, err := allowedIPsLine(pbsIP, block.OOBPeerIP)
if err != nil {
return "", err
}
var b strings.Builder
b.WriteString("# felhom offsite tunnel — agent-managed (S3); DO NOT EDIT\n")
@@ -236,11 +246,35 @@ func renderConf(block *hub.WireWireguard, privB64, endpointIPv4 string) (string,
b.WriteString("[Peer]\n")
fmt.Fprintf(&b, "PublicKey = %s\n", block.Endpoint.ServerPubkey)
fmt.Fprintf(&b, "Endpoint = %s:%d\n", epIP, block.Endpoint.WGPort)
fmt.Fprintf(&b, "AllowedIPs = %s/32\n", pbsIP)
fmt.Fprintf(&b, "AllowedIPs = %s\n", allowed)
b.WriteString("PersistentKeepalive = 25\n")
return b.String(), nil
}
// allowedIPsLine builds the peer AllowedIPs value: pbsIP/32 always, plus the operator OOB /32 when
// oobPeerIP is a non-empty valid IPv4. The result is SORTED by address (netip.Addr.Compare) so two
// renders of the same inputs are byte-identical — the conf-hash stability the reconcile loop relies
// on. oobPeerIP is validated (bare IPv4, like pbs_tunnel_ip); a bad value is a hard error, never a
// silently-dropped widening.
func allowedIPsLine(pbsIP netip.Addr, oobPeerIP string) (string, error) {
addrs := []netip.Addr{pbsIP}
if oobPeerIP != "" {
op, err := netip.ParseAddr(oobPeerIP)
if err != nil || !op.Is4() {
return "", fmt.Errorf("wgtunnel: oob_peer_ip %q is not an IPv4 address", oobPeerIP)
}
if op != pbsIP { // never duplicate if (misconfigured) equal to the PBS IP
addrs = append(addrs, op)
}
}
sort.Slice(addrs, func(i, j int) bool { return addrs[i].Compare(addrs[j]) < 0 })
parts := make([]string, len(addrs))
for i, a := range addrs {
parts[i] = a.String() + "/32"
}
return strings.Join(parts, ", "), nil
}
// --- the state machine (doc 06 §3.3/§3.5 + spec §7/§8) ---
// Apply reconciles local reality toward (fetched, block). fetched=false means "no desired-state