nx_sni_route.nx source
↩ module page · 106 lines · 5247 B
1// nx_sni_route.nx -- SOVEREIGN TLS SNI extraction + backend routing decision (PURE: no syscalls, no imports,
2// so it is unit-gateable offline against hand-crafted ClientHellos). Used by nx_aw_sni_router (the :443
3// passthrough front) to pick a backend by domain WITHOUT terminating TLS. Defensive byte parsing of the
4// ClientHello (untrusted network input): every offset is bounds-checked; any malformation -> default backend.
5// license_tier: ORIGINAL
6
7const NX_BK_NISHI: i64 = 8443 // sites daemon (nishifamily games + andelinwest static + /search)
8const NX_BK_NGINX: i64 = 443 // Synology nginx fallback (keeps ALL other vhosts + DSM exactly as-is)
9
10func sni_be16(b: *u8, i: i64) -> i64 { return ((b[i] as i64) << 8) | (b[i + 1] as i64) }
11
12// Extract the SNI host_name from a TLS ClientHello held in b[0..n). Lowercased into out (cap outcap).
13// Returns the host length, or 0 if absent/malformed.
14func sni_extract(b: *u8, n: i64, out: *u8, outcap: i64) -> i64 {
15 if n < 44 { return 0 }
16 if (b[0] as i64) != 22 { return 0 } // TLS record type 0x16 = handshake
17 if (b[5] as i64) != 1 { return 0 } // handshake type 0x01 = ClientHello
18 // fixed prefix: 5 (record hdr) + 4 (hs hdr) + 2 (client_version) + 32 (random) = 43; b[43] = session_id_len
19 var pos: i64 = 43
20 let sid: i64 = b[pos] as i64; pos = pos + 1 + sid
21 if pos + 2 > n { return 0 }
22 let cs: i64 = sni_be16(b, pos); pos = pos + 2 + cs // cipher_suites
23 if pos + 1 > n { return 0 }
24 let cp: i64 = b[pos] as i64; pos = pos + 1 + cp // compression_methods
25 if pos + 2 > n { return 0 }
26 let extlen: i64 = sni_be16(b, pos); pos = pos + 2
27 var ee: i64 = pos + extlen
28 if ee > n { ee = n }
29 while pos + 4 <= ee {
30 let et: i64 = sni_be16(b, pos)
31 let el: i64 = sni_be16(b, pos + 2)
32 let ed: i64 = pos + 4
33 if et == 0 { // server_name extension
34 // ed: server_name_list_len(2), entry_type(1)=0, name_len(2), name
35 if ed + 5 <= n {
36 let nl: i64 = sni_be16(b, ed + 3)
37 if ed + 5 + nl <= n {
38 var i: i64 = 0
39 while i < nl {
40 if i < outcap {
41 var c: i64 = b[ed + 5 + i] as i64
42 if c >= 65 { if c <= 90 { c = c + 32 } } // lowercase A-Z
43 out[i] = c as u8
44 }
45 i = i + 1
46 }
47 return nl
48 }
49 }
50 return 0
51 }
52 pos = ed + el
53 }
54 return 0
55}
56
57func sni_contains(h: *u8, hn: i64, ndl: *u8, nn: i64) -> i64 {
58 if nn == 0 { return 0 }
59 var i: i64 = 0
60 while i + nn <= hn {
61 var j: i64 = 0
62 while j < nn { if h[i + j] != ndl[j] { break } j = j + 1 }
63 if j == nn { return 1 }
64 i = i + 1
65 }
66 return 0
67}
68
69// exact host match (host already lowercased by sni_extract) -- NO substring, so www.nishifamily.com
70// does NOT match nishifamily.com (their certs differ: the LE cert has no www SAN).
71func sni_eq(h: *u8, hn: i64, lit: *u8, ln: i64) -> i64 {
72 if hn != ln { return 0 }
73 var i: i64 = 0
74 while i < ln { if h[i] != lit[i] { return 0 } i = i + 1 }
75 return 1
76}
77
78// LABEL-AWARE registrable-domain match (the SAME rule nx_sni_cert_select uses): host matches `dom` iff
79// host == dom (the apex) OR host ends with ".dom" (a real sub-label). So EVERY subdomain of a domain we
80// own (admin./mail./www./anything.andelinwest.com) rides the sovereign stack -- no per-subdomain rule,
81// no whack-a-mole -- while a look-alike (evilandelinwest.com) does NOT match (the byte before the suffix
82// must be '.'). This is why the apex "worked forever" (explicit rule) but new subdomains didn't (fell to
83// nginx); routing the whole registrable domain fixes the class, not one instance. Bounds-checked.
84func sni_dom(h: *u8, hn: i64, dom: *u8, dn: i64) -> i64 {
85 if dn <= 0 { return 0 }
86 if hn < dn { return 0 }
87 let start: i64 = hn - dn
88 var i: i64 = 0
89 while i < dn { if h[start + i] != dom[i] { return 0 } i = i + 1 }
90 if start == 0 { return 1 } // exact apex
91 if (h[start - 1] as i64) == 46 { return 1 } // 46 == '.' -> a real sub-label
92 return 0
93}
94
95// SOVEREIGN-EDGE routing.
96// andelinwest.com -> WHOLE registrable domain (apex + ALL subdomains) rides the sovereign sites
97// daemon (:8443), which SNI-selects the *.andelinwest.com wildcard LE cert (Route B) and path-routes
98// via proxy_routes.conf. Fixes the CLASS (admin./mail./future subdomains) not one instance.
99// nishifamily.com -> APEX ONLY. Its subdomains (chat./git./matrix./mfg./www.) are OTHER services the
100// DSM reverse proxy carries to 192.168.1.73 -- suffix-matching them would break those, so keep exact.
101// Everything else + any no-SNI/fragmented ClientHello -> local Synology nginx (:443) for DSM's own stuff.
102func sni_backend(h: *u8, hn: i64) -> i64 {
103 if sni_dom(h, hn, "andelinwest.com" as *u8, 15) == 1 { return NX_BK_NISHI }
104 if sni_eq(h, hn, "nishifamily.com" as *u8, 15) == 1 { return NX_BK_NISHI }
105 return NX_BK_NGINX
106}