Skip to main content

phantom_protocol/transport/phantom_udp/
envelope.rs

1//! PhantomUDP outer datagram envelope — transport framing (NOT the frozen inner wire).
2//! Every UDP datagram begins with `[flags: u8][cid: [u8;8]]`. Like `TcpSessionTransport`'s
3//! 4-byte length prefix, this is outside `core/tests/wire_vectors` and does not bump
4//! `WIRE_VERSION`/`PROTOCOL_VERSION`.
5
6/// Connection-ID length (bytes). 8 = QUIC-parity, single-cache-line demux key.
7pub const CID_LEN: usize = 8;
8/// Connection ID: a client-chosen, lifetime-stable routing label. Unauthenticated —
9/// security rests on the inner Phantom AEAD (Invariant 2/4). NEVER transcript-bound.
10pub type ConnId = [u8; CID_LEN];
11
12/// Outer header length: flags byte + cid.
13pub const HDR_LEN: usize = 1 + CID_LEN;
14/// Conservative fixed path-MTU budget: 1200 bytes is the QUIC-style floor that
15/// survives almost every Internet path without IP fragmentation. Static today —
16/// dynamic DPLPMTUD to raise it is future work.
17pub const PATH_MTU: usize = 1200;
18/// Fragment subheader: packet_id u32be + chunk_index u16be + total_chunks u16be.
19pub const FRAG_SUBHDR_LEN: usize = 8;
20/// Largest inner frame that fits one unfragmented datagram.
21pub const MAX_INNER_UNFRAGMENTED: usize = PATH_MTU - HDR_LEN;
22/// Largest chunk payload per fragmented datagram.
23pub const MAX_INNER_FRAG_CHUNK: usize = PATH_MTU - HDR_LEN - FRAG_SUBHDR_LEN;
24
25const TYPE_SHIFT: u8 = 6;
26const FRAG_BIT: u8 = 0b0010_0000;
27const RESERVED_MASK: u8 = 0b0001_1111; // bits 4..0 are reserved and must be 0 (rejected if set)
28
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30pub enum PacketType {
31    /// Long header: inner = a borsh handshake message (ClientHello/ServerHello/HelloRetryRequest/ServerReject).
32    Initial,
33    /// Short header: inner = a header-protected `PhantomPacket` (15-byte masked header + body).
34    OneRtt,
35    /// Server -> client stateless address-validation token (reserved; unused).
36    Retry,
37}
38
39#[derive(Clone, Copy, PartialEq, Eq, Debug)]
40pub struct OuterHeader {
41    pub ty: PacketType,
42    pub fragmented: bool,
43    pub cid: ConnId,
44}
45
46#[derive(Clone, Copy, PartialEq, Eq, Debug)]
47pub enum EnvelopeError {
48    Truncated,
49    ReservedType,
50    ReservedBitsSet,
51    FrameTooLarge,
52}
53
54impl core::fmt::Display for EnvelopeError {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        match self {
57            Self::Truncated => write!(f, "datagram too short for the PhantomUDP envelope header"),
58            Self::ReservedType => write!(f, "reserved PhantomUDP packet type"),
59            Self::ReservedBitsSet => write!(f, "reserved PhantomUDP flag bits set"),
60            Self::FrameTooLarge => write!(f, "frame exceeds the maximum fragmentable size"),
61        }
62    }
63}
64
65impl std::error::Error for EnvelopeError {}
66
67/// Append `[flags][cid]` to `buf`.
68pub fn encode_header(buf: &mut Vec<u8>, ty: PacketType, fragmented: bool, cid: &ConnId) {
69    let type_bits = match ty {
70        PacketType::Initial => 0b00,
71        PacketType::OneRtt => 0b01,
72        PacketType::Retry => 0b10,
73    };
74    let mut flags = type_bits << TYPE_SHIFT;
75    if fragmented {
76        flags |= FRAG_BIT;
77    }
78    buf.push(flags);
79    buf.extend_from_slice(cid);
80}
81
82/// Parse `[flags][cid]`; returns the header and the remaining bytes.
83pub fn decode_header(buf: &[u8]) -> Result<(OuterHeader, &[u8]), EnvelopeError> {
84    if buf.len() < HDR_LEN {
85        return Err(EnvelopeError::Truncated);
86    }
87    let flags = buf[0];
88    if flags & RESERVED_MASK != 0 {
89        return Err(EnvelopeError::ReservedBitsSet);
90    }
91    let ty = match flags >> TYPE_SHIFT {
92        0b00 => PacketType::Initial,
93        0b01 => PacketType::OneRtt,
94        0b10 => PacketType::Retry,
95        _ => return Err(EnvelopeError::ReservedType),
96    };
97    let fragmented = flags & FRAG_BIT != 0;
98    let mut cid = [0u8; CID_LEN];
99    cid.copy_from_slice(&buf[1..1 + CID_LEN]);
100    Ok((
101        OuterHeader {
102            ty,
103            fragmented,
104            cid,
105        },
106        &buf[HDR_LEN..],
107    ))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn roundtrip_initial_unfragmented() {
116        let cid: ConnId = [1, 2, 3, 4, 5, 6, 7, 8];
117        let mut buf = Vec::new();
118        encode_header(&mut buf, PacketType::Initial, false, &cid);
119        assert_eq!(buf.len(), HDR_LEN);
120        let (hdr, rest) = decode_header(&buf).expect("decode");
121        assert_eq!(hdr.ty, PacketType::Initial);
122        assert!(!hdr.fragmented);
123        assert_eq!(hdr.cid, cid);
124        assert!(rest.is_empty());
125    }
126
127    #[test]
128    fn roundtrip_onertt_fragmented_with_payload() {
129        let cid: ConnId = [9; 8];
130        let mut buf = Vec::new();
131        encode_header(&mut buf, PacketType::OneRtt, true, &cid);
132        buf.extend_from_slice(b"abc");
133        let (hdr, rest) = decode_header(&buf).expect("decode");
134        assert_eq!(hdr.ty, PacketType::OneRtt);
135        assert!(hdr.fragmented);
136        assert_eq!(rest, b"abc");
137    }
138
139    #[test]
140    fn rejects_truncated() {
141        assert!(matches!(
142            decode_header(&[0u8; 4]),
143            Err(EnvelopeError::Truncated)
144        ));
145    }
146
147    #[test]
148    fn rejects_reserved_type() {
149        let mut buf = vec![0b1100_0000]; // type = 0b11 reserved
150        buf.extend_from_slice(&[0u8; CID_LEN]);
151        assert!(matches!(
152            decode_header(&buf),
153            Err(EnvelopeError::ReservedType)
154        ));
155    }
156
157    #[test]
158    fn rejects_reserved_bits_set() {
159        let mut buf = vec![0b0000_0001]; // a reserved low bit set
160        buf.extend_from_slice(&[0u8; CID_LEN]);
161        assert!(matches!(
162            decode_header(&buf),
163            Err(EnvelopeError::ReservedBitsSet)
164        ));
165    }
166}