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