feat(net): resolve device .local names over mDNS - #728
Draft
HuggeK wants to merge 1 commit into
Draft
Conversation
Go never resolves ".local" itself. net/conf.go routes those names to libc only when cgo is available, and every FTW build sets CGO_ENABLED=0, so the pure Go resolver is always selected: a configured "zap.local" became a unicast DNS query to the site router and failed. That holds on every base image and every libc, musl and glibc alike, so the container's distro was never the variable here. Add internal/mdnsresolve, which answers those names over multicast DNS, and route every driver transport through it: Modbus TCP, MQTT (driver and Home Assistant bridge), HTTP including the TLS-pinned client, WebSocket and raw TCP. Only ".local" names take the new path; literal IPs and ordinary DNS names dial exactly as before. Resolution runs per dial rather than once at startup, so a device that moves to a new DHCP lease is found again on the next reconnect with no config edit. Answers are cached for the record TTL clamped to 30-120s so reconnect loops cannot flood the LAN, and failures are cached for 5s so a still-booting device is retried soon. Failures log "mDNS resolution failed" and name the mechanism rather than surfacing as a generic dial error. The scanner's reverse PTR lookup moves into the same package so there is one mDNS implementation instead of two. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
A device configured as
zap.localnever connects. The cause is not thecontainer's distro — it is that Go never resolves
.localitself.net/conf.go:395-409routes a.locallookup to libc only insideif canUseCgo { … }, and every FTW build setsCGO_ENABLED=0, somustUseGoResolvershort-circuits and that branch is dead code. The name fallsthrough to
hostLookupFilesDNS—/etc/hosts, then a unicast DNS query towhatever
resolv.confpoints at, which on a normal site is the router. Routersdon't answer
.local.This holds on every base image and every libc, musl and glibc alike. It also
holds on our own Pi image, which runs NetworkManager +
avahi-daemonwith nosystemd-resolved: Avahi serves glibc host programs through NSS, it does notanswer DNS queries.
This PR makes FTW resolve those names itself.
Why not switch the container to a glibc base and install
nss-mdnsIt cannot work without giving up much more than it buys. glibc's NSS loads
modules with
dlopen, so it needs a dynamically linked binary — that meansCGO_ENABLED=1, losing the static binary, breaking the$BUILDPLATFORMcross-compile in
Dockerfile:30-37andmake verify-all. And Debian'slibnss-mdnshas no standalone resolver; it talks toavahi-daemonover/run/avahi-daemon/socket, which is not in the container, does not exist on ageneric Docker host, and cannot exist under Docker Desktop.
Resolving in Go costs none of that and works on every host and every base.
Changes
go/internal/mdnsresolve/(new) — forward.localresolution overmulticast DNS, plus a
Dialerthat uses it. Reuses the socket pattern thescanner already proved: an unconnected
ListenUDPwith the RFC 6762 QU bit,so we never bind 5353 (Avahi owns it on the host).
a short-TTL device turning every Modbus reconnect into a multicast storm,
the ceiling stops a DHCP move taking effect arbitrarily late;
mDNS resolution failedand names themechanism, instead of surfacing as a generic dial error.
modbus/tcp_client.go,mqtt/client.go,ha/bridge.go,drivers/lua.go(both the plain HTTP client and theTLS-pinned clone),
drivers/ws_cap.go,drivers/tcp_cap.go.scanner/mdns.gomoves into the new package so there is one mDNSimplementation rather than two. Pure move; its test moves with it.
config.example.yaml,docs/sourceful-zap.md.Only
.localnames take the new path — a literal IP or an ordinary DNS namenever triggers a query, and there is a test asserting exactly that.
Resolution runs per dial, not once at startup. That is what makes binding by
name actually survive a DHCP change:
modbus/client.go:107-109rebuilds from theoriginal address on reconnect, so the device is found at its new IP with no
config edit.
Testing
go/internal/mdnsresolvesuite: packet parse/round-trip, TTL clamping atboth bounds, a loopback fake responder exercising the real send/receive
path, positive and negative cache behaviour, cache expiry, and two bypass
tests proving a plain IP never issues a query and that a resolution failure is
reported as one.
go build ./...,go vet ./...clean.scripts/test-container-boundaries.shandscripts/test-modular-compose.shboth pass — untouched, which is the point: this needs no container change.
CGO_ENABLED=0for linux/arm64, linux/amd64,windows/amd64, so the static binary is intact.
go test ./...: identical 21 pre-existing failures before and after(all Windows-only — unix sockets,
C:\SQLite URIs). Zero regressions.Limitation worth knowing
mDNS needs multicast reachability. The Linux Compose topology has it via
network_mode: host. Underdocker-compose.macos.ymlthe container is bridged,so configure devices by IP there — same constraint the existing discovery scan
already has, now written down in
docs/sourceful-zap.md.Relationship to #714
This is the prerequisite for #714. That PR makes the wizard bind every
discovered device to its
.localname; without forward resolution, it ships theexact silent failure its review warned about. Note this is already a live bug —
drivers/zap.lua:52,59,66default tozap.localtoday.web/settings/tabs/devices.jsis deliberately not touched here: #714rewrites that host-hint line, and editing it would only create a conflict.
🤖 Generated with Claude Code