ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
relay.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.cpp
6 * @brief TCP relay / DNAT byte pump implementation (see relay.h).
7 */
8
9#include "relay.h"
10
11#if PC_ENABLE_RELAY
12
13#include <string.h>
14
15// Read one non-blocking chunk from src and forward it to dst. Sets *src_eof on a src seam error;
16// returns -1 on a dst send error, else 0. A zero-length read leaves the buffers untouched.
17static int pump_refill(pc_relay_end *src, pc_relay_end *dst, uint8_t *buf, uint16_t *len, uint16_t *off, bool *src_eof,
18 uint32_t *counter)
19{
20 int r = src->recv(src->ctx, buf, PC_RELAY_BUF);
21 if (r < 0)
22 {
23 *src_eof = true;
24 return 0;
25 }
26 if (r > 0)
27 {
28 *len = (uint16_t)r;
29 int s = dst->send(dst->ctx, buf, *len);
30 if (s < 0)
31 {
32 return -1;
33 }
34 *off = (uint16_t)s;
35 *counter += (uint32_t)s;
36 }
37 return 0;
38}
39
40// Pump one direction (src -> dst) one non-blocking pass: flush pending bytes, then read more.
41// @param dst_shut_sent the "shutdown already called" flag for @p dst (the peer that stops receiving
42// once this direction finishes). Returns -1 on a seam error, else 0.
43static int pump(pc_relay_end *src, pc_relay_end *dst, uint8_t *buf, uint16_t *len, uint16_t *off, bool *src_eof,
44 bool *dir_done, bool *dst_shut_sent, uint32_t *counter)
45{
46 if (*dir_done)
47 {
48 return 0;
49 }
50
51 // 1. flush whatever is already buffered for dst
52 if (*off < *len)
53 {
54 int s = dst->send(dst->ctx, buf + *off, (size_t)(*len - *off));
55 if (s < 0)
56 {
57 return -1;
58 }
59 *off = (uint16_t)(*off + s);
60 *counter += (uint32_t)s;
61 }
62
63 // 2. buffer drained: read more from src (unless it already hit EOF), then try to send it right
64 // away so one step moves data end to end
65 if (*off >= *len)
66 {
67 *off = 0;
68 *len = 0;
69 if (!*src_eof && pump_refill(src, dst, buf, len, off, src_eof, counter) < 0)
70 {
71 return -1;
72 }
73 }
74
75 // 3. this direction is finished once src is at EOF and nothing is left to flush
76 if (*src_eof && *off >= *len)
77 {
78 *dir_done = true;
79 // The `!*dst_shut_sent` == false side is unreachable: *dst_shut_sent is written only inside
80 // this block (right below), which can execute at most once per direction over a pc_relay's
81 // lifetime - *dir_done just went true on the line above, and the guard at the top of this
82 // function (`if (*dir_done) return 0;`) skips this whole block on every later call for that
83 // direction. pc_relay_init() zero-fills the struct, so *dst_shut_sent starts false, and
84 // nothing outside this block (and outside pc_relay_init's memset) ever writes it. So on
85 // this - the only - entry, *dst_shut_sent cannot already be true.
86 if (dst->shutdown && !*dst_shut_sent) // GCOVR_EXCL_BR_LINE
87 {
88 dst->shutdown(dst->ctx); // propagate the half-close to the peer that stops receiving
89 *dst_shut_sent = true;
90 }
91 }
92 return 0;
93}
94
95void pc_relay_init(pc_relay *r, const pc_relay_end *client, const pc_relay_end *origin)
96{
97 if (!r || !client || !origin)
98 {
99 return;
100 }
101 memset(r, 0, sizeof(*r));
102 r->a = *client;
103 r->b = *origin;
104}
105
106pc_relay_status pc_relay_step(pc_relay *r)
107{
108 if (!r)
109 {
110 return pc_relay_status::PC_RELAY_ERROR;
111 }
112 // a -> b: dst is b, so b's shutdown fires when this direction finishes
113 if (pump(&r->a, &r->b, r->buf_a2b, &r->a2b_len, &r->a2b_off, &r->a_eof, &r->a2b_done, &r->b_shut_sent,
114 &r->bytes_a2b) < 0)
115 {
116 return pc_relay_status::PC_RELAY_ERROR;
117 }
118 // b -> a: dst is a
119 if (pump(&r->b, &r->a, r->buf_b2a, &r->b2a_len, &r->b2a_off, &r->b_eof, &r->b2a_done, &r->a_shut_sent,
120 &r->bytes_b2a) < 0)
121 {
122 return pc_relay_status::PC_RELAY_ERROR;
123 }
124
125 return (r->a2b_done && r->b2a_done) ? pc_relay_status::PC_RELAY_DONE : pc_relay_status::PC_RELAY_RUNNING;
126}
127
128void pc_relay_note_eof(pc_relay *r, bool origin)
129{
130 if (!r)
131 {
132 return;
133 }
134 // The next pc_relay_step drains that source's buffered bytes, then finishes its direction.
135 if (origin)
136 {
137 r->b_eof = true;
138 }
139 else
140 {
141 r->a_eof = true;
142 }
143}
144
145#endif // PC_ENABLE_RELAY
#define PC_RELAY_BUF
Per-direction relay buffer size (bytes) for services/net/relay (PC_ENABLE_RELAY).
TCP relay / DNAT port forwarding (PC_ENABLE_RELAY) - a bidirectional byte pump.