Skip to content

refactor(vm): read raw packets as ipfw_chk does - #116

Merged
3Hren merged 1 commit into
mainfrom
refactor/raw-packet
Sep 17, 2026
Merged

3Hren merged 1 commit into
mainfrom
refactor/raw-packet

Conversation

@3Hren

@3Hren 3Hren commented Sep 17, 2026

Copy link
Copy Markdown
Member

RawIPv4Packet took the IPv4 header to be twenty bytes whatever its IHL said, and RawIPv6Packet read the upper layer right after the fixed header. A packet with IP options or an IPv6 extension header therefore had its ports read from the wrong bytes, and no IPv6 packet was ever a fragment: IsFragment was a constant false. Follow-up to #115, which moved the decision of which fields mean something into the VM.

Decoding

  • RawIPv4Packet reads the transport header where the IHL puts it, and an IHL shorter than the fixed header leaves none.
  • RawIPv6Packet walks the extension headers ipfw_chk follows — hop-by-hop options, routing, fragment, destination options and authentication — up to the upper layer, or to a non-first fragment, whose fragment header makes it one. ESP and no next header end the walk, a header too short to name the next one leaves no transport header.
  • Both read the transport fields at the header past the IP headers whatever the protocol, the VM asking only where they mean something.

Builders

Every With method returns a copy, so packets built from one base share no bytes, and WithTCP sets the flags instead of adding to them. RawIPv6Packet gains WithFragmentOffset, which inserts a fragment header, and NewIPv6Packet names no next header instead of 0, which is hop-by-hop.

One type tried and dropped

A single RawPacket telling the version by its nibble was tried first. Every accessor then decided the version at run time, its offsets were no longer constants, so bounds checks stayed, and its call to the slow path made it a non-leaf function with a stack-check prologue: 3–4 ns an accessor and VM_Check_FirstRule 12% slower. Removing the double dispatch did not change FirstRule, so the typed packets stay. For IPv6 the extension-header check sits in each accessor and the walk behind a //go:noinline helper, one call level on the common path instead of two.

Performance

Interleaved prebuilt binaries against 406e132, taskset -c 12-15 on idle cores, benchstat, n=12, accessors called through the Packet interface:

benchmark before after
IPv4 SourcePort 8.88ns 5.62ns −36.7%
IPv4 TCPFlags 7.95ns 5.25ns −34.0%
IPv4 ICMPType 8.89ns 4.85ns −45.5%
IPv4 Version, Protocol, SourceAddr, IsFragment ~
IPv6 Protocol 3.63ns 6.08ns +67.4%
IPv6 IsFragment 2.82ns 6.03ns +114.0%
IPv6 SourcePort 4.86ns 7.28ns +49.7%
IPv6 TCPFlags 4.44ns 7.66ns +72.6%
IPv6 ICMPType 4.86ns 7.17ns +47.5%
VM_Check_SourceReject/IPv6/1 94.2ns 76.4ns −18.9%
VM_Check_FirstRule, NoRule, EveryMatcher, SourceReject/IPv4, SourceReject/IPv6/1024 ~

IPv4 gets faster because the VM now decides fragments and protocols, which the old type recomputed in every accessor. IPv6 pays for telling an extension header from the upper layer, which the old type never did. No VM benchmark gets slower, and every benchmark stays at 0 allocs/op.

Compatibility

The types and method names stay. Breaking in behaviour: the With builders return copies instead of editing the packet in place, WithTCP sets the flags instead of adding them, and NewIPv6Packet names no next header.

Validation

Tests first: IPv4 IHL, eleven IPv6 extension-header chains including truncated ones, fragments of both versions, builder copies, and Test_VM_Check_RawLayout for ports past IP options and hop-by-hop options, IPv6 fragments, SCTP and UDP-Lite. Fuzz_Packet feeds any bytes to both types and ran 30 s. make test (race) and make lint pass.

RawIPv4Packet took the IPv4 header to be twenty bytes whatever its IHL
said, and RawIPv6Packet read the upper layer right after the fixed
header, so a packet with IP options or an IPv6 extension header had its
ports read from the wrong bytes, and no IPv6 packet was ever a fragment.

RawIPv4Packet now reads the transport header where the IHL puts it.
RawIPv6Packet walks the extension headers ipfw_chk follows, hop-by-hop
options, routing, fragment, destination options and authentication, up
to the upper layer or to a non-first fragment, whose fragment header
makes it one. Both read the transport fields at the header past the IP
headers whatever the protocol, the VM asking only where they mean
something, and a fixed IPv6 header naming no extension header, nearly
every packet, is read without the walk.

The builders return copies, so that packets built from one base share no
bytes, and WithTCP sets the flags instead of adding to them.
RawIPv6Packet gains WithFragmentOffset, which inserts a fragment header,
and NewIPv6Packet names no next header, 0 being hop-by-hop.

A single RawPacket for both versions was tried and dropped: deciding the
version at run time in every accessor, with offsets no longer constant
and every accessor a non-leaf function, cost 3-4ns an accessor and made
VM_Check_FirstRule 12% slower, a typed packet paying none of it.

Interleaved prebuilt binaries against 406e132, taskset -c 12-15, n=12,
accessors through the Packet interface:

  IPv4 SourcePort                      8.88ns   5.62ns  -36.7%
  IPv4 TCPFlags                        7.95ns   5.25ns  -34.0%
  IPv4 ICMPType                        8.89ns   4.85ns  -45.5%
  IPv4 Version, Protocol, SourceAddr, IsFragment      ~
  IPv6 Protocol                        3.63ns   6.08ns  +67.4%
  IPv6 IsFragment                      2.82ns   6.03ns  +114.0%
  IPv6 SourcePort                      4.86ns   7.28ns  +49.7%
  IPv6 TCPFlags                        4.44ns   7.66ns  +72.6%
  IPv6 ICMPType                        4.86ns   7.17ns  +47.5%
  VM_Check_SourceReject/IPv6/1         94.2ns   76.4ns  -18.9%
  VM_Check_FirstRule, _NoRule, _EveryMatcher,
  _SourceReject/IPv4, _SourceReject/IPv6/1024          ~

IPv6 pays for telling an extension header from the upper layer, which
the old type never did: its IsFragment was a constant false and its
offsets constants. Every benchmark stays at 0 allocs/op.
@3Hren
3Hren merged commit 45683c1 into main Sep 17, 2026
1 check passed
@3Hren
3Hren deleted the refactor/raw-packet branch September 17, 2026 07:20
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