~zethra/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
5 3

[PATCH stargazer v2] Add proxy protocol support

Details
Message ID
<20240808021210.729132-1-bj@benjaminja.com>
DKIM signature
pass
Download raw message
Patch: +646 -14
---
 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

[stargazer/patches] build success

builds.sr.ht <builds@sr.ht>
Details
Message ID
<D3A66QTTSDX2.3JQ1V5KKH1VK@fra02>
In-Reply-To
<20240808021210.729132-1-bj@benjaminja.com> (view parent)
DKIM signature
missing
Download raw message
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]: 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
Details
Message ID
<D3A6GT3XNESW.YDZJCGDH38EG@benjaminja.com>
In-Reply-To
<20240808021210.729132-1-bj@benjaminja.com> (view parent)
DKIM signature
pass
Download raw message
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.

> 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;
>     }
> }

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
Details
Message ID
<D3B07GGIL9AG.13VV3AGSVEEKE@noraa.gay>
In-Reply-To
<20240808021210.729132-1-bj@benjaminja.com> (view parent)
DKIM signature
pass
Download raw message
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.
Details
Message ID
<D3B0P4IG5SAW.1FAETLNI9ZFKU@benjaminja.com>
In-Reply-To
<D3B07GGIL9AG.13VV3AGSVEEKE@noraa.gay> (view parent)
DKIM signature
pass
Download raw message
> 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.

Good to know! I can work on adding to the diagnostics when I get the
chance.

---

Thank you Sashanoraa for making stargazer. I discovered it when I tried
to make my own rust server and Stargazer is much better than what I would
have come up with. I really appreciate all the effort you have put into
making and maintaining it.

Thanks again,
Ben
Details
Message ID
<D3B0VXGEST1G.1D1DHAJT6T5PK@noraa.gay>
In-Reply-To
<D3B0P4IG5SAW.1FAETLNI9ZFKU@benjaminja.com> (view parent)
DKIM signature
pass
Download raw message
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!
Reply to thread Export thread (mbox)