Skip to main content

rama_haproxy/server/
layer.rs

1use crate::protocol::{HeaderResult, PartialResult, v1, v2};
2use rama_core::error::BoxErrorExt as _;
3use rama_core::{
4    Layer, Service,
5    bytes::Bytes,
6    error::{BoxError, ErrorContext, ErrorExt},
7    extensions::{Extension, ExtensionsRef},
8    io::{HeapReader, Io, PrefixedIo},
9    telemetry::tracing,
10};
11use rama_net::address::Domain;
12use rama_net::forwarded::{Forwarded, ForwardedElement};
13use rama_utils::macros::generate_set_and_with;
14use std::net::SocketAddr;
15use tokio::io::AsyncReadExt;
16
17/// Absolute upper bound on a single PROXY header: the v2 fixed header (16 bytes)
18/// plus the maximum advertised payload length (`u16::MAX`).
19///
20/// See vendored spec `rama-haproxy/specifications/proxy-protocol.txt`, section 2.2.
21const MAX_HEADER_LENGTH: usize = 16 + u16::MAX as usize;
22
23/// Initial buffer size used for reading the PROXY header. Sized to comfortably
24/// hold any v1 header (≤ 107 bytes) and a typical v2 header without TLVs in
25/// a single allocation.
26const INITIAL_BUFFER_CAPACITY: usize = 128;
27
28/// Configurable strictness toggles for the PROXY protocol server.
29///
30/// The defaults follow rama's proxy-first philosophy: be lenient with what is
31/// accepted from the wire (the protocol is regularly served by upstream
32/// software that diverges slightly from the spec) while still enforcing the
33/// security-critical invariants required by the specification (in particular,
34/// section 2.2.5: a present `PP2_TYPE_CRC32C` TLV "MUST be verified").
35///
36/// All knobs are opt-in towards stricter behaviour. [`Self::strict`] turns on
37/// every *spec-mandated* check; [`Self::reject_local_command`] is intentionally
38/// left off there because the spec explicitly permits the `LOCAL` command —
39/// flip it on per-deployment if you want to reject it.
40#[derive(Debug, Clone, Copy)]
41pub struct HaProxyStrictness {
42    /// Maximum number of bytes accepted for a single PROXY header.
43    /// Default: `16 + u16::MAX` — the largest spec-legal v2 header.
44    pub max_header_length: usize,
45    /// When `true` and a `PP2_TYPE_CRC32C` TLV is present, the header is
46    /// rejected if the CRC32C value does not match the recomputed value.
47    /// Default: `true` (spec MUST in section 2.2.5).
48    pub verify_crc32c_when_present: bool,
49    /// When `true`, reject any v2 header that does not carry a CRC32C TLV.
50    /// Default: `false`.
51    pub require_crc32c: bool,
52    /// When `true`, reject v1 `UNKNOWN` and v2 `AF_UNSPEC` headers.
53    /// Default: `false`.
54    pub reject_unknown_address_family: bool,
55    /// When `true`, reject v2 headers using the `LOCAL` command.
56    /// The spec mandates ignoring address info in `LOCAL` connections, which
57    /// rama always does — this flag goes one step further and treats a
58    /// `LOCAL` frame as an error rather than passing the connection through.
59    /// Default: `false`.
60    pub reject_local_command: bool,
61    /// When `true`, a v2 header is rejected if its TLV area cannot be parsed
62    /// in full (truncated TLV, advertised length beyond available bytes).
63    /// Default: `false`.
64    pub fail_on_malformed_tlv: bool,
65}
66
67impl HaProxyStrictness {
68    /// The default strictness configuration as a `const`, suitable for use
69    /// from other `const fn` constructors.
70    const DEFAULT: Self = Self {
71        max_header_length: MAX_HEADER_LENGTH,
72        verify_crc32c_when_present: true,
73        require_crc32c: false,
74        reject_unknown_address_family: false,
75        reject_local_command: false,
76        fail_on_malformed_tlv: false,
77    };
78}
79
80impl Default for HaProxyStrictness {
81    fn default() -> Self {
82        Self::DEFAULT
83    }
84}
85
86impl HaProxyStrictness {
87    /// A maximally lenient configuration — nothing is rejected beyond what the
88    /// parser itself can already not understand (invalid version byte etc.).
89    /// CRC32C verification is also disabled.
90    #[must_use]
91    pub const fn lenient() -> Self {
92        Self {
93            max_header_length: MAX_HEADER_LENGTH,
94            verify_crc32c_when_present: false,
95            require_crc32c: false,
96            reject_unknown_address_family: false,
97            reject_local_command: false,
98            fail_on_malformed_tlv: false,
99        }
100    }
101
102    /// A strict configuration suitable for trusted-frontline gateways that
103    /// want to enforce every spec-mandated check.
104    ///
105    /// Note: [`Self::reject_local_command`] is left **off** here because the
106    /// spec explicitly permits the `LOCAL` command (the receiver is required
107    /// to accept the connection and ignore the address info, which rama
108    /// always does). Enable it separately if your deployment wants to refuse
109    /// `LOCAL` frames.
110    #[must_use]
111    pub const fn strict() -> Self {
112        Self {
113            max_header_length: MAX_HEADER_LENGTH,
114            verify_crc32c_when_present: true,
115            require_crc32c: true,
116            reject_unknown_address_family: true,
117            reject_local_command: false,
118            fail_on_malformed_tlv: true,
119        }
120    }
121}
122
123/// A single TLV (Type-Length-Value) entry as carried in a PROXY protocol v2
124/// header.
125///
126/// `kind` is a typed [`v2::Type`] — unknown wire kinds are preserved as
127/// `Type::Unknown(u8)` so vendor-specific TLVs (e.g. AWS 0xEA, Azure 0xEE,
128/// GCP) are still inspectable.
129#[derive(Debug, Clone)]
130pub struct HaProxyTlv {
131    /// The TLV kind.
132    pub kind: v2::Type,
133    /// The TLV value.
134    pub value: Bytes,
135}
136
137/// A snapshot of the PROXY protocol v2 TLVs attached to the incoming
138/// connection, exposed as an extension on the IO stream so downstream
139/// services can inspect them.
140///
141/// Only present for v2 headers — v1 has no TLV concept. Typical PROXY v2
142/// senders attach 0–4 TLVs (CRC32C, NOOP padding, AUTHORITY/SNI, UNIQUE_ID,
143/// occasionally a vendor TLV); this type keeps them in iteration order.
144#[derive(Debug, Clone, Default, Extension)]
145#[extension(tags(proxy))]
146pub struct HaProxyTlvs {
147    entries: Vec<HaProxyTlv>,
148}
149
150impl HaProxyTlvs {
151    /// All TLV entries in iteration order.
152    #[must_use]
153    pub fn entries(&self) -> &[HaProxyTlv] {
154        &self.entries
155    }
156
157    /// Returns the value of the first TLV with the given kind, if any.
158    #[must_use]
159    pub fn get(&self, kind: v2::Type) -> Option<&Bytes> {
160        self.entries
161            .iter()
162            .find(|tlv| tlv.kind == kind)
163            .map(|tlv| &tlv.value)
164    }
165
166    /// Returns the value of the `PP2_TYPE_AUTHORITY` TLV (host name carried
167    /// through the proxy, e.g. SNI) parsed as a [`Domain`], when present and
168    /// well-formed.
169    ///
170    /// Per PROXY protocol spec section 2.2.5, this TLV "is typically passed
171    /// by the client to indicate the original host it was trying to connect
172    /// to" — i.e. a hostname. Returning a typed [`Domain`] both validates
173    /// the value and surfaces it in a form ready to be plugged into rama's
174    /// addressing/DNS plumbing.
175    ///
176    /// Use [`Self::get`] with `v2::Type::Authority` if you need the raw
177    /// bytes (e.g. to round-trip a non-domain value some non-spec sender
178    /// might have placed there).
179    #[must_use]
180    pub fn authority(&self) -> Option<Domain> {
181        self.get(v2::Type::Authority)
182            .and_then(|v| Domain::try_from(v.as_ref()).ok())
183    }
184
185    /// Returns the value of the `PP2_TYPE_UNIQUE_ID` TLV, when present.
186    #[must_use]
187    pub fn unique_id(&self) -> Option<&Bytes> {
188        self.get(v2::Type::UniqueId)
189    }
190}
191
192/// The PROXY protocol command extracted from a v2 header, exposed as an
193/// extension on the IO stream. Useful for services that want to distinguish
194/// `LOCAL` connections (e.g. upstream health checks) from real proxied flows.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Extension)]
196#[extension(tags(proxy))]
197pub enum HaProxyCommand {
198    /// `LOCAL` — the upstream proxy initiated this connection on its own
199    /// behalf. The spec requires the receiver to ignore the address info,
200    /// which rama does.
201    Local,
202    /// `PROXY` — the connection is being proxied for a remote client.
203    Proxy,
204}
205
206impl From<v2::Command> for HaProxyCommand {
207    fn from(c: v2::Command) -> Self {
208        match c {
209            v2::Command::Local => Self::Local,
210            v2::Command::Proxy => Self::Proxy,
211        }
212    }
213}
214
215/// Layer to decode the HaProxy Protocol
216#[derive(Debug, Default, Clone)]
217#[non_exhaustive]
218pub struct HaProxyLayer {
219    peek: bool,
220    strictness: HaProxyStrictness,
221}
222
223impl HaProxyLayer {
224    /// Create a new [`HaProxyLayer`].
225    #[must_use]
226    pub const fn new() -> Self {
227        Self {
228            peek: false,
229            strictness: HaProxyStrictness::DEFAULT,
230        }
231    }
232
233    generate_set_and_with!(
234        /// Instruct [`HaProxyLayer`] to peek prior to comitting to the `HaProxy` protocol.
235        ///
236        /// Doing so makes it possible to support traffic with or without the use of that data.
237        /// This can be useful to run services locally (not behind a loadbalancer) as well as in the
238        /// the cloud (production, behind a loadbalancer).
239        pub fn peek(mut self, value: bool) -> Self {
240            self.peek = value;
241            self
242        }
243    );
244
245    generate_set_and_with!(
246        /// Override the strictness configuration applied to incoming PROXY headers.
247        pub fn strictness(mut self, value: HaProxyStrictness) -> Self {
248            self.strictness = value;
249            self
250        }
251    );
252}
253
254impl<S> Layer<S> for HaProxyLayer {
255    type Service = HaProxyService<S>;
256
257    fn layer(&self, inner: S) -> Self::Service {
258        HaProxyService {
259            inner,
260            peek: self.peek,
261            strictness: self.strictness,
262        }
263    }
264}
265
266/// Service to decode the HaProxy Protocol
267///
268/// This service will decode the HaProxy Protocol header and pass the decoded
269/// information to the inner service.
270#[derive(Debug, Clone)]
271pub struct HaProxyService<S> {
272    inner: S,
273    peek: bool,
274    strictness: HaProxyStrictness,
275}
276
277impl<S> HaProxyService<S> {
278    /// Create a new [`HaProxyService`] with the given inner service.
279    pub const fn new(inner: S) -> Self {
280        Self {
281            inner,
282            peek: false,
283            strictness: HaProxyStrictness::DEFAULT,
284        }
285    }
286
287    generate_set_and_with!(
288        /// Instruct [`HaProxyService`] to peek prior to comitting to the `HaProxy` protocol.
289        ///
290        /// Doing so makes it possible to support traffic with or without the use of that data.
291        /// This can be useful to run services locally (not behind a loadbalancer) as well as in the
292        /// the cloud (production, behind a loadbalancer).
293        pub fn peek(mut self, value: bool) -> Self {
294            self.peek = value;
295            self
296        }
297    );
298
299    generate_set_and_with!(
300        /// Override the strictness configuration applied to incoming PROXY headers.
301        pub fn strictness(mut self, value: HaProxyStrictness) -> Self {
302            self.strictness = value;
303            self
304        }
305    );
306}
307
308/// Outcome of the peek phase: whether the upstream traffic looks like a
309/// PROXY header at all, and if so, which version.
310enum PeekOutcome {
311    /// Definitely a v1 header; bytes already consumed are in the buffer.
312    V1,
313    /// Definitely a v2 header; bytes already consumed are in the buffer.
314    V2,
315    /// Not a PROXY header. The buffered bytes should be replayed to the inner
316    /// service as-is.
317    NotProxy,
318}
319
320/// Performs the peek-mode disambiguation by reading enough bytes to decide
321/// whether the upstream traffic is v1, v2, or neither.
322///
323/// The previous implementation issued a single `read()` and compared the full
324/// 12-byte buffer to the v2 signature — but `read()` can return fewer bytes,
325/// causing legitimate fragmented PROXY headers to be misclassified as plain
326/// traffic and silently corrupted. This version reads incrementally until it
327/// can make a definitive decision.
328async fn peek_disambiguate<IO: tokio::io::AsyncRead + Unpin>(
329    stream: &mut IO,
330    buffer: &mut Vec<u8>,
331) -> Result<PeekOutcome, BoxError> {
332    // PROXY v1 prefix is "PROXY" (5 bytes); v2 prefix is 12 bytes starting
333    // with 0x0D 0x0A. The two prefixes differ at byte 0, so a single byte is
334    // enough to pick a candidate.
335    let v1_prefix = v1::PROTOCOL_PREFIX.as_bytes();
336    let v2_prefix = v2::PROTOCOL_PREFIX;
337
338    // Read at least one byte.
339    let mut scratch = [0u8; 16];
340    while buffer.is_empty() {
341        let n = stream
342            .read(&mut scratch)
343            .await
344            .context("haproxy peek: initial read")?;
345        if n == 0 {
346            // EOF before any bytes: nothing to commit to either protocol.
347            return Ok(PeekOutcome::NotProxy);
348        }
349        buffer.extend_from_slice(&scratch[..n]);
350    }
351
352    // First byte decides the candidate protocol.
353    let candidate_v1 = buffer[0] == v1_prefix[0];
354    let candidate_v2 = buffer[0] == v2_prefix[0];
355    if !candidate_v1 && !candidate_v2 {
356        return Ok(PeekOutcome::NotProxy);
357    }
358
359    let target_len = if candidate_v1 {
360        v1_prefix.len()
361    } else {
362        v2_prefix.len()
363    };
364
365    while buffer.len() < target_len {
366        let n = stream
367            .read(&mut scratch)
368            .await
369            .context("haproxy peek: prefix read")?;
370        if n == 0 {
371            // EOF before we could disambiguate: assume not PROXY and let the
372            // inner service deal with the bytes.
373            return Ok(PeekOutcome::NotProxy);
374        }
375        buffer.extend_from_slice(&scratch[..n]);
376
377        // Early bail: if the bytes seen so far already diverge from the
378        // candidate prefix, this is not a PROXY header.
379        let expected = if candidate_v1 { v1_prefix } else { v2_prefix };
380        let cmp_len = buffer.len().min(expected.len());
381        if buffer[..cmp_len] != expected[..cmp_len] {
382            return Ok(PeekOutcome::NotProxy);
383        }
384    }
385
386    if candidate_v1 && &buffer[..v1_prefix.len()] == v1_prefix {
387        Ok(PeekOutcome::V1)
388    } else if candidate_v2 && &buffer[..v2_prefix.len()] == v2_prefix {
389        Ok(PeekOutcome::V2)
390    } else {
391        Ok(PeekOutcome::NotProxy)
392    }
393}
394
395impl<S, IO> Service<IO> for HaProxyService<S>
396where
397    S: Service<PrefixedIo<HeapReader, IO>, Error: Into<BoxError>>,
398    IO: Io + Unpin + ExtensionsRef,
399{
400    type Output = S::Output;
401    type Error = BoxError;
402
403    async fn serve(&self, mut stream: IO) -> Result<Self::Output, Self::Error> {
404        let max_header_length = self
405            .strictness
406            .max_header_length
407            .min(MAX_HEADER_LENGTH)
408            .max(v1::PROTOCOL_PREFIX.len());
409
410        let mut buffer: Vec<u8> = Vec::with_capacity(INITIAL_BUFFER_CAPACITY);
411
412        if self.peek {
413            tracing::trace!("haproxy protocol peeking enabled: start detection");
414            match peek_disambiguate(&mut stream, &mut buffer).await? {
415                PeekOutcome::V1 => {
416                    tracing::trace!(
417                        "haproxy protocol peeked: v1 detected: continue with haproxy handling"
418                    );
419                }
420                PeekOutcome::V2 => {
421                    tracing::trace!(
422                        "haproxy protocol peeked: v2 detected: continue with haproxy handling"
423                    );
424                }
425                PeekOutcome::NotProxy => {
426                    tracing::trace!(
427                        "no haproxy protocol detected... delegating immediately to inner..."
428                    );
429                    let mem = HeapReader::new(buffer);
430                    let stream = PrefixedIo::new(mem, stream);
431                    return self.inner.serve(stream).await.into_box_error();
432                }
433            }
434        } else {
435            tracing::trace!("haproxy protocol enforced: skip peeking");
436        }
437
438        let header = loop {
439            // Always attempt to parse first — if the peek phase already left
440            // us with a complete header (small v1 case) this avoids a needless
441            // extra read.
442            let header = HeaderResult::parse(&buffer);
443            if header.is_complete() {
444                break header;
445            }
446
447            if buffer.len() >= max_header_length {
448                return Err(format!(
449                    "haproxy: buffer exhausted (read {} bytes, configured max {max_header_length}) before parsing completed",
450                    buffer.len()
451                )
452                .into());
453            }
454
455            // Grow the buffer geometrically up to the configured max.
456            let old_len = buffer.len();
457            let mut next_cap = old_len.saturating_mul(2).max(INITIAL_BUFFER_CAPACITY);
458            if next_cap > max_header_length {
459                next_cap = max_header_length;
460            }
461            buffer.resize(next_cap, 0);
462
463            let n = stream.read(&mut buffer[old_len..]).await?;
464            buffer.truncate(old_len + n);
465
466            if n == 0 {
467                return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)
468                    .context("HaProxy header incomplete"));
469            }
470
471            tracing::debug!("Incomplete header. Read {} bytes so far.", buffer.len());
472        };
473
474        let consumed = match header {
475            HeaderResult::V1(Ok(ref header)) => self.apply_v1(&stream, header)?,
476            HeaderResult::V2(Ok(ref header)) => self.apply_v2(&stream, header)?,
477            HeaderResult::V1(Err(error)) => return Err(error.into()),
478            HeaderResult::V2(Err(error)) => return Err(error.into()),
479        };
480
481        // put back the data that is read too much
482        let mem: HeapReader = buffer[consumed..].into();
483        let stream = PrefixedIo::new(mem, stream);
484
485        // read the rest of the data
486        self.inner.serve(stream).await.into_box_error()
487    }
488}
489
490impl<S> HaProxyService<S> {
491    fn apply_v1<IO: ExtensionsRef>(
492        &self,
493        stream: &IO,
494        header: &v1::Header<'_>,
495    ) -> Result<usize, BoxError> {
496        if self.strictness.reject_unknown_address_family
497            && matches!(header.addresses, v1::Addresses::Unknown)
498        {
499            return Err(BoxError::from_static_str(
500                "haproxy v1: UNKNOWN address family rejected by strictness configuration",
501            ));
502        }
503
504        match header.addresses {
505            v1::Addresses::Tcp4(info) => {
506                let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
507                insert_forwarded(stream, peer_addr);
508            }
509            v1::Addresses::Tcp6(info) => {
510                let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
511                insert_forwarded(stream, peer_addr);
512            }
513            v1::Addresses::Unknown => (),
514        }
515
516        Ok(header.header.len())
517    }
518
519    fn apply_v2<IO: ExtensionsRef>(
520        &self,
521        stream: &IO,
522        header: &v2::Header<'_>,
523    ) -> Result<usize, BoxError> {
524        if self.strictness.reject_unknown_address_family
525            && matches!(header.addresses, v2::Addresses::Unspecified)
526        {
527            return Err(BoxError::from_static_str(
528                "haproxy v2: AF_UNSPEC rejected by strictness configuration",
529            ));
530        }
531
532        if self.strictness.reject_local_command && header.command == v2::Command::Local {
533            return Err(BoxError::from_static_str(
534                "haproxy v2: LOCAL command rejected by strictness configuration",
535            ));
536        }
537
538        // CRC32C verification — section 2.2.5 of the spec mandates that a
539        // present `PP2_TYPE_CRC32C` value MUST be verified.
540        //
541        // Note: `MalformedBeforeCrc` means we couldn't decide whether a CRC
542        // TLV exists at all, so we must not treat it as "CRC invalid". That
543        // case is handled by `fail_on_malformed_tlv` further below — keeping
544        // the two concerns orthogonal.
545        let crc_status = header.verify_crc32c();
546        if self.strictness.verify_crc32c_when_present && crc_status == v2::Crc32cStatus::Invalid {
547            return Err(BoxError::from_static_str(
548                "haproxy v2: CRC32C TLV present but invalid",
549            ));
550        }
551        if self.strictness.require_crc32c
552            && !matches!(
553                crc_status,
554                v2::Crc32cStatus::Valid | v2::Crc32cStatus::Invalid
555            )
556        {
557            return Err(BoxError::from_static_str(
558                "haproxy v2: required CRC32C TLV missing",
559            ));
560        }
561
562        // Collect TLVs into an extension before consuming them.
563        let consumed_len = header.header.len();
564        let mut tlv_entries: Vec<HaProxyTlv> = Vec::new();
565        let mut tlv_malformed = false;
566        for tlv in header.tlvs() {
567            match tlv {
568                Ok(tlv) => tlv_entries.push(HaProxyTlv {
569                    kind: tlv.kind,
570                    value: Bytes::copy_from_slice(tlv.value.as_ref()),
571                }),
572                Err(e) => {
573                    tracing::debug!(error = %e, "haproxy v2: malformed TLV in header");
574                    tlv_malformed = true;
575                    break;
576                }
577            }
578        }
579
580        if self.strictness.fail_on_malformed_tlv && tlv_malformed {
581            return Err(BoxError::from_static_str(
582                "haproxy v2: malformed TLV area rejected by strictness configuration",
583            ));
584        }
585
586        // Spec section 2.2: the receiver MUST ignore any address information
587        // when the command is LOCAL. We honour this by not injecting a
588        // `Forwarded` element at all in that case.
589        if header.command == v2::Command::Proxy {
590            match header.addresses {
591                v2::Addresses::IPv4(info) => {
592                    let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
593                    insert_forwarded(stream, peer_addr);
594                }
595                v2::Addresses::IPv6(info) => {
596                    let peer_addr: SocketAddr = (info.source_address, info.source_port).into();
597                    insert_forwarded(stream, peer_addr);
598                }
599                v2::Addresses::Unix(_) | v2::Addresses::Unspecified => (),
600            }
601        }
602
603        // Expose command + TLVs to downstream services.
604        stream
605            .extensions()
606            .insert(HaProxyCommand::from(header.command));
607        if !tlv_entries.is_empty() {
608            stream.extensions().insert(HaProxyTlvs {
609                entries: tlv_entries,
610            });
611        }
612
613        Ok(consumed_len)
614    }
615}
616
617fn insert_forwarded<IO: ExtensionsRef>(stream: &IO, peer_addr: SocketAddr) {
618    let el = ForwardedElement::new_forwarded_for(peer_addr);
619    let forwarded = if let Some(mut forwarded) = stream.extensions().get_ref::<Forwarded>().cloned()
620    {
621        forwarded.append(el);
622        forwarded
623    } else {
624        Forwarded::new(el)
625    };
626    stream.extensions().insert(forwarded);
627}
628
629#[cfg(test)]
630mod test {
631    use rama_core::{ServiceInput, service::service_fn};
632
633    use super::*;
634
635    async fn echo(mut stream: impl Io + Unpin) -> Result<Vec<u8>, BoxError> {
636        let mut v = Vec::default();
637        _ = stream.read_to_end(&mut v).await?;
638        Ok(v)
639    }
640
641    #[tokio::test]
642    async fn test_haproxy_peek_direct() {
643        let proxy_svc = HaProxyService::new(service_fn(echo)).with_peek(true);
644
645        let request = ServiceInput::new(std::io::Cursor::new(b"foo".to_vec()));
646        let response = proxy_svc.serve(request).await.unwrap();
647
648        assert_eq!("foo", String::from_utf8(response).unwrap());
649
650        let request = ServiceInput::new(std::io::Cursor::new(
651            b"Hello, this is a test to check if it works.".to_vec(),
652        ));
653        let response = proxy_svc.serve(request).await.unwrap();
654
655        assert_eq!(
656            "Hello, this is a test to check if it works.",
657            String::from_utf8(response).unwrap()
658        );
659    }
660
661    #[tokio::test]
662    async fn test_haproxy_peek_with_haproxy_v1() {
663        let proxy_svc = HaProxyService::new(service_fn(echo));
664
665        let request = ServiceInput::new(std::io::Cursor::new(
666            b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\n".to_vec(),
667        ));
668        let response = proxy_svc.serve(request).await.unwrap();
669
670        assert_eq!("", String::from_utf8(response).unwrap());
671
672        let request = ServiceInput::new(std::io::Cursor::new(
673            b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\nfoo".to_vec(),
674        ));
675        let response = proxy_svc.serve(request).await.unwrap();
676
677        assert_eq!("foo", String::from_utf8(response).unwrap());
678
679        let proxy_svc = proxy_svc.with_peek(true);
680
681        let request = ServiceInput::new(std::io::Cursor::new(
682            b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\n".to_vec(),
683        ));
684        let response = proxy_svc.serve(request).await.unwrap();
685
686        assert_eq!("", String::from_utf8(response).unwrap());
687
688        let request = ServiceInput::new(std::io::Cursor::new(
689            b"PROXY TCP4 192.0.2.1 198.51.100.1 12345 80\r\nfoo".to_vec(),
690        ));
691        let response = proxy_svc.serve(request).await.unwrap();
692
693        assert_eq!("foo", String::from_utf8(response).unwrap());
694    }
695
696    #[tokio::test]
697    async fn test_haproxy_peek_with_haproxy_v2() {
698        const DATA: &[u8] = &[
699            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
700            0x0A, // Signature
701            0x21, // Version (0x2) + Command (PROXY = 0x1)
702            0x11, // Family (IPv4 = 0x1) + Protocol (TCP = 0x1)
703            0x00, 0x0C, // Address length = 12 bytes
704            // Source IP: 192.0.2.1
705            0xC0, 0x00, 0x02, 0x01, // Dest IP: 198.51.100.1
706            0xC6, 0x33, 0x64, 0x01, // Source Port: 12345 (0x3039)
707            0x30, 0x39, // Dest Port: 443 (0x01BB)
708            0x01, 0xBB, // foo data
709            0x66, 0x6F, 0x6F,
710        ];
711
712        let proxy_svc = HaProxyService::new(service_fn(echo));
713        let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
714        let response = proxy_svc.serve(request).await.unwrap();
715        assert_eq!("foo", String::from_utf8(response).unwrap());
716
717        let proxy_svc = proxy_svc.with_peek(true);
718        let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
719        let response = proxy_svc.serve(request).await.unwrap();
720        assert_eq!("foo", String::from_utf8(response).unwrap());
721    }
722
723    /// Regression test for the peek-mode short-read bug: the v2 prefix and
724    /// payload arrive in multiple `read()` calls. Previously the peek logic
725    /// compared a partially-filled buffer to the full 12-byte v2 signature,
726    /// failed to match, and silently forwarded the bytes to the inner service.
727    #[tokio::test]
728    async fn test_haproxy_peek_v2_fragmented() {
729        const DATA: &[u8] = &[
730            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, // sig
731            0x21, 0x11, 0x00, 0x0C, 0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39,
732            0x01, 0xBB, // header
733            b'b', b'a', b'r', // payload
734        ];
735
736        struct Chunked {
737            chunks: std::collections::VecDeque<Vec<u8>>,
738        }
739        impl tokio::io::AsyncRead for Chunked {
740            fn poll_read(
741                mut self: std::pin::Pin<&mut Self>,
742                _cx: &mut std::task::Context<'_>,
743                buf: &mut tokio::io::ReadBuf<'_>,
744            ) -> std::task::Poll<std::io::Result<()>> {
745                if let Some(chunk) = self.chunks.pop_front() {
746                    let n = chunk.len().min(buf.remaining());
747                    buf.put_slice(&chunk[..n]);
748                    if n < chunk.len() {
749                        self.chunks.push_front(chunk[n..].to_vec());
750                    }
751                }
752                std::task::Poll::Ready(Ok(()))
753            }
754        }
755        impl tokio::io::AsyncWrite for Chunked {
756            fn poll_write(
757                self: std::pin::Pin<&mut Self>,
758                _cx: &mut std::task::Context<'_>,
759                buf: &[u8],
760            ) -> std::task::Poll<std::io::Result<usize>> {
761                std::task::Poll::Ready(Ok(buf.len()))
762            }
763            fn poll_flush(
764                self: std::pin::Pin<&mut Self>,
765                _cx: &mut std::task::Context<'_>,
766            ) -> std::task::Poll<std::io::Result<()>> {
767                std::task::Poll::Ready(Ok(()))
768            }
769            fn poll_shutdown(
770                self: std::pin::Pin<&mut Self>,
771                _cx: &mut std::task::Context<'_>,
772            ) -> std::task::Poll<std::io::Result<()>> {
773                std::task::Poll::Ready(Ok(()))
774            }
775        }
776
777        let chunked = Chunked {
778            chunks: vec![
779                DATA[..4].to_vec(),
780                DATA[4..12].to_vec(),
781                DATA[12..].to_vec(),
782            ]
783            .into(),
784        };
785
786        let input = ServiceInput::new(chunked);
787        let proxy_svc = HaProxyService::new(service_fn(echo)).with_peek(true);
788        let response = proxy_svc.serve(input).await.unwrap();
789        assert_eq!("bar", String::from_utf8(response).unwrap());
790    }
791
792    /// Strict mode: `LOCAL` command is rejected.
793    #[tokio::test]
794    async fn test_haproxy_v2_local_command_strict_reject() {
795        const DATA: &[u8] = &[
796            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
797            0x0A, // Signature
798            0x20, // Version (0x2) + Command (LOCAL = 0x0)
799            0x00, // AF_UNSPEC + UNSPEC
800            0x00, 0x00, // length 0
801        ];
802        let strictness = HaProxyStrictness {
803            reject_local_command: true,
804            ..HaProxyStrictness::default()
805        };
806        let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
807        let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
808        proxy_svc.serve(request).await.unwrap_err();
809    }
810
811    /// Default (lenient) mode: a `LOCAL` command must NOT inject a
812    /// `Forwarded` element, even when address bytes are technically present
813    /// — spec section 2.2 requires the receiver to ignore them.
814    #[tokio::test]
815    async fn test_haproxy_v2_local_command_ignores_addresses() {
816        // LOCAL command, but with IPv4 address info present that an attacker
817        // could try to spoof a Forwarded for. The spec says ignore it.
818        const DATA: &[u8] = &[
819            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
820            0x0A, // Signature
821            0x20, // Version (0x2) + Command (LOCAL = 0x0)
822            0x11, // IPv4 + TCP
823            0x00, 0x0C, // length 12
824            0xDE, 0xAD, 0xBE, 0xEF, // src "ip" (must be ignored)
825            0xCA, 0xFE, 0xBA, 0xBE, // dst "ip"
826            0x30, 0x39, 0x01, 0xBB, // ports
827            b'h', b'i',
828        ];
829
830        let cmd = capture_command(DATA).await;
831        let fwd = capture_forwarded(DATA).await;
832        assert_eq!(cmd, Some(HaProxyCommand::Local));
833        assert!(
834            fwd.is_none(),
835            "LOCAL command must not inject Forwarded element"
836        );
837    }
838
839    /// PROXY command with IPv4 addresses DOES inject the Forwarded element.
840    #[tokio::test]
841    async fn test_haproxy_v2_proxy_command_injects_forwarded() {
842        const DATA: &[u8] = &[
843            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
844            0x0A, // Signature
845            0x21, // Version + PROXY
846            0x11, // IPv4 + TCP
847            0x00, 0x0C, // length 12
848            0xC0, 0x00, 0x02, 0x01, // 192.0.2.1
849            0xC6, 0x33, 0x64, 0x01, // 198.51.100.1
850            0x30, 0x39, 0x01, 0xBB, // ports
851        ];
852
853        let cmd = capture_command(DATA).await;
854        let fwd = capture_forwarded(DATA).await;
855        assert_eq!(cmd, Some(HaProxyCommand::Proxy));
856        assert!(fwd.is_some());
857    }
858
859    /// Helper: run a haproxy service against `data` and return the
860    /// `Forwarded` extension seen by the inner service.
861    async fn capture_forwarded(data: &[u8]) -> Option<Forwarded> {
862        let captured: std::sync::Arc<parking_lot::Mutex<Option<Forwarded>>> =
863            std::sync::Arc::new(parking_lot::Mutex::new(None));
864        let captured_clone = captured.clone();
865        let inner = service_fn(move |stream| {
866            let captured = captured_clone.clone();
867            async move {
868                let fwd = <_ as ExtensionsRef>::extensions(&stream)
869                    .get_ref::<Forwarded>()
870                    .cloned();
871                *captured.lock() = fwd;
872                Ok::<_, BoxError>(())
873            }
874        });
875
876        let proxy_svc = HaProxyService::new(inner);
877        let request = ServiceInput::new(std::io::Cursor::new(data.to_vec()));
878        proxy_svc.serve(request).await.unwrap();
879        let guard = captured.lock();
880        guard.clone()
881    }
882
883    /// Helper: run a haproxy service against `data` and return the
884    /// `HaProxyCommand` extension seen by the inner service.
885    async fn capture_command(data: &[u8]) -> Option<HaProxyCommand> {
886        let captured: std::sync::Arc<parking_lot::Mutex<Option<HaProxyCommand>>> =
887            std::sync::Arc::new(parking_lot::Mutex::new(None));
888        let captured_clone = captured.clone();
889        let inner = service_fn(move |stream| {
890            let captured = captured_clone.clone();
891            async move {
892                let cmd = <_ as ExtensionsRef>::extensions(&stream)
893                    .get_ref::<HaProxyCommand>()
894                    .copied();
895                *captured.lock() = cmd;
896                Ok::<_, BoxError>(())
897            }
898        });
899
900        let proxy_svc = HaProxyService::new(inner);
901        let request = ServiceInput::new(std::io::Cursor::new(data.to_vec()));
902        proxy_svc.serve(request).await.unwrap();
903        let guard = captured.lock();
904        *guard
905    }
906
907    /// CRC32C TLV with an invalid value must be rejected by default.
908    #[tokio::test]
909    async fn test_haproxy_v2_crc32c_invalid_is_rejected() {
910        // Build a v2 header that carries a CRC32C TLV with bogus contents.
911        let mut header = Vec::from(v2::PROTOCOL_PREFIX);
912        header.push(0x21); // version + proxy
913        header.push(0x11); // ipv4 + tcp
914        header.extend([0x00, 0x13]); // length = 12 + 7 (TLV)
915        header.extend([192, 0, 2, 1]);
916        header.extend([198, 51, 100, 1]);
917        header.extend([0x30, 0x39]);
918        header.extend([0x01, 0xBB]);
919        // CRC32C TLV: kind=0x03, length=4, value=00000000 (wrong)
920        header.extend([0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]);
921
922        let proxy_svc = HaProxyService::new(service_fn(echo));
923        let request = ServiceInput::new(std::io::Cursor::new(header));
924        proxy_svc.serve(request).await.unwrap_err();
925    }
926
927    /// CRC32C TLV with a correct value passes verification.
928    #[tokio::test]
929    async fn test_haproxy_v2_crc32c_valid_is_accepted() {
930        let mut header = Vec::from(v2::PROTOCOL_PREFIX);
931        header.push(0x21);
932        header.push(0x11);
933        header.extend([0x00, 0x13]); // 12 + 7
934        header.extend([192, 0, 2, 1]);
935        header.extend([198, 51, 100, 1]);
936        header.extend([0x30, 0x39]);
937        header.extend([0x01, 0xBB]);
938        // CRC32C placeholder
939        header.extend([0x03, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00]);
940        // Compute the expected CRC over the header with the CRC value zeroed
941        // (already the case since we wrote zeros) and patch it in.
942        let crc = {
943            let mut h = crate::protocol::v2::Crc32cHasher::new();
944            h.update(&header);
945            h.finalize()
946        };
947        let crc_value_offset = header.len() - 4;
948        header[crc_value_offset..crc_value_offset + 4].copy_from_slice(&crc.to_be_bytes());
949
950        // Append payload data after the header.
951        header.extend(b"ok");
952
953        let proxy_svc = HaProxyService::new(service_fn(echo));
954        let request = ServiceInput::new(std::io::Cursor::new(header));
955        let response = proxy_svc.serve(request).await.unwrap();
956        assert_eq!("ok", String::from_utf8(response).unwrap());
957    }
958
959    /// Strict mode demands a CRC32C TLV: a header without one is rejected.
960    #[tokio::test]
961    async fn test_haproxy_v2_require_crc32c() {
962        const DATA: &[u8] = &[
963            0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54,
964            0x0A, // Signature
965            0x21, 0x11, 0x00, 0x0C, // header
966            0xC0, 0x00, 0x02, 0x01, 0xC6, 0x33, 0x64, 0x01, 0x30, 0x39, 0x01, 0xBB,
967        ];
968        let strictness = HaProxyStrictness {
969            require_crc32c: true,
970            ..HaProxyStrictness::default()
971        };
972        let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
973        let request = ServiceInput::new(std::io::Cursor::new(DATA.to_vec()));
974        proxy_svc.serve(request).await.unwrap_err();
975    }
976
977    /// Regression: with `require_crc32c=true`, a header whose TLV stream is
978    /// malformed before reaching a CRC32C TLV must be rejected as
979    /// "CRC missing" (we couldn't confirm a CRC TLV is present — fail safe).
980    #[tokio::test]
981    async fn test_haproxy_v2_require_crc32c_with_malformed_before_crc_rejects() {
982        // 12-byte addresses + a truncated TLV (length=99, only 1 byte of
983        // value follows). No CRC32C TLV anywhere.
984        let mut header = Vec::from(v2::PROTOCOL_PREFIX);
985        header.push(0x21);
986        header.push(0x11);
987        header.extend([0x00, 0x10]); // 12 addresses + 4 truncated TLV bytes
988        header.extend([192, 0, 2, 1]);
989        header.extend([198, 51, 100, 1]);
990        header.extend([0x30, 0x39]);
991        header.extend([0x01, 0xBB]);
992        header.extend([0x04, 0x00, 0x63, 0xAA]); // NoOp TLV claiming length=99
993
994        let strictness = HaProxyStrictness {
995            require_crc32c: true,
996            ..HaProxyStrictness::default()
997        };
998        let proxy_svc = HaProxyService::new(service_fn(echo)).with_strictness(strictness);
999        let request = ServiceInput::new(std::io::Cursor::new(header));
1000        let err = proxy_svc.serve(request).await.unwrap_err();
1001        assert!(
1002            err.to_string().contains("CRC32C"),
1003            "expected CRC32C-missing-style error, got: {err}",
1004        );
1005    }
1006
1007    /// Regression: a header whose TLV area is malformed but does NOT carry a
1008    /// CRC32C TLV must NOT be rejected as "CRC invalid" by the default
1009    /// `verify_crc32c_when_present=true` strictness.
1010    ///
1011    /// `fail_on_malformed_tlv=false` (lenient default) → accept.
1012    /// `fail_on_malformed_tlv=true` → reject, but with a malformed-TLV error.
1013    #[tokio::test]
1014    async fn test_haproxy_v2_malformed_tlv_without_crc_is_accepted_by_default() {
1015        // 12-byte addresses + a truncated TLV (length=99 but only 1 byte
1016        // available afterwards). No CRC32C TLV.
1017        let mut header = Vec::from(v2::PROTOCOL_PREFIX);
1018        header.push(0x21);
1019        header.push(0x11);
1020        header.extend([0x00, 0x10]); // payload length = 16 (12 + 4 truncated TLV bytes)
1021        header.extend([192, 0, 2, 1]);
1022        header.extend([198, 51, 100, 1]);
1023        header.extend([0x30, 0x39]);
1024        header.extend([0x01, 0xBB]);
1025        // TLV: kind=0x04 (NoOp), length=99, but only 1 byte of value follows.
1026        header.extend([0x04, 0x00, 0x63, 0xAA]);
1027
1028        // Default strictness accepts (CRC verification is not tricked into
1029        // returning "invalid" by a malformed TLV that isn't a CRC TLV).
1030        let proxy_svc = HaProxyService::new(service_fn(echo));
1031        let request = ServiceInput::new(std::io::Cursor::new(header.clone()));
1032        proxy_svc.serve(request).await.expect("default accepts");
1033
1034        // Opting into strict TLV parsing rejects the same input.
1035        let strict_svc = HaProxyService::new(service_fn(echo)).with_strictness(HaProxyStrictness {
1036            fail_on_malformed_tlv: true,
1037            ..HaProxyStrictness::default()
1038        });
1039        let request = ServiceInput::new(std::io::Cursor::new(header));
1040        strict_svc.serve(request).await.unwrap_err();
1041    }
1042}