From 76418ebb3c6f879c1827ff902f13db60da3933a1 Mon Sep 17 00:00:00 2001 From: FrenchToblerone54 <218982835+FrenchToblerone54@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:53:03 +0000 Subject: [PATCH] Keep segments within the MSS announced by the peer Segments were cut to the stack's MTU alone, so a peer on a smaller link received segments larger than the MSS it had announced in its SYN. Read the option from the SYN and bound the payload by it (RFC 9293 3.7.1), counting the TCP options of the segment against it. An announced value below 536 is read as 536. --- src/stream/tcb.rs | 14 +++++++++++++- src/stream/tcp.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/stream/tcb.rs b/src/stream/tcb.rs index abff5c1..7b2bc7a 100644 --- a/src/stream/tcb.rs +++ b/src/stream/tcb.rs @@ -63,6 +63,8 @@ pub(crate) struct Tcb { seq: SeqNum, ack: SeqNum, mtu: u16, + /// The largest segment the peer announced in the MSS option of its SYN, unbounded without one. + peer_mss: u16, last_received_ack: SeqNum, send_window: u32, send_window_shift: u8, @@ -125,6 +127,7 @@ impl Tcb { seq: seq.into(), ack, mtu, + peer_mss: u16::MAX, last_received_ack: seq.into(), send_window: peer_window as u32, send_window_shift, @@ -144,10 +147,19 @@ impl Tcb { } } + /// RFC 9293 §3.7.1: a sender never sends a segment larger than the peer announced. Below 88, + /// the floor Linux applies (TCP_MIN_MSS), the option is not followed, so a peer cannot ask for + /// its data a few bytes at a time. + pub(super) fn set_peer_mss(&mut self, mss: u16) { + self.peer_mss = mss.max(88); + } + pub fn calculate_payload_max_len(&self, ip_header_size: usize, tcp_header_size: usize) -> usize { let send_window = self.get_send_window() as usize; let mtu = self.get_mtu() as usize; - std::cmp::min(send_window, mtu.saturating_sub(ip_header_size + tcp_header_size)) + // The MSS counts the payload and the TCP options of a segment, but not the 20-byte base header. + let mss = (self.peer_mss as usize).saturating_sub(tcp_header_size.saturating_sub(20)); + send_window.min(mtu.saturating_sub(ip_header_size + tcp_header_size)).min(mss) } pub fn update_duplicate_ack_count(&mut self, rcvd_ack: SeqNum) { diff --git a/src/stream/tcp.rs b/src/stream/tcp.rs index 212b7a0..ab6c62b 100644 --- a/src/stream/tcp.rs +++ b/src/stream/tcp.rs @@ -196,7 +196,7 @@ impl IpStackTcpStream { }) }) .flatten(); - let tcb = Tcb::new( + let mut tcb = Tcb::new( SeqNum(tcp.sequence_number), tcp.window_size, peer_window_shift, @@ -207,6 +207,15 @@ impl IpStackTcpStream { config.rto, config.max_retransmit_count, ); + // Like the window scale, the MSS option is read only from a segment carrying SYN. + if tcp.syn + && let Some(mss) = tcp.options_iterator().flatten().find_map(|option| match option { + TcpOptionElement::MaximumSegmentSize(mss) => Some(mss), + _ => None, + }) + { + tcb.set_peer_mss(mss); + } let tuple = NetworkTuple::new(src_addr, dst_addr, true); if !tcp.syn { if !tcp.rst @@ -1575,6 +1584,27 @@ mod tests { assert_eq!(tcb.get_send_window(), 40_000 << 14); } + /// RFC 9293 §3.7.1: a segment never carries more payload than the MSS the peer announced in its + /// SYN, and an announced MSS below 88 (Linux's TCP_MIN_MSS) is read as 88. + #[tokio::test] + async fn segments_stay_within_the_peers_mss() { + for (announced, expected) in [(600, 600), (100, 100), (40, 88)] { + let (mut stream, syn_ack, mut up_rx) = open(u16::MAX, &[TcpOptionElement::MaximumSegmentSize(announced)]).await; + let peer_seq = SeqNum(syn_ack.acknowledgment_number); + feed(&stream, &syn_ack, peer_seq, u16::MAX, &[1; 4]); + up_rx.recv().await.unwrap(); + + let (_, waker) = recording_waker(); + let mut cx = Context::from_waker(&waker); + let written = match Pin::new(&mut stream).poll_write(&mut cx, &[7; 1460]) { + Poll::Ready(Ok(n)) => n, + other => panic!("the write did not complete: {other:?}"), + }; + assert_eq!(written, expected, "peer MSS {announced}: wrong write size"); + assert_eq!(up_rx.recv().await.unwrap().payload.map(|p| p.len()), Some(expected)); + } + } + /// A closed peer window holds the writer, and the segment that reopens it wakes the writer, both /// when it completes the handshake and when it arrives on an established connection. #[tokio::test]