DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
forward.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 forward.h
6 * @brief Interface forwarding plane (DETWS_ENABLE_FORWARD) - the v5 bridge / router.
7 *
8 * A forwarding plane over the ingest pipeline. You register **interfaces** (Wi-Fi STA /
9 * AP, Ethernet, a peripheral bus, a radio), each with an egress **send callback**, then
10 * add per-pair **rules** (`src -> dst`, allow or deny, with an optional rate cap). When a
11 * frame arrives on an interface you call det_forward_ingress(); the plane evaluates the
12 * rules and forwards the bytes to **every allowed destination** by calling that
13 * destination's send callback - so the device bridges / routes between its interfaces
14 * instead of only terminating traffic.
15 *
16 * The canonical wiring is DMA-driven: an inbound DMA-complete event (services/dma) is
17 * posted onto the FORWARD lane (services/preempt_queue), whose task calls
18 * det_forward_ingress(), and each destination's send callback hands the bytes to that
19 * interface's egress DMA. The plane itself is decoupled from both - it only knows
20 * interfaces, rules, and the send callbacks - so it is pure and host-testable.
21 *
22 * **Default-deny**: a `(src, dst)` pair is forwarded only when an ALLOW rule matches and
23 * no DENY rule does (a DENY always wins). A frame is never reflected to its source
24 * interface. **Fail-closed**: an exceeded rate cap or a send callback returning false
25 * drops the frame for that destination and is counted - it never blocks. Storage is
26 * static (zero heap): DETWS_FWD_MAX_IFACES interfaces, DETWS_FWD_MAX_RULES rules.
27 *
28 * **Policy routing** (route-by-tag): a policy route (det_forward_route_add) matches a frame by
29 * the same byte-pattern primitive as the ACL - so it keys on any field at a known offset
30 * (EtherType, IP protocol, a port, an address prefix) - and binds the match to a single
31 * **egress interface**. A matched frame is forwarded only to that interface, taking precedence
32 * over the src->dst fan-out (first matching route wins); if no policy route matches, the normal
33 * rules apply. This is policy-based routing layered on the plane: tagged traffic leaves a chosen
34 * NIC / radio. The ingress ACL still runs first, and the same rate-cap / never-reflect /
35 * fail-closed guarantees apply to the chosen egress.
36 *
37 * **Inspection hook** (DETWS_FWD_INSPECT, off by default for cost + privacy): when built in, an
38 * app can register an inspector (det_forward_set_inspector) that runs on every ingress frame
39 * after the ACL and before routing - to observe / parse / meter, and optionally drop it. It is a
40 * flexible app callback (arbitrary logic), complementing the fast fixed-offset ACL.
41 *
42 * @author Douglas Quigg (dstroy0)
43 * @date 2026
44 */
45
46#ifndef DETERMINISTICESPASYNCWEBSERVER_DET_FORWARD_H
47#define DETERMINISTICESPASYNCWEBSERVER_DET_FORWARD_H
48
49#include "ServerConfig.h"
50
51#if DETWS_ENABLE_FORWARD
52
53#include <stddef.h>
54#include <stdint.h>
55
56/** @brief Interface kind (informational; the plane treats all interfaces the same). */
57enum class det_if_kind : uint8_t
58{
59 DET_IF_OTHER = 0,
60 DET_IF_WIFI_STA,
61 DET_IF_WIFI_AP,
62 DET_IF_ETH,
63 DET_IF_BUS,
64 DET_IF_RADIO,
65};
66
67/** @brief Rule action for a `(src, dst)` interface pair or an ACL entry. */
68enum class det_fwd_action : uint8_t
69{
70 DET_FWD_DENY = 0,
71 DET_FWD_ALLOW = 1,
72};
73
74/** @brief Wildcard source interface for an ACL entry (matches a frame from any source). */
75#define DET_FWD_IF_ANY 0xFF
76
77/**
78 * @brief Egress: emit @p len bytes on interface @p if_id.
79 * @return true if the interface accepted the bytes; false drops (counted as a send fail).
80 */
81using det_if_send_fn = bool (*)(uint8_t if_id, const uint8_t *data, uint16_t len, void *ctx);
82
83/** @brief Forwarding counters (monotonic since the last det_forward_reset()). */
84struct det_forward_stats
85{
86 uint32_t frames_in; ///< ingress calls
87 uint32_t forwarded; ///< destination sends that succeeded
88 uint32_t blocked; ///< destinations refused by a DENY / default-deny
89 uint32_t rate_dropped; ///< destinations dropped by a rate cap
90 uint32_t send_fail; ///< destination send callbacks that returned false
91 uint32_t acl_denied; ///< frames dropped at ingress by the access-control list
92 uint32_t policy_routed; ///< frames that matched a policy route (routed to its chosen egress)
93 uint32_t inspect_dropped; ///< frames dropped by the inspection hook (DETWS_FWD_INSPECT)
94};
95
96/** @brief Clear all interfaces, rules, and stats (start from empty). */
97void det_forward_reset(void);
98
99/**
100 * @brief Register an interface and its egress send callback.
101 * @return true; false if @p if_id is already registered, @p send is null, or the table
102 * is full (DETWS_FWD_MAX_IFACES).
103 */
104bool det_forward_add_if(uint8_t if_id, det_if_kind kind, det_if_send_fn send, void *ctx);
105
106/**
107 * @brief Add a forwarding rule. @p rate_cap_per_sec caps ALLOW rules (0 = unlimited); it
108 * is ignored for DENY rules.
109 * @return true; false if the table is full (DETWS_FWD_MAX_RULES).
110 */
111bool det_forward_add_rule(uint8_t src_if, uint8_t dst_if, det_fwd_action action, uint16_t rate_cap_per_sec);
112
113/**
114 * @brief Set the ACL default action - what happens to a frame that matches no ACL entry.
115 * Default det_fwd_action::DET_FWD_ALLOW (an empty ACL passes everything, so the ACL is opt-in);
116 * set det_fwd_action::DET_FWD_DENY for allowlist semantics (only explicitly permitted frames pass).
117 */
118void det_forward_acl_set_default(det_fwd_action action);
119
120/**
121 * @brief Add an ingress access-control entry (evaluated in add order, first match wins).
122 *
123 * A frame is matched when it arrived on @p src_if (or DET_FWD_IF_ANY) and its bytes at
124 * `[offset, offset + patlen)` equal @p pattern under @p mask (each `byte & mask == pattern`).
125 * @p patlen 0 matches any content (interface-only entry); a frame shorter than
126 * `offset + patlen` does not match the entry (evaluation continues). The first matching
127 * entry's @p action (permit/deny) decides; if none match, the ACL default applies.
128 * Denied frames are dropped at ingress before any forwarding rule runs.
129 * @return false if the ACL table is full or @p patlen exceeds DETWS_FWD_ACL_PATLEN.
130 */
131bool det_forward_acl_add(uint8_t src_if, uint16_t offset, const uint8_t *pattern, const uint8_t *mask, uint8_t patlen,
132 det_fwd_action action);
133
134/**
135 * @brief Add a policy route: a frame matching this byte pattern is forwarded only to
136 * @p egress_if, taking precedence over the src->dst rules (first matching route wins).
137 *
138 * The match is the same offset/pattern/mask primitive as the ACL (each `byte & mask == pattern`
139 * at `[offset, offset + patlen)`), keyed on frames from @p src_if or DET_FWD_IF_ANY; @p patlen 0
140 * matches any content (a default route). On a match the frame goes only to @p egress_if -
141 * subject to the same guarantees as a rule: never reflected to the source, dropped (counted) if
142 * @p egress_if is not registered, if @p rate_cap_per_sec (0 = unlimited) is exceeded, or if the
143 * egress send fails. A matched route ends the decision (the normal fan-out is skipped).
144 *
145 * @return true; false if @p patlen exceeds DETWS_FWD_ACL_PATLEN, a non-zero @p patlen has a null
146 * pattern/mask, or the table is full (DETWS_FWD_MAX_ROUTES).
147 */
148bool det_forward_route_add(uint8_t src_if, uint16_t offset, const uint8_t *pattern, const uint8_t *mask, uint8_t patlen,
149 uint8_t egress_if, uint16_t rate_cap_per_sec);
150
151#if DETWS_FWD_INSPECT
152/** @brief The verdict an inspection hook returns for a frame. */
153enum class det_fwd_verdict : uint8_t
154{
155 DET_FWD_INSPECT_PASS = 0, ///< let the frame continue to routing / forwarding
156 DET_FWD_INSPECT_DROP = 1, ///< drop the frame (counted as inspect_dropped)
157};
158
159/**
160 * @brief Ingress inspection hook: observe / parse @p data (from @p src_if, @p len bytes) and
161 * return a ::det_fwd_verdict. Runs after the ACL and before policy routes / the fan-out.
162 * The callback must not block; it may record metrics, log, or decide to drop.
163 */
164using det_fwd_inspect_fn = det_fwd_verdict (*)(uint8_t src_if, const uint8_t *data, uint16_t len, void *ctx);
165
166/**
167 * @brief Install (or clear, with @p fn null) the ingress inspection hook.
168 *
169 * Opt-in twice over: compiled in only when DETWS_FWD_INSPECT is set (cost + privacy), and inert
170 * until an inspector is registered. A DROP verdict discards the frame before any forwarding.
171 */
172void det_forward_set_inspector(det_fwd_inspect_fn fn, void *ctx);
173#endif
174
175/**
176 * @brief Forward a frame that arrived on @p src_if to every allowed destination.
177 *
178 * Evaluates the rules against each registered interface (never the source), applies the
179 * per-rule rate cap, and calls the destination's send callback. Fail-closed.
180 * @return the number of destinations the frame was successfully forwarded to.
181 */
182uint8_t det_forward_ingress(uint8_t src_if, const uint8_t *data, uint16_t len);
183
184/** @brief Copy the current forwarding counters into @p out. */
185void det_forward_get_stats(det_forward_stats *out);
186
187#if !defined(ARDUINO)
188/** @brief Host only: set the millisecond clock the rate cap uses (tests drive the window).
189 * On device the plane reads detws_millis(). */
190void det_forward_test_set_now(uint32_t ms);
191#endif
192
193#endif // DETWS_ENABLE_FORWARD
194
195#endif // DETERMINISTICESPASYNCWEBSERVER_DET_FORWARD_H
User-facing configuration for DeterministicESPAsyncWebServer.