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,47 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Windows host-route pinning: add a /32 route for the discovered IP via the
|
||||
// owning interface with metric 1. WireGuard's tunnel service adds routes for
|
||||
// its AllowedIPs with a low metric; a /32 host route at metric 1 wins for the
|
||||
// exact discovered IP, keeping the discovery path direct even with the tunnel
|
||||
// up. `route.exe` is used rather than the wireguard.exe client because the
|
||||
// tunnel is owned by a service we shouldn't rip down just to adjust one route.
|
||||
|
||||
func addHostRoute(ip string, ifaceIndex int, _ string) error {
|
||||
out, err := routeExec("route.exe",
|
||||
"add", ip,
|
||||
"mask", "255.255.255.255",
|
||||
"0.0.0.0", // on-link gateway; the interface index pins the interface
|
||||
"metric", "1",
|
||||
"IF", strconv.Itoa(ifaceIndex),
|
||||
)
|
||||
if err != nil {
|
||||
// Already present (previous apply never reverted, or route.exe
|
||||
// re-add) is the expected steady-state case -- treat as success.
|
||||
if strings.Contains(strings.ToLower(string(out)), "already exists") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("route add %s: %v: %s", ip, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func delHostRoute(ip string) error {
|
||||
out, err := routeExec("route.exe", "delete", ip, "mask", "255.255.255.255")
|
||||
if err != nil {
|
||||
lower := strings.ToLower(string(out))
|
||||
if strings.Contains(lower, "route not found") || strings.Contains(lower, "cannot find") {
|
||||
return nil // nothing to drop; not an error
|
||||
}
|
||||
return fmt.Errorf("route delete %s: %v: %s", ip, err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user