Skip to content

Enforce the TCP receive window to bound per-session memory - #90

Merged
SajjadPourali merged 2 commits into
narrowlink:mainfrom
IntellyCode:enforce-recv-window
Aug 30, 2026
Merged

Enforce the TCP receive window to bound per-session memory#90
SajjadPourali merged 2 commits into
narrowlink:mainfrom
IntellyCode:enforce-recv-window

Conversation

@IntellyCode

Copy link
Copy Markdown
Contributor

ipstack advertises a receive window but never enforces it: the reassembly buffer and the data_tx handoff channel are both unbounded, so a peer that outpaces the reader (or holds a reassembly gap open) grows a session's memory without limit — an OOM risk on memory-constrained hosts.

This makes the window real:

  • add_unordered_packet drops out-of-order segments once the reassembly buffer reaches read_buffer_size (head-of-line always admitted).
  • data_tx/data_rx are bounded; extract_data_n_write_upstream reserves a slot before consuming, so a stalled reader shrinks the advertised window and backpressures the peer. poll_read signals a Notify to re-drive the flush when space frees.
  • SWS avoidance: advertise zero below one MSS, so a full receiver uses the peer's persist mode.
  • A FIN is accepted only once ack reaches its sequence, so a full channel can't strand the tail.
  • consume_unordered_packets trims a segment a retransmission left straddling ack instead of wedging.

Covered by unit tests and an async test; validated live against a real TCP connection (reader stalled → server throughput drops to zero, recovers on resume, clean close). If you'd prefer an in-repo integration test, I can add ~200 lines to tests/ that build a packet-framed in-memory device and assert the window closes under a stalled reader and reopens on drain.

The receive window ipstack advertises now holds. A segment beyond it is
dropped for the peer to resend, the head-of-line segment always admitted
so the stream advances. The handoff channel to the reader is bounded and
filled by reserving a slot before consuming, so buffered data leaves the
reassembly map only once it has a home; a reader that frees space wakes
the loop to flush more and the follow-up ACK carries the reopened window.
The window is advertised as zero below one segment, so a stalled reader
puts the peer into persist mode until space frees.

Consuming trims a stale head entry a re-segmented retransmission left
below the ack, and a FIN is accepted only once the data before it has
been consumed, so a full channel never strands the tail.
The stream's first tokio test drives extract_data_n_write_upstream
against a full handoff channel: buffered data stays in the reassembly
map and the ack holds until the reader drains a slot, then the tail
flushes and the ack advances.
@IntellyCode
IntellyCode force-pushed the enforce-recv-window branch 2 times, most recently from 323baf0 to d0785d6 Compare August 25, 2026 17:01
@SajjadPourali
SajjadPourali self-requested a review August 27, 2026 14:51
@SajjadPourali SajjadPourali self-assigned this Aug 27, 2026
Comment thread src/stream/tcb.rs
while remaining_bytes > 0 {
if let Some(seq) = self.unordered_packets.keys().next().copied() {
if seq != self.ack {
if seq > self.ack {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this causes some issues with wrap-around after the maximum sequence number ($2^{32} - 1$) is reached.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not go through ordinary >.

SeqNum isn't a plain u32. it carries its own Ord.

impl Ord for SeqNum {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        let diff = self.0.wrapping_sub(other.0);
        if diff == 0 { Ordering::Equal }
        else if diff < MAX_DIFF { Ordering::Greater }
        else { Ordering::Less }
    }
}

So this comparison is wrap safe,

@SajjadPourali
SajjadPourali merged commit e1d8506 into narrowlink:main Aug 30, 2026
2 checks passed
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.

2 participants