swift-mDNS

2.0.0

Pure Swift implementation of mDNS and DNS-SD
1amageek/swift-mDNS

What's New

swift-mDNS 2.0.0

2026-08-14T02:30:38Z

Pure Swift mDNS runtime on swift-networking with POSIX, WASI, and Embedded WASM support. Includes typed lifecycle failures, hardened DNS parsing, bounded test execution, and isolated benchmarks.

swift-mDNS

A pure Swift implementation of Multicast DNS (mDNS, RFC 6762) and DNS Service Discovery (DNS-SD, RFC 6763). Embedded-first: the wire codec is Foundation-free and the byte currency is [UInt8] / MDNSService / NetworkingCore.IPAddress; no Data / ByteBuffer / NIO type appears on the public surface.

Release status. Current release: 2.0.0.

Features

  • Pure Swift — no C dependencies; the DNSWire codec works on all Swift platforms.
  • RFC compliant — RFC 1035 (DNS), RFC 6762 (mDNS), RFC 6763 (DNS-SD), RFC 2782 (SRV).
  • Embedded-first[UInt8] byte currency; the DNSWire codec has no Foundation / NIO / any.
  • WASM-awareDNSWire and the MDNS facade compile for WASI; default multicast I/O is unavailable there because WASI exposes no UDP multicast socket.
  • Modern concurrency — actors and Sendable types; typed-throws discovery stream, typed shutdown, and explicit responder failure events.
  • Hardened parsing — wire decoding strictly bounds-checks hostile input and throws DNSError on malformed data instead of trapping; compression-pointer jumps are capped; the message decoder enforces a size ceiling and caps speculative reservations; unknown opcode / rcode / class / record-type values are preserved (.unknown) rather than silently defaulted.

Requirements

  • Swift 6.4 development snapshot swift-6.4.x-DEVELOPMENT-SNAPSHOT-2026-07-23-a
  • macOS 26+ / iOS 26+ / tvOS 26+ / watchOS 26+ / visionOS 26+ (the shared networking-stack baseline)

Installation

Add swift-mDNS to your Package.swift:

dependencies: [
    .package(url: "https://github.com/1amageek/swift-mDNS.git", from: "2.0.0")
]

Then add the product(s) you need to your target dependencies:

.target(
    name: "YourTarget",
    dependencies: ["MDNS"]          // and/or "DNSWire" for the raw codec
)

Quick Start

Service browsing

import MDNS

let browser = MDNSBrowser()

// Iteration yields MDNSService and throws MDNSError. An updated/removed service
// arrives as a fresh value; deduplicate on service.id.
for try await service in try await browser.browse("_http._tcp.local.") {
    print("Found: \(service.name) at \(service.host ?? "unknown"):\(service.port ?? 0)")
}

Service advertising

import MDNS

let responder = MDNSResponder()

let service = MDNSService(
    name: "My Web Server",
    type: "_http._tcp",
    port: 8080,
    txt: ["path": Array("/api".utf8), "version": Array("1.0".utf8)]
)

try await responder.advertise(service)

// Later, to withdraw (sends a goodbye, TTL == 0):
try await responder.withdraw(service)
try await responder.stop()

MDNSResponder.failures publishes terminal background failures that occur after advertise(_:) returns, including receive, response construction, timer, and socket failures. stop() is typed-throwing so goodbye or socket shutdown failures are not reported as success. Browser and responder tasks own their active lifecycle on every target; call stop() to terminate that lifecycle.

Low-level DNS message handling (Tier-3)

import DNSWire

let query = try DNSMessage.mdnsQuery(for: "_http._tcp.local.")
let encoded: [UInt8] = query.encode()

let message = try DNSMessage.decode(from: receivedBytes)
for answer in message.answers {
    switch answer.rdata {
    case .ptr(let serviceName): print("PTR -> \(serviceName)")
    case .srv(let srv):         print("SRV -> \(srv.target):\(srv.port)")
    case .txt(let strings):     print("TXT -> \(strings)")
    case .a(let addr):          print("A -> \(addr)")
    case .aaaa(let addr):       print("AAAA -> \(addr)")
    default:                    break
    }
}

Products

This package ships two products following the Embedded-first 3-tier API design.

Product Tier Import Use it for
MDNS Tier-1 facade import MDNS Browse / advertise services. [UInt8] / MDNSService / IPAddress currency.
DNSWire Tier-3 codec import DNSWire The Foundation-free DNS/mDNS wire codec. Not re-exported by import MDNS.

Architecture

Three layers, top to bottom:

┌─────────────────────────────────────────────────────────────┐
│  Tier-1 facade  (import MDNS)                                │
│  MDNSBrowser (actor), MDNSResponder (actor)                  │
│  MDNSService (value), MDNSError, MDNSDiscoveries            │
│  Currency: [UInt8] / MDNSService / NetworkingCore.IPAddress│
├─────────────────────────────────────────────────────────────┤
│  MDNSTransport (package protocol)                           │
│  - mDNS-specific abstraction over UDP                       │
│  - converts DNSMessage to the shared OwnedBytes owner       │
│  - POSIXMDNSTransport joins multicast groups                │
│  - Native and Embedded share NetworkingPOSIX contracts     │
│  - WASI: UnavailableMDNSTransport fails loudly              │
├─────────────────────────────────────────────────────────────┤
│  Tier-3 codec  (import DNSWire)                             │
│  DNSMessage, DNSName, DNSResourceRecord, DNSRecordData,     │
│  IPv4Address, IPv6Address, DNSError, WriteBuffer            │
│  - Embedded-clean: no Foundation, no NIO, no `any`          │
└─────────────────────────────────────────────────────────────┘
  • MDNSService is a Foundation-free DNS-SD service instance: addresses are NetworkingCore.IPAddress; txt values are raw [UInt8] (no String-valued TXT API). id is the full service name, so consumers deduplicate discoveries by id.
  • MDNSDiscoveries is the named typed sequence the browser vends: AsyncSequence<MDNSService, MDNSError>. There is no .found / .updated / .removed event enum — an updated or removed service is delivered as a fresh value, and a goodbye (TTL == 0) re-emits the last-known state.
  • MDNSBrowser sends PTR queries and, when autoResolve is on, issues SRV/TXT follow-ups to resolve found instances. Calling browse(_:) more than once adds another service type to the same discovery stream.
  • MDNSResponder answers queries for registered services and announces with backoff; withdraw(_:) / stop() send goodbye messages (TTL == 0).
  • MDNSTransport is a package protocol over the shared NetworkingDatagram contract. POSIXMDNSTransport is the Native/Embedded production implementation and delegates UDP ownership, multicast membership, cancellation, and shutdown to NetworkingPOSIX. WASI uses UnavailableMDNSTransport so the facade compiles without NIO or host socket APIs; calling browse / advertise fails with MDNSError.transportUnavailable.

DNSWire remains a protocol-owned codec and does not import the networking foundation. The MDNS facade imports NetworkingCore, NetworkingTime, NetworkingDatagram, and the available platform adapter. See Sources/MDNS/CONTEXT.md for the load-bearing invariants.

Security

The DNSWire decoder rejects hostile input rather than trapping or silently substituting defaults:

  • strict bounds checks on all RDATA (including NSEC) and DNS names;
  • decode-time RFC 1035 name-length enforcement (255-byte cap, applied incrementally);
  • compression-pointer loop / forward-reference detection (jumps capped at 128, every pointer must point strictly backward and within bounds);
  • a hard DNSMessage size ceiling enforced before any attacker-controlled section count is read, plus capped speculative reservations (min(count, remainingBytes / minEntrySize)) against forged 0xFFFF section counts;
  • strict UTF-8 in TXT/HINFO labels (malformed input throws DNSError);
  • preservation of unrecognized opcode/rcode/class/record-type values as .unknown(...).

Inbound multicast datagrams that fail to decode are dropped per RFC 6762 (the receive loop is never torn down) but are counted (droppedDecodeFailureCount) and surfaced via a throttled log, so persistent malformed traffic stays detectable.

RFC Compliance

RFC Title Coverage
RFC 1035 Domain Names Message format, name encoding/decoding, compression
RFC 6762 Multicast DNS Multicast addressing/port, cache-flush bit, QU bit, goodbye (TTL 0)
RFC 6763 DNS-Based Service Discovery PTR/SRV/TXT service-discovery flow
RFC 2782 DNS SRV Records SRV target/port/priority/weight

Performance

The DNSWire codec is optimized for throughput with minimal allocations: index-based parsing over raw byte arrays, inline (stack-allocated) IPv4/IPv6 storage, DNS name compression, a ContiguousArray-backed write buffer, and ~Copyable buffers. The POSIX receive backend writes directly into the final OwnedBytes datagram owner.

Measured on 2026-08-14 using an Apple M4 Max MacBook Pro (36 GB), the pinned Swift 6.4 snapshot, and a Release build. The published value is the median of three process runs; each process reports the median of five measured samples.

Operation Throughput Latency
DNSName decoding 2.25M ops/sec 445 ns
DNSName encoding 1.38M ops/sec 726 ns
DNSMessage query decoding 2.04M ops/sec 489 ns
DNSMessage query encoding 1.28M ops/sec 783 ns
DNSMessage response decoding 293K ops/sec 3.41 μs
End-to-end query roundtrip 822K ops/sec 1.22 μs

Run the benchmarks:

TOOLCHAINS=org.swift.64202607231a \
SWIFT_MDNS_BENCHMARKS=1 \
swift build --configuration release --product mDNSBenchmarks

TOOLCHAINS=org.swift.64202607231a \
SWIFT_MDNS_BENCHMARKS=1 \
swift run --skip-build --configuration release mDNSBenchmarks

The benchmark is an independent executable target. It is absent from the normal test graph and is compiled only when SWIFT_MDNS_BENCHMARKS=1 is set.

Testing

The mDNSTests target covers both the Tier-3 codec (DNSWire) and the Tier-1 facade (MDNS). Run with a timeout to guard against hangs:

scripts/swift-test-timeout.sh 120 \
  env TOOLCHAINS=org.swift.64202607231a \
  xcodebuild test \
    -scheme swift-mDNS-Package \
    -destination 'platform=macOS'

Compile the WASM regression gate with:

./scripts/verify-wasm.sh

References

  • RFC 1035 — Domain Names — Implementation and Specification
  • RFC 6762 — Multicast DNS
  • RFC 6763 — DNS-Based Service Discovery
  • RFC 2782 — DNS SRV Records

License

MIT License

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

Last updated: Sun Aug 23 2026 03:26:17 GMT-0900 (Hawaii-Aleutian Daylight Time)