code wiki / (root) / nx_http_client_gate.nx

nx_http_client_gate.nx source

↩ module page · 286 lines · 18979 B

1// nx_http_client_gate.nx -- THE REQUEST BUILDER EVERY ESTATE FETCH CROSSES HAD NO GATE AT ALL. 2// Measured 2026-09-03 with nx_catalog: nx_http_client_gate and nx_httpclient_gate both ABSENT -- 3// "no source, no binary, nothing promoted". nx_http_client.nx is imported by nx_https_get_complete, 4// which is imported by nx_https_get, which is what nx_research_fetch and every sovereign fetch ride. 5// An unguarded builder on that path is the widest ungated surface in the fetch lane. 6// 7// WHAT IT PROVES. On 2026-09-03 hc_put_hints was added so the request carries the Chrome client hints 8// and fetch-metadata that match the Chrome-JA3 ClientHello we ALREADY pay for (nx_tls13_chrome_session). 9// The measured cause: fab.com (Cloudflare) answers 403 to the UA-alone profile and 200 to the full 10// profile OVER THE SAME TLS STACK. So the risk this gate exists to hold down is NOT "did we add the 11// headers" -- it is the COHERENCE of the identity we present: 12// 13// * a Chrome UA whose version disagrees with its own sec-ch-ua version is a MISMATCH, and a mismatch 14// is the exact signal anti-bot scoring hunts. T5 compares the two NUMBERS and emits both, so the 15// bar is readable from outside instead of living in a tooth name. 16// * HC_UA_BOT must NEVER acquire Chrome hints. An honest crawler identity that ships sec-ch-ua is no 17// longer honest, and that is a promise to site operators, not a preference. T6/T7 are neg-controls. 18// * a caller that declared its OWN User-Agent must not be contradicted (the 2026-08-25 duplicate-header 19// lesson: THE CALLER WINS). T8 is the neg-control for that. 20// 21// NON-VACUITY. T1-T4 alone would pass for an implementation that emitted the header block to EVERY 22// identity -- which is precisely the honesty regression. The three neg-controls are what make the 23// forward teeth mean anything, and they are named neg-control- so nx_gatelaw_gate's L2 census can see 24// them (a control nobody can find is a control nobody maintains). 25// 26// BITE-PROVEN 2026-09-03 (laptop, admission refused the NAS build): removing the hc_put_hints call site 27// takes it 16/16 GREEN exit 0 -> 11/16 RED exit 1 with exactly the 5 forward teeth failing and all three 28// neg-controls still passing (they assert ABSENCE, which the mutant preserves -- correct discrimination, 29// not vacuity). Restore verified byte-identical, sha256 1952206f85deae97667635964936a64fcabfc5328c47b00bd365805694d4c75e. 30// 31// license_tier: ORIGINAL expect_exit: 0 32import "nx_syscalls.nx" 33import "nx_gate_verdict.nx" 34import "nx_http_client.nx" 35import "nx_https_fetch_lib.nx" 36 37// Historical failure boundary, retained only as a regression-test input. 38const HG_RETIRED_URL_SCRATCH_BYTES: i64=4096 39const HG_ABSENT: i64 = 0 - 1 40const HG_DIGIT_0: i64 = 48 41const HG_DIGIT_9: i64 = 57 42const HG_SCAN_SLACK: i64 = 16 43 44// Length-bounded substring search. nx_cc_find is NUL-TERMINATED and a built request is 45// LENGTH-DELIMITED, so this is a different contract rather than a duplicate ruler: running a 46// NUL-terminated finder over a buffer whose tail is uninitialised is how a gate reads garbage 47// and calls it a pass. 48func hg_find(buf: *u8, n: i64, pat: *u8) -> i64 { 49 var pl: i64 = 0 50 while pat[pl] != (0 as u8) { pl = pl + 1 } 51 if pl == 0 { return HG_ABSENT } 52 var i: i64 = 0 53 while i + pl <= n { 54 var m: i64 = 1 55 var j: i64 = 0 56 while j < pl { 57 if buf[i + j] != pat[j] { m = 0; j = pl } else { j = j + 1 } 58 } 59 if m == 1 { return i } 60 i = i + 1 61 } 62 return HG_ABSENT 63} 64 65func hg_has(buf: *u8, n: i64, pat: *u8) -> i64 { 66 if hg_find(buf, n, pat) >= 0 { return 1 } 67 return 0 68} 69 70func hg_count(buf: *u8, n: i64, pat: *u8) -> i64 { 71 var pl: i64 = 0 72 while pat[pl] != (0 as u8) { pl = pl + 1 } 73 if pl == 0 { return 0 } 74 var c: i64 = 0 75 var i: i64 = 0 76 while i + pl <= n { 77 var m: i64 = 1 78 var j: i64 = 0 79 while j < pl { 80 if buf[i + j] != pat[j] { m = 0; j = pl } else { j = j + 1 } 81 } 82 if m == 1 { c = c + 1; i = i + pl } else { i = i + 1 } 83 } 84 return c 85} 86 87// Find `pat`, then read the first run of decimal digits that follows it within a short slack window. 88// Returns HG_ABSENT when the pattern is missing, so "not found" can never be mistaken for a value. 89func hg_num_after(buf: *u8, n: i64, pat: *u8) -> i64 { 90 let at: i64 = hg_find(buf, n, pat) 91 if at < 0 { return HG_ABSENT } 92 var pl: i64 = 0 93 while pat[pl] != (0 as u8) { pl = pl + 1 } 94 var i: i64 = at + pl 95 var stop: i64 = i + HG_SCAN_SLACK 96 if stop > n { stop = n } 97 while i < stop { 98 let c: i64 = buf[i] as i64 99 if c >= HG_DIGIT_0 { 100 if c <= HG_DIGIT_9 { 101 var v: i64 = 0 102 while i < n { 103 let d: i64 = buf[i] as i64 104 if d < HG_DIGIT_0 { return v } 105 if d > HG_DIGIT_9 { return v } 106 v = v * 10 + (d - HG_DIGIT_0) 107 i = i + 1 108 } 109 return v 110 } 111 } 112 i = i + 1 113 } 114 return HG_ABSENT 115} 116 117 118func hg_resolve(base: *u8,ref: *u8,want: *u8) -> i64 { 119 let cap: i64=_gc_slen(base)+_gc_slen(ref)+1 120 let out: *u8=sys_mmap(cap) 121 let size: *i64=sys_mmap(__size_of(i64)) as *i64 122 let rc: i64=nx_url_resolve(base,_gc_slen(base),ref,_gc_slen(ref),out,cap,size) 123 var ok: i64=(rc==NX_URL_RESOLVE_OK && size[0]==_gc_slen(want)) as i64 124 var i: i64=0 125 while ok==1 && i<size[0] { if out[i]!=want[i] { ok=0 };i=i+1 } 126 sys_munmap(out,cap);sys_munmap(size,__size_of(i64)) 127 return ok 128} 129 130func main() -> i64 { 131 let ctr: *i64 = gv_ctr() 132 let plan: *i64=gv_plan_new("browser-emits-sec-ch-ua\nbrowser-emits-sec-ch-ua-platform\nbrowser-emits-sec-fetch-mode-navigate\nbrowser-emits-upgrade-insecure-requests\nua-version-equals-sec-ch-ua-version\nneg-control-bot-emits-no-sec-ch-ua\nneg-control-bot-emits-no-sec-fetch\nneg-control-bot-keeps-nishibot-identity\nneg-control-caller-ua-suppresses-chrome-hints\nneg-control-caller-ua-survives\nbrowser-exactly-one-user-agent\nbot-exactly-one-user-agent\nxhdr-exactly-one-user-agent\nbrowser-terminates-blank-line\nbrowser-fits-declared-cap\nbot-fits-declared-cap\nheader-range-valid\nheader-empty-name-refused\nheader-whitespace-name-refused\nheader-missing-colon-refused\nheader-injected-request-refused\nheader-host-override-refused\nheader-length-override-refused\nheader-transfer-framing-refused\nheader-block-valid\nheader-block-missing-lf-refused\nheader-block-blank-line-refused\nheader-empty-block-compatible\nheader-redirect-same-authority\nheader-redirect-other-host-refused\nheader-redirect-other-port-refused\nheader-redirect-downgrade-refused\nrange-request-exact-header-once\nempty-header-block-preserves-ordinary-request-bytes\nuri-reference-1\nuri-reference-2\nuri-reference-3\nuri-reference-4\nuri-reference-5\nuri-reference-6\nuri-reference-7\nuri-reference-8\nuri-reference-9\nuri-reference-10\nuri-reference-11\nuri-reference-12\nuri-reference-20\nuri-reference-21\nuri-reference-22\nuri-reference-23\nuri-reference-24\nuri-reference-40\nuri-reference-41\nuri-reference-42\nuri-reference-43\nbeach-staged-relative-redirect-resolved\nresolved-relative-redirect-retains-header-authority\nrelative-url-exceeds-retired-scratch-and-stack\nabsolute-url-exceeds-retired-segment-stack\n") 133 gv_head("=== nx_http_client_gate -- identity coherence on the request builder ===" as *u8) 134 135 let path: *u8 = "/" as *u8 136 let host: *u8 = "example.com" as *u8 137 let path_len: i64 = 1 138 let host_len: i64 = 11 139 let cap: i64 = nx_http_client_request_cap(path_len, host_len, 0, 0) 140 141 // ---- BROWSER IDENTITY ---- 142 let rb: *u8 = sys_mmap(cap) 143 hc_set_identity(HC_UA_BROWSER) 144 let nb: i64 = nx_http_client_build_request(path, path_len, host, host_len, rb) 145 146 gv_plan_check(plan,"browser-emits-sec-ch-ua", hg_has(rb, nb, "sec-ch-ua: " as *u8), ctr) 147 gv_plan_check(plan,"browser-emits-sec-ch-ua-platform", hg_has(rb, nb, "sec-ch-ua-platform: " as *u8), ctr) 148 gv_plan_check(plan,"browser-emits-sec-fetch-mode-navigate", hg_has(rb, nb, "Sec-Fetch-Mode: navigate" as *u8), ctr) 149 gv_plan_check(plan,"browser-emits-upgrade-insecure-requests", hg_has(rb, nb, "Upgrade-Insecure-Requests: 1" as *u8), ctr) 150 151 // T5 -- THE COHERENCE TOOTH. Both numbers are EMITTED, not just compared, so an outside reader 152 // (or a second method class) can recompute the claim instead of trusting a PASS. 153 let ua_ver: i64 = hg_num_after(rb, nb, "Chrome/" as *u8) 154 let ch_ver: i64 = hg_num_after(rb, nb, "Chromium" as *u8) 155 gv_plan_check(plan,"ua-version-equals-sec-ch-ua-version",(ua_ver==ch_ver) as i64,ctr) 156 157 // ---- NEG-CONTROL: THE HONEST CRAWLER MUST STAY HONEST ---- 158 let rn: *u8 = sys_mmap(cap) 159 hc_set_identity(HC_UA_BOT) 160 let nn: i64 = nx_http_client_build_request(path, path_len, host, host_len, rn) 161 162 gv_plan_check(plan,"neg-control-bot-emits-no-sec-ch-ua", 1 - hg_has(rn, nn, "sec-ch-ua" as *u8), ctr) 163 gv_plan_check(plan,"neg-control-bot-emits-no-sec-fetch", 1 - hg_has(rn, nn, "Sec-Fetch-" as *u8), ctr) 164 gv_plan_check(plan,"neg-control-bot-keeps-nishibot-identity", hg_has(rn, nn, "NishiBot" as *u8), ctr) 165 166 // ---- NEG-CONTROL: A CALLER THAT DECLARED ITS OWN IDENTITY IS NOT CONTRADICTED ---- 167 let rx: *u8 = sys_mmap(cap + 64) 168 hc_set_identity(HC_UA_BROWSER) 169 let xh: *u8 = "User-Agent: HonestClient/1.0\r\n" as *u8 170 var xl: i64 = 0 171 while xh[xl] != (0 as u8) { xl = xl + 1 } 172 let nx: i64 = nx_http_client_build_request_xhdr(path, path_len, host, host_len, xh, xl, rx) 173 174 gv_plan_check(plan,"neg-control-caller-ua-suppresses-chrome-hints", 1 - hg_has(rx, nx, "sec-ch-ua" as *u8), ctr) 175 gv_plan_check(plan,"neg-control-caller-ua-survives", hg_has(rx, nx, "HonestClient/1.0" as *u8), ctr) 176 177 // ---- WELL-FORMEDNESS: these hold for EVERY identity, so they are asserted on all three ---- 178 gv_plan_check(plan,"browser-exactly-one-user-agent",(hg_count(rb,nb,"User-Agent:")==1) as i64,ctr) 179 gv_plan_check(plan,"bot-exactly-one-user-agent",(hg_count(rn,nn,"User-Agent:")==1) as i64,ctr) 180 gv_plan_check(plan,"xhdr-exactly-one-user-agent",(hg_count(rx,nx,"User-Agent:")==1) as i64,ctr) 181 182 // A request that lost its terminating blank line is a request the peer waits forever for. 183 gv_plan_check(plan,"browser-terminates-blank-line", hg_has(rb, nb, "\r\n\r\n" as *u8), ctr) 184 185 // THE BOUND IS OWNED BY nx_http_client_request_cap AND THIS ASSERTS WE STILL FIT INSIDE IT. 186 // Adding headers without moving the cap is exactly how a builder overruns its own buffer, and 187 // the overrun would be silent -- mmap'd slack reads as zero, not as a fault. 188 gv_plan_check(plan,"browser-fits-declared-cap", (nb < cap) as i64, ctr) 189 gv_plan_check(plan,"bot-fits-declared-cap", (nn < cap) as i64, ctr) 190 191 192 gv_plan_check(plan,"header-range-valid",hc_header_line_size("Range: bytes=0-7",_gc_slen("Range: bytes=0-7"))==_gc_slen("Range: bytes=0-7\r\n"),ctr) 193 gv_plan_check(plan,"header-empty-name-refused",hc_header_line_size(": x",_gc_slen(": x"))==HC_HEADER_BAD_NAME,ctr) 194 gv_plan_check(plan,"header-whitespace-name-refused",hc_header_line_size("Bad Name: x",_gc_slen("Bad Name: x"))==HC_HEADER_BAD_NAME,ctr) 195 gv_plan_check(plan,"header-missing-colon-refused",hc_header_line_size("Range",_gc_slen("Range"))==HC_HEADER_BAD_NAME,ctr) 196 gv_plan_check(plan,"header-injected-request-refused",hc_header_line_size("X: x\r\nGET /",_gc_slen("X: x\r\nGET /"))==HC_HEADER_BAD_VALUE,ctr) 197 gv_plan_check(plan,"header-host-override-refused",hc_header_line_size("hOsT: evil",_gc_slen("hOsT: evil"))==HC_HEADER_OWNED_FIELD,ctr) 198 gv_plan_check(plan,"header-length-override-refused",hc_header_line_size("Content-Length: 1",_gc_slen("Content-Length: 1"))==HC_HEADER_OWNED_FIELD,ctr) 199 gv_plan_check(plan,"header-transfer-framing-refused",hc_header_line_size("Transfer-Encoding: chunked",_gc_slen("Transfer-Encoding: chunked"))==HC_HEADER_OWNED_FIELD,ctr) 200 let range_headers: *u8="Range: bytes=0-7\r\n" 201 let range_n: i64=_gc_slen(range_headers) 202 gv_plan_check(plan,"header-block-valid",hf_header_block_valid(range_headers,range_n),ctr) 203 gv_plan_check(plan,"header-block-missing-lf-refused",1-hf_header_block_valid(range_headers,range_n-1),ctr) 204 gv_plan_check(plan,"header-block-blank-line-refused",1-hf_header_block_valid("\r\n",2),ctr) 205 gv_plan_check(plan,"header-empty-block-compatible",hf_header_block_valid(0 as *u8,0),ctr) 206 gv_plan_check(plan,"header-redirect-same-authority",hf_header_redirect_same_origin("https://example.com/a","https://example.com/b"),ctr) 207 gv_plan_check(plan,"header-redirect-other-host-refused",1-hf_header_redirect_same_origin("https://example.com/a","https://example.com.evil/b"),ctr) 208 gv_plan_check(plan,"header-redirect-other-port-refused",1-hf_header_redirect_same_origin("https://example.com/a","https://example.com:8443/b"),ctr) 209 gv_plan_check(plan,"header-redirect-downgrade-refused",1-hf_header_redirect_same_origin("https://example.com/a","http://example.com/b"),ctr) 210 let range_cap: i64=nx_http_client_request_cap(path_len,host_len,0,range_n) 211 let range_req: *u8=sys_mmap(range_cap) 212 let range_len: i64=nx_http_client_build_request_cookie_xhdr(path,path_len,host,host_len,0 as *u8,0,range_headers,range_n,range_req) 213 gv_plan_check(plan,"range-request-exact-header-once",hg_count(range_req,range_len,range_headers)==1,ctr) 214 let plain_req: *u8=sys_mmap(cap) 215 let plain_n: i64=nx_http_client_build_request_cookie_xhdr(path,path_len,host,host_len,0 as *u8,0,0 as *u8,0,plain_req) 216 var identical: i64=(plain_n==nb) as i64 217 var pi: i64=0 218 while pi<plain_n && pi<nb { if plain_req[pi]!=rb[pi] { identical=0 };pi=pi+1 } 219 gv_plan_check(plan,"empty-header-block-preserves-ordinary-request-bytes",identical,ctr) 220 221 gv_plan_check(plan,"uri-reference-1",hg_resolve("http://a/b/c/d;p?q","g:h","g:h"),ctr) 222 gv_plan_check(plan,"uri-reference-2",hg_resolve("http://a/b/c/d;p?q","g","http://a/b/c/g"),ctr) 223 gv_plan_check(plan,"uri-reference-3",hg_resolve("http://a/b/c/d;p?q","./g","http://a/b/c/g"),ctr) 224 gv_plan_check(plan,"uri-reference-4",hg_resolve("http://a/b/c/d;p?q","g/","http://a/b/c/g/"),ctr) 225 gv_plan_check(plan,"uri-reference-5",hg_resolve("http://a/b/c/d;p?q","/g","http://a/g"),ctr) 226 gv_plan_check(plan,"uri-reference-6",hg_resolve("http://a/b/c/d;p?q","//g","http://g"),ctr) 227 gv_plan_check(plan,"uri-reference-7",hg_resolve("http://a/b/c/d;p?q","?y","http://a/b/c/d;p?y"),ctr) 228 gv_plan_check(plan,"uri-reference-8",hg_resolve("http://a/b/c/d;p?q","g?y","http://a/b/c/g?y"),ctr) 229 gv_plan_check(plan,"uri-reference-9",hg_resolve("http://a/b/c/d;p?q","#s","http://a/b/c/d;p?q#s"),ctr) 230 gv_plan_check(plan,"uri-reference-10",hg_resolve("http://a/b/c/d;p?q","g#s","http://a/b/c/g#s"),ctr) 231 gv_plan_check(plan,"uri-reference-11",hg_resolve("http://a/b/c/d;p?q","g?y#s","http://a/b/c/g?y#s"),ctr) 232 gv_plan_check(plan,"uri-reference-12",hg_resolve("http://a/b/c/d;p?q",";x","http://a/b/c/;x"),ctr) 233 gv_plan_check(plan,"uri-reference-20",hg_resolve("http://a/b/c/d;p?q","../g","http://a/b/g"),ctr) 234 gv_plan_check(plan,"uri-reference-21",hg_resolve("http://a/b/c/d;p?q","../","http://a/b/"),ctr) 235 gv_plan_check(plan,"uri-reference-22",hg_resolve("http://a/b/c/d;p?q","../..","http://a/"),ctr) 236 gv_plan_check(plan,"uri-reference-23",hg_resolve("http://a/b/c/d;p?q","../../","http://a/"),ctr) 237 gv_plan_check(plan,"uri-reference-24",hg_resolve("http://a/b/c/d;p?q","../../g","http://a/g"),ctr) 238 gv_plan_check(plan,"uri-reference-40",hg_resolve("https://www.google.com/","/images/logo.png","https://www.google.com/images/logo.png"),ctr) 239 gv_plan_check(plan,"uri-reference-41",hg_resolve("https://www.google.com/","images/logo.png","https://www.google.com/images/logo.png"),ctr) 240 gv_plan_check(plan,"uri-reference-42",hg_resolve("https://www.google.com/","https://other.com/x.png","https://other.com/x.png"),ctr) 241 gv_plan_check(plan,"uri-reference-43",hg_resolve("https://www.google.com/","//cdn.com/a.png","https://cdn.com/a.png"),ctr) 242 243 let redir_response: *u8="HTTP/1.1 301 Moved Permanently\r\nLocation: /releases/beach-contact-16c8ca4d\r\nContent-Length: 0\r\n\r\n" 244 let redir_base: *u8="https://nishifamily.com/releases/beach-contact-16c8ca4d.html" 245 let redir_cap: i64=_gc_slen(redir_base)+_gc_slen(redir_response)+1 246 let redir_out: *u8=sys_mmap(redir_cap) 247 let redir_n: *i64=sys_mmap(__size_of(i64)) as *i64 248 let redir_rc: i64=nx_http_resolve_redirect(redir_response,_gc_slen(redir_response),301,redir_base,_gc_slen(redir_base),redir_out,redir_cap-1,redir_n) 249 redir_out[redir_n[0]]=0 as u8 250 gv_plan_check(plan,"beach-staged-relative-redirect-resolved",(redir_rc==NX_REDIRECT_RESOLVED && hg_has(redir_out,redir_n[0],"https://nishifamily.com/releases/beach-contact-16c8ca4d")==1) as i64,ctr) 251 gv_plan_check(plan,"resolved-relative-redirect-retains-header-authority",hf_header_redirect_same_origin(redir_base,redir_out),ctr) 252 sys_munmap(redir_out,redir_cap);sys_munmap(redir_n,__size_of(i64)) 253 // Regression fixture exceeds the former 4096-byte scratch and 256-segment stack. 254 let retired_scratch_bytes: i64=HG_RETIRED_URL_SCRATCH_BYTES 255 let segment_count: i64=retired_scratch_bytes+1 256 let long_n: i64=segment_count*2 257 let long_ref: *u8=sys_mmap(long_n+1) 258 let expected_prefix: *u8="https://example.com/" 259 let prefix_n: i64=_gc_slen(expected_prefix) 260 let long_want: *u8=sys_mmap(prefix_n+long_n+1) 261 var li: i64=0 262 while li<long_n { if li%2==0 { long_ref[li]=97 as u8 } else { long_ref[li]=47 as u8 };li=li+1 } 263 long_ref[long_n]=0 as u8 264 li=0;while li<prefix_n { long_want[li]=expected_prefix[li];li=li+1 } 265 li=0;while li<=long_n { long_want[prefix_n+li]=long_ref[li];li=li+1 } 266 gv_plan_check(plan,"relative-url-exceeds-retired-scratch-and-stack",hg_resolve(expected_prefix,long_ref,long_want),ctr) 267 let absolute_ref: *u8=sys_mmap(long_n+2) 268 absolute_ref[0]=47 as u8 269 li=0;while li<=long_n { absolute_ref[li+1]=long_ref[li];li=li+1 } 270 gv_plan_check(plan,"absolute-url-exceeds-retired-segment-stack",hg_resolve(expected_prefix,absolute_ref,long_want),ctr) 271 sys_munmap(absolute_ref,long_n+2) 272 sys_munmap(long_ref,long_n+1);sys_munmap(long_want,prefix_n+long_n+1) 273 274 // ---- VALUES: a gate that prints only PASS is unfalsifiable from outside (2026-09-03 L4 law) ---- 275 gv_values_head() 276 gv_kv("browser_request_bytes" as *u8, nb) 277 gv_kv("bot_request_bytes" as *u8, nn) 278 gv_kv("xhdr_request_bytes" as *u8, nx) 279 gv_kv("declared_cap_bytes" as *u8, cap) 280 gv_kv("browser_minus_bot_bytes" as *u8, nb - nn) 281 gv_kv("ua_version" as *u8, ua_ver) 282 gv_kv("sec_ch_ua_version" as *u8, ch_ver) 283 284 gv_plan_finish(plan,ctr) 285 return gv_verdict("nx_http_client_gate" as *u8, ctr, "identity coherence on the ONE request builder every sovereign fetch crosses" as *u8) 286}