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}