mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 15:57:01 +02:00
234 lines
7.5 KiB
Go
234 lines
7.5 KiB
Go
//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)
|
|
}
|
|
}
|