Skip to content

Commit ddcb5a5

Browse files
haikoscholalgesten
andauthored
Negotiate SCTP max message size (algesten#852)
* Init SCTP with max msg size constants * Extract and apply max-message-size from SDP * Consume self in SdpApi::accept_offer * Use correct const for max_receive_message_size LOCAL_MAX_MESSAGE_SIZE is what we are willing to receive. DEFAULT_REMOTE_MAX_MESSAGE_SIZE is what the remote is willing to receive, hence we are able to send. * Set remote_max_message_size in SCTP initialization * Set max_send_message_size on newly connected Association * Fix linter issues * cargo fmt * Use upstream sctp-proto at a fixed commit * Revert "cargo fmt" This reverts commit d54639b. * cargo fmt * Revert unrelated changes in apply_offer & apply_answer * Revert formatting changes in sdp/data.rs * fix formatting even more * clean up imports * correctly handle zero value for max_message_size * set max send message size in SNAP init case * Fix max-message-size lint * Add changelog entry for max-message-size * Resolve changelog conflict for max-message-size --------- Co-authored-by: Martin Algesten <martin@algesten.se>
1 parent 4510e04 commit ddcb5a5

7 files changed

Lines changed: 281 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
* Negotiate SCTP max message size for data channels #852
34
* Expose the negotiated DTLS protocol version #979
45
* Make MTU size configurable #967
56
* Add optional `drv` feature for using public identity types as `drv::Input` #974 #975

src/change/direct.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl<'a> DirectApi<'a> {
126126
/// connecting party.
127127
pub fn start_sctp(&mut self, client: bool) {
128128
self.rtc
129-
.try_init_sctp(client, None)
129+
.try_init_sctp(client, None, None)
130130
.expect("starting SCTP should be infallible")
131131
}
132132

@@ -162,7 +162,7 @@ impl<'a> DirectApi<'a> {
162162
client: bool,
163163
sctp_init_data: SctpInitData,
164164
) -> Result<(), RtcError> {
165-
self.rtc.try_init_sctp(client, Some(sctp_init_data))
165+
self.rtc.try_init_sctp(client, Some(sctp_init_data), None)
166166
}
167167

168168
/// Create a new data channel.

src/change/sdp.rs

Lines changed: 222 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::fmt;
44
use std::ops::{Deref, DerefMut};
5+
use std::slice::Iter;
56

67
use crate::Rtc;
78
use crate::RtcError;
@@ -100,6 +101,8 @@ impl<'a> SdpApi<'a> {
100101
// Ensure setup=active/passive is corresponding remote and init dtls.
101102
init_dtls(self.rtc, &offer)?;
102103

104+
let remote_max_message_size = extract_max_message_size(offer.media_lines.iter());
105+
103106
// Extract a=sctp-init from remote offer before apply_offer consumes it.
104107
let remote_sctp_init = offer.sctp_init().map(|v| v.to_owned());
105108

@@ -119,7 +122,8 @@ impl<'a> SdpApi<'a> {
119122

120123
if self.rtc.session.app().is_some() {
121124
let init_data = self.rtc.sctp.build_snap_init_data();
122-
self.rtc.try_init_sctp(client, init_data)?;
125+
self.rtc
126+
.try_init_sctp(client, init_data, remote_max_message_size)?;
123127
}
124128

125129
let params = AsSdpParams::new(self.rtc, None);
@@ -198,6 +202,8 @@ impl<'a> SdpApi<'a> {
198202
// Split out new channels, since that is not handled by the Session.
199203
let new_channels = pending.changes.take_new_channels();
200204

205+
let remote_max_message_size = extract_max_message_size(answer.media_lines.iter());
206+
201207
// Modify session with answer
202208
apply_answer(&mut self.rtc.session, pending.changes, answer)?;
203209

@@ -211,7 +217,8 @@ impl<'a> SdpApi<'a> {
211217

212218
if self.rtc.session.app().is_some() {
213219
let init_data = self.rtc.sctp.build_snap_init_data();
214-
self.rtc.try_init_sctp(client, init_data)?;
220+
self.rtc
221+
.try_init_sctp(client, init_data, remote_max_message_size)?;
215222
}
216223

217224
for (id, config) in new_channels {
@@ -1392,6 +1399,23 @@ fn update_media(
13921399
}
13931400
}
13941401

1402+
fn extract_max_message_size(mut media_lines: Iter<MediaLine>) -> Option<u32> {
1403+
if let Some(app_line) = media_lines.find(|m| m.typ.is_channel()) {
1404+
if let Some(max_size) = app_line.max_message_size() {
1405+
// RFC 8841 §6.1: a value of 0 means the peer can receive a message of
1406+
// any size, subject to local capacity. Represent that as the maximum.
1407+
let max_size = if max_size == 0 {
1408+
u32::MAX
1409+
} else {
1410+
u32::try_from(max_size).unwrap_or(u32::MAX)
1411+
};
1412+
return Some(max_size);
1413+
}
1414+
}
1415+
1416+
None
1417+
}
1418+
13951419
trait AsSdpMediaLine {
13961420
fn mid(&self) -> Mid;
13971421
fn msid(&self) -> Option<&Msid>;
@@ -1428,7 +1452,9 @@ impl AsSdpMediaLine for (Mid, usize) {
14281452
) -> MediaLine {
14291453
attrs.push(MediaAttribute::Mid(self.0));
14301454
attrs.push(MediaAttribute::SctpPort(5000));
1431-
attrs.push(MediaAttribute::MaxMessageSize(262144));
1455+
attrs.push(MediaAttribute::MaxMessageSize(
1456+
crate::sctp::LOCAL_MAX_MESSAGE_SIZE as usize,
1457+
));
14321458

14331459
MediaLine {
14341460
typ: sdp::MediaType::Application,
@@ -2477,4 +2503,197 @@ mod test {
24772503
// No space at the end
24782504
assert_eq!(count_lines(&line_string, "a=rid:no_attrs send"), 1);
24792505
}
2506+
2507+
#[test]
2508+
fn test_local_max_message_size_advertised() {
2509+
crate::init_crypto_default();
2510+
2511+
let now = Instant::now();
2512+
let mut rtc = Rtc::new(now);
2513+
2514+
// Create an offer with a data channel
2515+
let mut change = rtc.sdp_api();
2516+
change.add_channel("test-channel".into());
2517+
let (offer, _) = change.apply().unwrap();
2518+
2519+
// Find the application m-line in the offer
2520+
let app_line = offer
2521+
.media_lines
2522+
.iter()
2523+
.find(|m| m.typ.is_channel())
2524+
.expect("should have application m-line");
2525+
2526+
// Verify that max-message-size attribute is present and matches LOCAL_MAX_MESSAGE_SIZE
2527+
let max_size = app_line
2528+
.max_message_size()
2529+
.expect("max-message-size attribute should be present");
2530+
2531+
assert_eq!(
2532+
max_size,
2533+
crate::sctp::LOCAL_MAX_MESSAGE_SIZE as usize,
2534+
"max-message-size should match LOCAL_MAX_MESSAGE_SIZE constant"
2535+
);
2536+
2537+
// Also verify it's in the SDP string output
2538+
let sdp_string = offer.to_sdp_string();
2539+
let expected_line = format!("a=max-message-size:{}", crate::sctp::LOCAL_MAX_MESSAGE_SIZE);
2540+
assert!(
2541+
sdp_string.contains(&expected_line),
2542+
"SDP should contain max-message-size attribute with LOCAL_MAX_MESSAGE_SIZE value"
2543+
);
2544+
}
2545+
2546+
#[test]
2547+
fn test_remote_max_message_size_parsing() {
2548+
// Parse SDP with max-message-size attribute and verify value is extracted correctly
2549+
let sdp = "v=0\r\n\
2550+
o=- 0 0 IN IP4 172.17.0.1\r\n\
2551+
s=-\r\n\
2552+
c=IN IP4 172.17.0.1\r\n\
2553+
t=0 0\r\n\
2554+
a=group:BUNDLE 0\r\n\
2555+
a=fingerprint:sha-256 B4:12:1C:7C:7D:ED:F1:FA:61:07:57:9C:29:BE:58:E3:BC:41:E7:13:8E:7D\
2556+
:D3:9D:1F:94:6E:A5:23:46:94:23\r\n\
2557+
m=application 9999 UDP/DTLS/SCTP webrtc-datachannel\r\n\
2558+
a=mid:0\r\n\
2559+
a=ice-ufrag:test\r\n\
2560+
a=ice-pwd:testpassword1234\r\n\
2561+
a=setup:actpass\r\n\
2562+
a=sctp-port:5000\r\n\
2563+
a=max-message-size:131072\r\n\
2564+
";
2565+
2566+
let offer = SdpOffer::from_sdp_string(sdp).expect("should parse");
2567+
2568+
// Find the application m-line
2569+
let app_line = offer
2570+
.media_lines
2571+
.iter()
2572+
.find(|m| m.typ.is_channel())
2573+
.expect("should have application m-line");
2574+
2575+
assert_eq!(
2576+
app_line.max_message_size(),
2577+
Some(131072),
2578+
"max-message-size should be parsed as 131072"
2579+
);
2580+
}
2581+
2582+
#[test]
2583+
fn test_remote_max_message_size_applied() {
2584+
crate::init_crypto_default();
2585+
2586+
let now = Instant::now();
2587+
let mut rtc1 = Rtc::new(now);
2588+
let mut rtc2 = Rtc::new(now);
2589+
2590+
// Create an offer from rtc1 with a channel
2591+
let mut change1 = rtc1.sdp_api();
2592+
change1.add_channel("test-channel".into());
2593+
let (offer1, pending1) = change1.apply().unwrap();
2594+
2595+
// Get the offer SDP string and modify it to have a custom max-message-size
2596+
let custom_max_size1 = 98304u32;
2597+
let sdp_string = offer1.to_sdp_string();
2598+
let modified_sdp = sdp_string.replace(
2599+
&format!("a=max-message-size:{}", crate::sctp::LOCAL_MAX_MESSAGE_SIZE),
2600+
&format!("a=max-message-size:{}", custom_max_size1),
2601+
);
2602+
2603+
let modified_offer =
2604+
SdpOffer::from_sdp_string(&modified_sdp).expect("modified SDP should parse");
2605+
2606+
let answer = rtc2.sdp_api().accept_offer(modified_offer).unwrap();
2607+
2608+
// Verify that rtc2's SCTP send limit is set to the custom value from rtc1's offer
2609+
assert_eq!(
2610+
rtc2.sctp.remote_max_message_size(),
2611+
custom_max_size1,
2612+
"rtc2 should have remote max message size set to rtc1's advertised value"
2613+
);
2614+
2615+
// Now verify the reverse: rtc1 accepts rtc2's answer and applies its max-message-size
2616+
let custom_max_size2 = 131072u32;
2617+
let sdp_string = answer.to_sdp_string();
2618+
let modified_sdp = sdp_string.replace(
2619+
&format!("a=max-message-size:{}", crate::sctp::LOCAL_MAX_MESSAGE_SIZE),
2620+
&format!("a=max-message-size:{}", custom_max_size2),
2621+
);
2622+
2623+
let modified_answer =
2624+
SdpAnswer::from_sdp_string(&modified_sdp).expect("modified SDP should parse");
2625+
2626+
rtc1.sdp_api()
2627+
.accept_answer(pending1, modified_answer)
2628+
.unwrap();
2629+
2630+
assert_eq!(
2631+
rtc1.sctp.remote_max_message_size(),
2632+
custom_max_size2,
2633+
"rtc1 should have remote max message size set to rtc2's advertised value"
2634+
);
2635+
}
2636+
2637+
#[test]
2638+
fn test_remote_max_message_size_zero_means_unbounded() {
2639+
// RFC 8841 §6.1: a=max-message-size:0 means the peer can receive a message
2640+
// of any size, so we represent it as u32::MAX rather than passing 0 through
2641+
// (which would reject every non-empty outbound message in sctp-proto).
2642+
crate::init_crypto_default();
2643+
2644+
let now = Instant::now();
2645+
let mut rtc1 = Rtc::new(now);
2646+
let mut rtc2 = Rtc::new(now);
2647+
2648+
let mut change1 = rtc1.sdp_api();
2649+
change1.add_channel("test-channel".into());
2650+
let (offer1, _pending1) = change1.apply().unwrap();
2651+
2652+
let sdp_string = offer1.to_sdp_string();
2653+
let modified_sdp = sdp_string.replace(
2654+
&format!("a=max-message-size:{}", crate::sctp::LOCAL_MAX_MESSAGE_SIZE),
2655+
"a=max-message-size:0",
2656+
);
2657+
2658+
let modified_offer =
2659+
SdpOffer::from_sdp_string(&modified_sdp).expect("modified SDP should parse");
2660+
2661+
rtc2.sdp_api().accept_offer(modified_offer).unwrap();
2662+
2663+
assert_eq!(
2664+
rtc2.sctp.remote_max_message_size(),
2665+
u32::MAX,
2666+
"a=max-message-size:0 should be treated as unbounded (u32::MAX)"
2667+
);
2668+
}
2669+
2670+
#[test]
2671+
fn test_remote_max_message_size_too_large_clamps_to_unbounded() {
2672+
crate::init_crypto_default();
2673+
2674+
let now = Instant::now();
2675+
let mut rtc1 = Rtc::new(now);
2676+
let mut rtc2 = Rtc::new(now);
2677+
2678+
let mut change1 = rtc1.sdp_api();
2679+
change1.add_channel("test-channel".into());
2680+
let (offer1, _pending1) = change1.apply().unwrap();
2681+
2682+
let sdp_string = offer1.to_sdp_string();
2683+
let modified_sdp = sdp_string.replace(
2684+
&format!("a=max-message-size:{}", crate::sctp::LOCAL_MAX_MESSAGE_SIZE),
2685+
"a=max-message-size:4294967296",
2686+
);
2687+
2688+
let modified_offer =
2689+
SdpOffer::from_sdp_string(&modified_sdp).expect("modified SDP should parse");
2690+
2691+
rtc2.sdp_api().accept_offer(modified_offer).unwrap();
2692+
2693+
assert_eq!(
2694+
rtc2.sctp.remote_max_message_size(),
2695+
u32::MAX,
2696+
"a=max-message-size larger than u32::MAX should be treated as unbounded"
2697+
);
2698+
}
24802699
}

src/lib.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1480,14 +1480,20 @@ impl Rtc {
14801480
&mut self,
14811481
client: bool,
14821482
sctp_init_data: Option<SctpInitData>,
1483+
remote_max_message_size: Option<u32>,
14831484
) -> Result<(), RtcError> {
14841485
// If we got an m=application line, ensure we have negotiated the
14851486
// SCTP association with the other side.
14861487
if self.sctp.is_inited() {
14871488
return Ok(());
14881489
}
14891490

1490-
self.sctp.init(client, self.last_now, sctp_init_data)?;
1491+
self.sctp.init(
1492+
client,
1493+
self.last_now,
1494+
sctp_init_data,
1495+
remote_max_message_size,
1496+
)?;
14911497
Ok(())
14921498
}
14931499

0 commit comments

Comments
 (0)