DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
coaps_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 coaps_server.cpp
6 * @brief CoAP-over-DTLS server front-end - implementation. See coaps_server.h.
7 */
8
10
11#if DETWS_ENABLE_DTLS && DETWS_ENABLE_COAP
12
14#include "services/clock.h" // detws_millis() - idle-reap clock (the DTLS PTO uses it internally too)
15#include "services/coap/coaps.h" // coaps_process()
16#include "shared_primitives/ring.h" // DetAtomic (SPSC ingest-ring cursors)
17#include <string.h>
18
19#if defined(ARDUINO)
21#endif
22
23namespace
24{
25// Largest inbound datagram buffered: a ClientHello, a client Finished, or one CoAP application record
26// (CoAP messages fit a single datagram, RFC 7252 §4.6). The server's outbound flight is larger but is
27// sent straight out, never buffered here.
28#ifndef DETWS_COAPS_MAX_DATAGRAM
29#define DETWS_COAPS_MAX_DATAGRAM 1500
30#endif
31// Scratch for one poll's outbound datagram: a full server flight (ServerHello..Finished) or a sealed
32// CoAP response. The Certificate-dominated flight is the largest thing written here.
33constexpr size_t DETWS_COAPS_OUT_CAP = 2048;
34// The HelloRetryRequest cookie binds the peer's IPv4 address (4) + port (2); the transport is IPv4.
35constexpr size_t DETWS_COAPS_PEER_SER = 6;
36
37// One buffered inbound datagram (payload + the peer it arrived from).
38struct CoapsIngest
39{
40 uint8_t data[DETWS_COAPS_MAX_DATAGRAM];
41 uint16_t len;
42 char ip[16];
43 uint16_t port;
44};
45
46// One pool slot: a DTLS connection plus the per-connection key material its config points at (so the
47// config's pointers outlive the DtlsConn) and the peer to reply to.
48struct CoapsSlot
49{
50 bool used;
51 DtlsConn conn;
52 DtlsServerConfig cfg; ///< per-slot config; its ephemeral/random pointers reference the buffers below
53 uint8_t eph[32]; ///< fresh X25519 ephemeral private key for this handshake
54 uint8_t srand[32]; ///< fresh ServerHello random for this handshake
55 char peer_ip[16];
56 uint16_t peer_port;
57 uint32_t last_ms; ///< detws_millis() of the last inbound datagram (idle-reaping clock)
58};
59
60// The DtlsConn pool + the ingest ring, owned by one instance (internal linkage). Kept separate from
61// the DRAM control state below so the large buffers group together (and could move to PSRAM like the
62// HTTP/3 pool if a large pool were ever wanted). One named owner, unreachable cross-TU.
63struct CoapsServerPoolCtx
64{
65 CoapsSlot pool[DETWS_COAPS_MAX_CONNS];
66 CoapsIngest ring[DETWS_COAPS_INGEST_RING];
67};
68CoapsServerPoolCtx s_cpool;
69
70// All CoAPs-server control state, owned by one instance (internal linkage): the ingest-ring cursors,
71// the server identity + randomness source, the bound port / running flag, and (host) the outbound
72// sink. One named owner, unreachable cross-TU.
73struct CoapsServerCtx
74{
75 DetAtomic<size_t> ring_head; ///< producer (udp / ingest) advances
76 DetAtomic<size_t> ring_tail; ///< consumer (poll) advances
77 const uint8_t *cert_der = nullptr;
78 size_t cert_len = 0;
79 uint8_t ed25519_seed[32];
80 uint8_t cookie_key[32];
81 void (*rng)(uint8_t *out, size_t len) = nullptr;
82 uint16_t port = 0;
83 bool running = false;
84#if !defined(ARDUINO)
85 CoapsServerOutFn out_sink = nullptr;
86 void *out_ctx = nullptr;
87#endif
88};
89CoapsServerCtx s_coaps;
90
91void copy_str(char *dst, size_t cap, const char *src)
92{
93 size_t n = 0;
94 if (src)
95 while (src[n] && n + 1 < cap)
96 {
97 dst[n] = src[n];
98 n++;
99 }
100 dst[n] = 0;
101}
102
103// Serialize an IPv4 dotted-quad @p ip + @p port into the address the HelloRetryRequest cookie binds, so
104// a cookie minted for one peer is worthless to another (RFC 9147 §5.1). Returns false on a malformed
105// address (then no address is bound and the peer simply cannot take the HRR path).
106bool serialize_peer(const char *ip, uint16_t port, uint8_t out[DETWS_COAPS_PEER_SER])
107{
108 uint32_t oct = 0;
109 int octets = 0;
110 int ndig = 0;
111 int idx = 0;
112 for (const char *p = ip;; p++)
113 {
114 if (*p >= '0' && *p <= '9')
115 {
116 oct = oct * 10 + (uint32_t)(*p - '0');
117 ndig++;
118 if (oct > 255 || ndig > 3)
119 return false;
120 }
121 else if (*p == '.' || *p == 0)
122 {
123 if (ndig == 0)
124 return false;
125 out[idx++] = (uint8_t)oct;
126 octets++;
127 oct = 0;
128 ndig = 0;
129 if (*p == 0)
130 break;
131 }
132 else
133 return false;
134 }
135 if (octets != 4)
136 return false;
137 out[4] = (uint8_t)(port >> 8);
138 out[5] = (uint8_t)(port & 0xFF);
139 return true;
140}
141
142void server_send(const char *ip, uint16_t port, const uint8_t *data, size_t len)
143{
144#if defined(ARDUINO)
145 det_udp_listener_sendto(s_coaps.port, ip, port, data, len);
146#else
147 if (s_coaps.out_sink)
148 s_coaps.out_sink(s_coaps.out_ctx, data, len, ip, port);
149#endif
150}
151
152// --- ingest ring (SPSC: one producer fills, coaps_server_poll consumes) ------------------------
153bool ring_push(const uint8_t *dg, size_t len, const char *ip, uint16_t port)
154{
155 if (len == 0 || len > DETWS_COAPS_MAX_DATAGRAM)
156 return false;
157 size_t head = s_coaps.ring_head;
158 size_t next = (head + 1) % DETWS_COAPS_INGEST_RING;
159 if (next == (size_t)s_coaps.ring_tail)
160 return false; // ring full: drop (DTLS recovers via retransmission)
161 CoapsIngest *e = &s_cpool.ring[head];
162 memcpy(e->data, dg, len);
163 e->len = (uint16_t)len;
164 copy_str(e->ip, sizeof e->ip, ip);
165 e->port = port;
166 s_coaps.ring_head = next; // publish after the record is fully written
167 return true;
168}
169
170bool ring_pop(CoapsIngest *out)
171{
172 size_t tail = s_coaps.ring_tail;
173 if (tail == (size_t)s_coaps.ring_head)
174 return false;
175 *out = s_cpool.ring[tail];
176 s_coaps.ring_tail = (tail + 1) % DETWS_COAPS_INGEST_RING;
177 return true;
178}
179
180// --- slot pool (keyed by peer address, or by connection id once one is negotiated) -------------
181CoapsSlot *slot_by_peer(const char *ip, uint16_t port)
182{
183 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
184 {
185 CoapsSlot *s = &s_cpool.pool[i];
186 if (s->used && s->peer_port == port && strcmp(s->peer_ip, ip) == 0)
187 return s;
188 }
189 return nullptr;
190}
191
192// Find the connection whose negotiated connection id (RFC 9146 / RFC 9147 §9) matches the id carried in
193// an inbound CID record, so a peer that has roamed to a new address is still routed to its connection.
194// @p cid points just past the record's first byte; @p avail is the bytes available there.
195CoapsSlot *slot_by_cid(const uint8_t *cid, size_t avail)
196{
197 uint8_t sc[DTLS_CID_MAX];
198 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
199 {
200 CoapsSlot *s = &s_cpool.pool[i];
201 if (!s->used)
202 continue;
203 size_t sl = dtls_conn_local_cid(&s->conn, sc);
204 if (sl && sl <= avail && memcmp(cid, sc, sl) == 0)
205 return s;
206 }
207 return nullptr;
208}
209
210CoapsSlot *alloc_slot()
211{
212 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
213 if (!s_cpool.pool[i].used)
214 {
215 CoapsSlot *s = &s_cpool.pool[i];
216 memset(s, 0, sizeof *s);
217 s->used = true;
218 return s;
219 }
220 return nullptr;
221}
222
223// Open a connection for a datagram from a peer with no existing slot.
224CoapsSlot *open_conn(const char *ip, uint16_t port)
225{
226 CoapsSlot *s = alloc_slot();
227 if (!s)
228 return nullptr;
229 s_coaps.rng(s->eph, sizeof s->eph); // fresh X25519 ephemeral private key
230 s_coaps.rng(s->srand, sizeof s->srand); // fresh ServerHello random
231 s->cfg.cert_der = s_coaps.cert_der;
232 s->cfg.cert_len = s_coaps.cert_len;
233 s->cfg.ed25519_seed = s_coaps.ed25519_seed;
234 s->cfg.ephemeral_priv = s->eph;
235 s->cfg.server_random = s->srand;
236 s->cfg.cookie_key = s_coaps.cookie_key;
237 uint8_t paddr[DETWS_COAPS_PEER_SER];
238 bool ok = serialize_peer(ip, port, paddr);
239 dtls_conn_init(&s->conn, &s->cfg, ok ? paddr : nullptr, ok ? sizeof paddr : 0);
240 copy_str(s->peer_ip, sizeof s->peer_ip, ip);
241 s->peer_port = port;
242 return s;
243}
244
245#if defined(ARDUINO)
246void udp_ingest_cb(const uint8_t *data, size_t len, DetUdpPeer *peer, void * /*ctx*/)
247{
248 char ip[16];
249 uint16_t port = 0;
250 if (!det_udp_peer_addr(peer, ip, sizeof ip, &port))
251 return;
252 ring_push(data, len, ip, port);
253}
254#endif
255
256// Route one ingested datagram to its peer slot (opening one for a fresh peer's ClientHello) and drive
257// the DTLS handshake / CoAP exchange through the bridge.
258void coaps_route_datagram(const CoapsIngest *ig, uint32_t now, uint8_t *out, size_t out_cap)
259{
260 // A DTLSCiphertext with the C bit (0b001C....) carries a connection id: route by it so a peer that
261 // has roamed to a new address still reaches its connection (RFC 9146 / RFC 9147 §9). Otherwise route
262 // by source address; a fresh peer's (plaintext) ClientHello opens a slot.
263 bool cid_rec = ig->len >= 1 && (ig->data[0] & 0xE0) == 0x20 && (ig->data[0] & 0x10);
264 CoapsSlot *s = cid_rec ? slot_by_cid(ig->data + 1, ig->len - 1) : nullptr;
265 if (!s)
266 s = slot_by_peer(ig->ip, ig->port);
267 if (!s && !cid_rec)
268 s = open_conn(ig->ip, ig->port);
269 if (!s)
270 return; // unknown connection id, or pool full: drop (the peer retransmits / DTLS PTO recovers)
271 // Address migration: a valid CID record from a new address updates where we send replies.
272 if (cid_rec && (s->peer_port != ig->port || strcmp(s->peer_ip, ig->ip) != 0))
273 {
274 copy_str(s->peer_ip, sizeof s->peer_ip, ig->ip);
275 s->peer_port = ig->port;
276 }
277 s->last_ms = now;
278 int n = coaps_process(&s->conn, ig->data, ig->len, out, out_cap);
279 if (n > 0)
280 server_send(s->peer_ip, s->peer_port, out, (size_t)n);
281 else if (n < 0)
282 s->used = false; // fatal handshake error: free the slot
283}
284
285// Fire the retransmission timer for a slot's outstanding flight, then reap it if the handshake failed or
286// the connection has gone idle.
287void coaps_service_slot(CoapsSlot *s, uint32_t now, uint8_t *out, size_t out_cap)
288{
289 if (!s->used)
290 return;
291 if (dtls_conn_timeout_ms(&s->conn) == 0) // 0 == due now (-1 == no timer, >0 == still pending)
292 {
293 int n = dtls_conn_on_timeout(&s->conn, out, out_cap);
294 if (n > 0)
295 server_send(s->peer_ip, s->peer_port, out, (size_t)n);
296 else if (n < 0)
297 {
298 s->used = false; // retransmission ceiling hit: abandon the handshake
299 return;
300 }
301 }
302 if (now - s->last_ms >= DETWS_COAPS_IDLE_MS) // wrap-safe idle delta (uint32_t arithmetic)
303 s->used = false;
304}
305} // namespace
306
307bool coaps_server_begin(uint16_t port, const CoapsServerConfig *cfg)
308{
309 if (!cfg || !cfg->rng || !cfg->cert_der || cfg->cert_len == 0)
310 return false;
311 s_coaps.cert_der = cfg->cert_der;
312 s_coaps.cert_len = cfg->cert_len;
313 memcpy(s_coaps.ed25519_seed, cfg->ed25519_seed, sizeof s_coaps.ed25519_seed);
314 memcpy(s_coaps.cookie_key, cfg->cookie_key, sizeof s_coaps.cookie_key);
315 s_coaps.rng = cfg->rng;
316 s_coaps.port = port ? port : DETWS_COAPS_PORT;
317 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
318 s_cpool.pool[i].used = false;
319 s_coaps.ring_head = 0;
320 s_coaps.ring_tail = 0;
321 s_coaps.running = true;
322#if defined(ARDUINO)
323 return det_udp_listen(s_coaps.port, udp_ingest_cb, nullptr);
324#else
325 return true; // host: fed through coaps_server_ingest()
326#endif
327}
328
329void coaps_server_poll()
330{
331 if (!s_coaps.running)
332 return;
333 uint32_t now = detws_millis();
334 uint8_t out[DETWS_COAPS_OUT_CAP];
335
336 // Drain queued datagrams: route each to its peer's slot (opening one for a new peer) and drive the
337 // handshake / CoAP exchange through the bridge.
338 CoapsIngest ig;
339 while (ring_pop(&ig))
340 coaps_route_datagram(&ig, now, out, sizeof out);
341
342 // Fire the retransmission timer for any outstanding flight, then reap closed / idle connections.
343 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
344 coaps_service_slot(&s_cpool.pool[i], now, out, sizeof out);
345}
346
347uint8_t coaps_server_active_conns()
348{
349 uint8_t n = 0;
350 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
351 if (s_cpool.pool[i].used)
352 n++;
353 return n;
354}
355
356void coaps_server_stop()
357{
358 s_coaps.running = false;
359 for (uint8_t i = 0; i < DETWS_COAPS_MAX_CONNS; i++)
360 s_cpool.pool[i].used = false;
361 s_coaps.ring_head = 0;
362 s_coaps.ring_tail = 0;
363}
364
365#if !defined(ARDUINO)
366void coaps_server_set_out_sink(CoapsServerOutFn fn, void *ctx)
367{
368 s_coaps.out_sink = fn;
369 s_coaps.out_ctx = ctx;
370}
371
372bool coaps_server_ingest(const uint8_t *datagram, size_t len, const char *ip, uint16_t port)
373{
374 return ring_push(datagram, len, ip, port);
375}
376#endif
377
378#endif // DETWS_ENABLE_DTLS && DETWS_ENABLE_COAP
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
CoAP over DTLS (CoAPs, RFC 7252 §9) - the bridge between the DTLS 1.3 server and the CoAP request han...
CoAP-over-DTLS server front-end - binds a UDP port to a pool of DtlsConn + the CoAPs bridge.
DTLS 1.3 server handshake state machine (RFC 9147 §5-6).
Shared single-producer / single-consumer byte-ring primitive.
bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
Copy a received peer's source IPv4 address (dotted-quad) and port out.
Definition udp.cpp:224
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
bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a datagram from the listener bound on listen_port to an address.
Definition udp.cpp:234
Layer 4 (Transport) - connectionless UDP datagram service.