Implement unified packet format with outer header first

Restructure LP packet format so cleartext fields (receiver_idx, counter)
are always first, enabling trivial header parsing for routing before
session lookup. Protocol version and reserved fields are now encrypted
in the inner payload for encrypted packets.

Wire format change:
- Before: proto(1B) + reserved(3B) + receiver_idx(4B) + counter(8B)
- After:  receiver_idx(4B) + counter(8B) | proto(1B) + reserved(3B) + ...

Key changes:
- Add OuterHeader struct (12 bytes) for routing/replay protection
- Update serialize_lp_packet/parse_lp_packet for unified format
- parse_lp_header_only now returns OuterHeader
- Gateway handler uses OuterHeader for session lookup
- Update DESIGN.md with new wire format diagrams

Security improvement: Only receiver_idx and counter visible after PSK
establishment (was also exposing protocol version and reserved).
This commit is contained in:
durch
2025-11-28 12:09:49 +01:00
parent 09447f5a1c
commit b05113e522
5 changed files with 259 additions and 123 deletions
+64 -30
View File
@@ -11,25 +11,41 @@ The Lewes Protocol (LP) provides authenticated, encrypted sessions with replay p
## Packet Structure
### Unified Format (v2)
All packets share the same outer structure - cleartext fields are always first:
```
┌───────────────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ trailer │
1B │ 3B4B 8B │ variable │ 16B │
└───────────────────┴────────────────┴─────────┴─────────────────────┴─────────┘
16B header 16B
┌────────────────┬─────────┬─────────┬─────────┬─────────────────────┬─────────┐
│ receiver_index │ counter │ version │ reserved │ payload │ trailer │
4B │ 8B │ 1B3B │ variable │ 16B │
└────────────────┴─────────┴─────────┴─────────┴─────────────────────┴─────────┘
│←── 12B outer header ────┤│←── inner (cleartext or encrypted) ──────┤│─ 16B ──┤
```
**Total overhead:** 32 bytes (16B header + 16B trailer)
**Total overhead:** 32 bytes (12B outer + 4B inner prefix + 16B trailer)
Key properties:
- **Outer header** (12 bytes): Always cleartext, used for routing before session lookup
- **Inner content**: Cleartext before PSK, encrypted after PSK
- **No disambiguation needed**: Format is identical for both modes
### Field Descriptions
**Outer Header** (always cleartext, 12 bytes):
| Field | Size | Description |
|-------|------|-------------|
| receiver_index | 4 bytes | Session identifier, proposed by client (routing key) |
| counter | 8 bytes | Monotonic counter, used as AEAD nonce and for replay protection |
**Inner Content** (cleartext or encrypted):
| Field | Size | Description |
|-------|------|-------------|
| version | 1 byte | Protocol version |
| reserved | 3 bytes | Reserved for future use |
| receiver_index | 4 bytes | Session identifier, proposed by client |
| counter | 8 bytes | Monotonic counter, used as AEAD nonce and for replay protection |
| payload | variable | Message type (2B) + content (plaintext or encrypted depending on state) |
| payload | variable | Message type (2B) + content |
| trailer | 16 bytes | Zeros (no PSK) or AEAD Poly1305 tag (with PSK) |
### Wire Format
@@ -54,10 +70,16 @@ Length-prefixed over TCP:
| KKTResponse | 0x0005 | KEM key transfer response |
| ForwardPacket | 0x0006 | Nested session forwarding |
| Collision | 0x0007 | Receiver index collision |
| SubsessionRequest | 0x0008 | Client requests new subsession |
| SubsessionKK1 | 0x0009 | KK handshake msg 1 (responder → initiator) |
| SubsessionKK2 | 0x000A | KK handshake msg 2 (initiator → responder) |
| SubsessionReady | 0x000B | Subsession established confirmation |
| Ack | 0x0008 | Gateway confirms receipt of message |
### Planned Message Types (not yet implemented)
| Type | Value | Description |
|------|-------|-------------|
| SubsessionRequest | 0x0009 | Client requests new subsession |
| SubsessionKK1 | 0x000A | KK handshake msg 1 (responder → initiator) |
| SubsessionKK2 | 0x000B | KK handshake msg 2 (initiator → responder) |
| SubsessionReady | 0x000C | Subsession established confirmation |
## Receiver Index
@@ -91,6 +113,7 @@ As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequen
| Packet | PSK Available | Header | Payload | Trailer |
|--------|---------------|--------|---------|---------|
| ClientHello | No | Clear | Clear | Zeros |
| Ack | No | Clear | Clear | Zeros |
| KKTRequest | No | Clear | Clear | Zeros |
| KKTResponse | No | Clear | Clear | Zeros |
| Noise msg 1 | No | Clear | Clear | Zeros |
@@ -104,38 +127,44 @@ As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequen
- **AEAD**: ChaCha20-Poly1305
- **Key**: outer_key = KDF(PSK, "lp-outer-aead") - derived from PSK, not PSK itself
- **Nonce**: counter (8 bytes, zero-padded to 12 bytes)
- **AAD**: version ‖ reserved ‖ receiver_index ‖ counter (16 bytes)
- **AAD**: receiver_index ‖ counter (12 bytes) - the outer header
- **Encrypted**: version ‖ reserved ‖ message_type ‖ content
Note: PSK is used as-is for Noise (which does internal key derivation). The outer_key derivation avoids key reuse between the two encryption layers.
### Before PSK
```
┌───────────────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ 00...00 │
│ │ │ │ (plaintext) │ │
└───────────────────┴────────────────┴─────────┴─────────────────────┴─────────┘
│←──────────────────────────── cleartext ──────────────────────────────────────┤
┌────────────────┬─────────┬─────────┬─────────┬─────────────────────┬─────────┐
│ receiver_index │ counter │ version │ reserved │ payload │ 00...00 │
│ │ │ │ (plaintext) │ │
└────────────────┴─────────┴─────────┴─────────┴─────────────────────┴─────────┘
│←── 12B outer ──────────┤│←────────────── cleartext inner ──────────┤│─zeros──┤
```
### After PSK
```
┌───────────────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ tag │
│ (encrypted) │ │
└───────────────────┴────────────────┴─────────┴─────────────────────┴─────────┘
│←───────── cleartext (authenticated via AAD) ─────────┤│← encrypted ─┤│─ auth ─┤
┌────────────────┬─────────┬─────────┬─────────┬─────────────────────┬─────────┐
│ receiver_index │ counter │ version │ reserved │ payload │ tag │
│ │ │ (enc) │ (enc) │ (encrypted) │ │
└────────────────┴─────────┴─────────┴─────────┴─────────────────────┴─────────┘
│←── 12B outer (AAD) ────┤│←────────── encrypted inner ──────────────┤│─ tag ──┤
```
## Handshake Flow
Each arrow represents a separate TCP connection (packet-per-connection model).
```
Client Gateway
│ │
│ [hdr][ClientHello][zeros] │
│──────────────────────────────────────►│ store state[receiver_index]
│ │
│ [hdr][Ack][zeros] │
│◄──────────────────────────────────────│ confirm ClientHello
│ │
│ [hdr][KKTRequest][zeros] │
│──────────────────────────────────────►│
│ │
@@ -299,18 +328,23 @@ SubsessionReadyData {
### Always Visible to Observer
- Version (1 byte)
- Reserved (3 bytes)
Only the outer header (12 bytes) is visible after PSK establishment:
- Receiver index (4 bytes) - opaque, unlinkable to identity
- Counter (8 bytes) - reveals packet ordering
- Packet size
Note: Before PSK, version, reserved, and message type are also visible.
### Protected After PSK
- Header integrity (authenticated via AEAD AAD)
- Payload confidentiality (encrypted)
- Message type (hidden)
- Application data (double encrypted)
- Outer header integrity (authenticated via AEAD AAD)
- Inner content confidentiality (encrypted):
- Protocol version
- Reserved field
- Message type
- Payload
- Application data (double encrypted: outer AEAD + inner Noise)
### Cryptographic Guarantees
+132 -85
View File
@@ -6,8 +6,14 @@ use crate::message::{
ClientHelloData, EncryptedDataPayload, ForwardPacketData, HandshakeData, KKTRequestData,
KKTResponseData, LpMessage, MessageType,
};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::packet::{LpHeader, LpPacket, OuterHeader, TRAILER_LEN};
use bytes::{BufMut, BytesMut};
/// Size of outer header (receiver_idx + counter) - always cleartext
pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes
/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode
const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
use chacha20poly1305::{
aead::{AeadInPlace, KeyInit},
ChaCha20Poly1305, Key, Nonce, Tag,
@@ -135,28 +141,28 @@ fn parse_message_from_type_and_content(
}
}
/// Parse only the LP header from raw packet bytes.
/// Parse only the outer header from raw packet bytes.
///
/// Used for routing before session lookup when the header is always cleartext.
/// This allows the caller to determine the receiver_idx and look up the appropriate
/// session to get the outer AEAD key before calling `parse_lp_packet()`.
/// Used for routing before session lookup. The outer header (receiver_idx + counter)
/// is always cleartext at bytes 0-12 in the unified packet format.
///
/// # Arguments
/// * `src` - Raw packet bytes (at least LpHeader::SIZE bytes)
/// * `src` - Raw packet bytes (at least OuterHeader::SIZE bytes)
///
/// # Errors
/// * `LpError::InsufficientBufferSize` - Packet too small for header
pub fn parse_lp_header_only(src: &[u8]) -> Result<LpHeader, LpError> {
if src.len() < LpHeader::SIZE {
return Err(LpError::InsufficientBufferSize);
}
LpHeader::parse(&src[..LpHeader::SIZE])
/// * `LpError::InsufficientBufferSize` - Packet too small for outer header
pub fn parse_lp_header_only(src: &[u8]) -> Result<OuterHeader, LpError> {
OuterHeader::parse(src)
}
/// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload).
///
/// Assumes the input `src` contains exactly one complete packet. It does not handle
/// stream fragmentation or provide replay protection checks (these belong at the session level).
/// ## Unified Packet Format
///
/// Both cleartext and encrypted packets have the same structure:
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
/// - Inner payload: proto(1) + reserved(3) + msg_type(2) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
/// * `src` - Raw packet bytes
@@ -169,40 +175,61 @@ pub fn parse_lp_packet(
src: &[u8],
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError> {
// Minimum size check: LpHeader + Type + Trailer (for 0-payload message)
let min_size = LpHeader::SIZE + 2 + TRAILER_LEN;
// Minimum size check: OuterHeader + InnerPrefix + MsgType + Trailer (for 0-payload message)
// 12 + 4 + 2 + 16 = 34 bytes
let min_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + TRAILER_LEN;
if src.len() < min_size {
return Err(LpError::InsufficientBufferSize);
}
// Parse LpHeader (always cleartext for routing)
let header = LpHeader::parse(&src[..LpHeader::SIZE])?;
// Parse outer header (always cleartext at bytes 0-12)
let outer_header = OuterHeader::parse(src)?;
// Extract trailer (potential AEAD tag)
let trailer_start = src.len() - TRAILER_LEN;
let mut trailer = [0u8; TRAILER_LEN];
trailer.copy_from_slice(&src[trailer_start..]);
// Payload is everything between header and trailer
let payload_bytes = &src[LpHeader::SIZE..trailer_start];
// Inner payload is everything between outer header and trailer
let inner_bytes = &src[OUTER_HEADER_SIZE..trailer_start];
// Handle decryption if outer key provided
let (message_type_raw, message_content) = match outer_key {
match outer_key {
None => {
// Cleartext mode - parse directly
if payload_bytes.len() < 2 {
// Cleartext mode - parse inner directly
// Inner format: proto(1) + reserved(3) + msg_type(2) + content
if inner_bytes.len() < INNER_PREFIX_SIZE + 2 {
return Err(LpError::InsufficientBufferSize);
}
let msg_type = u16::from_le_bytes([payload_bytes[0], payload_bytes[1]]);
(msg_type, &payload_bytes[2..])
let protocol_version = inner_bytes[0];
// reserved bytes [1..4] are ignored
let msg_type = u16::from_le_bytes([inner_bytes[4], inner_bytes[5]]);
let message_content = &inner_bytes[6..];
let header = LpHeader {
protocol_version,
reserved: 0,
receiver_idx: outer_header.receiver_idx,
counter: outer_header.counter,
};
let message = parse_message_from_type_and_content(msg_type, message_content)?;
Ok(LpPacket {
header,
message,
trailer,
})
}
Some(key) => {
// AEAD decryption mode
let nonce = build_nonce(header.counter);
let aad = &src[..LpHeader::SIZE]; // Header as AAD
// AAD is the outer header (12 bytes)
let nonce = build_nonce(outer_header.counter);
let aad = &src[..OUTER_HEADER_SIZE];
// Copy payload for in-place decryption
let mut decrypted = payload_bytes.to_vec();
// Copy inner payload for in-place decryption
let mut decrypted = inner_bytes.to_vec();
// Convert trailer to Tag
let tag = Tag::from_slice(&trailer);
@@ -213,67 +240,85 @@ pub fn parse_lp_packet(
.decrypt_in_place_detached(Nonce::from_slice(&nonce), aad, &mut decrypted, tag)
.map_err(|_| LpError::AeadTagMismatch)?;
// Extract message type from decrypted payload
if decrypted.len() < 2 {
// Decrypted format: proto(1) + reserved(3) + msg_type(2) + content
if decrypted.len() < INNER_PREFIX_SIZE + 2 {
return Err(LpError::InsufficientBufferSize);
}
let msg_type = u16::from_le_bytes([decrypted[0], decrypted[1]]);
// Return decrypted content (owned, so we handle it differently)
return parse_message_from_type_and_content(msg_type, &decrypted[2..]).map(|message| {
LpPacket {
header,
message,
trailer,
}
});
let protocol_version = decrypted[0];
// reserved bytes [1..4] are ignored
let msg_type = u16::from_le_bytes([decrypted[4], decrypted[5]]);
let message_content = &decrypted[6..];
let header = LpHeader {
protocol_version,
reserved: 0,
receiver_idx: outer_header.receiver_idx,
counter: outer_header.counter,
};
let message = parse_message_from_type_and_content(msg_type, message_content)?;
Ok(LpPacket {
header,
message,
trailer,
})
}
};
// Cleartext path: parse message from payload
let message = parse_message_from_type_and_content(message_type_raw, message_content)?;
Ok(LpPacket {
header,
message,
trailer,
})
}
}
/// Serializes an LpPacket into the provided BytesMut buffer.
///
/// ## Unified Packet Format
///
/// Both cleartext and encrypted packets have the same structure:
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
/// - Inner payload: proto(1) + reserved(3) + msg_type(2) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
/// * `item` - Packet to serialize
/// * `dst` - Output buffer
/// * `outer_key` - None for cleartext (uses packet's trailer), Some for AEAD encryption
///
/// When `outer_key` is provided:
/// - Header is written in cleartext (used as AAD)
/// - Message type + content is encrypted
/// - Trailer is set to the AEAD tag
/// * `outer_key` - None for cleartext, Some for AEAD encryption
pub fn serialize_lp_packet(
item: &LpPacket,
dst: &mut BytesMut,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError> {
// Total size: outer_header(12) + inner_prefix(4) + msg_type(2) + content + trailer(16)
let total_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + item.message.len() + TRAILER_LEN;
dst.reserve(total_size);
// 1. Write outer header (always cleartext) - 12 bytes
let outer_header = OuterHeader::new(item.header.receiver_idx, item.header.counter);
outer_header.encode_into(dst);
match outer_key {
None => {
// Cleartext mode - use existing encode method
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
item.encode(dst);
// Cleartext mode
// 2. Write inner prefix: proto(1) + reserved(3)
dst.put_u8(item.header.protocol_version);
dst.put_slice(&[0, 0, 0]); // reserved
// 3. Write message type (2B) + content
dst.put_slice(&(item.message.typ() as u16).to_le_bytes());
item.message.encode_content(dst);
// 4. Write zeros trailer
dst.put_slice(&[0u8; TRAILER_LEN]);
Ok(())
}
Some(key) => {
// AEAD encryption mode
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
// AAD is the outer header (first 12 bytes)
let aad = outer_header.encode();
// 1. Encode header (AAD - not encrypted)
let header_start = dst.len();
item.header.encode(dst);
let header_end = dst.len();
// 2. Build plaintext: message_type (2B) + content
// 2. Build plaintext: proto(1) + reserved(3) + msg_type(2) + content
let mut plaintext = BytesMut::new();
plaintext.put_u8(item.header.protocol_version);
plaintext.put_slice(&[0, 0, 0]); // reserved
plaintext.put_slice(&(item.message.typ() as u16).to_le_bytes());
item.message.encode_content(&mut plaintext);
@@ -281,16 +326,15 @@ pub fn serialize_lp_packet(
let payload_start = dst.len();
dst.put_slice(&plaintext);
// 4. Build nonce and get AAD
// 4. Build nonce from counter
let nonce = build_nonce(item.header.counter);
let aad = &dst[header_start..header_end].to_vec(); // Copy AAD since we mutate dst
// 5. Encrypt payload in-place
let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
let tag = cipher
.encrypt_in_place_detached(
Nonce::from_slice(&nonce),
aad,
&aad,
&mut dst[payload_start..],
)
.map_err(|_| LpError::Internal("AEAD encryption failed".to_string()))?;
@@ -320,8 +364,9 @@ mod tests {
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
// Header length: version(1) + reserved(3) + receiver_index(4) + counter(8) = 16 bytes
const HEADER_LEN: usize = 16;
// AIDEV-NOTE: With unified format, outer header (receiver_idx + counter) is always first
// and is the only cleartext portion for encrypted packets
const OUTER_HDR: usize = super::OUTER_HEADER_SIZE; // 12 bytes
// === Cleartext Encode/Decode Tests ===
@@ -483,17 +528,18 @@ mod tests {
// This *should* parse successfully based on the logic, but the trailer is garbage.
// Let's rethink: parse_lp_packet assumes the *entire slice* is the packet.
// If the slice doesn't end exactly where the trailer should, it's an error.
// In this case, total length is 58. LpHeader(16) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
// In this case, total length is 58. OuterHdr(12) + InnerPrefix(4) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
// Trailer starts at 16+2+24 = 42. Ends at 42+16=58. It fits exactly.
// This test *still* doesn't test incompleteness correctly for the datagram parser.
// Let's test a buffer that's *too short* even for header+type+trailer+min_payload
// Note: Buffer order doesn't matter for this test since we fail on minimum size check
let mut buf_too_short = BytesMut::new();
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
// No payload, no trailer. Length = 16+2=18. Min size = 34.
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx (outer header)
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // counter (outer header)
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // version + reserved (inner prefix)
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // msg type
// No payload, no trailer. Length = 12+4+2=18. Min size = 34.
let result_too_short = parse_lp_packet(&buf_too_short, None);
assert!(result_too_short.is_err());
assert!(matches!(
@@ -873,11 +919,11 @@ mod tests {
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Header should be the same (it's authenticated but not encrypted)
assert_eq!(&cleartext[..HEADER_LEN], &encrypted[..HEADER_LEN]);
// Outer header (receiver_idx + counter) should be the same - always cleartext
assert_eq!(&cleartext[..OUTER_HDR], &encrypted[..OUTER_HDR]);
// Payload should differ (it's encrypted)
let payload_start = HEADER_LEN;
// Inner payload (proto + reserved + msg_type + content) should differ (encrypted)
let payload_start = OUTER_HDR;
let payload_end_cleartext = cleartext.len() - TRAILER_LEN;
let payload_end_encrypted = encrypted.len() - TRAILER_LEN;
@@ -944,7 +990,8 @@ mod tests {
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Tamper with the header (flip a bit in receiver_idx)
// Tamper with the outer header AAD (flip a bit in counter at byte 4)
// New format: [receiver_idx(0-3), counter(4-11)], so byte 4 is counter's LSB
encrypted[4] ^= 0x01;
// Parsing should fail with AeadTagMismatch
@@ -986,9 +1033,9 @@ mod tests {
let mut encrypted2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut encrypted2, Some(&outer_key)).unwrap();
// The encrypted payloads should differ even though the message is the same
// (because nonce is different)
let payload_start = HEADER_LEN;
// The encrypted inner payloads should differ even though the message is the same
// (because nonce is different). Inner payload starts after outer header.
let payload_start = OUTER_HDR;
assert_ne!(
&encrypted1[payload_start..],
&encrypted2[payload_start..],
+1 -1
View File
@@ -16,7 +16,7 @@ pub mod session_manager;
pub use error::LpError;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{LpPacket, BOOTSTRAP_RECEIVER_IDX};
pub use packet::{LpPacket, OuterHeader, BOOTSTRAP_RECEIVER_IDX};
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
pub use session::{LpSession, generate_fresh_salt};
pub use session_manager::SessionManager;
+54 -1
View File
@@ -130,7 +130,60 @@ impl LpPacket {
/// the same session ID from their keys, and all subsequent packets use that ID.
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
// VERSION [1B] || RESERVED [3B] || SENDER_INDEX [4B] || COUNTER [8B]
/// Outer header (12 bytes) - always cleartext, used for routing.
///
/// This is the first 12 bytes of every LP packet, containing only the fields
/// needed for session lookup (receiver_idx) and replay protection (counter).
/// For encrypted packets, this is the AAD (additional authenticated data).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OuterHeader {
pub receiver_idx: u32,
pub counter: u64,
}
impl OuterHeader {
pub const SIZE: usize = 12; // receiver_idx(4) + counter(8)
pub fn new(receiver_idx: u32, counter: u64) -> Self {
Self {
receiver_idx,
counter,
}
}
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
if src.len() < Self::SIZE {
return Err(LpError::InsufficientBufferSize);
}
Ok(Self {
receiver_idx: u32::from_le_bytes(src[0..4].try_into().unwrap()),
counter: u64::from_le_bytes(src[4..12].try_into().unwrap()),
})
}
pub fn encode(&self) -> [u8; Self::SIZE] {
let mut buf = [0u8; Self::SIZE];
buf[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes());
buf[4..12].copy_from_slice(&self.counter.to_le_bytes());
buf
}
/// Encode directly into a BytesMut buffer
pub fn encode_into(&self, dst: &mut BytesMut) {
dst.put_slice(&self.receiver_idx.to_le_bytes());
dst.put_slice(&self.counter.to_le_bytes());
}
}
/// Internal LP header representation containing all logical header fields.
///
/// **Note**: This struct represents the LOGICAL header, not the wire format.
/// On the wire, packets use the unified format where:
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
///
/// The `LpHeader::encode()` method outputs the old logical format for debug purposes only.
/// Use `serialize_lp_packet()` in codec.rs for actual wire serialization.
#[derive(Debug, Clone)]
pub struct LpHeader {
pub protocol_version: u8,
+8 -6
View File
@@ -6,7 +6,7 @@ use super::registration::process_registration;
use super::LpHandlerState;
use crate::error::GatewayError;
use nym_lp::{
codec::OuterAeadKey, keypair::PublicKey, message::ForwardPacketData, packet::LpHeader,
codec::OuterAeadKey, keypair::PublicKey, message::ForwardPacketData, OuterHeader,
LpMessage, LpPacket,
};
use nym_metrics::{add_histogram_obs, inc};
@@ -775,11 +775,12 @@ impl LpConnectionHandler {
Ok(response_buf)
}
/// Receive raw packet bytes and parse header only (for routing before session lookup).
/// Receive raw packet bytes and parse outer header only (for routing before session lookup).
///
/// Returns the raw packet bytes and parsed header. The caller should look up
/// the session to get outer_aead_key, then call `parse_lp_packet()` with the key.
async fn receive_raw_packet(&mut self) -> Result<(Vec<u8>, LpHeader), GatewayError> {
/// Returns the raw packet bytes and parsed outer header (receiver_idx + counter).
/// The caller should look up the session to get outer_aead_key, then call
/// `parse_lp_packet()` with the key.
async fn receive_raw_packet(&mut self) -> Result<(Vec<u8>, OuterHeader), GatewayError> {
use nym_lp::codec::parse_lp_header_only;
// Read 4-byte length prefix (u32 big-endian)
@@ -1096,9 +1097,10 @@ mod tests {
.unwrap();
// Handler should receive and parse it correctly
// Note: header is OuterHeader (receiver_idx + counter only), not LpHeader
let (header, received) = server_task.await.unwrap().unwrap();
assert_eq!(header.protocol_version, 1);
assert_eq!(header.receiver_idx, 42);
assert_eq!(header.counter, 0);
assert_eq!(received.header().protocol_version, 1);
assert_eq!(received.header().receiver_idx, 42);
assert_eq!(received.header().counter, 0);