Skip to content

Add MDnsDeviceFinder for reliable discovery on home/multi-AP networks (broadcast UDP is fragile) #183

@tylerkron

Description

@tylerkron

Why

The existing WiFiDeviceFinder (post-#180) does the host side of UDP broadcast discovery correctly: it binds per-NIC, filters virtual adapters, and routes replies to the originating socket. But the broadcast transport itself is unreliable on a typical home network with multiple APs / Band Steering / lossy 2.4 GHz radios — and we cannot fix that from the library side, no matter how cleanly we bind sockets.

Concrete failure session against a Nyquist1 on a default UniFi setup (Dream-Machine + wired-PoE U6 LR, single SSID, Band Steering on, Mac on 5 GHz at one AP, device on 2.4 GHz at the other). Healthy controller-side state, device fully online, signal -57 dBm, getting DHCP. From the Mac, ARP for the device was (incomplete) — broadcast frames across the AP boundary were dropping at the rate the U6 LR's 2.4 GHz radio reported (17% Tx Retry / 10% Tx Drop). UDP discovery silently returns 0 devices in this scenario. So does ICMP. So does any L2-resolution-dependent traffic.

The structural answer industry-wide is mDNS: it's the multicast group every prosumer router has tuned reflection / forwarding for (UniFi mDNS reflector, Eero cross-SSID forwarding, etc.). It also provides hostname resolution and structured TXT-record metadata as side benefits.

Companion firmware ticket: daqifi-nyquist-firmware#404. The firmware change adds an mDNS responder advertising _daqifi._tcp.local.. This ticket is the client-side counterpart.

Proposed shape

Add Daqifi.Core.Device.Discovery.MDnsDeviceFinder : IDeviceFinder, IDisposable alongside the existing WiFiDeviceFinder. Same IDeviceInfo output type, same event surface. The two are then composable (most consumers will want both, merged).

namespace Daqifi.Core.Device.Discovery;

public class MDnsDeviceFinder : IDeviceFinder, IDisposable
{
    public MDnsDeviceFinder(string serviceType = "_daqifi._tcp"); // local. is implicit
    public event EventHandler<DeviceDiscoveredEventArgs>? DeviceDiscovered;
    public event EventHandler? DiscoveryCompleted;
    public Task<IEnumerable<IDeviceInfo>> DiscoverAsync(TimeSpan timeout);
    public Task<IEnumerable<IDeviceInfo>> DiscoverAsync(CancellationToken cancellationToken = default);
}

MDnsDeviceFinder parses the service instance + SRV (host + port) + A (IP) + TXT (pn, sn, fw, hw) records into the existing IDeviceInfo shape. LocalInterfaceAddress is populated from the receiving socket's bound IP, same trick as WiFiDeviceFinder.

Aggregator (recommended)

Most callers should not care which transport found the device. Add a thin façade that runs both finders in parallel and returns the union, deduplicated by MAC (or serial, when MAC is unavailable):

public class DeviceFinder : IDeviceFinder, IDisposable
{
    public DeviceFinder(); // wires up WiFiDeviceFinder + MDnsDeviceFinder by default
    // ...
}

Both finders run concurrently. mDNS will typically return faster on networks where it's available; UDP broadcast remains the path for older firmware that doesn't yet advertise mDNS. If both surface the same device, dedupe by MAC.

This makes the rollout strictly additive: existing devices on older firmware continue to be found via UDP broadcast; new firmware (#404) gets the better path automatically; the library needs no firmware-version awareness.

Library implementation choices

In rough order of preference:

  1. Take a small dependency on a managed mDNS library. Candidates:

    • Makaretu.Dns.Multicast — actively maintained, MIT, well-tested, no native deps. Most aligned with Daqifi.Core's "pure managed, multi-platform" stance.
    • Tmds.MDns — older but stable.
    • Zeroconf — popular but heavier surface area.
      The existing project already takes managed deps (Google.Protobuf, etc.); one more well-scoped one is reasonable.
  2. Roll a minimal in-tree mDNS-SD client. mDNS-SD client (not responder) is a few hundred lines of straightforward DNS message parsing. Avoids the dependency. Worth doing if the team wants zero new deps; not worth it if not.

  3. P/Invoke into platform Bonjour / Avahi / Windows DNS-SD APIs. Cross-platform API differs per OS; gives up the managed-everywhere story. Recommend against.

I'd start with (1) and a clear adapter boundary, so swapping to (2) later is contained.

Code locations / additions

  • src/Daqifi.Core/Device/Discovery/MDnsDeviceFinder.cs — new
  • src/Daqifi.Core/Device/Discovery/DeviceFinder.cs — new aggregator (or fold into existing if there's already a façade)
  • src/Daqifi.Core.Tests/Device/Discovery/MDnsDeviceFinderTests.cs — unit tests with a mocked mDNS query/response transcript
  • README + docs/ — document the discovery story and the older-firmware fallback

Acceptance criteria

  • MDnsDeviceFinder finds a device advertising _daqifi._tcp.local. and populates IDeviceInfo with name (instance), IP, port, MAC (parsed from TXT or computed from advertised hostname), serial / fw / hw / part number from TXT
  • LocalInterfaceAddress is populated from the actual receiving NIC (same correctness PR fix: bind UDP discovery per-NIC and skip virtual adapters (#179) #180 established for the broadcast path)
  • Aggregator runs both finders concurrently, dedupes by MAC, surfaces results as they arrive (events) and as the merged final set
  • Discovery succeeds on the multi-AP UniFi setup that broke WiFiDeviceFinder (the network this issue is about), once paired with firmware-side #404
  • Existing WiFiDeviceFinder-only consumers continue to work unchanged (additive change, no breaking API)
  • Cancellation / timeout / disposal semantics match WiFiDeviceFinder exactly
  • Unit tests cover: TXT parsing, instance name → device-info mapping, conflict-resolved instance names (-2 suffix), goodbye packets (TTL=0 means "device gone"), partial reply (SRV without A — re-query), multi-NIC

Risks

  • mDNS is multicast — some networks (corporate, captive portals, hardened guest WiFi) filter all multicast. Same problem affects existing UDP broadcast and arguably more so; mDNS is more likely to be allowed because of how widespread its consumer use is. Document the --ip direct-connect fallback for these cases.
  • mDNS responders / clients require IPv4 multicast support. Expected on every target platform but worth verifying on .NET on Linux containers (some Docker default networks don't pass mcast).
  • _daqifi._tcp is not registered with IANA. Should we register it under _<your-name>._<protocol>.local. per RFC 6763 conventions? Not blocking; revisit before broad release.

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions