code wiki / _hdl_build / nx_email_mx_gate.nx

nx_email_mx_gate.nx source

↩ module page · 228 lines · 9564 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" 26 27const EMX_LOG: *u8 = "knowledge/status/email_mx.log" 28 29func 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 } 30func ewn(fd: i64, v: i64) -> i64 { 31 let bb: *u8 = sys_mmap(28); var m: i64 = v 32 if m < 0 { m = 0 - m; sys_write(fd, "-" as *u8, 1) } 33 let t: *u8 = sys_mmap(28); var k: i64 = 0 34 if m == 0 { t[0] = 48; k = 1 } 35 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 36 var i: i64 = 0; while i < k { bb[i] = t[k - 1 - i]; i = i + 1 } 37 sys_write(fd, bb, k); return 0 38} 39 40func pu16(p: *u8, off: i64, v: i64) -> i64 { 41 p[off] = ((v >> 8) & 0xff) as u8 42 p[off + 1] = (v & 0xff) as u8 43 return off + 2 44} 45 46func streq(a: *u8, b: *u8) -> i64 { 47 var i: i64 = 0 48 while a[i] != (0 as u8) && b[i] != (0 as u8) { 49 if a[i] != b[i] { return 0 } 50 i = i + 1 51 } 52 if a[i] != b[i] { return 0 } 53 return 1 54} 55 56// Build the shared question "example.com" + MX/IN at offset 12; return 57// the offset just past the question (29). 58func build_question_example_com(p: *u8) -> i64 { 59 p[12] = 7 as u8 60 p[13] = 101 as u8; p[14] = 120 as u8; p[15] = 97 as u8; p[16] = 109 as u8 61 p[17] = 112 as u8; p[18] = 108 as u8; p[19] = 101 as u8 // "example" 62 p[20] = 3 as u8 63 p[21] = 99 as u8; p[22] = 111 as u8; p[23] = 109 as u8 // "com" 64 p[24] = 0 as u8 65 pu16(p, 25, NX_DNS_TYPE_MX) 66 pu16(p, 27, NX_DNS_CLASS_IN) 67 return 29 68} 69 70// Build a full 2-record MX response into p. Returns packet length (69). 71// answer1: pref 10, exchange "mx1" + ptr->example.com 72// answer2: pref 5, exchange "mx2" + ptr->example.com 73func build_mx_response(p: *u8) -> i64 { 74 pu16(p, 0, 0xBEEF) // tx id 75 pu16(p, 2, 0x8180) // QR=1, RD=1, RA=1, rcode=0 76 pu16(p, 4, 1) // qdcount 77 pu16(p, 6, 2) // ancount 78 pu16(p, 8, 0) // nscount 79 pu16(p, 10, 0) // arcount 80 build_question_example_com(p) 81 // answer 1 @29 82 p[29] = 0xC0 as u8; p[30] = 0x0C as u8 // name -> offset 12 83 pu16(p, 31, NX_DNS_TYPE_MX) 84 pu16(p, 33, NX_DNS_CLASS_IN) 85 pu16(p, 35, 0); pu16(p, 37, 300) // ttl = 300 86 pu16(p, 39, 8) // rdlength 87 pu16(p, 41, 10) // preference 10 88 p[43] = 3 as u8; p[44] = 109 as u8; p[45] = 120 as u8; p[46] = 49 as u8 // "mx1" 89 p[47] = 0xC0 as u8; p[48] = 0x0C as u8 // -> example.com 90 // answer 2 @49 91 p[49] = 0xC0 as u8; p[50] = 0x0C as u8 92 pu16(p, 51, NX_DNS_TYPE_MX) 93 pu16(p, 53, NX_DNS_CLASS_IN) 94 pu16(p, 55, 0); pu16(p, 57, 300) 95 pu16(p, 59, 8) 96 pu16(p, 61, 5) // preference 5 97 p[63] = 3 as u8; p[64] = 109 as u8; p[65] = 120 as u8; p[66] = 50 as u8 // "mx2" 98 p[67] = 0xC0 as u8; p[68] = 0x0C as u8 99 return 69 100} 101 102// A clean response whose single answer is an A record, not MX. len 49. 103func build_a_only_response(p: *u8) -> i64 { 104 pu16(p, 0, 0xBEEF) 105 pu16(p, 2, 0x8180) 106 pu16(p, 4, 1) 107 pu16(p, 6, 1) // ancount = 1 (the A record) 108 pu16(p, 8, 0); pu16(p, 10, 0) 109 build_question_example_com(p) 110 p[29] = 0xC0 as u8; p[30] = 0x0C as u8 111 pu16(p, 31, NX_DNS_TYPE_A) 112 pu16(p, 33, NX_DNS_CLASS_IN) 113 pu16(p, 35, 0); pu16(p, 37, 300) 114 pu16(p, 39, 4) // rdlength 4 115 p[41] = 93 as u8; p[42] = 184 as u8; p[43] = 216 as u8; p[44] = 34 as u8 // 93.184.216.34 116 return 45 117} 118 119// An MX response whose exchange name is a self-referential pointer 120// (C0 pointing at its own offset 37). len 39. 121func build_ptrloop_response(p: *u8) -> i64 { 122 pu16(p, 0, 0xBEEF) 123 pu16(p, 2, 0x8180) 124 pu16(p, 4, 1) 125 pu16(p, 6, 1) 126 pu16(p, 8, 0); pu16(p, 10, 0) 127 // question "a.com" @12 128 p[12] = 1 as u8; p[13] = 97 as u8 // "a" 129 p[14] = 3 as u8; p[15] = 99 as u8; p[16] = 111 as u8; p[17] = 109 as u8 // "com" 130 p[18] = 0 as u8 131 pu16(p, 19, NX_DNS_TYPE_MX) 132 pu16(p, 21, NX_DNS_CLASS_IN) 133 // answer @23 134 p[23] = 0xC0 as u8; p[24] = 0x0C as u8 135 pu16(p, 25, NX_DNS_TYPE_MX) 136 pu16(p, 27, NX_DNS_CLASS_IN) 137 pu16(p, 29, 0); pu16(p, 31, 300) 138 pu16(p, 33, 4) // rdlength 4 139 pu16(p, 35, 10) // preference 140 p[37] = 0xC0 as u8; p[38] = 0x25 as u8 // exchange -> 0x25 = 37 (itself) 141 return 39 142} 143 144func main() -> i64 { 145 let prefs: *i64 = sys_mmap(8 * 16) as *i64 146 let ex_off: *i64 = sys_mmap(8 * 16) as *i64 147 let names: *u8 = sys_mmap(512) 148 let p: *u8 = sys_mmap(256) 149 150 var ok: i64 = 1 151 152 // ---- COMPLETENESS: parse 2-record MX response ---- 153 let n1: i64 = build_mx_response(p) 154 let c1: i64 = nx_email_mx_parse(p, n1, 0xBEEF, prefs, ex_off, 16, names, 512) 155 if c1 != 2 { ok = 0 } 156 // pre-sort order matches packet order: rec0 pref10 mx1, rec1 pref5 mx2 157 var pre0: i64 = 0 - 1 158 var pre1: i64 = 0 - 1 159 if c1 == 2 { 160 pre0 = prefs[0] 161 pre1 = prefs[1] 162 if prefs[0] != 10 { ok = 0 } 163 if prefs[1] != 5 { ok = 0 } 164 if streq(names + ex_off[0], "mx1.example.com" as *u8) != 1 { ok = 0 } 165 if streq(names + ex_off[1], "mx2.example.com" as *u8) != 1 { ok = 0 } 166 } 167 168 // ---- SORT: lowest preference first = connect order ---- 169 if c1 == 2 { nx_email_mx_sort(prefs, ex_off, c1) } 170 var s0p: i64 = 0 - 1 171 var s1p: i64 = 0 - 1 172 if c1 == 2 { 173 s0p = prefs[0] 174 s1p = prefs[1] 175 if prefs[0] != 5 { ok = 0 } // mx2 (pref 5) is now first 176 if prefs[1] != 10 { ok = 0 } 177 if streq(names + ex_off[0], "mx2.example.com" as *u8) != 1 { ok = 0 } 178 if streq(names + ex_off[1], "mx1.example.com" as *u8) != 1 { ok = 0 } 179 } 180 181 // ---- NEG-CONTROL: A-only response yields NO_MX ---- 182 let n2: i64 = build_a_only_response(p) 183 let c2: i64 = nx_email_mx_parse(p, n2, 0xBEEF, prefs, ex_off, 16, names, 512) 184 var negctrl: i64 = 0 185 if c2 == (0 - NX_EMX_NO_MX) { negctrl = 1 } else { ok = 0 } 186 187 // ---- TAMPER txid: valid MX packet, wrong expected tx_id ---- 188 let n3: i64 = build_mx_response(p) 189 let c3: i64 = nx_email_mx_parse(p, n3, 0x1234, prefs, ex_off, 16, names, 512) 190 var tamper_txid: i64 = 0 191 if c3 == (0 - NX_EMX_TXID_MISMATCH) { tamper_txid = 1 } else { ok = 0 } 192 193 // ---- TAMPER ptrloop: self-referential exchange pointer ---- 194 let n4: i64 = build_ptrloop_response(p) 195 let c4: i64 = nx_email_mx_parse(p, n4, 0xBEEF, prefs, ex_off, 16, names, 512) 196 var tamper_loop: i64 = 0 197 if c4 == (0 - NX_EMX_PTR_LOOP) { tamper_loop = 1 } else { ok = 0 } 198 // reaching here at all proves the chase was bounded (no hang) 199 200 // re-derive sorted exchange strings for the marker (parse again, sort) 201 let nm: i64 = build_mx_response(p) 202 let cm: i64 = nx_email_mx_parse(p, nm, 0xBEEF, prefs, ex_off, 16, names, 512) 203 if cm == 2 { nx_email_mx_sort(prefs, ex_off, cm) } 204 205 var fd: i64 = 1 206 while fd >= 1 { 207 ew(fd, "EMAILMXGATE authored=organ rfc=5321-mx compose=nx_dns count=" as *u8); ewn(fd, c1) 208 ew(fd, " presort_pref0=" as *u8); ewn(fd, pre0) 209 ew(fd, " presort_pref1=" as *u8); ewn(fd, pre1) 210 ew(fd, " sorted_pref0=" as *u8); ewn(fd, s0p) 211 ew(fd, " sorted_pref1=" as *u8); ewn(fd, s1p) 212 ew(fd, " connect1=" as *u8); ew(fd, names + ex_off[0]) 213 ew(fd, " connect2=" as *u8); ew(fd, names + ex_off[1]) 214 ew(fd, " negctrl_noMX=" as *u8); if negctrl == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 215 ew(fd, " tamper_txid=" as *u8); if tamper_txid == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 216 ew(fd, " tamper_ptrloop=" as *u8); if tamper_loop == 1 { ew(fd, "PASS" as *u8) } else { ew(fd, "FAIL" as *u8) } 217 if ok == 1 { ew(fd, " verdict=GREEN\n" as *u8) } else { ew(fd, " verdict=RED\n" as *u8) } 218 if fd == 1 { 219 let lf: i64 = sys_openat_append(EMX_LOG, 420) 220 if lf >= 1 { fd = lf } else { fd = 0 } 221 } else { 222 sys_close(fd); fd = 0 223 } 224 } 225 226 if ok == 1 { return 0 } 227 return 1 228}