mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-08 21:16:10 +09:00
Add net/msgx, which sends and receives multiple datagrams per system call on macOS using the kernel's recvmsg_x and sendmsg_x entry points, the Darwin counterparts of recvmmsg and sendmmsg. They are declared only in xnu's private socket_private.h, so msgx invokes them as raw system calls by number rather than through libSystem. That keeps their names out of the binary, returns errno reliably from the trap itself, and avoids a cgo transition per call. The calls need no privileges, but msgx.Available limits their use: never in a sandboxed App Store process (Tailscale's own GUI builds, detected by bundle ID, or a third-party app embedding tsnet, detected by the App Sandbox's APP_SANDBOX_CONTAINER_ID environment variable), and only after a loopback self-test with the running kernel passes, since Apple does not promise these numbers or layouts stay stable. Otherwise callers fall back to one datagram per call. Whether the self-test ran, whether it passed, and which step failed are exported as client metrics so a kernel change in the wild is visible. On top of it, add a Darwin batching.Conn: reads are batched on every socket (the main magicsock sockets included); writes are batched only on connected sockets, since sendmsg_x takes no per-message destination, which is what the per-peer connected sockets are for. connBind.BatchSize now comes from batching.MaxBatchSize rather than a GOOS switch. Measured between two M5 Pro Mac minis on a 10 Gbps LAN (iperf3 over Tailscale, 10 second runs, tailscaled CPU seconds sender/receiver), with per-packet utun I/O still in place: baseline 1.37 Gbps 29.3s / 22.1s connected sockets only 1.41 Gbps 24.8s / 15.6s connected sockets + msgx 1.44 Gbps 25.3s / 9.4s Throughput is bounded by the utun path (addressed separately); receiver CPU drops by more than half here. Three debug knobs (TS_DEBUG_MSGX, TS_DEBUG_MSGX_RX_MAIN, TS_DEBUG_MSGX_BATCHSIZE) remain for evaluation. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I4e9b7d2c8a1f5063b9e2d7c4a8f1e6b3d5c2a9f7
66 lines
2.9 KiB
Go
66 lines
2.9 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package msgx sends and receives multiple datagrams per system call on
|
|
// macOS using the kernel's recvmsg_x and sendmsg_x entry points, the Darwin
|
|
// counterparts of Linux's recvmmsg and sendmmsg.
|
|
//
|
|
// Those entry points are declared only in xnu's private socket_private.h and
|
|
// are not part of any public API, so this package makes them as raw system
|
|
// calls by number rather than through libSystem. That keeps their names out
|
|
// of the binary, gives a reliable errno (the trap's carry flag, rather than a
|
|
// separate __error() read that can race with other libc calls on the same
|
|
// thread), and avoids a cgo transition per call.
|
|
//
|
|
// [Available] reports whether this package may be used. The system calls
|
|
// need no special privileges, but their use is deliberately limited: never
|
|
// from a sandboxed App Store process (Apple's review process forbids
|
|
// non-public APIs there), whether Tailscale's own GUI builds or a third-party
|
|
// app embedding tsnet; and only after a loopback self-test exchanging real
|
|
// datagrams with the running kernel passes, since Apple does not promise that
|
|
// system call numbers or private structure layouts stay stable across
|
|
// releases. When Available reports false, every other function returns
|
|
// [ErrUnavailable] and callers must fall back to one datagram per call.
|
|
//
|
|
// The self-test's outcome is exported as client metrics (msgx_selftest_ran,
|
|
// msgx_selftest_passed, msgx_selftest_failure) so that a kernel change in
|
|
// the wild shows up rather than silently disabling batching.
|
|
//
|
|
// Both entry points work on any socket type the kernel treats as a datagram
|
|
// socket, including the utun kernel control socket, not only UDP.
|
|
package msgx
|
|
|
|
import (
|
|
"errors"
|
|
"net/netip"
|
|
)
|
|
|
|
// ErrUnavailable is returned by [Recv] and [Send] when [Available] is false.
|
|
var ErrUnavailable = errors.New("msgx: recvmsg_x/sendmsg_x unavailable")
|
|
|
|
// MaxBatch is the most messages [Recv] or [Send] handle per call. Callers may
|
|
// pass more; the excess is ignored by Recv and left for a later call by Send.
|
|
// The kernel itself clamps a call to 256 messages (its somaxrecvmsgx and
|
|
// somaxsendmsgx sysctls); 128 matches wireguard-go's ideal batch size.
|
|
const MaxBatch = 128
|
|
|
|
// Message is one received datagram.
|
|
type Message struct {
|
|
// Payload is the caller-provided buffer to receive into. A datagram
|
|
// larger than Payload is truncated; see Flags.
|
|
Payload []byte
|
|
|
|
// N is the number of bytes written into Payload.
|
|
N int
|
|
|
|
// Addr is the datagram's source address, if the socket reports one.
|
|
Addr netip.AddrPort
|
|
|
|
// Flags holds the kernel's per-message flags. The header documents
|
|
// syscall.MSG_TRUNC for a truncated datagram, but macOS 26 and later
|
|
// have been observed to clamp N to len(Payload) without setting it,
|
|
// so callers should size Payload above the largest datagram they
|
|
// expect rather than rely on this.
|
|
Flags int
|
|
}
|