DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ip.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 ip.cpp
6 * @brief DetIp implementation: RFC 4291 text parsing, RFC 5952 canonical formatting, scope
7 * classification. Pure, hand-rolled (no stdlib parsing), host-identical.
8 *
9 * The public entry points (det_ip_parse / det_ip_format / det_ip_classify) are thin: the work is
10 * split into small single-purpose helpers in the anonymous namespace below - one per concern
11 * (parse a hextet, assemble the 16 bytes, find the zero run to compress, classify one family).
12 */
13
15#include <string.h>
16
17namespace
18{
19// -------------------------------------------------------------------------------------------
20// Parsing helpers (text -> bytes)
21// -------------------------------------------------------------------------------------------
22
23/** Value of one hex digit, or -1 if @p c is not a hex digit. */
24int hexval(char c)
25{
26 if (c >= '0' && c <= '9')
27 return c - '0';
28 if (c >= 'a' && c <= 'f')
29 return c - 'a' + 10;
30 if (c >= 'A' && c <= 'F')
31 return c - 'A' + 10;
32 return -1;
33}
34
35/** Parse the dotted-quad in s[0, len) into out[4]. Rejects empty octets, > 3 digits, or > 255. */
36bool parse_v4(const char *s, size_t len, uint8_t out[4])
37{
38 int oct = 0;
39 int val = -1;
40 int digits = 0;
41 for (size_t i = 0; i < len; i++)
42 {
43 char c = s[i];
44 if (c == '.') // octet separator: commit the octet in progress
45 {
46 if (digits == 0 || oct >= 3)
47 return false;
48 out[oct++] = (uint8_t)val;
49 val = -1;
50 digits = 0;
51 }
52 else if (c >= '0' && c <= '9')
53 {
54 if (digits >= 3)
55 return false;
56 val = (val < 0 ? 0 : val) * 10 + (c - '0');
57 if (val > 255)
58 return false;
59 digits++;
60 }
61 else
62 return false;
63 }
64 if (oct != 3 || digits == 0) // exactly four octets, the last one non-empty
65 return false;
66 out[3] = (uint8_t)val;
67 return true;
68}
69
70/** Parse one 1-4 digit IPv6 hextet in s[0, len) into *out. False if empty / too long / non-hex. */
71bool parse_hextet(const char *s, size_t len, uint16_t *out)
72{
73 if (len == 0 || len > 4)
74 return false;
75 int v = 0;
76 for (size_t i = 0; i < len; i++)
77 {
78 int h = hexval(s[i]);
79 if (h < 0)
80 return false;
81 v = (v << 4) | h;
82 }
83 *out = (uint16_t)v;
84 return true;
85}
86
87/**
88 * Place the @p nhead hextets that preceded "::" and the @p ntail that followed it into the 16
89 * output bytes (big-endian), zero-filling the gap the "::" stands for: head fills from the left,
90 * tail from the right.
91 */
92void assemble_v6(const uint16_t *head, int nhead, const uint16_t *tail, int ntail, uint8_t out[16])
93{
94 uint16_t g[8];
95 memset(g, 0, sizeof(g));
96 for (int k = 0; k < nhead; k++)
97 g[k] = head[k];
98 for (int k = 0; k < ntail; k++)
99 g[8 - ntail + k] = tail[k];
100 for (int k = 0; k < 8; k++)
101 {
102 out[2 * k] = (uint8_t)(g[k] >> 8);
103 out[2 * k + 1] = (uint8_t)(g[k] & 0xFF);
104 }
105}
106
107/**
108 * Parse an IPv6 text address (RFC 4291 §2.2) into out[16].
109 *
110 * The address is up to eight 16-bit hextets separated by ':'. A single "::" may stand in for one
111 * or more all-zero hextets: the groups before it fill a head list from the left, those after fill
112 * a tail list from the right, and assemble_v6() zero-fills the middle. The last token may be a
113 * dotted-quad (an embedded IPv4 tail such as ::ffff:1.2.3.4), which expands to two hextets.
114 */
115bool parse_v6(const char *s, size_t len, uint8_t out[16])
116{
117 uint16_t head[8];
118 uint16_t tail[8];
119 int nhead = 0;
120 int ntail = 0;
121 bool seen_dc = false; // have we passed the "::" yet?
122 uint16_t *cur = head; // the list currently being filled (head, then tail after "::")
123 int *ncur = &nhead;
124
125 size_t i = 0;
126 if (len >= 1 && s[0] == ':') // a leading "::" (the address opens in the zero gap)
127 {
128 if (len < 2 || s[1] != ':')
129 return false; // a lone leading ':' is illegal
130 seen_dc = true;
131 cur = tail;
132 ncur = &ntail;
133 i = 2;
134 if (i == len) // the whole address is "::"
135 {
136 memset(out, 0, 16);
137 return true;
138 }
139 }
140
141 while (i < len)
142 {
143 // Span this token up to the next ':' (noting a '.' -> it is an embedded-v4 tail).
144 size_t j = i;
145 bool has_dot = false;
146 while (j < len && s[j] != ':')
147 {
148 if (s[j] == '.')
149 has_dot = true;
150 j++;
151 }
152 size_t tlen = j - i;
153
154 if (has_dot) // embedded IPv4 -> two hextets; it must be the final token
155 {
156 uint8_t q[4];
157 if (!parse_v4(s + i, tlen, q) || *ncur > 6 || j != len)
158 return false;
159 cur[(*ncur)++] = (uint16_t)((q[0] << 8) | q[1]);
160 cur[(*ncur)++] = (uint16_t)((q[2] << 8) | q[3]);
161 break;
162 }
163
164 uint16_t hx;
165 if (!parse_hextet(s + i, tlen, &hx) || *ncur >= 8)
166 return false;
167 cur[(*ncur)++] = hx;
168 i = j;
169
170 // Consume the separator: "::" switches us to the tail list, a single ":" just continues.
171 if (i < len)
172 {
173 if (i + 1 < len && s[i + 1] == ':') // "::"
174 {
175 if (seen_dc)
176 return false; // only one "::" is allowed
177 seen_dc = true;
178 cur = tail;
179 ncur = &ntail;
180 i += 2;
181 if (i == len)
182 break; // trailing "::"
183 }
184 else // single ':'
185 {
186 i += 1;
187 if (i == len)
188 return false; // a trailing lone ':' is illegal
189 }
190 }
191 }
192
193 // Without "::" all eight hextets must be present; with it, the gap stands for >= 1 group.
194 int total = nhead + ntail;
195 if (seen_dc ? (total > 7) : (total != 8))
196 return false;
197
198 assemble_v6(head, nhead, tail, ntail, out);
199 return true;
200}
201
202// -------------------------------------------------------------------------------------------
203// Formatting helpers (bytes -> text)
204// -------------------------------------------------------------------------------------------
205
206/** Append the decimal form of @p v (0-255) at @p o. Returns the digit count (1-3). */
207size_t put_u8(uint8_t v, char *o)
208{
209 size_t n = 0;
210 if (v >= 100)
211 o[n++] = (char)('0' + v / 100);
212 if (v >= 10)
213 o[n++] = (char)('0' + (v / 10) % 10);
214 o[n++] = (char)('0' + v % 10);
215 return n;
216}
217
218/** Format the four bytes at @p b as "a.b.c.d" into @p out (NUL-terminated). 0 if it won't fit. */
219size_t format_v4(const uint8_t *b, char *out, size_t cap)
220{
221 char tmp[16];
222 size_t n = 0;
223 for (int k = 0; k < 4; k++)
224 {
225 if (k)
226 tmp[n++] = '.';
227 n += put_u8(b[k], tmp + n);
228 }
229 if (n + 1 > cap)
230 return 0;
231 memcpy(out, tmp, n);
232 out[n] = '\0';
233 return n;
234}
235
236/** Append @p v as lower-case hex with no leading zeros at @p o. Returns the digit count (1-4). */
237size_t put_hex16(uint16_t v, char *o)
238{
239 static const char H[] = "0123456789abcdef";
240 char t[4];
241 int n = 0;
242 do
243 {
244 t[n++] = H[v & 0xF];
245 v >>= 4;
246 } while (v);
247 for (int k = 0; k < n; k++) // reverse into place (we built it least-significant first)
248 o[k] = t[n - 1 - k];
249 return (size_t)n;
250}
251
252/**
253 * Find the longest run of >= 2 zero hextets to compress to "::" (leftmost wins a tie,
254 * RFC 5952 §4.2.3). Writes the run start to *start (-1 if none qualifies) and its length to *len.
255 */
256void longest_zero_run(const uint16_t g[8], int *start, int *len)
257{
258 int best_start = -1;
259 int best_len = 0;
260 int cur_start = -1;
261 int cur_len = 0;
262 for (int k = 0; k < 8; k++)
263 {
264 if (g[k] == 0)
265 {
266 if (cur_start < 0)
267 cur_start = k;
268 cur_len++;
269 if (cur_len > best_len)
270 {
271 best_len = cur_len;
272 best_start = cur_start;
273 }
274 }
275 else
276 {
277 cur_start = -1;
278 cur_len = 0;
279 }
280 }
281 *start = (best_len >= 2) ? best_start : -1;
282 *len = best_len;
283}
284
285// -------------------------------------------------------------------------------------------
286// Classification helpers
287// -------------------------------------------------------------------------------------------
288
289/** True if the 16 v6 bytes carry the ::ffff:0:0/96 IPv4-mapped prefix (RFC 4291 §2.5.5.2). */
290bool is_v4_mapped_bytes(const uint8_t *b)
291{
292 for (int k = 0; k < 10; k++)
293 if (b[k])
294 return false;
295 return b[10] == 0xff && b[11] == 0xff;
296}
297
298/** Classify the four v4 bytes at @p b. */
299DetIpScope classify_v4(const uint8_t *b)
300{
301 if (b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 0)
302 return DetIpScope::DET_IP_SCOPE_UNSPECIFIED; // 0.0.0.0
303 if (b[0] == 127)
304 return DetIpScope::DET_IP_SCOPE_LOOPBACK; // 127/8
305 if (b[0] == 169 && b[1] == 254)
306 return DetIpScope::DET_IP_SCOPE_LINK_LOCAL; // 169.254/16
307 if (b[0] == 10 || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) || (b[0] == 192 && b[1] == 168))
308 return DetIpScope::DET_IP_SCOPE_PRIVATE; // RFC 1918
309 if (b[0] >= 224 && b[0] <= 239)
312}
313
314/** Classify the sixteen v6 bytes at @p b (v4-mapped addresses defer to their embedded v4). */
315DetIpScope classify_v6(const uint8_t *b)
316{
317 bool allzero = true;
318 for (int k = 0; k < 16; k++)
319 if (b[k])
320 {
321 allzero = false;
322 break;
323 }
324 if (allzero)
326
327 bool loopback = (b[15] == 1);
328 for (int k = 0; k < 15 && loopback; k++)
329 if (b[k])
330 loopback = false;
331 if (loopback)
333
334 if (is_v4_mapped_bytes(b))
335 return classify_v4(b + 12); // ::ffff:a.b.c.d takes the v4 scope
336 if (b[0] == 0xff)
337 return DetIpScope::DET_IP_SCOPE_MULTICAST; // ff00::/8
338 if (b[0] == 0xfe && (b[1] & 0xc0) == 0x80)
339 return DetIpScope::DET_IP_SCOPE_LINK_LOCAL; // fe80::/10
340 if ((b[0] & 0xfe) == 0xfc)
341 return DetIpScope::DET_IP_SCOPE_PRIVATE; // fc00::/7 (unique-local)
343}
344} // namespace
345
346// -------------------------------------------------------------------------------------------
347// Public API
348// -------------------------------------------------------------------------------------------
349
350bool det_ip_parse(const char *s, DetIp *out)
351{
352 if (!s || !out)
353 return false;
354 // A ':' means it is v6; a '.' (and no ':') means v4. Bound the scan to a legal length.
355 size_t len = 0;
356 bool colon = false;
357 bool dot = false;
358 while (s[len])
359 {
360 if (s[len] == ':')
361 colon = true;
362 else if (s[len] == '.')
363 dot = true;
364 if (++len > 45)
365 return false; // longer than any legal textual address
366 }
367 if (len == 0)
368 return false;
369
370 memset(out->bytes, 0, 16);
371 if (colon)
372 {
373 if (!parse_v6(s, len, out->bytes))
374 return false;
376 return true;
377 }
378 if (dot)
379 {
380 if (!parse_v4(s, len, out->bytes))
381 return false;
383 return true;
384 }
385 return false;
386}
387
388size_t det_ip_format(const DetIp *ip, char *out, size_t cap)
389{
390 if (!ip || !out || cap == 0)
391 return 0;
393 return format_v4(ip->bytes, out, cap);
395 return 0;
396
397 // IPv4-mapped addresses print with a dotted tail: ::ffff:a.b.c.d (RFC 5952 §5).
398 if (det_ip_is_v4_mapped(ip))
399 {
400 char tail[16];
401 size_t tn = format_v4(ip->bytes + 12, tail, sizeof(tail));
402 if (tn == 0 || 7 + tn + 1 > cap)
403 return 0;
404 memcpy(out, "::ffff:", 7);
405 memcpy(out + 7, tail, tn);
406 out[7 + tn] = '\0';
407 return 7 + tn;
408 }
409
410 uint16_t g[8];
411 for (int k = 0; k < 8; k++)
412 g[k] = (uint16_t)((ip->bytes[2 * k] << 8) | ip->bytes[2 * k + 1]);
413
414 int zs;
415 int zl;
416 longest_zero_run(g, &zs, &zl);
417
418 // Emit the hextets, replacing the [zs, zs+zl) run with "::" (inet_ntop6-style colon placement).
419 char tmp[DET_IP_STR_MAX];
420 size_t n = 0;
421 for (int k = 0; k < 8; k++)
422 {
423 if (zs != -1 && k >= zs && k < zs + zl)
424 {
425 if (k == zs)
426 tmp[n++] = ':'; // one colon here; the pair around the run forms "::"
427 continue;
428 }
429 if (k != 0)
430 tmp[n++] = ':';
431 n += put_hex16(g[k], tmp + n);
432 }
433 if (zs != -1 && zs + zl == 8)
434 tmp[n++] = ':'; // the run reached the end: close the trailing "::"
435
436 if (n + 1 > cap)
437 return 0;
438 memcpy(out, tmp, n);
439 out[n] = '\0';
440 return n;
441}
442
444{
445 return ip && ip->family == DetIpFamily::DET_IP_V6 && is_v4_mapped_bytes(ip->bytes);
446}
447
449{
450 if (!ip)
453 return classify_v4(ip->bytes);
455 return classify_v6(ip->bytes);
457}
458
459bool det_ip_equal(const DetIp *a, const DetIp *b)
460{
461 if (!a || !b || a->family != b->family)
462 return false;
463 int n = 0;
465 n = 4;
466 else if (a->family == DetIpFamily::DET_IP_V6)
467 n = 16;
468 if (n == 0)
469 return true; // both the same non-address family (DetIpFamily::DET_IP_NONE)
470 return memcmp(a->bytes, b->bytes, (size_t)n) == 0;
471}
472
473DetIp det_ip_from_v4_octets(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
474{
475 DetIp ip;
476 memset(&ip, 0, sizeof(ip));
478 ip.bytes[0] = a;
479 ip.bytes[1] = b;
480 ip.bytes[2] = c;
481 ip.bytes[3] = d;
482 return ip;
483}
484
485DetIp det_ip_from_v6_bytes(const uint8_t bytes[16])
486{
487 DetIp ip;
489 memcpy(ip.bytes, bytes, 16);
490 return ip;
491}
492
493uint32_t det_ip_to_v4_be(const DetIp *ip)
494{
495 if (!ip)
496 return 0;
497 const uint8_t *b = ip->bytes;
499 return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
500 if (det_ip_is_v4_mapped(ip))
501 return ((uint32_t)b[12] << 24) | ((uint32_t)b[13] << 16) | ((uint32_t)b[14] << 8) | b[15];
502 return 0;
503}
504
506{
507 if (!ip || ip->family == DetIpFamily::DET_IP_NONE)
508 return true;
509 int n = (ip->family == DetIpFamily::DET_IP_V4) ? 4 : 16;
510 for (int i = 0; i < n; i++)
511 if (ip->bytes[i])
512 return false;
513 return true;
514}
515
516bool det_ip_prefix_match(const DetIp *addr, const DetIp *net, uint8_t prefix_len)
517{
518 if (!addr || !net || addr->family != net->family)
519 return false;
520 int bits = (addr->family == DetIpFamily::DET_IP_V4) ? 32 : (addr->family == DetIpFamily::DET_IP_V6 ? 128 : 0);
521 if (bits == 0 || prefix_len > bits)
522 return false;
523 int whole = prefix_len / 8; // bytes that must match exactly
524 for (int i = 0; i < whole; i++)
525 if (addr->bytes[i] != net->bytes[i])
526 return false;
527 int rem = prefix_len % 8; // leftover high bits in the next byte
528 if (rem)
529 {
530 uint8_t mask = (uint8_t)(0xFF << (8 - rem));
531 if ((addr->bytes[whole] & mask) != (net->bytes[whole] & mask))
532 return false;
533 }
534 return true;
535}
uint32_t det_ip_to_v4_be(const DetIp *ip)
The v4 address as a big-endian (network-order) uint32 (a<<24 | b<<16 | c<<8 | d).
Definition ip.cpp:493
DetIpScope det_ip_classify(const DetIp *ip)
Classify ip into a DetIpScope.
Definition ip.cpp:448
bool det_ip_prefix_match(const DetIp *addr, const DetIp *net, uint8_t prefix_len)
CIDR containment: is addr inside the net / prefix_len block?
Definition ip.cpp:516
bool det_ip_is_unspecified(const DetIp *ip)
True if ip is empty (DetIpFamily::DET_IP_NONE) or the all-zero unspecified address (0....
Definition ip.cpp:505
size_t det_ip_format(const DetIp *ip, char *out, size_t cap)
Format ip into out as its RFC 5952 canonical text.
Definition ip.cpp:388
bool det_ip_is_v4_mapped(const DetIp *ip)
True if ip is an IPv4-mapped IPv6 address (::ffff:a.b.c.d, RFC 4291 §2.5.5.2).
Definition ip.cpp:443
bool det_ip_equal(const DetIp *a, const DetIp *b)
True if a and b are the same family and address.
Definition ip.cpp:459
bool det_ip_parse(const char *s, DetIp *out)
Parse an IPv4 or IPv6 textual address (RFC 4291 §2.2) into out.
Definition ip.cpp:350
DetIp det_ip_from_v6_bytes(const uint8_t bytes[16])
Build a v6 DetIp from 16 address bytes in network (big-endian) order.
Definition ip.cpp:485
DetIp det_ip_from_v4_octets(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
Build a v4 DetIp from four octets (a.b.c.d).
Definition ip.cpp:473
Layer 3 (Network) - a family-tagged IP address (IPv4 or IPv6) with RFC-faithful text parsing,...
#define DET_IP_STR_MAX
Longest text an det_ip_format can produce, including the NUL (RFC 5952 v4-mapped).
Definition ip.h:58
@ DET_IP_V4
IPv4 (bytes[0..3])
@ DET_IP_V6
IPv6 (bytes[0..15])
@ DET_IP_NONE
empty / unparsed
DetIpScope
Address scope, in rough order of reachability (used for allow/deny policy + logging).
Definition ip.h:41
@ DET_IP_SCOPE_GLOBAL
globally routable unicast
@ DET_IP_SCOPE_LOOPBACK
127.0.0.0/8 / ::1
@ DET_IP_SCOPE_PRIVATE
RFC1918 (10/8, 172.16/12, 192.168/16) / ULA fc00::/7.
@ DET_IP_SCOPE_UNSPECIFIED
0.0.0.0 / ::
@ DET_IP_SCOPE_MULTICAST
224.0.0.0/4 / ff00::/8
@ DET_IP_SCOPE_LINK_LOCAL
169.254.0.0/16 / fe80::/10
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
uint8_t bytes[16]
network order; v4 uses the first 4
Definition ip.h:54
DetIpFamily family
address family tag
Definition ip.h:53