Add UDP forwarder pattern (copied from VPN client) to enable proper two-hop
tunneling where traffic flows: Client → Entry Gateway → Exit Gateway → Internet.
Key changes:
- Add udp_forwarder.go for tunnel-in-tunnel traffic forwarding
- Add wgPingTwoHop() Go function and Rust FFI bindings
- Configure NAT/iptables in localnet for gateway routing
- Remove unnecessary PSK from gateway LP registration (was breaking handshakes)
- Document known container networking instability issue (nym-vbdo)
The probe now correctly uses the entry tunnel to reach the exit gateway's
WireGuard endpoint, rather than trying to connect directly to unreachable
container-internal IPs.
Key changes:
- Add outer_aead_key_for_sending() to gate outer encryption on PSQ completion
(fixes bug where initiator encrypted msg 1 before responder could decrypt)
- Add handshake_and_register_with_credential() to NestedLpSession for mock ecash
- Update PSQState::InitiatorWaiting to store PSK instead of ciphertext
- Add probe-localnet.sh script for two-hop localnet testing
- Update gateway handler with connection lifecycle statistics
The PSK timing fix ensures the first Noise message is sent in cleartext
because the responder hasn't derived the PSK yet from the PSQ payload.
- Add early validation for --mode two-hop without exit gateway (nym-c0hl)
Clear error message instead of silent failure deep in probe
- Add safe_ratio() helper for division by zero protection (nym-ktvz)
Returns 0.0 when sent_hosts/sent_ips is 0 instead of NaN/Inf
- Refactor run_tunnel_tests to take &mut WgProbeResults (nym-4v1p)
Eliminates 40+ lines of field-by-field copying at call sites
Closes: nym-c0hl, nym-ktvz, nym-4v1p
- Replace only_lp_registration and test_lp_wg boolean params with TestMode
- Keep only_wireguard separate (controls ping behavior in Mixnet mode)
- Use TestMode helper methods for cleaner control flow:
- needs_mixnet() && !only_wireguard → run ping tests
- tests_wireguard() → run WG tests
- uses_lp() → use LP path instead of authenticator
- Convert legacy flags to TestMode at call sites for backward compatibility
Closes: nym-dd70
- Extract TestMode enum to mode/mod.rs for cleaner organization
- Add common/wireguard.rs with shared WireGuard tunnel testing
- Deduplicate netstack code from wg_probe() and wg_probe_lp()
- Net reduction of 174 lines in lib.rs
The mode_to_flags() function was discarding the original only_wireguard
flag. Now we preserve args.only_wireguard since it's orthogonal to the
test mode (it means "skip ping tests" in mixnet mode).
- Store LpStateMachine in session_states (not LpSession) for subsession handling
- Add LpStateMachine::from_subsession() factory for promoted sessions
- Rewrite handle_transport_packet() to use state machine for all messages
- Add handle_subsession_complete() for session promotion flow
- Add collision check for new_receiver_index before insert (nym-90rw)
- Add demoted_session_ttl_secs config (default 60s) for ReadOnlyTransport
sessions to be cleaned up quickly after subsession promotion (nym-atza)
- Track demoted session cleanup separately with lp_states_cleanup_demoted_removed
Restructure LP packet format so cleartext fields (receiver_idx, counter)
are always first, enabling trivial header parsing for routing before
session lookup. Protocol version and reserved fields are now encrypted
in the inner payload for encrypted packets.
Wire format change:
- Before: proto(1B) + reserved(3B) + receiver_idx(4B) + counter(8B)
- After: receiver_idx(4B) + counter(8B) | proto(1B) + reserved(3B) + ...
Key changes:
- Add OuterHeader struct (12 bytes) for routing/replay protection
- Update serialize_lp_packet/parse_lp_packet for unified format
- parse_lp_header_only now returns OuterHeader
- Gateway handler uses OuterHeader for session lookup
- Update DESIGN.md with new wire format diagrams
Security improvement: Only receiver_idx and counter visible after PSK
establishment (was also exposing protocol version and reserved).
- Validate Ack response in NestedLpSession and LP client final handshake
- Replace manual Drop with derive(Zeroize, ZeroizeOnDrop) for OuterAeadKey
- Add replay counter check before AEAD decryption to prevent DoS
[nym-qm2q, nym-z82d, nym-9ik3, nym-62fs]
- Create TimestampedState<T> wrapper with created_at and last_activity tracking
- Add TTL config fields to LpConfig:
* handshake_ttl_secs (default: 90s)
* session_ttl_secs (default: 24h)
* state_cleanup_interval_secs (default: 5min)
- Update LpHandlerState maps to use TimestampedState wrappers
- Update all handler.rs access points to wrap/unwrap states
- Implement background cleanup task in LpListener:
* Spawns on startup, stops on shutdown
* Removes handshakes older than handshake_ttl
* Removes sessions with no activity > session_ttl
* Tracks metrics: lp_states_cleanup_handshake_removed, lp_states_cleanup_session_removed
- Touch last_activity on every packet in handle_transport_packet()
- All 13 tests passing
Prevents memory leaks from abandoned handshakes and expired sessions
in long-running gateways. Configurable TTLs for different use cases.
- Extend handle_transport_packet() to conditionally deserialize both
LpRegistrationRequest and ForwardPacketData messages
- Add handle_registration_request() helper for registration flow
- Add handle_forwarding_request() helper for telescoping flow
- Delete dead handle_forwarding_loop() method (persistent connection model)
- Clean up unused imports and dead code warnings
- All 13 tests passing
Entry gateway now fully supports single-packet-per-connection architecture
for both direct client registration and packet forwarding to exit gateways.
Each ForwardPacket arrives on a new connection, gets processed, response sent,
and connection closes - consistent with exit gateway behavior from nym-21th.
Gateway responder's StartHandshake doesn't return a packet - it just transitions
to KKTExchange state and waits for client's KKT request. Fixed handle_client_hello()
to not expect a response packet.
## Changes
### Add BOOTSTRAP_SESSION_ID constant
- `common/nym-lp/src/packet.rs`: Add `pub const BOOTSTRAP_SESSION_ID: u32 = 0`
- Document that session_id=0 is only used for ClientHello bootstrap
- Export from lib.rs for public use
### Fix handle_client_hello() logic
- `gateway/src/node/lp_listener/handler.rs:201-225`: - Remove expectation of SendPacket action from StartHandshake
- Responder transitions to KKTExchange without sending
- Store state machine and close connection
- Client sends KKT request on next connection with computed session_id
### Use constant throughout codebase
- `gateway/src/node/lp_listener/handler.rs:115`: Use BOOTSTRAP_SESSION_ID in routing
- `nym-registration-client/src/lp_client/client.rs:259`: Use constant instead of literal 0
## Protocol Flow (Fixed)
```
Connection 1: Client sends ClientHello (session_id=0)
→ Gateway stores state, closes (no response)
Connection 2: Client sends KKT request (session_id=X)
→ Gateway finds state, processes, responds
Connection 3+: Handshake continues until complete...
```
## Testing
- All 13 unit tests pass
- Real test: docker/localnet + nym-gateway-probe (next step)
Fixes: nym-v9un
Unblocks: nym-21th
Major architectural change: decouple handshake state from TCP connection
to enable packet-per-connection forwarding and future UDP support.
## Changes
### Gateway Handler (handler.rs)
- Refactor `handle()` from persistent-connection loop to single-packet processing
- Add `handle_client_hello()`: Process ClientHello (session_id=0), create LpStateMachine, store in handshake_states map
- Add `handle_handshake_packet()`: Process handshake packets incrementally, transition to session_states when complete
- Add `handle_transport_packet()`: Handle registration/forward requests via established sessions
### State Management
- Use handshake_states (DashMap<u32, LpStateMachine>) for in-progress handshakes
- Use session_states (DashMap<u32, LpSession>) for established sessions
- Session ID computed deterministically from X25519 keys immediately after ClientHello
- State persists across connection closes
### Packet Flow
```
packet arrives → parse session_id from header
if session_id == 0: handle_client_hello (create state)
elif in handshake_states: handle_handshake_packet (advance state)
elif in session_states: handle_transport_packet (decrypt, process)
else: error (unknown session)
send response → close connection
```
## Testing
- All 13 existing unit tests pass
- Tests verify low-level packet I/O, timestamp validation, etc.
- Integration testing via nym-gateway-probe (next step)
## Backwards Compatibility
- Old handshake.rs methods kept but unused (may remove later)
- Metrics unchanged
- Protocol wire format unchanged
## Next Steps
- Test with docker/localnet topology
- Verify telescoping works (nym-gateway-probe)
- Apply same architecture to entry gateway (nym-31hl)
Fixes: nym-21th
Blocks: nym-31hl
Add in-memory state storage for handshake and session state to enable
stateless transport layer. This is the foundation for packet-per-connection
forwarding and future UDP support.
Changes:
- Add handshake_states: Arc<DashMap<[u8; 32], LpStateMachine>>
Keyed by client Ed25519 public key for in-progress handshakes
- Add session_states: Arc<DashMap<u32, LpSession>>
Keyed by session_id for established sessions
- Initialize both maps in production code (node/mod.rs)
- Initialize both maps in test code (handler.rs)
No logic changes yet - pure infrastructure addition.
All existing tests pass.
Fixes: nym-qtji
* Add nymkkt with KKT convenience wrappers for nym-lp integration
Integrates nymkkt module from georgio/noise-psq branch to enable
post-quantum key distribution for nym-lp.
Changes:
- Add common/nymkkt from georgio/noise-psq (KKT protocol implementation)
- Add convenience wrapper layer (kkt.rs) with simplified API:
- request_kem_key() - Client requests gateway's KEM key
- validate_kem_response() - Client validates signed response
- handle_kem_request() - Gateway handles requests
- Add nymkkt to workspace members in root Cargo.toml
- Export kkt module in lib.rs
The KKT (Key Encapsulation Mechanism Transport) protocol enables efficient
distribution of post-quantum KEM public keys. Instead of storing large PQ
keys in the directory (1KB-500KB), we store 32-byte hashes and fetch actual
keys on-demand via this authenticated protocol.
Tests: All 5 unit tests passing (authenticated, anonymous, signature
verification, hash validation)
* feat(lp): add Ed25519 authentication to PSQ protocol
Replace basic PSQ v0 API with authenticated v1 API that includes
cryptographic authentication via Ed25519 signatures.
Changes:
- PSQ initiator now signs encapsulated keys with Ed25519 private key
- PSQ responder verifies Ed25519 signatures before deriving PSK
- Prevents MITM attacks through mutual authentication
- Fixed test helpers to use role-based Ed25519 keypair assignment
(initiator uses [1u8;32], responder uses [2u8;32])
Security: This adds a critical authentication layer to the post-quantum
PSK derivation protocol, ensuring both parties can verify each other's
identity during the handshake.
Tests: All 77 tests passing (was 11 failures, now 0)
* feat(lp): integrate PSQ post-quantum PSK derivation
Complete integration of Post-Quantum Secure (PSQ) protocol for PSK
derivation in the Lewes Protocol, replacing simple Blake3 derivation
with cryptographically secure DHKEM-based PSK establishment.
This commit encompasses three completed tasks:
- Add KKTRequest/KKTResponse message types to LpMessage enum
- Update codec to handle KKT message serialization/deserialization
- Add kkt_orchestrator.rs with high-level KKT API wrappers
- Enable key exchange orchestration for PSQ protocol
- Add set_psk() method to NoiseProtocol for dynamic PSK injection
- Integrate PSQ derivation into LpSession handshake flow
- PSQ payload embedded in first Noise message (ClientHello)
- Derive PSK using libcrux-psq before Noise handshake completion
- Add helper functions for X25519 to KEM conversions
- Add comprehensive PSQ integration tests in session_integration/
- Test PSQ handshake end-to-end flow
- Validate PSK derivation correctness between initiator/responder
- Test PSQ + Noise combined protocol operation
Dependencies:
- libcrux-psq: Post-quantum PSK protocol implementation
- libcrux-kem: Key Encapsulation Mechanism primitives
- nym-kkt: KKT key exchange protocol wrappers
- rand 0.9: Required for KKT compatibility
Security: This adds Harvest-Now-Decrypt-Later (HNDL) resistance by
combining classical ECDH with post-quantum KEM for PSK derivation.
Even if X25519 is broken by quantum computers, the PSK remains secure.
Tests: All 77 tests passing
* feat(lp): add PSQ error handling documentation and tests (nym-bbi)
Formalize the "always abort" error handling strategy for PSQ failures.
PSQ errors indicate attacks, misconfigurations, or protocol violations
that should not be silently ignored or worked around.
Changes:
- Add comprehensive error handling documentation to psk.rs module
- Add diagnostic logging with error categorization:
* CredError → warn about potential attack
* TimestampElapsed → warn about potential replay
* Other errors → log as errors
- Add 4 error scenario tests:
* test_psq_deserialization_failure
* test_handshake_abort_on_psq_failure
* test_psq_invalid_signature
* test_psq_state_unchanged_on_error
- Add log dependency to Cargo.toml
Error handling strategy: All PSQ failures abort the handshake cleanly
with no retry or fallback. This prevents silent security degradation
and ensures misconfigurations are detected early.
State guarantees: PSQ errors leave session in clean state - dummy PSK
remains, Noise HandshakeState unchanged, no partial data, no cleanup needed.
Tests: 81 tests passing (77 original + 4 new error tests)
Closes: nym-bbi
* feat(lp): add PSK injection tracking to prevent dummy PSK usage (nym-ep2)
Add safety mechanism to ensure real post-quantum PSK was injected before
allowing transport mode operations (encrypt/decrypt). This prevents
accidentally using the insecure dummy PSK [0u8; 32] if PSQ injection fails.
Changes:
- Add `psk_injected: AtomicBool` field to LpSession
- Initialize to `false` in LpSession::new()
- Set to `true` after successful PSK injection:
* Initiator: In prepare_handshake_message() after set_psk()
* Responder: In process_handshake_message() after set_psk()
- Add NoiseError::PskNotInjected error variant
- Add PSK injection checks in encrypt_data() and decrypt_data()
* Check happens before handshake completion check
* Returns PskNotInjected if flag is false
- Add comprehensive PSK injection lifecycle documentation to LpSession
- Add test_transport_fails_without_psk_injection test
- Update test_encrypt_decrypt_before_handshake to expect PskNotInjected
PSK Injection Lifecycle:
1. Session created with dummy PSK [0u8; 32] in Noise HandshakeState
2. During handshake, PSQ runs and derives real post-quantum PSK
3. Real PSK injected via set_psk() - psk_injected flag set to true
4. Handshake completes, transport mode available
5. Transport operations check psk_injected flag for safety
This is defensive programming - normal PSQ flow always injects the real PSK.
The safety check prevents transport mode if PSQ somehow fails silently or is
bypassed due to implementation bugs.
Tests: 82 tests passing (81 original + 1 new)
Closes: nym-ep2
* docs(lp): fix PSK state documentation inaccuracy
Correct error handling documentation to clarify that PSK slot 3
remains unmodified only on error, not in all cases.
Previous: "PSK slot 3 = dummy [0u8; 32] (never modified)"
Corrected: "PSK slot 3 = dummy [0u8; 32] (not modified on error)"
This is more accurate since:
- On error: PSK remains as dummy value (never injected)
- On success: PSK is replaced with real post-quantum PSK
Documentation-only change, no functional impact.
* feat(lp): add KKTExchange state to state machine for pre-handshake KEM key transfer (nym-4za)
Add KKTExchange state to LpStateMachine to properly orchestrate KKT (KEM Key Transfer)
protocol before Noise handshake begins. This enables dynamic KEM public key exchange,
allowing post-quantum KEM algorithms to be used without pre-published keys.
Changes:
- Add KKTExchange state and KKTComplete action to state machine
- Implement automatic KKT exchange on StartHandshake:
* Initiator: sends KKT request → waits for response → validates signature
* Responder: waits for request → validates → sends signed KEM key
- Update process_kkt_response() to accept Option<&[u8]> for hash validation:
* Some(hash): full KKT validation with directory hash (future)
* None: signature-only mode (current deployment)
- Add local_x25519_public() helper for responder KEM key derivation
- Update state flow: ReadyToHandshake → KKTExchange → Handshaking → Transport
- Add PSK handle storage (psk_handle) for future re-registration
- Export generate_fresh_salt() for session creation
- Update psq_responder_process_message() to return encrypted PSK handle (ctxt_B)
- Add comprehensive tests:
* test_kkt_exchange_initiator_flow
* test_kkt_exchange_responder_flow
* test_kkt_exchange_full_roundtrip
* test_kkt_exchange_close
* test_kkt_exchange_rejects_invalid_inputs
* Updated test_state_machine_simplified_flow for KKT phase
All tests passing. Ready for nym-8y5 (PSQ handshake KKT integration).
* docs(lp): add state machine and post-quantum security protocol documentation
Add comprehensive documentation of the Lewes Protocol state machine and
post-quantum security architecture to LP_PROTOCOL.md.
New sections:
- State Machine and Security Protocol overview
- Detailed state transition diagram (ReadyToHandshake → KKTExchange → Handshaking → Transport)
- Complete message sequence diagram showing KKT + PSQ + Noise flow
- KKT (KEM Key Transfer) protocol specification
- PSQ (Post-Quantum Secure PSK) protocol details
- Security guarantees and implementation status
- Algorithm choices (current X25519, future ML-KEM-768)
- Message type specifications for KKT
- Version 1.1 changelog entry documenting KKT/PSQ integration
Documentation includes:
- ASCII art state machine diagram
- Message sequence diagram with all protocol phases
- PSK derivation formulas
- Security properties checklist
- Migration path to post-quantum KEMs
- Integration details (PSQ embedded in Noise, no extra round-trips)
Related to nym-4za (KKTExchange state implementation).
* feat(lp): use KKT-authenticated KEM key in PSQ handshake (nym-8y5)
Replace direct X25519→KEM conversion with KKT-derived authenticated key
in PSQ initiator flow. This ensures PSQ uses the responder's authenticated
KEM public key obtained via KKT protocol instead of blindly converting
their X25519 key, properly completing the post-quantum security chain.
Changes:
- session.rs: Extract KEM key from KKTState::Completed in prepare_handshake_message()
- session.rs: Add set_kkt_completed_for_test() helper for test initialization
- session.rs: Update create_handshake_test_session() to initialize KKT state
- session.rs: Fix test_handshake_abort_on_psq_failure and test_psq_invalid_signature
- session_manager.rs: Add init_kkt_for_test() for integration test setup
- session_integration/mod.rs: Update tests for KKT-first flow (6 rounds total)
- session_integration/mod.rs: Fix state machine test expectations for KKTExchange state
All 87 tests passing. Unblocks nym-w8f (KKT tests) and nym-m15 (production integration).
* feat(lp): simplify API to Ed25519-only, derive X25519 internally
Refactored LP state machine to use Ed25519 keys exclusively in the public
API, with X25519 keys derived internally via RFC 7748. This simplifies the
API from 6 parameters to 4 while maintaining protocol security.
**Core API Changes:**
- LpStateMachine::new(): Removed explicit X25519 keypair parameters
- Old: new(is_initiator, local_keypair, local_ed25519_keypair,
remote_public_key, remote_ed25519_key, salt)
- New: new(is_initiator, local_ed25519_keypair, remote_ed25519_key, salt)
- X25519 keys now derived internally from Ed25519 using RFC 7748
- lp_id calculation moved inside state machine (uses derived X25519 keys)
**Protocol Changes:**
- ClientHello message extended from 65 to 97 bytes
- Now includes client_ed25519_public_key field (32 bytes)
- Required for PSQ authentication in KKT + PSQ handshake flow
- Breaking change: gateway must extract Ed25519 from ClientHello
**Gateway Updates:**
- receive_client_hello() now extracts Ed25519 public key
- LpGatewayHandshake::new_responder() accepts Ed25519 keys only
- Removed manual X25519 conversion (handled by state machine)
**Registration Client Updates:**
- LpRegistrationClient now uses Ed25519 keypairs
- Generate fresh ephemeral Ed25519 keys for LP registration
- ClientHello includes Ed25519 public key for gateway authentication
- Fixed 7 pre-existing build errors:
* mixnet_client_startup_timeout field removal
* IprClientConnect API change (async → sync)
* Error variant renames (use helper function)
* LP client key type mismatches (X25519 → Ed25519)
**Test Suite:**
- Updated 16+ test functions to use new 4-parameter constructor
- Fixed 5 integration test failures caused by lp_id mismatch
- Tests now derive X25519 from Ed25519 (matching production behavior)
- Added missing PublicKey imports in test modules
- All 87 tests passing (100% success rate)
**Implementation Details:**
- Added Ed25519RecoveryError variant to LpError enum
- Type conversion: nym_crypto X25519 → nym_lp keypair types
- Maintained backward compatibility for PSQ/KKT protocol flow
- Session manager updated to use new API signature
This change completes the Ed25519-only API migration, hiding X25519 as an
implementation detail while preserving all security properties of the
KKT-authenticated PSQ handshake protocol.
* chore: run cargo fmt
* chore: run cargo clippy --fix to resolve simple linter issues
* Basic handshake working
* Final tweaks
* Wrap PR comments, 2024
---------
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Implement LP (Lewes Protocol) registration flow testing in nym-gateway-probe
to validate gateway LP registration capabilities alongside existing WireGuard
and mixnet tests.
Changes:
- Add LpProbeResults struct to track LP registration test results
(can_connect, can_handshake, can_register, error)
- Add lp_registration_probe() function that tests full registration flow:
* TCP connection to LP listener (port 41264)
* Noise protocol handshake with PSK derivation
* Registration request with bandwidth credentials
* Registration response validation
- Integrate LP test into main probe flow - runs automatically if gateway
has LP address (derived from gateway IP + port 41264)
- Export LpRegistrationClient from nym-registration-client for probe use
- Add LP address field to TestedNodeDetails
The probe tests only successful registration without additional traffic,
keeping the implementation simple and focused.
* placeholder handling of wg registration with upgrade mode token
* include upgrade mode credentials as part of credential storage
* introduce helper for decoding JWT payload
* expose methods for removing emergency credentials from the storage
* don't allow duplicate emergency credentials with the same content
* added authenticator ClientMessage for upgrade mode check
* retrieve credentials with longest expiration first
* post rebasing fixes
* fixed gateway config
* feat: allow specifying minimum node performance for client init
* nym-node UM improvements
* fixed upgrade mode bandwidth on initial authentication
* fix: logs and thresholds
* expose attestation information from nym-node http api
* additional logs
* post rebasing fixes
* make @simonwicky happy by removing empty lines in emergency_credential table definition
* chore: remove '_' prefix for internal counters within in-mem ecash storage
* improved import of 'UpgradeModeState' within the nym-node
* use explicit time dependency within credential-storage
* re-order imports within the gateway-client
* moved 'AvailableBandwidth' definition to the monorepo
* squashing feat: merge intermediate upgrade mode changes #6174 to more easily resolve merge conflicts during rebasing
added additional v2 query for metadata endpoint for requesting upgrade mode recheck
added additional message to v6 authenticator to request explicit upgrade mode recheck
clippy
test fixes due to updated keys
updated assertion for upgrading v1 top up request to v2
compare attester public key against the expected value within the credential proxy
use pre-generated attestation public keys within nym-nodes
remove version deprecation
bugfix: default bandwidth response for authenticator
expose upgrade mode information in authenticator responses
adding tests for new v2 server
passing upgrade mode information in metadata endpoint
v2 wireguard private metadata
bugfix: make sure to immediately poll for attestation after spawning task
fix gateway probe and remove code duplication for finalizing registration
squashing before rebasing
post rebasing fixes
AuthenticatorVersion helpers
additional nits
allow unwraps in mocks
fixed linux build
clippy
integrating upgrade mode into authenticator
fixed build after adding wrappers to response types
conditionally updating peer handle bandwidth
cleanup
negotiate initial protocol during registration
change auth to use highest protocol
handler for JWT message
dont meter client bandwidth in upgrade mode
handling recheck requests
sending information about upgrade_mode on client messages
gateway watching for upgrade mode attestation
wip: gateways to disable bandwidth metering on upgrade mode
* fixed ServerResponse deserialisation
* fixed incorrect swagger path for upgrade mode check endpoint
* moved upgrade mode endpoint out of bandwidth routes
* chore: remove unused error variant
* removed re-export of UpgradeModeAttestation from credentials-interface
* chore: define single source of truth for minimum bandwidth threshold value
* moved type definitions out of traits.rs
* updated v6 versioning to point to niolo release instead
* fixed incorrect error mapping