171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
//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
|
|
}
|