mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 21:16:52 +01:00
internal/msgx is the one copy of xnu's private batched socket calls: the msghdr_x layout, Send/Recv through the raw syscall (EINTR retried) and Unsupported for the errnos a kernel or sandbox refuses them with. Every socket the listener owns reads with recvmsg_x: the listener and each connected socket run their own reader goroutine, and hand whole batches to ListenOut over a channel. ListenOut runs the EncReader over a batch and flushes once, so the reader stays single-threaded without a lock that readers contend on, and the next recvmsg_x overlaps the decrypt of the last batch. Batch buffers (64 x MTU) come from a per-listener free list that keeps up to 11; a reader takes one only while its socket is readable, so idle connected sockets hold none. A batch with a header intact msghdr_x entries can't produce, or ENOSYS/EPERM/EOPNOTSUPP, switches every reader to one datagram per read for good. Connected sockets send runs with sendmsg_x, which xnu batches through the stack on a connected socket. EAGAIN/ENOBUFS keep #1901's single-datagram stall handling; a refused sendmsg_x retries the datagram with write and turns sendmsg_x off once that goes through. The connected-socket knobs lose their NEBULA_CONNSOCK* environment overrides and keep #1901's defaults. Based on Caleb Jasik's #1895 (recvmsg_x) and #1897 (sendmsg_x), and Jack Doan's internal/msgx shape from #1898. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1026 lines
36 KiB
Go
1026 lines
36 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, applies cfg to its connected socket knobs, and starts ListenOut with r.
|
|
func newConnSockListener(t *testing.T, s Settings, h slog.Handler, r EncReader, cfg ...func(*connSockConfig)) *StdConn {
|
|
t.Helper()
|
|
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.listening.Load() }, 5*time.Second, time.Millisecond)
|
|
return u
|
|
}
|
|
|
|
// oneAtATime stands in for sendFd with f, which writes one datagram, so a test can fail chosen datagrams.
|
|
func oneAtATime(f func(fd int, b []byte) error) func(int, [][]byte, *sendScratch) (int, error) {
|
|
return func(fd int, bufs [][]byte, _ *sendScratch) (int, error) {
|
|
if err := f(fd, bufs[0]); err != nil {
|
|
return 0, err
|
|
}
|
|
return 1, nil
|
|
}
|
|
}
|
|
|
|
func writeFd(fd int, b []byte) error {
|
|
_, err := syscall.Write(fd, b)
|
|
return err
|
|
}
|
|
|
|
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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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 = oneAtATime(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))
|
|
}
|