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.
- Pure Swift — no C dependencies; the
DNSWirecodec 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; theDNSWirecodec has no Foundation / NIO /any. - WASM-aware —
DNSWireand theMDNSfacade compile for WASI; default multicast I/O is unavailable there because WASI exposes no UDP multicast socket. - Modern concurrency — actors and
Sendabletypes; typed-throws discovery stream, typed shutdown, and explicit responder failure events. - Hardened parsing — wire decoding strictly bounds-checks hostile input and throws
DNSErroron 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.
- 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)
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
)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)")
}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.
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
}
}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. |
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` │
└─────────────────────────────────────────────────────────────┘
MDNSServiceis a Foundation-free DNS-SD service instance:addressesareNetworkingCore.IPAddress;txtvalues are raw[UInt8](no String-valued TXT API).idis the full service name, so consumers deduplicate discoveries byid.MDNSDiscoveriesis the named typed sequence the browser vends:AsyncSequence<MDNSService, MDNSError>. There is no.found/.updated/.removedevent enum — an updated or removed service is delivered as a fresh value, and a goodbye (TTL == 0) re-emits the last-known state.MDNSBrowsersends PTR queries and, whenautoResolveis on, issues SRV/TXT follow-ups to resolve found instances. Callingbrowse(_:)more than once adds another service type to the same discovery stream.MDNSResponderanswers queries for registered services and announces with backoff;withdraw(_:)/stop()send goodbye messages (TTL == 0).MDNSTransportis apackageprotocol over the sharedNetworkingDatagramcontract.POSIXMDNSTransportis the Native/Embedded production implementation and delegates UDP ownership, multicast membership, cancellation, and shutdown toNetworkingPOSIX. WASI usesUnavailableMDNSTransportso the facade compiles without NIO or host socket APIs; callingbrowse/advertisefails withMDNSError.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.
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
DNSMessagesize 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 | 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 |
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 mDNSBenchmarksThe 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.
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- RFC 1035 — Domain Names — Implementation and Specification
- RFC 6762 — Multicast DNS
- RFC 6763 — DNS-Based Service Discovery
- RFC 2782 — DNS SRV Records
MIT License