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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user