code wiki / (root) / nx_external_path_audit.nx

nx_external_path_audit.nx source

↩ module page · 188 lines · 6792 B

1// nx_external_path_audit.nx -- NR-1.5 of NISHI_ROUTER_CONTROL_ROADMAP. 2// 3// Bits-up health check for the public WAN path. Runs on the NAS (or 4// any host with internet egress); periodically tries to TCP-connect 5// from this host's OUTBOUND interface to its OWN public IP on :443. 6// Logs OK / TIMEOUT / REFUSED per check + writes a verdict file the 7// operator (or other Nishi substrate) can grep. 8// 9// Today's outage (2026-05-21) revealed the operator had ZERO bits-up 10// signal that the router was dropping inbound :443. Friend reports 11// via Waterfox console blowups are NOT a substrate signal. This is. 12// 13// V1 simplicity: 14// - Hardcoded public IP (75.28.18.94) + port (443). NR-1.5b adds DNS- 15// resolved lookup so it auto-tracks WAN IP changes. 16// - Loops forever (5 sec interval). No daemon-level scheduling -- just 17// run under nx_supervisor_run. 18// - Writes one line per probe to /tmp/nx_external_path.log (path 19// configurable via env in V2). 20// 21// V2 sub-stones: 22// - NR-1.5b: resolve target via our sovereign DNS, not hardcoded IP 23// - NR-1.5c: emit signals through D5 nx_metrics (instead of /tmp log) 24// - NR-1.5d: per-port audit (probe :443 AND :80 AND :8443 etc) 25// - NR-1.5e: tail-and-alert mode (push alert when verdict changes) 26 27import "nx_syscalls.nx" 28 29// IP packed as 32-bit network-order (big-endian) for sockaddr_in. 30// Constants for 75.28.18.94 = 0x4B121E5E -> network-order = 0x5E1E124B 31// We'll compute via direct byte assembly to avoid endian errors. 32 33const NX_EPA_TARGET_IP_A: i64 = 75 34const NX_EPA_TARGET_IP_B: i64 = 28 35const NX_EPA_TARGET_IP_C: i64 = 18 36const NX_EPA_TARGET_IP_D: i64 = 94 37const NX_EPA_TARGET_PORT: i64 = 443 38 39const NX_EPA_INTERVAL_SEC: i64 = 5 40const NX_EPA_CONNECT_TIMEOUT_SEC: i64 = 3 41 42const NX_EPA_VERDICT_OK: i64 = 0 43const NX_EPA_VERDICT_TIMEOUT: i64 = 1 44const NX_EPA_VERDICT_REFUSED: i64 = 2 45const NX_EPA_VERDICT_ERROR: i64 = 3 46 47const AF_INET_CONST: i64 = 2 48const SOCK_STREAM_CONST: i64 = 1 49 50// ===== Helpers ====================================================== 51 52func _epa_strlen(s: *u8) -> i64 { 53 var n: i64 = 0 54 while s[n] != 0 { n = n + 1 } 55 return n 56} 57 58func _epa_w(fd: i64, s: *u8) -> i64 { 59 return sys_write(fd, s, _epa_strlen(s)) 60} 61 62// Write decimal i64. 63func _epa_wi(fd: i64, n: i64) -> i64 { 64 let buf: *u8 = sys_mmap(24) 65 var x: i64 = n 66 var neg: i64 = 0 67 if x < 0 { neg = 1; x = 0 - x } 68 let digits: *u8 = sys_mmap(24) 69 var nd: i64 = 0 70 if x == 0 { digits[0] = 48 as u8; nd = 1 } 71 while x > 0 { 72 digits[nd] = ((x - (x / 10) * 10) + 48) as u8 73 nd = nd + 1 74 x = x / 10 75 } 76 var off: i64 = 0 77 if neg == 1 { buf[off] = 45 as u8; off = off + 1 } 78 var i: i64 = nd 79 while i > 0 { i = i - 1; buf[off] = digits[i]; off = off + 1 } 80 return sys_write(fd, buf, off) 81} 82 83// Build a sockaddr_in for IPv4 connect. 16 bytes total: 84// bytes 0-1: AF_INET (2) little-endian = {0x02, 0x00} 85// bytes 2-3: port big-endian 86// bytes 4-7: IP big-endian 87// bytes 8-15: padding zeros 88func _epa_build_sockaddr(out: *u8, ip_a: i64, ip_b: i64, ip_c: i64, ip_d: i64, 89 port: i64) -> i64 { 90 out[0] = 2 as u8 // AF_INET low byte 91 out[1] = 0 as u8 // AF_INET high byte 92 out[2] = ((port / 256) % 256) as u8 // port high 93 out[3] = (port % 256) as u8 // port low 94 out[4] = ip_a as u8 95 out[5] = ip_b as u8 96 out[6] = ip_c as u8 97 out[7] = ip_d as u8 98 var i: i64 = 8 99 while i < 16 { out[i] = 0 as u8; i = i + 1 } 100 return 0 101} 102 103// Attempt one TCP connect probe. Returns NX_EPA_VERDICT_*. 104// We do a SYNCHRONOUS connect (no SO_TIMEOUT for V1 -- relies on the 105// kernel's TCP SYN backoff, which is ~3s on first SYN retry). For V2 106// we'd add fcntl O_NONBLOCK + poll for sub-second timeouts. 107func nx_epa_probe_once() -> i64 { 108 let fd: i64 = sys_socket(AF_INET_CONST, SOCK_STREAM_CONST, 0 as i64) 109 if fd < 0 { return NX_EPA_VERDICT_ERROR } 110 111 let addr: *u8 = sys_mmap(16) 112 _epa_build_sockaddr(addr, NX_EPA_TARGET_IP_A, NX_EPA_TARGET_IP_B, 113 NX_EPA_TARGET_IP_C, NX_EPA_TARGET_IP_D, 114 NX_EPA_TARGET_PORT) 115 let rc: i64 = sys_connect(fd, addr, 16 as i64) 116 sys_close(fd) 117 if rc == 0 { return NX_EPA_VERDICT_OK } 118 // Linux connect() failure codes: 119 // ECONNREFUSED = -111 -> REFUSED (router forwarded but no listener) 120 // ETIMEDOUT = -110 -> TIMEOUT (packet dropped) 121 // EHOSTUNREACH = -113 122 // ENETUNREACH = -101 123 // V1 maps anything non-zero to TIMEOUT-or-REFUSED bucket. 124 if rc == 0 - 111 { return NX_EPA_VERDICT_REFUSED } 125 if rc == 0 - 110 { return NX_EPA_VERDICT_TIMEOUT } 126 return NX_EPA_VERDICT_ERROR 127} 128 129func _epa_verdict_name(v: i64) -> *u8 { 130 if v == NX_EPA_VERDICT_OK { return "OK" as *u8 } 131 if v == NX_EPA_VERDICT_TIMEOUT { return "TIMEOUT" as *u8 } 132 if v == NX_EPA_VERDICT_REFUSED { return "REFUSED" as *u8 } 133 return "ERROR" as *u8 134} 135 136// Sleep `secs` via clock_nanosleep. 137func _epa_sleep(secs: i64) -> i64 { 138 let ts: *i64 = sys_mmap(16) as *i64 139 ts[0] = secs 140 ts[1] = 0 141 return __syscall(SYS_CLOCK_NANOSLEEP, 0, 0, ts, 0, 0, 0) 142} 143 144// ===== Daemon entry ================================================== 145 146func nx_external_path_audit_run() -> i64 { 147 let log_path: *u8 = "/tmp/nx_external_path.log" as *u8 148 let log_fd: i64 = sys_openat_append(log_path, 0o644 as i64) 149 if log_fd < 0 { return 1 } 150 151 _epa_w(log_fd, "nx_external_path_audit: target " as *u8) 152 _epa_wi(log_fd, NX_EPA_TARGET_IP_A); _epa_w(log_fd, "." as *u8) 153 _epa_wi(log_fd, NX_EPA_TARGET_IP_B); _epa_w(log_fd, "." as *u8) 154 _epa_wi(log_fd, NX_EPA_TARGET_IP_C); _epa_w(log_fd, "." as *u8) 155 _epa_wi(log_fd, NX_EPA_TARGET_IP_D); _epa_w(log_fd, ":" as *u8) 156 _epa_wi(log_fd, NX_EPA_TARGET_PORT) 157 _epa_w(log_fd, " interval=" as *u8) 158 _epa_wi(log_fd, NX_EPA_INTERVAL_SEC) 159 _epa_w(log_fd, "s starting\n" as *u8) 160 161 // Also banner to stdout for nx_supervisor_run visibility. 162 let banner: *u8 = "nx_external_path_audit: starting (log -> /tmp/nx_external_path.log)\n" as *u8 163 sys_write(1 as i64, banner, _epa_strlen(banner)) 164 165 var seq: i64 = 0 166 var running: i64 = 1 167 while running == 1 { 168 let v: i64 = nx_epa_probe_once() 169 let now: i64 = sys_now_ms() 170 171 _epa_w(log_fd, "seq=" as *u8) 172 _epa_wi(log_fd, seq) 173 _epa_w(log_fd, " ms=" as *u8) 174 _epa_wi(log_fd, now) 175 _epa_w(log_fd, " verdict=" as *u8) 176 _epa_w(log_fd, _epa_verdict_name(v)) 177 _epa_w(log_fd, "\n" as *u8) 178 179 seq = seq + 1 180 _epa_sleep(NX_EPA_INTERVAL_SEC) 181 } 182 sys_close(log_fd) 183 return 0 184} 185 186func main() -> i64 { 187 return nx_external_path_audit_run() 188}