feat(mdns): Linux local-discovery -- skip the WAN relay when on-site
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).
This commit is contained in:
@@ -89,6 +89,10 @@ func runAgent() {
|
||||
// Home detection + tray status push (polls public IP every 60s).
|
||||
go StartHomeMonitor(cfg, func() bool { return wsConnected.Load() })
|
||||
|
||||
// mDNS local-discovery (MULTI_SITE_SPEC.md Appendix B) -- no-op unless
|
||||
// prefer_local_directory is set.
|
||||
go StartLocalDiscovery(cm)
|
||||
|
||||
// Foreground: exit on SIGINT/SIGTERM. A Windows service ignores these and
|
||||
// is driven by its own handler.
|
||||
go func() {
|
||||
|
||||
Reference in New Issue
Block a user