Skip to main content

rust_web_server/server/
proxy_protocol.rs

1//! Inbound PROXY protocol, versions 1 (text) and 2 (binary), as HAProxy,
2//! AWS NLB, GCP and most L4 load balancers send it: the client's address,
3//! prepended to the connection. With `proxy_protocol = true`
4//! (`RWS_CONFIG_PROXY_PROTOCOL`) every connection to the main listener must
5//! start with one — before the TLS handshake on HTTPS — and its source
6//! address becomes the client address rws logs, rate-limits and forwards in
7//! `X-Forwarded-For`. A connection without a valid header is closed.
8//!
9//! The header is read exactly, byte for byte where needed, so what follows it
10//! (a TLS ClientHello, an HTTP request) is untouched. `LOCAL` (v2) and
11//! `UNKNOWN` (v1) headers — a load balancer's own health checks — keep the
12//! peer address. TLVs (v2) are skipped.
13
14use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
15
16/// The 12-byte version 2 signature.
17pub const V2_SIGNATURE: [u8; 12] = [0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A];
18
19/// Longest version 1 header, `\r\n` included (the spec's limit).
20pub const V1_MAX: usize = 107;
21
22/// Largest version 2 header accepted: 16 bytes plus addresses and TLVs.
23pub const V2_MAX: usize = 16 + 1024;
24
25/// Whether the main listener expects PROXY protocol headers. Read once from
26/// `RWS_CONFIG_PROXY_PROTOCOL` (default `false`); a change needs a restart.
27pub fn enabled() -> bool {
28    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
29    *ENABLED.get_or_init(|| std::env::var("RWS_CONFIG_PROXY_PROTOCOL").is_ok_and(|v| v.trim().eq_ignore_ascii_case("true")))
30}
31
32/// What the bytes read so far say.
33#[derive(Debug, PartialEq, Eq)]
34pub enum Parsed {
35    /// Read until the buffer holds this many bytes, then parse again.
36    Need(usize),
37    /// A complete header of `len` bytes. `source` is `None` for `LOCAL` /
38    /// `UNKNOWN` / unsupported address families: keep the peer address.
39    Header { source: Option<SocketAddr>, len: usize },
40    /// Not a PROXY protocol header (or a broken one).
41    Invalid(&'static str),
42}
43
44/// Parse the start of a connection. Never asks for more than [`V1_MAX`] or
45/// [`V2_MAX`] bytes, and for version 1 only one byte at a time past the
46/// first 12, so a reader following it never consumes what comes after the
47/// header.
48pub fn parse(buf: &[u8]) -> Parsed {
49    if buf.len() < 12 {
50        return Parsed::Need(12);
51    }
52    if buf[..12] == V2_SIGNATURE {
53        return parse_v2(buf);
54    }
55    if buf.starts_with(b"PROXY ") {
56        return parse_v1(buf);
57    }
58    Parsed::Invalid("no PROXY protocol header")
59}
60
61fn parse_v1(buf: &[u8]) -> Parsed {
62    let Some(end) = buf.windows(2).position(|w| w == b"\r\n") else {
63        return if buf.len() >= V1_MAX { Parsed::Invalid("PROXY v1 header too long") } else { Parsed::Need(buf.len() + 1) };
64    };
65    let len = end + 2;
66    let Ok(line) = std::str::from_utf8(&buf[..end]) else { return Parsed::Invalid("PROXY v1 header isn't text") };
67    let mut fields = line.split(' ');
68    fields.next(); // "PROXY"
69    match fields.next() {
70        Some("UNKNOWN") => Parsed::Header { source: None, len },
71        Some(family @ ("TCP4" | "TCP6")) => {
72            let (Some(src), Some(_dst), Some(sport), Some(_dport), None) =
73                (fields.next(), fields.next(), fields.next(), fields.next(), fields.next())
74            else {
75                return Parsed::Invalid("PROXY v1 header has the wrong number of fields");
76            };
77            let ip: Option<IpAddr> = match family {
78                "TCP4" => src.parse::<Ipv4Addr>().ok().map(IpAddr::V4),
79                _ => src.parse::<Ipv6Addr>().ok().map(IpAddr::V6),
80            };
81            match (ip, sport.parse::<u16>()) {
82                (Some(ip), Ok(port)) => Parsed::Header { source: Some(SocketAddr::new(ip, port)), len },
83                _ => Parsed::Invalid("PROXY v1 header has a bad address or port"),
84            }
85        }
86        _ => Parsed::Invalid("PROXY v1 header has an unknown protocol"),
87    }
88}
89
90fn parse_v2(buf: &[u8]) -> Parsed {
91    if buf.len() < 16 {
92        return Parsed::Need(16);
93    }
94    let version_command = buf[12];
95    if version_command >> 4 != 2 {
96        return Parsed::Invalid("PROXY v2 header has an unknown version");
97    }
98    let len = 16 + u16::from_be_bytes([buf[14], buf[15]]) as usize;
99    if len > V2_MAX {
100        return Parsed::Invalid("PROXY v2 header too long");
101    }
102    if buf.len() < len {
103        return Parsed::Need(len);
104    }
105    let body = &buf[16..len];
106    match version_command & 0x0F {
107        0x0 => return Parsed::Header { source: None, len }, // LOCAL
108        0x1 => {}                                            // PROXY
109        _ => return Parsed::Invalid("PROXY v2 header has an unknown command"),
110    }
111    // Address family in the high nibble; only stream transports carry HTTP.
112    let source = match buf[13] {
113        0x11 if body.len() >= 12 => {
114            let ip = Ipv4Addr::new(body[0], body[1], body[2], body[3]);
115            Some(SocketAddr::new(IpAddr::V4(ip), u16::from_be_bytes([body[8], body[9]])))
116        }
117        0x21 if body.len() >= 36 => {
118            let mut octets = [0u8; 16];
119            octets.copy_from_slice(&body[..16]);
120            Some(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(octets)), u16::from_be_bytes([body[32], body[33]])))
121        }
122        0x11 | 0x21 => return Parsed::Invalid("PROXY v2 header is shorter than its addresses"),
123        _ => None, // UNSPEC, UNIX sockets, datagrams: keep the peer address
124    };
125    Parsed::Header { source, len }
126}
127
128/// Read and parse the header at the start of `stream` (async builds).
129/// `Ok(None)`: a valid header without an address; keep the peer's.
130#[cfg(feature = "http2")]
131pub async fn read_async<S: tokio::io::AsyncRead + Unpin>(stream: &mut S) -> std::io::Result<Option<SocketAddr>> {
132    use tokio::io::AsyncReadExt;
133    let mut buf = Vec::with_capacity(32);
134    loop {
135        match parse(&buf) {
136            Parsed::Need(n) => {
137                let start = buf.len();
138                buf.resize(n, 0);
139                stream.read_exact(&mut buf[start..]).await?;
140            }
141            Parsed::Header { source, .. } => return Ok(source),
142            Parsed::Invalid(why) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, why)),
143        }
144    }
145}
146
147/// [`read_async`] for the blocking HTTP/1.1 server.
148pub fn read_blocking<S: std::io::Read>(stream: &mut S) -> std::io::Result<Option<SocketAddr>> {
149    let mut buf = Vec::with_capacity(32);
150    loop {
151        match parse(&buf) {
152            Parsed::Need(n) => {
153                let start = buf.len();
154                buf.resize(n, 0);
155                stream.read_exact(&mut buf[start..])?;
156            }
157            Parsed::Header { source, .. } => return Ok(source),
158            Parsed::Invalid(why) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, why)),
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn v2(command: u8, family: u8, addresses: &[u8]) -> Vec<u8> {
168        let mut header = V2_SIGNATURE.to_vec();
169        header.push(0x20 | command);
170        header.push(family);
171        header.extend_from_slice(&(addresses.len() as u16).to_be_bytes());
172        header.extend_from_slice(addresses);
173        header
174    }
175
176    #[test]
177    fn version_1_tcp4_tcp6_and_unknown() {
178        let header = b"PROXY TCP4 203.0.113.7 10.0.0.1 51234 443\r\n";
179        let line = [header.as_slice(), b"GET / HTTP/1.1\r\n"].concat();
180        assert_eq!(Parsed::Header { source: Some("203.0.113.7:51234".parse().unwrap()), len: header.len() }, parse(&line));
181        let line = b"PROXY TCP6 2001:db8::7 2001:db8::1 51234 443\r\n";
182        assert_eq!(Parsed::Header { source: Some("[2001:db8::7]:51234".parse().unwrap()), len: line.len() }, parse(line));
183        assert_eq!(Parsed::Header { source: None, len: 15 }, parse(b"PROXY UNKNOWN\r\n\x16\x03\x01"));
184    }
185
186    #[test]
187    fn version_1_is_read_a_byte_at_a_time_after_twelve() {
188        assert_eq!(Parsed::Need(12), parse(b"PROXY"));
189        assert_eq!(Parsed::Need(13), parse(b"PROXY TCP4 2"));
190    }
191
192    #[test]
193    fn version_1_rejects_broken_headers() {
194        for bad in [
195            &b"PROXY TCP4 203.0.113.7 10.0.0.1 51234\r\n"[..],
196            b"PROXY TCP4 2001:db8::1 10.0.0.1 1 2\r\n",
197            b"PROXY TCP4 203.0.113.7 10.0.0.1 99999 443\r\n",
198            b"PROXY UDP4 203.0.113.7 10.0.0.1 1 2\r\n",
199            b"GET / HTTP/1.1\r\nHost: x\r\n\r\n",
200        ] {
201            assert!(matches!(parse(bad), Parsed::Invalid(_)), "{:?}", String::from_utf8_lossy(bad));
202        }
203        let long = [b"PROXY ".as_slice(), &[b'1'; V1_MAX]].concat();
204        assert!(matches!(parse(&long), Parsed::Invalid(_)));
205    }
206
207    #[test]
208    fn version_2_inet_inet6_local_and_tlvs() {
209        let mut addresses = vec![203, 0, 113, 7, 10, 0, 0, 1];
210        addresses.extend_from_slice(&51234u16.to_be_bytes());
211        addresses.extend_from_slice(&443u16.to_be_bytes());
212        addresses.extend_from_slice(&[0x04, 0x00, 0x01, 0x00]); // a TLV, skipped
213        let header = v2(1, 0x11, &addresses);
214        assert_eq!(Parsed::Header { source: Some("203.0.113.7:51234".parse().unwrap()), len: header.len() }, parse(&header));
215
216        let mut addresses = "2001:db8::7".parse::<Ipv6Addr>().unwrap().octets().to_vec();
217        addresses.extend_from_slice(&"2001:db8::1".parse::<Ipv6Addr>().unwrap().octets());
218        addresses.extend_from_slice(&[0xC8, 0x22, 0x01, 0xBB]);
219        let header = v2(1, 0x21, &addresses);
220        assert_eq!(Parsed::Header { source: Some("[2001:db8::7]:51234".parse().unwrap()), len: 52 }, parse(&header));
221
222        assert_eq!(Parsed::Header { source: None, len: 16 }, parse(&v2(0, 0x00, &[])));
223        assert_eq!(Parsed::Need(16), parse(&V2_SIGNATURE));
224        assert_eq!(Parsed::Need(28), parse(&v2(1, 0x11, &[0; 12])[..20]));
225    }
226
227    #[test]
228    fn version_2_rejects_broken_headers() {
229        assert!(matches!(parse(&v2(1, 0x11, &[0; 4])), Parsed::Invalid(_)));
230        assert!(matches!(parse(&v2(7, 0x11, &[0; 12])), Parsed::Invalid(_)));
231        let mut wrong_version = v2(1, 0x11, &[0; 12]);
232        wrong_version[12] = 0x11;
233        assert!(matches!(parse(&wrong_version), Parsed::Invalid(_)));
234        let mut huge = V2_SIGNATURE.to_vec();
235        huge.extend_from_slice(&[0x21, 0x11, 0xFF, 0xFF]);
236        assert!(matches!(parse(&huge), Parsed::Invalid(_)));
237    }
238
239    #[test]
240    fn the_readers_stop_at_the_end_of_the_header() {
241        let mut input: &[u8] = b"PROXY TCP4 203.0.113.7 10.0.0.1 51234 443\r\nGET /";
242        assert_eq!(Some("203.0.113.7:51234".parse().unwrap()), read_blocking(&mut input).unwrap());
243        assert_eq!(b"GET /", input);
244        let mut input: &[u8] = b"GET / HTTP/1.1\r\n\r\n";
245        assert!(read_blocking(&mut input).is_err());
246    }
247
248    #[cfg(feature = "http2")]
249    #[tokio::test]
250    async fn the_async_reader_stops_at_the_end_of_the_header() {
251        let mut header = v2(1, 0x11, &[192, 0, 2, 1, 10, 0, 0, 1, 0x30, 0x39, 0x01, 0xBB]);
252        header.extend_from_slice(b"\x16\x03\x01");
253        let mut input: &[u8] = &header;
254        assert_eq!(Some("192.0.2.1:12345".parse().unwrap()), read_async(&mut input).await.unwrap());
255        assert_eq!(b"\x16\x03\x01", input);
256    }
257}