mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-14 06:48:32 -04:00
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 at1e69418. 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) Test1e69418After 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 Side1e69418After 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>
510 lines
14 KiB
Go
510 lines
14 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package tap registers Tailscale's experimental (demo) Linux TAP (Layer 2) support.
|
|
package tap
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
|
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
|
"github.com/tailscale/wireguard-go/tun"
|
|
"golang.org/x/sys/unix"
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
|
"tailscale.com/net/netaddr"
|
|
"tailscale.com/net/packet"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/net/tstun"
|
|
"tailscale.com/syncs"
|
|
"tailscale.com/types/ipproto"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
// TODO: this was randomly generated once. Maybe do it per process start? But
|
|
// then an upgraded tailscaled would be visible to devices behind it. So
|
|
// maybe instead make it a function of the tailscaled's wireguard public key?
|
|
// For now just hard code it.
|
|
var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93}
|
|
|
|
const tapDebug = tstun.TAPDebug
|
|
|
|
func init() {
|
|
tstun.CreateTAP.Set(createTAPLinux)
|
|
}
|
|
|
|
func createTAPLinux(logf logger.Logf, tapName, bridgeName string) (tun.Device, error) {
|
|
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dev, err := openDevice(logf, fd, tapName, bridgeName)
|
|
if err != nil {
|
|
unix.Close(fd)
|
|
return nil, err
|
|
}
|
|
|
|
return dev, nil
|
|
}
|
|
|
|
func openDevice(logf logger.Logf, fd int, tapName, bridgeName string) (tun.Device, error) {
|
|
ifr, err := unix.NewIfreq(tapName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Flags are stored as a uint16 in the ifreq union.
|
|
ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI)
|
|
if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := run("ip", "link", "set", "dev", tapName, "up"); err != nil {
|
|
return nil, err
|
|
}
|
|
if bridgeName != "" {
|
|
if err := run("brctl", "addif", bridgeName, tapName); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return newTAPDevice(logf, fd, tapName)
|
|
}
|
|
|
|
type etherType [2]byte
|
|
|
|
var (
|
|
etherTypeARP = etherType{0x08, 0x06}
|
|
etherTypeIPv4 = etherType{0x08, 0x00}
|
|
etherTypeIPv6 = etherType{0x86, 0xDD}
|
|
)
|
|
|
|
const (
|
|
ipv4HeaderLen = 20
|
|
ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype
|
|
)
|
|
|
|
const (
|
|
consumePacket = true
|
|
passOnPacket = false
|
|
)
|
|
|
|
// 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 {
|
|
// Corrupt. Ignore.
|
|
if tapDebug {
|
|
t.logf("tap: short TAP frame")
|
|
}
|
|
return consumePacket
|
|
}
|
|
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
|
|
_ = ethDstMAC
|
|
et := etherType{ethBuf[12], ethBuf[13]}
|
|
switch et {
|
|
default:
|
|
if tapDebug {
|
|
t.logf("tap: ignoring etherType %v", et)
|
|
}
|
|
return consumePacket // filter out packet we should ignore
|
|
case etherTypeIPv6:
|
|
// TODO: support DHCPv6/ND/etc later. For now pass all to WireGuard.
|
|
if tapDebug {
|
|
t.logf("tap: ignoring IPv6 %v", et)
|
|
}
|
|
return passOnPacket
|
|
case etherTypeIPv4:
|
|
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen {
|
|
// Bogus IPv4. Eat.
|
|
if tapDebug {
|
|
t.logf("tap: short ipv4")
|
|
}
|
|
return consumePacket
|
|
}
|
|
return t.handleDHCPRequest(ethBuf)
|
|
case etherTypeARP:
|
|
arpPacket := header.ARP(ethBuf[ethernetFrameSize:])
|
|
if !arpPacket.IsValid() {
|
|
// Bogus ARP. Eat.
|
|
return consumePacket
|
|
}
|
|
switch arpPacket.Op() {
|
|
case header.ARPRequest:
|
|
req := arpPacket // better name at this point
|
|
buf := make([]byte, header.EthernetMinimumSize+header.ARPSize)
|
|
|
|
// Our ARP "Table" of one:
|
|
var srcMAC [6]byte
|
|
copy(srcMAC[:], ethSrcMAC)
|
|
if old := t.destMAC(); old != srcMAC {
|
|
t.destMACAtomic.Store(srcMAC)
|
|
}
|
|
|
|
eth := header.Ethernet(buf)
|
|
eth.Encode(&header.EthernetFields{
|
|
SrcAddr: tcpip.LinkAddress(ourMAC[:]),
|
|
DstAddr: tcpip.LinkAddress(ethSrcMAC),
|
|
Type: 0x0806, // arp
|
|
})
|
|
res := header.ARP(buf[header.EthernetMinimumSize:])
|
|
res.SetIPv4OverEthernet()
|
|
res.SetOp(header.ARPReply)
|
|
|
|
// If the client's asking about their own IP, tell them it's
|
|
// their own MAC. TODO(bradfitz): remove String allocs.
|
|
if net.IP(req.ProtocolAddressTarget()).String() == t.clientIPv4.Load() {
|
|
copy(res.HardwareAddressSender(), ethSrcMAC)
|
|
} else {
|
|
copy(res.HardwareAddressSender(), ourMAC[:])
|
|
}
|
|
|
|
copy(res.ProtocolAddressSender(), req.ProtocolAddressTarget())
|
|
copy(res.HardwareAddressTarget(), req.HardwareAddressSender())
|
|
copy(res.ProtocolAddressTarget(), req.ProtocolAddressSender())
|
|
|
|
n, err := t.WriteEthernet(buf)
|
|
if tapDebug {
|
|
t.logf("tap: wrote ARP reply %v, %v", n, err)
|
|
}
|
|
}
|
|
|
|
return consumePacket
|
|
}
|
|
}
|
|
|
|
var (
|
|
// routerIP is the IP address of the DHCP server.
|
|
routerIP = net.ParseIP(tsaddr.TailscaleServiceIPString)
|
|
// cgnatNetMask is the netmask of the 100.64.0.0/10 CGNAT range.
|
|
cgnatNetMask = net.IPMask(net.ParseIP("255.192.0.0").To4())
|
|
)
|
|
|
|
// parsedPacketPool holds a pool of Parsed structs for use in filtering.
|
|
// This is needed because escape analysis cannot see that parsed packets
|
|
// do not escape through {Pre,Post}Filter{In,Out}.
|
|
var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
|
|
|
|
// handleDHCPRequest handles receiving a raw TAP ethernet frame and reports whether
|
|
// it's been handled as a DHCP request. That is, it reports whether the frame should
|
|
// be ignored by the caller and not passed on.
|
|
func (t *tapDevice) handleDHCPRequest(ethBuf []byte) bool {
|
|
const udpHeader = 8
|
|
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen+udpHeader {
|
|
if tapDebug {
|
|
t.logf("tap: DHCP short")
|
|
}
|
|
return passOnPacket
|
|
}
|
|
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
|
|
|
|
if string(ethDstMAC) != "\xff\xff\xff\xff\xff\xff" {
|
|
// Not a broadcast
|
|
if tapDebug {
|
|
t.logf("tap: dhcp no broadcast")
|
|
}
|
|
return passOnPacket
|
|
}
|
|
|
|
p := parsedPacketPool.Get().(*packet.Parsed)
|
|
defer parsedPacketPool.Put(p)
|
|
p.Decode(ethBuf[ethernetFrameSize:])
|
|
|
|
if p.IPProto != ipproto.UDP || p.Src.Port() != 68 || p.Dst.Port() != 67 {
|
|
// Not a DHCP request.
|
|
if tapDebug {
|
|
t.logf("tap: DHCP wrong meta: %+v", p)
|
|
}
|
|
return passOnPacket
|
|
}
|
|
|
|
dp, err := dhcpv4.FromBytes(ethBuf[ethernetFrameSize+ipv4HeaderLen+udpHeader:])
|
|
if err != nil {
|
|
// Bogus. Trash it.
|
|
if tapDebug {
|
|
t.logf("tap: DHCP FromBytes bad")
|
|
}
|
|
return consumePacket
|
|
}
|
|
if tapDebug {
|
|
t.logf("tap: DHCP request: %+v", dp)
|
|
}
|
|
switch dp.MessageType() {
|
|
case dhcpv4.MessageTypeDiscover:
|
|
ips := t.clientIPv4.Load()
|
|
if ips == "" {
|
|
t.logf("tap: DHCP no client IP")
|
|
return consumePacket
|
|
}
|
|
offer, err := dhcpv4.New(
|
|
dhcpv4.WithReply(dp),
|
|
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
|
dhcpv4.WithRouter(routerIP), // the default route
|
|
dhcpv4.WithDNS(routerIP),
|
|
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
|
|
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
|
|
dhcpv4.WithYourIP(net.ParseIP(ips)),
|
|
dhcpv4.WithLeaseTime(3600), // hour works
|
|
//dhcpv4.WithHwAddr(ethSrcMAC),
|
|
dhcpv4.WithNetmask(cgnatNetMask),
|
|
//dhcpv4.WithTransactionID(dp.TransactionID),
|
|
)
|
|
if err != nil {
|
|
t.logf("error building DHCP offer: %v", err)
|
|
return consumePacket
|
|
}
|
|
// Make a layer 2 packet to write out:
|
|
pkt := packLayer2UDP(
|
|
offer.ToBytes(),
|
|
ourMAC, ethSrcMAC,
|
|
netip.AddrPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
|
|
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
|
|
)
|
|
|
|
n, err := t.WriteEthernet(pkt)
|
|
if tapDebug {
|
|
t.logf("tap: wrote DHCP OFFER %v, %v", n, err)
|
|
}
|
|
case dhcpv4.MessageTypeRequest:
|
|
ips := t.clientIPv4.Load()
|
|
if ips == "" {
|
|
t.logf("tap: DHCP no client IP")
|
|
return consumePacket
|
|
}
|
|
ack, err := dhcpv4.New(
|
|
dhcpv4.WithReply(dp),
|
|
dhcpv4.WithMessageType(dhcpv4.MessageTypeAck),
|
|
dhcpv4.WithDNS(routerIP),
|
|
dhcpv4.WithRouter(routerIP), // the default route
|
|
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
|
|
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
|
|
dhcpv4.WithYourIP(net.ParseIP(ips)), // Hello world
|
|
dhcpv4.WithLeaseTime(3600), // hour works
|
|
dhcpv4.WithNetmask(cgnatNetMask),
|
|
)
|
|
if err != nil {
|
|
t.logf("error building DHCP ack: %v", err)
|
|
return consumePacket
|
|
}
|
|
// Make a layer 2 packet to write out:
|
|
pkt := packLayer2UDP(
|
|
ack.ToBytes(),
|
|
ourMAC, ethSrcMAC,
|
|
netip.AddrPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
|
|
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
|
|
)
|
|
n, err := t.WriteEthernet(pkt)
|
|
if tapDebug {
|
|
t.logf("tap: wrote DHCP ACK %v, %v", n, err)
|
|
}
|
|
default:
|
|
if tapDebug {
|
|
t.logf("tap: unknown DHCP type")
|
|
}
|
|
}
|
|
return consumePacket
|
|
}
|
|
|
|
func writeEthernetFrame(buf []byte, srcMAC, dstMAC net.HardwareAddr, proto tcpip.NetworkProtocolNumber) {
|
|
// Ethernet header
|
|
eth := header.Ethernet(buf)
|
|
eth.Encode(&header.EthernetFields{
|
|
SrcAddr: tcpip.LinkAddress(srcMAC),
|
|
DstAddr: tcpip.LinkAddress(dstMAC),
|
|
Type: proto,
|
|
})
|
|
}
|
|
|
|
func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netip.AddrPort) []byte {
|
|
buf := make([]byte, header.EthernetMinimumSize+header.UDPMinimumSize+header.IPv4MinimumSize+len(payload))
|
|
payloadStart := len(buf) - len(payload)
|
|
copy(buf[payloadStart:], payload)
|
|
srcB := src.Addr().As4()
|
|
srcIP := tcpip.AddrFromSlice(srcB[:])
|
|
dstB := dst.Addr().As4()
|
|
dstIP := tcpip.AddrFromSlice(dstB[:])
|
|
// Ethernet header
|
|
writeEthernetFrame(buf, srcMAC, dstMAC, ipv4.ProtocolNumber)
|
|
// IP header
|
|
ipbuf := buf[header.EthernetMinimumSize:]
|
|
ip := header.IPv4(ipbuf)
|
|
ip.Encode(&header.IPv4Fields{
|
|
TotalLength: uint16(len(ipbuf)),
|
|
TTL: 65,
|
|
Protocol: uint8(udp.ProtocolNumber),
|
|
SrcAddr: srcIP,
|
|
DstAddr: dstIP,
|
|
})
|
|
ip.SetChecksum(^ip.CalculateChecksum())
|
|
// UDP header
|
|
u := header.UDP(buf[header.EthernetMinimumSize+header.IPv4MinimumSize:])
|
|
u.Encode(&header.UDPFields{
|
|
SrcPort: src.Port(),
|
|
DstPort: dst.Port(),
|
|
Length: uint16(header.UDPMinimumSize + len(payload)),
|
|
})
|
|
// Calculate the UDP pseudo-header checksum.
|
|
xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, srcIP, dstIP, uint16(len(u)))
|
|
// Calculate the UDP checksum and set it.
|
|
xsum = checksum.Checksum(payload, xsum)
|
|
u.SetChecksum(^u.CalculateChecksum(xsum))
|
|
return []byte(buf)
|
|
}
|
|
|
|
func run(prog string, args ...string) error {
|
|
cmd := exec.Command(prog, args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("error running %v: %v", cmd, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *tapDevice) destMAC() [6]byte {
|
|
return t.destMACAtomic.Load()
|
|
}
|
|
|
|
func newTAPDevice(logf logger.Logf, fd int, tapName string) (tun.Device, error) {
|
|
err := unix.SetNonblock(fd, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file := os.NewFile(uintptr(fd), "/dev/tap")
|
|
d := &tapDevice{
|
|
logf: logf,
|
|
file: file,
|
|
events: make(chan tun.Event),
|
|
name: tapName,
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
type tapDevice struct {
|
|
file *os.File
|
|
logf func(format string, args ...any)
|
|
events chan tun.Event
|
|
name string
|
|
closeOnce sync.Once
|
|
clientIPv4 syncs.AtomicValue[string]
|
|
|
|
destMACAtomic syncs.AtomicValue[[6]byte]
|
|
}
|
|
|
|
var _ tstun.SetIPer = (*tapDevice)(nil)
|
|
|
|
func (t *tapDevice) SetIP(ipV4, ipV6TODO netip.Addr) error {
|
|
t.clientIPv4.Store(ipV4.String())
|
|
return nil
|
|
}
|
|
|
|
func (t *tapDevice) File() *os.File {
|
|
return t.file
|
|
}
|
|
|
|
func (t *tapDevice) Name() (string, error) {
|
|
return t.name, nil
|
|
}
|
|
|
|
// 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(buf[:n]) {
|
|
return 0, nil
|
|
}
|
|
packets[0] = tun.ReadPacket{
|
|
Offset: tun.ReadPacketSpacing + ethernetFrameSize,
|
|
Size: n - ethernetFrameSize,
|
|
}
|
|
return 1, nil
|
|
}
|
|
|
|
// WriteEthernet writes a raw ethernet frame to the TAP device.
|
|
func (t *tapDevice) WriteEthernet(buf []byte) (int, error) {
|
|
return t.file.Write(buf)
|
|
}
|
|
|
|
// ethBufPool holds a pool of bytes.Buffers for use in [tapDevice.Write].
|
|
var ethBufPool = syncs.Pool[*bytes.Buffer]{New: func() *bytes.Buffer { return new(bytes.Buffer) }}
|
|
|
|
// Write writes a raw IP packet to the TAP device. It adds the ethernet frame header.
|
|
func (t *tapDevice) Write(buffs [][]byte, offset int) (int, error) {
|
|
errs := make([]error, 0)
|
|
wrote := 0
|
|
m := t.destMAC()
|
|
dstMac := net.HardwareAddr(m[:])
|
|
buf := ethBufPool.Get()
|
|
defer ethBufPool.Put(buf)
|
|
for _, buff := range buffs {
|
|
buf.Reset()
|
|
buf.Grow(header.EthernetMinimumSize + len(buff) - offset)
|
|
|
|
var ebuf [14]byte
|
|
switch buff[offset] >> 4 {
|
|
case 4:
|
|
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv4.ProtocolNumber)
|
|
case 6:
|
|
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv6.ProtocolNumber)
|
|
default:
|
|
continue
|
|
}
|
|
buf.Write(ebuf[:])
|
|
buf.Write(buff[offset:])
|
|
_, err := t.WriteEthernet(buf.Bytes())
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
} else {
|
|
wrote++
|
|
}
|
|
}
|
|
return wrote, errors.Join(errs...)
|
|
}
|
|
|
|
func (t *tapDevice) MTU() (int, error) {
|
|
ifr, err := unix.NewIfreq(t.name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := unix.IoctlIfreq(int(t.file.Fd()), unix.SIOCGIFMTU, ifr); err != nil {
|
|
return 0, err
|
|
}
|
|
return int(ifr.Uint32()), nil
|
|
}
|
|
|
|
func (t *tapDevice) Events() <-chan tun.Event {
|
|
return t.events
|
|
}
|
|
|
|
func (t *tapDevice) Close() error {
|
|
var err error
|
|
t.closeOnce.Do(func() {
|
|
close(t.events)
|
|
err = t.file.Close()
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (t *tapDevice) BatchSize() int {
|
|
return 1
|
|
}
|