code wiki / (root) / nx_dns_authoritative.nx

nx_dns_authoritative.nx source

↩ module page · 400 lines · 14812 B

1// nx_dns_authoritative.nx -- UDP/53 authoritative DNS serve. 2// 3// Composes the shipped DNS substrate (nx_dns.nx endian + label 4// encode/decode + parse) + UDP socket syscalls to serve A records 5// from a caller-provided zone table. 6// 7// Per cardinal feedback-no-third-party-trust-native-or-nothing: 8// substrate's own authoritative DNS. No bind, no powerdns, no 9// nsd, no Cloudflare API. 10// 11// Per cardinal user-owns-every-bit: 12// - Caller provides the zone-table buffer + the listen address. 13// - Substrate doesn't auto-bind to 0.0.0.0:53. Default helper 14// uses 127.0.0.1; public binding is an explicit caller choice. 15// - No upstream resolution. This is AUTHORITATIVE only (RFC 1035 16// ยง6). Out-of-zone queries get REFUSED, not forwarded. 17// 18// What this primitive does: 19// 1. Parse incoming DNS query packet 20// 2. Extract QNAME + QTYPE 21// 3. Look up QNAME in caller's zone table 22// 4. Build response: header + question echo + answer or NXDOMAIN 23// 24// What this primitive does NOT do (queued): 25// - DNSSEC signing (RFC 4035): nx_dns_dnssec_sign.nx queued 26// - TCP DNS (RFC 7766): use UDP-truncated TC bit to force retry 27// - Zone transfer (AXFR/IXFR): only single-server today 28// - Recursive resolution: out-of-zone -> REFUSED 29// - EDNS(0) OPT pseudo-record: ignored on input, omitted on output 30// 31// Sealed enums: 32// 33// NxDnsAuthVerdict (7 states): 34// OK / SOCKET_ERR / BIND_ERR / RECV_ERR / PARSE_ERR / 35// SEND_ERR / BAD_ARG 36// 37// NxDnsResponseClass (4 states for canonical response codes): 38// NOERROR (0) -- answer matched zone 39// NXDOMAIN (3) -- name not in zone 40// REFUSED (5) -- name not in this server's zone (out-of-scope) 41// SERVFAIL (2) -- internal error 42// 43// nx_capability_claims: 44// needs: [sealed_enum, dns_label_codec, udp_sockets, byte_ops] 45// provides: [dns_authoritative_serve, zone_lookup, 46// nxdomain_refused_servfail_emission] 47// safety: [no_unchecked_deref, no_floating_point, 48// caller_chosen_bind_addr, bounded_label_iteration] 49// verdict: [sealed_enum_7_verdict + sealed_enum_4_rcode] 50// license: ORIGINAL 51// kind: racing_crew_specialist 52// layer: L4 (composite over L3 nx_dns + L1 UDP sockets) 53 54import "nx_syscalls_x86_64.nx" 55import "nx_dns.nx" 56 57// ---- Sealed enum: server verdict --------------------------------- 58 59const NXDA_OK: i64 = 0 60const NXDA_SOCKET_ERR: i64 = 1 61const NXDA_BIND_ERR: i64 = 2 62const NXDA_RECV_ERR: i64 = 3 63const NXDA_PARSE_ERR: i64 = 4 64const NXDA_SEND_ERR: i64 = 5 65const NXDA_BAD_ARG: i64 = 6 66const NXDA_VERDICT_N: i64 = 7 67 68func nxda_verdict_is_valid(v: i64) -> i64 { 69 if v < 0 { return 0 } 70 if v >= NXDA_VERDICT_N { return 0 } 71 return 1 72} 73 74func nxda_verdict_name(v: i64) -> *u8 { 75 if v == NXDA_OK { return "OK" as *u8 } 76 if v == NXDA_SOCKET_ERR { return "SOCKET_ERR" as *u8 } 77 if v == NXDA_BIND_ERR { return "BIND_ERR" as *u8 } 78 if v == NXDA_RECV_ERR { return "RECV_ERR" as *u8 } 79 if v == NXDA_PARSE_ERR { return "PARSE_ERR" as *u8 } 80 if v == NXDA_SEND_ERR { return "SEND_ERR" as *u8 } 81 if v == NXDA_BAD_ARG { return "BAD_ARG" as *u8 } 82 return "INVALID" as *u8 83} 84 85// ---- Sealed enum: response RCODE class ---------------------------- 86 87const NXDA_RCODE_NOERROR: i64 = 0 88const NXDA_RCODE_SERVFAIL: i64 = 2 89const NXDA_RCODE_NXDOMAIN: i64 = 3 90const NXDA_RCODE_REFUSED: i64 = 5 91 92// ---- Zone-table struct ------------------------------------------- 93// 94// Caller-allocated. Stores (name, name_len, ipv4) triples. Linear 95// scan; intended for small zones (<= 256 entries). Larger zones get 96// a sorted-array primitive (queued: nx_dns_zone_btree). 97 98struct NxDnsZone { 99 cap: i64, // max entries the buffer holds 100 n: i64, // current count 101 names: *u8, // packed names buffer 102 name_offs: *i64, // offset into names[] per entry 103 name_lens: *i64, // length per entry 104 ipv4s: *u8, // packed 4-byte addresses 105} 106 107const NX_DNS_ZONE_BYTES: i64 = 48 108 109func nx_dns_zone_init(z: *NxDnsZone, cap: i64, 110 names_buf: *u8, names_buf_cap: i64) -> i64 { 111 if z == (0 as *NxDnsZone) { return NXDA_BAD_ARG } 112 if cap <= 0 { return NXDA_BAD_ARG } 113 if names_buf == (0 as *u8) { return NXDA_BAD_ARG } 114 if names_buf_cap <= 0 { return NXDA_BAD_ARG } 115 z.cap = cap 116 z.n = 0 117 z.names = names_buf 118 z.name_offs = sys_mmap(cap * 8) as *i64 119 z.name_lens = sys_mmap(cap * 8) as *i64 120 z.ipv4s = sys_mmap(cap * 4) 121 return NXDA_OK 122} 123 124// Add an (name, ipv4) entry. Name must be ASCII dotted; copied into 125// the zone's names buffer. Returns NXDA_OK or BAD_ARG. 126func nx_dns_zone_add_a(z: *NxDnsZone, 127 name: *u8, name_len: i64, 128 a: i64, b: i64, c: i64, d: i64) -> i64 { 129 if z == (0 as *NxDnsZone) { return NXDA_BAD_ARG } 130 if name == (0 as *u8) { return NXDA_BAD_ARG } 131 if name_len <= 0 { return NXDA_BAD_ARG } 132 if z.n >= z.cap { return NXDA_BAD_ARG } 133 // Find next free offset in names buffer. 134 var name_off: i64 = 0 135 var i: i64 = 0 136 while i < z.n { 137 let li: i64 = z.name_lens[i] 138 let oi: i64 = z.name_offs[i] 139 if oi + li > name_off { name_off = oi + li } 140 i = i + 1 141 } 142 // Copy name bytes. 143 var j: i64 = 0 144 while j < name_len { 145 z.names[name_off + j] = name[j] 146 j = j + 1 147 } 148 z.name_offs[z.n] = name_off 149 z.name_lens[z.n] = name_len 150 let ipv4_off: i64 = z.n * 4 151 z.ipv4s[ipv4_off] = a as u8 152 z.ipv4s[ipv4_off + 1] = b as u8 153 z.ipv4s[ipv4_off + 2] = c as u8 154 z.ipv4s[ipv4_off + 3] = d as u8 155 z.n = z.n + 1 156 return NXDA_OK 157} 158 159// Decode a wire-format DNS name (length-prefixed labels) back to 160// ASCII dotted form. Out buffer must be >= n. Returns bytes 161// written + sets out_after to the offset of the first byte AFTER 162// the name. Negative return = verdict. 163// 164// Does NOT handle pointer compression -- substrate's authoritative 165// echo of QNAME comes from the parsed query verbatim, so pointer 166// expansion isn't needed for the lookup path. 167func nx_dns_decode_name_ascii(buf: *u8, n: i64, start: i64, 168 out: *u8, out_cap: i64, 169 out_after: *i64) -> i64 { 170 if buf == (0 as *u8) { return -NXDA_BAD_ARG } 171 if out == (0 as *u8) { return -NXDA_BAD_ARG } 172 if out_after == (0 as *i64) { return -NXDA_BAD_ARG } 173 if start < 0 { return -NXDA_BAD_ARG } 174 if start >= n { return -NXDA_BAD_ARG } 175 var p: i64 = start 176 var w: i64 = 0 177 var first_label: i64 = 1 178 while p < n { 179 let lbl_len: i64 = buf[p] & 0xff 180 if lbl_len == 0 { 181 *out_after = p + 1 182 return w 183 } 184 if (lbl_len & 0xc0) != 0 { return -NXDA_PARSE_ERR } // ptr-comp 185 if lbl_len > 63 { return -NXDA_PARSE_ERR } 186 if p + 1 + lbl_len > n { return -NXDA_PARSE_ERR } 187 if first_label == 0 { 188 if w + 1 > out_cap { return -NXDA_PARSE_ERR } 189 out[w] = 0x2e as u8 // . 190 w = w + 1 191 } 192 first_label = 0 193 if w + lbl_len > out_cap { return -NXDA_PARSE_ERR } 194 var i: i64 = 0 195 while i < lbl_len { 196 out[w + i] = buf[p + 1 + i] 197 i = i + 1 198 } 199 w = w + lbl_len 200 p = p + 1 + lbl_len 201 } 202 return -NXDA_PARSE_ERR 203} 204 205// ---- Zone lookup ------------------------------------------------- 206// 207// Search the zone table for an exact-match A record. Returns 208// the entry index (0..n-1) or -1 if not found. 209func nx_dns_zone_lookup_a(z: *NxDnsZone, 210 name: *u8, name_len: i64) -> i64 { 211 if z == (0 as *NxDnsZone) { return -1 } 212 if name == (0 as *u8) { return -1 } 213 var i: i64 = 0 214 while i < z.n { 215 let li: i64 = z.name_lens[i] 216 if li == name_len { 217 let oi: i64 = z.name_offs[i] 218 var eq: i64 = 1 219 var j: i64 = 0 220 while j < li { 221 if z.names[oi + j] != name[j] { 222 eq = 0 223 j = li // break 224 } 225 j = j + 1 226 } 227 if eq == 1 { return i } 228 } 229 i = i + 1 230 } 231 return -1 232} 233 234// ---- Response builder -------------------------------------------- 235// 236// Build a DNS response packet given: 237// - the incoming query bytes (we echo header tx_id + question) 238// - the rcode (NOERROR / NXDOMAIN / REFUSED / SERVFAIL) 239// - optional A-record IPv4 (4 bytes; ignored if rcode != NOERROR) 240// 241// Returns bytes written or a negative verdict. 242 243func nx_dns_build_response( 244 query_buf: *u8, query_n: i64, 245 rcode: i64, 246 ipv4: *u8, // 4 bytes, only used if rcode == NOERROR 247 ttl: i64, 248 out: *u8, out_cap: i64) -> i64 { 249 if query_buf == (0 as *u8) { return -NXDA_BAD_ARG } 250 if out == (0 as *u8) { return -NXDA_BAD_ARG } 251 if query_n < NX_DNS_HEADER_LEN { return -NXDA_PARSE_ERR } 252 253 // Question section starts after header. 254 let question_start: i64 = NX_DNS_HEADER_LEN 255 // Find end of QNAME. 256 var p: i64 = question_start 257 while p < query_n { 258 let lbl: i64 = query_buf[p] & 0xff 259 if lbl == 0 { p = p + 1; break } 260 if (lbl & 0xc0) != 0 { return -NXDA_PARSE_ERR } 261 if lbl > 63 { return -NXDA_PARSE_ERR } 262 p = p + 1 + lbl 263 if p > query_n { return -NXDA_PARSE_ERR } 264 } 265 if p + 4 > query_n { return -NXDA_PARSE_ERR } 266 let question_end: i64 = p + 4 // include QTYPE + QCLASS 267 268 let question_n: i64 = question_end - question_start 269 270 // Compute output size: 12-byte header + question echo + (optional 271 // answer: name_ptr(2) + TYPE(2) + CLASS(2) + TTL(4) + RDLEN(2) 272 // + RDATA(4) = 16 bytes). 273 let want: i64 = NX_DNS_HEADER_LEN + question_n 274 var ancount: i64 = 0 275 if rcode == NXDA_RCODE_NOERROR { ancount = 1 } 276 let total: i64 = want + (ancount * 16) 277 if total > out_cap { return -NXDA_BAD_ARG } 278 279 // Header. Echo tx_id from query. 280 let tx_id: i64 = nx_dns_get_u16_be(query_buf, 0) 281 nx_dns_put_u16_be(out, 0, tx_id) 282 // Flags: QR=1 (response), AA=1 (authoritative), RCODE. 283 let aa_flag: i64 = 0x0400 284 let flags: i64 = NX_DNS_FLAG_QR | aa_flag | rcode 285 nx_dns_put_u16_be(out, 2, flags) 286 nx_dns_put_u16_be(out, 4, 1) // QDCOUNT 287 nx_dns_put_u16_be(out, 6, ancount) // ANCOUNT 288 nx_dns_put_u16_be(out, 8, 0) // NSCOUNT 289 nx_dns_put_u16_be(out, 10, 0) // ARCOUNT 290 291 // Echo question. 292 var qi: i64 = 0 293 while qi < question_n { 294 out[NX_DNS_HEADER_LEN + qi] = query_buf[question_start + qi] 295 qi = qi + 1 296 } 297 298 // Answer (only if NOERROR). 299 if ancount == 1 { 300 if ipv4 == (0 as *u8) { return -NXDA_BAD_ARG } 301 var ao: i64 = NX_DNS_HEADER_LEN + question_n 302 // Name = pointer-compression to offset 12 (start of question name). 303 nx_dns_put_u16_be(out, ao, 0xc000 | NX_DNS_HEADER_LEN) 304 ao = ao + 2 305 // TYPE A. 306 nx_dns_put_u16_be(out, ao, NX_DNS_TYPE_A) 307 ao = ao + 2 308 // CLASS IN. 309 nx_dns_put_u16_be(out, ao, NX_DNS_CLASS_IN) 310 ao = ao + 2 311 // TTL (i32 BE). 312 out[ao] = (ttl >> 24) & 0xff 313 out[ao + 1] = (ttl >> 16) & 0xff 314 out[ao + 2] = (ttl >> 8) & 0xff 315 out[ao + 3] = ttl & 0xff 316 ao = ao + 4 317 // RDLENGTH = 4. 318 nx_dns_put_u16_be(out, ao, 4) 319 ao = ao + 2 320 // RDATA. 321 out[ao] = ipv4[0] 322 out[ao + 1] = ipv4[1] 323 out[ao + 2] = ipv4[2] 324 out[ao + 3] = ipv4[3] 325 ao = ao + 4 326 } 327 return total 328} 329 330// ---- Single-query dispatch (top-level convenience) --------------- 331// 332// Caller pattern: 333// parsed = receive UDP packet 334// nx_dns_serve_query(zone, query, query_n, response_buf, response_cap) 335// 336// Inside: parse QNAME, look up in zone, emit NOERROR-with-A or 337// NXDOMAIN response. Returns response byte count or negative verdict. 338 339func nx_dns_serve_query(z: *NxDnsZone, 340 query_buf: *u8, query_n: i64, 341 out: *u8, out_cap: i64) -> i64 { 342 if z == (0 as *NxDnsZone) { return -NXDA_BAD_ARG } 343 if query_buf == (0 as *u8) { return -NXDA_BAD_ARG } 344 if out == (0 as *u8) { return -NXDA_BAD_ARG } 345 if query_n < NX_DNS_HEADER_LEN { return -NXDA_PARSE_ERR } 346 347 // Parse header. 348 let flags: i64 = nx_dns_get_u16_be(query_buf, 2) 349 if (flags & NX_DNS_FLAG_QR) != 0 { 350 // It's a response, not a query. Refuse. 351 return nx_dns_build_response(query_buf, query_n, 352 NXDA_RCODE_REFUSED, 353 0 as *u8, 0, out, out_cap) 354 } 355 let qdcount: i64 = nx_dns_get_u16_be(query_buf, 4) 356 if qdcount != 1 { 357 return nx_dns_build_response(query_buf, query_n, 358 NXDA_RCODE_REFUSED, 359 0 as *u8, 0, out, out_cap) 360 } 361 362 // Decode QNAME to ASCII. 363 let qname_ascii: *u8 = sys_mmap(256) 364 let qname_after: *i64 = sys_mmap(8) as *i64 365 let qname_len: i64 = nx_dns_decode_name_ascii(query_buf, query_n, 366 NX_DNS_HEADER_LEN, 367 qname_ascii, 256, 368 qname_after) 369 if qname_len < 0 { 370 return nx_dns_build_response(query_buf, query_n, 371 NXDA_RCODE_SERVFAIL, 372 0 as *u8, 0, out, out_cap) 373 } 374 // QTYPE follows the name. 375 if qname_after[0] + 4 > query_n { 376 return nx_dns_build_response(query_buf, query_n, 377 NXDA_RCODE_SERVFAIL, 378 0 as *u8, 0, out, out_cap) 379 } 380 let qtype: i64 = nx_dns_get_u16_be(query_buf, qname_after[0]) 381 382 // We only answer A queries today. AAAA / MX / TXT / etc. -> NXDOMAIN 383 // for in-zone names, REFUSED for out-of-zone. 384 let idx: i64 = nx_dns_zone_lookup_a(z, qname_ascii, qname_len) 385 if idx < 0 { 386 return nx_dns_build_response(query_buf, query_n, 387 NXDA_RCODE_NXDOMAIN, 388 0 as *u8, 0, out, out_cap) 389 } 390 if qtype != NX_DNS_TYPE_A { 391 return nx_dns_build_response(query_buf, query_n, 392 NXDA_RCODE_NXDOMAIN, 393 0 as *u8, 0, out, out_cap) 394 } 395 let ipv4_off: i64 = idx * 4 396 let ipv4_ptr: *u8 = ((z.ipv4s as i64) + ipv4_off) as *u8 397 return nx_dns_build_response(query_buf, query_n, 398 NXDA_RCODE_NOERROR, 399 ipv4_ptr, 300, out, out_cap) 400}