//! SNAP (SCTP Negotiation Acceleration Protocol) types. //! //! See [draft-hancke-tsvwg-snap](https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/). use std::sync::Arc; use base64ct::{Base64, Encoding}; use sctp_proto::{ClientConfig, TransportConfig, generate_snap_token}; use super::{LOCAL_MAX_MESSAGE_SIZE, SctpError as Error, SctpReceiveLimits}; /// Build the WebRTC transport config with unlimited retransmits. /// /// For WebRTC, we never want to give up retransmitting init and data packets. /// The connectivity is in ICE, and SCTP should not give up until ICE gives up. pub(super) fn webrtc_transport_config(limits: Option) -> Arc { let mut config = TransportConfig::default() .with_max_init_retransmits(None) .with_max_data_retransmits(None) .with_max_receive_message_size(LOCAL_MAX_MESSAGE_SIZE); if let Some(limits) = limits { config = config.with_receive_limits(limits); } Arc::new(config) } /// Out-of-band SCTP INIT data for SNAP negotiation. /// /// Holds the local and remote SCTP INIT chunks exchanged via signaling /// (e.g. SDP `a=sctp-init`). When both are present, the SCTP association /// skips the 4-way handshake and goes directly to established state. /// /// Typical flow: /// /// 1. Create `SctpInitData`. /// 2. Call [`Self::local_init_chunk()`] and signal the returned bytes. /// 3. Receive the peer's INIT bytes over signaling. /// 4. Call [`Self::set_remote_init_chunk()`]. /// 5. Pass the populated value to /// [`DirectApi::start_sctp_with_snap()`][crate::change::DirectApi::start_sctp_with_snap]. /// /// The local INIT chunk is lazily generated and cached on first access. /// /// # Example /// ``` /// use str0m::channel::SctpInitData; /// /// let mut data = SctpInitData::new(); /// /// // Get local INIT chunk to send to remote peer /// let local_init = data.local_init_chunk().expect("valid chunk"); /// /// // Later, set the remote INIT chunk received from peer /// // data.set_remote_init_chunk(remote_init_bytes); /// ``` #[derive(Debug, Clone)] pub struct SctpInitData { pub(crate) transport: Arc, pub(crate) local_init: Option>, pub(crate) remote_init: Option>, } impl Default for SctpInitData { fn default() -> Self { SctpInitData { transport: webrtc_transport_config(None), local_init: None, remote_init: None, } } } impl SctpInitData { /// Creates new default SCTP INIT data. /// /// By default, max init and data retransmits are set to `None` (unlimited), /// which is recommended for WebRTC where connectivity is managed by ICE. pub fn new() -> Self { Self::default() } /// Create SNAP data with the same receive limits used by `RtcConfig`. /// Configure this before generating the local INIT bytes so its advertised /// receive window matches the association's resource policy. pub fn with_receive_limits(limits: SctpReceiveLimits) -> Self { Self::with_optional_receive_limits(Some(limits)) } pub(super) fn with_optional_receive_limits(limits: Option) -> Self { Self { transport: webrtc_transport_config(limits), local_init: None, remote_init: None, } } /// Get the local INIT chunk bytes for out-of-band signaling. /// /// This generates a fresh INIT chunk using the transport config parameters. /// The result can be exchanged with the remote peer via a signaling channel, /// allowing both sides to skip the SCTP 4-way handshake. /// /// The generated bytes are cached so subsequent calls return the same INIT. /// /// If you plan to call `start_sctp_with_snap()`, call this method first so /// the local INIT is captured in the `SctpInitData` you pass in. pub fn local_init_chunk(&mut self) -> Result, Error> { if let Some(init) = &self.local_init { return Ok(init.clone()); } let init = generate_snap_token(&self.transport)?.to_vec(); self.local_init = Some(init.clone()); Ok(init) } /// Get the local INIT chunk as a base64-encoded string for out-of-band signaling. /// /// This generates a fresh INIT chunk using the transport config parameters. /// The result can be exchanged with the remote peer via a signaling channel, /// allowing both sides to skip the SCTP 4-way handshake. /// /// The generated bytes are cached so subsequent calls return the same string. /// /// If you plan to call `start_sctp_with_snap()`, call this method first so /// the local INIT is captured in the `SctpInitData` you pass in. pub fn local_init_string(&mut self) -> Result { self.local_init_chunk().map(|c| b64_encode(&c)) } /// Check if remote INIT chunk has been configured for out-of-band signaling. pub fn has_remote_init_chunk(&self) -> bool { self.remote_init.is_some() } /// Get the remote INIT chunk as a base64-encoded string, if set. pub fn remote_init_string(&self) -> Option { self.remote_init.as_ref().map(|b| b64_encode(b)) } /// Set the remote INIT chunk for out-of-band signaling. /// /// When both local and remote INIT chunks are exchanged via a signaling /// channel, the SCTP association can skip the 4-way handshake and go /// directly to established state. /// /// This must be called before starting SCTP with SNAP. pub fn set_remote_init_chunk(&mut self, value: Vec) { self.remote_init = Some(value); } /// Set the remote INIT chunk from a base64-encoded string. /// /// Convenience method for signaling channels that exchange strings (e.g. SDP). pub fn set_remote_init_string(&mut self, value: &str) -> Result<(), Error> { let mut buf = vec![0u8; value.len()]; let len = Base64::decode(value, &mut buf) .map_err(|_| Error::InvalidSnap)? .len(); buf.truncate(len); self.set_remote_init_chunk(buf); Ok(()) } /// Build a `ClientConfig` from this data, consuming it. /// /// Only used internally by [`super::RtcSctp::init()`] to feed sctp-proto. pub(crate) fn into_client_config(self) -> ClientConfig { let mut config = ClientConfig::new(); config.transport = self.transport; match (self.local_init, self.remote_init) { (Some(local), Some(remote)) => { config = config.with_snap(local.into(), remote.into()); } (Some(_), None) | (None, Some(_)) => { unreachable!("SNAP requires both local and remote INIT chunks"); } (None, None) => {} } config } } pub(super) fn b64_encode(data: &[u8]) -> String { let mut buf = vec![0u8; Base64::encoded_len(data)]; let encoded = Base64::encode(data, &mut buf).expect("buffer sized correctly"); encoded.to_string() }