Files
theta-agent/hosts_override_windows.go
wmantly b2ad8f4844 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.
2026-08-10 17:24:21 -07:00

64 lines
2.1 KiB
Go

//go:build windows
package main
import (
"log"
"os"
"strings"
)
// Windows hosts override (AGENT_LOCAL_DISCOVERY_SPEC.md):
// - The hosts file lives at %SystemRoot%\System32\drivers\etc\hosts. The
// theta-agent runs as a SYSTEM service (DESIGN-WINDOWS.md), so elevation
// is not a blocker here -- SYSTEM can write it directly.
// - Windows caches DNS in the DNS Client service. An edit to the hosts file
// does not immediately change resolution until the cache is flushed, so
// every successful change runs `ipconfig /flushdns`.
// - Windows hosts files conventionally use CRLF line endings; the shared
// rewrite normalizes on read and writes back with hostsEOL().
// hostsFilePathWindows, when set (tests only), redirects hostsFilePath() at a
// temp file so unit tests never touch the real system hosts file.
var hostsFilePathWindows string
// systemHostsPath resolves the real system hosts file.
func systemHostsPath() string {
root := os.Getenv("SystemRoot")
if root == "" {
root = `C:\Windows`
}
return root + `\System32\drivers\etc\hosts`
}
func hostsFilePath() string {
if hostsFilePathWindows != "" {
return hostsFilePathWindows
}
return systemHostsPath()
}
func hostsEOL() string { return "\r\n" }
// flushDNSOnHostsChange invalidates the Windows DNS cache after a hosts edit.
// No-op when a test redirected the path to a temp file -- a temp file has no
// cached entries and running ipconfig here would just slow the tests down.
func flushDNSOnHostsChange() {
if hostsFilePathWindows != "" {
return
}
out, err := (&SystemExecutor{}).Execute("ipconfig", "/flushdns")
if err != nil {
log.Printf("[local-discovery] ipconfig /flushdns failed (hosts override may not take effect immediately): %v: %s", err, strings.TrimSpace(string(out)))
}
}
// setTestHostsPath points hostsFilePath() at a temp file for tests and
// returns a restore func. Exists in both platform files so the shared test
// code can compile everywhere.
func setTestHostsPath(path string) (restore func()) {
prev := hostsFilePathWindows
hostsFilePathWindows = path
return func() { hostsFilePathWindows = prev }
}