| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- //go:build linux && !android && !e2e_testing
- package udp
- import (
- "encoding/binary"
- "log/slog"
- "net"
- "net/netip"
- "syscall"
- "testing"
- "time"
- "unsafe"
- "golang.org/x/sys/unix"
- )
- // TestGSOMaxSegmentsKernelGate pins the corrected kernel-version gate: the
- // 128-segment cap (127 usable) only lands in Linux v6.9 (commit 1382e3b6a350),
- // not 5.5. Everything older stays at the conservative 63.
- func TestGSOMaxSegmentsKernelGate(t *testing.T) {
- cases := []struct {
- release string
- want int
- }{
- {"5.4.0", 63},
- {"5.5.0-generic", 63}, // the old bug bumped here — it must not now
- {"5.15.0", 63},
- {"6.1.0", 63},
- {"6.8.0-generic", 63},
- {"6.9.0", 127},
- {"6.10.1-arch1-1", 127},
- {"7.0.5-arch1-1", 127},
- {"garbage", 63},
- {"", 63},
- }
- for _, c := range cases {
- if got := gsoMaxSegments(c.release); got != c.want {
- t.Errorf("gsoMaxSegments(%q) = %d, want %d", c.release, got, c.want)
- }
- }
- }
- // buildCmsg lays out a single ancillary cmsg (header + data) in a fresh buffer
- // the way the kernel would deliver it, so parseRecvCmsg can be exercised
- // without a live socket.
- func buildCmsg(level, typ int32, data []byte) []byte {
- buf := make([]byte, unix.CmsgSpace(len(data)))
- h := (*unix.Cmsghdr)(unsafe.Pointer(&buf[0]))
- h.Level = level
- h.Type = typ
- setCmsgLen(h, unix.CmsgLen(len(data)))
- copy(buf[unix.CmsgLen(0):], data)
- return buf
- }
- // TestParseRecvCmsgOuterECNFamily is the RX half of the dual-stack ECN fix:
- // parseRecvCmsg must read the outer ECN from whichever family the kernel
- // delivered, not from the socket family. On the default `::` dual-stack bind
- // a v4 peer's outer ECN arrives as an IP_TOS cmsg, which the old socket-family
- // gate ignored entirely.
- func TestParseRecvCmsgOuterECNFamily(t *testing.T) {
- tc := make([]byte, 4)
- binary.NativeEndian.PutUint32(tc, 0x02)
- cases := []struct {
- name string
- ctrl []byte
- want byte
- }{
- {"ip_tos_ce", buildCmsg(int32(unix.IPPROTO_IP), int32(unix.IP_TOS), []byte{0x03}), 0x03},
- {"ip_tos_ect0", buildCmsg(int32(unix.IPPROTO_IP), int32(unix.IP_TOS), []byte{0x02}), 0x02},
- {"ipv6_tclass_ect0", buildCmsg(int32(unix.IPPROTO_IPV6), int32(unix.IPV6_TCLASS), tc), 0x02},
- }
- for _, c := range cases {
- t.Run(c.name, func(t *testing.T) {
- hdr := &msghdr{Control: &c.ctrl[0]}
- setMsgControllen(hdr, len(c.ctrl))
- gso, ecn := parseRecvCmsg(hdr, false, true)
- if gso != 0 {
- t.Errorf("gso = %d, want 0 (no UDP_GRO cmsg present)", gso)
- }
- if ecn != c.want {
- t.Errorf("ecn = 0x%02x, want 0x%02x", ecn, c.want)
- }
- })
- }
- }
- func testLogger() *slog.Logger {
- return slog.New(slog.DiscardHandler)
- }
- // TestWriteBatchBadFamilyDeliversOthers is the H3 regression: a batch that
- // contains one destination the socket can't reach (an IPv6 remote on a
- // v4-bound socket) must still deliver every other packet. Before the fix the
- // writeSockaddr error returned early and dropped the whole chunk.
- func TestWriteBatchBadFamilyDeliversOthers(t *testing.T) {
- rx, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
- if err != nil {
- t.Skipf("cannot open v4 receiver (sandbox?): %v", err)
- }
- defer rx.Close()
- rxPort := rx.LocalAddr().(*net.UDPAddr).Port
- // Bind a *non-wildcard* v4 address so Go gives us a genuine AF_INET
- // socket. A wildcard v4 bind (0.0.0.0) via network "udp" comes up as a
- // dual-stack AF_INET6 socket on Linux, for which a v6 dest is not a bad
- // family — which would defeat the point of this test.
- c, err := NewListener(testLogger(), netip.MustParseAddr("127.0.0.1"), 0, false, 1)
- if err != nil {
- t.Skipf("cannot open v4 sender (sandbox?): %v", err)
- }
- defer c.Close()
- sender := c.(*StdConn)
- if !sender.isV4 {
- t.Fatalf("expected a v4-bound sender socket, got isV4=false")
- }
- good := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), uint16(rxPort))
- bad := netip.MustParseAddrPort("[2001:db8::1]:9999") // genuine v6, unreachable on v4 socket
- bufs := [][]byte{[]byte("AAA"), []byte("BBB"), []byte("CCC")}
- addrs := []netip.AddrPort{good, bad, good}
- if err := sender.WriteBatch(bufs, addrs, nil); err != nil {
- t.Fatalf("WriteBatch returned error, want nil (bad dest should be isolated): %v", err)
- }
- got := map[string]bool{}
- rx.SetReadDeadline(time.Now().Add(2 * time.Second))
- buf := make([]byte, 64)
- for i := 0; i < 2; i++ {
- n, _, rerr := rx.ReadFromUDPAddrPort(buf)
- if rerr != nil {
- t.Fatalf("expected 2 delivered packets, read #%d failed: %v", i+1, rerr)
- }
- got[string(buf[:n])] = true
- }
- if !got["AAA"] || !got["CCC"] {
- t.Errorf("delivered set = %v, want AAA and CCC both present", got)
- }
- if got["BBB"] {
- t.Errorf("the bad-family packet BBB was somehow delivered")
- }
- }
- // TestWriteBatchOuterTOSToV4Mapped is the TX half of the dual-stack ECN fix,
- // verified against a live kernel: WriteBatch on the default `::` dual-stack
- // socket, sending to a v4-mapped destination, must stamp the outer ECN via an
- // IP_TOS cmsg (not IPV6_TCLASS, which the kernel's v4 path ignores) so a v4
- // receiver actually sees it.
- func TestWriteBatchOuterTOSToV4Mapped(t *testing.T) {
- rx, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
- if err != nil {
- t.Skipf("cannot open v4 receiver (sandbox?): %v", err)
- }
- defer rx.Close()
- rxPort := rx.LocalAddr().(*net.UDPAddr).Port
- // Ask the kernel to deliver the received outer TOS as ancillary data.
- rxRaw, err := rx.SyscallConn()
- if err != nil {
- t.Fatalf("SyscallConn: %v", err)
- }
- var soErr error
- if err := rxRaw.Control(func(fd uintptr) {
- soErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_RECVTOS, 1)
- }); err != nil || soErr != nil {
- t.Skipf("cannot enable IP_RECVTOS (sandbox/kernel?): ctrl=%v so=%v", err, soErr)
- }
- c, err := NewListener(testLogger(), netip.IPv6Unspecified(), 0, false, 1)
- if err != nil {
- t.Skipf("cannot open dual-stack sender (sandbox?): %v", err)
- }
- defer c.Close()
- sender := c.(*StdConn)
- if sender.isV4 {
- t.Skipf("sender came up v4-only; need a dual-stack v6 socket for this test")
- }
- // v4-mapped-in-v6 destination: routed through the kernel's IPv4 path.
- dst := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), uint16(rxPort))
- const wantECN = byte(0x02) // ECT(0)
- if err := sender.WriteBatch([][]byte{[]byte("tos-probe")}, []netip.AddrPort{dst}, []byte{wantECN}); err != nil {
- t.Fatalf("WriteBatch: %v", err)
- }
- // Read the datagram plus its ancillary TOS.
- rx.SetReadDeadline(time.Now().Add(3 * time.Second))
- payload := make([]byte, 128)
- oob := make([]byte, 512)
- var n, oobn int
- var rerr error
- if err := rxRaw.Read(func(fd uintptr) bool {
- n, oobn, _, _, rerr = unix.Recvmsg(int(fd), payload, oob, 0)
- if rerr == syscall.EAGAIN || rerr == syscall.EWOULDBLOCK {
- return false
- }
- return true
- }); err != nil {
- t.Fatalf("waiting for datagram failed (no delivery?): %v", err)
- }
- if rerr != nil {
- t.Fatalf("Recvmsg: %v", rerr)
- }
- if string(payload[:n]) != "tos-probe" {
- t.Fatalf("payload = %q, want %q", string(payload[:n]), "tos-probe")
- }
- cmsgs, err := unix.ParseSocketControlMessage(oob[:oobn])
- if err != nil {
- t.Fatalf("ParseSocketControlMessage: %v", err)
- }
- found := false
- var gotTOS byte
- for _, m := range cmsgs {
- if m.Header.Level == unix.IPPROTO_IP && m.Header.Type == unix.IP_TOS && len(m.Data) >= 1 {
- found = true
- gotTOS = m.Data[0]
- }
- }
- if !found {
- t.Fatalf("no IP_TOS cmsg delivered to v4 receiver — outer ECN did not land (%d cmsgs)", len(cmsgs))
- }
- if gotTOS&0x03 != wantECN {
- t.Errorf("received outer TOS = 0x%02x, want low-2-bits = 0x%02x", gotTOS, wantECN)
- } else {
- t.Logf("verified: v4 receiver saw outer TOS 0x%02x (ECN=0x%02x) from dual-stack sender", gotTOS, gotTOS&0x03)
- }
- }
|