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