From 532662e7010fcf7252951d3c12e8c32a94e54840 Mon Sep 17 00:00:00 2001 From: Nick Khyl Date: Thu, 22 Jan 2026 11:11:16 -0600 Subject: [PATCH] various: use tsvnic instead of wintun when building with ts_include_tsvnic enabled Updates tailscale/corp#36208 Signed-off-by: Nick Khyl --- cmd/featuretags/featuretags.go | 3 ++ .../buildfeatures/feature_tsvnic_disabled.go | 11 +++++++ .../buildfeatures/feature_tsvnic_enabled.go | 11 +++++++ feature/condregister/maybe_tsvnic.go | 8 +++++ feature/featuretags/featuretags.go | 12 +++++--- feature/tsvnic/tsvnic.go | 28 +++++++++++++++++ go.mod | 8 ++--- go.sum | 14 +++++---- net/tstun/ifstatus_windows.go | 2 +- net/tstun/tun.go | 30 ++++++++++++------- net/tstun/tun_windows.go | 14 ++++++++- wgengine/router/osrouter/ifconfig_windows.go | 5 ++-- wgengine/router/osrouter/router_windows.go | 5 ++-- 13 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 feature/buildfeatures/feature_tsvnic_disabled.go create mode 100644 feature/buildfeatures/feature_tsvnic_enabled.go create mode 100644 feature/condregister/maybe_tsvnic.go create mode 100644 feature/tsvnic/tsvnic.go diff --git a/cmd/featuretags/featuretags.go b/cmd/featuretags/featuretags.go index 8c8a2ceaf..025de4b6b 100644 --- a/cmd/featuretags/featuretags.go +++ b/cmd/featuretags/featuretags.go @@ -48,6 +48,9 @@ func main() { if keep[featuretags.CLI] { tags = append(tags, "ts_include_cli") } + if keep[featuretags.TSVNIC] { + tags = append(tags, "ts_include_tsvnic") + } if *min { for _, f := range slices.Sorted(maps.Keys(features)) { if f == "" { diff --git a/feature/buildfeatures/feature_tsvnic_disabled.go b/feature/buildfeatures/feature_tsvnic_disabled.go new file mode 100644 index 000000000..0d7afc847 --- /dev/null +++ b/feature/buildfeatures/feature_tsvnic_disabled.go @@ -0,0 +1,11 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !ts_include_tsvnic + +package buildfeatures + +// HasTSVNIC is whether the binary was built with support for modular feature "Experimental Windows driver". +// Specifically, it's whether the binary was built with the "ts_include_tsvnic" build tag. +// It's a const so it can be used for dead code elimination. +const HasTSVNIC = false diff --git a/feature/buildfeatures/feature_tsvnic_enabled.go b/feature/buildfeatures/feature_tsvnic_enabled.go new file mode 100644 index 000000000..a4961b652 --- /dev/null +++ b/feature/buildfeatures/feature_tsvnic_enabled.go @@ -0,0 +1,11 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build ts_include_tsvnic + +package buildfeatures + +// HasTSVNIC is whether the binary was built with support for modular feature "Experimental Windows driver". +// Specifically, it's whether the binary was built with the "ts_include_tsvnic" build tag. +// It's a const so it can be used for dead code elimination. +const HasTSVNIC = true diff --git a/feature/condregister/maybe_tsvnic.go b/feature/condregister/maybe_tsvnic.go new file mode 100644 index 000000000..03475be9b --- /dev/null +++ b/feature/condregister/maybe_tsvnic.go @@ -0,0 +1,8 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build windows && ts_include_tsvnic + +package condregister + +import _ "tailscale.com/feature/tsvnic" diff --git a/feature/featuretags/featuretags.go b/feature/featuretags/featuretags.go index 99df18b5a..49a862ae1 100644 --- a/feature/featuretags/featuretags.go +++ b/feature/featuretags/featuretags.go @@ -6,10 +6,13 @@ package featuretags import "tailscale.com/util/set" -// CLI is a special feature in the [Features] map that works opposite -// from the others: it is opt-in, rather than opt-out, having a different +// CLI and TSVNIC are special features in the [Features] map that work opposite +// from the others: they are opt-in, rather than opt-out, having a different // build tag format. -const CLI FeatureTag = "cli" +const ( + CLI FeatureTag = "cli" + TSVNIC FeatureTag = "tsvnic" +) // FeatureTag names a Tailscale feature that can be selectively added or removed // via build tags. @@ -19,7 +22,7 @@ type FeatureTag string // omitted via a ts_omit_ build tag. func (ft FeatureTag) IsOmittable() bool { switch ft { - case CLI: + case CLI, TSVNIC: return false } return true @@ -263,6 +266,7 @@ var Features = map[FeatureTag]FeatureMeta{ "tailnetlock": {Sym: "TailnetLock", Desc: "Tailnet Lock support"}, "tap": {Sym: "Tap", Desc: "Experimental Layer 2 (ethernet) support"}, "tpm": {Sym: "TPM", Desc: "TPM support"}, + "tsvnic": {Sym: "TSVNIC", Desc: "Experimental Windows driver"}, "unixsocketidentity": { Sym: "UnixSocketIdentity", Desc: "differentiate between users accessing the LocalAPI over unix sockets (if omitted, all users have full access)", diff --git a/feature/tsvnic/tsvnic.go b/feature/tsvnic/tsvnic.go new file mode 100644 index 000000000..8179ccfae --- /dev/null +++ b/feature/tsvnic/tsvnic.go @@ -0,0 +1,28 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build windows + +// Package tsvnic enables the experimental Windows driver. +package tsvnic + +import ( + "github.com/dblohm7/wingoes" + "github.com/tailscale/wireguard-go/tun" + "tailscale.com/net/tstun" + "tailscale.com/types/logger" + + "github.com/tailscale/tsvnic-experiment/wgtun" +) + +func init() { + tstun.CreateTSVNIC.Set(createTUN) +} + +func createTUN(logf logger.Logf, tunName string, mtu int) (tun.Device, error) { + if err := wgtun.MaybeInstallDriver(); err != nil { + return nil, err + } + guid := wingoes.MustGetGUID("{FC4CAFB3-26BA-4375-8450-97FB42C27531}") + return wgtun.NewTUN(logger.WithPrefix(logf, "tsvnic: "), "Tailscale Tunnel (Experimental)", guid, mtu) +} diff --git a/go.mod b/go.mod index a8ec79e6e..cda1be25b 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/creachadair/msync v0.7.1 github.com/creachadair/taskgroup v0.13.2 github.com/creack/pty v1.1.23 - github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa + github.com/dblohm7/wingoes v0.0.0-20250611174154-e3e096948d18 github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e github.com/distribution/reference v0.6.0 github.com/djherbis/times v1.6.0 @@ -36,7 +36,7 @@ require ( github.com/fogleman/gg v1.3.0 github.com/frankban/quicktest v1.14.6 github.com/fxamacker/cbor/v2 v2.9.0 - github.com/gaissmai/bart v0.18.0 + github.com/gaissmai/bart v0.26.0 github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced github.com/go-logr/zapr v1.3.0 github.com/go-ole/go-ole v1.3.0 @@ -92,6 +92,7 @@ require ( github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc github.com/tailscale/setec v0.0.0-20251203133219-2ab774e4129a + github.com/tailscale/tsvnic-experiment v0.0.0-20260122072412-abba21a48195 github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da @@ -105,7 +106,7 @@ require ( go4.org/mem v0.0.0-20240501181205-ae6ca9944745 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.46.0 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/exp v0.0.0-20250808145144-a408d31f581a golang.org/x/mod v0.30.0 golang.org/x/net v0.48.0 golang.org/x/oauth2 v0.32.0 @@ -231,7 +232,6 @@ require ( go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect diff --git a/go.sum b/go.sum index 541cef605..dedbe53c2 100644 --- a/go.sum +++ b/go.sum @@ -290,8 +290,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= -github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= +github.com/dblohm7/wingoes v0.0.0-20250611174154-e3e096948d18 h1:1+ezXI2ZjiS8zenp08GFowF3zkVQ4j8/CPaALxqCBq0= +github.com/dblohm7/wingoes v0.0.0-20250611174154-e3e096948d18/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= @@ -373,8 +373,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo= -github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY= +github.com/gaissmai/bart v0.26.0 h1:xOZ57E9hJLBiQaSyeZa9wgWhGuzfGACgqp4BE77OkO0= +github.com/gaissmai/bart v0.26.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c= github.com/ghostiam/protogetter v0.3.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbxOK4Ug= github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= @@ -1104,6 +1104,8 @@ 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/setec v0.0.0-20251203133219-2ab774e4129a h1:TApskGPim53XY5WRt5hX4DnO8V6CmVoimSklryIoGMM= github.com/tailscale/setec v0.0.0-20251203133219-2ab774e4129a/go.mod h1:+6WyG6kub5/5uPsMdYQuSti8i6F5WuKpFWLQnZt/Mms= +github.com/tailscale/tsvnic-experiment v0.0.0-20260122072412-abba21a48195 h1:Bsn/In94ICVXTrlHQE14B2CbttRRvLTx1MokK6IC7Nk= +github.com/tailscale/tsvnic-experiment v0.0.0-20260122072412-abba21a48195/go.mod h1:YtG3cZymy3n0YRWr1zFWiC5yvgNPjjwClazpJXxMk3Q= 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/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= @@ -1279,8 +1281,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= diff --git a/net/tstun/ifstatus_windows.go b/net/tstun/ifstatus_windows.go index fd9fc2112..0b14b1b23 100644 --- a/net/tstun/ifstatus_windows.go +++ b/net/tstun/ifstatus_windows.go @@ -61,7 +61,7 @@ func (iw *ifaceWatcher) getOperStatus() winipcfg.IfOperStatus { func waitInterfaceUp(iface tun.Device, timeout time.Duration, logf logger.Logf) error { iw := &ifaceWatcher{ - luid: winipcfg.LUID(iface.(*tun.NativeTun).LUID()), + luid: winipcfg.LUID(iface.(WindowsTun).LUID()), logf: logger.WithPrefix(logf, "waitInterfaceUp: "), } diff --git a/net/tstun/tun.go b/net/tstun/tun.go index 19b0a53f5..e2db8f1e5 100644 --- a/net/tstun/tun.go +++ b/net/tstun/tun.go @@ -25,6 +25,9 @@ import ( // CreateTAP is the hook maybe set by feature/tap. var CreateTAP feature.Hook[func(logf logger.Logf, tapName, bridgeName string) (tun.Device, error)] +// CreateTSVNIC is the hook maybe set by feature/tsvnic. +var CreateTSVNIC feature.Hook[func(logf logger.Logf, tunName string, mtu int) (tun.Device, error)] + // HookSetLinkAttrs is the hook maybe set by feature/linkspeed. var HookSetLinkAttrs feature.Hook[func(tun.Device) error] @@ -58,20 +61,27 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) { if runtime.GOOS == "plan9" { cleanUpPlan9Interfaces() } + if CreateTSVNIC.IsSet() { + if dev, err = CreateTSVNIC.Get()(logf, tunName, int(DefaultTUNMTU())); err != nil { + logf("CreateTSVNIC failed: %v. Falling back to the default implementation.", err) + } + } // 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) + if dev == nil { + 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 } - break - } - if modprobeTunHook.Get()() != nil { - // modprobe failed; no point trying again. - break } } } diff --git a/net/tstun/tun_windows.go b/net/tstun/tun_windows.go index 2b1d3054e..6c2f58c23 100644 --- a/net/tstun/tun_windows.go +++ b/net/tstun/tun_windows.go @@ -9,6 +9,18 @@ import ( "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" ) +// WindowsTun is a [tun.Device] that provides access to Windows-specific +// functionality implemented by both wintun ([tun.NativeTun]) and tsvnic. +type WindowsTun interface { + tun.Device + // LUID returns Windows interface instance ID. + LUID() uint64 + // ForceMTU causes subsequent [tun.Device.MTU] calls to return mtu and + // sends a [tun.EventMTUUpdate] on the [tun.Device.Events] channel, + // without changing the underlying interface MTU. + ForceMTU(mtu int) +} + func init() { tun.WintunTunnelType = "Tailscale" guid, err := windows.GUIDFromString("{37217669-42da-4657-a55b-0d995d328250}") @@ -19,7 +31,7 @@ func init() { } func interfaceName(dev tun.Device) (string, error) { - guid, err := winipcfg.LUID(dev.(*tun.NativeTun).LUID()).GUID() + guid, err := winipcfg.LUID(dev.(WindowsTun).LUID()).GUID() if err != nil { return "", err } diff --git a/wgengine/router/osrouter/ifconfig_windows.go b/wgengine/router/osrouter/ifconfig_windows.go index cb87ad5f2..5bfa1d1ce 100644 --- a/wgengine/router/osrouter/ifconfig_windows.go +++ b/wgengine/router/osrouter/ifconfig_windows.go @@ -22,7 +22,6 @@ import ( "tailscale.com/wgengine/winnet" ole "github.com/go-ole/go-ole" - "github.com/tailscale/wireguard-go/tun" "go4.org/netipx" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" @@ -42,7 +41,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 tstun.WindowsTun) (*winipcfg.RouteChangeCallback, error) { ourLuid := winipcfg.LUID(tun.LUID()) lastMtu := uint32(0) doIt := func() error { @@ -246,7 +245,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 tstun.WindowsTun, ht *health.Tracker) (retErr error) { var mtu = tstun.DefaultTUNMTU() luid := winipcfg.LUID(tun.LUID()) iface, err := interfaceFromLUID(luid, diff --git a/wgengine/router/osrouter/router_windows.go b/wgengine/router/osrouter/router_windows.go index a1acbe3b6..7e6cc71d6 100644 --- a/wgengine/router/osrouter/router_windows.go +++ b/wgengine/router/osrouter/router_windows.go @@ -25,6 +25,7 @@ import ( "tailscale.com/health" "tailscale.com/net/dns" "tailscale.com/net/netmon" + "tailscale.com/net/tstun" "tailscale.com/types/logger" "tailscale.com/util/backoff" "tailscale.com/util/eventbus" @@ -41,13 +42,13 @@ type winRouter struct { logf func(fmt string, args ...any) netMon *netmon.Monitor // may be nil health *health.Tracker - nativeTun *tun.NativeTun + nativeTun tstun.WindowsTun routeChangeCallback *winipcfg.RouteChangeCallback firewall *firewallTweaker } 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 := tundev.(tstun.WindowsTun) luid := winipcfg.LUID(nativeTun.LUID()) guid, err := luid.GUID() if err != nil {