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}