DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
listener.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 listener.cpp
6 * @brief Layer 4 (Listener) - TCP accept callback and port lifecycle.
7 *
8 * `listener_accept_cb` is the single lwIP accept callback registered for
9 * every listener. The listener index is embedded in the PCB user-data via
10 * `tcp_arg(listen_pcb, (void*)(uintptr_t)idx)` so this one function handles
11 * all ports without a lookup table.
12 *
13 * The non-static per-connection callbacks (lowlevel_recv_cb, lowlevel_sent_cb,
14 * lowlevel_err_cb) are defined in tcp.cpp and declared extern here.
15 * The transport layer's enqueue() helper calls listener_enqueue(), which is
16 * defined in this file - that indirection breaks the circular header dependency
17 * (listener.h includes tcp.h; tcp.cpp includes listener.h).
18 */
19
20#include "listener.h"
21#include "freertos/FreeRTOS.h"
22#include "freertos/queue.h"
23#include "lwip/tcp.h"
24#include "network_drivers/tls/tls.h" // TLS handshake begin (self-stubbing)
25#ifdef ARDUINO
26#include "lwip/ip_addr.h" // ip_2_ip4 / ip4_addr_get_u32 for interface tagging
27#include "lwip/priv/tcpip_priv.h" // tcpip_api_call - marshal dynamic listener ops to tcpip_thread
28#include "network_drivers/session/worker.h" // detws_worker_wake() - nudge the owning worker task
29#endif
30#include "services/clock.h" // detws_millis() pluggable monotonic clock (host-safe)
31#include <Arduino.h>
32
33// Listener pool - all storage in BSS.
35
36// Per-connection callbacks defined in tcp.cpp.
37extern err_t lowlevel_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
38extern err_t lowlevel_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len);
39extern void lowlevel_err_cb(void *arg, err_t err);
40
41// ---------------------------------------------------------------------------
42// Accept-rate throttle (fixed window, global). State persists across accepts.
43// Always compiled (unit-testable); only consulted when the feature is enabled.
44// ---------------------------------------------------------------------------
45
46// Global accept-rate-limit state, owned by one instance (internal linkage): the fixed-window
47// start and the accept count in the current window. One named owner, unreachable cross-TU.
49{
50 uint32_t window_start = 0;
51 uint16_t count = 0;
52};
53static AcceptThrottleCtx s_accept;
54
55bool listener_accept_allowed(uint32_t now_ms)
56{
57 // Unsigned subtraction wraps correctly across the millis() rollover.
58 if ((uint32_t)(now_ms - s_accept.window_start) >= DETWS_ACCEPT_THROTTLE_WINDOW_MS)
59 {
60 s_accept.window_start = now_ms;
61 s_accept.count = 0;
62 }
63 if (s_accept.count >= DETWS_ACCEPT_THROTTLE_MAX)
64 return false;
65 s_accept.count++;
66 return true;
67}
68
70{
71 s_accept.window_start = 0;
72 s_accept.count = 0;
73}
74
75// ---------------------------------------------------------------------------
76// Per-IP accept-rate throttle (fixed window per source IPv4). A bounded BSS table
77// of buckets - no heap. Always compiled (unit-testable); only consulted when the
78// feature is enabled.
79// ---------------------------------------------------------------------------
80
82{
83 DetIp addr; ///< source address (family DetIpFamily::DET_IP_NONE marks an empty bucket).
84 uint32_t window_start; ///< millis() at the start of this bucket's current window.
85 uint16_t count; ///< connections counted from this address in the window.
86};
87// Per-source-IP accept-throttle state, owned by one instance (internal linkage): the bounded
88// bucket table keyed by source address. One named owner, unreachable from any other TU.
93static IpThrottleCtx s_iptt;
94
95bool listener_accept_allowed_ip(const DetIp *ip, uint32_t now_ms)
96{
98 return true; // untrackable source - defer to the global accept throttle
99
100 int empty = -1;
101 int expired = -1;
102 int lru = 0;
103 for (int i = 0; i < DETWS_PER_IP_THROTTLE_SLOTS; i++)
104 {
105 IpThrottleBucket *b = &s_iptt.buckets[i];
107 {
108 // Unsigned subtraction wraps correctly across the millis() rollover.
109 if ((uint32_t)(now_ms - b->window_start) >= DETWS_PER_IP_THROTTLE_WINDOW_MS)
110 {
111 b->window_start = now_ms;
112 b->count = 0;
113 }
115 return false;
116 b->count++;
117 return true;
118 }
120 {
121 if (empty < 0)
122 empty = i;
123 }
124 else
125 {
126 if (expired < 0 && (uint32_t)(now_ms - b->window_start) >= DETWS_PER_IP_THROTTLE_WINDOW_MS)
127 expired = i;
128 // Track the oldest active bucket (largest elapsed) as the eviction victim.
129 if ((uint32_t)(now_ms - b->window_start) > (uint32_t)(now_ms - s_iptt.buckets[lru].window_start))
130 lru = i;
131 }
132 }
133
134 // No bucket yet for this address: claim one - empty, else expired, else evict
135 // the least-recently-started active bucket.
136 int slot = (empty >= 0) ? empty : (expired >= 0) ? expired : lru;
137 IpThrottleBucket *b = &s_iptt.buckets[slot];
138 b->addr = *ip;
139 b->window_start = now_ms;
140 b->count = 1;
141 return true; // first connection of a fresh window is always allowed
142}
143
145{
146 for (int i = 0; i < DETWS_PER_IP_THROTTLE_SLOTS; i++)
147 {
149 s_iptt.buckets[i].window_start = 0;
150 s_iptt.buckets[i].count = 0;
151 }
152}
153
154// ---------------------------------------------------------------------------
155// Source-IP allowlist (accept-time firewall). A bounded BSS table of CIDR rules
156// in host byte order. Always compiled (unit-testable); only consulted when
157// DETWS_ENABLE_IP_ALLOWLIST is set. An empty table allows everything so enabling
158// the feature before adding rules cannot lock the device out.
159// ---------------------------------------------------------------------------
160
162{
163 DetIp network; ///< network address (family DetIpFamily::DET_IP_V4 / V6; DetIpFamily::DET_IP_NONE marks unused).
164 uint8_t prefix_len; ///< CIDR prefix length: 0..32 for v4, 0..128 for v6.
165};
166// IP allowlist state, owned by one instance (internal linkage): the CIDR rule table and its
167// count (empty = allow all). One named owner, unreachable from any other translation unit.
173static IpAllowCtx s_allow;
174
175bool listener_ip_allow_add(const DetIp *network, uint8_t prefix_len)
176{
177 if (!network)
178 return false;
179 int bits =
180 (network->family == DetIpFamily::DET_IP_V4) ? 32 : (network->family == DetIpFamily::DET_IP_V6 ? 128 : -1);
181 if (bits < 0 || prefix_len > (uint8_t)bits)
182 return false; // reject a malformed family or an over-long prefix
183 if (s_allow.count >= DETWS_IP_ALLOWLIST_SLOTS)
184 return false;
185 s_allow.rules[s_allow.count].network = *network;
186 s_allow.rules[s_allow.count].prefix_len = prefix_len;
187 s_allow.count++;
188 return true;
189}
190
191bool listener_ip_allow_add_cidr(const char *cidr)
192{
193 if (!cidr)
194 return false;
195
196 // Split "address/prefix" at the slash. The address half is copied into a bounded
197 // buffer (a CIDR string is never longer than an address plus "/128") for the parser.
198 char addr[DET_IP_STR_MAX];
199 const char *slash = nullptr;
200 size_t n = 0;
201 for (const char *p = cidr; *p; p++)
202 {
203 if (*p == '/')
204 {
205 slash = p;
206 break;
207 }
208 if (n + 1 >= sizeof(addr))
209 return false; // address text too long to be valid
210 addr[n++] = *p;
211 }
212 addr[n] = '\0';
213
214 DetIp net;
216 if (!det_ip_parse(addr, &net))
217 return false;
218
219 uint8_t width = (net.family == DetIpFamily::DET_IP_V4) ? 32 : 128;
220 uint8_t prefix = width; // bare address -> host route
221 if (slash)
222 {
223 // Parse the decimal prefix by hand (no stdlib in src/); reject empty or non-digit.
224 uint32_t v = 0;
225 const char *p = slash + 1;
226 if (!*p)
227 return false;
228 for (; *p; p++)
229 {
230 if (*p < '0' || *p > '9')
231 return false;
232 v = v * 10 + (uint32_t)(*p - '0');
233 if (v > width)
234 return false; // out of range for the family
235 }
236 prefix = (uint8_t)v;
237 }
238
239 return listener_ip_allow_add(&net, prefix);
240}
241
243{
244 if (s_allow.count == 0)
245 return true; // no rules configured -> allow all (fail-open by design)
246 for (uint8_t i = 0; i < s_allow.count; i++)
247 {
248 // det_ip_prefix_match requires the same family, so a v4 peer never matches a v6 rule.
249 if (det_ip_prefix_match(ip, &s_allow.rules[i].network, s_allow.rules[i].prefix_len))
250 return true;
251 }
252 return false;
253}
254
256{
257 for (int i = 0; i < DETWS_IP_ALLOWLIST_SLOTS; i++)
259 s_allow.count = 0;
260}
261
262#if DETWS_WORKER_COUNT > 1
263// Per-worker event queues: each worker drains only its own queue, so connection
264// slots partition across workers with no shared-queue contention. Static BSS, no
265// heap. Created once (idempotent) before the first accept can fire.
266// Per-worker event-queue state, owned by one instance (internal linkage): the static queue
267// control blocks, their storage, and the queue handles. One named owner, unreachable cross-TU.
268struct ListenerQueueCtx
269{
270 StaticQueue_t wq_struct[DETWS_WORKER_COUNT];
271 uint8_t wq_storage[DETWS_WORKER_COUNT][EVT_QUEUE_DEPTH * sizeof(TcpEvt)];
272 QueueHandle_t wq[DETWS_WORKER_COUNT] = {nullptr};
273};
274static ListenerQueueCtx s_lq;
275
276void listener_worker_queues_init(void)
277{
278 for (int i = 0; i < DETWS_WORKER_COUNT; i++)
279 if (!s_lq.wq[i])
280 s_lq.wq[i] = xQueueCreateStatic(EVT_QUEUE_DEPTH, sizeof(TcpEvt), s_lq.wq_storage[i], &s_lq.wq_struct[i]);
281}
282
283QueueHandle_t listener_worker_queue(int worker_id)
284{
285 if (worker_id < 0 || worker_id >= DETWS_WORKER_COUNT)
286 return nullptr;
287 return s_lq.wq[worker_id];
288}
289#endif // DETWS_WORKER_COUNT > 1
290
291bool listener_enqueue(uint8_t listener_id, const TcpEvt *evt)
292{
293#if DETWS_WORKER_COUNT > 1
294 // Route by the slot's owner so the owning worker is the sole consumer.
295 (void)listener_id;
296 uint8_t owner = conn_pool[evt->slot_id].owner;
297 if (owner >= DETWS_WORKER_COUNT || !s_lq.wq[owner])
298 return false;
299 if (xQueueSend(s_lq.wq[owner], evt, 0) != pdTRUE)
300 return false;
301#ifdef ARDUINO
302 detws_worker_wake(owner); // nudge the owning worker so it services this now
303#endif
304#else
305 if (listener_id >= MAX_LISTENERS)
306 return false;
307 Listener *lst = &listener_pool[listener_id];
308 if (!lst->active || !lst->queue)
309 return false;
310 if (xQueueSend(lst->queue, evt, 0) != pdTRUE)
311 return false;
312#ifdef ARDUINO
313 detws_worker_wake(0); // single worker owns every slot - nudge it now
314#endif
315#endif
316 return true;
317}
318
319/**
320 * @brief lwIP accept callback - single handler for all listener ports.
321 *
322 * @p arg carries the listener index cast to a pointer via
323 * `tcp_arg(listen_pcb, (void*)(uintptr_t)idx)`. Finds a free TcpConn slot,
324 * sets its protocol, wires the per-connection callbacks, and posts EvtType::EVT_CONNECT
325 * to the owning listener's queue. Rejects the connection with ERR_ABRT when
326 * the pool is full - ERR_ABRT tells lwIP the PCB is already gone from our side.
327 */
328static err_t listener_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t err)
329{
330 if (err != ERR_OK || newpcb == nullptr)
331 return ERR_VAL;
332
333 uint8_t idx = (uint8_t)(uintptr_t)arg;
334 if (idx >= MAX_LISTENERS)
335 return ERR_VAL;
336 Listener *lst = &listener_pool[idx];
337
338#if DETWS_ENABLE_ACCEPT_THROTTLE
339 // Connection-flood defense: drop accepts beyond the per-window budget before
340 // claiming a pool slot or doing any per-connection work.
342 {
343 tcp_abort(newpcb);
344 return ERR_ABRT;
345 }
346#endif
347
348#if DETWS_ENABLE_PER_IP_THROTTLE || DETWS_ENABLE_IP_ALLOWLIST
349 // Resolve the peer's family-tagged source address once for the accept-time abuse
350 // gates below - the full IPv4 or IPv6 address, never a lossy hash. On native
351 // there is no real lwIP pcb, so it stays unspecified and the gates pass it
352 // through; the host unit tests drive those gates directly with synthetic DetIp.
353 DetIp remote;
355#ifdef ARDUINO
356 det_lwip_to_detip(&newpcb->remote_ip, &remote);
357#endif
358#endif
359
360#if DETWS_ENABLE_PER_IP_THROTTLE
361 // Per-source-IP flood defense: drop accepts beyond one address's per-window
362 // budget (the global throttle cannot tell one noisy client from many). Keyed on
363 // the full address, so an IPv6 peer cannot spray a /64 past a per-address cap.
365 {
366 tcp_abort(newpcb);
367 return ERR_ABRT;
368 }
369#endif
370
371#if DETWS_ENABLE_IP_ALLOWLIST
372 // Source-IP firewall: drop connections from addresses outside the configured
373 // allowlist (an empty allowlist allows all, so this is a no-op until rules are
374 // added). CIDR prefix match on the full v4/v6 address.
375 if (!listener_ip_allowed(&remote))
376 {
377 tcp_abort(newpcb);
378 return ERR_ABRT;
379 }
380#endif
381
382 int free_slot = -1;
383 for (int i = 0; i < MAX_CONNS; i++)
384 {
385 if (conn_pool[i].state == ConnState::CONN_FREE)
386 {
387 free_slot = i;
388 break;
389 }
390 }
391
392 if (free_slot == -1)
393 {
394 tcp_abort(newpcb);
395 return ERR_ABRT;
396 }
397
398 TcpConn *slot = &conn_pool[free_slot];
399#if DETWS_WORKER_COUNT > 1
400 // Round-robin the new connection across workers. Runs only in tcpip_thread,
401 // so the counter needs no lock. Set BEFORE the state release store so a worker
402 // that observes ConnState::CONN_ACTIVE also sees the owner, and so the EvtType::EVT_CONNECT below
403 // routes to the owner's queue.
404 static uint8_t s_next_owner = 0;
405 slot->owner = s_next_owner;
406 s_next_owner = (uint8_t)((s_next_owner + 1) % DETWS_WORKER_COUNT);
407#else
408 slot->owner = 0;
409#endif
411 slot->pcb = newpcb;
413 slot->rx_head = 0;
414 slot->rx_tail = 0;
415 slot->rx_acked = 0; // window-ack cursor starts level with an empty ring
416 slot->listener_id = idx;
417 slot->proto = lst->proto;
418
419 // Tag the ingress interface for per-route STA/AP filtering. On ESP32 compare
420 // the connection's local IP to the configured softAP IP; on native (no real
421 // pcb IP) leave it unclassified for tests to set directly.
422#ifdef ARDUINO
423 {
424 uint32_t lip = ip4_addr_get_u32(ip_2_ip4(&newpcb->local_ip));
426 }
427#else
429#endif
430
431 tcp_arg(newpcb, slot);
432
433#if DETWS_TCP_NODELAY
434 // Latency-first: disable Nagle so the final sub-MSS segment of a response (or a streamed chunk) is not held
435 // waiting for the peer's ACK of the prior segment (a ~40-200 ms delayed-ACK stall). Runs in tcpip_thread
436 // (accept callback), so touching the pcb here is safe. See DETWS_TCP_NODELAY.
437 tcp_nagle_disable(newpcb);
438#endif
439
440#if DETWS_ENABLE_TLS
441 // TLS listeners begin a handshake immediately; the session loop pumps it.
442 slot->tls = lst->tls ? 1 : 0;
443 if (lst->tls)
444 det_tls_conn_begin(free_slot);
445#else
446 slot->tls = 0;
447#endif
448 tcp_recv(newpcb, lowlevel_recv_cb);
449 tcp_sent(newpcb, lowlevel_sent_cb);
450 tcp_err(newpcb, lowlevel_err_cb);
451
453 DetConnReason::DET_CONN_R_ACCEPT);
454
455 TcpEvt evt = {EvtType::EVT_CONNECT, (uint8_t)free_slot, 0};
456 if (!listener_enqueue(idx, &evt))
457 DETWS_OBS_NOTICE((uint8_t)free_slot, ConnState::CONN_ACTIVE, DetConnReason::DET_CONN_R_DEFER_DROP);
458
459 return ERR_OK;
460}
461
462#ifdef ARDUINO
463static err_t listener_lwip_marshal(uint8_t idx, uint16_t port, bool create);
464#endif
465
466int32_t listener_add(uint8_t idx, uint16_t port, ConnProto proto, bool tls)
467{
468 if (idx >= MAX_LISTENERS)
469 return -1;
470
471 listener_stop(idx); // clean up if already active
472
473#if DETWS_WORKER_COUNT > 1
474 listener_worker_queues_init(); // create the per-worker event queues once (idempotent)
475#endif
476
477 Listener *lst = &listener_pool[idx];
478 lst->port = port;
479 lst->proto = proto;
480 lst->tls = tls;
481
482 lst->queue = xQueueCreateStatic(EVT_QUEUE_DEPTH, sizeof(TcpEvt), lst->_queue_storage, &lst->_queue_struct);
483 if (!lst->queue)
484 return -1;
485
486#ifdef ARDUINO
487 // Create the listening PCB in tcpip_thread. With lwIP core-locking (arduino-esp32
488 // 3.x / IDF 5.x) a raw tcp_new/bind/listen from the app or worker task that calls
489 // begin() asserts ("Required to lock TCPIP core functionality"), so marshal it -
490 // the same path the dynamic listener uses. Fields the accept callback reads (proto,
491 // queue) are set above, before the pcb can accept.
492 if (listener_lwip_marshal(idx, port, true) != ERR_OK)
493 {
494 vQueueDelete(lst->queue);
495 lst->queue = nullptr;
496 return -1;
497 }
498#else
499 struct tcp_pcb *pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
500 if (!pcb)
501 return -1;
502
503 err_t bind_err = tcp_bind(pcb, IP_ANY_TYPE, port);
504 if (bind_err != ERR_OK)
505 {
506 tcp_abort(pcb);
507 return -1;
508 }
509
510 lst->listen_pcb = tcp_listen_with_backlog(pcb, MAX_CONNS);
511 if (!lst->listen_pcb)
512 {
513 tcp_abort(pcb);
514 return -1;
515 }
516
517 tcp_arg(lst->listen_pcb, (void *)(uintptr_t)idx);
518 tcp_accept(lst->listen_pcb, listener_accept_cb);
519#endif
520 lst->active = true;
521
522 return 1;
523}
524
525void listener_stop(uint8_t idx)
526{
527 if (idx >= MAX_LISTENERS)
528 return;
529 Listener *lst = &listener_pool[idx];
530 if (!lst->active)
531 return;
532 lst->active = false;
533#ifdef ARDUINO
534 listener_lwip_marshal(idx, 0, false); // close the listen pcb in tcpip_thread
535#else
536 lst->listen_pcb = nullptr; // host build: no real pcb to close (matches listener_stop_dynamic)
537#endif
538 if (lst->queue)
539 {
540 vQueueDelete(lst->queue);
541 lst->queue = nullptr;
542 }
543}
544
546{
547 for (uint8_t i = 0; i < MAX_LISTENERS; i++)
548 listener_stop(i);
549}
550
551// ---------------------------------------------------------------------------
552// tcpip_thread-marshaled listener create / close. Raw lwIP tcp_new/bind/listen/close
553// must run in tcpip_thread: with lwIP core-locking (arduino-esp32 3.x / IDF 5.x) a
554// call from any other task asserts, and without it a call off tcpip_thread races the
555// stack. Both listener_add/stop (from begin()) and the dynamic listeners (SSH `ssh -R`
556// remote-forward, opened from a worker task) route through here via tcpip_api_call().
557// ---------------------------------------------------------------------------
558
559#ifdef ARDUINO
561{
562 struct tcpip_api_call_data base;
563 uint8_t idx;
564 uint16_t port;
565 bool create; // true = new+bind+listen+accept, false = close the listen pcb
566 err_t result;
567};
568
569// Runs in tcpip_thread. Creates or closes the listening PCB for listener_pool[idx].
570static err_t listener_lwip_do(struct tcpip_api_call_data *c)
571{
573 Listener *lst = &listener_pool[k->idx];
574 k->result = ERR_OK;
575 if (k->create)
576 {
577 struct tcp_pcb *pcb = tcp_new_ip_type(IPADDR_TYPE_ANY);
578 if (!pcb)
579 {
580 k->result = ERR_MEM;
581 return ERR_OK;
582 }
583 if (tcp_bind(pcb, IP_ANY_TYPE, k->port) != ERR_OK)
584 {
585 tcp_abort(pcb);
586 k->result = ERR_USE; // port already bound
587 return ERR_OK;
588 }
589 struct tcp_pcb *lp = tcp_listen_with_backlog(pcb, MAX_CONNS);
590 if (!lp)
591 {
592 tcp_abort(pcb); // tcp_listen did not consume pcb on failure
593 k->result = ERR_MEM;
594 return ERR_OK;
595 }
596 tcp_arg(lp, (void *)(uintptr_t)k->idx);
597 tcp_accept(lp, listener_accept_cb);
598 lst->listen_pcb = lp;
599 }
600 else if (lst->listen_pcb)
601 {
602 tcp_close(lst->listen_pcb);
603 lst->listen_pcb = nullptr;
604 }
605 return ERR_OK;
606}
607
608static err_t listener_lwip_marshal(uint8_t idx, uint16_t port, bool create)
609{
610 DetListenerCall k = {};
611 k.idx = idx;
612 k.port = port;
613 k.create = create;
614 tcpip_api_call(listener_lwip_do, &k.base);
615 return k.result;
616}
617#endif // ARDUINO
618
619int32_t listener_add_dynamic(uint8_t idx, uint16_t port, ConnProto proto)
620{
621 if (idx >= MAX_LISTENERS)
622 return -1;
623 listener_stop_dynamic(idx); // clean up if this slot was already active
624
625#if DETWS_WORKER_COUNT > 1
626 listener_worker_queues_init(); // idempotent (xQueueCreateStatic is task-safe)
627#endif
628
629 Listener *lst = &listener_pool[idx];
630 lst->port = port;
631 lst->proto = proto;
632 lst->tls = false; // forwarded ports are plaintext bridges
633
634 lst->queue = xQueueCreateStatic(EVT_QUEUE_DEPTH, sizeof(TcpEvt), lst->_queue_storage, &lst->_queue_struct);
635 if (!lst->queue)
636 return -1;
637
638#ifdef ARDUINO
639 // Create the listening PCB in tcpip_thread. Fields the accept callback reads
640 // (proto, queue) are set above, before the pcb can accept anything.
641 if (listener_lwip_marshal(idx, port, true) != ERR_OK)
642 {
643 vQueueDelete(lst->queue);
644 lst->queue = nullptr;
645 return -1;
646 }
647#else
648 lst->listen_pcb = nullptr; // native host: no lwIP, exercised via the accept-gate unit paths
649#endif
650
651 lst->active = true;
652 return 1;
653}
654
655void listener_stop_dynamic(uint8_t idx)
656{
657 if (idx >= MAX_LISTENERS)
658 return;
659 Listener *lst = &listener_pool[idx];
660 if (!lst->active)
661 return;
662 lst->active = false;
663#ifdef ARDUINO
664 listener_lwip_marshal(idx, 0, false); // close the listen pcb in tcpip_thread
665#else
666 lst->listen_pcb = nullptr;
667#endif
668 if (lst->queue)
669 {
670 vQueueDelete(lst->queue);
671 lst->queue = nullptr;
672 }
673}
#define DETWS_ACCEPT_THROTTLE_MAX
Max accepted connections per throttle window (see DETWS_ENABLE_ACCEPT_THROTTLE).
#define DETWS_PER_IP_THROTTLE_WINDOW_MS
Per-IP throttle window length in milliseconds (see DETWS_ENABLE_PER_IP_THROTTLE).
DetIface
Network interface a connection arrived on (for per-route filtering).
@ DETIFACE_STA
Station interface (joined to an AP / your LAN).
@ DETIFACE_ANY
Unknown / no filter (matches any interface).
@ DETIFACE_AP
softAP interface (clients joined to the device).
#define DETWS_ACCEPT_THROTTLE_WINDOW_MS
Throttle window length in milliseconds (see DETWS_ENABLE_ACCEPT_THROTTLE).
#define DETWS_PER_IP_THROTTLE_MAX
Max accepted connections per window from one source IP (see DETWS_ENABLE_PER_IP_THROTTLE).
#define DETWS_PER_IP_THROTTLE_SLOTS
Number of source IPv4 addresses tracked by the per-IP throttle (BSS bucket table).
#define MAX_LISTENERS
Maximum number of simultaneously active listener ports.
ConnProto
Application protocol spoken on a listener port or connection slot.
#define DETWS_WORKER_COUNT
Number of server worker tasks (slots partitioned i % N). Default 1.
#define EVT_QUEUE_DEPTH
Depth of the FreeRTOS event queue shared between lwIP callbacks and the main-loop task.
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
#define DETWS_IP_ALLOWLIST_SLOTS
Number of CIDR rules the source-IP allowlist can hold (BSS table).
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
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
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
#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
bool listener_enqueue(uint8_t listener_id, const TcpEvt *evt)
Post evt to the queue owned by listener listener_id.
Definition listener.cpp:291
bool listener_ip_allow_add(const DetIp *network, uint8_t prefix_len)
Add a CIDR rule to the source-IP allowlist.
Definition listener.cpp:175
int32_t listener_add(uint8_t idx, uint16_t port, ConnProto proto, bool tls)
Create a listening socket on port and register it at idx.
Definition listener.cpp:466
void listener_stop_all()
Stop all active listeners.
Definition listener.cpp:545
err_t lowlevel_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
lwIP receive callback - fires when data arrives on a connection.
Definition tcp.cpp:795
bool listener_accept_allowed_ip(const DetIp *ip, uint32_t now_ms)
Fixed-window per-IP accept-rate gate (connection-flood defense, keyed by source address).
Definition listener.cpp:95
void listener_per_ip_throttle_reset(void)
Reset the per-IP throttle bucket table.
Definition listener.cpp:144
err_t lowlevel_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len)
lwIP sent callback - fires after the stack acknowledges sent bytes.
Definition tcp.cpp:896
void lowlevel_err_cb(void *arg, err_t err)
lwIP error callback - fires when the stack detects a fatal error.
Definition tcp.cpp:922
void listener_stop(uint8_t idx)
Stop listening on the port at idx and release its resources.
Definition listener.cpp:525
bool listener_ip_allowed(const DetIp *ip)
Test a source address against the allowlist (accept-time firewall).
Definition listener.cpp:242
int32_t listener_add_dynamic(uint8_t idx, uint16_t port, ConnProto proto)
Add / stop a listener from a running task (thread-safe variant).
Definition listener.cpp:619
bool listener_accept_allowed(uint32_t now_ms)
Fixed-window global accept-rate gate (connection-flood defense).
Definition listener.cpp:55
bool listener_ip_allow_add_cidr(const char *cidr)
Add an allowlist rule from CIDR text (the ergonomic public entry point).
Definition listener.cpp:191
void listener_accept_throttle_reset(void)
Reset the accept-throttle window counters.
Definition listener.cpp:69
Listener listener_pool[MAX_LISTENERS]
Static pool of listener contexts. Defined in listener.cpp.
Definition listener.cpp:34
void listener_stop_dynamic(uint8_t idx)
Definition listener.cpp:655
void listener_ip_allowlist_reset(void)
Clear all allowlist rules (the allowlist becomes empty = allow all).
Definition listener.cpp:255
Layer 4 (Listener) - per-port TCP listener abstraction.
uint32_t window_start
Definition listener.cpp:50
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
DetIpFamily family
address family tag
Definition ip.h:53
struct tcpip_api_call_data base
Definition listener.cpp:562
IpAllowRule rules[DETWS_IP_ALLOWLIST_SLOTS]
Definition listener.cpp:170
uint8_t count
Definition listener.cpp:171
uint8_t prefix_len
CIDR prefix length: 0..32 for v4, 0..128 for v6.
Definition listener.cpp:164
DetIp network
network address (family DetIpFamily::DET_IP_V4 / V6; DetIpFamily::DET_IP_NONE marks unused).
Definition listener.cpp:163
DetIp addr
source address (family DetIpFamily::DET_IP_NONE marks an empty bucket).
Definition listener.cpp:83
uint16_t count
connections counted from this address in the window.
Definition listener.cpp:85
uint32_t window_start
millis() at the start of this bucket's current window.
Definition listener.cpp:84
IpThrottleBucket buckets[DETWS_PER_IP_THROTTLE_SLOTS]
Definition listener.cpp:91
State for one TCP listening port.
Definition listener.h:55
QueueHandle_t queue
Handle returned by xQueueCreateStatic().
Definition listener.h:61
ConnProto proto
Application protocol for all connections accepted here.
Definition listener.h:57
bool tls
True when connections accepted here begin a TLS handshake.
Definition listener.h:63
uint8_t _queue_storage[EVT_QUEUE_DEPTH *sizeof(TcpEvt)]
Queue backing store.
Definition listener.h:60
uint16_t port
TCP port this listener binds.
Definition listener.h:56
bool active
True after listener_add(), false after listener_stop().
Definition listener.h:62
StaticQueue_t _queue_struct
FreeRTOS static queue descriptor.
Definition listener.h:59
struct tcp_pcb * listen_pcb
lwIP listen PCB; nullptr when inactive.
Definition listener.h:58
A single TCP connection context.
Definition tcp.h:72
DetIface iface
Interface this connection arrived on; set at accept time.
Definition tcp.h:90
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
Definition tcp.h:75
uint32_t last_activity_ms
millis() timestamp of last TX/RX event.
Definition tcp.h:76
uint8_t owner
Worker that owns this slot (round-robin at accept). Always 0 at N=1.
Definition tcp.h:86
uint8_t listener_id
Index into listener_pool[]; set at accept time.
Definition tcp.h:85
DetAtomic< size_t > rx_tail
Consumer read index (worker context).
Definition tcp.h:80
DetAtomic< size_t > rx_head
Producer write index (lwIP/tcpip context).
Definition tcp.h:79
DetAtomic< ConnState > state
Lifecycle state; acquire/release for inter-task visibility.
Definition tcp.h:74
uint8_t tls
Non-zero when this connection is TLS (set at accept time).
Definition tcp.h:91
size_t rx_acked
Definition tcp.h:81
ConnProto proto
Application protocol for this connection.
Definition tcp.h:87
Event record posted from lwIP callbacks to the session layer.
Definition tcp.h:149
uint8_t slot_id
Which connection slot is affected.
Definition tcp.h:151
uint32_t detws_ap_ip
softAP IPv4 address (network byte order) for STA/AP interface tagging.
Definition tcp.cpp:349
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:347
void det_lwip_to_detip(const ip_addr_t *ra, DetIp *out)
A stable per-peer 32-bit identity key for slot (the v4 address, or an FNV-1a hash of a v6 address)....
Definition tcp.cpp:673
#define DETWS_OBS_NOTICE(slot, st, reason)
Definition tcp.h:517
@ EVT_CONNECT
New connection accepted.
#define DETWS_OBS_TRANSITION(slot, olds, news, reason)
Definition tcp.h:516
@ CONN_ACTIVE
Live connection; PCB is valid.
@ CONN_FREE
Slot is available; no PCB is attached.
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).
void detws_worker_wake(int worker_id)
Wake worker worker_id so it services a freshly-queued event now.
Definition worker.cpp:143
Layer 5 (Session) - server worker identity.