code wiki / _hdl_build / nx_iot_ctl_gate.nx
nx_iot_ctl_gate.nx source
↩ module page · 316 lines · 14360 B
1// nx_iot_ctl_gate.nx -- gate for the sovereign home-electronics organ.
2//
3// Every tooth is designed to FAIL if the claim is false, not to co-sign it:
4//
5// T1 manifest defaults load, and ORDER IS PRECEDENCE (kasa:9999 outranks
6// http:80) -- the exact inversion that makes the live survey call a
7// smart plug a "web-device"
8// T2 conf parsing is real: a conf image REPLACES the defaults, with the
9// parsed values readable back (not merely "a file was opened")
10// T3 fail-safe: a MALFORMED / absent conf degrades to defaults, never to
11// an empty manifest (an organ that silently sees nothing is worse than
12// one that sees the default set)
13// T4 the UDP blind spot is DECLARED: wiz/38899 is present in the manifest,
14// marked udp, and excluded from the swept set
15// T5 NEVER-BRICK: no write verb exists; on/off/set/flash/provision/gcode
16// all resolve to BAD, and the verb map has exactly 4 read-only entries
17// T6 REAL loopback sweep: a live listener on 127.0.0.1 is found through the
18// SHARED lscan_sweep primitive (the same bytes nx_printer_ctl runs)
19// T7 REAL end-to-end classification: a fork'd fake Kasa device on loopback
20// is swept, and the emitted JSON says smart-plug + kasa -- NOT web-device
21// T8 REAL Kasa probe roundtrip: framed+autokey-encrypted get_sysinfo over
22// the kernel TCP stack, reply decrypted, vendor decided by REPLY signal
23// T9 NEGATIVE CONTROL for T7/T8: an HTTP-only listener must NOT be called a
24// kasa plug (a classifier that says "kasa" to everything passes T7)
25//
26// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
27import "nx_syscalls.nx"
28import "nx_tls13_io.nx"
29import "nx_lan_scan.nx"
30import "nx_iot_ctl_lib.nx"
31import "nx_gate_verdict.nx"
32
33func ig_contains(buf: *u8, len: i64, needle: *u8) -> i64 {
34 var nlen: i64 = 0
35 while needle[nlen] != (0 as u8) { nlen = nlen + 1 }
36 if nlen == 0 { return 1 }
37 if len < nlen { return 0 }
38 var i: i64 = 0
39 while i <= len - nlen {
40 var j: i64 = 0
41 var matched: i64 = 1
42 while j < nlen {
43 if buf[i + j] != needle[j] { matched = 0; j = nlen }
44 j = j + 1
45 }
46 if matched == 1 { return 1 }
47 i = i + 1
48 }
49 return 0
50}
51
52// A fake Kasa device: accept one connection, read the framed request, reply
53// with a framed + autokey-encrypted sysinfo body (real bytes, real cipher).
54func ig_fake_kasa(listen_fd: i64) -> i64 {
55 let cfd: i64 = sys_accept(listen_fd)
56 if cfd < 0 { return 0 }
57 let req: *u8 = sys_mmap(4096)
58 sys_read(cfd, req, 4096)
59 let body: *u8 = "{\"system\":{\"get_sysinfo\":{\"sw_ver\":\"1.0.6\",\"model\":\"HS210(US)\",\"relay_state\":1,\"mic_type\":\"IOT.SMARTPLUGSWITCH\"}}}"
60 var bn: i64 = 0
61 while body[bn] != (0 as u8) { bn = bn + 1 }
62 let frame: *u8 = sys_mmap(4096)
63 let vp: *i64 = sys_mmap(8) as *i64
64 vp[0] = 0
65 let fn2: i64 = nx_iot_kasa_frame_tcp(body, bn, frame, 4096, vp)
66 if fn2 > 0 { sys_write(cfd, frame, fn2) }
67 sys_close(cfd)
68 return 1
69}
70
71func main() -> i64 {
72 let ctr: *i64 = gv_ctr()
73 gv_head("nx_iot_ctl_gate -- sovereign home-electronics identification (data-driven manifest, declared blind spots, real loopback)" as *u8)
74
75 // ---- T1: defaults + manifest order IS precedence ----
76 let m1: *u8 = iotm_new()
77 iotm_defaults(m1)
78 var t1: i64 = 0
79 if iotm_count(m1) > 6 {
80 // kasa:9999 must sit at a LOWER index than http:80, else a plug that
81 // also serves a web UI gets called a web-device (the live defect).
82 var i_kasa: i64 = 0 - 1
83 var i_http: i64 = 0 - 1
84 var i: i64 = 0
85 while i < iotm_count(m1) {
86 if iotm_port(m1, i) == 9999 { if i_kasa < 0 { i_kasa = i } }
87 if iotm_port(m1, i) == 80 { if i_http < 0 { i_http = i } }
88 i = i + 1
89 }
90 if i_kasa >= 0 {
91 if i_http > i_kasa {
92 // and the resolver must actually honour that order
93 let bits: i64 = (1 << i_kasa) | (1 << i_http)
94 if iotc_mask_row(m1, bits) == i_kasa {
95 if iotc_streq(iotc_kind_name(iotm_kind(m1, i_kasa)), "smart-plug" as *u8) == 1 { t1 = 1 }
96 }
97 }
98 }
99 }
100 gv_check("T1 defaults load; manifest ORDER=precedence (kasa:9999 outranks http:80 -> smart-plug not web-device)" as *u8, t1, ctr)
101
102 // ---- T2: conf parsing REPLACES defaults, values read back ----
103 let m2: *u8 = iotm_new()
104 let conf: *u8 = "# comment line\n4711 tcp yeelight bulb lab-bulb\n1234 udp wiz bulb lab-wiz\n"
105 var cn: i64 = 0
106 while conf[cn] != (0 as u8) { cn = cn + 1 }
107 let got2: i64 = iotm_parse(m2, conf, cn)
108 var t2: i64 = 0
109 if got2 == 2 {
110 if iotm_count(m2) == 2 {
111 if iotm_port(m2, 0) == 4711 {
112 if iotm_proto(m2, 0) == IOTC_PROTO_TCP {
113 if iotm_vendor(m2, 0) == NX_IOT_VENDOR_YEELIGHT {
114 if iotm_kind(m2, 0) == IOTC_KIND_BULB {
115 if iotc_streq(iotm_label(m2, 0), "lab-bulb" as *u8) == 1 {
116 if iotm_port(m2, 1) == 1234 {
117 if iotm_proto(m2, 1) == IOTC_PROTO_UDP { t2 = 1 }
118 }
119 }
120 }
121 }
122 }
123 }
124 }
125 }
126 gv_check("T2 conf rows parse + read back (port/proto/vendor/kind/label); '#' comment skipped" as *u8, t2, ctr)
127
128 // ---- T3: malformed conf -> defaults, never an empty manifest ----
129 let m3: *u8 = iotm_new()
130 let junk: *u8 = "not a row at all\n### \nzzz qqq\n"
131 var jn: i64 = 0
132 while junk[jn] != (0 as u8) { jn = jn + 1 }
133 let got3: i64 = iotm_parse(m3, junk, jn)
134 var t3: i64 = 0
135 if got3 == 0 {
136 if iotm_count(m3) == 0 {
137 // this is the path iotm_load takes: no usable rows -> defaults
138 iotm_defaults(m3)
139 if iotm_count(m3) > 6 { t3 = 1 }
140 }
141 }
142 gv_check("T3 fail-safe: malformed conf yields 0 rows -> compiled defaults, never a blind empty manifest" as *u8, t3, ctr)
143
144 // ---- T4: the UDP blind spot is declared, not hidden ----
145 var t4: i64 = 0
146 var wiz_i: i64 = 0 - 1
147 var k: i64 = 0
148 while k < iotm_count(m1) {
149 if iotm_port(m1, k) == 38899 { if wiz_i < 0 { wiz_i = k } }
150 k = k + 1
151 }
152 if wiz_i >= 0 {
153 if iotm_proto(m1, wiz_i) == IOTC_PROTO_UDP {
154 if iotm_vendor(m1, wiz_i) == NX_IOT_VENDOR_WIZ {
155 // and a scan emission must name it as unswept
156 let mask4: *i64 = sys_mmap(256 * 8) as *i64
157 let out4: *u8 = sys_mmap(IOTC_OUT)
158 let n4: i64 = iotc_emit_scan(out4, m1, "10.0.0" as *u8, 0, mask4, 0)
159 if ig_contains(out4, n4, "\"udp_ports_unswept\":[38899]" as *u8) == 1 {
160 if ig_contains(out4, n4, "NOT evidence of absence" as *u8) == 1 { t4 = 1 }
161 }
162 }
163 }
164 }
165 gv_check("T4 blind spot DECLARED: wiz/38899 is udp, excluded from the sweep, and named in udp_ports_unswept" as *u8, t4, ctr)
166
167 // ---- T5: NEVER-BRICK -- no write verb is reachable ----
168 var t5: i64 = 1
169 if iotc_verb_id("on" as *u8) != IOTC_V_BAD { t5 = 0 }
170 if iotc_verb_id("off" as *u8) != IOTC_V_BAD { t5 = 0 }
171 if iotc_verb_id("set" as *u8) != IOTC_V_BAD { t5 = 0 }
172 if iotc_verb_id("flash" as *u8) != IOTC_V_BAD { t5 = 0 }
173 if iotc_verb_id("ota" as *u8) != IOTC_V_BAD { t5 = 0 }
174 if iotc_verb_id("provision" as *u8) != IOTC_V_BAD { t5 = 0 }
175 if iotc_verb_id("set_stainfo" as *u8) != IOTC_V_BAD { t5 = 0 }
176 if iotc_verb_id("gcode" as *u8) != IOTC_V_BAD { t5 = 0 }
177 if iotc_verb_id("reboot" as *u8) != IOTC_V_BAD { t5 = 0 }
178 // and the four read verbs DO resolve (a map that rejects everything also
179 // passes the negatives above -- this is the non-vacuity control)
180 if iotc_verb_id("services" as *u8) != IOTC_V_SERVICES { t5 = 0 }
181 if iotc_verb_id("scan" as *u8) != IOTC_V_SCAN { t5 = 0 }
182 if iotc_verb_id("probe" as *u8) != IOTC_V_PROBE { t5 = 0 }
183 if iotc_verb_id("contract" as *u8) != IOTC_V_CONTRACT { t5 = 0 }
184 gv_check("T5 never-brick: no write/flash/provision verb resolves; the 4 read verbs do (non-vacuous)" as *u8, t5, ctr)
185
186 // ---- T6: the SHARED sweep finds a real listener ----
187 var t6: i64 = 0
188 let lfd6: i64 = tcp_listen_loopback(19311)
189 if lfd6 >= 0 {
190 let base: i64 = lscan_parse_prefix("127.0.0" as *u8)
191 let ips: *i64 = sys_mmap(LSCAN_HOSTS * 8) as *i64
192 let cnt: i64 = lscan_sweep(base, 19311, 120, ips, LSCAN_HOSTS)
193 let want: i64 = ((127 << 24) | 1)
194 if cnt >= 1 { if ips[0] == want { t6 = 1 } }
195 sys_close(lfd6)
196 }
197 gv_check("T6 shared lscan_sweep (the one nx_printer_ctl also uses) finds a real 127.0.0.1 listener" as *u8, t6, ctr)
198
199 // ---- T7: real sweep -> emitted JSON says smart-plug + kasa ----
200 var t7: i64 = 0
201 let lfd7: i64 = tcp_listen_loopback(9999)
202 if lfd7 >= 0 {
203 let m7: *u8 = iotm_new()
204 iotm_defaults(m7)
205 let mask7: *i64 = sys_mmap(256 * 8) as *i64
206 iotc_scan(m7, lscan_parse_prefix("127.0.0" as *u8), 60, mask7)
207 let out7: *u8 = sys_mmap(IOTC_OUT)
208 let n7: i64 = iotc_emit_scan(out7, m7, "127.0.0" as *u8, lscan_parse_prefix("127.0.0" as *u8), mask7, 0)
209 if ig_contains(out7, n7, "\"ip\":\"127.0.0.1\"" as *u8) == 1 {
210 if ig_contains(out7, n7, "\"kind\":\"smart-plug\"" as *u8) == 1 {
211 if ig_contains(out7, n7, "\"vendor\":\"kasa\"" as *u8) == 1 { t7 = 1 }
212 }
213 }
214 sys_close(lfd7)
215 }
216 gv_check("T7 real sweep of a :9999 listener emits kind=smart-plug vendor=kasa (NOT web-device)" as *u8, t7, ctr)
217
218 // ---- T8: real Kasa probe roundtrip, vendor decided by REPLY ----
219 var t8: i64 = 0
220 let lfd8: i64 = tcp_listen_loopback(19399)
221 if lfd8 >= 0 {
222 let pid: i64 = sys_fork()
223 if pid == 0 {
224 // child = the production client path
225 var spin: i64 = 0
226 while spin < 80000 { spin = spin + 1 }
227 let plain: *u8 = sys_mmap(IOTC_RESP)
228 let pn: i64 = iotc_kasa_probe((127 << 24) | 1, 19399, plain, IOTC_RESP)
229 if pn > 0 {
230 if nx_iot_kasa_looks_like_sysinfo(plain, pn) == 1 {
231 if nx_iot_classify_by_reply(plain, pn) == NX_IOT_VENDOR_KASA { sys_exit(0) }
232 }
233 }
234 sys_exit(1)
235 }
236 if pid > 0 {
237 ig_fake_kasa(lfd8)
238 sys_close(lfd8)
239 let st: *i64 = sys_mmap(8) as *i64
240 st[0] = 0
241 sys_wait4(pid, st, 0)
242 if ((st[0] >> 8) & 0xff) == 0 { t8 = 1 }
243 }
244 }
245 gv_check("T8 real Kasa probe: framed+autokey get_sysinfo over loopback TCP, reply decrypted, vendor by REPLY signal" as *u8, t8, ctr)
246
247 // ---- T9: negative control -- an HTTP-only host is NOT a kasa plug ----
248 var t9: i64 = 0
249 let lfd9: i64 = tcp_listen_loopback(80)
250 if lfd9 >= 0 {
251 let m9: *u8 = iotm_new()
252 iotm_defaults(m9)
253 let mask9: *i64 = sys_mmap(256 * 8) as *i64
254 iotc_scan(m9, lscan_parse_prefix("127.0.0" as *u8), 60, mask9)
255 let out9: *u8 = sys_mmap(IOTC_OUT)
256 let n9: i64 = iotc_emit_scan(out9, m9, "127.0.0" as *u8, lscan_parse_prefix("127.0.0" as *u8), mask9, 0)
257 if ig_contains(out9, n9, "\"kind\":\"web-device\"" as *u8) == 1 {
258 if ig_contains(out9, n9, "\"vendor\":\"kasa\"" as *u8) == 0 { t9 = 1 }
259 }
260 sys_close(lfd9)
261 } else {
262 // binding :80 needs privilege; a skipped control must NOT count as a
263 // pass -- prove the classifier discriminates on a synthetic mask instead
264 let mA: *u8 = iotm_new()
265 iotm_defaults(mA)
266 var i_http: i64 = 0 - 1
267 var q: i64 = 0
268 while q < iotm_count(mA) {
269 if iotm_port(mA, q) == 80 { if i_http < 0 { i_http = q } }
270 q = q + 1
271 }
272 if i_http >= 0 {
273 let bits: i64 = 1 << i_http
274 if iotc_streq(iotc_kind_name(iotm_kind(mA, iotc_mask_row(mA, bits))), "web-device" as *u8) == 1 {
275 if iotc_mask_vendor(mA, bits) == NX_IOT_VENDOR_UNKNOWN { t9 = 1 }
276 }
277 }
278 }
279 gv_check("T9 negative control: an HTTP-only host classifies web-device/unknown, never kasa" as *u8, t9, ctr)
280
281 // ---- T10: COMPOSITE -- one real host answering on TWO manifest ports ----
282 // T7 binds a single port, so it can pass even if precedence is inverted
283 // (proven: a mutation that reordered the defaults left T7 green while T1
284 // went red). The live case is a host that answers on several ports at once,
285 // so bind BOTH a kasa port and a lower-precedence printer port on the same
286 // loopback host and require the higher-precedence row to win.
287 var t10: i64 = 0
288 let lfdA: i64 = tcp_listen_loopback(9999)
289 let lfdB: i64 = tcp_listen_loopback(10088)
290 if lfdA >= 0 {
291 if lfdB >= 0 {
292 let mB: *u8 = iotm_new()
293 iotm_defaults(mB)
294 let maskB: *i64 = sys_mmap(256 * 8) as *i64
295 let baseB: i64 = lscan_parse_prefix("127.0.0" as *u8)
296 iotc_scan(mB, baseB, 60, maskB)
297 // the host must have BOTH bits set, else this is not a composite test
298 let outB: *u8 = sys_mmap(IOTC_OUT)
299 let nB: i64 = iotc_emit_scan(outB, mB, "127.0.0" as *u8, baseB, maskB, 0)
300 if ig_contains(outB, nB, "9999" as *u8) == 1 {
301 if ig_contains(outB, nB, "10088" as *u8) == 1 {
302 if ig_contains(outB, nB, "\"kind\":\"smart-plug\"" as *u8) == 1 {
303 if ig_contains(outB, nB, "\"kind\":\"3d-printer\"" as *u8) == 0 { t10 = 1 }
304 }
305 }
306 }
307 sys_close(lfdB)
308 }
309 sys_close(lfdA)
310 }
311 gv_check("T10 composite: a host open on BOTH 9999 and 10088 resolves smart-plug (higher-precedence row wins)" as *u8, t10, ctr)
312
313 gv_verdict("nx_iot_ctl_gate" as *u8, ctr,
314 "sovereign home-electronics identification: data-driven manifest, declared UDP blind spot, never-brick read-only surface, real loopback classification" as *u8)
315 return 0
316}