code wiki / (root) / nx_iot_discover.nx

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}