DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
listener.h
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.h
6 * @brief Layer 4 (Listener) - per-port TCP listener abstraction.
7 *
8 * Each active listener owns one lwIP listening PCB and one statically-
9 * allocated FreeRTOS queue. When a new client connects, `listener_accept_cb`
10 * claims a slot from the shared `conn_pool`, wires the standard per-connection
11 * callbacks, and posts `EvtType::EVT_CONNECT` to the owning listener's queue.
12 *
13 * The session layer drains all active listener queues each `server_tick()`,
14 * routing events to the correct protocol handler via `TcpConn::proto`.
15 *
16 * **Single accept callback**
17 * `tcp_arg(listen_pcb, (void*)(uintptr_t)idx)` embeds the listener index in
18 * the PCB user-data so a single static `listener_accept_cb` handles all ports.
19 *
20 * **Circular-dependency resolution**
21 * tcp.cpp needs to post events to listener queues but cannot include
22 * this header (listener.h already includes tcp.h). The symbol
23 * `listener_enqueue()` is exported from listener.cpp; tcp.cpp calls it
24 * via a forward declaration added to tcp.h so no circular include
25 * is introduced.
26 *
27 * @author Douglas Quigg (dstroy0)
28 * @date 2026
29 */
30
31#ifndef DETERMINISTICESPASYNCWEBSERVER_LISTENER_H
32#define DETERMINISTICESPASYNCWEBSERVER_LISTENER_H
33
34#include "ServerConfig.h"
35#include "freertos/FreeRTOS.h"
36#include "freertos/queue.h"
37#include "lwip/tcp.h"
38#include "tcp.h"
39
40// ---------------------------------------------------------------------------
41// Listener pool entry
42// ---------------------------------------------------------------------------
43
44/**
45 * @brief State for one TCP listening port.
46 *
47 * All queue storage is embedded in this struct so the entire listener pool
48 * lives in BSS - no heap allocation anywhere in the listener layer.
49 *
50 * A single `Listener` instance consumes:
51 * sizeof(tcp_pcb*) + sizeof(StaticQueue_t) + EVT_QUEUE_DEPTH*sizeof(TcpEvt)
52 * + sizeof(QueueHandle_t) + 3 bytes overhead (port, proto, active).
53 */
55{
56 uint16_t port; ///< TCP port this listener binds.
57 ConnProto proto; ///< Application protocol for all connections accepted here.
58 struct tcp_pcb *listen_pcb; ///< lwIP listen PCB; nullptr when inactive.
59 StaticQueue_t _queue_struct; ///< FreeRTOS static queue descriptor.
60 uint8_t _queue_storage[EVT_QUEUE_DEPTH * sizeof(TcpEvt)]; ///< Queue backing store.
61 QueueHandle_t queue; ///< Handle returned by xQueueCreateStatic().
62 bool active; ///< True after listener_add(), false after listener_stop().
63 bool tls; ///< True when connections accepted here begin a TLS handshake.
64};
65
66/** @brief Static pool of listener contexts. Defined in listener.cpp. */
68
69// ---------------------------------------------------------------------------
70// Listener management API
71// ---------------------------------------------------------------------------
72
73/**
74 * @brief Create a listening socket on @p port and register it at @p idx.
75 *
76 * If the slot at @p idx is already active it is stopped first.
77 * Creates a per-listener FreeRTOS static queue and an lwIP listening PCB.
78 * Wires `listener_accept_cb` as the accept handler with the listener index
79 * embedded as the PCB user-data argument.
80 *
81 * @param idx Slot in listener_pool[] (0 … MAX_LISTENERS-1).
82 * @param port TCP port to bind and listen on.
83 * @param proto Application protocol spoken on connections from this port.
84 * @param tls When true, connections accepted here start a TLS handshake.
85 * @return Positive value on success; -1 on failure (pool full or lwIP error).
86 */
87int32_t listener_add(uint8_t idx, uint16_t port, ConnProto proto, bool tls = false);
88
89/**
90 * @brief Stop listening on the port at @p idx and release its resources.
91 *
92 * Idempotent - safe to call on an already-stopped slot.
93 * Closes the lwIP listening PCB and deletes the FreeRTOS queue.
94 * Does not close any connections already accepted on this port.
95 *
96 * @param idx Slot in listener_pool[].
97 */
98void listener_stop(uint8_t idx);
99
100/**
101 * @brief Stop all active listeners.
102 *
103 * Convenience wrapper that calls listener_stop() for every slot in
104 * listener_pool[]. Called by DetWebServer::stop().
105 */
106void listener_stop_all();
107
108/**
109 * @brief Add / stop a listener from a running task (thread-safe variant).
110 *
111 * listener_add() runs the raw lwIP tcp_bind/tcp_listen inline, which is only safe at
112 * setup (before tcpip_thread is servicing our sockets). These variants marshal the
113 * lwIP operations onto tcpip_thread via tcpip_api_call(), so a listener may be created
114 * or torn down dynamically from a worker/session task - used by the SSH remote-forward
115 * owner (`ssh -R`), which opens a listener when a client requests one. TLS listeners
116 * are not supported here (forwarded ports are plaintext bridges). On the native host
117 * (no lwIP) these behave like the inline path for unit tests.
118 *
119 * @return listener_add_dynamic: 1 on success, -1 on failure (bad idx, bind in use,
120 * or lwIP error). listener_stop_dynamic: void, idempotent.
121 */
122int32_t listener_add_dynamic(uint8_t idx, uint16_t port, ConnProto proto);
123void listener_stop_dynamic(uint8_t idx);
124
125/**
126 * @brief Post @p evt to the queue owned by listener @p listener_id.
127 *
128 * Called from tcp.cpp callbacks (running in tcpip_thread context) to
129 * deliver connection events to the session layer. Uses xQueueSend with
130 * timeout=0 - a full queue means the application is not calling server_tick()
131 * fast enough; the dropped event is recoverable via connection timeout.
132 *
133 * @param listener_id Index into listener_pool[]; must be < MAX_LISTENERS.
134 * @param evt Event to copy into the queue.
135 * @return true if queued; false if dropped (full queue / inactive listener).
136 */
137bool listener_enqueue(uint8_t listener_id, const TcpEvt *evt);
138
139#if DETWS_WORKER_COUNT > 1
140/** @brief Create the per-worker event queues (idempotent; called from listener_add). */
141void listener_worker_queues_init(void);
142
143/** @brief The FreeRTOS event queue for worker @p worker_id (nullptr if out of range). */
144QueueHandle_t listener_worker_queue(int worker_id);
145#endif
146
147/**
148 * @brief Fixed-window global accept-rate gate (connection-flood defense).
149 *
150 * Returns true if a new connection accepted at @p now_ms is within the
151 * DETWS_ACCEPT_THROTTLE_MAX-per-DETWS_ACCEPT_THROTTLE_WINDOW_MS budget (and
152 * counts it), false if the budget for the current window is exhausted. State is
153 * two static counters shared across all listeners. The accept callback consults
154 * this only when DETWS_ENABLE_ACCEPT_THROTTLE is set; the function is always
155 * compiled so it can be unit-tested. Call listener_accept_throttle_reset() to
156 * clear the window (e.g. between tests).
157 */
158bool listener_accept_allowed(uint32_t now_ms);
159
160/** @brief Reset the accept-throttle window counters. */
162
163/**
164 * @brief Fixed-window per-IP accept-rate gate (connection-flood defense, keyed by source address).
165 *
166 * Returns true if a connection from source address @p ip accepted at @p now_ms is
167 * within that address's DETWS_PER_IP_THROTTLE_MAX-per-DETWS_PER_IP_THROTTLE_WINDOW_MS
168 * budget (and counts it), false once that address has exhausted its budget for the
169 * current window. The key is the full family-tagged address (DetIp): an IPv4 and an
170 * IPv6 peer are always distinct buckets, and an IPv6 attacker cannot fold many
171 * addresses onto one bucket (or evict a victim's) through a lossy hash. State is a
172 * fixed BSS table of DETWS_PER_IP_THROTTLE_SLOTS buckets; a new address reuses an
173 * empty, expired, or least-recently-started bucket so memory stays bounded. An
174 * unspecified @p ip (family DetIpFamily::DET_IP_NONE) is passed through (allowed) since it cannot
175 * be tracked. The accept callback consults this only when DETWS_ENABLE_PER_IP_THROTTLE
176 * is set; the function is always compiled so it can be unit-tested. Call
177 * listener_per_ip_throttle_reset() to clear the table.
178 */
179bool listener_accept_allowed_ip(const DetIp *ip, uint32_t now_ms);
180
181/** @brief Reset the per-IP throttle bucket table. */
183
184// ---------------------------------------------------------------------------
185// Source-IP allowlist (accept-time firewall)
186// ---------------------------------------------------------------------------
187
188/**
189 * @brief Add a CIDR rule to the source-IP allowlist.
190 *
191 * @param network Family-tagged network address (DetIp, IPv4 or IPv6). Host bits
192 * outside the prefix do not need to be pre-masked; matching masks
193 * them at compare time.
194 * @param prefix_len CIDR prefix length: 0..32 for IPv4, 0..128 for IPv6 (32 / 128
195 * for a single host, 0 to match every address of that family).
196 * @return true if the rule was stored; false if @p network is unspecified, the
197 * prefix length exceeds the family width, or the table
198 * (DETWS_IP_ALLOWLIST_SLOTS entries) is full.
199 */
200bool listener_ip_allow_add(const DetIp *network, uint8_t prefix_len);
201
202/**
203 * @brief Add an allowlist rule from CIDR text (the ergonomic public entry point).
204 *
205 * Accepts IPv4 or IPv6 in `address/prefix` form (e.g. "192.168.1.0/24",
206 * "2001:db8::/32") or a bare address (e.g. "10.0.0.5", "::1") which is treated as
207 * a host route (/32 for v4, /128 for v6). The address is parsed with det_ip_parse
208 * so every RFC 4291 v6 text form is accepted.
209 *
210 * @return true if the rule was stored; false if @p cidr is malformed, the prefix
211 * is out of range for the family, or the table is full.
212 */
213bool listener_ip_allow_add_cidr(const char *cidr);
214
215/**
216 * @brief Test a source address against the allowlist (accept-time firewall).
217 *
218 * @param ip Family-tagged source address (DetIp).
219 * @return true if the address is allowed: always true while the allowlist is
220 * empty (so enabling the feature without rules never locks the device
221 * out), otherwise true only if @p ip is contained in at least one CIDR
222 * rule of the same family (prefix match on the full address, never a hash).
223 * The accept callback consults this only when DETWS_ENABLE_IP_ALLOWLIST
224 * is set; the function is always compiled so it can be unit-tested.
225 */
226bool listener_ip_allowed(const DetIp *ip);
227
228/** @brief Clear all allowlist rules (the allowlist becomes empty = allow all). */
230
231#endif
User-facing configuration for DeterministicESPAsyncWebServer.
#define MAX_LISTENERS
Maximum number of simultaneously active listener ports.
ConnProto
Application protocol spoken on a listener port or connection slot.
#define EVT_QUEUE_DEPTH
Depth of the FreeRTOS event queue shared between lwIP callbacks and the main-loop task.
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
void listener_stop_all()
Stop all active listeners.
Definition listener.cpp:545
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
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(uint8_t idx, uint16_t port, ConnProto proto, bool tls=false)
Create a listening socket on port and register it at idx.
Definition listener.cpp:466
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
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
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
Event record posted from lwIP callbacks to the session layer.
Definition tcp.h:149
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.