code wiki / _hdl_build / nx_aw_sni_router.nx
nx_aw_sni_router.nx source
↩ module page · 268 lines · 12556 B
1// nx_aw_sni_router.nx -- SOVEREIGN SNI router for .240:443 (TLS PASSTHROUGH, no termination). Peeks the
2// ClientHello's SNI, picks a backend by domain (apex nishifamily.com -> :8443 sovereign sites daemon;
3// EVERYTHING else -> local Synology nginx :443 fallback), connects, REPLAYS the ClientHello, splices the raw
4// TLS stream. Each backend completes its own TLS with the shared LE cert (SAN covers both domains), so the
5// router never holds a key. Fork-per-connection, bounded + reaped; splice via a second fork (one process per
6// direction) with socket timeouts so no connection can hang a slot. DNAT .240:443 -> NX_RT_PORT.
7// argv[1] (optional) = listen port (default 7443). license_tier: ORIGINAL (spine from nx_aw_tlsproxy)
8import "nx_syscalls.nx"
9import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
10import "nx_sni_route.nx"
11const NX_MAGIC_1000000000: i64 = 1000000000
12
13const NX_RT_PORT: i64 = 7443
14const NX_RT_MAX: i64 = 64
15const NX_RT_BUF: i64 = 16384
16
17func rt_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c>=48 { if c<=57 { v=v*10+(c-48) } } i=i+1 } return v }
18func rt_puts(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
19
20// connect to 127.0.0.1:<port>; return fd or -1
21func rt_connect(port: i64) -> i64 {
22 let bfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
23 if bfd < 0 { return 0 - 1 }
24 let a: *u8 = sys_mmap(16)
25 a[0]=2 as u8; a[1]=0 as u8
26 a[2]=((port >> 8) & 0xff) as u8; a[3]=(port & 0xff) as u8
27 a[4]=127 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=1 as u8
28 var z: i64 = 8; while z < 16 { a[z]=0 as u8; z=z+1 }
29 if nx_connect_bounded(bfd, a, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(bfd); return 0 - 1 }
30 return bfd
31}
32
33// ---- DATA-DRIVEN backend routing from property_registry.conf (R1b 2026-07-15) --------------------
34// The registry's own header calls itself "data-driven routing for nx_sni_router". Now we honour it:
35// any domain listed there (col1=domain col2=port) routes to its sovereign backend; unlisted -> sni_backend
36// (nishifamily hardcode + else->nginx). Add a domain = a registry line + reload; NEVER a router recompile.
37const NX_RT_REGISTRY: *u8 = "/volume1/homes/elderwesto/nishihost/property_registry.conf" as *u8
38const NX_RT_REG_MAX: i64 = 64
39
40func rt_field(buf: *u8, pos: i64, end: i64, out: *u8, cap: i64) -> i64 {
41 var p: i64 = pos
42 var skip: i64 = 1
43 while skip == 1 {
44 if p >= end { skip = 0 }
45 else { let c: i64 = buf[p] as i64; if c == 32 { p = p + 1 } else { if c == 9 { p = p + 1 } else { skip = 0 } } }
46 }
47 if p >= end { return 0 - 1 }
48 let c0: i64 = buf[p] as i64
49 if c0 == 10 { return 0 - 1 }
50 if c0 == 13 { return 0 - 1 }
51 if c0 == 35 { return 0 - 1 }
52 var o: i64 = 0
53 var cp: i64 = 1
54 while cp == 1 {
55 if p >= end { cp = 0 }
56 else { let c: i64 = buf[p] as i64
57 if c == 32 { cp = 0 } else { if c == 9 { cp = 0 } else { if c == 10 { cp = 0 } else { if c == 13 { cp = 0 }
58 else { if o < cap - 1 { out[o] = buf[p] as u8; o = o + 1 } p = p + 1 } } } } }
59 }
60 out[o] = 0 as u8
61 return p
62}
63
64func rt_next_line(buf: *u8, pos: i64, end: i64) -> i64 {
65 var p: i64 = pos
66 var go: i64 = 1
67 while go == 1 { if p >= end { go = 0 } else { if (buf[p] as i64) == 10 { p = p + 1; go = 0 } else { p = p + 1 } } }
68 return p
69}
70
71// case-insensitive label-aware match: host == dom OR host ends with ".dom" (whole registrable domain).
72func rt_label_match(host: *u8, hn: i64, dom: *u8, dom_n: i64) -> i64 {
73 if dom_n <= 0 { return 0 }
74 if hn < dom_n { return 0 }
75 let start: i64 = hn - dom_n
76 var i: i64 = 0
77 while i < dom_n {
78 var a: i64 = host[start + i] as i64
79 var b: i64 = dom[i] as i64
80 if a >= 65 { if a <= 90 { a = a + 32 } }
81 if b >= 65 { if b <= 90 { b = b + 32 } }
82 if a != b { return 0 }
83 i = i + 1
84 }
85 if start == 0 { return 1 }
86 if (host[start - 1] as i64) == 46 { return 1 }
87 return 0
88}
89
90// parse property_registry.conf -> parallel domain/port arrays; returns count.
91func rt_load_registry(buf: *u8, n: i64, doms: *i64, dlens: *i64, ports: *i64, max: i64) -> i64 {
92 var count: i64 = 0
93 var pos: i64 = 0
94 while pos < n {
95 if count >= max { pos = n }
96 else {
97 let db: *u8 = sys_mmap(256)
98 let p1: i64 = rt_field(buf, pos, n, db, 256)
99 if p1 < 0 { pos = rt_next_line(buf, pos, n) }
100 else {
101 let pb: *u8 = sys_mmap(32)
102 let p2: i64 = rt_field(buf, p1, n, pb, 32)
103 if p2 < 0 { pos = rt_next_line(buf, pos, n) }
104 else {
105 var dl: i64 = 0
106 while db[dl] != (0 as u8) { dl = dl + 1 }
107 doms[count] = db as i64; dlens[count] = dl; ports[count] = rt_atoi(pb)
108 count = count + 1; pos = rt_next_line(buf, pos, n)
109 }
110 }
111 }
112 }
113 return count
114}
115
116// look up the SNI host in the registry table; return its backend port, or -1 if unlisted.
117func rt_lookup(host: *u8, hn: i64, doms: *i64, dlens: *i64, ports: *i64, count: i64) -> i64 {
118 if hn <= 0 { return 0 - 1 }
119 var i: i64 = 0
120 while i < count {
121 if rt_label_match(host, hn, doms[i] as *u8, dlens[i]) == 1 { return ports[i] }
122 i = i + 1
123 }
124 return 0 - 1
125}
126
127// (mtime,size) signature for HOT-RELOAD change detection (stat mtime byte 88 = i64[11], size byte 48
128// = i64[6]; same convention as nx_torrent_up). 0 ok / -1 stat-fail.
129func rt_statsig(path: *u8, statb: *u8, out2: *i64) -> i64 {
130 if sys_fstatat(path, statb) != 0 { out2[0] = 0 - 1; out2[1] = 0 - 1; return 0 - 1 }
131 let sb: *i64 = statb as *i64
132 out2[0] = sb[11]
133 out2[1] = sb[6]
134 return 0
135}
136
137func main(argc: i64, argv: *i64) -> i64 {
138 var port: i64 = NX_RT_PORT
139 if argc > 1 { port = rt_atoi(argv[1] as *u8) }
140
141 let lfd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
142 if lfd < 0 { rt_puts("RT-SOCKET-FAIL\n" as *u8); return 2 }
143 let one: *u8 = sys_mmap(4); one[0]=1 as u8; one[1]=0 as u8; one[2]=0 as u8; one[3]=0 as u8
144 sys_setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, one, 4)
145 let a: *u8 = sys_mmap(16)
146 a[0]=2 as u8; a[1]=0 as u8
147 a[2]=((port >> 8) & 0xff) as u8; a[3]=(port & 0xff) as u8
148 a[4]=0 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=0 as u8
149 var z: i64 = 8; while z < 16 { a[z]=0 as u8; z=z+1 }
150 if sys_bind(lfd, a, 16) < 0 { rt_puts("RT-BIND-FAIL\n" as *u8); return 3 }
151 if sys_listen(lfd, 64) < 0 { rt_puts("RT-LISTEN-FAIL\n" as *u8); return 4 }
152 // ZOMBIE-REAP: accept-timeout so the wait4(WNOHANG) at the loop top reaps idle splice/conn children
153 // promptly (every 5s) instead of only when the next connection lands. cfd<0 already -> continue.
154 sys_set_socket_timeout(lfd, 5)
155 // DATA-DRIVEN: load property_registry (domain->backend). Unlisted hosts fall to sni_backend/nginx.
156 let reg_box: *i64 = sys_mmap(8) as *i64; reg_box[0] = 0
157 let reg_buf: *u8 = sys_read_file(NX_RT_REGISTRY, reg_box)
158 let reg_doms: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
159 let reg_dlens: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
160 let reg_ports: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
161 var reg_n: i64 = 0
162 if (reg_buf as i64) != 0 { reg_n = rt_load_registry(reg_buf, reg_box[0], reg_doms, reg_dlens, reg_ports, NX_RT_REG_MAX) }
163 // HOT-RELOAD state: re-read the registry on (mtime,size) change, checked per accepted connection.
164 // Loads into TEMP arrays; copies over only on a non-empty parse (a broken file never wipes routing).
165 let hr_statb: *u8 = sys_mmap(160)
166 let hr_sig: *i64 = (sys_mmap(16)) as *i64
167 var reg_mt: i64 = 0 - 1
168 var reg_sz: i64 = 0 - 1
169 if rt_statsig(NX_RT_REGISTRY, hr_statb, hr_sig) == 0 { reg_mt = hr_sig[0]; reg_sz = hr_sig[1] }
170 let tmp_doms: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
171 let tmp_dlens: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
172 let tmp_ports: *i64 = (sys_mmap(NX_RT_REG_MAX * 8)) as *i64
173 rt_puts("nx_aw_sni_router: :443 passthrough up -> DATA-DRIVEN (property_registry) sovereign backends / else->nginx:443\n" as *u8)
174
175 let reap: *i64 = sys_mmap(8) as *i64
176 let sa: *u8 = sys_mmap(64)
177 let sl: *i64 = sys_mmap(8) as *i64
178 var live: i64 = 0
179 var served: i64 = 0
180 while served < NX_MAGIC_1000000000 {
181 while sys_wait4(0 - 1, reap, 1) > 0 { live = live - 1 }
182 sl[0] = 16
183 let cfd: i64 = sys_accept_with_addr(lfd, sa, sl)
184 if cfd < 0 { continue }
185 // hot-reload check (parent, pre-fork): this very connection routes with fresh registry rows.
186 if rt_statsig(NX_RT_REGISTRY, hr_statb, hr_sig) == 0 {
187 if hr_sig[0] != reg_mt || hr_sig[1] != reg_sz {
188 reg_mt = hr_sig[0]
189 reg_sz = hr_sig[1]
190 let rb2: *i64 = (sys_mmap(8)) as *i64
191 rb2[0] = 0
192 let rbuf2: *u8 = sys_read_file(NX_RT_REGISTRY, rb2)
193 if (rbuf2 as i64) != 0 {
194 let rn2: i64 = rt_load_registry(rbuf2, rb2[0], tmp_doms, tmp_dlens, tmp_ports, NX_RT_REG_MAX)
195 if rn2 > 0 {
196 var ri2: i64 = 0
197 while ri2 < rn2 {
198 reg_doms[ri2] = tmp_doms[ri2]
199 reg_dlens[ri2] = tmp_dlens[ri2]
200 reg_ports[ri2] = tmp_ports[ri2]
201 ri2 = ri2 + 1
202 }
203 reg_n = rn2
204 rt_puts("hot-reload: registry\n" as *u8)
205 }
206 }
207 }
208 }
209 if live >= NX_RT_MAX { if sys_wait4(0 - 1, reap, 0) > 0 { live = live - 1 } }
210 let pid: i64 = sys_fork()
211 if pid == 0 {
212 sys_close(lfd)
213 sys_set_socket_timeout(cfd, 20)
214 let hbuf: *u8 = sys_mmap(NX_RT_BUF)
215 // read until the FULL ClientHello TLS record is buffered (large/fragmented ClientHellos span
216 // TCP segments; a single read can miss the SNI -> wrong backend). Bounded by NX_RT_BUF, 64
217 // iterations, and the 20s socket timeout. We then replay all hn bytes + splice the rest.
218 var hn: i64 = 0
219 var rloop: i64 = 0
220 while rloop < 64 {
221 if hn >= 5 {
222 let rl: i64 = ((hbuf[3] as i64) << 8) | (hbuf[4] as i64) // TLS record length (bytes 3-4)
223 if hn >= 5 + rl { break } // full ClientHello buffered
224 }
225 if hn >= NX_RT_BUF { break }
226 let rr: i64 = sys_read(cfd, (hbuf as i64 + hn) as *u8, NX_RT_BUF - hn)
227 if rr <= 0 { break }
228 hn = hn + rr
229 rloop = rloop + 1
230 }
231 if hn > 0 {
232 let host: *u8 = sys_mmap(256)
233 let hl: i64 = sni_extract(hbuf, hn, host, 255)
234 var bport: i64 = rt_lookup(host, hl, reg_doms, reg_dlens, reg_ports, reg_n)
235 if bport < 0 { bport = sni_backend(host, hl) }
236 let bfd: i64 = rt_connect(bport)
237 if bfd >= 0 {
238 sys_set_socket_timeout(bfd, 20)
239 sys_write(bfd, hbuf, hn) // replay the ClientHello (incl any 0-RTT)
240 let g: i64 = sys_fork()
241 if g == 0 {
242 // client -> backend
243 let b2: *u8 = sys_mmap(NX_RT_BUF)
244 var run: i64 = 1
245 while run == 1 { let r: i64 = sys_read(cfd, b2, NX_RT_BUF); if r <= 0 { run = 0 } else { sys_write(bfd, b2, r) } }
246 sys_shutdown(bfd, 1)
247 sys_exit(0)
248 }
249 // backend -> client
250 let b1: *u8 = sys_mmap(NX_RT_BUF)
251 var run2: i64 = 1
252 while run2 == 1 { let r: i64 = sys_read(bfd, b1, NX_RT_BUF); if r <= 0 { run2 = 0 } else { sys_write(cfd, b1, r) } }
253 sys_shutdown(cfd, 1)
254 let gst: *i64 = sys_mmap(8) as *i64
255 sys_wait4(g, gst, 0)
256 sys_close(bfd)
257 }
258 }
259 sys_close(cfd)
260 sys_exit(0)
261 }
262 sys_close(cfd)
263 if pid > 0 { live = live + 1 }
264 served = served + 1
265 }
266 sys_close(lfd)
267 return 0
268}