nx_iot_discover.nx source
↩ module page · 231 lines · 11124 B
1// nx_iot_discover.nx -- universal multi-vendor device DISCOVERY brain.
2//
3// Roadmap: NISHI_IOT_HUB_ROADMAP.md Epoch 1 (DISCOVER), rung D1.
4// Operator ask (2026-06-22): "it needs a discovery mechanism like all the
5// hardware discovery" -- find EVERY device on the LAN and figure out what
6// each one is, WITHOUT knowing brands in advance.
7//
8// This is the DISCOVERY ORCHESTRATOR that ties the existing thin organs
9// into one pass: a captured datagram (raw bytes + source port + source IP
10// + optional MAC) goes IN; a classified, de-duplicated logical device in
11// the anchor registry comes OUT. It composes -- it duplicates nothing:
12// - nx_iot_local_kasa : decrypt the autokey-XOR :9999 replies + confirm
13// sysinfo (the one wire format nx_iot_announce's
14// first-byte classifier could not read)
15// - nx_iot_classify : fuse 4 signals (reply / OUI / port / SSID) -> vendor
16// - nx_iot_anchor : idempotent adopt keyed by stable hardware id ->
17// re-discovery RE-ADOPTS (no duplicate-on-repair)
18//
19// The live socket sweep (rung D2) calls nx_iot_disc_ingest once per
20// captured datagram; this brain has NO sockets so it is deterministic,
21// portable across every nxc2 backend, and hard-gateable with canned beacons.
22//
23// NEVER-BRICK (CLAUDE.md #26): discovery is READ-ONLY classification plus an
24// ADDITIVE in-memory registry adopt (soft-state, #13). It emits no device
25// write of any kind -- brick-safe by construction; there is no firmware path
26// here to prove safe because none exists.
27//
28// UNKNOWN is a FIRST-CLASS result (no silent drop): an unrecognised device
29// still anchors + gets a logical id, so the hub can active-probe / build a
30// driver for it later (operator's "build it autonomously" path).
31//
32// genealogy_id: NISHI_IOT_HUB_ROADMAP.md Epoch 1 + project-iot-hub-multi-vendor-kickoff-2026-05-16
33// license_tier: ORIGINAL
34//
35// nx_capability_claims:
36// needs: [pointer_arithmetic]
37// provides: [universal_discovery_ingest, kasa_reply_decode,
38// fused_vendor_classify, dedup_by_hardware_id,
39// unknown_is_first_class, measured_duplicates_prevented]
40// safety: [no_unchecked_deref, no_floating_point, no_syscall,
41// bounded_iteration, additive_registry_only,
42// no_firmware_write_by_construction, kind_isolated_orchestrator]
43// verdict: [sealed_enum_discover, no_silent_failure]
44// license: ORIGINAL
45// kind: iot_runtime_primitive
46// sss: [S6 (no cloud), S7 (sealed-enum verdict)]
47//
48// icl_override: kind_isolated: like nx_iot_announce + nx_iot_classify,
49// this IS the cross-vendor orchestrator at the discovery layer; it
50// reasons about every family by design.
51//
52// nx_safety_envelope:
53// intended_use: "LAN device discovery brain -- decode/classify/dedup a
54// captured beacon into one stable logical device."
55// sil_target: SIL2 (smart-home; mis-routing a command is the hazard,
56// bounded to lighting -- not life-safety)
57// evidence: [no_syscall, no_floating_point, bounded_iteration,
58// additive_only, sealed_enum_complete, dedup_KAT,
59// no_firmware_write_by_construction]
60// verdict: NOT_YET_EVALUATED
61
62import "nx_iot_classify.nx" // classify_by_* + classify_fuse; (via anchor) NX_IOT_VENDOR_*
63import "nx_iot_anchor.nx" // adopt + IotAnchorReg/IotDeviceMeta (deduped by import I1)
64import "nx_iot_local_kasa.nx" // nx_iot_kasa_decrypt + _looks_like_sysinfo + NX_IOT_KASA_PORT
65
66// ---- Sealed enum: discovery verdict --------------------------------
67
68const NX_IOT_DISC_UNKNOWN: i64 = 0
69const NX_IOT_DISC_NEW: i64 = 1 // newly discovered + anchored
70const NX_IOT_DISC_READOPTED: i64 = 2 // known device re-seen -> same logical id, no dup
71const NX_IOT_DISC_FULL: i64 = 3 // registry at capacity
72const NX_IOT_DISC_BAD_ARG: i64 = 4
73const NX_IOT_DISC_N: i64 = 5
74
75func nx_iot_disc_verdict_is_valid(v: i64) -> i64 {
76 if v < 0 { return 0 }
77 if v >= NX_IOT_DISC_N { return 0 }
78 return 1
79}
80
81// ---- Decode a captured datagram into its classifiable "plain" form --
82//
83// Kasa (:9999) replies are autokey-XOR obfuscated, so decrypt into
84// plain_out. Every other family speaks cleartext, so copy raw through.
85// Pure byte transform; writes nothing to any device. Returns plaintext
86// length, or -1 if plain_cap is too small (loud, never silent-truncate).
87
88func nx_iot_disc_to_plain(raw: *u8, n: i64, src_port: i64,
89 plain_out: *u8, plain_cap: i64) -> i64 {
90 if n <= 0 { return 0 }
91 if n > plain_cap { return -1 }
92 if src_port == NX_IOT_KASA_PORT {
93 nx_iot_kasa_decrypt(raw, n, plain_out)
94 return n
95 }
96 var i: i64 = 0
97 while i < n {
98 plain_out[i] = raw[i]
99 i = i + 1
100 }
101 return n
102}
103
104// ---- Fuse the four discovery signals into a vendor verdict ---------
105//
106// reply (strongest) > OUI > port > SSID. SSID is not available once a
107// device has joined the LAN (it is the pairing-mode signal), so it is
108// UNKNOWN here. mac_present=0 skips the OUI signal safely.
109
110func nx_iot_disc_classify(plain: *u8, plain_n: i64, src_port: i64,
111 mac: *u8, mac_present: i64) -> i64 {
112 let v_reply: i64 = nx_iot_classify_by_reply(plain, plain_n)
113 var v_oui: i64 = NX_IOT_VENDOR_UNKNOWN
114 if mac_present == 1 {
115 v_oui = nx_iot_classify_by_oui(mac)
116 }
117 let v_port: i64 = nx_iot_classify_by_port(src_port)
118 let v_ssid: i64 = NX_IOT_VENDOR_UNKNOWN
119 return nx_iot_classify_fuse(v_reply, v_oui, v_port, v_ssid)
120}
121
122// ---- Ingest (the keystone): decode -> classify -> dedup-adopt ------
123//
124// hwid is the STABLE hardware id (lowercase MAC string or vendor gwId) the
125// caller extracted for this device; it is what makes a re-discovery
126// RE-ADOPT instead of duplicate. Returns the stable logical id (>=1), or
127// -1 on bad arg / full. out_vendor := fused vendor; out_disc_verdict :=
128// NX_IOT_DISC_* (NEW / READOPTED / FULL / BAD_ARG). out_disc_verdict
129// doubles as the scratch cell for the anchor verdict (no syscall -> no
130// local heap cell), then is remapped in place.
131
132func nx_iot_disc_ingest(reg: *IotAnchorReg, metas: *IotDeviceMeta, blob: *u8,
133 raw: *u8, n: i64, src_port: i64, src_ip_be: i64,
134 mac: *u8, mac_present: i64,
135 hwid: *u8, hwid_n: i64, wall_ms: i64,
136 plain_scratch: *u8, plain_cap: i64,
137 out_vendor: *i64, out_disc_verdict: *i64) -> i64 {
138 *out_vendor = NX_IOT_VENDOR_UNKNOWN
139 *out_disc_verdict = NX_IOT_DISC_UNKNOWN
140
141 let pn: i64 = nx_iot_disc_to_plain(raw, n, src_port, plain_scratch, plain_cap)
142 if pn < 0 {
143 *out_disc_verdict = NX_IOT_DISC_BAD_ARG
144 return -1
145 }
146
147 let vendor: i64 = nx_iot_disc_classify(plain_scratch, pn, src_port, mac, mac_present)
148 *out_vendor = vendor
149
150 // out_disc_verdict carries the anchor verdict out of adopt, then is
151 // remapped to the discovery sealed enum below.
152 let logical: i64 = nx_iot_anchor_adopt(reg, metas, blob, hwid, hwid_n,
153 vendor, src_ip_be, wall_ms,
154 out_disc_verdict)
155 let avv: i64 = *out_disc_verdict
156 *out_disc_verdict = NX_IOT_DISC_UNKNOWN
157 if avv == NX_IOT_ANCHOR_NEW { *out_disc_verdict = NX_IOT_DISC_NEW }
158 if avv == NX_IOT_ANCHOR_READOPTED { *out_disc_verdict = NX_IOT_DISC_READOPTED }
159 if avv == NX_IOT_ANCHOR_FULL { *out_disc_verdict = NX_IOT_DISC_FULL }
160 if avv == NX_IOT_ANCHOR_BAD_ARG { *out_disc_verdict = NX_IOT_DISC_BAD_ARG }
161 return logical
162}
163
164// ---- Measured-exceed passthrough (the phantom-device count) --------
165//
166// duplicates_prevented = naive_adopts - distinct devices kept = the exact
167// number of duplicate rows a cloud-token controller (Smart Life / Kasa
168// app) would have accrued over the same discovery events.
169
170func nx_iot_disc_duplicates_prevented(reg: *IotAnchorReg) -> i64 {
171 return nx_iot_anchor_duplicates_prevented(reg)
172}
173
174// ---- SoftAP (pairing-mode) discovery -------------------------------
175//
176// An UNPAIRED device advertises its own OPEN Wi-Fi SoftAP whose SSID (and
177// BSSID OUI) name the vendor BEFORE it has joined the LAN -- the "I am
178// ready to pair" signal a wired NAS cannot see, captured by a Wi-Fi-
179// capable vantage and fed here. Classify by the two pairing-mode signals
180// (SSID prefix + BSSID OUI); reply/port are not available yet.
181
182func nx_iot_disc_classify_softap(ssid: *u8, ssid_n: i64,
183 bssid_mac: *u8, mac_present: i64) -> i64 {
184 let v_ssid: i64 = nx_iot_classify_by_ssid(ssid, ssid_n)
185 var v_oui: i64 = NX_IOT_VENDOR_UNKNOWN
186 if mac_present == 1 {
187 v_oui = nx_iot_classify_by_oui(bssid_mac)
188 }
189 return nx_iot_classify_fuse(NX_IOT_VENDOR_UNKNOWN, v_oui,
190 NX_IOT_VENDOR_UNKNOWN, v_ssid)
191}
192
193// Pairable = a recognised vendor AND an OPEN SoftAP (pairing mode). A
194// neighbour's WPA2/WPA3 infrastructure AP is never pairable; neither is an
195// open AP we do not recognise as an IoT device. Both default safe (0) ->
196// we never try to provision something that is not ours.
197
198func nx_iot_disc_softap_pairable(vendor: i64, auth_open: i64) -> i64 {
199 if auth_open != 1 { return 0 }
200 if vendor == NX_IOT_VENDOR_UNKNOWN { return 0 }
201 return 1
202}
203
204// Ingest a SoftAP sighting: classify; if it is a pairable IoT device,
205// anchor it on its stable hardware id (the BSSID MAC string) so a later
206// on-LAN rediscovery RE-ADOPTS the SAME logical device -- one identity for
207// pairing-mode AND on-LAN. ip_be = 0 (not on the LAN yet). Returns the
208// logical id (>=1) if anchored, 0 if seen-but-not-pairable (no silent
209// error -- out_vendor still reports what it is), -1 on bad arg / full.
210
211func nx_iot_disc_ingest_softap(reg: *IotAnchorReg, metas: *IotDeviceMeta, blob: *u8,
212 ssid: *u8, ssid_n: i64,
213 bssid_mac: *u8, mac_present: i64,
214 hwid: *u8, hwid_n: i64, auth_open: i64, wall_ms: i64,
215 out_vendor: *i64, out_disc_verdict: *i64) -> i64 {
216 let vendor: i64 = nx_iot_disc_classify_softap(ssid, ssid_n, bssid_mac, mac_present)
217 *out_vendor = vendor
218 if nx_iot_disc_softap_pairable(vendor, auth_open) != 1 {
219 *out_disc_verdict = NX_IOT_DISC_UNKNOWN
220 return 0
221 }
222 let logical: i64 = nx_iot_anchor_adopt(reg, metas, blob, hwid, hwid_n,
223 vendor, 0, wall_ms, out_disc_verdict)
224 let avv: i64 = *out_disc_verdict
225 *out_disc_verdict = NX_IOT_DISC_UNKNOWN
226 if avv == NX_IOT_ANCHOR_NEW { *out_disc_verdict = NX_IOT_DISC_NEW }
227 if avv == NX_IOT_ANCHOR_READOPTED { *out_disc_verdict = NX_IOT_DISC_READOPTED }
228 if avv == NX_IOT_ANCHOR_FULL { *out_disc_verdict = NX_IOT_DISC_FULL }
229 if avv == NX_IOT_ANCHOR_BAD_ARG { *out_disc_verdict = NX_IOT_DISC_BAD_ARG }
230 return logical
231}