Skip to content

Repository files navigation

mDNS Viewer

A small self-hosted tool that lists every mDNS/Bonjour service and every SSDP/UPnP device on your local network, live, in a dark web UI. Click an entry to see everything the device gives away: IP addresses, hostname, port, TXT records (firmware, model, manufacturer, MAC, serial number, …), TTLs, UPnP services, and the other services running on the same host.

🇩🇪 Deutsche Fassung: README.de.md

mDNS / Bonjour tab SSDP / UPnP tab
mDNS tab with service list and detail panel SSDP/UPnP tab with device details

Features

  • mDNS / Bonjour: browses _services._dns-sd._udp for advertised service types, starts a browser for every type found, plus a seed list of ~40 common types (HomeKit, Matter, Chromecast, AirPlay, Sonos, printers, SSH/SMB, ESPHome, Shelly, Home Assistant, GL.iNet, …).
  • SSDP / UPnP: the devices Windows shows under Network → Media devices / Other devices (TVs, Sonos, NAS, AV receivers, set-top boxes, routers). Sends M-SEARCH, listens for NOTIFY, fetches and parses each device description XML.
  • Detail view: highlighted fields (firmware, model, manufacturer, MAC, serial), network table, all TXT records, UPnP service list with control URLs, embedded devices, vendor-specific fields (e.g. Sonos roomName, softwareVersion), raw JSON, one-click copy.
  • Live updates via Server-Sent Events, search across all fields, type filter, sorting, offline entries stay visible greyed out, keyboard navigation.
  • Zero dependencies besides Python and zeroconf; the UI is a single HTML file, no CDN.
  • Docker/Compose ready (host networking), runs as an unprivileged user with a read-only filesystem.

Quick start (Docker Compose, Linux host)

git clone https://github.com/macpit/mdns-viewer.git
cd mdns-viewer
docker compose up -d --build

Open http://<docker-host-ip>:8765/. Update with git pull && docker compose up -d --build, logs with docker compose logs -f.

Why network_mode: host: mDNS (224.0.0.251:5353) and SSDP (239.255.255.250:1900) are multicast. Inside the default Docker bridge network none of it arrives, so the container shares the host's network stack instead. That is also why compose.yaml has no ports: mapping; the port is opened directly on the host.

Linux hosts only. Docker Desktop on macOS or Windows runs containers in a Linux VM behind NAT; LAN multicast never reaches the VM, not even with network_mode: host. On a Mac, run the script natively (see below). Proxmox LXC/VM, Raspberry Pi, NAS with Docker and plain Linux servers all work.

Environment variable Meaning
MDNS_VIEWER_HOST Bind address, 0.0.0.0 in the image (native default 127.0.0.1)
MDNS_VIEWER_PORT HTTP port, default 8765
MDNS_VIEWER_NO_SSDP 1 disables the SSDP/UPnP tab
TZ Time zone for log timestamps

Exposure note: with 0.0.0.0 the UI is reachable on your LAN without a login. It only shows information the devices already broadcast, and the only action is "Rescan". Put a reverse proxy with authentication in front of it or firewall the port if that matters to you.

Run natively (macOS / Linux)

Requirements: Python 3.13 (macOS: brew install python@3.13).

git clone https://github.com/macpit/mdns-viewer.git
cd mdns-viewer
python3.13 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python mdns_viewer.py

The server starts on http://127.0.0.1:8765/ and opens your default browser. Stop with Ctrl+C.

Command-line options

Option Meaning
--host ADDRESS Bind address (default 127.0.0.1; 0.0.0.0 for LAN/container)
--port N HTTP port (default 8765)
--no-browser Don't open the browser automatically
--json SECONDS Headless: scan for N seconds, print JSON to stdout, exit
--types _a._tcp,_b._udp Browse additional mDNS service types
--backend auto|zeroconf|dns-sd Discovery backend (see below), default auto
--ip-version v4|all zeroconf transport, default v4 (see below)
--no-ssdp Disable SSDP/UPnP discovery (second tab)
-v / --verbose Verbose logging

Example: one-off scan as JSON for further processing

.venv/bin/python mdns_viewer.py --json 8 > scan.json

Using the UI

  • Tabs in the header (keys 1 / 2): mDNS and SSDP / UPnP, each with a counter of entries currently online. Search, type filter, sorting and the offline switch apply to the active tab.
  • List (left): name, type pill, IP:port, hostname and, when available, firmware/model. New entries flash teal. Entries that went away stay visible greyed out ("Show offline").
  • Search (key /): matches name, type, hostname, IPs and all TXT values / device fields. Multiple words are AND-combined.
  • Detail (right) after clicking an entry (or ↑/↓):
    • online/offline status, "Open" for web services (HTTP, Home Assistant, Shelly, ESPHome, GL.iNet, UPnP presentation URL, …), "Copy JSON"
    • Highlighted: firmware/version, model, manufacturer, MAC, serial number, device name, board, features, extracted heuristically from TXT records or the UPnP description
    • Network: hostname, IPv4/IPv6, port, priority/weight, TTLs, timestamps
    • TXT records (mDNS) or device / SSDP / services / embedded devices / vendor fields / SSDP headers (UPnP), values copy to the clipboard on click
    • other services on the same host as clickable chips, collapsible raw JSON
  • Rescan: recreates all mDNS browsers, re-resolves known entries and sends an SSDP M-SEARCH round.

How discovery works

mDNS / Bonjour

  1. Browsing _services._dns-sd._udp.local. yields the service types devices advertise; a new browser is started for every type found.
  2. A seed list of ~40 common types is browsed directly as well, because not every device answers the type enumeration.
  3. Every service is resolved (SRV, TXT, A/AAAA). If the IPv4 address is missing (typical for Chromecast) the tool retries once and then asks the system resolver.
  4. Changes are pushed to the browser immediately via Server-Sent Events.

SSDP / UPnP

  1. Every 30 s (three times in quick succession at start-up) an M-SEARCH for ssdp:all and upnp:rootdevice goes to 239.255.255.250:1900 on every IPv4 interface.
  2. Port 1900 is monitored as well: NOTIFY ssdp:alive adds devices instantly, ssdp:byebye marks them offline. Devices that neither answer nor announce for ~100 s go offline too.
  3. For every LOCATION URL the UPnP device description (XML) is fetched and parsed (4 s timeout, 1 MB max). One device = one description URL; several UDNs of the same description (embedded devices) are grouped.

Backends: zeroconf and dns-sd

  • zeroconf (default): pure-Python mDNS implementation. Provides TTLs and priority/weight and is the fastest.
  • dns-sd (macOS fallback): uses the macOS dns-sd tool, which talks to the system mDNSResponder.

Background: since macOS 15 every process needs the "Local Network" privacy permission to send multicast. Without it all zeroconf sockets fail with No route to host. At start-up the script therefore sends a multicast probe and falls back to dns-sd automatically. This is logged and shown in the status pill ("Live · dns-sd").

Why IPv4-only multicast (--ip-version v4)

In dual-stack mode (IPVersion.All) the replies of many embedded devices got lost on macOS: GL.iNet KVM (_glinet._tcp), a Eufy camera (_hap._tcp), Shelly and SLZB (_http._tcp) were reproducibly never found, while IPv4-only mode found all of them every time. Apple and Sonos devices showed up in both modes because they announce themselves periodically. IPv6 addresses of services are still shown, because the AAAA records arrive as additional records inside the IPv4 answers. To test dual-stack: --ip-version all.

macOS "Local Network" permission

The first time you start it from the Terminal, macOS asks whether the Terminal may find devices on the local network. Allow it. To change it later: System Settings → Privacy & Security → Local Network for the app you launch it from (Terminal, iTerm, VS Code, …).

Troubleshooting

Symptom Cause / fix
Log: weiche auf dns-sd aus (falling back to dns-sd) "Local Network" permission missing for the launching app, see above. Still works, just slower and without TTL/priority.
No entries, not even with dns-sd Active VPN that tunnels everything, wrong Wi-Fi, or a guest network with client isolation. Cross-check: dns-sd -B _services._dns-sd._udp local.
Container finds nothing Not running with network_mode: host, or Docker Desktop on macOS/Windows (see above).
Port 8765 cannot be bound Another process uses the port: use --port 9000 / MDNS_VIEWER_PORT.
Device shows IPv6 only The device doesn't answer A queries immediately. After a few seconds or a rescan the IPv4 address usually appears.
Entries with suffixes (2), (3) mDNS name-conflict resolution done by the device itself (e.g. after reboots), not duplicates created by the viewer.
Same TV listed several times in the UPnP tab The TV publishes one UPnP description per function (MediaRenderer, MediaServer, DIAL). They are cross-linked in the detail view.

Cross-check with built-in macOS tools:

dns-sd -B _services._dns-sd._udp local.     # all service types
dns-sd -L "Name" _hap._tcp local.           # resolve one service

Files

mdns_viewer.py       backend: discovery (zeroconf / dns-sd / SSDP), HTTP + SSE server, CLI
static/index.html    UI (HTML, CSS, JS in one file, no external dependencies)
requirements.txt     zeroconf
Dockerfile           container image (python:3.13-slim, unprivileged user, healthcheck)
compose.yaml         Docker Compose with network_mode: host
docs/                screenshots
LICENSE              MIT

API (for your own scripts)

Endpoint Description
GET /api/services JSON snapshot mDNS: services[] + stats + backend
GET /api/ssdp JSON snapshot UPnP: devices[] + stats
GET /api/events Server-Sent Events added, updated, removed, ssdp_added, ssdp_updated, ssdp_removed, heartbeat
POST /api/rescan Trigger a new scan (mDNS + SSDP)

An mDNS service entry contains among others name, instance, type, server, port, addresses_v4, addresses_v6, txt (all TXT keys), highlights, host_ttl, other_ttl, online, first_seen, last_seen. A UPnP entry adds manufacturer, model_name, model_number, serial_number, udn, location, presentation_url, server_header, services[], embedded[], extra{} and headers{}.

In headless mode (--json) the output additionally contains the key ssdp with the same structure as GET /api/ssdp.

License

MIT, see LICENSE.

About

mDNS/Bonjour- and SSDP/UPnP-Viewer (Python, zeroconf, Docker)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages