This commit is contained in:
2026-02-19 10:07:43 +00:00
parent 007438e372
commit 6e637ecf77
1763 changed files with 60820 additions and 279516 deletions

View File

@@ -1,63 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tstun
import (
"github.com/mdlayher/genetlink"
"github.com/mdlayher/netlink"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/unix"
)
// setLinkSpeed sets the advertised link speed of the TUN interface.
func setLinkSpeed(iface tun.Device, mbps int) error {
name, err := iface.Name()
if err != nil {
return err
}
conn, err := genetlink.Dial(&netlink.Config{Strict: true})
if err != nil {
return err
}
defer conn.Close()
f, err := conn.GetFamily(unix.ETHTOOL_GENL_NAME)
if err != nil {
return err
}
ae := netlink.NewAttributeEncoder()
ae.Nested(unix.ETHTOOL_A_LINKMODES_HEADER, func(nae *netlink.AttributeEncoder) error {
nae.String(unix.ETHTOOL_A_HEADER_DEV_NAME, name)
return nil
})
ae.Uint32(unix.ETHTOOL_A_LINKMODES_SPEED, uint32(mbps))
b, err := ae.Encode()
if err != nil {
return err
}
_, err = conn.Execute(
genetlink.Message{
Header: genetlink.Header{
Command: unix.ETHTOOL_MSG_LINKMODES_SET,
Version: unix.ETHTOOL_GENL_VERSION,
},
Data: b,
},
f.ID,
netlink.Request|netlink.Acknowledge,
)
return err
}
// setLinkAttrs sets up link attributes that can be queried by external tools.
// Its failure is non-fatal to interface bringup.
func setLinkAttrs(iface tun.Device) error {
// By default the link speed is 10Mbps, which is easily exceeded and causes monitoring tools to complain (#3933).
return setLinkSpeed(iface, unix.SPEED_UNKNOWN)
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
package tstun
import "github.com/tailscale/wireguard-go/tun"
func setLinkAttrs(iface tun.Device) error {
return nil
}

69
vendor/tailscale.com/net/tstun/netstack_disabled.go generated vendored Normal file
View File

@@ -0,0 +1,69 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ts_omit_netstack
package tstun
type netstack_PacketBuffer struct {
GSOOptions netstack_GSO
}
func (*netstack_PacketBuffer) DecRef() { panic("unreachable") }
func (*netstack_PacketBuffer) Size() int { panic("unreachable") }
type netstack_GSOType int
const (
netstack_GSONone netstack_GSOType = iota
netstack_GSOTCPv4
netstack_GSOTCPv6
netstack_GSOGvisor
)
type netstack_GSO struct {
// Type is one of GSONone, GSOTCPv4, etc.
Type netstack_GSOType
// NeedsCsum is set if the checksum offload is enabled.
NeedsCsum bool
// CsumOffset is offset after that to place checksum.
CsumOffset uint16
// Mss is maximum segment size.
MSS uint16
// L3Len is L3 (IP) header length.
L3HdrLen uint16
// MaxSize is maximum GSO packet size.
MaxSize uint32
}
func (p *netstack_PacketBuffer) NetworkHeader() slicer {
panic("unreachable")
}
func (p *netstack_PacketBuffer) TransportHeader() slicer {
panic("unreachable")
}
func (p *netstack_PacketBuffer) ToBuffer() netstack_Buffer { panic("unreachable") }
func (p *netstack_PacketBuffer) Data() asRanger {
panic("unreachable")
}
type asRanger struct{}
func (asRanger) AsRange() toSlicer { panic("unreachable") }
type toSlicer struct{}
func (toSlicer) ToSlice() []byte { panic("unreachable") }
type slicer struct{}
func (s slicer) Slice() []byte { panic("unreachable") }
type netstack_Buffer struct{}
func (netstack_Buffer) Flatten() []byte { panic("unreachable") }

22
vendor/tailscale.com/net/tstun/netstack_enabled.go generated vendored Normal file
View File

@@ -0,0 +1,22 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_netstack
package tstun
import (
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
type (
netstack_PacketBuffer = stack.PacketBuffer
netstack_GSO = stack.GSO
)
const (
netstack_GSONone = stack.GSONone
netstack_GSOTCPv4 = stack.GSOTCPv4
netstack_GSOTCPv6 = stack.GSOTCPv6
netstack_GSOGvisor = stack.GSOGvisor
)

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build plan9 || aix || solaris || illumos
//go:build aix || solaris || illumos
package tstun

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !wasm && !plan9 && !tamago && !aix && !solaris && !illumos
//go:build !wasm && !tamago && !aix && !solaris && !illumos
// Package tun creates a tuntap device, working around OS-specific
// quirks if necessary.
@@ -9,18 +9,28 @@ package tstun
import (
"errors"
"fmt"
"log"
"os"
"runtime"
"strings"
"time"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/types/logger"
)
// CrateTAP is the hook set by feature/tap.
// CreateTAP is the hook maybe set by feature/tap.
var CreateTAP feature.Hook[func(logf logger.Logf, tapName, bridgeName string) (tun.Device, error)]
// HookSetLinkAttrs is the hook maybe set by feature/linkspeed.
var HookSetLinkAttrs feature.Hook[func(tun.Device) error]
// modprobeTunHook is a Linux-specific hook to run "/sbin/modprobe tun".
var modprobeTunHook feature.Hook[func() error]
// New returns a tun.Device for the requested device name, along with
// the OS-dependent name that was allocated to the device.
func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
@@ -45,7 +55,25 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
}
dev, err = CreateTAP.Get()(logf, tapName, bridgeName)
} else {
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU()))
if runtime.GOOS == "plan9" {
cleanUpPlan9Interfaces()
}
// Try to create the TUN device up to two times. If it fails
// the first time and we're on Linux, try a desperate
// "modprobe tun" to load the tun module and try again.
for try := range 2 {
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU()))
if err == nil || !modprobeTunHook.IsSet() {
if try > 0 {
logf("created TUN device %q after doing `modprobe tun`", tunName)
}
break
}
if modprobeTunHook.Get()() != nil {
// modprobe failed; no point trying again.
break
}
}
}
if err != nil {
return nil, "", err
@@ -54,8 +82,12 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
dev.Close()
return nil, "", err
}
if err := setLinkAttrs(dev); err != nil {
logf("setting link attributes: %v", err)
if buildfeatures.HasLinkSpeed {
if f, ok := HookSetLinkAttrs.GetOk(); ok {
if err := f(dev); err != nil {
logf("setting link attributes: %v", err)
}
}
}
name, err := interfaceName(dev)
if err != nil {
@@ -65,6 +97,36 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
return dev, name, nil
}
func cleanUpPlan9Interfaces() {
maybeUnbind := func(n int) {
b, err := os.ReadFile(fmt.Sprintf("/net/ipifc/%d/status", n))
if err != nil {
return
}
status := string(b)
if !(strings.HasPrefix(status, "device maxtu ") ||
strings.Contains(status, "fd7a:115c:a1e0:")) {
return
}
f, err := os.OpenFile(fmt.Sprintf("/net/ipifc/%d/ctl", n), os.O_RDWR, 0)
if err != nil {
return
}
defer f.Close()
if _, err := fmt.Fprintf(f, "unbind\n"); err != nil {
log.Printf("unbind interface %v: %v", n, err)
return
}
log.Printf("tun: unbound stale interface %v", n)
}
// A common case: after unclean shutdown we might leave interfaces
// behind. Look for our straggler(s) and clean them up.
for n := 2; n < 5; n++ {
maybeUnbind(n)
}
}
// tunDiagnoseFailure, if non-nil, does OS-specific diagnostics of why
// TUN failed to work.
var tunDiagnoseFailure func(tunName string, logf logger.Logf, err error)

View File

@@ -17,6 +17,14 @@ import (
func init() {
tunDiagnoseFailure = diagnoseLinuxTUNFailure
modprobeTunHook.Set(func() error {
_, err := modprobeTun()
return err
})
}
func modprobeTun() ([]byte, error) {
return exec.Command("/sbin/modprobe", "tun").CombinedOutput()
}
func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error) {
@@ -36,7 +44,7 @@ func diagnoseLinuxTUNFailure(tunName string, logf logger.Logf, createErr error)
kernel := utsReleaseField(&un)
logf("Linux kernel version: %s", kernel)
modprobeOut, err := exec.Command("/sbin/modprobe", "tun").CombinedOutput()
modprobeOut, err := modprobeTun()
if err == nil {
logf("'modprobe tun' successful")
// Either tun is currently loaded, or it's statically

View File

@@ -22,10 +22,8 @@ import (
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"go4.org/mem"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/disco"
tsmetrics "tailscale.com/metrics"
"tailscale.com/net/connstats"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/packet"
"tailscale.com/net/packet/checksum"
"tailscale.com/net/tsaddr"
@@ -34,7 +32,9 @@ import (
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netlogfunc"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/netstack/gro"
@@ -204,17 +204,20 @@ type Wrapper struct {
// disableTSMPRejected disables TSMP rejected responses. For tests.
disableTSMPRejected bool
// stats maintains per-connection counters.
stats atomic.Pointer[connstats.Statistics]
// connCounter maintains per-connection counters.
connCounter syncs.AtomicValue[netlogfunc.ConnectionCounter]
captureHook syncs.AtomicValue[packet.CaptureCallback]
metrics *metrics
eventClient *eventbus.Client
discoKeyAdvertisementPub *eventbus.Publisher[DiscoKeyAdvertisement]
}
type metrics struct {
inboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
outboundDroppedPacketsTotal *tsmetrics.MultiLabelMap[usermetric.DropLabels]
inboundDroppedPacketsTotal *usermetric.MultiLabelMap[usermetric.DropLabels]
outboundDroppedPacketsTotal *usermetric.MultiLabelMap[usermetric.DropLabels]
}
func registerMetrics(reg *usermetric.Registry) *metrics {
@@ -228,7 +231,7 @@ func registerMetrics(reg *usermetric.Registry) *metrics {
type tunInjectedRead struct {
// Only one of packet or data should be set, and are read in that order of
// precedence.
packet *stack.PacketBuffer
packet *netstack_PacketBuffer
data []byte
}
@@ -255,15 +258,15 @@ func (w *Wrapper) Start() {
close(w.startCh)
}
func WrapTAP(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
return wrap(logf, tdev, true, m)
func WrapTAP(logf logger.Logf, tdev tun.Device, m *usermetric.Registry, bus *eventbus.Bus) *Wrapper {
return wrap(logf, tdev, true, m, bus)
}
func Wrap(logf logger.Logf, tdev tun.Device, m *usermetric.Registry) *Wrapper {
return wrap(logf, tdev, false, m)
func Wrap(logf logger.Logf, tdev tun.Device, m *usermetric.Registry, bus *eventbus.Bus) *Wrapper {
return wrap(logf, tdev, false, m, bus)
}
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry) *Wrapper {
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry, bus *eventbus.Bus) *Wrapper {
logf = logger.WithPrefix(logf, "tstun: ")
w := &Wrapper{
logf: logf,
@@ -284,6 +287,9 @@ func wrap(logf logger.Logf, tdev tun.Device, isTAP bool, m *usermetric.Registry)
metrics: registerMetrics(m),
}
w.eventClient = bus.Client("net.tstun")
w.discoKeyAdvertisementPub = eventbus.Publish[DiscoKeyAdvertisement](w.eventClient)
w.vectorBuffer = make([][]byte, tdev.BatchSize())
for i := range w.vectorBuffer {
w.vectorBuffer[i] = make([]byte, maxBufferSize)
@@ -312,7 +318,9 @@ func (t *Wrapper) now() time.Time {
//
// The map ownership passes to the Wrapper. It must be non-nil.
func (t *Wrapper) SetDestIPActivityFuncs(m map[netip.Addr]func()) {
t.destIPActivity.Store(m)
if buildfeatures.HasLazyWG {
t.destIPActivity.Store(m)
}
}
// SetDiscoKey sets the current discovery key.
@@ -356,6 +364,7 @@ func (t *Wrapper) Close() error {
close(t.vectorOutbound)
t.outboundMu.Unlock()
err = t.tdev.Close()
t.eventClient.Close()
})
return err
}
@@ -948,12 +957,14 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
if buildfeatures.HasLazyWG {
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
}
}
}
if captHook != nil {
if buildfeatures.HasCapture && captHook != nil {
captHook(packet.FromLocal, t.now(), p.Buffer(), p.CaptureMeta)
}
if !t.disableFilter {
@@ -964,6 +975,11 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
continue
}
}
if buildfeatures.HasNetLog {
if update := t.connCounter.Load(); update != nil {
updateConnCounter(update, p.Buffer(), false)
}
}
// Make sure to do SNAT after filtering, so that any flow tracking in
// the filter sees the original source address. See #12133.
@@ -973,9 +989,6 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
panic(fmt.Sprintf("short copy: %d != %d", n, len(data)-res.dataOffset))
}
sizes[buffsPos] = n
if stats := t.stats.Load(); stats != nil {
stats.UpdateTxVirtual(p.Buffer())
}
buffsPos++
}
if buffsGRO != nil {
@@ -998,7 +1011,10 @@ const (
minTCPHeaderSize = 20
)
func stackGSOToTunGSO(pkt []byte, gso stack.GSO) (tun.GSOOptions, error) {
func stackGSOToTunGSO(pkt []byte, gso netstack_GSO) (tun.GSOOptions, error) {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
options := tun.GSOOptions{
CsumStart: gso.L3HdrLen,
CsumOffset: gso.CsumOffset,
@@ -1006,12 +1022,12 @@ func stackGSOToTunGSO(pkt []byte, gso stack.GSO) (tun.GSOOptions, error) {
NeedsCsum: gso.NeedsCsum,
}
switch gso.Type {
case stack.GSONone:
case netstack_GSONone:
options.GSOType = tun.GSONone
return options, nil
case stack.GSOTCPv4:
case netstack_GSOTCPv4:
options.GSOType = tun.GSOTCPv4
case stack.GSOTCPv6:
case netstack_GSOTCPv6:
options.GSOType = tun.GSOTCPv6
default:
return tun.GSOOptions{}, fmt.Errorf("unsupported gVisor GSOType: %v", gso.Type)
@@ -1034,7 +1050,10 @@ func stackGSOToTunGSO(pkt []byte, gso stack.GSO) (tun.GSOOptions, error) {
// both before and after partial checksum updates where later checksum
// offloading still expects a partial checksum.
// TODO(jwhited): plumb partial checksum awareness into net/packet/checksum.
func invertGSOChecksum(pkt []byte, gso stack.GSO) {
func invertGSOChecksum(pkt []byte, gso netstack_GSO) {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
if gso.NeedsCsum != true {
return
}
@@ -1048,10 +1067,13 @@ func invertGSOChecksum(pkt []byte, gso stack.GSO) {
// injectedRead handles injected reads, which bypass filters.
func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []int, offset int) (n int, err error) {
var gso stack.GSO
var gso netstack_GSO
pkt := outBuffs[0][offset:]
if res.packet != nil {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
bufN := copy(pkt, res.packet.NetworkHeader().Slice())
bufN += copy(pkt[bufN:], res.packet.TransportHeader().Slice())
bufN += copy(pkt[bufN:], res.packet.Data().AsRange().ToSlice())
@@ -1074,9 +1096,11 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
pc.snat(p)
invertGSOChecksum(pkt, gso)
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
if buildfeatures.HasLazyWG {
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {
fn()
}
}
}
@@ -1089,9 +1113,11 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
n, err = tun.GSOSplit(pkt, gsoOptions, outBuffs, sizes, offset)
}
if stats := t.stats.Load(); stats != nil {
for i := 0; i < n; i++ {
stats.UpdateTxVirtual(outBuffs[i][offset : offset+sizes[i]])
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)
}
}
}
@@ -1100,6 +1126,13 @@ func (t *Wrapper) injectedRead(res tunInjectedRead, outBuffs [][]byte, sizes []i
return n, err
}
// DiscoKeyAdvertisement is a TSMP message used for distributing disco keys.
// This struct is used an an event on the [eventbus.Bus].
type DiscoKeyAdvertisement struct {
Src netip.Addr // Src field is populated by the IP header of the packet, not from the payload itself.
Key key.DiscoPublic
}
func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook packet.CaptureCallback, pc *peerConfigTable, gro *gro.GRO) (filter.Response, *gro.GRO) {
if captHook != nil {
captHook(packet.FromPeer, t.now(), p.Buffer(), p.CaptureMeta)
@@ -1110,6 +1143,12 @@ func (t *Wrapper) filterPacketInboundFromWireGuard(p *packet.Parsed, captHook pa
t.noteActivity()
t.injectOutboundPong(p, pingReq)
return filter.DropSilently, gro
} else if discoKeyAdvert, ok := p.AsTSMPDiscoAdvertisement(); ok {
t.discoKeyAdvertisementPub.Publish(DiscoKeyAdvertisement{
Src: discoKeyAdvert.Src,
Key: discoKeyAdvert.Key,
})
return filter.DropSilently, gro
} else if data, ok := p.AsTSMPPong(); ok {
if f := t.OnTSMPPongReceived; f != nil {
f(data)
@@ -1257,9 +1296,11 @@ func (t *Wrapper) Write(buffs [][]byte, offset int) (int, error) {
}
func (t *Wrapper) tdevWrite(buffs [][]byte, offset int) (int, error) {
if stats := t.stats.Load(); stats != nil {
for i := range buffs {
stats.UpdateRxVirtual((buffs)[i][offset:])
if buildfeatures.HasNetLog {
if update := t.connCounter.Load(); update != nil {
for i := range buffs {
updateConnCounter(update, buffs[i][offset:], true)
}
}
}
return t.tdev.Write(buffs, offset)
@@ -1297,7 +1338,10 @@ func (t *Wrapper) SetJailedFilter(filt *filter.Filter) {
//
// This path is typically used to deliver synthesized packets to the
// host networking stack.
func (t *Wrapper) InjectInboundPacketBuffer(pkt *stack.PacketBuffer, buffs [][]byte, sizes []int) error {
func (t *Wrapper) InjectInboundPacketBuffer(pkt *netstack_PacketBuffer, buffs [][]byte, sizes []int) error {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
buf := buffs[0][PacketStartOffset:]
bufN := copy(buf, pkt.NetworkHeader().Slice())
@@ -1436,7 +1480,10 @@ func (t *Wrapper) InjectOutbound(pkt []byte) error {
// InjectOutboundPacketBuffer logically behaves as InjectOutbound. It takes ownership of one
// reference count on the packet, and the packet may be mutated. The packet refcount will be
// decremented after the injected buffer has been read.
func (t *Wrapper) InjectOutboundPacketBuffer(pkt *stack.PacketBuffer) error {
func (t *Wrapper) InjectOutboundPacketBuffer(pkt *netstack_PacketBuffer) error {
if !buildfeatures.HasNetstack {
panic("unreachable")
}
size := pkt.Size()
if size > MaxPacketSize {
pkt.DecRef()
@@ -1472,10 +1519,12 @@ func (t *Wrapper) Unwrap() tun.Device {
return t.tdev
}
// SetStatistics specifies a per-connection statistics aggregator.
// SetConnectionCounter specifies a per-connection statistics aggregator.
// Nil may be specified to disable statistics gathering.
func (t *Wrapper) SetStatistics(stats *connstats.Statistics) {
t.stats.Store(stats)
func (t *Wrapper) SetConnectionCounter(fn netlogfunc.ConnectionCounter) {
if buildfeatures.HasNetLog {
t.connCounter.Store(fn)
}
}
var (
@@ -1491,5 +1540,18 @@ var (
)
func (t *Wrapper) InstallCaptureHook(cb packet.CaptureCallback) {
if !buildfeatures.HasCapture {
return
}
t.captureHook.Store(cb)
}
func updateConnCounter(update netlogfunc.ConnectionCounter, b []byte, receive bool) {
var p packet.Parsed
p.Decode(b)
if receive {
update(p.IPProto, p.Dst, p.Src, 1, len(b), true)
} else {
update(p.IPProto, p.Src, p.Dst, 1, len(b), false)
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !ts_omit_gro
package tstun
import (

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
//go:build !linux || ts_omit_gro
package tstun