feat(discovery): Windows local-discovery + local route pinning + prompt reconnect

Completes the mDNS local-discovery feature on Windows (was Linux-only since
v2.1.2). Three parts:

1. Windows hosts override (hosts_override_windows.go): %SystemRoot%...\\hosts,
   CRLF-aware read/write, ipconfig /flushdns after each change. The agent runs
   as a SYSTEM service so elevation is a non-issue. hosts_override.go split
   into shared rewrite logic + platform files; the hosts tests now run the real
   Windows write path on CI instead of skipping.

2. Local route pinning (local_route*.go): the hosts override only fixes name
   resolution -- the packet path is the routing table's job. If the WG mesh
   tunnel is up with AllowedIPs covering the LAN (or full-tunnel 0.0.0.0/0) it
   swallows the direct connection. Discovery now pins a /32 host route via the
   owning local interface (route.exe metric 1 on Windows, ip route replace on
   Linux) and drops it on revert. Closes a gap in the shipped Linux path too.

3. Prompt reconnect: apply/revert signals the WS loop so it reconnects
   immediately instead of waiting out the 5s backoff.

Route/hosts code is injectable + unit tested; go test passes natively on
Windows (this machine), and linux/amd64 + windows/arm64 cross-builds are clean.
This commit is contained in:
2026-08-10 17:24:21 -07:00
parent a3eeed8112
commit b2ad8f4844
11 changed files with 525 additions and 49 deletions
+118
View File
@@ -0,0 +1,118 @@
package main
import (
"errors"
"net"
"strings"
"testing"
)
var (
errAlreadyExists = errors.New("already exists")
errRouteOp = errors.New("route op failed")
)
func withFakeInterfaces(nets []*net.IPNet) func() {
orig := localInterfaces
localInterfaces = func() ([]localIface, error) {
return []localIface{{index: 7, name: "fake0", nets: nets}}, nil
}
return func() { localInterfaces = orig }
}
func TestInterfaceForIP_FindsOwningInterface(t *testing.T) {
restore := withFakeInterfaces([]*net.IPNet{ipNet("192.168.1.0/24")})
defer restore()
idx, name, ok := interfaceForIP("192.168.1.50")
if !ok || idx != 7 || name != "fake0" {
t.Fatalf("interfaceForIP(192.168.1.50) = (%d, %q, %v), want (7, fake0, true)", idx, name, ok)
}
}
func TestInterfaceForIP_NotOnLocalSegment(t *testing.T) {
restore := withFakeInterfaces([]*net.IPNet{ipNet("192.168.1.0/24")})
defer restore()
if _, _, ok := interfaceForIP("10.99.99.99"); ok {
t.Fatal("interfaceForIP should not claim a non-local IP")
}
}
func TestInterfaceForIP_InvalidIP(t *testing.T) {
restore := withFakeInterfaces([]*net.IPNet{ipNet("192.168.1.0/24")})
defer restore()
if _, _, ok := interfaceForIP("not-an-ip"); ok {
t.Fatal("interfaceForIP should reject garbage input")
}
}
func TestInterfaceForIP_TakesFirstMatchAcrossInterfaces(t *testing.T) {
orig := localInterfaces
defer func() { localInterfaces = orig }()
localInterfaces = func() ([]localIface, error) {
return []localIface{
{index: 1, name: "eth0", nets: []*net.IPNet{ipNet("10.0.0.0/24")}},
{index: 2, name: "wlan0", nets: []*net.IPNet{ipNet("192.168.50.0/24")}},
}, nil
}
idx, name, ok := interfaceForIP("192.168.50.9")
if !ok || idx != 2 || name != "wlan0" {
t.Fatalf("expected wlan0 (idx 2) to own 192.168.50.9, got (%d, %q, %v)", idx, name, ok)
}
}
func TestAddHostRoute_IgnoresAlreadyExists(t *testing.T) {
orig := routeExec
defer func() { routeExec = orig }()
routeExec = func(name string, args ...string) ([]byte, error) {
return []byte("The object already exists."), errAlreadyExists
}
if err := addHostRoute("192.168.1.50", 7, "fake0"); err != nil {
t.Fatalf("addHostRoute should treat an already-present route as success, got %v", err)
}
}
func TestAddHostRoute_ReturnsOtherErrors(t *testing.T) {
orig := routeExec
defer func() { routeExec = orig }()
routeExec = func(name string, args ...string) ([]byte, error) {
return []byte("The parameter is incorrect."), errRouteOp
}
if err := addHostRoute("192.168.1.50", 7, "fake0"); err == nil {
t.Fatal("addHostRoute should surface non-already-exists errors")
}
}
func TestDelHostRoute_IgnoresMissingRoute(t *testing.T) {
orig := routeExec
defer func() { routeExec = orig }()
routeExec = func(name string, args ...string) ([]byte, error) {
return []byte("route not found"), errRouteOp
}
if err := delHostRoute("192.168.1.50"); err != nil {
t.Fatalf("delHostRoute should treat a missing route as success, got %v", err)
}
}
func TestApplyLocalRoute_FailsWhenNoOwningInterface(t *testing.T) {
restore := withFakeInterfaces([]*net.IPNet{ipNet("192.168.1.0/24")})
defer restore()
if err := applyLocalRoute("172.16.0.9"); err == nil || !strings.Contains(err.Error(), "no local interface") {
t.Fatalf("applyLocalRoute should fail with a clear error for a non-local IP, got %v", err)
}
}
func ipNet(cidr string) *net.IPNet {
_, n, err := net.ParseCIDR(cidr)
if err != nil {
panic(err)
}
return n
}