code wiki / (root) / nx_dns.nx

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}