初版功能完成
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled

This commit is contained in:
qsc
2026-08-29 13:12:17 +08:00
commit 142e5dc7d6
217 changed files with 21313 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,152 @@
//go:build windows
// Package windowsplatform centralizes RemLink-owned Windows networking changes.
package windowsplatform
import (
"errors"
"fmt"
"net/netip"
"sync"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"remlink/internal/platform/windows/wintunruntime"
)
const (
AdapterName = "RemLink"
AdapterTunnelType = "RemLink"
DefaultMTU = 1280
)
var ErrAdministratorRequired = errors.New("administrator privileges are required to manage the RemLink Wintun adapter")
// AdapterConfig defines the single IPv4 Overlay address owned by RemLink.
type AdapterConfig struct {
Address netip.Prefix
MTU int
}
// Adapter owns one live Wintun session. Closing it leaves the persistent
// Windows adapter installed so a later RemLink process can reuse it.
type Adapter struct {
device tun.Device
luid winipcfg.LUID
interfaceIndex uint32
closeOnce sync.Once
closeErr error
}
// OpenRemLink creates or reuses the one fixed-name RemLink Wintun, applies its
// IPv4 address and MTU through Windows APIs, and returns the live TUN device.
func OpenRemLink(config AdapterConfig) (*Adapter, error) {
if err := validateAdapterConfig(config); err != nil {
return nil, err
}
if !windows.GetCurrentProcessToken().IsElevated() {
return nil, ErrAdministratorRequired
}
if _, err := wintunruntime.PreloadDefault(); err != nil {
return nil, err
}
tun.WintunTunnelType = AdapterTunnelType
base, err := tun.CreateTUN(AdapterName, config.MTU)
if err != nil {
return nil, fmt.Errorf("create or reuse %s Wintun: %w", AdapterName, err)
}
succeeded := false
defer func() {
if !succeeded {
_ = base.Close()
}
}()
luidSource, ok := base.(interface{ LUID() uint64 })
if !ok {
return nil, errors.New("wireguard-go Windows TUN does not expose its interface LUID")
}
luid := winipcfg.LUID(luidSource.LUID())
if luid == 0 {
return nil, errors.New("RemLink Wintun returned an invalid interface LUID")
}
if err := luid.SetIPAddressesForFamily(
winipcfg.AddressFamily(windows.AF_INET),
[]netip.Prefix{config.Address},
); err != nil {
return nil, fmt.Errorf("set RemLink IPv4 address %s: %w", config.Address, err)
}
ipInterface, err := luid.IPInterface(winipcfg.AddressFamily(windows.AF_INET))
if err != nil {
return nil, fmt.Errorf("read RemLink IPv4 interface: %w", err)
}
ipInterface.NLMTU = uint32(config.MTU)
if err := ipInterface.Set(); err != nil {
return nil, fmt.Errorf("set RemLink MTU %d: %w", config.MTU, err)
}
interfaceRow, err := luid.Interface()
if err != nil {
return nil, fmt.Errorf("read RemLink interface index: %w", err)
}
succeeded = true
return &Adapter{
device: base,
luid: luid,
interfaceIndex: interfaceRow.InterfaceIndex,
}, nil
}
// Device returns the TUN device. Ownership transfers to wireguard-go when it is
// passed to clientwg.NewDevice; callers must then close the wireguard-go owner.
func (a *Adapter) Device() tun.Device {
return a.device
}
// LUID returns the Windows interface locally unique identifier.
func (a *Adapter) LUID() uint64 {
return uint64(a.luid)
}
// InterfaceIndex returns the Windows interface index used by later RouteManager work.
func (a *Adapter) InterfaceIndex() uint32 {
return a.interfaceIndex
}
// Close stops the Wintun session without deleting the persistent adapter.
func (a *Adapter) Close() error {
if a == nil || a.device == nil {
return nil
}
a.closeOnce.Do(func() {
a.closeErr = a.device.Close()
})
return a.closeErr
}
func validateAdapterConfig(config AdapterConfig) error {
if !config.Address.IsValid() || !config.Address.Addr().Is4() {
return errors.New("RemLink adapter address must be a valid IPv4 prefix")
}
if config.Address.Bits() > 30 {
return errors.New("RemLink adapter prefix must leave usable host addresses")
}
if config.Address.Addr() == config.Address.Masked().Addr() || config.Address.Addr() == lastAddress(config.Address) {
return errors.New("RemLink adapter address must not be the network or broadcast address")
}
if config.MTU < 576 || config.MTU > 65535 {
return errors.New("RemLink adapter MTU must be between 576 and 65535")
}
return nil
}
func lastAddress(prefix netip.Prefix) netip.Addr {
bytes := prefix.Masked().Addr().As4()
value := uint32(bytes[0])<<24 | uint32(bytes[1])<<16 | uint32(bytes[2])<<8 | uint32(bytes[3])
value |= ^uint32(0) >> prefix.Bits()
return netip.AddrFrom4([4]byte{byte(value >> 24), byte(value >> 16), byte(value >> 8), byte(value)})
}
@@ -0,0 +1,35 @@
//go:build windows
package windowsplatform
import (
"net/netip"
"testing"
)
func TestValidateAdapterConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
address string
mtu int
wantErr bool
}{
{name: "valid", address: "10.88.0.2/16", mtu: 1280},
{name: "IPv6", address: "fd00::2/64", mtu: 1280, wantErr: true},
{name: "network", address: "10.88.0.0/16", mtu: 1280, wantErr: true},
{name: "broadcast", address: "10.88.255.255/16", mtu: 1280, wantErr: true},
{name: "small MTU", address: "10.88.0.2/16", mtu: 575, wantErr: true},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
prefix := netip.MustParsePrefix(test.address)
err := validateAdapterConfig(AdapterConfig{Address: prefix, MTU: test.mtu})
if (err != nil) != test.wantErr {
t.Fatalf("validateAdapterConfig() error = %v, wantErr %v", err, test.wantErr)
}
})
}
}
@@ -0,0 +1,12 @@
//go:build !windows
package dpapi
import "errors"
var ErrWindowsRequired = errors.New("DPAPI requires Windows")
type Protector struct{}
func (Protector) Protect([]byte) ([]byte, error) { return nil, ErrWindowsRequired }
func (Protector) Unprotect([]byte) ([]byte, error) { return nil, ErrWindowsRequired }
@@ -0,0 +1,64 @@
//go:build windows
// Package dpapi protects Windows Node secrets using the operating system DPAPI.
package dpapi
import (
"errors"
"fmt"
"unsafe"
"golang.org/x/sys/windows"
)
var entropy = []byte("RemLink-v1-NodeIdentity")
// Protector uses machine-scoped DPAPI so Console/service identities can read
// the package-local identity after Windows restart. Directory ACLs remain required.
type Protector struct{}
func (Protector) Protect(plain []byte) ([]byte, error) {
if len(plain) == 0 {
return nil, errors.New("DPAPI plaintext must not be empty")
}
input := blob(plain)
extra := blob(entropy)
var output windows.DataBlob
name, err := windows.UTF16PtrFromString("RemLink Node Identity")
if err != nil {
return nil, err
}
flags := uint32(windows.CRYPTPROTECT_LOCAL_MACHINE | windows.CRYPTPROTECT_UI_FORBIDDEN)
if err := windows.CryptProtectData(&input, name, &extra, 0, nil, flags, &output); err != nil {
return nil, fmt.Errorf("protect secret with DPAPI: %w", err)
}
return copyAndFree(output)
}
func (Protector) Unprotect(ciphertext []byte) ([]byte, error) {
if len(ciphertext) == 0 {
return nil, errors.New("DPAPI ciphertext must not be empty")
}
input := blob(ciphertext)
extra := blob(entropy)
var output windows.DataBlob
flags := uint32(windows.CRYPTPROTECT_UI_FORBIDDEN)
if err := windows.CryptUnprotectData(&input, nil, &extra, 0, nil, flags, &output); err != nil {
return nil, fmt.Errorf("unprotect secret with DPAPI: %w", err)
}
return copyAndFree(output)
}
func blob(value []byte) windows.DataBlob {
return windows.DataBlob{Size: uint32(len(value)), Data: &value[0]}
}
func copyAndFree(value windows.DataBlob) ([]byte, error) {
if value.Data == nil || value.Size == 0 {
return nil, errors.New("DPAPI returned empty output")
}
defer windows.LocalFree(windows.Handle(uintptr(unsafe.Pointer(value.Data))))
result := make([]byte, int(value.Size))
copy(result, unsafe.Slice(value.Data, int(value.Size)))
return result, nil
}
@@ -0,0 +1,27 @@
//go:build windows
package dpapi
import (
"bytes"
"testing"
)
func TestProtectUnprotect(t *testing.T) {
protector := Protector{}
plain := []byte("RemLink test secret")
protected, err := protector.Protect(plain)
if err != nil {
t.Fatal(err)
}
if bytes.Equal(protected, plain) {
t.Fatal("DPAPI returned plaintext")
}
unprotected, err := protector.Unprotect(protected)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(unprotected, plain) {
t.Fatalf("unprotected = %q, want %q", unprotected, plain)
}
}
@@ -0,0 +1,55 @@
// Package netinfo inspects local IPv4 networks without mutating Windows state.
package netinfo
import (
"fmt"
"net"
"net/netip"
)
// DirectIPv4Prefixes returns assigned non-loopback IPv4 interface prefixes,
// excluding the named RemLink adapter.
func DirectIPv4Prefixes(excludedInterface string) ([]netip.Prefix, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("list network interfaces: %w", err)
}
var prefixes []netip.Prefix
for _, networkInterface := range interfaces {
if networkInterface.Name == excludedInterface || networkInterface.Flags&net.FlagLoopback != 0 || networkInterface.Flags&net.FlagUp == 0 {
continue
}
addresses, err := networkInterface.Addrs()
if err != nil {
return nil, fmt.Errorf("list addresses for %s: %w", networkInterface.Name, err)
}
for _, address := range addresses {
prefix, err := netip.ParsePrefix(address.String())
if err != nil || !prefix.Addr().Is4() {
continue
}
prefixes = append(prefixes, prefix.Masked())
}
}
return prefixes, nil
}
// PrefixesOverlap performs true containment-based IPv4 prefix overlap.
func PrefixesOverlap(left, right netip.Prefix) bool {
if !left.Addr().Is4() || !right.Addr().Is4() {
return false
}
left = left.Masked()
right = right.Masked()
return left.Contains(right.Addr()) || right.Contains(left.Addr())
}
// FindConflict returns the first local network overlapping desired.
func FindConflict(desired netip.Prefix, existing []netip.Prefix) (netip.Prefix, bool) {
for _, candidate := range existing {
if PrefixesOverlap(desired, candidate) {
return candidate, true
}
}
return netip.Prefix{}, false
}
@@ -0,0 +1,23 @@
package netinfo
import (
"net/netip"
"testing"
)
func TestPrefixesOverlap(t *testing.T) {
for _, test := range []struct {
left, right string
want bool
}{
{"192.168.0.0/16", "192.168.13.0/24", true},
{"192.168.13.0/24", "192.168.13.10/32", true},
{"10.88.0.0/16", "10.89.0.0/16", false},
{"0.0.0.0/0", "10.88.0.0/16", true},
} {
got := PrefixesOverlap(netip.MustParsePrefix(test.left), netip.MustParsePrefix(test.right))
if got != test.want {
t.Errorf("PrefixesOverlap(%s, %s) = %v, want %v", test.left, test.right, got, test.want)
}
}
}
@@ -0,0 +1,80 @@
// Package route owns RemLink Remote routes and performs conflict/lookup checks.
package route
import (
"errors"
"net/netip"
"remlink/internal/platform/windows/netinfo"
)
type LookupResult string
const (
LookupDirect LookupResult = "DIRECT"
LookupRouted LookupResult = "ROUTED"
LookupDefaultOnly LookupResult = "DEFAULT_ONLY"
LookupNoRoute LookupResult = "NO_ROUTE"
LookupOverlayConflict LookupResult = "OVERLAY_CONFLICT"
)
type Entry struct {
InterfaceLUID uint64
Destination netip.Prefix
NextHop netip.Addr
}
// ConflictsFrom applies the Engineer rule: ignore default and RemLink-owned
// routes, reject any other prefix overlap.
func ConflictsFrom(entries []Entry, desired netip.Prefix, remLinkLUID uint64) []Entry {
var conflicts []Entry
for _, entry := range entries {
if entry.InterfaceLUID == remLinkLUID || !entry.Destination.Addr().Is4() || entry.Destination.Bits() == 0 {
continue
}
if netinfo.PrefixesOverlap(desired, entry.Destination) {
conflicts = append(conflicts, entry)
}
}
return conflicts
}
// LookupFrom returns the most-specific Windows route classification used by Site PREPARE.
func LookupFrom(entries []Entry, target netip.Addr, overlay netip.Prefix, remLinkLUID uint64) LookupResult {
if !target.Is4() {
return LookupNoRoute
}
if overlay.Contains(target) {
return LookupOverlayConflict
}
var best *Entry
for index := range entries {
entry := &entries[index]
if !entry.Destination.Contains(target) || entry.InterfaceLUID == remLinkLUID {
continue
}
if best == nil || entry.Destination.Bits() > best.Destination.Bits() {
best = entry
}
}
if best == nil {
return LookupNoRoute
}
if best.Destination.Bits() == 0 {
return LookupDefaultOnly
}
if !best.NextHop.IsValid() || best.NextHop.IsUnspecified() {
return LookupDirect
}
return LookupRouted
}
func validateRemote(prefix, overlay netip.Prefix) error {
if !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 {
return errors.New("Remote CIDR must be canonical IPv4 and must not be 0.0.0.0/0")
}
if netinfo.PrefixesOverlap(prefix, overlay) {
return errors.New("Remote CIDR overlaps Overlay CIDR")
}
return nil
}
@@ -0,0 +1,55 @@
package route
import (
"net/netip"
"testing"
)
func TestConflictsIgnoresDefaultAndRemLink(t *testing.T) {
entries := []Entry{
{InterfaceLUID: 1, Destination: netip.MustParsePrefix("0.0.0.0/0"), NextHop: netip.MustParseAddr("192.0.2.1")},
{InterfaceLUID: 2, Destination: netip.MustParsePrefix("192.168.0.0/16")},
{InterfaceLUID: 99, Destination: netip.MustParsePrefix("192.168.13.0/24")},
}
conflicts := ConflictsFrom(entries, netip.MustParsePrefix("192.168.13.0/24"), 99)
if len(conflicts) != 1 || conflicts[0].Destination.String() != "192.168.0.0/16" {
t.Fatalf("conflicts = %+v", conflicts)
}
}
func TestLookupClassifications(t *testing.T) {
overlay := netip.MustParsePrefix("10.88.0.0/16")
entries := []Entry{
{InterfaceLUID: 1, Destination: netip.MustParsePrefix("0.0.0.0/0"), NextHop: netip.MustParseAddr("192.0.2.1")},
{InterfaceLUID: 2, Destination: netip.MustParsePrefix("192.168.13.0/24"), NextHop: netip.IPv4Unspecified()},
{InterfaceLUID: 3, Destination: netip.MustParsePrefix("172.16.0.0/16"), NextHop: netip.MustParseAddr("192.0.2.254")},
}
for _, test := range []struct {
target string
want LookupResult
}{
{"10.88.0.5", LookupOverlayConflict},
{"192.168.13.10", LookupDirect},
{"172.16.4.2", LookupRouted},
{"8.8.8.8", LookupDefaultOnly},
} {
if got := LookupFrom(entries, netip.MustParseAddr(test.target), overlay, 99); got != test.want {
t.Errorf("LookupFrom(%s) = %s, want %s", test.target, got, test.want)
}
}
if got := LookupFrom(nil, netip.MustParseAddr("192.168.1.2"), overlay, 99); got != LookupNoRoute {
t.Fatalf("empty route lookup = %s", got)
}
}
func TestValidateRemote(t *testing.T) {
overlay := netip.MustParsePrefix("10.88.0.0/16")
for _, invalid := range []string{"0.0.0.0/0", "10.88.5.0/24"} {
if err := validateRemote(netip.MustParsePrefix(invalid), overlay); err == nil {
t.Errorf("validateRemote accepted %s", invalid)
}
}
if err := validateRemote(netip.MustParsePrefix("192.168.13.0/24"), overlay); err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,26 @@
//go:build !windows
package route
import (
"errors"
"net/netip"
)
var ErrWindowsRequired = errors.New("Windows RouteManager requires Windows")
type OwnershipStore interface {
LoadOwnedRoutes() ([]netip.Prefix, error)
SaveOwnedRoutes([]netip.Prefix) error
}
type Manager struct{}
func NewManager(uint64, netip.Prefix, OwnershipStore) (*Manager, error) {
return nil, ErrWindowsRequired
}
func (*Manager) AddRemote(netip.Prefix) error { return ErrWindowsRequired }
func (*Manager) RemoveRemote(netip.Prefix) error { return ErrWindowsRequired }
func (*Manager) Conflicts(netip.Prefix) ([]Entry, error) { return nil, ErrWindowsRequired }
func (*Manager) Lookup(netip.Addr) (LookupResult, error) { return LookupNoRoute, ErrWindowsRequired }
func (*Manager) Reconcile() error { return ErrWindowsRequired }
@@ -0,0 +1,151 @@
//go:build windows
package route
import (
"errors"
"fmt"
"net/netip"
"sync"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
const remoteRouteMetric = 0
type OwnershipStore interface {
LoadOwnedRoutes() ([]netip.Prefix, error)
SaveOwnedRoutes([]netip.Prefix) error
}
// Manager is the sole writer for Engineer Remote CIDR routes.
type Manager struct {
mu sync.Mutex
luid winipcfg.LUID
overlay netip.Prefix
store OwnershipStore
owned map[netip.Prefix]struct{}
}
func NewManager(luid uint64, overlay netip.Prefix, store OwnershipStore) (*Manager, error) {
if luid == 0 || !overlay.Addr().Is4() || store == nil {
return nil, errors.New("RouteManager requires RemLink LUID, IPv4 Overlay, and ownership store")
}
ownedRoutes, err := store.LoadOwnedRoutes()
if err != nil {
return nil, err
}
owned := make(map[netip.Prefix]struct{}, len(ownedRoutes))
for _, prefix := range ownedRoutes {
owned[prefix.Masked()] = struct{}{}
}
return &Manager{luid: winipcfg.LUID(luid), overlay: overlay.Masked(), store: store, owned: owned}, nil
}
func (m *Manager) AddRemote(prefix netip.Prefix) error {
prefix = prefix.Masked()
if err := validateRemote(prefix, m.overlay); err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.owned[prefix]; exists {
return nil
}
conflicts, err := m.conflictsLocked(prefix)
if err != nil {
return err
}
if len(conflicts) != 0 {
return fmt.Errorf("Remote CIDR %s conflicts with existing route %s", prefix, conflicts[0].Destination)
}
if err := m.luid.AddRoute(prefix, netip.IPv4Unspecified(), remoteRouteMetric); err != nil {
return fmt.Errorf("add RemLink Remote route %s: %w", prefix, err)
}
m.owned[prefix] = struct{}{}
if err := m.persistLocked(); err != nil {
delete(m.owned, prefix)
_ = m.luid.DeleteRoute(prefix, netip.IPv4Unspecified())
return err
}
return nil
}
func (m *Manager) RemoveRemote(prefix netip.Prefix) error {
prefix = prefix.Masked()
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.owned[prefix]; !exists {
return nil
}
if err := m.luid.DeleteRoute(prefix, netip.IPv4Unspecified()); err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("remove RemLink Remote route %s: %w", prefix, err)
}
delete(m.owned, prefix)
return m.persistLocked()
}
func (m *Manager) Conflicts(prefix netip.Prefix) ([]Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.conflictsLocked(prefix.Masked())
}
func (m *Manager) Lookup(target netip.Addr) (LookupResult, error) {
entries, err := windowsEntries()
if err != nil {
return LookupNoRoute, err
}
return LookupFrom(entries, target, m.overlay, uint64(m.luid)), nil
}
// Reconcile removes every route recorded by a previous non-Active Session.
func (m *Manager) Reconcile() error {
m.mu.Lock()
defer m.mu.Unlock()
for prefix := range m.owned {
if err := m.luid.DeleteRoute(prefix, netip.IPv4Unspecified()); err != nil && !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("reconcile stale Remote route %s: %w", prefix, err)
}
delete(m.owned, prefix)
}
return m.persistLocked()
}
func (m *Manager) conflictsLocked(prefix netip.Prefix) ([]Entry, error) {
entries, err := windowsEntries()
if err != nil {
return nil, err
}
return ConflictsFrom(entries, prefix, uint64(m.luid)), nil
}
func (m *Manager) persistLocked() error {
prefixes := make([]netip.Prefix, 0, len(m.owned))
for prefix := range m.owned {
prefixes = append(prefixes, prefix)
}
if err := m.store.SaveOwnedRoutes(prefixes); err != nil {
return fmt.Errorf("persist RemLink route ownership: %w", err)
}
return nil
}
func windowsEntries() ([]Entry, error) {
rows, err := winipcfg.GetIPForwardTable2(winipcfg.AddressFamily(windows.AF_INET))
if err != nil {
return nil, fmt.Errorf("read Windows IPv4 route table: %w", err)
}
entries := make([]Entry, 0, len(rows))
for index := range rows {
prefix := rows[index].DestinationPrefix.Prefix()
if !prefix.Addr().Is4() {
continue
}
entries = append(entries, Entry{
InterfaceLUID: uint64(rows[index].InterfaceLUID), Destination: prefix.Masked(), NextHop: rows[index].NextHop.Addr(),
})
}
return entries, nil
}
@@ -0,0 +1 @@
@@ -0,0 +1,22 @@
//go:build windows && amd64
package wintunruntime
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
)
//go:embed assets/amd64/wintun.dll
var embeddedDLL []byte
func assetBytes() ([]byte, [sha256.Size]byte, error) {
var expected [sha256.Size]byte
decoded, err := hex.DecodeString(DLLSHA256AMD64)
if err != nil {
return nil, expected, err
}
copy(expected[:], decoded)
return embeddedDLL, expected, nil
}
@@ -0,0 +1,13 @@
//go:build windows && !amd64
package wintunruntime
import (
"crypto/sha256"
"fmt"
"runtime"
)
func assetBytes() ([]byte, [sha256.Size]byte, error) {
return nil, [sha256.Size]byte{}, fmt.Errorf("Phase 1 Wintun asset is not bundled for windows/%s", runtime.GOARCH)
}
@@ -0,0 +1,193 @@
//go:build windows
// Package wintunruntime installs and preloads the pinned embedded Wintun DLL.
package wintunruntime
import (
"crypto/sha256"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wintun"
"remlink/internal/appdir"
)
const (
Version = "0.14.1"
DLLName = "wintun.dll"
DLLSHA256AMD64 = "e5da8447dc2c320edc0fc52fa01885c103de8c118481f683643cacc3220dafce"
)
var (
preloadOnce sync.Once
preloadPath string
preloadErr error
// Keep the module referenced for the process lifetime. The upstream Go
// binding subsequently resolves the already-loaded DLL by base name.
preloadHandle windows.Handle
)
// Install writes the embedded signed DLL into one portable package directory.
// An existing verified file is reused; an unexpected file is replaced.
func Install(packageDirectory string) (string, error) {
if packageDirectory == "" {
return "", errors.New("package directory must not be empty")
}
data, expectedHash, err := assetBytes()
if err != nil {
return "", err
}
if err := os.MkdirAll(packageDirectory, 0o750); err != nil {
return "", fmt.Errorf("create Wintun runtime directory: %w", err)
}
target := filepath.Join(packageDirectory, DLLName)
verified, err := fileHasSHA256Stable(target, expectedHash)
if err != nil {
return "", err
}
if verified {
return target, nil
}
if err := writeAtomically(target, data); err != nil {
return "", err
}
verified, err = fileHasSHA256Stable(target, expectedHash)
if err != nil {
return "", err
}
if !verified {
return "", errors.New("installed Wintun DLL failed SHA-256 verification")
}
return target, nil
}
// PreloadDefault installs and loads Wintun beside the running executable before
// the upstream lazy binding attempts to resolve wintun.dll by its base name.
func PreloadDefault() (string, error) {
root, err := appdir.Executable()
if err != nil {
return "", err
}
return Preload(root)
}
// Preload installs and loads Wintun exactly once for the process.
func Preload(packageDirectory string) (string, error) {
preloadOnce.Do(func() {
preloadPath, preloadErr = Install(packageDirectory)
if preloadErr != nil {
return
}
preloadHandle, preloadErr = windows.LoadLibraryEx(
preloadPath,
0,
windows.LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|windows.LOAD_LIBRARY_SEARCH_SYSTEM32,
)
if preloadErr != nil {
preloadErr = fmt.Errorf("preload Wintun DLL %q: %w", preloadPath, preloadErr)
}
})
return preloadPath, preloadErr
}
// Probe installs/preloads the DLL and asks the official binding for its version.
func Probe() (path string, version string, err error) {
path, err = PreloadDefault()
if err != nil {
return "", "", err
}
version = wintun.Version()
if version == "unknown" {
return "", "", errors.New("official Wintun binding could not resolve the preloaded DLL")
}
return path, version, nil
}
func fileHasSHA256(path string, expected [sha256.Size]byte) (bool, error) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("read existing Wintun DLL: %w", err)
}
return sha256.Sum256(data) == expected, nil
}
func fileHasSHA256Stable(path string, expected [sha256.Size]byte) (bool, error) {
var lastErr error
for attempt := 0; attempt < 16; attempt++ {
verified, err := fileHasSHA256(path, expected)
if err == nil {
return verified, nil
}
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) && !errors.Is(err, windows.ERROR_SHARING_VIOLATION) {
return false, err
}
lastErr = err
time.Sleep(time.Duration(attempt+1) * time.Millisecond)
}
return false, lastErr
}
func writeAtomically(target string, data []byte) error {
temporary, err := os.CreateTemp(filepath.Dir(target), ".wintun-*.tmp")
if err != nil {
return fmt.Errorf("create temporary Wintun DLL: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
if _, err := temporary.Write(data); err != nil {
temporary.Close()
return fmt.Errorf("write temporary Wintun DLL: %w", err)
}
if err := temporary.Sync(); err != nil {
temporary.Close()
return fmt.Errorf("sync temporary Wintun DLL: %w", err)
}
if err := temporary.Close(); err != nil {
return fmt.Errorf("close temporary Wintun DLL: %w", err)
}
if err := os.Chmod(temporaryPath, 0o644); err != nil {
return fmt.Errorf("set Wintun DLL permissions: %w", err)
}
from, err := windows.UTF16PtrFromString(temporaryPath)
if err != nil {
return fmt.Errorf("encode temporary Wintun DLL path: %w", err)
}
to, err := windows.UTF16PtrFromString(target)
if err != nil {
return fmt.Errorf("encode Wintun DLL target path: %w", err)
}
// MoveFileExW publishes the verified bytes in one replace operation. This
// avoids a remove/rename gap where another launch from the same package could
// observe the DLL path as missing. Concurrent publishers
// may briefly hold a Windows file handle; accept their verified result or
// retry only the documented sharing/access failures.
for attempt := 0; attempt < 8; attempt++ {
if verified, verifyErr := fileHasSHA256(target, sha256.Sum256(data)); verifyErr == nil && verified {
return nil
}
err = windows.MoveFileEx(from, to, windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH)
if err == nil {
return nil
}
if !errors.Is(err, windows.ERROR_ACCESS_DENIED) && !errors.Is(err, windows.ERROR_SHARING_VIOLATION) {
return fmt.Errorf("install Wintun DLL: %w", err)
}
time.Sleep(time.Duration(attempt+1) * time.Millisecond)
}
if verified, verifyErr := fileHasSHA256(target, sha256.Sum256(data)); verifyErr == nil && verified {
return nil
}
return fmt.Errorf("install Wintun DLL after concurrent publish retries: %w", err)
}
@@ -0,0 +1,86 @@
//go:build windows && amd64
package wintunruntime
import (
"os"
"path/filepath"
"sync"
"testing"
)
func TestInstallIsVerifiedIdempotentAndRepairsUnexpectedFile(t *testing.T) {
root := t.TempDir()
path, err := Install(root)
if err != nil {
t.Fatalf("Install() error = %v", err)
}
if got, want := path, filepath.Join(root, DLLName); got != want {
t.Fatalf("Install() path = %q, want %q", got, want)
}
first, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
secondPath, err := Install(root)
if err != nil {
t.Fatalf("second Install() error = %v", err)
}
if secondPath != path {
t.Fatalf("second Install() path = %q, want %q", secondPath, path)
}
if err := os.WriteFile(path, []byte("tampered"), 0o644); err != nil {
t.Fatalf("tamper DLL: %v", err)
}
if _, err := Install(root); err != nil {
t.Fatalf("repair Install() error = %v", err)
}
repaired, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read repaired DLL: %v", err)
}
if string(repaired) != string(first) {
t.Fatal("Install() did not repair the unexpected DLL")
}
}
func TestInstallPublishesAtomicallyAcrossConcurrentProcesses(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, DLLName)
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(target, []byte("corrupt"), 0o644); err != nil {
t.Fatal(err)
}
const workers = 16
errorsByWorker := make([]error, workers)
paths := make([]string, workers)
var wait sync.WaitGroup
for index := 0; index < workers; index++ {
wait.Add(1)
go func(index int) {
defer wait.Done()
paths[index], errorsByWorker[index] = Install(root)
}(index)
}
wait.Wait()
for index := range errorsByWorker {
if errorsByWorker[index] != nil || paths[index] != target {
t.Fatalf("worker %d path=%q error=%v", index, paths[index], errorsByWorker[index])
}
}
data, expected, err := assetBytes()
if err != nil {
t.Fatal(err)
}
installed, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
if string(installed) != string(data) {
t.Fatalf("concurrent install produced %d bytes, want %d (expected hash %x)", len(installed), len(data), expected)
}
}