Skip to content
File

Blob: firmware/vendor/str0m/src/sctp/snap.rs

rust188 lines
1//! SNAP (SCTP Negotiation Acceleration Protocol) types.
2//!
3//! See [draft-hancke-tsvwg-snap](https://datatracker.ietf.org/doc/draft-hancke-tsvwg-snap/).
4 
5use std::sync::Arc;
6 
7use base64ct::{Base64, Encoding};
8use sctp_proto::{ClientConfig, TransportConfig, generate_snap_token};
9 
10use 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.
16pub(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)]
57pub 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 
63impl 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 
73impl 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 
183pub(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}