mdns-sd
A safe, dependency-light Rust library for mDNS-based service discovery, supporting both browsing and publishing services.
Repository Health
Technical Analysis
mdns-sd is a small implementation of Multicast DNS (mDNS) and DNS-Based Service Discovery (DNS-SD) written in safe Rust (#![forbid(unsafe_code)]). It lets an application act as a querier that browses the local network for services of a given type, and/or as a responder that publishes its own service so other hosts can find it, all without depending on any particular async runtime.
Internally the daemon runs its own event loop on a dedicated thread and exposes its API to callers through flume channels, which support both blocking recv() and recv_async() calls. That design lets the same daemon be driven from plain synchronous code, a Tokio task, or any other async executor with no adapter layer. The crate implements a meaningful slice of RFC 6762 (mDNS) and RFC 6763 (DNS-SD) — probing, conflict resolution with automatic name changes, known-answer suppression, cache-flush announcements, and goodbye packets — and runs on macOS, Linux, and Windows over both IPv4 and IPv6.
What You Get
- A
ServiceDaemonthat runs mDNS on its own background thread and is cheaply cloneable across threads browse()andbrowse_cache()to discover instances of a service type and receiveServiceEvents (found/resolved/removed) over a channelregister()/unregister()to publish and retract aServiceInfo(service type, instance name, hostname, addresses, port, TXT properties)resolve_hostname()to resolve a plain hostname to IP addresses via mDNS, independent of any service- Automatic RFC 6762 conflict resolution with
DaemonEvent::NameChangenotifications when a name collision forces a rename - Runtime controls for interface selection (
enable_interface/disable_interface/IfKind), multicast loopback, IPv6, and Apple P2P interfaces - Optional
serdefeature for serializingServiceInfo/TXT properties, and an optionalloggingfeature (on by default) built on thelogcrate
Common Use Cases
- Discovering printers, cameras, smart-home devices, or other IoT peripherals that advertise themselves via mDNS/Bonjour
- Publishing a local network service (e.g. a media server, dev tool, or peer-to-peer node) so other devices on the LAN can find it without manual configuration
- Building cross-platform zero-configuration networking features that need to interoperate with Apple’s Bonjour or Linux’s Avahi
- Resolving
.localhostnames on networks without a conventional DNS server
Under The Hood
Architecture — The crate is organized around a single ServiceDaemon (src/service_daemon.rs, ~6,200 lines) that owns a background thread running an mio-based poll loop (Zeroconf) over raw UDP sockets bound to the mDNS multicast group. The public struct is just a thin, cloneable handle holding a flume::Sender<Command>; every public method (browse, register, unregister, monitor, shutdown, set_ip_check_interval, etc.) constructs a Command and does a non-blocking try_send() into that channel, so callers never block on the daemon and the same handle works from sync or async code. Inbound results flow back out through per-call flume::Receivers (e.g. Receiver<ServiceEvent> from browse, Receiver<UnregisterStatus> from unregister), which support both recv() and recv_async(). DNS wire parsing/building lives in src/dns_parser.rs (~3,000 lines) and the resource-record cache with TTL/known-answer-suppression logic lives in src/dns_cache.rs (~1,000 lines), keeping protocol mechanics separate from the daemon’s command loop.
Tech Stack — Pure safe Rust (#![forbid(unsafe_code)]), edition 2018, MSRV 1.71. Core dependencies are deliberately minimal: flume for channels (with an optional async feature), mio for cross-platform socket polling, socket2/socket-pktinfo for low-level UDP/pktinfo socket options, if-addrs for enumerating local interfaces, and fastrand for jitter/backoff timing. log is pulled in only behind the default logging feature, and serde is fully optional. There is no dependency on Tokio, async-std, or any other runtime — async support is purely a channel-compatibility feature (flume/async), not a runtime integration.
Code Quality — The crate ships a large, purpose-built test suite: tests/mdns_test.rs (~2,600 lines, ~80 test functions covering browse/register/resolve/conflict flows end-to-end over real sockets), plus tests/shutdown_test.rs and tests/addr_parse.rs. Errors are modeled as a small #[non_exhaustive] Error enum (Again, DaemonShutdown, Msg, ParseIpAddr) with Display/std::error::Error impls rather than panics or stringly-typed failures, and public methods consistently return Result<T>. Logging is gated behind a feature flag with a zero-cost no-op macro fallback when disabled, so the library imposes no runtime cost on consumers who don’t want logging.
API Design — The public surface centers on two types, ServiceDaemon and ServiceInfo, with builder-style construction (ServiceInfo::new(...).enable_addr_auto()) and a small, consistent verb set (browse, register, unregister, monitor, shutdown, status). Every mutating call funnels through the same request/response-channel pattern, so once a caller learns one method they know the shape of all of them. Extensive doc comments on lib.rs include runnable end-to-end examples for both the querier and responder roles, and the examples/ directory ships register.rs and query.rs as copy-pasteable CLI programs with their own --help-style usage text.