The person setting up an attendance machine rarely knows its address. They know where it hangs on the wall. So the connector's setup window has a Search button, and this post is what happens behind it.
Some brands make that easy: Hikvision and Dahua answer a multicast probe with their serial, MAC, model and address. ZKTeco, about seven in ten of the machines on the platform, announces nothing at all, and neither do Anviz and Suprema. And the machines are not always on the PC's network: they sit on a branch's subnet, a second VLAN, or the internet provider's LAN one router up. Reaching them is the companion post; this one is finding them, being sure which one you found, and finding it again when it moves. A search runs when someone asks, once on a network the PC has not searched, and when a machine it reads goes missing; never as a continuous scan of a customer's network.
Machines that answer a call
Hikvision speaks SADP: an XML inquiry to the multicast group 239.255.255.250 on UDP 37020, which every Hikvision device on the network answers with its serial, MAC, address, model and firmware. The connector sends one from every office network the PC is on and listens for four seconds.
A SADP reply carries no proof of anything, and the address in it can become the place this PC reads attendance from. So a reply is believed only when the address it claims is the address the packet came from, and is on one of the PC's own networks, or at least private (a machine still on its factory address, marked off-range). A reply with neither serial nor MAC is dropped. "Own networks" leaves out the second addresses the connector added itself, because judged against those, a factory-address machine stops looking off-range the moment the alias that reaches it goes up.
func parseMatch(raw []byte, from *net.UDPAddr, own []*net.IPNet) (Discovered, bool) {
var m probeMatch // DeviceSN, MAC, IPv4Address, DeviceType, SoftwareVersion
if xml.Unmarshal(raw, &m) != nil {
return Discovered{}, false
}
claimed := net.ParseIP(strings.TrimSpace(m.IPv4Address)).To4()
if claimed == nil || from == nil || !claimed.Equal(from.IP.To4()) {
return Discovered{}, false // claims to be somewhere it did not send from
}
offRange := !onLocal(claimed, own)
if offRange && !private(claimed) {
return Discovered{}, false
}
// ... serial or MAC required
}
Multicast does not always get through: office switches, Wi-Fi access points and IGMP snooping drop it. Every Hikvision also answers the same inquiry sent straight to its address, so when the group probe finds nothing, the search asks every address of the PC's own /24s directly (at most four /24s, at most 50 inquiries a second across the whole PC) and judges the replies the same way. On Windows a host's ICMP "port unreachable" arrives as a read error; the reader keeps going past those, but not on a socket that only fails.
Dahua has DHDiscover.search, multicast to 239.255.255.251 on UDP
37810: a 32-byte header (0x20000000, DHIP, then the payload length
twice, little-endian) in front of a JSON body. I did not find the format in
Dahua's public documentation; the clearest public description was an
open-source home-automation binding, so I treat it as medium confidence, and
the trust rules are SADP's exactly. Both probes go out together and share
one wait, so "call every machine" costs four seconds, not eight, and one
brand's probe failing to send does not stop the other's replies counting.
Machines that answer a knock
ZKTeco answers no probe. What it has is a TCP port, 4370. So the search
knocks: a plain TCP connect to every address of the PC's private networks,
closed at once, nothing sent. Up to 1,024 addresses per network (on a wider
one, the /22 around the PC), 64 connects at a time, 250 ms each. An answered
connect leaves the machine's MAC in the OS's ARP table, read once afterwards
(on Windows through the IP Helper API, without starting arp.exe). A MAC
with ZKTeco's IEEE prefix makes the row zk_port; anything else on 4370 is
zk_port_guess, likely but not proven. Then each ZKTeco is asked for its
serial with comm key 0 (connect, prove the key, read, close), four at a
time, ten seconds each, so the row shows the real serial and "add it" can
prefill it.
The same pass knocks on TCP 5010 (Anviz's client-mode port) and TCP 51211 (the port Suprema devices listen on). An open port there is specific enough on its own. The connector does not read either brand; it lists them, so someone can ask for support rather than conclude there is no machine.
Last, any ARP neighbour on the swept networks that no probe or port named is checked against a table of MAC prefixes (IEEE MA-L, via Wireshark's manufacturer list): ZKTeco, the OEM behind Sintech, Anviz, Suprema, and the many Hikvision and Dahua blocks. An OUI match is a hint, never identity, so it is used only after every active check has had its chance.
The cheap steps go first and the slow ones get their own budget, so what was found next door is never lost to a sweep further out.
Where it searches
The window shows three brand-neutral steps (call, sweep, router), with the per-address inquiry and the port knock folded into one sweep. Behind them the search covers more than the PC's own network, and each addition was a field case where a machine existed and the search said it did not.
The /24 of machines the platform already knows. A branch or a second VLAN the PC is not on was never searched. Now the addresses the platform has for the company's machines become their /24s (private only, at most eight, skipping any the PC already covers) and the search asks them by routing: SADP and DHIP address by address, plus the port knock. A hit there is never marked off-range, because off-range means "add a second address", and a routed network must not get one.
Networks a person types. Some networks have no known machine yet. The
Search screen has a field for them, and the platform can hold a company-wide
list every PC searches. It takes 192.168.10.x, 192.168.10.0/24 or any
private CIDR from /22 to /32, comma-separated, at most eight together with
the automatic ones, and an error names the entry that failed in the person's
own words. Each is swept at its own width, not forced into a /24. The
setting lives in the connector's protected folder under the same trust check
as the proxy setting, and the background service re-reads it every minute.
One router away. The office in the companion post had its PC behind its own router and two ZKTecos on the provider's LAN, which none of the above contained. So the search also asks eight addresses home and office routers ship with (192.168.0.1, 192.168.1.1, 10.0.0.1 and the like) on ports 53, 80 and 443, all at once with a one-second timeout, in the background while the multicast wait runs. Three guards keep that honest:
- TTL 2. Each connect leaves with an IP time-to-live of two: it reaches a router one hop past the PC's own and no further, so an address the internet provider answers for is never taken for one in this office.
- Five or more answering is interception. A captive portal or security box that answers for every address would make every candidate look real, so then none is used.
- No overlap. A candidate whose /24 overlaps the PC's own networks, its machine link or a range already being searched is skipped.
A candidate that answers gives its /24 to the routed sweep, with two more ports in the knock: Hikvision's 8000 and Dahua's 37777. Rows there are off-range and never earn a second address; no MAC crosses a router, so the platform tells them apart by serial alone. Order and budget matter. The PC's own networks go first, and the sweep one router away runs after them in its own 30-second budget, so a slow sweep further out never costs what was found next door. Inside that budget the knock runs beside SADP and DHIP, not after: on two networks one router away, address-by-address SADP and DHIP used the whole budget and the knock never ran. In that office SADP sent to single addresses never came back across the router at all. The knock is what found the Hikvisions there.
A network whose router is silent. The first build of this found nothing in the office it was written for. The provider's network had no router answering at any usual address (as happens when an office router is joined to it by a second port), so the gate skipped the range holding both machines. Now a candidate with a silent router counts if any address in its /24 answers on 4370, at the same TTL 2. Addresses go in the order machines and DHCP pools usually take them (.2–.30, .100–.120, .200–.254, then the rest), 128 at a time, capped at eight seconds: about a thousand addresses, the likely ranges of every candidate rather than every address of all of them. A machine elsewhere on a silent network is found once someone types its network.
The PC's own network is swept in full. Everything else is reached by routing, so it is bounded, never given a second address, and named by serial.
Devices that will not say what they are. Across a router there is no
MAC, and many devices give no serial without a password. In that office the
search found about thirty Hikvision devices on a third range, almost
certainly the CCTV cameras, which answered on their ports and said nothing
else. Listing each as an "add this machine" row was noise, and adding one
made a card for nothing. A device with neither serial nor MAC is now never
reported to the platform; the window shows one line per brand and network,
in the shape n Hikvision devices on 192.168.50.x · usually cameras, with
an "Add by address" link, and the search counts only machines it named.
This PC's own ZKTeco. The most embarrassing miss: a ZKTeco takes one connection at a time, so one this PC was reading during the sweep did not answer the knock, and the person looking at the list saw their own working machine missing. After the search, each of this PC's ZKTecos on a searched network that did not answer is knocked once more, after taking that machine's read lock (waiting at most 15 seconds for the read to let go).
Proving identity before reading
Finding a machine at an address is not knowing which machine it is. Two machines that swap addresses (DHCP after a power cut, the same admin password on both) used to have one machine's punches, and its staff numbers, recorded as the other's. Every read would succeed. Nothing would be wrong except everything.
Now, before anything is read, the connector asks the machine at the address for its own serial and reads only if it matches. The first version checked once an hour per address. An adversarial review the same day found the flaw: a swap keeps both addresses the platform has, so a cached "this address is that machine" is exactly what a swap breaks. It is now asked on every read, one small request a minute per machine:
got, err := reader.Identify(ctx) // the machine's own serial, nothing else
if err != nil {
reader.Close()
return nil, err
}
if got != "" && !sameSerial(got, t.Serial) {
reader.Close()
return nil, &WrongMachineError{Host: t.Host, Want: t.Serial, Got: got}
}
return reader, nil // only now is anything read
A different machine is never read: the window says which serial answered instead of the one expected, and the right machine is looked for by its serial at once. Serials match in either form, because a Hikvision reports a long serial while its label shows a short one: equal, or one ending with the other and at least eight characters long. A correctly set-up machine is never blocked by its own label.
Company keys stay with their own machines
A ZKTeco with a comm key will not give its serial for key 0. The platform holds the keys of the company's own ZKTecos, and the obvious move is to try them all on every locked machine a search finds. That would leak them. The protocol does not send a hash of the key: it sends a reversible scramble of the key's bits and the session number, so whoever receives the proof can work the key out. A host that merely answers on 4370 (a neighbour's machine in a shared building, or anything pretending to be one) would collect every key offered.
So a search offers a key in one place: the address the platform has for the
machine it belongs to, its own address or one reported for that same
serial. Anywhere else the row stays serial-less. A search never tries a
Hikvision or Dahua login either; their serial comes from SADP and DHIP.
A ZKTeco that refuses key 0 at no address the platform knows is listed as
locked. Someone types its key once in the web app, and an identify
instruction tries that key at that one private address, reads the serial
and nothing else, and forgets the key.
In a search, a key goes where the platform already places its machine, or where a person typed it for one address.
Finding a machine again after it moves
An office router handing out a new address looks exactly like a dead machine. The connector does not wait for someone to drive there. After three failed reads in a row it looks for the machine, and again every 30 failures after that, because a machine switched off during the first look may come back somewhere new. Three cases look sooner or not at all:
- Added with only a serial, no address at all: looked for on the first failed read, not the third.
- A different machine at the address: looked for on the first failure.
- Answered and refused the comm key: no look. It is where it was, and a search would not help.
A Hikvision is looked for by SADP, by serial and MAC. ZKTeco was the gap: refinding was Hikvision-only, and the setup advice that covered for it, "turn DHCP off on the machine", was wrong for offices whose provider's router renumbers the network. Now a ZKTeco that stopped answering sets off a search of every network this PC searches (its own, one router away, the company's) and is matched by the serial that search reads. A locked one is matched by trying its own comm key at each unnamed ZKTeco not at another machine's address. That is the one place a key leaves its machine's known address, and it is bounded: only on networks this PC already searches, only for a machine that is already failing, at most sixteen hosts. One look per machine at a time, and machines that stop together share one search, since a router that renumbers moves them all at once. When the machine turns up, the next read goes to the new address and the platform is told, so the saved address follows the machine.
A moved machine is the common case of a dead one, so the looks are on a schedule rather than on someone's visit.
The window
Two changes made the search feel like it worked without changing what it found.
List a machine the moment it answers. The address check takes about 30 seconds on a /24, and the window used to list nothing until the whole search ended. Half a minute of an empty box reads as "nothing found". Now each machine appears as soon as its port answers, under "Found so far", partial (no MAC, model or serial yet), and the search's full answer replaces the list when it ends. A new search starts with an empty list, so the previous one's rows never pass for this one's.
One steady progress bar. The whole search, own networks plus one router away, takes about a minute. The window used to promise 30 seconds, restart its address count on each pass, give up after 80 seconds, and, on a click within two minutes of the last search, quietly show the old results instead of searching. Now:
// One bar for the whole search, moving with the clock against how long the
// last one took (a minute before any), so it never jumps back between passes.
var expect = Math.max(10, run.last_secs || 60), over = secs > expect;
fill.style.width = Math.min(95, Math.round(100 * secs / expect)) + '%';
label = over ? 'Taking longer than last time, still checking'
: run.last_secs ? 'About ' + about + ', like the last search' : 'Usually about a minute';
The bar stops at 95% until the search really ends, so it never claims to be done. The address count only rises; each pass adds to it instead of resetting it. The window waits while the search runs, up to five minutes. And "Search again" searches: the background service takes a new request 20 seconds after the last, which absorbs a double-click and is short enough that nobody notices.
What I would tell someone building this
- Most machines will not announce themselves. Multicast discovery covers the brands that implement it. Budget for a port knock, an ARP read and an OUI table for the rest, and record which kind of evidence named each row.
- Believe a discovery reply only where it came from. The claimed address must be the source address, on a network you can justify.
- Search where the machines are, not where the PC is. Known machines' networks, typed networks and one router away each found machines the PC's own sweep never could.
- Bound everything you send. TTL 2, an interception threshold, a budget per step, and the cheap steps first.
- Identity before data, on every read. An address is a guess and a serial is the machine; a cached mapping is exactly what a swap breaks.
- A reversible proof is the secret. If trying a key gives it away, offer it only where you have a reason to.
- Treat a moved machine as the usual dead one. Look again on a schedule, share one search across machines that failed together, and tell the platform the new address.
- Show progress honestly. Hits as they land, one bar that only moves forward, and a button that does what it says.
What the search still cannot do, such as calling a camera a camera before anyone types its password, belongs with the rest of what the connector still needs.