mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 08:17:03 +02:00
192 lines
5.5 KiB
Go
192 lines
5.5 KiB
Go
//go:build linux && !android && !e2e_testing
|
|
|
|
package udp
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net/netip"
|
|
"testing"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// TestPlanRunBreaksOnECNChange confirms that two same-destination, same-size
|
|
// packets with different outer ECN end up in separate sendmmsg entries (the
|
|
// kernel stamps one outer codepoint per entry, so a run that straddled the
|
|
// boundary would silently lose information).
|
|
func TestPlanRunBreaksOnECNChange(t *testing.T) {
|
|
u := &batchWriter{gsoSupported: true, maxGSOSegments: 63}
|
|
dst := netip.MustParseAddrPort("10.0.0.1:4242")
|
|
|
|
bufs := [][]byte{
|
|
make([]byte, 1200),
|
|
make([]byte, 1200),
|
|
make([]byte, 1200),
|
|
}
|
|
addrs := []netip.AddrPort{dst, dst, dst}
|
|
|
|
t.Run("uniform_ecn_runs_together", func(t *testing.T) {
|
|
ecns := []byte{0x02, 0x02, 0x02}
|
|
runLen, segSize := u.planRun(bufs, addrs, ecns, 0, 64)
|
|
if runLen != 3 {
|
|
t.Errorf("runLen=%d want 3 (uniform ECT(0))", runLen)
|
|
}
|
|
if segSize != 1200 {
|
|
t.Errorf("segSize=%d want 1200", segSize)
|
|
}
|
|
})
|
|
|
|
t.Run("ecn_change_truncates_run", func(t *testing.T) {
|
|
// 0,0,3: first two run together, CE seeds a fresh entry.
|
|
ecns := []byte{0x00, 0x00, 0x03}
|
|
runLen, _ := u.planRun(bufs, addrs, ecns, 0, 64)
|
|
if runLen != 2 {
|
|
t.Errorf("runLen=%d want 2 (ECN changes at index 2)", runLen)
|
|
}
|
|
})
|
|
|
|
t.Run("nil_ecns_runs_full", func(t *testing.T) {
|
|
runLen, _ := u.planRun(bufs, addrs, nil, 0, 64)
|
|
if runLen != 3 {
|
|
t.Errorf("runLen=%d want 3 (nil ecns means no break)", runLen)
|
|
}
|
|
})
|
|
|
|
t.Run("first_ecn_is_singleton", func(t *testing.T) {
|
|
// Second packet has different ECN from the first → run halts at 1
|
|
// (the first packet alone forms the run).
|
|
ecns := []byte{0x00, 0x03, 0x03}
|
|
runLen, _ := u.planRun(bufs, addrs, ecns, 0, 64)
|
|
if runLen != 1 {
|
|
t.Errorf("runLen=%d want 1 (different ECN immediately)", runLen)
|
|
}
|
|
})
|
|
}
|
|
|
|
// ecnReceiver is a raw UDP socket with IP_RECVTOS / IPV6_RECVTCLASS enabled,
|
|
// used to observe the outer ECN codepoint WriteTo stamps on the wire.
|
|
type ecnReceiver struct {
|
|
fd int
|
|
addr netip.AddrPort
|
|
}
|
|
|
|
func newEcnReceiver(t *testing.T, v6 bool) *ecnReceiver {
|
|
t.Helper()
|
|
family := unix.AF_INET
|
|
if v6 {
|
|
family = unix.AF_INET6
|
|
}
|
|
fd, err := unix.Socket(family, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
t.Fatalf("socket: %v", err)
|
|
}
|
|
t.Cleanup(func() { unix.Close(fd) })
|
|
|
|
var bindAddr netip.Addr
|
|
if v6 {
|
|
if err = unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_RECVTCLASS, 1); err != nil {
|
|
t.Fatalf("IPV6_RECVTCLASS: %v", err)
|
|
}
|
|
if err = unix.Bind(fd, &unix.SockaddrInet6{Addr: [16]byte{15: 1}}); err != nil {
|
|
t.Fatalf("bind ::1: %v", err)
|
|
}
|
|
bindAddr = netip.MustParseAddr("::1")
|
|
} else {
|
|
if err = unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_RECVTOS, 1); err != nil {
|
|
t.Fatalf("IP_RECVTOS: %v", err)
|
|
}
|
|
if err = unix.Bind(fd, &unix.SockaddrInet4{Addr: [4]byte{127, 0, 0, 1}}); err != nil {
|
|
t.Fatalf("bind 127.0.0.1: %v", err)
|
|
}
|
|
bindAddr = netip.MustParseAddr("127.0.0.1")
|
|
}
|
|
tv := unix.Timeval{Sec: 5}
|
|
if err = unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
|
|
t.Fatalf("SO_RCVTIMEO: %v", err)
|
|
}
|
|
|
|
sa, err := unix.Getsockname(fd)
|
|
if err != nil {
|
|
t.Fatalf("getsockname: %v", err)
|
|
}
|
|
var port int
|
|
switch v := sa.(type) {
|
|
case *unix.SockaddrInet4:
|
|
port = v.Port
|
|
case *unix.SockaddrInet6:
|
|
port = v.Port
|
|
default:
|
|
t.Fatalf("unexpected sockaddr %T", sa)
|
|
}
|
|
return &ecnReceiver{fd: fd, addr: netip.AddrPortFrom(bindAddr, uint16(port))}
|
|
}
|
|
|
|
// recvECN receives one datagram and returns the 2-bit ECN codepoint from its
|
|
// TOS / TCLASS cmsg.
|
|
func (r *ecnReceiver) recvECN(t *testing.T) byte {
|
|
t.Helper()
|
|
buf := make([]byte, 128)
|
|
oob := make([]byte, 128)
|
|
_, oobn, _, _, err := unix.Recvmsg(r.fd, buf, oob, 0)
|
|
if err != nil {
|
|
t.Fatalf("recvmsg: %v", err)
|
|
}
|
|
cmsgs, err := unix.ParseSocketControlMessage(oob[:oobn])
|
|
if err != nil {
|
|
t.Fatalf("parse cmsg: %v", err)
|
|
}
|
|
for _, m := range cmsgs {
|
|
switch {
|
|
case m.Header.Level == unix.IPPROTO_IP && m.Header.Type == unix.IP_TOS:
|
|
return m.Data[0] & 0x03
|
|
case m.Header.Level == unix.IPPROTO_IPV6 && m.Header.Type == unix.IPV6_TCLASS:
|
|
return byte(binary.NativeEndian.Uint32(m.Data)) & 0x03
|
|
}
|
|
}
|
|
t.Fatal("no TOS/TCLASS cmsg received")
|
|
return 0
|
|
}
|
|
|
|
// TestWriteToStampsOuterECN sends single packets through StdConn.WriteTo and
|
|
// asserts the requested ECN codepoint lands on the outer IP header, for a
|
|
// v4 socket, a v6 socket, and the dual-stack case where a v4-mapped
|
|
// destination must be stamped via IP_TOS rather than IPV6_TCLASS.
|
|
func TestWriteToStampsOuterECN(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
bind string
|
|
recvV6 bool
|
|
sendECN byte
|
|
}{
|
|
{"v4_socket_to_v4", "127.0.0.1", false, 0x03},
|
|
{"v6_socket_to_v6", "::1", true, 0x01},
|
|
{"dualstack_v6_socket_to_v4_mapped", "::", false, 0x02},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c, err := NewListener(testLogger(), netip.MustParseAddr(tc.bind), 0, false, 8)
|
|
if err != nil {
|
|
t.Fatalf("NewListener: %v", err)
|
|
}
|
|
defer c.Close()
|
|
rx := newEcnReceiver(t, tc.recvV6)
|
|
|
|
if err = c.WriteTo([]byte("ecn"), rx.addr, tc.sendECN); err != nil {
|
|
t.Fatalf("WriteTo(ecn=%#02x): %v", tc.sendECN, err)
|
|
}
|
|
if got := rx.recvECN(t); got != tc.sendECN {
|
|
t.Errorf("outer ECN = %#02x, want %#02x", got, tc.sendECN)
|
|
}
|
|
|
|
// The zero codepoint sends no TOS cmsg and must arrive Not-ECT
|
|
// (the socket-default TOS byte).
|
|
if err = c.WriteTo([]byte("ecn"), rx.addr, 0); err != nil {
|
|
t.Fatalf("WriteTo(ecn=0): %v", err)
|
|
}
|
|
if got := rx.recvECN(t); got != 0 {
|
|
t.Errorf("outer ECN = %#02x, want 0 (Not-ECT)", got)
|
|
}
|
|
})
|
|
}
|
|
}
|