signaling.rs
1 //! WebRTC Signaling via DHT 2 //! 3 //! Glue layer between `RtcStream` and the DHT for offer/answer/ICE exchange. 4 //! Uses "mailbox" pattern: store offers at `hash(prefix || target_pubkey)`. 5 6 #[cfg(feature = "webrtc")] 7 use abzu_dht::key::mailbox_id; 8 9 /// Mailbox prefix for WebRTC signaling offers 10 #[cfg(feature = "webrtc")] 11 const WEBRTC_INBOX_PREFIX: &[u8] = b"abzu-webrtc-inbox"; 12 13 /// A pending WebRTC offer from a remote peer 14 #[derive(Debug, Clone)] 15 #[cfg(feature = "webrtc")] 16 pub struct SignalOffer { 17 /// The offerer's public key 18 pub from_pubkey: [u8; 32], 19 /// The SDP offer 20 pub sdp: String, 21 } 22 23 /// A pending WebRTC answer 24 #[derive(Debug, Clone)] 25 #[cfg(feature = "webrtc")] 26 pub struct SignalAnswer { 27 /// The answerer's public key 28 pub from_pubkey: [u8; 32], 29 /// The SDP answer 30 pub sdp: String, 31 } 32 33 /// Compute the DHT key for a peer's WebRTC inbox 34 #[cfg(feature = "webrtc")] 35 pub fn webrtc_inbox_key(target_pubkey: &[u8; 32]) -> [u8; 32] { 36 mailbox_id(WEBRTC_INBOX_PREFIX, target_pubkey) 37 } 38 39 /// Encode an SDP offer as a DHT-storable payload 40 #[cfg(feature = "webrtc")] 41 pub fn encode_offer(from_pubkey: [u8; 32], sdp: &str) -> Vec<u8> { 42 let mut payload = Vec::with_capacity(32 + sdp.len()); 43 payload.extend_from_slice(&from_pubkey); 44 payload.extend_from_slice(sdp.as_bytes()); 45 payload 46 } 47 48 /// Decode an SDP offer from DHT payload 49 #[cfg(feature = "webrtc")] 50 pub fn decode_offer(payload: &[u8]) -> Option<SignalOffer> { 51 if payload.len() < 33 { 52 return None; 53 } 54 let from_pubkey: [u8; 32] = payload[..32].try_into().ok()?; 55 let sdp = String::from_utf8(payload[32..].to_vec()).ok()?; 56 Some(SignalOffer { from_pubkey, sdp }) 57 } 58 59 /// Encode an SDP answer as a DHT-storable payload 60 #[cfg(feature = "webrtc")] 61 pub fn encode_answer(from_pubkey: [u8; 32], sdp: &str) -> Vec<u8> { 62 encode_offer(from_pubkey, sdp) // Same format 63 } 64 65 /// Decode an SDP answer from DHT payload 66 #[cfg(feature = "webrtc")] 67 pub fn decode_answer(payload: &[u8]) -> Option<SignalAnswer> { 68 decode_offer(payload).map(|o| SignalAnswer { 69 from_pubkey: o.from_pubkey, 70 sdp: o.sdp, 71 }) 72 } 73 74 // ============================================================================ 75 // High-Level Orchestration 76 // ============================================================================ 77 78 #[cfg(feature = "webrtc")] 79 use std::sync::Arc; 80 81 #[cfg(feature = "webrtc")] 82 use abzu_transport::transports::webrtc::{default_ice_servers, RtcStream}; 83 84 #[cfg(feature = "webrtc")] 85 use crate::node::{Node, NodeError, PeerConnection}; 86 87 /// Initiate a WebRTC connection to a peer. 88 /// 89 /// This creates an SDP offer that should be stored in the target's DHT mailbox. 90 /// Returns the RtcStream (not yet connected) and the offer payload to store. 91 /// 92 /// # Flow 93 /// 1. Caller stores `offer_payload` at `webrtc_inbox_key(&peer_pubkey)` in DHT 94 /// 2. Caller polls for answer at their own inbox 95 /// 3. On answer, call `stream.set_remote_answer()` 96 #[cfg(feature = "webrtc")] 97 pub async fn create_webrtc_offer( 98 our_pubkey: [u8; 32], 99 peer_pubkey: [u8; 32], 100 ) -> Result<(RtcStream, Vec<u8>), NodeError> { 101 let ice_servers = default_ice_servers(); 102 103 let (stream, offer_sdp) = RtcStream::create_offer(peer_pubkey, ice_servers) 104 .await 105 .map_err(|e| NodeError::Transport(e))?; 106 107 let offer_payload = encode_offer(our_pubkey, &offer_sdp); 108 109 Ok((stream, offer_payload)) 110 } 111 112 /// Accept a WebRTC offer from a peer. 113 /// 114 /// This creates an SDP answer that should be stored in the offerer's DHT mailbox. 115 /// Returns the connected RtcStream and the answer payload to store. 116 /// 117 /// # Flow 118 /// 1. Caller retrieves offer from their DHT inbox 119 /// 2. Caller calls this function with the decoded offer 120 /// 3. Caller stores `answer_payload` at offerer's inbox 121 /// 4. RtcStream is ready to use (after ICE completes) 122 #[cfg(feature = "webrtc")] 123 pub async fn accept_webrtc_offer( 124 our_pubkey: [u8; 32], 125 offer: &SignalOffer, 126 ) -> Result<(RtcStream, Vec<u8>), NodeError> { 127 let ice_servers = default_ice_servers(); 128 129 let (stream, answer_sdp) = RtcStream::accept_offer(offer.from_pubkey, &offer.sdp, ice_servers) 130 .await 131 .map_err(|e| NodeError::Transport(e))?; 132 133 let answer_payload = encode_answer(our_pubkey, &answer_sdp); 134 135 Ok((stream, answer_payload)) 136 } 137 138 /// Register a connected WebRTC stream as a peer on the node. 139 /// 140 /// Call this after the ICE handshake completes (stream.is_connected() == true). 141 #[cfg(feature = "webrtc")] 142 pub async fn register_webrtc_peer(node: Arc<Node>, peer_pubkey: [u8; 32], stream: RtcStream) { 143 let conn = PeerConnection::new(Box::new(stream)); 144 node.add_peer(peer_pubkey, conn).await; 145 } 146 147 #[cfg(test)] 148 #[cfg(feature = "webrtc")] 149 mod tests { 150 use super::*; 151 152 #[test] 153 fn test_inbox_key_deterministic() { 154 let pubkey = [0xAB; 32]; 155 let key1 = webrtc_inbox_key(&pubkey); 156 let key2 = webrtc_inbox_key(&pubkey); 157 assert_eq!(key1, key2); 158 } 159 160 #[test] 161 fn test_inbox_key_different_pubkeys() { 162 let pk1 = [0xAB; 32]; 163 let pk2 = [0xCD; 32]; 164 assert_ne!(webrtc_inbox_key(&pk1), webrtc_inbox_key(&pk2)); 165 } 166 167 #[test] 168 fn test_offer_roundtrip() { 169 let from = [0x01; 32]; 170 let sdp = "v=0\r\no=- 123 456 IN IP4 127.0.0.1\r\n"; 171 172 let encoded = encode_offer(from, sdp); 173 let decoded = decode_offer(&encoded).unwrap(); 174 175 assert_eq!(decoded.from_pubkey, from); 176 assert_eq!(decoded.sdp, sdp); 177 } 178 179 #[test] 180 fn test_answer_roundtrip() { 181 let from = [0x02; 32]; 182 let sdp = "v=0\r\no=answer\r\n"; 183 184 let encoded = encode_answer(from, sdp); 185 let decoded = decode_answer(&encoded).unwrap(); 186 187 assert_eq!(decoded.from_pubkey, from); 188 assert_eq!(decoded.sdp, sdp); 189 } 190 191 #[test] 192 fn test_decode_short_payload() { 193 let short = vec![0u8; 31]; // Too short 194 assert!(decode_offer(&short).is_none()); 195 } 196 }