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