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:
committed by
Jędrzej Stuczyński
parent
0a6f78a921
commit
637459b153
@@ -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
@@ -6683,6 +6683,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"criterion",
|
||||
"dashmap",
|
||||
"libcrux-kem",
|
||||
@@ -6704,6 +6705,7 @@ dependencies = [
|
||||
"tls_codec",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user