mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-16 00:16:52 +01:00
udp/darwin: batch receives and connected-socket sends with recvmsg_x/sendmsg_x
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>
This commit is contained in:
1 parent
37032a22bf
commit
21075ba32e
7 files changed
+808
-125
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
// Package msgx calls xnu's private batched socket calls, sendmsg_x and recvmsg_x, which move many datagrams per
|
||||||
|
// syscall. They are not public API, so a kernel or sandbox may refuse them; callers fall back to one datagram per
|
||||||
|
// syscall when Unsupported reports that.
|
||||||
|
package msgx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Batch is the most messages the callers hand one sendmsg_x or recvmsg_x call.
|
||||||
|
const Batch = 64
|
||||||
|
|
||||||
|
// Hdr mirrors xnu's struct msghdr_x (bsd/sys/socket_private.h), which the SDK doesn't ship. recvmsg_x reports each
|
||||||
|
// message's length in Datalen rather than in its return value.
|
||||||
|
type Hdr struct {
|
||||||
|
Name *byte
|
||||||
|
Namelen uint32
|
||||||
|
Iov *unix.Iovec
|
||||||
|
Iovlen int32
|
||||||
|
Control *byte
|
||||||
|
Controllen uint32
|
||||||
|
Flags int32
|
||||||
|
Datalen uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send passes hdrs to sendmsg_x on fd and returns how many messages the kernel took. EINTR is retried.
|
||||||
|
func Send(fd uintptr, hdrs []Hdr) (int, syscall.Errno) {
|
||||||
|
return call(unix.SYS_SENDMSG_X, fd, hdrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recv passes hdrs to recvmsg_x on fd and returns how many messages it filled. EINTR is retried.
|
||||||
|
func Recv(fd uintptr, hdrs []Hdr) (int, syscall.Errno) {
|
||||||
|
return call(unix.SYS_RECVMSG_X, fd, hdrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func call(trap, fd uintptr, hdrs []Hdr) (int, syscall.Errno) {
|
||||||
|
if len(hdrs) == 0 {
|
||||||
|
return 0, syscall.EINVAL
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
r, _, errno := unix.Syscall6(trap, fd, uintptr(unsafe.Pointer(&hdrs[0])), uintptr(len(hdrs)), 0, 0, 0)
|
||||||
|
switch errno {
|
||||||
|
case 0:
|
||||||
|
return int(r), 0
|
||||||
|
case syscall.EINTR:
|
||||||
|
default:
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsupported reports whether errno means the kernel or a sandbox refuses the private calls.
|
||||||
|
func Unsupported(errno syscall.Errno) bool {
|
||||||
|
return errno == syscall.ENOSYS || errno == syscall.EPERM || errno == syscall.EOPNOTSUPP
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package msgx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHdrLayout pins Hdr to struct msghdr_x's LP64 layout: 56 bytes, Datalen last at 48.
|
||||||
|
func TestHdrLayout(t *testing.T) {
|
||||||
|
var h Hdr
|
||||||
|
assert.EqualValues(t, 56, unsafe.Sizeof(h))
|
||||||
|
assert.EqualValues(t, 48, unsafe.Offsetof(h.Datalen))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendRecvRoundTrip sends a batch through sendmsg_x in one call and reads it back through recvmsg_x in one
|
||||||
|
// call, each message whole, in order, with Datalen set.
|
||||||
|
func TestSendRecvRoundTrip(t *testing.T) {
|
||||||
|
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = unix.Close(fds[0])
|
||||||
|
_ = unix.Close(fds[1])
|
||||||
|
})
|
||||||
|
// A dropped datagram fails the receive instead of hanging it.
|
||||||
|
require.NoError(t, unix.SetsockoptTimeval(fds[1], unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 2}))
|
||||||
|
|
||||||
|
const n = 5
|
||||||
|
var want [n][]byte
|
||||||
|
var iovs [n]unix.Iovec
|
||||||
|
hdrs := make([]Hdr, n)
|
||||||
|
for i := range n {
|
||||||
|
want[i] = bytes.Repeat([]byte{byte(i + 1)}, 10+i)
|
||||||
|
iovs[i] = unix.Iovec{Base: &want[i][0]}
|
||||||
|
iovs[i].SetLen(len(want[i]))
|
||||||
|
hdrs[i] = Hdr{Iov: &iovs[i], Iovlen: 1}
|
||||||
|
}
|
||||||
|
sent, errno := Send(uintptr(fds[0]), hdrs)
|
||||||
|
require.Zero(t, errno)
|
||||||
|
require.Equal(t, n, sent)
|
||||||
|
|
||||||
|
var bufs [n + 1][64]byte
|
||||||
|
var riovs [n + 1]unix.Iovec
|
||||||
|
rhdrs := make([]Hdr, n+1)
|
||||||
|
for i := range rhdrs {
|
||||||
|
riovs[i] = unix.Iovec{Base: &bufs[i][0]}
|
||||||
|
riovs[i].SetLen(len(bufs[i]))
|
||||||
|
rhdrs[i] = Hdr{Iov: &riovs[i], Iovlen: 1}
|
||||||
|
}
|
||||||
|
got, errno := Recv(uintptr(fds[1]), rhdrs)
|
||||||
|
require.Zero(t, errno)
|
||||||
|
require.Equal(t, n, got)
|
||||||
|
for i := range n {
|
||||||
|
l := int(rhdrs[i].Datalen)
|
||||||
|
assert.Equal(t, want[i], bufs[i][:l], "message %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEmptyBatch pins that an empty batch is refused before it reaches the kernel.
|
||||||
|
func TestEmptyBatch(t *testing.T) {
|
||||||
|
_, errno := Send(0, nil)
|
||||||
|
assert.Equal(t, syscall.EINVAL, errno)
|
||||||
|
_, errno = Recv(0, nil)
|
||||||
|
assert.Equal(t, syscall.EINVAL, errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnsupported(t *testing.T) {
|
||||||
|
for _, e := range []syscall.Errno{syscall.ENOSYS, syscall.EPERM, syscall.EOPNOTSUPP} {
|
||||||
|
assert.True(t, Unsupported(e), "%v", e)
|
||||||
|
}
|
||||||
|
for _, e := range []syscall.Errno{0, syscall.EAGAIN, syscall.ENOBUFS, syscall.EINVAL} {
|
||||||
|
assert.False(t, Unsupported(e), "%v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,19 +10,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// benchConnSocks opens a wildcard listener with ListenOut running, with the connected socket knobs pinned to
|
// benchConnSocks opens a wildcard listener with ListenOut running and cfg applied, and a loopback peer that never
|
||||||
// their defaults and then cfg applied, and a loopback peer that never reads, so its sends cost only the sender.
|
// reads, so its sends cost only the sender.
|
||||||
func benchConnSocks(b *testing.B, s Settings, cfg func(*connSockConfig)) (*StdConn, netip.AddrPort) {
|
func benchConnSocks(b *testing.B, s Settings, cfg func(*connSockConfig)) (*StdConn, netip.AddrPort) {
|
||||||
b.Helper()
|
b.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",
|
|
||||||
} {
|
|
||||||
b.Setenv(k, v)
|
|
||||||
}
|
|
||||||
c, err := NewListener(slog.New(slog.DiscardHandler), s)
|
c, err := NewListener(slog.New(slog.DiscardHandler), s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
@@ -33,7 +24,7 @@ func benchConnSocks(b *testing.B, s Settings, cfg func(*connSockConfig)) (*StdCo
|
|||||||
cfg(&u.socks.cfg)
|
cfg(&u.socks.cfg)
|
||||||
}
|
}
|
||||||
go func() { _ = u.ListenOut(func(netip.AddrPort, []byte) {}, func() {}) }()
|
go func() { _ = u.ListenOut(func(netip.AddrPort, []byte) {}, func() {}) }()
|
||||||
for u.reader.Load() == nil {
|
for !u.listening.Load() {
|
||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
}
|
}
|
||||||
pc, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
pc, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||||
|
|||||||
+101
-65
@@ -12,12 +12,13 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/internal/msgx"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,13 +53,46 @@ type connSock struct {
|
|||||||
// parkMu serializes the writes that wait for a full send buffer, since the write deadline bounding each wait
|
// parkMu serializes the writes that wait for a full send buffer, since the write deadline bounding each wait
|
||||||
// is the descriptor's, not the writer's.
|
// is the descriptor's, not the writer's.
|
||||||
parkMu sync.Mutex
|
parkMu sync.Mutex
|
||||||
// sys writes one datagram on the descriptor: writeFd, or a stand-in for the interface in tests.
|
// sys writes bufs on the descriptor and returns how many went out: sendFd, or a stand-in for the interface in
|
||||||
sys func(fd int, b []byte) error
|
// tests.
|
||||||
|
sys func(fd int, bufs [][]byte, x *sendScratch) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFd(fd int, b []byte) error {
|
// sendScratch is the msghdr_x array one sendmsg_x reads.
|
||||||
_, err := syscall.Write(fd, b)
|
type sendScratch struct {
|
||||||
return err
|
hdrs [msgx.Batch]msgx.Hdr
|
||||||
|
iovs [msgx.Batch]unix.Iovec
|
||||||
|
}
|
||||||
|
|
||||||
|
// noSendX is set once sendmsg_x is refused; connected sockets then write one datagram per syscall.
|
||||||
|
var noSendX atomic.Bool
|
||||||
|
|
||||||
|
// sendFd writes bufs on fd, a lone datagram with write and more with one sendmsg_x, which xnu runs through the stack
|
||||||
|
// as a batch on a connected socket. It returns how many went out, or none and the errno. sendmsg_x reports EAGAIN,
|
||||||
|
// ENOBUFS and EMSGSIZE only when it took nothing, and any other error without the count it took, which a connected
|
||||||
|
// socket gets only on its first datagram, as its peer's unreachable.
|
||||||
|
func sendFd(fd int, bufs [][]byte, x *sendScratch) (int, error) {
|
||||||
|
if len(bufs) == 1 {
|
||||||
|
if _, err := syscall.Write(fd, bufs[0]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
n := min(len(bufs), msgx.Batch)
|
||||||
|
for i, b := range bufs[:n] {
|
||||||
|
x.iovs[i] = unix.Iovec{Base: unsafe.SliceData(b)}
|
||||||
|
x.iovs[i].SetLen(len(b))
|
||||||
|
x.hdrs[i] = msgx.Hdr{Iov: &x.iovs[i], Iovlen: 1}
|
||||||
|
}
|
||||||
|
sent, errno := msgx.Send(uintptr(fd), x.hdrs[:n])
|
||||||
|
clear(x.iovs[:n])
|
||||||
|
if errno != 0 {
|
||||||
|
return 0, errno
|
||||||
|
}
|
||||||
|
if sent == 0 {
|
||||||
|
return 0, syscall.EAGAIN
|
||||||
|
}
|
||||||
|
return sent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// errStallSpent is a write that found the connected socket's stall budget spent, so its datagram is dropped.
|
// errStallSpent is a write that found the connected socket's stall budget spent, so its datagram is dropped.
|
||||||
@@ -67,11 +101,16 @@ var errStallSpent = errors.New("connected socket stall budget spent")
|
|||||||
// connSockWriter carries one write through RawConn.Write. They are pooled with fn bound once, since a closure
|
// connSockWriter carries one write through RawConn.Write. They are pooled with fn bound once, since a closure
|
||||||
// passed through the RawConn interface escapes and would allocate on every send.
|
// passed through the RawConn interface escapes and would allocate on every send.
|
||||||
type connSockWriter struct {
|
type connSockWriter struct {
|
||||||
s *connSock
|
s *connSock
|
||||||
b []byte
|
// bufs holds a copy of the datagrams' slice headers rather than the caller's slice, which would escape
|
||||||
park bool
|
// through the pool and cost WriteTo's one-datagram array an allocation per send.
|
||||||
err error
|
bufs [msgx.Batch][]byte
|
||||||
fn func(fd uintptr) bool
|
nbufs int
|
||||||
|
park bool
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
fn func(fd uintptr) bool
|
||||||
|
x sendScratch
|
||||||
}
|
}
|
||||||
|
|
||||||
var connSockWriters = sync.Pool{New: func() any {
|
var connSockWriters = sync.Pool{New: func() any {
|
||||||
@@ -82,7 +121,7 @@ var connSockWriters = sync.Pool{New: func() any {
|
|||||||
|
|
||||||
func (w *connSockWriter) write(fd uintptr) bool {
|
func (w *connSockWriter) write(fd uintptr) bool {
|
||||||
for {
|
for {
|
||||||
w.err = w.s.sys(int(fd), w.b)
|
w.n, w.err = w.s.sys(int(fd), w.bufs[:w.nbufs], &w.x)
|
||||||
switch {
|
switch {
|
||||||
case w.err == syscall.EINTR:
|
case w.err == syscall.EINTR:
|
||||||
case w.err == syscall.EAGAIN && w.park:
|
case w.err == syscall.EAGAIN && w.park:
|
||||||
@@ -93,28 +132,28 @@ func (w *connSockWriter) write(fd uintptr) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try writes b on s once and returns nil, the write's errno, os.ErrDeadlineExceeded, or net.ErrClosed. With park
|
// try writes bufs on s once and returns how many went out, or none and the write's errno,
|
||||||
// set, EAGAIN waits in the netpoller until the socket is writable or the write deadline passes; without it, EAGAIN
|
// os.ErrDeadlineExceeded, or net.ErrClosed. With park set, EAGAIN waits in the netpoller until the socket is
|
||||||
// is returned.
|
// writable or the write deadline passes; without it, EAGAIN is returned.
|
||||||
func (s *connSock) try(b []byte, park bool) error {
|
func (s *connSock) try(bufs [][]byte, park bool) (int, error) {
|
||||||
w := connSockWriters.Get().(*connSockWriter)
|
w := connSockWriters.Get().(*connSockWriter)
|
||||||
w.s, w.b, w.park = s, b, park
|
w.s, w.nbufs, w.park = s, copy(w.bufs[:], bufs), park
|
||||||
rerr := s.rc.Write(w.fn)
|
rerr := s.rc.Write(w.fn)
|
||||||
err := w.err
|
n, err := w.n, w.err
|
||||||
*w = connSockWriter{fn: w.fn}
|
clear(w.bufs[:w.nbufs])
|
||||||
|
w.s, w.nbufs, w.n, w.err = nil, 0, 0, nil
|
||||||
connSockWriters.Put(w)
|
connSockWriters.Put(w)
|
||||||
switch {
|
switch {
|
||||||
case rerr == nil:
|
case rerr == nil:
|
||||||
return err
|
return n, err
|
||||||
case errors.Is(rerr, os.ErrDeadlineExceeded):
|
case errors.Is(rerr, os.ErrDeadlineExceeded):
|
||||||
return os.ErrDeadlineExceeded
|
return 0, os.ErrDeadlineExceeded
|
||||||
default:
|
default:
|
||||||
return net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// connSockConfig holds the knobs. connSockConfigFromEnv reads all but backoff and enobufsMax from the environment,
|
// connSockConfig holds the knobs, which tests change.
|
||||||
// so one binary can be tuned without a rebuild.
|
|
||||||
type connSockConfig struct {
|
type connSockConfig struct {
|
||||||
// max is how many connected sockets the listen routines may have open at once; 0 turns connected sockets off.
|
// max is how many connected sockets the listen routines may have open at once; 0 turns connected sockets off.
|
||||||
max int
|
max int
|
||||||
@@ -135,8 +174,8 @@ type connSockConfig struct {
|
|||||||
enobufsMax time.Duration
|
enobufsMax time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func connSockConfigFromEnv() connSockConfig {
|
func defaultConnSockConfig() connSockConfig {
|
||||||
c := connSockConfig{
|
return connSockConfig{
|
||||||
max: 8,
|
max: 8,
|
||||||
run: 64,
|
run: 64,
|
||||||
window: time.Second,
|
window: time.Second,
|
||||||
@@ -145,22 +184,6 @@ func connSockConfigFromEnv() connSockConfig {
|
|||||||
openEvery: time.Second,
|
openEvery: time.Second,
|
||||||
enobufsMax: 10 * time.Millisecond,
|
enobufsMax: 10 * time.Millisecond,
|
||||||
}
|
}
|
||||||
if v, err := strconv.Atoi(os.Getenv("NEBULA_CONNSOCKS")); err == nil {
|
|
||||||
c.max = v
|
|
||||||
}
|
|
||||||
if v, err := strconv.Atoi(os.Getenv("NEBULA_CONNSOCK_RUN")); err == nil && v > 0 {
|
|
||||||
c.run = v
|
|
||||||
}
|
|
||||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_WINDOW")); err == nil && v > 0 {
|
|
||||||
c.window = v
|
|
||||||
}
|
|
||||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_IDLE")); err == nil && v > 0 {
|
|
||||||
c.idle = v
|
|
||||||
}
|
|
||||||
if v, err := time.ParseDuration(os.Getenv("NEBULA_CONNSOCK_OPEN_EVERY")); err == nil && v > 0 {
|
|
||||||
c.openEvery = v
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// connSockGroups holds the connSocks that the listen routines bound to one address share, so the limits, the
|
// connSockGroups holds the connSocks that the listen routines bound to one address share, so the limits, the
|
||||||
@@ -182,7 +205,7 @@ func joinConnSocks(u *StdConn, la netip.AddrPort, s Settings) *connSocks {
|
|||||||
cs.mu.Unlock()
|
cs.mu.Unlock()
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
cs := &connSocks{cfg: connSockConfigFromEnv(), key: la, members: []*StdConn{u}, clock: monoNow, sleep: time.Sleep}
|
cs := &connSocks{cfg: defaultConnSockConfig(), key: la, members: []*StdConn{u}, clock: monoNow, sleep: time.Sleep}
|
||||||
if cs.cfg.max > 0 && !u.listenHost.IsUnspecified() && !s.Multi {
|
if cs.cfg.max > 0 && !u.listenHost.IsUnspecified() && !s.Multi {
|
||||||
// A connected socket on a specific listen.host binds the listener's own address, which xnu allows only when
|
// A connected socket on a specific listen.host binds the listener's own address, which xnu allows only when
|
||||||
// both sockets set SO_REUSEPORT, and a listener with it lets another uid bind a dual-stack wildcard on the
|
// both sockets set SO_REUSEPORT, and a listener with it lets another uid bind a dual-stack wildcard on the
|
||||||
@@ -433,7 +456,7 @@ func (u *StdConn) connSockRun(p *connSockPeer, dst netip.AddrPort, bufs [][]byte
|
|||||||
// one out.
|
// one out.
|
||||||
func (u *StdConn) maybeOpenConnSock(p *connSockPeer, dst netip.AddrPort, n int) *connSock {
|
func (u *StdConn) maybeOpenConnSock(p *connSockPeer, dst netip.AddrPort, n int) *connSock {
|
||||||
cs := u.socks
|
cs := u.socks
|
||||||
if n <= 0 || u.reader.Load() == nil || int(cs.reserved.Load()) >= cs.cfg.max || !p.busy(cs.clock(), n, &cs.cfg) {
|
if n <= 0 || !u.listening.Load() || int(cs.reserved.Load()) >= cs.cfg.max || !p.busy(cs.clock(), n, &cs.cfg) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cs.mu.Lock()
|
cs.mu.Lock()
|
||||||
@@ -527,7 +550,7 @@ func (u *StdConn) dialConnSock(p *connSockPeer, dst netip.AddrPort) (*connSock,
|
|||||||
_ = uc.Close()
|
_ = uc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s := &connSock{dst: dst, peer: p, conn: uc, rc: rc, sys: writeFd}
|
s := &connSock{dst: dst, peer: p, conn: uc, rc: rc, sys: sendFd}
|
||||||
s.used.Store(u.socks.clock())
|
s.used.Store(u.socks.clock())
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
@@ -677,7 +700,7 @@ func connSockDead(errno syscall.Errno) bool {
|
|||||||
// the 09-25 probes.
|
// the 09-25 probes.
|
||||||
const enobufsWait = 50 * time.Microsecond
|
const enobufsWait = 50 * time.Microsecond
|
||||||
|
|
||||||
// connSockWrite writes bufs on s, one datagram per write. A full socket buffer parks the caller in the netpoller
|
// connSockWrite writes bufs on s, up to msgx.Batch datagrams per sendmsg_x. A full socket buffer parks the caller in the netpoller
|
||||||
// until the socket is writable, and a flow-controlled interface makes it wait out ENOBUFS, so a busy link slows
|
// until the socket is writable, and a flow-controlled interface makes it wait out ENOBUFS, so a busy link slows
|
||||||
// the tun reader instead of dropping. Once the connected socket has waited cfg.enobufsMax without a send going
|
// the tun reader instead of dropping. Once the connected socket has waited cfg.enobufsMax without a send going
|
||||||
// through, either one drops the datagram instead, and a dropped datagram counts as sent, as the listener's
|
// through, either one drops the datagram instead, and a dropped datagram counts as sent, as the listener's
|
||||||
@@ -691,15 +714,30 @@ func (u *StdConn) connSockWrite(s *connSock, bufs [][]byte) (sent int, errno sys
|
|||||||
s.used.Store(cs.clock())
|
s.used.Store(cs.clock())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
probe := false
|
||||||
for sent < len(bufs) {
|
for sent < len(bufs) {
|
||||||
err := s.try(bufs[sent], false)
|
n := 1
|
||||||
|
if !noSendX.Load() && !probe {
|
||||||
|
n = len(bufs) - sent
|
||||||
|
}
|
||||||
|
k, err := s.try(bufs[sent:sent+n], false)
|
||||||
|
if errno, ok := err.(syscall.Errno); ok && n > 1 && msgx.Unsupported(errno) {
|
||||||
|
// EPERM may be the datagram's own fault, so sendmsg_x is off for good only once a plain write of the same
|
||||||
|
// datagram goes through.
|
||||||
|
probe = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if probe && err == nil && noSendX.CompareAndSwap(false, true) {
|
||||||
|
u.l.Warn("sendmsg_x unavailable, writing one datagram per syscall")
|
||||||
|
}
|
||||||
|
probe = false
|
||||||
if err == syscall.EAGAIN || err == os.ErrDeadlineExceeded {
|
if err == syscall.EAGAIN || err == os.ErrDeadlineExceeded {
|
||||||
// os.ErrDeadlineExceeded here is another writer's parked deadline, passed but not yet cleared.
|
// os.ErrDeadlineExceeded here is another writer's parked deadline, passed but not yet cleared.
|
||||||
err = u.parkedWrite(s, bufs[sent])
|
k, err = u.parkedWrite(s, bufs[sent:sent+1])
|
||||||
}
|
}
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
sent++
|
sent += k
|
||||||
if s.stallAt.Load() != 0 {
|
if s.stallAt.Load() != 0 {
|
||||||
s.stallAt.Store(0)
|
s.stallAt.Store(0)
|
||||||
s.dropping.Store(false)
|
s.dropping.Store(false)
|
||||||
@@ -731,23 +769,23 @@ func (u *StdConn) connSockWrite(s *connSock, bufs [][]byte) (sent int, errno sys
|
|||||||
// parkedWrite writes b after EAGAIN, parked in the netpoller for what is left of s's stall budget, and returns
|
// parkedWrite writes b after EAGAIN, parked in the netpoller for what is left of s's stall budget, and returns
|
||||||
// errStallSpent once none is. golang/go#73919: a darwin UDP write parked on EAGAIN can wait forever for a
|
// errStallSpent once none is. golang/go#73919: a darwin UDP write parked on EAGAIN can wait forever for a
|
||||||
// writability event that never comes, so the wait always has a deadline.
|
// writability event that never comes, so the wait always has a deadline.
|
||||||
func (u *StdConn) parkedWrite(s *connSock, b []byte) error {
|
func (u *StdConn) parkedWrite(s *connSock, b [][]byte) (int, error) {
|
||||||
cs := u.socks
|
cs := u.socks
|
||||||
s.parkMu.Lock()
|
s.parkMu.Lock()
|
||||||
defer s.parkMu.Unlock()
|
defer s.parkMu.Unlock()
|
||||||
left := cs.cfg.enobufsMax - s.stalledFor(cs.clock())
|
left := cs.cfg.enobufsMax - s.stalledFor(cs.clock())
|
||||||
if left <= 0 {
|
if left <= 0 {
|
||||||
return errStallSpent
|
return 0, errStallSpent
|
||||||
}
|
}
|
||||||
if err := s.conn.SetWriteDeadline(time.Now().Add(left)); err != nil {
|
if err := s.conn.SetWriteDeadline(time.Now().Add(left)); err != nil {
|
||||||
return net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
}
|
}
|
||||||
err := s.try(b, true)
|
n, err := s.try(b, true)
|
||||||
_ = s.conn.SetWriteDeadline(time.Time{})
|
_ = s.conn.SetWriteDeadline(time.Time{})
|
||||||
if err == os.ErrDeadlineExceeded {
|
if err == os.ErrDeadlineExceeded {
|
||||||
return errStallSpent
|
return 0, errStallSpent
|
||||||
}
|
}
|
||||||
return err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropStalled logs the first datagram a stall drops.
|
// dropStalled logs the first datagram a stall drops.
|
||||||
@@ -772,14 +810,13 @@ func (s *connSock) stalledFor(now int64) time.Duration {
|
|||||||
return time.Duration(now - at)
|
return time.Duration(now - at)
|
||||||
}
|
}
|
||||||
|
|
||||||
// readConnSock delivers a connected socket's datagrams through the listener's reader, serialized with ListenOut,
|
// readConnSock hands a connected socket's batches to ListenOut until the connected socket is closed. Any receive
|
||||||
// until the connected socket is closed. Any receive error closes it: a connected socket reports its peer's
|
// error closes it: a connected socket reports its peer's unreachables here, and retrying an unexpected error could
|
||||||
// unreachables here, and retrying an unexpected error could spin.
|
// spin.
|
||||||
func (u *StdConn) readConnSock(s *connSock) {
|
func (u *StdConn) readConnSock(s *connSock) {
|
||||||
rd := u.reader.Load()
|
r := u.newBatchReader(s.conn, s.rc)
|
||||||
buf := make([]byte, MTU)
|
|
||||||
for {
|
for {
|
||||||
n, from, err := s.conn.ReadFromUDPAddrPort(buf)
|
b, err := u.read(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
if errors.Is(err, net.ErrClosed) {
|
||||||
return
|
return
|
||||||
@@ -795,9 +832,8 @@ func (u *StdConn) readConnSock(s *connSock) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.used.Store(u.socks.clock())
|
s.used.Store(u.socks.clock())
|
||||||
u.readMu.Lock()
|
if !u.deliver(b) {
|
||||||
rd.r(netip.AddrPortFrom(from.Addr().Unmap(), from.Port()), buf[:n:n])
|
return
|
||||||
rd.flush()
|
}
|
||||||
u.readMu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+33
-28
@@ -20,19 +20,9 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newConnSockListener opens a listener with the connected socket knobs pinned to their defaults, so NEBULA_*
|
// newConnSockListener opens a listener, applies cfg to its connected socket knobs, and starts ListenOut with r.
|
||||||
// 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 {
|
func newConnSockListener(t *testing.T, s Settings, h slog.Handler, r EncReader, cfg ...func(*connSockConfig)) *StdConn {
|
||||||
t.Helper()
|
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 {
|
if h == nil {
|
||||||
h = slog.DiscardHandler
|
h = slog.DiscardHandler
|
||||||
}
|
}
|
||||||
@@ -47,10 +37,25 @@ func newConnSockListener(t *testing.T, s Settings, h slog.Handler, r EncReader,
|
|||||||
f(&u.socks.cfg)
|
f(&u.socks.cfg)
|
||||||
}
|
}
|
||||||
go func() { _ = u.ListenOut(r, func() {}) }()
|
go func() { _ = u.ListenOut(r, func() {}) }()
|
||||||
require.Eventually(t, func() bool { return u.reader.Load() != nil }, 5*time.Second, time.Millisecond)
|
require.Eventually(t, func() bool { return u.listening.Load() }, 5*time.Second, time.Millisecond)
|
||||||
return u
|
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)} }
|
func listen(s string) Settings { return Settings{Listen: netip.MustParseAddrPort(s)} }
|
||||||
|
|
||||||
// run builds n datagrams to dst.
|
// run builds n datagrams to dst.
|
||||||
@@ -282,10 +287,10 @@ func TestConnSocksSharedAcrossRoutines(t *testing.T) {
|
|||||||
s1 := u1.connSockFor(a)
|
s1 := u1.connSockFor(a)
|
||||||
require.NotNil(t, s1)
|
require.NotNil(t, s1)
|
||||||
var sends atomic.Int64
|
var sends atomic.Int64
|
||||||
s1.sys = func(fd int, b []byte) error {
|
s1.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
sends.Add(1)
|
sends.Add(1)
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
require.NoError(t, u0.WriteTo([]byte("via u0"), a))
|
require.NoError(t, u0.WriteTo([]byte("via u0"), a))
|
||||||
readFrom(t, peerA, 1)
|
readFrom(t, peerA, 1)
|
||||||
assert.EqualValues(t, 1, sends.Load(), "routine 0 didn't send on routine 1's connected socket")
|
assert.EqualValues(t, 1, sends.Load(), "routine 0 didn't send on routine 1's connected socket")
|
||||||
@@ -566,10 +571,10 @@ func TestConnSocksWriteBatchMixedDestinations(t *testing.T) {
|
|||||||
peerB, b := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0")
|
peerB, b := newEstablishedPeer(t, u, "udp4", "127.0.0.1:0")
|
||||||
peerC, c := newPeer(t, "udp4", "127.0.0.1:0")
|
peerC, c := newPeer(t, "udp4", "127.0.0.1:0")
|
||||||
var onSock atomic.Int64
|
var onSock atomic.Int64
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
onSock.Add(1)
|
onSock.Add(1)
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
|
|
||||||
var bufs [][]byte
|
var bufs [][]byte
|
||||||
var addrs []netip.AddrPort
|
var addrs []netip.AddrPort
|
||||||
@@ -598,12 +603,12 @@ func TestConnSocksWriteBatchMixedDestinations(t *testing.T) {
|
|||||||
func TestConnSocksWriteError(t *testing.T) {
|
func TestConnSocksWriteError(t *testing.T) {
|
||||||
u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil)
|
u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil)
|
||||||
s, peer := openConnSock(t, u)
|
s, peer := openConnSock(t, u)
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if string(b) == "bad" {
|
if string(b) == "bad" {
|
||||||
return syscall.EMSGSIZE
|
return syscall.EMSGSIZE
|
||||||
}
|
}
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
bufs := [][]byte{[]byte("ok0"), []byte("bad"), []byte("ok2")}
|
bufs := [][]byte{[]byte("ok0"), []byte("bad"), []byte("ok2")}
|
||||||
n, err := u.WriteBatch(bufs, []netip.AddrPort{s.dst, s.dst, s.dst})
|
n, err := u.WriteBatch(bufs, []netip.AddrPort{s.dst, s.dst, s.dst})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -621,12 +626,12 @@ func TestConnSocksWriteBatchFallsBackMidRun(t *testing.T) {
|
|||||||
u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil)
|
u := newConnSockListener(t, listen("0.0.0.0:0"), nil, nil)
|
||||||
s, peer := openConnSock(t, u)
|
s, peer := openConnSock(t, u)
|
||||||
var calls atomic.Int64
|
var calls atomic.Int64
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if calls.Add(1) > 2 {
|
if calls.Add(1) > 2 {
|
||||||
return fail
|
return fail
|
||||||
}
|
}
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
bufs, addrs := run(s.dst, 5, "r")
|
bufs, addrs := run(s.dst, 5, "r")
|
||||||
n, err := u.WriteBatch(bufs, addrs)
|
n, err := u.WriteBatch(bufs, addrs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -673,12 +678,12 @@ func TestConnSockWriteENOBUFS(t *testing.T) {
|
|||||||
now.Add(int64(d))
|
now.Add(int64(d))
|
||||||
}
|
}
|
||||||
var enobufs atomic.Bool
|
var enobufs atomic.Bool
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if enobufs.Load() {
|
if enobufs.Load() {
|
||||||
return syscall.ENOBUFS
|
return syscall.ENOBUFS
|
||||||
}
|
}
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
perStall := int(u.socks.cfg.enobufsMax / enobufsWait)
|
perStall := int(u.socks.cfg.enobufsMax / enobufsWait)
|
||||||
bufs, _ := run(s.dst, 3, "q")
|
bufs, _ := run(s.dst, 3, "q")
|
||||||
|
|
||||||
@@ -718,13 +723,13 @@ func TestConnSockWriteENOBUFSConcurrentWriters(t *testing.T) {
|
|||||||
// Datagrams starting with 'e' get ENOBUFS while enobufs is set; the rest go out.
|
// Datagrams starting with 'e' get ENOBUFS while enobufs is set; the rest go out.
|
||||||
var enobufs atomic.Bool
|
var enobufs atomic.Bool
|
||||||
var wrote atomic.Int64
|
var wrote atomic.Int64
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if b[0] == 'e' && enobufs.Load() {
|
if b[0] == 'e' && enobufs.Load() {
|
||||||
return syscall.ENOBUFS
|
return syscall.ENOBUFS
|
||||||
}
|
}
|
||||||
wrote.Add(1)
|
wrote.Add(1)
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
write := func(tag string, done chan<- int) {
|
write := func(tag string, done chan<- int) {
|
||||||
sent, _, _ := u.connSockWrite(s, [][]byte{[]byte(tag)})
|
sent, _, _ := u.connSockWrite(s, [][]byte{[]byte(tag)})
|
||||||
done <- sent
|
done <- sent
|
||||||
@@ -782,12 +787,12 @@ func TestConnSockWriteEAGAINBounded(t *testing.T) {
|
|||||||
s, peer := openConnSock(t, u)
|
s, peer := openConnSock(t, u)
|
||||||
var eagain atomic.Bool
|
var eagain atomic.Bool
|
||||||
eagain.Store(true)
|
eagain.Store(true)
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if eagain.Load() {
|
if eagain.Load() {
|
||||||
return syscall.EAGAIN
|
return syscall.EAGAIN
|
||||||
}
|
}
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
})
|
||||||
budget := u.socks.cfg.enobufsMax
|
budget := u.socks.cfg.enobufsMax
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -853,12 +858,12 @@ func TestConnSockWriteDoesNotBlockClose(t *testing.T) {
|
|||||||
<-wake
|
<-wake
|
||||||
}
|
}
|
||||||
var gone atomic.Bool
|
var gone atomic.Bool
|
||||||
s.sys = func(fd int, b []byte) error {
|
s.sys = oneAtATime(func(fd int, b []byte) error {
|
||||||
if gone.Load() {
|
if gone.Load() {
|
||||||
return writeFd(fd, b)
|
return writeFd(fd, b)
|
||||||
}
|
}
|
||||||
return syscall.ENOBUFS
|
return syscall.ENOBUFS
|
||||||
}
|
})
|
||||||
done := make(chan bool)
|
done := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
bufs, _ := run(s.dst, 1, "q")
|
bufs, _ := run(s.dst, 1, "q")
|
||||||
|
|||||||
+190
-20
@@ -10,12 +10,12 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
|
"github.com/slackhq/nebula/internal/msgx"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,15 +31,13 @@ type StdConn struct {
|
|||||||
// rc reaches the listener's descriptor for another listen routine's Rebind.
|
// rc reaches the listener's descriptor for another listen routine's Rebind.
|
||||||
rc syscall.RawConn
|
rc syscall.RawConn
|
||||||
socks *connSocks
|
socks *connSocks
|
||||||
// reader is ListenOut's reader, which connected socket readers share; readMu serializes them, since an
|
// listening is set once ListenOut runs. The listener's and every connected socket's readers hand their batches
|
||||||
// EncReader isn't safe for concurrent use.
|
// to it on rx, so the EncReader runs on ListenOut's goroutine alone; done closes when ListenOut returns.
|
||||||
reader atomic.Pointer[readerPair]
|
listening atomic.Bool
|
||||||
readMu sync.Mutex
|
rx chan *rxBatch
|
||||||
}
|
done chan struct{}
|
||||||
|
// free holds the batches ListenOut has delivered, for the readers to fill again.
|
||||||
type readerPair struct {
|
free chan *rxBatch
|
||||||
r EncReader
|
|
||||||
flush func()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Conn = &StdConn{}
|
var _ Conn = &StdConn{}
|
||||||
@@ -53,7 +51,8 @@ func NewListener(l *slog.Logger, s Settings) (Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if uc, ok := pc.(*net.UDPConn); ok {
|
if uc, ok := pc.(*net.UDPConn); ok {
|
||||||
c := &StdConn{UDPConn: uc, l: l, listenHost: s.Listen.Addr()}
|
c := &StdConn{UDPConn: uc, l: l, listenHost: s.Listen.Addr(), rx: make(chan *rxBatch, 1), done: make(chan struct{}),
|
||||||
|
free: make(chan *rxBatch, rxBatchesKept)}
|
||||||
|
|
||||||
rc, err := uc.SyscallConn()
|
rc, err := uc.SyscallConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -247,14 +246,176 @@ func NewUDPStatsEmitter(udpConns []Conn) func() {
|
|||||||
return func() {}
|
return func() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenOut delivers the batches that the listener's reader and the connected sockets' readers hand it.
|
||||||
func (u *StdConn) ListenOut(r EncReader, flush func()) error {
|
func (u *StdConn) ListenOut(r EncReader, flush func()) error {
|
||||||
buffer := make([]byte, MTU)
|
defer close(u.done)
|
||||||
u.reader.Store(&readerPair{r: r, flush: flush})
|
u.listening.Store(true)
|
||||||
u.startConnSocks()
|
u.startConnSocks()
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
go func() { errc <- u.readListener() }()
|
||||||
for {
|
for {
|
||||||
// Just read one packet at a time
|
select {
|
||||||
n, rua, err := u.ReadFromUDPAddrPort(buffer)
|
case b := <-u.rx:
|
||||||
|
for i := range b.n {
|
||||||
|
r(b.addrs[i], b.pkts[i])
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
u.putBatch(b)
|
||||||
|
case err := <-errc:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// rxBatch is one recvmsg_x batch: n datagrams in pkts, from addrs. A reader takes one only while its socket is
|
||||||
|
// readable, so an idle connected socket holds none.
|
||||||
|
type rxBatch struct {
|
||||||
|
n int
|
||||||
|
pkts [msgx.Batch][]byte
|
||||||
|
addrs [msgx.Batch]netip.AddrPort
|
||||||
|
buf []byte
|
||||||
|
names [msgx.Batch]unix.RawSockaddrInet6
|
||||||
|
iovs [msgx.Batch]unix.Iovec
|
||||||
|
hdrs [msgx.Batch]msgx.Hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// rxBatchesKept is how many batches a listener keeps for reuse: one being delivered and one queued, plus one for
|
||||||
|
// each socket reading at once, the listener and up to 8 connected sockets. Each is msgx.Batch*MTU, about 576KB.
|
||||||
|
const rxBatchesKept = 11
|
||||||
|
|
||||||
|
func (u *StdConn) getBatch() *rxBatch {
|
||||||
|
select {
|
||||||
|
case b := <-u.free:
|
||||||
|
return b
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
b := &rxBatch{buf: make([]byte, msgx.Batch*MTU)}
|
||||||
|
for i := range b.hdrs {
|
||||||
|
b.iovs[i].Base = &b.buf[i*MTU]
|
||||||
|
b.iovs[i].SetLen(MTU)
|
||||||
|
b.hdrs[i] = msgx.Hdr{Name: (*byte)(unsafe.Pointer(&b.names[i])), Iov: &b.iovs[i], Iovlen: 1}
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) putBatch(b *rxBatch) {
|
||||||
|
select {
|
||||||
|
case u.free <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// noRecvX is set once recvmsg_x is refused or misbehaves; every reader then reads one datagram per syscall.
|
||||||
|
var noRecvX atomic.Bool
|
||||||
|
|
||||||
|
// parse checks the n entries recvmsg_x filled and sets pkts and addrs from them. It rejects a batch that intact
|
||||||
|
// msghdr_x entries can't produce, which is how a change to xnu's private struct layout would show up.
|
||||||
|
func (b *rxBatch) parse(n int) error {
|
||||||
|
if n > len(b.hdrs) {
|
||||||
|
return fmt.Errorf("returned %d datagrams for %d headers", n, len(b.hdrs))
|
||||||
|
}
|
||||||
|
for i := range n {
|
||||||
|
h := &b.hdrs[i]
|
||||||
|
if h.Datalen > MTU {
|
||||||
|
return fmt.Errorf("datagram %d is %d bytes, past the %d byte buffer", i, h.Datalen, MTU)
|
||||||
|
}
|
||||||
|
sa := &b.names[i]
|
||||||
|
switch {
|
||||||
|
case sa.Family == unix.AF_INET && h.Namelen == unix.SizeofSockaddrInet4:
|
||||||
|
sa4 := (*unix.RawSockaddrInet4)(unsafe.Pointer(sa))
|
||||||
|
b.addrs[i] = netip.AddrPortFrom(netip.AddrFrom4(sa4.Addr), binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&sa4.Port))[:]))
|
||||||
|
case sa.Family == unix.AF_INET6 && h.Namelen == unix.SizeofSockaddrInet6:
|
||||||
|
b.addrs[i] = netip.AddrPortFrom(netip.AddrFrom16(sa.Addr).Unmap(), binary.BigEndian.Uint16((*[2]byte)(unsafe.Pointer(&sa.Port))[:]))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("datagram %d has address family %d with length %d", i, sa.Family, h.Namelen)
|
||||||
|
}
|
||||||
|
b.pkts[i] = b.buf[i*MTU : i*MTU+int(h.Datalen) : i*MTU+int(h.Datalen)]
|
||||||
|
}
|
||||||
|
b.n = n
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// batchReader reads one socket in batches. Its fn is bound once, since a closure passed through the RawConn
|
||||||
|
// interface escapes and would allocate on every read.
|
||||||
|
type batchReader struct {
|
||||||
|
u *StdConn
|
||||||
|
conn *net.UDPConn
|
||||||
|
rc syscall.RawConn
|
||||||
|
b *rxBatch
|
||||||
|
errno syscall.Errno
|
||||||
|
fn func(fd uintptr) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) newBatchReader(conn *net.UDPConn, rc syscall.RawConn) *batchReader {
|
||||||
|
r := &batchReader{u: u, conn: conn, rc: rc}
|
||||||
|
r.fn = r.recv
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *batchReader) recv(fd uintptr) bool {
|
||||||
|
b := r.u.getBatch()
|
||||||
|
for i := range b.hdrs {
|
||||||
|
b.hdrs[i].Namelen = unix.SizeofSockaddrInet6
|
||||||
|
b.hdrs[i].Flags = 0
|
||||||
|
b.hdrs[i].Datalen = 0
|
||||||
|
}
|
||||||
|
n, errno := msgx.Recv(fd, b.hdrs[:])
|
||||||
|
if errno == unix.EAGAIN {
|
||||||
|
r.u.putBatch(b)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b.n, r.b, r.errno = n, b, errno
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// read returns the next batch: up to msgx.Batch datagrams from one recvmsg_x, or one datagram once recvmsg_x is off.
|
||||||
|
func (u *StdConn) read(r *batchReader) (*rxBatch, error) {
|
||||||
|
if !noRecvX.Load() {
|
||||||
|
r.b = nil
|
||||||
|
err := r.rc.Read(r.fn)
|
||||||
|
b := r.b
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if b != nil {
|
||||||
|
u.putBatch(b)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
case r.errno == 0:
|
||||||
|
perr := b.parse(b.n)
|
||||||
|
if perr == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
// The batch was read through a layout the kernel no longer writes, so none of it is trustworthy.
|
||||||
|
u.disableRecvX("returned an unexpected header", perr)
|
||||||
|
case msgx.Unsupported(r.errno):
|
||||||
|
u.disableRecvX("unavailable", r.errno)
|
||||||
|
default:
|
||||||
|
u.putBatch(b)
|
||||||
|
return nil, r.errno
|
||||||
|
}
|
||||||
|
u.putBatch(b)
|
||||||
|
}
|
||||||
|
b := u.getBatch()
|
||||||
|
n, from, err := r.conn.ReadFromUDPAddrPort(b.buf[:MTU])
|
||||||
|
if err != nil {
|
||||||
|
u.putBatch(b)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.n, b.pkts[0], b.addrs[0] = 1, b.buf[:n:n], netip.AddrPortFrom(from.Addr().Unmap(), from.Port())
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) disableRecvX(why string, err error) {
|
||||||
|
if noRecvX.CompareAndSwap(false, true) {
|
||||||
|
u.l.Warn("recvmsg_x "+why+", reading one datagram per syscall", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readListener hands the listener's batches to ListenOut until the listener closes.
|
||||||
|
func (u *StdConn) readListener() error {
|
||||||
|
r := u.newBatchReader(u.UDPConn, u.rc)
|
||||||
|
for {
|
||||||
|
b, err := u.read(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
if errors.Is(err, net.ErrClosed) {
|
||||||
return err
|
return err
|
||||||
@@ -262,11 +423,20 @@ func (u *StdConn) ListenOut(r EncReader, flush func()) error {
|
|||||||
u.l.Error("unexpected udp socket receive error", "error", err)
|
u.l.Error("unexpected udp socket receive error", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !u.deliver(b) {
|
||||||
|
return net.ErrClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u.readMu.Lock()
|
// deliver hands b to ListenOut, and reports false once ListenOut has returned.
|
||||||
r(netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port()), buffer[:n:n])
|
func (u *StdConn) deliver(b *rxBatch) bool {
|
||||||
flush()
|
select {
|
||||||
u.readMu.Unlock()
|
case u.rx <- b:
|
||||||
|
return true
|
||||||
|
case <-u.done:
|
||||||
|
u.putBatch(b)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
//go:build !e2e_testing
|
||||||
|
|
||||||
|
package udp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/slackhq/nebula/internal/msgx"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A burst larger than msgx.Batch arrives whole, with each datagram's own bytes and source address, and
|
||||||
|
// flush runs once per recvmsg_x batch rather than once per datagram.
|
||||||
|
func TestListenOutBatchesBurst(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ name, listen string }{
|
||||||
|
{"v4", "127.0.0.1"},
|
||||||
|
{"v6", "::1"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
c, err := NewListener(slog.New(slog.DiscardHandler), Settings{Listen: netip.AddrPortFrom(netip.MustParseAddr(tc.listen), 0)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
la, err := c.LocalAddr()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tx, err := net.Dial("udp", la.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer tx.Close()
|
||||||
|
txAddr := netip.MustParseAddrPort(tx.LocalAddr().String())
|
||||||
|
|
||||||
|
const total = msgx.Batch*3 + 7
|
||||||
|
var mu sync.Mutex
|
||||||
|
var got []string
|
||||||
|
var froms []netip.AddrPort
|
||||||
|
flushes := 0
|
||||||
|
// Queued before the reader starts, so recvmsg_x sees full batches.
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
_, err := tx.Write([]byte(fmt.Sprintf("pkt-%03d", i)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_ = c.ListenOut(func(addr netip.AddrPort, payload []byte) {
|
||||||
|
mu.Lock()
|
||||||
|
got = append(got, string(payload))
|
||||||
|
froms = append(froms, addr)
|
||||||
|
mu.Unlock()
|
||||||
|
}, func() {
|
||||||
|
mu.Lock()
|
||||||
|
flushes++
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return len(got) == total
|
||||||
|
}, 5*time.Second, 10*time.Millisecond)
|
||||||
|
|
||||||
|
require.NoError(t, c.Close())
|
||||||
|
<-done
|
||||||
|
|
||||||
|
for i, p := range got {
|
||||||
|
assert.Equal(t, fmt.Sprintf("pkt-%03d", i), p)
|
||||||
|
assert.Equal(t, txAddr, froms[i])
|
||||||
|
}
|
||||||
|
assert.Equal(t, (total+msgx.Batch-1)/msgx.Batch, flushes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once recvmsg_x is off, every datagram still arrives, one per batch.
|
||||||
|
func TestListenOutSingleFallback(t *testing.T) {
|
||||||
|
noRecvX.Store(true)
|
||||||
|
t.Cleanup(func() { noRecvX.Store(false) })
|
||||||
|
c, err := NewListener(slog.New(slog.DiscardHandler), Settings{Listen: netip.MustParseAddrPort("127.0.0.1:0")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
la, err := c.LocalAddr()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx, err := net.Dial("udp", la.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer tx.Close()
|
||||||
|
txAddr := netip.MustParseAddrPort(tx.LocalAddr().String())
|
||||||
|
|
||||||
|
const total = 5
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
_, err := tx.Write([]byte(fmt.Sprintf("pkt-%03d", i)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
var got []string
|
||||||
|
flushes := 0
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_ = c.ListenOut(func(addr netip.AddrPort, payload []byte) {
|
||||||
|
assert.Equal(t, txAddr, addr)
|
||||||
|
mu.Lock()
|
||||||
|
got = append(got, string(payload))
|
||||||
|
mu.Unlock()
|
||||||
|
}, func() {
|
||||||
|
mu.Lock()
|
||||||
|
flushes++
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
return len(got) == total
|
||||||
|
}, 5*time.Second, 10*time.Millisecond)
|
||||||
|
require.NoError(t, c.Close())
|
||||||
|
<-done
|
||||||
|
|
||||||
|
for i, p := range got {
|
||||||
|
assert.Equal(t, fmt.Sprintf("pkt-%03d", i), p)
|
||||||
|
}
|
||||||
|
assert.Equal(t, total, flushes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRxBatchParse(t *testing.T) {
|
||||||
|
valid := func() *rxBatch {
|
||||||
|
b := (&StdConn{}).getBatch()
|
||||||
|
b.hdrs[0].Namelen, b.hdrs[0].Datalen = unix.SizeofSockaddrInet4, 1
|
||||||
|
b.names[0].Family = unix.AF_INET
|
||||||
|
sa4 := (*unix.RawSockaddrInet4)(unsafe.Pointer(&b.names[0]))
|
||||||
|
sa4.Addr = [4]byte{10, 0, 0, 1}
|
||||||
|
sa4.Port = 0x0110 // 4097 in network order
|
||||||
|
b.hdrs[1].Namelen, b.hdrs[1].Datalen = unix.SizeofSockaddrInet6, MTU
|
||||||
|
b.names[1].Family = unix.AF_INET6
|
||||||
|
b.names[1].Addr = netip.MustParseAddr("::ffff:10.0.0.2").As16()
|
||||||
|
b.names[1].Port = 0x0210
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
b := valid()
|
||||||
|
require.NoError(t, b.parse(2))
|
||||||
|
assert.Equal(t, 2, b.n)
|
||||||
|
assert.Equal(t, netip.MustParseAddrPort("10.0.0.1:4097"), b.addrs[0])
|
||||||
|
assert.Equal(t, netip.MustParseAddrPort("10.0.0.2:4098"), b.addrs[1], "v4-mapped sources arrive unmapped")
|
||||||
|
assert.Len(t, b.pkts[0], 1)
|
||||||
|
assert.Equal(t, 1, cap(b.pkts[0]))
|
||||||
|
assert.Len(t, b.pkts[1], MTU)
|
||||||
|
require.NoError(t, valid().parse(0))
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
n int
|
||||||
|
mutate func(*rxBatch)
|
||||||
|
}{
|
||||||
|
{"count past headers", msgx.Batch + 1, func(*rxBatch) {}},
|
||||||
|
{"datalen past buffer", 2, func(b *rxBatch) { b.hdrs[1].Datalen = MTU + 1 }},
|
||||||
|
{"unknown family", 2, func(b *rxBatch) { b.names[0].Family = unix.AF_UNIX }},
|
||||||
|
{"family and length disagree", 2, func(b *rxBatch) { b.hdrs[0].Namelen = unix.SizeofSockaddrInet6 }},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
b := valid()
|
||||||
|
tc.mutate(b)
|
||||||
|
assert.Error(t, b.parse(tc.n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsghdrXKernelRoundTrip(t *testing.T) {
|
||||||
|
for _, v6 := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("v6=%v", v6), func(t *testing.T) {
|
||||||
|
// Loopback delivery goes through the dlil input thread, so the settle gives all three time to
|
||||||
|
// queue and come back from one call, which is what exercises the stride.
|
||||||
|
got := recvmsgXRoundTrip(t, v6, []int{1, 300, 1400}, 4, 50*time.Millisecond)
|
||||||
|
assert.Equal(t, 3, got, "largest batch from one recvmsg_x call")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any burst, of any datagram lengths up to the kernel's maxdgram, into any number of headers, comes back in order
|
||||||
|
// with every entry's fields intact, and headers past the returned count untouched.
|
||||||
|
func FuzzMsghdrXKernelRoundTrip(f *testing.F) {
|
||||||
|
f.Add(false, uint8(3), []byte{0, 1, 1, 44, 5, 120})
|
||||||
|
f.Add(true, uint8(0), []byte{0, 0, 0x23, 0x29}) // one empty datagram, then one at MTU
|
||||||
|
f.Add(false, uint8(1), []byte{0x23, 0x2a, 0x24, 0x00, 0}) // past MTU: cut to MTU, no MSG_TRUNC
|
||||||
|
f.Add(true, uint8(msgx.Batch-1), bytes.Repeat([]byte{0x05, 0xdc}, msgx.Batch))
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, v6 bool, spare uint8, sizes []byte) {
|
||||||
|
// Two bytes per datagram, big-endian, folded into [0, maxdgram].
|
||||||
|
var lens []int
|
||||||
|
for i := 0; i+1 < len(sizes) && len(lens) < msgx.Batch; i += 2 {
|
||||||
|
lens = append(lens, (int(sizes[i])<<8|int(sizes[i+1]))%(maxDgram+1))
|
||||||
|
}
|
||||||
|
if len(lens) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recvmsgXRoundTrip(t, v6, lens, 1+int(spare)%msgx.Batch, time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxDgram is darwin's default net.inet.udp.maxdgram, the largest datagram a loopback sender may write.
|
||||||
|
const maxDgram = 9216
|
||||||
|
|
||||||
|
// recvmsgXRoundTrip sends one datagram per entry of lens to a fresh loopback socket, drains them with recvmsg_x
|
||||||
|
// into nh headers, and checks every field the kernel writes. Neighboring datagrams differ in sender, length and
|
||||||
|
// fill byte, so no field can pass by reading a neighbor's value. It returns the largest batch one call returned.
|
||||||
|
func recvmsgXRoundTrip(t *testing.T, v6 bool, lens []int, nh int, settle time.Duration) int {
|
||||||
|
t.Helper()
|
||||||
|
network, lo := "udp4", netip.MustParseAddr("127.0.0.1")
|
||||||
|
if v6 {
|
||||||
|
network, lo = "udp6", netip.IPv6Loopback()
|
||||||
|
}
|
||||||
|
rx, err := net.ListenUDP(network, net.UDPAddrFromAddrPort(netip.AddrPortFrom(lo, 0)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rx.Close()
|
||||||
|
|
||||||
|
txs := make([]*net.UDPConn, 3)
|
||||||
|
for i := range txs {
|
||||||
|
txs[i], err = net.DialUDP(network, nil, rx.LocalAddr().(*net.UDPAddr))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer txs[i].Close()
|
||||||
|
}
|
||||||
|
senders := make([]netip.AddrPort, len(lens))
|
||||||
|
for i, l := range lens {
|
||||||
|
tx := txs[i%len(txs)]
|
||||||
|
senders[i] = tx.LocalAddr().(*net.UDPAddr).AddrPort()
|
||||||
|
_, err := tx.Write(bytes.Repeat([]byte{byte(i + 1)}, l))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
time.Sleep(settle)
|
||||||
|
|
||||||
|
// Namelen goes in as the v6 size on both families, so the v4 size coming back proves the kernel wrote it.
|
||||||
|
const sentinel = 0xa5a5a5a5a5a5a5a5
|
||||||
|
wantNamelen := uint32(unix.SizeofSockaddrInet4)
|
||||||
|
if v6 {
|
||||||
|
wantNamelen = unix.SizeofSockaddrInet6
|
||||||
|
}
|
||||||
|
bufs := make([][]byte, nh)
|
||||||
|
names := make([]unix.RawSockaddrInet6, nh)
|
||||||
|
iovs := make([]unix.Iovec, nh)
|
||||||
|
hdrs := make([]msgx.Hdr, nh)
|
||||||
|
for i := range hdrs {
|
||||||
|
bufs[i] = make([]byte, MTU)
|
||||||
|
iovs[i].Base = &bufs[i][0]
|
||||||
|
iovs[i].SetLen(MTU)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A datagram dropped on loopback would otherwise block rc.Read forever.
|
||||||
|
require.NoError(t, rx.SetReadDeadline(time.Now().Add(5*time.Second)))
|
||||||
|
rc, err := rx.SyscallConn()
|
||||||
|
require.NoError(t, err)
|
||||||
|
largest := 0
|
||||||
|
for next := 0; next < len(lens); {
|
||||||
|
for i := range hdrs {
|
||||||
|
clear(bufs[i])
|
||||||
|
hdrs[i] = msgx.Hdr{
|
||||||
|
Name: (*byte)(unsafe.Pointer(&names[i])),
|
||||||
|
Namelen: unix.SizeofSockaddrInet6,
|
||||||
|
Iov: &iovs[i],
|
||||||
|
Iovlen: 1,
|
||||||
|
Flags: -1,
|
||||||
|
Datalen: sentinel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
var errno syscall.Errno
|
||||||
|
require.NoError(t, rc.Read(func(fd uintptr) bool {
|
||||||
|
n, errno = msgx.Recv(fd, hdrs)
|
||||||
|
return errno != unix.EAGAIN
|
||||||
|
}), "received %d of %d datagrams", next, len(lens))
|
||||||
|
require.Zero(t, errno)
|
||||||
|
require.Positive(t, n)
|
||||||
|
require.LessOrEqual(t, n, min(nh, len(lens)-next))
|
||||||
|
largest = max(largest, n)
|
||||||
|
b := &rxBatch{buf: make([]byte, n*MTU)}
|
||||||
|
copy(b.hdrs[:], hdrs[:n])
|
||||||
|
copy(b.names[:], names[:n])
|
||||||
|
require.NoError(t, b.parse(n), "ListenOut would fall back on this batch")
|
||||||
|
|
||||||
|
for i := range n {
|
||||||
|
d := next + i
|
||||||
|
h := &hdrs[i]
|
||||||
|
want := min(lens[d], MTU)
|
||||||
|
require.EqualValues(t, want, h.Datalen, "datagram %d Datalen", d)
|
||||||
|
require.Equal(t, wantNamelen, h.Namelen, "datagram %d Namelen", d)
|
||||||
|
require.Equal(t, senders[d], b.addrs[i], "datagram %d sender", d)
|
||||||
|
// Flags goes in as -1 and must come back 0. xnu's list receive (soreceive_m_list) never sets
|
||||||
|
// MSG_TRUNC, so a datagram past MTU arrives silently cut to MTU, as recvfrom through net.UDPConn does.
|
||||||
|
require.Zero(t, h.Flags, "datagram %d Flags", d)
|
||||||
|
require.Equal(t, bytes.Repeat([]byte{byte(d + 1)}, want), bufs[i][:want], "datagram %d payload", d)
|
||||||
|
if want < MTU {
|
||||||
|
require.Zero(t, bufs[i][want], "datagram %d wrote past Datalen", d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := n; i < nh; i++ {
|
||||||
|
require.EqualValues(t, uint64(sentinel), hdrs[i].Datalen, "spare header %d Datalen", i)
|
||||||
|
require.EqualValues(t, unix.SizeofSockaddrInet6, hdrs[i].Namelen, "spare header %d Namelen", i)
|
||||||
|
}
|
||||||
|
next += n
|
||||||
|
}
|
||||||
|
return largest
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once a listener has batches to reuse, reading one allocates nothing, on recvmsg_x or on the fallback.
|
||||||
|
func TestReadDoesNotAllocate(t *testing.T) {
|
||||||
|
for _, single := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("single=%v", single), func(t *testing.T) {
|
||||||
|
noRecvX.Store(single)
|
||||||
|
t.Cleanup(func() { noRecvX.Store(false) })
|
||||||
|
c, err := NewListener(slog.New(slog.DiscardHandler), Settings{Listen: netip.MustParseAddrPort("127.0.0.1:0")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = c.Close() })
|
||||||
|
u := c.(*StdConn)
|
||||||
|
la, err := c.LocalAddr()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tx, err := net.DialUDP("udp4", nil, net.UDPAddrFromAddrPort(la))
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = tx.Close() })
|
||||||
|
r := u.newBatchReader(u.UDPConn, u.rc)
|
||||||
|
pkt := []byte("steady")
|
||||||
|
allocs := testing.AllocsPerRun(100, func() {
|
||||||
|
if _, err := tx.Write(pkt); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, err := u.read(r)
|
||||||
|
if err != nil || b.n != 1 {
|
||||||
|
t.Fatalf("read %v: %v", b, err)
|
||||||
|
}
|
||||||
|
u.putBatch(b)
|
||||||
|
})
|
||||||
|
assert.Zero(t, allocs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user