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}