proxy_protocol_codec/
v1.rs

1//! PROXY Protocol v1.
2
3pub mod model;
4
5#[cfg(feature = "feat-codec-encode")]
6use alloc::string::{String, ToString};
7use core::{fmt, str};
8
9pub use model::AddressPair;
10#[cfg(feature = "feat-codec-decode")]
11pub use model::Decoded;
12
13/// The maximum length of a PROXY Protocol v1 header, including the magic bytes,
14/// protocol, addresses, ports, and the CRLF at the end.
15pub const MAXIMUM_LENGTH: usize = 107;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18/// The PROXY Protocol v1 header.
19pub struct Header {
20    /// The address family and protocol used in the PROXY Protocol v1 header.
21    protocol: FamProto,
22
23    /// The source address in the PROXY Protocol v1 header.
24    address_pair: AddressPair,
25}
26
27#[cfg(feature = "feat-codec-encode")]
28impl fmt::Display for Header {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self.address_pair {
31            AddressPair::Unspecified => write!(f, "{} {FAM_PROTO_UNKNOWN}\r\n", Self::MAGIC),
32            AddressPair::Inet {
33                src_ip,
34                dst_ip,
35                src_port,
36                dst_port,
37            } => write!(
38                f,
39                "{} {FAM_PROTO_TCP4} {src_ip} {dst_ip} {src_port} {dst_port}\r\n",
40                Self::MAGIC
41            ),
42            AddressPair::Inet6 {
43                src_ip,
44                dst_ip,
45                src_port,
46                dst_port,
47            } => write!(
48                f,
49                "{} {FAM_PROTO_TCP6} {src_ip} {dst_ip} {src_port} {dst_port}\r\n",
50                Self::MAGIC
51            ),
52        }
53    }
54}
55
56impl Header {
57    /// PROXY Protocol v1 magic bytes.
58    pub const MAGIC: &'static str = "PROXY";
59
60    /// Creates a new PROXY Protocol v1 header.
61    pub const fn new(address_pair: AddressPair) -> Self {
62        Header {
63            protocol: match &address_pair {
64                AddressPair::Inet { .. } => FamProto::TCP4,
65                AddressPair::Inet6 { .. } => FamProto::TCP6,
66                AddressPair::Unspecified => FamProto::Unknown,
67            },
68            address_pair,
69        }
70    }
71
72    #[inline]
73    /// Returns the address pair of the PROXY Protocol v1 header.
74    pub const fn address_pair(self) -> AddressPair {
75        self.address_pair
76    }
77
78    #[inline]
79    #[cfg(feature = "feat-codec-encode")]
80    /// Encodes the PROXY Protocol v1 header into a string representation.
81    pub fn encode(&self) -> String {
82        self.to_string()
83    }
84
85    #[cfg(feature = "feat-codec-decode")]
86    /// Try to decode the PROXY Protocol v1 header from the given buffer.
87    ///
88    /// The caller SHOULD first **peek** exactly **5** bytes from the network
89    /// input into a buffer and [`decode`](Self::decode) it, to detect the
90    /// presence of a PROXY Protocol v1 header.
91    ///
92    /// When the buffer is not prefixed with PROXY Protocol v1 header
93    /// [`MAGIC`](Header::MAGIC), this method returns [`Decoded::None`]. The
94    /// caller MAY reject the connection, or treat the connection as a
95    /// normal one w/o PROXY Protocol v1 header.
96    ///
97    /// When a PROXY protocol v1 header is detected, [`Decoded::Partial`] is
98    /// returned (this is what we expect, since we only have the MAGIC bytes
99    /// peeked). The caller SHOULD read from network input into a buffer (may
100    /// reuse the buffer peeking the MAGIC bytes) until the first `\n`, then
101    /// [`decode`](Self::decode) it. This method will reject bytes w/o CRLF, and
102    /// any trailing bytes after CRLF.
103    ///
104    /// The caller MAY ensure that the length of the `header_bytes` does not
105    /// exceed the [`MAXIMUM_LENGTH`].
106    ///
107    /// When any error is returned, the caller SHOULD reject the connection.
108    pub fn decode(header_bytes: &[u8]) -> Result<Decoded, DecodeError> {
109        // 1. Magic bytes flight check
110        {
111            use core::cmp::min;
112
113            let magic_length = min(Header::MAGIC.len(), header_bytes.len());
114
115            if header_bytes[..magic_length] != Header::MAGIC.as_bytes()[..magic_length] {
116                return Ok(Decoded::None);
117            }
118        }
119
120        if header_bytes.len() > MAXIMUM_LENGTH {
121            // Too long, will never be a valid PROXY Protocol v1 header.
122            return Err(DecodeError::MalformedData("bytes too long"));
123        }
124
125        let header_str = str::from_utf8(header_bytes).map_err(|_| DecodeError::MalformedData("not UTF-8"))?;
126
127        // CRLF must be at the end of the header.
128        if !header_str.ends_with("\r\n") {
129            return Err(DecodeError::MalformedData("missing CRLF or trailing data"));
130        }
131
132        let mut header_parts_iter = header_str.split_whitespace();
133
134        // 1. Magic bytes
135        let magic = header_parts_iter
136            .next()
137            .ok_or(DecodeError::MalformedData("missing MAGIC"))?;
138
139        if magic != Header::MAGIC {
140            return Ok(Decoded::None);
141        }
142
143        // 2. Check the FamProto
144        let Some(family_protocol) = header_parts_iter.next() else {
145            return Err(DecodeError::InvalidFamProto);
146        };
147
148        // 3. Check the address family and protocol.
149        let (protocol, address_pair) = match family_protocol {
150            FAM_PROTO_TCP4 => {
151                let src_ip = header_parts_iter
152                    .next()
153                    .ok_or(DecodeError::MissingData("SRC_IP"))
154                    .and_then(|s| s.parse().map_err(|_| DecodeError::MalformedData("SRC_IP")))?;
155
156                let dst_ip = header_parts_iter
157                    .next()
158                    .ok_or(DecodeError::MissingData("DST_IP"))
159                    .and_then(|s| s.parse().map_err(|_| DecodeError::MalformedData("DST_IP")))?;
160
161                let src_port = header_parts_iter
162                    .next()
163                    .ok_or(DecodeError::MissingData("SRC_PORT"))
164                    .and_then(|s| s.parse::<u16>().map_err(|_| DecodeError::MalformedData("SRC_PORT")))?;
165
166                let dst_port = header_parts_iter
167                    .next()
168                    .ok_or(DecodeError::MissingData("DST_PORT"))
169                    .and_then(|s| s.parse::<u16>().map_err(|_| DecodeError::MalformedData("DST_PORT")))?;
170
171                (
172                    FamProto::TCP4,
173                    AddressPair::Inet {
174                        src_ip,
175                        dst_ip,
176                        src_port,
177                        dst_port,
178                    },
179                )
180            }
181            FAM_PROTO_TCP6 => {
182                let src_ip = header_parts_iter
183                    .next()
184                    .ok_or(DecodeError::MissingData("SRC_IP"))
185                    .and_then(|s| s.parse().map_err(|_| DecodeError::MalformedData("SRC_IP")))?;
186
187                let dst_ip = header_parts_iter
188                    .next()
189                    .ok_or(DecodeError::MissingData("DST_IP"))
190                    .and_then(|s| s.parse().map_err(|_| DecodeError::MalformedData("DST_IP")))?;
191
192                let src_port = header_parts_iter
193                    .next()
194                    .ok_or(DecodeError::MissingData("SRC_PORT"))
195                    .and_then(|s| s.parse::<u16>().map_err(|_| DecodeError::MalformedData("SRC_PORT")))?;
196
197                let dst_port = header_parts_iter
198                    .next()
199                    .ok_or(DecodeError::MissingData("DST_PORT"))
200                    .and_then(|s| s.parse::<u16>().map_err(|_| DecodeError::MalformedData("DST_PORT")))?;
201
202                (
203                    FamProto::TCP6,
204                    AddressPair::Inet6 {
205                        src_ip,
206                        dst_ip,
207                        src_port,
208                        dst_port,
209                    },
210                )
211            }
212            FAM_PROTO_UNKNOWN => {
213                // Ignore anything presented before the CRLF
214                (FamProto::Unknown, AddressPair::Unspecified)
215            }
216            _ => {
217                return Err(DecodeError::InvalidFamProto);
218            }
219        };
220
221        Ok(Decoded::Some(Self { protocol, address_pair }))
222    }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226/// The address family and the transport protocol used in the PROXY Protocol v1
227/// header.
228enum FamProto {
229    /// Unspecified address family and protocol.
230    Unknown,
231
232    /// TCP protocol, IPv4.
233    TCP4,
234
235    /// TCP protocol, IPv6.
236    TCP6,
237}
238
239#[cfg(any(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
240const FAM_PROTO_UNKNOWN: &str = "UNKNOWN";
241
242#[cfg(any(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
243const FAM_PROTO_TCP4: &str = "TCP4";
244
245#[cfg(any(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
246const FAM_PROTO_TCP6: &str = "TCP6";
247
248#[cfg(feature = "feat-codec-decode")]
249#[derive(Debug)]
250#[derive(thiserror::Error)]
251/// Errors that can occur while decoding a PROXY Protocol v1 header.
252pub enum DecodeError {
253    #[error("Invalid PROXY addr family & protocol")]
254    /// Invalid PROXY addr family & protocol
255    InvalidFamProto,
256
257    #[error("Missing data: {0}")]
258    /// Invalid PROXY Protocol command
259    MissingData(&'static str),
260
261    #[error("Malformed data: {0}")]
262    /// The data is malformed, e.g. the length of an extension does not match
263    /// the actual data length.
264    MalformedData(&'static str),
265
266    #[error("Trailing data after the PROXY Protocol v1 header")]
267    /// The buffer contains trailing data after the PROXY Protocol v1 header.
268    TrailingData,
269}
270
271#[cfg(test)]
272mod tests {
273    use core::net::{Ipv4Addr, Ipv6Addr};
274
275    use super::*;
276
277    #[test]
278    fn test_header_new_tcp4() {
279        let src_ip = Ipv4Addr::new(192, 168, 1, 1);
280        let dst_ip = Ipv4Addr::new(10, 0, 0, 1);
281        let src_port = 8080;
282        let dst_port = 80;
283
284        let address_pair = AddressPair::Inet {
285            src_ip,
286            dst_ip,
287            src_port,
288            dst_port,
289        };
290        let header = Header::new(address_pair);
291
292        assert_eq!(header.protocol, FamProto::TCP4);
293        match header.address_pair {
294            AddressPair::Inet {
295                src_ip: s_ip,
296                dst_ip: d_ip,
297                src_port: s_port,
298                dst_port: d_port,
299            } => {
300                assert_eq!(s_ip, src_ip);
301                assert_eq!(d_ip, dst_ip);
302                assert_eq!(s_port, src_port);
303                assert_eq!(d_port, dst_port);
304            }
305            _ => panic!("Expected Inet address pair"),
306        }
307    }
308
309    #[test]
310    fn test_header_new_tcp6() {
311        let src_ip = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
312        let dst_ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
313        let src_port = 8080;
314        let dst_port = 80;
315
316        let address_pair = AddressPair::Inet6 {
317            src_ip,
318            dst_ip,
319            src_port,
320            dst_port,
321        };
322        let header = Header::new(address_pair);
323
324        assert_eq!(header.protocol, FamProto::TCP6);
325        match header.address_pair {
326            AddressPair::Inet6 {
327                src_ip: s_ip,
328                dst_ip: d_ip,
329                src_port: s_port,
330                dst_port: d_port,
331            } => {
332                assert_eq!(s_ip, src_ip);
333                assert_eq!(d_ip, dst_ip);
334                assert_eq!(s_port, src_port);
335                assert_eq!(d_port, dst_port);
336            }
337            _ => panic!("Expected Inet6 address pair"),
338        }
339    }
340
341    #[test]
342    fn test_header_new_unknown() {
343        let header = Header::new(AddressPair::Unspecified);
344        assert_eq!(header.protocol, FamProto::Unknown);
345        assert_eq!(header.address_pair, AddressPair::Unspecified);
346    }
347
348    #[test]
349    #[cfg(feature = "feat-codec-encode")]
350    fn test_encode_tcp4() {
351        let src_ip = Ipv4Addr::new(192, 168, 1, 1);
352        let dst_ip = Ipv4Addr::new(10, 0, 0, 1);
353        let src_port = 8080;
354        let dst_port = 80;
355
356        let address_pair = AddressPair::Inet {
357            src_ip,
358            dst_ip,
359            src_port,
360            dst_port,
361        };
362        let header = Header::new(address_pair);
363
364        let encoded = header.encode();
365        assert_eq!(encoded, "PROXY TCP4 192.168.1.1 10.0.0.1 8080 80\r\n");
366    }
367
368    #[test]
369    #[cfg(feature = "feat-codec-encode")]
370    fn test_encode_tcp6() {
371        let src_ip = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
372        let dst_ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
373        let src_port = 8080;
374        let dst_port = 80;
375
376        let address_pair = AddressPair::Inet6 {
377            src_ip,
378            dst_ip,
379            src_port,
380            dst_port,
381        };
382        let header = Header::new(address_pair);
383
384        let encoded = header.encode();
385        assert_eq!(encoded, "PROXY TCP6 2001:db8::1 fe80::1 8080 80\r\n");
386    }
387
388    #[test]
389    #[cfg(feature = "feat-codec-encode")]
390    fn test_encode_unknown() {
391        let header = Header::new(AddressPair::Unspecified);
392        let encoded = header.encode();
393        assert_eq!(encoded, "PROXY UNKNOWN\r\n");
394    }
395
396    #[test]
397    #[cfg(feature = "feat-codec-decode")]
398    fn test_decode_tcp4_valid() {
399        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080 80\r\n";
400        let Decoded::Some(header) = Header::decode(input).unwrap() else {
401            unreachable!()
402        };
403
404        assert_eq!(header.protocol, FamProto::TCP4);
405        match header.address_pair {
406            AddressPair::Inet {
407                src_ip,
408                dst_ip,
409                src_port,
410                dst_port,
411            } => {
412                assert_eq!(src_ip, Ipv4Addr::new(192, 168, 1, 1));
413                assert_eq!(src_port, 8080);
414                assert_eq!(dst_ip, Ipv4Addr::new(10, 0, 0, 1));
415                assert_eq!(dst_port, 80);
416            }
417            _ => panic!("Expected Inet address pair"),
418        }
419    }
420
421    #[test]
422    #[cfg(feature = "feat-codec-decode")]
423    fn test_decode_tcp6_valid() {
424        let input = b"PROXY TCP6 2001:db8::1 fe80::1 8080 80\r\n";
425        let Decoded::Some(header) = Header::decode(input).unwrap() else {
426            unreachable!()
427        };
428
429        assert_eq!(header.protocol, FamProto::TCP6);
430        match header.address_pair {
431            AddressPair::Inet6 {
432                src_ip,
433                dst_ip,
434                src_port,
435                dst_port,
436            } => {
437                assert_eq!(src_ip, Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
438                assert_eq!(src_port, 8080);
439                assert_eq!(dst_ip, Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
440                assert_eq!(dst_port, 80);
441            }
442            _ => panic!("Expected Inet6 address pair"),
443        }
444    }
445
446    #[test]
447    #[cfg(feature = "feat-codec-decode")]
448    fn test_decode_unknown_valid() {
449        let input = b"PROXY UNKNOWN\r\n";
450        let Decoded::Some(header) = Header::decode(input).unwrap() else {
451            unreachable!()
452        };
453
454        assert_eq!(header.protocol, FamProto::Unknown);
455        assert_eq!(header.address_pair, AddressPair::Unspecified);
456    }
457
458    #[test]
459    #[cfg(feature = "feat-codec-decode")]
460    fn test_decode_unknown_with_extra_data() {
461        let input = b"PROXY UNKNOWN some extra data here\r\n";
462        let Decoded::Some(header) = Header::decode(input).unwrap() else {
463            unreachable!()
464        };
465
466        assert_eq!(header.protocol, FamProto::Unknown);
467        assert_eq!(header.address_pair, AddressPair::Unspecified);
468    }
469
470    #[test]
471    #[cfg(feature = "feat-codec-decode")]
472    fn test_decode_too_long() {
473        // Create a buffer that's too long
474        let mut input = [b'A'; MAXIMUM_LENGTH + 1];
475        input[0] = b'P';
476        input[1] = b'R';
477        input[2] = b'O';
478        input[3] = b'X';
479        input[4] = b'Y';
480
481        let result = Header::decode(&input);
482
483        assert!(matches!(result, Err(DecodeError::MalformedData("bytes too long"))));
484    }
485
486    #[test]
487    #[cfg(feature = "feat-codec-decode")]
488    fn test_decode_not_utf8() {
489        let input = b"PROXY TCP4 \xff\xff\xff\xff 10.0.0.1 8080 80\r\n";
490        let result = Header::decode(input);
491
492        assert!(matches!(result, Err(DecodeError::MalformedData("not UTF-8"))));
493    }
494
495    #[test]
496    #[cfg(feature = "feat-codec-decode")]
497    fn test_decode_missing_crlf() {
498        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080 80";
499        let result = Header::decode(input);
500
501        assert!(matches!(
502            result,
503            Err(DecodeError::MalformedData("missing CRLF or trailing data"))
504        ));
505    }
506
507    #[test]
508    #[cfg(feature = "feat-codec-decode")]
509    fn test_decode_trailing_data() {
510        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080 80\r\ntrailing data";
511        let result = Header::decode(input);
512
513        assert!(matches!(
514            result,
515            Err(DecodeError::MalformedData("missing CRLF or trailing data"))
516        ));
517    }
518
519    #[test]
520    #[cfg(feature = "feat-codec-decode")]
521    fn test_decode_no_magic() {
522        let input = b"NOTPROXY TCP4 192.168.1.1 10.0.0.1 8080 80\r\n";
523        let result = Header::decode(input);
524
525        assert!(matches!(result, Ok(Decoded::None)));
526    }
527
528    #[test]
529    #[cfg(feature = "feat-codec-decode")]
530    fn test_decode_empty_input() {
531        let input = b"";
532        let result = Header::decode(input);
533
534        // Empty input should result in missing CRLF or trailing data error because
535        // str::from_utf8 succeeds, but split_once("\r\n") fails on empty string
536        assert!(matches!(
537            result,
538            Err(DecodeError::MalformedData("missing CRLF or trailing data"))
539        ));
540    }
541
542    #[test]
543    #[cfg(feature = "feat-codec-decode")]
544    fn test_decode_only_magic() {
545        let input = b"PROXY\r\n";
546        let result = Header::decode(input);
547
548        assert!(matches!(result, Err(DecodeError::InvalidFamProto)));
549    }
550
551    #[test]
552    #[cfg(feature = "feat-codec-decode")]
553    fn test_decode_invalid_protocol() {
554        let input = b"PROXY INVALID 192.168.1.1 10.0.0.1 8080 80\r\n";
555        let result = Header::decode(input);
556
557        assert!(matches!(result, Err(DecodeError::InvalidFamProto)));
558    }
559
560    #[test]
561    #[cfg(feature = "feat-codec-decode")]
562    fn test_decode_tcp4_missing_src_ip() {
563        let input = b"PROXY TCP4\r\n";
564        let result = Header::decode(input);
565
566        assert!(matches!(result, Err(DecodeError::MissingData("SRC_IP"))));
567    }
568
569    #[test]
570    #[cfg(feature = "feat-codec-decode")]
571    fn test_decode_tcp4_missing_dst_ip() {
572        let input = b"PROXY TCP4 192.168.1.1\r\n";
573        let result = Header::decode(input);
574
575        assert!(matches!(result, Err(DecodeError::MissingData("DST_IP"))));
576    }
577
578    #[test]
579    #[cfg(feature = "feat-codec-decode")]
580    fn test_decode_tcp4_missing_src_port() {
581        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1\r\n";
582        let result = Header::decode(input);
583
584        assert!(matches!(result, Err(DecodeError::MissingData("SRC_PORT"))));
585    }
586
587    #[test]
588    #[cfg(feature = "feat-codec-decode")]
589    fn test_decode_tcp4_missing_dst_port() {
590        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080\r\n";
591        let result = Header::decode(input);
592
593        assert!(matches!(result, Err(DecodeError::MissingData("DST_PORT"))));
594    }
595
596    #[test]
597    #[cfg(feature = "feat-codec-decode")]
598    fn test_decode_tcp4_invalid_src_ip() {
599        let input = b"PROXY TCP4 999.999.999.999 10.0.0.1 8080 80\r\n";
600        let result = Header::decode(input);
601
602        assert!(matches!(result, Err(DecodeError::MalformedData("SRC_IP"))));
603    }
604
605    #[test]
606    #[cfg(feature = "feat-codec-decode")]
607    fn test_decode_tcp4_invalid_dst_ip() {
608        let input = b"PROXY TCP4 192.168.1.1 invalid_ip 8080 80\r\n";
609        let result = Header::decode(input);
610
611        assert!(matches!(result, Err(DecodeError::MalformedData("DST_IP"))));
612    }
613
614    #[test]
615    #[cfg(feature = "feat-codec-decode")]
616    fn test_decode_tcp4_invalid_src_port() {
617        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 65536 80\r\n";
618        let result = Header::decode(input);
619
620        assert!(matches!(result, Err(DecodeError::MalformedData("SRC_PORT"))));
621    }
622
623    #[test]
624    #[cfg(feature = "feat-codec-decode")]
625    fn test_decode_tcp4_invalid_dst_port() {
626        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080 invalid_port\r\n";
627        let result = Header::decode(input);
628
629        assert!(matches!(result, Err(DecodeError::MalformedData("DST_PORT"))));
630    }
631
632    #[test]
633    #[cfg(feature = "feat-codec-decode")]
634    fn test_decode_tcp6_missing_fields() {
635        let input = b"PROXY TCP6 2001:db8::1\r\n";
636        let result = Header::decode(input);
637
638        assert!(matches!(result, Err(DecodeError::MissingData("DST_IP"))));
639    }
640
641    #[test]
642    #[cfg(feature = "feat-codec-decode")]
643    fn test_decode_tcp6_invalid_ip() {
644        let input = b"PROXY TCP6 invalid::ip fe80::1 8080 80\r\n";
645        let result = Header::decode(input);
646
647        assert!(matches!(result, Err(DecodeError::MalformedData("SRC_IP"))));
648    }
649
650    #[test]
651    #[cfg(all(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
652    fn test_roundtrip_tcp4() {
653        let src_ip = Ipv4Addr::new(192, 168, 1, 100);
654        let dst_ip = Ipv4Addr::new(10, 0, 0, 50);
655        let src_port = 12345;
656        let dst_port = 443;
657
658        let address_pair = AddressPair::Inet {
659            src_ip,
660            dst_ip,
661            src_port,
662            dst_port,
663        };
664        let original = Header::new(address_pair);
665
666        let encoded = original.encode();
667        let Decoded::Some(decoded) = Header::decode(encoded.as_bytes()).unwrap() else {
668            unreachable!()
669        };
670
671        assert_eq!(original, decoded);
672    }
673
674    #[test]
675    #[cfg(all(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
676    fn test_roundtrip_tcp6() {
677        let src_ip = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0x100);
678        let dst_ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0x50);
679        let src_port = 12345;
680        let dst_port = 443;
681
682        let address_pair = AddressPair::Inet6 {
683            src_ip,
684            dst_ip,
685            src_port,
686            dst_port,
687        };
688        let original = Header::new(address_pair);
689
690        let encoded = original.encode();
691        let Decoded::Some(decoded) = Header::decode(encoded.as_bytes()).unwrap() else {
692            unreachable!()
693        };
694
695        assert_eq!(original, decoded);
696    }
697
698    #[test]
699    #[cfg(all(feature = "feat-codec-encode", feature = "feat-codec-decode"))]
700    fn test_roundtrip_unknown() {
701        let original = Header::new(AddressPair::Unspecified);
702
703        let encoded = original.encode();
704        let Decoded::Some(decoded) = Header::decode(encoded.as_bytes()).unwrap() else {
705            unreachable!()
706        };
707
708        assert_eq!(original, decoded);
709    }
710
711    #[test]
712    fn test_maximum_length_constant() {
713        // Test that the MAXIMUM_LENGTH constant is reasonable
714        // The longest possible v1 header would be something like:
715        // "PROXY UNKNOWN ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
716        // ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n"
717        let longest_possible = "PROXY UNKNOWN ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff \
718                                ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n";
719        assert!(longest_possible.len() == MAXIMUM_LENGTH);
720    }
721
722    #[test]
723    #[cfg(feature = "feat-codec-decode")]
724    fn test_decode_edge_case_whitespace() {
725        // Test with multiple spaces (should work with split_whitespace())
726        let input = b"PROXY  TCP4   192.168.1.1    10.0.0.1   8080   80\r\n";
727        let Decoded::Some(header) = Header::decode(input).unwrap() else {
728            unreachable!()
729        };
730
731        assert_eq!(header.protocol, FamProto::TCP4);
732        match header.address_pair {
733            AddressPair::Inet {
734                src_ip,
735                dst_ip,
736                src_port,
737                dst_port,
738            } => {
739                assert_eq!(src_ip, Ipv4Addr::new(192, 168, 1, 1));
740                assert_eq!(src_port, 8080);
741                assert_eq!(dst_ip, Ipv4Addr::new(10, 0, 0, 1));
742                assert_eq!(dst_port, 80);
743            }
744            _ => panic!("Expected Inet address pair"),
745        }
746    }
747
748    // Additional test cases for better coverage
749    #[test]
750    #[cfg(feature = "feat-codec-decode")]
751    fn test_decode_exact_maximum_length() {
752        // Test a header that's exactly at the maximum length limit
753        let input = b"PROXY TCP6 ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n";
754        assert!(input.len() <= MAXIMUM_LENGTH);
755        let Decoded::Some(header) = Header::decode(input).unwrap() else {
756            unreachable!()
757        };
758
759        assert_eq!(header.protocol, FamProto::TCP6);
760    }
761
762    #[test]
763    #[cfg(feature = "feat-codec-decode")]
764    fn test_decode_minimal_tcp4() {
765        let input = b"PROXY TCP4 0.0.0.0 0.0.0.0 0 0\r\n";
766        let Decoded::Some(header) = Header::decode(input).unwrap() else {
767            unreachable!()
768        };
769
770        assert_eq!(header.protocol, FamProto::TCP4);
771        match header.address_pair {
772            AddressPair::Inet {
773                src_ip,
774                dst_ip,
775                src_port,
776                dst_port,
777            } => {
778                assert_eq!(src_ip, Ipv4Addr::new(0, 0, 0, 0));
779                assert_eq!(src_port, 0);
780                assert_eq!(dst_ip, Ipv4Addr::new(0, 0, 0, 0));
781                assert_eq!(dst_port, 0);
782            }
783            _ => panic!("Expected Inet address pair"),
784        }
785    }
786
787    #[test]
788    #[cfg(feature = "feat-codec-decode")]
789    fn test_decode_minimal_tcp6() {
790        let input = b"PROXY TCP6 :: :: 0 0\r\n";
791        let Decoded::Some(header) = Header::decode(input).unwrap() else {
792            unreachable!()
793        };
794
795        assert_eq!(header.protocol, FamProto::TCP6);
796        match header.address_pair {
797            AddressPair::Inet6 {
798                src_ip,
799                dst_ip,
800                src_port,
801                dst_port,
802            } => {
803                assert_eq!(src_ip, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
804                assert_eq!(src_port, 0);
805                assert_eq!(dst_ip, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
806                assert_eq!(dst_port, 0);
807            }
808            _ => panic!("Expected Inet6 address pair"),
809        }
810    }
811
812    #[test]
813    #[cfg(feature = "feat-codec-decode")]
814    fn test_decode_mixed_case_protocol() {
815        // Protocol should be case sensitive
816        let input = b"PROXY tcp4 192.168.1.1 10.0.0.1 8080 80\r\n";
817        let result = Header::decode(input);
818
819        assert!(matches!(result, Err(DecodeError::InvalidFamProto)));
820    }
821
822    #[test]
823    #[cfg(feature = "feat-codec-decode")]
824    fn test_decode_different_line_ending() {
825        // Should only accept \r\n, not just \n
826        let input = b"PROXY TCP4 192.168.1.1 10.0.0.1 8080 80\n";
827        let result = Header::decode(input);
828
829        assert!(matches!(
830            result,
831            Err(DecodeError::MalformedData("missing CRLF or trailing data"))
832        ));
833    }
834}