Skip to content

Add initial TCP reconstruction implementation - #163

Open
JulianSchmid wants to merge 1 commit into
masterfrom
tcp-reassembly
Open

Add initial TCP reconstruction implementation#163
JulianSchmid wants to merge 1 commit into
masterfrom
tcp-reassembly

Conversation

@JulianSchmid

@JulianSchmid JulianSchmid commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added TCP stream reassembly for reconstructing ordered payloads across out-of-order, duplicated, and fragmented segments.
    • Added multi-stream management with buffering, stream lifecycle controls, FIN/RST handling, and configurable capacity.
    • Added zero-copy IGMPv1–v3 parsing and packet construction support.
    • Added an example demonstrating TCP reassembly with synthetic packets.
  • Testing
    • Expanded coverage for sequence wrapping, retransmissions, buffering limits, stream management, and IGMP integration.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds a std-only TCP stream reassembly module with stream identifiers, range tracking, bounded sequence-aware buffers, pooled multi-stream processing, buffer recycling, tests, a regression seed, and an executable example. The changelog documents the new functionality.

TCP stream reassembly

Layer / File(s) Summary
Reassembly contracts and module wiring
etherparse/src/lib.rs, etherparse/src/tcp_reassembly/mod.rs, etherparse/src/tcp_reassembly/tcp_stream_*.rs, etherparse/src/tcp_reassembly/tcp_segment_range.rs, etherparse/src/tcp_reassembly/tcp_reassemble_error.rs
Exports the std-gated module and defines stream identity, IPv4/IPv6 endpoint keys, mergeable absolute ranges, default capacity, and typed reassembly errors.
Single-stream payload reconstruction
etherparse/src/tcp_reassembly/tcp_stream_reassembly_buf.rs, etherparse/proptest-regressions/tcp_reassembly/*
Adds bounded TCP payload reconstruction with out-of-order and overlapping segment handling, sequence-number wrap-around support, FIN tracking, contiguous reads, consumption, reset, allocation reuse, and randomized regression coverage.
Multi-stream packet processing and usage
etherparse/src/tcp_reassembly/tcp_stream_reassembly_pool.rs, etherparse/examples/tcp_reassembly.rs, changelog.md
Adds packet-driven stream creation and lifecycle handling for SYN, RST, FIN, lazy initialization, eviction, and buffer recycling, with an example that feeds out-of-order packets and prints contiguous data.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PacketSource
  participant TcpStreamReassemblyPool
  participant TcpStreamReassemblyBuf
  participant Example

  PacketSource->>TcpStreamReassemblyPool: process_sliced_packet(SlicedPacket)
  TcpStreamReassemblyPool->>TcpStreamReassemblyPool: identify TCP stream and process flags
  TcpStreamReassemblyPool->>TcpStreamReassemblyBuf: add(sequence, payload, FIN)
  TcpStreamReassemblyBuf-->>TcpStreamReassemblyPool: contiguous payload
  TcpStreamReassemblyPool-->>Example: mutable reassembly buffer
  Example->>TcpStreamReassemblyBuf: consume(processed length)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: an initial TCP reconstruction/reassembly implementation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tcp-reassembly

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
etherparse/src/tcp_reassembly/tcp_stream_reassembly_pool.rs (1)

30-34: 🧹 Nitpick | 🔵 Trivial

Recycled buffer pools retain full capacity indefinitely.

Buffers pushed onto finished_data_bufs / finished_section_bufs keep their allocated capacity (a data buffer can be as large as max_capacity, 1 MiB by default). While the count is bounded by peak concurrent streams, a single traffic spike keeps that memory resident for the pool's lifetime. Consider capping the number of retained buffers (and/or shrinking oversized ones) so idle memory can be released after churn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@etherparse/src/tcp_reassembly/tcp_stream_reassembly_pool.rs` around lines 30
- 34, Bound the recycled-buffer pools finished_data_bufs and
finished_section_bufs so they cannot retain all peak-concurrency allocations
indefinitely. When returning buffers to these pools, retain only a capped number
and discard or shrink oversized buffers, while preserving reuse for
appropriately sized buffers and allowing idle memory to be released after
traffic churn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.md`:
- Line 6: Update the TcpStreamReassemblyBuf changelog description to hyphenate
the compound adjectives: use “32-bit sequence number” and “monotonic 64-bit”
while preserving the rest of the wording.

In `@etherparse/src/tcp_reassembly/tcp_stream_reassembly_buf.rs`:
- Around line 228-264: Defer FIN recording in the add flow until after the
max-window validation succeeds for non-empty payloads. Keep the existing
fin_offset update for the empty-payload fast path, remove the earlier
unconditional update, and after the SegmentBeyondMaxWindow check set fin_offset
from the validated post-trim end_abs when fin is true.

---

Nitpick comments:
In `@etherparse/src/tcp_reassembly/tcp_stream_reassembly_pool.rs`:
- Around line 30-34: Bound the recycled-buffer pools finished_data_bufs and
finished_section_bufs so they cannot retain all peak-concurrency allocations
indefinitely. When returning buffers to these pools, retain only a capped number
and discard or shrink oversized buffers, while preserving reuse for
appropriately sized buffers and allowing idle memory to be released after
traffic churn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a2add71-fdeb-4864-8363-af8b1e6422f7

📥 Commits

Reviewing files that changed from the base of the PR and between 70f72be and 7adf8c2.

📒 Files selected for processing (11)
  • changelog.md
  • etherparse/examples/tcp_reassembly.rs
  • etherparse/proptest-regressions/tcp_reassembly/tcp_stream_reassembly_buf.txt
  • etherparse/src/lib.rs
  • etherparse/src/tcp_reassembly/mod.rs
  • etherparse/src/tcp_reassembly/tcp_reassemble_error.rs
  • etherparse/src/tcp_reassembly/tcp_segment_range.rs
  • etherparse/src/tcp_reassembly/tcp_stream_id.rs
  • etherparse/src/tcp_reassembly/tcp_stream_ip_id.rs
  • etherparse/src/tcp_reassembly/tcp_stream_reassembly_buf.rs
  • etherparse/src/tcp_reassembly/tcp_stream_reassembly_pool.rs

Comment thread changelog.md
## Unreleased

* Added TCP stream reassembly support (in the `std`-only `tcp_reassembly` module, contains allocations):
* `TcpStreamReassemblyBuf`, a re-usable buffer that reconstructs the payload byte stream of a single direction of a TCP connection. It is robust against retransmits, re-ordered & duplicated/overlapping segments, big offset jumps and 32 bit sequence number wrap-around (all bookkeeping is done in a monotonic 64 bit "absolute stream offset" space). Data is exposed in-place via `contiguous` and freed via `consume` ("collect then clean").

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hyphenate the compound adjectives. "32 bit sequence number" → "32-bit sequence number" and "monotonic 64 bit" → "monotonic 64-bit".

🧰 Tools
🪛 LanguageTool

[grammar] ~6-~6: Use a hyphen to join words.
Context: ...apping segments, big offset jumps and 32 bit sequence number wrap-around (all boo...

(QB_NEW_EN_HYPHEN)


[grammar] ~6-~6: Use a hyphen to join words.
Context: ...ll bookkeeping is done in a monotonic 64 bit "absolute stream offset" space). Dat...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.md` at line 6, Update the TcpStreamReassemblyBuf changelog
description to hyphenate the compound adjectives: use “32-bit sequence number”
and “monotonic 64-bit” while preserving the rest of the wording.

Source: Linters/SAST tools

Comment on lines +228 to +264
let end_abs = start_abs + payload.len() as i128;

// record the end of the stream if the FIN flag is set (FIN occupies the
// sequence number right after the payload)
if fin {
let fin_pos = if end_abs < 0 { 0 } else { end_abs as u64 };
self.fin_offset = Some(fin_pos);
}

// nothing to store for an empty payload (e.g. pure SYN/ACK/FIN segments)
if payload.is_empty() {
return Ok(());
}

// trim the part that lies before the read cursor (already consumed /
// retransmitted data)
let cursor = self.base_offset as i128;
let (payload, start_abs) = if start_abs < cursor {
let trim = (cursor - start_abs) as usize;
if trim >= payload.len() {
// fully before the cursor -> nothing to store
return Ok(());
}
(&payload[trim..], cursor)
} else {
(payload, start_abs)
};
let end_abs = start_abs + payload.len() as i128;

// enforce the maximum buffer window (measured from the read cursor)
let end_offset = (end_abs - cursor) as u64;
if end_offset > self.max_capacity as u64 {
return Err(SegmentBeyondMaxWindow {
end_offset,
max_capacity: self.max_capacity,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

fin_offset is mutated before the max-window check, so a rejected FIN segment still records the FIN.

For a FIN-bearing segment with a non-empty payload that lands beyond max_capacity, add sets self.fin_offset (Line 234) and then returns Err(SegmentBeyondMaxWindow) (Line 260). On an existing stream (retained on error by the pool at tcp_stream_reassembly_pool.rs Line 204) this leaves the buffer believing the stream ends at an offset whose data was never stored. Defer the FIN recording until after the segment is accepted; keep it for the empty-payload fast path where there is no window concern.

🐛 Proposed fix
         let end_abs = start_abs + payload.len() as i128;

-        // record the end of the stream if the FIN flag is set (FIN occupies the
-        // sequence number right after the payload)
-        if fin {
-            let fin_pos = if end_abs < 0 { 0 } else { end_abs as u64 };
-            self.fin_offset = Some(fin_pos);
-        }
-
-        // nothing to store for an empty payload (e.g. pure SYN/ACK/FIN segments)
-        if payload.is_empty() {
-            return Ok(());
-        }
+        // nothing to store for an empty payload (e.g. pure SYN/ACK/FIN segments)
+        if payload.is_empty() {
+            if fin {
+                self.fin_offset = Some(if end_abs < 0 { 0 } else { end_abs as u64 });
+            }
+            return Ok(());
+        }

Then, after the window check passes (following Line 264, where the post-trim end_abs is >= cursor >= 0):

        // record the end of the stream only once the segment is accepted
        if fin {
            self.fin_offset = Some(end_abs as u64);
        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@etherparse/src/tcp_reassembly/tcp_stream_reassembly_buf.rs` around lines 228
- 264, Defer FIN recording in the add flow until after the max-window validation
succeeds for non-empty payloads. Keep the existing fin_offset update for the
empty-payload fast path, remove the earlier unconditional update, and after the
SegmentBeyondMaxWindow check set fin_offset from the validated post-trim end_abs
when fin is true.

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