Skip to content

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:

  1. Ambos satellites enviam UDP packet ao reflexive address do par simultaneamente
  2. NAT learns mapping (pinhole open)
  3. Retransmits a cada 200ms × 5 tentativas (timeout 1s total)
  4. Success = ACK recebido em ambas direções
  5. 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

Cross-references