core: RTC fetches server TURN creds from /api/turn-cred (cross-network P2P)
The RTC transport hardcoded Google STUN, so native clients never used the TURN
relay the homelab server already runs — meaning hole-punch failures (symmetric
NAT / CGNAT, i.e. cellular) had no E2E fallback. Now RtcTransport fetches
{server}/api/turn-cred at startup (same contract the web client uses) and feeds
the returned STUN+TURN iceServers into every RTCPeerConnection. Falls back to
STUN if the endpoint is absent (verified: local server 404s → STUN, delivery
still works via the other transports).
This is the client half of "cross-network P2P that just works" — the server
half (integrated pion TURN + /api/turn-cred) is already deployed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,14 @@ pub(crate) struct RtcTransport {
|
||||
api: Arc<API>,
|
||||
peers: Arc<Mutex<HashMap<String, Arc<Peer>>>>,
|
||||
sink: StdMutex<Option<Sink>>,
|
||||
ice: StdMutex<Vec<RTCIceServer>>, // refreshed from /api/turn-cred at start
|
||||
}
|
||||
|
||||
fn default_ice() -> Vec<RTCIceServer> {
|
||||
vec![RTCIceServer {
|
||||
urls: vec!["stun:stun.l.google.com:19302".to_owned()],
|
||||
..Default::default()
|
||||
}]
|
||||
}
|
||||
|
||||
impl RtcTransport {
|
||||
@@ -59,6 +67,7 @@ impl RtcTransport {
|
||||
api: Arc::new(api),
|
||||
peers: Arc::new(Mutex::new(HashMap::new())),
|
||||
sink: StdMutex::new(None),
|
||||
ice: StdMutex::new(default_ice()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,16 +75,49 @@ impl RtcTransport {
|
||||
self.sink.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn rtc_config() -> RTCConfiguration {
|
||||
fn rtc_config(&self) -> RTCConfiguration {
|
||||
RTCConfiguration {
|
||||
ice_servers: vec![RTCIceServer {
|
||||
urls: vec!["stun:stun.l.google.com:19302".to_owned()],
|
||||
..Default::default()
|
||||
}],
|
||||
ice_servers: self.ice.lock().unwrap().clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch server-issued ICE servers (STUN + short-lived TURN creds) from
|
||||
/// `/api/turn-cred`. This is what makes cross-network P2P actually connect
|
||||
/// when hole-punching fails (symmetric NAT / CGNAT). Falls back to STUN.
|
||||
async fn refresh_ice(&self) {
|
||||
let url = format!("{}/api/turn-cred", self.cfg.server.trim_end_matches('/'));
|
||||
let resp = match self.http.get(&url).send().await {
|
||||
Ok(r) if r.status().is_success() => r,
|
||||
_ => return, // keep the STUN fallback
|
||||
};
|
||||
let Ok(v) = resp.json::<serde_json::Value>().await else {
|
||||
return;
|
||||
};
|
||||
let Some(arr) = v["iceServers"].as_array() else {
|
||||
return;
|
||||
};
|
||||
let mut servers = Vec::new();
|
||||
for s in arr {
|
||||
let urls: Vec<String> = s["urls"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().filter_map(|u| u.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
if urls.is_empty() {
|
||||
continue;
|
||||
}
|
||||
servers.push(RTCIceServer {
|
||||
urls,
|
||||
username: s["username"].as_str().unwrap_or("").to_string(),
|
||||
credential: s["credential"].as_str().unwrap_or("").to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if !servers.is_empty() {
|
||||
*self.ice.lock().unwrap() = servers;
|
||||
}
|
||||
}
|
||||
|
||||
// ── bus helpers ────────────────────────────────────────────────────────
|
||||
|
||||
async fn post(&self, body: Value) {
|
||||
@@ -112,7 +154,7 @@ impl RtcTransport {
|
||||
async fn new_peer(self: &Arc<Self>, remote: &str, offerer: bool) -> Arc<Peer> {
|
||||
let pc = Arc::new(
|
||||
self.api
|
||||
.new_peer_connection(Self::rtc_config())
|
||||
.new_peer_connection(self.rtc_config())
|
||||
.await
|
||||
.expect("peer connection"),
|
||||
);
|
||||
@@ -416,6 +458,10 @@ impl Transport for RtcTransport {
|
||||
|
||||
// Signaling receive loop with reconnect/backoff.
|
||||
rt.spawn(async move {
|
||||
// Pull TURN creds before any peer is built so they're in the ICE
|
||||
// config (presence→offer takes a beat, so this lands in time).
|
||||
self.refresh_ice().await;
|
||||
|
||||
let min = Duration::from_millis(500);
|
||||
let max = Duration::from_secs(10);
|
||||
let mut backoff = min;
|
||||
|
||||
Reference in New Issue
Block a user