Skip to content

fix: send mDNS responses from source port 5353 - #70

Open
woodsonl wants to merge 3 commits into
NVIDIA:mainfrom
woodsonl:fix/review-findings
Open

woodsonl wants to merge 3 commits into
NVIDIA:mainfrom
woodsonl:fix/review-findings

Conversation

@woodsonl

@woodsonl woodsonl commented Sep 12, 2026

Copy link
Copy Markdown

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.local queries 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: Run installs its receive socket as the send socket (setSendPC); sendOnInterface sends from it with a per-datagram ipv4.ControlMessage{IfIndex}, falling back to sendOnFreshConn on Windows and for sends before Run installs it. Sets the multicast TTL once on each send socket with SetMulticastTTLControlMessage.TTL is receive-only in x/net/ipv4 — via a named mdnsTTL constant.
  • services/shared/mdns/socketreuse_unix.go / socketreuse_windows.go: add sendFromRecvSocket (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: bump nvpair-node-scanner 0.20.3 → 0.20.4 (the only consumer of the shared responder).

Validation

  • go test -race -count=1 ./mdns/ in services/shared — passes, including the send-socket tests (ran, not skipped).
  • go test -count=1 ./... and go build ./... in services/nvpair-node-scanner — pass.
  • Cross-compiles clean for windows/amd64, darwin/arm64, and linux/arm64.
  • Live bind probe: a unicast datagram to src:5353 goes 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.

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 woodsonl reopened this Sep 12, 2026
@woodsonl woodsonl changed the title fix: resolve 14 review issues across the service control plane fix: send mDNS responses from source port 5353 Sep 12, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: mDNS answers are sent from an ephemeral source port, so reflectors never relay node records across subnets

1 participant