Skip to content

fix(tcp): Nagle treats a full options-sized segment as full, not partial - #50

Open
Yossioren wants to merge 1 commit into
ccie18643:masterfrom
Yossioren:fix/nagle-full-segment-with-options
Open

Yossioren wants to merge 1 commit into
ccie18643:masterfrom
Yossioren:fix/nagle-full-segment-with-options

Conversation

@Yossioren

Copy link
Copy Markdown

'TcpTxEngine.transmit_data' decided whether a segment is "partial" (sub-MSS, subject to Nagle / Minshall deferral) by comparing its payload length with '_snd_mss'. But when options ride on every segment

  • TSopt (12 bytes), plus SACK blocks or AccECN when active - the largest payload is 'mss_for_data' = '_snd_mss - options_overhead' (RFC 6691 §2 Req B, already applied a few lines earlier when sizing the segment). Every full segment was therefore classified as partial, 'SND.SML' moved past it, and the next full segment was held until the previous one was ACKed: with timestamps negotiated (the Linux default) a bulk send went out one segment per ACK. Against a Linux receiver this also hits its 40 ms delayed-ACK timer, since it never gets the second segment it waits for; measured upload through PyTCP fell to ~0.2 Mbit/s on a local link.

Compare with 'mss_for_data' instead, matching Linux, whose Nagle and Minshall checks ('tcp_nagle_check' / 'tcp_minshall_update' in net/ipv4/tcp_output.c) use 'mss_now' from 'tcp_current_mss()', which already subtracts the established-connection option length.

Reference: RFC 1122 §4.2.3.4 (Nagle algorithm).
Reference: RFC 6691 §2 Req B (sender reduces data length for options).

New integration test
'test__data_transfer_send__nagle_does_not_hold_full_tsopt_segments' sent 1 of 3 full TSopt segments before the fix (the rest held by Nagle) and all 3 on 3 ticks after it.

make test: 13845 passing, 11 skipped. make lint clean.

'TcpTxEngine.transmit_data' decided whether a segment is "partial"
(sub-MSS, subject to Nagle / Minshall deferral) by comparing its
payload length with '_snd_mss'. But when options ride on every segment
- TSopt (12 bytes), plus SACK blocks or AccECN when active - the
largest payload is 'mss_for_data' = '_snd_mss - options_overhead'
(RFC 6691 §2 Req B, already applied a few lines earlier when sizing
the segment). Every full segment was therefore classified as partial,
'SND.SML' moved past it, and the next full segment was held until the
previous one was ACKed: with timestamps negotiated (the Linux default)
a bulk send went out one segment per ACK. Against a Linux receiver
this also hits its 40 ms delayed-ACK timer, since it never gets the
second segment it waits for; measured upload through PyTCP fell to
~0.2 Mbit/s on a local link.

Compare with 'mss_for_data' instead, matching Linux, whose Nagle and
Minshall checks ('tcp_nagle_check' / 'tcp_minshall_update' in
net/ipv4/tcp_output.c) use 'mss_now' from 'tcp_current_mss()', which
already subtracts the established-connection option length.

Reference: RFC 1122 §4.2.3.4 (Nagle algorithm).
Reference: RFC 6691 §2 Req B (sender reduces data length for options).

New integration test
'test__data_transfer_send__nagle_does_not_hold_full_tsopt_segments'
sent 1 of 3 full TSopt segments before the fix (the rest held by
Nagle) and all 3 on 3 ticks after it.

make test: 13845 passing, 11 skipped. make lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant