1use std::net::SocketAddr;
16use std::time::Duration;
17
18use plecto_control::ProxyProtocolTrust;
19use tokio::net::TcpStream;
20
21pub const SIGNATURE: [u8; 12] = *b"\r\n\r\n\0\r\nQUIT\n";
23pub const PREFIX_LEN: usize = 16;
25pub const MAX_DECLARED_LEN: usize = 2048;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ProxyV2 {
33 Local,
36 Proxy { src: SocketAddr },
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Parsed {
44 Complete { decision: ProxyV2, consumed: usize },
46 Incomplete { needed: usize },
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
53pub enum ProxyV2Error {
54 #[error("signature mismatch")]
55 BadSignature,
56 #[error("unsupported version in version/command byte {0:#04x}")]
57 BadVersion(u8),
58 #[error("unsupported command in version/command byte {0:#04x}")]
59 BadCommand(u8),
60 #[error("unsupported family/protocol byte {0:#04x} (only TCP over IPv4/IPv6 is accepted)")]
64 UnsupportedFamilyProtocol(u8),
65 #[error("declared length {declared} exceeds the {MAX_DECLARED_LEN}-byte cap")]
66 DeclaredLenTooLarge { declared: usize },
67 #[error("declared length {declared} is shorter than the {need}-byte address block")]
68 AddressBlockTooShort { declared: usize, need: usize },
69}
70
71pub fn parse_proxy_v2(buf: &[u8]) -> Result<Parsed, ProxyV2Error> {
76 let sig_seen = buf.len().min(SIGNATURE.len());
78 if buf.get(..sig_seen) != SIGNATURE.get(..sig_seen) {
79 return Err(ProxyV2Error::BadSignature);
80 }
81 let (Some(&ver_cmd), Some(&fam_proto), Some(len_hi), Some(len_lo)) = (
82 buf.get(12),
83 buf.get(13),
84 buf.get(14).copied(),
85 buf.get(15).copied(),
86 ) else {
87 return Ok(Parsed::Incomplete { needed: PREFIX_LEN });
88 };
89 if ver_cmd >> 4 != 0x2 {
90 return Err(ProxyV2Error::BadVersion(ver_cmd));
91 }
92 let declared = usize::from(u16::from_be_bytes([len_hi, len_lo]));
93 if declared > MAX_DECLARED_LEN {
94 return Err(ProxyV2Error::DeclaredLenTooLarge { declared });
95 }
96 let needed = PREFIX_LEN + declared;
97 match ver_cmd & 0x0F {
98 0x0 => {
101 if buf.len() < needed {
102 return Ok(Parsed::Incomplete { needed });
103 }
104 Ok(Parsed::Complete {
105 decision: ProxyV2::Local,
106 consumed: needed,
107 })
108 }
109 0x1 => {
111 let need = match fam_proto {
112 0x11 => 12, 0x21 => 36, other => return Err(ProxyV2Error::UnsupportedFamilyProtocol(other)),
115 };
116 if declared < need {
117 return Err(ProxyV2Error::AddressBlockTooShort { declared, need });
118 }
119 if buf.len() < needed {
120 return Ok(Parsed::Incomplete { needed });
121 }
122 let block = buf.get(PREFIX_LEN..PREFIX_LEN + need).unwrap_or(&[]);
123 let src = match fam_proto {
124 0x11 => parse_src_v4(block),
125 _ => parse_src_v6(block),
126 };
127 let Some(src) = src else {
130 return Err(ProxyV2Error::AddressBlockTooShort { declared, need });
131 };
132 Ok(Parsed::Complete {
133 decision: ProxyV2::Proxy { src },
134 consumed: needed,
135 })
136 }
137 _ => Err(ProxyV2Error::BadCommand(ver_cmd)),
138 }
139}
140
141fn parse_src_v4(block: &[u8]) -> Option<SocketAddr> {
144 let ip: [u8; 4] = block.get(0..4)?.try_into().ok()?;
145 let port: [u8; 2] = block.get(8..10)?.try_into().ok()?;
146 Some(SocketAddr::from((ip, u16::from_be_bytes(port))))
147}
148
149fn parse_src_v6(block: &[u8]) -> Option<SocketAddr> {
151 let ip: [u8; 16] = block.get(0..16)?.try_into().ok()?;
152 let port: [u8; 2] = block.get(32..34)?.try_into().ok()?;
153 Some(SocketAddr::from((ip, u16::from_be_bytes(port))))
154}
155
156#[derive(Debug, thiserror::Error)]
158pub(crate) enum ProxyFault {
159 #[error("malformed PROXY v2 header: {0}")]
160 Header(#[from] ProxyV2Error),
161 #[error("PROXY v2 signature from a peer outside the trusted CIDRs")]
162 UntrustedHeader,
163 #[error("connection ended before a complete PROXY v2 header: {0}")]
164 Truncated(std::io::Error),
165 #[error("deadline exceeded before a complete PROXY v2 header")]
166 Deadline,
167}
168
169pub(crate) async fn resolve_peer(
175 stream: &mut TcpStream,
176 peer: SocketAddr,
177 trusted: &ProxyProtocolTrust,
178 deadline: Duration,
179) -> Result<SocketAddr, ProxyFault> {
180 let work = async {
181 if trusted.contains(peer.ip()) {
182 read_trusted_header(stream, peer).await
183 } else {
184 detect_untrusted_header(stream, peer).await
185 }
186 };
187 (tokio::time::timeout(deadline, work).await).unwrap_or(Err(ProxyFault::Deadline))
188}
189
190async fn read_trusted_header(
194 stream: &mut TcpStream,
195 peer: SocketAddr,
196) -> Result<SocketAddr, ProxyFault> {
197 use tokio::io::AsyncReadExt;
198 let mut buf = vec![0u8; PREFIX_LEN];
199 stream
200 .read_exact(&mut buf)
201 .await
202 .map_err(ProxyFault::Truncated)?;
203 loop {
204 match parse_proxy_v2(&buf)? {
205 Parsed::Complete { decision, .. } => {
206 return Ok(match decision {
207 ProxyV2::Local => peer,
208 ProxyV2::Proxy { src } => src,
209 });
210 }
211 Parsed::Incomplete { needed } => {
212 let have = buf.len();
216 if needed <= have {
217 return Err(ProxyFault::Truncated(
218 std::io::ErrorKind::UnexpectedEof.into(),
219 ));
220 }
221 buf.resize(needed, 0);
222 if let Some(tail) = buf.get_mut(have..) {
223 stream
224 .read_exact(tail)
225 .await
226 .map_err(ProxyFault::Truncated)?;
227 }
228 }
229 }
230 }
231}
232
233async fn detect_untrusted_header(
237 stream: &TcpStream,
238 peer: SocketAddr,
239) -> Result<SocketAddr, ProxyFault> {
240 let mut probe = [0u8; SIGNATURE.len()];
241 loop {
242 let n = stream
243 .peek(&mut probe)
244 .await
245 .map_err(ProxyFault::Truncated)?;
246 if n == 0 {
247 return Err(ProxyFault::Truncated(
248 std::io::ErrorKind::UnexpectedEof.into(),
249 ));
250 }
251 if probe.get(..n) != SIGNATURE.get(..n) {
252 return Ok(peer);
253 }
254 if n >= SIGNATURE.len() {
255 return Err(ProxyFault::UntrustedHeader);
256 }
257 tokio::time::sleep(Duration::from_millis(10)).await;
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
268
269 fn header(ver_cmd: u8, fam_proto: u8, declared: u16, payload: &[u8]) -> Vec<u8> {
271 let mut h = Vec::with_capacity(PREFIX_LEN + payload.len());
272 h.extend_from_slice(&SIGNATURE);
273 h.push(ver_cmd);
274 h.push(fam_proto);
275 h.extend_from_slice(&declared.to_be_bytes());
276 h.extend_from_slice(payload);
277 h
278 }
279
280 fn v4_block(src: SocketAddrV4, dst: SocketAddrV4) -> Vec<u8> {
281 let mut b = Vec::with_capacity(12);
282 b.extend_from_slice(&src.ip().octets());
283 b.extend_from_slice(&dst.ip().octets());
284 b.extend_from_slice(&src.port().to_be_bytes());
285 b.extend_from_slice(&dst.port().to_be_bytes());
286 b
287 }
288
289 fn v6_block(src: SocketAddrV6, dst: SocketAddrV6) -> Vec<u8> {
290 let mut b = Vec::with_capacity(36);
291 b.extend_from_slice(&src.ip().octets());
292 b.extend_from_slice(&dst.ip().octets());
293 b.extend_from_slice(&src.port().to_be_bytes());
294 b.extend_from_slice(&dst.port().to_be_bytes());
295 b
296 }
297
298 #[test]
299 fn parse_table() {
300 let src4 = SocketAddrV4::new(Ipv4Addr::new(203, 0, 113, 9), 51234);
301 let dst4 = SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 443);
302 let src6 = SocketAddrV6::new("2001:db8::9".parse::<Ipv6Addr>().unwrap(), 51234, 0, 0);
303 let dst6 = SocketAddrV6::new("2001:db8::1".parse::<Ipv6Addr>().unwrap(), 443, 0, 0);
304
305 struct Case {
306 name: &'static str,
307 input: Vec<u8>,
308 want: Result<Parsed, ProxyV2Error>,
309 }
310 let cases = vec![
311 Case {
312 name: "proxy over tcp/ipv4 restores the source",
313 input: header(0x21, 0x11, 12, &v4_block(src4, dst4)),
314 want: Ok(Parsed::Complete {
315 decision: ProxyV2::Proxy { src: src4.into() },
316 consumed: 28,
317 }),
318 },
319 Case {
320 name: "proxy over tcp/ipv6 restores the source",
321 input: header(0x21, 0x21, 36, &v6_block(src6, dst6)),
322 want: Ok(Parsed::Complete {
323 decision: ProxyV2::Proxy { src: src6.into() },
324 consumed: 52,
325 }),
326 },
327 Case {
328 name: "tlv bytes after the address block are skipped",
329 input: header(0x21, 0x11, 12 + 5, &{
330 let mut p = v4_block(src4, dst4);
331 p.extend_from_slice(&[0x04, 0x00, 0x02, 0xAA, 0xBB]); p
333 }),
334 want: Ok(Parsed::Complete {
335 decision: ProxyV2::Proxy { src: src4.into() },
336 consumed: 33,
337 }),
338 },
339 Case {
340 name: "local with zero length uses the real endpoints",
341 input: header(0x20, 0x00, 0, &[]),
342 want: Ok(Parsed::Complete {
343 decision: ProxyV2::Local,
344 consumed: 16,
345 }),
346 },
347 Case {
348 name: "local must still skip its declared bytes (spec: never assume zero)",
349 input: header(0x20, 0x11, 12, &v4_block(src4, dst4)),
350 want: Ok(Parsed::Complete {
351 decision: ProxyV2::Local,
352 consumed: 28,
353 }),
354 },
355 Case {
356 name: "empty buffer needs the fixed prefix",
357 input: Vec::new(),
358 want: Ok(Parsed::Incomplete { needed: PREFIX_LEN }),
359 },
360 Case {
361 name: "a strict signature prefix is incomplete, not a mismatch",
362 input: b"\r\n\r".to_vec(),
363 want: Ok(Parsed::Incomplete { needed: PREFIX_LEN }),
364 },
365 Case {
366 name: "prefix complete but address block still in flight",
367 input: header(0x21, 0x11, 12, &v4_block(src4, dst4)[..4]),
368 want: Ok(Parsed::Incomplete { needed: 28 }),
369 },
370 Case {
371 name: "signature mismatch",
372 input: b"GET /api HTTP/1.1\r\n".to_vec(),
373 want: Err(ProxyV2Error::BadSignature),
374 },
375 Case {
376 name: "v1 text form is not accepted (ADR 000057)",
377 input: b"PROXY TCP4 203.0.113.9 10.0.0.1 51234 443\r\n".to_vec(),
378 want: Err(ProxyV2Error::BadSignature),
379 },
380 Case {
381 name: "version nibble other than 2 is rejected",
382 input: header(0x11, 0x11, 12, &v4_block(src4, dst4)),
383 want: Err(ProxyV2Error::BadVersion(0x11)),
384 },
385 Case {
386 name: "command nibble beyond LOCAL/PROXY is rejected",
387 input: header(0x22, 0x11, 12, &v4_block(src4, dst4)),
388 want: Err(ProxyV2Error::BadCommand(0x22)),
389 },
390 Case {
391 name: "proxy with AF_UNSPEC is cut, not degraded to the LB address",
392 input: header(0x21, 0x00, 0, &[]),
393 want: Err(ProxyV2Error::UnsupportedFamilyProtocol(0x00)),
394 },
395 Case {
396 name: "proxy over AF_UNIX is rejected",
397 input: header(0x21, 0x31, 216, &[0u8; 216]),
398 want: Err(ProxyV2Error::UnsupportedFamilyProtocol(0x31)),
399 },
400 Case {
401 name: "proxy over DGRAM is rejected",
402 input: header(0x21, 0x12, 12, &v4_block(src4, dst4)),
403 want: Err(ProxyV2Error::UnsupportedFamilyProtocol(0x12)),
404 },
405 Case {
406 name: "declared length above the 2 KiB cap is rejected before any read",
407 input: header(0x21, 0x11, 2049, &[]),
408 want: Err(ProxyV2Error::DeclaredLenTooLarge { declared: 2049 }),
409 },
410 Case {
411 name: "declared length shorter than the ipv4 address block",
412 input: header(0x21, 0x11, 11, &[0u8; 11]),
413 want: Err(ProxyV2Error::AddressBlockTooShort {
414 declared: 11,
415 need: 12,
416 }),
417 },
418 Case {
419 name: "declared length shorter than the ipv6 address block",
420 input: header(0x21, 0x21, 12, &v4_block(src4, dst4)),
421 want: Err(ProxyV2Error::AddressBlockTooShort {
422 declared: 12,
423 need: 36,
424 }),
425 },
426 ];
427 for case in cases {
428 let got = parse_proxy_v2(&case.input);
429 assert_eq!(got, case.want, "case: {}", case.name);
430 }
431 }
432}