wgtunnel: client MTU 1420 → 1280 (S3.1, resolve §4.3 open decision)
The 2026-07-04 CGNAT smoke test found MTU 1420 silently black-holes bulk TCP on sub-~1480 paths (mobile ~1400, DS-Lite ~1452): handshake+ping stay healthy, PBS TLS page (and at S4 the backup itself) drops. Set a fleet-wide, permanent, family-agnostic client MTU of 1280 (RFC 8200 IPv6-minimum floor; outer 1340 v4 / 1360 v6 fits every realistic path). Client-only by construction — interface MTU caps box→PBS, advertised MSS caps PBS→box; the endpoint's wg0 is untouched (zero live-endpoint risk). New const clientMTU=1280 as the single home; golden pins exact "MTU = 1280" (red-proofed against a 1420 flip). Stale report.go comment updated. No wire/JSON change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -1,3 +1,24 @@
|
|||||||
|
## v0.65.0 — S3.1 offsite-tunnel client MTU 1420 → 1280 (resolve the CGNAT-smoke MTU open decision) (2026-07-04)
|
||||||
|
|
||||||
|
One-constant fix closing `06 §4.3`'s OPEN DECISION. The 2026-07-04 CGNAT smoke test found the
|
||||||
|
shipped interface MTU 1420 **silently black-holes bulk TCP** on any path below ~1480 B (mobile
|
||||||
|
~1400, DS-Lite ~1452): the WG handshake and ping stay healthy (small packets) while the PBS TLS
|
||||||
|
page — and, at S4, the backup itself — drops. "Looks green, loses backups." Must be safe before
|
||||||
|
S4 flows bulk TCP over the tunnel.
|
||||||
|
|
||||||
|
- **`internal/wgtunnel/manager.go`**: new `const clientMTU = 1280` (the RFC 8200 IPv6-minimum link
|
||||||
|
MTU — every path carries ≥1280; outer = 1280+60 v4 / +80 v6, fits every realistic path);
|
||||||
|
`renderConf` emits `MTU = %d` from it. **Fleet-wide, family-agnostic, permanent** — decouples
|
||||||
|
the fix from the v4/v6 endpoint-resolution question (§4.2). **Client-only by construction:** the
|
||||||
|
interface MTU caps box→PBS and the advertised MSS (=MTU−40) caps PBS→box, so the endpoint's `wg0`
|
||||||
|
is deliberately untouched (zero live-endpoint risk). Rejected: auto-probe / per-connection-type
|
||||||
|
(fragile moving part optimizing throughput, a non-metric here) and MSS-clamp (no forwarded flows).
|
||||||
|
- **`internal/wgtunnel/manager_test.go`**: golden pins exact `MTU = 1280`; red-proofed (flip const
|
||||||
|
→ 1420 fails the golden on the MTU line — non-vacuous).
|
||||||
|
- **`internal/hub/report.go`**: stale "MTU 1420" comment → 1280 (still NOT a wire field).
|
||||||
|
- No wire/JSON-golden change (MTU is a client-derived constant, never on the wire); no endpoint,
|
||||||
|
hub, controller, key, or desired-state change.
|
||||||
|
|
||||||
## v0.64.0 — S3 offsite WG tunnel: keygen + registration + agent-managed wg-quick@wg-felhom + escrow join (2026-07-04)
|
## v0.64.0 — S3 offsite WG tunnel: keygen + registration + agent-managed wg-quick@wg-felhom + escrow join (2026-07-04)
|
||||||
|
|
||||||
The agent half of doc 06 §3.3 (felhom.eu S1/S2 built the endpoint + hub half). **DEFAULT OFF —
|
The agent half of doc 06 §3.3 (felhom.eu S1/S2 built the endpoint + hub half). **DEFAULT OFF —
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ type WireDesiredState struct {
|
|||||||
|
|
||||||
// WireWireguard is the hub-owned offsite-tunnel assignment (S3) — field-exact with the S2 golden
|
// WireWireguard is the hub-owned offsite-tunnel assignment (S3) — field-exact with the S2 golden
|
||||||
// (testdata/desired-state-wireguard.golden.json, byte-identical hub copy). Client-side
|
// (testdata/desired-state-wireguard.golden.json, byte-identical hub copy). Client-side
|
||||||
// AllowedIPs, PersistentKeepalive=25 and MTU 1420 are deliberately NOT wire fields — wgtunnel
|
// AllowedIPs, PersistentKeepalive=25 and MTU 1280 are deliberately NOT wire fields — wgtunnel
|
||||||
// constants derived from endpoint.pbs_tunnel_ip + doc 06 §4.
|
// constants derived from endpoint.pbs_tunnel_ip + doc 06 §4.
|
||||||
type WireWireguard struct {
|
type WireWireguard struct {
|
||||||
Endpoint WireWireguardEndpoint `json:"endpoint"`
|
Endpoint WireWireguardEndpoint `json:"endpoint"`
|
||||||
|
|||||||
@@ -140,8 +140,16 @@ func validKeyB64(s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientMTU is the offsite tunnel's interface MTU — the IPv6-minimum floor (RFC 8200 guarantees
|
||||||
|
// every path carries ≥1280). Chosen fleet-wide + family-agnostic so bulk TCP never black-holes on
|
||||||
|
// a constrained path: outer = 1280+60 (v4) / 1280+80 (v6), both fit the mobile ~1400, DS-Lite
|
||||||
|
// ~1452, PPPoE 1492 and clean 1500 cases. Client-only bounds both directions (interface MTU caps
|
||||||
|
// box→PBS; advertised MSS = MTU−40 caps PBS→box), so the endpoint's wg0 stays untouched. Was 1420
|
||||||
|
// (silently black-holed sub-~1480 paths — CGNAT smoke test 2026-07-04). See doc 06 §4.3.
|
||||||
|
const clientMTU = 1280
|
||||||
|
|
||||||
// renderConf builds the wg-felhom.conf content from the hub block + the local private key.
|
// renderConf builds the wg-felhom.conf content from the hub block + the local private key.
|
||||||
// Client-side constants per doc 06 §4: MTU 1420, AllowedIPs = pbs_tunnel_ip/32 (the tunnel
|
// Client-side constants per doc 06 §4: MTU 1280, AllowedIPs = pbs_tunnel_ip/32 (the tunnel
|
||||||
// carries ONLY box→PBS traffic), PersistentKeepalive 25. All inputs validated — nothing
|
// carries ONLY box→PBS traffic), PersistentKeepalive 25. All inputs validated — nothing
|
||||||
// user-controlled is interpolatable (strict charsets, netip parses).
|
// user-controlled is interpolatable (strict charsets, netip parses).
|
||||||
func renderConf(block *hub.WireWireguard, privB64 string) (string, error) {
|
func renderConf(block *hub.WireWireguard, privB64 string) (string, error) {
|
||||||
@@ -171,7 +179,7 @@ func renderConf(block *hub.WireWireguard, privB64 string) (string, error) {
|
|||||||
b.WriteString("[Interface]\n")
|
b.WriteString("[Interface]\n")
|
||||||
fmt.Fprintf(&b, "PrivateKey = %s\n", privB64)
|
fmt.Fprintf(&b, "PrivateKey = %s\n", privB64)
|
||||||
fmt.Fprintf(&b, "Address = %s\n", block.AssignedIP)
|
fmt.Fprintf(&b, "Address = %s\n", block.AssignedIP)
|
||||||
b.WriteString("MTU = 1420\n\n")
|
fmt.Fprintf(&b, "MTU = %d\n\n", clientMTU)
|
||||||
b.WriteString("[Peer]\n")
|
b.WriteString("[Peer]\n")
|
||||||
fmt.Fprintf(&b, "PublicKey = %s\n", block.Endpoint.ServerPubkey)
|
fmt.Fprintf(&b, "PublicKey = %s\n", block.Endpoint.ServerPubkey)
|
||||||
fmt.Fprintf(&b, "Endpoint = %s:%d\n", block.Endpoint.DNSName, block.Endpoint.WGPort)
|
fmt.Fprintf(&b, "Endpoint = %s:%d\n", block.Endpoint.DNSName, block.Endpoint.WGPort)
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ func localPub(t *testing.T, m *Manager) string {
|
|||||||
return pub
|
return pub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRenderConf_Golden pins the EXACT conf bytes, including "MTU = 1280" (the IPv6-minimum
|
||||||
|
// floor, doc 06 §4.3). Red-proofed 2026-07-04: flipping clientMTU back to 1420 fails this test
|
||||||
|
// on the MTU line mismatch — the golden is non-vacuous, not a "contains MTU" check.
|
||||||
func TestRenderConf_Golden(t *testing.T) {
|
func TestRenderConf_Golden(t *testing.T) {
|
||||||
block := testBlock("ignored")
|
block := testBlock("ignored")
|
||||||
conf, err := renderConf(block, vectorPrivB64)
|
conf, err := renderConf(block, vectorPrivB64)
|
||||||
@@ -140,7 +143,7 @@ func TestRenderConf_Golden(t *testing.T) {
|
|||||||
[Interface]
|
[Interface]
|
||||||
PrivateKey = ` + vectorPrivB64 + `
|
PrivateKey = ` + vectorPrivB64 + `
|
||||||
Address = 10.77.0.2/32
|
Address = 10.77.0.2/32
|
||||||
MTU = 1420
|
MTU = 1280
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
PublicKey = CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=
|
PublicKey = CQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQk=
|
||||||
|
|||||||
Reference in New Issue
Block a user