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.
Listen, don’t scan
Section titled “Listen, don’t scan”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:alivewhen 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.
What it knows how to talk to
Section titled “What it knows how to talk 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.
Untrusted, all of it
Section titled “Untrusted, all of it”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
NOTIFYpointingLOCATIONat 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.
Where HotLoop has to sit to hear anything
Section titled “Where HotLoop has to sit to hear anything”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:
- 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.
- 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’snet.ipv4.ip_unprivileged_port_startto 67 or lower. In Kubernetes, addingNET_BIND_SERVICEto 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.
Several replicas
Section titled “Several replicas”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.
Backups
Section titled “Backups”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.
How it’s tested
Section titled “How it’s tested”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
NOTIFYand answeringM-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.