all: use ~128KiB packet buf for batched i/o

This commit bumps the wireguard-go dependency to incorporate changes to
the packet memory model and the tun.Device.Read and conn.ReceiveFunc I/O
interfaces. It updates their implementations accordingly.

These changes improve throughput in all measured benchmarks and reduce
peak RSS in six of eight cases. The two regressions will be addressed in
a follow-up commit that reduces peak RSS below the baseline measured at
1e69418. That work is kept separate to simplify review.

The following throughput and peak RSS benchmarks were performed with
iperf3 between two Intel i5-12400 nodes running Ubuntu 24.04 (Linux 6.8).

The UDP benchmarks did not use UDP GSO on the sender, so they were
roughly equivalent to single packet I/O through wireguard-go.

TCP/1 signifies one TCP stream; TCP/128 signifies 128 parallel TCP
streams.

Throughput (Mb/s)
  Test     1e69418  After  Change
  TCP/1    10,371  11,354   +9.5%
  TCP/128   7,886   8,404   +6.6%
  UDP/1     2,111   2,853  +35.1%
  UDP/128   1,747   2,235  +28.0%

Peak memory (VmHWM, kB)
  Test     Side   1e69418    After   Change
  TCP/1    TX      98,240   52,596   -46.5%
           RX     287,748   73,384   -74.5%
  TCP/128  TX     101,196   52,812   -47.8%
           RX     290,420   63,620   -78.1%
  UDP/1    TX      58,864  160,840  +173.2%
           RX     137,516   49,900   -63.7%
  UDP/128  TX      66,148  116,096   +75.5%
           RX     154,384   56,556   -63.4%

Updates tailscale/corp#46716
Updates tailscale/corp#22467
Updates tailscale/corp#36989
Updates tailscale/corp#37878

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited authored and Jordan Whited committed 2026-09-04 09:32:55 -07:00
1 parent b62350fbec
commit 8fc6dca15e
32 files changed
+620 -611

No files matched your search

+1 -1
View File
@@ -792,7 +792,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
tailscale.com/metrics from tailscale.com/tsweb+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/resolvconffile from tailscale.com/cmd/k8s-operator+
+1 -1
View File
@@ -84,7 +84,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/logtail from tailscale.com/cmd/tailscaled+
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
+1 -1
View File
@@ -101,7 +101,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
tailscale.com/net/ace from tailscale.com/cmd/tailscale/cli
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
+1 -1
View File
@@ -181,7 +181,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
tailscale.com/metrics from tailscale.com/tsweb+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
+5 -5
View File
@@ -10,6 +10,7 @@ import (
"testing"
"time"
wgtun "github.com/tailscale/wireguard-go/tun"
"github.com/tailscale/wireguard-go/tun/tuntest"
"go4.org/netipx"
"tailscale.com/net/packet"
@@ -414,17 +415,16 @@ func TestHandlePacketFromWireGuard(t *testing.T) {
t.Errorf("unexpected packet dst: want %v, got %v", want, got)
}
if tt.expectedInjectedPkt != nil {
var buf [tstun.MaxPacketSize]byte
bufs := [][]byte{buf[:]}
sizes := []int{0}
n, err := tun.Read(bufs, sizes, 0)
slab := make([]byte, (2*wgtun.ReadPacketSpacing)+(2*(1<<16-1)))
packets := make([]wgtun.ReadPacket, 1)
n, err := tun.Read(slab, packets)
if err != nil {
t.Errorf("error reading injected packet: %v", err)
}
if n != 1 {
t.Errorf("expected to read 1 packet, got %d", n)
}
if want, got := tt.expectedInjectedPkt, buf[:sizes[0]]; !bytes.Equal(want, got) {
if want, got := tt.expectedInjectedPkt, slab[packets[0].Offset:packets[0].Offset+packets[0].Size]; !bytes.Equal(want, got) {
t.Errorf("unexpected contents of injected packet: want %+x, got %+x", want, got)
}
+12 -17
View File
@@ -103,6 +103,7 @@ const (
// handleTAPFrame handles receiving a raw TAP ethernet frame and reports whether
// it's been handled (that is, whether it should NOT be passed to wireguard).
// handleTAPFrame returns [consumePacket] (true) if len(ethBuf) < [ethernetFrameSize].
func (t *tapDevice) handleTAPFrame(ethBuf []byte) bool {
if len(ethBuf) < ethernetFrameSize {
@@ -419,28 +420,22 @@ func (t *tapDevice) Name() (string, error) {
return t.name, nil
}
// Read reads an IP packet from the TAP device. It strips the ethernet frame header.
func (t *tapDevice) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
n, err := t.ReadEthernet(buffs, sizes, offset)
if err != nil || n == 0 {
return n, err
}
// Strip the ethernet frame header.
copy(buffs[0][offset:], buffs[0][offset+ethernetFrameSize:offset+sizes[0]])
sizes[0] -= ethernetFrameSize
return 1, nil
}
// ReadEthernet reads a raw ethernet frame from the TAP device.
func (t *tapDevice) ReadEthernet(buffs [][]byte, sizes []int, offset int) (int, error) {
n, err := t.file.Read(buffs[0][offset:])
// Read implements [tun.Device.Read]. Read swallows frames that should not be
// passed to wireguard-go, as evaluated by [tapDevice.handleTAPFrame]. Read
// excludes the Ethernet header for returned IP packets described by packets[:n].
func (t *tapDevice) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
buf := slab[tun.ReadPacketSpacing : len(slab)-tun.ReadPacketSpacing]
n, err := t.file.Read(buf)
if err != nil {
return 0, err
}
if t.handleTAPFrame(buffs[0][offset : offset+n]) {
if t.handleTAPFrame(buf[:n]) {
return 0, nil
}
sizes[0] = n
packets[0] = tun.ReadPacket{
Offset: tun.ReadPacketSpacing + ethernetFrameSize,
Size: n - ethernetFrameSize,
}
return 1, nil
}
@@ -34,14 +34,14 @@ type fakeDevice struct {
name string
}
func (f *fakeDevice) File() *os.File { return nil }
func (f *fakeDevice) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) { return 0, nil }
func (f *fakeDevice) Write(bufs [][]byte, offset int) (int, error) { return 0, nil }
func (f *fakeDevice) MTU() (int, error) { return 0, nil }
func (f *fakeDevice) Name() (string, error) { return f.name, nil }
func (f *fakeDevice) Events() <-chan tun.Event { return nil }
func (f *fakeDevice) Close() error { return nil }
func (f *fakeDevice) BatchSize() int { return 0 }
func (f *fakeDevice) File() *os.File { return nil }
func (f *fakeDevice) Read(slab []byte, packets []tun.ReadPacket) (n int, err error) { return 0, nil }
func (f *fakeDevice) Write(bufs [][]byte, offset int) (int, error) { return 0, nil }
func (f *fakeDevice) MTU() (int, error) { return 0, nil }
func (f *fakeDevice) Name() (string, error) { return f.name, nil }
func (f *fakeDevice) Events() <-chan tun.Event { return nil }
func (f *fakeDevice) Close() error { return nil }
func (f *fakeDevice) BatchSize() int { return 0 }
func Test_poller(t *testing.T) {
getTXQDropsMetric().Set(0) // reset for test count > 1
+1 -1
View File
@@ -173,4 +173,4 @@
});
};
}
# nix-direnv cache busting line: sha256-w7VuEoXsnL7wmHbKlwd6qgUqyRj09JAqHw073Zgnidc=
# nix-direnv cache busting line: sha256-edmocquubPBqaakW8/TfQTLZS48HR7SOcWjYiLeJGLY=
+2 -2
View File
@@ -4,7 +4,7 @@
"sri": "sha256-+cTlDLusJthxTayVx6yGoqZHRN4ylvs/9Qpy4P2dlDI="
},
"vendor": {
"goModSum": "sha256-aYPi3BUkgTUZ3ESpK33W2CtHruPSgb7gMJlAP/tUL0I=",
"sri": "sha256-w7VuEoXsnL7wmHbKlwd6qgUqyRj09JAqHw073Zgnidc="
"goModSum": "sha256-PGvkYIOJdO2bTiy4Rsp+82idPQuvqqPW5XSFutoEaqQ=",
"sri": "sha256-edmocquubPBqaakW8/TfQTLZS48HR7SOcWjYiLeJGLY="
}
}
+1 -1
View File
@@ -126,7 +126,7 @@ require (
github.com/tailscale/ts-gokrazy v0.0.0-20260630224145-b83088f2e52e
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
github.com/tailscale/wireguard-go v0.0.0-20260904023712-e855235c55a2
github.com/tailscale/wireguard-go v0.0.0-20260904160635-24b5b6917431
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e
github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0
+2 -2
View File
@@ -1217,8 +1217,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20260904023712-e855235c55a2 h1:nRCnARSGqRvSAK9S+vbH87dMykfv2vWwx6kb8rw9Ayk=
github.com/tailscale/wireguard-go v0.0.0-20260904023712-e855235c55a2/go.mod h1:rUelGmuK4UnSJYM5gl5Mknp6YbwwcL8+VAPMhNYe+jg=
github.com/tailscale/wireguard-go v0.0.0-20260904160635-24b5b6917431 h1:opfY1yfV/mJzmbxJZw/cXLkmNwp4qjRZ51tC6RG3VPI=
github.com/tailscale/wireguard-go v0.0.0-20260904160635-24b5b6917431/go.mod h1:rUelGmuK4UnSJYM5gl5Mknp6YbwwcL8+VAPMhNYe+jg=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
+33 -23
View File
@@ -9,33 +9,41 @@ import (
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/envknob"
"tailscale.com/net/packet"
"tailscale.com/types/nettype"
)
// BatchSizeFromEnv returns ideal, unless the TS_DEBUG_WG_BATCH_SIZE
// environment variable is set to a positive integer, in which case it returns
// that value clamped to [1, ideal].
//
// Batch size determines how much packet memory wireguard-go pins per reader
// goroutine (batch size × 64 KiB message buffers per reader, ~32 MiB total at
// the Linux default of 128), so memory-budget tests set the env var to 1 to
// approximate the configuration used on memory-constrained (mobile)
// platforms, where batch size is always 1.
func BatchSizeFromEnv(ideal int) int {
if v, ok := envknob.LookupInt("TS_DEBUG_WG_BATCH_SIZE"); ok && v > 0 {
return min(v, ideal)
}
return ideal
}
var (
// This acts as a compile-time check for our usage of ipv6.Message in
// [Conn] for both IPv6 and IPv4 operations.
_ ipv6.Message = ipv4.Message{}
)
// ReceivedPacket describes a packet read by [Conn.ReadBatch].
type ReceivedPacket struct {
// Offset is the starting byte offset into the slab supplied to [Conn.ReadBatch].
Offset int
// Size is the size of the packet.
Size int
// Source is the source address that sent the packet.
Source netip.AddrPort
}
const (
// ReadSlabMultiple is the minimum slab length accepted by [Conn.ReadBatch],
// and is the suggested multiple when passing a larger value.
ReadSlabMultiple = 1<<16 - 1
// MinimumReadBatchSize is the minimum number of packets descriptors accepted
// by [Conn.ReadBatch].
MinimumReadBatchSize = udpGROCountMax
// MaximumWriteBatchSize is the maximum number of buffs accepted by
// [Conn.WriteBatchTo].
MaximumWriteBatchSize = 128
// udpGROCountMax is the maximum number of datagrams the kernel will coalesce
// together to present in a single recvmmsg() slot.
udpGROCountMax = 64
)
// Conn is a [nettype.PacketConn] that provides batched i/o using
// platform-specific optimizations, e.g. {recv,send}mmsg & UDP GSO/GRO.
//
@@ -54,19 +62,21 @@ type Conn interface {
//
// TODO: consider detaching Conn from [nettype.PacketConn]
ReadFromUDPAddrPort([]byte) (int, netip.AddrPort, error)
// ReadBatch reads messages from [Conn] into msgs. It returns the number of
// messages the caller should evaluate for nonzero len, as a zero len
// message may fall on either side of a nonzero.
// ReadBatch reads datagrams from [Conn] into slab, and describes them in
// packets. It returns the number of populated packet descriptors. A single
// GRO-coalesced datagram may produce multiple descriptors.
//
// Each [ipv6.Message.OOB] must be sized to at least MinControlMessageSize().
ReadBatch(msgs []ipv6.Message, flags int) (n int, err error)
// packets must have a length >= [MinimumReadBatchSize]. slab must have a
// length >= [ReadSlabMultiple]. ReadBatch reads only as many datagrams as
// both arguments can accommodate.
ReadBatch(slab []byte, packets []ReceivedPacket) (n int, err error)
// WriteBatchTo writes buffs to addr.
//
// If geneve.VNI.IsSet(), then geneve is encoded into the space preceding
// offset, and offset must equal [packet.GeneveFixedHeaderLength]. If
// !geneve.VNI.IsSet() then the space preceding offset is ignored.
//
// len(buffs) must be <= batchSize supplied in TryUpgradeToConn().
// len(buffs) must be <= [MaximumWriteBatchSize].
//
// WriteBatchTo may return a [neterror.ErrUDPGSODisabled] error if UDP GSO
// was disabled as a result of a send error.
+1 -9
View File
@@ -11,14 +11,6 @@ import (
)
// TryUpgradeToConn is no-op on all platforms except linux.
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ int, _ string, _ *controlknobs.Knobs) nettype.PacketConn {
func TryUpgradeToConn(pconn nettype.PacketConn, _ string, _ string, _ *controlknobs.Knobs) nettype.PacketConn {
return pconn
}
var controlMessageSize = 0
func MinControlMessageSize() int {
return controlMessageSize
}
const IdealBatchSize = 1
+126 -99
View File
@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"runtime"
@@ -58,7 +59,7 @@ type linuxBatchingConn struct {
xpc xnetBatchReaderWriter
rxOffload bool // supports UDP GRO or similar
txOffload atomic.Bool // supports UDP GSO or similar
sendBatchPool sync.Pool
msgsPool sync.Pool
rxqOverflowsMetric *clientmetric.Metric
// neverGSOEqualTail, when non-nil and true, enables a sentinel-tail
// workaround in the UDP GSO TX path. It points at a
@@ -231,24 +232,31 @@ func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, geneve packet.Ge
return base + 1
}
type sendBatch struct {
msgs []ipv6.Message
ua *net.UDPAddr
// msgsBatch contains recyclable (via [linuxBatchingConn.msgsPool] elements
// used across [linuxBatchingConn.ReadBatch] & [linuxBatchingConn.WriteBatchTo].
type msgsBatch struct {
msgs []ipv6.Message
writeBatchToUDPAddr *net.UDPAddr
}
func (c *linuxBatchingConn) getSendBatch() *sendBatch {
batch := c.sendBatchPool.Get().(*sendBatch)
func (c *linuxBatchingConn) getMsgsBatch() *msgsBatch {
batch := c.msgsPool.Get().(*msgsBatch)
return batch
}
func (c *linuxBatchingConn) putSendBatch(batch *sendBatch) {
for i := range batch.msgs {
// Non coalesced write paths access only batch.msgs[i].Buffers[0],
// putMsgsBatch resets each message in batch.msgs[:usedMsgs] for reuse, then
// returns batch to its [sync.Pool].
func (c *linuxBatchingConn) putMsgsBatch(batch *msgsBatch, usedMsgs int) {
// Clear references to previously used packet buffers, otherwise they
// may never be GC'd.
for i := range batch.msgs[:usedMsgs] {
clear((batch.msgs)[i].Buffers)
// Non-coalesced write paths access only batch.msgs[i].Buffers[0],
// but we append more during [linuxBatchingConn.coalesceMessages].
// Leave index zero accessible:
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers[:1], OOB: batch.msgs[i].OOB}
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers[:1], OOB: batch.msgs[i].OOB[:0]}
}
c.sendBatchPool.Put(batch)
c.msgsPool.Put(batch)
}
// appendSentinelTailBatchSizeThreshold represents the minimum batch size
@@ -273,18 +281,19 @@ func (c *linuxBatchingConn) putSendBatch(batch *sendBatch) {
const appendSentinelTailBatchSizeThreshold = 8
func (c *linuxBatchingConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort, geneve packet.GeneveHeader, offset int) error {
batch := c.getSendBatch()
defer c.putSendBatch(batch)
batch := c.getMsgsBatch()
usedMsgs := len(buffs)
defer c.putMsgsBatch(batch, usedMsgs)
if addr.Addr().Is6() {
as16 := addr.Addr().As16()
copy(batch.ua.IP, as16[:])
batch.ua.IP = batch.ua.IP[:16]
copy(batch.writeBatchToUDPAddr.IP, as16[:])
batch.writeBatchToUDPAddr.IP = batch.writeBatchToUDPAddr.IP[:16]
} else {
as4 := addr.Addr().As4()
copy(batch.ua.IP, as4[:])
batch.ua.IP = batch.ua.IP[:4]
copy(batch.writeBatchToUDPAddr.IP, as4[:])
batch.writeBatchToUDPAddr.IP = batch.writeBatchToUDPAddr.IP[:4]
}
batch.ua.Port = int(addr.Port())
batch.writeBatchToUDPAddr.Port = int(addr.Port())
// Load the control knob once per write so a single call sees a consistent
// value even if the knob flips concurrently.
neverGSOEqualTail := c.neverGSOEqualTail != nil && c.neverGSOEqualTail.Load()
@@ -294,7 +303,7 @@ func (c *linuxBatchingConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort, ge
)
retry:
if c.txOffload.Load() && (!neverGSOEqualTail || len(buffs) >= appendSentinelTailBatchSizeThreshold) {
n = c.coalesceMessages(batch.ua, geneve, buffs, batch.msgs, offset, neverGSOEqualTail)
n = c.coalesceMessages(batch.writeBatchToUDPAddr, geneve, buffs, batch.msgs, offset, neverGSOEqualTail)
} else {
mutableOffset := offset // don't mutate offset across retries
vniIsSet := geneve.VNI.IsSet()
@@ -310,7 +319,7 @@ retry:
// coalesceMessages during a first pass, and landed here as part of
// goto retry.
batch.msgs[i].Buffers = batch.msgs[i].Buffers[:1]
batch.msgs[i].Addr = batch.ua
batch.msgs[i].Addr = batch.writeBatchToUDPAddr
batch.msgs[i].OOB = batch.msgs[i].OOB[:0]
}
n = len(buffs)
@@ -347,55 +356,6 @@ func (c *linuxBatchingConn) writeBatch(msgs []ipv6.Message) error {
}
}
// splitCoalescedMessages splits coalesced messages from the tail of dst
// beginning at index 'firstMsgAt' into the head of the same slice. It reports
// the number of elements to evaluate in msgs for nonzero len (msgs[i].N). An
// error is returned if a socket control message cannot be parsed or a split
// operation would overflow msgs.
func (c *linuxBatchingConn) splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int) (n int, err error) {
for i := firstMsgAt; i < len(msgs); i++ {
msg := &msgs[i]
if msg.N == 0 {
return n, err
}
var (
gsoSize int
start int
end = msg.N
numToSplit = 1
)
gsoSize, err = getGSOSizeFromControl(msg.OOB[:msg.NN])
if err != nil {
return n, err
}
if gsoSize > 0 {
numToSplit = (msg.N + gsoSize - 1) / gsoSize
end = gsoSize
}
for j := 0; j < numToSplit; j++ {
if n > i {
return n, errors.New("splitting coalesced packet resulted in overflow")
}
copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end])
msgs[n].N = copied
msgs[n].Addr = msg.Addr
start = end
end += gsoSize
if end > msg.N {
end = msg.N
}
n++
}
if i != n-1 {
// It is legal for bytes to move within msg.Buffers[0] as a result
// of splitting, so we only zero the source msg len when it is not
// the destination of the last split operation above.
msg.N = 0
}
}
return n, nil
}
// getDataFromControl returns the data portion of the first control msg with
// matching cmsgLevel, matching cmsgType, and min data len of minDataLen, in
// control. If no matching cmsg is found or the len(control) < unix.SizeofCmsghdr,
@@ -439,11 +399,11 @@ func getRXQOverflowsFromControl(control []byte) (uint32, error) {
// handleRXQOverflowCounter handles any rx queue overflow counter contained in
// the tail of msgs.
func (c *linuxBatchingConn) handleRXQOverflowCounter(msgs []ipv6.Message, n int, rxErr error) {
if n == 0 || rxErr != nil || c.rxqOverflowsMetric == nil {
func (c *linuxBatchingConn) handleRXQOverflowCounter(msgs []ipv6.Message, rxErr error) {
if len(msgs) == 0 || rxErr != nil || c.rxqOverflowsMetric == nil {
return
}
tailMsg := msgs[n-1] // we only care about the latest value as it's a cumulative counter
tailMsg := msgs[len(msgs)-1] // we only care about the latest value as it's a cumulative counter
if tailMsg.NN == 0 {
return
}
@@ -462,22 +422,98 @@ func (c *linuxBatchingConn) handleRXQOverflowCounter(msgs []ipv6.Message, n int,
c.rxqOverflows = rxqOverflows
}
func (c *linuxBatchingConn) ReadBatch(msgs []ipv6.Message, flags int) (n int, err error) {
const (
// groEnabledSlotSize is the size to supply per recvmmsg() slot when GRO is
// enabled. If we supply less than this, coalesced datagrams may be silently
// truncated by the kernel.
groEnabledSlotSize = 1<<16 - 1
// groDisabledSlotSize is the size to supply per recvmmsg() slot when GRO is
// disabled. It's ~twice the size of max Ethernet MTU, but smaller than
// [ReadSlabMultiple]. Without GRO we might as well read more datagrams per
// syscall, and it's reasonable to expect datagrams to be no larger than this
// size.
//
// TODO: Reconsider divisor. Older kernels without UDP GRO support may
// benefit from more slots per recvmmsg syscall. Run benchmarks to inform.
groDisabledSlotSize = groEnabledSlotSize / 4
)
func (c *linuxBatchingConn) ReadBatch(slab []byte, packets []ReceivedPacket) (n int, err error) {
if len(slab) < ReadSlabMultiple {
return 0, fmt.Errorf("len(slab): %d < ReadSlabMultiple(%d)", len(slab), ReadSlabMultiple)
}
if len(packets) < MinimumReadBatchSize {
return 0, fmt.Errorf("len(packets): %d < MinimumReadBatchSize(%d)", len(packets), MinimumReadBatchSize)
}
c.readOpMu.Lock()
defer c.readOpMu.Unlock()
if !c.rxOffload || len(msgs) < 2 {
n, err = c.xpc.ReadBatch(msgs, flags)
c.handleRXQOverflowCounter(msgs, n, err)
return n, err
batch := c.getMsgsBatch()
recvmmsgSlotSize := groEnabledSlotSize
maxMsgsPerRecvmmsgSlot := udpGROCountMax
if !c.rxOffload {
recvmmsgSlotSize = groDisabledSlotSize
maxMsgsPerRecvmmsgSlot = 1
}
// Read into the tail of msgs, split into the head.
readAt := len(msgs) - 2
n, err = c.xpc.ReadBatch(msgs[readAt:], 0)
if err != nil || n == 0 {
maxMsgCount := min(
len(slab)/recvmmsgSlotSize, // how many slots fit in slab
len(packets)/maxMsgsPerRecvmmsgSlot, // how many datagrams fit in packets
len(batch.msgs), // how many [ipv6.Message] we have
)
defer c.putMsgsBatch(batch, maxMsgCount)
rem := slab
for i := range maxMsgCount {
end := recvmmsgSlotSize
if len(rem) < recvmmsgSlotSize {
end = len(rem)
}
batch.msgs[i].Buffers[0] = rem[:end]
batch.msgs[i].OOB = batch.msgs[i].OOB[:cap(batch.msgs[i].OOB)]
rem = rem[end:]
}
n, err = c.xpc.ReadBatch(batch.msgs[:maxMsgCount], 0)
if err != nil {
return 0, err
}
c.handleRXQOverflowCounter(msgs[readAt:], n, err)
return c.splitCoalescedMessages(msgs, readAt)
c.handleRXQOverflowCounter(batch.msgs[:n], err)
return fillReceivedPackets(batch.msgs[:n], recvmmsgSlotSize, packets, c.rxOffload)
}
func fillReceivedPackets(msgs []ipv6.Message, slabOffset int, packets []ReceivedPacket, rxOffload bool) (n int, err error) {
for i, msg := range msgs {
var (
gsoSize int
start int
numToSplit = 1
)
if rxOffload {
gsoSize, err = getGSOSizeFromControl(msg.OOB[:msg.NN])
if err != nil {
return n, err
}
if gsoSize > 0 {
numToSplit = (msg.N + gsoSize - 1) / gsoSize
}
}
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
regionOffset := i * slabOffset // region may contain multiple coalesced packets
for j := 0; j < numToSplit; j++ {
if n >= len(packets) {
return n, fmt.Errorf("%w: filling received packet metadata resulted in overflow", io.ErrShortBuffer)
}
end := msg.N
if gsoSize > 0 && start+gsoSize < end {
end = start + gsoSize
}
packets[n] = ReceivedPacket{
Offset: regionOffset + start,
Size: end - start,
Source: addrPort,
}
n++
start = end
}
}
return n, nil
}
func (c *linuxBatchingConn) LocalAddr() net.Addr {
@@ -603,12 +639,11 @@ func getRXQOverflowsMetric(name string) *clientmetric.Metric {
}
// TryUpgradeToConn probes the capabilities of the OS and pconn, and upgrades
// pconn to a [Conn] if appropriate. A batch size of [IdealBatchSize] is
// suggested for the best performance. If len(rxqOverflowsMetricName) is
// pconn to a [Conn] if appropriate. If len(rxqOverflowsMetricName) is
// nonzero, then read ops will propagate the SO_RXQ_OVFL control message counter
// to a clientmetric with the supplied name. If knobs is non-nil, UDP GSO
// and/or UDP GRO may be disabled via control-plane node attributes.
func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int, rxqOverflowsMetricName string, knobs *controlknobs.Knobs) nettype.PacketConn {
func TryUpgradeToConn(pconn nettype.PacketConn, network string, rxqOverflowsMetricName string, knobs *controlknobs.Knobs) nettype.PacketConn {
if runtime.GOOS != "linux" {
// Exclude Android.
return pconn
@@ -632,20 +667,20 @@ func TryUpgradeToConn(pconn nettype.PacketConn, network string, batchSize int, r
}
b := &linuxBatchingConn{
pc: uc,
sendBatchPool: sync.Pool{
msgsPool: sync.Pool{
New: func() any {
ua := &net.UDPAddr{
IP: make([]byte, 16),
}
msgs := make([]ipv6.Message, batchSize)
msgs := make([]ipv6.Message, MaximumWriteBatchSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{
ua: ua,
msgs: msgs,
return &msgsBatch{
writeBatchToUDPAddr: ua,
msgs: msgs,
}
},
},
@@ -682,11 +717,3 @@ func init() {
unix.CmsgSpace(2) + // UDP_GRO or UDP_SEGMENT gsoSize (uint16)
unix.CmsgSpace(4) // SO_RXQ_OVFL counter (uint32)
}
// MinControlMessageSize returns the minimum control message size required to
// support read batching via [Conn.ReadBatch].
func MinControlMessageSize() int {
return controlMessageSize
}
const IdealBatchSize = 128
+94 -62
View File
@@ -24,16 +24,18 @@ import (
"tailscale.com/net/packet"
)
func Test_linuxBatchingConn_splitCoalescedMessages(t *testing.T) {
c := &linuxBatchingConn{}
func Test_fillReceivedPackets(t *testing.T) {
const maxDatagramSize = 1<<16 - 1
source := netip.MustParseAddrPort("192.0.2.1:1234")
newMsg := func(n int, gso uint16) ipv6.Message {
msg := ipv6.Message{
Buffers: [][]byte{make([]byte, 1024)},
Buffers: [][]byte{make([]byte, maxDatagramSize)},
N: n,
OOB: gsoControl(gso),
Addr: net.UDPAddrFromAddrPort(source),
}
if gso > 0 {
msg.OOB = gsoControl(gso)
msg.NN = len(msg.OOB)
}
return msg
@@ -42,103 +44,133 @@ func Test_linuxBatchingConn_splitCoalescedMessages(t *testing.T) {
cases := []struct {
name string
msgs []ipv6.Message
firstMsgAt int
wantNumEval int
wantMsgLens []int
wantPackets [][2]int // {offset, size}
wantErr bool
}{
{
name: "second-last-split-last-empty",
name: "first_split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(3, 1),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 3,
wantMsgLens: []int{1, 1, 1, 0},
wantErr: false,
wantPackets: [][2]int{
{0, 1},
{1, 1},
{2, 1},
},
wantErr: false,
},
{
name: "second-last-no-split-last-empty",
name: "first_no_split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(0, 0),
},
firstMsgAt: 2,
wantNumEval: 1,
wantMsgLens: []int{1, 0, 0, 0},
wantErr: false,
wantPackets: [][2]int{
{0, 1},
},
wantErr: false,
},
{
name: "second-last-no-split-last-no-split",
name: "first_no_split_last_no_split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(1, 0),
},
firstMsgAt: 2,
wantNumEval: 2,
wantMsgLens: []int{1, 1, 0, 0},
wantErr: false,
wantPackets: [][2]int{
{0, 1},
{maxDatagramSize, 1},
},
wantErr: false,
},
{
name: "second-last-no-split-last-split",
name: "first_no_split_last_split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(3, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
wantPackets: [][2]int{
{0, 1},
{maxDatagramSize, 1},
{maxDatagramSize + 1, 1},
{maxDatagramSize + 2, 1},
},
wantErr: false,
},
{
name: "second-last-split-last-split",
name: "first_split_last_split",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(2, 1),
newMsg(2, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: false,
wantPackets: [][2]int{
{0, 1},
{1, 1},
{maxDatagramSize, 1},
{maxDatagramSize + 1, 1},
},
wantErr: false,
},
{
name: "second-last-no-split-last-split-overflow",
name: "first_no_split_last_split_overflow",
msgs: []ipv6.Message{
newMsg(0, 0),
newMsg(0, 0),
newMsg(1, 0),
newMsg(4, 1),
},
firstMsgAt: 2,
wantNumEval: 4,
wantMsgLens: []int{1, 1, 1, 1},
wantErr: true,
wantPackets: [][2]int{
{0, 1},
{maxDatagramSize, 1},
{maxDatagramSize + 1, 1},
{maxDatagramSize + 2, 1},
},
wantErr: true,
},
{
name: "split_with_short_tail",
msgs: []ipv6.Message{
newMsg(5, 2),
},
wantNumEval: 3,
wantPackets: [][2]int{
{0, 2},
{2, 2},
{4, 1},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got, err := c.splitCoalescedMessages(tt.msgs, 2)
if err != nil && !tt.wantErr {
t.Fatalf("err: %v", err)
packets := make([]ReceivedPacket, len(tt.wantPackets))
got, err := fillReceivedPackets(
tt.msgs,
maxDatagramSize,
packets,
true,
)
if (err != nil) != tt.wantErr {
t.Fatalf("err: %v, wantErr: %v", err, tt.wantErr)
}
if got != tt.wantNumEval {
t.Fatalf("got to eval: %d want: %d", got, tt.wantNumEval)
}
for i, msg := range tt.msgs {
if msg.N != tt.wantMsgLens[i] {
t.Fatalf("msg[%d].N: %d want: %d", i, msg.N, tt.wantMsgLens[i])
if len(packets) != len(tt.wantPackets) {
t.Fatalf("got %d packets, want %d", len(packets), len(tt.wantPackets))
}
for i, want := range tt.wantPackets {
got := packets[i]
if got.Offset != want[0] ||
got.Size != want[1] ||
got.Source != source {
t.Errorf(
"packets[%d] = {Offset: %d, Size: %d, Source: %v}, want {Offset: %d, Size: %d, Source: %v}",
i, got.Offset, got.Size, got.Source, want[0], want[1], source,
)
}
}
})
@@ -466,7 +498,7 @@ func Test_linuxBatchingConn_WriteBatchTo_resetsBuffersOnGSORetry(t *testing.T) {
c := &linuxBatchingConn{
pc: uc,
xpc: xpc,
sendBatchPool: sync.Pool{New: func() any {
msgsPool: sync.Pool{New: func() any {
ua := &net.UDPAddr{IP: make([]byte, 16)}
msgs := make([]ipv6.Message, 8)
for i := range msgs {
@@ -474,7 +506,7 @@ func Test_linuxBatchingConn_WriteBatchTo_resetsBuffersOnGSORetry(t *testing.T) {
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{ua: ua, msgs: msgs}
return &msgsBatch{writeBatchToUDPAddr: ua, msgs: msgs}
}},
}
c.txOffload.Store(true) // force the coalesce path on the first pass
@@ -529,7 +561,7 @@ func Test_linuxBatchingConn_WriteBatchTo_offsetStableOnNonCoalesceRetry(t *testi
c := &linuxBatchingConn{
pc: uc,
xpc: xpc,
sendBatchPool: sync.Pool{New: func() any {
msgsPool: sync.Pool{New: func() any {
ua := &net.UDPAddr{IP: make([]byte, 16)}
msgs := make([]ipv6.Message, appendSentinelTailBatchSizeThreshold)
for i := range msgs {
@@ -537,7 +569,7 @@ func Test_linuxBatchingConn_WriteBatchTo_offsetStableOnNonCoalesceRetry(t *testi
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{ua: ua, msgs: msgs}
return &msgsBatch{writeBatchToUDPAddr: ua, msgs: msgs}
}},
}
c.txOffload.Store(true)
@@ -583,8 +615,8 @@ func TestMinReadBatchMsgsLen(t *testing.T) {
// So long as magicsock uses [Conn], and [wireguard-go/conn.Bind] API is
// shaped for wireguard-go to control packet memory, these values should be
// aligned.
if IdealBatchSize != conn.IdealBatchSize {
t.Fatalf("IdealBatchSize: %d != conn.IdealBatchSize(): %d", IdealBatchSize, conn.IdealBatchSize)
if MaximumWriteBatchSize != conn.IdealBatchSize {
t.Fatalf("MaximumWriteBatchSize: %d != conn.IdealBatchSize(): %d", MaximumWriteBatchSize, conn.IdealBatchSize)
}
}
@@ -741,12 +773,12 @@ func Test_linuxBatchingConn_handleRXQOverflowCounter(t *testing.T) {
}
conn.rxqOverflowsMetric.Set(0) // test count > 1 will accumulate, reset
// n == 0
conn.handleRXQOverflowCounter([]ipv6.Message{{}}, 0, nil)
// len(msgs) == 0
conn.handleRXQOverflowCounter([]ipv6.Message{}, nil)
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(0))
// rxErr non-nil
conn.handleRXQOverflowCounter([]ipv6.Message{{}}, 0, io.EOF)
conn.handleRXQOverflowCounter([]ipv6.Message{{}}, io.EOF)
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(0))
// nonzero counter
@@ -754,14 +786,14 @@ func Test_linuxBatchingConn_handleRXQOverflowCounter(t *testing.T) {
conn.handleRXQOverflowCounter([]ipv6.Message{{
OOB: control,
NN: len(control),
}}, 1, nil)
}}, nil)
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
// nonzero counter, no change
conn.handleRXQOverflowCounter([]ipv6.Message{{
OOB: control,
NN: len(control),
}}, 1, nil)
}}, nil)
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1))
// counter rollover
@@ -769,6 +801,6 @@ func Test_linuxBatchingConn_handleRXQOverflowCounter(t *testing.T) {
conn.handleRXQOverflowCounter([]ipv6.Message{{
OOB: control,
NN: len(control),
}}, 1, nil)
}}, nil)
c.Assert(conn.rxqOverflowsMetric.Value(), qt.Equals, int64(1+math.MaxUint32))
}
+1 -1
View File
@@ -33,7 +33,7 @@ func (t *fakeTUN) Close() error {
return nil
}
func (t *fakeTUN) Read(out [][]byte, sizes []int, offset int) (int, error) {
func (t *fakeTUN) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
<-t.closechan
return 0, io.EOF
}
+127 -116
View File
@@ -24,7 +24,6 @@ import (
"tailscale.com/disco"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/batching"
"tailscale.com/net/packet"
"tailscale.com/net/packet/checksum"
"tailscale.com/net/routemanager"
@@ -44,12 +43,16 @@ import (
"tailscale.com/wgengine/wgcfg"
)
const maxBufferSize = device.MaxMessageSize
// PacketStartOffset is the minimal amount of leading space that must exist
// before &packet[offset] in a packet passed to Read, Write, or InjectInboundDirect.
// This is necessary to avoid reallocation in wireguard-go internals.
const PacketStartOffset = device.MessageTransportHeaderSize
// WritePacketStartOffset is the minimal amount of leading space that must exist
// before &packet[offset] in a packet passed to [tun.Device.Write], which includes
// "injected" packet code paths. This leading space is required by [tun.Device]
// implementations.
//
// TODO(jwhited): consider collapsing with and renaming [tun.ReadPacketSpacing]
// for simplicity's sake. Exact math between [tun.Device] implementation and
// direction varies, but we can just pick the maximum value to reduce cognitive
// burden.
const WritePacketStartOffset = device.MessageTransportHeaderSize
// MaxPacketSize is the maximum size (in bytes)
// of a packet that can be injected into a tstun.Wrapper.
@@ -68,7 +71,7 @@ var (
var (
errPacketTooBig = errors.New("packet too big")
errOffsetTooBig = errors.New("offset larger than buffer length")
errOffsetTooSmall = errors.New("offset smaller than PacketStartOffset")
errOffsetTooSmall = errors.New("offset smaller than WritePacketStartOffset")
)
// parsedPacketPool holds a pool of Parsed structs for use in filtering.
@@ -119,17 +122,17 @@ type Wrapper struct {
// peerConfig stores the current NAT configuration.
peerConfig atomic.Pointer[peerConfigTable]
// vectorBuffer stores the oldest unconsumed packet vector from tdev. It is
// allocated in wrap() and the underlying arrays should never grow.
vectorBuffer [][]byte
// startPollingOnce is used to start a [Wrapper.pollVector] goroutine at the
// first call to [Wrapper.Read].
startPollingOnce sync.Once
// bufferConsumedMu protects bufferConsumed from concurrent sends, closes,
// and send-after-close (by way of bufferConsumedClosed).
bufferConsumedMu sync.Mutex
// bufferConsumedClosed is true when bufferConsumed has been closed. This is
// read by bufferConsumed writers to prevent send-after-close.
bufferConsumedClosed bool
// bufferConsumed synchronizes access to vectorBuffer (shared by Read() and
// pollVector()).
// bufferConsumed synchronizes access to packet bufs and descriptors shared
// by [Wrapper.Read] and [Wrapper.pollVector].
//
// Close closes bufferConsumed and sets bufferConsumedClosed to true.
bufferConsumed chan struct{}
@@ -253,17 +256,25 @@ type tunInjectedRead struct {
data []byte
}
// tunVectorReadResult is the result of a tun.Read(), or an injected packet
// pretending to be a tun.Read().
// tunVectorReadResult is the result of a [tun.Device.Read], or an injected
// packet pretending to be a [tun.Device.Read].
type tunVectorReadResult struct {
// When err AND data are nil, injected will be set with meaningful data
// (injected packet). If either err OR data is non-nil, injected should be
// ignored (a "real" tun.Read).
err error
data [][]byte
injected tunInjectedRead
// isInjected indicates if tunVectorReadResult contains a "real" [tun.Device.Read]
// result, or an injected packet. When true, injected will be set with meaningful
// data, otherwise real will be set with meaningful data.
isInjected bool
// Result consumer ([Wrapper.Read]) must call a non-nil doneHandlingFn when
// they are done with the result. The consumer must not access [tunVectorReadResult]
// fields once this func has been called, as it provides synchronization
// around shared memory.
doneHandlingFn func()
dataOffset int
real struct {
err error
slab []byte
packets []tun.ReadPacket
}
injected tunInjectedRead
}
// Start unblocks any Wrapper.Read calls that have already started
@@ -318,12 +329,6 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry,
w.eventClient = bus.Client("net.tstun")
w.discoKeyAdvertisementPub = eventbus.Publish[events.DiscoKeyAdvertisement](w.eventClient)
w.vectorBuffer = make([][]byte, tdev.BatchSize())
for i := range w.vectorBuffer {
w.vectorBuffer[i] = make([]byte, maxBufferSize)
}
go w.pollVector()
go w.pumpEvents()
// The buffer starts out consumed.
w.bufferConsumed <- struct{}{}
@@ -461,52 +466,45 @@ func (t *Wrapper) Name() (string, error) {
return t.tdev.Name()
}
const ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype
// pollVector polls t.tdev.Read(), placing the oldest unconsumed packet vector
// into t.vectorBuffer. This is needed because t.tdev.Read() in general may
// block (it does on Windows), so packets may be stuck in t.vectorOutbound if
// t.Read() called t.tdev.Read() directly.
func (t *Wrapper) pollVector() {
sizes := make([]int, len(t.vectorBuffer))
readOffset := PacketStartOffset
reader := t.tdev.Read
if t.isTAP {
type tapReader interface {
ReadEthernet(buffs [][]byte, sizes []int, offset int) (int, error)
}
if r, ok := t.tdev.(tapReader); ok {
readOffset = PacketStartOffset - ethernetFrameSize
reader = r.ReadEthernet
}
}
// pollVector polls [Wrapper.tdev.Read], writing the oldest unconsumed packet
// slab and packet descriptors into the [Wrapper.vectorOutbound] channel.
// slabLen and packetsLen should originate from the first call to [Wrapper.Read],
// and are used for sizing the equivalent arguments pollVector passes to
// [Wrapper.tdev.Read].
//
// [Wrapper.tdev.Read] can block, so we poll tdev in a goroutine independent of
// wireguard-go's calls to [Wrapper.Read], in order to support native tdev reads
// alongside packets we inject.
//
// pollVector returns when [t.bufferConsumed] is closed, or when [Wrapper.isClosed]
// returns true.
func (t *Wrapper) pollVector(slabLen, packetsLen int) {
slab := make([]byte, slabLen)
packets := make([]tun.ReadPacket, packetsLen)
for range t.bufferConsumed {
for i := range t.vectorBuffer {
t.vectorBuffer[i] = t.vectorBuffer[i][:cap(t.vectorBuffer[i])]
}
var n int
var err error
for n == 0 && err == nil {
if t.isClosed() {
return
}
n, err = reader(t.vectorBuffer[:], sizes, readOffset)
n, err = t.tdev.Read(slab, packets)
if t.isTAP && TAPDebug {
s := fmt.Sprintf("% x", t.vectorBuffer[0][:])
s := fmt.Sprintf("% x", slab)
for strings.HasSuffix(s, " 00") {
s = strings.TrimSuffix(s, " 00")
}
t.logf("TAP read %v, %v: %s", n, err, s)
}
}
for i := range sizes[:n] {
t.vectorBuffer[i] = t.vectorBuffer[i][:readOffset+sizes[i]]
}
t.sendVectorOutbound(tunVectorReadResult{
data: t.vectorBuffer[:n],
dataOffset: PacketStartOffset,
err: err,
isInjected: false,
doneHandlingFn: t.sendBufferConsumed,
real: struct {
err error
slab []byte
packets []tun.ReadPacket
}{err: err, slab: slab, packets: packets[:n]},
})
}
}
@@ -529,7 +527,7 @@ func (t *Wrapper) injectOutbound(r tunInjectedRead) {
return
}
select {
case t.vectorOutbound <- tunVectorReadResult{injected: r}:
case t.vectorOutbound <- tunVectorReadResult{injected: r, isInjected: true}:
case <-t.closed:
}
}
@@ -548,6 +546,7 @@ func (t *Wrapper) sendVectorOutbound(r tunVectorReadResult) {
}
// snat does SNAT on p if the destination address requires a different source address.
// snat never mutates packet length or IP address family.
func (pc *peerConfigTable) snat(p *packet.Parsed) {
oldSrc := p.Src.Addr()
newSrc := pc.selectSrcIP(oldSrc, p.Dst.Addr())
@@ -556,7 +555,8 @@ func (pc *peerConfigTable) snat(p *packet.Parsed) {
}
}
// dnat does destination NAT on p.
// dnat does destination NAT on p. dnat never mutates packet length or IP address
// family.
func (pc *peerConfigTable) dnat(p *packet.Parsed) {
oldDst := p.Dst.Addr()
newDst := pc.mapDstIP(p.Src.Addr(), oldDst)
@@ -868,32 +868,42 @@ func (t *Wrapper) awaitStart() {
}
}
func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
// Read implements [tun.Device.Read].
func (t *Wrapper) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
if !t.started.Load() {
t.awaitStart()
}
t.startPollingOnce.Do(func() {
go t.pollVector(len(slab), len(packets))
})
// packet from OS read and sent to WG
res, ok := <-t.vectorOutbound
if !ok {
return 0, io.EOF
}
if res.err != nil && len(res.data) == 0 {
return 0, res.err
defer func() {
if res.doneHandlingFn != nil {
res.doneHandlingFn()
}
}()
if res.isInjected {
return t.injectedRead(res.injected, slab, packets, tun.ReadPacketSpacing)
}
if res.data == nil {
return t.injectedRead(res.injected, buffs, sizes, offset)
if res.real.err != nil && len(res.real.packets) == 0 {
return 0, res.real.err
}
metricPacketOut.Add(int64(len(res.data)))
metricPacketOut.Add(int64(len(res.real.packets)))
var buffsPos int
var numPackets int
p := parsedPacketPool.Get().(*packet.Parsed)
defer parsedPacketPool.Put(p)
captHook := t.captureHook.Load()
pc := t.peerConfig.Load()
var buffsGRO *gro.GRO
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
for _, meta := range res.real.packets {
data := res.real.slab[meta.Offset : meta.Offset+meta.Size]
p.Decode(data)
if buildfeatures.HasCapture && captHook != nil {
captHook(packet.FromLocal, t.now(), p.Buffer(), p.CaptureMeta)
@@ -915,27 +925,19 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
// Make sure to do SNAT after filtering, so that any flow tracking in
// the filter sees the original source address. See #12133.
pc.snat(p)
n := copy(buffs[buffsPos][offset:], p.Buffer())
if n != len(data)-res.dataOffset {
panic(fmt.Sprintf("short copy: %d != %d", n, len(data)-res.dataOffset))
n := copy(slab[meta.Offset:meta.Offset+meta.Size], p.Buffer())
if n != len(data) {
panic(fmt.Sprintf("short copy: %d != %d", n, len(data)))
}
sizes[buffsPos] = n
buffsPos++
packets[numPackets] = meta
numPackets++
}
if buffsGRO != nil {
buffsGRO.Flush()
}
// t.vectorBuffer has a fixed location in memory.
// TODO(raggi): add an explicit field and possibly method to the tunVectorReadResult
// to signal when sendBufferConsumed should be called.
if &res.data[0] == &t.vectorBuffer[0] {
// We are done with t.buffer. Let poll() re-use it.
t.sendBufferConsumed()
}
t.noteActivity()
return buffsPos, res.err
return numPackets, res.real.err
}
const (
@@ -1000,10 +1002,10 @@ func invertGSOChecksum(pkt []byte, gso netstack_GSO) {
// filter rules, but UDP/SCTP flow state is still recorded via
// [filter.Filter.UpdateOutboundFlowState] so inbound replies are admitted by
// [filter.Filter.RunIn].
func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []int, offset int) (n int, err error) {
func (t *Wrapper) injectedRead(res tunInjectedRead, slab []byte, packets []tun.ReadPacket, spacing int) (n int, err error) {
var gso netstack_GSO
pkt := outBuffs[0][offset:]
pkt := slab[spacing : len(slab)-spacing]
if res.packet != nil {
if !buildfeatures.HasNetstack {
panic("unreachable")
@@ -1015,8 +1017,11 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
pkt = pkt[:bufN]
defer res.packet.DecRef() // defer DecRef so we may continue to reference it
} else {
sizes[0] = copy(pkt, res.data)
pkt = pkt[:sizes[0]]
packets[0] = tun.ReadPacket{
Offset: spacing,
Size: copy(pkt, res.data),
}
pkt = pkt[:packets[0].Size]
n = 1
}
@@ -1078,13 +1083,15 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
if err != nil {
return 0, err
}
n, err = tun.GSOSplit(pkt, gsoOptions, outBuffs, sizes, offset)
n, err = tun.GSOSplit(pkt, gsoOptions, slab, packets, spacing)
}
if buildfeatures.HasNetLog {
if update := t.connCounter.Load(); update != nil {
for i := 0; i < n; i++ {
updateConnCounter(update, outBuffs[i][offset:offset+sizes[i]], false)
start := packets[i].Offset
end := packets[i].Offset + packets[i].Size
updateConnCounter(update, slab[start:end], false)
}
}
}
@@ -1298,27 +1305,36 @@ func (t *Wrapper) SetJailedFilter(filt *filter.Filter) {
t.jailedFilter.Store(filt)
}
// InjectInboundPacketBuffer makes the Wrapper device behave as if a packet
// InjectInboundPacketBuffer makes the [Wrapper] device behave as if a packet
// (pkt) with the given contents was received from the network.
// It takes ownership of one reference count on pkt. The injected
// packet will not pass through inbound filters.
//
// pkt will be copied into buffs before writing to the underlying tun.Device.
// Therefore, callers must allocate and pass a buffs slice that is sized
// appropriately for holding pkt.Size() + PacketStartOffset as a single
// element (buffs[0]) and split across multiple elements if the originating
// stack supports GSO. sizes must be sized with similar consideration,
// len(buffs) should be equal to len(sizes). If any len(buffs[<index>]) was
// mutated by InjectInboundPacketBuffer it will be reset to cap(buffs[<index>])
// before returning.
// pkt will be copied into slab, and potentially GSO split, before writing to
// the underlying [tun.Device]. Therefore, callers must allocate and pass a slab
// slice that is sized for holding:
//
// pkt.Data.Size() +
// (N * network headers len) +
// ((N+1) * [WritePacketStartOffset])
//
// N is 1 when the originating netstack does not support GSO, otherwise N should
// be [conn.IdealBatchSize]. Similarly, callers must pass packets and writeBufs
// sized to N.
//
// [tun.ReadPacket] is the shape required by [tun.GSOSplit], but is
// admittedly confusing in the context of the direction of our eventual
// i/o op, [tun.Device.Write]. This is purely historical, as [tun.GSOSplit]
// was initially only used post-[tun.Device.Read] heading out to the network,
// but we are using it in the opposite direction here.
//
// This path is typically used to deliver synthesized packets to the
// host networking stack.
func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, buffs [][]byte, sizes []int) error {
func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, slab []byte, packets []tun.ReadPacket, writeBufs [][]byte) error {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
buf := buffs[0][PacketStartOffset:]
buf := slab[WritePacketStartOffset:]
bufN := copy(buf, pkt.NetworkHeader().Slice())
bufN += copy(buf[bufN:], pkt.TransportHeader().Slice())
@@ -1351,7 +1367,7 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, buffs []
// TODO(jwhited): support GSO passthrough to t.tdev. If t.tdev supports
// GSO we don't need to split here and coalesce inside wireguard-go,
// we can pass a coalesced segment all the way through.
n, err := tun.GSOSplit(buf, gso, buffs, sizes, PacketStartOffset)
n, err := tun.GSOSplit(buf, gso, slab, packets, WritePacketStartOffset)
if err != nil {
if errors.Is(err, tun.ErrTooManySegments) {
t.limitedLogf("InjectInboundPacketBuffer: GSO split overflows buffs")
@@ -1359,15 +1375,10 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, buffs []
return err
}
}
for i := range n {
buffs[i] = buffs[i][:PacketStartOffset+sizes[i]]
for i, meta := range packets[:n] {
writeBufs[i] = slab[meta.Offset-WritePacketStartOffset : meta.Offset+meta.Size]
}
defer func() {
for i := range n {
buffs[i] = buffs[i][:cap(buffs[i])]
}
}()
_, err = t.tdevWrite(buffs[:n], PacketStartOffset)
_, err = t.tdevWrite(writeBufs[:n], WritePacketStartOffset)
return err
}
@@ -1377,7 +1388,7 @@ func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, buffs []
// The injected packet will not pass through inbound filters.
//
// The packet contents are to start at &buf[offset].
// offset must be greater or equal to PacketStartOffset.
// offset must be greater or equal to WritePacketStartOffset.
// The space before &buf[offset] will be used by WireGuard.
func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error {
if len(buf) > MaxPacketSize {
@@ -1386,7 +1397,7 @@ func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error {
if len(buf) < offset {
return errOffsetTooBig
}
if offset < PacketStartOffset {
if offset < WritePacketStartOffset {
return errOffsetTooSmall
}
@@ -1408,10 +1419,10 @@ func (t *Wrapper) InjectInboundCopy(packet []byte) error {
return nil
}
buf := make([]byte, PacketStartOffset+len(packet))
copy(buf[PacketStartOffset:], packet)
buf := make([]byte, WritePacketStartOffset+len(packet))
copy(buf[WritePacketStartOffset:], packet)
return t.InjectInboundDirect(buf, PacketStartOffset)
return t.InjectInboundDirect(buf, WritePacketStartOffset)
}
func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingRequest) {
@@ -1486,7 +1497,7 @@ func (t *Wrapper) BatchSize() int {
// Linux, and we cannot make a determination on gVisor usage at
// wireguard-go.Device startup, which is when this value matters for
// packet memory init.
return batching.BatchSizeFromEnv(conn.IdealBatchSize)
return conn.IdealBatchSize
}
return t.tdev.BatchSize()
}
+6 -6
View File
@@ -81,8 +81,8 @@ func probeTCPGRO(dev tun.GRODevice) error {
ipAs4 := ipPort.Addr().As4()
bufs := make([][]byte, 2)
for i := range bufs {
bufs[i] = make([]byte, PacketStartOffset+totalLen, PacketStartOffset+(totalLen*2))
ipv4H := header.IPv4(bufs[i][PacketStartOffset:])
bufs[i] = make([]byte, WritePacketStartOffset+totalLen, WritePacketStartOffset+(totalLen*2))
ipv4H := header.IPv4(bufs[i][WritePacketStartOffset:])
ipv4H.Encode(&header.IPv4Fields{
SrcAddr: tcpip.AddrFromSlice(ipAs4[:]),
DstAddr: tcpip.AddrFromSlice(ipAs4[:]),
@@ -92,7 +92,7 @@ func probeTCPGRO(dev tun.GRODevice) error {
TTL: 0,
TotalLength: uint16(totalLen),
})
tcpH := header.TCP(bufs[i][PacketStartOffset+iphLen:])
tcpH := header.TCP(bufs[i][WritePacketStartOffset+iphLen:])
tcpH.Encode(&header.TCPFields{
SrcPort: ipPort.Port(),
DstPort: ipPort.Port(),
@@ -102,12 +102,12 @@ func probeTCPGRO(dev tun.GRODevice) error {
Flags: header.TCPFlagAck,
WindowSize: 3000,
})
copy(bufs[i][PacketStartOffset+iphLen+tcphLen:], fingerprint)
copy(bufs[i][WritePacketStartOffset+iphLen+tcphLen:], fingerprint)
ipv4H.SetChecksum(^ipv4H.CalculateChecksum())
pseudoCsum := header.PseudoHeaderChecksum(unix.IPPROTO_TCP, ipv4H.SourceAddress(), ipv4H.DestinationAddress(), uint16(tcphLen+segmentSize))
pseudoCsum = checksum.Checksum(bufs[i][PacketStartOffset+iphLen+tcphLen:], pseudoCsum)
pseudoCsum = checksum.Checksum(bufs[i][WritePacketStartOffset+iphLen+tcphLen:], pseudoCsum)
tcpH.SetChecksum(^tcpH.CalculateChecksum(pseudoCsum))
}
_, err := dev.Write(bufs, PacketStartOffset)
_, err := dev.Write(bufs, WritePacketStartOffset)
return err
}
+31 -31
View File
@@ -21,6 +21,7 @@ import (
"github.com/gaissmai/bart"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
wgtun "github.com/tailscale/wireguard-go/tun"
"github.com/tailscale/wireguard-go/tun/tuntest"
"go4.org/mem"
"go4.org/netipx"
@@ -199,6 +200,13 @@ func newFakeTUN(logf logger.Logf, bus *eventbus.Bus, secure bool) (*fakeTUN, *Wr
return ftun.(*fakeTUN), tun
}
// getSinglePacketReadArgs returns a slab and packets slice sized for passing to
// [tun.Device.Read] where [tun.Device.BatchSize] returns 1.
func getSinglePacketReadArgs() (slab []byte, packets []wgtun.ReadPacket) {
return make([]byte, MaxPacketSize+(2*wgtun.ReadPacketSpacing)),
make([]wgtun.ReadPacket, 1)
}
func TestReadAndInject(t *testing.T) {
bus := eventbustest.NewBus(t)
chtun, tun := newChannelTUN(t.Logf, bus, false)
@@ -225,21 +233,18 @@ func TestReadAndInject(t *testing.T) {
}(packet)
}
var buf [MaxPacketSize]byte
seen := make(map[string]bool)
sizes := make([]int, 1)
slab, packets := getSinglePacketReadArgs()
// We expect the same packets back, in no particular order.
for i := range len(written) + len(injected) {
packet := buf[:]
buffs := [][]byte{packet}
numPackets, err := tun.Read(buffs, sizes, 0)
numPackets, err := tun.Read(slab, packets)
if err != nil {
t.Errorf("read %d: error: %v", i, err)
}
if numPackets != 1 {
t.Fatalf("read %d packets, expected %d", numPackets, 1)
}
packet = packet[:sizes[0]]
packet := slab[packets[0].Offset : packets[0].Offset+packets[0].Size]
packetLen := len(packet)
if packetLen != size {
t.Errorf("read %d: got size %d; want %d", i, packetLen, size)
@@ -373,7 +378,6 @@ func TestFilter(t *testing.T) {
}
}()
var buf [MaxPacketSize]byte
var stats netlogtype.CountsByConnection
tun.SetConnectionCounter(stats.Add)
for _, tt := range tests {
@@ -381,7 +385,6 @@ func TestFilter(t *testing.T) {
var n int
var err error
var filtered bool
sizes := make([]int, 1)
tunStats := stats.Clone()
stats.Reset()
@@ -399,8 +402,10 @@ func TestFilter(t *testing.T) {
_, err = tun.Write([][]byte{tt.data}, 0)
filtered = tun.lastActivityAtomic.LoadAtomic() == 0
} else {
chtun.Outbound <- tt.data
n, err = tun.Read([][]byte{buf[:]}, sizes, 0)
// chtun.Outbound is unbuffered, and won't be drained until the
// first Read call.
go func() { chtun.Outbound <- tt.data }()
n, err = tun.Read(getSinglePacketReadArgs())
// In the read direction, errors are fatal, so we return n = 0 instead.
filtered = (n == 0)
}
@@ -487,9 +492,7 @@ func TestInjectOutboundRecordsUDPFlowState(t *testing.T) {
// Drain the injected packet via Read. This drives injectedRead, which
// is what records the reverse-flow tuple in filter state.
var buf [MaxPacketSize]byte
sizes := make([]int, 1)
if n, err := tun.Read([][]byte{buf[:]}, sizes, 0); err != nil {
if n, err := tun.Read(getSinglePacketReadArgs()); err != nil {
t.Fatalf("Read: %v", err)
} else if n != 1 {
t.Fatalf("Read returned %d packets, want 1", n)
@@ -972,12 +975,9 @@ func TestCaptureHook(t *testing.T) {
// Loop reading and discarding packets; this ensures that we don't have
// packets stuck in vectorOutbound
go func() {
var (
buf [MaxPacketSize]byte
sizes = make([]int, 1)
)
slab, packets := getSinglePacketReadArgs()
for {
_, err := w.Read([][]byte{buf[:]}, sizes, 0)
_, err := w.Read(slab, packets)
if err != nil {
return
}
@@ -992,10 +992,10 @@ func TestCaptureHook(t *testing.T) {
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData([]byte("InjectInboundPacketBuffer")),
})
buffs := make([][]byte, 1)
buffs[0] = make([]byte, PacketStartOffset+packetBuf.Size())
sizes := make([]int, 1)
w.InjectInboundPacketBuffer(packetBuf, buffs, sizes)
slab := make([]byte, packetBuf.Size()+(2*WritePacketStartOffset))
packets := make([]wgtun.ReadPacket, 1)
writeBufs := make([][]byte, 1)
w.InjectInboundPacketBuffer(packetBuf, slab, packets, writeBufs)
packetBuf = stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData([]byte("InjectOutboundPacketBuffer")),
@@ -1113,11 +1113,12 @@ func TestInterceptOrdering(t *testing.T) {
tun.PreFilterPacketOutboundToWireGuardAppConnectorIntercept = orderedFilterFn(3)
tun.PostFilterPacketOutboundToWireGuard = orderedFilterFn(4)
// Read the packet.
var buf [MaxPacketSize]byte
sizes := make([]int, 1)
chtun.Outbound <- udp4("1.2.3.4", "5.6.7.8", 98, 98) // Simulate tun device sending.
tun.Read([][]byte{buf[:]}, sizes, 0)
// Read the packet. chtun.Outbound is unbuffered, and won't be drained until
// first Read call.
go func() {
chtun.Outbound <- udp4("1.2.3.4", "5.6.7.8", 98, 98) // Simulate tun device sending.
}()
tun.Read(getSinglePacketReadArgs())
if seq != numOutboundIntercepts {
t.Errorf("got number of intercepts run in Read(): %d; want: %d", seq, numOutboundIntercepts)
@@ -1142,16 +1143,15 @@ func TestInjectedReadCallsAppConnectorHook(t *testing.T) {
t.Fatalf("InjectOutbound error: %v", err)
}
var buf [MaxPacketSize]byte
sizes := make([]int, 1)
tun.Read([][]byte{buf[:]}, sizes, 0)
slab, packets := getSinglePacketReadArgs()
tun.Read(slab, packets)
if !called {
t.Error("app connector hook was not called in InjectOutbound")
}
wantPkt := udp4("169.254.0.1", "100.25.63.57", 80, 12345)
gotPkt := buf[:sizes[0]]
gotPkt := slab[packets[0].Offset : packets[0].Offset+packets[0].Size]
if !bytes.Equal(wantPkt, gotPkt) {
t.Errorf("packet mismatch\nwant:\t% x\ngot:\t% x", wantPkt, gotPkt)
}
+17 -26
View File
@@ -24,7 +24,6 @@ import (
"go4.org/mem"
"golang.org/x/crypto/blake2s"
"golang.org/x/net/ipv6"
"tailscale.com/control/controlknobs"
"tailscale.com/disco"
"tailscale.com/net/batching"
@@ -569,13 +568,14 @@ type singlePacketConn struct {
*net.UDPConn
}
func (c *singlePacketConn) ReadBatch(msgs []ipv6.Message, _ int) (int, error) {
n, ap, err := c.UDPConn.ReadFromUDPAddrPort(msgs[0].Buffers[0])
func (c *singlePacketConn) ReadBatch(slab []byte, packets []batching.ReceivedPacket) (int, error) {
n, ap, err := c.UDPConn.ReadFromUDPAddrPort(slab)
if err != nil {
return 0, err
}
msgs[0].N = n
msgs[0].Addr = net.UDPAddrFromAddrPort(netaddr.Unmap(ap))
packets[0].Offset = 0
packets[0].Size = n
packets[0].Source = netaddr.Unmap(ap)
return 1, nil
}
@@ -692,7 +692,7 @@ func (s *Server) bindSockets(desiredPort uint16) error {
break SocketsLoop
}
}
pc := batching.TryUpgradeToConn(uc, network, batching.IdealBatchSize, "udprelay_rxq_overflows", s.controlKnobs)
pc := batching.TryUpgradeToConn(uc, network, "udprelay_rxq_overflows", s.controlKnobs)
bc, ok := pc.(batching.Conn)
if !ok {
bc = &singlePacketConn{uc}
@@ -874,19 +874,11 @@ func (s *Server) packetReadLoop(readFromSocket, otherSocket batching.Conn, readF
s.Close()
}()
msgs := make([]ipv6.Message, batching.IdealBatchSize)
for i := range msgs {
msgs[i].OOB = make([]byte, batching.MinControlMessageSize())
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Buffers[0] = make([]byte, 1<<16-1)
}
writeBuffsByDest := make(map[netip.AddrPort][][]byte, batching.IdealBatchSize)
slab := make([]byte, 2*batching.ReadSlabMultiple)
packets := make([]batching.ReceivedPacket, 2*batching.MinimumReadBatchSize)
writeBuffsByDest := make(map[netip.AddrPort][][]byte, batching.MaximumWriteBatchSize)
for {
for i := range msgs {
msgs[i] = ipv6.Message{Buffers: msgs[i].Buffers, OOB: msgs[i].OOB[:cap(msgs[i].OOB)]}
}
// TODO: extract laddr from IP_PKTINFO for use in reply
// ReadBatch will split coalesced datagrams before returning, which
// WriteBatchTo will re-coalesce further down. We _could_ be more
@@ -894,7 +886,7 @@ func (s *Server) packetReadLoop(readFromSocket, otherSocket batching.Conn, readF
// are non-control/handshake packets. We pay the memmove/memcopy
// performance penalty for now in the interest of simple single packet
// handlers.
n, err := readFromSocket.ReadBatch(msgs, 0)
n, err := readFromSocket.ReadBatch(slab, packets)
if err != nil {
s.logf("error reading from socket(%v): %v", readFromSocket.LocalAddr(), err)
return
@@ -907,13 +899,12 @@ func (s *Server) packetReadLoop(readFromSocket, otherSocket batching.Conn, readF
bytes6 int64
packets6 int64
}{}
for _, msg := range msgs[:n] {
if msg.N == 0 {
for _, packet := range packets[:n] {
if packet.Size == 0 {
continue
}
buf := msg.Buffers[0][:msg.N]
from := msg.Addr.(*net.UDPAddr).AddrPort()
write, to, isDataPacket := s.handlePacket(from, buf)
buf := slab[packet.Offset : packet.Offset+packet.Size]
write, to, isDataPacket := s.handlePacket(packet.Source, buf)
if !to.IsValid() {
continue
}
@@ -926,10 +917,10 @@ func (s *Server) packetReadLoop(readFromSocket, otherSocket batching.Conn, readF
forwardedByOutAF.packets6++
}
}
if from.Addr().Is4() == to.Addr().Is4() || otherSocket != nil {
if packet.Source.Addr().Is4() == to.Addr().Is4() || otherSocket != nil {
buffs, ok := writeBuffsByDest[to]
if !ok {
buffs = make([][]byte, 0, batching.IdealBatchSize)
buffs = make([][]byte, 0, batching.MaximumWriteBatchSize)
}
buffs = append(buffs, write)
writeBuffsByDest[to] = buffs
@@ -939,7 +930,7 @@ func (s *Server) packetReadLoop(readFromSocket, otherSocket batching.Conn, readF
// [server.handlePacket] has to see a packet from a particular
// address family at least once in order for it to return a
// packet to write towards a dest for the same address family.
s.logf("[unexpected] packet from: %v produced packet to: %v while otherSocket is nil", from, to)
s.logf("[unexpected] packet from: %v produced packet to: %v while otherSocket is nil", packet.Source, to)
}
}
+13 -19
View File
@@ -28,7 +28,6 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
wgconn "github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"go4.org/netipx"
@@ -944,11 +943,6 @@ func derpProbeBandwidthTUN(ctx context.Context, transferTimeSeconds, totalBytesT
return fmt.Errorf("failed to configure tun: %w", err)
}
// Depending on platform, we need some space for headers at the front
// of TUN I/O op buffers. The below constant is more than enough space
// for any platform that this might run on.
tunStartOffset := device.MessageTransportHeaderSize
// This goroutine reads packets from the TUN device and evaluates if they
// are IPv4 packets destined for loopback via DERP. If so, it performs L3 NAT
// (swap src/dst) and writes them towards DERP in order to loopback via the
@@ -958,25 +952,21 @@ func derpProbeBandwidthTUN(ctx context.Context, transferTimeSeconds, totalBytesT
go func() {
defer wg.Done()
numBufs := wgconn.IdealBatchSize
bufs := make([][]byte, 0, numBufs)
sizes := make([]int, numBufs)
for range numBufs {
bufs = append(bufs, make([]byte, mtu+tunStartOffset))
}
slab := make([]byte, 2*(1<<16-1)+(2*tun.ReadPacketSpacing))
packets := make([]tun.ReadPacket, dev.BatchSize())
destinationAddrBytes := destinationAddr.AsSlice()
scratch := make([]byte, 4)
toDERPPubKey := toc.SelfPublicKey()
for {
n, err := dev.Read(bufs, sizes, tunStartOffset)
n, err := dev.Read(slab, packets)
if err != nil {
tunReadErrC <- err
return
}
for i := range n {
pkt := bufs[i][tunStartOffset : sizes[i]+tunStartOffset]
for _, metadata := range packets[:n] {
pkt := slab[metadata.Offset : metadata.Offset+metadata.Size]
// Skip everything except valid IPv4 packets
if len(pkt) < 20 {
// Doesn't even have a full IPv4 header
@@ -1011,7 +1001,11 @@ func derpProbeBandwidthTUN(ctx context.Context, transferTimeSeconds, totalBytesT
go func() {
defer wg.Done()
buf := make([]byte, mtu+tunStartOffset)
// Depending on platform, we need some space for headers at the front
// of TUN I/O op buffers. The below constant is more than enough space
// for any platform that this might run on.
tunWriteStartOffset := device.MessageTransportHeaderSize
buf := make([]byte, mtu+tunWriteStartOffset)
bufs := make([][]byte, 1)
fromDERPPubKey := fromc.SelfPublicKey()
@@ -1028,9 +1022,9 @@ func derpProbeBandwidthTUN(ctx context.Context, transferTimeSeconds, totalBytesT
return
}
pkt := v.Data
copy(buf[tunStartOffset:], pkt)
bufs[0] = buf[:len(pkt)+tunStartOffset]
if _, err := dev.Write(bufs, tunStartOffset); err != nil {
copy(buf[tunWriteStartOffset:], pkt)
bufs[0] = buf[:len(pkt)+tunWriteStartOffset]
if _, err := dev.Write(bufs, tunWriteStartOffset); err != nil {
recvErrC <- fmt.Errorf("failed to write to TUN device: %w", err)
return
}
+1 -1
View File
@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-w7VuEoXsnL7wmHbKlwd6qgUqyRj09JAqHw073Zgnidc=
# nix-direnv cache busting line: sha256-edmocquubPBqaakW8/TfQTLZS48HR7SOcWjYiLeJGLY=
+1 -1
View File
@@ -177,7 +177,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
tailscale.com/metrics from tailscale.com/tsweb+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock+
💣 tailscale.com/net/batching from tailscale.com/wgengine/magicsock
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
+3 -2
View File
@@ -2721,12 +2721,13 @@ func (t *chanTUN) Close() error {
return nil
}
func (t *chanTUN) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
func (t *chanTUN) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
select {
case <-t.closed:
return 0, io.EOF
case pkt := <-t.Outbound:
sizes[0] = copy(bufs[0][offset:], pkt)
packets[0].Offset = tun.ReadPacketSpacing
packets[0].Size = copy(slab[tun.ReadPacketSpacing:len(slab)-tun.ReadPacketSpacing], pkt)
return 1, nil
}
}
+2 -15
View File
@@ -48,16 +48,12 @@ import (
// http://go/ios-memory or `go tool pprof` the heap) or, if the growth is
// genuinely necessary and accounted for, raise the budget in the same change
// with a justification.
//
// Note that these tests run with TS_DEBUG_WG_BATCH_SIZE=1 (see
// measureNodeCost), so wireguard-go packet-pool memory is measured at its
// mobile configuration, not the Linux server default.
const (
// startupBudget is the live-heap budget for bringing up a backend
// connected to a tailnet with zero peers: feature/extension init,
// engine, netstack, DNS, control client, etc.
//
// As of 2026-08 the measured cost is ~1.3 MiB; the budget leaves ~6x
// As of 2026-09 the measured cost is ~1.73 MiB; the budget leaves ~5x
// headroom for organic growth while still catching multi-MiB
// regressions (the conn25 flow table pre-allocation this test was
// written for measured 17 MiB here).
@@ -135,15 +131,6 @@ func measureNodeCost(t *testing.T, peers int) uint64 {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Use the minimum wireguard-go batch size, matching iOS and other
// memory-constrained platforms (which are what these budgets protect).
// Without this, on Linux each of wireguard-go's four reader goroutines
// (three UDP receive paths plus the TUN reader) pins batch-size (128) ×
// 64 KiB message buffers at startup: ~32 MiB of live heap that exists
// only in the Linux configuration and would drown out the regressions
// this test exists to catch.
t.Setenv("TS_DEBUG_WG_BATCH_SIZE", "1")
controlURL := startControl(t, peers)
baseline := liveHeap()
s := startNode(t, ctx, controlURL)
@@ -192,6 +179,6 @@ func TestBackendPerPeerMemory(t *testing.T) {
float64(base)/(1<<20), nPeers, float64(loaded)/(1<<20),
float64(perPeer)/(1<<10), float64(perPeerBudget)/(1<<10))
if perPeer > perPeerBudget {
t.Errorf("per-peer live heap %v exceeds budget %v", perPeer, int(perPeerBudget))
t.Errorf("per-peer live heap %v exceeds budget %v", perPeer, perPeerBudget)
}
}
+9 -10
View File
@@ -165,16 +165,15 @@ func (t *sourceTun) Write(b [][]byte, ofs int) (int, error) {
return len(b), nil
}
func (t *sourceTun) Read(b [][]byte, sizes []int, ofs int) (int, error) {
for i, b := range b {
// Continually generate "input" packets
n := t.traf.Generate(b, ofs)
sizes[i] = n
if n == 0 {
return 0, io.EOF
}
func (t *sourceTun) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
// Continually generate "input" packets.
n := t.traf.Generate(slab, tun.ReadPacketSpacing)
packets[0].Size = n
packets[0].Offset = tun.ReadPacketSpacing
if n == 0 {
return 0, io.EOF
}
return len(b), nil
return 1, nil
}
type sinkTun struct {
@@ -189,7 +188,7 @@ func (t *sinkTun) Flush() error { return nil }
func (t *sinkTun) MTU() (int, error) { return 1500, nil }
func (t *sinkTun) Name() (string, error) { return "sink", nil }
func (t *sinkTun) Read(b [][]byte, sizes []int, ofs int) (int, error) {
func (t *sinkTun) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
// Never returns
select {}
}
+5 -4
View File
@@ -708,7 +708,7 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan
}
}
func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
func (c *connBind) receiveDERP(slab []byte, packets []conn.ReceivedPacket) (int, error) {
if s := c.Conn.health.ReceiveFuncStats(health.ReceiveDERP); s != nil {
s.Enter()
defer s.Exit()
@@ -718,13 +718,14 @@ func (c *connBind) receiveDERP(buffs [][]byte, sizes []int, eps []conn.Endpoint)
if c.isClosed() {
break
}
n, ep := c.processDERPReadResult(dm, buffs[0])
n, ep := c.processDERPReadResult(dm, slab)
if n == 0 {
// No data read occurred. Wait for another packet.
continue
}
sizes[0] = n
eps[0] = ep
packets[0].Size = n
packets[0].Offset = 0
packets[0].Endpoint = ep
return 1, nil
}
return 0, net.ErrClosed
+38 -70
View File
@@ -28,7 +28,6 @@ import (
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"go4.org/mem"
"golang.org/x/net/ipv6"
"tailscale.com/control/controlknobs"
"tailscale.com/disco"
"tailscale.com/envknob"
@@ -193,8 +192,6 @@ type Conn struct {
pconn4 RebindingUDPConn
pconn6 RebindingUDPConn
receiveBatchPool sync.Pool
// closeDisco4 and closeDisco6 are io.Closers to shut down the raw
// disco packet receivers. If nil, no raw disco receiver is
// running for the given family.
@@ -593,17 +590,6 @@ func newConn(logf logger.Logf) *Conn {
}
c.discoAtomic.Set(discoPrivate)
c.bind = &connBind{Conn: c, closed: true}
c.receiveBatchPool = sync.Pool{New: func() any {
msgs := make([]ipv6.Message, c.bind.BatchSize())
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].OOB = make([]byte, batching.MinControlMessageSize())
}
batch := &receiveBatch{
msgs: msgs,
}
return batch
}}
c.muCond = sync.NewCond(&c.mu)
c.networkUp.Store(true) // assume up until told otherwise
return c
@@ -1705,26 +1691,6 @@ func (c *Conn) sendAddr(addr netip.AddrPort, pubKey key.NodePublic, b []byte, is
return false, errDropDerpPacket
}
type receiveBatch struct {
msgs []ipv6.Message
}
func (c *Conn) getReceiveBatchForBuffs(buffs [][]byte) *receiveBatch {
batch := c.receiveBatchPool.Get().(*receiveBatch)
for i := range buffs {
batch.msgs[i].Buffers[0] = buffs[i]
batch.msgs[i].OOB = batch.msgs[i].OOB[:cap(batch.msgs[i].OOB)]
}
return batch
}
func (c *Conn) putReceiveBatch(batch *receiveBatch) {
for i := range batch.msgs {
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers, OOB: batch.msgs[i].OOB}
}
c.receiveBatchPool.Put(batch)
}
func (c *Conn) receiveIPv4() conn.ReceiveFunc {
return c.mkReceiveFunc(&c.pconn4, c.health.ReceiveFuncStats(health.ReceiveIPv4),
&c.metrics.inboundPacketsIPv4Total,
@@ -1749,8 +1715,9 @@ func (c *Conn) receiveIPv6() conn.ReceiveFunc {
func (c *Conn) mkReceiveFunc(ruc *RebindingUDPConn, healthItem *health.ReceiveFuncStats, directPacketMetric, peerRelayPacketMetric, directBytesMetric, peerRelayBytesMetric *expvar.Int) conn.ReceiveFunc {
// epCache caches an epAddr->endpoint for hot flows.
var epCache epAddrEndpointCache
var batchingPackets []batching.ReceivedPacket
return func(buffs [][]byte, sizes []int, eps []conn.Endpoint) (_ int, retErr error) {
return func(slab []byte, packets []conn.ReceivedPacket) (_ int, retErr error) {
if buildfeatures.HasHealth && healthItem != nil {
healthItem.Enter()
defer healthItem.Exit()
@@ -1763,11 +1730,12 @@ func (c *Conn) mkReceiveFunc(ruc *RebindingUDPConn, healthItem *health.ReceiveFu
if ruc == nil {
panic("nil RebindingUDPConn")
}
if len(batchingPackets) != len(packets) {
batchingPackets = make([]batching.ReceivedPacket, len(packets))
}
batch := c.getReceiveBatchForBuffs(buffs)
defer c.putReceiveBatch(batch)
for {
numMsgs, err := ruc.ReadBatch(batch.msgs[:len(buffs)], 0)
numMsgs, err := ruc.ReadBatch(slab, batchingPackets)
if err != nil {
if neterror.PacketWasTruncated(err) {
continue
@@ -1776,33 +1744,33 @@ func (c *Conn) mkReceiveFunc(ruc *RebindingUDPConn, healthItem *health.ReceiveFu
}
reportToCaller := false
for i, msg := range batch.msgs[:numMsgs] {
if msg.N == 0 {
sizes[i] = 0
for i, batchingPacket := range batchingPackets[:numMsgs] {
if batchingPacket.Size == 0 {
packets[i].Size = 0
continue
}
ipp := msg.Addr.(*net.UDPAddr).AddrPort()
if ep, size, isGeneveEncap, ok := c.receiveIP(msg.Buffers[0][:msg.N], ipp, &epCache); ok {
buf := slab[batchingPacket.Offset : batchingPacket.Offset+batchingPacket.Size]
packets[i].Size = batchingPacket.Size
packets[i].Offset = batchingPacket.Offset
if isGeneveEncap, ok := c.receiveIP(buf, batchingPacket.Source, &epCache, &packets[i]); ok {
if isGeneveEncap {
if peerRelayPacketMetric != nil {
peerRelayPacketMetric.Add(1)
}
if peerRelayBytesMetric != nil {
peerRelayBytesMetric.Add(int64(msg.N))
peerRelayBytesMetric.Add(int64(len(buf)))
}
} else {
if directPacketMetric != nil {
directPacketMetric.Add(1)
}
if directBytesMetric != nil {
directBytesMetric.Add(int64(msg.N))
directBytesMetric.Add(int64(len(buf)))
}
}
eps[i] = ep
sizes[i] = size
reportToCaller = true
} else {
sizes[i] = 0
packets[i].Size = 0
}
}
if reportToCaller {
@@ -1821,17 +1789,17 @@ func looksLikeInitiationMsg(b []byte) bool {
// receiveIP is the shared bits of ReceiveIPv4 and ReceiveIPv6.
//
// size is the length of 'b' to report up to wireguard-go (only relevant if
// 'ok' is true).
// rp.Size & rp.Offset are pre-populated by callers. receiveIP must keep them
// up to date if b's length is mutated, and ok will be true. receiveIP must set
// rp.Endpoint if ok is true.
//
// isGeneveEncap is whether 'b' is encapsulated by a Geneve header (only
// relevant if 'ok' is true).
//
// ok is whether this read should be reported up to wireguard-go (our
// caller).
func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCache) (_ conn.Endpoint, size int, isGeneveEncap bool, ok bool) {
func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCache, rp *conn.ReceivedPacket) (isGeneveEncap bool, ok bool) {
var ep *endpoint
size = len(b)
var geneve packet.GeneveHeader
pt, isGeneveEncap := packetLooksLike(b)
@@ -1842,7 +1810,7 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
// Decode only returns an error when 'b' is too short, and
// 'isGeneveEncap' indicates it's a sufficient length.
c.logf("[unexpected] geneve header decoding error: %v", err)
return nil, 0, false, false
return false, false
}
src.vni = geneve.VNI
}
@@ -1857,10 +1825,10 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
// [disco.MessageType], but we assert it should be handshake-related.
shouldByRelayHandshakeMsg := geneve.Control == true
c.handleDiscoMessage(b, src, shouldByRelayHandshakeMsg, key.NodePublic{}, discoRXPathUDP)
return nil, 0, false, false
return false, false
case packetLooksLikeSTUNBinding:
c.netChecker.ReceiveSTUNPacket(b, ipp)
return nil, 0, false, false
return false, false
default:
// Fall through for all other packet types as they are assumed to
// be potentially WireGuard.
@@ -1870,7 +1838,7 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
// If we have no private key, we're logged out or
// stopped. Don't try to pass these wireguard packets
// up to wireguard-go; it'll just complain (issue 1167).
return nil, 0, false, false
return false, false
}
// geneveInclusivePacketLen holds the packet length prior to any potential
@@ -1879,13 +1847,10 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
if src.vni.IsSet() {
// Strip away the Geneve header before returning the packet to
// wireguard-go.
//
// TODO(jwhited): update [github.com/tailscale/wireguard-go/conn.ReceiveFunc]
// to support returning start offset in order to get rid of this memmove perf
// penalty.
size = copy(b, b[packet.GeneveFixedHeaderLength:])
b = b[:size]
b = b[packet.GeneveFixedHeaderLength:]
rp.Offset += packet.GeneveFixedHeaderLength
}
rp.Size = len(b)
if cache.epAddr == src && cache.de != nil && cache.gen == cache.de.numStopAndReset() {
ep = cache.de
@@ -1896,7 +1861,8 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
if !ok {
// TODO(jwhited): reuse [lazyEndpoint] across calls to receiveIP()
// for the same batch & [epAddr] src.
return &lazyEndpoint{c: c, src: src}, size, isGeneveEncap, true
rp.Endpoint = &lazyEndpoint{c: c, src: src}
return isGeneveEncap, true
}
cache.epAddr = src
cache.de = de
@@ -1917,9 +1883,11 @@ func (c *Conn) receiveIP(b []byte, ipp netip.AddrPort, cache *epAddrEndpointCach
// unlucky and fail to JIT configure the "correct" peer.
// TODO(jwhited): relax this to include direct connections
// See http://go/corp/29422 & http://go/corp/30042
return &lazyEndpoint{c: c, maybeEP: ep, src: src}, size, isGeneveEncap, true
rp.Endpoint = &lazyEndpoint{c: c, maybeEP: ep, src: src}
return isGeneveEncap, true
}
return ep, size, isGeneveEncap, true
rp.Endpoint = ep
return isGeneveEncap, true
}
// discoLogLevel controls the verbosity of discovery log messages.
@@ -3483,7 +3451,7 @@ func (c *connBind) BatchSize() int {
// TODO(raggi): determine by properties rather than hardcoding platform behavior
switch runtime.GOOS {
case "linux":
return batching.BatchSizeFromEnv(conn.IdealBatchSize)
return conn.IdealBatchSize
default:
return 1
}
@@ -3731,13 +3699,13 @@ func (c *Conn) bindSocket(ruc *RebindingUDPConn, network string, curPortFate cur
defer ruc.mu.Unlock()
if runtime.GOOS == "js" {
ruc.setConnLocked(newBlockForeverConn(), "", c.bind.BatchSize(), c.controlKnobs)
ruc.setConnLocked(newBlockForeverConn(), "", c.controlKnobs)
return nil
}
if debugAlwaysDERP() {
c.logf("disabled %v per TS_DEBUG_ALWAYS_USE_DERP", network)
ruc.setConnLocked(newBlockForeverConn(), "", c.bind.BatchSize(), c.controlKnobs)
ruc.setConnLocked(newBlockForeverConn(), "", c.controlKnobs)
return nil
}
@@ -3796,7 +3764,7 @@ func (c *Conn) bindSocket(ruc *RebindingUDPConn, network string, curPortFate cur
if debugBindSocket() {
c.logf("magicsock: bindSocket: successfully listened %v port %d", network, port)
}
ruc.setConnLocked(pconn, network, c.bind.BatchSize(), c.controlKnobs)
ruc.setConnLocked(pconn, network, c.controlKnobs)
if network == "udp4" {
c.health.SetUDP4Unbound(false)
}
@@ -3807,7 +3775,7 @@ func (c *Conn) bindSocket(ruc *RebindingUDPConn, network string, curPortFate cur
// Set pconn to a dummy conn whose reads block until closed.
// This keeps the receive funcs alive for a future in which
// we get a link change and we can try binding again.
ruc.setConnLocked(newBlockForeverConn(), "", c.bind.BatchSize(), c.controlKnobs)
ruc.setConnLocked(newBlockForeverConn(), "", c.controlKnobs)
if network == "udp4" {
c.health.SetUDP4Unbound(true)
}
+52 -47
View File
@@ -49,6 +49,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/batching"
"tailscale.com/net/netaddr"
"tailscale.com/net/netcheck"
"tailscale.com/net/netmon"
@@ -511,13 +512,11 @@ func TestNewConn(t *testing.T) {
conn.SetPrivateKey(key.NewNode())
go func() {
pkts := make([][]byte, 1)
sizes := make([]int, 1)
eps := make([]wgconn.Endpoint, 1)
pkts[0] = make([]byte, 64<<10)
slab := make([]byte, batching.ReadSlabMultiple)
packets := make([]wgconn.ReceivedPacket, batching.MinimumReadBatchSize)
receiveIPv4 := conn.receiveIPv4()
for {
_, err := receiveIPv4(pkts, sizes, eps)
_, err := receiveIPv4(slab, packets)
if err != nil {
return
}
@@ -1683,16 +1682,14 @@ func setUpReceiveFrom(tb testing.TB) (roundTrip func()) {
for i := range sendBuf {
sendBuf[i] = 'x'
}
buffs := make([][]byte, 1)
buffs[0] = make([]byte, 2<<10)
sizes := make([]int, 1)
eps := make([]wgconn.Endpoint, 1)
slab := make([]byte, batching.ReadSlabMultiple)
packets := make([]wgconn.ReceivedPacket, batching.MinimumReadBatchSize)
receiveIPv4 := conn.receiveIPv4()
return func() {
if _, err := sendConn.WriteTo(sendBuf, dstAddr); err != nil {
tb.Fatalf("WriteTo: %v", err)
}
n, err := receiveIPv4(buffs, sizes, eps)
n, err := receiveIPv4(slab, packets)
if err != nil {
tb.Fatal(err)
}
@@ -1919,13 +1916,11 @@ func TestRebindStress(t *testing.T) {
errc := make(chan error, 1)
go func() {
buffs := make([][]byte, 1)
sizes := make([]int, 1)
eps := make([]wgconn.Endpoint, 1)
buffs[0] = make([]byte, 1500)
slab := make([]byte, batching.ReadSlabMultiple)
packets := make([]wgconn.ReceivedPacket, batching.MinimumReadBatchSize)
receiveIPv4 := conn.receiveIPv4()
for {
_, err := receiveIPv4(buffs, sizes, eps)
_, err := receiveIPv4(slab, packets)
if ctx.Err() != nil {
errc <- nil
return
@@ -2356,8 +2351,8 @@ func TestRebindingUDPConn(t *testing.T) {
t.Fatal(err)
}
defer realConn.Close()
c.setConnLocked(realConn.(nettype.PacketConn), "udp4", 1, nil)
c.setConnLocked(newBlockForeverConn(), "", 1, nil)
c.setConnLocked(realConn.(nettype.PacketConn), "udp4", nil)
c.setConnLocked(newBlockForeverConn(), "", nil)
}
// https://github.com/tailscale/tailscale/issues/6680: don't ignore
@@ -4219,7 +4214,6 @@ func TestConn_receiveIP(t *testing.T) {
// [*lazyEndpoint] and [*lazyEndpoint.maybeEP] is non-nil, we expect
// got.maybeEP to also be non-nil. Must not be reused across tests.
wantEndpointType wgconn.Endpoint
wantSize int
wantIsGeneveEncap bool
wantOk bool
wantMetricInc *clientmetric.Metric
@@ -4230,7 +4224,6 @@ func TestConn_receiveIP(t *testing.T) {
ipp: netip.MustParseAddrPort("127.0.0.1:7777"),
cache: &epAddrEndpointCache{},
wantEndpointType: nil,
wantSize: 0,
wantIsGeneveEncap: false,
wantOk: false,
wantMetricInc: metricRecvDiscoBadPeer,
@@ -4241,7 +4234,6 @@ func TestConn_receiveIP(t *testing.T) {
ipp: netip.MustParseAddrPort("127.0.0.1:7777"),
cache: &epAddrEndpointCache{},
wantEndpointType: nil,
wantSize: 0,
wantIsGeneveEncap: false,
wantOk: false,
wantMetricInc: metricRecvDiscoBadPeer,
@@ -4252,7 +4244,6 @@ func TestConn_receiveIP(t *testing.T) {
ipp: netip.MustParseAddrPort("127.0.0.1:7777"),
cache: &epAddrEndpointCache{},
wantEndpointType: nil,
wantSize: 0,
wantIsGeneveEncap: false,
wantOk: false,
wantMetricInc: findMetricByName("netcheck_stun_recv_ipv4"),
@@ -4263,7 +4254,6 @@ func TestConn_receiveIP(t *testing.T) {
ipp: netip.MustParseAddrPort("127.0.0.1:7777"),
cache: &epAddrEndpointCache{},
wantEndpointType: &lazyEndpoint{},
wantSize: len(looksLikeNakedWireGuardInit),
wantIsGeneveEncap: false,
wantOk: true,
wantMetricInc: nil,
@@ -4276,7 +4266,6 @@ func TestConn_receiveIP(t *testing.T) {
insertWantEndpointTypeInPeerMap: true,
peerMapEpAddr: epAddr{ap: netip.MustParseAddrPort("127.0.0.1:7777")},
wantEndpointType: newPeerMapInsertableEndpoint(0),
wantSize: len(looksLikeNakedWireGuardInit),
wantIsGeneveEncap: false,
wantOk: true,
wantMetricInc: nil,
@@ -4287,7 +4276,6 @@ func TestConn_receiveIP(t *testing.T) {
ipp: netip.MustParseAddrPort("127.0.0.1:7777"),
cache: &epAddrEndpointCache{},
wantEndpointType: &lazyEndpoint{},
wantSize: len(looksLikeGeneveWireGuardInit) - packet.GeneveFixedHeaderLength,
wantIsGeneveEncap: true,
wantOk: true,
wantMetricInc: nil,
@@ -4302,7 +4290,6 @@ func TestConn_receiveIP(t *testing.T) {
wantEndpointType: &lazyEndpoint{
maybeEP: newPeerMapInsertableEndpoint(0),
},
wantSize: len(looksLikeGeneveWireGuardInit) - packet.GeneveFixedHeaderLength,
wantIsGeneveEncap: true,
wantOk: true,
wantMetricInc: nil,
@@ -4317,7 +4304,6 @@ func TestConn_receiveIP(t *testing.T) {
wantEndpointType: &lazyEndpoint{
maybeEP: newPeerMapInsertableEndpoint(mono.Now().Add(time.Hour * 24)),
},
wantSize: len(looksLikeGeneveWireGuardInit) - packet.GeneveFixedHeaderLength,
wantIsGeneveEncap: true,
wantOk: true,
wantMetricInc: nil,
@@ -4356,44 +4342,66 @@ func TestConn_receiveIP(t *testing.T) {
c.peerMap.setNodeKeyForEpAddr(tt.peerMapEpAddr, insertEPIntoPeerMap.publicKey)
}
// Allow the same input packet to be used across tests, receiveIP()
// may mutate.
inputPacket := make([]byte, len(tt.b))
copy(inputPacket, tt.b)
const initialOffset = 13
slab := make([]byte, initialOffset+len(tt.b))
copy(slab[initialOffset:], tt.b)
before := bytes.Clone(slab)
got, gotSize, gotIsGeneveEncap, gotOk := c.receiveIP(inputPacket, tt.ipp, tt.cache)
if (tt.wantEndpointType == nil) != (got == nil) {
t.Errorf("receiveIP() (tt.wantEndpointType == nil): %v != (got == nil): %v", tt.wantEndpointType == nil, got == nil)
rp := wgconn.ReceivedPacket{Size: len(tt.b), Offset: initialOffset}
gotIsGeneveEncap, gotOk := c.receiveIP(rp.Bytes(slab), tt.ipp, tt.cache, &rp)
gotEndpoint := rp.Endpoint
if (tt.wantEndpointType == nil) != (gotEndpoint == nil) {
t.Errorf("receiveIP() (tt.wantEndpointType == nil): %v != (got == nil): %v", tt.wantEndpointType == nil, gotEndpoint == nil)
}
if tt.wantEndpointType != nil && reflect.TypeOf(got).String() != reflect.TypeOf(tt.wantEndpointType).String() {
t.Errorf("receiveIP() got = %v, want %v", reflect.TypeOf(got).String(), reflect.TypeOf(tt.wantEndpointType).String())
if tt.wantEndpointType != nil && reflect.TypeOf(gotEndpoint).String() != reflect.TypeOf(tt.wantEndpointType).String() {
t.Errorf("receiveIP() got = %v, want %v", reflect.TypeOf(gotEndpoint).String(), reflect.TypeOf(tt.wantEndpointType).String())
} else {
switch ep := tt.wantEndpointType.(type) {
case *endpoint:
if ep != got.(*endpoint) {
t.Errorf("receiveIP() want [*endpoint]: %p != got [*endpoint]: %p", ep, got)
if ep != gotEndpoint.(*endpoint) {
t.Errorf("receiveIP() want [*endpoint]: %p != got [*endpoint]: %p", ep, gotEndpoint)
}
case *lazyEndpoint:
if ep.maybeEP != nil && ep.maybeEP != got.(*lazyEndpoint).maybeEP {
t.Errorf("receiveIP() want [*lazyEndpoint.maybeEP]: %p != got [*lazyEndpoint.maybeEP] %p", ep, got)
if ep.maybeEP != nil && ep.maybeEP != gotEndpoint.(*lazyEndpoint).maybeEP {
t.Errorf("receiveIP() want [*lazyEndpoint.maybeEP]: %p != got [*lazyEndpoint.maybeEP] %p", ep, gotEndpoint)
}
}
}
if gotSize != tt.wantSize {
t.Errorf("receiveIP() gotSize = %v, want %v", gotSize, tt.wantSize)
}
if gotIsGeneveEncap != tt.wantIsGeneveEncap {
t.Errorf("receiveIP() gotIsGeneveEncap = %v, want %v", gotIsGeneveEncap, tt.wantIsGeneveEncap)
}
if gotOk != tt.wantOk {
t.Errorf("receiveIP() gotOk = %v, want %v", gotOk, tt.wantOk)
}
if tt.wantOk {
wantOffset := initialOffset
wantBytes := tt.b
if tt.wantIsGeneveEncap {
wantOffset += packet.GeneveFixedHeaderLength
wantBytes = tt.b[packet.GeneveFixedHeaderLength:]
}
if rp.Offset != wantOffset {
t.Errorf("receiveIP() Offset = %d, want %d", rp.Offset, wantOffset)
}
if rp.Size != len(wantBytes) {
t.Errorf("receiveIP() Size = %d, want %d", rp.Size, len(wantBytes))
}
if got := rp.Bytes(slab); !bytes.Equal(got, wantBytes) {
t.Errorf("receiveIP() bytes = %x, want %x", got, wantBytes)
}
if !bytes.Equal(slab, before) {
t.Error("receiveIP() mutated the receive slab")
}
}
if tt.wantMetricInc != nil && tt.wantMetricInc.Value() != metricBefore+1 {
t.Errorf("receiveIP() metric %v not incremented", tt.wantMetricInc.Name())
}
if tt.cache.de != nil {
switch ep := got.(type) {
switch ep := gotEndpoint.(type) {
case *endpoint:
if tt.cache.de != ep {
t.Errorf("receiveIP() cache populated with [*endpoint] %p, want %p", tt.cache.de, ep)
@@ -4419,10 +4427,7 @@ func TestConn_receiveIP(t *testing.T) {
wantNonzeroRxStats = true
}
if tt.wantOk && wantNonzeroRxStats {
wantRxBytes := uint64(tt.wantSize)
if tt.wantIsGeneveEncap {
wantRxBytes += packet.GeneveFixedHeaderLength
}
wantRxBytes := uint64(len(tt.b))
wantPhy := map[netlogtype.Connection]netlogtype.Counts{
{Dst: tt.ipp}: {
RxPackets: 1,
+8 -8
View File
@@ -11,7 +11,6 @@ import (
"sync/atomic"
"syscall"
"golang.org/x/net/ipv6"
"tailscale.com/control/controlknobs"
"tailscale.com/net/batching"
"tailscale.com/net/netaddr"
@@ -43,8 +42,8 @@ type RebindingUDPConn struct {
// intentionally pushed closest to where read/write ops occur in order to avoid
// disrupting surrounding code that assumes nettype.PacketConn is a
// *net.UDPConn. knobs may be nil.
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, batchSize int, knobs *controlknobs.Knobs) {
upc := batching.TryUpgradeToConn(p, network, batchSize, "magicsock_udp_rxq_overflows", knobs)
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, knobs *controlknobs.Knobs) {
upc := batching.TryUpgradeToConn(p, network, "magicsock_udp_rxq_overflows", knobs)
c.pconn = upc
c.pconnAtomic.Store(&upc)
c.port = uint16(c.localAddrLocked().Port)
@@ -119,20 +118,21 @@ func (c *RebindingUDPConn) WriteWireGuardBatchTo(buffs [][]byte, addr epAddr, of
// ReadBatch is an alias for [batching.Conn.ReadBatch] with fallback to single
// packet operations if c.pconn is not a [batching.Conn].
func (c *RebindingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (int, error) {
func (c *RebindingUDPConn) ReadBatch(slab []byte, batchingPackets []batching.ReceivedPacket) (int, error) {
for {
pconn := *c.pconnAtomic.Load()
b, ok := pconn.(batching.Conn)
if !ok {
n, ap, err := c.readFromWithInitPconn(pconn, msgs[0].Buffers[0])
n, ap, err := c.readFromWithInitPconn(pconn, slab)
if err == nil {
msgs[0].N = n
msgs[0].Addr = net.UDPAddrFromAddrPort(netaddr.Unmap(ap))
batchingPackets[0].Offset = 0
batchingPackets[0].Size = n
batchingPackets[0].Source = netaddr.Unmap(ap)
return 1, nil
}
return 0, err
}
n, err := b.ReadBatch(msgs, flags)
n, err := b.ReadBatch(slab, batchingPackets)
if err != nil && pconn != c.currentConn() {
continue
}
+16 -20
View File
@@ -21,6 +21,7 @@ import (
"time"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/tun"
"gvisor.dev/gvisor/pkg/refs"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
@@ -1020,32 +1021,27 @@ func (ns *Impl) DialContextUDPWithBind(ctx context.Context, localAddr netip.Addr
return gonet.DialUDP(ns.ipstack, localAddress, remoteAddress, ipType)
}
// getInjectInboundBuffsSizes returns packet memory and a sizes slice for usage
// when calling tstun.Wrapper.InjectInboundPacketBuffer(). These are sized with
// consideration for MTU and GSO support on ns.linkEP. They should be recycled
// getInjectInboundPacketSlab returns packet memory and related descriptors to
// be used when calling [tstun.Wrapper.InjectInboundPacketBuffer]. The returned
// elements are sized with consideration for MTU and GSO support on [Impl.linkEP],
// and inter-packet spacing requirements of [tun.GSOSplit]. It should be recycled
// across subsequent inbound packet injection calls.
func (ns *Impl) getInjectInboundBuffsSizes() (buffs [][]byte, sizes []int) {
batchSize := 1
gsoEnabled := ns.linkEP.SupportedGSO() == stack.HostGSOSupported
if gsoEnabled {
batchSize = conn.IdealBatchSize
func (ns *Impl) getInjectInboundPacketSlab() (slab []byte, packets []tun.ReadPacket, writeBufs [][]byte) {
payloadLen := int(tstun.DefaultTUNMTU())
packetCount := 1
if ns.linkEP.SupportedGSO() == stack.HostGSOSupported {
payloadLen = 2 * int(ns.linkEP.GSOMaxSize())
packetCount = conn.IdealBatchSize
}
buffs = make([][]byte, batchSize)
sizes = make([]int, batchSize)
for i := 0; i < batchSize; i++ {
if i == 0 && gsoEnabled {
buffs[i] = make([]byte, tstun.PacketStartOffset+ns.linkEP.GSOMaxSize())
} else {
buffs[i] = make([]byte, tstun.PacketStartOffset+tstun.DefaultTUNMTU())
}
}
return buffs, sizes
return make([]byte, tstun.WritePacketStartOffset+payloadLen+tstun.WritePacketStartOffset),
make([]tun.ReadPacket, packetCount),
make([][]byte, packetCount)
}
// The inject goroutine reads in packets that netstack generated, and delivers
// them to the correct path.
func (ns *Impl) inject() {
inboundBuffs, inboundBuffsSizes := ns.getInjectInboundBuffsSizes()
inboundSlab, packets, writeBufs := ns.getInjectInboundPacketSlab()
for {
pkt := ns.linkEP.ReadContext(ns.ctx)
if pkt == nil {
@@ -1071,7 +1067,7 @@ func (ns *Impl) inject() {
// pkt has a non-zero refcount, so injection methods takes
// ownership of one count and will decrement on completion.
if sendToHost {
if err := ns.tundev.InjectInboundPacketBuffer(pkt, inboundBuffs, inboundBuffsSizes); err != nil {
if err := ns.tundev.InjectInboundPacketBuffer(pkt, inboundSlab, packets, writeBufs); err != nil {
ns.logf("netstack inject inbound: %v", err)
return
}
+1 -1
View File
@@ -100,7 +100,7 @@ func (t *nilTun) MTU() (int, error) { return 1420, nil }
func (t *nilTun) Name() (string, error) { return "niltun", nil }
func (t *nilTun) Events() <-chan tun.Event { return t.events }
func (t *nilTun) Read(data [][]byte, sizes []int, offset int) (int, error) {
func (t *nilTun) Read(slab []byte, packets []tun.ReadPacket) (int, error) {
<-t.closed
return 0, io.EOF
}