Skip to content

Commit 2808816

Browse files
authored
chore: get TUN/DNS working on Windows for CoderVPN (#16310)
1 parent a658ccf commit 2808816

File tree

9 files changed

+199
-55
lines changed

9 files changed

+199
-55
lines changed

cli/vpndaemon_windows.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
4141
},
4242
Handler: func(inv *serpent.Invocation) error {
4343
ctx := inv.Context()
44-
logger := inv.Logger.AppendSinks(sloghuman.Sink(inv.Stderr)).Leveled(slog.LevelDebug)
44+
sinks := []slog.Sink{
45+
sloghuman.Sink(inv.Stderr),
46+
}
47+
logger := inv.Logger.AppendSinks(sinks...).Leveled(slog.LevelDebug)
4548

4649
if rpcReadHandleInt < 0 || rpcWriteHandleInt < 0 {
4750
return xerrors.Errorf("rpc-read-handle (%v) and rpc-write-handle (%v) must be positive", rpcReadHandleInt, rpcWriteHandleInt)
@@ -60,7 +63,11 @@ func (r *RootCmd) vpnDaemonRun() *serpent.Command {
6063
defer pipe.Close()
6164

6265
logger.Info(ctx, "starting tunnel")
63-
tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient())
66+
tunnel, err := vpn.NewTunnel(ctx, logger, pipe, vpn.NewClient(),
67+
vpn.UseOSNetworkingStack(),
68+
vpn.UseAsLogger(),
69+
vpn.UseCustomLogSinks(sinks...),
70+
)
6471
if err != nil {
6572
return xerrors.Errorf("create new tunnel for client: %w", err)
6673
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ require (
423423
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
424424
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
425425
golang.org/x/time v0.9.0 // indirect
426-
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
426+
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
427427
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
428428
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
429429
google.golang.org/appengine v1.6.8 // indirect

tailnet/conn.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ type Options struct {
116116
Router router.Router
117117
// TUNDev is optional, and is passed to the underlying wireguard engine.
118118
TUNDev tun.Device
119+
// WireguardMonitor is optional, and is passed to the underlying wireguard
120+
// engine.
121+
WireguardMonitor *netmon.Monitor
119122
}
120123

121124
// TelemetrySink allows tailnet.Conn to send network telemetry to the Coder
@@ -171,13 +174,15 @@ func NewConn(options *Options) (conn *Conn, err error) {
171174
nodeID = tailcfg.NodeID(uid)
172175
}
173176

174-
wireguardMonitor, err := netmon.New(Logger(options.Logger.Named("net.wgmonitor")))
175-
if err != nil {
176-
return nil, xerrors.Errorf("create wireguard link monitor: %w", err)
177+
if options.WireguardMonitor == nil {
178+
options.WireguardMonitor, err = netmon.New(Logger(options.Logger.Named("net.wgmonitor")))
179+
if err != nil {
180+
return nil, xerrors.Errorf("create wireguard link monitor: %w", err)
181+
}
177182
}
178183
defer func() {
179184
if err != nil {
180-
wireguardMonitor.Close()
185+
options.WireguardMonitor.Close()
181186
}
182187
}()
183188

@@ -186,7 +191,7 @@ func NewConn(options *Options) (conn *Conn, err error) {
186191
}
187192
sys := new(tsd.System)
188193
wireguardEngine, err := wgengine.NewUserspaceEngine(Logger(options.Logger.Named("net.wgengine")), wgengine.Config{
189-
NetMon: wireguardMonitor,
194+
NetMon: options.WireguardMonitor,
190195
Dialer: dialer,
191196
ListenPort: options.ListenPort,
192197
SetSubsystem: sys.Set,
@@ -293,7 +298,7 @@ func NewConn(options *Options) (conn *Conn, err error) {
293298
listeners: map[listenKey]*listener{},
294299
tunDevice: sys.Tun.Get(),
295300
netStack: netStack,
296-
wireguardMonitor: wireguardMonitor,
301+
wireguardMonitor: options.WireguardMonitor,
297302
wireguardRouter: &router.Config{
298303
LocalAddrs: options.Addresses,
299304
},

vpn/client.go

+10-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"golang.org/x/xerrors"
1010
"tailscale.com/net/dns"
11+
"tailscale.com/net/netmon"
1112
"tailscale.com/wgengine/router"
1213

1314
"github.com/google/uuid"
@@ -57,12 +58,13 @@ func NewClient() Client {
5758
}
5859

5960
type Options struct {
60-
Headers http.Header
61-
Logger slog.Logger
62-
DNSConfigurator dns.OSConfigurator
63-
Router router.Router
64-
TUNFileDescriptor *int
65-
UpdateHandler tailnet.UpdatesHandler
61+
Headers http.Header
62+
Logger slog.Logger
63+
DNSConfigurator dns.OSConfigurator
64+
Router router.Router
65+
TUNDevice tun.Device
66+
WireguardMonitor *netmon.Monitor
67+
UpdateHandler tailnet.UpdatesHandler
6668
}
6769

6870
func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string, options *Options) (vpnC Conn, err error) {
@@ -74,15 +76,6 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
7476
options.Headers = http.Header{}
7577
}
7678

77-
var dev tun.Device
78-
if options.TUNFileDescriptor != nil {
79-
// No-op on non-Darwin platforms.
80-
dev, err = makeTUN(*options.TUNFileDescriptor)
81-
if err != nil {
82-
return nil, xerrors.Errorf("make TUN: %w", err)
83-
}
84-
}
85-
8679
headers := options.Headers
8780
sdk := codersdk.New(serverURL)
8881
sdk.SetSessionToken(token)
@@ -134,7 +127,8 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string
134127
BlockEndpoints: connInfo.DisableDirectConnections,
135128
DNSConfigurator: options.DNSConfigurator,
136129
Router: options.Router,
137-
TUNDev: dev,
130+
TUNDev: options.TUNDevice,
131+
WireguardMonitor: options.WireguardMonitor,
138132
})
139133
if err != nil {
140134
return nil, xerrors.Errorf("create tailnet: %w", err)

vpn/dylib/lib.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ func OpenTunnel(cReadFD, cWriteFD int32) int32 {
4747
}
4848

4949
_, err = vpn.NewTunnel(ctx, slog.Make(), conn, vpn.NewClient(),
50-
vpn.UseAsDNSConfig(),
51-
vpn.UseAsRouter(),
50+
vpn.UseOSNetworkingStack(),
5251
vpn.UseAsLogger(),
5352
)
5453
if err != nil {

vpn/tun.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
//go:build !darwin
1+
//go:build !darwin && !windows
22

33
package vpn
44

5-
import "github.com/tailscale/wireguard-go/tun"
5+
import "cdr.dev/slog"
66

7-
// This is a no-op on non-Darwin platforms.
8-
func makeTUN(int) (tun.Device, error) {
9-
return nil, nil
7+
// This is a no-op on every platform except Darwin and Windows.
8+
func GetNetworkingStack(_ *Tunnel, _ *StartRequest, _ slog.Logger) (NetworkStack, error) {
9+
return NetworkStack{}, nil
1010
}

vpn/tun_darwin.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,34 @@ package vpn
55
import (
66
"os"
77

8+
"cdr.dev/slog"
89
"github.com/tailscale/wireguard-go/tun"
910
"golang.org/x/sys/unix"
1011
"golang.org/x/xerrors"
1112
)
1213

13-
func makeTUN(tunFD int) (tun.Device, error) {
14-
dupTunFd, err := unix.Dup(tunFD)
14+
func GetNetworkingStack(t *Tunnel, req *StartRequest, _ slog.Logger) (NetworkStack, error) {
15+
tunFd := int(req.GetTunnelFileDescriptor())
16+
dupTunFd, err := unix.Dup(tunFd)
1517
if err != nil {
16-
return nil, xerrors.Errorf("dup tun fd: %w", err)
18+
return NetworkStack{}, xerrors.Errorf("dup tun fd: %w", err)
1719
}
1820

1921
err = unix.SetNonblock(dupTunFd, true)
2022
if err != nil {
2123
unix.Close(dupTunFd)
22-
return nil, xerrors.Errorf("set nonblock: %w", err)
24+
return NetworkStack{}, xerrors.Errorf("set nonblock: %w", err)
2325
}
2426
fileTun, err := tun.CreateTUNFromFile(os.NewFile(uintptr(dupTunFd), "/dev/tun"), 0)
2527
if err != nil {
2628
unix.Close(dupTunFd)
27-
return nil, xerrors.Errorf("create TUN from File: %w", err)
29+
return NetworkStack{}, xerrors.Errorf("create TUN from File: %w", err)
2830
}
29-
return fileTun, nil
31+
32+
return NetworkStack{
33+
WireguardMonitor: nil, // default is fine
34+
TUNDevice: fileTun,
35+
Router: NewRouter(t),
36+
DNSConfigurator: NewDNSConfigurator(t),
37+
}, nil
3038
}

vpn/tun_windows.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//go:build windows
2+
3+
package vpn
4+
5+
import (
6+
"context"
7+
"errors"
8+
"time"
9+
10+
"github.com/coder/retry"
11+
"github.com/tailscale/wireguard-go/tun"
12+
"golang.org/x/sys/windows"
13+
"golang.org/x/xerrors"
14+
"golang.zx2c4.com/wintun"
15+
"tailscale.com/net/dns"
16+
"tailscale.com/net/netmon"
17+
"tailscale.com/net/tstun"
18+
"tailscale.com/types/logger"
19+
"tailscale.com/util/winutil"
20+
"tailscale.com/wgengine/router"
21+
22+
"cdr.dev/slog"
23+
"github.com/coder/coder/v2/tailnet"
24+
)
25+
26+
const tunName = "Coder"
27+
28+
func GetNetworkingStack(t *Tunnel, _ *StartRequest, logger slog.Logger) (NetworkStack, error) {
29+
tun.WintunTunnelType = tunName
30+
guid, err := windows.GUIDFromString("{0ed1515d-04a4-4c46-abae-11ad07cf0e6d}")
31+
if err != nil {
32+
panic(err)
33+
}
34+
tun.WintunStaticRequestedGUID = &guid
35+
36+
tunDev, tunName, err := tstunNewWithWindowsRetries(tailnet.Logger(logger.Named("net.tun.device")), tunName)
37+
if err != nil {
38+
return NetworkStack{}, xerrors.Errorf("create tun device: %w", err)
39+
}
40+
logger.Info(context.Background(), "tun created", slog.F("name", tunName))
41+
42+
wireguardMonitor, err := netmon.New(tailnet.Logger(logger.Named("net.wgmonitor")))
43+
44+
coderRouter, err := router.New(tailnet.Logger(logger.Named("net.router")), tunDev, wireguardMonitor)
45+
if err != nil {
46+
return NetworkStack{}, xerrors.Errorf("create router: %w", err)
47+
}
48+
49+
dnsConfigurator, err := dns.NewOSConfigurator(tailnet.Logger(logger.Named("net.dns")), tunName)
50+
if err != nil {
51+
return NetworkStack{}, xerrors.Errorf("create dns configurator: %w", err)
52+
}
53+
54+
return NetworkStack{
55+
WireguardMonitor: nil, // default is fine
56+
TUNDevice: tunDev,
57+
Router: coderRouter,
58+
DNSConfigurator: dnsConfigurator,
59+
}, nil
60+
}
61+
62+
// tstunNewOrRetry is a wrapper around tstun.New that retries on Windows for certain
63+
// errors.
64+
//
65+
// This is taken from Tailscale:
66+
// https://github.com/tailscale/tailscale/blob/3abfbf50aebbe3ba57dc749165edb56be6715c0a/cmd/tailscaled/tailscaled_windows.go#L107
67+
func tstunNewWithWindowsRetries(logf logger.Logf, tunName string) (_ tun.Device, devName string, _ error) {
68+
r := retry.New(250*time.Millisecond, 10*time.Second)
69+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
70+
defer cancel()
71+
for r.Wait(ctx) {
72+
dev, devName, err := tstun.New(logf, tunName)
73+
if err == nil {
74+
return dev, devName, err
75+
}
76+
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) || windowsUptime() < 10*time.Minute {
77+
// Wintun is not installing correctly. Dump the state of NetSetupSvc
78+
// (which is a user-mode service that must be active for network devices
79+
// to install) and its dependencies to the log.
80+
winutil.LogSvcState(logf, "NetSetupSvc")
81+
}
82+
}
83+
84+
return nil, "", ctx.Err()
85+
}
86+
87+
var (
88+
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
89+
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
90+
)
91+
92+
func windowsUptime() time.Duration {
93+
r, _, _ := getTickCount64Proc.Call()
94+
return time.Duration(int64(r)) * time.Millisecond
95+
}
96+
97+
// TODO(@dean): implement a way to install/uninstall the wintun driver, most
98+
// likely as a CLI command
99+
//
100+
// This is taken from Tailscale:
101+
// https://github.com/tailscale/tailscale/blob/3abfbf50aebbe3ba57dc749165edb56be6715c0a/cmd/tailscaled/tailscaled_windows.go#L543
102+
func uninstallWinTun(logf logger.Logf) {
103+
dll := windows.NewLazyDLL("wintun.dll")
104+
if err := dll.Load(); err != nil {
105+
logf("Cannot load wintun.dll for uninstall: %v", err)
106+
return
107+
}
108+
109+
logf("Removing wintun driver...")
110+
err := wintun.Uninstall()
111+
logf("Uninstall: %v", err)
112+
}
113+
114+
// TODO(@dean): remove
115+
var _ = uninstallWinTun

0 commit comments

Comments
 (0)