feat(nat): Tailscale-inspired STUN/ICE + port mapping + mid-call re-gathering (#28)
Phase 8: 5 new modules bringing NAT traversal close to Tailscale's approach. - stun.rs: RFC 5389 STUN client — public server reflexive discovery, XOR-MAPPED-ADDRESS parsing, parallel probe with retry, STUN fallback in desktop try_reflect_own_addr() - portmap.rs: NAT-PMP (RFC 6886) + PCP (RFC 6887) + UPnP IGD port mapping — gateway discovery, acquire/release/refresh lifecycle, new PeerCandidates.mapped candidate type in dial order - ice_agent.rs: candidate lifecycle — gather(), re_gather(), apply_peer_update() with monotonic generation counter, CandidateUpdate signal message forwarded by relay - netcheck.rs: comprehensive diagnostic — NAT type, IPv4/v6, port mapping availability, relay latencies, CLI --netcheck - relay_map.rs: RTT-sorted relay map, preferred() selection, populate_from_ack() for RegisterPresenceAck.available_relays Relay: CallRegistry stores + cross-wires caller/callee_mapped_addr into CallSetup.peer_mapped_addr. Region config + available_relays populated from federation peers in RegisterPresenceAck. Desktop: place_call/answer_call call acquire_port_mapping() and fill caller/callee_mapped_addr. STUN+relay combined NAT detection. 571 tests pass (66 new), 0 regressions, 0 warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -538,6 +538,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
ref call_id,
|
||||
ref caller_reflexive_addr,
|
||||
ref caller_local_addrs,
|
||||
ref caller_mapped_addr,
|
||||
..
|
||||
} => {
|
||||
// Is the target on THIS relay? If not, drop —
|
||||
@@ -557,7 +558,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Stash in local registry so the answer path
|
||||
// can find the call + route the reply back
|
||||
// through the same federation link. Include
|
||||
// Phase 5.5 LAN host candidates too.
|
||||
// Phase 5.5 LAN host candidates + Phase 8
|
||||
// port-mapped addr.
|
||||
{
|
||||
let mut reg = call_registry_d.lock().await;
|
||||
reg.create_call(
|
||||
@@ -567,6 +569,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
reg.set_caller_reflexive_addr(call_id, caller_reflexive_addr.clone());
|
||||
reg.set_caller_local_addrs(call_id, caller_local_addrs.clone());
|
||||
reg.set_caller_mapped_addr(call_id, caller_mapped_addr.clone());
|
||||
reg.set_peer_relay_fp(call_id, Some(origin_relay_fp.clone()));
|
||||
}
|
||||
// Deliver the offer to the local target.
|
||||
@@ -585,6 +588,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
accept_mode,
|
||||
ref callee_reflexive_addr,
|
||||
ref callee_local_addrs,
|
||||
ref callee_mapped_addr,
|
||||
..
|
||||
} => {
|
||||
// Look up the local caller fp from the registry.
|
||||
@@ -616,14 +620,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Accept — stash the callee's reflex addr + LAN
|
||||
// host candidates + mark the call active,
|
||||
// then read back everything needed to cross-
|
||||
// wire peer_direct_addr + peer_local_addrs in
|
||||
// the local CallSetup.
|
||||
// Also set peer_relay_fp so the originating
|
||||
// relay knows where to forward MediaPathReport.
|
||||
// host candidates + mapped addr + mark the call
|
||||
// active, then read back everything needed to
|
||||
// cross-wire into the local CallSetup.
|
||||
let room_name = format!("call-{call_id}");
|
||||
let (callee_addr_for_setup, callee_local_for_setup) = {
|
||||
let (callee_addr_for_setup, callee_local_for_setup, callee_mapped_for_setup) = {
|
||||
let mut reg = call_registry_d.lock().await;
|
||||
reg.set_active(call_id, accept_mode, room_name.clone());
|
||||
reg.set_peer_relay_fp(call_id, Some(origin_relay_fp.clone()));
|
||||
@@ -632,10 +633,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
callee_reflexive_addr.clone(),
|
||||
);
|
||||
reg.set_callee_local_addrs(call_id, callee_local_addrs.clone());
|
||||
reg.set_callee_mapped_addr(call_id, callee_mapped_addr.clone());
|
||||
let c = reg.get(call_id);
|
||||
(
|
||||
c.and_then(|c| c.callee_reflexive_addr.clone()),
|
||||
c.map(|c| c.callee_local_addrs.clone()).unwrap_or_default(),
|
||||
c.and_then(|c| c.callee_mapped_addr.clone()),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -648,19 +651,13 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Emit the LOCAL CallSetup to our local caller.
|
||||
// relay_addr = our own advertised addr so if P2P
|
||||
// fails the caller will at least dial OUR relay
|
||||
// (single-relay fallback — Phase 4.1 will wire
|
||||
// federated media so that actually reaches the
|
||||
// peer). peer_direct_addr = the callee's reflex
|
||||
// addr carried in the answer. peer_local_addrs
|
||||
// = callee's LAN host candidates (Phase 5.5 ICE).
|
||||
let setup = SignalMessage::CallSetup {
|
||||
call_id: call_id.clone(),
|
||||
room: room_name.clone(),
|
||||
relay_addr: advertised_addr_d.clone(),
|
||||
peer_direct_addr: callee_addr_for_setup,
|
||||
peer_local_addrs: callee_local_for_setup,
|
||||
peer_mapped_addr: callee_mapped_for_setup,
|
||||
};
|
||||
let hub = signal_hub_d.lock().await;
|
||||
let _ = hub.send_to(&caller_fp, &setup).await;
|
||||
@@ -772,6 +769,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
let signal_hub = signal_hub.clone();
|
||||
let call_registry = call_registry.clone();
|
||||
let advertised_addr_str = advertised_addr_str.clone();
|
||||
// Phase 8: relay region + peer addresses for RegisterPresenceAck
|
||||
let relay_region = config.region.clone();
|
||||
let relay_peers_for_ack: Vec<String> = config.peers.iter()
|
||||
.filter_map(|p| {
|
||||
let label = p.label.as_deref().unwrap_or("peer");
|
||||
Some(format!("{label}|{}", p.url))
|
||||
})
|
||||
.collect();
|
||||
// Phase 4: per-task clone of this relay's federation TLS
|
||||
// fingerprint so the FederatedSignalForward envelopes the
|
||||
// spawned signal handler builds carry `origin_relay_fp`.
|
||||
@@ -1005,6 +1010,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
success: true,
|
||||
error: None,
|
||||
relay_build: Some(BUILD_GIT_HASH.to_string()),
|
||||
relay_region: relay_region.clone(),
|
||||
available_relays: relay_peers_for_ack.clone(),
|
||||
}).await;
|
||||
|
||||
info!(%addr, fingerprint = %client_fp, alias = ?client_alias, "signal client registered");
|
||||
@@ -1019,12 +1026,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
ref call_id,
|
||||
ref caller_reflexive_addr,
|
||||
ref caller_local_addrs,
|
||||
ref caller_mapped_addr,
|
||||
..
|
||||
} => {
|
||||
let target_fp = target_fingerprint.clone();
|
||||
let call_id = call_id.clone();
|
||||
let caller_addr_for_registry = caller_reflexive_addr.clone();
|
||||
let caller_local_for_registry = caller_local_addrs.clone();
|
||||
let caller_mapped_for_registry = caller_mapped_addr.clone();
|
||||
|
||||
// Check if target is online
|
||||
let online = {
|
||||
@@ -1097,6 +1106,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
&call_id,
|
||||
caller_local_for_registry.clone(),
|
||||
);
|
||||
reg.set_caller_mapped_addr(
|
||||
&call_id,
|
||||
caller_mapped_for_registry.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
// Send ringing to caller immediately
|
||||
@@ -1118,6 +1131,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
reg.create_call(call_id.clone(), client_fp.clone(), target_fp.clone());
|
||||
reg.set_caller_reflexive_addr(&call_id, caller_addr_for_registry);
|
||||
reg.set_caller_local_addrs(&call_id, caller_local_for_registry);
|
||||
reg.set_caller_mapped_addr(&call_id, caller_mapped_for_registry);
|
||||
}
|
||||
|
||||
// Forward offer to callee
|
||||
@@ -1139,12 +1153,14 @@ async fn main() -> anyhow::Result<()> {
|
||||
ref accept_mode,
|
||||
ref callee_reflexive_addr,
|
||||
ref callee_local_addrs,
|
||||
ref callee_mapped_addr,
|
||||
..
|
||||
} => {
|
||||
let call_id = call_id.clone();
|
||||
let mode = *accept_mode;
|
||||
let callee_addr_for_registry = callee_reflexive_addr.clone();
|
||||
let callee_local_for_registry = callee_local_addrs.clone();
|
||||
let callee_mapped_for_registry = callee_mapped_addr.clone();
|
||||
|
||||
// Phase 4: look up peer fingerprint AND
|
||||
// peer_relay_fp in one lock acquisition.
|
||||
@@ -1207,17 +1223,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
// BOTH parties' addrs so we can cross-wire
|
||||
// peer_direct_addr on the CallSetups below.
|
||||
let room = format!("call-{call_id}");
|
||||
let (caller_addr, callee_addr, caller_local, callee_local) = {
|
||||
let (caller_addr, callee_addr, caller_local, callee_local, caller_mapped, callee_mapped) = {
|
||||
let mut reg = call_registry.lock().await;
|
||||
reg.set_active(&call_id, mode, room.clone());
|
||||
reg.set_callee_reflexive_addr(&call_id, callee_addr_for_registry);
|
||||
reg.set_callee_local_addrs(&call_id, callee_local_for_registry.clone());
|
||||
reg.set_callee_mapped_addr(&call_id, callee_mapped_for_registry);
|
||||
let call = reg.get(&call_id);
|
||||
(
|
||||
call.and_then(|c| c.caller_reflexive_addr.clone()),
|
||||
call.and_then(|c| c.callee_reflexive_addr.clone()),
|
||||
call.map(|c| c.caller_local_addrs.clone()).unwrap_or_default(),
|
||||
call.map(|c| c.callee_local_addrs.clone()).unwrap_or_default(),
|
||||
call.and_then(|c| c.caller_mapped_addr.clone()),
|
||||
call.and_then(|c| c.callee_mapped_addr.clone()),
|
||||
)
|
||||
};
|
||||
info!(
|
||||
@@ -1266,6 +1285,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
relay_addr: relay_addr_for_setup,
|
||||
peer_direct_addr: caller_addr.clone(),
|
||||
peer_local_addrs: caller_local.clone(),
|
||||
peer_mapped_addr: caller_mapped.clone(),
|
||||
};
|
||||
let hub = signal_hub.lock().await;
|
||||
let _ = hub.send_to(&client_fp, &setup_for_callee).await;
|
||||
@@ -1278,14 +1298,15 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Send CallSetup to BOTH parties with
|
||||
// cross-wired peer_direct_addr +
|
||||
// peer_local_addrs (Phase 5.5 ICE).
|
||||
// cross-wired candidates (Phase 5.5 ICE
|
||||
// + Phase 8 port-mapped addrs).
|
||||
let setup_for_caller = SignalMessage::CallSetup {
|
||||
call_id: call_id.clone(),
|
||||
room: room.clone(),
|
||||
relay_addr: relay_addr_for_setup.clone(),
|
||||
peer_direct_addr: callee_addr.clone(),
|
||||
peer_local_addrs: callee_local.clone(),
|
||||
peer_mapped_addr: callee_mapped,
|
||||
};
|
||||
let setup_for_callee = SignalMessage::CallSetup {
|
||||
call_id: call_id.clone(),
|
||||
@@ -1293,6 +1314,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
relay_addr: relay_addr_for_setup,
|
||||
peer_direct_addr: caller_addr.clone(),
|
||||
peer_local_addrs: caller_local.clone(),
|
||||
peer_mapped_addr: caller_mapped,
|
||||
};
|
||||
let hub = signal_hub.lock().await;
|
||||
let _ = hub.send_to(&peer_fp, &setup_for_caller).await;
|
||||
@@ -1382,6 +1404,45 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 8: forward CandidateUpdate to the
|
||||
// call peer for mid-call ICE re-gathering.
|
||||
// Same forwarding pattern as MediaPathReport.
|
||||
SignalMessage::CandidateUpdate { ref call_id, .. } => {
|
||||
let (peer_fp, peer_relay_fp) = {
|
||||
let reg = call_registry.lock().await;
|
||||
match reg.get(call_id) {
|
||||
Some(c) => (
|
||||
reg.peer_fingerprint(call_id, &client_fp)
|
||||
.map(|s| s.to_string()),
|
||||
c.peer_relay_fp.clone(),
|
||||
),
|
||||
None => (None, None),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(fp) = peer_fp {
|
||||
if let Some(ref origin_fp) = peer_relay_fp {
|
||||
if let Some(ref fm) = federation_mgr {
|
||||
let forward = SignalMessage::FederatedSignalForward {
|
||||
inner: Box::new(msg.clone()),
|
||||
origin_relay_fp: tls_fp.clone(),
|
||||
};
|
||||
if let Err(e) = fm.send_signal_to_peer(origin_fp, &forward).await {
|
||||
warn!(
|
||||
%call_id,
|
||||
%origin_fp,
|
||||
error = %e,
|
||||
"cross-relay CandidateUpdate forward failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let hub = signal_hub.lock().await;
|
||||
let _ = hub.send_to(&fp, &msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SignalMessage::Ping { timestamp_ms } => {
|
||||
let _ = transport.send_signal(&SignalMessage::Pong { timestamp_ms }).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user