wgengine/netstack: accept IPv4 fragments before reassembly

The netstack GRO receive path validates L4 checksums before marking
packets as RX checksum validated for gVisor. That validation is invalid
for IPv4 fragments because TCP and UDP checksums cover the complete
reassembled transport packet, not an individual fragment.

Keep validating the IPv4 header checksum, but let IPv4 fragments through
to gVisor for reassembly without pre-validating TCP or UDP.

Fixes #20320

Change-Id: I779363a5e0ac5abee6a8e2a2a44b418fbc5f5e27
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-02 14:44:00 -07:00
committed by Brad Fitzpatrick
parent 74235b46c1
commit 52fdadbf8b
3 changed files with 147 additions and 2 deletions
+10 -2
View File
@@ -25,11 +25,14 @@ import (
// !stack.PacketBuffer.RXChecksumValidated, i.e. it satisfies
// stack.CapabilityRXChecksumOffload. Other protocols with checksum fields,
// e.g. ICMP{v6}, are still validated by gVisor regardless of rx checksum
// offloading capabilities.
// offloading capabilities. IPv4 fragments cannot have their L4 checksums
// validated before reassembly, so only their IPv4 header checksum is validated
// here.
func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
var (
pn tcpip.NetworkProtocolNumber
csumStart int
fragment bool
)
buf := p.Buffer()
@@ -45,6 +48,11 @@ func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
if ^tun.Checksum(buf[:csumStart], 0) != 0 {
return nil
}
// Non-first fragments (FragmentOffset != 0) arrive here with
// p.IPProto == ipproto.Fragment (set by packet.Parsed.Decode), so
// they already skip the L4 checksum check below. We only need to
// catch the first fragment, which still has its real IPProto.
fragment = header.IPv4(buf).More()
pn = header.IPv4ProtocolNumber
case 6:
if len(buf) < header.IPv6FixedHeaderSize {
@@ -80,7 +88,7 @@ func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
}
}
if p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP {
if !fragment && (p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP) {
lenForPseudo := len(buf) - csumStart
csum := tun.PseudoHeaderChecksum(
uint8(p.IPProto),
+27
View File
@@ -62,6 +62,23 @@ func Test_RXChecksumOffload(t *testing.T) {
at := 20 + 16
tcp4InvalidCsum[at] = ^tcp4InvalidCsum[at]
tcp4FirstFragment := make([]byte, 20+20+60)
copy(tcp4FirstFragment, tcp4[:len(tcp4FirstFragment)])
ipv4H = header.IPv4(tcp4FirstFragment)
ipv4H.SetTotalLength(uint16(len(tcp4FirstFragment)))
ipv4H.SetFlagsFragmentOffset(header.IPv4FlagMoreFragments, 0)
ipv4H.SetChecksum(0)
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
tcp4SecondFragment := make([]byte, 20+40)
copy(tcp4SecondFragment, tcp4[:20])
copy(tcp4SecondFragment[20:], tcp4[20+80:])
ipv4H = header.IPv4(tcp4SecondFragment)
ipv4H.SetTotalLength(uint16(len(tcp4SecondFragment)))
ipv4H.SetFlagsFragmentOffset(0, 80)
ipv4H.SetChecksum(0)
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
tcp6ExtHeaderInvalidCsum := make([]byte, len(tcp6ExtHeader))
copy(tcp6ExtHeaderInvalidCsum, tcp6ExtHeader)
at = 40 + 8 + 16
@@ -87,6 +104,16 @@ func Test_RXChecksumOffload(t *testing.T) {
tcp4InvalidCsum,
false,
},
{
"tcp4 first fragment skips L4 csum",
tcp4FirstFragment,
true,
},
{
"tcp4 second fragment skips L4 csum",
tcp4SecondFragment,
true,
},
{
"tcp6 with ext header invalid csum",
tcp6ExtHeaderInvalidCsum,