nx_dns.nx source
↩ module page · 344 lines · 13945 B
1// nx_dns.nx -- sovereign DNS stub-resolver primitives (RFC 1035).
2//
3// Phase 0a of the Nishi browser per docs/NISHI_BROWSER_ROADMAP.md.
4// The deepest missing L2 dependency between nx_udp (shipped) and
5// nx_http_client (shipped) on the way to a sovereign GET against an
6// arbitrary hostname. Pure parse/build primitives -- no syscalls,
7// no allocation beyond the caller's buffer, architecture-clean so
8// the self-test runs under qemu-riscv64 via nx_smoke_lib.
9//
10// Live network resolve composes against nx_udp in a separate
11// nx_dns_io.nx (queued) so this file stays portable.
12//
13// What it does today:
14// - encode a domain name into RFC 1035 label-length wire format
15// - build a query packet (header + question, RD bit set)
16// - parse a response and extract the first IPv4 A record
17// - skip a name with BOUNDED pointer-decompression per
18// [[feedback-bounded-loop-discipline-jpl-rule-2]] -- no infinite
19// loop on a crafted self-referential pointer
20// - sealed verdict enum with NX_DNS_VERDICT_N validity gate
21//
22// What it doesn't do yet:
23// - IPv6 AAAA extraction (constants present; helper queued)
24// - CNAME chase (caller-level retry for v1)
25// - Truncation (TC=1) -> TCP fallback (Phase 0b)
26// - EDNS0 OPT record (Phase 0c)
27// - DNSSEC validation per RFC 4035 (queued separate phase)
28// - DoH / DoT (queued once nx_tls13 audit completes)
29//
30// Composes with:
31// - nx_udp (transport for live resolve, in nx_dns_io.nx)
32// - nx_csprng (caller supplies tx_id from nx_csprng_u64 per RFC 5452
33// off-path spoof defense)
34// - nx_http_client (replaces its "caller packs IPv4 manually" gap
35// once nx_dns_io.nx wires the chain end-to-end)
36//
37// license_tier: INDEPENDENT_REDERIVE
38// genealogy_id: international-research-sources/ietf/rfc_1035 + ietf/rfc_5452
39// lineage_id: nishi_browser_dns_stub_resolver_q10
40//
41// No syscall import: this module is pure byte-buffer operations and
42// stays architecture-neutral so it composes from BOTH the x86_64
43// native target (with nx_syscalls_x86_64) and the RV64 qemu target
44// (with nx_syscalls). Live UDP composition lives in nx_dns_io.nx.
45
46// nx_safety_envelope:
47// intended_use: "DNS message encode/decode + bounded-hop name
48// compression -- RFC 1035 substrate primitive.
49// Live UDP composition in nx_dns_io."
50// sil_target: SIL2 (DNS-spoof = arbitrary endpoint)
51// asil_target: QM
52// dal_target: DAL C
53// evidence: [RFC_1035_canonical_basis,
54// sealed_verdict_enum, no_FP,
55// skip_name_bounded_128_hops_anti_cycle,
56// rcode_table_complete]
57// hazard_register: [bug-tape-DNS-cache-poisoning,
58// bug-tape-compression-pointer-cycle-OOM,
59// bug-tape-Kaminsky-2008-attack]
60// residual_risk: "Cache-poisoning defense requires DNSSEC
61// validation in upstream consumer (queued).
62// Substrate enforces 128-hop bound on
63// compression pointer chase (anti-cycle)."
64// verdict: NOT_YET_EVALUATED
65
66// ---- Sealed verdict for a parse / build operation. ----
67const NX_DNS_VERDICT_UNKNOWN: i64 = 0
68const NX_DNS_VERDICT_OK: i64 = 1
69const NX_DNS_VERDICT_BUF_TOO_SMALL: i64 = 2
70const NX_DNS_VERDICT_BAD_HEADER: i64 = 3
71const NX_DNS_VERDICT_BAD_LABEL: i64 = 4
72const NX_DNS_VERDICT_PTR_LOOP: i64 = 5
73const NX_DNS_VERDICT_PTR_OUT_OF_RANGE: i64 = 6
74const NX_DNS_VERDICT_TXID_MISMATCH: i64 = 7
75const NX_DNS_VERDICT_NOT_RESPONSE: i64 = 8
76const NX_DNS_VERDICT_TRUNCATED: i64 = 9
77const NX_DNS_VERDICT_RCODE_NONZERO: i64 = 10
78const NX_DNS_VERDICT_NO_A_RECORD: i64 = 11
79const NX_DNS_VERDICT_N: i64 = 12
80
81// ---- Wire-format constants (RFC 1035 §3.2.2 + §4.1). ----
82const NX_DNS_PORT: i64 = 53
83const NX_DNS_HEADER_LEN: i64 = 12
84const NX_DNS_TYPE_A: i64 = 1
85const NX_DNS_TYPE_NS: i64 = 2
86const NX_DNS_TYPE_CNAME: i64 = 5
87const NX_DNS_TYPE_SOA: i64 = 6
88const NX_DNS_TYPE_PTR: i64 = 12
89const NX_DNS_TYPE_MX: i64 = 15
90const NX_DNS_TYPE_TXT: i64 = 16
91const NX_DNS_TYPE_AAAA: i64 = 28
92const NX_DNS_CLASS_IN: i64 = 1
93
94// Header flag bit layout (RFC 1035 §4.1.1, big-endian u16 at offset 2).
95const NX_DNS_FLAG_QR: i64 = 0x8000 // 1 = response
96const NX_DNS_FLAG_TC: i64 = 0x0200 // 1 = truncated
97const NX_DNS_FLAG_RD: i64 = 0x0100 // 1 = recursion desired
98const NX_DNS_FLAG_RA: i64 = 0x0080 // 1 = recursion available
99const NX_DNS_RCODE_MASK: i64 = 0x000f
100
101// Bounded pointer-decompression budget. RFC 1035 §4.1.4 implies a
102// finite chain but doesn't cap it; practical packets never exceed
103// ~3 hops, so 128 is a defensive ceiling that still terminates a
104// crafted infinite loop in microseconds. Per
105// [[feedback-bounded-loop-discipline-jpl-rule-2]].
106const NX_DNS_MAX_PTR_HOPS: i64 = 128
107
108// Per-section iteration budget. Defends against a crafted ANCOUNT
109// of 65535 (max u16) sending us off a cliff. Real responses have
110// <16 records; 64 is the operational ceiling.
111const NX_DNS_MAX_RECORDS: i64 = 64
112
113// ---- Endian helpers (DNS is big-endian on the wire). ----
114
115func nx_dns_put_u16_be(out: *u8, o: i64, v: i64) -> i64 {
116 out[o] = (v >> 8) & 0xff
117 out[o + 1] = v & 0xff
118 return o + 2
119}
120
121func nx_dns_get_u16_be(buf: *u8, o: i64) -> i64 {
122 let hi: i64 = buf[o] & 0xff
123 let lo: i64 = buf[o + 1] & 0xff
124 return (hi << 8) | lo
125}
126
127func nx_dns_get_u32_be(buf: *u8, o: i64) -> i64 {
128 let b0: i64 = buf[o] & 0xff
129 let b1: i64 = buf[o + 1] & 0xff
130 let b2: i64 = buf[o + 2] & 0xff
131 let b3: i64 = buf[o + 3] & 0xff
132 return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
133}
134
135// ---- Encode a hostname into RFC 1035 label format. ----
136//
137// "example.com" (11 input bytes) -> [7]example[3]com[0] (13 output bytes).
138// Caller buffer must hold >= name_len + 2. Returns bytes written or
139// a negative verdict on error.
140//
141// Restrictions: no empty labels (two consecutive dots), no leading or
142// trailing dot, each label <= 63 bytes. Length checks per RFC 1035
143// §2.3.4.
144func nx_dns_encode_name(name: *u8, name_len: i64, out: *u8, out_cap: i64) -> i64 {
145 if name_len <= 0 { return -NX_DNS_VERDICT_BAD_LABEL }
146 if out_cap < name_len + 2 { return -NX_DNS_VERDICT_BUF_TOO_SMALL }
147 var o: i64 = 0
148 var i: i64 = 0
149 var label_start: i64 = 0
150 while i <= name_len {
151 var at_end: i64 = 0
152 if i == name_len { at_end = 1 }
153 var at_dot: i64 = 0
154 if at_end == 0 {
155 if name[i] == 46 { at_dot = 1 }
156 }
157 if at_end == 1 || at_dot == 1 {
158 let label_len: i64 = i - label_start
159 if label_len <= 0 { return -NX_DNS_VERDICT_BAD_LABEL }
160 if label_len > 63 { return -NX_DNS_VERDICT_BAD_LABEL }
161 out[o] = label_len
162 o = o + 1
163 var k: i64 = 0
164 while k < label_len {
165 out[o + k] = name[label_start + k]
166 k = k + 1
167 }
168 o = o + label_len
169 label_start = i + 1
170 }
171 i = i + 1
172 }
173 out[o] = 0
174 o = o + 1
175 return o
176}
177
178// ---- Skip a wire-format name with bounded pointer-decompression. ----
179//
180// Returns the offset PAST this name's encoding from the outer cursor's
181// perspective. Per RFC 1035 §4.1.4: a label byte with the top two
182// bits set (0b11xxxxxx) is a 14-bit pointer; the outer cursor advances
183// past the 2 pointer bytes, the pointer chain follows internally.
184//
185// Defense: NX_DNS_MAX_PTR_HOPS bounds the chain, so a crafted packet
186// with ptr@12 -> ptr@12 terminates with NX_DNS_VERDICT_PTR_LOOP instead
187// of hanging. Returns a NEGATIVE verdict on error (caller negates).
188func nx_dns_skip_name(buf: *u8, n: i64, start: i64) -> i64 {
189 if start < 0 { return -NX_DNS_VERDICT_PTR_OUT_OF_RANGE }
190 if start >= n { return -NX_DNS_VERDICT_PTR_OUT_OF_RANGE }
191 var cur: i64 = start
192 var seen_ptr: i64 = 0
193 var end_offset: i64 = -1
194 var hops: i64 = 0
195 var verdict: i64 = NX_DNS_VERDICT_OK
196 var done: i64 = 0
197 while done == 0 && verdict == NX_DNS_VERDICT_OK && hops < NX_DNS_MAX_PTR_HOPS {
198 if cur >= n {
199 verdict = NX_DNS_VERDICT_PTR_OUT_OF_RANGE
200 } else {
201 let b: i64 = buf[cur] & 0xff
202 if b == 0 {
203 cur = cur + 1
204 if end_offset < 0 { end_offset = cur }
205 done = 1
206 } else {
207 let high2: i64 = b & 0xc0
208 if high2 == 0xc0 {
209 if cur + 1 >= n {
210 verdict = NX_DNS_VERDICT_PTR_OUT_OF_RANGE
211 } else {
212 let lo: i64 = buf[cur + 1] & 0xff
213 let tgt: i64 = ((b & 0x3f) << 8) | lo
214 if tgt >= n {
215 verdict = NX_DNS_VERDICT_PTR_OUT_OF_RANGE
216 } else {
217 if seen_ptr == 0 {
218 end_offset = cur + 2
219 seen_ptr = 1
220 }
221 cur = tgt
222 hops = hops + 1
223 }
224 }
225 } else {
226 if high2 != 0 {
227 verdict = NX_DNS_VERDICT_BAD_LABEL
228 } else {
229 if b > 63 {
230 verdict = NX_DNS_VERDICT_BAD_LABEL
231 } else {
232 if cur + 1 + b > n {
233 verdict = NX_DNS_VERDICT_PTR_OUT_OF_RANGE
234 } else {
235 cur = cur + 1 + b
236 }
237 }
238 }
239 }
240 }
241 }
242 }
243 if done == 1 { return end_offset }
244 if verdict != NX_DNS_VERDICT_OK { return -verdict }
245 return -NX_DNS_VERDICT_PTR_LOOP
246}
247
248// ---- Build a DNS query packet. ----
249//
250// Returns bytes written or a negative verdict. RD bit is set; QR is
251// clear (we are the client). tx_id MUST come from the caller's
252// CSPRNG (nx_csprng_u64) per RFC 5452 off-path spoof defense -- the
253// 16-bit entropy combined with source-port randomisation is the only
254// thing standing between this resolver and a cached poisoning attack.
255func nx_dns_build_query(
256 name: *u8, name_len: i64,
257 qtype: i64,
258 tx_id: i64,
259 out: *u8, out_cap: i64
260) -> i64 {
261 if out_cap < NX_DNS_HEADER_LEN + name_len + 2 + 4 {
262 return -NX_DNS_VERDICT_BUF_TOO_SMALL
263 }
264 nx_dns_put_u16_be(out, 0, tx_id & 0xffff)
265 nx_dns_put_u16_be(out, 2, NX_DNS_FLAG_RD)
266 nx_dns_put_u16_be(out, 4, 1)
267 nx_dns_put_u16_be(out, 6, 0)
268 nx_dns_put_u16_be(out, 8, 0)
269 nx_dns_put_u16_be(out, 10, 0)
270 var o: i64 = NX_DNS_HEADER_LEN
271 let qname_len: i64 = nx_dns_encode_name(name, name_len, out + o, out_cap - o)
272 if qname_len < 0 { return qname_len }
273 o = o + qname_len
274 nx_dns_put_u16_be(out, o, qtype)
275 o = o + 2
276 nx_dns_put_u16_be(out, o, NX_DNS_CLASS_IN)
277 o = o + 2
278 return o
279}
280
281// ---- Parse a response, extract the first A record's IPv4 into out4. ----
282//
283// Returns NX_DNS_VERDICT_OK on success or a non-OK verdict. out4
284// receives 4 bytes (a, b, c, d) on success. expected_tx_id MUST match
285// the query's tx_id; mismatch is the primary spoof-defense gate.
286func nx_dns_parse_response_a(
287 buf: *u8, n: i64,
288 expected_tx_id: i64,
289 out4: *u8
290) -> i64 {
291 if n < NX_DNS_HEADER_LEN { return NX_DNS_VERDICT_BAD_HEADER }
292 let tx: i64 = nx_dns_get_u16_be(buf, 0)
293 if tx != (expected_tx_id & 0xffff) { return NX_DNS_VERDICT_TXID_MISMATCH }
294 let flags: i64 = nx_dns_get_u16_be(buf, 2)
295 if (flags & NX_DNS_FLAG_QR) == 0 { return NX_DNS_VERDICT_NOT_RESPONSE }
296 if (flags & NX_DNS_FLAG_TC) != 0 { return NX_DNS_VERDICT_TRUNCATED }
297 let rcode: i64 = flags & NX_DNS_RCODE_MASK
298 if rcode != 0 { return NX_DNS_VERDICT_RCODE_NONZERO }
299 let qdcount: i64 = nx_dns_get_u16_be(buf, 4)
300 let ancount: i64 = nx_dns_get_u16_be(buf, 6)
301 if qdcount > NX_DNS_MAX_RECORDS { return NX_DNS_VERDICT_BAD_HEADER }
302 if ancount > NX_DNS_MAX_RECORDS { return NX_DNS_VERDICT_BAD_HEADER }
303 if ancount == 0 { return NX_DNS_VERDICT_NO_A_RECORD }
304 var o: i64 = NX_DNS_HEADER_LEN
305 var qi: i64 = 0
306 while qi < qdcount {
307 let after_name: i64 = nx_dns_skip_name(buf, n, o)
308 if after_name < 0 { return -after_name }
309 if after_name + 4 > n { return NX_DNS_VERDICT_BAD_HEADER }
310 o = after_name + 4
311 qi = qi + 1
312 }
313 var ai: i64 = 0
314 var found: i64 = 0
315 var verdict: i64 = NX_DNS_VERDICT_NO_A_RECORD
316 while ai < ancount && found == 0 {
317 let after_name2: i64 = nx_dns_skip_name(buf, n, o)
318 if after_name2 < 0 { return -after_name2 }
319 if after_name2 + 10 > n { return NX_DNS_VERDICT_BAD_HEADER }
320 let rtype: i64 = nx_dns_get_u16_be(buf, after_name2)
321 let rclass: i64 = nx_dns_get_u16_be(buf, after_name2 + 2)
322 let rdlength: i64 = nx_dns_get_u16_be(buf, after_name2 + 8)
323 let rdata_off: i64 = after_name2 + 10
324 if rdata_off + rdlength > n { return NX_DNS_VERDICT_BAD_HEADER }
325 if rtype == NX_DNS_TYPE_A && rclass == NX_DNS_CLASS_IN && rdlength == 4 {
326 out4[0] = buf[rdata_off] & 0xff
327 out4[1] = buf[rdata_off + 1] & 0xff
328 out4[2] = buf[rdata_off + 2] & 0xff
329 out4[3] = buf[rdata_off + 3] & 0xff
330 found = 1
331 verdict = NX_DNS_VERDICT_OK
332 }
333 o = rdata_off + rdlength
334 ai = ai + 1
335 }
336 return verdict
337}
338
339// ---- Sealed-enum validity gate. ----
340func nx_dns_verdict_is_valid(v: i64) -> i64 {
341 if v < 0 { return 0 }
342 if v >= NX_DNS_VERDICT_N { return 0 }
343 return 1
344}