Files
tailscale/wgengine/wgcfg/device_test.go
Jordan Whited 8fc6dca15e 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>
2026-09-04 09:32:55 -07:00

144 lines
4.1 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package wgcfg
import (
"io"
"net/netip"
"os"
"strings"
"testing"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/key"
)
func TestNewPeerLookupFunc(t *testing.T) {
k1, _ := newK()
k2, _ := newK()
ip2 := netip.MustParsePrefix("10.0.0.2/32")
k3, _ := newK()
dev := NewDevice(newNilTun(), new(noopBind), device.NewLogger(device.LogLevelError, "test"))
defer dev.Close()
// peers is the live per-peer config source, standing in for what
// LocalBackend provides via wgengine.Engine.SetPeerConfigFunc.
psk2 := device.NoisePresharedKey{1, 2, 3}
peers := map[device.NoisePublicKey]PeerConfig{
k2.Raw32(): {AllowedIPs: []netip.Prefix{ip2}, PresharedKey: psk2},
}
dev.SetPeerLookupFunc(NewPeerLookupFunc(dev.Bind(), t.Logf, func(pubk device.NoisePublicKey) (PeerConfig, bool) {
conf, ok := peers[pubk]
return conf, ok
}))
t.Run("lazy-creation", func(t *testing.T) {
// A peer known to the config source should be creatable on
// demand via LookupPeer.
if p := dev.LookupPeer(k2.Raw32()); p == nil {
t.Fatal("expected peer k2 to exist via LookupPeer")
}
got, err := dev.IpcGet()
if err != nil {
t.Fatal(err)
}
if want := "preshared_key=0102030000000000000000000000000000000000000000000000000000000000"; !strings.Contains(got, want) {
t.Fatalf("device config does not contain %q:\n%s", want, got)
}
// An unknown peer should not be found.
if p := dev.LookupPeer(k3.Raw32()); p != nil {
t.Fatal("expected unknown peer k3 to not exist")
}
})
t.Run("remove-peer", func(t *testing.T) {
delete(peers, k2.Raw32())
dev.RemoveMatchingPeers(func(pk device.NoisePublicKey) bool {
_, ok := peers[pk]
return !ok
})
if p := dev.LookupPeer(k2.Raw32()); p != nil {
t.Fatal("expected peer k2 to not exist after removal")
}
})
t.Run("self-key-not-peer", func(t *testing.T) {
// The device's own key should not be a peer.
if p := dev.LookupPeer(k1.Raw32()); p != nil {
t.Fatal("expected own key to not be a peer")
}
})
}
func newK() (key.NodePublic, key.NodePrivate) {
k := key.NewNode()
return k.Public(), k
}
// TODO: replace with a loopback tunnel
type nilTun struct {
events chan tun.Event
closed chan struct{}
}
func newNilTun() tun.Device {
return &nilTun{
events: make(chan tun.Event),
closed: make(chan struct{}),
}
}
func (t *nilTun) File() *os.File { return nil }
func (t *nilTun) Flush() error { return nil }
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(slab []byte, packets []tun.ReadPacket) (int, error) {
<-t.closed
return 0, io.EOF
}
func (t *nilTun) Write(data [][]byte, offset int) (int, error) {
<-t.closed
return 0, io.EOF
}
func (t *nilTun) Close() error {
close(t.events)
close(t.closed)
return nil
}
func (t *nilTun) BatchSize() int { return 1 }
// A noopBind is a conn.Bind that does no actual binding work.
type noopBind struct{}
func (noopBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
return nil, 1, nil
}
func (noopBind) Close() error { return nil }
func (noopBind) SetMark(mark uint32) error { return nil }
func (noopBind) Send(b [][]byte, ep conn.Endpoint, offset int) error { return nil }
func (noopBind) ParseEndpoint(s string) (conn.Endpoint, error) {
return dummyEndpoint(s), nil
}
func (noopBind) BatchSize() int { return 1 }
// A dummyEndpoint is a string holding the endpoint destination.
type dummyEndpoint string
func (e dummyEndpoint) ClearSrc() {}
func (e dummyEndpoint) SrcToString() string { return "" }
func (e dummyEndpoint) DstToString() string { return string(e) }
func (e dummyEndpoint) DstToBytes() []byte { return nil }
func (e dummyEndpoint) DstIP() netip.Addr { return netip.Addr{} }
func (dummyEndpoint) SrcIP() netip.Addr { return netip.Addr{} }