Add LP telescoping with nested sessions and subsession support

Extends LP protocol with telescoping architecture for nested sessions:
- Add nested session support with KKpsk0 rekeying
- Add subsession support with collision detection
- Implement unified packet format with outer header
- Refactor gateway handlers for single-packet forwarding
- Add TTL-based state cleanup for stale sessions
- Add outer AEAD encryption layer
- Refactor registration client for packet-per-connection model
This commit is contained in:
durch
2025-12-12 16:30:42 +01:00
committed by Jędrzej Stuczyński
parent 0a6f78a921
commit 637459b153
33 changed files with 5964 additions and 1290 deletions
+3
View File
@@ -1,2 +1,5 @@
nym-validator-rewarder/.sqlx/** diff=nodiff
nym-node-status-api/nym-node-status-api/.sqlx/** diff=nodiff
# Use bd merge for beads JSONL files
.beads/beads.jsonl merge=beads
Generated
+2
View File
@@ -6683,6 +6683,7 @@ dependencies = [
"bincode",
"bs58",
"bytes",
"chacha20poly1305",
"criterion",
"dashmap",
"libcrux-kem",
@@ -6704,6 +6705,7 @@ dependencies = [
"tls_codec",
"tracing",
"utoipa",
"zeroize",
]
[[package]]
+2
View File
@@ -34,6 +34,8 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
tls_codec = { workspace = true }
num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
+365
View File
@@ -0,0 +1,365 @@
# LP Protocol Design
## Overview
The Lewes Protocol (LP) provides authenticated, encrypted sessions with replay protection. Key design principles:
1. **Unified packet structure** - Same format for all packet types
2. **Receiver index** - Client-proposed session identifier (replaces computed session_id)
3. **Opportunistic encryption** - Header authentication and payload encryption as soon as PSK is available
4. **WireGuard-inspired simplicity** - Minimal header, clear security model
## Packet Structure
### Unified Format (v2)
All packets share the same outer structure - cleartext fields are always first:
```
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
│ receiver_index │ counter │ version │ reserved │ payload │ trailer │
│ 4B │ 8B │ 1B │ 3B │ variable │ 16B │
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
│←── 12B outer header ────┤│←── inner (cleartext or encrypted) ──────┤│─ 16B ──┤
```
**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 |
| payload | variable | Message type (2B) + content |
| trailer | 16 bytes | Zeros (no PSK) or AEAD Poly1305 tag (with PSK) |
### Wire Format
Length-prefixed over TCP:
```
┌────────────────────┬─────────────────────────────────────────────────────┐
│ length (4B BE u32) │ LpPacket │
└────────────────────┴─────────────────────────────────────────────────────┘
```
## Message Types
| Type | Value | Description |
|------|-------|-------------|
| Busy | 0x0000 | Server congestion signal |
| Handshake | 0x0001 | Noise protocol messages |
| EncryptedData | 0x0002 | Encrypted application data |
| ClientHello | 0x0003 | Initial session setup |
| KKTRequest | 0x0004 | KEM key transfer request |
| KKTResponse | 0x0005 | KEM key transfer response |
| ForwardPacket | 0x0006 | Nested session forwarding |
| Collision | 0x0007 | Receiver index collision |
| 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
### Assignment
The client generates a random 4-byte receiver_index and includes it in ClientHello. The gateway uses this as the session lookup key. This replaces the previous approach of computing a deterministic session_id from both parties' keys.
### Collision Handling
With 4 bytes (2^32 values), collision probability is negligible:
| Active Sessions | Collision Probability |
|-----------------|----------------------|
| 10,000 | ~0.001% |
| 100,000 | ~0.1% |
If collision detected, gateway rejects ClientHello and client retries with new index.
## Opportunistic Encryption
### Principle
As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequent packets use outer AEAD encryption:
- **Header**: Authenticated as associated data (AD)
- **Payload**: Encrypted (message type + content)
- **Trailer**: AEAD tag
### Timeline
| 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 |
| | | **PSK derived** | | |
| Noise msg 2 | Yes | Authenticated | Encrypted | Tag |
| Noise msg 3 | Yes | Authenticated | Encrypted | Tag |
| Data | Yes | Authenticated | Encrypted | Tag |
### Encryption Scheme
- **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**: 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
```
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
│ receiver_index │ counter │ version │ reserved │ payload │ 00...00 │
│ │ │ │ │ (plaintext) │ │
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
│←── 12B outer ──────────┤│←────────────── cleartext inner ──────────┤│─zeros──┤
```
### After PSK
```
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
│ 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] │
│──────────────────────────────────────►│
│ │
│ [hdr][KKTResponse][zeros] │
│◄──────────────────────────────────────│
│ │
│ [hdr][Noise1+PSQ][zeros] │
│──────────────────────────────────────►│ derive PSK
│ │
│ [hdr][encrypted Noise2][tag] │ ← authenticated
│◄──────────────────────────────────────│
│ │
│ [hdr][encrypted Noise3][tag] │ ← authenticated
│──────────────────────────────────────►│
│ │
│ ════════ Session Established ═════════│
│ │
│ [hdr][encrypted Data][tag] │
│◄─────────────────────────────────────►│
```
## Data Packet Encryption
Data packets have two encryption layers:
```
Application Data
┌─────────────────────┐
│ Noise encrypt │ Inner layer (forward secrecy, ratcheting)
│ (session keys) │
└─────────────────────┘
┌─────────────────────┐
│ PSK AEAD │ Outer layer (header auth, payload encryption)
│ (pre-shared key) │
└─────────────────────┘
Wire: [header][encrypted payload][tag]
```
### What Outer AEAD Encrypts
The outer AEAD encrypts: message_type (2B) + message content
This hides the message type from observers after PSK is available.
## Subsessions and Rekeying
Subsessions enable **forward secrecy** through periodic rekeying and **channel multiplexing** for independent encrypted streams.
### Design Principles
| Aspect | Decision | Rationale |
|--------|----------|-----------|
| Key derivation | Noise KK handshake | Clean crypto, both parties already authenticated |
| Initiation channel | Tunneled through parent | Already authenticated, no proof-of-ownership needed |
| Hierarchy | Promotion model (chain) | Simpler than tree, natural for rekeying |
| Old session after promotion | Read-only until TTL | Drains in-flight packets, provides grace period |
### Noise KK Pattern
Subsessions use `Noise_KK_25519_ChaChaPoly_SHA256`:
- **KK** = Both parties already know each other's static keys
- **2 messages** to complete (vs 3 for XKpsk3)
- **No PSK needed** - already authenticated via parent session
### Promotion Model
When a subsession is created, it becomes the new "master" and the old session becomes read-only:
```
Session A (master) → Session B created → A demoted, B is master
A: read-only until TTL
```
This creates a chain (A → B → C) but maintains only one level of nesting conceptually. Each promotion replaces the previous master.
### Protocol Flow
```
Client Gateway
│ │
│═══════ Parent Session (A) ════════│ Transport mode
│ │
│──[SubsessionRequest{idx=B}]──────►│ Encrypted in parent
│ │ Gateway creates KK responder
│◄──[SubsessionKK1{idx=B, e}]───────│ KK handshake msg 1
│──[SubsessionKK2{idx=B, e,ee,se}]─►│ KK handshake msg 2
│◄──[SubsessionReady{idx=B}]────────│ Subsession established
│ │
│ Session A: read-only (receive) │
│═══════ Session B (new master) ════│ New Transport mode
```
### Session State Transitions
```
Parent Session (A):
Transport → ReadOnlyTransport (on subsession creation)
ReadOnlyTransport → (expires via TTL cleanup)
Subsession (B):
(created) → KKHandshaking → Transport (becomes new master)
```
### Read-Only Session Semantics
After demotion:
- **Can receive**: Decrypt and process incoming packets (drain in-flight)
- **Cannot send**: Encryption blocked, returns error
- **Cleaned up**: Via normal TTL expiration
### Message Formats
```rust
SubsessionRequestData {
new_receiver_index: u32, // Client-proposed index for subsession
}
SubsessionKK1Data {
new_receiver_index: u32,
kk_message: Vec<u8>, // Noise KK message 1
}
SubsessionKK2Data {
new_receiver_index: u32,
kk_message: Vec<u8>, // Noise KK message 2
}
SubsessionReadyData {
new_receiver_index: u32,
}
```
### Counter Independence
- Each session has independent counters
- Subsession starts at counter 0
- No counter coordination needed between parent and subsession
### Failure Handling
| Scenario | Action |
|----------|--------|
| KK handshake fails | Discard attempt, keep using parent |
| Receiver index collision | Retry with new receiver_index |
| Parent session not found | Return error, client reconnects |
### Security Benefits
1. **Forward secrecy**: Compromise of current keys doesn't expose past traffic
2. **Key rotation**: Periodic rekeying limits exposure window
3. **Channel isolation**: Independent streams can't cross-decrypt
## Security Properties
### Always Visible to Observer
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
- 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
| Property | Mechanism |
|----------|-----------|
| Confidentiality | ChaCha20 (outer) + Noise ChaCha20 (inner) |
| Integrity | Poly1305 (outer) + Noise Poly1305 (inner) |
| Replay protection | Counter validation (before decryption) |
| Forward secrecy | Noise session keys (inner) + subsession rekeying |
| Header authentication | AEAD associated data |
| Key rotation | Periodic subsession creation (Noise KK) |
## References
- WireGuard Protocol - Inspiration for receiver_index and packet simplicity
- Noise Protocol Framework - Inner encryption layer, KK pattern for subsessions
- RFC 8439 ChaCha20-Poly1305 - AEAD cipher
- Noise Explorer KK - https://noiseexplorer.com/patterns/KK/
+289 -51
View File
@@ -1,71 +1,309 @@
# Nym Lewes Protocol
The Lewes Protocol (LP) is a secure network communication protocol implemented in Rust. This README provides an overview of the protocol's session management and replay protection mechanisms.
The Lewes Protocol (LP) is a secure network communication protocol implemented in Rust. It provides authenticated, encrypted sessions with replay protection and supports nested session forwarding for privacy-preserving multi-hop connections.
## Architecture Overview
```
+-----------------+ +----------------+ +---------------+
| Transport Layer |<--->| LP Session |<--->| LP Codec |
| (UDP/TCP) | | - Replay prot. | | - Enc/dec only|
+-----------------+ | - Crypto state | +---------------+
+----------------+
┌─────────────────┐ ┌────────────────┐ ┌───────────────┐
Transport Layer │◄───►│ LP Session │◄───►│ LP Codec
(TCP) - State machine│ - Serialize │
└─────────────────┘ - Noise crypto │ │ - Deserialize │
│ - Replay prot. │ └───────────────┘
└────────────────┘
```
## Packet Structure
The protocol uses a structured packet format:
The protocol uses a length-prefixed packet format over TCP:
```
+------------------+-------------------+------------------+
| Header (16B) | Message | Trailer (16B) |
| - Version (1B) | - Type (2B) | - Authentication |
| - Reserved (3B) | - Content | - tag/MAC |
| - SenderIdx (4B) | | |
| - Counter (8B) | | |
+------------------+-------------------+------------------+
Wire Format:
┌────────────────────┬─────────────────────────────────────────┐
│ Length (4B BE u32) │ LpPacket │
└────────────────────┴─────────────────────────────────────────┘
LpPacket:
┌──────────────────┬───────────────────┬──────────────────┐
│ Header (16B) │ Message │ Trailer (16B) │
├──────────────────┼───────────────────┼──────────────────┤
│ Version (1B) │ Type (2B LE u16) │ Reserved │
│ Reserved (3B) │ Content (var) │ (16 bytes) │
│ SessionID (4B LE)│ │ │
│ Counter (8B LE) │ │ │
└──────────────────┴───────────────────┴──────────────────┘
```
- Header contains protocol version, sender identification, and counter for replay protection
- Message carries the actual payload with a type identifier
- Trailer provides authentication and integrity verification (16 bytes)
- Total packet size is constrained by MTU (1500 bytes), accounting for network overhead
- **Header**: Protocol version (1), session identifier, monotonic counter
- **Message**: Type discriminant + variable-length content
- **Trailer**: Reserved for future use (16 bytes)
## Message Types
| Type | Value | Purpose |
|------|-------|---------|
| `Busy` | 0x0000 | Server congestion signal |
| `Handshake` | 0x0001 | Noise protocol handshake messages |
| `EncryptedData` | 0x0002 | Encrypted application data |
| `ClientHello` | 0x0003 | Initial session negotiation |
| `KKTRequest` | 0x0004 | KEM Key Transfer request |
| `KKTResponse` | 0x0005 | KEM Key Transfer response |
| `ForwardPacket` | 0x0006 | Nested session forwarding |
## Session Establishment
### Session ID
Sessions are identified by a deterministic 32-bit ID computed from both parties' X25519 public keys:
```
session_id = make_lp_id(client_x25519_pub, gateway_x25519_pub)
```
The computation is order-independent, allowing both sides to derive the same ID independently.
**BOOTSTRAP_SESSION_ID (0)**: A special session ID used only for the initial `ClientHello` packet, since neither side can compute the final ID until both X25519 keys are known.
### Handshake Flow
```
┌────────┐ ┌─────────┐
│ Client │ │ Gateway │
└───┬────┘ └────┬────┘
│ │
│ 1. ClientHello (session_id=0) │
│ [client_x25519, client_ed25519, salt]│
│───────────────────────────────────────►│
│ │ (computes session_id)
│ │ (stores state machine)
│ │
│ 2. KKTRequest (session_id=N) │
│ [signed request for KEM key] │
│───────────────────────────────────────►│
│ │
│ 3. KKTResponse │
│ [gateway KEM key + signature] │
│◄───────────────────────────────────────│
│ │
│ 4. Noise Handshake Msg 1 │
│ [PSQ payload + noise message] │
│───────────────────────────────────────►│
│ │ (derives PSK from PSQ)
│ 5. Noise Handshake Msg 2 │
│ [PSK handle + noise message] │
│◄───────────────────────────────────────│
│ │
│ 6. Noise Handshake Msg 3 │
│───────────────────────────────────────►│
│ │
│ ═══════ Session Established ═══════ │
│ │
│ 7. EncryptedData │
│ [encrypted application data] │
│◄──────────────────────────────────────►│
│ │
```
### ClientHello Data
```rust
struct ClientHelloData {
client_lp_public_key: [u8; 32], // X25519 (derived from Ed25519)
client_ed25519_public_key: [u8; 32], // For authentication
salt: [u8; 32], // timestamp (8B) + nonce (24B)
}
```
## Packet-Per-Connection Model
The gateway processes **exactly one packet per TCP connection**, then closes. State persists between connections via in-memory maps:
```
TCP Connect → Receive Packet → Process → Send Response → TCP Close
```
**State Storage:**
- `handshake_states`: Maps `session_id → LpStateMachine` (during handshake)
- `session_states`: Maps `session_id → LpSession` (after handshake complete)
Both maps use TTL-based cleanup to remove stale entries (default: 5 min handshake, 1 hour session).
### Gateway Packet Routing
```
Packet Received
├─► session_id == 0 (BOOTSTRAP)
│ └─► handle_client_hello()
│ └─► Create state machine, store in handshake_states
├─► session_id in handshake_states
│ └─► handle_handshake_packet()
│ └─► Process KKT/Noise, move to session_states when complete
└─► session_id in session_states
└─► handle_transport_packet()
└─► Decrypt, process registration or forwarding
```
## Session Forwarding
Forwarding enables a client to establish an independent session with an exit gateway through an entry gateway, providing network-level privacy.
### Architecture
```
┌──────────┐
│ Client │
└────┬─────┘
│ Outer LP Session (established, encrypted)
┌────────────────┐
│ Entry Gateway │ Sees: Client IP
│ │ Doesn't see: Exit destination
└────────┬───────┘
│ Forwards inner packets (TCP)
┌────────────────┐
│ Exit Gateway │ Sees: Entry Gateway IP
│ │ Doesn't see: Client IP
└────────────────┘
```
### ForwardPacket Message
```rust
struct ForwardPacketData {
target_gateway_identity: [u8; 32], // Exit gateway's Ed25519 key
target_lp_address: String, // e.g., "2.2.2.2:41264"
inner_packet_bytes: Vec<u8>, // Complete LP packet for exit
}
```
### Forwarding Flow
1. **Client** establishes outer LP session with entry gateway
2. **Client** creates `ClientHello` packet for exit gateway
3. **Client** wraps inner packet in `ForwardPacketData`:
- Sets `target_gateway_identity` to exit's Ed25519 key
- Sets `target_lp_address` to exit's LP listener address
- Serializes complete LP packet as `inner_packet_bytes`
4. **Client** encrypts `ForwardPacketData` using outer session
5. **Client** sends as `EncryptedData` to entry gateway
6. **Entry Gateway** decrypts, sees `ForwardPacketData`
7. **Entry Gateway** connects to exit gateway (new TCP)
8. **Entry Gateway** sends `inner_packet_bytes` directly
9. **Entry Gateway** receives exit's response
10. **Entry Gateway** encrypts response using outer session
11. **Entry Gateway** sends encrypted response to client
12. **Client** decrypts response, processes in inner session state
### NestedLpSession
The `NestedLpSession` struct manages the inner session from the client's perspective:
```rust
struct NestedLpSession {
exit_identity: [u8; 32], // Exit gateway Ed25519
exit_address: String, // Exit LP address
client_keypair: Arc<ed25519::KeyPair>,
exit_public_key: ed25519::PublicKey,
state_machine: Option<LpStateMachine>,
}
```
**Usage:**
```rust
// Create nested session targeting exit gateway
let nested = NestedLpSession::new(exit_identity, exit_address, keypair, exit_pubkey);
// Perform handshake through outer session
nested.handshake_and_register(&mut outer_client).await?;
// Inner session now established with exit gateway
```
## State Machine States
```
ReadyToHandshake
KKTExchange ◄─── KKTRequest/KKTResponse
Handshaking ◄─── Noise messages + PSQ
Transport ◄─── EncryptedData
Closed
```
## Cryptography
### Key Types
- **Ed25519**: Identity keys, signing
- **X25519**: Key exchange (derived from Ed25519 via RFC 7748)
### Noise Protocol
- Pattern: `Noise_XKpsk3_25519_ChaChaPoly_SHA256`
- Provides: Forward secrecy, mutual authentication, PSK binding
### PSK Derivation (PSQ)
The Pre-Shared Key is derived via Post-Quantum Secure Key Exchange:
1. Client encapsulates using authenticated KEM key from KKT
2. Produces 32-byte PSK + ciphertext
3. Gateway decapsulates to derive same PSK
4. PSK injected into Noise at position 3
### Replay Protection
- **Monotonic counter**: Each packet has incrementing 64-bit counter
- **Sliding window**: Bitmap tracks received counters (1024 packet window)
- **SIMD optimized**: Branchless validation for constant-time operation
```rust
// Validation flow
validator.will_accept_branchless(counter) // Check before decrypt
validator.mark_did_receive_branchless(counter) // Mark after decrypt
```
## Sessions
- Sessions are managed by `LPSession` and `SessionManager` classes
- Each session has unique receiving and sending indices to identify connections
- Sessions maintain:
- Cryptographic state (currently mocked implementations)
- Counter for outgoing packets
- Replay protection mechanism for incoming packets
### LpSession Fields
```rust
struct LpSession {
id: u32, // Session identifier
is_initiator: bool, // Client or gateway role
noise_state: NoiseState, // Noise transport state
kkt_state: KktState, // KKT exchange progress
psq_state: PsqState, // PSQ handshake progress
psk_handle: Option<Vec<u8>>,// PSK handle from responder
sending_counter: AtomicU64, // Outgoing packet counter
receiving_counter: Validator, // Replay protection
psk_injected: AtomicBool, // Safety: real PSK injected?
}
```
## Session Management
### PSK Safety
Sessions initialize with a dummy PSK. The `psk_injected` flag must be `true` before `encrypt_data()` or `decrypt_data()` will operate, preventing accidental use of the insecure dummy.
- `SessionManager` handles session lifecycle (creation, retrieval, removal)
- Sessions are stored in a thread-safe HashMap indexed by receiving index
- The manager generates unique indices for new sessions
- Sessions are Arc-wrapped for safe concurrent access
## File Structure
## Replay Protection
- Implemented in the `ReceivingKeyCounterValidator` class
- Uses a bitmap-based approach to track received packet counters
- The bitmap allows reordering of up to 1024 packets (configurable)
- SIMD optimizations are used when available for performance
## Replay Protection Process
1. Quick validation (`will_accept_branchless`):
- Checks if counter is valid before expensive operations
- Detects duplicates, out-of-window packets
2. Marking packets (`mark_did_receive_branchless`):
- Updates the bitmap to mark counter as received
- Updates statistics and sliding window as needed
3. Window Sliding:
- Automatically slides the acceptance window when new higher counters arrive
- Clears bits for old counters that fall outside the window
This architecture effectively prevents replay attacks while allowing some packet reordering, an essential feature for secure network protocols.
```
common/nym-lp/src/
├── lib.rs # Module exports
├── message.rs # LpMessage enum, ClientHelloData, ForwardPacketData
├── packet.rs # LpPacket, LpHeader, BOOTSTRAP_SESSION_ID
├── codec.rs # Serialization/deserialization
├── session.rs # LpSession, cryptographic operations
├── state_machine.rs # LpStateMachine, state transitions
├── psk.rs # PSK derivation utilities
└── error.rs # Error types
```
+902 -130
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -78,4 +78,8 @@ pub enum LpError {
/// Ed25519 to X25519 conversion error.
#[error("Ed25519 key conversion error: {0}")]
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
/// Outer AEAD authentication tag verification failed.
#[error("AEAD authentication tag verification failed")]
AeadTagMismatch,
}
+35 -85
View File
@@ -14,12 +14,9 @@ pub mod session;
mod session_integration;
pub mod session_manager;
use std::hash::{DefaultHasher, Hasher as _};
pub use error::LpError;
use keypair::PublicKey;
pub use message::{ClientHelloData, LpMessage};
pub use packet::LpPacket;
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;
@@ -33,13 +30,15 @@ pub const NOISE_PSK_INDEX: u8 = 3;
#[cfg(test)]
pub fn sessions_for_tests() -> (LpSession, LpSession) {
use crate::{keypair::Keypair, make_lp_id};
use crate::keypair::Keypair;
use nym_crypto::asymmetric::ed25519;
// X25519 keypairs for Noise protocol
let keypair_1 = Keypair::default();
let keypair_2 = Keypair::default();
let id = make_lp_id(keypair_1.public_key(), keypair_2.public_key());
// Use a fixed receiver_index for deterministic tests
let receiver_index: u32 = 12345;
// Ed25519 keypairs for PSQ authentication (placeholders for testing)
let ed25519_keypair_1 = ed25519::KeyPair::from_secret([1u8; 32], 0);
@@ -51,7 +50,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
// PSQ will always derive the PSK during handshake using X25519 as DHKEM
let initiator_session = LpSession::new(
id,
receiver_index,
true,
(
ed25519_keypair_1.private_key(),
@@ -65,7 +64,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
.expect("Test session creation failed");
let responder_session = LpSession::new(
id,
receiver_index,
false,
(
ed25519_keypair_2.private_key(),
@@ -81,47 +80,12 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
(initiator_session, responder_session)
}
/// Generates a deterministic u32 session ID for the Lewes Protocol
/// based on two public keys. The order of the keys does not matter.
///
/// Uses a different internal delimiter than `make_conv_id` to avoid
/// potential collisions if the same key pairs were used in both contexts.
fn make_id(key1_bytes: &[u8], key2_bytes: &[u8], sep: u8) -> u32 {
let mut hasher = DefaultHasher::new();
// Ensure consistent order for hashing to make the ID order-independent.
// This guarantees make_lp_id(a, b) == make_lp_id(b, a).
if key1_bytes < key2_bytes {
hasher.write(key1_bytes);
// Use a delimiter specific to Lewes Protocol ID generation
// (0xCC chosen arbitrarily, could be any value different from 0xFF)
hasher.write_u8(sep);
hasher.write(key2_bytes);
} else {
hasher.write(key2_bytes);
hasher.write_u8(sep);
hasher.write(key1_bytes);
}
// Truncate the u64 hash result to u32
(hasher.finish() & 0xFFFF_FFFF) as u32
}
pub fn make_lp_id(key1_bytes: &PublicKey, key2_bytes: &PublicKey) -> u32 {
make_id(key1_bytes.as_bytes(), key2_bytes.as_bytes(), 0xCC)
}
pub fn make_conv_id(src: &[u8], dst: &[u8]) -> u32 {
make_id(src, dst, 0xFF)
}
#[cfg(test)]
mod tests {
use crate::keypair::PublicKey;
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::session_manager::SessionManager;
use crate::{LpError, make_lp_id, sessions_for_tests};
use crate::{LpError, sessions_for_tests};
use bytes::BytesMut;
// Import the new standalone functions
@@ -137,7 +101,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42, // Matches session's sending_index assumption for this test
receiver_idx: 42, // Matches session's sending_index assumption for this test
counter: 0,
},
message: LpMessage::Busy,
@@ -146,10 +110,10 @@ mod tests {
// Serialize packet
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1).unwrap();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
// Parse packet
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Perform replay check (should pass)
session
@@ -166,7 +130,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 0, // Same counter as before (replay)
},
message: LpMessage::Busy,
@@ -175,10 +139,10 @@ mod tests {
// Serialize packet
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2).unwrap();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// Parse packet
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Perform replay check (should fail)
let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter);
@@ -196,7 +160,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 1, // Incremented counter
},
message: LpMessage::Busy,
@@ -205,10 +169,10 @@ mod tests {
// Serialize packet
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3).unwrap();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// Parse packet
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// Perform replay check (should pass)
session
@@ -238,24 +202,8 @@ mod tests {
let ed25519_keypair_local = ed25519::KeyPair::from_secret([8u8; 32], 0);
let ed25519_keypair_remote = ed25519::KeyPair::from_secret([9u8; 32], 1);
// Derive X25519 keys from Ed25519 (same as state machine does internally)
let x25519_pub_local = ed25519_keypair_local
.public_key()
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
let x25519_pub_remote = ed25519_keypair_remote
.public_key()
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair types
let lp_pub_local = PublicKey::from_bytes(x25519_pub_local.as_bytes())
.expect("Failed to create PublicKey from bytes");
let lp_pub_remote = PublicKey::from_bytes(x25519_pub_remote.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_local, &lp_pub_remote);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
// Test salt
let salt = [46u8; 32];
@@ -263,6 +211,7 @@ mod tests {
// Create a session via manager
let _ = local_manager
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_local.private_key(),
ed25519_keypair_local.public_key(),
@@ -275,6 +224,7 @@ mod tests {
let _ = remote_manager
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_remote.private_key(),
ed25519_keypair_remote.public_key(),
@@ -289,7 +239,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 0,
},
message: LpMessage::Busy,
@@ -298,10 +248,10 @@ mod tests {
// Serialize
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1).unwrap();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
// Parse
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Process via SessionManager method (which should handle checks + marking)
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
@@ -310,11 +260,11 @@ mod tests {
// Perform replay check
local_manager
.receiving_counter_quick_check(lp_id, parsed_packet1.header.counter)
.receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter)
.expect("Packet 1 check failed");
// Mark received
local_manager
.receiving_counter_mark(lp_id, parsed_packet1.header.counter)
.receiving_counter_mark(receiver_index, parsed_packet1.header.counter)
.expect("Packet 1 mark failed");
// === Packet 2 (Counter 1 - Should succeed on same session) ===
@@ -322,7 +272,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 1,
},
message: LpMessage::Busy,
@@ -331,18 +281,18 @@ mod tests {
// Serialize
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2).unwrap();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// Parse
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Perform replay check
local_manager
.receiving_counter_quick_check(lp_id, parsed_packet2.header.counter)
.receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter)
.expect("Packet 2 check failed");
// Mark received
local_manager
.receiving_counter_mark(lp_id, parsed_packet2.header.counter)
.receiving_counter_mark(receiver_index, parsed_packet2.header.counter)
.expect("Packet 2 mark failed");
// === Packet 3 (Counter 0 - Replay, should fail check) ===
@@ -350,7 +300,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 0, // Replay of first packet
},
message: LpMessage::Busy,
@@ -359,14 +309,14 @@ mod tests {
// Serialize
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3).unwrap();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// Parse
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// Perform replay check (should fail)
let replay_result =
local_manager.receiving_counter_quick_check(lp_id, parsed_packet3.header.counter);
local_manager.receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter);
assert!(replay_result.is_err());
match replay_result.unwrap_err() {
LpError::Replay(e) => {
+141 -3
View File
@@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
pub receiver_index: u32,
/// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key
pub client_lp_public_key: [u8; 32],
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
@@ -46,6 +49,7 @@ impl ClientHelloData {
rand::thread_rng().fill_bytes(&mut salt[8..]);
Self {
receiver_index: rand::random(), // Auto-generate random receiver index
client_lp_public_key,
client_ed25519_public_key,
salt,
@@ -72,6 +76,21 @@ pub enum MessageType {
ClientHello = 0x0003,
KKTRequest = 0x0004,
KKTResponse = 0x0005,
ForwardPacket = 0x0006,
/// Receiver index collision - client should retry with new index
Collision = 0x0007,
/// Acknowledgment - gateway confirms receipt of message
Ack = 0x0008,
/// Subsession request - client initiates subsession creation
SubsessionRequest = 0x0009,
/// Subsession KK1 - first message of Noise KK handshake
SubsessionKK1 = 0x000A,
/// Subsession KK2 - second message of Noise KK handshake
SubsessionKK2 = 0x000B,
/// Subsession ready - subsession established confirmation
SubsessionReady = 0x000C,
/// Subsession abort - race winner tells loser to become responder
SubsessionAbort = 0x000D,
}
impl MessageType {
@@ -98,6 +117,41 @@ pub struct KKTRequestData(pub Vec<u8>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KKTResponseData(pub Vec<u8>);
/// Packet forwarding request with embedded inner LP packet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForwardPacketData {
/// Target gateway's Ed25519 identity (32 bytes)
pub target_gateway_identity: [u8; 32],
/// Target gateway's LP address (IP:port string)
pub target_lp_address: String,
/// Complete inner LP packet bytes (serialized LpPacket)
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
pub inner_packet_bytes: Vec<u8>,
}
/// Subsession KK1 message - first message of Noise KK handshake
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubsessionKK1Data {
/// Noise KK first message payload (ephemeral key + encrypted static)
pub payload: Vec<u8>,
}
/// Subsession KK2 message - second message of Noise KK handshake
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubsessionKK2Data {
/// Noise KK second message payload (ephemeral key + encrypted response)
pub payload: Vec<u8>,
}
/// Subsession ready confirmation with new session index
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SubsessionReadyData {
/// New subsession's receiver index for routing
pub receiver_index: u32,
}
#[derive(Debug, Clone)]
pub enum LpMessage {
Busy,
@@ -106,6 +160,21 @@ pub enum LpMessage {
ClientHello(ClientHelloData),
KKTRequest(KKTRequestData),
KKTResponse(KKTResponseData),
ForwardPacket(ForwardPacketData),
/// Receiver index collision - client should retry with new receiver_index
Collision,
/// Acknowledgment - gateway confirms receipt of message
Ack,
/// Subsession request - client initiates subsession creation (empty, signal only)
SubsessionRequest,
/// Subsession KK1 - first message of Noise KK handshake
SubsessionKK1(SubsessionKK1Data),
/// Subsession KK2 - second message of Noise KK handshake
SubsessionKK2(SubsessionKK2Data),
/// Subsession ready - subsession established confirmation
SubsessionReady(SubsessionReadyData),
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
SubsessionAbort,
}
impl Display for LpMessage {
@@ -117,6 +186,14 @@ impl Display for LpMessage {
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
LpMessage::KKTResponse(_) => write!(f, "KKTResponse"),
LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"),
LpMessage::Collision => write!(f, "Collision"),
LpMessage::Ack => write!(f, "Ack"),
LpMessage::SubsessionRequest => write!(f, "SubsessionRequest"),
LpMessage::SubsessionKK1(_) => write!(f, "SubsessionKK1"),
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
}
}
}
@@ -127,9 +204,17 @@ impl LpMessage {
LpMessage::Busy => &[],
LpMessage::Handshake(payload) => payload.0.as_slice(),
LpMessage::EncryptedData(payload) => payload.0.as_slice(),
LpMessage::ClientHello(_) => unimplemented!(), // Structured data, serialized in encode_content
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
LpMessage::Collision => &[],
LpMessage::Ack => &[],
LpMessage::SubsessionRequest => &[],
LpMessage::SubsessionKK1(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionAbort => &[],
}
}
@@ -141,6 +226,14 @@ impl LpMessage {
LpMessage::ClientHello(_) => false, // Always has data
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
LpMessage::KKTResponse(payload) => payload.0.is_empty(),
LpMessage::ForwardPacket(_) => false, // Always has data
LpMessage::Collision => true,
LpMessage::Ack => true,
LpMessage::SubsessionRequest => true, // Empty signal
LpMessage::SubsessionKK1(_) => false, // Always has payload
LpMessage::SubsessionKK2(_) => false, // Always has payload
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
LpMessage::SubsessionAbort => true, // Empty signal
}
}
@@ -149,9 +242,22 @@ impl LpMessage {
LpMessage::Busy => 0,
LpMessage::Handshake(payload) => payload.0.len(),
LpMessage::EncryptedData(payload) => payload.0.len(),
LpMessage::ClientHello(_) => 97, // 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + 1 byte bincode overhead
// 4 bytes receiver_index + 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + bincode overhead
LpMessage::ClientHello(_) => 101,
LpMessage::KKTRequest(payload) => payload.0.len(),
LpMessage::KKTResponse(payload) => payload.0.len(),
LpMessage::ForwardPacket(data) => {
32 + data.target_lp_address.len() + data.inner_packet_bytes.len() + 10
}
LpMessage::Collision => 0,
LpMessage::Ack => 0,
LpMessage::SubsessionRequest => 0,
// Variable length: bincode overhead (~8 bytes for Vec length) + payload
LpMessage::SubsessionKK1(data) => 8 + data.payload.len(),
LpMessage::SubsessionKK2(data) => 8 + data.payload.len(),
// 4 bytes u32 + bincode overhead (~4 bytes)
LpMessage::SubsessionReady(_) => 8,
LpMessage::SubsessionAbort => 0,
}
}
@@ -163,6 +269,14 @@ impl LpMessage {
LpMessage::ClientHello(_) => MessageType::ClientHello,
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
LpMessage::KKTResponse(_) => MessageType::KKTResponse,
LpMessage::ForwardPacket(_) => MessageType::ForwardPacket,
LpMessage::Collision => MessageType::Collision,
LpMessage::Ack => MessageType::Ack,
LpMessage::SubsessionRequest => MessageType::SubsessionRequest,
LpMessage::SubsessionKK1(_) => MessageType::SubsessionKK1,
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
}
}
@@ -187,6 +301,30 @@ impl LpMessage {
LpMessage::KKTResponse(payload) => {
dst.put_slice(&payload.0);
}
LpMessage::ForwardPacket(data) => {
let serialized =
bincode::serialize(data).expect("Failed to serialize ForwardPacketData");
dst.put_slice(&serialized);
}
LpMessage::Collision => { /* No content */ }
LpMessage::Ack => { /* No content */ }
LpMessage::SubsessionRequest => { /* No content - signal only */ }
LpMessage::SubsessionKK1(data) => {
let serialized =
bincode::serialize(data).expect("Failed to serialize SubsessionKK1Data");
dst.put_slice(&serialized);
}
LpMessage::SubsessionKK2(data) => {
let serialized =
bincode::serialize(data).expect("Failed to serialize SubsessionKK2Data");
dst.put_slice(&serialized);
}
LpMessage::SubsessionReady(data) => {
let serialized =
bincode::serialize(data).expect("Failed to serialize SubsessionReadyData");
dst.put_slice(&serialized);
}
LpMessage::SubsessionAbort => { /* No content - signal only */ }
}
}
}
@@ -204,7 +342,7 @@ mod tests {
let resp_header = LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 0,
receiver_idx: 0,
counter: 0,
};
+3
View File
@@ -25,6 +25,9 @@ pub enum NoiseError {
#[error("Other Noise-related error: {0}")]
Other(String),
#[error("session is read-only after demotion")]
SessionReadOnly,
}
impl From<snow::Error> for NoiseError {
+72 -11
View File
@@ -122,12 +122,73 @@ impl LpPacket {
}
}
// VERSION [1B] || RESERVED [3B] || SENDER_INDEX [4B] || COUNTER [8B]
/// Session ID used for ClientHello bootstrap packets before session is established.
///
/// When a client first connects, it sends a ClientHello packet with receiver_idx=0
/// because neither side can compute the deterministic session ID yet (requires
/// both parties' X25519 keys). After ClientHello is processed, both sides derive
/// the same session ID from their keys, and all subsequent packets use that ID.
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
/// 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,
pub reserved: u16,
pub session_id: u32,
pub receiver_idx: u32,
pub counter: u64,
}
@@ -136,11 +197,11 @@ impl LpHeader {
}
impl LpHeader {
pub fn new(session_id: u32, counter: u64) -> Self {
pub fn new(receiver_idx: u32, counter: u64) -> Self {
Self {
protocol_version: 1,
reserved: 0,
session_id,
receiver_idx,
counter,
}
}
@@ -153,7 +214,7 @@ impl LpHeader {
dst.put_slice(&[0, 0, 0]);
// sender index
dst.put_slice(&self.session_id.to_le_bytes());
dst.put_slice(&self.receiver_idx.to_le_bytes());
// counter
dst.put_slice(&self.counter.to_le_bytes());
@@ -167,9 +228,9 @@ impl LpHeader {
let protocol_version = src[0];
// Skip reserved bytes [1..4]
let mut session_id_bytes = [0u8; 4];
session_id_bytes.copy_from_slice(&src[4..8]);
let session_id = u32::from_le_bytes(session_id_bytes);
let mut receiver_idx_bytes = [0u8; 4];
receiver_idx_bytes.copy_from_slice(&src[4..8]);
let receiver_idx = u32::from_le_bytes(receiver_idx_bytes);
let mut counter_bytes = [0u8; 8];
counter_bytes.copy_from_slice(&src[8..16]);
@@ -178,7 +239,7 @@ impl LpHeader {
Ok(LpHeader {
protocol_version,
reserved: 0,
session_id,
receiver_idx,
counter,
})
}
@@ -189,8 +250,8 @@ impl LpHeader {
}
/// Get the sender index from the header
pub fn session_id(&self) -> u32 {
self.session_id
pub fn receiver_idx(&self) -> u32 {
self.receiver_idx
}
}
+88 -12
View File
@@ -57,6 +57,43 @@ const PSK_PSQ_CONTEXT: &str = "nym-lp-psk-psq-v1";
/// Session context for PSQ protocol.
const PSQ_SESSION_CONTEXT: &[u8] = b"nym-lp-psq-session";
/// Context string for subsession PSK derivation.
const SUBSESSION_PSK_CONTEXT: &str = "lp-subsession-psk-v1";
/// Result from PSQ initiator message creation.
///
/// Contains all outputs needed for session establishment:
/// - `psk`: Final derived PSK for Noise handshake (ECDH || K_pq || salt → Blake3)
/// - `payload`: Serialized PSQ message to send to responder
/// - `pq_shared_secret`: Raw K_pq from KEM encapsulation (for subsession derivation)
#[derive(Debug)]
pub struct PsqInitiatorResult {
/// Final PSK for Noise XKpsk3 handshake
pub psk: [u8; 32],
/// Serialized PSQ payload to embed in handshake message
pub payload: Vec<u8>,
/// Raw PQ shared secret (K_pq) before KDF combination.
/// Used for deriving subsession PSKs to preserve PQ protection.
pub pq_shared_secret: [u8; 32],
}
/// Result from PSQ responder message processing.
///
/// Contains all outputs needed for session establishment:
/// - `psk`: Final derived PSK for Noise handshake (matches initiator's)
/// - `psk_handle`: Encrypted PSK handle (ctxt_B) to send back to initiator
/// - `pq_shared_secret`: Raw K_pq from KEM decapsulation (for subsession derivation)
#[derive(Debug)]
pub struct PsqResponderResult {
/// Final PSK for Noise XKpsk3 handshake
pub psk: [u8; 32],
/// Encrypted PSK handle (ctxt_B) from PSQ responder message
pub psk_handle: Vec<u8>,
/// Raw PQ shared secret (K_pq) before KDF combination.
/// Used for deriving subsession PSKs to preserve PQ protection.
pub pq_shared_secret: [u8; 32],
}
/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Initiator side.
///
/// This function combines classical ECDH with post-quantum KEM to provide forward secrecy
@@ -230,7 +267,7 @@ pub fn derive_psk_with_psq_responder(
/// * `session_context` - Context bytes for PSQ (e.g., b"nym-lp-psq-session")
///
/// # Returns
/// `(psk, psq_payload_bytes)` - PSK for Noise and serialized PSQ payload to embed
/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret
pub fn psq_initiator_create_message(
local_x25519_private: &PrivateKey,
remote_x25519_public: &PublicKey,
@@ -239,7 +276,7 @@ pub fn psq_initiator_create_message(
client_ed25519_pk: &ed25519::PublicKey,
salt: &[u8; 32],
session_context: &[u8],
) -> Result<([u8; 32], Vec<u8>), LpError> {
) -> Result<PsqInitiatorResult, LpError> {
// Step 1: Classical ECDH for baseline security
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
@@ -278,9 +315,13 @@ pub fn psq_initiator_create_message(
LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e))
})?;
// Extract PSQ shared secret (unregistered PSK)
// Extract PSQ shared secret (unregistered PSK) - this is K_pq
let psq_psk = state.unregistered_psk();
// pq_shared_secret is the raw K_pq from KEM encapsulation.
// Store it for subsession derivation before it's combined with ECDH.
let pq_shared_secret: [u8; 32] = *psq_psk;
// Step 3: Combine ECDH + PSQ via Blake3 KDF
let mut combined = Vec::with_capacity(64 + psq_psk.len());
combined.extend_from_slice(ecdh_secret.as_bytes());
@@ -294,7 +335,11 @@ pub fn psq_initiator_create_message(
.tls_serialize_detached()
.map_err(|e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e)))?;
Ok((final_psk, msg_bytes))
Ok(PsqInitiatorResult {
psk: final_psk,
payload: msg_bytes,
pq_shared_secret,
})
}
/// PSQ protocol wrapper for responder (gateway) side.
@@ -317,11 +362,7 @@ pub fn psq_initiator_create_message(
/// * `session_context` - Context bytes for PSQ
///
/// # Returns
/// `psk` - Derived PSK for Noise
/// Processes a PSQ initiator message and generates a PSK with encrypted handle.
///
/// Returns a tuple of (derived_psk, responder_msg_bytes) where responder_msg_bytes
/// contains the encrypted PSK handle (ctxt_B) that should be sent to the initiator.
/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret
pub fn psq_responder_process_message(
local_x25519_private: &PrivateKey,
remote_x25519_public: &PublicKey,
@@ -330,7 +371,7 @@ pub fn psq_responder_process_message(
psq_payload: &[u8],
salt: &[u8; 32],
session_context: &[u8],
) -> Result<([u8; 32], Vec<u8>), LpError> {
) -> Result<PsqResponderResult, LpError> {
// Step 1: Classical ECDH for baseline security
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
@@ -383,9 +424,13 @@ pub fn psq_responder_process_message(
LpError::Internal(format!("PSQ v1 responder send failed: {:?}", e))
})?;
// Extract the PSQ PSK from the registered PSK
// Extract the PSQ PSK from the registered PSK - this is K_pq
let psq_psk = registered_psk.psk;
// pq_shared_secret is the raw K_pq from KEM decapsulation.
// Store it for subsession derivation before it's combined with ECDH.
let pq_shared_secret: [u8; 32] = psq_psk;
// Step 6: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator)
let mut combined = Vec::with_capacity(64 + psq_psk.len());
combined.extend_from_slice(ecdh_secret.as_bytes());
@@ -400,7 +445,38 @@ pub fn psq_responder_process_message(
.tls_serialize_detached()
.map_err(|e| LpError::Internal(format!("ResponderMsg serialization failed: {:?}", e)))?;
Ok((final_psk, responder_msg_bytes))
Ok(PsqResponderResult {
psk: final_psk,
psk_handle: responder_msg_bytes,
pq_shared_secret,
})
}
/// Derive subsession PSK from parent's PQ shared secret.
///
/// Uses Blake3 KDF with domain separation to derive unique PSK for each subsession.
/// This preserves PQ protection: subsession keys inherit quantum resistance from
/// parent's KEM shared secret (K_pq).
///
/// # Security Model
///
/// Subsessions use Noise KKpsk0 pattern where:
/// - Both parties already know each other's static X25519 keys (from parent session)
/// - PSK provides PQ protection by deriving from parent's K_pq
/// - Each subsession gets unique PSK via index parameter (prevents key reuse)
///
/// # Arguments
/// * `pq_shared_secret` - Parent session's K_pq (32 bytes from KEM)
/// * `subsession_index` - Monotonic index for this subsession (prevents reuse)
///
/// # Returns
/// 32-byte PSK for Noise KKpsk0 handshake
pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] {
nym_crypto::kdf::derive_key_blake3(
SUBSESSION_PSK_CONTEXT,
pq_shared_secret,
&subsession_index.to_le_bytes(),
)
}
#[cfg(test)]
+506 -34
View File
@@ -6,11 +6,12 @@
//! This module implements session management functionality, including replay protection
//! and Noise protocol state handling.
use crate::codec::OuterAeadKey;
use crate::keypair::{PrivateKey, PublicKey};
use crate::message::{EncryptedDataPayload, HandshakeData};
use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult};
use crate::packet::LpHeader;
use crate::psk::{psq_initiator_create_message, psq_responder_process_message};
use crate::psk::{derive_subsession_psk, psq_initiator_create_message, psq_responder_process_message};
use crate::replay::ReceivingKeyCounterValidator;
use crate::{LpError, LpMessage, LpPacket};
use nym_crypto::asymmetric::ed25519;
@@ -18,6 +19,30 @@ use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
use parking_lot::Mutex;
use snow::Builder;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use zeroize::{Zeroize, ZeroizeOnDrop};
/// PQ shared secret wrapper with automatic memory zeroization.
/// Ensures K_pq is cleared from memory when dropped.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct PqSharedSecret([u8; 32]);
impl PqSharedSecret {
pub fn new(secret: [u8; 32]) -> Self {
Self(secret)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Debug for PqSharedSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PqSharedSecret")
.field("secret", &"<redacted>")
.finish()
}
}
/// KKT (KEM Key Transfer) exchange state.
///
@@ -165,6 +190,29 @@ pub struct LpSession {
/// Salt for PSK derivation
salt: [u8; 32],
/// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake).
/// None before PSK is available, Some after PSK injection.
outer_aead_key: Mutex<Option<OuterAeadKey>>,
/// Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation.
/// Stored after PSQ handshake completes for subsession PSK derivation.
/// This preserves PQ protection when creating subsessions via KKpsk0.
/// Wrapped in PqSharedSecret for automatic memory zeroization on drop.
pq_shared_secret: Mutex<Option<PqSharedSecret>>,
/// Monotonically increasing counter for subsession indices.
/// Each subsession gets a unique index to ensure unique PSK derivation.
/// Uses u64 to make overflow practically impossible (~585k years at 1M/sec).
subsession_counter: AtomicU64,
/// True if this session has been demoted to read-only mode.
/// Demoted sessions can still receive/decrypt but cannot send/encrypt.
read_only: AtomicBool,
/// ID of the successor session that replaced this one.
/// Set when demote() is called.
successor_session_id: Mutex<Option<u32>>,
}
/// Generates a fresh salt for PSK derivation.
@@ -217,6 +265,25 @@ impl LpSession {
self.local_x25519_private.public_key()
}
/// Returns the remote X25519 public key.
///
/// Used for tie-breaking in simultaneous subsession initiation.
/// Lower key loses and becomes responder.
pub fn remote_x25519_public(&self) -> &PublicKey {
&self.remote_x25519_public
}
/// Returns the outer AEAD key for packet encryption/decryption.
///
/// Returns `None` before PSK is derived (during initial handshake),
/// `Some(&OuterAeadKey)` after PSK injection via PSQ.
///
/// Callers should use `None` for packet encryption/decryption during
/// the handshake phase, and use the returned key for transport phase.
pub fn outer_aead_key(&self) -> Option<OuterAeadKey> {
self.outer_aead_key.lock().clone()
}
/// Creates a new session and initializes the Noise protocol state.
///
/// PSQ always runs during the handshake to derive the real PSK from X25519 DHKEM.
@@ -301,6 +368,11 @@ impl LpSession {
local_x25519_private: local_x25519_key.clone(),
remote_x25519_public: remote_x25519_key.clone(),
salt: *salt,
outer_aead_key: Mutex::new(None),
pq_shared_secret: Mutex::new(None),
subsession_counter: AtomicU64::new(0),
read_only: AtomicBool::new(false),
successor_session_id: Mutex::new(None),
})
}
@@ -615,7 +687,7 @@ impl LpSession {
// Generate PSQ payload and PSK using KKT-authenticated KEM key
let session_context = self.id.to_le_bytes();
let (psk, psq_payload) = match psq_initiator_create_message(
let psq_result = match psq_initiator_create_message(
&self.local_x25519_private,
&self.remote_x25519_public,
remote_kem,
@@ -630,6 +702,11 @@ impl LpSession {
return Some(Err(e));
}
};
let psk = psq_result.psk;
let psq_payload = psq_result.payload;
// Store PQ shared secret for subsession PSK derivation
*self.pq_shared_secret.lock() = Some(PqSharedSecret::new(psq_result.pq_shared_secret));
// Inject PSK into Noise HandshakeState
if let Err(e) = noise_state.set_psk(3, &psk) {
@@ -638,6 +715,12 @@ impl LpSession {
// Mark PSK as injected for safety checks in transport mode
self.psk_injected.store(true, Ordering::Release);
// Derive and store outer AEAD key from PSK
{
let mut outer_key = self.outer_aead_key.lock();
*outer_key = Some(OuterAeadKey::from_psk(&psk));
}
// Get the Noise handshake message
let noise_msg = match noise_state.get_bytes_to_send() {
Some(Ok(msg)) => msg,
@@ -774,7 +857,7 @@ impl LpSession {
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
let session_context = self.id.to_le_bytes();
let (psk, responder_msg_bytes) = match psq_responder_process_message(
let psq_result = match psq_responder_process_message(
&self.local_x25519_private,
&self.remote_x25519_public,
(&dec_key, &enc_key),
@@ -789,11 +872,15 @@ impl LpSession {
return Err(e);
}
};
let psk = psq_result.psk;
// Store PQ shared secret for subsession PSK derivation
*self.pq_shared_secret.lock() = Some(PqSharedSecret::new(psq_result.pq_shared_secret));
// Store the PSK handle (ctxt_B) for transmission in next message
{
let mut psk_handle = self.psk_handle.lock();
*psk_handle = Some(responder_msg_bytes);
*psk_handle = Some(psq_result.psk_handle);
}
// Inject PSK into Noise HandshakeState
@@ -801,6 +888,12 @@ impl LpSession {
// Mark PSK as injected for safety checks in transport mode
self.psk_injected.store(true, Ordering::Release);
// Derive and store outer AEAD key from PSK
{
let mut outer_key = self.outer_aead_key.lock();
*outer_key = Some(OuterAeadKey::from_psk(&psk));
}
// Update PSQ state to Completed
*psq_state = PSQState::Completed { psk };
@@ -858,6 +951,49 @@ impl LpSession {
self.noise_state.lock().is_handshake_finished()
}
/// Returns the PQ shared secret (K_pq) if available.
///
/// This is the raw KEM output from PSQ before Blake3 KDF combination.
/// Used for deriving subsession PSKs to maintain PQ protection.
pub fn pq_shared_secret(&self) -> Option<[u8; 32]> {
self.pq_shared_secret.lock().as_ref().map(|s| *s.as_bytes())
}
/// Gets the next subsession index and increments the counter.
///
/// Each subsession requires a unique index to ensure unique PSK derivation.
/// The index is monotonically increasing per session.
pub fn next_subsession_index(&self) -> u64 {
self.subsession_counter.fetch_add(1, Ordering::Relaxed)
}
/// Returns true if this session is in read-only mode.
///
/// Read-only sessions have been demoted after a subsession was promoted.
/// They can still decrypt incoming messages but cannot encrypt outgoing ones.
pub fn is_read_only(&self) -> bool {
self.read_only.load(Ordering::Acquire)
}
/// Demotes this session to read-only mode after a subsession replaces it.
///
/// After demotion:
/// - `encrypt_data()` will return `NoiseError::SessionReadOnly`
/// - `decrypt_data()` still works (to drain in-flight messages)
/// - Session should be cleaned up after TTL expires
///
/// # Arguments
/// * `successor_idx` - The receiver index of the session that replaced this one
pub fn demote(&self, successor_idx: u32) {
*self.successor_session_id.lock() = Some(successor_idx);
self.read_only.store(true, Ordering::Release);
}
/// Returns the successor session ID if this session was demoted.
pub fn successor_session_id(&self) -> Option<u32> {
*self.successor_session_id.lock()
}
/// Encrypts application data payload using the established Noise transport session.
///
/// This should only be called after the handshake is complete (`is_handshake_complete` returns true).
@@ -871,6 +1007,11 @@ impl LpSession {
/// * `Ok(Vec<u8>)` containing the encrypted Noise message ciphertext.
/// * `Err(NoiseError)` if the session is not in transport mode or encryption fails.
pub fn encrypt_data(&self, payload: &[u8]) -> Result<LpMessage, NoiseError> {
// Check if session is read-only (demoted)
if self.read_only.load(Ordering::Acquire) {
return Err(NoiseError::SessionReadOnly);
}
let mut noise_state = self.noise_state.lock();
// Safety: Prevent transport mode with dummy PSK
if !self.psk_injected.load(Ordering::Acquire) {
@@ -932,6 +1073,220 @@ impl LpSession {
kem_pk: Box::new(kem_pk),
};
}
/// Creates a new subsession using Noise KKpsk0 pattern.
///
/// KKpsk0 reuses parent's static X25519 keys (both parties know each other from parent session).
/// PSK is derived from parent's PQ shared secret, preserving quantum resistance.
///
/// # Arguments
/// * `subsession_index` - Unique index for this subsession (use `next_subsession_index()`)
/// * `is_initiator` - True if this side initiates the subsession handshake
///
/// # Returns
/// `SubsessionHandshake` ready for KK1/KK2 message exchange
///
/// # Errors
/// * Returns error if parent handshake not complete
/// * Returns error if PQ shared secret not available
pub fn create_subsession(
&self,
subsession_index: u64,
is_initiator: bool,
) -> Result<SubsessionHandshake, LpError> {
// Verify parent handshake is complete
if !self.is_handshake_complete() {
return Err(LpError::Internal(
"Parent handshake not complete".into(),
));
}
// Get PQ shared secret
let pq_secret = self
.pq_shared_secret()
.ok_or_else(|| LpError::Internal("PQ shared secret not available".into()))?;
// Derive subsession PSK from parent's PQ shared secret
let subsession_psk = derive_subsession_psk(&pq_secret, subsession_index);
// Build KKpsk0 handshake
// Pattern: Noise_KKpsk0_25519_ChaChaPoly_SHA256
// Both parties already know each other's static keys from parent session
let pattern_name = "Noise_KKpsk0_25519_ChaChaPoly_SHA256";
let params = pattern_name.parse()?;
let local_key_bytes = self.local_x25519_private.to_bytes();
let remote_key_bytes = self.remote_x25519_public.to_bytes();
let builder = Builder::new(params)
.local_private_key(&local_key_bytes)
.remote_public_key(&remote_key_bytes)
.psk(0, &subsession_psk); // PSK at position 0 for KKpsk0
let handshake_state = if is_initiator {
builder.build_initiator().map_err(LpError::SnowKeyError)?
} else {
builder.build_responder().map_err(LpError::SnowKeyError)?
};
Ok(SubsessionHandshake {
index: subsession_index,
noise_state: Mutex::new(NoiseProtocol::new(handshake_state)),
is_initiator,
// Copy key material from parent for into_session() conversion
local_ed25519_private: ed25519::PrivateKey::from_bytes(
&self.local_ed25519_private.to_bytes(),
).expect("Valid Ed25519 private key from parent"),
local_ed25519_public: ed25519::PublicKey::from_bytes(&self.local_ed25519_public.to_bytes())
.expect("Valid Ed25519 public key from parent"),
remote_ed25519_public: ed25519::PublicKey::from_bytes(&self.remote_ed25519_public.to_bytes())
.expect("Valid Ed25519 public key from parent"),
local_x25519_private: self.local_x25519_private.clone(),
remote_x25519_public: self.remote_x25519_public.clone(),
pq_shared_secret: PqSharedSecret::new(pq_secret),
subsession_psk,
})
}
}
/// Subsession created via Noise KKpsk0 handshake tunneled through parent session.
///
/// Subsessions provide fresh session keys while inheriting PQ protection from parent's
/// ML-KEM shared secret. After handshake completes, the subsession can be promoted
/// to replace the parent session.
///
/// # Lifecycle
/// 1. Parent calls `create_subsession()` to get `SubsessionHandshake`
/// 2. Initiator calls `prepare_message()` to get KK1
/// 3. KK1 sent through parent session (encrypted tunnel)
/// 4. Responder calls `process_message(kk1)` to process KK1
/// 5. Responder calls `prepare_message()` to get KK2
/// 6. KK2 sent through parent session
/// 7. Initiator calls `process_message(kk2)` to complete handshake
/// 8. Both call `is_complete()` to verify
#[derive(Debug)]
pub struct SubsessionHandshake {
/// Subsession index (unique per parent session)
pub index: u64,
/// Noise KKpsk0 handshake state
noise_state: Mutex<NoiseProtocol>,
/// Is this side the initiator?
is_initiator: bool,
// Key material inherited from parent session for into_session() conversion
/// Local Ed25519 private key (for PSQ auth if needed)
local_ed25519_private: ed25519::PrivateKey,
/// Local Ed25519 public key
local_ed25519_public: ed25519::PublicKey,
/// Remote Ed25519 public key
remote_ed25519_public: ed25519::PublicKey,
/// Local X25519 private key (Noise static key)
local_x25519_private: PrivateKey,
/// Remote X25519 public key (Noise static key)
remote_x25519_public: PublicKey,
/// PQ shared secret inherited from parent (for creating further subsessions)
pq_shared_secret: PqSharedSecret,
/// Subsession PSK (for deriving outer AEAD key)
subsession_psk: [u8; 32],
}
impl SubsessionHandshake {
/// Prepares the next KK handshake message (KK1 or KK2 depending on role/state).
///
/// # Returns
/// Noise handshake message bytes to send through parent session tunnel.
pub fn prepare_message(&self) -> Result<Vec<u8>, LpError> {
let mut noise_state = self.noise_state.lock();
noise_state
.get_bytes_to_send()
.ok_or_else(|| LpError::Internal("Not our turn to send".into()))?
.map_err(LpError::NoiseError)
}
/// Processes a received KK handshake message (KK1 or KK2).
///
/// # Arguments
/// * `message` - Noise handshake message received through parent session tunnel.
///
/// # Returns
/// Any payload embedded in the handshake message (usually empty for KK).
pub fn process_message(&self, message: &[u8]) -> Result<Vec<u8>, LpError> {
let mut noise_state = self.noise_state.lock();
let result = noise_state
.read_message(message)
.map_err(LpError::NoiseError)?;
match result {
ReadResult::HandshakeComplete | ReadResult::NoOp => Ok(vec![]),
ReadResult::DecryptedData(data) => Ok(data),
}
}
/// Checks if the handshake is complete (ready for transport mode).
pub fn is_complete(&self) -> bool {
self.noise_state.lock().is_handshake_finished()
}
/// Returns whether this side is the initiator.
pub fn is_initiator(&self) -> bool {
self.is_initiator
}
/// Returns the subsession index.
pub fn subsession_index(&self) -> u64 {
self.index
}
/// Convert completed subsession handshake into a full LpSession.
///
/// This consumes the SubsessionHandshake and creates a new LpSession
/// that can be used as a replacement for the parent session.
///
/// # Arguments
/// * `receiver_index` - New receiver index for the promoted session
///
/// # Errors
/// Returns error if handshake is not complete
pub fn into_session(self, receiver_index: u32) -> Result<LpSession, LpError> {
if !self.is_complete() {
return Err(LpError::Internal(
"Cannot convert incomplete subsession to session".to_string(),
));
}
// Extract the noise state (now in transport mode)
let noise_state = self.noise_state.into_inner();
// Generate fresh salt for the new session
let salt = generate_fresh_salt();
// Derive outer AEAD key from the subsession PSK
let outer_key = OuterAeadKey::from_psk(&self.subsession_psk);
Ok(LpSession {
id: receiver_index,
is_initiator: self.is_initiator,
noise_state: Mutex::new(noise_state),
// KKT: subsession inherits from parent, mark as processed
kkt_state: Mutex::new(KKTState::ResponderProcessed),
// PSQ: subsession uses PSK derived from parent's PQ secret
psq_state: Mutex::new(PSQState::Completed { psk: self.subsession_psk }),
psk_handle: Mutex::new(None), // Subsession doesn't have its own handle
sending_counter: AtomicU64::new(0),
receiving_counter: Mutex::new(ReceivingKeyCounterValidator::new(0)),
psk_injected: AtomicBool::new(true), // PSK was in KKpsk0
local_ed25519_private: self.local_ed25519_private,
local_ed25519_public: self.local_ed25519_public,
remote_ed25519_public: self.remote_ed25519_public,
local_x25519_private: self.local_x25519_private,
remote_x25519_public: self.remote_x25519_public,
salt,
outer_aead_key: Mutex::new(Some(outer_key)),
pq_shared_secret: Mutex::new(Some(self.pq_shared_secret)),
subsession_counter: AtomicU64::new(0),
read_only: AtomicBool::new(false),
successor_session_id: Mutex::new(None),
})
}
}
#[cfg(test)]
@@ -946,15 +1301,13 @@ mod tests {
// Helper function to create a session with real keys for handshake tests
fn create_handshake_test_session(
receiver_index: u32,
is_initiator: bool,
local_keys: &crate::keypair::Keypair,
remote_pub_key: &crate::keypair::PublicKey,
) -> LpSession {
use nym_crypto::asymmetric::ed25519;
// Compute the shared lp_id from both keypairs (order-independent)
let lp_id = crate::make_lp_id(local_keys.public_key(), remote_pub_key);
// Create Ed25519 keypairs that correspond to initiator/responder roles
// Initiator uses [1u8], Responder uses [2u8]
let (local_ed25519_seed, remote_ed25519_seed) = if is_initiator {
@@ -970,7 +1323,7 @@ mod tests {
// PSQ will derive the PSK during handshake using X25519 as DHKEM
let session = LpSession::new(
lp_id,
receiver_index,
is_initiator,
(local_ed25519.private_key(), local_ed25519.public_key()),
local_keys.private_key(),
@@ -1080,10 +1433,12 @@ mod tests {
fn test_prepare_handshake_message_initial_state() {
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let receiver_index = 12345u32;
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(receiver_index, true, &initiator_keys, responder_keys.public_key());
let responder_session = create_handshake_test_session(
receiver_index,
false,
&responder_keys,
initiator_keys.public_key(), // Responder also needs initiator's key for XK
@@ -1106,11 +1461,12 @@ mod tests {
fn test_process_handshake_message_first_step() {
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let receiver_index = 12345u32;
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(receiver_index, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(receiver_index, false, &responder_keys, initiator_keys.public_key());
// 1. Initiator prepares the first message (-> e)
let initiator_msg_result = initiator_session.prepare_handshake_message();
@@ -1145,9 +1501,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
let mut responder_to_initiator_msg = None;
let mut rounds = 0;
@@ -1232,9 +1588,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive handshake to completion (simplified loop from previous test)
let mut i_msg = initiator_session
@@ -1293,7 +1649,7 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
assert!(!initiator_session.is_handshake_complete());
@@ -1365,9 +1721,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive the handshake
let mut i_msg = initiator_session
@@ -1459,9 +1815,9 @@ mod tests {
// Create sessions - they start with dummy PSK [0u8; 32]
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Prepare first message (initiator runs PSQ and injects PSK)
let i_msg = initiator_session
@@ -1524,9 +1880,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Verify initial state
assert!(!initiator_session.is_handshake_complete());
@@ -1603,9 +1959,9 @@ mod tests {
// Create sessions with explicit Ed25519 keys
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Verify sessions store Ed25519 keys
// (Internal verification - keys are used in PSQ calls)
@@ -1648,7 +2004,7 @@ mod tests {
let initiator_keys = generate_keypair();
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Create a handshake message with corrupted PSQ payload
let corrupted_psq_data = vec![0xFF; 128]; // Random garbage
@@ -1677,11 +2033,11 @@ mod tests {
let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0);
let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key!
let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key());
let receiver_index: u32 = 55555;
let salt = [0u8; 32];
let initiator_session = LpSession::new(
lp_id,
receiver_index,
true,
(
initiator_ed25519.private_key(),
@@ -1699,7 +2055,7 @@ mod tests {
let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1);
let responder_session = LpSession::new(
lp_id,
receiver_index,
false,
(
responder_ed25519.private_key(),
@@ -1748,11 +2104,11 @@ mod tests {
let wrong_ed25519_keypair = ed25519::KeyPair::from_secret([99u8; 32], 99);
let wrong_ed25519_public = wrong_ed25519_keypair.public_key();
let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key());
let receiver_index: u32 = 66666;
let salt = [0u8; 32];
let initiator_session = LpSession::new(
lp_id,
receiver_index,
true,
(
initiator_ed25519.private_key(),
@@ -1770,7 +2126,7 @@ mod tests {
let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1);
let responder_session = LpSession::new(
lp_id,
receiver_index,
false,
(
responder_ed25519.private_key(),
@@ -1813,7 +2169,7 @@ mod tests {
let initiator_keys = generate_keypair();
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Capture initial PSQ state (should be ResponderWaiting)
// (We can't directly access psq_state, but we can verify behavior)
@@ -1831,7 +2187,7 @@ mod tests {
// Session should still be functional - can process valid messages
// Create a proper initiator to send valid message
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let valid_msg = initiator_session
.prepare_handshake_message()
@@ -1858,7 +2214,7 @@ mod tests {
// Create session but don't complete handshake (no PSK injection will occur)
let session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
// Verify session was created successfully
assert!(!session.is_handshake_complete());
@@ -1895,4 +2251,120 @@ mod tests {
e => panic!("Expected PskNotInjected error, got: {:?}", e),
}
}
#[test]
fn test_demote_sets_read_only() {
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let session =
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
// Initially not read-only
assert!(!session.is_read_only());
assert!(session.successor_session_id().is_none());
// Demote the session
session.demote(99999);
// Now read-only with successor
assert!(session.is_read_only());
assert_eq!(session.successor_session_id(), Some(99999));
}
#[test]
fn test_encrypt_fails_after_demotion() {
// --- Setup Handshake ---
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive handshake to completion
let i_msg = initiator_session
.prepare_handshake_message()
.unwrap()
.unwrap();
responder_session.process_handshake_message(&i_msg).unwrap();
let r_msg = responder_session
.prepare_handshake_message()
.unwrap()
.unwrap();
initiator_session.process_handshake_message(&r_msg).unwrap();
let i_msg = initiator_session
.prepare_handshake_message()
.unwrap()
.unwrap();
responder_session.process_handshake_message(&i_msg).unwrap();
assert!(initiator_session.is_handshake_complete());
// Encryption works before demotion
let plaintext = b"Hello before demotion";
assert!(initiator_session.encrypt_data(plaintext).is_ok());
// Demote the session
initiator_session.demote(99999);
// Encryption fails after demotion
let result = initiator_session.encrypt_data(plaintext);
assert!(result.is_err());
match result.unwrap_err() {
NoiseError::SessionReadOnly => {
// Expected
}
e => panic!("Expected SessionReadOnly error, got: {:?}", e),
}
}
#[test]
fn test_decrypt_works_after_demotion() {
// --- Setup Handshake ---
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive handshake to completion
let i_msg = initiator_session
.prepare_handshake_message()
.unwrap()
.unwrap();
responder_session.process_handshake_message(&i_msg).unwrap();
let r_msg = responder_session
.prepare_handshake_message()
.unwrap()
.unwrap();
initiator_session.process_handshake_message(&r_msg).unwrap();
let i_msg = initiator_session
.prepare_handshake_message()
.unwrap()
.unwrap();
responder_session.process_handshake_message(&i_msg).unwrap();
assert!(initiator_session.is_handshake_complete());
assert!(responder_session.is_handshake_complete());
// Responder encrypts a message
let plaintext = b"Message to demoted initiator";
let ciphertext = responder_session
.encrypt_data(plaintext)
.expect("Encryption failed");
// Demote the initiator session
initiator_session.demote(99999);
assert!(initiator_session.is_read_only());
// Decryption still works on demoted session (drain in-flight)
let decrypted = initiator_session
.decrypt_data(&ciphertext)
.expect("Decryption should work on demoted session");
assert_eq!(decrypted, plaintext);
}
}
+116 -108
View File
@@ -2,7 +2,6 @@
mod tests {
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::keypair::PublicKey;
use crate::make_lp_id;
use crate::{
LpError,
message::LpMessage,
@@ -15,7 +14,7 @@ mod tests {
// Function to create a test packet - similar to how it's done in codec.rs tests
fn create_test_packet(
protocol_version: u8,
session_id: u32,
receiver_idx: u32,
counter: u64,
message: LpMessage,
) -> LpPacket {
@@ -23,7 +22,7 @@ mod tests {
let header = LpHeader {
protocol_version,
reserved: 0u16, // reserved
session_id,
receiver_idx,
counter,
};
@@ -54,7 +53,7 @@ mod tests {
let ed25519_keypair_a = ed25519::KeyPair::from_secret([1u8; 32], 0);
let ed25519_keypair_b = ed25519::KeyPair::from_secret([2u8; 32], 1);
// Derive X25519 keys from Ed25519 (same as state machine does internally)
// Derive X25519 keys from Ed25519 (needed for KKT init test)
let x25519_pub_a = ed25519_keypair_a
.public_key()
.to_x25519()
@@ -70,8 +69,8 @@ mod tests {
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 100001;
// Test salt
let salt = [42u8; 32];
@@ -79,6 +78,7 @@ mod tests {
// 4. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key(),
@@ -91,6 +91,7 @@ mod tests {
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key(),
@@ -145,13 +146,13 @@ mod tests {
);
// A prepares packet
let counter = session_manager_1.next_counter(lp_id).unwrap();
let message_a_to_b = create_test_packet(1, lp_id, counter, payload);
let counter = session_manager_1.next_counter(receiver_index).unwrap();
let message_a_to_b = create_test_packet(1, receiver_index, counter, payload);
let mut encoded_msg = BytesMut::new();
serialize_lp_packet(&message_a_to_b, &mut encoded_msg).expect("A serialize failed");
serialize_lp_packet(&message_a_to_b, &mut encoded_msg, None).expect("A serialize failed");
// B parses packet and checks replay
let decoded_packet = parse_lp_packet(&encoded_msg).expect("B parse failed");
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("B parse failed");
assert_eq!(decoded_packet.header.counter, counter);
// Check replay before processing handshake
@@ -197,12 +198,12 @@ mod tests {
// B prepares packet
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
let message_b_to_a = create_test_packet(1, lp_id, counter, payload);
let message_b_to_a = create_test_packet(1, receiver_index, counter, payload);
let mut encoded_msg = BytesMut::new();
serialize_lp_packet(&message_b_to_a, &mut encoded_msg).expect("B serialize failed");
serialize_lp_packet(&message_b_to_a, &mut encoded_msg, None).expect("B serialize failed");
// A parses packet and checks replay
let decoded_packet = parse_lp_packet(&encoded_msg).expect("A parse failed");
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("A parse failed");
assert_eq!(decoded_packet.header.counter, counter);
// Check replay before processing handshake
@@ -282,13 +283,13 @@ mod tests {
// A prepares packet
let counter_a = session_manager_1.next_counter(peer_a_sm).unwrap();
let message_a_to_b = create_test_packet(1, lp_id, counter_a, ciphertext_a_to_b);
let message_a_to_b = create_test_packet(1, receiver_index, counter_a, ciphertext_a_to_b);
let mut encoded_data_a_to_b = BytesMut::new();
serialize_lp_packet(&message_a_to_b, &mut encoded_data_a_to_b)
serialize_lp_packet(&message_a_to_b, &mut encoded_data_a_to_b, None)
.expect("A serialize data failed");
// B parses packet and checks replay
let decoded_packet_b = parse_lp_packet(&encoded_data_a_to_b).expect("B parse data failed");
let decoded_packet_b = parse_lp_packet(&encoded_data_a_to_b, None).expect("B parse data failed");
assert_eq!(decoded_packet_b.header.counter, counter_a);
// Check replay before decrypting
@@ -316,13 +317,13 @@ mod tests {
.encrypt_data(peer_b_sm, plaintext_b_to_a)
.expect("B encrypt failed");
let counter_b = session_manager_2.next_counter(peer_b_sm).unwrap();
let message_b_to_a = create_test_packet(1, lp_id, counter_b, ciphertext_b_to_a);
let message_b_to_a = create_test_packet(1, receiver_index, counter_b, ciphertext_b_to_a);
let mut encoded_data_b_to_a = BytesMut::new();
serialize_lp_packet(&message_b_to_a, &mut encoded_data_b_to_a)
serialize_lp_packet(&message_b_to_a, &mut encoded_data_b_to_a, None)
.expect("B serialize data failed");
// A parses packet and checks replay
let decoded_packet_a = parse_lp_packet(&encoded_data_b_to_a).expect("A parse data failed");
let decoded_packet_a = parse_lp_packet(&encoded_data_b_to_a, None).expect("A parse data failed");
assert_eq!(decoded_packet_a.header.counter, counter_b);
// Check replay before decrypting
@@ -352,18 +353,18 @@ mod tests {
// Need to re-encode because decode consumes the buffer
let message_b_to_a_replay = create_test_packet(
1,
lp_id,
receiver_index,
counter_b,
LpMessage::EncryptedData(crate::message::EncryptedDataPayload(
plaintext_b_to_a.to_vec(),
)), // Using plaintext here, but content doesn't matter for replay check
);
let mut encoded_data_b_to_a_replay = BytesMut::new();
serialize_lp_packet(&message_b_to_a_replay, &mut encoded_data_b_to_a_replay)
serialize_lp_packet(&message_b_to_a_replay, &mut encoded_data_b_to_a_replay, None)
.expect("B serialize replay failed");
let parsed_replay_packet =
parse_lp_packet(&encoded_data_b_to_a_replay).expect("A parse replay failed");
parse_lp_packet(&encoded_data_b_to_a_replay, None).expect("A parse replay failed");
let replay_result = session_manager_1
.receiving_counter_quick_check(peer_a_sm, parsed_replay_packet.header.counter);
assert!(replay_result.is_err(), "Data replay should be prevented");
@@ -386,18 +387,18 @@ mod tests {
let message_a_to_b_skip = create_test_packet(
1, // protocol version
lp_id,
receiver_index,
counter_a_skip, // Send N+1 first
ciphertext_skip,
);
// Encode the skip message
let mut encoded_skip = BytesMut::new();
serialize_lp_packet(&message_a_to_b_skip, &mut encoded_skip)
serialize_lp_packet(&message_a_to_b_skip, &mut encoded_skip, None)
.expect("Failed to serialize skip message");
// B parses skip message and checks replay
let decoded_packet_skip = parse_lp_packet(&encoded_skip).expect("B parse skip failed");
let decoded_packet_skip = parse_lp_packet(&encoded_skip, None).expect("B parse skip failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_skip.header.counter)
.expect("B replay check skip failed");
@@ -428,14 +429,14 @@ mod tests {
let message_a_to_b_delayed = create_test_packet(
1, // protocol version
lp_id,
receiver_index,
counter_a_next, // counter N (delayed packet)
ciphertext_delayed,
);
// Encode the delayed message
let mut encoded_delayed = BytesMut::new();
serialize_lp_packet(&message_a_to_b_delayed, &mut encoded_delayed)
serialize_lp_packet(&message_a_to_b_delayed, &mut encoded_delayed, None)
.expect("Failed to serialize delayed message");
// Make a copy for replay test later
@@ -443,7 +444,7 @@ mod tests {
// B parses delayed message and checks replay
let decoded_packet_delayed =
parse_lp_packet(&encoded_delayed).expect("B parse delayed failed");
parse_lp_packet(&encoded_delayed, None).expect("B parse delayed failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_delayed.header.counter)
.expect("B replay check delayed failed");
@@ -469,7 +470,7 @@ mod tests {
// 11. Try to replay message with counter N (should fail)
println!("Testing replay of delayed packet...");
let parsed_delayed_replay =
parse_lp_packet(&encoded_delayed_copy).expect("Parse delayed replay failed");
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
let result = session_manager_2
.receiving_counter_quick_check(peer_b_sm, parsed_delayed_replay.header.counter);
assert!(result.is_err(), "Replay attack should be prevented");
@@ -479,15 +480,15 @@ mod tests {
);
// 12. Session removal
assert!(session_manager_1.remove_state_machine(lp_id));
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
// Verify the session is gone
let session = session_manager_1.state_machine_exists(lp_id);
let session = session_manager_1.state_machine_exists(receiver_index);
assert!(!session, "Session should be removed");
// But the other session still exists
let session = session_manager_2.state_machine_exists(lp_id);
let session = session_manager_2.state_machine_exists(receiver_index);
assert!(session, "Session still exists in the other manager");
}
@@ -518,14 +519,15 @@ mod tests {
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for test
let receiver_index: u32 = 100002;
// Test salt
let salt = [43u8; 32];
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key(),
@@ -537,6 +539,7 @@ mod tests {
.unwrap();
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key(),
@@ -612,12 +615,12 @@ mod tests {
let current_counter_a = counter_a;
counter_a += 1;
let message_a = create_test_packet(1, lp_id, current_counter_a, ciphertext_a);
let message_a = create_test_packet(1, receiver_index, current_counter_a, ciphertext_a);
let mut encoded_a = BytesMut::new();
serialize_lp_packet(&message_a, &mut encoded_a).expect("A serialize failed");
serialize_lp_packet(&message_a, &mut encoded_a, None).expect("A serialize failed");
// B parses and checks replay
let decoded_packet_b = parse_lp_packet(&encoded_a).expect("B parse failed");
let decoded_packet_b = parse_lp_packet(&encoded_a, None).expect("B parse failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_b.header.counter)
.expect("B replay check failed (A->B)");
@@ -638,12 +641,12 @@ mod tests {
let current_counter_b = counter_b;
counter_b += 1;
let message_b = create_test_packet(1, lp_id, current_counter_b, ciphertext_b);
let message_b = create_test_packet(1, receiver_index, current_counter_b, ciphertext_b);
let mut encoded_b = BytesMut::new();
serialize_lp_packet(&message_b, &mut encoded_b).expect("B serialize failed");
serialize_lp_packet(&message_b, &mut encoded_b, None).expect("B serialize failed");
// A parses and checks replay
let decoded_packet_a = parse_lp_packet(&encoded_b).expect("A parse failed");
let decoded_packet_a = parse_lp_packet(&encoded_b, None).expect("A parse failed");
session_manager_1
.receiving_counter_quick_check(peer_a_sm, decoded_packet_a.header.counter)
.expect("A replay check failed (B->A)");
@@ -716,12 +719,12 @@ mod tests {
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair type
let lp_pub = PublicKey::from_bytes(x25519_pub.as_bytes())
// Convert to LP keypair type (still needed for init_kkt_for_test below if used)
let _lp_pub = PublicKey::from_bytes(x25519_pub.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (self-connection: both sides use same key)
let lp_id = make_lp_id(&lp_pub, &lp_pub);
// Use fixed receiver_index for test
let receiver_index: u32 = 100003;
// Test salt
let salt = [44u8; 32];
@@ -729,6 +732,7 @@ mod tests {
// 2. Create a session (using real noise state)
let _session = session_manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -748,8 +752,10 @@ mod tests {
);
// 5. Create and immediately remove a session
let receiver_index_temp: u32 = 100004;
let _temp_session = session_manager
.create_session_state_machine(
receiver_index_temp,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -758,7 +764,7 @@ mod tests {
.expect("Failed to create temp session");
assert!(
session_manager.remove_state_machine(lp_id),
session_manager.remove_state_machine(receiver_index_temp),
"Should remove the session"
);
@@ -770,7 +776,7 @@ mod tests {
// Add header
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&lp_id.to_le_bytes()); // Sender index
buf.extend_from_slice(&receiver_index.to_le_bytes()); // Sender index
buf.extend_from_slice(&0u64.to_le_bytes()); // Counter
// Add invalid message type
@@ -783,7 +789,7 @@ mod tests {
buf.extend_from_slice(&[0u8; TRAILER_LEN]);
// Try to parse the invalid message type
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err(), "Decoding invalid message type should fail");
// Add assertion for the specific error type
@@ -796,7 +802,7 @@ mod tests {
let partial_packet = &buf[0..10]; // Too short to be a valid packet
let partial_bytes = BytesMut::from(partial_packet);
let result = parse_lp_packet(&partial_bytes);
let result = parse_lp_packet(&partial_bytes, None);
assert!(result.is_err(), "Parsing partial packet should fail");
assert!(matches!(
result.unwrap_err(),
@@ -844,14 +850,14 @@ mod tests {
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair types
// Convert to LP keypair types (needed for init_kkt_for_test if used)
let lp_pub_a = PublicKey::from_bytes(x25519_pub_a.as_bytes())
.expect("Failed to create PublicKey from bytes");
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for test
let receiver_index: u32 = 100005;
// Test salt
let salt = [45u8; 32];
@@ -860,6 +866,7 @@ mod tests {
assert!(
session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key()
@@ -873,6 +880,7 @@ mod tests {
assert!(
session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key()
@@ -886,16 +894,16 @@ mod tests {
assert_eq!(session_manager_1.session_count(), 1);
assert_eq!(session_manager_2.session_count(), 1);
assert!(session_manager_1.state_machine_exists(lp_id));
assert!(session_manager_2.state_machine_exists(lp_id));
assert!(session_manager_1.state_machine_exists(receiver_index));
assert!(session_manager_2.state_machine_exists(receiver_index));
// Verify initial states are ReadyToHandshake
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
@@ -910,7 +918,7 @@ mod tests {
// --- Round 1: Initiator Starts ---
println!(" Round {}: Initiator starts handshake", rounds);
let action_a1 = session_manager_1
.process_input(lp_id, LpInput::StartHandshake)
.process_input(receiver_index, LpInput::StartHandshake)
.expect("Initiator StartHandshake should produce an action")
.expect("Initiator StartHandshake failed");
@@ -922,7 +930,7 @@ mod tests {
}
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange,
"Initiator state wrong after StartHandshake (should be KKTExchange)"
);
@@ -932,7 +940,7 @@ mod tests {
" Round {}: Responder explicitly enters KKTExchange state",
rounds
);
let action_b_start = session_manager_2.process_input(lp_id, LpInput::StartHandshake);
let action_b_start = session_manager_2.process_input(receiver_index, LpInput::StartHandshake);
// Responder's StartHandshake should not produce an action to send
assert!(
action_b_start.as_ref().unwrap().is_none(),
@@ -941,7 +949,7 @@ mod tests {
);
// Verify responder transitions to KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
"Responder state should be KKTExchange after its StartHandshake"
);
@@ -959,12 +967,12 @@ mod tests {
// Simulate network: serialize -> parse (optional but good practice)
let mut buf_a = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a).unwrap();
let parsed_packet_a = parse_lp_packet(&buf_a).unwrap();
serialize_lp_packet(&packet_to_process, &mut buf_a, None).unwrap();
let parsed_packet_a = parse_lp_packet(&buf_a, None).unwrap();
// Responder processes KKT request
let action_b1 = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
@@ -976,7 +984,7 @@ mod tests {
}
// Responder transitions to Handshaking after KKT completes
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should be Handshaking after KKT exchange"
);
@@ -993,12 +1001,12 @@ mod tests {
// Simulate network
let mut buf_b = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b).unwrap();
let parsed_packet_b = parse_lp_packet(&buf_b).unwrap();
serialize_lp_packet(&packet_to_process, &mut buf_b, None).unwrap();
let parsed_packet_b = parse_lp_packet(&buf_b, None).unwrap();
// Initiator processes KKT response
let action_a2 = session_manager_1
.process_input(lp_id, LpInput::ReceivePacket(parsed_packet_b))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
@@ -1010,7 +1018,7 @@ mod tests {
packet_a_to_b = Some(packet);
// Initiator transitions to Handshaking after KKT completes
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Initiator state should be Handshaking after receiving KKT response"
);
@@ -1022,10 +1030,10 @@ mod tests {
// KKT completed, now need to explicitly trigger handshake message
// This might be the case if KKT completion doesn't automatically send the first Noise message
// Let's try to prepare the handshake message
if let Some(msg_result) = session_manager_1.prepare_handshake_message(lp_id) {
if let Some(msg_result) = session_manager_1.prepare_handshake_message(receiver_index) {
let msg = msg_result.expect("Failed to prepare handshake message after KKT");
// Create a packet from the message
let packet = create_test_packet(1, lp_id, 0, msg);
let packet = create_test_packet(1, receiver_index, 0, msg);
packet_a_to_b = Some(packet);
println!(" Prepared first Noise message after KKTComplete");
} else {
@@ -1052,12 +1060,12 @@ mod tests {
// Simulate network
let mut buf_a2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a2).unwrap();
let parsed_packet_a2 = parse_lp_packet(&buf_a2).unwrap();
serialize_lp_packet(&packet_to_process, &mut buf_a2, None).unwrap();
let parsed_packet_a2 = parse_lp_packet(&buf_a2, None).unwrap();
// Responder processes first Noise message and sends second Noise message
let action_b2 = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a2))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
@@ -1071,7 +1079,7 @@ mod tests {
}
// Responder still in Handshaking, waiting for final message
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should still be Handshaking after sending second message"
);
@@ -1087,11 +1095,11 @@ mod tests {
.expect("Second Noise packet from B was missing");
let mut buf_b2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b2).unwrap();
let parsed_packet_b2 = parse_lp_packet(&buf_b2).unwrap();
serialize_lp_packet(&packet_to_process, &mut buf_b2, None).unwrap();
let parsed_packet_b2 = parse_lp_packet(&buf_b2, None).unwrap();
let action_a3 = session_manager_1
.process_input(lp_id, LpInput::ReceivePacket(parsed_packet_b2))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
@@ -1105,7 +1113,7 @@ mod tests {
}
// Initiator transitions to Transport after sending third message
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Initiator state should be Transport after sending third message"
);
@@ -1121,11 +1129,11 @@ mod tests {
.expect("Third Noise packet from A was missing");
let mut buf_a3 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a3).unwrap();
let parsed_packet_a3 = parse_lp_packet(&buf_a3).unwrap();
serialize_lp_packet(&packet_to_process, &mut buf_a3, None).unwrap();
let parsed_packet_a3 = parse_lp_packet(&buf_a3, None).unwrap();
let action_b3 = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(parsed_packet_a3))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
.expect("Responder final ReceivePacket should produce an action")
.expect("Responder final ReceivePacket failed");
@@ -1139,7 +1147,7 @@ mod tests {
);
}
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Responder state should be Transport after processing third message"
);
@@ -1147,11 +1155,11 @@ mod tests {
// --- Verification ---
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
println!("Handshake simulation completed successfully via process_input.");
@@ -1164,7 +1172,7 @@ mod tests {
// --- A sends to B ---
println!(" A sends to B");
let action_a_send = session_manager_1
.process_input(lp_id, LpInput::SendData(plaintext_a_to_b.to_vec()))
.process_input(receiver_index, LpInput::SendData(plaintext_a_to_b.to_vec()))
.expect("A SendData should produce action")
.expect("A SendData failed");
@@ -1176,13 +1184,13 @@ mod tests {
// Simulate network
let mut buf_data_a = BytesMut::new();
serialize_lp_packet(&data_packet_a, &mut buf_data_a).unwrap();
let parsed_data_a = parse_lp_packet(&buf_data_a).unwrap();
serialize_lp_packet(&data_packet_a, &mut buf_data_a, None).unwrap();
let parsed_data_a = parse_lp_packet(&buf_data_a, None).unwrap();
// B receives
println!(" B receives from A");
let action_b_recv = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(parsed_data_a))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_data_a))
.expect("B ReceivePacket (data) should produce action")
.expect("B ReceivePacket (data) failed");
@@ -1203,7 +1211,7 @@ mod tests {
// --- B sends to A ---
println!(" B sends to A");
let action_b_send = session_manager_2
.process_input(lp_id, LpInput::SendData(plaintext_b_to_a.to_vec()))
.process_input(receiver_index, LpInput::SendData(plaintext_b_to_a.to_vec()))
.expect("B SendData should produce action")
.expect("B SendData failed");
@@ -1217,13 +1225,13 @@ mod tests {
// Simulate network
let mut buf_data_b = BytesMut::new();
serialize_lp_packet(&data_packet_b, &mut buf_data_b).unwrap();
let parsed_data_b = parse_lp_packet(&buf_data_b).unwrap();
serialize_lp_packet(&data_packet_b, &mut buf_data_b, None).unwrap();
let parsed_data_b = parse_lp_packet(&buf_data_b, None).unwrap();
// A receives
println!(" A receives from B");
let action_a_recv = session_manager_1
.process_input(lp_id, LpInput::ReceivePacket(parsed_data_b))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_data_b))
.expect("A ReceivePacket (data) should produce action")
.expect("A ReceivePacket (data) failed");
@@ -1245,7 +1253,7 @@ mod tests {
// --- 6. Replay Protection Test ---
println!("Testing data packet replay protection via process_input...");
let replay_result =
session_manager_1.process_input(lp_id, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
session_manager_1.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
assert!(replay_result.is_err(), "Replay should produce Err(...)");
let error = replay_result.err().unwrap();
@@ -1264,7 +1272,7 @@ mod tests {
let data_n = Bytes::from_static(b"Message N");
let action_send_n1 = session_manager_1
.process_input(lp_id, LpInput::SendData(data_n_plus_1.to_vec()))
.process_input(receiver_index, LpInput::SendData(data_n_plus_1.to_vec()))
.unwrap()
.unwrap();
let packet_n1 = match action_send_n1 {
@@ -1273,7 +1281,7 @@ mod tests {
};
let action_send_n = session_manager_1
.process_input(lp_id, LpInput::SendData(data_n.to_vec()))
.process_input(receiver_index, LpInput::SendData(data_n.to_vec()))
.unwrap()
.unwrap();
let packet_n = match action_send_n {
@@ -1285,7 +1293,7 @@ mod tests {
// B receives N+1 first
println!(" B receives N+1");
let action_recv_n1 = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(packet_n1))
.process_input(receiver_index, LpInput::ReceivePacket(packet_n1))
.unwrap()
.unwrap();
match action_recv_n1 {
@@ -1296,7 +1304,7 @@ mod tests {
// B receives N second (should work)
println!(" B receives N");
let action_recv_n = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(packet_n))
.process_input(receiver_index, LpInput::ReceivePacket(packet_n))
.unwrap()
.unwrap();
match action_recv_n {
@@ -1307,7 +1315,7 @@ mod tests {
// B tries to replay N (should fail)
println!(" B tries to replay N");
let replay_n_result =
session_manager_2.process_input(lp_id, LpInput::ReceivePacket(packet_n_replay));
session_manager_2.process_input(receiver_index, LpInput::ReceivePacket(packet_n_replay));
assert!(replay_n_result.is_err(), "Replay N should produce Err");
assert!(
matches!(replay_n_result.err().unwrap(), LpError::Replay(_)),
@@ -1320,18 +1328,18 @@ mod tests {
// A closes
let action_a_close = session_manager_1
.process_input(lp_id, LpInput::Close)
.process_input(receiver_index, LpInput::Close)
.expect("A Close should produce action")
.expect("A Close failed");
assert!(matches!(action_a_close, LpAction::ConnectionClosed));
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Closed
);
// Further actions on A fail
let send_after_close_a =
session_manager_1.process_input(lp_id, LpInput::SendData(b"fail".to_vec()));
session_manager_1.process_input(receiver_index, LpInput::SendData(b"fail".to_vec()));
assert!(send_after_close_a.is_err());
assert!(matches!(
send_after_close_a.err().unwrap(),
@@ -1340,18 +1348,18 @@ mod tests {
// B closes
let action_b_close = session_manager_2
.process_input(lp_id, LpInput::Close)
.process_input(receiver_index, LpInput::Close)
.expect("B Close should produce action")
.expect("B Close failed");
assert!(matches!(action_b_close, LpAction::ConnectionClosed));
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Closed
);
// Further actions on B fail
let send_after_close_b =
session_manager_2.process_input(lp_id, LpInput::SendData(b"fail".to_vec()));
session_manager_2.process_input(receiver_index, LpInput::SendData(b"fail".to_vec()));
assert!(send_after_close_b.is_err());
assert!(matches!(
send_after_close_b.err().unwrap(),
@@ -1360,15 +1368,15 @@ mod tests {
println!("Close test passed.");
// --- 9. Session Removal ---
assert!(session_manager_1.remove_state_machine(lp_id));
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
assert!(!session_manager_1.state_machine_exists(lp_id));
assert!(!session_manager_1.state_machine_exists(receiver_index));
// B's session manager still has it until removed
assert!(session_manager_2.state_machine_exists(lp_id));
assert!(session_manager_2.remove_state_machine(lp_id));
assert!(session_manager_2.state_machine_exists(receiver_index));
assert!(session_manager_2.remove_state_machine(receiver_index));
assert_eq!(session_manager_2.session_count(), 0);
assert!(!session_manager_2.state_machine_exists(lp_id));
assert!(!session_manager_2.state_machine_exists(receiver_index));
println!("Session removal test passed.");
}
// ... other tests ...
+13 -3
View File
@@ -166,21 +166,22 @@ impl SessionManager {
pub fn create_session_state_machine(
&self,
receiver_index: u32,
local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey),
remote_ed25519_key: &ed25519::PublicKey,
is_initiator: bool,
salt: &[u8; 32],
) -> Result<u32, LpError> {
let sm = LpStateMachine::new(
receiver_index,
is_initiator,
local_ed25519_keypair,
remote_ed25519_key,
salt,
)?;
let sm_id = sm.id()?;
self.state_machines.insert(sm_id, sm);
Ok(sm_id)
self.state_machines.insert(receiver_index, sm);
Ok(receiver_index)
}
/// Method to remove a state machine
@@ -215,9 +216,11 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([10u8; 32], 0);
let salt = [47u8; 32];
let receiver_index: u32 = 1001;
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -237,9 +240,11 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([11u8; 32], 0);
let salt = [48u8; 32];
let receiver_index: u32 = 2002;
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -265,6 +270,7 @@ mod tests {
let sm_1 = manager
.create_session_state_machine(
3001,
(
ed25519_keypair_1.private_key(),
ed25519_keypair_1.public_key(),
@@ -277,6 +283,7 @@ mod tests {
let sm_2 = manager
.create_session_state_machine(
3002,
(
ed25519_keypair_2.private_key(),
ed25519_keypair_2.public_key(),
@@ -289,6 +296,7 @@ mod tests {
let sm_3 = manager
.create_session_state_machine(
3003,
(
ed25519_keypair_3.private_key(),
ed25519_keypair_3.public_key(),
@@ -315,8 +323,10 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([15u8; 32], 0);
let salt = [50u8; 32];
let receiver_index: u32 = 4004;
let sm = manager.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
File diff suppressed because it is too large Load Diff
+4 -67
View File
@@ -60,9 +60,6 @@ pub struct LpRegistrationResponse {
/// Allocated bandwidth in bytes
pub allocated_bandwidth: i64,
/// Session identifier for future reference
pub session_id: u32,
}
impl LpRegistrationRequest {
@@ -100,24 +97,22 @@ impl LpRegistrationRequest {
impl LpRegistrationResponse {
/// Create a success response with GatewayData
pub fn success(session_id: u32, allocated_bandwidth: i64, gateway_data: GatewayData) -> Self {
pub fn success(allocated_bandwidth: i64, gateway_data: GatewayData) -> Self {
Self {
success: true,
error: None,
gateway_data: Some(gateway_data),
allocated_bandwidth,
session_id,
}
}
/// Create an error response
pub fn error(session_id: u32, error: String) -> Self {
pub fn error(error: String) -> Self {
Self {
success: false,
error: Some(error),
gateway_data: None,
allocated_bandwidth: 0,
session_id,
}
}
}
@@ -153,13 +148,12 @@ mod tests {
let allocated_bandwidth = 1_000_000_000;
let response =
LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data.clone());
LpRegistrationResponse::success(allocated_bandwidth, gateway_data.clone());
assert!(response.success);
assert!(response.error.is_none());
assert!(response.gateway_data.is_some());
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
assert_eq!(response.session_id, session_id);
let returned_gw_data = response
.gateway_data
@@ -172,72 +166,15 @@ mod tests {
#[test]
fn test_lp_registration_response_error() {
let session_id = 54321;
let error_msg = String::from("Insufficient bandwidth");
let response = LpRegistrationResponse::error(session_id, error_msg.clone());
let response = LpRegistrationResponse::error(error_msg.clone());
assert!(!response.success);
assert_eq!(response.error, Some(error_msg));
assert!(response.gateway_data.is_none());
assert_eq!(response.allocated_bandwidth, 0);
assert_eq!(response.session_id, session_id);
}
#[test]
fn test_lp_registration_response_serialize_deserialize_success() {
let gateway_data = create_test_gateway_data();
let original = LpRegistrationResponse::success(999, 5_000_000_000, gateway_data);
// Serialize
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
// Deserialize
let deserialized: LpRegistrationResponse =
bincode::deserialize(&serialized).expect("Failed to deserialize response");
assert_eq!(deserialized.success, original.success);
assert_eq!(deserialized.error, original.error);
assert_eq!(
deserialized.allocated_bandwidth,
original.allocated_bandwidth
);
assert_eq!(deserialized.session_id, original.session_id);
assert!(deserialized.gateway_data.is_some());
}
#[test]
fn test_lp_registration_response_serialize_deserialize_error() {
let original = LpRegistrationResponse::error(777, String::from("Test error message"));
// Serialize
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
// Deserialize
let deserialized: LpRegistrationResponse =
bincode::deserialize(&serialized).expect("Failed to deserialize response");
assert_eq!(deserialized.success, original.success);
assert_eq!(deserialized.error, original.error);
assert_eq!(deserialized.allocated_bandwidth, 0);
assert_eq!(deserialized.session_id, original.session_id);
assert!(deserialized.gateway_data.is_none());
}
#[test]
fn test_lp_registration_response_malformed_deserialize() {
// Create invalid bincode data
let invalid_data = vec![0xFF; 100];
// Attempt to deserialize
let result: Result<LpRegistrationResponse, _> = bincode::deserialize(&invalid_data);
assert!(
result.is_err(),
"Expected deserialization to fail for malformed data"
);
}
// ==================== RegistrationMode Tests ====================
#[test]
@@ -26,6 +26,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
fn from(req: &PeerControlRequest) -> Self {
match req {
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RegisterPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
PeerControlRequest::GetClientBandwidthByKey { .. } => {
@@ -112,6 +113,15 @@ impl MockPeerControllerV2 {
)
.unwrap();
}
PeerControlRequest::RegisterPeer { response_tx, .. } => {
response_tx
.send(
*response
.downcast()
.expect("registered response has mismatched type"),
)
.unwrap();
}
PeerControlRequest::RemovePeer { response_tx, .. } => {
response_tx
.send(
+1
View File
@@ -644,6 +644,7 @@ pub fn start_controller(
let wg_api = Arc::new(MockWgApi::default());
// Create IP pool for testing
#[allow(clippy::expect_used)]
let ip_pool = IpPool::new(
Ipv4Addr::new(10, 0, 0, 0),
24,
+16 -13
View File
@@ -178,30 +178,31 @@ def create_mixnode_entry(base_dir, mix_id, port_delta, suffix, host_ip):
return entry
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip, gateway_name="gateway"):
"""Create a node_details entry for a gateway"""
debug(f"\n=== Creating gateway entry ===")
gateway_file = Path(base_dir) / "gateway.json"
debug(f"\n=== Creating {gateway_name} entry ===")
gateway_file = Path(base_dir) / f"{gateway_name}.json"
debug(f"Reading bonding JSON from: {gateway_file}")
with gateway_file.open("r") as json_blob:
gateway_data = json.load(json_blob)
node_details = read_node_details("gateway", suffix)
node_details = read_node_details(gateway_name, suffix)
# Get identity key from bonding JSON (already byte array)
identity = gateway_data.get("identity_key")
if not identity:
raise RuntimeError("Missing identity_key in gateway.json")
raise RuntimeError(f"Missing identity_key in {gateway_name}.json")
debug(f" ✓ Got identity_key from bonding JSON: {len(identity)} bytes")
# Get sphinx key from node-details (decoded from Base58)
sphinx_key = node_details.get("sphinx_key")
if not sphinx_key:
raise RuntimeError("Missing sphinx_key from node-details for gateway")
raise RuntimeError(f"Missing sphinx_key from node-details for {gateway_name}")
host = host_ip
mix_port = 10000 + port_delta
clients_port = 9000
# Calculate clients_port: gateway uses 9000, gateway2 uses 9001, etc.
clients_port = 9000 + (port_delta - 4)
debug(f" Using host: {host} (mix:{mix_port}, clients:{clients_port})")
entry = {
@@ -229,7 +230,7 @@ def create_gateway_entry(base_dir, node_id, port_delta, suffix, host_ip):
def main(args):
if not args:
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip]")
raise SystemExit("Usage: build_topology.py <output_dir> [node_suffix] [mix1_ip] [mix2_ip] [mix3_ip] [gateway_ip] [gateway2_ip]")
base_dir = args[0]
suffix = args[1] if len(args) > 1 and args[1] else DEFAULT_SUFFIX
@@ -239,18 +240,20 @@ def main(args):
mix2_ip = args[3] if len(args) > 3 else "127.0.0.1"
mix3_ip = args[4] if len(args) > 4 else "127.0.0.1"
gateway_ip = args[5] if len(args) > 5 else "127.0.0.1"
gateway2_ip = args[6] if len(args) > 6 else "127.0.0.1"
debug(f"\n=== Starting topology generation ===")
debug(f"Output directory: {base_dir}")
debug(f"Node suffix: {suffix}")
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}")
debug(f"Container IPs: mix1={mix1_ip}, mix2={mix2_ip}, mix3={mix3_ip}, gateway={gateway_ip}, gateway2={gateway2_ip}")
# Create node_details entries with integer keys
node_details = {
1: create_mixnode_entry(base_dir, 1, 1, suffix, mix1_ip),
2: create_mixnode_entry(base_dir, 2, 2, suffix, mix2_ip),
3: create_mixnode_entry(base_dir, 3, 3, suffix, mix3_ip),
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip)
4: create_gateway_entry(base_dir, 4, 4, suffix, gateway_ip, "gateway"),
5: create_gateway_entry(base_dir, 5, 5, suffix, gateway2_ip, "gateway2")
}
# Create the NymTopology structure
@@ -262,8 +265,8 @@ def main(args):
},
"rewarded_set": {
"epoch_id": 0,
"entry_gateways": [4],
"exit_gateways": [4],
"entry_gateways": [4, 5],
"exit_gateways": [4, 5],
"layer1": [1],
"layer2": [2],
"layer3": [3],
@@ -279,7 +282,7 @@ def main(args):
print(f"✓ Generated topology with {len(node_details)} nodes")
print(f" - 3 mixnodes (layers 1, 2, 3)")
print(f" - 1 gateway (entry + exit)")
print(f" - 2 gateways (entry + exit)")
debug(f"\n=== Topology generation complete ===\n")
+80 -7
View File
@@ -20,6 +20,7 @@ MIXNODE1_CONTAINER="nym-mixnode1"
MIXNODE2_CONTAINER="nym-mixnode2"
MIXNODE3_CONTAINER="nym-mixnode3"
GATEWAY_CONTAINER="nym-gateway"
GATEWAY2_CONTAINER="nym-gateway2"
REQUESTER_CONTAINER="nym-network-requester"
SOCKS5_CONTAINER="nym-socks5-client"
@@ -28,6 +29,7 @@ ALL_CONTAINERS=(
"$MIXNODE2_CONTAINER"
"$MIXNODE3_CONTAINER"
"$GATEWAY_CONTAINER"
"$GATEWAY2_CONTAINER"
"$REQUESTER_CONTAINER"
"$SOCKS5_CONTAINER"
)
@@ -57,7 +59,7 @@ log_error() {
cleanup_host_state() {
log_info "Cleaning local nym-node state for suffix ${SUFFIX}"
for node in mix1 mix2 mix3 gateway; do
for node in mix1 mix2 mix3 gateway gateway2; do
rm -rf "$HOME/.nym/nym-nodes/${node}-${SUFFIX}"
done
}
@@ -283,6 +285,73 @@ start_gateway() {
done
log_success "Gateway is ready on port 9000"
}
# Start gateway2
start_gateway2() {
log_info "Starting $GATEWAY2_CONTAINER..."
container run \
--name "$GATEWAY2_CONTAINER" \
-m 2G \
--network "$NETWORK_NAME" \
-p 9001:9001 \
-p 10005:10005 \
-p 20005:20005 \
-p 30005:30005 \
-p 41265:41265 \
-p 51265:51265 \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
-d \
-e "NYM_NODE_SUFFIX=$SUFFIX" \
"$IMAGE_NAME" \
sh -c '
CONTAINER_IP=$(hostname -i);
echo "Container IP: $CONTAINER_IP";
echo "Initializing gateway2...";
nym-node run --id gateway2-localnet --init-only \
--unsafe-disable-replay-protection \
--local \
--mode entry-gateway \
--mode exit-gateway \
--mixnet-bind-address=0.0.0.0:10005 \
--entry-bind-address=0.0.0.0:9001 \
--verloc-bind-address=0.0.0.0:20005 \
--http-bind-address=0.0.0.0:30005 \
--http-access-token=lala \
--public-ips $CONTAINER_IP \
--enable-lp true \
--lp-use-mock-ecash true \
--output=json \
--wireguard-enabled true \
--wireguard-userspace true \
--bonding-information-output="/localnet/gateway2.json";
echo "Waiting for network.json...";
while [ ! -f /localnet/network.json ]; do
sleep 2;
done;
echo "Starting gateway2 with LP listener (mock ecash)...";
exec nym-node run --id gateway2-localnet --unsafe-disable-replay-protection --local --wireguard-enabled true --wireguard-userspace true --lp-use-mock-ecash true
'
log_success "$GATEWAY2_CONTAINER started"
# Wait for gateway2 to be ready
log_info "Waiting for gateway2 to listen on port 9001..."
local retries=0
local max_retries=30
while ! nc -z 127.0.0.1 9001 2>/dev/null; do
sleep 2
retries=$((retries + 1))
if [ $retries -ge $max_retries ]; then
log_error "Gateway2 failed to start on port 9001"
return 1
fi
done
log_success "Gateway2 is ready on port 9001"
}
# Start network requester
start_network_requester() {
log_info "Starting $REQUESTER_CONTAINER..."
@@ -473,7 +542,7 @@ build_topology() {
# Wait for all bonding JSON files to be created
log_info "Waiting for all nodes to complete initialization..."
for file in mix1.json mix2.json mix3.json gateway.json; do
for file in mix1.json mix2.json mix3.json gateway.json gateway2.json; do
while [ ! -f "$VOLUME_PATH/$file" ]; do
echo " Waiting for $file..."
sleep 1
@@ -487,12 +556,14 @@ build_topology() {
MIX2_IP=$(container exec "$MIXNODE2_CONTAINER" hostname -i)
MIX3_IP=$(container exec "$MIXNODE3_CONTAINER" hostname -i)
GATEWAY_IP=$(container exec "$GATEWAY_CONTAINER" hostname -i)
GATEWAY2_IP=$(container exec "$GATEWAY2_CONTAINER" hostname -i)
log_info "Container IPs:"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
echo " mix1: $MIX1_IP"
echo " mix2: $MIX2_IP"
echo " mix3: $MIX3_IP"
echo " gateway: $GATEWAY_IP"
echo " gateway2: $GATEWAY2_IP"
# Run build_topology.py in a container with access to the volumes
container run \
@@ -508,7 +579,8 @@ build_topology() {
"$MIX1_IP" \
"$MIX2_IP" \
"$MIX3_IP" \
"$GATEWAY_IP"
"$GATEWAY_IP" \
"$GATEWAY2_IP"
# Verify network.json was created
if [ -f "$VOLUME_PATH/network.json" ]; then
@@ -532,6 +604,7 @@ start_all() {
start_mixnode 2 "$MIXNODE2_CONTAINER"
start_mixnode 3 "$MIXNODE3_CONTAINER"
start_gateway
start_gateway2
build_topology
start_network_requester
start_socks5_client
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -19,10 +19,12 @@ impl LpGatewayHandshake {
/// Create a new responder (gateway side) handshake
///
/// # Arguments
/// * `receiver_index` - Client-proposed receiver_index (from ClientHello)
/// * `gateway_ed25519_keypair` - Gateway's Ed25519 identity keypair (for PSQ auth and X25519 derivation)
/// * `client_ed25519_public_key` - Client's Ed25519 public key (from ClientHello)
/// * `salt` - Salt from ClientHello (for PSK derivation)
pub fn new_responder(
receiver_index: u32,
gateway_ed25519_keypair: (
&nym_crypto::asymmetric::ed25519::PrivateKey,
&nym_crypto::asymmetric::ed25519::PublicKey,
@@ -31,6 +33,7 @@ impl LpGatewayHandshake {
salt: &[u8; 32],
) -> Result<Self, GatewayError> {
let state_machine = LpStateMachine::new(
receiver_index,
false, // responder
gateway_ed25519_keypair,
client_ed25519_public_key,
@@ -114,9 +117,9 @@ impl LpGatewayHandshake {
use bytes::BytesMut;
use nym_lp::codec::serialize_lp_packet;
// Serialize the packet first
// Serialize the packet first (None key during handshake phase)
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf).map_err(|e| {
serialize_lp_packet(packet, &mut packet_buf, None).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e))
})?;
@@ -169,7 +172,8 @@ impl LpGatewayHandshake {
GatewayError::LpConnectionError(format!("Failed to read packet data: {}", e))
})?;
let packet = parse_lp_packet(&packet_buf)
// Parse packet (None key during handshake phase)
let packet = parse_lp_packet(&packet_buf, None)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)))?;
debug!("Received LP packet ({} bytes + 4 byte header)", packet_len);
+290
View File
@@ -53,14 +53,26 @@
// - lp_connections_completed_gracefully: Counter for connections that completed successfully
// - lp_connections_completed_with_error: Counter for connections that terminated with an error
//
// ## State Cleanup Metrics (in cleanup task)
// - lp_states_cleanup_handshake_removed: Counter for stale handshakes removed by cleanup task
// - lp_states_cleanup_session_removed: Counter for stale sessions removed by cleanup task
// - lp_states_cleanup_demoted_removed: Counter for demoted (read-only) sessions removed by cleanup task
//
// ## Subsession/Rekeying Metrics (in handler.rs)
// - lp_subsession_kk2_sent: Counter for SubsessionKK2 responses sent (indicates client initiated rekeying)
// - lp_subsession_complete: Counter for successful subsession promotions
// - lp_subsession_receiver_index_collision: Counter for subsession receiver_index collisions
//
// ## Usage Example
// To view metrics, the nym-metrics registry automatically collects all metrics.
// They can be exported via Prometheus format using the metrics endpoint.
use crate::error::GatewayError;
use crate::node::ActiveClientsStore;
use dashmap::DashMap;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_storage::GatewayStorage;
use nym_lp::state_machine::LpStateMachine;
use nym_node_metrics::NymNodeMetrics;
use nym_task::ShutdownTracker;
use nym_wireguard::{PeerControlRequest, WireguardGatewayData};
@@ -119,6 +131,44 @@ pub struct LpConfig {
/// WARNING: Only use this for local testing! Never enable in production.
#[serde(default = "default_use_mock_ecash")]
pub use_mock_ecash: bool,
/// Maximum age of in-progress handshakes before cleanup (default: 90s)
///
/// Handshakes should complete quickly (3-5 packets). This TTL accounts for:
/// - Network latency and retransmits
/// - Slow clients
/// - Clock skew tolerance
///
/// Stale handshakes are removed by the cleanup task to prevent memory leaks.
#[serde(default = "default_handshake_ttl_secs")]
pub handshake_ttl_secs: u64,
/// Maximum age of established sessions before cleanup (default: 24h)
///
/// Sessions can be long-lived for dVPN tunnels. This TTL should be set
/// high enough to accommodate expected usage patterns:
/// - dVPN sessions: hours to days
/// - Registration: minutes
///
/// Sessions with no activity for this duration are removed by the cleanup task.
#[serde(default = "default_session_ttl_secs")]
pub session_ttl_secs: u64,
/// Maximum age of demoted (read-only) sessions before cleanup (default: 60s)
///
/// After subsession promotion, old sessions enter ReadOnlyTransport state.
/// They only need to stay alive briefly to drain in-flight packets.
/// This shorter TTL prevents memory buildup from frequent rekeying.
#[serde(default = "default_demoted_session_ttl_secs")]
pub demoted_session_ttl_secs: u64,
/// How often to run the state cleanup task (default: 5 minutes)
///
/// The cleanup task scans for and removes stale handshakes and sessions.
/// Lower values = more frequent cleanup but higher overhead.
/// Higher values = less overhead but slower memory reclamation.
#[serde(default = "default_state_cleanup_interval_secs")]
pub state_cleanup_interval_secs: u64,
}
impl Default for LpConfig {
@@ -131,6 +181,10 @@ impl Default for LpConfig {
max_connections: default_max_connections(),
timestamp_tolerance_secs: default_timestamp_tolerance_secs(),
use_mock_ecash: default_use_mock_ecash(),
handshake_ttl_secs: default_handshake_ttl_secs(),
session_ttl_secs: default_session_ttl_secs(),
demoted_session_ttl_secs: default_demoted_session_ttl_secs(),
state_cleanup_interval_secs: default_state_cleanup_interval_secs(),
}
}
}
@@ -159,6 +213,81 @@ fn default_use_mock_ecash() -> bool {
false // Always default to real ecash for security
}
fn default_handshake_ttl_secs() -> u64 {
90 // 90 seconds - handshakes should complete quickly
}
fn default_session_ttl_secs() -> u64 {
86400 // 24 hours - for long-lived dVPN sessions
}
fn default_demoted_session_ttl_secs() -> u64 {
60 // 1 minute - enough to drain in-flight packets after subsession promotion
}
fn default_state_cleanup_interval_secs() -> u64 {
300 // 5 minutes - balances memory reclamation with task overhead
}
/// Wrapper for state entries with timestamp tracking for cleanup
///
/// This wrapper adds `created_at` and `last_activity` timestamps to state entries,
/// enabling TTL-based cleanup of stale handshakes and sessions.
pub struct TimestampedState<T> {
/// The actual state (LpStateMachine or LpSession)
pub state: T,
/// When this state was created (never changes)
created_at: std::time::Instant,
/// Last activity timestamp (unix seconds, atomically updated)
///
/// For handshakes: never updated (use created_at for TTL)
/// For sessions: updated on every packet received
last_activity: std::sync::atomic::AtomicU64,
}
impl<T> TimestampedState<T> {
/// Create a new timestamped state
pub fn new(state: T) -> Self {
let now_instant = std::time::Instant::now();
let now_unix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
state,
created_at: now_instant,
last_activity: std::sync::atomic::AtomicU64::new(now_unix),
}
}
/// Update last_activity timestamp (cheap, lock-free operation)
pub fn touch(&self) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.last_activity.store(now, std::sync::atomic::Ordering::Relaxed);
}
/// Get age since creation
pub fn age(&self) -> std::time::Duration {
self.created_at.elapsed()
}
/// Get time since last activity (in seconds)
pub fn seconds_since_activity(&self) -> u64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let last = self.last_activity.load(std::sync::atomic::Ordering::Relaxed);
now.saturating_sub(last)
}
}
/// Shared state for LP connection handlers
#[derive(Clone)]
pub struct LpHandlerState {
@@ -186,6 +315,29 @@ pub struct LpHandlerState {
/// LP configuration (for timestamp validation, etc.)
pub lp_config: LpConfig,
/// In-progress handshakes keyed by session_id
///
/// Session ID is deterministically computed from both parties' X25519 keys immediately
/// after ClientHello. Used during handshake phase. After handshake completes,
/// state moves to session_states map.
///
/// Wrapped in TimestampedState for TTL-based cleanup of stale handshakes.
pub handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
/// Established sessions keyed by session_id
///
/// Used after handshake completes (session_id is deterministically computed from
/// both parties' X25519 keys). Enables stateless transport - each packet lookup
/// by session_id, decrypt/process, respond.
///
/// Wrapped in TimestampedState for TTL-based cleanup of inactive sessions.
///
/// Sessions are stored as LpStateMachine (not LpSession) to enable
/// subsession/rekeying support. The state machine handles subsession initiation
/// (SubsessionKK1/KK2/Ready) during transport phase, allowing long-lived connections
/// to rekey without re-authentication.
pub session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
}
/// LP listener that accepts TCP connections on port 41264
@@ -242,6 +394,9 @@ impl LpListener {
let shutdown_token = self.shutdown.clone_shutdown_token();
// Spawn background task for state cleanup
let _cleanup_handle = self.spawn_state_cleanup_task();
loop {
tokio::select! {
biased;
@@ -312,6 +467,141 @@ impl LpListener {
);
}
/// Spawn background task for cleaning up stale state entries
///
/// This task runs periodically (every `state_cleanup_interval_secs`) to remove:
/// - Handshake states older than `handshake_ttl_secs`
/// - Session states with no activity for `session_ttl_secs`
///
/// The task automatically stops when the shutdown signal is received.
fn spawn_state_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
let handshake_states = Arc::clone(&self.handler_state.handshake_states);
let session_states = Arc::clone(&self.handler_state.session_states);
let handshake_ttl = self.handler_state.lp_config.handshake_ttl_secs;
let session_ttl = self.handler_state.lp_config.session_ttl_secs;
let demoted_session_ttl = self.handler_state.lp_config.demoted_session_ttl_secs;
let interval_secs = self.handler_state.lp_config.state_cleanup_interval_secs;
let shutdown = self.shutdown.clone_shutdown_token();
let metrics = self.handler_state.metrics.clone();
info!(
"Starting LP state cleanup task (handshake_ttl={}s, session_ttl={}s, demoted_ttl={}s, interval={}s)",
handshake_ttl, session_ttl, demoted_session_ttl, interval_secs
);
self.shutdown.try_spawn_named(
Self::cleanup_loop(
handshake_states,
session_states,
handshake_ttl,
session_ttl,
demoted_session_ttl,
interval_secs,
shutdown,
metrics,
),
"LP::StateCleanup",
)
}
/// Background loop for cleaning up stale state entries
///
/// Runs periodically to scan handshake_states and session_states maps,
/// removing entries that have exceeded their TTL.
///
/// Demoted sessions (ReadOnlyTransport) use shorter TTL since they
/// only need to drain in-flight packets after subsession promotion.
async fn cleanup_loop(
handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
handshake_ttl_secs: u64,
session_ttl_secs: u64,
demoted_session_ttl_secs: u64,
interval_secs: u64,
shutdown: nym_task::ShutdownToken,
_metrics: NymNodeMetrics,
) {
use nym_lp::state_machine::LpStateBare;
use nym_metrics::inc_by;
let mut cleanup_interval =
tokio::time::interval(std::time::Duration::from_secs(interval_secs));
loop {
tokio::select! {
biased;
_ = shutdown.cancelled() => {
debug!("LP state cleanup task: received shutdown signal");
break;
}
_ = cleanup_interval.tick() => {
let start = std::time::Instant::now();
let mut hs_removed = 0u64;
let mut ss_removed = 0u64;
let mut demoted_removed = 0u64;
// Remove stale handshakes (based on age since creation)
handshake_states.retain(|_, timestamped| {
if timestamped.age().as_secs() > handshake_ttl_secs {
hs_removed += 1;
false
} else {
true
}
});
// Remove stale sessions (based on time since last activity)
// Use shorter TTL for demoted (ReadOnlyTransport) sessions
session_states.retain(|_, timestamped| {
let is_demoted = timestamped.state.bare_state() == LpStateBare::ReadOnlyTransport;
let ttl = if is_demoted {
demoted_session_ttl_secs
} else {
session_ttl_secs
};
if timestamped.seconds_since_activity() > ttl {
if is_demoted {
demoted_removed += 1;
} else {
ss_removed += 1;
}
false
} else {
true
}
});
if hs_removed > 0 || ss_removed > 0 || demoted_removed > 0 {
let duration = start.elapsed();
info!(
"LP state cleanup: removed {} handshakes, {} sessions, {} demoted (took {:.3}s)",
hs_removed,
ss_removed,
demoted_removed,
duration.as_secs_f64()
);
// Track metrics
if hs_removed > 0 {
inc_by!("lp_states_cleanup_handshake_removed", hs_removed as i64);
}
if ss_removed > 0 {
inc_by!("lp_states_cleanup_session_removed", ss_removed as i64);
}
if demoted_removed > 0 {
inc_by!("lp_states_cleanup_demoted_removed", demoted_removed as i64);
}
}
}
}
}
info!("LP state cleanup task shutdown complete");
}
fn active_lp_connections(&self) -> usize {
self.handler_state
.metrics
+17 -21
View File
@@ -142,7 +142,7 @@ pub async fn process_registration(
if !request.validate_timestamp(30) {
warn!("LP registration failed: timestamp too old or too far in future");
inc!("lp_registration_failed_timestamp");
return LpRegistrationResponse::error(session_id, "Invalid timestamp".to_string());
return LpRegistrationResponse::error("Invalid timestamp".to_string());
}
// 2. Process based on mode
@@ -163,10 +163,10 @@ pub async fn process_registration(
error!("LP WireGuard peer registration failed: {}", e);
inc!("lp_registration_dvpn_failed");
inc!("lp_errors_wg_peer_registration");
return LpRegistrationResponse::error(
session_id,
format!("WireGuard peer registration failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"WireGuard peer registration failed: {}",
e
));
}
};
@@ -196,19 +196,16 @@ pub async fn process_registration(
remove_err
);
}
return LpRegistrationResponse::error(
session_id,
format!("Credential verification failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"Credential verification failed: {}",
e
));
}
};
info!(
"LP dVPN registration successful for session {} (client_id: {})",
session_id, client_id
);
info!("LP dVPN registration successful (client_id: {})", client_id);
inc!("lp_registration_dvpn_success");
LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data)
LpRegistrationResponse::success(allocated_bandwidth, gateway_data)
}
RegistrationMode::Mixnet {
client_id: client_id_bytes,
@@ -244,18 +241,18 @@ pub async fn process_registration(
client_id, e
);
inc!("lp_registration_mixnet_failed");
return LpRegistrationResponse::error(
session_id,
format!("Credential verification failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"Credential verification failed: {}",
e
));
}
};
// For mixnet mode, we don't have WireGuard data
// In the future, this would set up mixnet-specific state
info!(
"LP Mixnet registration successful for session {} (client_id: {})",
session_id, client_id
"LP Mixnet registration successful (client_id: {})",
client_id
);
inc!("lp_registration_mixnet_success");
LpRegistrationResponse {
@@ -263,7 +260,6 @@ pub async fn process_registration(
error: None,
gateway_data: None,
allocated_bandwidth,
session_id,
}
}
};
+2
View File
@@ -337,6 +337,8 @@ impl GatewayTasksBuilder {
wg_peer_controller,
wireguard_data: self.wireguard_data.as_ref().map(|wd| wd.inner.clone()),
lp_config: self.config.lp.clone(),
handshake_states: Arc::new(dashmap::DashMap::new()),
session_states: Arc::new(dashmap::DashMap::new()),
};
// Parse bind address from config
+429 -33
View File
@@ -147,7 +147,7 @@ impl TestedNode {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct TestedNodeDetails {
identity: NodeIdentity,
exit_router_address: Option<Recipient>,
@@ -165,6 +165,8 @@ pub struct Probe {
credentials_args: CredentialArgs,
/// Pre-queried gateway node (used when --gateway-ip is specified)
direct_gateway_node: Option<DirectoryNode>,
/// Pre-queried exit gateway node (used when --exit-gateway-ip is specified for LP forwarding)
exit_gateway_node: Option<DirectoryNode>,
}
impl Probe {
@@ -181,6 +183,7 @@ impl Probe {
netstack_args,
credentials_args,
direct_gateway_node: None,
exit_gateway_node: None,
}
}
@@ -199,6 +202,27 @@ impl Probe {
netstack_args,
credentials_args,
direct_gateway_node: Some(gateway_node),
exit_gateway_node: None,
}
}
/// Create a probe with both entry and exit gateways pre-queried (for LP forwarding tests)
pub fn new_with_gateways(
entrypoint: NodeIdentity,
tested_node: TestedNode,
netstack_args: NetstackArgs,
credentials_args: CredentialArgs,
entry_gateway_node: DirectoryNode,
exit_gateway_node: DirectoryNode,
) -> Self {
Self {
entrypoint,
tested_node,
amnezia_args: "".into(),
netstack_args,
credentials_args,
direct_gateway_node: Some(entry_gateway_node),
exit_gateway_node: Some(exit_gateway_node),
}
}
@@ -214,6 +238,7 @@ impl Probe {
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
min_mixnet_performance: Option<u8>,
) -> anyhow::Result<ProbeResult> {
let tickets_materials = self.credentials_args.decode_attached_ticket_materials()?;
@@ -242,14 +267,16 @@ impl Probe {
let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await;
self.do_probe_test(
mixnet_client,
Some(mixnet_client),
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
tested_entry,
only_wireguard,
only_lp_registration,
test_lp_wg,
false, // Not using mock ecash in regular probe mode
)
.await
@@ -265,9 +292,46 @@ impl Probe {
ignore_egress_epoch_role: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
min_mixnet_performance: Option<u8>,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult> {
// If both gateways are pre-queried via --gateway-ip and --exit-gateway-ip,
// skip mixnet setup entirely - we have all the data we need
if self.direct_gateway_node.is_some() && self.exit_gateway_node.is_some() {
let entry_node = self.direct_gateway_node.as_ref().unwrap();
let exit_node = self.exit_gateway_node.as_ref().unwrap();
// Initialize storage (needed for credentials)
if !config_dir.exists() {
std::fs::create_dir_all(config_dir)?;
}
let storage_paths = StoragePaths::new_from_dir(config_dir)?;
let storage = storage_paths
.initialise_default_persistent_storage()
.await?;
// Get node details from pre-queried nodes
let mixnet_entry_gateway_id = entry_node.identity();
let node_info = exit_node.to_testable_node()?;
return self
.do_probe_test(
None,
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
false, // tested_entry
only_wireguard,
only_lp_registration,
test_lp_wg,
use_mock_ecash,
)
.await;
}
// If only testing LP registration, use the dedicated LP-only path
// This skips mixnet setup entirely and allows testing local gateways
if only_lp_registration {
@@ -340,14 +404,16 @@ impl Probe {
let mixnet_client = Box::pin(disconnected_mixnet_client.connect_to_mixnet()).await;
self.do_probe_test(
mixnet_client,
Some(mixnet_client),
storage,
mixnet_entry_gateway_id,
node_info,
directory.as_ref(),
nyxd_url,
tested_entry,
only_wireguard,
only_lp_registration,
test_lp_wg,
use_mock_ecash,
)
.await
@@ -497,14 +563,16 @@ impl Probe {
#[allow(clippy::too_many_arguments)]
pub async fn do_probe_test<T>(
&self,
mixnet_client: nym_sdk::Result<MixnetClient>,
mixnet_client: Option<nym_sdk::Result<MixnetClient>>,
storage: T,
mixnet_entry_gateway_id: NodeIdentity,
node_info: TestedNodeDetails,
directory: Option<&NymApiDirectory>,
nyxd_url: Url,
tested_entry: bool,
only_wireguard: bool,
only_lp_registration: bool,
test_lp_wg: bool,
use_mock_ecash: bool,
) -> anyhow::Result<ProbeResult>
where
@@ -513,8 +581,8 @@ impl Probe {
{
let mut rng = rand::thread_rng();
let mixnet_client = match mixnet_client {
Ok(mixnet_client) => mixnet_client,
Err(err) => {
Some(Ok(mixnet_client)) => Some(mixnet_client),
Some(Err(err)) => {
error!("Failed to connect to mixnet: {err}");
return Ok(ProbeResult {
node: node_info.identity.to_string(),
@@ -531,45 +599,131 @@ impl Probe {
},
});
}
None => None,
};
let nym_address = *mixnet_client.nym_address();
let entry_gateway = nym_address.gateway().to_base58_string();
let (outcome, mixnet_client) = if let Some(mixnet_client) = mixnet_client {
let nym_address = *mixnet_client.nym_address();
let entry_gateway = nym_address.gateway().to_base58_string();
info!("Successfully connected to entry gateway: {entry_gateway}");
info!("Our nym address: {nym_address}");
info!("Successfully connected to entry gateway: {entry_gateway}");
info!("Our nym address: {nym_address}");
// Now that we have a connected mixnet client, we can start pinging
let (outcome, mixnet_client) = if only_wireguard || only_lp_registration {
(
Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::success()
} else {
Entry::NotTested
},
as_exit: None,
wg: None,
lp: None,
}),
mixnet_client,
)
// Now that we have a connected mixnet client, we can start pinging
let (outcome, mixnet_client) = if only_wireguard || only_lp_registration {
(
Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::success()
} else {
Entry::NotTested
},
as_exit: None,
wg: None,
lp: None,
}),
mixnet_client,
)
} else {
do_ping(
mixnet_client,
nym_address,
node_info.exit_router_address,
tested_entry,
)
.await
};
(outcome, Some(mixnet_client))
} else if test_lp_wg {
// No mixnet client needed for LP-WG test with pre-queried nodes
// Create default outcome and continue to LP-WG test below
(Ok(ProbeOutcome {
as_entry: Entry::NotTested,
as_exit: None,
wg: None,
lp: None,
}), None)
} else {
do_ping(
mixnet_client,
nym_address,
node_info.exit_router_address,
tested_entry,
)
.await
// For non-LP-WG modes, missing mixnet client is a failure
(Ok(ProbeOutcome {
as_entry: if tested_entry {
Entry::fail_to_connect()
} else {
Entry::EntryFailure
},
as_exit: None,
wg: None,
lp: None,
}), None)
};
let wg_outcome = if only_lp_registration {
// Skip WireGuard test when only testing LP registration
WgProbeResults::default()
} else if test_lp_wg {
// Test WireGuard via LP registration (nested session forwarding)
info!("Testing WireGuard via LP registration (no mixnet)");
// Create bandwidth controller for LP registration
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(
&NymNetworkDetails::new_from_env(),
)?;
let client =
nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?;
let bw_controller = nym_bandwidth_controller::BandwidthController::new(
storage.credential_store().clone(),
client,
);
// Determine entry and exit gateways
let (entry_gateway, exit_gateway) = if let Some(exit_node) = &self.exit_gateway_node {
// Both entry and exit gateways were pre-queried (direct IP mode)
info!("Using pre-queried entry and exit gateways for LP forwarding test");
let entry_node = self
.direct_gateway_node
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Entry gateway not available"))?;
let entry_gateway = entry_node.to_testable_node()?;
let exit_gateway = exit_node.to_testable_node()?;
(entry_gateway, exit_gateway)
} else {
// Original behavior: query from directory
// The tested node is the exit
let exit_gateway = node_info.clone();
let directory = directory
.ok_or_else(|| anyhow::anyhow!("Directory is required for LP-WG test mode"))?;
let entry_gateway_node = directory.entry_gateway(&mixnet_entry_gateway_id)?;
let entry_gateway = entry_gateway_node.to_testable_node()?;
(entry_gateway, exit_gateway)
};
wg_probe_lp(
&entry_gateway,
&exit_gateway,
&bw_controller,
storage.credential_store().clone(),
use_mock_ecash,
self.amnezia_args.clone(),
self.netstack_args.clone(),
)
.await
.unwrap_or_default()
} else if let (Some(authenticator), Some(ip_address)) =
(node_info.authenticator_address, node_info.ip_address)
{
let mixnet_client = if let Some(mixnet_client) = mixnet_client {
mixnet_client
} else {
bail!(
"Mixnet client is required for authenticator WireGuard probe, run in LP mode instead"
);
};
let nym_address = *mixnet_client.nym_address();
// Start the mixnet listener that the auth clients use to receive messages.
let mixnet_listener_task =
AuthClientMixnetListener::new(mixnet_client, CancellationToken::new()).start();
@@ -621,7 +775,6 @@ impl Probe {
outcome
} else {
mixnet_client.disconnect().await;
WgProbeResults::default()
};
@@ -1009,6 +1162,249 @@ where
Ok(lp_outcome)
}
/// LP-based WireGuard probe: Tests LP nested session registration + WireGuard tunnel connectivity
///
/// This function tests the full VPN flow using LP registration instead of mixnet+authenticator:
/// 1. Connects to entry gateway (outer LP session)
/// 2. Registers with exit gateway via entry forwarding (nested LP session)
/// 3. Receives WireGuard configuration from both gateways
/// 4. Tests WireGuard tunnel connectivity (IPv4/IPv6)
///
/// This validates that IP hiding works (exit sees entry IP, not client IP) and that the
/// full VPN tunnel operates correctly after LP registration.
async fn wg_probe_lp<St>(
entry_gateway: &TestedNodeDetails,
exit_gateway: &TestedNodeDetails,
bandwidth_controller: &nym_bandwidth_controller::BandwidthController<
nym_validator_client::nyxd::NyxdClient<nym_validator_client::HttpRpcClient>,
St,
>,
_storage: St,
use_mock_ecash: bool,
awg_args: String,
netstack_args: NetstackArgs,
) -> anyhow::Result<WgProbeResults>
where
St: nym_sdk::mixnet::CredentialStorage + Clone + Send + Sync + 'static,
<St as nym_sdk::mixnet::CredentialStorage>::StorageError: Send + Sync,
{
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
let mut wg_outcome = WgProbeResults::default();
// Validate that both gateways have required information
let entry_lp_address = entry_gateway
.lp_address
.ok_or_else(|| anyhow::anyhow!("Entry gateway missing LP address"))?;
let exit_lp_address = exit_gateway
.lp_address
.ok_or_else(|| anyhow::anyhow!("Exit gateway missing LP address"))?;
let entry_ip = entry_gateway
.ip_address
.ok_or_else(|| anyhow::anyhow!("Entry gateway missing IP address"))?;
let exit_ip = exit_gateway
.ip_address
.ok_or_else(|| anyhow::anyhow!("Exit gateway missing IP address"))?;
// Generate Ed25519 keypairs for LP protocol
let mut rng = rand::thread_rng();
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
// Generate WireGuard keypairs for VPN registration
let entry_wg_keypair = x25519::KeyPair::new(&mut rng);
let exit_wg_keypair = x25519::KeyPair::new(&mut rng);
// STEP 1: Establish outer LP session with entry gateway
info!("Connecting to entry gateway via LP...");
let mut entry_client = LpRegistrationClient::new_with_default_psk(
entry_lp_keypair,
entry_gateway.identity,
entry_lp_address,
entry_ip,
);
// Connect to entry gateway
if let Err(e) = entry_client.connect().await {
error!("Failed to connect to entry gateway: {}", e);
return Ok(wg_outcome);
}
// Perform handshake with entry gateway
if let Err(e) = entry_client.perform_handshake().await {
error!("Failed to handshake with entry gateway: {}", e);
return Ok(wg_outcome);
}
info!("Outer LP session with entry gateway established");
// STEP 2: Use nested session to register with exit gateway via forwarding
info!("Registering with exit gateway via entry forwarding...");
let mut nested_session = NestedLpSession::new(
exit_gateway.identity.to_bytes(),
exit_lp_address.to_string(),
exit_lp_keypair,
ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?,
);
// Convert exit gateway identity to ed25519 public key for registration
let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?;
// Perform handshake and registration with exit gateway via forwarding
if use_mock_ecash {
info!("Note: Using mock ecash mode - gateways must be started with --lp-use-mock-ecash");
}
let exit_gateway_data = match nested_session
.handshake_and_register(
&mut entry_client,
&exit_wg_keypair,
&exit_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardExit,
exit_ip,
)
.await
{
Ok(data) => data,
Err(e) => {
error!("Failed to register with exit gateway: {}", e);
return Ok(wg_outcome);
}
};
info!("Exit gateway registration successful via forwarding");
// STEP 3: Register with entry gateway
info!("Registering with entry gateway...");
let entry_gateway_pubkey =
ed25519::PublicKey::from_bytes(&entry_gateway.identity.to_bytes())
.map_err(|e| anyhow::anyhow!("Invalid entry gateway identity: {}", e))?;
if let Err(e) = entry_client
.send_registration_request(
&entry_wg_keypair,
&entry_gateway_pubkey,
bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await
{
error!("Failed to send entry registration request: {}", e);
return Ok(wg_outcome);
}
let _entry_gateway_data = match entry_client.receive_registration_response().await {
Ok(data) => data,
Err(e) => {
error!("Failed to receive entry registration response: {}", e);
return Ok(wg_outcome);
}
};
info!("Entry gateway registration successful");
info!("LP registration successful for both gateways!");
wg_outcome.can_register = true;
// STEP 4: Test WireGuard tunnels using exit gateway configuration
// Convert keys to hex for netstack
let private_key_hex = hex::encode(exit_wg_keypair.private_key().to_bytes());
let public_key_hex = hex::encode(exit_gateway_data.public_key.to_bytes());
// Build WireGuard endpoint address
let wg_endpoint = format!("{}:{}", exit_ip, exit_gateway_data.endpoint.port());
info!("Exit WireGuard configuration:");
info!(" Private IPv4: {}", exit_gateway_data.private_ipv4);
info!(" Private IPv6: {}", exit_gateway_data.private_ipv6);
info!(" Endpoint: {}", wg_endpoint);
// Run tunnel tests (copied from wg_probe)
let netstack_request = crate::netstack::NetstackRequest::new(
&exit_gateway_data.private_ipv4.to_string(),
&exit_gateway_data.private_ipv6.to_string(),
&private_key_hex,
&public_key_hex,
&wg_endpoint,
&format!("http://{WG_TUN_DEVICE_IP_ADDRESS_V4}:{WG_METADATA_PORT}"),
netstack_args.netstack_download_timeout_sec,
&awg_args,
netstack_args,
);
// Perform IPv4 ping test
info!("Testing IPv4 tunnel connectivity...");
let ipv4_request = crate::netstack::NetstackRequestGo::from_rust_v4(&netstack_request);
match crate::netstack::ping(&ipv4_request) {
Ok(NetstackResult::Response(netstack_response_v4)) => {
info!(
"Wireguard probe response for IPv4: {:#?}",
netstack_response_v4
);
wg_outcome.can_query_metadata_v4 = netstack_response_v4.can_query_metadata;
wg_outcome.can_handshake_v4 = netstack_response_v4.can_handshake;
wg_outcome.can_resolve_dns_v4 = netstack_response_v4.can_resolve_dns;
wg_outcome.ping_hosts_performance_v4 =
netstack_response_v4.received_hosts as f32 / netstack_response_v4.sent_hosts as f32;
wg_outcome.ping_ips_performance_v4 =
netstack_response_v4.received_ips as f32 / netstack_response_v4.sent_ips as f32;
wg_outcome.download_duration_sec_v4 = netstack_response_v4.download_duration_sec;
wg_outcome.download_duration_milliseconds_v4 =
netstack_response_v4.download_duration_milliseconds;
wg_outcome.downloaded_file_size_bytes_v4 =
netstack_response_v4.downloaded_file_size_bytes;
wg_outcome.downloaded_file_v4 = netstack_response_v4.downloaded_file;
wg_outcome.download_error_v4 = netstack_response_v4.download_error;
}
Ok(NetstackResult::Error { error }) => {
error!("Netstack runtime error (IPv4): {error}")
}
Err(error) => {
error!("Internal error (IPv4): {error}")
}
}
// Perform IPv6 ping test
info!("Testing IPv6 tunnel connectivity...");
let ipv6_request = crate::netstack::NetstackRequestGo::from_rust_v6(&netstack_request);
match crate::netstack::ping(&ipv6_request) {
Ok(NetstackResult::Response(netstack_response_v6)) => {
info!(
"Wireguard probe response for IPv6: {:#?}",
netstack_response_v6
);
wg_outcome.can_handshake_v6 = netstack_response_v6.can_handshake;
wg_outcome.can_resolve_dns_v6 = netstack_response_v6.can_resolve_dns;
wg_outcome.ping_hosts_performance_v6 =
netstack_response_v6.received_hosts as f32 / netstack_response_v6.sent_hosts as f32;
wg_outcome.ping_ips_performance_v6 =
netstack_response_v6.received_ips as f32 / netstack_response_v6.sent_ips as f32;
wg_outcome.download_duration_sec_v6 = netstack_response_v6.download_duration_sec;
wg_outcome.download_duration_milliseconds_v6 =
netstack_response_v6.download_duration_milliseconds;
wg_outcome.downloaded_file_size_bytes_v6 =
netstack_response_v6.downloaded_file_size_bytes;
wg_outcome.downloaded_file_v6 = netstack_response_v6.downloaded_file;
wg_outcome.download_error_v6 = netstack_response_v6.download_error;
}
Ok(NetstackResult::Error { error }) => {
error!("Netstack runtime error (IPv6): {error}")
}
Err(error) => {
error!("Internal error (IPv6): {error}")
}
}
info!("LP-based WireGuard probe completed");
Ok(wg_outcome)
}
fn mixnet_debug_config(
min_gateway_performance: Option<u8>,
ignore_egress_epoch_role: bool,
+1
View File
@@ -92,6 +92,7 @@ pub struct GatewayTasksConfig {
pub auth_opts: Option<LocalAuthenticatorOpts>,
#[allow(dead_code)]
pub wg_opts: LocalWireguardOpts,
#[allow(dead_code)]
pub lp: nym_gateway::node::LpConfig,
}
@@ -1404,7 +1404,6 @@ pub async fn try_upgrade_config_v10<P: AsRef<Path>>(
},
..Default::default()
},
lp: Default::default(),
},
service_providers: ServiceProvidersConfig {
storage_paths: ServiceProvidersPaths {
+67 -102
View File
@@ -14,7 +14,6 @@ use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
use std::sync::Arc;
use crate::config::RegistrationClientConfig;
use crate::lp_client::{LpClientError, LpTransport};
mod builder;
mod config;
@@ -29,7 +28,7 @@ pub use builder::config::{
};
pub use config::RegistrationMode;
pub use error::RegistrationClientError;
pub use lp_client::{LpConfig, LpRegistrationClient};
pub use lp_client::{LpConfig, LpRegistrationClient, NestedLpSession};
pub use types::{
LpRegistrationResult, MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult,
};
@@ -155,6 +154,8 @@ impl RegistrationClient {
}
async fn register_lp(self) -> Result<RegistrationResult, RegistrationClientError> {
use crate::lp_client::{LpRegistrationClient, NestedLpSession};
// Extract and validate LP addresses
let entry_lp_address = self.config.entry.node.lp_address.ok_or(
RegistrationClientError::LpRegistrationNotPossible {
@@ -178,118 +179,82 @@ impl RegistrationClient {
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
// Register entry gateway via LP
let entry_fut = {
let bandwidth_controller = &self.bandwidth_controller;
let entry_keys = self.config.entry.keys.clone();
let entry_identity = self.config.entry.node.identity;
let entry_ip = self.config.entry.node.ip_address;
let entry_lp_keys = entry_lp_keypair.clone();
// STEP 1: Establish outer session with entry gateway
// This creates the LP session that will be used to forward packets to exit.
// Uses packet-per-connection model: each handshake packet on new TCP connection.
tracing::info!("Establishing outer session with entry gateway");
let mut entry_client = LpRegistrationClient::new_with_default_psk(
entry_lp_keypair.clone(),
self.config.entry.node.identity,
entry_lp_address,
self.config.entry.node.ip_address,
);
async move {
let mut client = LpRegistrationClient::new_with_default_psk(
entry_lp_keys,
entry_identity,
entry_lp_address,
entry_ip,
);
// Connect
client.connect().await?;
// Perform handshake
client.perform_handshake().await?;
// Send registration request
client
.send_registration_request(
&entry_keys,
&entry_identity,
&**bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await?;
// Receive registration response
let gateway_data = client.receive_registration_response().await?;
// Convert to transport for ongoing communication
let transport = client.into_transport()?;
Ok::<(LpTransport, _), LpClientError>((transport, gateway_data))
}
};
// Register exit gateway via LP
let exit_fut = {
let bandwidth_controller = &self.bandwidth_controller;
let exit_keys = self.config.exit.keys.clone();
let exit_identity = self.config.exit.node.identity;
let exit_ip = self.config.exit.node.ip_address;
let exit_lp_keys = exit_lp_keypair;
async move {
let mut client = LpRegistrationClient::new_with_default_psk(
exit_lp_keys,
exit_identity,
exit_lp_address,
exit_ip,
);
// Connect
client.connect().await?;
// Perform handshake
client.perform_handshake().await?;
// Send registration request
client
.send_registration_request(
&exit_keys,
&exit_identity,
&**bandwidth_controller,
TicketType::V1WireguardExit,
)
.await?;
// Receive registration response
let gateway_data = client.receive_registration_response().await?;
// Convert to transport for ongoing communication
let transport = client.into_transport()?;
Ok::<(LpTransport, _), LpClientError>((transport, gateway_data))
}
};
// Execute registrations in parallel
let (entry_result, exit_result) =
Box::pin(async { tokio::join!(entry_fut, exit_fut) }).await;
// Handle entry gateway result
// Note: entry_transport is dropped here, closing the LP connection
let (_entry_transport, entry_gateway_data) =
entry_result.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
// Perform handshake with entry gateway (outer session now established)
entry_client
.perform_handshake()
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
// Handle exit gateway result
// Note: exit_transport is dropped here, closing the LP connection
let (_exit_transport, exit_gateway_data) =
exit_result.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
tracing::info!("Outer session with entry gateway established");
// STEP 2: Use nested session to register with exit gateway via forwarding
// This hides the client's IP address from the exit gateway
tracing::info!("Registering with exit gateway via entry forwarding");
let mut nested_session = NestedLpSession::new(
self.config.exit.node.identity.to_bytes(),
exit_lp_address.to_string(),
exit_lp_keypair,
self.config.exit.node.identity,
);
// Perform handshake and registration with exit gateway (all via entry forwarding)
let exit_gateway_data = nested_session
.handshake_and_register(
&mut entry_client,
&self.config.exit.keys,
&self.config.exit.node.identity,
&*self.bandwidth_controller,
TicketType::V1WireguardExit,
self.config.exit.node.ip_address,
)
.await
.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
gateway_id: self.config.exit.node.identity.to_base58_string(),
lp_address: exit_lp_address,
source: Box::new(source),
})?;
tracing::info!(
"LP registration successful for both gateways (LP connections will be closed)"
);
tracing::info!("Exit gateway registration completed via forwarding");
// LP is registration-only. All data flows through WireGuard after this point.
// The LP transports have been dropped, automatically closing TCP connections.
// STEP 3: Register with entry gateway (packet-per-connection)
tracing::info!("Registering with entry gateway");
let entry_gateway_data = entry_client
.register(
&self.config.entry.keys,
&self.config.entry.node.identity,
&*self.bandwidth_controller,
TicketType::V1WireguardEntry,
)
.await
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
gateway_id: self.config.entry.node.identity.to_base58_string(),
lp_address: entry_lp_address,
source: Box::new(source),
})?;
tracing::info!("Entry gateway registration successful");
tracing::info!("LP registration successful for both gateways");
// LP is registration-only (packet-per-connection model).
// All data flows through WireGuard after this point.
// Each LP packet used its own TCP connection which was closed after the exchange.
// Exit registration was completed via forwarding through entry gateway.
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
entry_gateway_data,
exit_gateway_data,
File diff suppressed because it is too large Load Diff
+10 -9
View File
@@ -8,34 +8,35 @@
//! registration while maintaining security through Noise protocol handshakes and credential
//! verification.
//!
//! Uses a packet-per-connection model: each LP packet exchange opens a new TCP connection,
//! sends one packet, receives one response, then closes. Session state is maintained in
//! the state machine across connections.
//!
//! # Usage
//!
//! ```ignore
//! use nym_registration_client::lp_client::LpRegistrationClient;
//!
//! let client = LpRegistrationClient::new_with_default_psk(
//! let mut client = LpRegistrationClient::new_with_default_psk(
//! keypair,
//! gateway_public_key,
//! gateway_lp_address,
//! client_ip,
//! );
//!
//! // Establish TCP connection
//! client.connect().await?;
//!
//! // Perform handshake (nym-79)
//! // Perform handshake (multiple packet-per-connection exchanges)
//! client.perform_handshake().await?;
//!
//! // Register with gateway (nym-80, nym-81)
//! let response = client.register(credential, ticket_type).await?;
//! // Register with gateway (single packet-per-connection exchange)
//! let gateway_data = client.register(wg_keypair, gateway_identity, bandwidth_controller, ticket_type).await?;
//! ```
mod client;
mod config;
mod error;
mod transport;
mod nested_session;
pub use client::LpRegistrationClient;
pub use config::LpConfig;
pub use error::LpClientError;
pub use transport::LpTransport;
pub use nested_session::NestedLpSession;
@@ -0,0 +1,520 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Nested LP session for client-exit handshake through entry gateway forwarding.
//!
//! This module implements the inner LP session management where a client establishes
//! a secure connection with an exit gateway by forwarding LP packets through an
//! entry gateway. This hides the client's IP address from the exit gateway.
//!
//! # Architecture
//!
//! ```text
//! Client ←→ Entry Gateway (outer session, encrypted)
//! ↓ forwards
//! Exit Gateway (inner session, client establishes handshake)
//! ```
//!
//! The entry gateway sees the client's IP but doesn't know the final destination.
//! The exit gateway processes the LP handshake but only sees the entry gateway's IP.
use super::client::LpRegistrationClient;
use super::error::{LpClientError, Result};
use bytes::BytesMut;
use nym_bandwidth_controller::BandwidthTicketProvider;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey};
use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine};
use nym_lp::{LpMessage, LpPacket};
use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse};
use nym_wireguard_types::PeerPublicKey;
use std::net::IpAddr;
use std::sync::Arc;
/// Manages a nested LP session where the client establishes a handshake with
/// an exit gateway by forwarding packets through an entry gateway.
///
/// # Example
///
/// ```ignore
/// // Outer session already established with entry gateway
/// let mut outer_client = LpRegistrationClient::new(...);
/// outer_client.perform_handshake().await?;
///
/// // Now establish inner session with exit gateway
/// let mut nested = NestedLpSession::new(
/// exit_identity,
/// "2.2.2.2:41264".to_string(),
/// client_keypair,
/// exit_public_key,
/// );
///
/// let gateway_data = nested.handshake_and_register(&mut outer_client, ...).await?;
/// ```
pub struct NestedLpSession {
/// Exit gateway's Ed25519 identity (32 bytes)
exit_identity: [u8; 32],
/// Exit gateway's LP address (e.g., "2.2.2.2:41264")
exit_address: String,
/// Client's Ed25519 keypair (for PSQ authentication and X25519 derivation)
client_keypair: Arc<ed25519::KeyPair>,
/// Exit gateway's Ed25519 public key
exit_public_key: ed25519::PublicKey,
/// LP state machine for exit gateway session (populated after handshake)
state_machine: Option<LpStateMachine>,
}
impl NestedLpSession {
/// Creates a new nested LP session handler.
///
/// # Arguments
/// * `exit_identity` - Exit gateway's Ed25519 identity (32 bytes)
/// * `exit_address` - Exit gateway's LP address (e.g., "2.2.2.2:41264")
/// * `client_keypair` - Client's Ed25519 keypair
/// * `exit_public_key` - Exit gateway's Ed25519 public key
pub fn new(
exit_identity: [u8; 32],
exit_address: String,
client_keypair: Arc<ed25519::KeyPair>,
exit_public_key: ed25519::PublicKey,
) -> Self {
Self {
exit_identity,
exit_address,
client_keypair,
exit_public_key,
state_machine: None,
}
}
/// Performs the LP handshake with the exit gateway by forwarding packets
/// through the entry gateway.
///
/// This method:
/// 1. Generates ClientHello for exit gateway
/// 2. Creates LP state machine for exit handshake
/// 3. Runs handshake loop, forwarding all packets through entry gateway
/// 4. Stores established session in internal state machine
///
/// # Arguments
/// * `outer_client` - Connected LP client with established outer session to entry gateway
///
/// # Errors
/// Returns an error if:
/// - Packet serialization/parsing fails
/// - Forwarding through entry gateway fails
/// - Exit gateway handshake fails
/// - Cryptographic operations fail
async fn perform_handshake(
&mut self,
outer_client: &mut LpRegistrationClient,
) -> Result<()> {
tracing::debug!(
"Starting nested LP handshake with exit gateway {}",
self.exit_address
);
// Step 1: Derive X25519 keys from Ed25519 for Noise protocol
let client_x25519_public = self
.client_keypair
.public_key()
.to_x25519()
.map_err(|e| {
LpClientError::Crypto(format!("Failed to derive X25519 public key: {}", e))
})?;
// Step 2: Generate ClientHello for exit gateway
let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt(
client_x25519_public.to_bytes(),
self.client_keypair.public_key().to_bytes(),
);
let salt = client_hello_data.salt;
let receiver_index = client_hello_data.receiver_index;
tracing::trace!(
"Generated ClientHello for exit gateway (timestamp: {})",
client_hello_data.extract_timestamp()
);
// Step 3: Send ClientHello to exit gateway via forwarding
let client_hello_header = nym_lp::packet::LpHeader::new(
nym_lp::BOOTSTRAP_RECEIVER_IDX, // Use constant for bootstrap session
0, // counter starts at 0
);
let client_hello_packet = nym_lp::LpPacket::new(
client_hello_header,
LpMessage::ClientHello(client_hello_data),
);
// Serialize and forward ClientHello (no state machine yet, no outer key)
let client_hello_bytes = Self::serialize_packet(&client_hello_packet, None)?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
client_hello_bytes,
)
.await?;
// Parse and validate Ack response (cleartext, no outer key before PSK derivation)
let ack_response = Self::parse_packet(&response_bytes, None)?;
match ack_response.message() {
LpMessage::Ack => {
tracing::debug!("Received Ack for ClientHello from exit gateway");
}
LpMessage::Collision => {
return Err(LpClientError::Transport(format!(
"Exit gateway returned Collision - receiver_index {} already in use",
receiver_index
)));
}
other => {
return Err(LpClientError::Transport(format!(
"Expected Ack for ClientHello from exit gateway, got: {:?}",
other
)));
}
}
// Step 4: Create state machine for exit gateway handshake
let mut state_machine = LpStateMachine::new(
receiver_index,
true, // is_initiator
(
self.client_keypair.private_key(),
self.client_keypair.public_key(),
),
&self.exit_public_key,
&salt,
)?;
// Step 5: Get initial packet from StartHandshake
let mut pending_packet: Option<LpPacket> = None;
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
match action? {
LpAction::SendPacket(packet) => {
pending_packet = Some(packet);
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action at handshake start: {:?}",
other
)));
}
}
}
// Step 6: Handshake loop - each packet on new connection via forwarding
loop {
if let Some(packet) = pending_packet.take() {
tracing::trace!("Sending handshake packet to exit via forwarding");
let response = self
.send_and_receive_via_forward(outer_client, &state_machine, &packet)
.await?;
tracing::trace!("Received handshake response from exit");
// Process the received packet
if let Some(action) =
state_machine.process_input(LpInput::ReceivePacket(response))
{
match action? {
LpAction::SendPacket(response_packet) => {
pending_packet = Some(response_packet);
// Check if handshake completed - send final packet if so
if state_machine.session()?.is_handshake_complete() {
if let Some(final_packet) = pending_packet.take() {
tracing::trace!("Sending final handshake packet to exit");
let _ = self
.send_and_receive_via_forward(
outer_client,
&state_machine,
&final_packet,
)
.await?;
}
tracing::info!(
"Nested LP handshake completed with exit gateway"
);
break;
}
}
LpAction::HandshakeComplete => {
tracing::info!("Nested LP handshake completed with exit gateway");
break;
}
LpAction::KKTComplete => {
tracing::info!("KKT exchange completed with exit, starting Noise");
// After KKT completes, initiator must send first Noise handshake message
let noise_msg = state_machine
.session()?
.prepare_handshake_message()
.ok_or_else(|| {
LpClientError::Transport(
"No handshake message available after KKT".to_string(),
)
})??;
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
pending_packet = Some(noise_packet);
}
other => {
tracing::trace!("Received action during handshake: {:?}", other);
}
}
}
} else {
// No pending packet and not complete - something is wrong
return Err(LpClientError::Transport(
"Nested handshake stalled: no packet to send".to_string(),
));
}
}
// Store the state machine (with established session) for later use
self.state_machine = Some(state_machine);
Ok(())
}
/// Performs handshake and registration with the exit gateway via forwarding.
///
/// This is the main entry point for nested LP registration. It:
/// 1. Performs handshake with exit gateway (via `perform_handshake`)
/// 2. Builds and sends registration request through the forwarded connection
/// 3. Receives and processes registration response
/// 4. Returns gateway data on successful registration
///
/// # Arguments
/// * `outer_client` - Connected LP client with established outer session to entry gateway
/// * `wg_keypair` - Client's WireGuard x25519 keypair
/// * `gateway_identity` - Exit gateway's Ed25519 identity (for credential verification)
/// * `bandwidth_controller` - Provider for bandwidth credentials
/// * `ticket_type` - Type of bandwidth ticket to use
/// * `client_ip` - Client IP address for registration metadata
///
/// # Returns
/// * `Ok(GatewayData)` - Exit gateway configuration data on successful registration
///
/// # Errors
/// Returns an error if:
/// - Handshake fails
/// - Credential acquisition fails
/// - Request serialization/encryption fails
/// - Forwarding through entry gateway fails
/// - Response decryption/deserialization fails
/// - Gateway rejects the registration
pub async fn handshake_and_register(
&mut self,
outer_client: &mut LpRegistrationClient,
wg_keypair: &x25519::KeyPair,
gateway_identity: &ed25519::PublicKey,
bandwidth_controller: &dyn BandwidthTicketProvider,
ticket_type: TicketType,
client_ip: IpAddr,
) -> Result<GatewayData> {
// Step 1: Perform handshake with exit gateway via forwarding
self.perform_handshake(outer_client).await?;
// Step 2: Get the state machine (must exist after successful handshake)
let state_machine = self.state_machine.as_mut().ok_or_else(|| {
LpClientError::Transport("State machine missing after handshake".to_string())
})?;
tracing::debug!("Building registration request for exit gateway");
// Step 3: Acquire bandwidth credential
let credential = bandwidth_controller
.get_ecash_ticket(ticket_type, *gateway_identity, nym_bandwidth_controller::DEFAULT_TICKETS_TO_SPEND)
.await
.map_err(|e| {
LpClientError::Transport(format!(
"Failed to acquire bandwidth credential: {}",
e
))
})?
.data;
// Step 4: Build registration request
let wg_public_key = PeerPublicKey::new(wg_keypair.public_key().to_bytes().into());
let request = LpRegistrationRequest::new_dvpn(wg_public_key, credential, ticket_type, client_ip);
tracing::trace!("Built registration request: {:?}", request);
// Step 5: Serialize the request
let request_bytes = bincode::serialize(&request).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize registration request: {}", e))
})?;
tracing::debug!(
"Sending registration request to exit gateway via forwarding ({} bytes)",
request_bytes.len()
);
// Step 6: Encrypt and prepare packet via state machine
let action = state_machine
.process_input(LpInput::SendData(request_bytes))
.ok_or_else(|| {
LpClientError::Transport("State machine returned no action".to_string())
})?
.map_err(|e| {
LpClientError::Transport(format!(
"Failed to encrypt registration request: {}",
e
))
})?;
// Step 7: Send the encrypted packet via forwarding
// Get outer key for AEAD encryption (PSK is available after handshake)
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_bytes = match action {
LpAction::SendPacket(packet) => {
let packet_bytes = Self::serialize_packet(&packet, outer_key.as_ref())?;
outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?
}
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when sending registration data: {:?}",
other
)));
}
};
tracing::trace!("Received registration response from exit gateway");
// Step 8: Parse response bytes to LP packet
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_packet = Self::parse_packet(&response_bytes, outer_key.as_ref())?;
// Step 9: Decrypt via state machine
let action = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
.ok_or_else(|| {
LpClientError::Transport("State machine returned no action".to_string())
})?
.map_err(|e| {
LpClientError::Transport(format!(
"Failed to decrypt registration response: {}",
e
))
})?;
// Step 10: Extract decrypted data
let response_data = match action {
LpAction::DeliverData(data) => data,
other => {
return Err(LpClientError::Transport(format!(
"Unexpected action when receiving registration response: {:?}",
other
)));
}
};
// Step 11: Deserialize the response
let response: LpRegistrationResponse =
bincode::deserialize(&response_data).map_err(|e| {
LpClientError::Transport(format!(
"Failed to deserialize registration response: {}",
e
))
})?;
tracing::debug!(
"Received registration response from exit: success={}",
response.success,
);
// Step 12: Validate and extract GatewayData
if !response.success {
let error_msg = response
.error
.unwrap_or_else(|| "Unknown error".to_string());
tracing::warn!("Exit gateway rejected registration: {}", error_msg);
return Err(LpClientError::RegistrationRejected { reason: error_msg });
}
// Extract gateway_data
let gateway_data = response.gateway_data.ok_or_else(|| {
LpClientError::Transport(
"Gateway response missing gateway_data despite success=true".to_string(),
)
})?;
tracing::info!(
"Exit gateway registration successful! Allocated bandwidth: {} bytes",
response.allocated_bandwidth
);
Ok(gateway_data)
}
/// Sends a packet via forwarding through the entry gateway and returns the parsed response.
///
/// This helper consolidates the send/receive pattern used throughout the handshake:
/// 1. Gets outer AEAD key from state machine (if available)
/// 2. Serializes the packet with outer encryption
/// 3. Forwards via entry gateway
/// 4. Parses and returns the response
async fn send_and_receive_via_forward(
&self,
outer_client: &mut LpRegistrationClient,
state_machine: &LpStateMachine,
packet: &LpPacket,
) -> Result<LpPacket> {
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let packet_bytes = Self::serialize_packet(packet, outer_key.as_ref())?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
self.exit_address.clone(),
packet_bytes,
)
.await?;
Self::parse_packet(&response_bytes, outer_key.as_ref())
}
/// Serializes an LP packet to bytes.
///
/// # Arguments
/// * `packet` - The LP packet to serialize
///
/// # Returns
/// * `Ok(Vec<u8>)` - Serialized packet bytes
///
/// # Errors
/// Returns an error if serialization fails
fn serialize_packet(packet: &LpPacket, outer_key: Option<&OuterAeadKey>) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
// Use outer AEAD key when available (after PSK derivation)
serialize_lp_packet(packet, &mut buf, outer_key).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize LP packet: {}", e))
})?;
Ok(buf.to_vec())
}
/// Parses an LP packet from bytes.
///
/// # Arguments
/// * `bytes` - The bytes to parse
///
/// # Returns
/// * `Ok(LpPacket)` - Parsed LP packet
///
/// # Errors
/// Returns an error if parsing fails
fn parse_packet(bytes: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<LpPacket> {
// Use outer AEAD key when available (after PSK derivation)
parse_lp_packet(bytes, outer_key).map_err(|e| {
LpClientError::Transport(format!("Failed to parse LP packet: {}", e))
})
}
}