DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
relay_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 relay_listener.cpp
6 * @brief Server-side TCP relay / DNAT listener (see relay_listener.h). Bridges a ConnProto::PROTO_RELAY
7 * connection to an origin det_client connection via the pure relay engine.
8 */
9
10#include "relay_listener.h"
11
12#if DETWS_ENABLE_RELAY
13
17#include "relay.h"
18#if DETWS_ENABLE_RADIO_POWER
19#include "services/radio_power/radio_power.h" // keep the radio awake during a relayed transfer
20#endif
21#include <string.h>
22
23namespace
24{
25
26// One published front port -> origin.
27struct RelayBind
28{
29 bool active;
30 uint8_t listener_id;
31 char host[DETWS_RELAY_HOST_MAX];
32 uint16_t port;
33};
34
35// One live relayed connection: an inbound conn slot bridged to an origin det_client.
36struct RelayBridge
37{
38 bool active;
39 uint8_t conn_slot;
40 int origin_cid;
41 DetRelay relay;
42};
43
44// All of the listener's mutable state in one owned, feature-gated context (least-privilege; the
45// owner-context guard requires the single file-scope mutable to be a `*Ctx` instance).
46struct RelayListenerCtx
47{
48 RelayBind binds[DETWS_RELAY_MAX_PUBLISH];
49 RelayBridge bridges[DETWS_RELAY_MAX_CONNS];
50 bool registered;
51};
52RelayListenerCtx s_ctx;
53
54RelayBind *bind_by_listener(uint8_t lid)
55{
56 for (int i = 0; i < DETWS_RELAY_MAX_PUBLISH; i++)
57 if (s_ctx.binds[i].active && s_ctx.binds[i].listener_id == lid)
58 return &s_ctx.binds[i];
59 return nullptr;
60}
61
62RelayBridge *bridge_by_conn(uint8_t slot)
63{
64 for (int i = 0; i < DETWS_RELAY_MAX_CONNS; i++)
65 if (s_ctx.bridges[i].active && s_ctx.bridges[i].conn_slot == slot)
66 return &s_ctx.bridges[i];
67 return nullptr;
68}
69
70int bridge_find_free()
71{
72 for (int i = 0; i < DETWS_RELAY_MAX_CONNS; i++)
73 if (!s_ctx.bridges[i].active)
74 return i;
75 return -1;
76}
77
78// --- Relay seams (ctx = the RelayBridge). ---
79// Inbound (a) = the accepted server connection; its EOF arrives out of band via relay_on_close.
80int a_recv(void *c, uint8_t *buf, size_t cap)
81{
82 RelayBridge *br = (RelayBridge *)c;
83 if (det_conn_available(br->conn_slot))
84 return (int)det_conn_read(br->conn_slot, buf, cap);
85 return 0;
86}
87int a_send(void *c, const uint8_t *buf, size_t len)
88{
89 RelayBridge *br = (RelayBridge *)c;
90 // Send as much as the inbound TCP send window currently allows (partial), not all-or-nothing: a
91 // whole DETWS_RELAY_BUF chunk rarely fits tcp_sndbuf in one shot, and a failed all-or-nothing send
92 // forwards zero bytes and stalls the transfer. room==0 is real backpressure - the pump retries.
93 u16_t room = det_conn_sndbuf(br->conn_slot);
94 if (room == 0)
95 return 0;
96 u16_t n = (len < (size_t)room) ? (u16_t)len : room;
97 return det_conn_send(br->conn_slot, buf, n) ? (int)n : 0;
98}
99// Origin (b) = the outbound det_client; it reports EOF through the recv seam.
100int b_recv(void *c, uint8_t *buf, size_t cap)
101{
102 RelayBridge *br = (RelayBridge *)c;
103 size_t n = det_client_read(br->origin_cid, buf, cap);
104 if (n)
105 return (int)n;
106 return det_client_is_closed(br->origin_cid) ? -1 : 0;
107}
108int b_send(void *c, const uint8_t *buf, size_t len)
109{
110 RelayBridge *br = (RelayBridge *)c;
111 return det_client_send(br->origin_cid, buf, len) ? (int)len : 0;
112}
113
114// Close the origin (and optionally the inbound) and free the bridge. active=false first so a
115// re-entrant close callback is a no-op.
116void teardown(RelayBridge *br, bool close_inbound)
117{
118 br->active = false;
119#if DETWS_ENABLE_RADIO_POWER
120 detws_radio_busy_release(); // this bridge is done relaying
121#endif
122 det_client_close(br->origin_cid);
123 if (close_inbound)
124 det_conn_close(br->conn_slot);
125}
126
127// Pump the bridge one pass and tear it down if the origin ended or the pump errored.
128void service(uint8_t slot)
129{
130 RelayBridge *br = bridge_by_conn(slot);
131 if (!br)
132 return;
133 // Drain as much as the buffers allow this pass: keep stepping while a step actually moves bytes,
134 // so one poll forwards the whole buffered origin RX ring (DETWS_CLIENT_RX_BUF) instead of a single
135 // DETWS_RELAY_BUF chunk. Bounded by DETWS_RELAY_DRAIN_MAX so one busy bridge cannot starve others.
136 for (int pass = 0; pass < DETWS_RELAY_DRAIN_MAX; pass++)
137 {
138 uint32_t moved = br->relay.bytes_a2b + br->relay.bytes_b2a;
139 DetRelayStatus st = det_relay_step(&br->relay);
140 if (st == DetRelayStatus::DET_RELAY_ERROR || st == DetRelayStatus::DET_RELAY_DONE)
141 {
142 teardown(br, true);
143 return;
144 }
145 if (br->relay.bytes_a2b + br->relay.bytes_b2a == moved)
146 break; // no progress this pass; nothing more buffered to move right now
147 }
148 // origin closed and everything it sent has been forwarded -> nothing more to do
149 if (det_client_is_closed(br->origin_cid) && det_client_available(br->origin_cid) == 0 &&
150 br->relay.b2a_off >= br->relay.b2a_len)
151 teardown(br, true);
152}
153
154void relay_on_accept(uint8_t slot)
155{
156 RelayBind *bd = bind_by_listener(det_conn_listener_id(slot));
157 if (!bd)
158 {
159 det_conn_close(slot); // no origin published for this listener
160 return;
161 }
162 int idx = bridge_find_free();
163 if (idx < 0)
164 {
165 det_conn_close(slot); // bridge table full
166 return;
167 }
168 int cid = det_client_open(bd->host, bd->port, DETWS_RELAY_CONNECT_MS); // blocking connect (LAN origin)
169 if (cid < 0)
170 {
171 det_conn_close(slot); // origin unreachable
172 return;
173 }
174 RelayBridge *br = &s_ctx.bridges[idx];
175 br->active = true;
176 br->conn_slot = slot;
177 br->origin_cid = cid;
178 DetRelayEnd a = {a_recv, a_send, nullptr, br};
179 DetRelayEnd b = {b_recv, b_send, nullptr, br};
180 det_relay_init(&br->relay, &a, &b);
181#if DETWS_ENABLE_RADIO_POWER
182 detws_radio_busy_hold(); // hold the radio awake for the life of this bridge
183#endif
184}
185
186void relay_on_data(uint8_t slot)
187{
188 service(slot);
189}
190
191void relay_on_poll(uint8_t slot)
192{
193 if (!det_conn_active(slot))
194 return;
195 service(slot);
196}
197
198void relay_on_close(uint8_t slot)
199{
200 RelayBridge *br = bridge_by_conn(slot);
201 if (br)
202 teardown(br, false); // the transport already owns the closing inbound slot
203}
204
205const ProtoHandler s_relay_handler = {relay_on_accept, relay_on_data, relay_on_close, relay_on_poll};
206
207} // namespace
208
209bool det_relay_publish(uint8_t listener_id, const char *origin_host, uint16_t origin_port)
210{
211 if (!origin_host)
212 return false;
213 size_t hl = strnlen(origin_host, DETWS_RELAY_HOST_MAX + 1);
214 if (hl == 0 || hl >= DETWS_RELAY_HOST_MAX)
215 return false;
216 int idx = -1;
217 for (int i = 0; i < DETWS_RELAY_MAX_PUBLISH; i++)
218 if (!s_ctx.binds[i].active)
219 {
220 idx = i;
221 break;
222 }
223 if (idx < 0)
224 return false;
225 s_ctx.binds[idx].active = true;
226 s_ctx.binds[idx].listener_id = listener_id;
227 memcpy(s_ctx.binds[idx].host, origin_host, hl + 1);
228 s_ctx.binds[idx].port = origin_port;
229 if (!s_ctx.registered)
230 {
231 proto_register(ConnProto::PROTO_RELAY, &s_relay_handler);
232 s_ctx.registered = true;
233 }
234 return true;
235}
236
237void det_relay_listener_reset(void)
238{
239 for (int i = 0; i < DETWS_RELAY_MAX_PUBLISH; i++)
240 s_ctx.binds[i].active = false;
241 for (int i = 0; i < DETWS_RELAY_MAX_CONNS; i++)
242 if (s_ctx.bridges[i].active)
243 {
244 s_ctx.bridges[i].active = false;
245#if DETWS_ENABLE_RADIO_POWER
246 detws_radio_busy_release(); // balance the hold taken when the bridge was opened
247#endif
248 }
249}
250
251#endif // DETWS_ENABLE_RELAY
#define DETWS_RELAY_HOST_MAX
Max origin hostname length (bytes, incl. NUL) stored per published relay port.
@ PROTO_RELAY
TCP relay / DNAT (DETWS_ENABLE_RELAY): bridge to an origin det_client connection.
#define DETWS_RELAY_MAX_PUBLISH
Max published relay ports (bind table size) for the relay listener (DETWS_ENABLE_RELAY).
#define DETWS_RELAY_CONNECT_MS
Blocking connect timeout (ms) when the relay listener dials the origin on a new inbound.
#define DETWS_RELAY_MAX_CONNS
Max concurrent relayed connections (bridge table size) for the relay listener (DETWS_ENABLE_RELAY)....
#define DETWS_RELAY_DRAIN_MAX
Max det_relay_step passes per poll for the relay listener (DETWS_ENABLE_RELAY).
size_t det_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:326
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Layer 5 (Session) - per-protocol connection handler dispatch table.
void proto_register(ConnProto proto, const ProtoHandler *h)
Register h for protocol proto (replaces any previous handler).
Definition session.cpp:38
WiFi radio power controls (DETWS_ENABLE_RADIO_POWER).
TCP relay / DNAT port forwarding (DETWS_ENABLE_RELAY) - a bidirectional byte pump.
Server-side TCP relay / DNAT listener (DETWS_ENABLE_RELAY) - publish an internal host:port on a serve...
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
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
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
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.