Skip to content

Resolve multiple mDNS issues and macOS network stack degredation - #72

Open
stevbev wants to merge 1 commit into
NVIDIA:mainfrom
stevbev:native-mdns
Open

stevbev wants to merge 1 commit into
NVIDIA:mainfrom
stevbev:native-mdns

Conversation

@stevbev

@stevbev stevbev commented Sep 13, 2026

Copy link
Copy Markdown

Description

Closes #59 #19 #1

Resolves macOS network stack degradation caused by two primary issues:

  • opening new sockets for UDP:5353 for every broadcast and never closing the socket, leading to excessive socket binds that severely degrade the network stack over time
  • a race condition caused by other macOS apps that bind mDNS port UDP:5353 shared, but Nvidia PAIR repeatedly binds UDP:5353 exclusive

Scope

The change scope focuses on the mDNS implementation:

  • replacing grandcat/zeroconf with a local mDNS solution that does not continuously open new sockets for broadcasts.
  • on macOS binds UDP:5353 with SO_REUSEPORT to enable multiple apps to use the port, such as macOS system mDNSResponder and user programs like Chrome.
  • limits macOS network interfaces used to broadcast to ones with a valid IPv4 address, and explicitly excludes the Airdrop (awdl) and low latency (llw) interfaces because those are invalid targets for PAIR.
  • Feature gates the Windows broadcast workaround to only run on Windows devices, so that the double broadcast on unix devices is resolved.

Validation

The code update was tested on a 4-node Nvidia PAIR cluster (3 macOS, 1 Windows 11), with the affected macOS node experiencing severe network issues on PAIR v0.1.1 having no issues on this PR after 2 days of operation.

Risk

  • The features of grandcat/zeroconf used in PAIR v0.1.1 were minimal, so there aren't any expected negative impacts from replacing grandcat/zeroconf with a local minimal mDNS solution.
  • There might be some edge use cases where macOS users are trying to use the Airdrop or low-latency-direct-connection interfaces with PAIR. Since these WiFi access modes require exclusive access to the network that prevents normal traffic to the network, the risk should be minimal. Removing the network interface filtering will eliminate this risk.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • New or existing tests cover the change.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded the validation commands and results above.
  • I bumped any affected component in services/versions.json, and described user-visible changes above so they reach the release notes.

Signed-off-by: Steven Beverly <steven@arlynaccess.ai>

@mkalkere mkalkere left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for the socket churn -- pooled per-interface send socket plus one long-lived receiver is the right shape.

Blocking concern: the new receiver binds 224.0.0.251:5353 without SO_REUSEADDR/SO_REUSEPORT, while the responder in the same process and the system mDNSResponder already hold that port with reuse options. On macOS the receiver bind will likely fail with EADDRINUSE and every scan returns empty -- discovery goes fully dark on the platform this is fixing. Same on Linux where node-scanner and both proxies each run a browser: first bind wins, the rest go dark. NewReceiver needs the same reuse-address control hook as the responder (with SO_REUSEPORT on darwin) before merge.

Non-blocking: net.Interfaces() polling cadence is unchanged, the receive path is IPv4-only, and per-datagram assembly drops cross-packet aggregation. Minor: despite the branch name this is a hand-rolled client rather than native mDNS, but that's cosmetic.

@stevbev

stevbev commented Sep 15, 2026

Copy link
Copy Markdown
Author

Good fix for the socket churn -- pooled per-interface send socket plus one long-lived receiver is the right shape.

Blocking concern: the new receiver binds 224.0.0.251:5353 without SO_REUSEADDR/SO_REUSEPORT, while the responder in the same process and the system mDNSResponder already hold that port with reuse options. On macOS the receiver bind will likely fail with EADDRINUSE and every scan returns empty -- discovery goes fully dark on the platform this is fixing. Same on Linux where node-scanner and both proxies each run a browser: first bind wins, the rest go dark. NewReceiver needs the same reuse-address control hook as the responder (with SO_REUSEPORT on darwin) before merge.

Non-blocking: net.Interfaces() polling cadence is unchanged, the receive path is IPv4-only, and per-datagram assembly drops cross-packet aggregation. Minor: despite the branch name this is a hand-rolled client rather than native mDNS, but that's cosmetic.

Thank you for the feedback on the PR. I am a little puzzled though because I double checked and the new receiver binds to port 5353 using SO_REUSEADDR and SO_REUSEPORT on macOS, here in the new socketreuse_darwin.go. Can you help identify which line of code you think is binding the port without SO_REUSEPORT?

Here is a screenshot showing that port 5353 is bound shared with Chrome and PAIR using it.
Screenshot 2026-09-14 at 9 16 49 PM

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]: WiFi saturation on wireless host node / WLAN driver deadlock

2 participants