Add tun device support
This commit is contained in:
6
feature/condregister/osrouter/doc.go
Normal file
6
feature/condregister/osrouter/doc.go
Normal 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
|
||||
8
feature/condregister/osrouter/maybe_osrouter.go
Normal file
8
feature/condregister/osrouter/maybe_osrouter.go
Normal 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
1
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user