Peer Endpoint Exchange
The internal/peerexchange package orchestrates the exchange of discovered public endpoints between mesh peers. It wires together STUN-based NAT discovery (internal/nat), control plane endpoint reporting (internal/api), and WireGuard peer configuration (internal/wireguard) into a single lifecycle component.
The Exchanger is a thin orchestration layer. It delegates STUN discovery and the refresh loop to nat.Discoverer.Run, SSE event handling to wireguard.HandlePeerEndpointChanged, and endpoint reporting to api.ControlPlane.ReportEndpoint. No discovery, reporting, or WireGuard logic is duplicated.
Config
Config embeds nat.Config, reusing all NAT traversal settings.
type Config struct {
nat.Config
}ApplyDefaults() and Validate() delegate to the embedded nat.Config methods. See NAT Traversal for the full configuration reference.
cfg := peerexchange.Config{}
cfg.ApplyDefaults() // Enabled=true, default STUN servers, RefreshInterval=60s, Timeout=5s
if err := cfg.Validate(); err != nil {
log.Fatal(err)
}To disable endpoint exchange (e.g., nodes with static public IPs), set Enabled=false after ApplyDefaults:
cfg := peerexchange.Config{}
cfg.ApplyDefaults()
cfg.Enabled = falseWhen disabled, Run returns nil immediately. SSE handlers for inbound peer endpoint updates are still registered, so the node receives updates from peers that do use STUN.
Exchanger
Central component managing the endpoint exchange lifecycle.
Constructor
func NewExchanger(
discoverer *nat.Discoverer,
wgManager *wireguard.Manager,
cpClient *api.ControlPlane,
cfg Config,
logger *slog.Logger,
) *Exchanger| Parameter | Description |
|---|---|
discoverer | NAT discoverer (created with the WireGuard listen port) |
wgManager | WireGuard manager (satisfies nat.PeerUpdater) |
cpClient | Control plane client (wrapped as nat.EndpointReporter) |
cfg | Endpoint exchange configuration |
logger | Structured logger (log/slog) |
NewExchanger calls cfg.ApplyDefaults() automatically.
Methods
| Method | Signature | Description |
|---|---|---|
RegisterHandlers | (sseManager *api.SSEManager) | Registers peer_endpoint_changed SSE handler |
Run | (ctx context.Context, nodeID string) error | Starts discovery + reporting loop (blocks until context cancelled) |
LastResult | () *api.NATInfo | Most recent NAT info (thread-safe, nil before first discovery) |
Lifecycle
// 1. Create dependencies
stunClient := &nat.UDPSTUNClient{Timeout: natCfg.Timeout}
discoverer := nat.NewDiscoverer(stunClient, natCfg, wgCfg.ListenPort, logger)
wgManager := wireguard.NewManager(ctrl, wgCfg, logger)
cpClient, _ := api.NewControlPlane(apiCfg, version, logger)
// 2. Create exchanger
cfg := peerexchange.Config{}
cfg.Config = natCfg
exchanger := peerexchange.NewExchanger(discoverer, wgManager, cpClient, cfg, logger)
// 3. Register SSE handlers (before SSEManager.Start)
exchanger.RegisterHandlers(sseManager)
// 4. Run exchange loop (blocks until ctx done)
err := exchanger.Run(ctx, nodeID)
// returns ctx.Err() on cancellationRegisterHandlers
Registers wireguard.HandlePeerEndpointChanged for peer_endpoint_changed SSE events on the provided SSEManager. Must be called before SSEManager.Start.
Handlers are registered regardless of the Enabled flag. When NAT is disabled, the node still receives inbound endpoint updates from peers that use STUN.
Run
When Enabled=true:
- Log info with
component=exchangeandnode_id - Create a
controlPlaneReporteradapter wrappingcpClient - Call
discoverer.Run(ctx, reporter, wgManager, nodeID)— blocks until context cancelled
When Enabled=false:
- Log info indicating NAT traversal is disabled
- Return nil immediately
The full discovery/report/refresh loop is handled by nat.Discoverer.Run:
- Initial STUN discovery — returns error if all servers fail
- Report endpoint to control plane, apply peer endpoints from response
- Ticker loop at
RefreshInterval: re-discover, report, apply updates - Context cancellation stops the loop
LastResult
Delegates to nat.Discoverer.LastResult(). Returns *api.NATInfo for heartbeat integration:
heartbeat := api.HeartbeatRequest{
NAT: exchanger.LastResult(), // nil-safe
}controlPlaneReporter
Internal adapter wrapping *api.ControlPlane to satisfy the nat.EndpointReporter interface.
type controlPlaneReporter struct {
client *api.ControlPlane
}
func (r *controlPlaneReporter) ReportEndpoint(ctx context.Context, nodeID string, req api.EndpointReport) (*api.EndpointResponse, error) {
return r.client.ReportEndpoint(ctx, nodeID, req)
}The adapter is created inside Run with the cpClient from the Exchanger. The nodeID flows through the nat.Discoverer.Run call, which passes it to EndpointReporter.ReportEndpoint on each cycle.
wireguard.Manager satisfies nat.PeerUpdater directly — no adapter is needed.
Data Flow
Outbound (STUN refresh loop)
┌──────────────────────────────────────────┐
│ │
▼ │
┌─────────────┐ ReportEndpoint ┌──────────┴───┐
STUN Servers ──▶│ Discoverer │──────────────────────▶│ Control Plane│
│ (nat pkg) │ │ (api pkg) │
└─────────────┘ └──────┬───────┘
│ │
│ (same cycle) PeerEndpoints │
│ in response │
▼ ▼
┌─────────────┐ UpdatePeer ┌─────────────────┐
│ Exchanger │───────────────────▶│ WireGuard │
│ (this pkg) │ │ Manager │
└─────────────┘ │ (wireguard pkg) │
└────────▲────────┘
Inbound (SSE events) │
┌─────────────────────────────────────┘
│
┌──────────┴──────────┐
│ SSEManager │
│ peer_endpoint_ │
│ changed event │
│ (api pkg) │
└─────────────────────┘Outbound path (refresh loop): STUN discovery produces the node's public endpoint. The Exchanger reports it to the control plane via controlPlaneReporter. The control plane response contains peer endpoints, which are applied to WireGuard via Manager.UpdatePeer.
Inbound path (SSE): When a remote peer discovers a new endpoint, the control plane pushes a peer_endpoint_changed SSE event. The registered wireguard.HandlePeerEndpointChanged handler updates WireGuard immediately, without waiting for the next refresh cycle.
Integration Points
With internal/nat
nat.Discovererperforms STUN discovery and runs the refresh loopnat.Configprovides all configuration (embedded inpeerexchange.Config)nat.EndpointReporterinterface satisfied bycontrolPlaneReporteradapternat.PeerUpdaterinterface satisfied bywireguard.Managerdirectly
With internal/wireguard
wireguard.Managerreceives peer endpoint updates viaUpdatePeerwireguard.HandlePeerEndpointChangedprovides the SSE event handler
With internal/api
api.ControlPlane.ReportEndpointreports endpoints (wrapped by adapter)api.SSEManager.RegisterHandlerregisters the SSE handlerapi.EventPeerEndpointChangedis the event type constantapi.NATInfois the return type ofLastResult
Error Handling
| Scenario | Behavior |
|---|---|
| All STUN servers fail (initial) | Run returns error from nat.Discoverer.Run |
| STUN refresh failure | Log warn, keep previous endpoint, retry next cycle |
| Endpoint report failure | Log warn, continue refresh loop |
| Individual peer update failure | Log warn, continue processing remaining peers |
| Context cancellation | Clean abort, return ctx.Err() |
| NAT disabled | Run returns nil immediately |
Logging
All log entries use component=exchange.
| Level | Event | Keys |
|---|---|---|
Info | Starting endpoint exchange | node_id |
Info | NAT traversal disabled | (none) |
Debug | SSE handler registered | (none) |
Discovery and reporting logs use component=nat (from the nat package). See NAT Traversal for those log entries.