mDNS in NetNeighbor

May 31, 2026 · View on GitHub

Protocol overview

mDNS (multicast DNS / DNS-SD) advertises services on the local network under *.local. names. NetNeighbor uses zeroconf browsing callbacks; it does not perform an active LAN scan.

Typical service types discovered:

TypeMeaning
_http._tcpWeb UI
_smb._tcp, _microsoft-ds._tcpFile sharing
_ssh._tcpSSH
_ftp._tcpFTP
_telnet._tcpTelnet
_esp3d._tcpESP3D firmware

Code map

AreaFile / symbolRole
Providerdiscovery/mdns.pyMDNSDiscovery — zeroconf browser/listener, TXT decode, host aggregation
Contractdiscovery/base.pyBaseDiscovery._emit("device", payload) → normalized dict to manager
Orchestrationdiscovery/manager.pyReceives mdns payloads, applies overrides, stores and notifies
Mapping hintsconfig/device_types.jsonPer-service defaults (type, icon, default_port)
UI payloadutils/details_payload.pybuild_mdns_payload — per-service sections for the Services tab
Rulesconfig/mdns_rules.jsonTXT → summary line mappings and optional type_rules

Browse strategy

  • Service types from config/device_types.json (mdns section) are browsed immediately.
  • DNS-SD enumeration (_services._dns-sd._udp) runs in background to catch unlisted types.
  • Enumeration repeats on a timer and on refresh() — no types need to be listed statically.

Host-level aggregation

mDNS announces per-service. NetNeighbor aggregates services to one logical host entry:

  1. Track per-service payloads (service_type, instance_name)
  2. Map each service to a host key (prefer IP, fallback hostname)
  3. Emit an aggregated payload with all services under metadata["services"]

A device like ESP3D exposing both _esp3d._tcp and _telnet._tcp appears as one entry with both services visible in the Details dialog.

URL policy

A presentation URL is built only if _http._tcp is actually present. Port numbers alone do not imply HTTP — this avoids wrong browser links for non-HTTP services on common ports.

TXT records

  • Raw ServiceInfo.text is decoded preserving duplicate keys and ordering → metadata["services"][].txt_records
  • A flat metadata["txt"] (last-key-wins) is kept for type heuristics
  • Details dialog shows one expandable row per service with TXT pairs nested under it

Type heuristics

Type starts from device_types.json (per-service mapping), then refined entirely by the type_rules in mdns_rules.json (first match wins). All classification heuristics (fluidnc→cnc, laserjet→networkprinter, synology/qnap/nas→nas, 3D-printer/CNC keywords, …) live in that JSON — there are no hardcoded type rules in mdns.py.

Online / offline lifecycle

A host stays online while at least one tracked service remains. Removing one service updates the aggregated payload; offline is emitted only when all services are gone — avoids flapping for multi-service hosts.

When the last service disappears while the host is still otherwise alive, the removal is delayed by a 120 s grace period to absorb mDNS TTL jitter. A user-initiated refresh() reschedules any pending grace-removes to a short 15 s window, so a device that truly left the network disappears quickly instead of lingering for the full grace period.

Brutal disconnections (power loss, cable pull) emit no byebye and would otherwise persist until the mDNS TTL expires. The manager's periodic ICMP probe (see BACKEND_ARCHITECTURE.md) flips such hosts offline within ~180 s and drops non-monitored ones from the list.


Rules file (config/mdns_rules.json)

Lets you map TXT keys onto summary lines and extend type classification without Python changes. User overlay: ~/.config/netneighbor/mdns_rules.json — merged with the bundled file. See COMMUNITY_OVERRIDES.md.

Single source of truth. This bundled file is the rules — there is no Python copy. The startup integrity check (utils/config_integrity.py) refuses to launch with a "corrupted installation, please reinstall" dialog if it is missing or not valid JSON.

summary_from_txt

Ordered list; each entry emits at most one first-tab detail row.

FieldTypeMeaning
labelstringDetail column heading (match SSDP wording to enable cross-protocol merge)
keyslistTXT key aliases, priority order, case-insensitive

type_rules

Evaluated after fixed heuristics. First match wins.

FieldTypeMeaning
contains_anylistLowercase substrings searched in service key, name, and TXT pairs
typestringInternal type id (e.g. nas, router)

How to extend

  1. Add summary_from_txt entries matching your devices' actual TXT keys (check Services tab in Details).
  2. Use the same label as SSDP counterparts so SSDP+mDNS detail rows merge correctly.
  3. Add type_rules with specific contains_any before generic ones.
  4. Validate: python -m json.tool config/mdns_rules.json
  5. Restart the app (rules are cached at startup).

Debugging

  • Enable mdns in ~/.config/netneighbor/logging.json
  • Details dialog → Services tab: verify TXT records, services list, URL present only when _http._tcp exists

See also