code wiki / (root) / nx_dns_txt.nx

nx_dns_txt.nx source

↩ module page · 194 lines · 7903 B

1// nx_dns_txt.nx -- TXT + NS DNS response parsing for ACME DNS-01 2// propagation polling (RFC 1035 §3.3.11 NS, §3.3.14 TXT). 3// 4// nx_dns.nx ships an A-record answer-section walker (nx_dns_parse_response_a) 5// but ACME DNS-01 propagation checking needs two more record types: 6// - TXT: is the _acme-challenge value visible yet? (the challenge proof) 7// - NS: who are the AUTHORITATIVE nameservers for the zone? (so we can 8// query them DIRECTLY and dodge a public resolver's stale cache of 9// the previous run's TXT set -- exactly what Let's Encrypt checks). 10// 11// Pure byte parsing over a caller-supplied response buffer -- composes 12// nx_dns's bounded name-skip (anti-cycle) + endian helpers. No syscalls. 13// 14// license_tier: ORIGINAL (composes nx_dns RFC 1035 primitives) 15// genealogy_id: international-research-sources/ietf/rfc_1035 16 17import "nx_syscalls.nx" 18import "nx_dns.nx" 19 20// Sealed verdicts (distinct band so they never collide with NX_DNS_VERDICT_*). 21const NX_DNS_TXT_OK: i64 = 1 // parsed; (caller inspects found flag) 22const NX_DNS_TXT_BAD_HEADER: i64 = 2 23const NX_DNS_TXT_TXID_MISMATCH: i64 = 3 24const NX_DNS_TXT_NOT_RESPONSE: i64 = 4 25const NX_DNS_TXT_TRUNCATED: i64 = 5 26const NX_DNS_TXT_RCODE_NONZERO: i64 = 6 // NXDOMAIN / SERVFAIL / ... 27const NX_DNS_TXT_PARSE_FAIL: i64 = 7 28 29// Walk the answer section of a DNS response and test whether ANY TXT 30// record's content EQUALS want[0..want_len). RFC 1035 §3.3.14: TXT rdata 31// is one-or-more <character-string>s, each = 1 length byte + that many 32// bytes. The ACME _acme-challenge value is a single 43-byte base64url 33// character-string, so we compare each char-string to `want`. 34// 35// Returns NX_DNS_TXT_OK and sets found[0]=1 iff the value is present (0 if 36// the name resolves but the value isn't there yet), or a non-OK verdict on 37// a malformed / NXDOMAIN / spoofed (tx mismatch) response. 38func nx_dns_txt_contains( 39 buf: *u8, n: i64, expected_tx_id: i64, 40 want: *u8, want_len: i64, 41 found: *i64 42) -> i64 { 43 found[0] = 0 44 if n < NX_DNS_HEADER_LEN { return NX_DNS_TXT_BAD_HEADER } 45 let tx: i64 = nx_dns_get_u16_be(buf, 0) 46 if tx != (expected_tx_id & 0xffff) { return NX_DNS_TXT_TXID_MISMATCH } 47 let flags: i64 = nx_dns_get_u16_be(buf, 2) 48 if (flags & NX_DNS_FLAG_QR) == 0 { return NX_DNS_TXT_NOT_RESPONSE } 49 if (flags & NX_DNS_FLAG_TC) != 0 { return NX_DNS_TXT_TRUNCATED } 50 let rcode: i64 = flags & NX_DNS_RCODE_MASK 51 if rcode != 0 { return NX_DNS_TXT_RCODE_NONZERO } 52 let qdcount: i64 = nx_dns_get_u16_be(buf, 4) 53 let ancount: i64 = nx_dns_get_u16_be(buf, 6) 54 if qdcount > NX_DNS_MAX_RECORDS { return NX_DNS_TXT_BAD_HEADER } 55 if ancount > NX_DNS_MAX_RECORDS { return NX_DNS_TXT_BAD_HEADER } 56 57 var o: i64 = NX_DNS_HEADER_LEN 58 // skip the question section 59 var qi: i64 = 0 60 while qi < qdcount { 61 let an: i64 = nx_dns_skip_name(buf, n, o) 62 if an < 0 { return NX_DNS_TXT_PARSE_FAIL } 63 if an + 4 > n { return NX_DNS_TXT_BAD_HEADER } 64 o = an + 4 65 qi = qi + 1 66 } 67 // walk the answers 68 var ai: i64 = 0 69 while ai < ancount { 70 let an2: i64 = nx_dns_skip_name(buf, n, o) 71 if an2 < 0 { return NX_DNS_TXT_PARSE_FAIL } 72 if an2 + 10 > n { return NX_DNS_TXT_BAD_HEADER } 73 let rtype: i64 = nx_dns_get_u16_be(buf, an2) 74 let rdlength: i64 = nx_dns_get_u16_be(buf, an2 + 8) 75 let rdata_off: i64 = an2 + 10 76 if rdata_off + rdlength > n { return NX_DNS_TXT_BAD_HEADER } 77 if rtype == NX_DNS_TYPE_TXT { 78 // iterate the character-strings inside this TXT rdata 79 var cs: i64 = rdata_off 80 let rdend: i64 = rdata_off + rdlength 81 while cs < rdend { 82 let slen: i64 = buf[cs] & 0xff 83 let sstart: i64 = cs + 1 84 if sstart + slen > rdend { cs = rdend } // malformed -> stop this rdata 85 else { 86 if slen == want_len { 87 var eq: i64 = 1 88 var k: i64 = 0 89 while k < want_len { 90 if (buf[sstart + k] & 0xff) != (want[k] & 0xff) { eq = 0; k = want_len } 91 else { k = k + 1 } 92 } 93 if eq == 1 { found[0] = 1 } 94 } 95 cs = sstart + slen 96 } 97 } 98 } 99 o = rdata_off + rdlength 100 ai = ai + 1 101 } 102 return NX_DNS_TXT_OK 103} 104 105// Extract the FIRST NS hostname from a response's ANSWER or AUTHORITY 106// section into out (NUL-terminated, dotted). RFC 1035 §3.3.11: NS rdata is 107// a single <domain-name> (may use compression pointers back into the 108// packet). Returns the hostname length (>0) or 0 if none / malformed. 109// 110// We decode the name manually (following one level of compression) rather 111// than reuse skip_name, because we need the BYTES, not just the end offset. 112func nx_dns_decode_name_at(buf: *u8, n: i64, start: i64, out: *u8, out_cap: i64) -> i64 { 113 var o: i64 = 0 114 var cur: i64 = start 115 var hops: i64 = 0 116 var first_label: i64 = 1 117 while hops < NX_DNS_MAX_PTR_HOPS { 118 if cur < 0 { return 0 } 119 if cur >= n { return 0 } 120 let b: i64 = buf[cur] & 0xff 121 if b == 0 { return o } // end of name 122 let high2: i64 = b & 0xc0 123 if high2 == 0xc0 { 124 if cur + 1 >= n { return 0 } 125 let lo: i64 = buf[cur + 1] & 0xff 126 cur = ((b & 0x3f) << 8) | lo 127 hops = hops + 1 128 } else { 129 if high2 != 0 { return 0 } // reserved label type 130 if b > 63 { return 0 } 131 if cur + 1 + b > n { return 0 } 132 if first_label == 0 { if o < out_cap { out[o] = 0x2e as u8; o = o + 1 } } // '.' 133 first_label = 0 134 var i: i64 = 0 135 while i < b { 136 if o < out_cap - 1 { out[o] = buf[cur + 1 + i]; o = o + 1 } 137 i = i + 1 138 } 139 cur = cur + 1 + b 140 } 141 } 142 return 0 // pointer loop budget exhausted 143} 144 145func nx_dns_first_ns( 146 buf: *u8, n: i64, expected_tx_id: i64, 147 out: *u8, out_cap: i64 148) -> i64 { 149 if n < NX_DNS_HEADER_LEN { return 0 } 150 let tx: i64 = nx_dns_get_u16_be(buf, 0) 151 if tx != (expected_tx_id & 0xffff) { return 0 } 152 let flags: i64 = nx_dns_get_u16_be(buf, 2) 153 if (flags & NX_DNS_FLAG_QR) == 0 { return 0 } 154 let qdcount: i64 = nx_dns_get_u16_be(buf, 4) 155 let ancount: i64 = nx_dns_get_u16_be(buf, 6) 156 let nscount: i64 = nx_dns_get_u16_be(buf, 8) 157 if qdcount > NX_DNS_MAX_RECORDS { return 0 } 158 if ancount > NX_DNS_MAX_RECORDS { return 0 } 159 if nscount > NX_DNS_MAX_RECORDS { return 0 } 160 161 var o: i64 = NX_DNS_HEADER_LEN 162 var qi: i64 = 0 163 while qi < qdcount { 164 let an: i64 = nx_dns_skip_name(buf, n, o) 165 if an < 0 { return 0 } 166 if an + 4 > n { return 0 } 167 o = an + 4 168 qi = qi + 1 169 } 170 // Scan ANSWER then AUTHORITY records for the first NS rdata. 171 let total: i64 = ancount + nscount 172 var ri: i64 = 0 173 while ri < total { 174 let an2: i64 = nx_dns_skip_name(buf, n, o) 175 if an2 < 0 { return 0 } 176 if an2 + 10 > n { return 0 } 177 let rtype: i64 = nx_dns_get_u16_be(buf, an2) 178 let rdlength: i64 = nx_dns_get_u16_be(buf, an2 + 8) 179 let rdata_off: i64 = an2 + 10 180 if rdata_off + rdlength > n { return 0 } 181 if rtype == NX_DNS_TYPE_NS { 182 let nl: i64 = nx_dns_decode_name_at(buf, n, rdata_off, out, out_cap) 183 if nl > 0 { return nl } 184 } 185 o = rdata_off + rdlength 186 ri = ri + 1 187 } 188 return 0 189} 190 191// Compile-only smoke. Real proof in nx_acme_dns01_propagation_gate.nx. 192func main() -> i64 { 193 return 0 194}