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