Files
nebula/udp/connsock_darwin_test.go
T
Caleb JasikandClaude Opus 5.5 37032a22bf udp/darwin: send to busy established peers on connected sockets
On darwin an unconnected sendto runs the address and MAC policy checks
and the EndpointSecurity send check for every datagram, and while
xprotectd throttles it each send takes about 185us, which caps a
tunnel's upload and, through its ACKs, its download. A connected socket
runs those checks once, at connect, and it is the only UDP send that
reports the interface queue's flow control, as ENOBUFS.

Only the current direct remote of an established tunnel is eligible.
udp.EstablishedPeerConn is an optional Conn interface, implemented on
darwin; establishedRemotes refcounts remotes over the main hostmap and
tells each writer on the first and last reference: handshake completion
adds one, deleting or retiring a hostinfo removes it, SetRemote (roaming,
SetRemoteIfPreferred, control, ssh) moves it. Pending and relayed
hostinfos hold none, and a rehandshake's two hostinfos share one.

An eligible peer that Nebula sends NEBULA_CONNSOCK_RUN (64) datagrams to
within NEBULA_CONNSOCK_WINDOW (1s) gets a connected socket: SO_REUSEPORT,
bound to the source address the listener's route to the peer picks on
the listener's port, carrying the listener's IP_BOUND_IF. The peer sees
the same address and port as from the listener, and the socket's reader
delivers through the listener's reader. The listener itself never sets
SO_REUSEPORT (listen routines still do), so no other uid can bind the
port; with a specific listen.host and no routines connected sockets stay
off, since sharing an exact address needs SO_REUSEPORT on both.

All routines listening on one address share one set of connected
sockets: at most NEBULA_CONNSOCKS (8), one per peer, with opens from a
bucket that refills one per NEBULA_CONNSOCK_OPEN_EVERY (1s). A socket
closes when its peer stops being eligible, after NEBULA_CONNSOCK_IDLE
(30s) without traffic, on Rebind (of every routine) and Close, and on an
error that means the connected path is gone. Closing never runs under
the hostmap's or the eligibility lock; the peer can't reopen until the
close finishes. A send that gets ENOBUFS or EAGAIN waits within one
10ms budget shared by every writer since the last send that went
through, then drops; nothing waits in the netpoller without a deadline.
When nothing is eligible or open, a send costs one atomic load before
the listener path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:59:09 +00:00

1021 lines
35 KiB
Go

//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))
}