Harden Dingtian: authenticated binary relay + disable unused channels
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.
This commit is contained in:
@@ -33,20 +33,39 @@ car arrives → driver presses button (input I_N, dry contact to GND)
|
||||
|
||||
## Trust model (important — flat network, no VLAN)
|
||||
|
||||
The relay-control direction (host → device) is **unauthenticated UDP**, and 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. Instead:
|
||||
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:
|
||||
|
||||
- **Every barrier open is a host decision, recorded as a signed event BEFORE the relay fires**
|
||||
([[append-only-event-chain]]). If anyone opens the relay out-of-band (which the flat network
|
||||
allows), there is **no matching signed event → a detectable anomaly**. The anti-fraud guarantee
|
||||
is the **signed log**, not device/network auth.
|
||||
- The inbound push endpoint is intentionally **not behind the SPA's cookie/CSRF auth** (it's a
|
||||
machine call from the device). It is guarded by **HTTP Digest auth** + a **source-IP allowlist**
|
||||
(defence-in-depth), but these are *not* the security 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 in `lane_devices` by 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|unmanned]] 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|HardenableDevice]]):
|
||||
|
||||
- **Sets a random `relay_pw`** (1–9999) so binary relay commands need it; stores it in
|
||||
`lane_devices` so 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 leaves `session_en` off. 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
|
||||
|
||||
@@ -28,9 +28,14 @@ the ticket-first entry flow. See [[autonomous-direction]].
|
||||
Transport options: UDP/TCP string, UDP binary, HTTP CGI, Modbus, MQTT. We use **HTTP + UDP** —
|
||||
see [[dingtian-vs-mqtt]].
|
||||
|
||||
- **Relay control — UDP ASCII, port 60001:** `1`+ch = ON, `2`+ch = OFF, `T`+ch = toggle.
|
||||
Pulse/jog `11*` (default 500 ms), delay `11:30` (30 s then off), flash `11F5`. `X` = all relays.
|
||||
Intent-only pulse for a barrier = `pulseOpen` ([[barrier-not-a-door]]).
|
||||
- **Relay control — UDP *binary*, port 60000 (authenticated):** the driver's `pulseOpen` sends a
|
||||
binary "write relay with jogging" frame carrying the `relay_pw` (the only relay option with a
|
||||
password). Frame (verified on hardware):
|
||||
`FF AA <session> 03 <pwLo> <pwHi> <relayByte> <jogLo> <jogHi>` — relayByte bit0=on, bits1-7=
|
||||
channel-1; jog is 100 ms units, LSB-first; password 16-bit LSB-first (0 = none). The relay jogs
|
||||
ON then auto-releases, so we never time a close ([[barrier-not-a-door]]). *(The simpler string
|
||||
protocol — `1`+ch on, `2`+ch off, `11*` jog — works too but has no auth; we use it only for the
|
||||
read-only status query.)*
|
||||
- **Status / inputs — send `00`** → `「relays」:「inputs」:「count」`, e.g. **`0000:1111:4`** (4ch:
|
||||
relays off, inputs high). `0` = OFF/Low, `1` = ON/High. Poll-based.
|
||||
- **Input push — `input_link_url`:** device **HTTP POSTs to a host URL on input change** — the
|
||||
|
||||
+18
@@ -174,3 +174,21 @@ button. Verified in-browser against the real device: Test shows ● ready +
|
||||
preconditions OK; Save persists the row AND writes the device's Input Link URL
|
||||
(push path matches the saved device id). Admin never logs into the device web UI.
|
||||
Updated [[first-run-setup]].
|
||||
|
||||
## [2026-06-15] feature | Device hardening: binary relay + relay_pw + disable channels
|
||||
Hardened the Dingtian relay control for the flat (no-VLAN) network. Switched
|
||||
pulseOpen from the unauthenticated string protocol (:60001) to the **binary
|
||||
protocol (:60000) with a relay password** — the only authenticated relay option
|
||||
(frame verified on hardware: FF AA <sess> 03 <pwLE> <relayByte> <jogLE>). New
|
||||
HardenableDevice capability: harden() sets a random relay_pw + disables unused
|
||||
channels (rs485/can/tcp×2/mqtt → p:255, keep UDP binary+string). Folded into the
|
||||
assign/Save flow (preconditions → harden → push); relayPassword stored in
|
||||
lane_devices. Verified end to end: assign configures + hardens the device, config
|
||||
API stays reachable, pulseOpen with the stored password fires the relay, without
|
||||
it is rejected.
|
||||
|
||||
⚠️ LESSON: enabling the device's HTTP CGI session check (session_en) on this
|
||||
firmware breaks the config-READ API (ECONNRESET) — locked us out, needed a FACTORY
|
||||
RESET to recover. harden() deliberately does NOT touch session_en. The open CGI
|
||||
API is accepted as flat-network reality; the signed log is the real guarantee.
|
||||
Recorded in [[device-input-flow]] + [[dingtian-relay]].
|
||||
|
||||
Reference in New Issue
Block a user