/ abzu-core / src / signaling.rs
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  }