Files
coder/vpn/tun_windows.go
Dean Sheather b23e05b1fe fix(vpn): fail early if wintun.dll is not present (#16707)
Prevents the VPN startup from hanging for 5 minutes due to a startup
backoff if `wintun.dll` cannot be loaded.

Because the `wintun` package doesn't expose an easy `Load() error`
method for us, the only way for us to force it to load (without unwanted
side effects) is through `wintun.Version()` which doesn't return an
error message.

So, we call that function so the `wintun` package loads the DLL and
configures the logging properly, then we try to load the DLL ourselves.
`LoadLibraryEx` will not load the library multiple times and returns a
reference to the existing library.

Closes https://github.com/coder/coder-desktop-windows/issues/24
2025-02-28 15:20:00 +11:00

161 lines
5.3 KiB
Go

//go:build windows
package vpn
import (
"context"
"errors"
"time"
"github.com/dblohm7/wingoes/com"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"golang.org/x/xerrors"
"golang.zx2c4.com/wintun"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/types/logger"
"tailscale.com/util/winutil"
"tailscale.com/wgengine/router"
"cdr.dev/slog"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/retry"
)
const (
tunName = "Coder"
tunGUID = "{0ed1515d-04a4-4c46-abae-11ad07cf0e6d}"
wintunDLL = "wintun.dll"
)
func GetNetworkingStack(t *Tunnel, _ *StartRequest, logger slog.Logger) (NetworkStack, error) {
// Initialize COM process-wide so Tailscale can make calls to the windows
// network APIs to read/write adapter state.
comProcessType := com.ConsoleApp
isSvc, err := svc.IsWindowsService()
if err != nil {
return NetworkStack{}, xerrors.Errorf("svc.IsWindowsService failed: %w", err)
}
if isSvc {
comProcessType = com.Service
}
if err := com.StartRuntime(comProcessType); err != nil {
return NetworkStack{}, xerrors.Errorf("could not initialize COM: com.StartRuntime(%d): %w", comProcessType, err)
}
// Set the name and GUID for the TUN interface.
tun.WintunTunnelType = tunName
guid, err := windows.GUIDFromString(tunGUID)
if err != nil {
return NetworkStack{}, xerrors.Errorf("could not parse GUID %q: %w", tunGUID, err)
}
tun.WintunStaticRequestedGUID = &guid
// Ensure wintun.dll is available, and fail early if it's not to avoid
// hanging for 5 minutes in tstunNewWithWindowsRetries.
//
// First, we call wintun.Version() to make the wintun package attempt to
// load wintun.dll. This allows the wintun package to set the logging
// callback in the DLL before we load it ourselves.
_ = wintun.Version()
// Then, we try to load wintun.dll ourselves so we get a better error
// message if there was a problem. This call matches the wintun package, so
// we're loading it in the same way.
//
// Note: this leaks the handle to wintun.dll, but since it's already loaded
// it wouldn't be freed anyways.
const (
LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
)
_, err = windows.LoadLibraryEx(wintunDLL, 0, LOAD_LIBRARY_SEARCH_APPLICATION_DIR|LOAD_LIBRARY_SEARCH_SYSTEM32)
if err != nil {
return NetworkStack{}, xerrors.Errorf("could not load %q, it should be in the same directory as the executable (in Coder Desktop, this should have been installed automatically): %w", wintunDLL, err)
}
tunDev, tunName, err := tstunNewWithWindowsRetries(tailnet.Logger(logger.Named("net.tun.device")), tunName)
if err != nil {
return NetworkStack{}, xerrors.Errorf("create tun device: %w", err)
}
logger.Info(context.Background(), "tun created", slog.F("name", tunName))
wireguardMonitor, err := netmon.New(tailnet.Logger(logger.Named("net.wgmonitor")))
coderRouter, err := router.New(tailnet.Logger(logger.Named("net.router")), tunDev, wireguardMonitor)
if err != nil {
return NetworkStack{}, xerrors.Errorf("create router: %w", err)
}
dnsConfigurator, err := dns.NewOSConfigurator(tailnet.Logger(logger.Named("net.dns")), tunName)
if err != nil {
return NetworkStack{}, xerrors.Errorf("create dns configurator: %w", err)
}
return NetworkStack{
WireguardMonitor: nil, // default is fine
TUNDevice: tunDev,
Router: coderRouter,
DNSConfigurator: dnsConfigurator,
}, nil
}
// tstunNewOrRetry is a wrapper around tstun.New that retries on Windows for certain
// errors.
//
// This is taken from Tailscale:
// https://github.com/tailscale/tailscale/blob/3abfbf50aebbe3ba57dc749165edb56be6715c0a/cmd/tailscaled/tailscaled_windows.go#L107
func tstunNewWithWindowsRetries(logf logger.Logf, tunName string) (_ tun.Device, devName string, _ error) {
r := retry.New(250*time.Millisecond, 10*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
for r.Wait(ctx) {
dev, devName, err := tstun.New(logf, tunName)
if err == nil {
return dev, devName, err
}
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) || windowsUptime() < 10*time.Minute {
// Wintun is not installing correctly. Dump the state of NetSetupSvc
// (which is a user-mode service that must be active for network devices
// to install) and its dependencies to the log.
winutil.LogSvcState(logf, "NetSetupSvc")
}
}
return nil, "", ctx.Err()
}
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
)
func windowsUptime() time.Duration {
r, _, _ := getTickCount64Proc.Call()
return time.Duration(int64(r)) * time.Millisecond
}
// TODO(@dean): implement a way to install/uninstall the wintun driver, most
// likely as a CLI command
//
// This is taken from Tailscale:
// https://github.com/tailscale/tailscale/blob/3abfbf50aebbe3ba57dc749165edb56be6715c0a/cmd/tailscaled/tailscaled_windows.go#L543
func uninstallWinTun(logf logger.Logf) {
dll := windows.NewLazyDLL("wintun.dll")
if err := dll.Load(); err != nil {
logf("Cannot load wintun.dll for uninstall: %v", err)
return
}
logf("Removing wintun driver...")
err := wintun.Uninstall()
logf("Uninstall: %v", err)
}
// TODO(@dean): remove
var _ = uninstallWinTun