Terminal UI to browse and debug mDNS/DNS-SD on the local link — live services, records, queries, and raw packets.
Built with Rust (ratatui, crossterm, simple-dns). Listens passively on UDP port 5353 (IPv4 + IPv6 multicast) and also sends the standard browse queries (service enumeration, PTR/SRV/TXT/A/AAAA).
Prebuilt binaries are attached to GitHub releases: Linux binaries (x86_64 glibc + musl static) and macOS DMGs (Apple Silicon + Intel). On Arch Linux, install from the AUR instead:
yay -S mexOn macOS, install via Homebrew:
brew tap hrzlgnm/tap
brew install --cask mexOr directly without tapping:
brew install --cask hrzlgnm/tap/mexmex [OPTIONS]
Options:
--passive Listen only: never transmit queries, just observe announcements
-i, --interfaces <IFACES> Network interfaces to use for mDNS discovery (e.g., br0, eth0)
--log-file <PATH> Override the default log file (state-dir mex.log on Linux,
~/Library/Logs/mex on macOS, %LOCALAPPDATA%\mex\logs on Windows).
Appended; rotated past 5 MiB with 3 backups kept. Without it,
both modes log to the default file; headless mode additionally
logs warnings to stderr.
--log-level <LEVEL> Minimum log level recorded by every active sink
(off, error, warn, info, debug, trace). Default: split
defaults (file info, stderr warn).
-h, --help Print help
-V, --version Print versionWith --passive, mex transmits nothing: no startup QU batch, no continuous
queries, no TTL refreshes — the view shows recently heard traffic
decaying by TTL instead of actively maintained state.
With -i/--interfaces, mex joins, queries, and ingests only the named
links (repeatable and comma-separated: -i br0,eth0). Unknown names abort
at startup with the available list; an explicit selection overrides the
default loopback/container skip. Without the flag, all usable links are
browsed as before.
- Left — Hosts. Collapsible tree grouped by hostname (
printer,.localstripped as redundant, hostname in italic as app-side grouping, not a wire record); each header shows(service count) · primary address (+extra)— e.g.printer (2) · 192.168.1.10 (+1)— with(+N)only when more than one live address is known. Each row showsinstance [short type:port](http.tcpis short for_http._tcp.local, e.g.My Printer [http.tcp:9100]) with the state dot up front.Up/Downskips open headers (collapsed ones stay selectable), the selection is inverted, and the pane title always names the selected row's host. - Right top — Answers. For the selected service: derived rows
(
srvc,subt, in italic as constructed summaries, not wire records), then one row per observed raw record (deduplicated across responders, sorted by type then source) with remaining TTL (heat-colored by requery schedule: yellow once past 80% elapsed — a refresh is due — red past ~95% or expired), flush bit, source, and rdata. Multi-pair TXT records expand into unlabeled child rows. Records announced by this host are highlighted; goodbye (TTL 0) records show as expired. - Right middle — Questions. Recently observed queries asking for this
service, newest first (capped), with per-query age instead of TTL.
Questions from this host are highlighted like local records.
Queriers asking from an ephemeral port (RFC 6762 §5.1) are tagged
(one-shot): stub resolvers, not full continuous queriers. - Right bottom — Hex. Wire bytes of the selected record or question
packet, falling back to the service's latest packet.
Tabfocuses it for scrolling. - Mouse. Drag the shared pane borders to resize Hosts vs detail and Answers / Questions / Anomalies / Hex (the seam lights up). Click a row to focus its pane and move the cursor there; clicking a host header folds it. While mouse input is captured, use Shift+drag to select text in most terminals.
| Key | Action |
|---|---|
Tab / Shift+Tab |
switch pane (Hosts → Answers → Questions → Anomalies → Hex) |
Up / Down, j / k |
move within the focused pane (scroll Hex / Anomalies when focused) |
h / l, ← / → |
jump between host groups |
H / L |
first / last host group |
Home / End |
first / last row of the focused pane (top / bottom of Hex) |
PageUp / PageDown, b / f, Space |
page (viewport scroll in Hex) |
Enter |
fold / unfold host (Hosts) or TXT record (Answers) |
+ / - |
fold / unfold host (Hosts) |
* |
collapse all host groups / unfold all |
Ctrl+↑/↓, Ctrl+k, Shift+↑/↓, J / K |
scroll Answers viewport freely (Hex viewport when Hex focused) |
Ctrl+J |
dump mex state to JSON, <timestamp>-mex-dump.json (experimental; tokens, public URLs/hosts redacted) |
m |
metrics overlay (traffic, discovery, queries, links) |
q / Esc / Ctrl+C |
quit (m / Esc closes the overlay first) |
Ctrl+Z (unix) |
suspend to shell (fg resumes) |
| mouse drag on pane borders | resize panes |
| mouse click | focus pane + move cursor (host header folds) |
| mouse wheel | move cursor one row / scroll one line (focused pane; overlay when open) |
The header always shows the live hints. In the metrics overlay, j/k
and paging keys scroll instead, and Tab/Enter do nothing.
mex continuously checks observed traffic against the mDNS/DNS-SD RFCs and surfaces each deviation as an anomaly with a severity:
- error — a definite RFC violation in confirmed packet context (e.g. a shared record with the cache-flush bit set, NS/SOA answered in a multicast answer section).
- warning — a likely misconfiguration (e.g. a
.localservice with no address records, a subtype listed in service enumeration). - info — a SHOULD-level deviation, shown only in the metrics
overlay and state dumps (e.g. a PTR re-announced with a 120 s TTL
instead of the recommended 4500 s, a unique record missing its
cache-flush bit, an SRV target outside
.local).
Findings show up in several places: anomalous Answers rows get a !
marker (on the type cell, or on the TTL cell when the TTL itself is
the problem), anomalous Questions rows get one on the type, instances
carry a [!N] badge in the Hosts pane, and the Anomalies pane lists
every finding for the selected service with a > marker on the
highlighted row's findings. Each line reads
<icon> <source>: <kind> <record>: <message> (<rfc>, <requirement>) —
the announcer/querier address leads, the level icon carries the
severity (no severity word), and enumeration-row findings omit their
constant scope. The metrics overlay (m) lists all findings with
severity icons as <icon> <kind> → <source>: <service> <record>: <message> (<rfc>), and Ctrl+J state dumps include the
full list.
| Finding | Meaning |
|---|---|
missing-addr (warning: .local, info: otherwise) |
hostname with a live SRV record but no live A/AAAA (RFC 6763 §12, SHOULD) |
type-format (warning) |
malformed service type, subtype, or enumerated type (RFC 6763 §7, MUST) |
enum-subtype (warning) |
subtype listed in service enumeration instead of a base type (RFC 6763 §9, SHOULD) |
shared-flush (error) |
shared PTR record with the cache-flush bit set (RFC 6762 §10.2, MUST NOT) |
no-flush (warning when confirmed, else info) |
unique SRV/TXT record missing the cache-flush bit (RFC 6762 §10.2, SHOULD); confirmed only in a multicast answer |
unicast-leak (error when confirmed, else warning) |
NS/SOA/ANY/MX outside unicast DNS (RFC 6762 §3); confirmed only in a multicast answer, otherwise pending capture verification |
unicast-question (error: NS/SOA, warning: MX) |
multicast question asking for a unicast-only type (RFC 6762 §3); ANY questions are ownership probes and stay quiet; ephemeral-port queriers are tagged one-shot |
ttl-low / ttl-high (warning) |
TTL below 10 s (churn risk) or above 7200 s (stale-cache risk) (RFC 6762 §5.2, SHOULD) |
ttl-mismatch (info) |
service PTR at ≤ 120 s instead of the recommended 4500 s base (RFC 6762 §10, SHOULD) |
ttl-unstable (warning) |
same name and type announced with different TTL bases (RFC 6762 §5.2, SHOULD) |
srv-target (warning: malformed, info: target needs unicast DNS) |
SRV target malformed (MUST) or resolving outside mDNS (MAY) (RFC 6763 §12) |
txt-oversize (warning) |
TXT payload over ~1300 B, risking fragmentation (RFC 6762 §17, SHOULD) |
Reverse-mapping PTRs (*.in-addr.arpa, *.ip6.arpa) are exempt from
the shared-record rules: they are unique records. Omitting the flush
bit on A/AAAA records and publishing TXT records at 120 s are
universal practice and stay quiet.
cargo build
./target/debug/mexRequires no external commands — interface enumeration goes through the
if-addrs crate. No root needed. Without a TTY it runs headless and logs
to stderr.
Quiet publishers' subtypes are undiscoverable. A service subtype (e.g.
_magic._sub._test._tcp) can only be learned two ways: from a
_services._dns-sd._udp enumeration that lists it, or by overhearing
subtype traffic (startup announcements, someone else's subtype
queries/responses). Some responders (e.g. avahi) do not list subtypes in
their enumeration answers, and there is no DNS query that enumerates
unknown subtype labels. So if mex starts after such a publisher has gone
quiet, it shows the service but not its subtype; starting mex first (or
any subtype traffic while it runs) picks it up normally via the
announcement burst. Once learned, subtype enumerations are re-queried to
stay fresh.