//! HAProxy PROXY protocol (v1 text + v2 binary) — the header a trusted load //! balancer / TCP proxy prepends to a connection to carry the real client address. //! Only the source address is needed; when the header says LOCAL (a health check) //! or an address family we don't translate, the original peer address is kept. //! //! Enabled per source with `proxy = ` (repeatable); connections from a //! matching proxy must lead with a PROXY header, which is stripped before the first //! IRC byte so `add_conn`'s connect-time checks see the real client IP. use std::io::Read; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// The 12-byte v2 signature. const V2_SIG: [u8; 12] = [ 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, ]; /// A v1 line is at most 107 bytes including CRLF. const V1_MAX: usize = 107; /// The result of trying to parse a PROXY header from a byte prefix. pub enum Parsed { /// A full header giving the real client (source) address, plus any TLS metadata /// a v2 header forwarded (a TLS-terminating proxy sets `secure` and, if it /// forwards one, the client cert `certfp`). Proxy { addr: SocketAddr, secure: bool, certfp: Option, }, /// A full header with no address to apply (LOCAL / unsupported family). Local, /// Not enough bytes yet — read more and retry. Need, /// Not a valid PROXY header. Invalid, } /// Try to parse a PROXY header at the start of `buf`. Returns the parse result and, /// when a full header was consumed, how many bytes it occupied. pub fn parse(buf: &[u8]) -> (Parsed, usize) { let vlen = buf.len().min(12); if buf[..vlen] == V2_SIG[..vlen] { if buf.len() < 12 { return (Parsed::Need, 0); } return parse_v2(buf); } if buf.starts_with(b"PROXY ") { return parse_v1(buf); } if buf.len() < 6 && b"PROXY "[..buf.len()] == *buf { return (Parsed::Need, 0); // still could become "PROXY " } (Parsed::Invalid, 0) } /// Blocking-read exactly one PROXY header from `r` for the thread-model paths (TLS). /// Reads a byte at a time and re-parses, so it never consumes bytes past the header /// (which would corrupt the following TLS handshake). pub fn read_header(r: &mut R) -> Parsed { let mut buf = Vec::with_capacity(64); let mut one = [0u8; 1]; loop { match r.read(&mut one) { Ok(0) | Err(_) => return Parsed::Invalid, Ok(_) => buf.push(one[0]), } if buf.len() > 256 { return Parsed::Invalid; } match parse(&buf) { (Parsed::Need, _) => continue, (result, _) => return result, // complete: used == buf.len() by construction } } } fn parse_v1(buf: &[u8]) -> (Parsed, usize) { let Some(nl) = buf.windows(2).position(|w| w == b"\r\n") else { return if buf.len() > V1_MAX { (Parsed::Invalid, 0) } else { (Parsed::Need, 0) }; }; let consumed = nl + 2; let Ok(line) = std::str::from_utf8(&buf[..nl]) else { return (Parsed::Invalid, consumed); }; let p: Vec<&str> = line.split(' ').collect(); if p.len() < 2 { return (Parsed::Invalid, consumed); } match p[1] { "TCP4" | "TCP6" => { if p.len() != 6 { return (Parsed::Invalid, consumed); } match (p[2].parse::(), p[4].parse::()) { (Ok(ip), Ok(port)) => ( Parsed::Proxy { addr: SocketAddr::new(ip, port), secure: false, certfp: None, }, consumed, ), _ => (Parsed::Invalid, consumed), } } "UNKNOWN" => (Parsed::Local, consumed), _ => (Parsed::Invalid, consumed), } } fn parse_v2(buf: &[u8]) -> (Parsed, usize) { if buf.len() < 16 { return (Parsed::Need, 0); } let ver_cmd = buf[12]; if ver_cmd >> 4 != 2 { return (Parsed::Invalid, 0); } let cmd = ver_cmd & 0x0f; let family = buf[13] >> 4; let len = u16::from_be_bytes([buf[14], buf[15]]) as usize; let total = 16 + len; if buf.len() < total { return (Parsed::Need, 0); } if cmd == 0 { return (Parsed::Local, total); // LOCAL (health check) } if cmd != 1 { return (Parsed::Invalid, total); } let a = &buf[16..total]; let (addr, fixed) = match family { 1 if len >= 12 => { let src = Ipv4Addr::new(a[0], a[1], a[2], a[3]); let sport = u16::from_be_bytes([a[8], a[9]]); (SocketAddr::new(IpAddr::V4(src), sport), 12) } 2 if len >= 36 => { let mut o = [0u8; 16]; o.copy_from_slice(&a[0..16]); let sport = u16::from_be_bytes([a[32], a[33]]); (SocketAddr::new(IpAddr::V6(Ipv6Addr::from(o)), sport), 36) } _ => return (Parsed::Local, total), // AF_UNIX / unspecified: keep peer addr }; // any bytes after the fixed address are TLVs: a TLS-terminating proxy may // forward the client's TLS status (PP2_TYPE_SSL) and cert fingerprint (CERTFP) let (secure, certfp) = parse_v2_tlvs(&a[fixed..]); ( Parsed::Proxy { addr, secure, certfp, }, total, ) } // PROXY v2 TLV types we care about. const PP2_TYPE_SSL: u8 = 0x20; const PP2_TYPE_CERTFP: u8 = 0xE0; const PP2_CLIENT_SSL: u8 = 0x01; /// Walk the v2 TLV block: `type(1) len(2, big-endian) value(len)`. Returns whether /// the client was on TLS and its forwarded cert fingerprint, if any. fn parse_v2_tlvs(mut tlv: &[u8]) -> (bool, Option) { let mut secure = false; let mut certfp = None; while tlv.len() >= 3 { let ttype = tlv[0]; let tlen = u16::from_be_bytes([tlv[1], tlv[2]]) as usize; if tlv.len() < 3 + tlen { break; // truncated TLV } let val = &tlv[3..3 + tlen]; match ttype { PP2_TYPE_SSL => { if !val.is_empty() && val[0] & PP2_CLIENT_SSL != 0 { secure = true; } } PP2_TYPE_CERTFP => { if let Ok(s) = std::str::from_utf8(val) { if !s.is_empty() && s.len() <= 128 && s.bytes().all(|c| c.is_ascii_hexdigit()) { certfp = Some(s.to_string()); } } } _ => {} } tlv = &tlv[3 + tlen..]; } (secure, certfp) } #[cfg(test)] mod tests { use super::*; fn src(p: &Parsed) -> Option { match p { Parsed::Proxy { addr, .. } => Some(*addr), _ => None, } } #[test] fn v1_tcp4() { let (r, n) = parse(b"PROXY TCP4 192.0.2.9 10.0.0.1 56324 6667\r\nNICK bob\r\n"); assert_eq!(src(&r).unwrap().to_string(), "192.0.2.9:56324"); assert_eq!(n, 42); // header up to and including CRLF } #[test] fn v1_partial_needs_more() { assert!(matches!(parse(b"PROXY TCP4 192.0.2.9 10.0"), (Parsed::Need, _))); assert!(matches!(parse(b"PRO"), (Parsed::Need, _))); } #[test] fn v1_unknown_is_local() { assert!(matches!(parse(b"PROXY UNKNOWN\r\n"), (Parsed::Local, _))); } #[test] fn v1_garbage_invalid() { assert!(matches!(parse(b"HELLO THERE\r\n"), (Parsed::Invalid, _))); assert!(matches!(parse(b"PROXY TCP4 bad ip x y\r\n"), (Parsed::Invalid, _))); } #[test] fn v2_ipv4() { let mut h = V2_SIG.to_vec(); h.push(0x21); // v2, PROXY h.push(0x11); // AF_INET, STREAM h.extend_from_slice(&12u16.to_be_bytes()); h.extend_from_slice(&[203, 0, 113, 7]); // src ip h.extend_from_slice(&[10, 0, 0, 1]); // dst ip h.extend_from_slice(&0xC000u16.to_be_bytes()); // src port 49152 h.extend_from_slice(&6667u16.to_be_bytes()); // dst port h.extend_from_slice(b"NICK x\r\n"); let (r, n) = parse(&h); assert_eq!(src(&r).unwrap().to_string(), "203.0.113.7:49152"); assert_eq!(n, 28); } #[test] fn v2_tls_tlvs() { // a TLS-terminating proxy forwards PP2_TYPE_SSL (client-on-TLS) + CERTFP let mut h = V2_SIG.to_vec(); h.push(0x21); // v2, PROXY h.push(0x11); // AF_INET, STREAM h.extend_from_slice(&31u16.to_be_bytes()); // 12 addr + 8 SSL TLV + 11 CERTFP TLV h.extend_from_slice(&[198, 51, 100, 10]); // src h.extend_from_slice(&[10, 0, 0, 1]); // dst h.extend_from_slice(&5000u16.to_be_bytes()); h.extend_from_slice(&443u16.to_be_bytes()); h.push(0x20); // PP2_TYPE_SSL h.extend_from_slice(&5u16.to_be_bytes()); h.extend_from_slice(&[0x01, 0, 0, 0, 0]); // client=PP2_CLIENT_SSL, verify=0 h.push(0xE0); // PP2_TYPE_CERTFP h.extend_from_slice(&8u16.to_be_bytes()); h.extend_from_slice(b"abcd1234"); match parse(&h).0 { Parsed::Proxy { addr, secure, certfp, } => { assert_eq!(addr.to_string(), "198.51.100.10:5000"); assert!(secure); assert_eq!(certfp.as_deref(), Some("abcd1234")); } _ => panic!("expected Proxy"), } } #[test] fn v2_partial_and_local() { assert!(matches!(parse(&V2_SIG[..8]), (Parsed::Need, _))); let mut h = V2_SIG.to_vec(); h.push(0x20); // v2, LOCAL h.push(0x00); h.extend_from_slice(&0u16.to_be_bytes()); assert!(matches!(parse(&h), (Parsed::Local, 16))); } }