ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 "protocore_config.h"
11
12#if PC_ENABLE_DNS_SERVER
13
14#include <string.h> // memcpy, strlen
15
16#if defined(ARDUINO)
18#endif
19namespace
20{
21// Case-insensitive ASCII string equality (DNS names are case-insensitive).
22bool ci_eq(const char *a, const char *b)
23{
24 while (*a && *b)
25 {
26 char ca = *a;
27 char cb = *b;
28 if (ca >= 'A' && ca <= 'Z')
29 {
30 ca += 32;
31 }
32 if (cb >= 'A' && cb <= 'Z')
33 {
34 cb += 32;
35 }
36 if (ca != cb)
37 {
38 return false;
39 }
40 a++;
41 b++;
42 }
43 return *a == *b;
44}
45
46// Parse the first question: write the dotted name into @p name, set *qtype and *qend (the
47// byte just past QTYPE/QCLASS). Returns false on a malformed or over-long question.
48bool parse_question(const uint8_t *q, size_t qlen, char *name, size_t name_cap, uint16_t *qtype, size_t *qend)
49{
50 // GCOVR_EXCL_START the sole caller (pc_dns_server_build_response) already rejects qlen < 12
51 if (qlen < 12)
52 {
53 return false;
54 }
55 // GCOVR_EXCL_STOP
56 size_t i = 12;
57 size_t n = 0;
58 for (;;)
59 {
60 if (i >= qlen)
61 {
62 return false;
63 }
64 uint8_t len = q[i++];
65 if (len == 0)
66 {
67 break;
68 }
69 if (len & 0xC0) // a compression pointer is illegal inside a question
70 {
71 return false;
72 }
73 if (i + len > qlen)
74 {
75 return false;
76 }
77 if (n)
78 {
79 if (n + 1 >= name_cap)
80 {
81 return false;
82 }
83 name[n++] = '.';
84 }
85 for (uint8_t k = 0; k < len; k++)
86 {
87 if (n + 1 >= name_cap)
88 {
89 return false;
90 }
91 name[n++] = (char)q[i++];
92 }
93 }
94 name[n] = '\0';
95 if (i + 4 > qlen)
96 {
97 return false;
98 }
99 *qtype = (uint16_t)((q[i] << 8) | q[i + 1]);
100 *qend = i + 4;
101 return true;
102}
103} // namespace
104
105size_t pc_dns_server_build_response(const uint8_t *query, size_t qlen, uint32_t ttl, DnsResolveFn resolve, uint8_t *out,
106 size_t out_cap)
107{
108 if (!query || !out || !resolve || qlen < 12)
109 {
110 return 0;
111 }
112
113 // A valid header but a non-standard query (IQUERY / STATUS / ...): answer NOTIMP.
114 uint8_t opcode = (uint8_t)((query[2] >> 3) & 0xF);
115 if (opcode != 0)
116 {
117 if (out_cap < 12)
118 {
119 return 0;
120 }
121 memcpy(out, query, 12);
122 out[2] = (uint8_t)(0x84 | (query[2] & 0x01)); // QR=1, AA=1, RD copied
123 out[3] = 0x04; // NOTIMP
124 out[6] = out[7] = 0; // ANCOUNT 0
125 out[8] = out[9] = out[10] = out[11] = 0; // NS/AR 0
126 return 12;
127 }
128
129 char name[PC_DNS_NAME_MAX];
130 uint16_t qtype = 0;
131 size_t qend = 0;
132 if (!parse_question(query, qlen, name, sizeof(name), &qtype, &qend))
133 {
134 return 0; // malformed question - drop it rather than echo garbage back
135 }
136
137 if (qend > out_cap)
138 {
139 return 0;
140 }
141 memcpy(out, query, qend); // header + question (preserves id + question bytes)
142 out[2] = (uint8_t)(0x84 | (query[2] & 0x01)); // QR=1, OPCODE=0, AA=1, RD copied
143 out[3] = 0x00; // RA=0, RCODE=0
144 out[4] = 0x00;
145 out[5] = 0x01; // QDCOUNT = 1
146 out[8] = out[9] = out[10] = out[11] = 0; // NSCOUNT / ARCOUNT = 0
147
148 uint32_t ip = (qtype == 1) ? resolve(name) : 0; // 1 = A record
149 if (!ip)
150 {
151 out[6] = 0x00;
152 out[7] = 0x00; // ANCOUNT = 0
153 out[3] = (qtype == 1) ? 0x03 : 0; // A miss -> NXDOMAIN; other type -> no error, no answer
154 return qend;
155 }
156
157 if (qend + 16 > out_cap)
158 {
159 return 0;
160 }
161 out[6] = 0x00;
162 out[7] = 0x01; // ANCOUNT = 1
163 size_t n = qend;
164 out[n++] = 0xC0; // name: compression pointer to the question at offset 0x000C
165 out[n++] = 0x0C;
166 out[n++] = 0x00;
167 out[n++] = 0x01; // TYPE = A
168 out[n++] = 0x00;
169 out[n++] = 0x01; // CLASS = IN
170 out[n++] = (uint8_t)(ttl >> 24);
171 out[n++] = (uint8_t)(ttl >> 16);
172 out[n++] = (uint8_t)(ttl >> 8);
173 out[n++] = (uint8_t)ttl;
174 out[n++] = 0x00;
175 out[n++] = 0x04; // RDLENGTH = 4
176 out[n++] = (uint8_t)(ip >> 24);
177 out[n++] = (uint8_t)(ip >> 16);
178 out[n++] = (uint8_t)(ip >> 8);
179 out[n++] = (uint8_t)ip;
180 return n;
181}
182
183// ---------------------------------------------------------------------------
184// Built-in A-record table (host-testable; used by pc_dns_server_begin()).
185// ---------------------------------------------------------------------------
186
187namespace
188{
189// All DNS-server state, owned by one instance (internal linkage): the A-record table,
190// grouped so it is one named owner, unreachable from other translation units.
191struct DnsSrvCtx
192{
194 uint32_t ips[PC_DNS_SERVER_MAX_RECORDS];
195 size_t count = 0;
196};
197DnsSrvCtx s_dns;
198} // namespace
199
200bool pc_dns_server_add(const char *name, uint8_t a, uint8_t b, uint8_t c, uint8_t d)
201{
202 if (!name || !name[0])
203 {
204 return false;
205 }
206 size_t nlen = strnlen(name, PC_DNS_NAME_MAX);
207 if (nlen >= PC_DNS_NAME_MAX)
208 {
209 return false;
210 }
211 if (s_dns.count >= PC_DNS_SERVER_MAX_RECORDS)
212 {
213 return false;
214 }
215 memcpy(s_dns.names[s_dns.count], name, nlen + 1);
216 s_dns.ips[s_dns.count] = ((uint32_t)a << 24) | ((uint32_t)b << 16) | ((uint32_t)c << 8) | (uint32_t)d;
217 s_dns.count++;
218 return true;
219}
220
221uint32_t pc_dns_server_lookup(const char *name)
222{
223 if (!name)
224 {
225 return 0;
226 }
227 for (size_t i = 0; i < s_dns.count; i++)
228 {
229 if (ci_eq(s_dns.names[i], name))
230 {
231 return s_dns.ips[i];
232 }
233 }
234 return 0;
235}
236
238{
239 s_dns.count = 0;
240}
241
242#if defined(ARDUINO)
243
244namespace
245{
246void pc_dns_server_udp_handler(const uint8_t *data, size_t len, const struct pc_udp_peer *peer, void *ctx)
247{
248 (void)ctx;
249 uint8_t resp[PC_DNS_NAME_MAX + 32]; // header + question + one A answer
250 size_t n = pc_dns_server_build_response(data, len, PC_DNS_SERVER_TTL, pc_dns_server_lookup, resp, sizeof(resp));
251 if (n)
252 {
253 pc_udp_send(peer, resp, n);
254 }
255}
256} // namespace
257
259{
260 return pc_udp_listen(53, pc_dns_server_udp_handler, nullptr);
261}
262
263#else // host build: no lwIP. The codec + table above are host-tested; begin is a stub.
264
266{
267 return false;
268}
269
270#endif // ARDUINO
271
272#endif // PC_ENABLE_DNS_SERVER
Authoritative DNS server (UDP/53) - resolve local names on an offline LAN.
size_t pc_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 pc_dns_server_begin()
Start answering DNS queries on UDP/53 from the built-in table.
bool pc_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).
uint32_t(* DnsResolveFn)(const char *name)
Resolve a queried name to an IPv4 address.
Definition dns_server.h:35
void pc_dns_server_clear()
Remove every record from the built-in table.
uint32_t pc_dns_server_lookup(const char *name)
Look name up in the built-in table.
User-facing configuration for ProtoCore.
#define PC_DNS_SERVER_TTL
TTL (seconds) the DNS server puts on its answers.
#define PC_DNS_NAME_MAX
Max length of a queried/stored DNS name (bytes, incl NUL).
#define PC_DNS_SERVER_MAX_RECORDS
Max A records in the DNS server's fixed table.
bool pc_udp_listen(uint16_t port, pc_udp_handler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:226
bool pc_udp_send(const struct pc_udp_peer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:336
Layer 4 (Transport) - connectionless UDP datagram service.