//go:build !e2e_testing package udp import ( "context" "fmt" "log/slog" "net" "net/netip" "strings" "sync" "sync/atomic" "syscall" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) // newConnSockListener opens a listener with the connected socket knobs pinned to their defaults, so NEBULA_* // variables in the environment can't change what a test sees, applies cfg, and starts ListenOut with r. func newConnSockListener(t *testing.T, s Settings, h slog.Handler, r EncReader, cfg ...func(*connSockConfig)) *StdConn { t.Helper() for k, v := range map[string]string{ "NEBULA_CONNSOCKS": "8", "NEBULA_CONNSOCK_RUN": "64", "NEBULA_CONNSOCK_WINDOW": "1s", "NEBULA_CONNSOCK_IDLE": "30s", "NEBULA_CONNSOCK_OPEN_EVERY": "1s", } { t.Setenv(k, v) } if h == nil { h = slog.DiscardHandler } if r == nil { r = func(netip.AddrPort, []byte) {} } c, err := NewListener(slog.New(h), s) require.NoError(t, err) t.Cleanup(func() { _ = c.Close() }) u := c.(*StdConn) for _, f := range cfg { f(&u.socks.cfg) } go func() { _ = u.ListenOut(r, func() {}) }() require.Eventually(t, func() bool { return u.reader.Load() != nil }, 5*time.Second, time.Millisecond) return u } func listen(s string) Settings { return Settings{Listen: netip.MustParseAddrPort(s)} } // run builds n datagrams to dst. func run(dst netip.AddrPort, n int, tag string) ([][]byte, []netip.AddrPort) { bufs := make([][]byte, n) addrs := make([]netip.AddrPort, n) for i := range bufs { bufs[i] = fmt.Appendf(nil, "%s-%d", tag, i) addrs[i] = dst } return bufs, addrs } func newPeer(t *testing.T, network, addr string) (*net.UDPConn, netip.AddrPort) { t.Helper() pc, err := net.ListenPacket(network, addr) require.NoError(t, err) t.Cleanup(func() { _ = pc.Close() }) uc := pc.(*net.UDPConn) return uc, uc.LocalAddr().(*net.UDPAddr).AddrPort() } // newEstablishedPeer opens a peer as newPeer does and tells u it is an established tunnel's remote. func newEstablishedPeer(t *testing.T, u *StdConn, network, addr string) (*net.UDPConn, netip.AddrPort) { t.Helper() c, dst := newPeer(t, network, addr) u.SetEstablishedPeer(dst, true) return c, dst } // readFrom reads n datagrams at peer and returns the source of each. func readFrom(t *testing.T, peer *net.UDPConn, n int) []netip.AddrPort { t.Helper() buf := make([]byte, MTU) var from []netip.AddrPort for range n { require.NoError(t, peer.SetReadDeadline(time.Now().Add(5*time.Second))) _, f, err := peer.ReadFromUDPAddrPort(buf) require.NoError(t, err) from = append(from, netip.AddrPortFrom(f.Addr().Unmap(), f.Port())) } return from } func reusePort(t *testing.T, c syscall.Conn) int { t.Helper() rc, err := c.SyscallConn() require.NoError(t, err) var v int var serr error require.NoError(t, rc.Control(func(fd uintptr) { v, serr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT) })) require.NoError(t, serr) return v } // rogueBinds tries each of binds, "network address plain|reuseport", on port and returns the ones that bound. func rogueBinds(t *testing.T, port uint16, binds []string) []string { t.Helper() var bound []string for _, b := range binds { f := strings.Fields(b) network, addr, reuse := f[0], f[1], f[2] lc := net.ListenConfig{Control: connSockControl(reuse == "reuseport", 0)} pc, err := lc.ListenPacket(context.Background(), network, net.JoinHostPort(addr, fmt.Sprint(port))) if err == nil { _ = pc.Close() bound = append(bound, b) } } return bound } // The listener never sets SO_REUSEPORT, and a connected socket binds with it only to a specific address, so every // bind a lone listener refuses is refused after NewListener returns and while connected sockets are open. Same-uid // SO_REUSEPORT binds to a specific address are left out: xnu allows them beside a lone wildcard listener too, // which is how a connected socket binds, and refuses them to any other uid. func TestConnSocksNoSharedBind(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) la, err := u.LocalAddr() require.NoError(t, err) binds := []string{ "udp4 0.0.0.0 plain", "udp4 0.0.0.0 reuseport", "udp4 127.0.0.1 plain", "udp6 :: plain", "udp6 :: reuseport", "udp6 ::1 plain", } assert.Zero(t, reusePort(t, u.UDPConn), "listener") assert.Empty(t, rogueBinds(t, la.Port(), binds), "bound beside the listener") peer4, dst4 := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") peer6, dst6 := newEstablishedPeer(t, u, "udp6", "[::1]:0") for _, p := range []struct { c *net.UDPConn dst netip.AddrPort }{{peer4, dst4}, {peer6, dst6}} { bufs, addrs := run(p.dst, u.socks.cfg.run, "p") _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, p.c, len(bufs)) s := u.connSockFor(p.dst) require.NotNil(t, s, "no connected socket to %v", p.dst) assert.NotZero(t, reusePort(t, s.conn), "connected socket to %v", p.dst) } assert.Empty(t, rogueBinds(t, la.Port(), binds), "bound beside the connected sockets") } // A connected socket binds the source address the listener's route to its peer picks, for either family on a // dual-stack listener, so the peer sees one address and port whichever socket sent. func TestConnSocksSourceMatchesListener(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) la, err := u.LocalAddr() require.NoError(t, err) for _, p := range []struct{ network, addr string }{{"udp4", "127.0.0.1:0"}, {"udp6", "[::1]:0"}} { peer, dst := newEstablishedPeer(t, u, p.network, p.addr) require.NoError(t, u.WriteTo([]byte("probe"), dst)) require.Nil(t, u.connSockFor(dst)) viaListener := readFrom(t, peer, 1)[0] assert.Equal(t, la.Port(), viaListener.Port()) bufs, addrs := run(dst, u.socks.cfg.run, p.network) _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) s := u.connSockFor(dst) require.NotNil(t, s, "no connected socket to %v", dst) for _, from := range readFrom(t, peer, len(bufs)) { assert.Equal(t, viaListener, from, "source seen by %v", dst) } local := s.conn.LocalAddr().(*net.UDPAddr).AddrPort() assert.Equal(t, viaListener, netip.AddrPortFrom(local.Addr().Unmap(), local.Port())) } } // A run long enough opens a connected socket per peer, its replies reach the listener's reader, and a peer that // goes away sends its traffic back through the listener with its connected socket closed, while the next peer gets // a connected socket of its own. func TestConnSocks(t *testing.T) { var mu sync.Mutex got := map[netip.AddrPort]int{} u := newConnSockListener(t, listen("0.0.0.0:0"), nil, func(addr netip.AddrPort, _ []byte) { mu.Lock() got[addr]++ mu.Unlock() }) la, err := u.LocalAddr() require.NoError(t, err) to := netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), la.Port()) var peers [3]*net.UDPConn var dsts [3]netip.AddrPort for i := range peers { peers[i], dsts[i] = newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") } for i, dst := range dsts[:2] { bufs, addrs := run(dst, u.socks.cfg.run, fmt.Sprint("p", i)) n, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) assert.Equal(t, len(bufs), n) require.NotNil(t, u.connSockFor(dst), "no connected socket to peer %d", i) for _, from := range readFrom(t, peers[i], len(bufs)) { assert.Equal(t, to, from) } for range 3 { _, err := peers[i].WriteToUDPAddrPort([]byte("reply"), to) require.NoError(t, err) } } require.Eventually(t, func() bool { mu.Lock() defer mu.Unlock() return got[dsts[0]] == 3 && got[dsts[1]] == 3 }, 5*time.Second, time.Millisecond) // The peer's ICMP port unreachable surfaces on the connected socket's next call. The deadline is generous // because xnu rate-limits and delivers the unreachable on its own schedule. gone := dsts[1] goneSock := u.connSockFor(gone) require.NoError(t, peers[1].Close()) bufs, addrs := run(gone, u.socks.cfg.run, "gone") require.Eventually(t, func() bool { _, _ = u.WriteBatch(bufs, addrs) return u.connSockFor(gone) == nil }, 10*time.Second, 10*time.Millisecond) assert.NotNil(t, u.connSockFor(dsts[0])) u.socks.closers.Wait() _, err = goneSock.conn.Write([]byte("x")) assert.ErrorIs(t, err, net.ErrClosed, "closed connected socket left open") bufs, addrs = run(dsts[2], u.socks.cfg.run, "p2") _, err = u.WriteBatch(bufs, addrs) require.NoError(t, err) require.NotNil(t, u.connSockFor(dsts[2])) for _, from := range readFrom(t, peers[2], len(bufs)) { assert.Equal(t, to, from) } require.NoError(t, u.Rebind()) for _, dst := range dsts { assert.Nil(t, u.connSockFor(dst), "connected socket left after Rebind") } } // Listen routines on one port share one set of connected sockets: the cap and the open bucket are theirs together, // a peer's connected socket carries every routine's sends to it, and Rebind on the first routine, which is all // Control.RebindUDPServer calls, clears every routine's listener and closes the connected sockets. func TestConnSocksSharedAcrossRoutines(t *testing.T) { s := listen("0.0.0.0:0") s.Multi = true u0 := newConnSockListener(t, s, nil, nil, func(c *connSockConfig) { c.max = 1 }) la, err := u0.LocalAddr() require.NoError(t, err) s.Listen = netip.AddrPortFrom(s.Listen.Addr(), la.Port()) u1 := newConnSockListener(t, s, nil, nil) require.Same(t, u0.socks, u1.socks) lo, err := net.InterfaceByName("lo0") require.NoError(t, err) peerA, a := newPeer(t, "udp4", "127.0.0.1:0") peerB, b := newPeer(t, "udp4", "127.0.0.1:0") for _, dst := range []netip.AddrPort{a, b} { // Nebula tells every writer. u0.SetEstablishedPeer(dst, true) u1.SetEstablishedPeer(dst, true) } bufs, addrs := run(a, u1.socks.cfg.run, "a") _, err = u1.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peerA, len(bufs)) s1 := u1.connSockFor(a) require.NotNil(t, s1) var sends atomic.Int64 s1.sys = func(fd int, b []byte) error { sends.Add(1) return writeFd(fd, b) } require.NoError(t, u0.WriteTo([]byte("via u0"), a)) readFrom(t, peerA, 1) assert.EqualValues(t, 1, sends.Load(), "routine 0 didn't send on routine 1's connected socket") bufs, addrs = run(b, 2*u0.socks.cfg.run, "b") _, err = u0.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peerB, len(bufs)) assert.Nil(t, u0.connSockFor(b), "a second connected socket past a cap of 1") level, opt := unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF if u0.isV4 { level, opt = unix.IPPROTO_IP, unix.IP_BOUND_IF } boundIf := func(u *StdConn) int { v, err := unix.GetsockoptInt(int(u.sysFd), level, opt) require.NoError(t, err) return v } for _, u := range []*StdConn{u0, u1} { require.NoError(t, unix.SetsockoptInt(int(u.sysFd), level, opt, lo.Index)) require.Equal(t, lo.Index, boundIf(u)) } require.NoError(t, u0.Rebind()) assert.Zero(t, boundIf(u0)) assert.Zero(t, boundIf(u1), "Rebind left routine 1 scoped") assert.Nil(t, u1.connSockFor(a), "Rebind left routine 1's connected socket open") u0.socks.closers.Wait() _, err = s1.conn.Write([]byte("x")) assert.ErrorIs(t, err, net.ErrClosed) } // A connected socket on a specific listen.host would need SO_REUSEPORT on the listener, so connected sockets stay // off there unless listen routines already set it. func TestConnSocksSpecificHost(t *testing.T) { for _, multi := range []bool{false, true} { t.Run(fmt.Sprint("multi=", multi), func(t *testing.T) { s := listen("127.0.0.1:0") s.Multi = multi u := newConnSockListener(t, s, nil, nil) la, err := u.LocalAddr() require.NoError(t, err) if !multi { assert.Zero(t, reusePort(t, u.UDPConn), "listener") } peer, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") bufs, addrs := run(dst, u.socks.cfg.run, "p") _, err = u.WriteBatch(bufs, addrs) require.NoError(t, err) for _, from := range readFrom(t, peer, len(bufs)) { assert.Equal(t, la, from) } if multi { assert.NotNil(t, u.connSockFor(dst)) } else { assert.Nil(t, u.connSockFor(dst)) } }) } } // A v4 listener can't send to a v6 destination; however often it's asked, it says so and opens no connected // socket. func TestConnSocksV6DestinationOnV4Listener(t *testing.T) { s := listen("127.0.0.1:0") s.Multi = true u := newConnSockListener(t, s, nil, nil) require.True(t, u.isV4) dst := netip.MustParseAddrPort("[::1]:9") u.SetEstablishedPeer(dst, true) bufs, _ := run(dst, 2*u.socks.cfg.run, "v6") for _, b := range bufs { assert.ErrorIs(t, u.WriteTo(b, dst), ErrInvalidIPv6RemoteForSocket) } assert.Nil(t, u.connSockFor(dst)) } // darwin's tun hands over one packet per read, so a busy peer's traffic reaches the socket as single WriteTo // calls. cfg.run of them inside one window earn a connected socket; the same count spread over more than a window // doesn't. func TestConnSocksOpenFromWriteTo(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) var now atomic.Int64 now.Store(int64(time.Hour)) u.socks.clock = now.Load _, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") pkt := []byte("pkt") for range u.socks.cfg.run - 1 { require.NoError(t, u.WriteTo(pkt, dst)) } now.Add(int64(u.socks.cfg.window) + 1) require.NoError(t, u.WriteTo(pkt, dst)) assert.Nil(t, u.connSockFor(dst), "a connected socket for sends spread over two windows") for range u.socks.cfg.run - 2 { require.NoError(t, u.WriteTo(pkt, dst)) } assert.Nil(t, u.connSockFor(dst)) require.NoError(t, u.WriteTo(pkt, dst)) assert.NotNil(t, u.connSockFor(dst), "no connected socket after %d sends in one window", u.socks.cfg.run) } // Only the remote of an established tunnel gets a connected socket, however busy another destination is, and one // that stops being established loses its connected socket and has to be busy again to get a new one. func TestConnSocksOnlyForEstablishedPeers(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) peer, dst := newPeer(t, "udp4", "127.0.0.1:0") run1 := u.socks.cfg.run send := func(n int) { t.Helper() bufs, addrs := run(dst, n, "p") _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peer, n) } send(2 * run1) assert.Nil(t, u.connSockFor(dst), "a connected socket for a busy destination with no tunnel") u.SetEstablishedPeer(dst, true) send(run1) s := u.connSockFor(dst) require.NotNil(t, s, "no connected socket for a busy established peer") u.SetEstablishedPeer(dst, false) assert.Nil(t, u.connSockFor(dst)) u.socks.closers.Wait() _, err := s.conn.Write([]byte("x")) assert.ErrorIs(t, err, net.ErrClosed, "connected socket left open after its tunnel went away") send(2 * run1) assert.Nil(t, u.connSockFor(dst), "a connected socket after the tunnel went away") u.SetEstablishedPeer(dst, true) send(run1 - 1) assert.Nil(t, u.connSockFor(dst), "a re-established peer kept its old count") send(1) assert.NotNil(t, u.connSockFor(dst)) } // A tunnel going away while its busy remote is opening a connected socket never leaves one open, whether or not // the remote is established again before the open finishes. The dial hook holds the open between reserving its // destination and dialing until the tunnel has changed, so every round races. func TestConnSocksEstablishedRace(t *testing.T) { h := &countingHandler{} u := newConnSockListener(t, listen("0.0.0.0:0"), h, nil, func(c *connSockConfig) { c.run = 1 c.openEvery = time.Nanosecond }) cs := u.socks peer, dst := newPeer(t, "udp4", "127.0.0.1:0") for i, reestablish := range []bool{false, true} { u.SetEstablishedPeer(dst, true) dialing, release := make(chan struct{}), make(chan struct{}) cs.dialHook = func(netip.AddrPort) { close(dialing) <-release } done := make(chan error) go func() { done <- u.WriteTo([]byte("p"), dst) }() <-dialing u.SetEstablishedPeer(dst, false) if reestablish { u.SetEstablishedPeer(dst, true) } close(release) require.NoError(t, <-done) cs.dialHook = nil readFrom(t, peer, 1) assert.Nil(t, u.connSockFor(dst), "reestablish=%v", reestablish) assert.EqualValues(t, i+1, h.discarded.Load(), "reestablish=%v", reestablish) cs.mu.Lock() assert.Empty(t, cs.pending) cs.mu.Unlock() assert.Zero(t, cs.reserved.Load()) u.SetEstablishedPeer(dst, false) } assert.Zero(t, h.opened.Load()) } // Neither an open in flight nor a close holds anything SetEstablishedPeer waits on, since Nebula calls it under // the hostmap's write lock; and a peer whose connected socket is still closing can't open another until the close // completes. The hooks hold the open in its dials and the close before its Close. func TestConnSocksSetEstablishedPeerMakesNoSyscalls(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil, func(c *connSockConfig) { c.openEvery = time.Nanosecond }) cs := u.socks peer, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") _, other := newPeer(t, "udp4", "127.0.0.1:0") dialing, releaseDial := make(chan struct{}), make(chan struct{}) cs.dialHook = func(netip.AddrPort) { close(dialing) <-releaseDial } opened := make(chan *connSock) go func() { bufs, addrs := run(dst, cs.cfg.run, "p") _, _ = u.WriteBatch(bufs, addrs) opened <- u.connSockFor(dst) }() <-dialing u.SetEstablishedPeer(other, true) u.SetEstablishedPeer(other, false) close(releaseDial) s := <-opened require.NotNil(t, s) readFrom(t, peer, cs.cfg.run) cs.dialHook = nil closing, releaseClose := make(chan struct{}), make(chan struct{}) cs.closeHook = func(netip.AddrPort) { close(closing) <-releaseClose } u.SetEstablishedPeer(dst, false) <-closing assert.Nil(t, u.connSockFor(dst)) u.SetEstablishedPeer(dst, true) bufs, addrs := run(dst, 2*cs.cfg.run, "q") _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peer, len(bufs)) assert.Nil(t, u.connSockFor(dst), "a connected socket opened while the last one to its peer was closing") close(releaseClose) cs.closers.Wait() cs.closeHook = nil _, err = s.conn.Write([]byte("x")) assert.ErrorIs(t, err, net.ErrClosed) bufs, addrs = run(dst, cs.cfg.run, "r") _, err = u.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peer, len(bufs)) assert.NotNil(t, u.connSockFor(dst), "no connected socket once the last one closed") } // A listener whose interface scope can't be read gives a busy peer no connected socket, rather than one without // the scope; the peer stays on the listener for the backoff. A pipe stands in for the listener's descriptor, so // getsockopt fails with ENOTSOCK. func TestConnSocksBoundIfFailsClosed(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) _, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") var pipe [2]int require.NoError(t, unix.Pipe(pipe[:])) t.Cleanup(func() { _ = unix.Close(pipe[0]) _ = unix.Close(pipe[1]) }) listener := u.sysFd u.sysFd = uintptr(pipe[0]) s := u.maybeOpenConnSock(u.socks.peer(dst), dst, u.socks.cfg.run) u.sysFd = listener assert.Nil(t, s) assert.Nil(t, u.connSockFor(dst)) assert.Greater(t, u.socks.peer(dst).retryAt, u.socks.clock(), "no backoff after a failed open") } // readTags reads n datagrams at peer and returns their payloads. func readTags(t *testing.T, peer *net.UDPConn, n int) []string { t.Helper() buf := make([]byte, MTU) var tags []string for range n { require.NoError(t, peer.SetReadDeadline(time.Now().Add(5*time.Second))) m, _, err := peer.ReadFromUDPAddrPort(buf) require.NoError(t, err) tags = append(tags, string(buf[:m])) } return tags } // One batch can hold runs to a peer with a connected socket, to an established peer without one, and to a // destination that is no tunnel's remote, interleaved; each run goes out its own way, in order, and all count. func TestConnSocksWriteBatchMixedDestinations(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, peerA := openConnSock(t, u) peerB, b := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") peerC, c := newPeer(t, "udp4", "127.0.0.1:0") var onSock atomic.Int64 s.sys = func(fd int, b []byte) error { onSock.Add(1) return writeFd(fd, b) } var bufs [][]byte var addrs []netip.AddrPort for _, r := range []struct { dst netip.AddrPort tag string n int }{{s.dst, "a", 3}, {b, "b", 2}, {c, "c", 2}, {s.dst, "A", 2}, {c, "C", 1}} { for i := range r.n { bufs = append(bufs, fmt.Appendf(nil, "%s%d", r.tag, i)) addrs = append(addrs, r.dst) } } n, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) assert.Equal(t, len(bufs), n) assert.EqualValues(t, 5, onSock.Load(), "datagrams on a's connected socket") assert.Equal(t, []string{"a0", "a1", "a2", "A0", "A1"}, readTags(t, peerA, 5)) assert.Equal(t, []string{"b0", "b1"}, readTags(t, peerB, 2)) assert.Equal(t, []string{"c0", "c1", "C0"}, readTags(t, peerC, 3)) assert.Nil(t, u.connSockFor(b)) } // A connected socket error that doesn't mean the path is gone costs only its datagram: WriteBatch skips it and // sends the rest of the run on the connected socket, WriteTo returns it, and the connected socket stays open. func TestConnSocksWriteError(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, peer := openConnSock(t, u) s.sys = func(fd int, b []byte) error { if string(b) == "bad" { return syscall.EMSGSIZE } return writeFd(fd, b) } bufs := [][]byte{[]byte("ok0"), []byte("bad"), []byte("ok2")} n, err := u.WriteBatch(bufs, []netip.AddrPort{s.dst, s.dst, s.dst}) require.NoError(t, err) assert.Equal(t, 2, n) assert.Equal(t, []string{"ok0", "ok2"}, readTags(t, peer, 2)) assert.ErrorIs(t, u.WriteTo([]byte("bad"), s.dst), syscall.EMSGSIZE) assert.Same(t, s, u.connSockFor(s.dst)) } // A connected socket that turns out closed or dead partway through a run leaves the rest of the run to the // listener, in order and all counted; a dead one is closed and its peer backs off to the listener. func TestConnSocksWriteBatchFallsBackMidRun(t *testing.T) { for _, fail := range []error{net.ErrClosed, syscall.ECONNREFUSED} { t.Run(fail.Error(), func(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, peer := openConnSock(t, u) var calls atomic.Int64 s.sys = func(fd int, b []byte) error { if calls.Add(1) > 2 { return fail } return writeFd(fd, b) } bufs, addrs := run(s.dst, 5, "r") n, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) assert.Equal(t, 5, n) assert.Equal(t, []string{"r-0", "r-1", "r-2", "r-3", "r-4"}, readTags(t, peer, 5)) assert.EqualValues(t, 3, calls.Load(), "the connected socket was tried again after it failed") if fail == syscall.ECONNREFUSED { assert.Nil(t, u.connSockFor(s.dst)) u.socks.mu.Lock() assert.Greater(t, u.socks.peer(s.dst).retryAt, u.socks.clock(), "no backoff after a dead connected socket") u.socks.mu.Unlock() } }) } } // openConnSock earns a connected socket to a fresh loopback peer and returns it and the peer. func openConnSock(t *testing.T, u *StdConn) (*connSock, *net.UDPConn) { t.Helper() peer, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") bufs, addrs := run(dst, u.socks.cfg.run, "p") _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) readFrom(t, peer, len(bufs)) s := u.connSockFor(dst) require.NotNil(t, s) return s, peer } // A flow-controlled interface makes a connected socket's sends wait, up to cfg.enobufsMax without one going // through; after that each datagram is dropped without waiting until a send succeeds again, and each stall logs // its drops once. s.sys stands in for an interface that answers every write with ENOBUFS until enobufs is // cleared, and each sleep advances the clock. func TestConnSockWriteENOBUFS(t *testing.T) { h := &countingHandler{} u := newConnSockListener(t, listen("0.0.0.0:0"), h, nil) s, _ := openConnSock(t, u) var now atomic.Int64 now.Store(int64(time.Hour)) sleeps := 0 u.socks.clock = now.Load u.socks.sleep = func(d time.Duration) { sleeps++ now.Add(int64(d)) } var enobufs atomic.Bool s.sys = func(fd int, b []byte) error { if enobufs.Load() { return syscall.ENOBUFS } return writeFd(fd, b) } perStall := int(u.socks.cfg.enobufsMax / enobufsWait) bufs, _ := run(s.dst, 3, "q") for stall := 1; stall <= 2; stall++ { enobufs.Store(true) sent, errno, closed := u.connSockWrite(s, bufs) assert.Equal(t, 3, sent) assert.Zero(t, errno) assert.False(t, closed) assert.Equal(t, stall*perStall, sleeps) sent, _, _ = u.connSockWrite(s, bufs) assert.Equal(t, 3, sent) assert.Equal(t, stall*perStall, sleeps, "a stalled connected socket waited again") assert.EqualValues(t, stall, h.dropping.Load(), "drop log lines after stall %d", stall) enobufs.Store(false) sent, _, _ = u.connSockWrite(s, bufs[:1]) assert.Equal(t, 1, sent) assert.Zero(t, s.stallAt.Load()) } } // Concurrent writers share one ENOBUFS budget measured from the first ENOBUFS since a send went through: two // writers stalled together wait as long as one would, and a send going through while a writer sleeps gives that // writer a fresh budget. The sleep hook parks each writer until the test has moved the clock. func TestConnSockWriteENOBUFSConcurrentWriters(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, _ := openConnSock(t, u) var now atomic.Int64 u.socks.clock = now.Load asleep, wake := make(chan struct{}), make(chan struct{}) u.socks.sleep = func(time.Duration) { asleep <- struct{}{} <-wake } // Datagrams starting with 'e' get ENOBUFS while enobufs is set; the rest go out. var enobufs atomic.Bool var wrote atomic.Int64 s.sys = func(fd int, b []byte) error { if b[0] == 'e' && enobufs.Load() { return syscall.ENOBUFS } wrote.Add(1) return writeFd(fd, b) } write := func(tag string, done chan<- int) { sent, _, _ := u.connSockWrite(s, [][]byte{[]byte(tag)}) done <- sent } budget := int64(u.socks.cfg.enobufsMax) t.Run("a send going through ends the stall", func(t *testing.T) { enobufs.Store(true) now.Store(int64(time.Hour)) done := make(chan int) go write("e1", done) <-asleep now.Add(budget / 2) sent, _, _ := u.connSockWrite(s, [][]byte{[]byte("ok")}) require.Equal(t, 1, sent) now.Add(budget) wake <- struct{}{} <-asleep // e1 hit ENOBUFS again 1.5x the budget after its first, and waited instead of dropping. enobufs.Store(false) before := wrote.Load() wake <- struct{}{} assert.Equal(t, 1, <-done) assert.Equal(t, before+1, wrote.Load(), "e1 was dropped") }) t.Run("two stalled writers spend one budget", func(t *testing.T) { enobufs.Store(true) now.Store(int64(2 * time.Hour)) done := make(chan int) go write("e2", done) go write("e3", done) <-asleep <-asleep now.Add(budget * 6 / 10) wake <- struct{}{} wake <- struct{}{} <-asleep // Both are 0.6x the budget into one stall, not 1.2x, so both wait again. <-asleep now.Add(budget / 2) before := wrote.Load() wake <- struct{}{} wake <- struct{}{} assert.Equal(t, 1, <-done) assert.Equal(t, 1, <-done) assert.Equal(t, before, wrote.Load(), "a datagram went out while ENOBUFS lasted") }) } // A full send buffer that never drains parks a connected socket send only for cfg.enobufsMax, the stall budget // ENOBUFS gets: golang/go#73919 has darwin UDP writes parked on EAGAIN waiting forever. Past it the datagrams are // dropped and counted as sent, concurrent writers spend that one budget, and once a send goes through the // connected socket works as before. s.sys stands in for a send buffer that stays full while eagain is set. func TestConnSockWriteEAGAINBounded(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, peer := openConnSock(t, u) var eagain atomic.Bool eagain.Store(true) s.sys = func(fd int, b []byte) error { if eagain.Load() { return syscall.EAGAIN } return writeFd(fd, b) } budget := u.socks.cfg.enobufsMax start := time.Now() sent, errno, closed := u.connSockWrite(s, [][]byte{[]byte("x")}) elapsed := time.Since(start) assert.Equal(t, 1, sent) assert.Zero(t, errno) assert.False(t, closed) assert.GreaterOrEqual(t, elapsed, budget, "the send didn't wait for the buffer to drain") assert.Less(t, elapsed, time.Second, "the send waited past its deadline") // The budget is spent, so these drop without waiting, however many writers there are. var wg sync.WaitGroup start = time.Now() for range 4 { wg.Go(func() { bufs, _ := run(s.dst, 8, "y") sent, _, _ := u.connSockWrite(s, bufs) assert.Equal(t, 8, sent) }) } wg.Wait() assert.Less(t, time.Since(start), time.Second) // A send going through starts a fresh budget, which two writers stalled together spend once; the upper bound // is far looser than that, so a descheduled runner doesn't fail it. eagain.Store(false) sent, _, _ = u.connSockWrite(s, [][]byte{[]byte("ok")}) require.Equal(t, 1, sent) readTags(t, peer, 1) eagain.Store(true) start = time.Now() for range 2 { wg.Go(func() { sent, _, _ := u.connSockWrite(s, [][]byte{[]byte("w")}) assert.Equal(t, 1, sent) }) } wg.Wait() elapsed = time.Since(start) assert.GreaterOrEqual(t, elapsed, budget) assert.Less(t, elapsed, time.Second) eagain.Store(false) bufs, addrs := run(s.dst, 3, "z") n, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) assert.Equal(t, 3, n) assert.Equal(t, []string{"z-0", "z-1", "z-2"}, readTags(t, peer, 3)) assert.Zero(t, s.stallAt.Load()) assert.Same(t, s, u.connSockFor(s.dst)) } // A send waiting out ENOBUFS holds nothing a close or Close needs, and returns closed once it wakes to a closed // connected socket. The sleep hook parks the writer until both closes have finished; after the close, s.sys // writes for real, so the closed descriptor answers. func TestConnSockWriteDoesNotBlockClose(t *testing.T) { u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil) s, _ := openConnSock(t, u) asleep, wake := make(chan struct{}), make(chan struct{}) u.socks.sleep = func(time.Duration) { asleep <- struct{}{} <-wake } var gone atomic.Bool s.sys = func(fd int, b []byte) error { if gone.Load() { return writeFd(fd, b) } return syscall.ENOBUFS } done := make(chan bool) go func() { bufs, _ := run(s.dst, 1, "q") _, _, closed := u.connSockWrite(s, bufs) done <- closed }() <-asleep u.closeConnSock(s, "test", false) u.socks.closers.Wait() require.NoError(t, u.Close()) gone.Store(true) close(wake) assert.True(t, <-done, "the writer didn't see its connected socket close") } // countingHandler counts connected socket open, close, discard and ENOBUFS drop log lines. type countingHandler struct{ opened, closed, discarded, dropping atomic.Int64 } func (h *countingHandler) Enabled(context.Context, slog.Level) bool { return true } func (h *countingHandler) Handle(_ context.Context, r slog.Record) error { switch r.Message { case "connected socket: opened": h.opened.Add(1) case "connected socket: closed": h.closed.Add(1) case "connected socket: discarded, its tunnel or route changed while it opened": h.discarded.Add(1) case "connected socket: sends stalled, dropping datagrams until one goes through": h.dropping.Add(1) } return nil } func (h *countingHandler) WithAttrs([]slog.Attr) slog.Handler { return h } func (h *countingHandler) WithGroup(string) slog.Handler { return h } // Anyone can make Nebula reply to them, as recv_error does to every datagram with an unknown index; the reader // below stands in for that reply. However many replies a sender draws, it never earns a connected socket without // an established tunnel. func TestConnSocksNoneForUnestablishedSenders(t *testing.T) { h := &countingHandler{} reply := []byte("reply") var c *StdConn var ready atomic.Bool c = newConnSockListener(t, listen("0.0.0.0:0"), h, func(from netip.AddrPort, _ []byte) { if ready.Load() { _ = c.WriteTo(reply, from) } }) ready.Store(true) la, err := c.LocalAddr() require.NoError(t, err) to := netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), la.Port()) pkt := []byte("junk") buf := make([]byte, 64) for range 10 { pc, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) require.NoError(t, err) for range 2 * c.socks.cfg.run { _, err := pc.WriteToUDPAddrPort(pkt, to) require.NoError(t, err) require.NoError(t, pc.SetReadDeadline(time.Now().Add(5*time.Second))) _, _, err = pc.ReadFromUDPAddrPort(buf) require.NoError(t, err) } _ = pc.Close() } assert.Zero(t, h.opened.Load()) assert.Nil(t, c.socks.table.Load()) } // Established peers that keep going away churn connected sockets at most as fast as the open bucket allows, and // what connSocks keeps about each goes when its tunnel does. Each round a fresh peer is established, gets busy and // goes away, and the clock moves 10ms. func TestConnSocksChurnBounded(t *testing.T) { h := &countingHandler{} u := newConnSockListener(t, listen("0.0.0.0:0"), h, nil) var now atomic.Int64 now.Store(int64(time.Hour)) u.socks.clock = now.Load const rounds, step = 300, 10 * time.Millisecond for range rounds { peer, dst := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0") bufs, addrs := run(dst, u.socks.cfg.run, "p") n, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) require.Equal(t, len(bufs), n) readFrom(t, peer, len(bufs)) u.SetEstablishedPeer(dst, false) u.socks.closers.Wait() require.NoError(t, peer.Close()) now.Add(int64(step)) } elapsed := time.Duration(rounds) * step budget := int64(u.socks.cfg.max) + int64(elapsed/u.socks.cfg.openEvery) + 1 t.Logf("%d rounds over %v: %d opens, %d closes", rounds, elapsed, h.opened.Load(), h.closed.Load()) require.GreaterOrEqual(t, h.opened.Load(), int64(u.socks.cfg.max), "the rounds never churned") require.LessOrEqual(t, h.opened.Load(), budget) assert.Equal(t, h.opened.Load(), h.closed.Load()) assert.Nil(t, u.socks.table.Load(), "peers left after their tunnels went away") assert.Zero(t, u.socks.reserved.Load()) } // No send allocates, down any path: the listener with connected sockets off, to a destination that is no // tunnel's remote, to an established peer that isn't busy, or on a connected socket, one datagram or a run. func TestConnSockSendsDontAllocate(t *testing.T) { if raceEnabled { t.Skip("-race makes sync.Pool drop writers at random, so connected sends allocate") } pkt := make([]byte, 1400) for _, c := range []struct { name string cfg func(*connSockConfig) setup func(u *StdConn, dst netip.AddrPort) }{ {name: "off", cfg: func(c *connSockConfig) { c.max = 0 }}, {name: "ineligible", setup: func(u *StdConn, dst netip.AddrPort) { u.SetEstablishedPeer(netip.AddrPortFrom(dst.Addr(), dst.Port()+1), true) }}, {name: "eligible", cfg: func(c *connSockConfig) { c.run = 1 << 30 }, setup: func(u *StdConn, dst netip.AddrPort) { u.SetEstablishedPeer(dst, true) }}, {name: "connected", setup: func(u *StdConn, dst netip.AddrPort) { u.SetEstablishedPeer(dst, true) bufs, addrs := run(dst, u.socks.cfg.run, "p") _, err := u.WriteBatch(bufs, addrs) require.NoError(t, err) require.NotNil(t, u.connSockFor(dst)) }}, } { t.Run(c.name, func(t *testing.T) { var cfg []func(*connSockConfig) if c.cfg != nil { cfg = append(cfg, c.cfg) } u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil, cfg...) _, dst := newPeer(t, "udp4", "127.0.0.1:0") if c.setup != nil { c.setup(u, dst) } bufs, addrs := run(dst, 8, "b") assert.Zero(t, testing.AllocsPerRun(100, func() { _ = u.WriteTo(pkt, dst) }), "WriteTo") assert.Zero(t, testing.AllocsPerRun(100, func() { _, _ = u.WriteBatch(bufs, addrs) }), "WriteBatch") }) } } // Once burst tokens are spent the bucket earns one back per interval. func TestGCRA(t *testing.T) { var g gcra now := int64(time.Hour) for i := range 3 { require.True(t, g.take(now, 3, time.Second), "token %d of the burst", i) } require.False(t, g.take(now, 3, time.Second)) require.False(t, g.take(now+int64(999*time.Millisecond), 3, time.Second)) require.True(t, g.take(now+int64(time.Second), 3, time.Second)) require.False(t, g.take(now+int64(time.Second), 3, time.Second)) require.True(t, g.take(now+int64(time.Hour), 3, time.Second)) }