~zethra/public-inbox

stargazer: Add proxy protocol support v2 APPLIED

Benjamin Jacobs: 1
 Add proxy protocol support

 7 files changed, 646 insertions(+), 14 deletions(-)
#1297207 freebsd.yml success
#1297208 linux.yml success
Welcome!

Honestly it's just nice to know that anyone else out there is using it
and cares. My personal enthusiasm for Gemini has waned since I wrote
Stargazer. But it's the most substantial project I've created that I can
share in the open (not for work) and I'm proud of it.

Though it's far from perfect. If I had a lot more time, energy, and will,
I'd make some substantial changes. I'm not really happy with the config
layout, and I think the current way routes are defined can be unintuitive
and confusing. Maybe one day I'll redo it and there'll be a Stargazer
2.0 with a new config syntax!
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~zethra/public-inbox/patches/54349/mbox | git am -3
Learn more about email & git

[PATCH stargazer v2] Add proxy protocol support Export this patch

---
 Cargo.lock              | 134 ++++++++++-
 Cargo.toml              |   5 +
 doc/stargazer-ini.scd   |   9 +
 doc/stargazer.1.txt     |   2 +-
 doc/stargazer.ini.5.txt |  12 +-
 src/config.rs           |   6 +
 src/main.rs             | 492 +++++++++++++++++++++++++++++++++++++++-
 7 files changed, 646 insertions(+), 14 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index de04070..4fdec0e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -41,7 +41,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
 "synstructure",
]

@@ -53,7 +53,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
]

[[package]]
@@ -271,6 +271,18 @@ dependencies = [
 "serde",
]

[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"

[[package]]
name = "bytes"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"

[[package]]
name = "cc"
version = "1.0.96"
@@ -430,7 +442,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
]

[[package]]
@@ -442,6 +454,12 @@ dependencies = [
 "const-random",
]

[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"

[[package]]
name = "errno"
version = "0.3.8"
@@ -875,6 +893,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"

[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
 "zerocopy",
]

[[package]]
name = "proc-macro2"
version = "1.0.81"
@@ -884,6 +911,16 @@ dependencies = [
 "unicode-ident",
]

[[package]]
name = "proxy-protocol"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e50c72c21c738f5c5f350cc33640aee30bf7cd20f9d9da20ed41bce2671d532"
dependencies = [
 "bytes",
 "snafu",
]

[[package]]
name = "quote"
version = "1.0.36"
@@ -893,6 +930,36 @@ dependencies = [
 "proc-macro2",
]

[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
 "libc",
 "rand_chacha",
 "rand_core",
]

[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
 "ppv-lite86",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
 "getrandom",
]

[[package]]
name = "rcgen"
version = "0.13.1"
@@ -1067,7 +1134,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
]

[[package]]
@@ -1121,6 +1188,27 @@ dependencies = [
 "autocfg",
]

[[package]]
name = "snafu"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7"
dependencies = [
 "doc-comment",
 "snafu-derive",
]

[[package]]
name = "snafu-derive"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 1.0.109",
]

[[package]]
name = "socket2"
version = "0.4.10"
@@ -1174,6 +1262,8 @@ dependencies = [
 "mime_guess",
 "once_cell",
 "percent-encoding",
 "proxy-protocol",
 "rand",
 "rcgen",
 "regex",
 "rust-ini",
@@ -1194,6 +1284,17 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"

[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
 "proc-macro2",
 "quote",
 "unicode-ident",
]

[[package]]
name = "syn"
version = "2.0.60"
@@ -1213,7 +1314,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
]

[[package]]
@@ -1233,7 +1334,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
 "syn 2.0.60",
]

[[package]]
@@ -1552,6 +1653,27 @@ dependencies = [
 "time",
]

[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
 "byteorder",
 "zerocopy-derive",
]

[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 2.0.60",
]

[[package]]
name = "zeroize"
version = "1.7.0"
diff --git a/Cargo.toml b/Cargo.toml
index 22779c3..c2cedcd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,6 +53,7 @@ mime_guess = "2.0.4"
socket2 = "0.5.7"
percent-encoding = "2.3.1"
uriparse = "0.6"
proxy-protocol = "0.5.0"

anyhow = "1.0.82"
once_cell = "1.19.0"
@@ -89,3 +90,7 @@ features = ["std"]
[profile.release]
lto = "fat"
codegen-units = 1

[dev-dependencies]
rand = "0.8.5"

diff --git a/doc/stargazer-ini.scd b/doc/stargazer-ini.scd
index 2c23867..c03cfa4 100644
--- a/doc/stargazer-ini.scd
+++ b/doc/stargazer-ini.scd
@@ -81,6 +81,15 @@ that *stargazer* will server requests to. At least one route must be specified.
	default. *Warning*, if this is set, large files and cgi scripts may be cut
	off before their response finishes.

*proxy-protocol*
	Whether or not to expect a proxy protocol header before the tls connection.
	It is generally used with a reverse proxy in situations where you need to
	know the real IP address of the client. Off by default.

	See the formal specification of proxy protocol:

	http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

## TLS KEYS

The following keys are accepted under the *[:tls]* section:
diff --git a/doc/stargazer.1.txt b/doc/stargazer.1.txt
index 1f24b10..4a190f0 100644
--- a/doc/stargazer.1.txt
+++ b/doc/stargazer.1.txt
@@ -51,4 +51,4 @@ AUTHORS
       bugs/patches  can  be  submitted by email to ~zethra/public-in‐
       box@lists.sr.ht with the prefix stargazer.

                              2024-08-03                  stargazer(1)
                              2024-08-05                  stargazer(1)
diff --git a/doc/stargazer.ini.5.txt b/doc/stargazer.ini.5.txt
index 826bff3..fa5c67e 100644
--- a/doc/stargazer.ini.5.txt
+++ b/doc/stargazer.ini.5.txt
@@ -83,6 +83,16 @@ CONFIGURATION KEYS
           Warning, if this is set, large files and cgi scripts may be
           cut off before their response finishes.

       proxy-protocol
           Whether or not to expect a proxy protocol header before the
           tls connection. It is designed for use with a reverse proxy
           in  situations where you need to know the IP address of the
           client. Off by default.

           See the formal specification of proxy protocol:

           http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

   TLS KEYS
       The following keys are accepted under the [:tls] section:

@@ -410,4 +420,4 @@ AUTHORS
       bugs/patches can be submitted by  email  to  ~zethra/public-in‐
       box@lists.sr.ht with the prefix stargazer.

                              2024-08-03              stargazer.ini(5)
                              2024-08-05              stargazer.ini(5)
diff --git a/src/config.rs b/src/config.rs
index 7f1196b..5d5cc25 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -60,6 +60,8 @@ pub struct Config {
    pub cert_lifetime: u64,
    /// Should stargazer log full or partial IPs or not at all
    pub ip_log: IpLogAmount,
    /// Should stargazer expect a proxy protocol header
    pub proxy_protocol: bool,
}

#[derive(Debug, Clone, Copy)]
@@ -122,6 +124,8 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
                "log-ip and log-ip-partial can't both be turned on at once"
            ),
        };

        let proxy_protocol = general.remove_yn("proxy-protocol");
        check_section_empty("general", general)?;

        let tls_section = conf
@@ -332,6 +336,7 @@ pub fn load(config_path: impl AsRef<Path>) -> Result<Config> {
            regen_certs,
            cert_lifetime,
            ip_log,
            proxy_protocol,
        })
    })();
    res.with_context(|| {
@@ -381,6 +386,7 @@ pub fn dev_config() -> Result<Config> {
        regen_certs: true,
        cert_lifetime: 0,
        ip_log: IpLogAmount::Full,
        proxy_protocol: true,
    };
    fs::create_dir_all(&conf.store).with_context(|| {
        format!(
diff --git a/src/main.rs b/src/main.rs
index b0fa9ba..a4eca13 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -29,12 +29,14 @@ use async_channel::{Receiver, Sender};
use async_executor::Executor;
use async_io::Timer;
use async_net::{SocketAddrV6, TcpListener, TcpStream};
use bstr::ByteSlice;
use cgi::{serve_cgi, serve_scgi};
use futures_lite::*;
use futures_rustls::{server::TlsStream, TlsAcceptor};
use get_file::get_file;
use log::{debug, error};
use once_cell::sync::{Lazy, OnceCell};
use proxy_protocol::ProxyHeader;
use router::{route, Request, Route};
use std::clone::Clone;
use std::convert::TryFrom;
@@ -229,20 +231,157 @@ async fn exit_on_sig() -> Result<()> {
    std::process::exit(0);
}

#[cfg(not(unix))]
async fn exit_on_sig() -> Result<()> {
    Ok(future::pending::<()>().await)
/// A Reader which can peek into the stream without changing the cursor.
///
/// This is used with TcpStream and str so parse_proxy_protocol can be tested
trait AsyncPeek {
    async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

impl<'a> AsyncPeek for &'a [u8] {
    async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let to_read = buf.len().min(self.len());
        buf[..to_read].copy_from_slice(&self[..to_read]);
        Ok(to_read)
    }
}

impl AsyncPeek for TcpStream {
    async fn peek(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        TcpStream::peek(self, buf).await
    }
}

/// Parse the proxy protocol given a peek buffer and a stream.
///
/// It is separated out from handle_proxy_protocol for testing purposes.
async fn parse_proxy_protocol<R: AsyncReadExt + AsyncPeek + Unpin>(
    stream: &mut R,
) -> anyhow::Result<Option<ProxyHeader>> {
    let mut peek = [0u8; 12];
    let n = stream.peek(&mut peek).await?;
    let peek = &peek[..n];

    if peek.starts_with(b"PROXY ") {
        // V1 - Read until \r\n
        let mut buf: Vec<u8> = Vec::new();
        loop {
            // Peek in blocks of 64 bytes, and read only until the \n.
            let mut peek = [0u8; 64];
            let n = stream.peek(&mut peek).await?;
            let peek = &mut peek[..n];
            let (found, i) = match peek.find(b"\n") {
                Some(i) => (true, i + 1),
                None => (false, n),
            };
            let mut peek = &mut peek[..i];
            stream.read_exact(&mut peek).await?;
            buf.extend_from_slice(peek);
            if found {
                break;
            }
        }
        let mut buf = buf.as_slice();

        Ok(Some(proxy_protocol::parse(&mut buf)?))
    } else if peek.starts_with(&[
        0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
    ]) {
        // V2 - Read header length bytes after the first 16 at [14..16]

        let mut header = [0u8; 16];
        stream.read_exact(&mut header).await?;
        let len = u16::from_be_bytes([header[14], header[15]]);

        let mut buf = vec![0u8; 16 + len as usize];
        let buf_slice = buf.as_mut_slice();
        buf_slice[..16].copy_from_slice(&header);
        stream.read_exact(&mut buf_slice[16..]).await?;
        let mut buf = buf.as_slice();

        Ok(Some(proxy_protocol::parse(&mut buf)?))
    } else {
        Ok(None)
    }
}

/// Try to parse and apply a proxy protocol header.
///
/// * If the header is not found, then no data is read from the stream and no
///     action is taken.
/// * If the header is found: `remote_addr` will be swapped with the header's
///     source addr.
/// * If the header is found, but contains invalid data: no action is taken.
/// * If the header is found, but could not be parsed: an error is returned and
///     the connection should be closed.
async fn handle_proxy_protocol(
    stream: &mut TcpStream,
    remote_addr: &mut SocketAddr,
) -> anyhow::Result<()> {
    use proxy_protocol::{
        version1::ProxyAddresses as ProxyAddrV1,
        version2::ProxyAddresses as ProxyAddrV2,
    };
    // proxy-protocol doesn't support async streams, so in order to parse the
    // proxy protocol, we need to pre-read the header before passing it in
    // without reading into tls data.
    //
    // First, check if there is a proxy protocol header with the first 12 bytes.
    // Second, read the whole proxy protocol header.
    // Third, pass the raw data into proxy-protocol to get parsed.

    match parse_proxy_protocol(stream).await? {
        Some(ProxyHeader::Version1 { addresses }) => match addresses {
            ProxyAddrV1::Ipv4 {
                source,
                destination: _,
            } => *remote_addr = SocketAddr::V4(source),
            ProxyAddrV1::Ipv6 {
                source,
                destination: _,
            } => *remote_addr = SocketAddr::V6(source),
            _ => {}
        },
        Some(ProxyHeader::Version2 {
            command: _,
            transport_protocol: _,
            addresses,
        }) => match addresses {
            ProxyAddrV2::Ipv4 {
                source,
                destination: _,
            } => *remote_addr = SocketAddr::V4(source),
            ProxyAddrV2::Ipv6 {
                source,
                destination: _,
            } => *remote_addr = SocketAddr::V6(source),
            _ => {}
        },
        _ => {}
    }
    Ok(())
}

// This is a separate function for ease of error handling
async fn start_task(
    stream: TcpStream,
    mut stream: TcpStream,
    acceptor: TlsAcceptor,
    remote_addr: SocketAddr,
    mut remote_addr: SocketAddr,
    server_port: u16,
) {
    use anyhow::Context;
    // Accept tsl connection

    if CONF.proxy_protocol {
        if let Err(e) = handle_proxy_protocol(&mut stream, &mut remote_addr)
            .await
            .context("Error parsing proxy protocol")
        {
            debug!("{:#}", e);
            return;
        }
    }

    // Accept tls connection
    match acceptor
        .accept(stream)
        .await
@@ -593,3 +732,344 @@ impl From<GemError> for ErrorLogInfo {
        }
    }
}

#[cfg(test)]
mod test {
    use std::time::{Duration, Instant};

    use super::*;

    use async_net::SocketAddrV4;
    use proxy_protocol::{
        version1::ProxyAddresses as ProxyAddrV1,
        version2::{
            ProxyAddresses as ProxyAddrV2, ProxyCommand, ProxyTransportProtocol,
        },
        ParseError, ProxyHeader,
    };
    use rand::{Rng, RngCore};

    fn parse_proxy(body: &[u8]) -> anyhow::Result<Option<ProxyHeader>> {
        parse_proxy_len(body).0
    }
    fn parse_proxy_len(
        body: &[u8],
    ) -> (anyhow::Result<Option<ProxyHeader>>, usize) {
        async_io::block_on(async {
            let mut stream = body;
            let output = parse_proxy_protocol(&mut stream).await;
            (output, body.len() - stream.len())
        })
    }

    fn random_u8() -> u8 {
        rand::random()
    }
    #[inline]
    fn random_bool() -> bool {
        rand::random()
    }

    fn random_addr_v4() -> SocketAddrV4 {
        let mut rng = rand::thread_rng();
        let addr: (u8, u8, u8, u8) = rng.gen();
        SocketAddrV4::new(
            Ipv4Addr::new(addr.0, addr.1, addr.2, addr.3),
            rng.gen(),
        )
    }
    fn random_addr_v6() -> SocketAddrV6 {
        let mut rng = rand::thread_rng();
        let addr: (u16, u16, u16, u16, u16, u16, u16, u16) = rng.gen();
        SocketAddrV6::new(
            Ipv6Addr::new(
                addr.0, addr.1, addr.2, addr.3, addr.4, addr.5, addr.6, addr.7,
            ),
            rng.gen(),
            0,
            0,
        )
    }

    fn random_header() -> ProxyHeader {
        if random_bool() {
            let addresses = if random_bool() {
                ProxyAddrV1::Ipv4 {
                    source: random_addr_v4(),
                    destination: random_addr_v4(),
                }
            } else {
                ProxyAddrV1::Ipv6 {
                    source: random_addr_v6(),
                    destination: random_addr_v6(),
                }
            };
            ProxyHeader::Version1 { addresses }
        } else {
            let addresses = match random_u8() & 0b11 {
                0 => ProxyAddrV2::Unspec,
                1 => ProxyAddrV2::Ipv4 {
                    source: random_addr_v4(),
                    destination: random_addr_v4(),
                },
                2 => ProxyAddrV2::Ipv6 {
                    source: random_addr_v6(),
                    destination: random_addr_v6(),
                },
                3 => {
                    let mut source = [0u8; 108];
                    let mut destination = [0u8; 108];
                    let mut rng = rand::thread_rng();
                    rng.fill_bytes(&mut source);
                    rng.fill_bytes(&mut destination);
                    ProxyAddrV2::Unix {
                        source,
                        destination,
                    }
                }
                _ => unreachable!(),
            };
            ProxyHeader::Version2 {
                command: match random_bool() {
                    true => ProxyCommand::Local,
                    false => ProxyCommand::Proxy,
                },
                transport_protocol: match random_u8() & 0b11 {
                    0 => ProxyTransportProtocol::Unspec,
                    1 => ProxyTransportProtocol::Stream,
                    2 => ProxyTransportProtocol::Datagram,
                    3 => ProxyTransportProtocol::Stream,
                    _ => unreachable!(),
                },
                addresses,
            }
        }
    }

    /// Perform a simple check that v1 headers parse properly
    #[test]
    fn test_proxy_proto_v1() {
        assert_eq!(parse_proxy(b"No header here").unwrap(), None);

        let hdr = ProxyHeader::Version1 {
            addresses: ProxyAddrV1::Ipv4 {
                source: random_addr_v4(),
                destination: random_addr_v4(),
            },
        };
        let data = proxy_protocol::encode(hdr.clone()).unwrap();
        assert_eq!(parse_proxy(&data).unwrap(), Some(hdr));

        let hdr = ProxyHeader::Version1 {
            addresses: ProxyAddrV1::Ipv6 {
                source: random_addr_v6(),
                destination: random_addr_v6(),
            },
        };
        let data = proxy_protocol::encode(hdr.clone()).unwrap();
        assert_eq!(parse_proxy(&data).unwrap(), Some(hdr));
    }

    /// Perform a simple check that v2 headers parse properly
    #[test]
    fn test_proxy_proto_v2() {
        let hdr = ProxyHeader::Version2 {
            command: ProxyCommand::Proxy,
            transport_protocol: ProxyTransportProtocol::Stream,
            addresses: ProxyAddrV2::Ipv4 {
                source: random_addr_v4(),
                destination: random_addr_v4(),
            },
        };
        let data = proxy_protocol::encode(hdr.clone()).unwrap();
        assert_eq!(parse_proxy(&data).unwrap(), Some(hdr));

        let hdr = ProxyHeader::Version2 {
            command: ProxyCommand::Proxy,
            transport_protocol: ProxyTransportProtocol::Stream,
            addresses: ProxyAddrV2::Ipv6 {
                source: random_addr_v6(),
                destination: random_addr_v6(),
            },
        };
        let data = proxy_protocol::encode(hdr.clone()).unwrap();
        assert_eq!(parse_proxy(&data).unwrap(), Some(hdr));
    }

    /// Check against as many as possible inputs to make sure that all valid headers do not error
    /// and all invalid headers match the error received by proxy_protocol
    ///
    /// This will always run for `duration` seconds
    #[test]
    fn test_proxy_proto_fuzz() {
        let duration = Duration::from_secs(15);
        let deadline = Instant::now() + duration;

        let mut count = 0;
        let mut v1_count = 0;
        let mut v2_count = 0;

        let mut rng = rand::thread_rng();

        while Instant::now() < deadline {
            // Create a random header and verify that it parses correctly
            let header = random_header();
            let mut data = proxy_protocol::encode(header.clone()).unwrap();
            let len = data.len();
            let mut extra = [0u8; 64];
            rng.fill_bytes(&mut extra);
            data.extend_from_slice(&extra);
            // We add a newline here because otherwise, parser will wait indefinitely until a
            // newline.
            data.extend_from_slice(b"\n");

            let mut data = data.to_vec();
            count += 1;

            assert_eq!(parse_proxy(&data).unwrap(), Some(header));

            let is_ascii = data.starts_with(b"PROXY ");
            if is_ascii {
                v1_count += 1 + len * 8;
            } else {
                v2_count += 1 + len * 8;
            }

            // perform N permutations, corrupting the header 1 random bit at a time where N is the
            // number of bits in the header
            for _ in 0..len * 8 {
                let i = random_u8() as usize % len;
                let o = random_u8() & 0b111;
                data[i] = data[i] ^ (1 << o);

                count += 1;

                let genbuf = || {
                    if is_ascii {
                        data[..len]
                            .iter()
                            .map(|c| match *c >= 0x20 && *c <= 0x7F {
                                true => (*c as char).to_string(),
                                false => format!("\\{c:X}"),
                            })
                            .collect::<Vec<String>>()
                            .join("")
                    } else {
                        data[..len]
                            .iter()
                            .map(|c| format!("{c:02X}"))
                            .collect::<Vec<_>>()
                            .join(" ")
                    }
                };

                let (output, read) = parse_proxy_len(&data);
                if let Err(e) = &output {
                    if let Some(_) = e.downcast_ref::<io::Error>() {
                        // This is an error with the length of the data.
                        // If we reach this point, then the input is absolutely corrupted and we
                        // cannot continue.
                        continue;
                    }
                }
                if let Ok(None) = &output {
                    if read != 0 {
                        let buf = genbuf();
                        panic!("{buf}\nThere no header, but {read} bytes were read")
                    }
                }

                let mut expected_slice = data.as_slice();
                let expected = match proxy_protocol::parse(&mut expected_slice)
                {
                    Ok(output) => Ok(Some(output)),
                    Err(ParseError::NotProxyHeader) => Ok(None),
                    Err(err) => Err(err),
                };
                let expected_read = data.len() - expected_slice.len();

                // Compare the outputs of the expected behaviour and the actual behaviour.
                // There are some edge cases because of the way we parse the data
                match (output, expected) {
                    (Ok(a), Ok(b)) => {
                        if a != b {
                            let buf = genbuf();
                            assert_eq!(a, b, "{buf}");
                        }
                        if read != expected_read {
                            let buf = genbuf();
                            assert_eq!(read, expected_read, "{buf}");
                        }
                    }
                    (Err(a), Err(b)) => {
                        // This is where results may differ. What matters however is that both
                        // produce the same type of error
                        match (a.downcast::<ParseError>().unwrap(), b) {
                            (ParseError::NotProxyHeader, _) => {
                                let buf = genbuf();
                                panic!("{buf}\nIt should not be possible for a to return a NotProxyHeader error")
                            }
                            (
                                ParseError::InvalidVersion { version: a },
                                ParseError::InvalidVersion { version: b },
                            ) => {
                                let buf = genbuf();
                                assert_eq!(
                                    ParseError::InvalidVersion { version: a }
                                        .to_string(),
                                    ParseError::InvalidVersion { version: b }
                                        .to_string(),
                                    "{buf}"
                                );
                            }
                            (
                                ParseError::Version1 { source: _ },
                                ParseError::Version1 { source: _ },
                            ) => {}
                            (
                                ParseError::Version2 { source: _ },
                                ParseError::Version2 { source: _ },
                            ) => {}
                            (a, b) => {
                                let buf = genbuf();
                                assert_eq!(
                                    a.to_string(),
                                    b.to_string(),
                                    "{buf}"
                                );
                            }
                        }
                    }
                    (Err(a), b) => match a.downcast::<ParseError>().unwrap() {
                        ParseError::Version1 { source } => {
                            match source {
                                // (V1 error - unexpected eof, an okay result)
                                // This is a rare occurrence, but it is possible that a technically
                                // valid header gets read as invalid because there is a \n hidden
                                // inside the header. This is okay so should not panic.
                                proxy_protocol::version1::ParseError::UnexpectedEof => {},
                                a => {
                                    let buf = genbuf();
                                    panic!("{buf}\n{a:?} != {b:?}")
                                }
                            }
                        }
                        a => {
                            let buf = genbuf();
                            panic!("{buf}\n{a:?} != {b:?}")
                        }
                    },
                    (a, b) => {
                        let buf = genbuf();
                        panic!("{buf}\n{a:?} != {b:?}")
                    }
                }
            }
        }
        println!(
            "Checked {count} headers ({}% v1 headers and {}% v2 headers)",
            (v1_count as f64 / count as f64 * 100.0).round(),
            (v2_count as f64 / count as f64 * 100.0).round()
        )
    }
}
-- 
2.30.2
Thanks! I appreciate you taking feedback and making changes.

To git@git.sr.ht:~zethra/stargazer
   5e4baa2..57a58ab  main -> main

If you have the inclination, it'd be good to add a functional test to
./stargazer/scripts/gemini-diagnostics to ensure that it doesn't break in
some other way later. But this isn't required. I may add one myself at
some point.

I plan to cut a new release containing this feature within the next week,
when I have time.
stargazer/patches: SUCCESS in 3m18s

[Add proxy protocol support][0] v2 from [Benjamin Jacobs][1]

[0]: https://lists.sr.ht/~zethra/public-inbox/patches/54349
[1]: mailto:bj@benjaminja.com

βœ“ #1297208 SUCCESS stargazer/patches/linux.yml   https://builds.sr.ht/~zethra/job/1297208
βœ“ #1297207 SUCCESS stargazer/patches/freebsd.yml https://builds.sr.ht/~zethra/job/1297207
Hi,

I switched the osrandom dev dependency with rand and made sure it
compiles using MSRV 1.77.0.

I couldn't figure out how to create an async buf reader out of the tcp
stream and have it also work with the tls acceptor. So what I have done
instead is to use the peek function to peek in blocks of 64 bytes and
read in those blocks until a newline is reached.
It also feels a bit clunky, but is much better than reading one byte at
a time. In order to make it work with the tests, I had to create the
AsyncPeek trait. I'm not super happy with this, but it's the best that I
could come up with.

Best,
Ben