Lock down the relay device for the flat (no-VLAN) network. Relay control: - pulseOpen/setRelay now use the Dingtian BINARY protocol (:60000) with a relay password — the only relay option with auth (string :60001 has none, and is kept only for the read-only status query). Frame verified on hardware. HardenableDevice capability (driver harden()): - set a random relay_pw (1-9999); disable unused channels (rs485/can/tcp x2/mqtt -> p:255), keeping UDP1 binary (control) + UDP2 string (status). - write-verified (device reboots on apply). Assign/Save flow now does: fix preconditions -> harden -> set up input push; the relay password is stored in lane_devices so the runtime device can command the relay. DELIBERATELY NOT touching the device's HTTP CGI session check (session_en): enabling it on this firmware breaks the config-READ API (ECONNRESET) and locked the backend out — required a factory reset to recover. The open CGI API is accepted as flat-network reality; the signed event log is the real guarantee. Verified end to end on hardware: assign hardens + configures the device, config API stays reachable, pulseOpen with the stored password fires the relay, without it is rejected. wiki: device-input-flow + dingtian-relay updated.
5.9 KiB
type, tags, sources, updated
| type | tags | sources | updated | ||||
|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-15 |
Device Input Flow (button → backend → relay)
How a physical button press drives the entry lane. The backend is the source of truth: the device only reports the press; the host decides and commands the relay. This is the host-in-the- loop flow the dingtian-relay makes possible (and the uhppote-controller could not).
The path (no polling)
car arrives → driver presses button (input I_N, dry contact to GND)
→ device HTTP-pushes GET …/api/devices/dingtian/<deviceId>/input/<N>/on
→ backend: emit internal device event (device-events bus)
→ backend entry flow: create + sign an entry event, print the ticket
→ backend: pulseOpen(N) over UDP → barrier opens
→ (on release) device pushes …/input/<N>/off
- Push, not poll. The device's
input_link_urlfeature is configured (by the driver'sconfigureInputPush()) to call the backend on each input edge — see dingtian-relay. The driver's poll path remains only as a dev/fallback aid. - Per-input path carries the input number in the URL (
…/input/3/on), so routing needs no body parsing. Both edges (on/off) are sent. - Internal event bus (
device-events.ts, a NodeEventEmitter) decouples the HTTP/transport layer from business logic — drivers/pushes emit; the entry flow subscribes. Keeps the app device-adapter-pattern.
Trust model (important — flat network, no VLAN)
The site is a flat network with no VLAN (network-isolation is not yet enforceable here), so we do not trust the device or the network. Both directions now have defence-in-depth, but neither is the real boundary:
- Relay control (host → device) — UDP, now via the Dingtian binary protocol on :60000 with a
relay_pw(the only authenticated relay option; the string protocol has none). Set on the device + stored inlane_devicesby the harden step (below). - Input push (device → host) — guarded by HTTP Digest auth + a source-IP allowlist.
- The real guarantee is the signed log: every barrier open is a host decision, recorded as a signed event BEFORE the relay fires (append-only-event-chain). An out-of-band open (which a flat network allows) has no matching signed event → a detectable anomaly. Device/network auth is just speed bumps; both are plaintext over a sniffable network.
- This sharpens under the autonomous-direction roadmap: with no operator, tamper detection via the signed log matters more than perimeter auth.
Device hardening (on assign)
The assign/Save step configures the device end-to-end (admin never touches the device web UI):
fix preconditions (disable input_link_relay) → harden → set up input push. The harden
capability (device-registry):
- Sets a random
relay_pw(1–9999) so binary relay commands need it; stores it inlane_devicesso the backend can keep commanding the relay. - Disables unused protocol channels (rs485, can, tcp×2, mqtt →
p:255), keeping only UDP1 binary (relay control) + UDP2 string (status read) — fewer open doors.
⚠️ Lesson (the hard way): do NOT enable the device's HTTP CGI session check (
session_en). On this firmware (DT-R004) it makes the config-read API drop connections (ECONNRESET), locking the backend out of the very API it depends on — it required a factory reset to recover. The harden step deliberately leavessession_enoff. The CGI config API being open is accepted as part of the flat-network reality (the signed log is the guarantee); the proper fix is network isolation, not this fragile device feature.
Push authentication — Digest (decided by hardware testing)
The secret must not be in the URL (sniffable, logged) and the password must not cross the wire in the clear. We empirically tested the device to pick the strongest achievable option:
| Option | Device result |
|---|---|
| HTTPS (self-signed) | ❌ device won't push to a self-signed cert |
Digest auth (auth=2) |
✅ works — full 401-nonce challenge/response |
| Basic auth | ✅ works (but password base64 on the wire) |
| URL token | rejected by design (visible in URL/logs) |
→ HTTP Digest (MD5, qop=auth). The password is never sent (only a nonce-keyed hash); nonces
are single-use (replay resistance). Per-device credentials (pushUser/pushPassword) are
generated by the backend on device assign, written to the device's input_link_url config,
and stored in lane_devices — the admin never types a URL or secret. HTTPS would be stronger but
the device can't do it here; Digest + the signed log is the practical answer on a flat network.
See apps/server/src/digest-auth.ts.
Dingtian config-write gotchas (cost a lot of debugging)
Writing the device's config API (/api/v2/config_set.cgi) has two non-obvious traps — both now
handled in the driver:
- Content-Length is mandatory. The device's embedded HTTP server does not accept chunked
request bodies. Node uses chunked encoding when
Content-Lengthis absent, so the device silently ignores the body and returns{"status":0}anyway — the write looks successful but nothing changes. Always setContent-Length. - The
passfield caps at 31 chars (longer is silently truncated → Digest mismatch). The generated push password is 24 hex chars (96 bits). - (Also: the device reboots on apply, so the driver writes then polls until the change is verified, retrying — back-to-back writes onto a rebooting device are lost.)
Status
Input push verified on hardware with Digest auth (all 4 inputs, real presses authenticated, no
failures). The entry
flow itself (signed event + ticket print + pulseOpen) is the next build — see dingtian-relay.