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.
v2.1.2's release CI failed on the Windows build leg: TestApplyHostsOverride_*
call applyHostsOverride(), which correctly refuses unconditionally on
non-Linux (hosts_override.go) -- but the tests didn't account for `go
test ./...` running on every platform the CI matrix builds for, only
Linux. Skip them on non-Linux with a clear reason instead.
Implements the Linux half of AGENT_LOCAL_DISCOVERY_SPEC.md: when a local
theta-gateway/theta-proxy announces itself via mDNS as fronting this
agent's ServerURL host, skip the relay/WAN path and talk to it directly.
Off by default (config.PreferLocalDirectory / prefer_local_directory)
since it changes host name resolution.
- local_discovery.go: polls for _theta-suite._tcp every 30s via
hashicorp/mdns, matches the TXT "hosts" field against the agent's own
target host, applies/clears a hosts-file override on change. Presence/
absence of the mDNS announcement IS the "on this LAN or not" signal --
no separate network detection needed, since multicast doesn't cross
routers/VLANs.
- hosts_override.go: writes a single marked, idempotent block into
/etc/hosts (never touches anything else in the file); clearing removes
the block entirely rather than leaving empty markers.
- HARD RULE preserved: this only ever changes DNS resolution, never TLS
trust -- nothing here touches certificate validation, so a spoofed
rogue mDNS announcement produces a TLS failure against the real
hostname's cert, not a silent MITM.
Verified end-to-end with real containers (Node mDNS announcer + this
actual Go binary, not mocked), which caught two real bugs neither showed
up in code review:
1. mdns.Lookup()'s DefaultParams() requests both IPv4 and IPv6. The
underlying client sends the v4 query (which got a real, valid
response per a packet capture), then sends the v6 query, and if THAT
send fails (no IPv6 route -- common on plain v4 hosts/containers) the
whole Query() returns that error synchronously, before ever entering
the response-listening loop. The v4 response was silently discarded.
Fixed by building QueryParam manually with DisableIPv6: true instead
of using the Lookup() convenience wrapper.
2. The original hosts-file writer used write-tmp-then-rename for
atomicity. /etc/hosts is frequently a bind mount (every container
runtime does this) -- rename() onto a bind-mounted file fails with
EBUSY, since you can't atomically replace a mountpoint. Switched to
truncate-and-rewrite in place; the process-local mutex already
serializes writers, so the lost atomicity is a small, acceptable
tradeoff against a confirmed hard failure.
Full cycle verified: announcer starts -> agent discovers it -> hosts
override applied -> announcer stops -> override cleanly reverts, no
stale entry, no discovery trace left.
Windows/macOS remain unbuilt -- need platform-native testing this
environment can't do (see AGENT_LOCAL_DISCOVERY_SPEC.md §3's open
question: hosts-file edits vs. a local stub resolver, per-OS elevation
and DNS-cache behavior).