Conversation
The responder transmits every announcement and reply from a fresh socket bound to an ephemeral port, so datagrams leave with a high source port. RFC 6762 §6 requires responses to originate from 5353, and a router-level mDNS reflector only classifies a datagram as mDNS when it comes from that port: it relays our queries but drops our answers, so nodes on reflected subnets never discover each other. Bind the per-interface send socket to 5353 on Unix via the same SO_REUSEADDR hook the receive socket uses, which lets it coexist with the group-bound receive socket (and any other local responder) on the shared port. Windows keeps the ephemeral-port path. Add regression coverage for the send socket's local port and for binding it alongside the group receive socket. Fixes NVIDIA#1 Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
woodsonl
force-pushed
the
fix/review-findings
branch
from
September 12, 2026 21:35
2b0b375 to
2f09eaf
Compare
The previous change bound a fresh per-send socket to the interface address on 5353. That socket is more specific than Run's receive socket, so while it was open the kernel delivered unicast datagrams addressed to the host on 5353 to it, and they were discarded when it closed: a unicast mDNS query arriving during a send was dropped. On Unix, send from Run's receive socket instead (issue NVIDIA#1's first suggestion). It is already bound to 5353, so responses still originate from the well-known port, and no second socket competes for unicast delivery. Set the outgoing interface and TTL per datagram through an ipv4.ControlMessage. Windows keeps the fresh ephemeral-socket path, since it refuses to send from the group-bound receive socket. Add a test that a send goes out the installed shared socket. Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
Three defects found reviewing the source-port fix: - Run closed its receive socket from a ctx.Done goroutine before calling sendGoodbye, so on Unix (where sends share that socket) the shutdown goodbye was dropped and peers cached the node for its full record TTL. Drop the closer; the read loop's 500ms deadline plus its ctx check end the loop promptly, and the deferred close now runs after sendGoodbye. - The fallback send socket bound <ip>:5353 on Unix. A unicast-bound 5353 socket is more specific than Run's receive socket and would capture its unicast traffic in the window before Run installs the shared socket (e.g. an UpdateTXT from the RPC loop). Bind ephemeral instead. - ControlMessage.TTL is receive-only in x/net/ipv4, so the multicast TTL was never applied. Set it once with SetMulticastTTL in Run and on the fresh-socket path, via a named mdnsTTL constant. Stale comments updated; tests cover ephemeral fallback and the no-address guard. Signed-off-by: woodsonl <65194841+woodsonl@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.
Summary
Fixes #1. mDNS answers were sent from a fresh socket bound to an ephemeral port, so a router-level mDNS reflector relayed PAIR's
_nvpair-node._tcp.localqueries but dropped every answer — it only classifies a datagram as mDNS when it originates from UDP 5353. Nodes on the same link still discovered each other; nodes on reflected subnets never did.RFC 6762 §6 requires responses to originate from the well-known mDNS port. On Unix the responder now transmits from the same socket it already receives on, which is bound to 5353. No second socket is opened on 5353, so a unicast query addressed to the host is never captured by a short-lived send socket. Windows keeps a fresh per-send socket, since it refuses to send from the group-bound receive socket.
Changes
services/shared/mdns/responder.go:Runinstalls its receive socket as the send socket (setSendPC);sendOnInterfacesends from it with a per-datagramipv4.ControlMessage{IfIndex}, falling back tosendOnFreshConnon Windows and for sends beforeRuninstalls it. Sets the multicast TTL once on each send socket withSetMulticastTTL—ControlMessage.TTLis receive-only inx/net/ipv4— via a namedmdnsTTLconstant.services/shared/mdns/socketreuse_unix.go/socketreuse_windows.go: addsendFromRecvSocket(true on Unix, false on Windows). The fallback send socket always binds an ephemeral port, so it can never capture unicast addressed to the 5353 receive socket.services/shared/mdns/responder_test.go: assert the fallback uses an ephemeral port, coexists with the group socket, errors when the interface has no addresses, and that a send uses the installed shared socket.services/versions.json: bumpnvpair-node-scanner0.20.3 → 0.20.4 (the only consumer of the shared responder).Validation
go test -race -count=1 ./mdns/inservices/shared— passes, including the send-socket tests (ran, not skipped).go test -count=1 ./...andgo build ./...inservices/nvpair-node-scanner— pass.windows/amd64,darwin/arm64, andlinux/arm64.src:5353goes to the most-specific socket, which is why the responder sends from its receive socket instead of a fresh one; the fallback is kept off 5353 so it cannot steal.Notes
No JSON-RPC, configuration, or user-facing surface changes. Cross-subnet discovery needs no manual peers once the reflector is in the path.