From 6cd61fc63b5c6660654dcc27499c015235736a49 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 12 Apr 2026 08:55:01 +0400 Subject: [PATCH] =?UTF-8?q?feat(federation):=20Phase=204.1=20=E2=80=94=20c?= =?UTF-8?q?all-*=20rooms=20are=20implicitly=20global?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All rooms with names starting with 'call-' are now treated as global rooms by the federation pipeline. This enables relay- mediated media fallback for cross-relay direct calls: when Alice on Relay A and Bob on Relay B both join the same call- room, the federation media forwarding pipeline (GlobalRoomActive announcements + datagram forwarding + presence replication) kicks in automatically without any runtime registration step. Previously, cross-relay direct calls that couldn't go P2P (symmetric NAT on either side) failed with "no media path" because the call- room wasn't in the configured global_rooms set and media datagrams weren't forwarded across the federation link. The relay's existing ACL for call-* rooms (only the two authorized fingerprints from the call registry can join) prevents random clients from creating or eavesdropping on call rooms. ## Changes ### `is_global_room` (federation.rs) Added `room.starts_with("call-")` check before the static global_rooms set lookup. Returns true immediately for any call-prefixed room. ### `resolve_global_room` (federation.rs) Return type changed from `Option<&str>` to `Option` (owned) because call-* room names aren't stored on `self` — they come from the caller and resolve to themselves as the canonical name. The 13 callers continue to work via String/&str auto-deref; 4 HashMap lookups needed explicit `.as_str()` or `&` borrows. Full workspace test: 423 passing (no regressions). Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-relay/src/federation.rs | 54 +++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/crates/wzp-relay/src/federation.rs b/crates/wzp-relay/src/federation.rs index c7a4e33..01eedc6 100644 --- a/crates/wzp-relay/src/federation.rs +++ b/crates/wzp-relay/src/federation.rs @@ -255,27 +255,55 @@ impl FederationManager { } /// Check if a room name (which may be hashed) is a global room. + /// + /// Phase 4.1: ALL `call-*` rooms are implicitly global for + /// federation. This is the simplest path to cross-relay direct + /// calling with relay-mediated media fallback: when both peers + /// join the same `call-` room on their respective relays, + /// the federation media pipeline automatically forwards + /// datagrams between them. The relay's existing ACL (`call-*` + /// rooms are restricted to the two authorized participants in + /// the call registry) prevents random clients from creating or + /// joining `call-*` rooms. pub fn is_global_room(&self, room: &str) -> bool { + if room.starts_with("call-") { + return true; + } self.resolve_global_room(room).is_some() } /// Resolve a room name (raw or hashed) to the canonical global room name. /// Returns the configured global room name if it matches. - pub fn resolve_global_room(&self, room: &str) -> Option<&str> { + /// + /// Phase 4.1: `call-*` rooms resolve to themselves (they ARE + /// the canonical name — no hashing or aliasing involved). + /// + /// Returns `Option` (owned) instead of `Option<&str>` + /// because call-* room names aren't stored on `self` — they + /// come from the caller and we just confirm "yes, this is + /// global" by returning it back. Pre-4.1 callers that used + /// the reference for equality checks or hashing work + /// unchanged via String/&str auto-deref. + pub fn resolve_global_room(&self, room: &str) -> Option { + // Phase 4.1: call-* rooms are implicitly global, resolve + // to themselves + if room.starts_with("call-") { + return Some(room.to_string()); + } // Direct match (raw room name, e.g. Android clients) if self.global_rooms.contains(room) { - return Some(self.global_rooms.iter().find(|n| n.as_str() == room).unwrap()); + return Some(room.to_string()); } // Hashed match (desktop clients hash room names for SNI privacy) self.global_rooms.iter().find(|name| { wzp_crypto::hash_room_name(name) == room - }).map(|s| s.as_str()) + }).map(|s| s.to_string()) } /// Get the canonical federation room hash for a room. /// Always uses the configured global room name, not the client-provided name. pub fn global_room_hash(&self, room: &str) -> [u8; 8] { - if let Some(canonical) = self.resolve_global_room(room) { + if let Some(ref canonical) = self.resolve_global_room(room) { room_hash(canonical) } else { room_hash(room) @@ -347,8 +375,8 @@ impl FederationManager { let mut result = Vec::new(); for link in links.values() { // Check canonical name - if let Some(c) = canonical { - if let Some(remote) = link.remote_participants.get(c) { + if let Some(ref c) = canonical { + if let Some(remote) = link.remote_participants.get(c.as_str()) { result.extend(remote.iter().cloned()); } // Also check raw room name, but only if different from canonical @@ -807,12 +835,12 @@ async fn handle_signal( let mut all_participants = mgr.local_participant_list(&local_room); let links = fm.peer_links.lock().await; for link in links.values() { - if let Some(canonical) = fm.resolve_global_room(&local_room) { - if let Some(remote) = link.remote_participants.get(canonical) { + if let Some(ref canonical) = fm.resolve_global_room(&local_room) { + if let Some(remote) = link.remote_participants.get(canonical.as_str()) { all_participants.extend(remote.iter().cloned()); } // Also check raw room name, but only if different from canonical - if canonical != local_room { + if canonical != &local_room { if let Some(remote) = link.remote_participants.get(&local_room) { all_participants.extend(remote.iter().cloned()); } @@ -843,8 +871,8 @@ async fn handle_signal( // Clear remote participants for this peer+room link.remote_participants.remove(&room); // Also try canonical name - if let Some(canonical) = fm.resolve_global_room(&room) { - link.remote_participants.remove(canonical); + if let Some(ref canonical) = fm.resolve_global_room(&room) { + link.remote_participants.remove(canonical.as_str()); } } @@ -858,8 +886,8 @@ async fn handle_signal( let mut result = Vec::new(); for (fp, link) in links.iter() { if fp == peer_fp { continue; } - if let Some(c) = canonical { - if let Some(remote) = link.remote_participants.get(c) { + if let Some(ref c) = canonical { + if let Some(remote) = link.remote_participants.get(c.as_str()) { result.extend(remote.iter().cloned()); } }