//go:build windows && rap_vpn_windows_tun package vpnruntime import ( "crypto/sha256" _ "embed" "fmt" "net" "os" "os/exec" "path/filepath" "strings" wgtun "golang.zx2c4.com/wireguard/tun" ) const windowsGatewayMTU = 1420 //go:embed assets/windows/amd64/wintun.dll var embeddedWintunDLL []byte type tunDevice struct { dev wgtun.Device name string } func openGatewayTun(name, addressCIDR, routeCIDR string) (*tunDevice, error) { if _, _, err := net.ParseCIDR(addressCIDR); err != nil { return nil, fmt.Errorf("invalid vpn gateway address %q: %w", addressCIDR, err) } if _, _, err := net.ParseCIDR(routeCIDR); err != nil { return nil, fmt.Errorf("invalid vpn gateway route %q: %w", routeCIDR, err) } if err := ensureWintunDLL(); err != nil { return nil, err } dev, err := wgtun.CreateTUN(name, windowsGatewayMTU) if err != nil { return nil, fmt.Errorf("create wintun interface %s: %w", name, err) } if err := configureGatewayInterface(name, addressCIDR, routeCIDR); err != nil { _ = dev.Close() return nil, err } return &tunDevice{dev: dev, name: name}, nil } func (d *tunDevice) Read(packet []byte) (int, error) { bufs := [][]byte{packet} sizes := []int{0} n, err := d.dev.Read(bufs, sizes, 0) if err != nil { return 0, err } if n <= 0 { return 0, nil } return sizes[0], nil } func (d *tunDevice) Write(packet []byte) (int, error) { n, err := d.dev.Write([][]byte{packet}, 0) if err != nil { return 0, err } if n <= 0 { return 0, nil } return len(packet), nil } func (d *tunDevice) Close() error { _ = removeWindowsGatewayNat() return d.dev.Close() } func configureGatewayInterface(name, addressCIDR, routeCIDR string) error { ip, network, err := net.ParseCIDR(addressCIDR) if err != nil { return fmt.Errorf("invalid vpn gateway address %q: %w", addressCIDR, err) } ones, bits := network.Mask.Size() if bits != 32 || ones <= 0 { return fmt.Errorf("invalid vpn gateway prefix %q", addressCIDR) } _, route, err := net.ParseCIDR(routeCIDR) if err != nil { return fmt.Errorf("invalid vpn gateway route %q: %w", routeCIDR, err) } script := fmt.Sprintf(` $ErrorActionPreference = 'Stop' $alias = %s $address = %s $prefixLength = %d $natPrefix = %s $natName = 'RAPVPN' $adapter = Get-NetAdapter -Name $alias -ErrorAction Stop $adapter | Enable-NetAdapter -Confirm:$false -ErrorAction SilentlyContinue | Out-Null $existing = Get-NetIPAddress -InterfaceAlias $alias -AddressFamily IPv4 -ErrorAction SilentlyContinue foreach ($addr in $existing) { if ($addr.IPAddress -ne $address -or $addr.PrefixLength -ne $prefixLength) { Remove-NetIPAddress -InterfaceAlias $alias -IPAddress $addr.IPAddress -Confirm:$false -ErrorAction SilentlyContinue } } if (-not (Get-NetIPAddress -InterfaceAlias $alias -IPAddress $address -AddressFamily IPv4 -ErrorAction SilentlyContinue)) { New-NetIPAddress -InterfaceAlias $alias -IPAddress $address -PrefixLength $prefixLength -Type Unicast | Out-Null } Set-NetIPInterface -InterfaceAlias $alias -AddressFamily IPv4 -Forwarding Enabled Get-NetIPInterface -AddressFamily IPv4 | Where-Object { $_.ConnectionState -eq 'Connected' -and $_.InterfaceAlias -ne 'Loopback Pseudo-Interface 1' } | Set-NetIPInterface -Forwarding Enabled $existingNat = Get-NetNat -Name $natName -ErrorAction SilentlyContinue if ($existingNat -and $existingNat.InternalIPInterfaceAddressPrefix -ne $natPrefix) { $existingNat | Remove-NetNat -Confirm:$false $existingNat = $null } if (-not $existingNat) { New-NetNat -Name $natName -InternalIPInterfaceAddressPrefix $natPrefix | Out-Null } `, psQuote(name), psQuote(ip.String()), ones, psQuote(route.String())) if err := runPowerShell(script); err != nil { return fmt.Errorf("configure windows vpn gateway interface %s: %w", name, err) } return nil } func removeWindowsGatewayNat() error { return runPowerShell(`Get-NetNat -Name 'RAPVPN' -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false -ErrorAction SilentlyContinue`) } func runPowerShell(script string) error { cmd := exec.Command("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("powershell failed: %w: %s", err, strings.TrimSpace(string(out))) } return nil } func psQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "''") + "'" } func ensureWintunDLL() error { exePath, err := os.Executable() if err != nil { return fmt.Errorf("locate node-agent executable for wintun.dll: %w", err) } target := filepath.Join(filepath.Dir(exePath), "wintun.dll") if payload, err := os.ReadFile(target); err == nil && sameSHA256(payload, embeddedWintunDLL) { return nil } tmp := target + ".tmp" if err := os.WriteFile(tmp, embeddedWintunDLL, 0o644); err != nil { return fmt.Errorf("write embedded wintun.dll: %w", err) } _ = os.Remove(target) if err := os.Rename(tmp, target); err != nil { _ = os.Remove(tmp) return fmt.Errorf("install embedded wintun.dll: %w", err) } return nil } func sameSHA256(a, b []byte) bool { left := sha256.Sum256(a) right := sha256.Sum256(b) return left == right }