ADR 0051 — STUN-coord Pattern B (P2P hole-punching)¶
| Status | Date | Author | Supersedes | Superseded by |
|---|---|---|---|---|
| Accepted | 2026-05-15 | André Luiz Gallon | — | — |
Context¶
ADR 0049 (OOBI Satellite Federation Framework) estabelece operator-choice entre 2 padrões de conectividade satellite-to-satellite cross-DC. ADR 0050 formalizou Pattern A (rendezvous via CONNECT.Art). Este ADR formaliza Pattern B: peer-to-peer hole-punching coordenado por STUN.
STUN-coord existe em pkg/octopus/stun-coord/ desde Wave-1 PR #753. Este
ADR documenta os contratos para que client libs (PR-OSF-8) e operator
policy (PR-OSF-9) possam ser implementados sem ambiguidade.
Pattern B characteristics¶
| Característica | Pattern A (rendezvous) | Pattern B (P2P) |
|---|---|---|
| Path bytes | satellite-A → cloud → satellite-B | satellite-A → satellite-B direct |
| NAT traversal | unnecessary (cloud aceita inbound) | hole-punching obrigatório |
| Cloud cost | egress payload bytes | só signaling/keepalive |
| Latency | A→cloud→B (~+30-80ms) | A↔B direct (~LAN) |
| Sensitivity to NAT type | nenhuma | falha em symmetric NAT |
| Bandwidth ceiling | limitado por cloud cell | limitado por par enlace |
Cliente típico de Pattern B: sites com IPv6 público estável ou enterprise com firewall full-cone NAT. Pattern A cobre o resto (CGNAT, symmetric NAT, ISP residencial).
Architectural decision¶
8 LOCKED decisions.
D1: STUN-coord não termina mTLS satellite — apenas signaling¶
A cloud só assina ICE candidates trocados entre A e B. O payload trafega direto A↔B (out-of-band do STUN-coord). Compromisso da cloud: small, predictable, auditable.
Implicação: cloud não vê bytes de aplicação após pairing. Mesma zero-trust posture do Pattern A D1.
D2: ICE candidate exchange via WebSocket Stream 1 mTLS¶
Signaling channel = WebSocket sobre TLS 1.3 mTLS PQC entre cada satellite
e STUN-coord. Após pareamento (via deployment_id), STUN-coord encaminha
candidates A↔B. Após hole-punching success, satellites mudam para tunnel
direto (signaling channel é fechado ou mantido só para keepalive 30s).
D3: STUN Binding Request RFC 5389 compliant + dual-stack¶
STUN server escuta UDP em :3478 (canonical RFC 5389) dual-stack:
IPv4 (0.0.0.0) + IPv6 ([::]). XOR-MAPPED-ADDRESS retorna address family
0x01 (v4) ou 0x02 (v6) conforme RFC 8489 §15.2.
Reflexive address descoberto é injetado na ICE candidate list.
D4: TLS strict idêntico ao Pattern A¶
TLS 1.3 only, tls.CurveID{X25519MLKEM768, X25519}, mTLS cert verify
contra Cisco Satellite Root CA, cert SAN
satellite-<site_id>.<deployment_id>.satellite.tlsstress.art.
Reusa pkg/octopus/common/tlsconfig/ shipped em Wave-1.
D5: Hole-punching algorithm = simultaneous open + UDP NAT learning¶
Após troca de candidates:
- Ambos satellites enviam UDP packet ao reflexive address do par simultaneamente
- NAT learns mapping (pinhole open)
- Retransmits a cada 200ms × 5 tentativas (timeout 1s total)
- Success = ACK recebido em ambas direções
- Failure = fallback automático para Pattern A (se operator policy permite hybrid mode — PR-OSF-9)
D6: ICE-lite implementation, NOT full ICE¶
Full ICE (RFC 8445) é overkill para satellite-to-satellite (2 peers conhecidos, sem media). Implementação MVP:
- 1 candidate type: server-reflexive (STUN-derived)
- Sem TURN candidates (Pattern A já é a alternativa)
- Sem host candidates (LANs internos não roteam cross-DC)
- Sem prflx candidates
- Sem connectivity checks redundantes
Trade-off: menor sucesso rate em NAT complex (~85% vs ~95% full ICE) mas ~10× menos código e debug surface.
D7: Audit chain Merkle integration com tunnel_pattern: "b"¶
Cada signaling session loga via pkg/octopus/common/audit/:
audit.Entry{
EventType: "satellite.federation.pairing",
DeploymentID: "01HXXX...",
SiteAID: "site-aaa",
SiteBID: "site-bbb",
TunnelPattern: "b",
SourceIPFamily: "v6", // ADR 0058
SuccessOrFailure: "success",
DurationMS: 842,
}
D8: P2P client lib em pkg/oobi/satellite/pattern_b/ (PR-OSF-8)¶
Estrutura sugerida:
pkg/oobi/satellite/pattern_b/
├── client.go // dial STUN-coord, swap candidates, hole-punch
├── ice_candidate.go // RFC 5245 format (subset)
├── nat_probe.go // STUN binding + reflexive learn
├── fallback.go // detect failure → trigger Pattern A
└── client_test.go
Cell topology + DNS¶
Per-cell STUN-coord deployment:
stun-us-east-1-a.tlsstress.art A 192.0.2.21 AAAA 2001:db8:1::21
stun-us-east-1-b.tlsstress.art A 192.0.2.22 AAAA 2001:db8:1::22
stun-eu-west-1-a.tlsstress.art A 198.51.100.21 AAAA 2001:db8:2::21
(via Coordinator W3.5-4 cell selection — ADR 0053.)
Cliente recebe (connect_endpoint, stun_endpoint) durante provisioning,
operator escolhe qual usar.
Implementation status¶
| Component | Status | PR |
|---|---|---|
| STUN UDP listener IPv4 | ✅ shipped | #753 |
| STUN UDP listener IPv6 dual-stack | ✅ shipped | #830 |
| XOR-MAPPED-ADDRESS family=0x01/0x02 | ✅ shipped | #830 |
| WebSocket signaling channel | ✅ shipped | #753 |
| mTLS PQC for signaling | ✅ shipped | #753 (reuses common/tlsconfig) |
| ICE candidate format + exchange | ✅ shipped (basic) | #753 |
| Audit chain integration | ✅ shipped | #753 |
| Satellite client lib (P2P side) | ⏳ pending | PR-OSF-8 |
| Hole-punching algorithm in client | ⏳ pending | PR-OSF-8 |
| Pattern A fallback detect | ⏳ pending | PR-OSF-8 |
| Operator policy (A vs B vs hybrid) | ⏳ pending | PR-OSF-9 |
Failure modes¶
| Failure | Detection | Action |
|---|---|---|
| Symmetric NAT no candidate | hole-punch timeout 1s | fallback to Pattern A (if hybrid) |
| ISP blocks UDP outbound | STUN binding request times out | fallback to Pattern A |
| Peer dies mid-session | UDP keepalive miss 3× (15s) | re-pair via STUN-coord |
| STUN-coord cell failover | WebSocket disconnect | re-dial next cell (Coordinator) |
| Cert revoked | mTLS handshake reject | abort, audit, alert security |
| Clock skew >5min cross-site | UDP NAT timeout mismatch | re-pair, log warning |
Comparison vs RFC 8445 full ICE¶
| Aspect | Full ICE | OCTOPUS Pattern B ICE-lite |
|---|---|---|
| Candidate types | host, srflx, prflx, relay | srflx only |
| Connectivity checks | Triggered + Ordinary | none (single best path) |
| Nomination | Aggressive + Regular | implicit (first success) |
| Retransmit algorithm | RTO-aware backoff | fixed 200ms × 5 |
| Code complexity | ~5000 LoC | ~800 LoC target |
Rationale: satellite-to-satellite federation tem 2 endpoints conhecidos e estáveis. Não estamos roteando media WebRTC em rede heterogênea de browsers — context muito diferente do design space original do ICE.
Operator policy guidance (PR-OSF-9 preview)¶
Dashboard /admin/satellite/federation/policy:
| Policy | Effect | Use case |
|---|---|---|
pattern_a_only |
sempre via cloud | regulated/defense, CGNAT |
pattern_b_only |
sempre direct | IPv6 enterprise, low-latency |
hybrid_b_preferred |
try B → fallback A | default (best balance) |
hybrid_a_preferred |
try A → fallback B | dev/test, low-cost |
Closes audit gap¶
Gap #4 — OOBI Satellite Federation Wave 2 Pattern B (rendezvous + P2P operator choice) sem ADR formalizado