nx_netscope_verdict.nx source
↩ module page · 229 lines · 10321 B
1// nx_netscope_verdict.nx -- NX-NETSCOPE "easy-for-anyone" verdict layer.
2//
3// The operator's #1 directive: move tooling from esoteric/engineer-only to
4// understandable by ANYONE. Research finding (dog/doggo/q/trippy/mtr +
5// Wireshark Expert-Info + clig.dev + APNIC + NO_COLOR): every modern tool
6// stops at PRETTIER RAW DATA; none ships a pre-interpreted plain-language
7// VERDICT with the FIX attached for the home/dual-homed failure classes.
8// NX-NETSCOPE's L1 detectors already COMPUTE those verdicts -- this layer
9// is the human-readable, fix-attached shell around them (the real exceed).
10//
11// Design (grounded): Wireshark severity ladder OK/NOTE/WARN/ERROR with the
12// worst child propagated to a one-line headline; named problem CATEGORY;
13// every protocol fact translated to plain language (raw token in parens);
14// every non-OK finding ships a copy-pasteable FIX; symbol+WORD+color so
15// color is never load-bearing (ASCII canonical: [OK]/[!!]/[XX]); exit code
16// 0 all-clear / 2 problem-found (clig.dev, machine-gateable).
17//
18// Reuses nx_dns_host_would_poison / nx_dns_host_hairpin_risk and the
19// ProbeCell fields from nx_netscope_dns.nx -- presentation only, no probing.
20//
21// license_tier: ORIGINAL
22
23import "nx_netscope_dns.nx"
24
25// ---- severity ladder (Wireshark model) ----
26const NXV_OK: i64 = 0
27const NXV_NOTE: i64 = 1
28const NXV_WARN: i64 = 2
29const NXV_ERROR: i64 = 3
30
31// ---- named problem categories ----
32const NXV_CAT_OK: i64 = 0
33const NXV_CAT_SPLIT_DNS: i64 = 1 // resolvers disagree (the race) -> ERROR
34const NXV_CAT_RESOLVE_FAIL: i64 = 2 // no resolver has a usable answer -> ERROR
35const NXV_CAT_HAIRPIN: i64 = 3 // owned name resolves to a PUBLIC ip -> WARN
36const NXV_CAT_TIMEOUT: i64 = 4 // works, but a resolver is slow/silent -> WARN
37
38func nxv_w(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 }
39
40func nxv_dec(n: i64) -> i64 {
41 if n == 0 { sys_write(1, "0" as *u8, 1); return 0 }
42 var m: i64 = n
43 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
44 let d: *u8 = sys_mmap(24)
45 var k: i64 = 0
46 while m > 0 { d[k] = (0x30 + (m % 10)) as u8; m = m / 10; k = k + 1 }
47 var i: i64 = k - 1
48 while i >= 0 { let one: *u8 = sys_mmap(1); one[0] = d[i]; sys_write(1, one, 1); i = i - 1 }
49 return 0
50}
51
52func nxv_ip(p: i64) -> i64 {
53 nxv_dec((p >> 24) & 0xff); nxv_w("." as *u8, 1)
54 nxv_dec((p >> 16) & 0xff); nxv_w("." as *u8, 1)
55 nxv_dec((p >> 8) & 0xff); nxv_w("." as *u8, 1)
56 nxv_dec(p & 0xff)
57 return 0
58}
59
60// cell accessor by (server, host) -> *ProbeCell
61func nxv_cell(cells: *ProbeCell, n_hosts: i64, s: i64, h: i64) -> *ProbeCell {
62 let base: i64 = cells as i64
63 return (base + (s * n_hosts + h) * NXNS_PROBECELL_BYTES) as *ProbeCell
64}
65
66// ---- count helpers over a host column ----
67// NOTE: field access on a CALL RESULT (`nxv_cell(...).verdict`) does NOT
68// read the field under the native compiler -- it yields the pointer. Bind
69// to a temp first (the idiom used everywhere else in the tree).
70func nxv_count_ok(cells: *ProbeCell, ns: i64, nh: i64, col: i64) -> i64 {
71 var c: i64 = 0
72 var s: i64 = 0
73 while s < ns {
74 let cell: *ProbeCell = nxv_cell(cells, nh, s, col)
75 if cell.verdict == NXNS_OK_A { c = c + 1 }
76 s = s + 1
77 }
78 return c
79}
80func nxv_count_timeout(cells: *ProbeCell, ns: i64, nh: i64, col: i64) -> i64 {
81 var c: i64 = 0
82 var s: i64 = 0
83 while s < ns {
84 let cell: *ProbeCell = nxv_cell(cells, nh, s, col)
85 if cell.verdict == NXNS_TIMEOUT { c = c + 1 }
86 s = s + 1
87 }
88 return c
89}
90
91// ---- the verdict logic: category + severity for a host ----
92func nxv_host_category(cells: *ProbeCell, ns: i64, nh: i64, col: i64) -> i64 {
93 if nx_dns_host_would_poison(cells, ns, nh, col) == 1 { return NXV_CAT_SPLIT_DNS }
94 if nxv_count_ok(cells, ns, nh, col) == 0 { return NXV_CAT_RESOLVE_FAIL }
95 if nx_dns_host_hairpin_risk(cells, ns, nh, col) == 1 { return NXV_CAT_HAIRPIN }
96 // NOTE: a lone resolver TIMEOUT alongside a good answer is already a
97 // SPLIT_DNS race (would_poison counts timeout as a race contributor in
98 // the dual-homed setting), so it is reported there -- a standalone
99 // TIMEOUT category is unreachable when any resolver answers. Kept as a
100 // const + render branch for the future "all-agree-but-slow" WARN tier.
101 return NXV_CAT_OK
102}
103func nxv_cat_severity(cat: i64) -> i64 {
104 if cat == NXV_CAT_SPLIT_DNS { return NXV_ERROR }
105 if cat == NXV_CAT_RESOLVE_FAIL { return NXV_ERROR }
106 if cat == NXV_CAT_HAIRPIN { return NXV_WARN }
107 if cat == NXV_CAT_TIMEOUT { return NXV_WARN }
108 return NXV_OK
109}
110
111// ---- plain-language renderers (ASCII canonical: color is never load-bearing) ----
112func nxv_sym(sev: i64) -> i64 {
113 if sev == NXV_ERROR { nxv_w("[XX]" as *u8, 4); return 0 }
114 if sev == NXV_WARN { nxv_w("[!!]" as *u8, 4); return 0 }
115 nxv_w("[OK]" as *u8, 4)
116 return 0
117}
118func nxv_cat_name(cat: i64) -> i64 {
119 if cat == NXV_CAT_SPLIT_DNS { nxv_w("SPLIT-DNS RACE" as *u8, 14); return 0 }
120 if cat == NXV_CAT_RESOLVE_FAIL { nxv_w("CANNOT RESOLVE" as *u8, 14); return 0 }
121 if cat == NXV_CAT_HAIRPIN { nxv_w("PUBLIC-IP HAIRPIN" as *u8, 17); return 0 }
122 if cat == NXV_CAT_TIMEOUT { nxv_w("RESOLVER TIMEOUT" as *u8, 16); return 0 }
123 nxv_w("OK" as *u8, 2)
124 return 0
125}
126// translate a cell verdict to plain language (raw token in parens for experts).
127func nxv_answer(c: *ProbeCell) -> i64 {
128 let v: i64 = c.verdict
129 if v == NXNS_OK_A {
130 nxv_ip(c.ipv4_packed)
131 if c.is_rfc1918 == 1 { nxv_w(" (on your LAN)" as *u8, 15) } else { nxv_w(" (public)" as *u8, 10) }
132 return 0
133 }
134 if v == NXNS_NXDOMAIN { nxv_w("does not exist (NXDOMAIN)" as *u8, 26); return 0 }
135 if v == NXNS_SERVFAIL { nxv_w("server failed (SERVFAIL)" as *u8, 26); return 0 }
136 if v == NXNS_REFUSED { nxv_w("refused query (REFUSED)" as *u8, 25); return 0 }
137 if v == NXNS_TIMEOUT { nxv_w("no reply (timeout)" as *u8, 25); return 0 }
138 nxv_w("unexpected reply" as *u8, 16)
139 return 0
140}
141func nxv_cell_mark(c: *ProbeCell) -> i64 {
142 if c.verdict == NXNS_OK_A { if c.is_rfc1918 == 1 { nxv_w("[OK] correct" as *u8, 12); return 0 } nxv_w("[!!] public path" as *u8, 16); return 0 }
143 nxv_w("[XX] no good answer" as *u8, 19)
144 return 0
145}
146
147// ---- the FIX block per category (every problem ships a copy-pasteable fix) ----
148func nxv_fix(cat: i64) -> i64 {
149 if cat == NXV_CAT_SPLIT_DNS {
150 nxv_w("HOW TO FIX (do these in order)\n", 32)
151 nxv_w(" 1. Make your home router answer first: lower the Ethernet NIC metric\n", 70)
152 nxv_w(" below Wi-Fi, and turn off Windows parallel DNS:\n", 52)
153 nxv_w(" (DisableSmartNameResolution = 1)\n", 40)
154 nxv_w(" 2. Clear the wrong cached answer now: ipconfig /flushdns\n", 60)
155 nxv_w(" 3. On the router, point BOTH owned names at the LAN address,\n", 63)
156 nxv_w(" and forward the other box upstream to it.\n", 47)
157 return 0
158 }
159 if cat == NXV_CAT_RESOLVE_FAIL {
160 nxv_w("HOW TO FIX\n", 11)
161 nxv_w(" No reachable resolver has this name. Check the name is correct and\n", 68)
162 nxv_w(" that at least one DNS server is reachable, then re-test.\n", 58)
163 return 0
164 }
165 if cat == NXV_CAT_HAIRPIN {
166 nxv_w("HOW TO FIX\n", 11)
167 nxv_w(" This name points at its PUBLIC address, so LAN traffic must hairpin\n", 69)
168 nxv_w(" through your gateway (often slow/broken). Add a local (LAN) record\n", 68)
169 nxv_w(" on your router so it resolves to the internal address on-LAN.\n", 63)
170 return 0
171 }
172 if cat == NXV_CAT_TIMEOUT {
173 nxv_w("HOW TO FIX\n", 11)
174 nxv_w(" One resolver is slow/silent; lookups can stall. Remove the slow\n", 65)
175 nxv_w(" secondary resolver, or lower its priority, then re-test.\n", 58)
176 return 0
177 }
178 return 0
179}
180
181// nx_netscope_verdict_host: render ONE host's verdict-first report.
182// servers[] = the probed resolver IPs (for the table). Returns severity.
183func nx_netscope_verdict_host(cells: *ProbeCell, ns: i64, nh: i64, col: i64,
184 host_name: *u8, host_len: i64, servers: *i64) -> i64 {
185 let cat: i64 = nxv_host_category(cells, ns, nh, col)
186 let sev: i64 = nxv_cat_severity(cat)
187
188 nxv_w("NX-NETSCOPE . DNS DIAGNOSIS ", 30); sys_write(1, host_name, host_len); nxv_w("\n", 1)
189 nxv_w("----------------------------------------------------------------\n", 65)
190 nxv_w(" ", 2); nxv_sym(sev); nxv_w(" ", 2)
191 if sev == NXV_OK { nxv_w("ALL CLEAR -- this name resolves correctly.\n", 43) }
192 if sev == NXV_WARN { nxv_w("WORKS, BUT DEGRADED -- see below.\n", 34) }
193 if sev == NXV_ERROR { nxv_w("PROBLEM FOUND -- this name fails on your network.\n", 50) }
194 nxv_w(" Category: ", 15); nxv_cat_name(cat); nxv_w("\n\n", 2)
195
196 // resolver table (plain answers)
197 nxv_w(" Resolver Answer for this name Verdict\n", 61)
198 nxv_w(" --------------------------------------------------------------\n", 66)
199 var s: i64 = 0
200 while s < ns {
201 let c: *ProbeCell = nxv_cell(cells, nh, s, col)
202 nxv_w(" ", 2); nxv_ip(servers[s])
203 if (servers[s] >> 24 & 0xff) == 192 { nxv_w(" (local) ", 9) } else { nxv_w(" (public)", 9) }
204 nxv_w(" ", 2); nxv_answer(c); nxv_w(" ", 2); nxv_cell_mark(c); nxv_w("\n", 1)
205 s = s + 1
206 }
207 nxv_w("\n", 1)
208 if sev != NXV_OK { nxv_fix(cat) }
209 if sev == NXV_OK {
210 nxv_w(" Checks: all resolvers agree . answer on your LAN . fast reply\n", 63)
211 }
212 nxv_w("----------------------------------------------------------------\n", 65)
213 return sev
214}
215
216// nx_netscope_verdict_all: render every host, return the process exit code
217// (0 = all clear/degraded-but-works, 2 = at least one ERROR). clig.dev.
218func nx_netscope_verdict_all(cells: *ProbeCell, ns: i64, nh: i64,
219 hosts: *i64, host_lens: *i64, servers: *i64) -> i64 {
220 var worst: i64 = NXV_OK
221 var h: i64 = 0
222 while h < nh {
223 let sev: i64 = nx_netscope_verdict_host(cells, ns, nh, h, (hosts[h]) as *u8, host_lens[h], servers)
224 if sev > worst { worst = sev }
225 h = h + 1
226 }
227 if worst == NXV_ERROR { return 2 }
228 return 0
229}