Skip to main content

sozu_lib/protocol/proxy_protocol/
parser.rs

1//! PROXY-v2 wire parser.
2//!
3//! Decodes the 12-byte PROXY-v2 signature plus the variable address block
4//! defined by the HAProxy PROXY protocol specification. Returns a
5//! `HeaderV2` to the caller (`expect.rs` / `relay.rs`); rejects malformed
6//! framing through `nom::Err` so the session can close cleanly. No I/O.
7
8use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
9
10use nom::{
11    Err, IResult,
12    bytes::streaming::{tag, take},
13    error::{Error, ErrorKind, ParseError},
14    number::streaming::{be_u8, be_u16},
15};
16
17use crate::protocol::proxy_protocol::header::{Command, HeaderV2, ProxyAddr};
18
19const PROTOCOL_SIGNATURE_V2: [u8; 12] = [
20    0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
21];
22
23fn parse_command(i: &[u8]) -> IResult<&[u8], Command> {
24    let i2 = i;
25    let (i, cmd) = be_u8(i)?;
26    // `be_u8` consumes exactly one byte on success; the remainder must be one
27    // shorter than the input we were given.
28    debug_assert_eq!(
29        i.len() + 1,
30        i2.len(),
31        "command byte parse consumes exactly one byte"
32    );
33    match cmd {
34        0x20 => Ok((i, Command::Local)),
35        0x21 => Ok((i, Command::Proxy)),
36        _ => Err(Err::Error(Error::from_error_kind(i2, ErrorKind::Switch))),
37    }
38}
39
40pub fn parse_v2_header(i: &[u8]) -> IResult<&[u8], HeaderV2> {
41    let input_len = i.len();
42    let (i, _) = tag(&PROTOCOL_SIGNATURE_V2[..])(i)?;
43    let (i, command) = parse_command(i)?;
44    let (i, family) = be_u8(i)?;
45    let (i, len) = be_u16(i)?;
46    let (i, data) = take(len)(i)?;
47    // The length field gates exactly `len` address bytes; `take` already
48    // enforces availability, so `data` is precisely that many bytes.
49    debug_assert_eq!(
50        data.len(),
51        len as usize,
52        "address block must be exactly the declared length"
53    );
54    let (data_rest, parsed_addr) = parse_addr_v2(family)(data)?;
55    // The address parser may not consume every advertised byte (TLVs / over-
56    // long length fields are tolerated by leaving a tail), but it must never
57    // read past the block it was handed.
58    debug_assert!(
59        data_rest.len() <= data.len(),
60        "address parser cannot grow its input"
61    );
62
63    // Postcondition: a parser never grows its input — the unconsumed remainder
64    // is no longer than the original, and the consumed prefix (the full v2
65    // header) is the 16-byte fixed part plus the declared address length.
66    debug_assert!(i.len() <= input_len, "parser cannot grow its input");
67    debug_assert_eq!(
68        input_len - i.len(),
69        PROTOCOL_SIGNATURE_V2.len() + 1 + 1 + 2 + len as usize,
70        "consumed header length must reconcile with the signature, fixed fields, and declared address length"
71    );
72    // The family byte is in the enumerated set the address parser accepts: a
73    // rejected family would have short-circuited via `?` above, so on this
74    // success path the high nibble is AF_UNSPEC/INET/INET6.
75    debug_assert!(
76        matches!((family >> 4) & 0x0f, 0x00..=0x02),
77        "accepted family nibble must be AF_UNSPEC, AF_INET, or AF_INET6"
78    );
79
80    // HAProxy PROXY protocol specification, §2.2: `LOCAL` describes a
81    // connection the upstream proxy originated on its own behalf (typically a
82    // health check), and the receiver must use the real socket endpoints and
83    // ignore the header's address block. Nothing on the wire ties `LOCAL` to
84    // `AF_UNSPEC` -- only HAProxy's own emitter happens to pair them -- so a
85    // crafted peer can send `LOCAL` with a fully populated `AF_INET` /
86    // `AF_INET6` block. `parse_addr_v2` switches on the family nibble alone,
87    // so that block used to reach every consumer of `HeaderV2::addr`. There are
88    // six of them, not three. Four are TCP-side and read this `ProxyAddr`
89    // directly: `expect.rs`, `relay.rs`, `tcp_preread/mod.rs`, and
90    // `TcpSession::effective_session_address` (`lib/src/tcp.rs`), which reads
91    // `ExpectProxyProtocol::addresses` / `RelayProxyProtocol::addresses` itself
92    // rather than through `into_pipe` and feeds the raw-TCP
93    // `max_connections_per_ip` gate in `TcpSession::connect_to_backend`
94    // (`lib/src/tcp.rs`). The other two are the HTTP and HTTPS expect upgrades,
95    // which read the same value back out of `ExpectProxyProtocol::addresses` in
96    // `HttpSession::upgrade_expect` (`lib/src/http.rs`) and
97    // `HttpsSession::upgrade` (`lib/src/https.rs`). All six attribute
98    // `ProxyAddr::source()` to the client: any peer could therefore forge the
99    // source address Sōzu records in its access logs, injects as `X-Real-IP`,
100    // and counts against `max_connections_per_ip`.
101    //
102    // Discarding it here rather than at each of those six keeps one decision
103    // in one place: none of them reads `command` or `family`, none
104    // re-serializes the parsed header (`relay.rs` forwards the buffered bytes
105    // verbatim), and all six obtain the address exclusively through this
106    // `ProxyAddr`.
107    //
108    // `AfUnspec` is a variant all six already handle, but they do NOT handle
109    // it alike, and that split is the operator-visible behaviour:
110    //
111    // * the four TCP-side consumers fall back to the front socket's
112    //   `peer_addr`, so the session proceeds, attributed to the real peer;
113    // * the HTTP and HTTPS expect upgrades require BOTH endpoints and get
114    //   neither -- `AfUnspec` returns `None` from `ProxyAddr::source()` and
115    //   `ProxyAddr::destination()` alike
116    //   (`lib/src/protocol/proxy_protocol/header.rs`) -- so `upgrade_expect`
117    //   returns `None` and `HttpSession::upgrade` reports `SessionIsToBeClosed`
118    //   (`lib/src/http.rs`). Such a session is closed at the expect stage, not
119    //   re-attributed to `peer_addr`.
120    //
121    // That close is not a regression for legitimate traffic: HAProxy pairs
122    // `LOCAL` with `AF_UNSPEC`, which already parsed to `AfUnspec`, so those
123    // sessions already closed. Only the forged populated block changes, from
124    // "upgrade with attacker-chosen addresses" to "close".
125    //
126    // The block is still parsed above, not skipped: a `LOCAL` header with a
127    // malformed or unknown family must keep being rejected, and its declared
128    // length must keep delimiting the header, so framing is unchanged.
129    // Matched exhaustively on purpose: a future command must make an explicit
130    // attribution decision rather than inherit `PROXY`'s by default.
131    let addr = match command {
132        Command::Local => ProxyAddr::AfUnspec,
133        Command::Proxy => parsed_addr,
134    };
135
136    Ok((
137        i,
138        (HeaderV2 {
139            command,
140            family,
141            addr,
142        }),
143    ))
144}
145
146fn parse_addr_v2(family: u8) -> impl Fn(&[u8]) -> IResult<&[u8], ProxyAddr> {
147    move |i: &[u8]| match (family >> 4) & 0x0f {
148        0x00 => Ok((i, ProxyAddr::AfUnspec)),
149        0x01 => parse_ipv4_on_v2(i),
150        0x02 => parse_ipv6_on_v2(i),
151        _ => Err(Err::Error(Error::from_error_kind(i, ErrorKind::Switch))),
152    }
153}
154
155fn parse_ipv4_on_v2(i: &[u8]) -> IResult<&[u8], ProxyAddr> {
156    let in_len = i.len();
157    let (i, src_ip) = take(4u8)(i)?;
158    let (i, dest_ip) = take(4u8)(i)?;
159    let (i, src_port) = be_u16(i)?;
160    let (i, dest_port) = be_u16(i)?;
161    // An IPv4 v2 address block is fixed at 4 + 4 + 2 + 2 = 12 bytes; the parser
162    // must have consumed exactly that on success and never grown its input.
163    debug_assert_eq!(src_ip.len(), 4, "IPv4 source address is 4 bytes");
164    debug_assert_eq!(dest_ip.len(), 4, "IPv4 destination address is 4 bytes");
165    debug_assert!(i.len() <= in_len, "parser cannot grow its input");
166    debug_assert_eq!(
167        in_len - i.len(),
168        12,
169        "IPv4 v2 address block is exactly 12 bytes"
170    );
171
172    Ok((
173        i,
174        ProxyAddr::Ipv4Addr {
175            src_addr: SocketAddrV4::new(
176                Ipv4Addr::new(src_ip[0], src_ip[1], src_ip[2], src_ip[3]),
177                src_port,
178            ),
179            dst_addr: SocketAddrV4::new(
180                Ipv4Addr::new(dest_ip[0], dest_ip[1], dest_ip[2], dest_ip[3]),
181                dest_port,
182            ),
183        },
184    ))
185}
186
187fn parse_ipv6_on_v2(i: &[u8]) -> IResult<&[u8], ProxyAddr> {
188    let in_len = i.len();
189    let (i, src_ip) = take(16u8)(i)?;
190    let (i, dest_ip) = take(16u8)(i)?;
191    let (i, src_port) = be_u16(i)?;
192    let (i, dest_port) = be_u16(i)?;
193    // An IPv6 v2 address block is fixed at 16 + 16 + 2 + 2 = 36 bytes; `take`
194    // guarantees the slice widths fed to `slice_to_ipv6`, so its 16-byte
195    // precondition holds on this path.
196    debug_assert_eq!(src_ip.len(), 16, "IPv6 source address is 16 bytes");
197    debug_assert_eq!(dest_ip.len(), 16, "IPv6 destination address is 16 bytes");
198    debug_assert!(i.len() <= in_len, "parser cannot grow its input");
199    debug_assert_eq!(
200        in_len - i.len(),
201        36,
202        "IPv6 v2 address block is exactly 36 bytes"
203    );
204
205    Ok((
206        i,
207        ProxyAddr::Ipv6Addr {
208            src_addr: SocketAddrV6::new(slice_to_ipv6(src_ip), src_port, 0, 0),
209            dst_addr: SocketAddrV6::new(slice_to_ipv6(dest_ip), dest_port, 0, 0),
210        },
211    ))
212}
213
214// assumes the slice has 16 bytes
215pub fn slice_to_ipv6(sl: &[u8]) -> Ipv6Addr {
216    // Precondition (internal invariant): every caller hands a slice sized by a
217    // `take(16)`, so a wrong length here is a logic bug, not attacker input.
218    // `clone_from_slice` below would itself panic on a mismatch; the
219    // debug_assert names the contract loudly in debug/test/fuzz builds.
220    debug_assert_eq!(sl.len(), 16, "slice_to_ipv6 requires exactly 16 bytes");
221    let mut arr: [u8; 16] = [0; 16];
222    arr.clone_from_slice(sl);
223    Ipv6Addr::from(arr)
224}
225
226#[cfg(test)]
227mod test {
228
229    use std::net::{IpAddr, SocketAddr};
230
231    use nom::{Err, Needed};
232
233    use super::*;
234
235    /// A `LOCAL` header describes a connection the upstream proxy originated
236    /// itself (typically a health check); the HAProxy PROXY protocol
237    /// specification §2.2 requires the receiver to discard its address block.
238    /// Nothing on the wire forces a `LOCAL` header to carry `AF_UNSPEC` --
239    /// only HAProxy's own emitter happens to pair the two -- so a crafted peer
240    /// may send `LOCAL` with a fully populated `AF_INET` block. Before the fix
241    /// `parse_addr_v2` switched on the family nibble alone and handed that
242    /// block back as a real address pair, letting any peer forge the source
243    /// address Sōzu attributes to the client (access logs, `X-Real-IP`,
244    /// `max_connections_per_ip`).
245    ///
246    /// The block is still parsed -- a malformed one must still be rejected,
247    /// see `it_should_not_parse_proxy_protocol_with_unknown_family` -- but its
248    /// contents are dropped.
249    ///
250    /// To SEE THIS RED: in [`super::parse_v2_header`], replace the
251    /// `Command::Local => ProxyAddr::AfUnspec` arm of the `addr` binding with
252    /// `parsed_addr` (the pre-fix behaviour) -- the parser then returns the
253    /// forged `125.25.10.1:8080` instead of `AfUnspec`.
254    #[test]
255    fn local_command_discards_a_populated_ipv4_address_block() {
256        let input = &[
257            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
258            0x0A, // MAGIC header
259            0x20, // Version 2 and command LOCAL
260            0x11, // family AF_INET over STREAM
261            0x00, 0x0C, // address sizes = 12
262            0x7D, 0x19, 0x0A, 0x01, // forged source address
263            0x0A, 0x04, 0x05, 0x08, // forged destination address
264            0x1F, 0x90, // forged source port
265            0x10, 0x68, // forged destination port
266        ];
267
268        // The family byte is reported as it was read off the wire; only the
269        // address block it describes is discarded.
270        let expected = HeaderV2 {
271            command: Command::Local,
272            family: 0x11,
273            addr: ProxyAddr::AfUnspec,
274        };
275
276        assert_eq!(Ok((&[][..], expected)), parse_v2_header(input));
277
278        let (_, header) = parse_v2_header(input).expect("the header itself stays well-formed");
279        assert_eq!(
280            header.addr.source(),
281            None,
282            "a LOCAL header must attribute no source address, whatever its address block holds"
283        );
284        assert_eq!(
285            header.addr.destination(),
286            None,
287            "a LOCAL header must attribute no destination address either"
288        );
289    }
290
291    /// Same crafted block behind the `PROXY` command: the fix must not
292    /// silently disable PROXY-protocol address attribution.
293    ///
294    /// To SEE THIS RED: make the `addr` binding in [`super::parse_v2_header`]
295    /// unconditionally `ProxyAddr::AfUnspec`.
296    #[test]
297    fn proxy_command_still_honours_the_same_address_block() {
298        let input = &[
299            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
300            0x0A, // MAGIC header
301            0x21, // Version 2 and command PROXY
302            0x11, // family AF_INET over STREAM
303            0x00, 0x0C, // address sizes = 12
304            0x7D, 0x19, 0x0A, 0x01, // source address
305            0x0A, 0x04, 0x05, 0x08, // destination address
306            0x1F, 0x90, // source port
307            0x10, 0x68, // destination port
308        ];
309
310        let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(125, 25, 10, 1)), 8080);
311        let (_, header) = parse_v2_header(input).expect("a PROXY header must still parse");
312        assert_eq!(
313            header.addr.source(),
314            Some(src_addr),
315            "a PROXY header still carries the encapsulated client address"
316        );
317    }
318
319    /// The IPv6 half of `local_command_discards_a_populated_ipv4_address_block`.
320    ///
321    /// To SEE THIS RED: same mutation -- restore `parsed_addr` unconditionally
322    /// in [`super::parse_v2_header`].
323    #[test]
324    fn local_command_discards_a_populated_ipv6_address_block() {
325        let input = &[
326            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
327            0x0A, // MAGIC header
328            0x20, // Version 2 and command LOCAL
329            0x21, // family AF_INET6 over STREAM
330            0x00, 0x24, // address sizes = 36
331            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
332            0x00, 0x01, // forged source address
333            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
334            0x00, 0x02, // forged destination address
335            0x1F, 0x90, // forged source port
336            0x10, 0x68, // forged destination port
337        ];
338
339        let expected = HeaderV2 {
340            command: Command::Local,
341            family: 0x21,
342            addr: ProxyAddr::AfUnspec,
343        };
344
345        assert_eq!(Ok((&[][..], expected)), parse_v2_header(input));
346    }
347
348    #[test]
349    fn test_parse_proxy_protocol_v2_proxy_ipv4_addr_header() {
350        let input = &[
351            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
352            0x0A, // MAGIC header
353            0x21, // Version 2 and command PROXY
354            0x11, // family AF_INET over STREAM
355            0x00, 0x0C, // address sizes = 12
356            0x7D, 0x19, 0x0A, 0x01, // source address
357            0x0A, 0x04, 0x05, 0x08, // destination address
358            0x1F, 0x90, // source port
359            0x10, 0x68, // destination port
360        ];
361
362        let src_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(125, 25, 10, 1)), 8080);
363        let dst_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 4, 5, 8)), 4200);
364        let expected = HeaderV2::new(Command::Proxy, src_addr, dst_addr);
365
366        assert_eq!(Ok((&[][..], expected)), parse_v2_header(input));
367    }
368
369    #[test]
370    fn it_should_parse_proxy_protocol_v2_ipv6_addr_header() {
371        let input = &[
372            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
373            0x0A, // MAGIC header
374            0x21, // Version 2 and command PROXY
375            0x21, // family AF_INET6 over STREAM
376            0x00, 0x24, // address sizes = 36
377            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
378            0x00, 0x01, // source address
379            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
380            0x00, 0x02, // destination address
381            0x1F, 0x90, // source port
382            0x10, 0x68, // destination port
383        ];
384
385        let src_addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080);
386        let dst_addr = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 2)), 4200);
387        // Driven with the PROXY command: a LOCAL header no longer yields an
388        // address pair at all (see
389        // `local_command_discards_a_populated_ipv6_address_block`), so this
390        // test would otherwise stop covering IPv6 address-block decoding.
391        let expected = HeaderV2::new(Command::Proxy, src_addr, dst_addr);
392
393        assert_eq!(Ok((&[][..], expected)), parse_v2_header(input));
394    }
395
396    #[test]
397    fn it_should_parse_proxy_protocol_v2_afunspec_header() {
398        let input = &[
399            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
400            0x0A, // MAGIC header
401            0x20, // Version 2 and command LOCAL
402            0x00, // family AF_UNSPEC and transport protocol unknown
403            0x00, 0x00, // address sizes = 0
404        ];
405
406        let expected = HeaderV2 {
407            command: Command::Local,
408            family: 0,
409            addr: ProxyAddr::AfUnspec,
410        };
411
412        assert_eq!(Ok((&[][..], expected)), parse_v2_header(input));
413    }
414
415    #[test]
416    fn it_should_not_parse_proxy_protocol_v2_with_unknown_version() {
417        let unknow_version = 0x30;
418
419        let input = &[
420            0x0D,
421            0x0A,
422            0x0D,
423            0x0A,
424            0x00,
425            0x0D,
426            0x0A,
427            0x51,
428            0x55,
429            0x49,
430            0x54,
431            0x0A,           // MAGIC header
432            unknow_version, // invalid version
433        ];
434
435        assert!(parse_v2_header(input).is_err());
436    }
437
438    #[test]
439    fn it_should_not_parse_proxy_protocol_v2_with_unknown_command() {
440        let unknow_command = 0x23;
441
442        let input = &[
443            0x0D,
444            0x0A,
445            0x0D,
446            0x0A,
447            0x00,
448            0x0D,
449            0x0A,
450            0x51,
451            0x55,
452            0x49,
453            0x54,
454            0x0A,           // MAGIC header
455            unknow_command, // Version 2 and invalid command
456        ];
457
458        assert!(parse_v2_header(input).is_err());
459    }
460
461    #[test]
462    fn it_should_not_parse_proxy_protocol_with_unknown_family() {
463        let unknow_family = 0x30;
464
465        let input = &[
466            0x0D,
467            0x0A,
468            0x0D,
469            0x0A,
470            0x00,
471            0x0D,
472            0x0A,
473            0x51,
474            0x55,
475            0x49,
476            0x54,
477            0x0A,          // MAGIC header
478            0x20,          // Version 2 and command LOCAL
479            unknow_family, // family
480            0x00,
481            0x00, // address sizes = 0
482        ];
483
484        assert!(parse_v2_header(input).is_err());
485    }
486
487    #[test]
488    fn it_should_not_parse_request_without_magic_header() {
489        let input = &[
490            0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
491            0x0D, // INCORRECT MAGIC header
492        ];
493
494        assert!(parse_v2_header(input).is_err());
495    }
496
497    #[test]
498    fn it_should_not_parse_proxy_protocol_v2_ipv4_addr_header_with_missing_data() {
499        let input = &[
500            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
501            0x0A, // MAGIC header
502            0x20, // Version 2 and command LOCAL
503            0x11, // family AF_INET over STREAM
504        ];
505
506        assert_eq!(Err(Err::Incomplete(Needed::new(2))), parse_v2_header(input));
507    }
508
509    #[test]
510    fn it_should_not_parse_proxy_protocol_v2_with_invalid_length() {
511        let input = &[
512            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
513            0x0A, // MAGIC header
514            0x20, // Version 2 and command LOCAL
515            0x21, // family AF_INET6 over STREAM
516            0x00, 0x10, // address sizes = 36
517            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
518            0x00, 0x01, // source address
519            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
520            0x00, 0x02, // destination address
521            0x1F, 0x90, // source port
522            0x10, 0x68, // destination port
523        ];
524
525        assert_eq!(
526            Err(Err::Incomplete(Needed::new(16))),
527            parse_v2_header(input)
528        );
529    }
530}