Skip to content

feat(dns): add explicit mdns resolver - #3028

Open
hengistchan wants to merge 2 commits into
MetaCubeX:Alphafrom
hengistchan:feat/mdns-resolver
Open

feat(dns): add explicit mdns resolver#3028
hengistchan wants to merge 2 commits into
MetaCubeX:Alphafrom
hengistchan:feat/mdns-resolver

Conversation

@hengistchan

@hengistchan hengistchan commented Jul 25, 2026

Copy link
Copy Markdown

Closes #3027

Summary

This adds an explicit mdns:// DNS nameserver without changing system:// or
automatically routing .local names.

dns:
  fake-ip-filter:
    - +.local
  nameserver-policy:
    '+.local':
      - mdns://

Root cause

Mihomo's current system resolver enumerates configured unicast DNS servers and
queries them through Mihomo's ordinary DNS client. On macOS this bypasses
mDNSResponder, so a hostname may resolve through dscacheutil/dns-sd but
return NXDOMAIN through Mihomo.

Design

  • registers mdns:// in nameserver parsing and client construction
  • rejects addresses, URL parameters, and proxy fragments for the link-local
    transport
  • leaves system:// unchanged and does not add implicit .local routing
  • adds no CGO or third-party dependency
  • uses a pure-Go multicast client on supported platforms and the CGO-free
    mDNSResponder Unix-domain IPC on macOS for A/AAAA

The portable transport:

  • queries IPv4 and IPv6 on every up, multicast-capable,
    non-point-to-point interface
  • sends with TTL/hop limit 255
  • receives IPv4/IPv6 packet control metadata and retains source address,
    destination multicast address, ingress interface index, and TTL/hop limit
  • requires response source port 5353 and the expected multicast destination
    and ingress interface, as required/recommended by
    RFC 6762
  • records but does not reject a received TTL/hop limit other than 255: RFC 6762
    says senders SHOULD use 255, while a packet received at the link-local
    multicast destination is already known to be local
  • merges Answer, Authority, and Additional records across multiple packets
  • builds CNAME chains only from records received on the same interface, so a
    CNAME on one link cannot attach to a target record from another link
  • returns the union of distinct positive addresses from all interfaces;
    identical records are collapsed with the greatest observed TTL
  • strips the mDNS cache-flush bit before returning ordinary DNS records
  • bounds retained responses and releases sockets/goroutines on completion,
    timeout, reset, cancellation, or close

This also fixes CNAME targets that arrive in an earlier packet than the CNAME:
filtering is deferred until the per-interface record set has been assembled.

Negative and fallback semantics

  • multicast silence or an empty response is not proof of non-existence and
    returns a timeout error; another successful nameserver in the same policy
    group may therefore win
  • an applicable RFC 6762 NSEC record returns NOERROR/NODATA with its TTL
  • macOS mDNSResponder NoSuchRecord returns NODATA and NoSuchName returns
    NXDOMAIN
  • if mDNSResponder is unavailable or its IPC stream is incompatible/malformed,
    Mihomo logs a clear diagnostic and falls back to portable multicast within
    the original query deadline
  • cancellation, close, timeout, and explicit daemon query errors remain final;
    if both IPC and multicast fail, the returned error contains both causes

Tests

Coverage includes:

  • nameserver parsing and resolver registration
  • A and AAAA over controlled local IPv4/IPv6 responders
  • multiple packets, multiple answers, duplicate TTL selection, and cache-flush
    handling
  • CNAME-first, target-first, and three-packet CNAME chains
  • per-interface CNAME isolation and cross-interface positive union semantics
  • source-port, destination, interface, and hop-limit metadata policy
  • NSEC NODATA, empty-response timeout, ordinary timeout, and context
    cancellation
  • explicit non-.local queries
  • socket/goroutine release after close
  • macOS IPC A/AAAA, interface index retention, explicit negative results,
    incompatible-protocol fallback, and joined fallback diagnostics

Passed locally:

CGO_ENABLED=1 go test -race ./dns ./config -count=1
CGO_ENABLED=0 go test ./dns ./config -count=1
GOTOOLCHAIN=go1.20.14 CGO_ENABLED=0 go test ./dns ./config -count=1

# Equivalent to the repository's macOS CI, which removes listener/inbound tests:
go list ./... | rg -v '/listener/inbound$' | xargs env CGO_ENABLED=0 SKIP_INTEROP_TEST=1 go test -count=1
go list -tags with_gvisor ./... | rg -v '/listener/inbound$' | xargs env CGO_ENABLED=0 SKIP_INTEROP_TEST=1 go test -tags with_gvisor -count=1

go vet ./dns ./config
golangci-lint run --new-from-rev origin/Alpha ./...
git diff --check

Running go test ./... without the repository's macOS exclusion reproduces an
unrelated TestInboundSudoku_HTTPMaskMode/Concurrent connection-reset failure
on an untouched origin/Alpha worktree. The CI-equivalent standard and
with_gvisor suites pass.

The full non-incremental lint command still reports existing findings in
dns/dhcp.go, dns/dot.go, dns/service.go, and dns/doq.go. Incremental
lint for this PR is clean.

Build compatibility

CGO-free with_gvisor builds passed for:

darwin/arm64
darwin/amd64
linux/amd64
linux/arm64
windows/amd64
freebsd/amd64

Real verification

An isolated Mihomo instance on 127.0.0.1:10554 returned:

open-webui.orb.local.  300  IN  A     192.168.138.3
open-webui.orb.local.  300  IN  AAAA  fd07:b51a:cc66:0:a617:db5e:c0a8:8a03

Both replies were NOERROR, matched the macOS host resolver, and the debug log
showed from mdns://. The existing Clash, OrbStack, routing, firewall, system
DNS, and TUN configuration were not changed.

Why system:// is unchanged

system:// is an established unicast-DNS abstraction. Adding mDNS implicitly
would change resolution order and could break private unicast-DNS deployments
that intentionally use .local. An explicit scheme keeps the behavior opt-in
and policy-controlled.

Known limitations

  • The macOS path intentionally implements only the A/AAAA portion required by
    Mihomo's IP resolver.
  • The mDNSResponder IPC is versioned and lower-level than the public
    dns_sd.h API. Incompatible protocol/transport errors now fall back with a
    clear diagnostic, but a future incompatible protocol may still require an
    adapter update.
  • The ordinary DNS response model cannot attach an IPv6 interface zone to an
    AAAA record. Interface metadata is retained for validation and per-interface
    merging, but a returned fe80::/10 address cannot carry its zone downstream.
  • Portable multicast depends on host UDP multicast and interface enumeration;
    sandboxes and containers may restrict either capability.
  • mdns:// is a direct link-local transport; proxy selection and
    respect-rules are intentionally not applied.

CI/merge gate

This PR should not be merged until the upstream Test and Build workflows have
actually run and passed. Fork workflow runs currently require a MetaCubeX
maintainer to approve execution.

@hengistchan
hengistchan marked this pull request as ready for review July 25, 2026 10:27
@hengistchan

Copy link
Copy Markdown
Author

Follow-up hardening is available in 1a842345:

  • require UDP source port 5353 and validate multicast destination/ingress interface
  • retain TTL/hop-limit and interface metadata; resolve CNAME chains per interface and union distinct positive answers
  • fix target-first and multi-packet CNAME chains
  • return timeout for silence, NODATA for applicable NSEC, and preserve explicit macOS negative results
  • fall back from unavailable/incompatible mDNSResponder IPC with joined diagnostics
  • add deterministic tests for all of the above

The updated fork workflows are waiting for maintainer approval and have not executed any jobs yet:

Please keep the PR unmerged until both upstream workflows have actually run and passed.

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.

1 participant