use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
pub const V2_SIGNATURE: [u8; 12] = [0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A];
pub const V1_MAX: usize = 107;
pub const V2_MAX: usize = 16 + 1024;
pub fn enabled() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED.get_or_init(|| std::env::var("RWS_CONFIG_PROXY_PROTOCOL").is_ok_and(|v| v.trim().eq_ignore_ascii_case("true")))
}
#[derive(Debug, PartialEq, Eq)]
pub enum Parsed {
Need(usize),
Header { source: Option<SocketAddr>, len: usize },
Invalid(&'static str),
}
pub fn parse(buf: &[u8]) -> Parsed {
if buf.len() < 12 {
return Parsed::Need(12);
}
if buf[..12] == V2_SIGNATURE {
return parse_v2(buf);
}
if buf.starts_with(b"PROXY ") {
return parse_v1(buf);
}
Parsed::Invalid("no PROXY protocol header")
}
fn parse_v1(buf: &[u8]) -> Parsed {
let Some(end) = buf.windows(2).position(|w| w == b"\r\n") else {
return if buf.len() >= V1_MAX { Parsed::Invalid("PROXY v1 header too long") } else { Parsed::Need(buf.len() + 1) };
};
let len = end + 2;
let Ok(line) = std::str::from_utf8(&buf[..end]) else { return Parsed::Invalid("PROXY v1 header isn't text") };
let mut fields = line.split(' ');
fields.next(); match fields.next() {
Some("UNKNOWN") => Parsed::Header { source: None, len },
Some(family @ ("TCP4" | "TCP6")) => {
let (Some(src), Some(_dst), Some(sport), Some(_dport), None) =
(fields.next(), fields.next(), fields.next(), fields.next(), fields.next())
else {
return Parsed::Invalid("PROXY v1 header has the wrong number of fields");
};
let ip: Option<IpAddr> = match family {
"TCP4" => src.parse::<Ipv4Addr>().ok().map(IpAddr::V4),
_ => src.parse::<Ipv6Addr>().ok().map(IpAddr::V6),
};
match (ip, sport.parse::<u16>()) {
(Some(ip), Ok(port)) => Parsed::Header { source: Some(SocketAddr::new(ip, port)), len },
_ => Parsed::Invalid("PROXY v1 header has a bad address or port"),
}
}
_ => Parsed::Invalid("PROXY v1 header has an unknown protocol"),
}
}
fn parse_v2(buf: &[u8]) -> Parsed {
if buf.len() < 16 {
return Parsed::Need(16);
}
let version_command = buf[12];
if version_command >> 4 != 2 {
return Parsed::Invalid("PROXY v2 header has an unknown version");
}
let len = 16 + u16::from_be_bytes([buf[14], buf[15]]) as usize;
if len > V2_MAX {
return Parsed::Invalid("PROXY v2 header too long");
}
if buf.len() < len {
return Parsed::Need(len);
}
let body = &buf[16..len];
match version_command & 0x0F {
0x0 => return Parsed::Header { source: None, len }, 0x1 => {} _ => return Parsed::Invalid("PROXY v2 header has an unknown command"),
}
let source = match buf[13] {
0x11 if body.len() >= 12 => {
let ip = Ipv4Addr::new(body[0], body[1], body[2], body[3]);
Some(SocketAddr::new(IpAddr::V4(ip), u16::from_be_bytes([body[8], body[9]])))
}
0x21 if body.len() >= 36 => {
let mut octets = [0u8; 16];
octets.copy_from_slice(&body[..16]);
Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), u16::from_be_bytes([body[32], body[33]])))
}
0x11 | 0x21 => return Parsed::Invalid("PROXY v2 header is shorter than its addresses"),
_ => None, };
Parsed::Header { source, len }
}
#[cfg(feature = "http2")]
pub async fn read_async<S: tokio::io::AsyncRead + Unpin>(stream: &mut S) -> std::io::Result<Option<SocketAddr>> {
use tokio::io::AsyncReadExt;
let mut buf = Vec::with_capacity(32);
loop {
match parse(&buf) {
Parsed::Need(n) => {
let start = buf.len();
buf.resize(n, 0);
stream.read_exact(&mut buf[start..]).await?;
}
Parsed::Header { source, .. } => return Ok(source),
Parsed::Invalid(why) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, why)),
}
}
}
pub fn read_blocking<S: std::io::Read>(stream: &mut S) -> std::io::Result<Option<SocketAddr>> {
let mut buf = Vec::with_capacity(32);
loop {
match parse(&buf) {
Parsed::Need(n) => {
let start = buf.len();
buf.resize(n, 0);
stream.read_exact(&mut buf[start..])?;
}
Parsed::Header { source, .. } => return Ok(source),
Parsed::Invalid(why) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, why)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v2(command: u8, family: u8, addresses: &[u8]) -> Vec<u8> {
let mut header = V2_SIGNATURE.to_vec();
header.push(0x20 | command);
header.push(family);
header.extend_from_slice(&(addresses.len() as u16).to_be_bytes());
header.extend_from_slice(addresses);
header
}
#[test]
fn version_1_tcp4_tcp6_and_unknown() {
let header = b"PROXY TCP4 203.0.113.7 10.0.0.1 51234 443\r\n";
let line = [header.as_slice(), b"GET / HTTP/1.1\r\n"].concat();
assert_eq!(Parsed::Header { source: Some("203.0.113.7:51234".parse().unwrap()), len: header.len() }, parse(&line));
let line = b"PROXY TCP6 2001:db8::7 2001:db8::1 51234 443\r\n";
assert_eq!(Parsed::Header { source: Some("[2001:db8::7]:51234".parse().unwrap()), len: line.len() }, parse(line));
assert_eq!(Parsed::Header { source: None, len: 15 }, parse(b"PROXY UNKNOWN\r\n\x16\x03\x01"));
}
#[test]
fn version_1_is_read_a_byte_at_a_time_after_twelve() {
assert_eq!(Parsed::Need(12), parse(b"PROXY"));
assert_eq!(Parsed::Need(13), parse(b"PROXY TCP4 2"));
}
#[test]
fn version_1_rejects_broken_headers() {
for bad in [
&b"PROXY TCP4 203.0.113.7 10.0.0.1 51234\r\n"[..],
b"PROXY TCP4 2001:db8::1 10.0.0.1 1 2\r\n",
b"PROXY TCP4 203.0.113.7 10.0.0.1 99999 443\r\n",
b"PROXY UDP4 203.0.113.7 10.0.0.1 1 2\r\n",
b"GET / HTTP/1.1\r\nHost: x\r\n\r\n",
] {
assert!(matches!(parse(bad), Parsed::Invalid(_)), "{:?}", String::from_utf8_lossy(bad));
}
let long = [b"PROXY ".as_slice(), &[b'1'; V1_MAX]].concat();
assert!(matches!(parse(&long), Parsed::Invalid(_)));
}
#[test]
fn version_2_inet_inet6_local_and_tlvs() {
let mut addresses = vec![203, 0, 113, 7, 10, 0, 0, 1];
addresses.extend_from_slice(&51234u16.to_be_bytes());
addresses.extend_from_slice(&443u16.to_be_bytes());
addresses.extend_from_slice(&[0x04, 0x00, 0x01, 0x00]); let header = v2(1, 0x11, &addresses);
assert_eq!(Parsed::Header { source: Some("203.0.113.7:51234".parse().unwrap()), len: header.len() }, parse(&header));
let mut addresses = "2001:db8::7".parse::<Ipv6Addr>().unwrap().octets().to_vec();
addresses.extend_from_slice(&"2001:db8::1".parse::<Ipv6Addr>().unwrap().octets());
addresses.extend_from_slice(&[0xC8, 0x22, 0x01, 0xBB]);
let header = v2(1, 0x21, &addresses);
assert_eq!(Parsed::Header { source: Some("[2001:db8::7]:51234".parse().unwrap()), len: 52 }, parse(&header));
assert_eq!(Parsed::Header { source: None, len: 16 }, parse(&v2(0, 0x00, &[])));
assert_eq!(Parsed::Need(16), parse(&V2_SIGNATURE));
assert_eq!(Parsed::Need(28), parse(&v2(1, 0x11, &[0; 12])[..20]));
}
#[test]
fn version_2_rejects_broken_headers() {
assert!(matches!(parse(&v2(1, 0x11, &[0; 4])), Parsed::Invalid(_)));
assert!(matches!(parse(&v2(7, 0x11, &[0; 12])), Parsed::Invalid(_)));
let mut wrong_version = v2(1, 0x11, &[0; 12]);
wrong_version[12] = 0x11;
assert!(matches!(parse(&wrong_version), Parsed::Invalid(_)));
let mut huge = V2_SIGNATURE.to_vec();
huge.extend_from_slice(&[0x21, 0x11, 0xFF, 0xFF]);
assert!(matches!(parse(&huge), Parsed::Invalid(_)));
}
#[test]
fn the_readers_stop_at_the_end_of_the_header() {
let mut input: &[u8] = b"PROXY TCP4 203.0.113.7 10.0.0.1 51234 443\r\nGET /";
assert_eq!(Some("203.0.113.7:51234".parse().unwrap()), read_blocking(&mut input).unwrap());
assert_eq!(b"GET /", input);
let mut input: &[u8] = b"GET / HTTP/1.1\r\n\r\n";
assert!(read_blocking(&mut input).is_err());
}
#[cfg(feature = "http2")]
#[tokio::test]
async fn the_async_reader_stops_at_the_end_of_the_header() {
let mut header = v2(1, 0x11, &[192, 0, 2, 1, 10, 0, 0, 1, 0x30, 0x39, 0x01, 0xBB]);
header.extend_from_slice(b"\x16\x03\x01");
let mut input: &[u8] = &header;
assert_eq!(Some("192.0.2.1:12345".parse().unwrap()), read_async(&mut input).await.unwrap());
assert_eq!(b"\x16\x03\x01", input);
}
}