ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
relay.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file relay.h
6 * @brief TCP relay / DNAT port forwarding (PROTOCORE_ENABLE_RELAY) - a bidirectional byte pump.
7 *
8 * Publishes an internal `host:port` through the server: an inbound (accepted) connection is relayed
9 * to an origin (an outbound connection to the internal service), moving bytes in both directions.
10 * The engine is pure - it touches the two sockets only through send/recv seams - so it is
11 * host-testable and rides `protocore_client` on the device. The app drives it: each poll tick (or whenever
12 * a socket is readable/writable) it calls protocore_relay_step() until the relay reports DONE, then closes
13 * both sockets.
14 *
15 * Correctness details:
16 * - **Backpressure**: a `send` seam may accept fewer bytes than offered; the un-accepted bytes are
17 * carried in a per-direction buffer and retried on the next step before more are read.
18 * - **Independent half-close**: each direction finishes when its source signals EOF and its buffer
19 * drains. When a direction finishes, the opposite peer's optional `shutdown` seam is called once
20 * (propagating the half-close so the origin sees the client's FIN); the relay is DONE only when
21 * both directions have finished.
22 *
23 * @author Douglas Quigg (dstroy0)
24 * @date 2026
25 */
26
27#ifndef PROTOCORE_RELAY_H
28#define PROTOCORE_RELAY_H
29
30#include "protocore_config.h" // the entry point: protocore_types.h for the widths
31
32#if PROTOCORE_ENABLE_RELAY
33
35
36// This module holds nothing between calls, so it carves no borrow and states none. An entry
37// takes one all the same, and never reads it, so every namespace in the tree is invoked the
38// same way.
39
40/** @brief protocore_relay_step() outcome. */
41typedef enum PROTO_ENUM_PACKED
42{
43 PROTOCORE_RELAY_ERROR = -1, ///< a send/recv seam reported an error; the caller should close both sides
44 PROTOCORE_RELAY_RUNNING = 0, ///< still relaying (keep stepping)
45 PROTOCORE_RELAY_DONE = 1, ///< both directions finished (EOF + drained); the caller closes both sides
46} protocore_relay_status;
47
48/**
49 * @brief Read up to @p cap bytes from the peer into @p buf.
50 * @return bytes read (> 0), 0 if none are available now, or < 0 once the peer has closed its send
51 * side (EOF) or errored.
52 */
53typedef int (*protocore_relay_recv_fn)(void *ctx, uint8_t *buf, size_t cap);
54
55/**
56 * @brief Write up to @p len bytes to the peer.
57 * @return bytes accepted (> 0, may be < @p len under backpressure), 0 if none can be accepted right
58 * now, or < 0 on error.
59 */
60typedef int (*protocore_relay_send_fn)(void *ctx, const uint8_t *buf, size_t len);
61
62/** @brief Optional: signal the peer that no more data will be sent to it (a write-side half-close). */
63typedef void (*protocore_relay_shutdown_fn)(void *ctx);
64
65/** @brief One end of a relay (a socket, behind seams). @p shutdown may be null. */
66typedef struct
67{
68 protocore_relay_recv_fn recv;
69 protocore_relay_send_fn send;
70 protocore_relay_shutdown_fn shutdown;
71 void *ctx;
72} protocore_relay_end;
73
74/** @brief A relay between two ends. Owns the per-direction carry buffers; zero heap. */
75typedef struct
76{
77 protocore_relay_end a;
78 protocore_relay_end b;
79 uint8_t buf_a2b[PROTOCORE_RELAY_BUF];
80 uint8_t buf_b2a[PROTOCORE_RELAY_BUF];
81 uint16_t a2b_len; ///< bytes read from a pending send to b
82 uint16_t a2b_off; ///< how many of those already sent
83 uint16_t b2a_len;
84 uint16_t b2a_off;
85 proto_bool a_eof; ///< the recv side of a has hit EOF
86 proto_bool b_eof; ///< the recv side of b has hit EOF
87 proto_bool a2b_done; ///< the a->b direction has finished (EOF + drained)
88 proto_bool b2a_done; ///< the b->a direction has finished (EOF + drained)
89 proto_bool a_shut_sent; ///< the shutdown seam of a has been called
90 proto_bool b_shut_sent; ///< the shutdown seam of b has been called
91 uint32_t bytes_a2b; ///< bytes relayed a->b (observability)
92 uint32_t bytes_b2a; ///< bytes relayed b->a (observability)
93} protocore_relay;
94
95/** @brief What init takes: r, client, origin. */
96typedef struct
97{
98 protocore_relay *r;
99 const protocore_relay_end *client;
100 const protocore_relay_end *origin;
101} RelayInitArgs;
102
103/** @brief What step takes: r. */
104typedef struct
105{
106 protocore_relay *r;
107} RelayStepArgs;
108
109/** @brief What note_eof takes: r, origin. */
110typedef struct
111{
112 protocore_relay *r;
113 proto_bool origin; ///< false for the client (inbound) side, true for the origin (outbound) side
114} RelayNoteEofArgs;
115
116/**
117 * @brief TCP relay / DNAT port forwarding (PROTOCORE_ENABLE_RELAY) - a bidirectional byte pump. Publishes an internal
118 * ...
119 *
120 * A caller sets the members a call takes, invokes it through ::Relay with the bytes it runs
121 * out of, and reads the outcome off the same handle.
122 *
123 * Relay.init_args.r = ...;
124 * Relay.init_args.client = ...;
125 * Relay.init_args.origin = ...;
126 * Relay.init(work);
127 *
128 * @var RelayNs::init_args what init takes: r, client, origin
129 * @var RelayNs::step_args what step takes: r
130 * @var RelayNs::note_eof_args what note_eof takes: r, origin
131 * @var RelayNs::ok a call's true/false outcome
132 * @var RelayNs::status a ::protocore_relay_status. Call repeatedly (per poll tick) until ...
133 * @var RelayNs::init initialize a relay between client (the inbound connection) and ...
134 * @var RelayNs::step do one non-blocking pass: flush any pending bytes and read more, in ...
135 * @var RelayNs::note_eof signal that a peer's send side has closed, when the transport ...
136 *
137 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
138 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
139 * a caller drives every namespace the same way.
140 */
141typedef struct
142{
143 RelayInitArgs init_args;
144 RelayStepArgs step_args;
145 RelayNoteEofArgs note_eof_args;
146 proto_bool ok;
147 protocore_relay_status status;
148} RelayVars;
149
150/** @brief The operands and the outcome. */
151extern RelayVars RelayV;
152
153/** @brief The entries. */
154typedef struct
155{
156 void (*const init)(uint8_t *work);
157 void (*const step)(uint8_t *work);
158 void (*const note_eof)(uint8_t *work);
159} RelayNs;
160
161// What the table binds, defined once in the .c and taking one parameter each: everything
162// else an entry needs is an operand in RelayV or a region of the borrow at a fixed offset.
163void protocore_relay_init(uint8_t *work);
164void protocore_relay_step(uint8_t *work);
165void protocore_relay_note_eof(uint8_t *work);
166
167// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
168// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
169// `Relay.init(work)` resolves to a named function and becomes a DIRECT call. An extern table
170// leaves the call indirect and the symbol live at every level, -O2 -flto included.
171static const RelayNs Relay __attribute__((unused)) = {
172 .init = protocore_relay_init,
173 .step = protocore_relay_step,
174 .note_eof = protocore_relay_note_eof,
175};
176
178
179#endif // PROTOCORE_ENABLE_RELAY
180
181#endif // PROTOCORE_RELAY_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_RELAY_BUF
Per-direction relay buffer size (bytes) for server/net/relay (PROTOCORE_ENABLE_RELAY).
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97