WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
3 changed files with 147 additions and 2 deletions
Showing only changes of commit 52fdadbf8b - Show all commits
+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,
+110
View File
@@ -17,7 +17,9 @@ import (
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
@@ -1680,6 +1682,114 @@ func udp4raw(t testing.TB, src, dst netip.Addr, sport, dport uint16, payload []b
return buf
}
func fragmentIPv4ForTest(t testing.TB, pkt []byte, firstPayloadLen uint16) (first, second []byte) {
t.Helper()
if firstPayloadLen%8 != 0 {
t.Fatalf("firstPayloadLen %d is not 8-byte aligned", firstPayloadLen)
}
if len(pkt) < header.IPv4MinimumSize+int(firstPayloadLen) {
t.Fatalf("packet length %d too short for firstPayloadLen %d", len(pkt), firstPayloadLen)
}
ip := header.IPv4(pkt)
if ip.HeaderLength() != header.IPv4MinimumSize {
t.Fatalf("test helper only supports 20-byte IPv4 headers; got %d", ip.HeaderLength())
}
ipPayloadLen := len(pkt) - header.IPv4MinimumSize
if int(firstPayloadLen) >= ipPayloadLen {
t.Fatalf("firstPayloadLen %d must be smaller than IP payload length %d", firstPayloadLen, ipPayloadLen)
}
first = make([]byte, header.IPv4MinimumSize+int(firstPayloadLen))
copy(first, pkt[:len(first)])
firstIP := header.IPv4(first)
firstIP.SetTotalLength(uint16(len(first)))
firstIP.SetFlagsFragmentOffset(header.IPv4FlagMoreFragments, 0)
firstIP.SetChecksum(0)
firstIP.SetChecksum(^firstIP.CalculateChecksum())
secondPayloadLen := ipPayloadLen - int(firstPayloadLen)
second = make([]byte, header.IPv4MinimumSize+secondPayloadLen)
copy(second[:header.IPv4MinimumSize], pkt[:header.IPv4MinimumSize])
copy(second[header.IPv4MinimumSize:], pkt[header.IPv4MinimumSize+int(firstPayloadLen):])
secondIP := header.IPv4(second)
secondIP.SetTotalLength(uint16(len(second)))
secondIP.SetFlagsFragmentOffset(0, firstPayloadLen)
secondIP.SetChecksum(0)
secondIP.SetChecksum(^secondIP.CalculateChecksum())
return first, second
}
// TestLinkEndpointInjectInboundIPv4Fragments verifies that Tailscale's inbound
// link endpoint path lets IPv4 fragments reach gVisor for reassembly.
// Previously (see https://github.com/tailscale/tailscale/issues/20320),
// gro.RXChecksumOffload validated L4 checksums before reassembly, so the first
// fragment was dropped and the UDP datagram never reached the socket.
func TestLinkEndpointInjectInboundIPv4Fragments(t *testing.T) {
const nicID tcpip.NICID = 1
localIP := netip.MustParseAddr("100.64.1.2")
remoteIP := netip.MustParseAddr("100.64.1.3")
payload := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz")
raw := udp4raw(t, remoteIP, localIP, 12345, 8081, payload)
first, second := fragmentIPv4ForTest(t, raw, 80)
s := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{
ipv4.NewProtocol,
},
TransportProtocols: []stack.TransportProtocolFactory{
udp.NewProtocol,
},
})
defer s.Close()
ep := newLinkEndpoint(64, 1280, "", groNotSupported)
if err := s.CreateNIC(nicID, ep); err != nil {
t.Fatalf("CreateNIC: %v", err)
}
if err := s.AddProtocolAddress(nicID, tcpip.ProtocolAddress{
Protocol: header.IPv4ProtocolNumber,
AddressWithPrefix: tcpip.AddrFrom4(localIP.As4()).WithPrefix(),
}, stack.AddressProperties{}); err != nil {
t.Fatalf("AddProtocolAddress: %v", err)
}
pc, err := gonet.DialUDP(s, &tcpip.FullAddress{
NIC: nicID,
Addr: tcpip.AddrFrom4(localIP.As4()),
Port: 8081,
}, nil, header.IPv4ProtocolNumber)
if err != nil {
t.Fatalf("DialUDP: %v", err)
}
defer pc.Close()
var parsed packet.Parsed
parsed.Decode(first)
ep.injectInbound(&parsed)
parsed.Decode(second)
ep.injectInbound(&parsed)
if err := pc.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
t.Fatalf("SetReadDeadline: %v", err)
}
buf := make([]byte, 512)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
t.Fatalf("ReadFrom: %v (fragmented packet was not reassembled and delivered)", err)
}
if got := string(buf[:n]); got != string(payload) {
t.Fatalf("payload = %q, want %q", got, payload)
}
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
t.Fatalf("remote addr = %T(%v), want *net.UDPAddr", addr, addr)
}
if got := udpAddr.AddrPort(); got != netip.MustParseAddrPort("100.64.1.3:12345") {
t.Fatalf("remote addr = %v, want 100.64.1.3:12345", got)
}
}
// TestInjectLoopback verifies that the inject goroutine delivers self-addressed
// packets back into gVisor (via DeliverLoopback) instead of sending them to
// WireGuard outbound. This is a regression test for a bug where self-dial