Skip to main content

pingap_core/
proxy_protocol.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The PROXY protocol on the connecting side, versions 1 and 2.
16//!
17//! A backend that reads the header (`listen ... proxy_protocol` of nginx,
18//! `accept-proxy` of HAProxy) is told by it which client a connection is
19//! made for, at the level of the connection: there is no HTTP header to
20//! trust or to forge. The header is the first thing on a connection,
21//! ahead of a TLS handshake where there is one, and it is said once: the
22//! connection belongs to that client from then on.
23//!
24//! pingora has no setting for it. It lets a peer bring its own way of
25//! connecting ([`Connect`], `PeerOptions::custom_l4`), and that is what
26//! [`ProxyProtocolConnector`] is: it connects as pingora does and writes
27//! the header before it hands the connection over.
28//!
29//! The listening side, which reads such a header, is in `pingap-proxy`.
30
31use async_trait::async_trait;
32use pingora::connectors::l4::Connect;
33#[cfg(unix)]
34use pingora::protocols::l4::ext::connect_uds;
35use pingora::protocols::l4::ext::{set_recv_buf, set_tcp_fastopen_connect};
36use pingora::protocols::l4::socket::SocketAddr as PeerAddr;
37use pingora::protocols::l4::stream::Stream;
38use pingora::{Error, ErrorType, OrErr, Result};
39use std::io::ErrorKind;
40use std::net::{IpAddr, SocketAddr};
41#[cfg(unix)]
42use std::os::unix::io::AsRawFd;
43#[cfg(windows)]
44use std::os::windows::io::AsRawSocket;
45use std::time::Duration;
46use tokio::io::AsyncWriteExt;
47use tokio::net::TcpSocket;
48
49/// The names a configuration has for the versions, in lower case.
50pub const PROXY_PROTOCOL_VERSIONS: [&str; 2] = ["v1", "v2"];
51
52const V2_SIGNATURE: &[u8] = b"\r\n\r\n\0\r\nQUIT\n";
53/// Version 2 and the command: `LOCAL` for a connection of the proxy's
54/// own, `PROXY` for one that is passed on.
55const V2_LOCAL: u8 = 0x20;
56const V2_PROXY: u8 = 0x21;
57/// The address family and the transport: TCP over IPv4, TCP over IPv6.
58const V2_TCP4: u8 = 0x11;
59const V2_TCP6: u8 = 0x21;
60
61/// The form of the header.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ProxyProtocolVersion {
64    /// A line of text: `PROXY TCP4 192.0.2.1 198.51.100.1 56324 443\r\n`.
65    V1,
66    /// The binary form.
67    V2,
68}
69
70impl ProxyProtocolVersion {
71    /// `v1` or `v2`, in any case. `None` for anything else.
72    pub fn parse(value: &str) -> Option<Self> {
73        match value.trim().to_ascii_lowercase().as_str() {
74            "v1" => Some(Self::V1),
75            "v2" => Some(Self::V2),
76            _ => None,
77        }
78    }
79}
80
81/// Both ends in one family, which is all a header has room for: IPv4 where
82/// both are, the IPv4-mapped addresses of a dual-stack socket included,
83/// and IPv6 otherwise, with the IPv4 end mapped into it.
84fn same_family(
85    source: SocketAddr,
86    destination: SocketAddr,
87) -> (SocketAddr, SocketAddr) {
88    let canonical = |addr: SocketAddr| {
89        SocketAddr::new(addr.ip().to_canonical(), addr.port())
90    };
91    let (source, destination) = (canonical(source), canonical(destination));
92    if source.is_ipv4() == destination.is_ipv4() {
93        return (source, destination);
94    }
95    let mapped = |addr: SocketAddr| match addr.ip() {
96        IpAddr::V4(ip) => {
97            SocketAddr::new(IpAddr::V6(ip.to_ipv6_mapped()), addr.port())
98        },
99        IpAddr::V6(_) => addr,
100    };
101    (mapped(source), mapped(destination))
102}
103
104/// An address as version 1 writes it. An IPv6 address is groups of
105/// hexadecimal digits there and nothing else: `Display` ends an
106/// IPv4-mapped one in dotted decimals.
107fn v1_ip(ip: IpAddr) -> String {
108    match ip {
109        IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some() => {
110            let segments = v6.segments();
111            format!("::ffff:{:x}:{:x}", segments[6], segments[7])
112        },
113        _ => ip.to_string(),
114    }
115}
116
117/// The header a connection starts with.
118///
119/// `addresses` are where the client comes from and what of this proxy it
120/// came to. `None` is for a connection that is made for nobody, a health
121/// check: the backend is told to go by the connection itself (`LOCAL` in
122/// version 2; version 1 has `UNKNOWN` for it).
123///
124/// A client that is passed on without an address to give would be
125/// `PROXY` with no family in version 2, which is not written here: every
126/// listener of this proxy is a TCP one, and its clients have addresses.
127pub fn new_proxy_protocol_header(
128    version: ProxyProtocolVersion,
129    addresses: Option<(SocketAddr, SocketAddr)>,
130) -> Vec<u8> {
131    let addresses =
132        addresses.map(|(source, destination)| same_family(source, destination));
133    match (version, addresses) {
134        (ProxyProtocolVersion::V1, None) => b"PROXY UNKNOWN\r\n".to_vec(),
135        (ProxyProtocolVersion::V1, Some((source, destination))) => {
136            let family = if source.is_ipv4() { "TCP4" } else { "TCP6" };
137            format!(
138                "PROXY {family} {} {} {} {}\r\n",
139                v1_ip(source.ip()),
140                v1_ip(destination.ip()),
141                source.port(),
142                destination.port()
143            )
144            .into_bytes()
145        },
146        (ProxyProtocolVersion::V2, None) => {
147            let mut header = V2_SIGNATURE.to_vec();
148            // LOCAL, no family, nothing that follows
149            header.extend_from_slice(&[V2_LOCAL, 0, 0, 0]);
150            header
151        },
152        (ProxyProtocolVersion::V2, Some((source, destination))) => {
153            let mut addresses = Vec::with_capacity(36);
154            let family = match (source.ip(), destination.ip()) {
155                (IpAddr::V4(source), IpAddr::V4(destination)) => {
156                    addresses.extend_from_slice(&source.octets());
157                    addresses.extend_from_slice(&destination.octets());
158                    V2_TCP4
159                },
160                (source, destination) => {
161                    // of one family by now: neither is IPv4
162                    for ip in [source, destination] {
163                        if let IpAddr::V6(ip) = ip {
164                            addresses.extend_from_slice(&ip.octets());
165                        }
166                    }
167                    V2_TCP6
168                },
169            };
170            addresses.extend_from_slice(&source.port().to_be_bytes());
171            addresses.extend_from_slice(&destination.port().to_be_bytes());
172            let mut header = V2_SIGNATURE.to_vec();
173            header.push(V2_PROXY);
174            header.push(family);
175            header.extend_from_slice(&(addresses.len() as u16).to_be_bytes());
176            header.extend_from_slice(&addresses);
177            header
178        },
179    }
180}
181
182/// What pingora's own connector takes from the peer for the socket it
183/// opens. A connector that stands in for it is given the address and
184/// nothing else, so these come with it.
185#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
186pub struct ProxyProtocolConnectOptions {
187    /// How long connecting and sending the header may take.
188    pub connection_timeout: Option<Duration>,
189    pub tcp_recv_buf: Option<usize>,
190    pub tcp_fast_open: bool,
191}
192
193/// Connects to a backend and writes a PROXY protocol header as the first
194/// bytes of the connection.
195#[derive(Debug)]
196pub struct ProxyProtocolConnector {
197    header: Vec<u8>,
198    options: ProxyProtocolConnectOptions,
199}
200
201/// The failure of a connection attempt as pingora has it, which is what
202/// says whether the request may go to another backend and what the client
203/// is answered.
204fn new_connect_error(e: std::io::Error, addr: &PeerAddr) -> Box<Error> {
205    let etype = match e.kind() {
206        ErrorKind::ConnectionRefused => ErrorType::ConnectRefused,
207        ErrorKind::TimedOut => ErrorType::ConnectTimedout,
208        ErrorKind::AddrNotAvailable
209        | ErrorKind::PermissionDenied
210        | ErrorKind::AddrInUse => ErrorType::InternalError,
211        ErrorKind::NetworkUnreachable | ErrorKind::HostUnreachable => {
212            ErrorType::ConnectNoRoute
213        },
214        _ => ErrorType::ConnectError,
215    };
216    Error::because(etype, format!("Fail to connect to {addr}"), e)
217}
218
219impl ProxyProtocolConnector {
220    pub fn new(header: Vec<u8>, options: ProxyProtocolConnectOptions) -> Self {
221        Self { header, options }
222    }
223
224    /// The header every connection of this connector starts with.
225    pub fn header(&self) -> &[u8] {
226        &self.header
227    }
228
229    fn set_socket(&self, socket: &TcpSocket) -> Result<()> {
230        #[cfg(unix)]
231        let raw = socket.as_raw_fd();
232        #[cfg(windows)]
233        let raw = socket.as_raw_socket();
234        if self.options.tcp_fast_open {
235            set_tcp_fastopen_connect(raw)?;
236        }
237        if let Some(size) = self.options.tcp_recv_buf {
238            set_recv_buf(raw, size)?;
239        }
240        Ok(())
241    }
242
243    async fn connect_and_send(&self, addr: &PeerAddr) -> Result<Stream> {
244        // Nothing of a request is on its way yet: a header that cannot be
245        // sent is a connection that was not made.
246        let not_sent = |e: std::io::Error| {
247            Error::because(
248                ErrorType::ConnectError,
249                format!("Fail to send the PROXY protocol header to {addr}"),
250                e,
251            )
252        };
253        match addr {
254            PeerAddr::Inet(inet) => {
255                let socket = if inet.is_ipv4() {
256                    TcpSocket::new_v4()
257                } else {
258                    TcpSocket::new_v6()
259                }
260                .or_err(ErrorType::SocketError, "failed to create socket")
261                .map_err(|e| {
262                    Error::because(
263                        ErrorType::InternalError,
264                        format!("Fail to connect to {addr}"),
265                        e,
266                    )
267                })?;
268                // An option the kernel refuses is a connection that was
269                // not made, as it is for pingora's own connector.
270                self.set_socket(&socket).map_err(|e| {
271                    e.more_context(format!("Fail to connect to {addr}"))
272                })?;
273                let mut stream = socket
274                    .connect(*inet)
275                    .await
276                    .map_err(|e| new_connect_error(e, addr))?;
277                stream.write_all(&self.header).await.map_err(not_sent)?;
278                Ok(stream.into())
279            },
280            #[cfg(unix)]
281            PeerAddr::Unix(unix) => {
282                let Some(path) = unix.as_pathname() else {
283                    return Error::e_explain(
284                        ErrorType::InternalError,
285                        format!("{addr} is not the path of a socket"),
286                    );
287                };
288                let mut stream = connect_uds(path).await?;
289                stream.write_all(&self.header).await.map_err(not_sent)?;
290                Ok(stream.into())
291            },
292        }
293    }
294}
295
296/// `connecting` to `addr`, given up after `limit` where there is one.
297async fn within<T>(
298    limit: Option<Duration>,
299    addr: &PeerAddr,
300    connecting: impl Future<Output = Result<T>>,
301) -> Result<T> {
302    let Some(limit) = limit else {
303        return connecting.await;
304    };
305    tokio::time::timeout(limit, connecting).await.map_err(|e| {
306        Error::because(
307            ErrorType::ConnectTimedout,
308            format!("timeout {limit:?} connecting to server {addr}"),
309            e,
310        )
311    })?
312}
313
314#[async_trait]
315impl Connect for ProxyProtocolConnector {
316    async fn connect(&self, addr: &PeerAddr) -> Result<Stream> {
317        within(
318            self.options.connection_timeout,
319            addr,
320            self.connect_and_send(addr),
321        )
322        .await
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use pretty_assertions::assert_eq;
330    use tokio::io::AsyncReadExt;
331    use tokio::net::TcpListener;
332
333    fn addr(value: &str) -> SocketAddr {
334        value.parse().unwrap()
335    }
336
337    #[test]
338    fn test_version() {
339        assert_eq!(
340            Some(ProxyProtocolVersion::V1),
341            ProxyProtocolVersion::parse("v1")
342        );
343        assert_eq!(
344            Some(ProxyProtocolVersion::V2),
345            ProxyProtocolVersion::parse(" V2 ")
346        );
347        for other in ["", "1", "2", "v3", "true", "on"] {
348            assert_eq!(None, ProxyProtocolVersion::parse(other), "{other}");
349        }
350        for name in PROXY_PROTOCOL_VERSIONS {
351            assert_eq!(true, ProxyProtocolVersion::parse(name).is_some());
352        }
353    }
354
355    #[test]
356    fn test_header_v1() {
357        let v1 = |source: &str, destination: &str| {
358            String::from_utf8(new_proxy_protocol_header(
359                ProxyProtocolVersion::V1,
360                Some((addr(source), addr(destination))),
361            ))
362            .unwrap()
363        };
364        assert_eq!(
365            "PROXY TCP4 192.0.2.1 198.51.100.1 56324 443\r\n",
366            v1("192.0.2.1:56324", "198.51.100.1:443")
367        );
368        assert_eq!(
369            "PROXY TCP6 2001:db8::1 2001:db8::2 4000 443\r\n",
370            v1("[2001:db8::1]:4000", "[2001:db8::2]:443")
371        );
372        // A dual-stack listener has IPv4 clients under mapped addresses:
373        // they are IPv4 clients.
374        assert_eq!(
375            "PROXY TCP4 192.0.2.1 198.51.100.1 56324 443\r\n",
376            v1("[::ffff:192.0.2.1]:56324", "[::ffff:198.51.100.1]:443")
377        );
378        // One end of each family: IPv6, and the mapped address in the
379        // digits the specification has for one, not in dotted decimals.
380        assert_eq!(
381            "PROXY TCP6 ::ffff:c000:201 2001:db8::2 56324 443\r\n",
382            v1("192.0.2.1:56324", "[2001:db8::2]:443")
383        );
384        assert_eq!(
385            "PROXY TCP6 2001:db8::1 ::ffff:c633:6401 4000 443\r\n",
386            v1("[2001:db8::1]:4000", "198.51.100.1:443")
387        );
388        // the longest there is fits the 107 bytes a reader allows for
389        let longest = v1(
390            "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe]:65535",
391            "[ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffd]:65535",
392        );
393        assert_eq!(true, longest.len() <= 107, "{}", longest.len());
394
395        assert_eq!(
396            b"PROXY UNKNOWN\r\n".to_vec(),
397            new_proxy_protocol_header(ProxyProtocolVersion::V1, None)
398        );
399    }
400
401    #[test]
402    fn test_header_v2() {
403        let v2 = |source: &str, destination: &str| {
404            new_proxy_protocol_header(
405                ProxyProtocolVersion::V2,
406                Some((addr(source), addr(destination))),
407            )
408        };
409        let mut tcp4 = V2_SIGNATURE.to_vec();
410        tcp4.extend_from_slice(&[0x21, 0x11, 0, 12]);
411        tcp4.extend_from_slice(&[192, 0, 2, 1, 198, 51, 100, 1]);
412        tcp4.extend_from_slice(&56324u16.to_be_bytes());
413        tcp4.extend_from_slice(&443u16.to_be_bytes());
414        assert_eq!(28, tcp4.len());
415        assert_eq!(tcp4, v2("192.0.2.1:56324", "198.51.100.1:443"));
416        assert_eq!(
417            tcp4,
418            v2("[::ffff:192.0.2.1]:56324", "[::ffff:198.51.100.1]:443")
419        );
420
421        let mut tcp6 = V2_SIGNATURE.to_vec();
422        tcp6.extend_from_slice(&[0x21, 0x21, 0, 36]);
423        tcp6.extend_from_slice(
424            &"2001:db8::1"
425                .parse::<std::net::Ipv6Addr>()
426                .unwrap()
427                .octets(),
428        );
429        tcp6.extend_from_slice(
430            &"::ffff:198.51.100.1"
431                .parse::<std::net::Ipv6Addr>()
432                .unwrap()
433                .octets(),
434        );
435        tcp6.extend_from_slice(&4000u16.to_be_bytes());
436        tcp6.extend_from_slice(&443u16.to_be_bytes());
437        assert_eq!(52, tcp6.len());
438        // one end of each family
439        assert_eq!(tcp6, v2("[2001:db8::1]:4000", "198.51.100.1:443"));
440
441        let mut local = V2_SIGNATURE.to_vec();
442        local.extend_from_slice(&[0x20, 0, 0, 0]);
443        assert_eq!(
444            local,
445            new_proxy_protocol_header(ProxyProtocolVersion::V2, None)
446        );
447    }
448
449    /// The header is on the connection before anything the caller writes,
450    /// and all of it.
451    #[tokio::test]
452    async fn test_connector_sends_the_header_first() {
453        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
454        let local = listener.local_addr().unwrap();
455        for (version, tcp_fast_open) in [
456            (ProxyProtocolVersion::V1, false),
457            (ProxyProtocolVersion::V2, false),
458            (ProxyProtocolVersion::V2, true),
459        ] {
460            let header = new_proxy_protocol_header(
461                version,
462                Some((addr("192.0.2.1:56324"), addr("198.51.100.1:443"))),
463            );
464            let connector = ProxyProtocolConnector::new(
465                header.clone(),
466                ProxyProtocolConnectOptions {
467                    connection_timeout: Some(Duration::from_secs(5)),
468                    tcp_recv_buf: Some(64 * 1024),
469                    tcp_fast_open,
470                },
471            );
472            assert_eq!(header, connector.header());
473            let mut stream =
474                match connector.connect(&PeerAddr::Inet(local)).await {
475                    Ok(stream) => stream,
476                    // A kernel that has fast open switched off for its
477                    // clients refuses the option, to pingora's own
478                    // connector as well: there is nothing to see here.
479                    Err(e)
480                        if tcp_fast_open
481                            && e.to_string()
482                                .contains("TCP_FASTOPEN_CONNECT") =>
483                    {
484                        continue;
485                    },
486                    Err(e) => panic!("{e}"),
487                };
488            stream.write_all(b"GET / HTTP/1.1\r\n\r\n").await.unwrap();
489            stream.flush().await.unwrap();
490            drop(stream);
491
492            let (mut accepted, _) = listener.accept().await.unwrap();
493            let mut received = Vec::new();
494            accepted.read_to_end(&mut received).await.unwrap();
495            let mut expected = header;
496            expected.extend_from_slice(b"GET / HTTP/1.1\r\n\r\n");
497            assert_eq!(expected, received);
498        }
499    }
500
501    /// A backend that is not there is one that refused, as it is for
502    /// pingora's own connector: the type of the error is what a retry and
503    /// the answer to the client go by.
504    #[tokio::test]
505    async fn test_connector_errors() {
506        // an address nothing listens on: bound, and given up again
507        let unused = {
508            let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
509            listener.local_addr().unwrap()
510        };
511        let connector = ProxyProtocolConnector::new(
512            new_proxy_protocol_header(ProxyProtocolVersion::V2, None),
513            ProxyProtocolConnectOptions::default(),
514        );
515        let error = connector
516            .connect(&PeerAddr::Inet(unused))
517            .await
518            .err()
519            .unwrap();
520        assert_eq!(ErrorType::ConnectRefused, error.etype);
521        assert_eq!(
522            true,
523            error.to_string().contains(&unused.to_string()),
524            "{error}"
525        );
526
527        // A backend that does not answer in the time it is given: one
528        // that timed out, and without a limit one that is waited for.
529        let silent = PeerAddr::Inet(unused);
530        let error = within(
531            Some(Duration::from_millis(20)),
532            &silent,
533            std::future::pending::<Result<()>>(),
534        )
535        .await
536        .err()
537        .unwrap();
538        assert_eq!(ErrorType::ConnectTimedout, error.etype);
539        assert_eq!(true, error.to_string().contains("timeout 20ms"), "{error}");
540        assert_eq!(7, within(None, &silent, async { Ok(7) }).await.unwrap());
541    }
542
543    #[cfg(unix)]
544    #[tokio::test]
545    async fn test_connector_unix_socket() {
546        let dir = tempfile::tempdir().unwrap();
547        let path = dir.path().join("backend.sock");
548        let listener = tokio::net::UnixListener::bind(&path).unwrap();
549        let header = new_proxy_protocol_header(
550            ProxyProtocolVersion::V1,
551            Some((addr("192.0.2.1:56324"), addr("198.51.100.1:443"))),
552        );
553        let connector = ProxyProtocolConnector::new(
554            header.clone(),
555            ProxyProtocolConnectOptions::default(),
556        );
557        let addr: PeerAddr =
558            format!("unix:{}", path.display()).parse().unwrap();
559        let stream = connector.connect(&addr).await.unwrap();
560        drop(stream);
561        let (mut accepted, _) = listener.accept().await.unwrap();
562        let mut received = Vec::new();
563        accepted.read_to_end(&mut received).await.unwrap();
564        assert_eq!(header, received);
565    }
566}