code wiki / _hdl_build / nx_email_mx_gate.nx

nx_email_mx_gate.nx source

↩ module page · 236 lines · 10098 B

1// nx_email_mx_gate.nx -- GATE for EMAIL R0 (MX resolution, nx_email_mx). 2// 3// Drives the REAL nx_email_mx parser over hand-crafted DNS response 4// packets (no network, fully deterministic) and asserts: 5// 6// COMPLETENESS : a 2-record MX response (both exchanges using RFC 7// 1035 compression pointers back to the question 8// name) parses to count=2 with correct preferences 9// and materialised exchange hostnames; sort orders 10// them lowest-preference-first (the connect order). 11// NEG-CONTROL : a clean response whose only answer is an A record 12// (not MX) returns NO_MX -- the parser does not 13// hallucinate a mail host. 14// TAMPER txid : the SAME valid MX packet parsed against a wrong 15// expected tx_id returns TXID_MISMATCH (off-path 16// spoof gate: a forged response cannot redirect mail). 17// TAMPER ptrloop: an exchange name that is a self-referential 18// compression pointer returns PTR_LOOP and the gate 19// COMPLETES (bounded chase, no hang / no OOM). 20// 21// Evidence -> knowledge/status/email_mx.log 22// (EMAILMXGATE authored=organ ... verdict=GREEN) 23// license_tier: ORIGINAL 24import "nx_email_mx.nx" 25import "nx_syscalls.nx" 26import "nx_gate_verdict.nx" 27 28const EMX_LOG: *u8 = "knowledge/status/email_mx.log" 29 30func ew(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 31func ewn(fd: i64, v: i64) -> i64 { 32 let bb: *u8 = sys_mmap(28); var m: i64 = v 33 if m < 0 { m = 0 - m; sys_write(fd, "-" as *u8, 1) } 34 let t: *u8 = sys_mmap(28); var k: i64 = 0 35 if m == 0 { t[0] = 48; k = 1 } 36 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 37 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } 38 sys_write(fd, bb, k); return 0 39} 40 41func pu16(p: *u8, off: i64, v: i64) -> i64 { 42 p[off] = ((v >> 8) & 0xff) as u8 43 p[off + 1] = (v & 0xff) as u8 44 return off + 2 45} 46 47func streq(a: *u8, b: *u8) -> i64 { 48 var i: i64 = 0 49 while a[i] != (0 as u8) && b[i] != (0 as u8) { 50 if a[i] != b[i] { return 0 } 51 i = i + 1 52 } 53 if a[i] != b[i] { return 0 } 54 return 1 55} 56 57// Build the shared question "example.com" + MX/IN at offset 12; return 58// the offset just past the question (29). 59func build_question_example_com(p: *u8) -> i64 { 60 p[12] = 7 as u8 61 p[13] = 101 as u8; p[14] = 120 as u8; p[15] = 97 as u8; p[16] = 109 as u8 62 p[17] = 112 as u8; p[18] = 108 as u8; p[19] = 101 as u8 // "example" 63 p[20] = 3 as u8 64 p[21] = 99 as u8; p[22] = 111 as u8; p[23] = 109 as u8 // "com" 65 p[24] = 0 as u8 66 pu16(p, 25, NX_DNS_TYPE_MX) 67 pu16(p, 27, NX_DNS_CLASS_IN) 68 return 29 69} 70 71// Build a full 2-record MX response into p. Returns packet length (69). 72// answer1: pref 10, exchange "mx1" + ptr->example.com 73// answer2: pref 5, exchange "mx2" + ptr->example.com 74func build_mx_response(p: *u8) -> i64 { 75 pu16(p, 0, 0xBEEF) // tx id 76 pu16(p, 2, 0x8180) // QR=1, RD=1, RA=1, rcode=0 77 pu16(p, 4, 1) // qdcount 78 pu16(p, 6, 2) // ancount 79 pu16(p, 8, 0) // nscount 80 pu16(p, 10, 0) // arcount 81 build_question_example_com(p) 82 // answer 1 @29 83 p[29] = 0xC0 as u8; p[30] = 0x0C as u8 // name -> offset 12 84 pu16(p, 31, NX_DNS_TYPE_MX) 85 pu16(p, 33, NX_DNS_CLASS_IN) 86 pu16(p, 35, 0); pu16(p, 37, 300) // ttl = 300 87 pu16(p, 39, 8) // rdlength 88 pu16(p, 41, 10) // preference 10 89 p[43] = 3 as u8; p[44] = 109 as u8; p[45] = 120 as u8; p[46] = 49 as u8 // "mx1" 90 p[47] = 0xC0 as u8; p[48] = 0x0C as u8 // -> example.com 91 // answer 2 @49 92 p[49] = 0xC0 as u8; p[50] = 0x0C as u8 93 pu16(p, 51, NX_DNS_TYPE_MX) 94 pu16(p, 53, NX_DNS_CLASS_IN) 95 pu16(p, 55, 0); pu16(p, 57, 300) 96 pu16(p, 59, 8) 97 pu16(p, 61, 5) // preference 5 98 p[63] = 3 as u8; p[64] = 109 as u8; p[65] = 120 as u8; p[66] = 50 as u8 // "mx2" 99 p[67] = 0xC0 as u8; p[68] = 0x0C as u8 100 return 69 101} 102 103// A clean response whose single answer is an A record, not MX. len 49. 104func build_a_only_response(p: *u8) -> i64 { 105 pu16(p, 0, 0xBEEF) 106 pu16(p, 2, 0x8180) 107 pu16(p, 4, 1) 108 pu16(p, 6, 1) // ancount = 1 (the A record) 109 pu16(p, 8, 0); pu16(p, 10, 0) 110 build_question_example_com(p) 111 p[29] = 0xC0 as u8; p[30] = 0x0C as u8 112 pu16(p, 31, NX_DNS_TYPE_A) 113 pu16(p, 33, NX_DNS_CLASS_IN) 114 pu16(p, 35, 0); pu16(p, 37, 300) 115 pu16(p, 39, 4) // rdlength 4 116 p[41] = 93 as u8; p[42] = 184 as u8; p[43] = 216 as u8; p[44] = 34 as u8 // 93.184.216.34 117 return 45 118} 119 120// An MX response whose exchange name is a self-referential pointer 121// (C0 pointing at its own offset 37). len 39. 122func build_ptrloop_response(p: *u8) -> i64 { 123 pu16(p, 0, 0xBEEF) 124 pu16(p, 2, 0x8180) 125 pu16(p, 4, 1) 126 pu16(p, 6, 1) 127 pu16(p, 8, 0); pu16(p, 10, 0) 128 // question "a.com" @12 129 p[12] = 1 as u8; p[13] = 97 as u8 // "a" 130 p[14] = 3 as u8; p[15] = 99 as u8; p[16] = 111 as u8; p[17] = 109 as u8 // "com" 131 p[18] = 0 as u8 132 pu16(p, 19, NX_DNS_TYPE_MX) 133 pu16(p, 21, NX_DNS_CLASS_IN) 134 // answer @23 135 p[23] = 0xC0 as u8; p[24] = 0x0C as u8 136 pu16(p, 25, NX_DNS_TYPE_MX) 137 pu16(p, 27, NX_DNS_CLASS_IN) 138 pu16(p, 29, 0); pu16(p, 31, 300) 139 pu16(p, 33, 4) // rdlength 4 140 pu16(p, 35, 10) // preference 141 p[37] = 0xC0 as u8; p[38] = 0x25 as u8 // exchange -> 0x25 = 37 (itself) 142 return 39 143} 144 145func main() -> i64 { 146 let prefs: *i64 = sys_mmap(8 * 16) as *i64 147 let ex_off: *i64 = sys_mmap(8 * 16) as *i64 148 let names: *u8 = sys_mmap(512) 149 let p: *u8 = sys_mmap(256) 150 151 var ok: i64 = 1 152 153 // ---- COMPLETENESS: parse 2-record MX response ---- 154 let n1: i64 = build_mx_response(p) 155 let c1: i64 = nx_email_mx_parse(p, n1, 0xBEEF, prefs, ex_off, 16, names, 512) 156 if c1 != 2 { ok = 0 } 157 // pre-sort order matches packet order: rec0 pref10 mx1, rec1 pref5 mx2 158 var pre0: i64 = 0 - 1 159 var pre1: i64 = 0 - 1 160 if c1 == 2 { 161 pre0 = prefs[0] 162 pre1 = prefs[1] 163 if prefs[0] != 10 { ok = 0 } 164 if prefs[1] != 5 { ok = 0 } 165 if streq(names + ex_off[0], "mx1.example.com" as *u8) != 1 { ok = 0 } 166 if streq(names + ex_off[1], "mx2.example.com" as *u8) != 1 { ok = 0 } 167 } 168 169 // ---- SORT: lowest preference first = connect order ---- 170 if c1 == 2 { nx_email_mx_sort(prefs, ex_off, c1) } 171 var s0p: i64 = 0 - 1 172 var s1p: i64 = 0 - 1 173 if c1 == 2 { 174 s0p = prefs[0] 175 s1p = prefs[1] 176 if prefs[0] != 5 { ok = 0 } // mx2 (pref 5) is now first 177 if prefs[1] != 10 { ok = 0 } 178 if streq(names + ex_off[0], "mx2.example.com" as *u8) != 1 { ok = 0 } 179 if streq(names + ex_off[1], "mx1.example.com" as *u8) != 1 { ok = 0 } 180 } 181 182 // ---- NEG-CONTROL: A-only response yields NO_MX ---- 183 let n2: i64 = build_a_only_response(p) 184 let c2: i64 = nx_email_mx_parse(p, n2, 0xBEEF, prefs, ex_off, 16, names, 512) 185 var negctrl: i64 = 0 186 if c2 == (0 - NX_EMX_NO_MX) { negctrl = 1 } else { ok = 0 } 187 188 // ---- TAMPER txid: valid MX packet, wrong expected tx_id ---- 189 let n3: i64 = build_mx_response(p) 190 let c3: i64 = nx_email_mx_parse(p, n3, 0x1234, prefs, ex_off, 16, names, 512) 191 var tamper_txid: i64 = 0 192 if c3 == (0 - NX_EMX_TXID_MISMATCH) { tamper_txid = 1 } else { ok = 0 } 193 194 // ---- TAMPER ptrloop: self-referential exchange pointer ---- 195 let n4: i64 = build_ptrloop_response(p) 196 let c4: i64 = nx_email_mx_parse(p, n4, 0xBEEF, prefs, ex_off, 16, names, 512) 197 var tamper_loop: i64 = 0 198 if c4 == (0 - NX_EMX_PTR_LOOP) { tamper_loop = 1 } else { ok = 0 } 199 // reaching here at all proves the chase was bounded (no hang) 200 201 // re-derive sorted exchange strings for the marker (parse again, sort) 202 let nm: i64 = build_mx_response(p) 203 let cm: i64 = nx_email_mx_parse(p, nm, 0xBEEF, prefs, ex_off, 16, names, 512) 204 if cm == 2 { nx_email_mx_sort(prefs, ex_off, cm) } 205 206 var fd: i64 = 1 207 while fd >= 1 { 208 ew(fd, "EMAILMXGATE authored=organ rfc=5321-mx compose=nx_dns count=" as *u8); ewn(fd, c1) 209 ew(fd, " presort_pref0=" as *u8); ewn(fd, pre0) 210 ew(fd, " presort_pref1=" as *u8); ewn(fd, pre1) 211 ew(fd, " sorted_pref0=" as *u8); ewn(fd, s0p) 212 ew(fd, " sorted_pref1=" as *u8); ewn(fd, s1p) 213 ew(fd, " connect1=" as *u8); ew(fd, names + ex_off[0]) 214 ew(fd, " connect2=" as *u8); ew(fd, names + ex_off[1]) 215 ew(fd, " negctrl_noMX=" as *u8); if negctrl == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 216 ew(fd, " tamper_txid=" as *u8); if tamper_txid == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 217 ew(fd, " tamper_ptrloop=" as *u8); if tamper_loop == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 218 if ok == 1 { ew(fd, " verdict=GREEN\n" as *u8) } else { ew(fd, " verdict=RED\n" as *u8) } 219 if fd == 1 { 220 let lf: i64 = sys_openat_append(EMX_LOG, 420) 221 if lf >= 1 { fd = lf } else { fd = 0 } 222 } else { 223 sys_close(fd); fd = 0 224 } 225 } 226 227 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 228 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 229 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 230 let ctr__dry: *i64 = gv_ctr() 231 ctr__dry[0] = ok 232 ctr__dry[1] = 1 233 let rc__dry: i64 = gv_verdict("EMAIL-MX-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 234 sys_exit(rc__dry) 235 return rc__dry 236}