refactor(vm): read raw packets as ipfw_chk does - #116
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RawIPv4Packettook the IPv4 header to be twenty bytes whatever its IHL said, andRawIPv6Packetread 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:IsFragmentwas a constantfalse. Follow-up to #115, which moved the decision of which fields mean something into the VM.Decoding
RawIPv4Packetreads the transport header where the IHL puts it, and an IHL shorter than the fixed header leaves none.RawIPv6Packetwalks the extension headersipfw_chkfollows — 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.Builders
Every
Withmethod returns a copy, so packets built from one base share no bytes, andWithTCPsets the flags instead of adding to them.RawIPv6PacketgainsWithFragmentOffset, which inserts a fragment header, andNewIPv6Packetnames no next header instead of 0, which is hop-by-hop.One type tried and dropped
A single
RawPackettelling 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 andVM_Check_FirstRule12% slower. Removing the double dispatch did not changeFirstRule, so the typed packets stay. For IPv6 the extension-header check sits in each accessor and the walk behind a//go:noinlinehelper, one call level on the common path instead of two.Performance
Interleaved prebuilt binaries against
406e132,taskset -c 12-15on idle cores,benchstat, n=12, accessors called through thePacketinterface:SourcePortTCPFlagsICMPTypeVersion,Protocol,SourceAddr,IsFragmentProtocolIsFragmentSourcePortTCPFlagsICMPTypeVM_Check_SourceReject/IPv6/1VM_Check_FirstRule,NoRule,EveryMatcher,SourceReject/IPv4,SourceReject/IPv6/1024IPv4 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
Withbuilders return copies instead of editing the packet in place,WithTCPsets the flags instead of adding them, andNewIPv6Packetnames 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_RawLayoutfor ports past IP options and hop-by-hop options, IPv6 fragments, SCTP and UDP-Lite.Fuzz_Packetfeeds any bytes to both types and ran 30 s.make test(race) andmake lintpass.