various: use tsvnic instead of wintun when building with ts_include_tsvnic enabled

Updates tailscale/corp#36208

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2026-01-22 11:11:16 -06:00
parent e30626c480
commit 532662e701
No known key found for this signature in database
13 changed files with 120 additions and 31 deletions

View File

@ -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 == "" {

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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)",

28
feature/tsvnic/tsvnic.go Normal file
View File

@ -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)
}

8
go.mod
View File

@ -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

14
go.sum
View File

@ -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=

View File

@ -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: "),
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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 {