use crate::protocol::{HeaderResult, PartialResult, v1, v2};
use rama_core::error::BoxErrorExt as _;
use rama_core::{
Layer, Service,
bytes::Bytes,
error::{BoxError, ErrorContext, ErrorExt},
extensions::{Extension, ExtensionsRef},
io::{HeapReader, Io, PrefixedIo},
telemetry::tracing,
};
use rama_net::address::Domain;
use rama_net::forwarded::{Forwarded, ForwardedElement};
use rama_utils::macros::generate_set_and_with;
use std::net::SocketAddr;
use tokio::io::AsyncReadExt;
const MAX_HEADER_LENGTH: usize = 16 + u16::MAX as usize;
const INITIAL_BUFFER_CAPACITY: usize = 128;
#[derive(Debug, Clone, Copy)]
pub struct HaProxyStrictness {
pub max_header_length: usize,
pub verify_crc32c_when_present: bool,
pub require_crc32c: bool,
pub reject_unknown_address_family: bool,
pub reject_local_command: bool,
pub fail_on_malformed_tlv: bool,
}
impl HaProxyStrictness {
const DEFAULT: Self = Self {
max_header_length: MAX_HEADER_LENGTH,
verify_crc32c_when_present: true,
require_crc32c: false,
reject_unknown_address_family: false,
reject_local_command: false,
fail_on_malformed_tlv: false,
};
}
impl Default for HaProxyStrictness {
fn default() -> Self {
Self::DEFAULT
}
}
impl HaProxyStrictness {
#[must_use]
pub const fn lenient() -> Self {
Self {
max_header_length: MAX_HEADER_LENGTH,
verify_crc32c_when_present: false,
require_crc32c: false,
reject_unknown_address_family: false,
reject_local_command: false,
fail_on_malformed_tlv: false,
}
}
#[must_use]
pub const fn strict() -> Self {
Self {
max_header_length: MAX_HEADER_LENGTH,
verify_crc32c_when_present: true,
require_crc32c: true,
reject_unknown_address_family: true,
reject_local_command: false,
fail_on_malformed_tlv: true,
}
}
}
#[derive(Debug, Clone)]
pub struct HaProxyTlv {
pub kind: v2::Type,
pub value: Bytes,
}
#[derive(Debug, Clone, Default, Extension)]
#[extension(tags(proxy))]
pub struct HaProxyTlvs {
entries: Vec<HaProxyTlv>,
}
impl HaProxyTlvs {
#[must_use]
pub fn entries(&self) -> &[HaProxyTlv] {
&self.entries
}
#[must_use]
pub fn get(&self, kind: v2::Type) -> Option<&Bytes> {
self.entries
.iter()
.find(|tlv| tlv.kind == kind)
.map(|tlv| &tlv.value)
}
#[must_use]
pub fn authority(&self) -> Option<Domain> {
self.get(v2::Type::Authority)
.and_then(|v| Domain::try_from(v.as_ref()).ok())
}
#[must_use]
pub fn unique_id(&self) -> Option<&Bytes> {
self.get(v2::Type::UniqueId)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Extension)]
#[extension(tags(proxy))]
pub enum HaProxyCommand {
Local,
Proxy,
}
impl From<v2::Command> for HaProxyCommand {
fn from(c: v2::Command) -> Self {
match c {
v2::Command::Local => Self::Local,
v2::Command::Proxy => Self::Proxy,
}
}
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct HaProxyLayer {
peek: bool,
strictness: HaProxyStrictness,
}
impl HaProxyLayer {
#[must_use]
pub const fn new() -> Self {
Self {
peek: false,
strictness: HaProxyStrictness::DEFAULT,
}
}
generate_set_and_with!(
pub fn peek(mut self, value: bool) -> Self {
self.peek = value;
self
}
);
generate_set_and_with!(
pub fn strictness(mut self, value: HaProxyStrictness) -> Self {
self.strictness = value;
self
}
);
}
impl<S> Layer<S> for HaProxyLayer {
type Service = HaProxyService<S>;
fn layer(&self, inner: S) -> Self::Service {
HaProxyService {
inner,
peek: self.peek,
strictness: self.strictness,
}
}
}
#[derive(Debug, Clone)]
pub struct HaProxyService<S> {
inner: S,
peek: bool,
strictness: HaProxyStrictness,
}
impl<S> HaProxyService<S> {
pub const fn new(inner: S) -> Self {
Self {
inner,
peek: false,
strictness: HaProxyStrictness::DEFAULT,
}
}
generate_set_and_with!(
pub fn peek(mut self, value: bool) -> Self {
self.peek = value;
self
}
);
generate_set_and_with!(
pub fn strictness(mut self, value: HaProxyStrictness) -> Self {
self.strictness = value;
self
}
);
}
enum PeekOutcome {
V1,
V2,
NotProxy,
}
async fn peek_disambiguate<IO: tokio::io::AsyncRead + Unpin>(
stream: &mut IO,
buffer: &mut Vec<u8>,
) -> Result<PeekOutcome, BoxError> {
let v1_prefix = v1::PROTOCOL_PREFIX.as_bytes();
let v2_prefix = v2::PROTOCOL_PREFIX;
let mut scratch = [0u8; 16];
while buffer.is_empty() {
let n = stream
.read(&mut scratch)
.await
.context("haproxy peek: initial read")?;
if n == 0 {
return Ok(PeekOutcome::NotProxy);
}
buffer.extend_from_slice(&scratch[..n]);
}
let candidate_v1 = buffer[0] == v1_prefix[0];
let candidate_v2 = buffer[0] == v2_prefix[0];
if !candidate_v1 && !candidate_v2 {
return Ok(PeekOutcome::NotProxy);
}
let target_len = if candidate_v1 {
v1_prefix.len()
} else {
v2_prefix.len()
};
while buffer.len() < target_len {
let n = stream
.read(&mut scratch)
.await
.context("haproxy peek: prefix read")?;
if n == 0 {
return Ok(PeekOutcome::NotProxy);
}
buffer.extend_from_slice(&scratch[..n]);
let expected = if candidate_v1 { v1_prefix } else { v2_prefix };
let cmp_len = buffer.len().min(expected.len());
if buffer[..cmp_len] != expected[..cmp_len] {
return Ok(PeekOutcome::NotProxy);
}
}
if candidate_v1 && &buffer[..v1_prefix.len()] == v1_prefix {
Ok(PeekOutcome::V1)
} else if candidate_v2 && &buffer[..v2_prefix.len()] == v2_prefix {
Ok(PeekOutcome::V2)
} else {
Ok(PeekOutcome::NotProxy)
}
}
impl<S, IO> Service<IO> for HaProxyService<S>
where
S: Service<PrefixedIo<HeapReader, IO>, Error: Into<BoxError>>,
IO: Io + Unpin + ExtensionsRef,
{
type Output = S::Output;
type Error = BoxError;
async fn serve(&self, mut stream: IO) -> Result<Self::Output, Self::Error> {
let max_header_length = self
.strictness
.max_header_length
.min(MAX_HEADER_LENGTH)
.max(v1::PROTOCOL_PREFIX.len());
let mut buffer: Vec<u8> = Vec::with_capacity(INITIAL_BUFFER_CAPACITY);
if self.peek {
tracing::trace!("haproxy protocol peeking enabled: start detection");
match peek_disambiguate(&mut stream, &mut buffer).await? {
PeekOutcome::V1 => {
tracing::trace!(
"haproxy protocol peeked: v1 detected: continue with haproxy handling"
);
}
PeekOutcome::V2 => {
tracing::trace!(
"haproxy protocol peeked: v2 detected: continue with haproxy handling"
);
}
PeekOutcome::NotProxy => {
tracing::trace!(
"no haproxy protocol detected... delegating immediately to inner..."
);
let mem = HeapReader::new(buffer);
let stream = PrefixedIo::new(mem, stream);
return self.inner.serve(stream).await.into_box_error();
}
}
} else {
tracing::trace!("haproxy protocol enforced: skip peeking");
}
let header = loop {
let header = HeaderResult::parse(&buffer);
if header.is_complete() {
break header;
}
if buffer.len() >= max_header_length {
return Err(format!(
"haproxy: buffer exhausted (read {} bytes, configured max {max_header_length}) before parsing completed",
buffer.len()
)
.into());
}
let old_len = buffer.len();
let mut next_cap = old_len.saturating_mul(2).max(INITIAL_BUFFER_CAPACITY);
if next_cap > max_header_length {
next_cap = max_header_length;
}
buffer.resize(next_cap, 0);
let n = stream.read(&mut buffer[old_len..]).await?;
buffer.truncate(old_len + n);
if n == 0 {
return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)
.context("HaProxy header incomplete"));
}
tracing::debug!("Incomplete header. Read {} bytes so far.", buffer.len());
};
let consumed = match header {
HeaderResult::V1(Ok(ref header)) => self.apply_v1(&stream, header)?,
HeaderResult::V2(Ok(ref header)) => self.apply_v2(&stream, header)?,
HeaderResult::V1(Err(error)) => return Err(error.into()),
HeaderResult::V2(Err(error)) => return Err(error.into()),
};
let mem: HeapReader = buffer[consumed..].into();
let stream = PrefixedIo::new(mem, stream);
self.inner.serve(stream).await.into_box_error()
}
}
impl<S> HaProxyService<S> {
fn apply_v1<IO: ExtensionsRef>(
&self,
stream: &IO,
header: &v1::Header<'_>,
) -> Result<usize, BoxError> {
if self.strictness.reject_unknown_address_family
&& matches!(header.addresses, v1::Addresses::Unknown)
{
return Err(BoxError::from_static_str(
"haproxy v1: UNKNOWN address family rejected by strictness configuration",
));
}
match header.addresses {
v1::Addresses::Tcp4(info) => {
let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
insert_forwarded(stream, peer_addr);
}
v1::Addresses::Tcp6(info) => {
let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
insert_forwarded(stream, peer_addr);
}
v1::Addresses::Unknown => (),
}
Ok(header.header.len())
}
fn apply_v2<IO: ExtensionsRef>(
&self,
stream: &IO,
header: &v2::Header<'_>,
) -> Result<usize, BoxError> {
if self.strictness.reject_unknown_address_family
&& matches!(header.addresses, v2::Addresses::Unspecified)
{
return Err(BoxError::from_static_str(
"haproxy v2: AF_UNSPEC rejected by strictness configuration",
));
}
if self.strictness.reject_local_command && header.command == v2::Command::Local {
return Err(BoxError::from_static_str(
"haproxy v2: LOCAL command rejected by strictness configuration",
));
}
let crc_status = header.verify_crc32c();
if self.strictness.verify_crc32c_when_present && crc_status == v2::Crc32cStatus::Invalid {
return Err(BoxError::from_static_str(
"haproxy v2: CRC32C TLV present but invalid",
));
}
if self.strictness.require_crc32c
&& !matches!(
crc_status,
v2::Crc32cStatus::Valid | v2::Crc32cStatus::Invalid
)
{
return Err(BoxError::from_static_str(
"haproxy v2: required CRC32C TLV missing",
));
}
let consumed_len = header.header.len();
let mut tlv_entries: Vec<HaProxyTlv> = Vec::new();
let mut tlv_malformed = false;
for tlv in header.tlvs() {
match tlv {
Ok(tlv) => tlv_entries.push(HaProxyTlv {
kind: tlv.kind,
value: Bytes::copy_from_slice(tlv.value.as_ref()),
}),
Err(e) => {
tracing::debug!(error = %e, "haproxy v2: malformed TLV in header");
tlv_malformed = true;
break;
}
}
}
if self.strictness.fail_on_malformed_tlv && tlv_malformed {
return Err(BoxError::from_static_str(
"haproxy v2: malformed TLV area rejected by strictness configuration",
));
}
if header.command == v2::Command::Proxy {
match header.addresses {
v2::Addresses::IPv4(info) => {
let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
insert_forwarded(stream, peer_addr);
}
v2::Addresses::IPv6(info) => {
let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
insert_forwarded(stream, peer_addr);
}
v2::Addresses::Unix(_) | v2::Addresses::Unspecified => (),
}
}
stream
.extensions()
.insert(HaProxyCommand::from(header.command));
if !tlv_entries.is_empty() {
stream.extensions().insert(HaProxyTlvs {
entries: tlv_entries,
});
}
Ok(consumed_len)
}
}
fn insert_forwarded<IO: ExtensionsRef>(stream: &IO, peer_addr: SocketAddr) {
let el = ForwardedElement::new_forwarded_for(peer_addr);
let forwarded = if let Some(mut forwarded) = stream.extensions().get_ref::<Forwarded>().cloned()
{
forwarded.append(el);
forwarded
} else {
Forwarded::new(el)
};
stream.extensions().insert(forwarded);
}
#[cfg(test)]
mod test {
use rama_core::{ServiceInput, service::service_fn};
use super::*;
async fn echo(mut stream: impl Io + Unpin) -> Result<Vec<u8>, BoxError> {
let mut v = Vec::default();
_ = stream.read_to_end(&mut v).await?;
Ok(v)
}
#[tokio::test]
async fn test_haproxy_peek_direct() {
let proxy_svc = HaProxyService::new(service_fn(echo)).with_peek(true);
let request = ServiceInput::new(std::io::Cursor::new(b"foo".to_vec()));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("foo", String::from_utf8(response).unwrap());
let request = ServiceInput::new(std::io::Cursor::new(
b"Hello, this is a test to check if it works.".to_vec(),
));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!(
"Hello, this is a test to check if it works.",
String::from_utf8(response).unwrap()
);
}
#[tokio::test]
async fn test_haproxy_peek_with_haproxy_v1() {
let proxy_svc = HaProxyService::new(service_fn(echo));
let request = ServiceInput::new(std::io::Cursor::new(
b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\n".to_vec(),
));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("", String::from_utf8(response).unwrap());
let request = ServiceInput::new(std::io::Cursor::new(
b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\nfoo".to_vec(),
));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("foo", String::from_utf8(response).unwrap());
let proxy_svc = proxy_svc.with_peek(true);
let request = ServiceInput::new(std::io::Cursor::new(
b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\n".to_vec(),
));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("", String::from_utf8(response).unwrap());
let request = ServiceInput::new(std::io::Cursor::new(
b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\nfoo".to_vec(),
));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("foo", String::from_utf8(response).unwrap());
}
#[tokio::test]
async fn test_haproxy_peek_with_haproxy_v2() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
0x0A, 0x21, 0x11, 0x00, 0x0C, 0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39, 0x01, 0xBB, 0x66, 0x6F, 0x6F,
];
let proxy_svc = HaProxyService::new(service_fn(echo));
let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("foo", String::from_utf8(response).unwrap());
let proxy_svc = proxy_svc.with_peek(true);
let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("foo", String::from_utf8(response).unwrap());
}
#[tokio::test]
async fn test_haproxy_peek_v2_fragmented() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, 0x21, 0x11, 0x00, 0x0C, 0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39,
0x01, 0xBB, b'b', b'a', b'r', ];
struct Chunked {
chunks: std::collections::VecDeque<Vec<u8>>,
}
impl tokio::io::AsyncRead for Chunked {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
if let Some(chunk) = self.chunks.pop_front() {
let n = chunk.len().min(buf.remaining());
buf.put_slice(&chunk[..n]);
if n < chunk.len() {
self.chunks.push_front(chunk[n..].to_vec());
}
}
std::task::Poll::Ready(Ok(()))
}
}
impl tokio::io::AsyncWrite for Chunked {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
std::task::Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
}
let chunked = Chunked {
chunks: vec![
DATA[..4].to_vec(),
DATA[4..12].to_vec(),
DATA[12..].to_vec(),
]
.into(),
};
let input = ServiceInput::new(chunked);
let proxy_svc = HaProxyService::new(service_fn(echo)).with_peek(true);
let response = proxy_svc.serve(input).await.unwrap();
assert_eq!("bar", String::from_utf8(response).unwrap());
}
#[tokio::test]
async fn test_haproxy_v2_local_command_strict_reject() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
0x0A, 0x20, 0x00, 0x00, 0x00, ];
let strictness = HaProxyStrictness {
reject_local_command: true,
..HaProxyStrictness::default()
};
let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
proxy_svc.serve(request).await.unwrap_err();
}
#[tokio::test]
async fn test_haproxy_v2_local_command_ignores_addresses() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
0x0A, 0x20, 0x11, 0x00, 0x0C, 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x30, 0x39, 0x01, 0xBB, b'h', b'i',
];
let cmd = capture_command(DATA).await;
let fwd = capture_forwarded(DATA).await;
assert_eq!(cmd, Some(HaProxyCommand::Local));
assert!(
fwd.is_none(),
"LOCAL command must not inject Forwarded element"
);
}
#[tokio::test]
async fn test_haproxy_v2_proxy_command_injects_forwarded() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
0x0A, 0x21, 0x11, 0x00, 0x0C, 0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39, 0x01, 0xBB, ];
let cmd = capture_command(DATA).await;
let fwd = capture_forwarded(DATA).await;
assert_eq!(cmd, Some(HaProxyCommand::Proxy));
assert!(fwd.is_some());
}
async fn capture_forwarded(data: &[u8]) -> Option<Forwarded> {
let captured: std::sync::Arc<parking_lot::Mutex<Option<Forwarded>>> =
std::sync::Arc::new(parking_lot::Mutex::new(None));
let captured_clone = captured.clone();
let inner = service_fn(move |stream| {
let captured = captured_clone.clone();
async move {
let fwd = <_ as ExtensionsRef>::extensions(&stream)
.get_ref::<Forwarded>()
.cloned();
*captured.lock() = fwd;
Ok::<_, BoxError>(())
}
});
let proxy_svc = HaProxyService::new(inner);
let request = ServiceInput::new(std::io::Cursor::new(data.to_vec()));
proxy_svc.serve(request).await.unwrap();
let guard = captured.lock();
guard.clone()
}
async fn capture_command(data: &[u8]) -> Option<HaProxyCommand> {
let captured: std::sync::Arc<parking_lot::Mutex<Option<HaProxyCommand>>> =
std::sync::Arc::new(parking_lot::Mutex::new(None));
let captured_clone = captured.clone();
let inner = service_fn(move |stream| {
let captured = captured_clone.clone();
async move {
let cmd = <_ as ExtensionsRef>::extensions(&stream)
.get_ref::<HaProxyCommand>()
.copied();
*captured.lock() = cmd;
Ok::<_, BoxError>(())
}
});
let proxy_svc = HaProxyService::new(inner);
let request = ServiceInput::new(std::io::Cursor::new(data.to_vec()));
proxy_svc.serve(request).await.unwrap();
let guard = captured.lock();
*guard
}
#[tokio::test]
async fn test_haproxy_v2_crc32c_invalid_is_rejected() {
let mut header = Vec::from(v2::PROTOCOL_PREFIX);
header.push(0x21); header.push(0x11); header.extend([0x00, 0x13]); header.extend([192, 0, 2, 1]);
header.extend([198, 51, 100, 1]);
header.extend([0x30, 0x39]);
header.extend([0x01, 0xBB]);
header.extend([0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]);
let proxy_svc = HaProxyService::new(service_fn(echo));
let request = ServiceInput::new(std::io::Cursor::new(header));
proxy_svc.serve(request).await.unwrap_err();
}
#[tokio::test]
async fn test_haproxy_v2_crc32c_valid_is_accepted() {
let mut header = Vec::from(v2::PROTOCOL_PREFIX);
header.push(0x21);
header.push(0x11);
header.extend([0x00, 0x13]); header.extend([192, 0, 2, 1]);
header.extend([198, 51, 100, 1]);
header.extend([0x30, 0x39]);
header.extend([0x01, 0xBB]);
header.extend([0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]);
let crc = {
let mut h = crate::protocol::v2::Crc32cHasher::new();
h.update(&header);
h.finalize()
};
let crc_value_offset = header.len() - 4;
header[crc_value_offset..crc_value_offset + 4].copy_from_slice(&crc.to_be_bytes());
header.extend(b"ok");
let proxy_svc = HaProxyService::new(service_fn(echo));
let request = ServiceInput::new(std::io::Cursor::new(header));
let response = proxy_svc.serve(request).await.unwrap();
assert_eq!("ok", String::from_utf8(response).unwrap());
}
#[tokio::test]
async fn test_haproxy_v2_require_crc32c() {
const DATA: &[u8] = &[
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
0x0A, 0x21, 0x11, 0x00, 0x0C, 0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39, 0x01, 0xBB,
];
let strictness = HaProxyStrictness {
require_crc32c: true,
..HaProxyStrictness::default()
};
let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
proxy_svc.serve(request).await.unwrap_err();
}
#[tokio::test]
async fn test_haproxy_v2_require_crc32c_with_malformed_before_crc_rejects() {
let mut header = Vec::from(v2::PROTOCOL_PREFIX);
header.push(0x21);
header.push(0x11);
header.extend([0x00, 0x10]); header.extend([192, 0, 2, 1]);
header.extend([198, 51, 100, 1]);
header.extend([0x30, 0x39]);
header.extend([0x01, 0xBB]);
header.extend([0x04, 0x00, 0x63, 0xAA]);
let strictness = HaProxyStrictness {
require_crc32c: true,
..HaProxyStrictness::default()
};
let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
let request = ServiceInput::new(std::io::Cursor::new(header));
let err = proxy_svc.serve(request).await.unwrap_err();
assert!(
err.to_string().contains("CRC32C"),
"expected CRC32C-missing-style error, got: {err}",
);
}
#[tokio::test]
async fn test_haproxy_v2_malformed_tlv_without_crc_is_accepted_by_default() {
let mut header = Vec::from(v2::PROTOCOL_PREFIX);
header.push(0x21);
header.push(0x11);
header.extend([0x00, 0x10]); header.extend([192, 0, 2, 1]);
header.extend([198, 51, 100, 1]);
header.extend([0x30, 0x39]);
header.extend([0x01, 0xBB]);
header.extend([0x04, 0x00, 0x63, 0xAA]);
let proxy_svc = HaProxyService::new(service_fn(echo));
let request = ServiceInput::new(std::io::Cursor::new(header.clone()));
proxy_svc.serve(request).await.expect("default accepts");
let strict_svc = HaProxyService::new(service_fn(echo)).with_strictness(HaProxyStrictness {
fail_on_malformed_tlv: true,
..HaProxyStrictness::default()
});
let request = ServiceInput::new(std::io::Cursor::new(header));
strict_svc.serve(request).await.unwrap_err();
}
}