File
Blob: firmware/vendor/str0m/src/sctp/snap.rs
| 1 | //! SNAP (SCTP Negotiation Acceleration Protocol) types. |
| 2 | //! |
| 3 | //! See [draft-hancke-tsvwg-snap](https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/). |
| 4 | |
| 5 | use std::sync::Arc; |
| 6 | |
| 7 | use base64ct::{Base64, Encoding}; |
| 8 | use sctp_proto::{ClientConfig, TransportConfig, generate_snap_token}; |
| 9 | |
| 10 | use super::{LOCAL_MAX_MESSAGE_SIZE, SctpError as Error, SctpReceiveLimits}; |
| 11 | |
| 12 | /// Build the WebRTC transport config with unlimited retransmits. |
| 13 | /// |
| 14 | /// For WebRTC, we never want to give up retransmitting init and data packets. |
| 15 | /// The connectivity is in ICE, and SCTP should not give up until ICE gives up. |
| 16 | pub(super) fn webrtc_transport_config(limits: Option<SctpReceiveLimits>) -> Arc<TransportConfig> { |
| 17 | let mut config = TransportConfig::default() |
| 18 | .with_max_init_retransmits(None) |
| 19 | .with_max_data_retransmits(None) |
| 20 | .with_max_receive_message_size(LOCAL_MAX_MESSAGE_SIZE); |
| 21 | if let Some(limits) = limits { |
| 22 | config = config.with_receive_limits(limits); |
| 23 | } |
| 24 | Arc::new(config) |
| 25 | } |
| 26 | |
| 27 | /// Out-of-band SCTP INIT data for SNAP negotiation. |
| 28 | /// |
| 29 | /// Holds the local and remote SCTP INIT chunks exchanged via signaling |
| 30 | /// (e.g. SDP `a=sctp-init`). When both are present, the SCTP association |
| 31 | /// skips the 4-way handshake and goes directly to established state. |
| 32 | /// |
| 33 | /// Typical flow: |
| 34 | /// |
| 35 | /// 1. Create `SctpInitData`. |
| 36 | /// 2. Call [`Self::local_init_chunk()`] and signal the returned bytes. |
| 37 | /// 3. Receive the peer's INIT bytes over signaling. |
| 38 | /// 4. Call [`Self::set_remote_init_chunk()`]. |
| 39 | /// 5. Pass the populated value to |
| 40 | /// [`DirectApi::start_sctp_with_snap()`][crate::change::DirectApi::start_sctp_with_snap]. |
| 41 | /// |
| 42 | /// The local INIT chunk is lazily generated and cached on first access. |
| 43 | /// |
| 44 | /// # Example |
| 45 | /// ``` |
| 46 | /// use str0m::channel::SctpInitData; |
| 47 | /// |
| 48 | /// let mut data = SctpInitData::new(); |
| 49 | /// |
| 50 | /// // Get local INIT chunk to send to remote peer |
| 51 | /// let local_init = data.local_init_chunk().expect("valid chunk"); |
| 52 | /// |
| 53 | /// // Later, set the remote INIT chunk received from peer |
| 54 | /// // data.set_remote_init_chunk(remote_init_bytes); |
| 55 | /// ``` |
| 56 | #[derive(Debug, Clone)] |
| 57 | pub struct SctpInitData { |
| 58 | pub(crate) transport: Arc<TransportConfig>, |
| 59 | pub(crate) local_init: Option<Vec<u8>>, |
| 60 | pub(crate) remote_init: Option<Vec<u8>>, |
| 61 | } |
| 62 | |
| 63 | impl Default for SctpInitData { |
| 64 | fn default() -> Self { |
| 65 | SctpInitData { |
| 66 | transport: webrtc_transport_config(None), |
| 67 | local_init: None, |
| 68 | remote_init: None, |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | impl SctpInitData { |
| 74 | /// Creates new default SCTP INIT data. |
| 75 | /// |
| 76 | /// By default, max init and data retransmits are set to `None` (unlimited), |
| 77 | /// which is recommended for WebRTC where connectivity is managed by ICE. |
| 78 | pub fn new() -> Self { |
| 79 | Self::default() |
| 80 | } |
| 81 | |
| 82 | /// Create SNAP data with the same receive limits used by `RtcConfig`. |
| 83 | /// Configure this before generating the local INIT bytes so its advertised |
| 84 | /// receive window matches the association's resource policy. |
| 85 | pub fn with_receive_limits(limits: SctpReceiveLimits) -> Self { |
| 86 | Self::with_optional_receive_limits(Some(limits)) |
| 87 | } |
| 88 | |
| 89 | pub(super) fn with_optional_receive_limits(limits: Option<SctpReceiveLimits>) -> Self { |
| 90 | Self { |
| 91 | transport: webrtc_transport_config(limits), |
| 92 | local_init: None, |
| 93 | remote_init: None, |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | /// Get the local INIT chunk bytes for out-of-band signaling. |
| 98 | /// |
| 99 | /// This generates a fresh INIT chunk using the transport config parameters. |
| 100 | /// The result can be exchanged with the remote peer via a signaling channel, |
| 101 | /// allowing both sides to skip the SCTP 4-way handshake. |
| 102 | /// |
| 103 | /// The generated bytes are cached so subsequent calls return the same INIT. |
| 104 | /// |
| 105 | /// If you plan to call `start_sctp_with_snap()`, call this method first so |
| 106 | /// the local INIT is captured in the `SctpInitData` you pass in. |
| 107 | pub fn local_init_chunk(&mut self) -> Result<Vec<u8>, Error> { |
| 108 | if let Some(init) = &self.local_init { |
| 109 | return Ok(init.clone()); |
| 110 | } |
| 111 | let init = generate_snap_token(&self.transport)?.to_vec(); |
| 112 | self.local_init = Some(init.clone()); |
| 113 | Ok(init) |
| 114 | } |
| 115 | |
| 116 | /// Get the local INIT chunk as a base64-encoded string for out-of-band signaling. |
| 117 | /// |
| 118 | /// This generates a fresh INIT chunk using the transport config parameters. |
| 119 | /// The result can be exchanged with the remote peer via a signaling channel, |
| 120 | /// allowing both sides to skip the SCTP 4-way handshake. |
| 121 | /// |
| 122 | /// The generated bytes are cached so subsequent calls return the same string. |
| 123 | /// |
| 124 | /// If you plan to call `start_sctp_with_snap()`, call this method first so |
| 125 | /// the local INIT is captured in the `SctpInitData` you pass in. |
| 126 | pub fn local_init_string(&mut self) -> Result<String, Error> { |
| 127 | self.local_init_chunk().map(|c| b64_encode(&c)) |
| 128 | } |
| 129 | |
| 130 | /// Check if remote INIT chunk has been configured for out-of-band signaling. |
| 131 | pub fn has_remote_init_chunk(&self) -> bool { |
| 132 | self.remote_init.is_some() |
| 133 | } |
| 134 | |
| 135 | /// Get the remote INIT chunk as a base64-encoded string, if set. |
| 136 | pub fn remote_init_string(&self) -> Option<String> { |
| 137 | self.remote_init.as_ref().map(|b| b64_encode(b)) |
| 138 | } |
| 139 | |
| 140 | /// Set the remote INIT chunk for out-of-band signaling. |
| 141 | /// |
| 142 | /// When both local and remote INIT chunks are exchanged via a signaling |
| 143 | /// channel, the SCTP association can skip the 4-way handshake and go |
| 144 | /// directly to established state. |
| 145 | /// |
| 146 | /// This must be called before starting SCTP with SNAP. |
| 147 | pub fn set_remote_init_chunk(&mut self, value: Vec<u8>) { |
| 148 | self.remote_init = Some(value); |
| 149 | } |
| 150 | |
| 151 | /// Set the remote INIT chunk from a base64-encoded string. |
| 152 | /// |
| 153 | /// Convenience method for signaling channels that exchange strings (e.g. SDP). |
| 154 | pub fn set_remote_init_string(&mut self, value: &str) -> Result<(), Error> { |
| 155 | let mut buf = vec![0u8; value.len()]; |
| 156 | let len = Base64::decode(value, &mut buf) |
| 157 | .map_err(|_| Error::InvalidSnap)? |
| 158 | .len(); |
| 159 | buf.truncate(len); |
| 160 | self.set_remote_init_chunk(buf); |
| 161 | Ok(()) |
| 162 | } |
| 163 | |
| 164 | /// Build a `ClientConfig` from this data, consuming it. |
| 165 | /// |
| 166 | /// Only used internally by [`super::RtcSctp::init()`] to feed sctp-proto. |
| 167 | pub(crate) fn into_client_config(self) -> ClientConfig { |
| 168 | let mut config = ClientConfig::new(); |
| 169 | config.transport = self.transport; |
| 170 | match (self.local_init, self.remote_init) { |
| 171 | (Some(local), Some(remote)) => { |
| 172 | config = config.with_snap(local.into(), remote.into()); |
| 173 | } |
| 174 | (Some(_), None) | (None, Some(_)) => { |
| 175 | unreachable!("SNAP requires both local and remote INIT chunks"); |
| 176 | } |
| 177 | (None, None) => {} |
| 178 | } |
| 179 | config |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | pub(super) fn b64_encode(data: &[u8]) -> String { |
| 184 | let mut buf = vec![0u8; Base64::encoded_len(data)]; |
| 185 | let encoded = Base64::encode(data, &mut buf).expect("buffer sized correctly"); |
| 186 | encoded.to_string() |
| 187 | } |