Skip to content
GitHub

Network discovery

HotLoop listens to the plant network and tells you what’s on it: the ESPHome node in the yard, the Shelly on the compressor, the OPC UA server on line 3, the UPnP box somebody plugged in and forgot, the thing that just asked for an address. Each one shows up on Devices, Discovered, under the devices that announced themselves over MQTT (DISCOVERY.md), with what it is and how HotLoop could talk to it.

Then a person decides. Nothing heard on the network is ever added by itself, and nothing is ever armed.

On an OT network you read, you don’t scan. A port sweep has knocked over more than one PLC that answered a packet it never expected, and “it’s just discovery” is no comfort at three in the morning. So by default HotLoop sends nothing at all:

  • mDNS (on): devices announce their services when they start, and every answer to anybody’s question is multicast to the whole segment. A phone opening an app or a laptop looking for a printer asks; HotLoop overhears.
  • SSDP (on): UPnP devices multicast NOTIFY ssdp:alive when they join and every few minutes after. HotLoop overhears those too.
  • DHCP (off, see below): a device joining the network broadcasts a request for an address with its MAC and usually its name.

Asking is probing, and it’s switched on per network, by the deployment, never by a button:

Variable Default Details
HOTLOOP_NETDISC_MDNS true listen for mDNS
HOTLOOP_NETDISC_SSDP true listen for SSDP
HOTLOOP_NETDISC_DHCP false listen for DHCP requests on UDP 67
HOTLOOP_NETDISC_PROBE_NETWORKS empty the networks probing is on for, as CIDRs: 10.20.0.0/24,192.168.7.0/24
HOTLOOP_NETDISC_PROBE_INTERVAL 15m how often a probe runs there

On a listed network a probe sends an mDNS question for every service type and for the ones HotLoop knows, asks again about each type that turned up, sends one SSDP M-SEARCH, and reads each UPnP device’s description (its name, maker and model) from the device. On every other network it sends nothing, and the Probe now button is refused where nothing is listed. List the networks you mean: a /8 is 16 million addresses, and HotLoop will ask on it if you tell it to.

Heard It is Accepting it
mDNS _opcua-tcp._tcp an OPC UA server (OPC 10000-12 Annex C: the endpoint is the SRV’s address and port plus the TXT’s path) makes an OPC UA device at that endpoint, anonymous and unsecured to start, no tags. Browse it with Discover tags and set its security before it carries anything that matters
mDNS _shelly._tcp, or _http._tcp with gen a Shelly, Gen2 or later (its TXT gen, app and ver) makes an HTTP device reading http://<it>/rpc/Shelly.GetStatus, Shelly’s own status call, read-only, no tags. Discover tags lists every value. A Shelly with its login on wants digest auth, which the HTTP driver doesn’t do
mDNS _esphomelib._tcp an ESPHome node (its TXT: friendly_name, version, mac, platform, board, network) records it as known and makes nothing. HotLoop reads ESPHome over MQTT: give the node an mqtt: block pointing at HotLoop’s broker and it announces itself, and the card says so (matched by MAC)
mDNS _mqtt._tcp an MQTT broker records it. Add it under Settings, MQTT to hear what announces on it
mDNS _http._tcp, anything else a web page, or a service HotLoop has no word for records it
SSDP a UPnP device, by its uuid: device types, SERVER, and with probing on, its description records it
DHCP a device asking for an address: MAC, host name, vendor class records it

“Records it” is still worth a click. On a plant network every box should be one somebody knows about, and an accepted find is a decision with a name and a time on it. The next unknown thing stands out.

A card says when two finds are the same box: an ESPHome node’s mDNS MAC and its MQTT announcement, a DHCP request and an mDNS host on the same address.

Accepting arms nothing. A device accepting makes has no tags until somebody picks them, and every point on it is read-only until somebody arms it in the tag editor, through the same write gate as everything else. The address in the proposal came from the device’s own announcement, and the accept dialog shows it so you can check it’s the box you think it is.

Ignore hides a find and keeps it hidden when it’s heard again. Un-ignore brings it back.

Anybody who can put a packet on the segment can claim to be anything. So:

  • every string is clipped and stripped of control characters, and the screen puts it on the page as text, never HTML;
  • a UPnP description is read only from the address the announcement came from, only on a probe network, never through a redirect, and never more than 64 KiB. A spoofed NOTIFY pointing LOCATION at a PLC’s web page, or at HotLoop itself, gets nothing fetched;
  • each listener takes 500 datagrams a second and drops and counts the rest;
  • the record holds 1,024 finds. New ones past that are dropped and counted until somebody deals with some, and the screen says so;
  • a question on the wire is somebody’s question, not an announcement: the known answers it carries are the asker’s beliefs, and nothing is made of them.

Multicast and broadcast don’t cross a router, a NAT or a container network. HotLoop hears the segment it’s on, and nothing else:

  • A box on the plant network (the binary, or Podman with --network host): hears it all.
  • Kubernetes with a pod network: hears nothing from the plant. The Discovered screen shows the interfaces it’s listening on, which is how you find out. Give the pod hostNetwork: true, or a second interface on the plant segment (Multus with macvlan), and it hears the plant. The chart doesn’t do either for you: putting a pod on the host’s network is a decision about the node, not a default.

mDNS (5353) and SSDP (1900) are ordinary ports above 1024, so they need no privilege. HotLoop shares them with whatever already holds them, an avahi-daemon on the host included.

Feasible, honestly, but it needs two things a container doesn’t get by default, which is why it’s off:

  1. The plant’s broadcasts. A DHCP request goes to 255.255.255.255, so HotLoop has to be on the segment: host networking or an interface on it, same as above.
  2. UDP port 67, which is below 1024. HotLoop’s image runs as uid 65532. Either give the binary CAP_NET_BIND_SERVICE (setcap cap_net_bind_service=+ep /hotloop), or set the host’s net.ipv4.ip_unprivileged_port_start to 67 or lower. In Kubernetes, adding NET_BIND_SERVICE to a non-root container’s capabilities doesn’t make it effective on its own; the file capability or the sysctl does.

It never needs a raw socket or a privileged container: an ordinary UDP socket on port 67 receives the broadcasts. It never answers, never offers an address, never sends a byte. It shares the port, so a DHCP server on the same host that shares it too keeps working; one that holds the port to itself makes HotLoop’s listener fail, and the screen says why, with the server untouched.

What it can’t see: renewals, which go unicast to the server. HotLoop hears a device when it joins, not every hour after.

Only the replica holding the network discovery lock listens and probes; the others try again every 30 seconds, so one takes over if the holder goes. Two probing would ask every device twice.

net_finds is in every backup: what was heard, and who accepted or ignored what. The decisions are the part the network never says again.

Against real announcers on a real network (test/services/netdisc), on the same Podman network as the tests:

  • avahi-daemon publishing exactly what ESPHome 2026.9.1’s mdns component publishes for a node with the native API, what Shelly documents for a Gen2 device, and an OPC UA record per OPC 10000-12 Annex C. The OPC UA one runs in the network namespace of a real opc-plc server, and the test accepts it and waits for the device to connect. ESPHome’s own host build can’t stand in: its mDNS on the host platform announces nothing (mdns_host.cpp: “Host platform doesn’t have actual mDNS implementation”), and Shelly firmware doesn’t run in a container. So the software on the wire is real, and what it claims to be is those devices’ own documented records. No Shelly answers the HTTP device accepting makes, so that one is checked for its address, not polled;
  • MiniDLNA, a real UPnP media server, sending NOTIFY and answering M-SEARCH, with its description read on a probe network and not read anywhere else;
  • BusyBox’s udhcpc broadcasting real DHCPDISCOVERs with a host name and a vendor class.

“Listening sends nothing” is held by the tests that run with probing off: a neighbour on the segment asks the questions, the way a phone does, and a probe is refused. What isn’t proven is the plant itself: no real ESPHome node, Shelly or PLC has been put on the bench for this yet.