ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
proxy_protocol.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 proxy_protocol.cpp
6 * @brief HAProxy PROXY protocol v1 / v2 parser + builder (pure, host-tested).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_PROXY_PROTOCOL
13
14#include <stdio.h> // snprintf for the v1 dotted-quad text
15#include <string.h>
16
17static const uint8_t kV2Sig[PROXY_V2_SIG_LEN] = {0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D,
18 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A};
19
20static uint16_t rd16(const uint8_t *p)
21{
22 return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
23}
24
25static uint32_t rd32(const uint8_t *p)
26{
27 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
28}
29
30// Parse a dotted-quad IPv4 in [s, s+n) into a host-order uint32; false on malformed.
31static bool parse_ipv4(const char *s, size_t n, uint32_t *out)
32{
33 uint32_t v = 0;
34 int octets = 0;
35 size_t i = 0;
36 while (octets < 4)
37 {
38 if (i >= n || s[i] < '0' || s[i] > '9')
39 {
40 return false;
41 }
42 uint32_t o = 0;
43 size_t digits = 0;
44 while (i < n && s[i] >= '0' && s[i] <= '9')
45 {
46 o = o * 10 + (uint32_t)(s[i] - '0');
47 i++;
48 if (++digits > 3 || o > 255)
49 {
50 return false;
51 }
52 }
53 v = (v << 8) | o;
54 octets++;
55 if (octets < 4)
56 {
57 if (i >= n || s[i] != '.')
58 {
59 return false;
60 }
61 i++;
62 }
63 }
64 if (i != n) // trailing junk
65 {
66 return false;
67 }
68 *out = v;
69 return true;
70}
71
72static bool parse_u16(const char *s, size_t n, uint16_t *out)
73{
74 // n == 0 is never true here: both call sites (below, in parse_v1) pass a token produced by
75 // parse_v1's own space-delimited tokenizer, which only ever records tokens of length >= 1.
76 if (n == 0 || n > 5) // GCOVR_EXCL_BR_LINE
77 {
78 return false;
79 }
80 uint32_t v = 0;
81 for (size_t i = 0; i < n; i++)
82 {
83 if (s[i] < '0' || s[i] > '9')
84 {
85 return false;
86 }
87 v = v * 10 + (uint32_t)(s[i] - '0');
88 }
89 if (v > 0xFFFF)
90 {
91 return false;
92 }
93 *out = (uint16_t)v;
94 return true;
95}
96
97// Parse the v1 text header (already known to start with "PROXY ").
98static bool parse_v1(const uint8_t *buf, size_t len, ProxyInfo *out, size_t *consumed)
99{
100 // Find the terminating CRLF (the line is bounded at 107 octets).
101 size_t crlf = len;
102 size_t scan = len < 108 ? len : 108;
103 for (size_t i = 0; i + 1 < scan; i++)
104 {
105 if (buf[i] == '\r' && buf[i + 1] == '\n')
106 {
107 crlf = i;
108 break;
109 }
110 }
111 if (crlf == len)
112 {
113 return false; // line not complete
114 }
115
116 const char *s = (const char *)buf;
117 // Tokenize the line by single spaces.
118 const char *tok[6];
119 size_t tlen[6];
120 size_t ntok = 0;
121 size_t i = 0;
122 while (i < crlf && ntok < 6)
123 {
124 while (i < crlf && s[i] == ' ')
125 {
126 i++;
127 }
128 if (i >= crlf)
129 {
130 break;
131 }
132 size_t start = i;
133 while (i < crlf && s[i] != ' ')
134 {
135 i++;
136 }
137 tok[ntok] = s + start;
138 tlen[ntok] = i - start;
139 ntok++;
140 }
141 out->version = 1;
142 out->has_addr = false;
143 out->src_addr = out->dst_addr = 0;
144 out->src_port = out->dst_port = 0;
145 *consumed = crlf + 2;
146 // "PROXY TCP4 <src> <dst> <sport> <dport>"; anything else (UNKNOWN/TCP6) yields no addr.
147 if (ntok == 6 && tlen[1] == 4 && memcmp(tok[1], "TCP4", 4) == 0)
148 {
149 if (parse_ipv4(tok[2], tlen[2], &out->src_addr) && parse_ipv4(tok[3], tlen[3], &out->dst_addr) &&
150 parse_u16(tok[4], tlen[4], &out->src_port) && parse_u16(tok[5], tlen[5], &out->dst_port))
151 {
152 out->has_addr = true;
153 }
154 }
155 return true;
156}
157
158bool proxy_parse(const uint8_t *buf, size_t len, ProxyInfo *out, size_t *consumed)
159{
160 if (!buf || !out || !consumed)
161 {
162 return false;
163 }
164
165 // v2: the 12-octet binary signature.
166 if (len >= PROXY_V2_SIG_LEN && memcmp(buf, kV2Sig, PROXY_V2_SIG_LEN) == 0)
167 {
168 if (len < 16) // signature + ver_cmd + fam + 2-octet length
169 {
170 return false;
171 }
172 uint8_t ver_cmd = buf[12];
173 uint8_t fam = buf[13];
174 uint16_t addr_len = rd16(buf + 14);
175 size_t total = 16 + (size_t)addr_len;
176 if (total > len)
177 {
178 return false; // address block not fully buffered
179 }
180 if ((ver_cmd & 0xF0) != 0x20) // must be version 2
181 {
182 return false;
183 }
184 out->version = 2;
185 out->has_addr = false;
186 out->src_addr = out->dst_addr = 0;
187 out->src_port = out->dst_port = 0;
188 if (ver_cmd == PROXY_V2_VER_CMD_PROXY && fam == PROXY_V2_FAM_TCP4 && addr_len >= 12)
189 {
190 out->src_addr = rd32(buf + 16);
191 out->dst_addr = rd32(buf + 20);
192 out->src_port = rd16(buf + 24);
193 out->dst_port = rd16(buf + 26);
194 out->has_addr = true;
195 }
196 *consumed = total;
197 return true;
198 }
199
200 // v1: the "PROXY " text prefix.
201 if (len >= 6 && memcmp(buf, "PROXY ", 6) == 0)
202 {
203 return parse_v1(buf, len, out, consumed);
204 }
205
206 return false; // no PROXY header present
207}
208
209size_t proxy_v1_build(char *buf, size_t cap, uint32_t src_addr, uint32_t dst_addr, uint16_t src_port, uint16_t dst_port)
210{
211 if (!buf)
212 {
213 return 0;
214 }
215 pc_sb sb_buf = {buf, cap, 0, true};
216 pc_sb_put(&sb_buf, "PROXY TCP4 ");
217 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((src_addr >> 24) & 0xFF)));
218 pc_sb_put(&sb_buf, ".");
219 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((src_addr >> 16) & 0xFF)));
220 pc_sb_put(&sb_buf, ".");
221 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((src_addr >> 8) & 0xFF)));
222 pc_sb_put(&sb_buf, ".");
223 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)(src_addr & 0xFF)));
224 pc_sb_put(&sb_buf, " ");
225 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((dst_addr >> 24) & 0xFF)));
226 pc_sb_put(&sb_buf, ".");
227 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((dst_addr >> 16) & 0xFF)));
228 pc_sb_put(&sb_buf, ".");
229 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)((dst_addr >> 8) & 0xFF)));
230 pc_sb_put(&sb_buf, ".");
231 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)(dst_addr & 0xFF)));
232 pc_sb_put(&sb_buf, " ");
233 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)src_port));
234 pc_sb_put(&sb_buf, " ");
235 pc_sb_u32(&sb_buf, (uint32_t)((unsigned)dst_port));
236 pc_sb_put(&sb_buf, "\r\n");
237 int n = (int)pc_sb_finish(&sb_buf);
238 // n < 0 is never true here: the format string uses only %u conversions (no wide/multibyte
239 // specifiers), so snprintf can't fail with an encoding error for this call.
240 if (n < 0 || (size_t)n >= cap) // snprintf truncated (no room for the content + NUL) GCOVR_EXCL_BR_LINE
241 {
242 return 0;
243 }
244 return (size_t)n;
245}
246
247size_t proxy_v2_build(uint8_t *buf, size_t cap, uint32_t src_addr, uint32_t dst_addr, uint16_t src_port,
248 uint16_t dst_port)
249{
250 const size_t total = 16 + 12; // header + TCP/IPv4 address block
251 if (!buf || cap < total)
252 {
253 return 0;
254 }
255 memcpy(buf, kV2Sig, PROXY_V2_SIG_LEN);
256 buf[12] = PROXY_V2_VER_CMD_PROXY;
257 buf[13] = PROXY_V2_FAM_TCP4;
258 buf[14] = 0x00; // address-block length (12), big-endian
259 buf[15] = 0x0C;
260 buf[16] = (uint8_t)(src_addr >> 24);
261 buf[17] = (uint8_t)(src_addr >> 16);
262 buf[18] = (uint8_t)(src_addr >> 8);
263 buf[19] = (uint8_t)(src_addr);
264 buf[20] = (uint8_t)(dst_addr >> 24);
265 buf[21] = (uint8_t)(dst_addr >> 16);
266 buf[22] = (uint8_t)(dst_addr >> 8);
267 buf[23] = (uint8_t)(dst_addr);
268 buf[24] = (uint8_t)(src_port >> 8);
269 buf[25] = (uint8_t)(src_port);
270 buf[26] = (uint8_t)(dst_port >> 8);
271 buf[27] = (uint8_t)(dst_port);
272 return total;
273}
274
275#endif // PC_ENABLE_PROXY_PROTOCOL
HAProxy PROXY protocol codec (PC_ENABLE_PROXY_PROTOCOL) - zero-heap parser + builder for the v1 (text...
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30