1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
use crate::peer_connection::configuration::setting_engine::SctpMaxMessageSize;
use crate::peer_connection::transport::RTCTransportId;
use crate::peer_connection::transport::dtls::role::RTCDtlsRole;
use crate::peer_connection::transport::sctp::capabilities::SCTPTransportCapabilities;
use crate::peer_connection::transport::sctp::state::RTCSctpTransportState;
use sctp::{Association, AssociationHandle};
use shared::error::Result;
use shared::{TransportContext, TransportProtocol};
use std::collections::HashMap;
pub(crate) mod capabilities;
pub(crate) mod state;
/// The stream-count ceiling used when no negotiated value is available yet.
///
/// Not the spec's `maxChannels` — that is the negotiated minimum, reported by
/// [`SctpTransport::max_channels`]. This is only the "no constraint known" fallback.
pub(crate) const SCTP_MAX_CHANNELS: u16 = u16::MAX;
/// SCTPTransport provides details about the SCTP transport.
///
/// Not `Default`: `id` and `dtls_transport_id` identify *this* connection's transports, and a
/// default-constructed id would compare equal across connections — the one thing
/// [`RTCTransportId`] must never do. Nothing constructed one this way.
pub(crate) struct SctpTransport {
pub(crate) id: RTCTransportId,
pub(crate) dtls_transport_id: RTCTransportId,
pub(crate) sctp_endpoint: Option<::sctp::Endpoint>,
pub(crate) sctp_transport_config: Option<::sctp::TransportConfig>,
pub(crate) sctp_associations: HashMap<AssociationHandle, Association>,
// SCTPTransportState doesn't have an enum to distinguish between New/Connecting
// so we need a dedicated field
pub(crate) is_started: bool,
// The *configured* ceiling from SettingEngine — an input to the negotiation below, not the
// value W3C `maxMessageSize` reports. See `negotiated_max_message_size`.
pub(crate) max_message_size: SctpMaxMessageSize,
// The negotiated [[MaxMessageSize]] slot: the result of reconciling the configured ceiling
// above with the peer's `max-message-size` SDP attribute, computed once in `start()`.
// `None` before the association has been negotiated.
pub(crate) negotiated_max_message_size: Option<u32>,
// Optional override for the SCTP receive-buffer size (a_rwnd flow-control window),
// in bytes. None uses the sctp crate default (INITIAL_RECV_BUF_SIZE, 1 MiB).
pub(crate) max_receive_buffer_size: Option<u32>,
pub(crate) internal_buffer: Vec<u8>,
}
impl SctpTransport {
pub(crate) fn new(
max_message_size: SctpMaxMessageSize,
max_receive_buffer_size: Option<u32>,
id: RTCTransportId,
dtls_transport_id: RTCTransportId,
) -> Self {
Self {
id,
dtls_transport_id,
sctp_endpoint: None,
sctp_transport_config: None,
sctp_associations: HashMap::new(),
is_started: false,
max_message_size,
negotiated_max_message_size: None,
max_receive_buffer_size,
internal_buffer: vec![],
}
}
pub(crate) fn calc_message_size(remote_max_message_size: u32, can_send_size: u32) -> u32 {
if remote_max_message_size == 0 && can_send_size == 0 {
u32::MAX
} else if remote_max_message_size == 0 {
can_send_size
} else if can_send_size == 0 || can_send_size > remote_max_message_size {
remote_max_message_size
} else {
can_send_size
}
}
/// The single association this transport carries, if one has been created.
///
/// `sctp_associations` is a map because the endpoint API is written for the general case, but
/// a peer connection negotiates exactly one SCTP association.
fn association(&self) -> Option<&Association> {
self.sctp_associations.values().next()
}
/// W3C `SctpTransport.state`.
///
/// Derived from the association rather than tracked separately: the previous `state` field was
/// assigned once at construction and never updated or read, so any getter over it would have
/// reported `Connecting` for the lifetime of the connection.
pub(crate) fn state(&self) -> RTCSctpTransportState {
match self.association() {
None => RTCSctpTransportState::Connecting,
Some(association) if association.is_closed() => RTCSctpTransportState::Closed,
Some(association) if association.is_handshaking() => RTCSctpTransportState::Connecting,
Some(_) => RTCSctpTransportState::Connected,
}
}
/// W3C `SctpTransport.maxMessageSize`, in bytes.
///
/// `None` until the association has been negotiated, at which point it is the reconciliation of
/// this endpoint's configured ceiling with the peer's `max-message-size` SDP attribute
/// ([RFC 8841 §6]).
///
/// Always finite. The spec types the attribute `unrestricted double` so that an
/// implementation with no limit can report positive infinity; this one always has a limit,
/// because the value also sizes a real allocation — see `start()`.
///
/// [RFC 8841 §6]: https://datatracker.ietf.org/doc/html/rfc8841#section-6
pub(crate) fn max_message_size(&self) -> Option<u32> {
self.negotiated_max_message_size
}
/// W3C `SctpTransport.maxChannels`: the minimum of the negotiated inbound and outbound
/// stream counts.
///
/// `None` until the association reaches the connected state, matching the spec's "this
/// attribute's value will be null until the SCTP transport goes into the `connected` state".
pub(crate) fn max_channels(&self) -> Option<u16> {
self.association()?.negotiated_max_streams()
}
/// Start the SCTPTransport. Since both local and remote parties must mutually
/// create an SCTPTransport, SCTP SO (Simultaneous Open) is used to establish
/// a connection over SCTP.
pub(crate) fn start(
&mut self,
dtls_role: RTCDtlsRole,
remote_caps: SCTPTransportCapabilities,
local_port: u16,
_remote_port: u16,
) -> Result<()> {
if self.is_started {
return Ok(());
}
self.is_started = true;
// W3C §6.1.1.2 defines `canSendSize` as what this endpoint can actually send, and allows
// 0 only when the implementation "can handle messages of any size". This one cannot: the
// working buffer below is a real allocation, so the ceiling is `MAX_MESSAGE_SIZE`. A
// configured 0 therefore resolves to that ceiling rather than to "unlimited".
//
// Without this, `calc_message_size(0, 0)` yields `u32::MAX` and the two lines below try to
// allocate a 4 GiB buffer and configure an unbounded message size — an OOM waiting for the
// first peer that advertises no limit. Reporting `u32::MAX` while enforcing something far
// smaller would be worse than either: `maxMessageSize` is a promise to the application
// about what it may pass to `send()`, so the value reported and the value enforced have to
// be the same one.
let can_send_size = match self.max_message_size.as_usize() as u32 {
0 => SctpMaxMessageSize::MAX_MESSAGE_SIZE,
configured => configured,
};
let max_message_size =
SctpTransport::calc_message_size(remote_caps.max_message_size, can_send_size);
// This is the spec's [[MaxMessageSize]] slot. It was previously computed here, used to size
// the buffer and the transport config, and then discarded, which left nothing for
// `maxMessageSize` to report.
self.negotiated_max_message_size = Some(max_message_size);
self.internal_buffer.resize(max_message_size as usize, 0u8);
let sctp_endpoint_config = ::sctp::EndpointConfig::default();
let mut sctp_transport_config = ::sctp::TransportConfig::default()
.with_max_message_size(max_message_size)
.with_sctp_port(local_port);
if let Some(recv_buf) = self.max_receive_buffer_size {
sctp_transport_config = sctp_transport_config.with_max_receive_buffer_size(recv_buf);
}
//TODO: add remote_port support
if dtls_role == RTCDtlsRole::Client {
self.sctp_endpoint = Some(sctp::Endpoint::new(
TransportContext::default().local_addr, // placeholder; rewritten per-transmit by the ICE handler
TransportProtocol::UDP, // placeholder; rewritten per-transmit by the ICE handler
sctp_endpoint_config.into(),
None,
));
self.sctp_transport_config = Some(sctp_transport_config);
} else {
self.sctp_endpoint = Some(::sctp::Endpoint::new(
TransportContext::default().local_addr, // placeholder; rewritten per-transmit by the ICE handler
TransportProtocol::UDP, // placeholder; rewritten per-transmit by the ICE handler
sctp_endpoint_config.into(),
Some(::sctp::ServerConfig::new(sctp_transport_config).into()),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::peer_connection::transport::{RTCTransportId, TransportKind};
/// A fixed nonce keeps test ids deterministic while still distinguishing the kinds.
fn test_transport_id(kind: TransportKind) -> RTCTransportId {
RTCTransportId::new(0xabcd_ef01_2345_6789, kind)
}
use super::*;
fn started_transport(
configured: SctpMaxMessageSize,
remote_max_message_size: u32,
) -> SctpTransport {
let mut transport = SctpTransport::new(
configured,
None,
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
transport
.start(
RTCDtlsRole::Client,
SCTPTransportCapabilities {
max_message_size: remote_max_message_size,
},
5000,
5000,
)
.expect("start");
transport
}
// `maxMessageSize` is the *negotiated* value, not the configured ceiling. Configure 64 KiB
// and have the peer advertise 16 KiB: reporting the configured value would answer 65536.
#[test]
fn max_message_size_reports_the_negotiated_value_not_the_configured_one() {
let transport = started_transport(SctpMaxMessageSize::Bounded(65536), 16384);
assert_eq!(Some(16384), transport.max_message_size());
}
// The reverse direction, so the test cannot pass by simply echoing the remote's number.
#[test]
fn max_message_size_takes_the_smaller_of_the_two_limits() {
let transport = started_transport(SctpMaxMessageSize::Bounded(16384), 65536);
assert_eq!(Some(16384), transport.max_message_size());
}
// Neither side names a limit. This implementation still has one — the working buffer is a
// real allocation — so `canSendSize` resolves to `MAX_MESSAGE_SIZE` rather than to
// "unlimited" (W3C §6.1.1.2 allows 0 only for an implementation that can handle any size).
// Reporting `u32::MAX` here would promise the application 4 GiB messages and then allocate a
// 256 KiB buffer.
#[test]
fn no_limit_on_either_side_resolves_to_the_implementation_ceiling() {
let transport = started_transport(SctpMaxMessageSize::Bounded(0), 0);
assert_eq!(
Some(SctpMaxMessageSize::MAX_MESSAGE_SIZE),
transport.max_message_size()
);
assert_eq!(
SctpMaxMessageSize::MAX_MESSAGE_SIZE as usize,
transport.internal_buffer.len(),
"the reported size and the buffer actually allocated must be the same number"
);
}
#[test]
fn max_message_size_is_none_before_start() {
let transport = SctpTransport::new(
SctpMaxMessageSize::default(),
None,
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
assert_eq!(None, transport.max_message_size());
}
// No association yet: the spec's initial state, and nothing to report for maxChannels.
#[test]
fn state_and_max_channels_before_any_association() {
let transport = SctpTransport::new(
SctpMaxMessageSize::default(),
None,
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
assert_eq!(RTCSctpTransportState::Connecting, transport.state());
assert_eq!(None, transport.max_channels());
}
// A default `Association` is in `AssociationState::Closed`, so the transport must report
// Closed rather than the Connecting it was constructed with. This is the case the old
// never-updated `state` field got wrong.
#[test]
fn state_follows_a_closed_association() {
let mut transport = SctpTransport::new(
SctpMaxMessageSize::default(),
None,
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
transport
.sctp_associations
.insert(AssociationHandle(0), Association::default());
assert_eq!(RTCSctpTransportState::Closed, transport.state());
assert_eq!(
None,
transport.max_channels(),
"a closed association negotiated nothing"
);
}
// Starting a Client transport stores the built TransportConfig on the struct, so we
// can assert the configured (or default) receive-buffer size flowed through `start()`.
#[test]
fn start_applies_configured_receive_buffer_size() {
let mut transport = SctpTransport::new(
SctpMaxMessageSize::default(),
Some(200_000),
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
transport
.start(
RTCDtlsRole::Client,
SCTPTransportCapabilities {
max_message_size: 0,
},
5000,
5000,
)
.expect("start");
assert_eq!(
transport
.sctp_transport_config
.expect("client transport config")
.max_receive_buffer_size(),
200_000
);
}
#[test]
fn start_without_override_uses_default_receive_buffer_size() {
let mut transport = SctpTransport::new(
SctpMaxMessageSize::default(),
None,
test_transport_id(TransportKind::Sctp),
test_transport_id(TransportKind::Dtls),
);
transport
.start(
RTCDtlsRole::Client,
SCTPTransportCapabilities {
max_message_size: 0,
},
5000,
5000,
)
.expect("start");
// `None` keeps the sctp crate default (INITIAL_RECV_BUF_SIZE = 1 MiB).
assert_eq!(
transport
.sctp_transport_config
.expect("client transport config")
.max_receive_buffer_size(),
1024 * 1024
);
}
}