code wiki / _hdl_build / nx_proxy_route.nx
nx_proxy_route.nx source
↩ module page · 363 lines · 18069 B
1// nx_proxy_route.nx -- SOVEREIGN data-driven PROXY route table (pure Nishi, no TLS, no crypto).
2// The DYNAMIC-routing analog of nx_host_router's static docroot table: a config file maps
3// (host, path-prefix) -> (backend_port, mode) so an app/site route is a CONFIG ROW, not a
4// recompiled `sd2_contains(" /gallery")` branch in the TLS daemon. This closes the nx_host_router
5// gap for DYNAMIC routes -> "migrate a bunch of sites" becomes add-a-row, zero recompile.
6// SOTA basis: reverse-proxy / API-gateway route tables reloadable without rebuild
7// (knowledge/fetched/plat_reverse_proxy.raw, plat_api_mgmt.raw); see
8// knowledge/research/2026-06-29-platform-sclass-exceed-standard.md. license_tier: ORIGINAL
9//
10// Config line: "<host|*> <path-prefix> <backend_port> <mode>" (hash = comment, blank lines ok)
11// host exact (case-insensitive) or '*' wildcard; exact host wins over '*'
12// path-prefix request-line path prefix, boundary-safe (/gen matches /gen,/gen/,/gen?x NOT /generated)
13// backend_port loopback 127.0.0.1:<port> to reverse-proxy to
14// mode buffered | stream | gated -- THE WORDS ARE THE VOCABULARY. The integers below are
15// the internal encoding handed to the daemon and are NOT accepted conf spellings.
16// Match rule: among all rows whose host+prefix match, LONGEST prefix wins; tie -> exact host beats '*'.
17//
18// FAIL-CLOSED MODE PARSE -- 2026-08-21, /compare/trafficsafety ITEM 1.
19// pr_mode used to `return PR_MODE_BUFFERED` for ANY token it did not recognise, so a mistyped
20// `gated` silently became the PERMISSIVE mode with no diagnostic, in the table that fronts every
21// request. `gated` is the FAIL-CLOSED variant (302 /login when the backend is down); `buffered`
22// is the permissive one -- so the old default was wrong in the dangerous direction by construction.
23// NOW: the three documented words resolve to themselves, an OMITTED token keeps the documented
24// default (buffered), and ANY other token resolves to PR_MODE_GATED -- fail closed.
25//
26// WHY NOT A HARD REFUSAL AT REQUEST TIME, decided from the call sites and not from taste.
27// pr_lookup has exactly one production caller (nx_sites_daemon_v2) and its 0 return does not mean
28// "refuse", it means "no route" -- the daemon then falls through to the legacy static cascade,
29// which is precisely the path that answered a machine client with the HOMEPAGE as a 200 and
30// triggered a retry storm (seq1294, both edge outages). Refusing a row at request time would
31// therefore make the failure LESS safe, not more, and would brick every route in a conf holding
32// one typo. The refusal belongs at LOAD time: pr_conf_scan enumerates every offending row with
33// its line number so the loader ANNOUNCES a worklist once per (re)load, instead of emitting one
34// wrong answer per request forever.
35import "nx_syscalls.nx"
36
37const PR_MODE_BUFFERED: i64 = 0
38const PR_MODE_STREAM: i64 = 1
39const PR_MODE_GATED: i64 = 2
40
41// ASCII bytes the row tokenizer compares against. Named because a bare 35 in a parser is a magic
42// number. PR_ASCII_SP and PR_CASE_DELTA are both 32 for UNRELATED reasons and therefore stay two
43// names -- one constant serving two purposes can never be tuned for either.
44const PR_ASCII_TAB: i64 = 9
45const PR_ASCII_LF: i64 = 10
46const PR_ASCII_CR: i64 = 13
47const PR_ASCII_SP: i64 = 32
48const PR_ASCII_HASH: i64 = 35
49const PR_ASCII_STAR: i64 = 42
50// decimal vocabulary, declared here ABOVE every reader (the parser resolves module consts in file order)
51const PR_ASCII_ZERO: i64 = 48
52const PR_DEC_BASE: i64 = 10
53
54// A row is exactly these four tokens; the fourth (mode) is optional.
55const PR_ROW_TOKS: i64 = 5 // host prefix port mode [timeout=<s>] -- the 5th is OPTIONAL (2026-09-02, search L2a)
56const PR_TOK_HOST: i64 = 0
57const PR_TOK_PREFIX: i64 = 1
58const PR_TOK_PORT: i64 = 2
59const PR_TOK_MODE: i64 = 3
60const PR_TOK_TMO: i64 = 4
61// Scratch layout handed to pr_row_toks: PR_ROW_TOKS offsets, then PR_ROW_TOKS lengths.
62const PR_TOK_OFF_BASE: i64 = 0
63const PR_TOK_LEN_BASE: i64 = 5 // == PR_ROW_TOKS: the len array sits past the off array
64const PR_I64_BYTES: i64 = 8
65const PR_LINE_ORIGIN: i64 = 1
66
67func pr_lower(c: u8) -> u8 {
68 if c >= (65 as u8) { if c <= (90 as u8) { return (c + (32 as u8)) } }
69 return c
70}
71
72// case-insensitive byte-compare of exactly n bytes
73func pr_ieq(a: *u8, b: *u8, n: i64) -> i64 {
74 var i: i64 = 0
75 while i < n { if pr_lower(a[i]) != pr_lower(b[i]) { return 0 } i = i + 1 }
76 return 1
77}
78
79func pr_strlen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
80
81// does literal `lit` equal the token at s of length n?
82func pr_word(s: *u8, n: i64, lit: *u8) -> i64 {
83 if pr_strlen(lit) != n { return 0 }
84 var i: i64 = 0
85 while i < n { if s[i] != lit[i] { return 0 } i = i + 1 }
86 return 1
87}
88
89// parse an unsigned decimal token (len n) -> i64 (non-digits ignored)
90func pr_parse_u(s: *u8, n: i64) -> i64 {
91 var v: i64 = 0
92 var i: i64 = 0
93 while i < n {
94 let c: u8 = s[i]
95 if c >= (48 as u8) { if c <= (57 as u8) { v = v * 10 + ((c as i64) - 48) } }
96 i = i + 1
97 }
98 return v
99}
100
101// Is this mode token INSIDE the documented vocabulary? An omitted token (n==0) is documented and
102// legal -- it means "take the default". Anything else is not, and the caller decides what to do
103// about it. Split out from pr_mode deliberately: a validator must be able to tell "this token was
104// RECOGNISED as buffered" from "this token FELL BACK", and one function returning only the mode
105// cannot express that difference.
106func pr_mode_known(s: *u8, n: i64) -> i64 {
107 if n == 0 { return 1 }
108 if pr_word(s, n, "stream" as *u8) == 1 { return 1 }
109 if pr_word(s, n, "gated" as *u8) == 1 { return 1 }
110 if pr_word(s, n, "buffered" as *u8) == 1 { return 1 }
111 return 0
112}
113
114// FAIL CLOSED. An unrecognised token resolves to the fail-closed mode, never the permissive one.
115// PER-ROUTE BACKEND TIMEOUT (2026-09-02, search plan L2a): an optional 5th token `timeout=<seconds>`. Absent = 0 =
116// the caller's default. Any other 5th token is UNKNOWN: pr_tmo_known says so (the validator counts the row bad,
117// fail-closed like an unknown mode) and pr_parse_tmo answers 0 so the route keeps the default rather than a guess.
118const PR_TMO_KEY: *u8 = "timeout=" as *u8
119const PR_TMO_KEYLEN: i64 = 8
120func pr_tmo_prefixed(s: *u8, n: i64) -> i64 {
121 if n <= PR_TMO_KEYLEN { return 0 }
122 var i: i64 = 0
123 while i < PR_TMO_KEYLEN { if pr_lower(s[i]) != PR_TMO_KEY[i] { return 0 } i = i + 1 }
124 return 1
125}
126func pr_tmo_known(s: *u8, n: i64) -> i64 {
127 if n == 0 { return 1 }
128 if pr_tmo_prefixed(s, n) == 0 { return 0 }
129 var i: i64 = PR_TMO_KEYLEN
130 while i < n { let c: i64 = s[i] as i64; if c < PR_ASCII_ZERO { return 0 } if c > PR_ASCII_ZERO + PR_DEC_BASE - 1 { return 0 } i = i + 1 }
131 return 1
132}
133func pr_parse_tmo(s: *u8, n: i64) -> i64 {
134 if pr_tmo_known(s, n) == 0 { return 0 }
135 if n == 0 { return 0 }
136 return pr_parse_u(((s as i64) + PR_TMO_KEYLEN) as *u8, n - PR_TMO_KEYLEN)
137}
138func pr_mode(s: *u8, n: i64) -> i64 {
139 if pr_word(s, n, "stream" as *u8) == 1 { return PR_MODE_STREAM }
140 if pr_word(s, n, "gated" as *u8) == 1 { return PR_MODE_GATED }
141 if pr_word(s, n, "buffered" as *u8) == 1 { return PR_MODE_BUFFERED }
142 if n == 0 { return PR_MODE_BUFFERED }
143 return PR_MODE_GATED
144}
145
146// boundary-safe path-prefix test: does `path`(len pn) begin with `pref`(len prefn) at a SEGMENT
147// boundary? prefn>0 required. /gen matches /gen, /gen/, /gen?x; NOT /generated.
148func pr_path_pref(path: *u8, pn: i64, pref: *u8, prefn: i64) -> i64 {
149 if prefn == 0 { return 0 }
150 if prefn > pn { return 0 }
151 var i: i64 = 0
152 while i < prefn { if path[i] != pref[i] { return 0 } i = i + 1 }
153 if pn == prefn { return 1 }
154 let nc: u8 = path[prefn]
155 if nc == (47 as u8) { return 1 } // '/'
156 if nc == (63 as u8) { return 1 } // '?'
157 if pref[prefn - 1] == (47 as u8) { return 1 } // prefix already ended in '/'
158 return 0
159}
160
161// THE ONE ROW TOKENIZER. pr_lookup and pr_conf_scan both call it, so the matcher and the validator
162// can never disagree about what a row is -- two walkers over one grammar is the duplicate-ruler
163// defect, and the half that drifts is always the one nobody runs.
164// Returns the index at which scanning should resume. tok[] is caller-supplied scratch of
165// PR_ROW_TOKS offsets followed by PR_ROW_TOKS lengths. A comment line, a blank line or end-of-input
166// yields ALL lengths 0, so the caller's host/prefix/port presence test is unchanged from the
167// original inline walk.
168func pr_row_toks(cfg: *u8, cfgn: i64, start: i64, tok: *i64) -> i64 {
169 var t: i64 = 0
170 while t < PR_ROW_TOKS { tok[PR_TOK_OFF_BASE + t] = 0; tok[PR_TOK_LEN_BASE + t] = 0; t = t + 1 }
171 var i: i64 = start
172 // skip leading spaces/tabs
173 while i < cfgn {
174 let c: i64 = cfg[i] as i64
175 if c == PR_ASCII_SP { i = i + 1 } else { if c == PR_ASCII_TAB { i = i + 1 } else { break } }
176 }
177 // comment -> skip to EOL
178 if i < cfgn { if (cfg[i] as i64) == PR_ASCII_HASH {
179 while i < cfgn { if (cfg[i] as i64) == PR_ASCII_LF { break } i = i + 1 }
180 return i + 1
181 } }
182 // blank line
183 if i < cfgn { if (cfg[i] as i64) == PR_ASCII_LF { return i + 1 } }
184 if i < cfgn { if (cfg[i] as i64) == PR_ASCII_CR { return i + 1 } }
185 if i >= cfgn { return i }
186 t = 0
187 while t < PR_ROW_TOKS {
188 let t0: i64 = i
189 while i < cfgn {
190 let c: i64 = cfg[i] as i64
191 if c == PR_ASCII_SP { break }
192 if c == PR_ASCII_TAB { break }
193 if c == PR_ASCII_LF { break }
194 if c == PR_ASCII_CR { break }
195 i = i + 1
196 }
197 tok[PR_TOK_OFF_BASE + t] = t0
198 tok[PR_TOK_LEN_BASE + t] = i - t0
199 while i < cfgn {
200 let c2: i64 = cfg[i] as i64
201 if c2 == PR_ASCII_SP { i = i + 1 } else { if c2 == PR_ASCII_TAB { i = i + 1 } else { break } }
202 }
203 t = t + 1
204 }
205 // advance to EOL
206 while i < cfgn { if (cfg[i] as i64) == PR_ASCII_LF { break } i = i + 1 }
207 return i + 1
208}
209
210// Look up (host, path) in the config buffer. On match: sets *out_port,*out_mode and returns 1.
211// No match -> returns 0 (caller falls through to static serving). Longest-prefix wins; exact host
212// beats '*' at equal prefix length. Reads the buffer ONLY (daemon supplies it hot, per request).
213// RESOURCE ENVELOPE, stated because this runs on the front door: exactly ONE sys_mmap of
214// PR_ROW_TOKS*PR_I64_BYTES*2 bytes per call for the tokenizer scratch, freed with the request
215// child. The single production caller already performs four mmaps in the same block before
216// reaching here, so this is a bounded, named addition and not an unmeasured one.
217func pr_lookup(cfg: *u8, cfgn: i64, host: *u8, hn: i64, path: *u8, pn: i64, out_port: *i64, out_mode: *i64) -> i64 {
218 let t: *i64 = (sys_mmap(PR_I64_BYTES)) as *i64
219 return pr_lookup_tmo(cfg, cfgn, host, hn, path, pn, out_port, out_mode, t)
220}
221// the same match, plus the route's timeout (0 = none declared). pr_lookup is this with the timeout discarded.
222func pr_lookup_tmo(cfg: *u8, cfgn: i64, host: *u8, hn: i64, path: *u8, pn: i64, out_port: *i64, out_mode: *i64, out_tmo: *i64) -> i64 {
223 var i: i64 = 0
224 var best_plen: i64 = 0 - 1
225 var best_exact: i64 = 0
226 var best_port: i64 = 0
227 var best_mode: i64 = 0
228 var best_tmo: i64 = 0
229 let tok: *i64 = (sys_mmap(PR_ROW_TOKS * PR_I64_BYTES * 2)) as *i64
230 while i < cfgn {
231 i = pr_row_toks(cfg, cfgn, i, tok)
232 let h0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_HOST]
233 let h0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_HOST]
234 let p0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_PREFIX]
235 let p0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_PREFIX]
236 let n0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_PORT]
237 let n0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_PORT]
238 let m0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_MODE]
239 let m0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_MODE]
240 let t0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_TMO]
241 let t0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_TMO]
242 // require host + prefix + port present
243 if h0len > 0 { if p0len > 0 { if n0len > 0 {
244 var hostok: i64 = 0
245 var exact: i64 = 0
246 if h0len == 1 { if (cfg[h0] as i64) == PR_ASCII_STAR { hostok = 1 } }
247 if hostok == 0 { if h0len == hn { if pr_ieq(((cfg as i64 + h0) as *u8), host, hn) == 1 { hostok = 1; exact = 1 } } }
248 if hostok == 1 {
249 if pr_path_pref(path, pn, ((cfg as i64 + p0) as *u8), p0len) == 1 {
250 var better: i64 = 0
251 if p0len > best_plen { better = 1 }
252 else { if p0len == best_plen { if exact == 1 { if best_exact == 0 { better = 1 } } } }
253 if better == 1 {
254 best_plen = p0len
255 best_exact = exact
256 best_port = pr_parse_u(((cfg as i64 + n0) as *u8), n0len)
257 best_mode = pr_mode(((cfg as i64 + m0) as *u8), m0len)
258 best_tmo = pr_parse_tmo(((cfg as i64 + t0) as *u8), t0len)
259 }
260 }
261 }
262 } } }
263 }
264 if best_plen >= 0 {
265 out_port[0] = best_port
266 out_mode[0] = best_mode
267 out_tmo[0] = best_tmo
268 return 1
269 }
270 return 0
271}
272
273// LOAD-TIME VALIDATOR -- the ANNOUNCE half of the fail-closed fix, and the only place a refusal is
274// safe. Walks every row with pr_row_toks (the same tokenizer pr_lookup uses) and reports each row
275// whose mode token is outside the documented vocabulary.
276// Returns the TOTAL number of offending rows.
277// out_lines[0..cap-1] 1-based LINE NUMBERS of the offenders -- a count without a worklist is not
278// actionable, so the reason travels with the count.
279// out_listed[0] how many line numbers were actually written. listed < returned means the
280// printed list is a PREFIX OF ITS OWN COUNT; no cap is ever reached silently.
281// out_rows[0] how many ROWS were examined, so a caller can tell "zero offenders" from
282// "nothing was examined". A verdict over the empty set is not a verdict.
283func pr_conf_scan(cfg: *u8, cfgn: i64, out_lines: *i64, cap: i64, out_listed: *i64, out_rows: *i64) -> i64 {
284 var i: i64 = 0
285 var line: i64 = PR_LINE_ORIGIN
286 var bad: i64 = 0
287 var rows: i64 = 0
288 out_listed[0] = 0
289 out_rows[0] = 0
290 let tok: *i64 = (sys_mmap(PR_ROW_TOKS * PR_I64_BYTES * 2)) as *i64
291 while i < cfgn {
292 let start: i64 = i
293 i = pr_row_toks(cfg, cfgn, i, tok)
294 let h0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_HOST]
295 let p0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_PREFIX]
296 let n0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_PORT]
297 if h0len > 0 { if p0len > 0 { if n0len > 0 {
298 rows = rows + 1
299 let m0: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_MODE]
300 let m0len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_MODE]
301 var rowbad: i64 = 0
302 if pr_mode_known(((cfg as i64 + m0) as *u8), m0len) == 0 { rowbad = 1 }
303 let t4: i64 = tok[PR_TOK_OFF_BASE + PR_TOK_TMO]
304 let t4len: i64 = tok[PR_TOK_LEN_BASE + PR_TOK_TMO]
305 if pr_tmo_known(((cfg as i64 + t4) as *u8), t4len) == 0 { rowbad = 1 }
306 if rowbad == 1 {
307 bad = bad + 1
308 if out_listed[0] < cap {
309 out_lines[out_listed[0]] = line
310 out_listed[0] = out_listed[0] + 1
311 }
312 }
313 } } }
314 // line number of the NEXT row = lines consumed by the row just walked
315 var k: i64 = start
316 while k < i { if k < cfgn { if (cfg[k] as i64) == PR_ASCII_LF { line = line + 1 } } k = k + 1 }
317 }
318 out_rows[0] = rows
319 return bad
320}
321
322// THE ANNOUNCE. One call a loader makes right after reading the table, so an unrecognised mode
323// token is reported ONCE PER LOAD with its line number instead of producing one wrong answer per
324// request forever. Returns the offender count so a caller can also branch on it.
325// Deliberately here and not in the daemon: the daemon then holds a single line at each of its two
326// load sites, which is the smallest possible change to the process every request crosses.
327const PR_ANN_LINES_CAP: i64 = 16
328const PR_ANN_LINES_BYTES: i64 = 128
329const PR_ANN_BOX_BYTES: i64 = 8
330const PR_ANN_NUM_BYTES: i64 = 24
331
332func pr_ann_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
333func pr_ann_num(v: i64) -> i64 {
334 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
335 var m: i64 = v
336 let d: *u8 = sys_mmap(PR_ANN_NUM_BYTES)
337 var k: i64 = 0
338 while m > 0 { d[k] = ((PR_ASCII_ZERO + (m - (m / PR_DEC_BASE) * PR_DEC_BASE)) as u8); m = m / PR_DEC_BASE; k = k + 1 }
339 var j: i64 = k - 1
340 while j >= 0 { sys_write(1, ((d as i64)+j) as *u8, 1); j = j - 1 }
341 return 0
342}
343
344func pr_conf_announce(cfg: *u8, cfgn: i64) -> i64 {
345 let lines: *i64 = (sys_mmap(PR_ANN_LINES_BYTES)) as *i64
346 let listed: *i64 = (sys_mmap(PR_ANN_BOX_BYTES)) as *i64
347 let rows: *i64 = (sys_mmap(PR_ANN_BOX_BYTES)) as *i64
348 let bad: i64 = pr_conf_scan(cfg, cfgn, lines, PR_ANN_LINES_CAP, listed, rows)
349 pr_ann_puts("proxy_routes.conf: rows=" as *u8)
350 pr_ann_num(rows[0])
351 pr_ann_puts(" unrecognised_mode_tokens=" as *u8)
352 pr_ann_num(bad)
353 if bad > 0 {
354 pr_ann_puts(" FAILING CLOSED (gated) on lines:" as *u8)
355 var i: i64 = 0
356 while i < listed[0] { pr_ann_puts(" " as *u8); pr_ann_num(lines[i]); i = i + 1 }
357 // A list shorter than its own count must say so: a cap reached in silence becomes a
358 // measurement nobody knows is partial.
359 if listed[0] < bad { pr_ann_puts(" <== THIS LIST IS A PREFIX OF ITS OWN COUNT" as *u8) }
360 }
361 pr_ann_puts("\n" as *u8)
362 return bad
363}