DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
tcp.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 tcp.cpp
6 * @brief Layer 4 (Transport) - TCP connection management implementation.
7 *
8 * All lwIP raw-API callbacks run in the `tcpip_thread` FreeRTOS task context.
9 * They are NOT hardware ISRs, which is why we use `xQueueSend()` (non-ISR
10 * variant) with timeout=0 rather than `xQueueSendFromISR()`.
11 *
12 * **Ring buffer write ordering**
13 * The producer (recv callback) writes payload bytes into `rx_buffer[]` and
14 * then advances `rx_head`. The consumer (worker) reads from `rx_buffer[]` at
15 * `rx_tail` and advances `rx_tail`. `rx_head`/`rx_tail` are `DetAtomic`
16 * (acquire/release): the producer's buffer writes are published by the release
17 * store of `rx_head` and observed by the consumer's acquire load, correct on
18 * either core. The ring math itself is the shared `ring.h` primitive.
19 *
20 * **Listener coupling**
21 * Each TcpConn carries `listener_id` (set at accept time by listener_accept_cb
22 * in listener.cpp). The `enqueue()` helper forwards events to
23 * `listener_enqueue()` - defined in listener.cpp - so tcp.cpp needs no
24 * direct knowledge of the Listener struct. `listener_enqueue()` is forward-
25 * declared in tcp.h to avoid a circular include.
26 */
27
28#include "tcp.h"
29#include "freertos/FreeRTOS.h"
30#include "freertos/queue.h"
31#include "freertos/task.h" // xTaskGetCurrentTaskHandle() - tcpip_thread self-detection for det_tcp_marshal
32#include "lwip/tcp.h"
33#include "services/clock.h" // detws_millis() pluggable monotonic clock
34
35#ifdef ARDUINO
36#include "network_drivers/session/worker.h" // detws_worker_wake() - resume a paced send when the window drains
37#endif
38
39#if DETWS_ENABLE_TLS
41#endif
42
43// ---------------------------------------------------------------------------
44// Observability (DETWS_ENABLE_OBSERVABILITY) - event hook + lock-free counters.
45// Zero cost when off: OBS_TRANSITION / OBS_NOTICE expand to nothing and their
46// arguments (incl. the DetConnReason names, which are only declared when the
47// feature is on) are dropped unparsed by the preprocessor.
48// ---------------------------------------------------------------------------
49#if DETWS_ENABLE_OBSERVABILITY
50#include <atomic>
51
52// All connection-observability state, owned by one instance (internal linkage): the event
53// callback and the cumulative per-reason counters (indexed 0..7). The live ConnState::CONN_CLOSING gauge
54// is not a counter - it is derived on read by scanning the pool, so it can never drift out of
55// sync with the actual slot states. One named owner, unreachable from any other TU.
56struct ObsCtx
57{
58 DetConnEventCb conn_event_cb = nullptr;
59 std::atomic<uint32_t> ctr[8];
60};
61static ObsCtx s_obs;
62
63void det_conn_on_event(DetConnEventCb cb)
64{
65 s_obs.conn_event_cb = cb;
66}
67
68DetConnCounters det_conn_counters()
69{
70 DetConnCounters c;
71 c.accepts = s_obs.ctr[0].load(std::memory_order_relaxed);
72 c.closes_remote = s_obs.ctr[1].load(std::memory_order_relaxed);
73 c.closes_local = s_obs.ctr[2].load(std::memory_order_relaxed);
74 c.closes_error = s_obs.ctr[3].load(std::memory_order_relaxed);
75 c.closes_timeout = s_obs.ctr[4].load(std::memory_order_relaxed);
76 c.closes_abort = s_obs.ctr[5].load(std::memory_order_relaxed);
77 c.backpressure = s_obs.ctr[6].load(std::memory_order_relaxed);
78 c.defer_drops = s_obs.ctr[7].load(std::memory_order_relaxed);
79 // Derive the live gauge from the actual pool so it cannot drift.
80 c.closing_gauge = 0;
81 for (int i = 0; i < MAX_CONNS; i++)
82 if (conn_pool[i].state == ConnState::CONN_CLOSING)
83 c.closing_gauge++;
84 return c;
85}
86
87void det_conn_counters_reset()
88{
89 for (int i = 0; i < 8; i++)
90 s_obs.ctr[i].store(0, std::memory_order_relaxed);
91}
92
93static void obs_bump(DetConnReason reason)
94{
95 int idx = -1;
96 switch (reason)
97 {
98 case DetConnReason::DET_CONN_R_ACCEPT:
99 idx = 0;
100 break;
101 case DetConnReason::DET_CONN_R_CLOSE_REMOTE:
102 idx = 1;
103 break;
104 case DetConnReason::DET_CONN_R_CLOSE_LOCAL:
105 idx = 2;
106 break;
107 case DetConnReason::DET_CONN_R_ERROR:
108 idx = 3;
109 break;
110 case DetConnReason::DET_CONN_R_TIMEOUT:
111 idx = 4;
112 break;
113 case DetConnReason::DET_CONN_R_ABORT:
114 idx = 5;
115 break;
116 case DetConnReason::DET_CONN_R_BACKPRESSURE:
117 idx = 6;
118 break;
119 case DetConnReason::DET_CONN_R_DEFER_DROP:
120 idx = 7;
121 break;
122 case DetConnReason::DET_CONN_R_DRAINED:
123 idx = -1; // the entering close reason was already counted; DRAINED is gauge-only
124 break;
125 }
126 if (idx >= 0)
127 s_obs.ctr[idx].fetch_add(1, std::memory_order_relaxed);
128}
129
130// A real state transition: bump the reason counter and fire the callback. The
131// ConnState::CONN_CLOSING gauge is derived on read (see det_conn_counters), so there is no
132// per-transition gauge bookkeeping to get wrong. Non-static so listener.cpp
133// (accept) can notify through the DETWS_OBS_TRANSITION macro declared in tcp.h.
134void detws_obs_transition(uint8_t slot, ConnState olds, ConnState news, DetConnReason reason)
135{
136 obs_bump(reason);
137 if (s_obs.conn_event_cb)
138 s_obs.conn_event_cb(slot, olds, news, reason);
139}
140
141// A non-transition notice (backpressure / defer-drop): bump + fire with old==new.
142void detws_obs_notice(uint8_t slot, ConnState st, DetConnReason reason)
143{
144 obs_bump(reason);
145 if (s_obs.conn_event_cb)
146 s_obs.conn_event_cb(slot, st, st, reason);
147}
148#endif // DETWS_ENABLE_OBSERVABILITY
149
150// ---------------------------------------------------------------------------
151// Cross-thread TCP serialization
152// ---------------------------------------------------------------------------
153// The raw lwIP API is not thread-safe: its callbacks run in the tcpip_thread
154// task, while this library issues writes/closes from the Arduino main-loop task.
155// Calling tcp_*() from the main loop concurrently with tcpip_thread processing
156// an inbound segment corrupts the PCB state - under a streaming upload (the peer
157// is actively sending as the server responds/closes) it trips lwIP's
158// "tcp_receive: wrong state" assert and panics.
159//
160// Arduino-esp32 ships lwIP with LWIP_TCPIP_CORE_LOCKING disabled, so
161// LOCK_TCPIP_CORE() is a no-op. The portable fix is tcpip_api_call(): it runs a
162// function *inside* tcpip_thread and blocks the caller until it completes, so
163// every main-loop-originated tcp_*() executes in the one safe context. lwIP's
164// own callbacks already run in that context and must NOT marshal again (they
165// call tcp_*() directly).
166// ConnState::CONN_CLOSING dwell helpers (defined below, near det_conn_begin_close). Forward
167// declared so the tcpip-thread op dispatch can reach closing_check().
168static void closing_check(uint8_t slot, struct tcp_pcb *pcb);
169
170#if defined(ARDUINO)
171#include "lwip/priv/tcpip_priv.h"
172#include <string.h>
173
174enum class DetTcpOp : uint8_t
175{
181 DET_OP_RAWSEND, // raw tcp_write of already-encrypted bytes (TLS BIO), no TLS re-entry
182 DET_OP_CLOSE_CHECK, // in tcpip_thread: finalize a ConnState::CONN_CLOSING slot if its TX has drained
183 DET_OP_RECVED // in tcpip_thread: tcp_recved() to reopen the window (ack-on-consume)
184};
185
186// TCP transport context, owned by one instance (internal linkage): the tcpip_thread FreeRTOS task
187// handle, captured the first time det_tcp_do() runs (that op is always marshaled onto tcpip_thread).
188// det_tcp_marshal() compares the running task against it, so a raw lwIP callback - which runs in
189// tcpip_thread but does NOT enter through det_tcp_do, so a plain "inside det_tcp_do" flag reads false
190// there - performs its op inline instead of re-marshaling, which would block on the very mailbox the
191// callback's thread services (self-deadlock: a close from the sent callback that sends a TLS
192// close_notify, found on hardware with a real TLS 1.3 client). One named owner, unreachable cross-TU.
194{
195 TaskHandle_t tcpip_task = nullptr;
196};
197static TransportCtx s_tp;
198
199// True when the caller may run a raw lwIP op directly instead of marshaling. lwIP has two threading
200// models and the answer differs, so branch on which one the framework built:
201static inline bool on_tcpip_thread()
202{
203#if defined(LWIP_TCPIP_CORE_LOCKING) && LWIP_TCPIP_CORE_LOCKING
204 // Core-locking (arduino-esp32 3.x / IDF 5.x): tcpip_api_call takes the core lock and runs the op
205 // INLINE on the calling task - there is no dedicated tcpip thread to marshal to - so a direct lwIP
206 // call is safe exactly when we already hold the core lock. Use lwIP's own holder query (the one
207 // LWIP_ASSERT_CORE_LOCKED checks). A task-handle compare is meaningless here (every caller runs its
208 // own tcpip_api_call inline, so the captured "tcpip task" is just whoever ran the first op), and a
209 // false positive calls tcp_write unlocked -> the "Required to lock TCPIP core functionality!" assert
210 // (found running TLS on the PSRAM/IDF-5.5 core: the handshake's record flush crashed the device).
211 return sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER);
212#else
213 // Mailbox (arduino-esp32 2.x / IDF 4.x): tcpip_api_call marshals to the single dedicated tcpip
214 // thread, so a direct call is safe exactly when we run on that thread. Captured on the first
215 // det_tcp_do (boot / first send), which precedes every raw-callback teardown path.
216 return s_tp.tcpip_task != nullptr && xTaskGetCurrentTaskHandle() == s_tp.tcpip_task;
217#endif
218}
219
221{
222 struct tcpip_api_call_data base;
224 uint8_t slot;
225 struct tcp_pcb *pcb;
226 const void *data;
227 u16_t len;
228 bool flush; ///< DetTcpOp::DET_OP_SEND: also tcp_output() after a successful write (coalesced write+flush)
229 err_t result; ///< outcome of the op (DetTcpOp::DET_OP_SEND: whether the write was queued)
230};
231
232// True if @p pcb is still bound to a live connection slot. A marshalled send/output captures the
233// pcb on the worker thread (from conn_pool[slot].pcb); by the time the op runs here the connection
234// can have been torn down - teardown nulls conn_pool[slot].pcb on the worker and then frees the pcb
235// on tcpip_thread (DET_OP_CLOSE/ABORT), and a remote RST frees it via the error callback. A
236// tcp_write/tcp_output on that freed pcb trips lwIP's `tcp_output: invalid pcb` assert and panics
237// the device (found by the pentest rig: oversized request line / connection saturation). Re-check
238// against the pool here - we are in tcpip_thread, where teardown also runs, so the compare is
239// race-free. The scan (not conn_pool[slot]) is correct for DET_OP_RAWSEND too, whose slot is 0.
240static bool pcb_still_bound(const struct tcp_pcb *pcb)
241{
242 if (!pcb)
243 return false;
244 for (uint8_t i = 0; i < CONN_POOL_SLOTS; i++)
245 if (conn_pool[i].pcb == pcb)
246 return true;
247 return false;
248}
249
250// Runs in tcpip_thread (via tcpip_api_call). Performs the requested raw lwIP op
251// in the one context where it is safe; TLS record I/O (which also reaches
252// tcp_write through the BIO) is done here too.
253static err_t det_tcp_do(struct tcpip_api_call_data *c)
254{
255 DetTcpCall *k = (DetTcpCall *)c;
256 k->result = ERR_OK;
257 if (!s_tp.tcpip_task) // capture the tcpip_thread task once; det_tcp_do only ever runs in that thread
258 s_tp.tcpip_task = xTaskGetCurrentTaskHandle();
259 switch (k->op)
260 {
262 // RAWSEND (TLS BIO) carries only the pcb, not its slot, so liveness needs a pool lookup. This
263 // is the cool TLS handshake / read-pump path (not per-packet app data), and CONN_POOL_SLOTS is
264 // small + compile-time so -O2 unrolls the scan (see docs/ROADMAP: unroll loops to bitmask).
265 if (!pcb_still_bound(k->pcb)) // stale pcb (connection torn down between capture and now)
266 {
267 k->result = ERR_CLSD;
268 break;
269 }
270 k->result = tcp_write(k->pcb, k->data, k->len, TCP_WRITE_FLAG_COPY);
271 if (k->result == ERR_OK)
272 tcp_output(k->pcb);
273 break;
275 // Hot path: SEND carries the real slot, so a stale pcb is just k->pcb != the slot's live pcb.
276 // O(1), no scan - the send/flush pair runs on every HTTP response.
277 if (k->pcb != conn_pool[k->slot].pcb)
278 {
279 k->result = ERR_CLSD; // connection torn down between capture and now; skip, do not assert
280 break;
281 }
282#if DETWS_ENABLE_TLS
283 if (conn_pool[k->slot].tls)
284 {
285 k->result = (det_tls_write(k->slot, k->data, k->len) >= 0) ? ERR_OK : ERR_MEM;
286 break;
287 }
288#endif
289 k->result = tcp_write(k->pcb, k->data, k->len, TCP_WRITE_FLAG_COPY);
290 if (k->flush && k->result == ERR_OK)
291 tcp_output(k->pcb); // coalesced write+flush: one marshal for a terminal single-shot response
292 break;
294 // Hot path (O(1)): flush only if the slot still owns this pcb; else it was torn down - skip
295 // rather than tcp_output on freed memory (lwIP's "invalid pcb" assert -> panic).
296 if (k->pcb == conn_pool[k->slot].pcb)
297 tcp_output(k->pcb);
298 else
299 k->result = ERR_CLSD;
300 break;
302#if DETWS_ENABLE_TLS
303 if (conn_pool[k->slot].tls)
304 det_tls_conn_end(k->slot); // close_notify + free the TLS context
305#endif
306 if (tcp_close(k->pcb) != ERR_OK)
307 tcp_abort(k->pcb);
308 break;
310 tcp_abort(k->pcb);
311 break;
313 tcp_arg(k->pcb, nullptr);
314 break;
316 closing_check(k->slot, k->pcb); // safe pcb access: we are in tcpip_thread
317 break;
319 tcp_recved(k->pcb, k->len); // reopen the receive window by the consumed bytes
320 break;
321 }
322 return ERR_OK;
323}
324
325static inline err_t det_tcp_marshal(DetTcpOp op, uint8_t slot, struct tcp_pcb *pcb, const void *data, u16_t len,
326 bool flush = false)
327{
328 DetTcpCall k;
329 memset(&k, 0, sizeof(k));
330 k.op = op;
331 k.slot = slot;
332 k.pcb = pcb;
333 k.data = data;
334 k.len = len;
335 k.flush = flush;
336 // On tcpip_thread already (a raw lwIP callback's teardown reaching a send/close): run the op inline.
337 // Re-marshaling here would call tcpip_api_call from the thread that services its mailbox and block
338 // forever (the TLS close_notify-from-sent-callback self-deadlock). Off-thread (worker): marshal.
339 if (on_tcpip_thread())
340 det_tcp_do(&k.base);
341 else
342 tcpip_api_call(det_tcp_do, &k.base);
343 return k.result;
344}
345#endif // ARDUINO
346
348
349uint32_t detws_ap_ip = 0;
350
352
353// ---------------------------------------------------------------------------
354// Connection output API
355// ---------------------------------------------------------------------------
356// The single send/flush/close path for every higher layer (HTTP app, WebSocket,
357// SSE, SSH). Keeping it here means presentation and application code never call
358// lwIP directly - they hand bytes to the transport layer, which decides whether
359// they go out as plaintext (tcp_write) or through the TLS record layer. With
360// DETWS_ENABLE_TLS off this is byte-identical to a direct tcp_write/tcp_output.
361
362bool det_conn_send(uint8_t slot, const void *data, u16_t len)
363{
364 // The write target is always the slot's own pcb (ingress reads resolve it the
365 // same way) - callers no longer thread it through, so it cannot disagree.
366#if defined(ARDUINO)
367 return det_tcp_marshal(DetTcpOp::DET_OP_SEND, slot, conn_pool[slot].pcb, data, len) ==
368 ERR_OK; // write runs in tcpip_thread
369#else
370#if DETWS_ENABLE_TLS
371 if (conn_pool[slot].tls)
372 return det_tls_write(slot, data, len) >= 0;
373#endif
374 return tcp_write(conn_pool[slot].pcb, data, len, TCP_WRITE_FLAG_COPY) == ERR_OK;
375#endif
376}
377
378bool det_conn_send_flush(uint8_t slot, const void *data, u16_t len)
379{
380 // Terminal single-shot write: the bytes AND their tcp_output() happen in one tcpip_thread
381 // round-trip, so a small response costs one marshal instead of the send()+flush() pair (each
382 // a ~23 us marshal on-device). For a TLS slot this is identical to det_conn_send: the record
383 // BIO already pushes ciphertext per record, so there is no separate flush to fold in.
384#if defined(ARDUINO)
385 return det_tcp_marshal(DetTcpOp::DET_OP_SEND, slot, conn_pool[slot].pcb, data, len, /*flush=*/true) == ERR_OK;
386#else
387#if DETWS_ENABLE_TLS
388 if (conn_pool[slot].tls)
389 return det_tls_write(slot, data, len) >= 0; // TLS BIO already output the record
390#endif
391 if (tcp_write(conn_pool[slot].pcb, data, len, TCP_WRITE_FLAG_COPY) != ERR_OK)
392 return false;
393 tcp_output(conn_pool[slot].pcb);
394 return true;
395#endif
396}
397
398u16_t det_conn_sndbuf(uint8_t slot)
399{
400 struct tcp_pcb *pcb = conn_pool[slot].pcb;
401 if (!pcb)
402 return 0;
403 u16_t avail = tcp_sndbuf(pcb);
404#if DETWS_ENABLE_TLS
405 // A TLS record adds header + MAC/tag overhead; report a conservative plaintext
406 // budget so a caller that fills it does not overrun the cipher's framing.
407 if (conn_pool[slot].tls)
408 avail = (avail > 64) ? (u16_t)(avail - 64) : 0;
409#endif
410 return avail;
411}
412
413void det_conn_flush(uint8_t slot)
414{
415#if DETWS_ENABLE_TLS
416 if (conn_pool[slot].tls)
417 return; // ciphertext was already pushed by the TLS BIO (tcp_output per record);
418 // flush must NOT end the session - persistent TLS (wss / TLS SSE) reuses it
419#endif
420#if defined(ARDUINO)
421 det_tcp_marshal(DetTcpOp::DET_OP_OUTPUT, slot, conn_pool[slot].pcb, nullptr, 0);
422#else
423 tcp_output(conn_pool[slot].pcb);
424#endif
425}
426
427void det_conn_ack_consumed(uint8_t slot)
428{
429 if (slot >= MAX_CONNS)
430 return;
431 TcpConn *c = &conn_pool[slot];
432 // Only the owning worker calls this, so rx_tail/rx_acked are read race-free
433 // here; rx_head (producer) is not touched. Ack nothing for a slot that is not
434 // actively receiving (the ConnState::CONN_CLOSING discard path ACKs its own bytes).
435 if (c->state != ConnState::CONN_ACTIVE || !c->pcb)
436 return;
437 size_t tail = c->rx_tail;
438 size_t consumed = (tail + RX_BUF_SIZE - c->rx_acked) % RX_BUF_SIZE;
439 if (!consumed)
440 return;
441 c->rx_acked = tail; // advance first: the marshaled tcp_recved is the slow part
442#if defined(ARDUINO)
443 det_tcp_marshal(DetTcpOp::DET_OP_RECVED, slot, c->pcb, nullptr, (u16_t)consumed);
444#else
445 tcp_recved(c->pcb, (u16_t)consumed);
446#endif
447}
448
449bool det_conn_raw_send(struct tcp_pcb *pcb, const void *data, u16_t len)
450{
451 if (!pcb)
452 return false;
453#if defined(ARDUINO)
454 // det_tcp_marshal owns the context choice: it runs the raw write inline when already in
455 // tcpip_thread (a TLS close_notify/alert emitted from inside a raw lwIP callback) and marshals it
456 // from the worker task (the handshake / read pump), so the tcp_write neither races the lwIP thread
457 // nor self-deadlocks on the tcpip mailbox. The RAWSEND op also re-checks the pcb is still bound.
458 return det_tcp_marshal(DetTcpOp::DET_OP_RAWSEND, 0, pcb, data, len) == ERR_OK;
459#else
460 err_t e = tcp_write(pcb, data, len, TCP_WRITE_FLAG_COPY);
461 if (e == ERR_OK)
462 tcp_output(pcb);
463 return e == ERR_OK;
464#endif
465}
466
467void det_conn_close(uint8_t slot)
468{
469 if (slot >= MAX_CONNS)
470 return;
471 TcpConn *c = &conn_pool[slot];
472 struct tcp_pcb *pcb = c->pcb;
473 if (!pcb)
474 return;
475 // The application-initiated close path (L4 primitive). Remote FIN, error, and
476 // timeout closes are observed at their own sites, so this is uniquely "local".
477 DETWS_OBS_TRANSITION(slot, ConnState::CONN_ACTIVE, ConnState::CONN_FREE, DetConnReason::DET_CONN_R_CLOSE_LOCAL);
478 // Detach the pcb and free the slot before the close, so a late callback for
479 // this pcb finds a null arg and does nothing. The close itself targets the
480 // captured pcb (DetTcpOp::DET_OP_CLOSE carries it), so nulling the slot first is safe.
481 det_conn_detach(pcb);
483 c->pcb = nullptr;
484#if defined(ARDUINO)
485 det_tcp_marshal(DetTcpOp::DET_OP_CLOSE, slot, pcb, nullptr, 0); // TLS teardown + FIN in tcpip_thread
486#else
487#if DETWS_ENABLE_TLS
488 if (c->tls)
489 det_tls_conn_end(slot);
490#endif
491 if (tcp_close(pcb) != ERR_OK)
492 tcp_abort(pcb);
493#endif
494}
495
496void det_conn_abort_slot(uint8_t slot)
497{
498 if (slot >= MAX_CONNS)
499 return;
500 TcpConn *c = &conn_pool[slot];
501 struct tcp_pcb *pcb = c->pcb;
502 if (!pcb)
503 return;
504 DETWS_OBS_TRANSITION(slot, ConnState::CONN_ACTIVE, ConnState::CONN_FREE, DetConnReason::DET_CONN_R_ABORT);
505#if DETWS_ENABLE_TLS
506 if (c->tls)
507 det_tls_conn_free(slot); // abrupt: free the per-conn TLS context, no close_notify
508#endif
509 // Detach + free the slot before the RST, so a late callback finds a null arg.
510 det_conn_detach(pcb);
512 c->pcb = nullptr;
513 det_conn_abort(pcb);
514}
515
516void det_conn_detach(struct tcp_pcb *pcb)
517{
518 // Disassociate the slot from this pcb's lwIP callbacks before freeing the
519 // slot, so any late callback for the pcb finds a null arg and does nothing.
520#if defined(ARDUINO)
521 det_tcp_marshal(DetTcpOp::DET_OP_DETACH, 0, pcb, nullptr, 0);
522#else
523 tcp_arg(pcb, nullptr);
524#endif
525}
526
527void det_conn_abort(struct tcp_pcb *pcb)
528{
529 // Hard reset (RST) for a fatal condition - no graceful FIN.
530#if defined(ARDUINO)
531 det_tcp_marshal(DetTcpOp::DET_OP_ABORT, 0, pcb, nullptr, 0);
532#else
533 tcp_abort(pcb);
534#endif
535}
536
537// ---------------------------------------------------------------------------
538// ConnState::CONN_CLOSING dwell: a graceful close that holds the slot until the peer ACKs.
539// ---------------------------------------------------------------------------
540// These run in tcpip_thread context (the sent callback, or the DetTcpOp::DET_OP_CLOSE_CHECK
541// marshaled op), so they touch the PCB directly - never marshal from here.
542
543// Finalize a ConnState::CONN_CLOSING slot: tear down the PCB and free the slot.
544static void closing_finalize(uint8_t slot, struct tcp_pcb *pcb)
545{
546 TcpConn *c = &conn_pool[slot];
547#if DETWS_ENABLE_TLS
548 if (c->tls)
549 det_tls_conn_end(slot); // close_notify + free the TLS context (in-thread)
550#endif
552 c->pcb = nullptr;
553 if (pcb)
554 {
555 tcp_arg(pcb, nullptr);
556 if (tcp_close(pcb) != ERR_OK)
557 tcp_abort(pcb);
558 }
559 DETWS_OBS_TRANSITION(slot, ConnState::CONN_CLOSING, ConnState::CONN_FREE, DetConnReason::DET_CONN_R_DRAINED);
560}
561
562// If the slot is ConnState::CONN_CLOSING and its TX queue has drained (peer ACKed the whole
563// response), finalize it now. Called only from tcpip_thread context.
564static void closing_check(uint8_t slot, struct tcp_pcb *pcb)
565{
566 if (slot >= MAX_CONNS || conn_pool[slot].state != ConnState::CONN_CLOSING)
567 return;
568 if (!pcb || pcb->snd_queuelen == 0)
569 closing_finalize(slot, pcb);
570}
571
572void det_conn_begin_close(uint8_t slot_id)
573{
574 if (slot_id >= MAX_CONNS)
575 return;
576 TcpConn *c = &conn_pool[slot_id];
577 if (c->state != ConnState::CONN_ACTIVE) // an error during the write may have freed it
578 return;
579 struct tcp_pcb *pcb = c->pcb;
580 c->last_activity_ms = detws_millis(); // start the ConnState::CONN_CLOSING dwell clock
581 c->state = ConnState::CONN_CLOSING; // release store: tcpip-thread callbacks now see CLOSING
583 DetConnReason::DET_CONN_R_CLOSE_LOCAL);
584 // Finalize immediately if the response already drained, else dwell until the
585 // sent callback (or the CLOSING-timeout sweep) reclaims it. The PCB read must
586 // happen in tcpip_thread, so marshal the check on device.
587#if defined(ARDUINO)
588 det_tcp_marshal(DetTcpOp::DET_OP_CLOSE_CHECK, slot_id, pcb, nullptr, 0);
589#else
590 closing_check(slot_id, pcb);
591#endif
592}
593
594/**
595 * @brief Non-blocking event enqueue helper.
596 *
597 * Forwards the event to the queue owned by the connection's listener.
598 * xQueueSend with timeout=0 returns immediately if the queue is full.
599 * A full queue indicates the application is not calling server_tick() fast
600 * enough; dropped events are recoverable via the idle-timeout sweep.
601 */
602static inline void enqueue(TcpConn *slot, const TcpEvt &evt)
603{
604 if (!listener_enqueue(slot->listener_id, &evt))
605 DETWS_OBS_NOTICE(slot->id, slot->state, DetConnReason::DET_CONN_R_DEFER_DROP);
606}
607
609{
611 // Reset from a single zeroed template in BSS rather than `conn_pool[i] = {}`:
612 // the latter materializes a full sizeof(TcpConn) temporary on the caller's
613 // stack (the whole rx_buffer[RX_BUF_SIZE]), which overflows the loopTask stack
614 // at begin() once RX_BUF_SIZE is set large. Copy-assigning from the static
615 // template stays in BSS and uses DetAtomic::operator= (no atomic memset UB).
616 static const TcpConn blank = {};
617 for (int i = 0; i < MAX_CONNS; i++)
618 {
619 conn_pool[i] = blank;
620 conn_pool[i].id = i;
622 }
623}
624
626{
627 // Abort all active connections - listener PCBs and queues are owned by
628 // the listener layer and must be cleaned up via listener_stop_all() first.
629 for (int i = 0; i < MAX_CONNS; i++)
630 {
631 ConnState st = conn_pool[i].state;
632 if ((st == ConnState::CONN_ACTIVE || st == ConnState::CONN_CLOSING) && conn_pool[i].pcb)
633 {
634 struct tcp_pcb *pcb = conn_pool[i].pcb;
636 conn_pool[i].pcb = nullptr;
637 det_conn_detach(pcb); // tcpip_thread-marshaled tcp_arg(null) + abort
638 det_conn_abort(pcb);
639 DETWS_OBS_TRANSITION((uint8_t)i, st, ConnState::CONN_FREE, DetConnReason::DET_CONN_R_ABORT);
640 }
642 conn_pool[i].pcb = nullptr;
643 }
644}
645
647{
648 uint8_t n = 0;
649 for (uint8_t i = 0; i < MAX_CONNS; i++)
650 if (conn_pool[i].state == ConnState::CONN_ACTIVE)
651 n++;
652 return n;
653}
654
655uint32_t det_conn_remote_ip(uint8_t slot)
656{
657#ifdef ARDUINO
658 if (slot >= MAX_CONNS)
659 return 0;
660 TcpConn *conn = &conn_pool[slot];
661 if (conn->state == ConnState::CONN_ACTIVE && conn->pcb)
662 return ip4_addr_get_u32(ip_2_ip4(&conn->pcb->remote_ip));
663#else
664 (void)slot;
665#endif
666 return 0;
667}
668
669#ifdef ARDUINO
670// Convert an lwIP address (itself a family-tagged union) into the portable DetIp, network-order
671// bytes preserved. The one owner of the pcb ip_addr_t -> DetIp mapping, for the per-slot accessor
672// below and the accept callback (which has the pcb but no slot yet).
673void det_lwip_to_detip(const ip_addr_t *ra, DetIp *out)
674{
675#if LWIP_IPV6
676 if (IP_IS_V6(ra))
677 {
678 uint8_t b[16];
679 memcpy(b, ip_2_ip6(ra)->addr, 16); // lwIP holds the 16 bytes in network order
680 *out = det_ip_from_v6_bytes(b);
681 return;
682 }
683#endif
684 // ip4_addr_get_u32 is network-order; on the (little-endian) ESP32 the first octet is the low
685 // byte. Peel the octets so DetIp holds them left-to-right.
686 uint32_t be = ip4_addr_get_u32(ip_2_ip4(ra));
687 *out = det_ip_from_v4_octets((uint8_t)be, (uint8_t)(be >> 8), (uint8_t)(be >> 16), (uint8_t)(be >> 24));
688}
689#endif
690
691bool det_conn_remote_addr(uint8_t slot, DetIp *out)
692{
693 if (out)
695#ifdef ARDUINO
696 if (!out || slot >= MAX_CONNS)
697 return false;
698 TcpConn *conn = &conn_pool[slot];
699 if (conn->state != ConnState::CONN_ACTIVE || !conn->pcb)
700 return false;
701 det_lwip_to_detip(&conn->pcb->remote_ip, out);
702 return true;
703#else
704 (void)slot;
705 return false;
706#endif
707}
708
709// Refresh a slot's idle-timeout timestamp from the owning worker while a response body is still
710// being paged out (the file/chunk send pumps call this every poll they run). Such a slot is
711// actively streaming - or briefly blocked on a full send window / a transient link stall - NOT
712// idle, so the CONN_TIMEOUT_MS idle sweep must not reap it mid-transfer: that truncates any body
713// larger than one TCP window (seen on a multi-hundred-MB download as an rc=56 reset or a short
714// read). Dead-peer teardown for an in-flight response stays owned by lwIP's retransmission timers,
715// which abort a black-holed pcb through the err callback. Worker-context safe: it only writes our
716// own last_activity_ms hint (the sent callback writes the same uint32 from tcpip_thread; a torn
717// read of a timestamp is benign).
718void det_conn_touch_active(uint8_t slot_id)
719{
720 if (slot_id >= MAX_CONNS)
721 return;
722 TcpConn *c = &conn_pool[slot_id];
725}
726
728{
729 uint32_t now = detws_millis();
730 for (int i = 0; i < MAX_CONNS; i++)
731 {
732 TcpConn *slot = &conn_pool[i];
733 if (slot->owner != worker_id) // each worker reaps only its own slots
734 continue;
735
736 // ConnState::CONN_CLOSING safety net: a graceful close whose peer never ACKs would
737 // dwell forever. After DETWS_CLOSING_TIMEOUT_MS, force it free so the
738 // fixed pool cannot leak. (The fast path is the sent callback finalizing
739 // on ACK; this only catches a black-holed peer.)
740 if (slot->state == ConnState::CONN_CLOSING)
741 {
742 if ((now - slot->last_activity_ms) < DETWS_CLOSING_TIMEOUT_MS)
743 continue;
744 struct tcp_pcb *cpcb = slot->pcb;
746 slot->pcb = nullptr;
747 if (cpcb)
748 {
749 det_conn_detach(cpcb);
750 det_conn_abort(cpcb);
751 }
753 DetConnReason::DET_CONN_R_DRAINED);
754 continue;
755 }
756
757 if (slot->state != ConnState::CONN_ACTIVE)
758 continue;
759 if ((now - slot->last_activity_ms) < conn_timeout_ms)
760 continue;
761
762 struct tcp_pcb *pcb = slot->pcb;
763 /*
764 * Clear state BEFORE calling tcp_abort so that any lwIP callback
765 * firing on the same PCB during or after abort sees state==ConnState::CONN_FREE
766 * and exits immediately without accessing freed memory.
767 */
769 slot->pcb = nullptr;
770 if (pcb)
771 {
772 det_conn_detach(pcb); // tcpip_thread-marshaled tcp_arg(null) + abort
773 det_conn_abort(pcb);
774 }
776 DetConnReason::DET_CONN_R_TIMEOUT);
777 TcpEvt evt = {EvtType::EVT_ERROR, (uint8_t)i, 0};
778 enqueue(slot, evt);
779 }
780}
781
782// ---------------------------------------------------------------------------
783// lwIP callbacks - execute in tcpip_thread task context
784// These are non-static so listener.cpp can take their address.
785// ---------------------------------------------------------------------------
786
787/**
788 * @brief lwIP receive callback - fires when data arrives on a connection.
789 *
790 * Copies pbuf chain bytes into the ring buffer; the window is reopened later by
791 * det_conn_ack_consumed() as the worker drains (ack-on-consume), not here. If the
792 * whole segment will not fit it is refused (ERR_MEM) for lossless backpressure.
793 * A null pbuf signals graceful remote close (FIN received).
794 */
795err_t lowlevel_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
796{
797 TcpConn *slot = (TcpConn *)arg;
798 if (!slot)
799 return ERR_VAL;
800
801 // While dwelling in ConnState::CONN_CLOSING we have already sent our final response and
802 // are waiting for the ACK. Drain (and ACK) anything the peer still sends so
803 // the window keeps moving, but do not process it. A peer FIN here just means
804 // both sides are done - finalize on the next sent/timeout.
805 if (slot->state == ConnState::CONN_CLOSING)
806 {
807 if (p)
808 {
809 tcp_recved(tpcb, p->tot_len);
810 pbuf_free(p);
811 }
812 return ERR_OK;
813 }
814
815 if (slot->state != ConnState::CONN_ACTIVE)
816 return ERR_VAL;
817
818 if (p == nullptr)
819 {
820 /*
821 * Null pbuf signals graceful remote close (FIN received).
822 * Clear state and pcb before tcp_close so any stale callbacks
823 * are harmless.
824 */
826 slot->pcb = nullptr;
827 tcp_arg(tpcb, nullptr);
828 if (tcp_close(tpcb) != ERR_OK)
829 tcp_abort(tpcb);
831 DetConnReason::DET_CONN_R_CLOSE_REMOTE);
832 TcpEvt evt = {EvtType::EVT_DISCONNECT, slot->id, 0};
833 enqueue(slot, evt);
834 return ERR_OK;
835 }
836
837 /*
838 * Backpressure without data loss: if the whole segment will not fit in the
839 * free ring space, refuse it (return ERR_MEM without freeing) so lwIP retains
840 * it as refused_data and redelivers once the application has drained the
841 * ring; nudge the main loop to drain. The previous code copied what fit and
842 * dropped the rest, silently corrupting bodies larger than the ring (e.g.
843 * streamed uploads). NOTE: needs RX_BUF_SIZE > the largest incoming segment
844 * (TCP_MSS) so a full segment can eventually fit; smaller rings only ever see
845 * sub-MSS requests, which always fit.
846 */
847 if (p->tot_len > det_ring_free(slot->rx_head, slot->rx_tail, RX_BUF_SIZE))
848 {
849 DETWS_OBS_NOTICE(slot->id, ConnState::CONN_ACTIVE, DetConnReason::DET_CONN_R_BACKPRESSURE);
850 TcpEvt evt = {EvtType::EVT_DATA, slot->id, 0}; // wake the loop so it drains the ring
851 enqueue(slot, evt);
852 // Do NOT refresh the idle timer here: a refused segment is redelivered by lwIP every
853 // retransmit until the ring drains, so refreshing on refusal keeps a backpressure-stuck
854 // connection alive forever (idle sweep never reaps it -> slot leak / pool-exhaustion DoS,
855 // e.g. an oversized request line that fills the ring and never completes). The timer is
856 // refreshed below only when data is actually ACCEPTED (real progress), so a connection
857 // that makes no progress times out and is reaped.
858 return ERR_MEM; // do NOT pbuf_free(p): lwIP keeps it and redelivers
859 }
860
861 slot->last_activity_ms = detws_millis(); // accepted data = progress: refresh the idle timer
862
863 // Bulk-copy the segment into the ring via the shared producer primitive: a
864 // contiguous memcpy per pbuf (two across the wrap), advancing a LOCAL head and
865 // publishing rx_head once at the end (one release store for the whole segment).
866 // The free-space check above guarantees it fits, so head can never overrun tail.
867 size_t head = slot->rx_head; // sole producer of head; one acquire load
868 for (struct pbuf *q = p; q != nullptr; q = q->next)
869 head = det_ring_write_span(slot->rx_buffer, RX_BUF_SIZE, head, (const uint8_t *)q->payload, q->len);
870 slot->rx_head = head; // single release store: publishes the whole segment at once
871 size_t bytes_copied = p->tot_len; // the whole segment fit (checked above)
872
873 // Do NOT tcp_recved() here: the window is reopened by det_conn_ack_consumed()
874 // as the worker drains the ring (ack-on-consume), so the advertised window
875 // tracks ring occupancy and a slow consumer cannot overflow the ring. ACKing
876 // on copy decoupled the window from drainage and deadlocked streamed uploads
877 // once RX_BUF_SIZE < TCP_WND (the refused segment past one ring-full stalled).
878 pbuf_free(p);
879
880 if (bytes_copied > 0)
881 {
882 TcpEvt evt = {EvtType::EVT_DATA, slot->id, bytes_copied};
883 enqueue(slot, evt);
884 }
885
886 return ERR_OK;
887}
888
889/**
890 * @brief lwIP sent callback - fires after the stack acknowledges sent bytes.
891 *
892 * Refreshes the idle-timeout timestamp so an active sender is not reaped while its
893 * responses are in flight, and - for a slot dwelling in ConnState::CONN_CLOSING - finalizes
894 * the close once the response has fully drained (the peer ACKed everything).
895 */
896err_t lowlevel_sent_cb(void *arg, struct tcp_pcb *tpcb, u16_t len)
897{
898 TcpConn *slot = (TcpConn *)arg;
899 if (slot)
900 {
902 if (slot->state == ConnState::CONN_CLOSING)
903 closing_check(slot->id, tpcb); // drained? -> tear down + free the slot
904#ifdef ARDUINO
905 // The send window just freed: wake the owning worker so a paced response
906 // (e.g. a large file) resumes now rather than on the next idle sweep.
907 else
909#endif
910 }
911 (void)len;
912 return ERR_OK;
913}
914
915/**
916 * @brief lwIP error callback - fires when the stack detects a fatal error.
917 *
918 * By the time this fires the PCB is already gone internally, so we must NOT
919 * call tcp_close() or tcp_abort(). Null out the slot's pcb pointer and post
920 * EvtType::EVT_ERROR so the session layer resets the protocol state.
921 */
922void lowlevel_err_cb(void *arg, err_t err)
923{
924 TcpConn *slot = (TcpConn *)arg;
925 if (!slot)
926 return;
927
928 /*
929 * When lwIP fires the error callback the PCB has already been freed
930 * internally. We must NOT call tcp_close/tcp_abort here - just null
931 * out our pointer to prevent any future access.
932 */
933 ConnState old = slot->state;
935 slot->pcb = nullptr;
936
937 // A slot that errored while dwelling in ConnState::CONN_CLOSING is already done from the
938 // session's view (its response was sent and the protocol state reset). Just
939 // release the slot + the CLOSING gauge; do not re-post a close event.
940 if (old == ConnState::CONN_CLOSING)
941 {
943 DetConnReason::DET_CONN_R_DRAINED);
944 (void)err;
945 return;
946 }
947
948 DETWS_OBS_TRANSITION(slot->id, ConnState::CONN_ACTIVE, ConnState::CONN_FREE, DetConnReason::DET_CONN_R_ERROR);
949 TcpEvt evt = {EvtType::EVT_ERROR, slot->id, 0};
950 enqueue(slot, evt);
951 (void)err;
952}
#define CONN_POOL_SLOTS
#define CONN_TIMEOUT_MS
Compile-time default for connection idle timeout in milliseconds.
#define DETWS_CLOSING_TIMEOUT_MS
Upper bound (ms) a slot may dwell in ConnState::CONN_CLOSING after a graceful close before the idle s...
#define RX_BUF_SIZE
Ring-buffer capacity in bytes per connection slot.
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
static void check_timeouts(int worker_id=0)
Scan the pool and force-close connections idle for > conn_timeout_ms.
Definition tcp.cpp:727
static void pool_init(const WebServerConfig *cfg=nullptr)
Initialize the connection pool and store the runtime config.
Definition tcp.cpp:608
static uint32_t conn_timeout_ms
Runtime connection-idle timeout in milliseconds.
Definition tcp.h:216
static void stop()
Abort all active connections and reset the pool to ConnState::CONN_FREE.
Definition tcp.cpp:625
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
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
@ 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
A v4 or v6 address in network (big-endian) byte order.
Definition ip.h:52
DetIpFamily family
address family tag
Definition ip.h:53
DetTcpOp op
Definition tcp.cpp:223
const void * data
Definition tcp.cpp:226
uint8_t slot
Definition tcp.cpp:224
err_t result
outcome of the op (DetTcpOp::DET_OP_SEND: whether the write was queued)
Definition tcp.cpp:229
bool flush
DetTcpOp::DET_OP_SEND: also tcp_output() after a successful write (coalesced write+flush)
Definition tcp.cpp:228
struct tcpip_api_call_data base
Definition tcp.cpp:222
struct tcp_pcb * pcb
Definition tcp.cpp:225
u16_t len
Definition tcp.cpp:227
A single TCP connection context.
Definition tcp.h:72
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 id
Fixed slot index (0 … MAX_CONNS-1).
Definition tcp.h:73
uint8_t rx_buffer[RX_BUF_SIZE]
Ring buffer storage.
Definition tcp.h:78
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
Event record posted from lwIP callbacks to the session layer.
Definition tcp.h:149
TaskHandle_t tcpip_task
Definition tcp.cpp:195
Runtime-tunable server parameters.
uint32_t conn_timeout_ms
uint32_t det_conn_remote_ip(uint8_t slot)
Raw source IPv4 of the connection in slot, or 0 if the slot has no active pcb (or on host builds)....
Definition tcp.cpp:655
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
void det_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
Definition tcp.cpp:572
uint32_t detws_ap_ip
softAP IPv4 address (network byte order) for STA/AP interface tagging.
Definition tcp.cpp:349
u16_t det_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
Definition tcp.cpp:398
void det_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:467
void det_conn_ack_consumed(uint8_t slot)
Reopen the TCP receive window by however much slot has drained.
Definition tcp.cpp:427
DetTcpOp
Definition tcp.cpp:175
@ DET_OP_RAWSEND
@ DET_OP_CLOSE_CHECK
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
bool det_conn_send_flush(uint8_t slot, const void *data, u16_t len)
Send len bytes on slot and flush in a single tcpip_thread round-trip.
Definition tcp.cpp:378
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
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
bool det_conn_raw_send(struct tcp_pcb *pcb, const void *data, u16_t len)
Write raw bytes straight to pcb (no TLS), context-safe.
Definition tcp.cpp:449
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
bool det_conn_remote_addr(uint8_t slot, DetIp *out)
The connected peer's address as a family-tagged DetIp (IPv4 or IPv6).
Definition tcp.cpp:691
void det_conn_detach(struct tcp_pcb *pcb)
Detach pcb from its slot's lwIP callbacks before the slot is freed.
Definition tcp.cpp:516
void det_conn_touch_active(uint8_t slot_id)
Refresh slot's idle-timeout timestamp while a response body is in flight.
Definition tcp.cpp:718
void det_conn_abort_slot(uint8_t slot)
Hard-abort connection slot (RST) for a fatal condition. The transport owns the teardown order: free t...
Definition tcp.cpp:496
void det_conn_abort(struct tcp_pcb *pcb)
Hard-abort pcb (RST) for a fatal condition; no graceful FIN.
Definition tcp.cpp:527
uint8_t det_conn_active_count()
Number of server connection slots currently in the CONN_ACTIVE state.
Definition tcp.cpp:646
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
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_conn_detach(struct tcp_pcb *pcb)
Detach pcb from its slot's lwIP callbacks before the slot is freed.
Definition tcp.cpp:516
#define DETWS_OBS_NOTICE(slot, st, reason)
Definition tcp.h:517
@ EVT_DATA
Data received; bytes are already in the ring buffer.
@ EVT_DISCONNECT
Remote peer closed the connection gracefully.
@ EVT_ERROR
lwIP reported an error (PCB may already be freed).
#define DETWS_OBS_TRANSITION(slot, olds, news, reason)
Definition tcp.h:516
ConnState
Lifecycle state of a connection pool slot.
Definition tcp.h:58
@ CONN_CLOSING
FIN sent; waiting for final ACK (reserved).
@ CONN_ACTIVE
Live connection; PCB is valid.
@ CONN_FREE
Slot is available; no PCB is attached.
void det_conn_abort(struct tcp_pcb *pcb)
Hard-abort pcb (RST) for a fatal condition; no graceful FIN.
Definition tcp.cpp:527
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.