DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
dns_server.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dns_server.cpp
6 * @brief Authoritative DNS server - implementation. See dns_server.h.
7 */
8
10#include "ServerConfig.h"
11
12#if DETWS_ENABLE_DNS_SERVER
13
14#include <string.h> // memcpy, strlen
15
16namespace
17{
18// Case-insensitive ASCII string equality (DNS names are case-insensitive).
19bool ci_eq(const char *a, const char *b)
20{
21 while (*a && *b)
22 {
23 char ca = *a;
24 char cb = *b;
25 if (ca >= 'A' && ca <= 'Z')
26 ca += 32;
27 if (cb >= 'A' && cb <= 'Z')
28 cb += 32;
29 if (ca != cb)
30 return false;
31 a++;
32 b++;
33 }
34 return *a == *b;
35}
36
37// Parse the first question: write the dotted name into @p name, set *qtype and *qend (the
38// byte just past QTYPE/QCLASS). Returns false on a malformed or over-long question.
39bool parse_question(const uint8_t *q, size_t qlen, char *name, size_t name_cap, uint16_t *qtype, size_t *qend)
40{
41 // GCOVR_EXCL_START the sole caller (dns_server_build_response) already rejects qlen < 12
42 if (qlen < 12)
43 return false;
44 // GCOVR_EXCL_STOP
45 size_t i = 12;
46 size_t n = 0;
47 for (;;)
48 {
49 if (i >= qlen)
50 return false;
51 uint8_t len = q[i++];
52 if (len == 0)
53 break;
54 if (len & 0xC0) // a compression pointer is illegal inside a question
55 return false;
56 if (i + len > qlen)
57 return false;
58 if (n)
59 {
60 if (n + 1 >= name_cap)
61 return false;
62 name[n++] = '.';
63 }
64 for (uint8_t k = 0; k < len; k++)
65 {
66 if (n + 1 >= name_cap)
67 return false;
68 name[n++] = (char)q[i++];
69 }
70 }
71 name[n] = '\0';
72 if (i + 4 > qlen)
73 return false;
74 *qtype = (uint16_t)((q[i] << 8) | q[i + 1]);
75 *qend = i + 4;
76 return true;
77}
78} // namespace
79
80size_t dns_server_build_response(const uint8_t *query, size_t qlen, uint32_t ttl, DnsResolveFn resolve, uint8_t *out,
81 size_t out_cap)
82{
83 if (!query || !out || !resolve || qlen < 12)
84 return 0;
85
86 // A valid header but a non-standard query (IQUERY / STATUS / ...): answer NOTIMP.
87 uint8_t opcode = (uint8_t)((query[2] >> 3) & 0xF);
88 if (opcode != 0)
89 {
90 if (out_cap < 12)
91 return 0;
92 memcpy(out, query, 12);
93 out[2] = (uint8_t)(0x84 | (query[2] & 0x01)); // QR=1, AA=1, RD copied
94 out[3] = 0x04; // NOTIMP
95 out[6] = out[7] = 0; // ANCOUNT 0
96 out[8] = out[9] = out[10] = out[11] = 0; // NS/AR 0
97 return 12;
98 }
99
100 char name[DETWS_DNS_NAME_MAX];
101 uint16_t qtype = 0;
102 size_t qend = 0;
103 if (!parse_question(query, qlen, name, sizeof(name), &qtype, &qend))
104 return 0; // malformed question - drop it rather than echo garbage back
105
106 if (qend > out_cap)
107 return 0;
108 memcpy(out, query, qend); // header + question (preserves id + question bytes)
109 out[2] = (uint8_t)(0x84 | (query[2] & 0x01)); // QR=1, OPCODE=0, AA=1, RD copied
110 out[3] = 0x00; // RA=0, RCODE=0
111 out[4] = 0x00;
112 out[5] = 0x01; // QDCOUNT = 1
113 out[8] = out[9] = out[10] = out[11] = 0; // NSCOUNT / ARCOUNT = 0
114
115 uint32_t ip = (qtype == 1) ? resolve(name) : 0; // 1 = A record
116 if (!ip)
117 {
118 out[6] = 0x00;
119 out[7] = 0x00; // ANCOUNT = 0
120 out[3] = (qtype == 1) ? 0x03 : 0; // A miss -> NXDOMAIN; other type -> no error, no answer
121 return qend;
122 }
123
124 if (qend + 16 > out_cap)
125 return 0;
126 out[6] = 0x00;
127 out[7] = 0x01; // ANCOUNT = 1
128 size_t n = qend;
129 out[n++] = 0xC0; // name: compression pointer to the question at offset 0x000C
130 out[n++] = 0x0C;
131 out[n++] = 0x00;
132 out[n++] = 0x01; // TYPE = A
133 out[n++] = 0x00;
134 out[n++] = 0x01; // CLASS = IN
135 out[n++] = (uint8_t)(ttl >> 24);
136 out[n++] = (uint8_t)(ttl >> 16);
137 out[n++] = (uint8_t)(ttl >> 8);
138 out[n++] = (uint8_t)ttl;
139 out[n++] = 0x00;
140 out[n++] = 0x04; // RDLENGTH = 4
141 out[n++] = (uint8_t)(ip >> 24);
142 out[n++] = (uint8_t)(ip >> 16);
143 out[n++] = (uint8_t)(ip >> 8);
144 out[n++] = (uint8_t)ip;
145 return n;
146}
147
148// ---------------------------------------------------------------------------
149// Built-in A-record table (host-testable; used by dns_server_begin()).
150// ---------------------------------------------------------------------------
151
152namespace
153{
154// All DNS-server state, owned by one instance (internal linkage): the A-record table,
155// grouped so it is one named owner, unreachable from other translation units.
156struct DnsSrvCtx
157{
159 uint32_t ips[DETWS_DNS_SERVER_MAX_RECORDS];
160 size_t count = 0;
161};
162DnsSrvCtx s_dns;
163} // namespace
164
165bool dns_server_add(const char *name, uint8_t a, uint8_t b, uint8_t c, uint8_t d)
166{
167 if (!name || !name[0])
168 return false;
169 size_t nlen = strnlen(name, DETWS_DNS_NAME_MAX);
170 if (nlen >= DETWS_DNS_NAME_MAX)
171 return false;
172 if (s_dns.count >= DETWS_DNS_SERVER_MAX_RECORDS)
173 return false;
174 memcpy(s_dns.names[s_dns.count], name, nlen + 1);
175 s_dns.ips[s_dns.count] = ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | (uint32_t)d;
176 s_dns.count++;
177 return true;
178}
179
180uint32_t dns_server_lookup(const char *name)
181{
182 if (!name)
183 return 0;
184 for (size_t i = 0; i < s_dns.count; i++)
185 if (ci_eq(s_dns.names[i], name))
186 return s_dns.ips[i];
187 return 0;
188}
189
190void dns_server_clear()
191{
192 s_dns.count = 0;
193}
194
195#if defined(ARDUINO)
196
198
199namespace
200{
201void dns_server_udp_handler(const uint8_t *data, size_t len, struct DetUdpPeer *peer, void *ctx)
202{
203 (void)ctx;
204 uint8_t resp[DETWS_DNS_NAME_MAX + 32]; // header + question + one A answer
205 size_t n = dns_server_build_response(data, len, DETWS_DNS_SERVER_TTL, dns_server_lookup, resp, sizeof(resp));
206 if (n)
207 det_udp_send(peer, resp, n);
208}
209} // namespace
210
211bool dns_server_begin()
212{
213 return det_udp_listen(53, dns_server_udp_handler, nullptr);
214}
215
216#else // host build: no lwIP. The codec + table above are host-tested; begin is a stub.
217
218bool dns_server_begin()
219{
220 return false;
221}
222
223#endif // ARDUINO
224
225#endif // DETWS_ENABLE_DNS_SERVER
User-facing configuration for DeterministicESPAsyncWebServer.
#define DETWS_DNS_SERVER_MAX_RECORDS
Max A records in the DNS server's fixed table.
#define DETWS_DNS_SERVER_TTL
TTL (seconds) the DNS server puts on its answers.
#define DETWS_DNS_NAME_MAX
Max length of a queried/stored DNS name (bytes, incl NUL).
Authoritative DNS server (UDP/53) - resolve local names on an offline LAN.
uint32_t(* DnsResolveFn)(const char *name)
Resolve a queried name to an IPv4 address.
Definition dns_server.h:35
size_t dns_server_build_response(const uint8_t *query, size_t qlen, uint32_t ttl, DnsResolveFn resolve, uint8_t *out, size_t out_cap)
Build a DNS reply to query. Pure - no clock, no I/O.
bool dns_server_add(const char *name, uint8_t a, uint8_t b, uint8_t c, uint8_t d)
Add an A record to the built-in table (case-insensitive name).
bool dns_server_begin()
Start answering DNS queries on UDP/53 from the built-in table.
uint32_t dns_server_lookup(const char *name)
Look name up in the built-in table.
void dns_server_clear()
Remove every record from the built-in table.
bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:178
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
Layer 4 (Transport) - connectionless UDP datagram service.