Add tun device support

This commit is contained in:
世界
2026-01-07 14:07:16 +08:00
parent b5ac99cdf5
commit a8153dda2d
13 changed files with 160 additions and 27 deletions

View File

@@ -0,0 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package osrouter registers support for OSRouter if it's not disabled via the
// ts_omit_osrouter build tag.
package osrouter

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_osrouter
package osrouter
import _ "github.com/sagernet/tailscale/wgengine/router/osrouter"

1
go.mod
View File

@@ -55,7 +55,6 @@ require (
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745

4
go.sum
View File

@@ -339,8 +339,6 @@ github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+y
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
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=
@@ -484,7 +482,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

@@ -51,20 +51,31 @@ func captivePortalHealthChange(b *LocalBackend, state *health.State) {
if isConnectivityImpacted {
b.logf("health: connectivity impacted; triggering captive portal detection")
// Ensure that we select on captiveCtx so that we can time out
// triggering captive portal detection if the backend is shutdown.
select {
case b.needsCaptiveDetection <- true:
case <-ctx.Done():
}
sendNeedsCaptiveDetection(ctx, b.needsCaptiveDetection, true)
} else {
// If connectivity is not impacted, we know for sure we're not behind a captive portal,
// so drop any warning, and signal that we don't need captive portal detection.
b.health.SetHealthy(captivePortalWarnable)
select {
case b.needsCaptiveDetection <- false:
case <-ctx.Done():
}
sendNeedsCaptiveDetection(ctx, b.needsCaptiveDetection, false)
}
}
func sendNeedsCaptiveDetection(ctx context.Context, needsDetection chan bool, needsCaptiveDetection bool) {
select {
case needsDetection <- needsCaptiveDetection:
return
case <-ctx.Done():
return
default:
}
select {
case <-needsDetection:
default:
}
select {
case needsDetection <- needsCaptiveDetection:
case <-ctx.Done():
default:
}
}

View File

@@ -517,7 +517,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
clock: clock,
captiveCtx: captiveCtx,
captiveCancel: nil, // so that we start checkCaptivePortalLoop when Running
needsCaptiveDetection: make(chan bool),
needsCaptiveDetection: make(chan bool, 1),
lookupHook: lookupHook,
onlyTCP443: onlyTCP443,
}

View File

@@ -37,9 +37,9 @@ import (
"github.com/sagernet/tailscale/tailcfg"
"github.com/sagernet/tailscale/types/key"
"github.com/sagernet/tailscale/types/logger"
wgconn "github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
wgconn "github.com/sagernet/wireguard-go/conn"
"github.com/sagernet/wireguard-go/device"
"github.com/sagernet/wireguard-go/tun"
"go4.org/netipx"
)

View File

@@ -31,6 +31,7 @@ import (
"github.com/sagernet/tailscale/control/controlclient"
"github.com/sagernet/tailscale/envknob"
_ "github.com/sagernet/tailscale/feature/c2n"
_ "github.com/sagernet/tailscale/feature/condregister/osrouter"
_ "github.com/sagernet/tailscale/feature/condregister/oauthkey"
_ "github.com/sagernet/tailscale/feature/condregister/portmapper"
_ "github.com/sagernet/tailscale/feature/condregister/useproxy"
@@ -64,6 +65,8 @@ import (
"github.com/sagernet/tailscale/util/testenv"
"github.com/sagernet/tailscale/wgengine"
"github.com/sagernet/tailscale/wgengine/netstack"
"github.com/sagernet/tailscale/wgengine/router"
wgTun "github.com/sagernet/wireguard-go/tun"
)
// Server is an embedded Tailscale server.
@@ -141,6 +144,8 @@ type Server struct {
OnlyTCP443 bool
DNS dns.OSConfigurator
HTTPClient *http.Client
TunDevice wgTun.Device
Router router.Router
getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error)
@@ -595,7 +600,7 @@ func (s *Server) start() (reterr error) {
s.dialer = &tsdial.Dialer{Logf: tsLogf, Dialer: s.Dialer} // mutated below (before used)
s.dialer.SetBus(sys.Bus.Get())
eng, err := wgengine.NewUserspaceEngine(tsLogf, wgengine.Config{
engineConfig := wgengine.Config{
DNS: s.DNS,
EventBus: sys.Bus.Get(),
ListenPort: s.Port,
@@ -605,7 +610,20 @@ func (s *Server) start() (reterr error) {
ControlKnobs: sys.ControlKnobs(),
HealthTracker: sys.HealthTracker.Get(),
Metrics: sys.UserMetricsRegistry(),
})
}
if s.TunDevice != nil {
engineConfig.Tun = s.TunDevice
if s.Router != nil {
engineConfig.Router = s.Router
} else {
systemRouter, err := router.New(tsLogf, s.TunDevice, s.netMon, sys.HealthTracker.Get(), sys.Bus.Get())
if err != nil {
return err
}
engineConfig.Router = systemRouter
}
}
eng, err := wgengine.NewUserspaceEngine(tsLogf, engineConfig)
if err != nil {
return err
}

View File

@@ -506,6 +506,11 @@ func (o *optionalPolicyLock) Lock() error {
o.state = gpLockRestricted
return nil
default:
if errors.Is(err, windows.ERROR_ACCESS_DENIED) {
loggerx.Errorf("GP lock not acquired: %v", err)
o.state = gpLockRestricted
return nil
}
return err
}
}

View File

@@ -197,6 +197,14 @@ type Impl struct {
peerapiPort4Atomic atomic.Uint32 // uint16 port number for IPv4 peerapi
peerapiPort6Atomic atomic.Uint32 // uint16 port number for IPv6 peerapi
// allowInboundBypass controls whether inbound bypass tracking is enabled.
// This should be disabled for fake TUNs (pure userspace mode) where the
// host network stack won't see bypassed packets.
allowInboundBypass bool
outboundTCPAccess sync.Mutex
outboundTCPFlows map[tcpFlowKey]time.Time
// atomicIsLocalIPFunc holds a func that reports whether an IP
// is a local (non-subnet) Tailscale IP address of this
// machine. It's always a non-nil func. It's changed on netmap
@@ -244,6 +252,21 @@ type Impl struct {
packetsInFlight map[stack.TransportEndpointID]struct{}
}
type tcpFlowKey struct {
src netip.AddrPort
dst netip.AddrPort
}
func shouldEnableInboundBypass(tundev *tstun.Wrapper) bool {
if tundev == nil {
return false
}
if ft, ok := tundev.Unwrap().(interface{ IsFakeTun() bool }); ok {
return !ft.IsFakeTun()
}
return true
}
const nicID = 1
// maxUDPPacketSize is the maximum size of a UDP packet we copy in
@@ -380,6 +403,7 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
NIC: nicID,
},
})
allowInboundBypass := shouldEnableInboundBypass(tundev)
ns := &Impl{
logf: logf,
ipstack: ipstack,
@@ -389,6 +413,8 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
pm: pm,
mc: mc,
dialer: dialer,
allowInboundBypass: allowInboundBypass,
outboundTCPFlows: make(map[tcpFlowKey]time.Time),
connsOpenBySubnetIP: make(map[netip.Addr]int),
connsInFlightByClient: make(map[netip.Addr]int),
packetsInFlight: make(map[stack.TransportEndpointID]struct{}),
@@ -834,6 +860,9 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper, gro *gro.
default:
// Not traffic to the service IP or a 4via6 IP, so we don't
// care about the packet; resume processing.
if p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && p.TCPFlags&packet.TCPAck == 0 {
ns.recordOutboundTCPFlow(p)
}
return filter.Accept, gro
}
if debugPackets {
@@ -1058,10 +1087,62 @@ func (ns *Impl) peerAPIPortAtomic(ip netip.Addr) *atomic.Uint32 {
}
var viaRange = tsaddr.TailscaleViaRange()
var outboundTCPFlowTTL = 2 * time.Minute
func (ns *Impl) recordOutboundTCPFlow(p *packet.Parsed) {
if !ns.allowInboundBypass {
return
}
key := tcpFlowKey{
src: p.Dst,
dst: p.Src,
}
now := time.Now()
ns.outboundTCPAccess.Lock()
ns.outboundTCPFlows[key] = now.Add(outboundTCPFlowTTL)
ns.outboundTCPAccess.Unlock()
}
func (ns *Impl) shouldBypassInbound(p *packet.Parsed) bool {
if !ns.allowInboundBypass {
return false
}
if p.IPProto != ipproto.TCP {
return false
}
key := tcpFlowKey{
src: p.Src,
dst: p.Dst,
}
now := time.Now()
ns.outboundTCPAccess.Lock()
defer ns.outboundTCPAccess.Unlock()
expiresAt, ok := ns.outboundTCPFlows[key]
if !ok {
if debugNetstack() && p.TCPFlags&packet.TCPSynAck == packet.TCPSynAck {
ns.logf("netstack: inbound bypass miss for %v -> %v flags=%v", p.Src, p.Dst, p.TCPFlags)
}
return false
}
if now.After(expiresAt) {
delete(ns.outboundTCPFlows, key)
if debugNetstack() && p.TCPFlags&packet.TCPSynAck == packet.TCPSynAck {
ns.logf("netstack: inbound bypass expired for %v -> %v flags=%v", p.Src, p.Dst, p.TCPFlags)
}
return false
}
if debugNetstack() && (p.TCPFlags&packet.TCPSynAck == packet.TCPSynAck || p.TCPFlags&packet.TCPRst != 0) {
ns.logf("netstack: inbound bypass hit for %v -> %v flags=%v", p.Src, p.Dst, p.TCPFlags)
}
return true
}
// shouldProcessInbound reports whether an inbound packet (a packet from a
// WireGuard peer) should be handled by netstack.
func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
if ns.shouldBypassInbound(p) {
return false
}
// Handle incoming peerapi connections in netstack.
dstIP := p.Dst.Addr()
isLocal := ns.isLocalIP(dstIP)

View File

@@ -21,7 +21,6 @@ import (
"github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/sagernet/tailscale/wgengine/winnet"
"github.com/sagernet/wireguard-go/tun"
"go4.org/netipx"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
@@ -41,7 +40,7 @@ import (
// ICMP fragmentation-needed messages within tailscaled. This code may
// address a few rare corner cases, but is unlikely to significantly
// help with MTU issues compared to a static 1280B implementation.
func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, error) {
func monitorDefaultRoutes(tun windowsTunDevice) (*winipcfg.RouteChangeCallback, error) {
ourLuid := winipcfg.LUID(tun.LUID())
lastMtu := uint32(0)
doIt := func() error {
@@ -245,7 +244,7 @@ var networkCategoryWarnable = health.Register(&health.Warnable{
MapDebugFlag: "warn-network-category-unhealthy",
})
func configureInterface(cfg *router.Config, tun *tun.NativeTun, ht *health.Tracker) (retErr error) {
func configureInterface(cfg *router.Config, tun windowsTunDevice, ht *health.Tracker) (retErr error) {
mtu := tstun.DefaultTUNMTU()
luid := winipcfg.LUID(tun.LUID())
iface, err := interfaceFromLUID(luid,

View File

@@ -15,7 +15,7 @@ import (
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/types/logger"
"github.com/sagernet/tailscale/wgengine/router"
"github.com/tailscale/wireguard-go/tun"
"github.com/sagernet/wireguard-go/tun"
)
func init() {

View File

@@ -41,13 +41,23 @@ type winRouter struct {
logf func(fmt string, args ...any)
netMon *netmon.Monitor // may be nil
health *health.Tracker
nativeTun *tun.NativeTun
nativeTun windowsTunDevice
routeChangeCallback *winipcfg.RouteChangeCallback
firewall *firewallTweaker
}
type windowsTunDevice interface {
tun.Device
LUID() uint64
MTU() (int, error)
ForceMTU(int)
}
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus) (router.Router, error) {
nativeTun := tundev.(*tun.NativeTun)
nativeTun, ok := tundev.(windowsTunDevice)
if !ok {
return nil, fmt.Errorf("unsupported tun device type %T", tundev)
}
luid := winipcfg.LUID(nativeTun.LUID())
guid, err := luid.GUID()
if err != nil {