Skip to content

fix(toolkit): dpkt reads SYN, FIN and RST from the flags octet whatever the NS bit (#1618) - #1621

Merged
JarryShaw merged 1 commit into
mainfrom
fix/1618-dpkt-tcp-ns-flags
Oct 12, 2026
Merged

JarryShaw merged 1 commit into
mainfrom
fix/1618-dpkt-tcp-ns-flags

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

Tick the commit type your subject line carries.

  • fix — corrects a defect
  • feat — adds a feature
  • perf — changes performance, not behaviour
  • refactor — changes neither behaviour nor performance
  • test — tests only
  • docs — documentation only
  • ci — workflows or build tooling
  • release — bumps the version or rolls up a distribution
  • chore — anything else

Description of your pull request and other information

Closes #1618.

tcp_reassembly and tcp_traceflow in pcapkit/toolkit/dpkt.py read RST, SYN and FIN as positions 5–7 of bin(tcp.flags) padded to eight digits. dpkt 1.9.8 makes TCP.flags 9 bits wide, with NS as 0x100 and the three reserved bits in a separate _rsv. So with NS set, every flag was read one bit low: RST came from PSH, SYN from RST, and FIN from SYN.

A new private _tcp_flags masks each flag out with dpkt's TH_RST, TH_SYN and TH_FIN. Those are the only flags either adapter reads (ack= is the acknowledgement number). Masking also works on older dpkt, where flags was a single octet.

I checked every flags octet (256) against every NS and reserved-bit value (16), so 4096 frames, comparing each against the default engine. Fields that disagreed: 3072 → 0 for reassembly, and the same for flow tracing (all of them with NS set). Scapy agreed both before and after. The new tests also check that every field of each reassembly and trace input, and the flows, match the default engine. These frames carry no payload, so no datagrams are reassembled from them.

…er the NS bit (#1618)

`tcp_reassembly` and `tcp_traceflow` read each flag by its position in
`bin(tcp.flags)` padded to eight digits, but dpkt 1.9.8 makes `TCP.flags`
nine bits wide with NS as bit 8, so with NS set every flag was read one bit
low (a SYN as a FIN, an RST as a SYN, a PSH as an RST). A new `_tcp_flags`
masks each one out with dpkt's `TH_RST`, `TH_SYN` and `TH_FIN`.
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) review: running A cross-review is in flight against the current head - no verdict yet labels Oct 10, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Verdict: GOOD TO GO at edd88ed5a.

Sonnet cross-review. It confirmed the root cause in dpkt's own tcp.py: flags is 9 bits, with TH_NS = 0x100, and the 9-bit layout already exists in 1.9.7.2. It rebuilt 4096 frames independently: 3072 → 0 differing fields for reassembly and the same for tracing, and scapy agrees with default before and after. No other tcp.flags reader exists in the toolkit. The new file fails on main's dpkt.py (5 failed) and passes with the fix, which I also reproduced (25 passed / 5 failed with the dpkt unit tests).

Its one required change was body wording: no datagrams are reassembled from these frames, so the datagram comparison is between two empty lists. I have reworded the body. Non-blocking: that 'ids' subtest is vacuous here, and a failing 4096-frame assertion prints a very long diff.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Coverage: 89.71% (unit tier, Python 3.14, edd88ed5a, Unit Tests run success)

Package Statements Missed Branches Partial Cover
pcapkit (top level) 104 3 20 3 95.16%
pcapkit/const 18858 954 2366 868 91.06%
pcapkit/corekit 2366 67 760 35 96.48%
pcapkit/dumpkit 275 3 100 3 98.40%
pcapkit/foundation 2884 147 1010 36 94.07%
pcapkit/interface 138 7 52 5 93.68%
pcapkit/protocols 17115 226 4570 191 98.06%
pcapkit/toolkit 935 134 334 6 84.40%
pcapkit/utilities 494 4 146 4 98.75%
pcapkit/vendor 4409 2342 1006 157 43.25%

Per-file detail: the coverage-html artifact of this run.

@JarryShaw
JarryShaw merged commit 9083163 into main Oct 12, 2026
40 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in PyPCAPKit Oct 12, 2026
@JarryShaw
JarryShaw deleted the fix/1618-dpkt-tcp-ns-flags branch October 12, 2026 23:36
@JarryShaw JarryShaw removed the review: good-to-go Cross-review at the current head says ready; CI state is separate label Oct 12, 2026
JarryShaw added a commit that referenced this pull request Oct 14, 2026
Third-party engines are best effort (#1687), so the dpkt code that exists
only to match the default engine comes out:

- revert #1662, #1655, #1624, #1622, #1614 and the dpkt hunks of #1605:
  the octet walk and its link-type stash, IPv6 in IPv4 and IPv4 in IPv6,
  refusing MPLS and ISL, the tunnel and extension-header walks, the IPv4
  header checks and the layer bounds
- keep #1690's timestamps, #1594's parser choice, #1621 and #1653
- re-land as guards: a layer dpkt's own lengths misplace (#1646) is
  re-serialised, not refused with ProtocolError; a Total Length below the
  IHL keeps the header whole (#1616), and a header the capture cuts is no
  input; packet2dict and packet2chain take a str payload and a raw frame
- delete the modules #1662 and #1655 added; the 1615/1616 module keeps
  only what its #1616 tests use, as no scapy test imports it now; dpkt's
  comparisons with the default engine assert constants or the capture's
  octets, or are dropped
- drop pypcapfile's VLAN_TAGS, VLAN_TAG_LEN and IPV6_EXTENSION_HEADERS,
  which only dpkt imported, and the 1668 module's _Agreement base, which
  nothing subclasses now
- the docs drop the depth claims and name what dpkt leaves out silently

The dpkt modules, the shared modules that run dpkt, tests/foundation/engines
and the project guards pass on 3.14 and 3.11 (dpkt 1.9.8, pypcapfile 0.12.0).
JarryShaw added a commit that referenced this pull request Oct 14, 2026
Third-party engines are best effort (#1687), so the dpkt code that exists
only to match the default engine comes out:

- revert #1662, #1655, #1624, #1622, #1614 and the dpkt hunks of #1605:
  the octet walk and its link-type stash, IPv6 in IPv4 and IPv4 in IPv6,
  refusing MPLS and ISL, the tunnel and extension-header walks, the IPv4
  header checks and the layer bounds
- keep #1690's timestamps, #1594's parser choice, #1621 and #1653
- re-land as guards: a layer dpkt's own lengths misplace (#1646) is
  re-serialised, not refused with ProtocolError; a Total Length below the
  IHL keeps the header whole (#1616), and a header the capture cuts is no
  input; packet2dict and packet2chain take a str payload and a raw frame
- delete the modules #1662 and #1655 added; the 1615/1616 module keeps
  only what its #1616 tests use, as no scapy test imports it now; dpkt's
  comparisons with the default engine assert constants or the capture's
  octets, or are dropped
- drop pypcapfile's VLAN_TAGS, VLAN_TAG_LEN and IPV6_EXTENSION_HEADERS,
  which only dpkt imported, and the 1668 module's _Agreement base, which
  nothing subclasses now
- the docs drop the depth claims and name what dpkt leaves out silently

The dpkt modules, the shared modules that run dpkt, tests/foundation/engines
and the project guards pass on 3.14 and 3.11 (dpkt 1.9.8, pypcapfile 0.12.0).
@JarryShaw JarryShaw added this to the PyPCAPKit v1.5 milestone Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(toolkit): dpkt misreads SYN/FIN/RST when the TCP NS bit is set

1 participant