Compare commits

...

47 Commits

Author SHA1 Message Date
mfahampshire 8a520df064 Update Readme with c->c note, no mixnet-as-proxy yet 2026-02-09 14:23:45 +00:00
mfahampshire 6bb5f1bcae Tidy up integration test loops 2026-02-09 14:20:56 +00:00
mfahampshire e6b10c708c Removed unused dependency 2026-02-09 13:21:51 +00:00
mfahampshire 56fc135c2f Remove unnused code 2026-02-09 13:21:23 +00:00
mfahampshire 24ff4272b6 Increase bindgen timout for headless testing for CI 2026-02-09 12:58:37 +00:00
mfahampshire e6d5f463e7 Fix poll_read bug 2026-02-09 12:58:10 +00:00
mfahampshire cad47732b7 Debug logging 2026-02-09 12:57:24 +00:00
mfahampshire f4ff6717e0 Streamline readme 2026-02-09 12:57:10 +00:00
mfahampshire d8b8b38101 Adding more integration tests 2026-02-09 12:22:38 +00:00
mfahampshire 8e6ceddc66 Port native reference implementation to WASM compatible state 2026-02-09 12:13:27 +00:00
mfahampshire d029a58e13 Add stream to wasm/client for libp2p 2026-02-09 12:12:47 +00:00
mfahampshire 21f8cb89d6 Streamline shutdown flush 2026-02-02 20:42:42 +00:00
mfahampshire 4ac9cdb1b1 Clippy 2026-02-02 15:24:12 +00:00
mfahampshire 296f243433 Remove serde and bincode from ReconstructedMessage - simplify
encoding/decoding
2026-02-02 15:13:06 +00:00
mfahampshire 10b6ad050b Remove unnecessary clippy flag 2026-02-02 14:28:53 +00:00
mfahampshire 5441960976 Change to use split_to on bytesmut 2026-02-02 13:14:46 +00:00
mfahampshire e6e25dacea Make send_self_pings() concurrent again 2026-02-02 12:26:41 +00:00
mfahampshire 77ab256588 Added stream mode guard + errors + example file 2026-02-02 11:42:44 +00:00
mfahampshire fa2cbb5d21 Move bincode out of nym-sphinx 2026-02-02 10:31:41 +00:00
mfahampshire c3e4f944d5 Fix mut clippy error for gateway probe 2026-02-02 09:55:38 +00:00
mfahampshire 933bbbb67d Wrapper for API stability with LP client 2026-02-02 09:55:10 +00:00
mfahampshire 3f300cc2c1 remove unused errs and imports related to IPR stream module 2026-02-02 09:32:02 +00:00
mfahampshire 30da87bf41 Fix network-requester Cargo.toml after rebase 2026-01-30 16:02:07 +00:00
mfahampshire e32783bced Remove serde requirement for MixPacket 2026-01-30 16:01:10 +00:00
mfahampshire 49687270b1 Fix future resolution problem 2026-01-30 16:00:16 +00:00
mfahampshire a49957cb5c Update Cargo.lock 2026-01-30 15:59:24 +00:00
mfahampshire 8b8c583ac5 Suppress clippy deprecated warnings for generic-array 2026-01-30 15:59:24 +00:00
mfahampshire 9d808d30c2 Update FFI shared lib for mutable sender 2026-01-30 15:59:24 +00:00
mfahampshire 880a33fb20 Update WASM client for mutable sender access 2026-01-30 15:59:24 +00:00
mfahampshire 932bd0660f Update SDK modules for &mut self and clippy fixes 2026-01-30 15:59:24 +00:00
mfahampshire 80f3ddca89 Update clients and tools for &mut self send signature 2026-01-30 15:59:24 +00:00
mfahampshire c7b0eed15f Update service providers for &mut self send signature 2026-01-30 15:55:53 +00:00
mfahampshire 9224f01d49 Implement AsyncRead/AsyncWrite/Sink for MixnetClient 2026-01-30 15:53:53 +00:00
mfahampshire 878fe85d66 Add serde, codecs, and PollSender to client-core 2026-01-30 15:53:37 +00:00
mfahampshire f060d63f2e Add serde derives and codecs for nymsphinx message types 2026-01-30 15:53:36 +00:00
Jędrzej Stuczyński 8916b021a9 lp: attempt to negotiate (and use) protocol version (#6399) 2026-01-30 12:38:32 +00:00
Jędrzej Stuczyński dccdde108c Lp/bugfix/share ip allocation (#6395)
* feat: use shared PeerManager between Authenticator and LpHandlerState

* feat: share IpPool

* clippy and test fixes

* PR suggestions
2026-01-30 11:38:17 +00:00
Jędrzej Stuczyński 9d661e7a7b bugfix: use correct reserved bytes when parsing LpHeader (#6398) 2026-01-30 09:39:47 +00:00
Jędrzej Stuczyński 76ce1bc0f9 feat: use hex-encoding for lp key digests (#6394)
* feat: use hex-encoding for lp key digests

* removed needless borrow in test code

* gateway probe fixes
2026-01-30 08:44:29 +00:00
dynco-nym d3648f13c5 NS API socks5 support (#6361)
* Add conversion from gw_probe crate type

* Move code around
- split 1000+ LoC files into smaller ones

* Add socks5 field
- code improvements in gw_probe crate

* Fix docker build
- install go
- required as build dependency of gw probe

* Add logs to agent

* NS API: configure DB via env

* rebase fix

* socks5 score calc

* Cargo fmt

* use existing div_ceil

* Code improvements

* Bump NS API version

* Rename variables

* Bump API & agent version

* Try to fix CI

* Build only on linux
2026-01-29 20:54:21 +01:00
dynco-nym 9a931b9251 Add socks5 test to gateway-probe (#6393)
* Socks5 in GW probe

Bump NS agent version

Fix bugs
- force route construction
- use same entry = exit

Fix NS API version check workflow

PR feedback

More robust test attempts

CLI arg validation

Fix clippy

PR feedback

* Test provided endpoints in config at startup

Require one valid endpoint

* Bump agent to 1.1.0
2026-01-29 18:20:51 +01:00
Jack Wampler f4ba8ac2b3 add extra configured nym api url to env (#6382) 2026-01-29 07:09:02 -07:00
Andrej Mihajlov c274cc588d Merge pull request #6390 from nymtech/am/reduce-http-error-size
Reduce the size of `HttpClientError`
2026-01-29 14:59:38 +01:00
Jędrzej Stuczyński 7dd1dd1a6c Lp/two step dvpn reg (#6386)
* squashing  Lp/two step dvpn reg #6386

fixed integration tests by extending the mocks

remove dead code

compiling client-side code

gateway side handling of updated lp-wg reg

wip: countless changes on the gateway handler side

splitting up NestedLpSession

* fixed lp-messages tests

* gateway probe fixes

* unused variable

* resolved nits
2026-01-29 13:38:21 +00:00
import this 982786b678 [NTM]: NIP-7 port update & [DOCs/operators]: Release notes for v2026.2 oscypek (#6384)
* add operators notes

* add dev notes

* bump up version

* open NIP-7 ports

* bump up stats

* fix incorrect dash
2026-01-29 13:21:28 +00:00
Simon Wicky 561182ce6b shuffling files around in the probe, before improving it (#6391) 2026-01-29 10:34:57 +01:00
Andrej Mihajlov f4b59158df Box reqwest::Url to keep HttpClientError below 128 byte size which triggers clippy 2026-01-29 07:41:43 +01:00
220 changed files with 12010 additions and 4366 deletions
+9 -1
View File
@@ -89,7 +89,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
- name: Clippy (non-macos)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
@@ -104,6 +104,14 @@ jobs:
with:
command: build
# only build on linux because of wg FFI bindings of its dependency (network probe)
- name: Build nym-node-status-api (linux only)
if: runner.os == 'Linux'
uses: actions-rs/cargo@v1
with:
command: build
args: -p nym-node-status-api
- name: Build all examples
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
on:
pull_request:
paths:
- "nym-node-status-api/**"
- "nym-node-status-api/nym-node-status-api/**"
env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
Generated
+543 -6
View File
@@ -536,6 +536,19 @@ dependencies = [
"tungstenite 0.21.0",
]
[[package]]
name = "asynchronous-codec"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
dependencies = [
"bytes",
"futures-sink",
"futures-util",
"memchr",
"pin-project-lite",
]
[[package]]
name = "atoi"
version = "2.0.0"
@@ -703,12 +716,28 @@ dependencies = [
"url",
]
[[package]]
name = "base-x"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base256emoji"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c"
dependencies = [
"const-str 0.4.3",
"match-lookup",
]
[[package]]
name = "base64"
version = "0.13.1"
@@ -1391,6 +1420,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-str"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3"
[[package]]
name = "const-str"
version = "0.5.7"
@@ -1459,6 +1494,15 @@ dependencies = [
"rand 0.9.2",
]
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cosmos-sdk-proto"
version = "0.27.0"
@@ -2134,6 +2178,26 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "data-encoding-macro"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d"
dependencies = [
"data-encoding",
"data-encoding-macro-internal",
]
[[package]]
name = "data-encoding-macro-internal"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [
"data-encoding",
"syn 2.0.114",
]
[[package]]
name = "defguard_boringtun"
version = "0.6.3"
@@ -2905,6 +2969,16 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-bounded"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e"
dependencies = [
"futures-timer",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -2930,6 +3004,7 @@ dependencies = [
"futures-core",
"futures-task",
"futures-util",
"num_cpus",
]
[[package]]
@@ -2987,6 +3062,27 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-ticker"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e"
dependencies = [
"futures",
"futures-timer",
"instant",
]
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
dependencies = [
"gloo-timers 0.2.6",
"send_wrapper 0.4.0",
]
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -3096,6 +3192,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
@@ -3383,6 +3491,12 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hex_fmt"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f"
[[package]]
name = "hickory-proto"
version = "0.25.2"
@@ -4108,6 +4222,7 @@ name = "integration-tests"
version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"nym-bandwidth-controller",
"nym-credential-verification",
"nym-credentials-interface",
@@ -4678,6 +4793,215 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libp2p"
version = "0.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4"
dependencies = [
"bytes",
"either",
"futures",
"futures-timer",
"getrandom 0.2.16",
"libp2p-allow-block-list",
"libp2p-connection-limits",
"libp2p-core",
"libp2p-gossipsub",
"libp2p-identify",
"libp2p-identity",
"libp2p-metrics",
"libp2p-ping",
"libp2p-swarm",
"multiaddr",
"pin-project",
"rw-stream-sink",
"thiserror 1.0.69",
]
[[package]]
name = "libp2p-allow-block-list"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1027ccf8d70320ed77e984f273bc8ce952f623762cb9bf2d126df73caef8041"
dependencies = [
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"void",
]
[[package]]
name = "libp2p-connection-limits"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d003540ee8baef0d254f7b6bfd79bac3ddf774662ca0abf69186d517ef82ad8"
dependencies = [
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"void",
]
[[package]]
name = "libp2p-core"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a61f26c83ed111104cd820fe9bc3aaabbac5f1652a1d213ed6e900b7918a1298"
dependencies = [
"either",
"fnv",
"futures",
"futures-timer",
"libp2p-identity",
"multiaddr",
"multihash",
"multistream-select",
"once_cell",
"parking_lot",
"pin-project",
"quick-protobuf",
"rand 0.8.5",
"rw-stream-sink",
"smallvec",
"thiserror 1.0.69",
"tracing",
"unsigned-varint 0.8.0",
"void",
"web-time",
]
[[package]]
name = "libp2p-gossipsub"
version = "0.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4e830fdf24ac8c444c12415903174d506e1e077fbe3875c404a78c5935a8543"
dependencies = [
"asynchronous-codec",
"base64 0.22.1",
"byteorder",
"bytes",
"either",
"fnv",
"futures",
"futures-ticker",
"getrandom 0.2.16",
"hex_fmt",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"prometheus-client",
"quick-protobuf",
"quick-protobuf-codec",
"rand 0.8.5",
"regex",
"sha2 0.10.9",
"smallvec",
"tracing",
"void",
"web-time",
]
[[package]]
name = "libp2p-identify"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1711b004a273be4f30202778856368683bd9a83c4c7dcc8f848847606831a4e3"
dependencies = [
"asynchronous-codec",
"either",
"futures",
"futures-bounded",
"futures-timer",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"lru",
"quick-protobuf",
"quick-protobuf-codec",
"smallvec",
"thiserror 1.0.69",
"tracing",
"void",
]
[[package]]
name = "libp2p-identity"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3"
dependencies = [
"bs58",
"ed25519-dalek",
"hkdf",
"multihash",
"quick-protobuf",
"rand 0.8.5",
"sha2 0.10.9",
"thiserror 2.0.17",
"tracing",
"zeroize",
]
[[package]]
name = "libp2p-metrics"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ebafa94a717c8442d8db8d3ae5d1c6a15e30f2d347e0cd31d057ca72e42566"
dependencies = [
"futures",
"libp2p-core",
"libp2p-identify",
"libp2p-identity",
"libp2p-ping",
"libp2p-swarm",
"pin-project",
"prometheus-client",
"web-time",
]
[[package]]
name = "libp2p-ping"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "005a34420359223b974ee344457095f027e51346e992d1e0dcd35173f4cdd422"
dependencies = [
"either",
"futures",
"futures-timer",
"libp2p-core",
"libp2p-identity",
"libp2p-swarm",
"rand 0.8.5",
"tracing",
"void",
"web-time",
]
[[package]]
name = "libp2p-swarm"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dd6741793d2c1fb2088f67f82cf07261f25272ebe3c0b0c311e0c6b50e851a"
dependencies = [
"either",
"fnv",
"futures",
"futures-timer",
"getrandom 0.2.16",
"libp2p-core",
"libp2p-identity",
"lru",
"multistream-select",
"once_cell",
"rand 0.8.5",
"smallvec",
"tracing",
"void",
"wasm-bindgen-futures",
"web-time",
]
[[package]]
name = "libredox"
version = "0.1.12"
@@ -4757,6 +5081,15 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.5",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -4834,6 +5167,17 @@ dependencies = [
"web_atoms",
]
[[package]]
name = "match-lookup"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "match_token"
version = "0.35.0"
@@ -5034,6 +5378,60 @@ dependencies = [
"version_check",
]
[[package]]
name = "multiaddr"
version = "0.18.2"
source = "git+https://github.com/mfahampshire/rust-multiaddr?branch=nym-protocol#b9966d6eedd09236ee76f157f985d04e02f8b880"
dependencies = [
"arrayref",
"byteorder",
"data-encoding",
"libp2p-identity",
"multibase",
"multihash",
"percent-encoding",
"serde",
"static_assertions",
"unsigned-varint 0.8.0",
"url",
]
[[package]]
name = "multibase"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"
dependencies = [
"base-x",
"base256emoji",
"data-encoding",
"data-encoding-macro",
]
[[package]]
name = "multihash"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d"
dependencies = [
"core2",
"unsigned-varint 0.8.0",
]
[[package]]
name = "multistream-select"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19"
dependencies = [
"bytes",
"futures",
"log",
"pin-project",
"smallvec",
"unsigned-varint 0.7.2",
]
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
@@ -5521,7 +5919,7 @@ dependencies = [
"clap",
"clap_complete",
"clap_complete_fig",
"const-str",
"const-str 0.5.7",
"log",
"opentelemetry",
"opentelemetry-jaeger",
@@ -5686,12 +6084,13 @@ version = "1.20.1"
dependencies = [
"async-trait",
"base64 0.22.1",
"bincode",
"bs58",
"cfg-if",
"clap",
"comfy-table",
"futures",
"gloo-timers",
"gloo-timers 0.3.0",
"http-body-util",
"humantime",
"hyper 1.8.1",
@@ -5731,6 +6130,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tokio-util",
"tokio_with_wasm",
"tracing",
"tungstenite 0.20.1",
@@ -5799,7 +6199,7 @@ version = "1.4.1"
dependencies = [
"anyhow",
"futures",
"gloo-timers",
"gloo-timers 0.3.0",
"js-sys",
"nym-bin-common",
"nym-gateway-requests",
@@ -5813,6 +6213,7 @@ dependencies = [
"serde-wasm-bindgen 0.6.5",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tokio_with_wasm",
"tsify",
"wasm-bindgen",
@@ -6488,6 +6889,7 @@ dependencies = [
"nym-validator-client",
"pnet_packet",
"rand 0.8.5",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror 2.0.17",
@@ -6497,6 +6899,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"url",
"utoipa",
"vergen-gitcl",
"x25519-dalek",
]
@@ -6831,6 +7234,38 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "nym-libp2p-wasm"
version = "0.1.0"
dependencies = [
"futures",
"getrandom 0.2.16",
"gloo-timers 0.3.0",
"hex",
"js-sys",
"libp2p",
"libp2p-identify",
"libp2p-identity",
"libp2p-ping",
"libp2p-swarm",
"log",
"nym-client-wasm",
"nym-sphinx-addressing",
"nym-sphinx-anonymous-replies",
"nym-wasm-client-core",
"nym-wasm-utils",
"parking_lot",
"send_wrapper 0.6.0",
"serde",
"serde-wasm-bindgen 0.6.5",
"thiserror 2.0.17",
"tokio_with_wasm",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "nym-lp"
version = "0.1.0"
@@ -6906,6 +7341,7 @@ version = "0.1.0"
dependencies = [
"nym-test-utils",
"tokio",
"tracing",
]
[[package]]
@@ -7097,6 +7533,7 @@ dependencies = [
"time",
"tokio",
"tokio-tungstenite",
"tokio-util",
"url",
"zeroize",
]
@@ -7125,6 +7562,7 @@ dependencies = [
"csv",
"cupid",
"futures",
"hex",
"hkdf",
"human-repr",
"humantime-serde",
@@ -7232,7 +7670,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "1.0.7"
version = "1.1.1"
dependencies = [
"anyhow",
"clap",
@@ -7241,6 +7679,8 @@ dependencies = [
"nym-crypto",
"nym-node-status-client",
"rand 0.8.5",
"regex",
"serde_json",
"tempfile",
"tokio",
"tracing",
@@ -7249,7 +7689,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.12"
version = "4.0.14"
dependencies = [
"ammonia",
"anyhow",
@@ -7270,6 +7710,7 @@ dependencies = [
"nym-credentials",
"nym-crypto",
"nym-ecash-time",
"nym-gateway-probe",
"nym-http-api-client",
"nym-http-api-common",
"nym-mixnet-contract-common",
@@ -7525,6 +7966,7 @@ dependencies = [
"nym-test-utils",
"nym-wireguard-types",
"serde",
"tracing",
]
[[package]]
@@ -7705,6 +8147,7 @@ dependencies = [
"tap",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"url",
]
@@ -7742,6 +8185,7 @@ dependencies = [
name = "nym-sphinx"
version = "1.20.1"
dependencies = [
"log",
"nym-crypto",
"nym-metrics",
"nym-mixnet-contract-common",
@@ -7759,6 +8203,7 @@ dependencies = [
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_distr",
"serde",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -7808,6 +8253,7 @@ dependencies = [
"nym-topology",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"thiserror 2.0.17",
"tracing",
"wasm-bindgen",
@@ -7856,6 +8302,7 @@ dependencies = [
"nym-sphinx-anonymous-replies",
"nym-sphinx-params",
"nym-sphinx-types",
"serde",
"thiserror 2.0.17",
]
@@ -7996,6 +8443,7 @@ dependencies = [
"futures",
"log",
"nym-test-utils",
"serde",
"thiserror 2.0.17",
"tokio",
"tokio-util",
@@ -8366,12 +8814,12 @@ dependencies = [
"futures",
"ip_network",
"ipnetwork",
"nym-authenticator-requests",
"nym-credential-verification",
"nym-credentials-interface",
"nym-crypto",
"nym-gateway-requests",
"nym-gateway-storage",
"nym-ip-packet-requests",
"nym-metrics",
"nym-network-defaults",
"nym-node-metrics",
@@ -9363,6 +9811,29 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "prometheus-client"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca"
dependencies = [
"dtoa",
"itoa",
"parking_lot",
"prometheus-client-derive-encode",
]
[[package]]
name = "prometheus-client-derive-encode"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "prost"
version = "0.13.5"
@@ -9446,6 +9917,28 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-protobuf"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f"
dependencies = [
"byteorder",
]
[[package]]
name = "quick-protobuf-codec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474"
dependencies = [
"asynchronous-codec",
"bytes",
"quick-protobuf",
"thiserror 1.0.69",
"unsigned-varint 0.8.0",
]
[[package]]
name = "quinn"
version = "0.11.9"
@@ -10087,6 +10580,17 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "rw-stream-sink"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1"
dependencies = [
"futures",
"pin-project",
"static_assertions",
]
[[package]]
name = "ryu"
version = "1.0.22"
@@ -10289,6 +10793,21 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "send_wrapper"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
dependencies = [
"futures-core",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -12656,6 +13175,18 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unsigned-varint"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105"
[[package]]
name = "unsigned-varint"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -12879,6 +13410,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "vt100"
version = "0.16.2"
+6 -1
View File
@@ -172,6 +172,7 @@ members = [
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/libp2p-nym",
"wasm/zknym-lib",
"nym-gateway-probe",
"integration-tests", "common/nym-lp-transport", "common/nym-kkt-ciphersuite",
@@ -185,7 +186,6 @@ default-members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -544,6 +544,11 @@ web-sys = "0.3.76"
#[patch.crates-io]
#sphinx-packet = { path = "../sphinx" }
# Patch multiaddr with Protocol::Nym support for libp2p-nym transport
[patch.crates-io.multiaddr]
git = "https://github.com/mfahampshire/rust-multiaddr"
branch = "nym-protocol"
# Profile settings for individual crates
# Compile-time verified queries do quite a bit of work at compile time. Incremental
@@ -20,13 +20,12 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
pub type PrivateIPs = HashMap<IpPair, SystemTime>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
+5 -1
View File
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
impl BandwidthTicketProvider for MockBandwidthController {
async fn get_ecash_ticket(
&self,
_ticket_type: TicketType,
ticket_type: TicketType,
_gateway_id: PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
@@ -100,6 +100,10 @@ impl BandwidthTicketProvider for MockBandwidthController {
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
.expect("Failed to deserialize test credential - this is a bug in the test harness");
// change the ticket type to the requested ticket
// note that verification outside mocks is going to fail
credential.payment.t_type = ticket_type.to_repr() as u8;
// Update spend_date to today to pass validation
credential.spend_date = OffsetDateTime::now_utc().date();
+19
View File
@@ -57,3 +57,22 @@ where
Ok(Some(token))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box<T> {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
(**self)
.get_ecash_ticket(ticket_type, gateway_id, tickets_to_spend)
.await
}
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
}
+2
View File
@@ -14,6 +14,7 @@ documentation.workspace = true
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true }
base64 = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, optional = true }
@@ -29,6 +30,7 @@ sha2 = { workspace = true }
si-scale = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] }
tokio-util = { workspace = true, features = ["codec"] }
time = { workspace = true }
tokio = { workspace = true, features = ["sync", "macros"] }
tracing = { workspace = true }
@@ -36,6 +36,7 @@ use crate::init::{
types::{GatewaySetup, InitialisationResult},
};
use futures::channel::mpsc;
use futures::SinkExt;
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_config_types::{ForgetMe, RememberMe};
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
@@ -66,7 +67,7 @@ use std::os::raw::c_int as RawFd;
use std::path::Path;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::Sender;
use tokio_util::sync::{PollSendError, PollSender};
use url::Url;
#[cfg(target_arch = "wasm32")]
@@ -112,10 +113,7 @@ pub struct ClientInput {
}
impl ClientInput {
pub async fn send(
&self,
message: InputMessage,
) -> Result<(), tokio::sync::mpsc::error::SendError<InputMessage>> {
pub async fn send(&mut self, message: InputMessage) -> Result<(), PollSendError<InputMessage>> {
self.input_sender.send(message).await
}
}
@@ -745,7 +743,7 @@ where
config: &Config,
user_agent: Option<UserAgent>,
client_stats_id: String,
input_sender: Sender<InputMessage>,
input_sender: PollSender<InputMessage>,
shutdown_tracker: &ShutdownTracker,
) -> ClientStatsSender {
tracing::info!("Starting statistics control...");
@@ -1013,7 +1011,7 @@ where
&self.config,
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
tokio_util::sync::PollSender::new(input_sender.clone()),
&shutdown_tracker.clone(),
);
@@ -1139,7 +1137,7 @@ where
client_input: ClientInputStatus::AwaitingProducer {
client_input: ClientInput {
connection_command_sender: client_connection_tx,
input_sender,
input_sender: PollSender::new(input_sender),
client_request_sender,
},
},
@@ -26,7 +26,7 @@ use crate::{
error::ClientCoreError,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_client_core_gateways_storage as gateways_storage;
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
@@ -1,22 +1,36 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ClientCoreError;
use crate::make_bincode_serializer;
use bincode::Options;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::connections::TransmissionLane;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tokio_util::{
bytes::Buf,
bytes::BytesMut,
codec::{Decoder, Encoder},
};
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
pub type InputMessageSender = tokio_util::sync::PollSender<InputMessage>;
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
#[derive(Debug)]
const LENGHT_ENCODING_PREFIX_SIZE: usize = 4;
#[derive(Serialize, Deserialize, Debug)]
pub enum InputMessage {
/// Fire an already prepared mix packets into the network.
/// No guarantees are made about it. For example no retransmssion
/// will be attempted if it gets dropped.
///
/// Packets are stored as pre-serialized bytes, which avoids
/// requiring Serde on MixPacket.
Premade {
msgs: Vec<MixPacket>,
packet_bytes: Vec<Vec<u8>>,
lane: TransmissionLane,
},
@@ -65,12 +79,16 @@ pub enum InputMessage {
}
impl InputMessage {
pub fn simple(data: &[u8], recipient: Recipient) -> Self {
InputMessage::new_regular(recipient, data.to_vec(), TransmissionLane::General, None)
}
pub fn new_premade(
msgs: Vec<MixPacket>,
packet_bytes: Vec<Vec<u8>>,
lane: TransmissionLane,
packet_type: PacketType,
) -> Self {
let message = InputMessage::Premade { msgs, lane };
let message = InputMessage::Premade { packet_bytes, lane };
if packet_type == PacketType::Mix {
message
} else {
@@ -185,4 +203,394 @@ impl InputMessage {
self.set_max_retransmissions(max_retransmissions);
self
}
#[allow(clippy::expect_used)]
pub fn serialized_size(&self) -> u64 {
make_bincode_serializer()
.serialized_size(self)
.expect("failed to get serialized InputMessage size")
+ LENGHT_ENCODING_PREFIX_SIZE as u64
}
}
pub struct AdressedInputMessageCodec(pub Recipient);
impl Encoder<&[u8]> for AdressedInputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: &[u8], buf: &mut BytesMut) -> Result<(), Self::Error> {
let mut codec = InputMessageCodec;
let input_message = InputMessage::simple(item, self.0);
codec.encode(input_message, buf)?;
Ok(())
}
}
pub struct InputMessageCodec;
impl Encoder<InputMessage> for InputMessageCodec {
type Error = ClientCoreError;
fn encode(&mut self, item: InputMessage, buf: &mut BytesMut) -> Result<(), Self::Error> {
#[allow(clippy::expect_used)]
let encoded = make_bincode_serializer().serialize(&item)?;
let encoded_len = encoded.len() as u32;
let mut encoded_with_len = encoded_len.to_le_bytes().to_vec();
encoded_with_len.extend(encoded);
buf.reserve(encoded_with_len.len());
buf.extend_from_slice(&encoded_with_len);
Ok(())
}
}
impl Decoder for InputMessageCodec {
type Item = InputMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
#[allow(clippy::expect_used)]
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = make_bincode_serializer()
.deserialize(&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE])?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
/// Codec for encoding/decoding `ReconstructedMessage` for the AsyncRead interface.
///
/// This codec was moved from `nymsphinx::receiver` to keep bincode serialization
/// out of the core sphinx crate.
///
/// Uses a simple length-prefixed binary format:
/// - 4 bytes: length of encoded message
/// - N bytes: encoded message using `ReconstructedMessage::encode()`
///
/// The message encoding format is:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub struct ReconstructedMessageCodec;
impl Encoder<ReconstructedMessage> for ReconstructedMessageCodec {
type Error = ClientCoreError;
fn encode(
&mut self,
item: ReconstructedMessage,
buf: &mut BytesMut,
) -> Result<(), Self::Error> {
let encoded = item.encode();
let encoded_len = encoded.len() as u32;
buf.reserve(LENGHT_ENCODING_PREFIX_SIZE + encoded.len());
buf.extend_from_slice(&encoded_len.to_le_bytes());
buf.extend_from_slice(&encoded);
Ok(())
}
}
impl Decoder for ReconstructedMessageCodec {
type Item = ReconstructedMessage;
type Error = ClientCoreError;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if buf.len() < LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let len = u32::from_le_bytes(buf[0..LENGHT_ENCODING_PREFIX_SIZE].try_into()?) as usize;
if buf.len() < len + LENGHT_ENCODING_PREFIX_SIZE {
return Ok(None);
}
let decoded = ReconstructedMessage::decode(
&buf[LENGHT_ENCODING_PREFIX_SIZE..len + LENGHT_ENCODING_PREFIX_SIZE],
)
.map_err(|e| ClientCoreError::CodecError(e.to_string()))?;
buf.advance(len + LENGHT_ENCODING_PREFIX_SIZE);
Ok(Some(decoded))
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::PacketType;
use rand::SeedableRng;
fn test_recipient() -> Recipient {
Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap()
}
fn test_sender_tag() -> AnonymousSenderTag {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
AnonymousSenderTag::new_random(&mut rng)
}
#[test]
fn encode_decode_all_variants() {
let mut codec = InputMessageCodec;
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
3,
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Expected Anonymous variant"),
}
}
{
let mut buf = BytesMut::new();
let msg = InputMessage::new_reply(
test_sender_tag(),
vec![6, 7, 8],
TransmissionLane::General,
None,
);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::Reply { data, .. } => {
assert_eq!(data, vec![6, 7, 8]);
}
_ => panic!("Expected Reply variant"),
}
}
{
let mut buf = BytesMut::new();
let inner = InputMessage::new_anonymous(
test_recipient(),
vec![9, 10],
2,
TransmissionLane::General,
None,
);
let msg = InputMessage::new_wrapper(inner, PacketType::Mix);
codec.encode(msg, &mut buf).unwrap();
let decoded = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode message");
match decoded {
InputMessage::MessageWrapper {
message,
packet_type,
} => {
assert_eq!(packet_type, PacketType::Mix);
match *message {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![9, 10]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Expected Anonymous inner message"),
}
}
_ => panic!("Expected MessageWrapper variant"),
}
}
}
#[test]
fn encode_decode_sequential_messages() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3],
1,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![4, 5, 6, 7],
2,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![8, 9],
3,
TransmissionLane::General,
None,
),
&mut buf,
)
.unwrap();
let decoded1 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode first message");
match decoded1 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![1, 2, 3]);
assert_eq!(reply_surbs, 1);
}
_ => panic!("Wrong variant"),
}
let decoded2 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode second message");
match decoded2 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![4, 5, 6, 7]);
assert_eq!(reply_surbs, 2);
}
_ => panic!("Wrong variant"),
}
let decoded3 = codec
.decode(&mut buf)
.unwrap()
.expect("Should decode third message");
match decoded3 {
InputMessage::Anonymous {
data, reply_surbs, ..
} => {
assert_eq!(data, vec![8, 9]);
assert_eq!(reply_surbs, 3);
}
_ => panic!("Wrong variant"),
}
// Buffer should be empty
let decoded4 = codec.decode(&mut buf).unwrap();
assert!(decoded4.is_none(), "Should have no more messages");
assert_eq!(buf.len(), 0, "Buffer should be empty");
}
#[test]
fn partial_message_handling() {
let mut codec = InputMessageCodec;
let mut buf = BytesMut::new();
// Empty @ beginning
assert!(codec.decode(&mut buf).unwrap().is_none());
let mut buf = BytesMut::from(&[0x10, 0x00][..]);
assert!(codec.decode(&mut buf).unwrap().is_none());
assert_eq!(buf.len(), 2, "Buffer should be unchanged");
let mut full_buf = BytesMut::new();
codec
.encode(
InputMessage::new_anonymous(
test_recipient(),
vec![1, 2, 3, 4, 5],
2,
TransmissionLane::General,
None,
),
&mut full_buf,
)
.unwrap();
// Only first half of the message
let partial_len = full_buf.len() / 2;
let mut partial_buf = full_buf.split_to(partial_len);
assert!(codec.decode(&mut partial_buf).unwrap().is_none());
assert_eq!(partial_buf.len(), partial_len, "Buffer should be unchanged");
partial_buf.unsplit(full_buf);
let decoded = codec.decode(&mut partial_buf).unwrap();
assert!(decoded.is_some(), "Should decode complete message");
match decoded.unwrap() {
InputMessage::Anonymous { data, .. } => {
assert_eq!(data, vec![1, 2, 3, 4, 5]);
}
_ => panic!("Expected Anonymous variant"),
}
}
#[test]
fn addressed_codec_compatibility() {
let recipient = test_recipient();
let data = b"test message payload";
let mut addressed_codec = AdressedInputMessageCodec(recipient);
let mut buf = BytesMut::new();
addressed_codec.encode(data.as_ref(), &mut buf).unwrap();
let mut input_codec = InputMessageCodec;
let decoded = input_codec
.decode(&mut buf)
.unwrap()
.expect("Should decode");
match decoded {
InputMessage::Regular {
data: decoded_data,
recipient: decoded_recipient,
lane,
..
} => {
assert_eq!(decoded_data, data, "Data should match");
assert_eq!(decoded_recipient, recipient, "Recipient should match");
assert_eq!(lane, TransmissionLane::General, "Should use General lane");
}
_ => panic!("Expected Regular variant"),
}
}
}
@@ -45,7 +45,25 @@ where
}
}
async fn handle_premade_packets(&mut self, packets: Vec<MixPacket>, lane: TransmissionLane) {
async fn handle_premade_packets(&mut self, packet_bytes: Vec<Vec<u8>>, lane: TransmissionLane) {
// Deserialize packet bytes back to MixPacket
let packets: Vec<MixPacket> = packet_bytes
.into_iter()
.filter_map(|bytes| {
MixPacket::try_from_v2_bytes(&bytes)
.map_err(|e| {
warn!("Failed to deserialize premade packet: {}", e);
e
})
.ok()
})
.collect();
if packets.is_empty() {
warn!("No valid premade packets to send");
return;
}
self.message_handler
.send_premade_mix_packets(
packets
@@ -156,7 +174,9 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await;
}
InputMessage::Premade { msgs, lane } => self.handle_premade_packets(msgs, lane).await,
InputMessage::Premade { packet_bytes, lane } => {
self.handle_premade_packets(packet_bytes, lane).await
}
InputMessage::MessageWrapper {
message,
packet_type,
@@ -202,8 +222,8 @@ where
self.handle_reply(recipient_tag, data, lane, max_retransmissions)
.await;
}
InputMessage::Premade { msgs, lane } => {
self.handle_premade_packets(msgs, lane).await
InputMessage::Premade { packet_bytes, lane } => {
self.handle_premade_packets(packet_bytes, lane).await
}
// MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => {
@@ -17,7 +17,7 @@
#![warn(clippy::dbg_macro)]
use crate::client::inbound_messages::{InputMessage, InputMessageSender};
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use nym_client_core_config_types::StatsReporting;
use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{
+9
View File
@@ -255,6 +255,15 @@ pub enum ClientCoreError {
#[error("Could not access task registry, {0}")]
RegistryAccess(#[from] RegistryAccessError),
#[error("Serialization error: {0}")]
BincodeError(#[from] Box<bincode::ErrorKind>),
#[error("Could not coarce to array")]
ArrayCreationFailure(#[from] std::array::TryFromSliceError),
#[error("Codec error: {0}")]
CodecError(String),
}
impl From<tungstenite::Error> for ClientCoreError {
+8
View File
@@ -1,3 +1,4 @@
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
use std::future::Future;
#[cfg(all(
@@ -39,3 +40,10 @@ where
{
tokio::spawn(future);
}
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: use of deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
pub use backend::*;
pub use combined::CombinedReplyStorage;
pub use key_storage::SentReplyKeys;
@@ -1,6 +1,8 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::derivable_impls)]
// MAX: surpressing warning for the moment, will be dealt with in a different PR (TODO)
use cosmwasm_schema::cw_serde;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
+2
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `generic_array::GenericArray::<T, N>::from_exact_iter`: please upgrade to generic-array 1.x - TODO
#[cfg(feature = "asymmetric")]
pub mod asymmetric;
pub mod bech32_address_validation;
+1
View File
@@ -1,5 +1,6 @@
// Copyright 2020-2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub use nym_crypto::generic_array;
use nym_crypto::OutputSizeUser;
@@ -70,7 +70,7 @@ impl BinaryRequest {
let plaintext = match self {
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.to_v2_bytes()?,
};
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
@@ -51,6 +51,9 @@ pub const VERCEL_COM_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
];
pub const NYM_API_CDN: &str = "cdn1.media-platform.net";
pub const NYM_API_CDN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(172, 104, 178, 252))];
pub const NYM_COM_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
@@ -88,6 +91,7 @@ pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
m.insert(NYM_API_CDN.to_string(), NYM_API_CDN_IPS.to_vec());
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
+15 -12
View File
@@ -1,6 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
// silences clippy warning: use of deprecated tuple variant `HttpClientError::GenericRequestFailure`: use another more strongly typed variant - this variant is only left for compatibility reasons - TODO
//! Nym HTTP API Client
//!
//! Centralizes and implements the core API client functionality. This crate provides custom,
@@ -329,14 +332,14 @@ pub enum HttpClientError {
#[error("failed to send request for {url}: {source}")]
RequestSendFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
#[source]
source: ReqwestErrorWrapper,
},
#[error("failed to read response body from {url}: {source}")]
ResponseReadFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
headers: Box<HeaderMap>,
status: StatusCode,
#[source]
@@ -353,7 +356,7 @@ pub enum HttpClientError {
},
#[error("the requested resource could not be found at {url}")]
NotFound { url: reqwest::Url },
NotFound { url: Box<reqwest::Url> },
#[error("attempted to use domain fronting and clone a request containing stream data")]
AttemptedToCloneStreamRequest,
@@ -365,7 +368,7 @@ pub enum HttpClientError {
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
)]
RequestFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -374,7 +377,7 @@ pub enum HttpClientError {
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
)]
EmptyResponse {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -383,7 +386,7 @@ pub enum HttpClientError {
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
)]
EndpointFailure {
url: reqwest::Url,
url: Box<reqwest::Url>,
status: StatusCode,
headers: Box<HeaderMap>,
error: String,
@@ -453,7 +456,7 @@ impl HttpClientError {
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
HttpClientError::RequestSendFailure {
url,
url: Box::new(url),
source: ReqwestErrorWrapper(source),
}
}
@@ -1517,7 +1520,7 @@ where
if !allow_empty && let Some(0) = res.content_length() {
return Err(HttpClientError::EmptyResponse {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
});
@@ -1530,25 +1533,25 @@ where
.bytes()
.await
.map_err(|source| HttpClientError::ResponseReadFailure {
url,
url: Box::new(url),
headers: Box::new(headers.clone()),
status,
source: ReqwestErrorWrapper(source),
})?;
decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url })
Err(HttpClientError::NotFound { url: Box::new(url) })
} else {
let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
});
};
Err(HttpClientError::EndpointFailure {
url,
url: Box::new(url),
status,
headers: Box::new(headers),
error: plaintext,
+4
View File
@@ -51,6 +51,10 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
ApiUrlConst {
url: "https://cdn1.media-platform.net/api/",
front_hosts: None,
},
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
@@ -28,7 +28,7 @@ impl MixnetConnectionBeacon {
}
}
async fn send_mixnet_self_ping(&self) -> Result<u64> {
async fn send_mixnet_self_ping(&mut self) -> Result<u64> {
trace!("Sending mixnet self ping");
let (input_message, request_id) = create_self_ping(self.our_address);
self.mixnet_client_sender
@@ -38,7 +38,7 @@ impl MixnetConnectionBeacon {
Ok(request_id)
}
pub async fn run(self, shutdown: CancellationToken) -> Result<()> {
pub async fn run(mut self, shutdown: CancellationToken) -> Result<()> {
debug!("Mixnet connection beacon is running");
let mut ping_interval = tokio::time::interval(MIXNET_SELF_PING_INTERVAL);
loop {
@@ -3,7 +3,6 @@
use std::time::Duration;
use futures::StreamExt;
use nym_sdk::mixnet::{MixnetClient, MixnetMessageSender, Recipient};
use tracing::{debug, error};
@@ -22,23 +21,25 @@ pub async fn self_ping_and_wait(
wait_for_self_ping_return(mixnet_client, &request_ids).await
}
async fn send_self_pings(our_address: Recipient, mixnet_client: &MixnetClient) -> Result<Vec<u64>> {
// Send pings
let request_ids = futures::stream::iter(1..=3)
.then(|_| async {
async fn send_self_pings(
our_address: Recipient,
mixnet_client: &mut MixnetClient,
) -> Result<Vec<u64>> {
let sender = mixnet_client.split_sender();
let futures = (1..=3).map(|_| {
let mut sender = sender.clone();
async move {
let (input_message, request_id) = create_self_ping(our_address);
mixnet_client
sender
.send(input_message)
.await
.map_err(|err| Error::NymSdkError(Box::new(err)))?;
Ok::<u64, Error>(request_id)
})
.collect::<Vec<_>>()
.await;
.map_err(|e| Error::NymSdkError(Box::new(e)))?;
Ok::<_, Error>(request_id)
}
});
// Check the vec of results and return the first error, if any. If there are not errors, unwrap
// all the results into a vec of u64s.
request_ids.into_iter().collect::<Result<Vec<_>>>()
futures::future::try_join_all(futures).await
}
async fn wait_for_self_ping_return(
+2 -1
View File
@@ -12,8 +12,9 @@ readme.workspace = true
publish = false
[dependencies]
tokio = { workspace = true, features = ["net"] }
tokio = { workspace = true, features = ["net", "io-util"] }
nym-test-utils = { path = "../test-utils", optional = true }
tracing = { workspace = true }
[features]
io-mocks = ["nym-test-utils"]
+74 -1
View File
@@ -4,8 +4,9 @@
#[cfg(feature = "io-mocks")]
use nym_test_utils::mocks::async_read_write::MockIOStream;
use std::net::SocketAddr;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::debug;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
@@ -13,6 +14,78 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
///
/// # Arguments
/// * `packet_data` - The serialised LP packet to send
///
/// # Errors
/// Returns an error on network transmission fails.
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>
where
Self: Unpin,
{
// Send 4-byte length prefix (u32 big-endian)
let len = packet_data.len() as u32;
self.write_all(&len.to_be_bytes())
.await
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
// Send the actual packet data
self.write_all(packet_data)
.await
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
// Flush to ensure data is sent immediately
self.flush()
.await
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
tracing::trace!(
"Sent LP packet ({} bytes + 4 byte header)",
packet_data.len()
);
Ok(())
}
/// Receives an LP packet from a TCP stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
///
/// # Errors
/// Returns an error on network transmission fails.
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>
where
Self: Unpin,
{
// Read 4-byte length prefix (u32 big-endian)
let mut len_buf = [0u8; 4];
self.read_exact(&mut len_buf)
.await
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
let packet_len = u32::from_be_bytes(len_buf) as usize;
// Sanity check to prevent huge allocations
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
if packet_len > MAX_PACKET_SIZE {
return Err(std::io::Error::other(format!(
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
)));
}
// Read the actual packet data
let mut packet_buf = vec![0u8; packet_len];
self.read_exact(&mut packet_buf)
.await
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
Ok(packet_buf)
}
}
impl LpTransport for TcpStream {
+28 -7
View File
@@ -38,12 +38,25 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
// Use consistent salt for deterministic tests
let salt = [1u8; 32];
let initiator_session =
LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
.expect("Test session creation failed");
let initiator_session = LpSession::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
packet::version::CURRENT,
)
.expect("Test session creation failed");
let responder_session = LpSession::new(receiver_index, false, resp, init.as_remote(), &salt)
.expect("Test session creation failed");
let responder_session = LpSession::new(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
packet::version::CURRENT,
)
.expect("Test session creation failed");
(initiator_session, responder_session)
}
@@ -51,7 +64,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
#[cfg(test)]
mod tests {
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN, version};
use crate::session_manager::SessionManager;
use crate::{LpError, sessions_for_tests};
use bytes::BytesMut;
@@ -182,11 +195,19 @@ mod tests {
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let _ = remote_manager
.create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// === Packet 1 (Counter 0 - Should succeed) ===
let packet1 = LpPacket {
+12
View File
@@ -239,6 +239,18 @@ pub struct ForwardPacketData {
}
impl ForwardPacketData {
pub fn new(
target_gateway_identity: ed25519::PublicKey,
target_lp_address: String,
inner_packet_bytes: Vec<u8>,
) -> Self {
ForwardPacketData {
target_gateway_identity: target_gateway_identity.to_bytes(),
target_lp_address,
inner_packet_bytes,
}
}
fn len(&self) -> usize {
// 32 bytes target gateway identity
// +
+3 -3
View File
@@ -203,9 +203,9 @@ impl LpHeader {
}
impl LpHeader {
pub fn new(receiver_idx: u32, counter: u64) -> Self {
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
Self {
protocol_version: version::CURRENT,
protocol_version,
reserved: [0u8; 3],
receiver_idx,
counter,
@@ -265,7 +265,7 @@ impl LpHeader {
Ok(LpHeader {
protocol_version,
reserved: [0u8; 3],
reserved,
receiver_idx,
counter,
})
+9
View File
@@ -1,6 +1,7 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ClientHelloData;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use std::collections::HashMap;
@@ -29,6 +30,14 @@ impl LpLocalPeer {
}
}
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
ClientHelloData::new_with_fresh_salt(
*self.x25519().public_key(),
*self.ed25519().public_key(),
timestamp,
)
}
#[must_use]
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
self.kem_psq = Some(key);
+59 -25
View File
@@ -231,7 +231,7 @@ pub struct LpSession {
/// Negotiated protocol version from handshake.
/// Set during handshake completion from the ClientHello/ServerHello packet header.
/// Used for future version negotiation and compatibility checks.
negotiated_version: std::sync::atomic::AtomicU8,
negotiated_version: u8,
}
// noiserm
@@ -276,18 +276,9 @@ impl LpSession {
/// Returns the negotiated protocol version from the handshake.
///
/// Defaults to 1 (current LP version). Set during handshake via
/// `set_negotiated_version()` when ClientHello/ServerHello is processed.
/// Set during `LpSession` creation after sending / receiving `ClientHelloData`
pub fn negotiated_version(&self) -> u8 {
self.negotiated_version.load(Ordering::Acquire)
}
/// Sets the negotiated protocol version from handshake packet header.
///
/// Should be called during handshake when processing ClientHello (responder)
/// or ServerHello (initiator) to record the agreed protocol version.
pub fn set_negotiated_version(&self, version: u8) {
self.negotiated_version.store(version, Ordering::Release);
self.negotiated_version
}
/// Returns the local X25519 public key.
@@ -364,12 +355,14 @@ impl LpSession {
/// * `local_peer` - This side's LP peer's keys
/// * `remote_peer` - The remote's LP peer's keys
/// * `salt` - Salt for PSK derivation
/// * `protocol_version` - Protocol version to attach in all `LpPacket`s
pub fn new(
id: u32,
is_initiator: bool,
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
salt: &[u8; 32],
protocol_version: u8,
) -> Result<Self, LpError> {
// noiserm
// if we're LP responder, we **must** set our kem key
@@ -444,13 +437,13 @@ impl LpSession {
subsession_counter: AtomicU64::new(0),
read_only: AtomicBool::new(false),
successor_session_id: Mutex::new(None),
negotiated_version: std::sync::atomic::AtomicU8::new(1), // Default to version 1
negotiated_version: protocol_version,
})
}
pub fn next_packet(&self, message: LpMessage) -> Result<LpPacket, LpError> {
let counter = self.next_counter();
let header = LpHeader::new(self.id(), counter);
let header = LpHeader::new(self.id(), counter, self.negotiated_version);
let packet = LpPacket::new(header, message);
Ok(packet)
}
@@ -1275,6 +1268,7 @@ impl LpSession {
remote_peer: self.remote_peer.clone(),
pq_shared_secret: PqSharedSecret::new(pq_secret),
subsession_psk,
negotiated_version: self.negotiated_version,
})
}
}
@@ -1314,8 +1308,12 @@ pub struct SubsessionHandshake {
/// 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],
/// Negotiated protocol version from handshake.
negotiated_version: u8,
}
impl SubsessionHandshake {
@@ -1415,7 +1413,7 @@ impl SubsessionHandshake {
read_only: AtomicBool::new(false),
successor_session_id: Mutex::new(None),
// Inherit parent's protocol version
negotiated_version: std::sync::atomic::AtomicU8::new(1),
negotiated_version: self.negotiated_version,
})
}
}
@@ -1423,6 +1421,7 @@ impl SubsessionHandshake {
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::version;
use crate::peer::mock_peers;
use crate::{replay::ReplayError, sessions_for_tests};
use nym_crypto::asymmetric::ed25519;
@@ -1448,8 +1447,15 @@ mod tests {
let salt = [0u8; 32]; // Test salt
// PSQ will derive the PSK during handshake using X25519 as DHKEM
let session = LpSession::new(receiver_index, is_initiator, local, remote.clone(), &salt)
.expect("Test session creation failed");
let session = LpSession::new(
receiver_index,
is_initiator,
local,
remote.clone(),
&salt,
version::CURRENT,
)
.expect("Test session creation failed");
// Initialize KKT state to Completed for tests (bypasses KKT exchange)
// This simulates having already received the remote party's KEM key via KKT
@@ -2112,14 +2118,28 @@ mod tests {
let receiver_index: u32 = 55555;
let salt = [0u8; 32];
let initiator_session =
LpSession::new(receiver_index, true, init.clone(), bad_resp, &salt).unwrap();
let initiator_session = LpSession::new(
receiver_index,
true,
init.clone(),
bad_resp,
&salt,
version::CURRENT,
)
.unwrap();
// Initialize KKT state for test
initiator_session.set_kkt_completed_for_test(resp.x25519.public_key());
let responder_session =
LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap();
let responder_session = LpSession::new(
receiver_index,
false,
resp,
bad_init,
&salt,
version::CURRENT,
)
.unwrap();
// Initialize KKT state for test
responder_session.set_kkt_completed_for_test(init.x25519.public_key());
@@ -2154,13 +2174,27 @@ mod tests {
let receiver_index: u32 = 66666;
let salt = [0u8; 32];
let initiator_session =
LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt).unwrap();
let initiator_session = LpSession::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Initialize KKT state for test
initiator_session.set_kkt_completed_for_test(resp.x25519.public_key());
let responder_session =
LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap();
let responder_session = LpSession::new(
receiver_index,
false,
resp,
bad_init,
&salt,
version::CURRENT,
)
.unwrap();
// Initialize KKT state for test
responder_session.set_kkt_completed_for_test(init.x25519.public_key());
+58 -7
View File
@@ -58,11 +58,25 @@ mod tests {
// 4. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session A");
let peer_b_sm = session_manager_2
.create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
false,
b.clone(),
a.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session B");
// Verify session count
@@ -481,11 +495,25 @@ mod tests {
let salt = [43u8; 32];
let peer_a_sm = session_manager_1
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session A");
let peer_b_sm = session_manager_2
.create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
false,
b.clone(),
a.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session B");
// Initialize KKT state for both sessions (test bypass)
@@ -659,7 +687,14 @@ mod tests {
// 2. Create a session (using real noise state)
let _session = session_manager
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create session");
// 3. Try to get a non-existent session
@@ -682,6 +717,7 @@ mod tests {
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.expect("Failed to create temp session");
@@ -733,6 +769,7 @@ mod tests {
}
// Remove unused imports if SessionManager methods are no longer direct dependencies
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
use crate::packet::version;
use crate::peer::mock_peers;
use crate::state_machine::LpData;
use crate::{
@@ -772,12 +809,26 @@ mod tests {
// 3. Create sessions state machines
assert!(
session_manager_1
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt,) // Initiator
.create_session_state_machine(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT
) // Initiator
.is_ok()
);
assert!(
session_manager_2
.create_session_state_machine(receiver_index, false, b, a.as_remote(), &salt,) // Responder
.create_session_state_machine(
receiver_index,
false,
b,
a.as_remote(),
&salt,
version::CURRENT
) // Responder
.is_ok()
);
+51 -6
View File
@@ -172,8 +172,16 @@ impl SessionManager {
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
salt: &[u8; 32],
protocol_version: u8,
) -> Result<u32, LpError> {
let sm = LpStateMachine::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?;
let sm = LpStateMachine::new(
receiver_index,
is_initiator,
local_peer,
remote_peer,
salt,
protocol_version,
)?;
self.state_machines.insert(receiver_index, sm);
Ok(receiver_index)
@@ -204,6 +212,7 @@ impl SessionManager {
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::version;
use crate::peer::{mock_peers, random_peer};
use nym_test_utils::helpers::deterministic_rng;
@@ -218,7 +227,14 @@ mod tests {
let receiver_index: u32 = 1001;
let sm_1_id = manager
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
true,
local,
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let retrieved = manager.state_machine_exists(sm_1_id);
@@ -239,7 +255,14 @@ mod tests {
let receiver_index: u32 = 2002;
let sm_1_id = manager
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
.create_session_state_machine(
receiver_index,
true,
local,
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let removed = manager.remove_state_machine(sm_1_id);
@@ -262,15 +285,36 @@ mod tests {
let salt = [49u8; 32];
let sm_1 = manager
.create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt)
.create_session_state_machine(
3001,
true,
local.clone(),
peer1.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_2 = manager
.create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt)
.create_session_state_machine(
3002,
true,
local.clone(),
peer2.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let sm_3 = manager
.create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt)
.create_session_state_machine(
3003,
true,
local.clone(),
peer3.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
assert_eq!(manager.session_count(), 3);
@@ -298,6 +342,7 @@ mod tests {
init,
resp.as_remote(),
&salt,
version::CURRENT,
);
assert!(sm.is_ok());
+101 -23
View File
@@ -256,16 +256,25 @@ impl LpStateMachine {
/// * `local_peer` - This side's LP peer's keys
/// * `remote_peer` - The remote's LP peer's keys
/// * `salt` - Fresh salt for PSK derivation (must be unique per session)
/// * `protocol_version` - Protocol version to attach in all `LpPacket`s
pub fn new(
receiver_index: u32,
is_initiator: bool,
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
salt: &[u8; 32],
protocol_version: u8,
) -> Result<Self, LpError> {
// Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise)
// receiver_index is client-proposed, passed through directly
let session = LpSession::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?;
let session = LpSession::new(
receiver_index,
is_initiator,
local_peer,
remote_peer,
salt,
protocol_version,
)?;
Ok(LpStateMachine {
state: LpState::ReadyToHandshake {
@@ -1144,6 +1153,7 @@ impl LpStateMachine {
#[cfg(test)]
mod tests {
use super::*;
use crate::packet::version;
use crate::peer::mock_peers;
#[test]
@@ -1155,8 +1165,14 @@ mod tests {
let receiver_index: u32 = 77777;
let initiator_sm =
LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt);
let initiator_sm = LpStateMachine::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
);
assert!(initiator_sm.is_ok());
let initiator_sm = initiator_sm.unwrap();
assert!(matches!(
@@ -1166,8 +1182,14 @@ mod tests {
let init_session = initiator_sm.session().unwrap();
assert!(init_session.is_initiator());
let responder_sm =
LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt);
let responder_sm = LpStateMachine::new(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
version::CURRENT,
);
assert!(responder_sm.is_ok());
let responder_sm = responder_sm.unwrap();
assert!(matches!(
@@ -1196,6 +1218,7 @@ mod tests {
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
@@ -1205,6 +1228,7 @@ mod tests {
resp,
init.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
@@ -1389,8 +1413,15 @@ mod tests {
let receiver_index: u32 = 99901;
// Create initiator state machine
let mut initiator =
LpStateMachine::new(receiver_index, true, init, resp.as_remote(), &salt).unwrap();
let mut initiator = LpStateMachine::new(
receiver_index,
true,
init,
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Verify initial state
assert!(matches!(initiator.state, LpState::ReadyToHandshake { .. }));
@@ -1409,8 +1440,15 @@ mod tests {
let receiver_index: u32 = 99902;
// Create responder state machine
let mut responder =
LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap();
let mut responder = LpStateMachine::new(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Verify initial state
assert!(matches!(responder.state, LpState::ReadyToHandshake { .. }));
@@ -1430,12 +1468,25 @@ mod tests {
let receiver_index: u32 = 99903;
// Create both state machines
let mut initiator =
LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
.unwrap();
let mut initiator = LpStateMachine::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let mut responder =
LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap();
let mut responder = LpStateMachine::new(
receiver_index,
false,
resp,
init.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Step 1: Initiator starts handshake, sends KKT request
let init_action = initiator.process_input(LpInput::StartHandshake);
@@ -1477,9 +1528,15 @@ mod tests {
let receiver_index: u32 = 99904;
// Create initiator state machine
let mut initiator =
LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
.unwrap();
let mut initiator = LpStateMachine::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Start handshake to enter KKTExchange state
initiator.process_input(LpInput::StartHandshake);
@@ -1499,9 +1556,15 @@ mod tests {
let receiver_index: u32 = 99905;
// Create initiator state machine
let mut initiator =
LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
.unwrap();
let mut initiator = LpStateMachine::new(
receiver_index,
true,
init.clone(),
resp.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// Start handshake to enter KKTExchange state
initiator.process_input(LpInput::StartHandshake);
@@ -1534,10 +1597,25 @@ mod tests {
let receiver_index: u32 = 111111;
// Create state machines - Alice is initiator, Bob is responder
let mut alice =
LpStateMachine::new(receiver_index, true, a.clone(), b.as_remote(), &salt).unwrap();
let mut alice = LpStateMachine::new(
receiver_index,
true,
a.clone(),
b.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
let mut bob = LpStateMachine::new(receiver_index, false, b, a.as_remote(), &salt).unwrap();
let mut bob = LpStateMachine::new(
receiver_index,
false,
b,
a.as_remote(),
&salt,
version::CURRENT,
)
.unwrap();
// --- Complete KKT Exchange ---
// Alice starts handshake
+4 -8
View File
@@ -13,6 +13,8 @@ rand = { workspace = true }
rand_distr = { workspace = true }
rand_chacha = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"] }
log = { workspace = true }
nym-sphinx-acknowledgements = { workspace = true }
nym-sphinx-addressing = { workspace = true }
@@ -47,11 +49,5 @@ features = ["sync"]
[features]
default = ["sphinx"]
sphinx = [
"nym-sphinx-params/sphinx",
"nym-sphinx-types/sphinx",
]
outfox = [
"nym-sphinx-params/outfox",
"nym-sphinx-types/outfox",
]
sphinx = ["nym-sphinx-params/sphinx", "nym-sphinx-types/sphinx"]
outfox = ["nym-sphinx-params/outfox", "nym-sphinx-types/outfox"]
@@ -1,5 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated associated function `nym_crypto::generic_array::GenericArray::<T, N>::clone_from_slice`: please upgrade to generic-array 1.x - TODO
pub mod identifier;
pub mod key;
@@ -12,6 +12,7 @@ rand = { workspace = true }
bs58 = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
serde = { workspace = true }
nym-crypto = { workspace = true, features = ["stream_cipher", "rand"] }
nym-sphinx-addressing = { workspace = true }
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)] // silences clippy warning: deprecated struct `nym_crypto::generic_array::GenericArray`: please upgrade to generic-array 1.x - TODO
pub mod encryption_key;
pub mod reply_surb;
pub mod requests;
@@ -7,6 +7,7 @@ use crate::{ReplySurbError, ReplySurbWithKeyRotation};
use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError};
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::mem;
use thiserror::Error;
@@ -30,7 +31,7 @@ pub enum InvalidAnonymousSenderTagRepresentation {
InvalidLength { received: usize, expected: usize },
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct AnonymousSenderTag([u8; SENDER_TAG_SIZE]);
+1
View File
@@ -13,3 +13,4 @@ nym-sphinx-params = { workspace = true }
nym-sphinx-types = { workspace = true, features = ["sphinx", "outfox"] }
nym-sphinx-anonymous-replies = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
+5 -3
View File
@@ -174,13 +174,15 @@ impl MixPacket {
})
}
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
pub fn to_v2_bytes(&self) -> Result<Vec<u8>, MixPacketFormattingError> {
Ok(std::iter::once(self.packet_type as u8)
.chain(std::iter::once(self.key_rotation as u8))
.chain(self.next_hop.as_bytes())
.chain(self.packet.to_bytes()?)
.collect())
}
}
// TODO: test for serialization and errors!
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
self.to_v2_bytes()
}
}
+149 -2
View File
@@ -1,6 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use crate::message::{NymMessage, NymMessageError, PaddedMessage, PlainMessage};
use nym_crypto::aes::cipher::{KeyIvInit, StreamCipher};
use nym_crypto::asymmetric::x25519;
@@ -9,6 +11,7 @@ use nym_crypto::symmetric::stream_cipher;
use nym_crypto::symmetric::stream_cipher::CipherKey;
use nym_sphinx_anonymous_replies::SurbEncryptionKey;
use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx_anonymous_replies::requests::SENDER_TAG_SIZE;
use nym_sphinx_chunking::ChunkingError;
use nym_sphinx_chunking::fragment::Fragment;
use nym_sphinx_chunking::reconstruction::MessageReconstructor;
@@ -17,8 +20,27 @@ use nym_sphinx_params::{
};
use thiserror::Error;
// TODO: should this live in this file?
#[derive(Debug)]
/// Error when decoding a `ReconstructedMessage` from bytes.
#[derive(Debug, Error)]
pub enum ReconstructedMessageError {
#[error("Not enough bytes to decode message: expected at least {expected}, got {received}")]
TooShort { expected: usize, received: usize },
#[error("Invalid sender tag flag: expected 0 or 1, got {0}")]
InvalidSenderTagFlag(u8),
}
/// A message that has been reconstructed from sphinx packets.
///
/// Format:
/// This type uses a simple binary encoding for serialization:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
///
/// The first byte indicates whether a sender tag is present (1) or not (0).
/// If present, it is followed by the 16-byte sender tag. The remaining bytes
/// are the message payload.
#[derive(Debug, Clone)]
pub struct ReconstructedMessage {
/// The actual plaintext message that was received.
pub message: Vec<u8>,
@@ -45,6 +67,71 @@ impl ReconstructedMessage {
pub fn into_inner(self) -> (Vec<u8>, Option<AnonymousSenderTag>) {
self.into()
}
/// Encodes this message into bytes.
///
/// Format:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub fn encode(&self) -> Vec<u8> {
match &self.sender_tag {
Some(tag) => {
let mut buf = Vec::with_capacity(1 + SENDER_TAG_SIZE + self.message.len());
buf.push(1);
buf.extend_from_slice(&tag.to_bytes());
buf.extend_from_slice(&self.message);
buf
}
None => {
let mut buf = Vec::with_capacity(1 + self.message.len());
buf.push(0);
buf.extend_from_slice(&self.message);
buf
}
}
}
/// Decodes a message from bytes.
///
/// Format:
/// - Without sender_tag: `[0][payload...]`
/// - With sender_tag: `[1][16-byte tag][payload...]`
pub fn decode(bytes: &[u8]) -> Result<Self, ReconstructedMessageError> {
if bytes.is_empty() {
return Err(ReconstructedMessageError::TooShort {
expected: 1,
received: 0,
});
}
match bytes[0] {
0 => Ok(ReconstructedMessage {
message: bytes[1..].to_vec(),
sender_tag: None,
}),
1 => {
if bytes.len() < 1 + SENDER_TAG_SIZE {
return Err(ReconstructedMessageError::TooShort {
expected: 1 + SENDER_TAG_SIZE,
received: bytes.len(),
});
}
let tag_bytes: [u8; SENDER_TAG_SIZE] = bytes[1..1 + SENDER_TAG_SIZE]
.try_into()
.expect("slice length verified above");
Ok(ReconstructedMessage {
message: bytes[1 + SENDER_TAG_SIZE..].to_vec(),
sender_tag: Some(AnonymousSenderTag::from_bytes(tag_bytes)),
})
}
flag => Err(ReconstructedMessageError::InvalidSenderTagFlag(flag)),
}
}
/// Returns the encoded size of this message.
pub fn encoded_size(&self) -> usize {
1 + self.sender_tag.map_or(0, |_| SENDER_TAG_SIZE) + self.message.len()
}
}
impl From<PlainMessage> for ReconstructedMessage {
@@ -75,6 +162,9 @@ pub enum MessageRecoveryError {
#[error("Failed to recover message fragment - {0}")]
FragmentRecoveryError(#[from] ChunkingError),
#[error("Failed to recover message fragment - {0}")]
MessageRecoveryError(#[from] io::Error),
}
pub trait MessageReceiver {
@@ -185,3 +275,60 @@ impl MessageReceiver for SphinxMessageReceiver {
&mut self.reconstructor
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_decode_without_sender_tag() {
let msg = ReconstructedMessage {
message: b"hello world".to_vec(),
sender_tag: None,
};
let encoded = msg.encode();
assert_eq!(encoded[0], 0); // flag byte
assert_eq!(&encoded[1..], b"hello world");
let decoded = ReconstructedMessage::decode(&encoded).unwrap();
assert_eq!(decoded.message, b"hello world");
assert!(decoded.sender_tag.is_none());
}
#[test]
fn encode_decode_with_sender_tag() {
let tag = AnonymousSenderTag::from_bytes([42u8; SENDER_TAG_SIZE]);
let msg = ReconstructedMessage {
message: b"hello world".to_vec(),
sender_tag: Some(tag),
};
let encoded = msg.encode();
assert_eq!(encoded[0], 1); // flag byte
assert_eq!(&encoded[1..1 + SENDER_TAG_SIZE], &[42u8; SENDER_TAG_SIZE]);
assert_eq!(&encoded[1 + SENDER_TAG_SIZE..], b"hello world");
let decoded = ReconstructedMessage::decode(&encoded).unwrap();
assert_eq!(decoded.message, b"hello world");
assert_eq!(
decoded.sender_tag.unwrap().to_bytes(),
[42u8; SENDER_TAG_SIZE]
);
}
#[test]
fn encoded_size_matches() {
let msg_no_tag = ReconstructedMessage {
message: b"test".to_vec(),
sender_tag: None,
};
assert_eq!(msg_no_tag.encoded_size(), msg_no_tag.encode().len());
let msg_with_tag = ReconstructedMessage {
message: b"test".to_vec(),
sender_tag: Some(AnonymousSenderTag::from_bytes([0u8; SENDER_TAG_SIZE])),
};
assert_eq!(msg_with_tag.encoded_size(), msg_with_tag.encode().len());
}
}
+1
View File
@@ -15,6 +15,7 @@ workspace = true
[dependencies]
bincode = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tracing = { workspace = true }
nym-authenticator-requests = { workspace = true }
nym-credentials-interface = { workspace = true }
+6 -5
View File
@@ -11,10 +11,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
pub use lp_messages::{
LpDvpnRegistrationRequest, LpMixnetGatewayData, LpMixnetRegistrationRequest,
LpRegistrationData, LpRegistrationRequest, LpRegistrationResponse, RegistrationMode,
};
pub use lp_messages::*;
pub use serialisation::BincodeError;
mod lp_messages;
@@ -30,7 +27,7 @@ pub struct NymNodeInformation {
pub version: AuthenticatorVersion,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct WireguardConfiguration {
#[serde(with = "bs58_x25519_pubkey")]
pub public_key: x25519::PublicKey,
@@ -45,6 +42,10 @@ pub struct NymNodeLPInformation {
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
pub x25519: x25519::PublicKey,
/// Supported protocol version of the remote gateway.
/// Included in case we have to downgrade our version.
pub lp_protocol_version: u8,
}
#[derive(Clone, Copy, Debug)]
+424 -161
View File
@@ -4,65 +4,22 @@
//! LP (Lewes Protocol) registration message types shared between client and gateway.
use crate::WireguardConfiguration;
use crate::dvpn::{
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
LpDvpnRegistrationRequestMessage, LpDvpnRegistrationRequestMessageContent,
LpDvpnRegistrationResponseMessage, LpDvpnRegistrationResponseMessageContent,
RequiresCredentialResponse,
};
use crate::mixnet::{
LpMixnetGatewayData, LpMixnetRegistrationRequestMessage, LpMixnetRegistrationResponseMessage,
LpMixnetRegistrationResponseMessageContent,
};
use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer};
use nym_credentials_interface::{CredentialSpendingData, TicketType};
use nym_authenticator_requests::models::BandwidthClaim;
use nym_credentials_interface::TicketType;
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
/// Registration request sent by client after LP handshake
/// Aligned with existing authenticator registration flow
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpRegistrationRequest {
/// Mode specific registration data
pub registration_data: LpRegistrationData,
/// Unix timestamp for replay protection
pub timestamp: u64,
}
impl LpRegistrationRequest {
pub fn mode(&self) -> RegistrationMode {
match self.registration_data {
LpRegistrationData::Dvpn { .. } => RegistrationMode::Dvpn,
LpRegistrationData::Mixnet { .. } => RegistrationMode::Mixnet,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpRegistrationData {
/// dVPN mode - register as WireGuard peer (most common)
Dvpn {
data: Box<LpDvpnRegistrationRequest>,
},
/// Mixnet mode - register for mixnet routing via IPR
Mixnet { data: LpMixnetRegistrationRequest },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpDvpnRegistrationRequest {
/// Client's WireGuard public key (for dVPN mode)
pub wg_public_key: nym_wireguard_types::PeerPublicKey,
/// Bandwidth credential for payment
pub credential: CredentialSpendingData,
/// Ticket type for bandwidth allocation
pub ticket_type: TicketType,
/// Preshared key to be used for the connection
pub psk: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpMixnetRegistrationRequest {
/// Client's ed25519 public key (identity)
///
/// Used to derive DestinationAddressBytes for ActiveClientsStore lookup.
pub client_ed25519_pubkey: ed25519::PublicKey,
}
use tracing::error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegistrationMode {
@@ -73,49 +30,112 @@ pub enum RegistrationMode {
Mixnet,
}
/// Gateway data for mixnet mode registration
///
/// Contains the gateway's identity and sphinx key needed for the client
/// to construct its full nym Recipient address.
/// Registration request sent by client after LP handshake
/// Aligned with existing authenticator registration flow
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpMixnetGatewayData {
/// Gateway's ed25519 identity public key
///
/// Forms part of the client's nym Recipient address.
pub gateway_identity: ed25519::PublicKey,
// TODO: what we really need in here is the address of internal IPR
pub struct LpRegistrationRequest {
/// Mode specific registration data
pub registration_data: LpRegistrationRequestData,
/// Unix timestamp for replay protection
pub timestamp: u64,
}
impl LpRegistrationRequest {
pub fn mode(&self) -> RegistrationMode {
match self.registration_data {
LpRegistrationRequestData::Dvpn { .. } => RegistrationMode::Dvpn,
LpRegistrationRequestData::Mixnet { .. } => RegistrationMode::Mixnet,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpRegistrationRequestData {
/// dVPN mode - register as WireGuard peer (most common)
Dvpn {
data: Box<LpDvpnRegistrationRequestMessage>,
},
/// Mixnet mode - register for mixnet routing via IPR
Mixnet {
data: LpMixnetRegistrationRequestMessage,
},
}
/// Registration response from gateway
/// Contains GatewayData for compatibility with existing client code
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpRegistrationResponse {
/// Whether registration succeeded
pub success: bool,
/// The status of this registration after the last received client message
pub status: RegistrationStatus,
/// Error message if registration failed
pub error: Option<String>,
/// Mode specific registration response
pub response_data: LpRegistrationResponseData,
}
/// Gateway configuration data for dVPN mode (WireGuard)
/// This matches what WireguardRegistrationResult expects
pub gateway_data: Option<WireguardConfiguration>,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpRegistrationResponseData {
/// dVPN mode - register as WireGuard peer (most common)
Dvpn {
data: LpDvpnRegistrationResponseMessage,
},
/// Gateway data for mixnet mode
///
/// Contains gateway identity and sphinx key needed for nym address construction.
/// Only populated for Mixnet mode registrations.
pub lp_gateway_data: Option<LpMixnetGatewayData>,
/// Mixnet mode - register for mixnet routing via IPR
Mixnet {
data: LpMixnetRegistrationResponseMessage,
},
}
/// Allocated bandwidth in bytes
pub allocated_bandwidth: i64,
/// Represents the registration status after the last received client message.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegistrationStatus {
/// The registration has been completed successfully
Completed,
/// The registration has failed
Failed,
/// To complete registration the client needs to send additional data,
/// e.g. a credential. it is context dependent.
PendingMoreData,
}
impl RegistrationStatus {
pub fn is_successful(&self) -> bool {
matches!(self, RegistrationStatus::Completed)
}
pub fn is_failed(&self) -> bool {
matches!(self, RegistrationStatus::Failed)
}
pub fn is_pending(&self) -> bool {
matches!(self, RegistrationStatus::PendingMoreData)
}
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.inspect_err(|_| error!("the current timestamp predates unix epoch!"))
.unwrap_or_default()
.as_secs()
}
impl LpRegistrationRequest {
/// Create a new dVPN registration request
pub fn new_dvpn<R>(
/// Helper wrapping timestamp extraction
fn new(registration_data: LpRegistrationRequestData) -> LpRegistrationRequest {
Self {
registration_data,
timestamp: current_timestamp(),
}
}
/// Create new dVPN registration initialisation request
pub fn new_initial_dvpn<R>(
rng: &mut R,
wg_public_key: nym_wireguard_types::PeerPublicKey,
credential: CredentialSpendingData,
ticket_type: TicketType,
) -> Self
where
@@ -124,29 +144,32 @@ impl LpRegistrationRequest {
let mut psk = [0u8; 32];
rng.fill_bytes(&mut psk);
Self {
registration_data: LpRegistrationData::Dvpn {
data: Box::new(LpDvpnRegistrationRequest {
wg_public_key,
credential,
ticket_type,
psk,
}),
},
#[allow(clippy::expect_used)]
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_secs(),
}
Self::new(LpRegistrationRequestData::Dvpn {
data: Box::new(LpDvpnRegistrationRequestMessage {
content: LpDvpnRegistrationRequestMessageContent::InitialRequest(
LpDvpnRegistrationInitialRequest {
wg_public_key,
psk,
ticket_type,
},
),
}),
})
}
pub fn new_finalise_dvpn(credential: BandwidthClaim) -> Self {
Self::new(LpRegistrationRequestData::Dvpn {
data: Box::new(LpDvpnRegistrationRequestMessage {
content: LpDvpnRegistrationRequestMessageContent::Finalisation(
LpDvpnRegistrationFinalisation { credential },
),
}),
})
}
/// Validate the request timestamp is within acceptable bounds
pub fn validate_timestamp(&self, max_skew_secs: u64) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let now = current_timestamp();
(now as i64 - self.timestamp as i64).abs() <= max_skew_secs as i64
}
@@ -164,35 +187,95 @@ impl LpRegistrationRequest {
impl LpRegistrationResponse {
/// Create a success response with GatewayData (for dVPN mode)
pub fn success(allocated_bandwidth: i64, gateway_data: WireguardConfiguration) -> Self {
pub fn success_dvpn(config: WireguardConfiguration, available_bandwidth: i64) -> Self {
Self {
success: true,
error: None,
gateway_data: Some(gateway_data),
lp_gateway_data: None,
allocated_bandwidth,
status: RegistrationStatus::Completed,
response_data: LpRegistrationResponseData::Dvpn {
data: LpDvpnRegistrationResponseMessage {
content: LpDvpnRegistrationResponseMessageContent::CompletedRegistration(
dvpn::CompletedRegistrationResponse {
config,
available_bandwidth,
},
),
},
},
}
}
/// Create a success response for mixnet mode with LpGatewayData
pub fn success_mixnet(allocated_bandwidth: i64, lp_gateway_data: LpMixnetGatewayData) -> Self {
pub fn success_mixnet(config: LpMixnetGatewayData, available_bandwidth: i64) -> Self {
Self {
success: true,
error: None,
gateway_data: None,
lp_gateway_data: Some(lp_gateway_data),
allocated_bandwidth,
status: RegistrationStatus::Completed,
response_data: LpRegistrationResponseData::Mixnet {
data: LpMixnetRegistrationResponseMessage {
content: LpMixnetRegistrationResponseMessageContent::CompletedRegistration(
mixnet::CompletedRegistrationResponse {
config,
available_bandwidth,
},
),
},
},
}
}
/// Create an error response
pub fn error(error: String) -> Self {
Self {
success: false,
error: Some(error),
gateway_data: None,
lp_gateway_data: None,
allocated_bandwidth: 0,
pub fn error(error: impl Into<String>, mode: RegistrationMode) -> Self {
let response_data = match mode {
RegistrationMode::Dvpn => LpRegistrationResponseData::Dvpn {
data: LpDvpnRegistrationResponseMessage::error(error),
},
RegistrationMode::Mixnet => LpRegistrationResponseData::Mixnet {
data: LpMixnetRegistrationResponseMessage::error(error),
},
};
LpRegistrationResponse {
status: RegistrationStatus::Failed,
response_data,
}
}
pub fn request_dvpn_credential() -> Self {
LpRegistrationResponse {
status: RegistrationStatus::PendingMoreData,
response_data: LpRegistrationResponseData::Dvpn {
data: LpDvpnRegistrationResponseMessage {
content: LpDvpnRegistrationResponseMessageContent::RequiresCredential(
RequiresCredentialResponse,
),
},
},
}
}
pub fn into_dvpn_response(self) -> Option<LpDvpnRegistrationResponseMessage> {
match self.response_data {
LpRegistrationResponseData::Dvpn { data } => Some(data),
LpRegistrationResponseData::Mixnet { .. } => None,
}
}
pub fn into_mixnet_response(self) -> Option<LpMixnetRegistrationResponseMessage> {
match self.response_data {
LpRegistrationResponseData::Mixnet { data } => Some(data),
LpRegistrationResponseData::Dvpn { .. } => None,
}
}
pub fn error_message(&self) -> Option<&str> {
match &self.response_data {
LpRegistrationResponseData::Dvpn { data } => match &data.content {
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(response) => {
Some(&response.error)
}
_ => None,
},
LpRegistrationResponseData::Mixnet { data } => match &data.content {
LpMixnetRegistrationResponseMessageContent::RegistrationFailure(response) => {
Some(&response.error)
}
_ => None,
},
}
}
@@ -207,16 +290,170 @@ impl LpRegistrationResponse {
}
}
pub mod dvpn {
use crate::WireguardConfiguration;
use nym_authenticator_requests::models::BandwidthClaim;
use nym_credentials_interface::TicketType;
use serde::{Deserialize, Serialize};
// client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpDvpnRegistrationRequestMessage {
pub content: LpDvpnRegistrationRequestMessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpDvpnRegistrationRequestMessageContent {
InitialRequest(LpDvpnRegistrationInitialRequest),
Finalisation(LpDvpnRegistrationFinalisation),
// in theory, we could also extend it with Bandwidth-related messages,
// but that shouldn't really be the responsibility of a Registration client.
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpDvpnRegistrationInitialRequest {
/// Client's WireGuard public key (for dVPN mode)
pub wg_public_key: nym_wireguard_types::PeerPublicKey,
/// Preshared key to be used for the connection
pub psk: [u8; 32],
/// Type of the ticket/gateway we're going to register with
pub ticket_type: TicketType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpDvpnRegistrationFinalisation {
/// Ecash credential
pub credential: BandwidthClaim,
}
// gateway
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpDvpnRegistrationResponseMessage {
pub content: LpDvpnRegistrationResponseMessageContent,
}
impl LpDvpnRegistrationResponseMessage {
pub fn error(error: impl Into<String>) -> Self {
LpDvpnRegistrationResponseMessage {
content: LpDvpnRegistrationResponseMessageContent::RegistrationFailure(
RegistrationFailureResponse {
error: error.into(),
},
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpDvpnRegistrationResponseMessageContent {
RequiresCredential(RequiresCredentialResponse),
CompletedRegistration(CompletedRegistrationResponse),
RegistrationFailure(RegistrationFailureResponse),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct CompletedRegistrationResponse {
/// Gateway configuration data for dVPN mode (WireGuard)
/// This matches what WireguardRegistrationResult expects
pub config: WireguardConfiguration,
/// The bandwidth available to this client,
pub available_bandwidth: i64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RequiresCredentialResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationFailureResponse {
pub error: String,
}
}
pub mod mixnet {
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
// client
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpMixnetRegistrationRequestMessage {
pub content: LpMixnetRegistrationRequestContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpMixnetRegistrationRequestContent {
/// Client's ed25519 public key (identity)
///
/// Used to derive DestinationAddressBytes for ActiveClientsStore lookup.
pub client_ed25519_pubkey: ed25519::PublicKey,
}
// gateway
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpMixnetRegistrationResponseMessage {
pub content: LpMixnetRegistrationResponseMessageContent,
}
impl LpMixnetRegistrationResponseMessage {
pub fn error(error: impl Into<String>) -> Self {
LpMixnetRegistrationResponseMessage {
content: LpMixnetRegistrationResponseMessageContent::RegistrationFailure(
RegistrationFailureResponse {
error: error.into(),
},
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LpMixnetRegistrationResponseMessageContent {
CompletedRegistration(CompletedRegistrationResponse),
RegistrationFailure(RegistrationFailureResponse),
}
/// Gateway data for mixnet mode registration
///
/// Contains the gateway's identity and sphinx key needed for the client
/// to construct its full nym Recipient address.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LpMixnetGatewayData {
/// Gateway's ed25519 identity public key
///
/// Forms part of the client's nym Recipient address.
pub gateway_identity: ed25519::PublicKey,
// TODO: what we really need in here is the address of internal IPR
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletedRegistrationResponse {
/// Gateway data for mixnet mode
///
/// Contains gateway identity and sphinx key needed for nym address construction.
pub config: LpMixnetGatewayData,
/// The bandwidth available to this client,
pub available_bandwidth: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationFailureResponse {
pub error: String,
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::ed25519;
use nym_test_utils::helpers::deterministic_rng;
use std::net::Ipv4Addr;
use std::net::{Ipv4Addr, Ipv6Addr};
// ==================== Helper Functions ====================
fn create_test_gateway_data() -> WireguardConfiguration {
use std::net::Ipv6Addr;
WireguardConfiguration {
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(
nym_sphinx::PublicKey::from([1u8; 32]),
@@ -231,37 +468,60 @@ mod tests {
// ==================== LpRegistrationResponse Tests ====================
#[test]
fn test_lp_registration_response_success() {
let gateway_data = create_test_gateway_data();
let allocated_bandwidth = 1_000_000_000;
let response = 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);
let returned_gw_data = response
.gateway_data
.expect("Gateway data should be present in success response");
assert_eq!(returned_gw_data.public_key, gateway_data.public_key);
assert_eq!(returned_gw_data.private_ipv4, gateway_data.private_ipv4);
assert_eq!(returned_gw_data.private_ipv6, gateway_data.private_ipv6);
assert_eq!(returned_gw_data.endpoint, gateway_data.endpoint);
}
#[test]
fn test_lp_registration_response_error() {
let error_msg = String::from("Insufficient bandwidth");
let response = LpRegistrationResponse::error(error_msg.clone());
let response_mixnet =
LpRegistrationResponse::error(error_msg.clone(), RegistrationMode::Mixnet);
let response_dvpn =
LpRegistrationResponse::error(error_msg.clone(), RegistrationMode::Dvpn);
assert!(!response.success);
assert_eq!(response.error, Some(error_msg));
assert!(response.gateway_data.is_none());
assert_eq!(response.allocated_bandwidth, 0);
assert!(response_mixnet.status.is_failed());
assert!(response_dvpn.status.is_failed());
// check mixnet
let LpRegistrationResponseData::Mixnet { data } = response_mixnet.response_data else {
panic!("unexpected response")
};
let LpMixnetRegistrationResponseMessageContent::RegistrationFailure(failure) = data.content
else {
panic!("unexpected response")
};
assert_eq!(failure.error, error_msg);
// check dvpn
let LpRegistrationResponseData::Dvpn { data } = response_dvpn.response_data else {
panic!("unexpected response")
};
let LpDvpnRegistrationResponseMessageContent::RegistrationFailure(failure) = data.content
else {
panic!("unexpected response")
};
assert_eq!(failure.error, error_msg);
}
#[test]
fn test_lp_registration_response_success_dvpn() {
let cfg = create_test_gateway_data();
let allocated_bandwidth = 500_000_000;
let response = LpRegistrationResponse::success_dvpn(cfg, allocated_bandwidth);
assert!(response.status.is_successful());
let LpRegistrationResponseData::Dvpn { data } = response.response_data else {
panic!("unexpected response")
};
let LpDvpnRegistrationResponseMessageContent::CompletedRegistration(complete) =
data.content
else {
panic!("unexpected response")
};
assert_eq!(complete.config, cfg);
assert_eq!(complete.available_bandwidth, allocated_bandwidth);
}
#[test]
@@ -274,17 +534,20 @@ mod tests {
};
let allocated_bandwidth = 500_000_000;
let response = LpRegistrationResponse::success_mixnet(allocated_bandwidth, lp_gateway_data);
let response =
LpRegistrationResponse::success_mixnet(lp_gateway_data.clone(), allocated_bandwidth);
assert!(response.status.is_successful());
assert!(response.success);
assert!(response.error.is_none());
assert!(response.gateway_data.is_none());
assert!(response.lp_gateway_data.is_some());
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
let LpRegistrationResponseData::Mixnet { data } = response.response_data else {
panic!("unexpected response")
};
let gw_data = response
.lp_gateway_data
.expect("LpGatewayData should be present");
assert_eq!(gw_data.gateway_identity, *valid_key.public_key());
let LpMixnetRegistrationResponseMessageContent::CompletedRegistration(complete) =
data.content
else {
panic!("unexpected response")
};
assert_eq!(complete.config, lp_gateway_data);
assert_eq!(complete.available_bandwidth, allocated_bandwidth);
}
}
+1
View File
@@ -23,6 +23,7 @@ serde = { workspace = true, features = ["derive"] } # for config serialization/d
tap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
tokio-util = { workspace = true }
url = { workspace = true }
nym-bandwidth-controller = { workspace = true }
@@ -7,6 +7,7 @@ use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION};
use crate::config;
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use futures::SinkExt;
use log::*;
use nym_client_core::client::inbound_messages::{InputMessage, InputMessageSender};
use nym_service_providers_common::interface::{ProviderInterfaceVersion, RequestVersion};
@@ -3,11 +3,12 @@
use crate::proxy_runner::MixProxySender;
use bytes::Bytes;
use futures::SinkExt;
use log::{debug, error};
use nym_socks5_requests::{ConnectionId, SocketData};
use std::io;
pub(crate) struct OrderedMessageSender<F, S> {
pub(crate) struct OrderedMessageSender<F, S: Send + 'static> {
connection_id: ConnectionId,
// addresses are provided for better logging
local_destination_address: String,
@@ -18,7 +19,7 @@ pub(crate) struct OrderedMessageSender<F, S> {
mix_message_adapter: F,
}
impl<F, S> OrderedMessageSender<F, S>
impl<F, S: Send + 'static> OrderedMessageSender<F, S>
where
F: Fn(SocketData) -> S,
{
@@ -55,7 +56,7 @@ where
(self.mix_message_adapter)(data)
}
async fn send_message(&self, message: S) {
async fn send_message(&mut self, message: S) {
if self.mixnet_sender.send(message).await.is_err() {
panic!("BatchRealMessageReceiver has stopped receiving!")
}
@@ -74,7 +74,7 @@ async fn wait_for_lane(
}
}
pub(super) async fn run_inbound<F, S>(
pub(super) async fn run_inbound<F, S: Send>(
mut reader: OwnedReadHalf,
mut message_sender: OrderedMessageSender<F, S>,
connection_id: ConnectionId,
@@ -9,6 +9,7 @@ use nym_task::ShutdownTracker;
use std::fmt::Debug;
use std::{sync::Arc, time::Duration};
use tokio::{net::TcpStream, sync::Notify};
use tokio_util::sync::PollSender;
mod inbound;
mod outbound;
@@ -35,7 +36,7 @@ impl From<(Vec<u8>, bool)> for ProxyMessage {
}
}
pub type MixProxySender<S> = tokio::sync::mpsc::Sender<S>;
pub type MixProxySender<S> = PollSender<S>;
pub type MixProxyReader<S> = tokio::sync::mpsc::Receiver<S>;
// TODO: when we finally get to implementing graceful shutdown,
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
use aes_gcm::aead::{Aead, Nonce};
use aes_gcm::{AeadCore, AeadInPlace, KeyInit};
use rand::{thread_rng, CryptoRng, Fill, RngCore};
+1
View File
@@ -15,6 +15,7 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "sync"] }
tokio-util = { workspace = true, features = ["rt"] }
tracing = { workspace = true }
serde = { workspace = true }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
+2 -1
View File
@@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// const LANE_CONSIDERED_CLEAR: usize = 10;
pub type ConnectionId = u64;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransmissionLane {
General,
// we need to treat surb-related requests and responses at higher priority
@@ -26,7 +26,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
fn from(req: &PeerControlRequest) -> Self {
match req {
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RegisterPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::AllocatePeerIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
PeerControlRequest::GetClientBandwidthByKey { .. } => {
@@ -41,6 +41,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
PeerControlRequest::GetVerifierByIp { ip, .. } => {
PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip }
}
PeerControlRequest::ReleaseIpPair { .. } => unreachable!(),
}
}
}
@@ -113,7 +114,7 @@ impl MockPeerControllerV2 {
)
.unwrap();
}
PeerControlRequest::RegisterPeer { response_tx, .. } => {
PeerControlRequest::AllocatePeerIpPair { response_tx, .. } => {
response_tx
.send(
*response
@@ -176,6 +177,15 @@ impl MockPeerControllerV2 {
)
.ok();
}
PeerControlRequest::ReleaseIpPair { response_tx, .. } => {
response_tx
.send(
*response
.downcast()
.expect("registered response has mismatched type"),
)
.ok();
}
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ nym-credential-verification = { workspace = true }
nym-crypto = { workspace = true, features = ["asymmetric"] }
nym-gateway-storage = { workspace = true }
nym-gateway-requests = { workspace = true }
nym-ip-packet-requests = { workspace = true }
nym-authenticator-requests = { workspace = true }
nym-metrics = { workspace = true }
nym-network-defaults = { workspace = true }
nym-task = { workspace = true }
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ip_pool::IpPair;
impl From<IpPair> for nym_authenticator_requests::v6::registration::IpPair {
fn from(ip_pair: IpPair) -> Self {
nym_authenticator_requests::v6::registration::IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
impl From<IpPair> for nym_authenticator_requests::v5::registration::IpPair {
fn from(ip_pair: IpPair) -> Self {
nym_authenticator_requests::v5::registration::IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
impl From<IpPair> for nym_authenticator_requests::v4::registration::IpPair {
fn from(ip_pair: IpPair) -> Self {
nym_authenticator_requests::v4::registration::IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
//
impl From<nym_authenticator_requests::v6::registration::IpPair> for IpPair {
fn from(ip_pair: nym_authenticator_requests::v6::registration::IpPair) -> Self {
IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
impl From<nym_authenticator_requests::v5::registration::IpPair> for IpPair {
fn from(ip_pair: nym_authenticator_requests::v5::registration::IpPair) -> Self {
IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
impl From<nym_authenticator_requests::v4::registration::IpPair> for IpPair {
fn from(ip_pair: nym_authenticator_requests::v4::registration::IpPair) -> Self {
IpPair {
ipv4: ip_pair.ipv4,
ipv6: ip_pair.ipv6,
}
}
}
@@ -1,14 +1,56 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod compat;
use defguard_wireguard_rs::host::Peer;
use ipnetwork::IpNetwork;
use nym_ip_packet_requests::IpPair;
use rand::seq::IteratorRandom;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::time::SystemTime;
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use tokio::time::Instant;
use tracing::{trace, warn};
// helper to convert peer's allocation into an `IpPair`
pub fn allocated_ip_pair(peer: &Peer) -> Option<IpPair> {
for allowed_ip in &peer.allowed_ips {
// Extract IPv4 and IPv6 from peer's allowed_ips
if let IpAddr::V4(ipv4) = allowed_ip.address {
// Find corresponding IPv6
if let Some(ipv6_mask) = peer
.allowed_ips
.iter()
.find(|ip| matches!(ip.address, IpAddr::V6(_)))
&& let IpAddr::V6(ipv6) = ipv6_mask.address
{
return Some(IpPair::new(ipv4, ipv6));
}
}
}
None
}
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Hash)]
pub struct IpPair {
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
}
impl IpPair {
pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl Display for IpPair {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "IPv4: {}, IPv6: {}", self.ipv4, self.ipv6)
}
}
/// Represents the state of an IP allocation
#[derive(Debug, Clone, Copy)]
@@ -83,12 +125,8 @@ impl IpPool {
}
tracing::info!(
"Initialized IP pool with {} address pairs from {}/{} and {}/{}",
"Initialized IP pool with {} address pairs from {ipv4_network}/{ipv4_prefix} and {ipv6_network}/{ipv6_prefix}",
allocations.len(),
ipv4_network,
ipv4_prefix,
ipv6_network,
ipv6_prefix
);
Ok(IpPool {
@@ -98,24 +136,28 @@ impl IpPool {
/// Allocate a free IP pair from the pool
///
/// Randomly selects an available IP pair and marks it as allocated.
///
/// # Errors
/// Returns `IpPoolError::NoFreeIp` if no IPs are available
pub async fn allocate(&self) -> Result<IpPair, IpPoolError> {
let mut pool = self.allocations.write().await;
// Find a free IP and allocate it
let assignment_start = Instant::now();
let free_ip = pool
.iter_mut()
.filter(|(_, state)| matches!(state, AllocationState::Free))
.choose(&mut rand::thread_rng())
.ok_or(IpPoolError::NoFreeIp)?;
let taken = assignment_start.elapsed();
trace!("assigning free ip pair took {taken:?}");
if taken > Duration::from_millis(500) {
warn!("assigning free ip pair took {taken:?}");
}
let ip_pair = *free_ip.0;
*free_ip.1 = AllocationState::Allocated(SystemTime::now());
tracing::debug!("Allocated IP pair: {}", ip_pair);
tracing::debug!("Allocated IP pair: {ip_pair}");
Ok(ip_pair)
}
@@ -126,7 +168,7 @@ impl IpPool {
let mut pool = self.allocations.write().await;
if let Some(state) = pool.get_mut(&ip_pair) {
*state = AllocationState::Free;
tracing::debug!("Released IP pair: {}", ip_pair);
tracing::debug!("Released IP pair: {ip_pair}");
}
}
@@ -137,9 +179,9 @@ impl IpPool {
let mut pool = self.allocations.write().await;
if let Some(state) = pool.get_mut(&ip_pair) {
*state = AllocationState::Allocated(SystemTime::now());
tracing::debug!("Marked IP pair as used: {}", ip_pair);
tracing::debug!("Marked IP pair as used: {ip_pair}");
} else {
tracing::warn!("Attempted to mark unknown IP pair as used: {}", ip_pair);
tracing::warn!("Attempted to mark unknown IP pair as used: {ip_pair}");
}
}
+18 -34
View File
@@ -2,9 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
// #![warn(clippy::pedantic)]
// #![warn(clippy::expect_used)]
// #![warn(clippy::unwrap_used)]
use defguard_wireguard_rs::{
WGApi, WireguardInterfaceApi, error::WireguardInterfaceError, host::Peer, key::Key,
@@ -16,9 +13,6 @@ use std::sync::Arc;
use tokio::sync::mpsc::{self, Receiver, Sender};
use tracing::error;
#[cfg(target_os = "linux")]
use nym_ip_packet_requests::IpPair;
#[cfg(target_os = "linux")]
use nym_network_defaults::constants::WG_TUN_BASE_NAME;
@@ -28,6 +22,7 @@ pub mod peer_controller;
pub mod peer_handle;
pub mod peer_storage_manager;
pub use defguard_wireguard_rs::host::Peer as DefguardPeer;
pub use error::Error;
pub use ip_pool::{IpPool, IpPoolError};
pub use nym_wireguard_types::Config as WireguardConfig;
@@ -287,6 +282,22 @@ pub async fn start_wireguard(
peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone()));
}
// Initialize IP pool from configuration
info!("Initializing IP pool for WireGuard peer allocation");
let ip_pool = IpPool::new(
wireguard_data.inner.config().private_ipv4,
wireguard_data.inner.config().private_network_prefix_v4,
wireguard_data.inner.config().private_ipv6,
wireguard_data.inner.config().private_network_prefix_v6,
)?;
// Mark existing peer IPs as used in the pool
for peer in &peers {
if let Some(ip_pair) = crate::ip_pool::allocated_ip_pair(peer) {
ip_pool.mark_used(ip_pair).await;
}
}
wg_api.create_interface()?;
let interface_config = InterfaceConfiguration {
name: ifname.clone(),
@@ -295,7 +306,7 @@ pub async fn start_wireguard(
wireguard_data.inner.config().private_ipv4,
))],
port: wireguard_data.inner.config().announced_tunnel_port,
peers: peers.clone(), // Clone since we need to use peers later to mark IPs as used
peers,
mtu: None,
};
info!(
@@ -349,33 +360,6 @@ pub async fn start_wireguard(
let host = wg_api.read_interface_data()?;
// Initialize IP pool from configuration
info!("Initializing IP pool for WireGuard peer allocation");
let ip_pool = IpPool::new(
wireguard_data.inner.config().private_ipv4,
wireguard_data.inner.config().private_network_prefix_v4,
wireguard_data.inner.config().private_ipv6,
wireguard_data.inner.config().private_network_prefix_v6,
)?;
// Mark existing peer IPs as used in the pool
for peer in &peers {
for allowed_ip in &peer.allowed_ips {
// Extract IPv4 and IPv6 from peer's allowed_ips
if let IpAddr::V4(ipv4) = allowed_ip.address {
// Find corresponding IPv6
if let Some(ipv6_mask) = peer
.allowed_ips
.iter()
.find(|ip| matches!(ip.address, IpAddr::V6(_)))
&& let IpAddr::V6(ipv6) = ipv6_mask.address
{
ip_pool.mark_used(IpPair::new(ipv4, ipv6)).await;
}
}
}
}
let wg_api = std::sync::Arc::new(wg_api);
let mut controller = PeerController::new(
ecash_manager,
+16 -16
View File
@@ -19,6 +19,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::mpsc::Receiver;
use crate::ip_pool::IpPair;
pub use defguard_wireguard_rs::key::Key;
pub fn mock_peer_controller(
@@ -70,7 +71,8 @@ impl From<&Key> for KeyWrapper {
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq)]
pub enum PeerControlRequestType {
AddPeer { public_key: KeyWrapper },
RegisterPeer { public_key: KeyWrapper },
AllocatePeerIpPair {},
ReleaseIpPair { ip_pair: IpPair },
RemovePeer { key: KeyWrapper },
QueryPeer { key: KeyWrapper },
GetClientBandwidthByKey { key: KeyWrapper },
@@ -83,7 +85,8 @@ impl PeerControlRequestType {
pub fn peer_key(&self) -> Option<KeyWrapper> {
match self {
PeerControlRequestType::AddPeer { public_key } => Some(public_key.clone()),
PeerControlRequestType::RegisterPeer { public_key } => Some(public_key.clone()),
PeerControlRequestType::AllocatePeerIpPair {} => None,
PeerControlRequestType::ReleaseIpPair { .. } => None,
PeerControlRequestType::RemovePeer { key } => Some(key.clone()),
PeerControlRequestType::QueryPeer { key } => Some(key.clone()),
PeerControlRequestType::GetClientBandwidthByKey { key } => Some(key.clone()),
@@ -104,11 +107,12 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
PeerControlRequest::AddPeer { peer, .. } => PeerControlRequestType::AddPeer {
public_key: (&peer.public_key).into(),
},
PeerControlRequest::RegisterPeer {
registration_data, ..
} => PeerControlRequestType::RegisterPeer {
public_key: (&registration_data.public_key).into(),
},
PeerControlRequest::AllocatePeerIpPair { .. } => {
PeerControlRequestType::AllocatePeerIpPair {}
}
PeerControlRequest::ReleaseIpPair { ip_pair, .. } => {
PeerControlRequestType::ReleaseIpPair { ip_pair: *ip_pair }
}
PeerControlRequest::RemovePeer { key, .. } => {
PeerControlRequestType::RemovePeer { key: key.into() }
}
@@ -166,9 +170,6 @@ pub struct MockPeerControllerState {
#[derive(Clone, Default)]
pub struct PeerState {
/// Has IpPair been allocated to the peer?
pub register_success: bool,
// in the future maybe we could extend it with `ClientBandwidth` information
/// Has the client handle been spawned
pub add_success: bool,
@@ -265,14 +266,13 @@ impl MockPeerController {
}
response_tx.send_downcasted(response.content)
}
PeerControlRequest::RegisterPeer { response_tx, .. } => {
let key = typ.peer_key_unchecked();
let peer = peers_guard.peers.entry(key).or_default();
if response.success {
peer.register_success = true;
}
PeerControlRequest::AllocatePeerIpPair { response_tx, .. } => {
response_tx.send_downcasted(response.content)
}
PeerControlRequest::ReleaseIpPair {
response_tx,
ip_pair: _,
} => response_tx.send_downcasted(response.content),
PeerControlRequest::RemovePeer { response_tx, .. } => {
let key = typ.peer_key_unchecked();
if response.success {
+101 -58
View File
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ip_pool::allocated_ip_pair;
use crate::{
IpPool,
error::{Error, Result},
@@ -27,14 +28,14 @@ use nym_wireguard_types::{
};
use std::{collections::HashMap, sync::Arc};
use std::{
net::{IpAddr, SocketAddr},
net::IpAddr,
time::{Duration, SystemTime},
};
use tokio::sync::{RwLock, mpsc};
use tokio_stream::{StreamExt, wrappers::IntervalStream};
use tracing::{debug, error, info, trace};
pub use nym_ip_packet_requests::IpPair;
pub use crate::ip_pool::IpPair;
#[cfg(feature = "mock")]
pub mod mock;
@@ -43,35 +44,30 @@ pub mod mock;
#[derive(Debug, Clone)]
pub struct PeerRegistrationData {
pub public_key: Key,
pub preshared_key: Option<Key>,
pub endpoint: Option<SocketAddr>,
pub persistent_keepalive_interval: Option<u16>,
pub preshared_key: Key,
// pub endpoint: Option<SocketAddr>,
// pub persistent_keepalive_interval: Option<u16>,
}
impl PeerRegistrationData {
pub fn new(public_key: Key) -> Self {
pub fn new(public_key: Key, psk: Key) -> Self {
Self {
public_key,
preshared_key: None,
endpoint: None,
persistent_keepalive_interval: None,
preshared_key: psk,
// endpoint: None,
// persistent_keepalive_interval: None,
}
}
pub fn with_preshared_key(mut self, key: Key) -> Self {
self.preshared_key = Some(key);
self
}
pub fn with_endpoint(mut self, endpoint: SocketAddr) -> Self {
self.endpoint = Some(endpoint);
self
}
pub fn with_keepalive(mut self, interval: u16) -> Self {
self.persistent_keepalive_interval = Some(interval);
self
}
//
// pub fn with_endpoint(mut self, endpoint: SocketAddr) -> Self {
// self.endpoint = Some(endpoint);
// self
// }
//
// pub fn with_keepalive(mut self, interval: u16) -> Self {
// self.persistent_keepalive_interval = Some(interval);
// self
// }
}
pub enum PeerControlRequest {
@@ -80,10 +76,14 @@ pub enum PeerControlRequest {
peer: Peer,
response_tx: oneshot::Sender<AddPeerControlResponse>,
},
/// Register a new peer and allocate IPs from the pool
RegisterPeer {
registration_data: PeerRegistrationData,
response_tx: oneshot::Sender<RegisterPeerControlResponse>,
/// Attempt to allocate an IP pair from the pool
AllocatePeerIpPair {
response_tx: oneshot::Sender<AllocatePeerControlResponse>,
},
/// Attempt to return an IP pair back to the pool
ReleaseIpPair {
response_tx: oneshot::Sender<ReleaseIpPairControlResponse>,
ip_pair: IpPair,
},
RemovePeer {
key: Key,
@@ -114,7 +114,8 @@ pub enum PeerControlRequest {
}
pub type AddPeerControlResponse = Result<()>;
pub type RegisterPeerControlResponse = Result<IpPair>;
pub type AllocatePeerControlResponse = Result<IpPair>;
pub type ReleaseIpPairControlResponse = Result<()>;
pub type RemovePeerControlResponse = Result<()>;
pub type QueryPeerControlResponse = Result<Option<Peer>>;
pub type GetClientBandwidthControlResponse = Result<ClientBandwidth>;
@@ -211,6 +212,13 @@ impl PeerController {
.remove_wireguard_peer(&key.to_string())
.await?;
self.bw_storage_managers.remove(key);
if let Ok(Some(peer)) = self.handle_query_peer_by_key(key).await
&& let Some(ip_pair) = allocated_ip_pair(&peer)
{
self.ip_pool.release(ip_pair).await
}
let ret = self.wg_api.remove_peer(key);
if ret.is_err() {
nym_metrics::inc!("wg_peer_removal_failed");
@@ -294,10 +302,7 @@ impl PeerController {
///
/// This only allocates IPs - the caller must handle database storage and
/// then call AddPeer with a complete Peer struct.
async fn handle_register_request(
&mut self,
_registration_data: PeerRegistrationData,
) -> Result<IpPair> {
async fn handle_ip_allocation_request(&mut self) -> Result<IpPair> {
nym_metrics::inc!("wg_ip_allocation_attempts");
// Allocate IP pair from pool
@@ -308,11 +313,16 @@ impl PeerController {
.map_err(|e| Error::IpPool(e.to_string()))?;
nym_metrics::inc!("wg_ip_allocation_success");
tracing::debug!("Allocated IP pair: {}", ip_pair);
tracing::debug!("Allocated IP pair: {ip_pair}");
Ok(ip_pair)
}
/// Return IP pair back to the pool
async fn handle_ip_release(&mut self, ip_pair: IpPair) {
self.ip_pool.release(ip_pair).await
}
async fn ip_to_key(&self, ip: IpAddr) -> Result<Option<Key>> {
Ok(self
.bw_storage_managers
@@ -477,6 +487,62 @@ impl PeerController {
);
}
async fn handle_peer_control_request(&mut self, msg: PeerControlRequest) {
match msg {
PeerControlRequest::AddPeer { peer, response_tx } => {
response_tx.send(self.handle_add_request(&peer).await).ok();
}
PeerControlRequest::AllocatePeerIpPair { response_tx } => {
response_tx
.send(self.handle_ip_allocation_request().await)
.ok();
}
PeerControlRequest::ReleaseIpPair {
response_tx,
ip_pair,
} => {
self.handle_ip_release(ip_pair).await;
response_tx.send(Ok(())).ok();
}
PeerControlRequest::RemovePeer { key, response_tx } => {
response_tx.send(self.remove_peer(&key).await).ok();
}
PeerControlRequest::QueryPeer { key, response_tx } => {
response_tx
.send(self.handle_query_peer_by_key(&key).await)
.ok();
}
PeerControlRequest::GetClientBandwidthByKey { key, response_tx } => {
response_tx
.send(self.handle_get_client_bandwidth_by_key(&key).await)
.ok();
}
PeerControlRequest::GetClientBandwidthByIp { ip, response_tx } => {
response_tx
.send(self.handle_get_client_bandwidth_by_ip(ip).await)
.ok();
}
PeerControlRequest::GetVerifierByKey {
key,
credential,
response_tx,
} => {
response_tx
.send(self.handle_query_verifier_by_key(&key, *credential).await)
.ok();
}
PeerControlRequest::GetVerifierByIp {
ip,
credential,
response_tx,
} => {
response_tx
.send(self.handle_query_verifier_by_ip(ip, *credential).await)
.ok();
}
}
}
pub async fn run(&mut self) {
info!("started wireguard peer controller");
loop {
@@ -504,30 +570,7 @@ impl PeerController {
}
msg = self.request_rx.recv() => {
match msg {
Some(PeerControlRequest::AddPeer { peer, response_tx }) => {
response_tx.send(self.handle_add_request(&peer).await).ok();
}
Some(PeerControlRequest::RegisterPeer { registration_data, response_tx }) => {
response_tx.send(self.handle_register_request(registration_data).await).ok();
}
Some(PeerControlRequest::RemovePeer { key, response_tx }) => {
response_tx.send(self.remove_peer(&key).await).ok();
}
Some(PeerControlRequest::QueryPeer { key, response_tx }) => {
response_tx.send(self.handle_query_peer_by_key(&key).await).ok();
}
Some(PeerControlRequest::GetClientBandwidthByKey { key, response_tx }) => {
response_tx.send(self.handle_get_client_bandwidth_by_key(&key).await).ok();
}
Some(PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }) => {
response_tx.send(self.handle_get_client_bandwidth_by_ip(ip).await).ok();
}
Some(PeerControlRequest::GetVerifierByKey { key, credential, response_tx }) => {
response_tx.send(self.handle_query_verifier_by_key(&key, *credential).await).ok();
}
Some(PeerControlRequest::GetVerifierByIp { ip, credential, response_tx }) => {
response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok();
}
Some(request) => self.handle_peer_control_request(request).await,
None => {
trace!("PeerController [main loop]: stopping since channel closed");
break;
@@ -1 +1 @@
Thursday, January 15th 2026, 09:44:55 UTC
Wednesday, January 28th 2026, 09:28:07 UTC
@@ -76,6 +76,10 @@ Options:
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
--mnemonic <MNEMONIC>
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
--open-proxy <OPEN_PROXY>
@@ -25,12 +25,12 @@
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 |
| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, USA, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Exit nodes not allowed on VPS offering, see their [Service Specific Terms](https://us.ovhcloud.com/legal/service-specific-terms/). Not all locations always available | 09/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, USA, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 |
| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 |
@@ -49,6 +49,136 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2026.2-oscypek`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.2-oscypek)
- [`nym-node`](nodes/nym-node.mdx) version `1.24.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2026-01-27T14:54:15.579821601Z
Build Version: 1.24.0
Commit SHA: 83bf9dc7cc2b01f65cab671733f2bf6c3abd471d
Commit Date: 2026-01-27T15:46:52.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
<Callout type="warning" emoji="⚠️">
**This release comes with breaking changes - please follow the [steps below](#oscypek-special-update) before upgrading!**
Secondly, the outcome of [NIP-7: Nym Exit Policy Update - Opening Ports for Steam, Discord & SSH](https://governator.nym.com/proposal/prop-281e9ec1-8e10-4e97-848c-311823e83f61), is added to the [Network tunnel manager (NTM)](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/network-tunnel-manager.sh) and operators are required to rerun the tool on their servers as [documented here](update-nym-exit-policy).
</ Callout>
#### `oscypek` special update
This release brings changes which would lead into a *foreign constraint bug* if operators just switched binaries and restarted the node. To prevent it we need to do a little `sqlite` tweak on the node database.
To simplify this, we made **a build in command, which operators must run after getting the new binary, but beofre restarting the node.**
These are the steps to follow:
<Steps>
###### 1. Get `oscypek` binary
- SSH to your machine as root
- Navigate to the destination where you have `nym-node` binary
- Get the latest binary and provide it with permissions to run
```sh
curl -L "https://github.com/nymtech/nym/releases/download/nym-binaries-v2026.2-oscypek/nym-node" -o nym-node && \
chmod +x nym-node
```
###### 2. Run `debug` command
```sh
./nym-node debug reset-providers-gateway-dbs --id <ID>
```
###### 3. Restart your node
- Restart the `nym-node.service`
```sh
systemctl restart nym-node
```
- Additionaly look for starus or serivice journal
```sh
service nym-node status
# or
journalctl -u nym-node -f --all
```
</ Steps>
#### Update Nym exit policy
As a result of [NIP-7: Nym Exit Policy Update - Opening Ports for Steam, Discord & SSH](https://governator.nym.com/proposal/prop-281e9ec1-8e10-4e97-848c-311823e83f61), we updated [`network-tunnel-manager.sh` (NTM)](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/network-tunnel-manager.sh). Every operator is required to download and re-run the current version of NTM on the servers hosting Nym nodes.
These are the steps for the exit policy update, using NTM.
<Steps>
###### 1. Get the new NTM
- Download the updated NTM and make executable
```sh
curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o ./network-tunnel-manager.sh && \
chmod +x network-tunnel-manager.sh
```
###### 2. Update exit policy
- To be sure that your routing is clean, run this command:
```sh
./network-tunnel-manager.sh complete_networking_configuration
```
</ Steps>
### Features
- [Deriving `Serialize` for `GatewayData`](https://github.com/nymtech/nym/pull/6314): Deriving `Serialize` for gateway data, that will be used by the diagnostic tool in the `vpn-client` repo
- [DNS static table pre-resolve](https://github.com/nymtech/nym/pull/6297): This PR adds pre-resolve stage that returns addres if we have used static table previously. This ensures that we don't continually suffer the penalty of a lookup timeout, while also allowing for the possibility of going back to the default internal secure resolver if one or more nameservers becomes usable again at a future time.
- [Add `Copy+Clone` to `nym_api_provider::Config`](https://github.com/nymtech/nym/pull/6296): Add `Copy+Clone` to `nym_client_core::client::topology_control::nym_api_provider::Config`
- [LP Registration + Telescoping + Gateway Probe Localnet Mode](https://github.com/nymtech/nym/pull/6286): Combines LP registration protocol implementation, adds telescoping/nested sessions support, adds localnet mode for `gateway-probe` testing, integrates KKT & PSQ cryptographic primitives
- [Minor DNS improvements](https://github.com/nymtech/nym/pull/6283): Increase timeouts back to 10 seconds for overall lookup and 5 seconds per query, gnore unreliable test, remove JIT resolution in http client as it is at best not useful, and at worst increasing timeout
- [HTTP client without default features](https://github.com/nymtech/nym/pull/6281): Fix compile issue caused when using the http client using `default-features=false`
- [DNS: reduce number of attempts](https://github.com/nymtech/nym/pull/6278): Reduce number of retry attempts performed by hickory to `0`, define `new_resolver` as infallible, use `ResolverOpts` to build builder
- [Fallback gateway listener and remove legacy key support](https://github.com/nymtech/nym/pull/6249)
- [Fix assertion](https://github.com/nymtech/nym/pull/6238)
- [Initial changes to support extra configurable parameters and to print…](https://github.com/nymtech/nym/pull/6237): This branch adds support for the additional configurable parameters introduced in `nicolas/sdk-param-support-debug` in the nym vpn client branch and also debugging messages to verify that it works
- [Data Observatory](https://github.com/nymtech/nym/pull/6172): This PR adds the Data Observatory that is:
- chain scraper
- parses Cosmos SDK messages
- parses Cosmwasm messages
- stores data in pgsql
### Bugfix
- [Downgrade gateway protocol to clients proposed version](https://github.com/nymtech/nym/pull/6377)
- [Ack fix](https://github.com/nymtech/nym/pull/6364)
- [Sqlite transaction escalation was causing errors ](https://github.com/nymtech/nym/pull/6299): Getting tickets from credential storage requires a transaction doing a read and then a write. Running registration in parallel was causing sqlite to return errors, because it can't escalate two transactions, only one.
- [Use proper mixing delay instead of poisson delay in cover traffic](https://github.com/nymtech/nym/pull/6269): Currently the secondary cover traffic loop uses its Poisson process delays instead of a proper mixing delay, this PR fixes that
### Refactors & Maintenance
- [Update nix to `v0.30.1`](https://github.com/nymtech/nym/pull/6316)
- [Rremove repetitive words in comment](https://github.com/nymtech/nym/pull/6313)
- [Clippy fixes and use fixed rust version from `REQUIRED_RUSTC_VERSION`](https://github.com/nymtech/nym/pull/6295)
## `v2026.1-niolo`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.1-niolo)
@@ -120,7 +250,7 @@ Please, let us know how that worked for you.
- [`gateway-probe` fixes for run-local](https://github.com/nymtech/nym/pull/6212)
- [Upgrade mode: VPN adjustments](https://github.com/nymtech/nym/pull/6189): This PR further builds up on [\#6174](https://github.com/nymtech/nym/pull/6174) to include changes required by the VPN-client to fully support the upgrade mode, what is relevant here is that this PR modifies the credential storage to allow it to storage an opaque `emergency credential` that lets it be shared between sessions (if it is still valid) ]
- [Upgrade mode: VPN adjustments](https://github.com/nymtech/nym/pull/6189): This PR further builds up on [\#6174](https://github.com/nymtech/nym/pull/6174) to include changes required by the VPN-client to fully support the upgrade mode, what is relevant here is that this PR modifies the credential storage to allow it to storage an opaque `emergency credential` that lets it be shared between sessions (if it is still valid)
- [Add weighted scoring to NS API](https://github.com/nymtech/nym/pull/6144)
@@ -21,11 +21,11 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-12-02T16:21:03.251191389Z
Build Version: 1.23.0
Commit SHA: 46fe1bc8191f42aa27f34743c96e9e9f26453d87
Commit Date: 2025-12-02T15:29:30.000000000Z
Commit Branch: release/2025.22-niolo
Build Timestamp: 2026-01-27T14:54:15.579821601Z
Build Version: 1.24.0
Commit SHA: 83bf9dc7cc2b01f65cab671733f2bf6c3abd471d
Commit Date: 2026-01-27T15:46:52.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
cargo Profile: release
+9 -2
View File
@@ -1,7 +1,10 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError;
use crate::node::internal_service_providers::authenticator::error::AuthenticatorError;
use crate::node::wireguard::GatewayWireguardError;
use nym_credential_verification::upgrade_mode::UpgradeModeEnableError;
use nym_gateway_stats_storage::error::StatsStorageError;
use nym_gateway_storage::error::GatewayStorageError;
use nym_ip_packet_router::error::IpPacketRouterError;
@@ -12,8 +15,6 @@ use nym_validator_client::ValidatorClientError;
use std::net::IpAddr;
use thiserror::Error;
pub use crate::node::client_handling::websocket::connection_handler::authenticated::RequestHandlingError;
#[derive(Debug, Error)]
pub enum GatewayError {
#[error("the configured version of the gateway ({config_version}) is incompatible with the binary version ({binary_version})")]
@@ -155,6 +156,12 @@ pub enum GatewayError {
#[error("Invalid SystemTime: {0}")]
InvalidSystemTime(#[from] std::time::SystemTimeError),
#[error(transparent)]
UpgradeModeEnable(#[from] UpgradeModeEnableError),
#[error(transparent)]
WireguardFailure(#[from] GatewayWireguardError),
}
impl From<ClientCoreError> for GatewayError {
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node::wireguard::GatewayWireguardError;
use ipnetwork::IpNetworkError;
use nym_client_core::error::ClientCoreError;
use nym_credential_verification::upgrade_mode::UpgradeModeEnableError;
@@ -82,9 +83,6 @@ pub enum AuthenticatorError {
#[error("internal data corruption: {0}")]
InternalDataCorruption(String),
#[error("peers can't be interacted with anymore")]
PeerInteractionStopped,
#[error("unknown version number")]
UnknownVersion,
@@ -108,6 +106,9 @@ pub enum AuthenticatorError {
#[error(transparent)]
UpgradeModeEnable(#[from] UpgradeModeEnableError),
#[error(transparent)]
WireguardFailure(#[from] GatewayWireguardError),
}
impl AuthenticatorError {
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::node::internal_service_providers::authenticator::{
config::Config, error::AuthenticatorError, peer_manager::PeerManager,
seen_credential_cache::SeenCredentialCache,
config::Config, error::AuthenticatorError, seen_credential_cache::SeenCredentialCache,
};
use crate::node::wireguard::PeerManager;
use defguard_wireguard_rs::net::IpAddrMask;
use defguard_wireguard_rs::{host::Peer, key::Key};
use futures::StreamExt;
@@ -35,7 +35,6 @@ use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::ShutdownToken;
use nym_wireguard::WireguardGatewayData;
use nym_wireguard_types::PeerPublicKey;
use rand::{prelude::IteratorRandom, thread_rng};
use std::cmp::max;
use std::{
net::IpAddr,
@@ -53,15 +52,15 @@ const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); //
const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024;
pub(crate) struct RegisteredAndFree {
registration_in_progres: PendingRegistrations,
free_private_network_ips: PrivateIPs,
registration_in_progress: PendingRegistrations,
taken_private_network_ips: PrivateIPs,
}
impl RegisteredAndFree {
pub(crate) fn new(free_private_network_ips: PrivateIPs) -> Self {
pub(crate) fn new() -> Self {
RegisteredAndFree {
registration_in_progres: Default::default(),
free_private_network_ips,
registration_in_progress: Default::default(),
taken_private_network_ips: Default::default(),
}
}
}
@@ -90,7 +89,6 @@ pub(crate) struct MixnetListener {
impl MixnetListener {
pub fn new(
config: Config,
free_private_network_ips: PrivateIPs,
wireguard_gateway_data: WireguardGatewayData,
mixnet_client: nym_sdk::mixnet::MixnetClient,
upgrade_mode: UpgradeModeDetails,
@@ -101,7 +99,7 @@ impl MixnetListener {
MixnetListener {
config,
mixnet_client,
registered_and_free: RwLock::new(RegisteredAndFree::new(free_private_network_ips)),
registered_and_free: RwLock::new(RegisteredAndFree::new()),
peer_manager: PeerManager::new(wireguard_gateway_data),
upgrade_mode,
ecash_verifier,
@@ -134,39 +132,35 @@ impl MixnetListener {
async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> {
let mut registered_and_free = self.registered_and_free.write().await;
let registered_values: Vec<_> = registered_and_free
.registration_in_progres
.registration_in_progress
.values()
.cloned()
.collect();
for reg in registered_values {
let ip = registered_and_free
.free_private_network_ips
let timestamp = registered_and_free
.taken_private_network_ips
.get_mut(&reg.gateway_data.private_ips)
.ok_or(AuthenticatorError::InternalDataCorruption(format!(
"IPs {} should be present",
reg.gateway_data.private_ips
)))?;
let Some(timestamp) = ip else {
registered_and_free
.registration_in_progres
.remove(&reg.gateway_data.pub_key());
tracing::debug!(
"Removed stale registration of {}",
reg.gateway_data.pub_key()
);
continue;
};
let duration = SystemTime::now().duration_since(*timestamp).map_err(|_| {
AuthenticatorError::InternalDataCorruption(
"set timestamp shouldn't have been set in the future".to_string(),
)
})?;
if duration > DEFAULT_REGISTRATION_TIMEOUT_CHECK {
*ip = None;
registered_and_free
.registration_in_progres
.registration_in_progress
.remove(&reg.gateway_data.pub_key());
registered_and_free
.taken_private_network_ips
.remove(&reg.gateway_data.private_ips);
self.peer_manager
.release_ip_pair(reg.gateway_data.private_ips.into())
.await?;
tracing::debug!(
"Removed stale registration of {}",
reg.gateway_data.pub_key()
@@ -187,7 +181,7 @@ impl MixnetListener {
let nonce: u64 = fastrand::u64(..);
let mut registered_and_free = self.registered_and_free.write().await;
if let Some(registration_data) = registered_and_free
.registration_in_progres
.registration_in_progress
.get(&remote_public)
{
let gateway_data = registration_data.gateway_data.clone();
@@ -383,19 +377,18 @@ impl MixnetListener {
return Ok((bytes, reply_to));
}
let private_ip_ref = registered_and_free
.free_private_network_ips
.iter_mut()
.filter(|r| r.1.is_none())
.choose(&mut thread_rng())
.ok_or(AuthenticatorError::NoFreeIp)?;
let private_ips = *private_ip_ref.0;
// mark it as used, even though it's not final
*private_ip_ref.1 = Some(SystemTime::now());
let ip_allocation = self.peer_manager.allocate_peer_ip_pair().await?;
self.registered_and_free
.write()
.await
.taken_private_network_ips
.insert(ip_allocation.into(), SystemTime::now());
let gateway_data = GatewayClient::new(
self.keypair().private_key(),
remote_public.inner(),
*private_ip_ref.0,
ip_allocation.into(),
nonce,
);
let registration_data = latest::registration::RegistrationData {
@@ -404,7 +397,7 @@ impl MixnetListener {
wg_port: self.config.authenticator.tunnel_announced_port,
};
registered_and_free
.registration_in_progres
.registration_in_progress
.insert(remote_public, registration_data.clone());
let bytes = match AuthenticatorVersion::from(protocol) {
AuthenticatorVersion::V1 => {
@@ -414,7 +407,7 @@ impl MixnetListener {
gateway_data: v1::registration::GatewayClient::new(
self.keypair().private_key(),
remote_public.inner(),
private_ips.ipv4.into(),
ip_allocation.ipv4.into(),
nonce,
),
wg_port: registration_data.wg_port,
@@ -432,7 +425,7 @@ impl MixnetListener {
gateway_data: v2::registration::GatewayClient::new(
self.keypair().private_key(),
remote_public.inner(),
private_ips.ipv4.into(),
ip_allocation.ipv4.into(),
nonce,
),
wg_port: registration_data.wg_port,
@@ -450,7 +443,7 @@ impl MixnetListener {
gateway_data: v3::registration::GatewayClient::new(
self.keypair().private_key(),
remote_public.inner(),
private_ips.ipv4.into(),
ip_allocation.ipv4.into(),
nonce,
),
wg_port: registration_data.wg_port,
@@ -541,7 +534,7 @@ impl MixnetListener {
) -> AuthenticatorHandleResult {
let mut registered_and_free = self.registered_and_free.write().await;
let registration_data = registered_and_free
.registration_in_progres
.registration_in_progress
.get(&final_message.gateway_client_pub_key())
.ok_or(AuthenticatorError::RegistrationNotInProgress)?
.clone();
@@ -592,11 +585,11 @@ impl MixnetListener {
.storage()
.remove_wireguard_peer(&public_key)
.await?;
return Err(e);
return Err(e.into());
}
registered_and_free
.registration_in_progres
.registration_in_progress
.remove(&final_message.gateway_client_pub_key());
let bytes = match AuthenticatorVersion::from(protocol) {
@@ -935,7 +928,7 @@ impl MixnetListener {
// When an incoming mixnet message triggers a response that we send back.
async fn handle_response(
&self,
&mut self,
response: Vec<u8>,
recipient: Option<Recipient>,
sender_tag: Option<AnonymousSenderTag>,
@@ -3,21 +3,19 @@
use crate::node::internal_service_providers::authenticator::error::AuthenticatorError;
use futures::channel::oneshot;
use ipnetwork::IpNetwork;
use nym_client_core::{HardcodedTopologyProvider, TopologyProvider};
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
use nym_sdk::{mixnet::Recipient, GatewayTransceiver};
use nym_task::ShutdownTracker;
use nym_wireguard::WireguardGatewayData;
use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime};
use std::{path::Path, sync::Arc};
pub use config::Config;
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
pub mod config;
pub mod error;
pub mod mixnet_client;
pub mod mixnet_listener;
mod peer_manager;
mod seen_credential_cache;
pub struct OnStartData {
@@ -40,7 +38,6 @@ pub struct Authenticator {
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
wireguard_gateway_data: WireguardGatewayData,
ecash_verifier: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
used_private_network_ips: Vec<IpAddr>,
shutdown: ShutdownTracker,
on_start: Option<oneshot::Sender<OnStartData>>,
}
@@ -50,7 +47,6 @@ impl Authenticator {
config: Config,
upgrade_mode_state: UpgradeModeDetails,
wireguard_gateway_data: WireguardGatewayData,
used_private_network_ips: Vec<IpAddr>,
ecash_verifier: Arc<
dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync,
>,
@@ -64,7 +60,6 @@ impl Authenticator {
custom_gateway_transceiver: None,
ecash_verifier,
wireguard_gateway_data,
used_private_network_ips,
shutdown,
on_start: None,
}
@@ -135,26 +130,8 @@ impl Authenticator {
let self_address = *mixnet_client.nym_address();
let used_private_network_ips =
std::collections::BTreeSet::from_iter(self.used_private_network_ips.iter());
let private_ip_network = IpNetwork::new(
self.config.authenticator.private_ipv4.into(),
self.config.authenticator.private_network_prefix_v4,
)?;
let now = SystemTime::now();
let free_private_network_ips = private_ip_network
.iter()
.map(|ip: IpAddr| {
if used_private_network_ips.contains(&ip) {
(ip.into(), Some(now))
} else {
(ip.into(), None)
}
})
.collect();
let mixnet_listener = crate::node::internal_service_providers::authenticator::mixnet_listener::MixnetListener::new(
let mixnet_listener = mixnet_listener::MixnetListener::new(
self.config,
free_private_network_ips,
self.wireguard_gateway_data,
mixnet_client,
self.upgrade_mode_state,
+88 -24
View File
@@ -1,8 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::registration::process_registration;
use super::LpHandlerState;
use super::{LpHandlerState, ReceiverIndex};
use crate::error::GatewayError;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput};
@@ -12,7 +11,7 @@ use nym_lp::{
};
use nym_lp_transport::traits::LpTransport;
use nym_metrics::{add_histogram_obs, inc};
use nym_registration_common::LpRegistrationRequest;
use nym_registration_common::{LpRegistrationRequest, RegistrationStatus};
use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -293,6 +292,10 @@ where
use nym_lp::state_machine::{LpInput, LpStateMachine};
let remote = self.remote_addr;
// if we managed to parse out the header, i.e. we have code supporting whatever
// protocol version has been sent - use that one instead
let protocol_version = packet.header().protocol_version;
// Extract ClientHello data
let hello_data = match packet.message() {
LpMessage::ClientHello(hello_data) => hello_data,
@@ -326,8 +329,10 @@ where
// Send Collision response to tell client to retry with new receiver_index
// No outer key - this is before PSK derivation
// Note: Do NOT set binding on collision - client may retry with new receiver_index
let collision_packet =
LpPacket::new(LpHeader::new(receiver_index, 0), LpMessage::Collision);
let collision_packet = LpPacket::new(
LpHeader::new(receiver_index, 0, protocol_version),
LpMessage::Collision,
);
self.send_lp_packet(collision_packet, None).await?;
return Ok(());
@@ -345,6 +350,7 @@ where
self.state.local_lp_peer.clone(),
hello_data.to_remote_peer(),
&hello_data.salt,
protocol_version,
)
.map_err(|e| {
inc!("lp_client_hello_failed");
@@ -375,7 +381,10 @@ where
// Send Ack to confirm ClientHello received
// No outer key - this is before PSK derivation
let ack_packet = LpPacket::new(LpHeader::new(receiver_index, 0), LpMessage::Ack);
let ack_packet = LpPacket::new(
LpHeader::new(receiver_index, 0, protocol_version),
LpMessage::Ack,
);
self.send_lp_packet(ack_packet, None).await?;
Ok(())
@@ -432,13 +441,15 @@ where
self.remote_addr, receiver_idx
);
let session = state_entry.value().state.session().map_err(|err| {
GatewayError::LpHandshakeError(format!("no session available: {err}"))
})?;
// Get outer key for Ack encryption before releasing borrow
let outer_key = state_entry
.value()
.state
.session()
.ok()
.and_then(|s| s.outer_aead_key());
let outer_key = session.outer_aead_key();
// Get previously negotiated protocol version for header creation
let negotiated_version = session.negotiated_version();
// Move state machine to session_states (already in Transport state)
// We keep the state machine (not just session) to enable
@@ -462,7 +473,10 @@ where
inc!("lp_handshakes_success");
// Send Ack to confirm handshake completion to the client
let ack_packet = LpPacket::new(LpHeader::new(receiver_idx, 0), LpMessage::Ack);
let ack_packet = LpPacket::new(
LpHeader::new(receiver_idx, 0, negotiated_version),
LpMessage::Ack,
);
trace!(
"Moved session {} to transport mode, sending Ack",
receiver_idx
@@ -742,11 +756,11 @@ where
/// Handle registration request on an established session
async fn handle_registration_request(
&mut self,
receiver_idx: u32,
receiver_idx: ReceiverIndex,
request: LpRegistrationRequest,
) -> Result<(), GatewayError> {
// Process registration (might modify state)
let response = process_registration(request, &self.state).await;
let response = self.state.process_registration(receiver_idx, request).await;
let response_bytes = response.serialise().map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to serialize response: {e}"))
})?;
@@ -754,13 +768,23 @@ where
self.send_response_packet(receiver_idx, response_bytes, LpDataKind::Registration)
.await?;
if response.success {
info!("LP registration successful for {})", self.remote_addr);
} else {
warn!(
"LP registration failed for {}: {:?}",
self.remote_addr, response.error
);
match response.status {
RegistrationStatus::Completed => {
info!("LP registration successful for {}", self.remote_addr);
}
RegistrationStatus::Failed => {
warn!(
"LP registration failed for {}: {:?}",
self.remote_addr,
response.error_message()
);
}
RegistrationStatus::PendingMoreData => {
info!(
"we required more deta from {} to complete registration",
self.remote_addr
);
}
}
Ok(())
@@ -1220,15 +1244,23 @@ where
mod tests {
use super::*;
use crate::node::lp_listener::{LpConfig, LpDebug};
use crate::node::wireguard::PeerManager;
use crate::node::ActiveClientsStore;
use bytes::BytesMut;
use nym_credential_verification::upgrade_mode::{
UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
};
use nym_credential_verification::UpgradeModeState;
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet};
use nym_lp::message::{ClientHelloData, EncryptedDataPayload, HandshakeData, LpMessage};
use nym_lp::packet::{LpHeader, LpPacket};
use nym_lp::peer::LpLocalPeer;
use nym_wireguard::{PeerControlRequest, WireguardConfig, WireguardGatewayData};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc::Receiver;
// ==================== Test Helpers ====================
/// Create a minimal test state for handler tests
@@ -1236,6 +1268,23 @@ mod tests {
use nym_crypto::asymmetric::ed25519;
use rand::rngs::OsRng;
fn wireguard_data(
keys: Arc<x25519::KeyPair>,
) -> (WireguardGatewayData, Receiver<PeerControlRequest>) {
// some sensible default values (ports don't matter anyway)
let cfg = WireguardConfig {
bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 51822),
private_ipv4: Ipv4Addr::new(10, 1, 0, 1),
private_ipv6: Ipv6Addr::new(0xfc01, 0, 0, 0, 0, 0, 0, 0x1), // fc01::1,
announced_tunnel_port: 51822,
announced_metadata_port: 51830,
private_network_prefix_v4: 16,
private_network_prefix_v6: 112,
};
WireguardGatewayData::new(cfg, keys)
}
// Create in-memory storage for testing
let storage = nym_gateway_storage::GatewayStorage::init(":memory:", 100)
.await
@@ -1262,6 +1311,20 @@ mod tests {
let id_keys = Arc::new(ed25519::KeyPair::new(&mut OsRng));
let x_keys = Arc::new(id_keys.to_x25519());
let (wireguard_data, _) = wireguard_data(x_keys.clone());
let (um_recheck_tx, _) = futures::channel::mpsc::unbounded();
let upgrade_mode_state = UpgradeModeState::new(*id_keys.public_key());
let upgrade_mode_details = UpgradeModeDetails::new(
UpgradeModeCheckConfig {
// essentially we never want to trigger this in our tests
min_staleness_recheck: Duration::from_nanos(1),
},
UpgradeModeCheckRequestSender::new(um_recheck_tx),
upgrade_mode_state.clone(),
);
let lp_peer = LpLocalPeer::new(id_keys, x_keys.clone()).with_kem_psq_key(x_keys);
LpHandlerState {
@@ -1272,12 +1335,13 @@ mod tests {
local_lp_peer: lp_peer,
metrics: nym_node_metrics::NymNodeMetrics::default(),
active_clients_store: ActiveClientsStore::new(),
wg_peer_controller: None,
wireguard_data: None,
upgrade_mode: upgrade_mode_details,
outbound_mix_sender: mix_sender,
handshake_states: Arc::new(dashmap::DashMap::new()),
session_states: Arc::new(dashmap::DashMap::new()),
registrations_in_progress: Default::default(),
forward_semaphore,
peer_manager: Arc::new(PeerManager::new(wireguard_data)),
}
}
+189 -119
View File
@@ -68,30 +68,36 @@
// They can be exported via Prometheus format using the metrics endpoint.
use crate::error::GatewayError;
use crate::node::lp_listener::registration::RegistrationsInProgress;
use crate::node::ActiveClientsStore;
use dashmap::DashMap;
use nym_config::serde_helpers::de_maybe_port;
use nym_credential_verification::ecash::traits::EcashManager;
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
use nym_gateway_storage::GatewayStorage;
use nym_lp::state_machine::LpStateMachine;
pub use nym_mixnet_client::forwarder::{
mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender,
};
use nym_node_metrics::NymNodeMetrics;
use nym_task::ShutdownTracker;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tokio::sync::{mpsc, Semaphore};
use tokio::sync::Semaphore;
use tracing::*;
use crate::node::wireguard::PeerManager;
pub use nym_lp::peer::LpLocalPeer;
pub use nym_mixnet_client::forwarder::{
mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender,
};
pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData};
mod data_handler;
pub mod handler;
mod registration;
pub type ReceiverIndex = u32;
/// Configuration for LP listener
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(default)]
@@ -111,7 +117,7 @@ pub struct LpConfig {
pub announce_control_port: Option<u16>,
/// Custom announced port for listening for the UDP LP data traffic.
/// If unspecified, the value from the `data_bind_address` will be used instead
/// If unspecified, the value from the `data_bind_address` will be used instead
/// (default: None)
#[serde(deserialize_with = "de_maybe_port")]
pub announce_data_port: Option<u16>,
@@ -178,6 +184,10 @@ pub struct LpDebug {
#[serde(with = "humantime_serde")]
pub demoted_session_ttl: Duration,
/// Maximum age of in-progress dVPN registration before cleanup (default: 60s)
#[serde(with = "humantime_serde")]
pub pending_registration_ttl: Duration,
/// How often to run the state cleanup task (default: 5 minutes)
///
/// The cleanup task scans for and removes stale handshakes and sessions.
@@ -247,6 +257,9 @@ impl LpDebug {
// 5 minutes - balances memory reclamation with task overhead
pub const DEFAULT_STATE_CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
// 1 minute - enough for client to send retrieve credential from its storage and send it across
pub const DEFAULT_PENDING_REGISTRATION_TTL: Duration = Duration::from_secs(60);
// Limits concurrent outbound connections to prevent fd exhaustion
pub const DEFAULT_MAX_CONCURRENT_FORWARDS: usize = 1000;
}
@@ -260,6 +273,7 @@ impl Default for LpDebug {
handshake_ttl: Self::DEFAULT_HANDSHAKE_TTL,
session_ttl: Self::DEFAULT_SESSION_TTL,
demoted_session_ttl: Self::DEFAULT_DEMOTED_SESSION_TTL,
pending_registration_ttl: Self::DEFAULT_PENDING_REGISTRATION_TTL,
state_cleanup_interval: Self::DEFAULT_STATE_CLEANUP_INTERVAL,
max_concurrent_forwards: Self::DEFAULT_MAX_CONCURRENT_FORWARDS,
}
@@ -311,12 +325,12 @@ impl<T> TimestampedState<T> {
}
/// Get age since creation
pub fn age(&self) -> std::time::Duration {
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
/// Get time since last activity
pub fn since_activity(&self) -> std::time::Duration {
pub fn since_activity(&self) -> Duration {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
@@ -332,8 +346,7 @@ impl<T> TimestampedState<T> {
#[derive(Clone)]
pub struct LpHandlerState {
/// Ecash verifier for bandwidth credentials
pub ecash_verifier:
Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
pub ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
/// Storage backend for persistence
pub storage: GatewayStorage,
@@ -347,11 +360,12 @@ pub struct LpHandlerState {
/// Active clients tracking
pub active_clients_store: ActiveClientsStore,
/// WireGuard peer controller channel (for dVPN registrations)
pub wg_peer_controller: Option<mpsc::Sender<PeerControlRequest>>,
/// Current state of the Upgrade Mode as perceived by this gateway
pub upgrade_mode: UpgradeModeDetails,
/// WireGuard gateway data (contains keypair and config)
pub wireguard_data: Option<WireguardGatewayData>,
/// alongside helpers for managing peers
pub peer_manager: Arc<PeerManager>,
/// LP configuration (for timestamp validation, etc.)
pub lp_config: LpConfig,
@@ -369,7 +383,7 @@ pub struct LpHandlerState {
/// state moves to session_states map.
///
/// Wrapped in TimestampedState for TTL-based cleanup of stale handshakes.
pub handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
pub handshake_states: Arc<DashMap<ReceiverIndex, TimestampedState<LpStateMachine>>>,
/// Established sessions keyed by session_id
///
@@ -383,7 +397,11 @@ pub struct LpHandlerState {
/// 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>>>,
pub session_states: Arc<DashMap<ReceiverIndex, TimestampedState<LpStateMachine>>>,
/// In-progress dVPN registrations that require additional data (e.g. credentials)
/// to finalise.
pub registrations_in_progress: RegistrationsInProgress,
/// Semaphore limiting concurrent forward connections
///
@@ -559,30 +577,32 @@ impl LpListener {
///
/// The task automatically stops when the shutdown signal is received.
fn spawn_state_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
let peer_manager = Arc::clone(&self.handler_state.peer_manager);
let handshake_states = Arc::clone(&self.handler_state.handshake_states);
let session_states = Arc::clone(&self.handler_state.session_states);
let pending_registrations = self.handler_state.registrations_in_progress.clone();
let dbg_cfg = self.handler_state.lp_config.debug;
let handshake_ttl = dbg_cfg.handshake_ttl;
let session_ttl = dbg_cfg.session_ttl;
let demoted_session_ttl = dbg_cfg.demoted_session_ttl;
let pending_reg_ttl = dbg_cfg.pending_registration_ttl;
let interval = dbg_cfg.state_cleanup_interval;
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.as_secs(), session_ttl.as_secs(), demoted_session_ttl.as_secs(), interval.as_secs()
"Starting LP state cleanup task (handshake_ttl={}s, session_ttl={}s, demoted_ttl={}s, reg_ttl={}s, interval={}s)",
handshake_ttl.as_secs(), session_ttl.as_secs(), demoted_session_ttl.as_secs(),pending_reg_ttl.as_secs(), interval.as_secs()
);
self.shutdown.try_spawn_named(
Self::cleanup_loop(
cleanup_task::cleanup_loop(
peer_manager,
handshake_states,
session_states,
handshake_ttl,
session_ttl,
demoted_session_ttl,
interval,
pending_registrations,
dbg_cfg,
shutdown,
metrics,
),
@@ -590,104 +610,6 @@ impl LpListener {
)
}
/// 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.
#[allow(clippy::too_many_arguments)]
async fn cleanup_loop(
handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
handshake_ttl: Duration,
session_ttl: Duration,
demoted_session_ttl: Duration,
interval: Duration,
shutdown: nym_task::ShutdownToken,
_metrics: NymNodeMetrics,
) {
use nym_lp::state_machine::LpStateBare;
use nym_metrics::inc_by;
let mut cleanup_interval = tokio::time::interval(interval);
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() > handshake_ttl {
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
} else {
session_ttl
};
if timestamped.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
@@ -695,3 +617,151 @@ impl LpListener {
.active_lp_connections_count()
}
}
pub(crate) mod cleanup_task {
use crate::node::lp_listener::registration::RegistrationsInProgress;
use crate::node::lp_listener::{LpDebug, TimestampedState};
use crate::node::wireguard::PeerManager;
use dashmap::DashMap;
use nym_lp::state_machine::LpStateBare;
use nym_lp::LpStateMachine;
use nym_metrics::inc_by;
use nym_node_metrics::NymNodeMetrics;
use std::sync::Arc;
use tracing::{debug, error, info};
async fn perform_cleanup(
peer_manager: &PeerManager,
handshake_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
registrations_in_progress: &RegistrationsInProgress,
cfg: LpDebug,
) {
let handshake_ttl = cfg.handshake_ttl;
let session_ttl = cfg.session_ttl;
let demoted_session_ttl = cfg.demoted_session_ttl;
let pending_registration_ttl = cfg.pending_registration_ttl;
let start = std::time::Instant::now();
let mut hs_removed = 0u64;
let mut ss_removed = 0u64;
let mut pending_reg_removed = 0u64;
let mut demoted_removed = 0u64;
// Remove stale handshakes (based on age since creation)
handshake_states.retain(|_, timestamped| {
if timestamped.age() > handshake_ttl {
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
} else {
session_ttl
};
if timestamped.since_activity() > ttl {
if is_demoted {
demoted_removed += 1;
} else {
ss_removed += 1;
}
false
} else {
true
}
});
// Remove stale registrations (based on time since last activity)
let mut reg_guard = registrations_in_progress.lock().await;
let mut stale_registrations = Vec::new();
for (k, timestamped) in reg_guard.iter() {
if timestamped.age() > pending_registration_ttl {
stale_registrations.push(*k)
}
}
for to_remove in stale_registrations {
pending_reg_removed += 1;
// SAFETY: we never dropped the guard and the entry existed
#[allow(clippy::unwrap_used)]
let entry = reg_guard.remove(&to_remove).unwrap();
if let Err(err) = peer_manager
.release_ip_pair(entry.state.allocated_ip_pair())
.await
{
error!("failed to release allocated ip pair: {err}")
}
}
if hs_removed > 0 || ss_removed > 0 || demoted_removed > 0 || pending_reg_removed > 0 {
let duration = start.elapsed();
info!(
"LP state cleanup: removed {hs_removed} handshakes, {pending_reg_removed} pending registrations, {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
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);
}
if pending_reg_removed > 0 {
inc_by!(
"lp_states_cleanup_pending_registrations_removed",
pending_reg_removed as i64
);
}
}
}
/// 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.
pub(crate) async fn cleanup_loop(
peer_manager: Arc<PeerManager>,
handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
registrations_in_progress: RegistrationsInProgress,
cfg: LpDebug,
shutdown: nym_task::ShutdownToken,
_metrics: NymNodeMetrics,
) {
let interval = cfg.state_cleanup_interval;
let mut cleanup_interval = tokio::time::interval(interval);
loop {
tokio::select! {
biased;
_ = shutdown.cancelled() => {
debug!("LP state cleanup task: received shutdown signal");
break;
}
_ = cleanup_interval.tick() => {
perform_cleanup(&peer_manager, &handshake_states, &session_states, &registrations_in_progress, cfg).await;
}
}
}
info!("LP state cleanup task shutdown complete");
}
}
+451 -381
View File
@@ -1,30 +1,38 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::LpHandlerState;
use super::{LpHandlerState, ReceiverIndex, TimestampedState};
use crate::error::GatewayError;
use crate::node::client_handling::websocket::message_receiver::IsActive;
use defguard_wireguard_rs::host::Peer;
use defguard_wireguard_rs::key::Key;
use futures::channel::{mpsc, oneshot};
use nym_authenticator_requests::models::BandwidthClaim;
use nym_credential_verification::ecash::traits::EcashManager;
use nym_credential_verification::{
bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig,
ClientBandwidth, CredentialVerifier,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType};
use nym_crypto::asymmetric::encryption::KeyPair;
use nym_gateway_requests::models::CredentialSpendingRequest;
use nym_gateway_storage::models::PersistedBandwidth;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_metrics::{add_histogram_obs, inc, inc_by};
use nym_registration_common::{
LpDvpnRegistrationRequest, LpMixnetGatewayData, LpMixnetRegistrationRequest,
LpRegistrationData, LpRegistrationRequest, LpRegistrationResponse, WireguardConfiguration,
use nym_registration_common::dvpn::{
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
LpDvpnRegistrationRequestMessage, LpDvpnRegistrationRequestMessageContent,
};
use nym_wireguard::PeerControlRequest;
use nym_registration_common::mixnet::LpMixnetRegistrationRequestMessage;
use nym_registration_common::{
LpRegistrationRequest, LpRegistrationRequestData, LpRegistrationResponse, RegistrationMode,
RegistrationStatus, WireguardConfiguration,
};
use nym_wireguard::peer_controller::IpPair;
use nym_wireguard::WireguardConfig;
use nym_wireguard_types::PeerPublicKey;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::{Mutex, MutexGuard};
use tracing::*;
// Histogram buckets for LP registration duration tracking
@@ -56,6 +64,439 @@ const WG_CONTROLLER_LATENCY_BUCKETS: &[f64] = &[
2.0, // 2s
];
#[derive(Clone, Copy)]
pub struct PendingRegistrationState {
client_id: i64,
peer_key: PeerPublicKey,
ticket_type: TicketType,
wireguard_config: WireguardConfiguration,
}
impl PendingRegistrationState {
pub(crate) fn allocated_ip_pair(&self) -> IpPair {
IpPair::new(
self.wireguard_config.private_ipv4,
self.wireguard_config.private_ipv6,
)
}
}
#[derive(Clone, Default)]
pub struct RegistrationsInProgress {
/// Wrapped in TimestampedState for TTL-based cleanup of stale data.
inner: Arc<Mutex<HashMap<ReceiverIndex, TimestampedState<PendingRegistrationState>>>>,
}
impl RegistrationsInProgress {
pub async fn lock(
&self,
) -> MutexGuard<'_, HashMap<ReceiverIndex, TimestampedState<PendingRegistrationState>>> {
self.inner.lock().await
}
}
impl LpHandlerState {
fn upgrade_mode_enabled(&self) -> bool {
self.upgrade_mode.enabled()
}
fn keypair(&self) -> &Arc<KeyPair> {
self.peer_manager.wireguard_gateway_data.keypair()
}
fn wireguard_config(&self) -> WireguardConfig {
self.peer_manager.wireguard_gateway_data.config()
}
fn successful_dvpn_registration(
&self,
peer_private_ipv4: Ipv4Addr,
peer_private_ipv6: Ipv6Addr,
bandwidth: i64,
) -> LpRegistrationResponse {
LpRegistrationResponse::success_dvpn(
WireguardConfiguration {
public_key: *self.keypair().public_key(),
// TODO: according to @SW this is most likely very wrong
endpoint: self.wireguard_config().bind_address,
private_ipv4: peer_private_ipv4,
private_ipv6: peer_private_ipv6,
},
bandwidth,
)
}
/// Check if WG peer already registered, return cached response if so.
///
/// This enables idempotent registration: if a client retries registration
/// with the same WG public key (e.g., after network failure), we return
/// the existing registration data instead of re-processing. This prevents
/// wasting credentials on network issues.
async fn check_existing_dvpn_registration(
&self,
public_key: PeerPublicKey,
) -> Option<LpRegistrationResponse> {
// Look up existing peer
let Ok(maybe_peer) = self.peer_manager.query_peer(public_key).await else {
return Some(LpRegistrationResponse::error(
"iternal failure: failed to resolve peer information",
RegistrationMode::Dvpn,
));
};
let peer = maybe_peer?;
// Extract IPv4 and IPv6 from allowed_ips
let mut private_ipv4 = None;
let mut private_ipv6 = None;
for ip_mask in &peer.allowed_ips {
match ip_mask.address {
IpAddr::V4(v4) => private_ipv4 = Some(v4),
IpAddr::V6(v6) => private_ipv6 = Some(v6),
}
if private_ipv4.is_some() && private_ipv6.is_some() {
break;
}
}
// Incomplete data, treat as new registration
let (Some(private_ipv4), Some(private_ipv6)) = (private_ipv4, private_ipv6) else {
return None;
};
// Get current bandwidth
let Ok(bandwidth) = self.peer_manager.query_client_bandwidth(public_key).await else {
return Some(LpRegistrationResponse::error(
"iternal failure: failed to resolve peer bandwidth",
RegistrationMode::Dvpn,
));
};
Some(self.successful_dvpn_registration(
private_ipv4,
private_ipv6,
bandwidth.available().await,
))
}
/// In the case of an already registered WG peer, update its PSK.
async fn update_peer_psk(&self, peer: PeerPublicKey, psk: Key) -> Result<(), GatewayError> {
let encoded_psk = psk.to_lower_hex();
self.storage
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
.await?;
// TODO: do we have to go through a peer manager to also update PSK if a peer is currently active?
// seems like an edge case. maybe we should force disconnect here?
Ok(())
}
async fn process_dvpn_initial_registration(
&self,
sender: ReceiverIndex,
request: LpDvpnRegistrationInitialRequest,
) -> LpRegistrationResponse {
let wg_key_str = request.wg_public_key.to_string();
// check for an existing registration (same WG key already registered)
// This allows clients to retry registration after network failures
// or to re-use gateway without spending additional bandwidth
if let Some(existing_response) = self
.check_existing_dvpn_registration(request.wg_public_key)
.await
{
// if there already exists registration for this client, update the psk and return the peer data
if let Err(err) = self
.update_peer_psk(request.wg_public_key, Key::new(request.psk))
.await
{
return LpRegistrationResponse::error(
format!("WireGuard peer PSK update failed: {err}"),
RegistrationMode::Dvpn,
);
}
info!("LP dVPN re-registration for existing peer {wg_key_str} (idempotent)",);
inc!("lp_registration_dvpn_idempotent");
return existing_response;
}
// TODO: this could be a source of some issue as we pre-allocate ip before validating credentials
// (but we do the same in the authenticator anyway...)
if let Err(err) = self
.register_wg_peer(
sender,
request.wg_public_key,
request.ticket_type,
Key::new(request.psk),
)
.await
{
return LpRegistrationResponse::error(
format!("WireGuard peer IP allocation failed: {err}"),
RegistrationMode::Dvpn,
);
}
LpRegistrationResponse::request_dvpn_credential()
}
// TODO: dedup
async fn handle_final_credential_claim(
&self,
claim: BandwidthClaim,
client_id: i64,
) -> Result<i64, GatewayError> {
match claim.credential {
BandwidthCredential::ZkNym(zk_nym) => {
// if we got zk-nym, we just try to verify it
let bandwidth =
credential_verification(self.ecash_verifier.clone(), *zk_nym, client_id)
.await?;
Ok(bandwidth)
}
BandwidthCredential::UpgradeModeJWT { token } => {
// TODO: move
const UM_BANDWIDTH: i64 = 1024 * 1024 * 1024;
// if we're already in the upgrade mode, don't bother validating the token
if self.upgrade_mode_enabled() {
return Ok(UM_BANDWIDTH);
}
self.upgrade_mode.try_enable_via_received_jwt(token).await?;
Ok(UM_BANDWIDTH)
}
}
}
async fn process_dvpn_registration_finalisation(
&self,
sender: ReceiverIndex,
request: LpDvpnRegistrationFinalisation,
) -> LpRegistrationResponse {
// see if we still have the pending registration
// (e.g. it's illegal for client to request registration and only finalise it,
// for example the next day; we can't keep the data forever)
let Some(pending) = self
.registrations_in_progress
.lock()
.await
.get(&sender)
.map(|pending| pending.state)
else {
return LpRegistrationResponse::error(
"no pending registration",
RegistrationMode::Dvpn,
);
};
if pending.ticket_type != request.credential.kind {
return LpRegistrationResponse::error(
format!(
"inconsistent ticket type. used {} for initial request and {} for finalisation",
pending.ticket_type, request.credential.kind
),
RegistrationMode::Dvpn,
);
}
let client_id = pending.client_id;
let allocated_bandwidth = match self
.handle_final_credential_claim(request.credential, client_id)
.await
{
Ok(bandwidth) => bandwidth,
Err(err) => {
// Credential verification failed, remove the peer
warn!("LP credential verification failed for client {client_id}: {err}");
inc!("lp_registration_dvpn_failed");
if let Err(remove_err) = self
.storage
.remove_wireguard_peer(&pending.peer_key.to_string())
.await
{
error!(
"Failed to remove peer after credential verification failure: {remove_err}"
);
}
self.registrations_in_progress.lock().await.remove(&sender);
return LpRegistrationResponse::error(
format!("Credential verification failed: {err}"),
RegistrationMode::Dvpn,
);
}
};
info!("LP dVPN registration successful (client_id: {client_id})");
inc!("lp_registration_dvpn_success");
LpRegistrationResponse::success_dvpn(pending.wireguard_config, allocated_bandwidth)
}
async fn process_dvpn_registration(
&self,
sender: ReceiverIndex,
request: Box<LpDvpnRegistrationRequestMessage>,
) -> LpRegistrationResponse {
// Track dVPN registration attempts
inc!("lp_registration_dvpn_attempts");
match request.content {
LpDvpnRegistrationRequestMessageContent::InitialRequest(req) => {
self.process_dvpn_initial_registration(sender, req).await
}
LpDvpnRegistrationRequestMessageContent::Finalisation(req) => {
self.process_dvpn_registration_finalisation(sender, req)
.await
}
}
}
async fn process_mixnet_registration(
&self,
request: LpMixnetRegistrationRequestMessage,
) -> LpRegistrationResponse {
let _ = request;
LpRegistrationResponse::error(
"mixnet registration is not yet supported",
RegistrationMode::Mixnet,
)
}
/// Process an LP registration request
pub async fn process_registration(
&self,
sender: ReceiverIndex,
request: LpRegistrationRequest,
) -> LpRegistrationResponse {
let registration_start = std::time::Instant::now();
// Track total registration attempts
inc!("lp_registration_attempts_total");
// 1. Validate timestamp for replay protection
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("invalid timestamp", request.mode());
}
// 2. Process based on mode
let result = match request.registration_data {
LpRegistrationRequestData::Dvpn { data } => {
self.process_dvpn_registration(sender, data).await
}
LpRegistrationRequestData::Mixnet { data } => {
self.process_mixnet_registration(data).await
}
};
// Track registration duration
let duration = registration_start.elapsed().as_secs_f64();
add_histogram_obs!(
"lp_registration_duration_seconds",
duration,
LP_REGISTRATION_DURATION_BUCKETS
);
// Track overall success/failure
match result.status {
RegistrationStatus::Completed => {
inc!("lp_registration_success_total");
}
RegistrationStatus::Failed => {
inc!("lp_registration_failed_total");
}
RegistrationStatus::PendingMoreData => {
inc!("lp_registration_pending_more_data");
}
}
result
}
/// Register a WireGuard peer and return gateway data along with the client_id
async fn register_wg_peer(
&self,
sender: ReceiverIndex,
peer_key: PeerPublicKey,
ticket_type: nym_credentials_interface::TicketType,
psk: Key,
) -> Result<(), GatewayError> {
// Allocate IPs from centralized pool managed by PeerController
let defguard_key = Key::new(peer_key.to_bytes());
// Request IP allocation from PeerController
let ip_pair = self.peer_manager.allocate_peer_ip_pair().await?;
let client_ipv4 = ip_pair.ipv4;
let client_ipv6 = ip_pair.ipv6;
info!("Allocated IPs for peer {peer_key}: {client_ipv4} / {client_ipv6}");
// Create WireGuard peer with allocated IPs
let mut peer = Peer::new(defguard_key);
peer.endpoint = None;
peer.allowed_ips = vec![
format!("{client_ipv4}/32").parse()?,
format!("{client_ipv6}/128").parse()?,
];
peer.persistent_keepalive_interval = Some(25);
peer.preshared_key = Some(psk);
// Store peer in database FIRST (before adding to controller)
// This ensures bandwidth storage exists when controller's generate_bandwidth_manager() is called
let client_id = self
.storage
.insert_wireguard_peer(&peer, ticket_type.into())
.await
.map_err(|e| {
error!("Failed to store WireGuard peer in database: {}", e);
GatewayError::InternalError(format!("Failed to store peer: {}", e))
})?;
// Create bandwidth entry for the client
// This must happen BEFORE AddPeer because generate_bandwidth_manager() expects it to exist
credential_storage_preparation(self.ecash_verifier.clone(), client_id).await?;
// Now send peer to WireGuard controller and track latency
let controller_start = std::time::Instant::now();
let result = self.peer_manager.add_peer(peer).await;
// Record peer controller channel latency
let latency = controller_start.elapsed().as_secs_f64();
add_histogram_obs!(
"wg_peer_controller_channel_latency_seconds",
latency,
WG_CONTROLLER_LATENCY_BUCKETS
);
result?;
// Get gateway's actual WireGuard public key
let gateway_pubkey = *self.keypair().public_key();
// Get gateway's WireGuard endpoint from config
let gateway_endpoint = self.wireguard_config().bind_address;
self.registrations_in_progress.lock().await.insert(
sender,
TimestampedState::new(PendingRegistrationState {
client_id,
peer_key,
ticket_type,
wireguard_config: WireguardConfiguration {
public_key: gateway_pubkey,
endpoint: gateway_endpoint,
private_ipv4: client_ipv4,
private_ipv6: client_ipv6,
},
}),
);
Ok(())
}
}
// TODO: dedup
/// Prepare bandwidth storage for a client
async fn credential_storage_preparation(
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
@@ -83,6 +524,7 @@ async fn credential_storage_preparation(
Ok(bandwidth)
}
// TODO: dedup
/// Verify credential and allocate bandwidth using CredentialVerifier
async fn credential_verification(
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
@@ -131,375 +573,3 @@ async fn credential_verification(
Ok(allocated)
}
/// Check if WG peer already registered, return cached response if so.
///
/// This enables idempotent registration: if a client retries registration
/// with the same WG public key (e.g., after network failure), we return
/// the existing registration data instead of re-processing. This prevents
/// wasting credentials on network issues.
async fn check_existing_registration(
wg_key_str: &str,
state: &LpHandlerState,
) -> Option<LpRegistrationResponse> {
// Need WG data to build GatewayData
let wg_data = state.wireguard_data.as_ref()?;
// Look up existing peer
let peer = state.storage.get_wireguard_peer(wg_key_str).await.ok()??;
// Convert to defguard Peer to access allowed_ips
let defguard_peer: Peer = peer.clone().try_into().ok()?;
// Extract IPv4 and IPv6 from allowed_ips
let mut ipv4 = None;
let mut ipv6 = None;
for ip_mask in &defguard_peer.allowed_ips {
match ip_mask.address {
std::net::IpAddr::V4(v4) => ipv4 = Some(v4),
std::net::IpAddr::V6(v6) => ipv6 = Some(v6),
}
}
let (private_ipv4, private_ipv6) = match (ipv4, ipv6) {
(Some(v4), Some(v6)) => (v4, v6),
_ => return None, // Incomplete data, treat as new registration
};
// Get current bandwidth
let bandwidth = state
.ecash_verifier
.storage()
.get_available_bandwidth(peer.client_id)
.await
.ok()?
.map(|b| b.available)
.unwrap_or(0);
// Only return cached response if bandwidth was actually allocated.
// If bandwidth is 0, registration was incomplete (peer exists but
// credential verification failed or never completed). Let the caller
// proceed with normal registration flow which will handle cleanup.
if bandwidth == 0 {
return None;
}
Some(LpRegistrationResponse::success(
bandwidth,
WireguardConfiguration {
public_key: *wg_data.keypair().public_key(),
endpoint: wg_data.config().bind_address,
private_ipv4,
private_ipv6,
},
))
}
/// In the case of an already registered WG peer, update its PSK.
async fn update_peer_psk(
peer: PeerPublicKey,
psk: Key,
state: &LpHandlerState,
) -> Result<(), GatewayError> {
let encoded_psk = psk.to_lower_hex();
state
.storage
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
.await?;
Ok(())
}
async fn process_dvpn_registration(
request: Box<LpDvpnRegistrationRequest>,
state: &LpHandlerState,
) -> LpRegistrationResponse {
// Track dVPN registration attempts
inc!("lp_registration_dvpn_attempts");
// Check for idempotent re-registration (same WG key already registered)
// This allows clients to retry registration after network failures
// without wasting credentials
let wg_key_str = request.wg_public_key.to_string();
if let Some(existing_response) = check_existing_registration(&wg_key_str, state).await {
// TODO: this flow will be changed in subsequent PRs as it's wasting credentials regardless
if let Err(err) = update_peer_psk(request.wg_public_key, Key::new(request.psk), state).await
{
return LpRegistrationResponse::error(format!(
"WireGuard peer PSK update failed: {err}"
));
}
info!("LP dVPN re-registration for existing peer {wg_key_str} (idempotent)",);
inc!("lp_registration_dvpn_idempotent");
return existing_response;
}
// Register as WireGuard peer first to get client_id
let (gateway_data, client_id) = match register_wg_peer(
request.wg_public_key.inner().as_ref(),
request.ticket_type,
Key::new(request.psk),
state,
)
.await
{
Ok(result) => result,
Err(e) => {
error!("LP WireGuard peer registration failed: {e}");
inc!("lp_registration_dvpn_failed");
inc!("lp_errors_wg_peer_registration");
return LpRegistrationResponse::error(format!(
"WireGuard peer registration failed: {e}",
));
}
};
// Verify credential with CredentialVerifier (handles double-spend, storage, etc.)
let allocated_bandwidth =
match credential_verification(state.ecash_verifier.clone(), request.credential, client_id)
.await
{
Ok(bandwidth) => bandwidth,
Err(e) => {
// Credential verification failed, remove the peer
warn!("LP credential verification failed for client {client_id}: {e}",);
inc!("lp_registration_dvpn_failed");
if let Err(remove_err) = state
.storage
.remove_wireguard_peer(&request.wg_public_key.to_string())
.await
{
error!(
"Failed to remove peer after credential verification failure: {remove_err}"
);
}
return LpRegistrationResponse::error(format!(
"Credential verification failed: {e}",
));
}
};
info!("LP dVPN registration successful (client_id: {client_id})");
inc!("lp_registration_dvpn_success");
LpRegistrationResponse::success(allocated_bandwidth, gateway_data)
}
async fn process_mixnet_registration(
request: LpMixnetRegistrationRequest,
state: &LpHandlerState,
) -> LpRegistrationResponse {
let session_id = rand::random::<u32>();
// Track mixnet registration attempts
inc!("lp_registration_mixnet_attempts");
// Derive destination address for ActiveClientsStore lookup
let client_identity = request.client_ed25519_pubkey;
let client_address = client_identity.derive_destination_address();
info!("LP Mixnet registration for client {client_identity}, session {session_id}");
warn!("unimplemented: LP mixnet registration initial bandwidth allocation");
// (the old implementation was wrong - it wasn't creating correct db entries)
// Create channels for client message delivery
let (mix_sender, _mix_receiver) = mpsc::unbounded();
let (is_active_request_sender, _is_active_request_receiver) =
mpsc::unbounded::<oneshot::Sender<IsActive>>();
// Insert client into ActiveClientsStore for SURB reply delivery
if !state.active_clients_store.insert_remote(
client_address,
mix_sender,
is_active_request_sender,
OffsetDateTime::now_utc(),
) {
warn!("LP Mixnet registration failed: client {client_identity} already registered",);
inc!("lp_registration_mixnet_failed");
return LpRegistrationResponse::error("Client already registered".to_string());
}
// Get gateway identity and derive sphinx key
let gateway_identity = *state.local_lp_peer.ed25519().public_key();
info!("LP Mixnet registration successful (client: {client_identity})",);
inc!("lp_registration_mixnet_success");
LpRegistrationResponse::success_mixnet(0, LpMixnetGatewayData { gateway_identity })
}
/// Process an LP registration request
pub async fn process_registration(
request: LpRegistrationRequest,
state: &LpHandlerState,
) -> LpRegistrationResponse {
let registration_start = std::time::Instant::now();
// Track total registration attempts
inc!("lp_registration_attempts_total");
// 1. Validate timestamp for replay protection
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("Invalid timestamp".to_string());
}
// 2. Process based on mode
let result = match request.registration_data {
LpRegistrationData::Dvpn { data } => process_dvpn_registration(data, state).await,
LpRegistrationData::Mixnet { data } => process_mixnet_registration(data, state).await,
};
// Track registration duration
let duration = registration_start.elapsed().as_secs_f64();
add_histogram_obs!(
"lp_registration_duration_seconds",
duration,
LP_REGISTRATION_DURATION_BUCKETS
);
// Track overall success/failure
if result.success {
inc!("lp_registration_success_total");
} else {
inc!("lp_registration_failed_total");
}
result
}
/// Register a WireGuard peer and return gateway data along with the client_id
async fn register_wg_peer(
public_key_bytes: &[u8],
ticket_type: nym_credentials_interface::TicketType,
psk: Key,
state: &LpHandlerState,
) -> Result<(WireguardConfiguration, i64), GatewayError> {
let Some(wg_controller) = &state.wg_peer_controller else {
return Err(GatewayError::ServiceProviderNotRunning {
service: "WireGuard".to_string(),
});
};
let Some(wg_data) = &state.wireguard_data else {
return Err(GatewayError::ServiceProviderNotRunning {
service: "WireGuard".to_string(),
});
};
// Convert public key bytes to WireGuard Key
let mut key_bytes = [0u8; 32];
if public_key_bytes.len() != 32 {
return Err(GatewayError::LpProtocolError(
"Invalid WireGuard public key length".to_string(),
));
}
key_bytes.copy_from_slice(public_key_bytes);
let peer_key = Key::new(key_bytes);
// Allocate IPs from centralized pool managed by PeerController
let registration_data =
nym_wireguard::PeerRegistrationData::new(peer_key.clone()).with_preshared_key(psk);
let psk = registration_data.preshared_key.clone();
// Request IP allocation from PeerController
let (tx, rx) = oneshot::channel();
wg_controller
.send(PeerControlRequest::RegisterPeer {
registration_data,
response_tx: tx,
})
.await
.map_err(|e| {
GatewayError::InternalError(format!("Failed to send IP allocation request: {}", e))
})?;
// Wait for IP allocation from pool
let ip_pair = rx
.await
.map_err(|e| {
GatewayError::InternalError(format!("Failed to receive IP allocation: {}", e))
})?
.map_err(|e| {
error!("Failed to allocate IPs from pool: {}", e);
GatewayError::InternalError(format!("Failed to allocate IPs: {:?}", e))
})?;
let client_ipv4 = ip_pair.ipv4;
let client_ipv6 = ip_pair.ipv6;
info!(
"Allocated IPs for peer {}: {} / {}",
peer_key, client_ipv4, client_ipv6
);
// Create WireGuard peer with allocated IPs
let mut peer = Peer::new(peer_key.clone());
peer.endpoint = None;
peer.allowed_ips = vec![
format!("{client_ipv4}/32").parse()?,
format!("{client_ipv6}/128").parse()?,
];
peer.persistent_keepalive_interval = Some(25);
peer.preshared_key = psk;
// Store peer in database FIRST (before adding to controller)
// This ensures bandwidth storage exists when controller's generate_bandwidth_manager() is called
let client_id = state
.storage
.insert_wireguard_peer(&peer, ticket_type.into())
.await
.map_err(|e| {
error!("Failed to store WireGuard peer in database: {}", e);
GatewayError::InternalError(format!("Failed to store peer: {}", e))
})?;
// Create bandwidth entry for the client
// This must happen BEFORE AddPeer because generate_bandwidth_manager() expects it to exist
credential_storage_preparation(state.ecash_verifier.clone(), client_id).await?;
// Now send peer to WireGuard controller and track latency
let controller_start = std::time::Instant::now();
let (tx, rx) = oneshot::channel();
wg_controller
.send(PeerControlRequest::AddPeer {
peer: peer.clone(),
response_tx: tx,
})
.await
.map_err(|e| GatewayError::InternalError(format!("Failed to send peer request: {}", e)))?;
let result = rx
.await
.map_err(|e| {
GatewayError::InternalError(format!("Failed to receive peer response: {}", e))
})?
.map_err(|e| GatewayError::InternalError(format!("Failed to add peer: {:?}", e)));
// Record peer controller channel latency
let latency = controller_start.elapsed().as_secs_f64();
add_histogram_obs!(
"wg_peer_controller_channel_latency_seconds",
latency,
WG_CONTROLLER_LATENCY_BUCKETS
);
result?;
// Get gateway's actual WireGuard public key
let gateway_pubkey = *wg_data.keypair().public_key();
// Get gateway's WireGuard endpoint from config
let gateway_endpoint = wg_data.config().bind_address;
// Create GatewayData response (matching authenticator response format)
Ok((
WireguardConfiguration {
public_key: gateway_pubkey,
endpoint: gateway_endpoint,
private_ipv4: client_ipv4,
private_ipv6: client_ipv6,
},
client_id,
))
}
+15 -21
View File
@@ -18,6 +18,7 @@ use nym_credential_verification::upgrade_mode::{
};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_ip_packet_router::IpPacketRouter;
use nym_lp::peer::LpLocalPeer;
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_network_defaults::NymNetworkDetails;
use nym_network_requester::NRServiceProviderBuilder;
@@ -37,6 +38,7 @@ use tracing::*;
use zeroize::Zeroizing;
pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher;
use crate::node::wireguard::PeerManager;
pub use client_handling::active_clients::ActiveClientsStore;
pub use lp_listener::LpConfig;
pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender;
@@ -46,7 +48,6 @@ pub use nym_gateway_storage::{
traits::{BandwidthGatewayStorage, InboxGatewayStorage},
GatewayStorage,
};
use nym_lp::peer::LpLocalPeer;
pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
pub(crate) mod client_handling;
@@ -54,6 +55,7 @@ pub(crate) mod internal_service_providers;
pub mod lp_listener;
mod stale_data_cleaner;
pub mod upgrade_mode;
pub mod wireguard;
#[derive(Debug, Clone)]
pub struct LocalNetworkRequesterOpts {
@@ -328,13 +330,18 @@ impl GatewayTasksBuilder {
pub async fn build_lp_listener(
&mut self,
upgrade_mode_common_state: UpgradeModeDetails,
active_clients_store: ActiveClientsStore,
) -> Result<lp_listener::LpListener, GatewayError> {
// Get WireGuard peer controller if available
let wg_peer_controller = self
.wireguard_data
.as_ref()
.map(|wg_data| wg_data.inner.peer_tx().clone());
let Some(wireguard_data) = &self.wireguard_data else {
return Err(GatewayError::InternalWireguardError(
"wireguard not set".to_string(),
));
};
// TODO: combine this `PeerManager` with the one used within the authenticator
let peer_manager = Arc::new(PeerManager::new(wireguard_data.inner.clone()));
let handler_state = lp_listener::LpHandlerState {
ecash_verifier: self.ecash_manager().await?,
@@ -346,12 +353,13 @@ impl GatewayTasksBuilder {
.with_kem_psq_key(self.kem_psq_keys.clone()),
metrics: self.metrics.clone(),
active_clients_store,
wg_peer_controller,
wireguard_data: self.wireguard_data.as_ref().map(|wd| wd.inner.clone()),
upgrade_mode: upgrade_mode_common_state,
peer_manager,
lp_config: self.config.lp,
outbound_mix_sender: self.mix_packet_sender.clone(),
handshake_states: Arc::new(dashmap::DashMap::new()),
session_states: Arc::new(dashmap::DashMap::new()),
registrations_in_progress: Default::default(),
forward_semaphore: Arc::new(Semaphore::new(
self.config.lp.debug.max_concurrent_forwards,
)),
@@ -485,25 +493,12 @@ impl GatewayTasksBuilder {
Ok(peers)
}
async fn get_wireguard_networks(&mut self) -> Result<Vec<IpAddr>, GatewayError> {
if let Some(cached) = self.wireguard_networks.take() {
return Ok(cached);
}
let (peers, used_private_network_ips) = self.build_wireguard_peers_and_networks().await?;
// cache peers for the other task
self.wireguard_peers = Some(peers);
Ok(used_private_network_ips)
}
pub async fn build_wireguard_authenticator(
&mut self,
upgrade_mode_common: UpgradeModeDetails,
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
) -> Result<ServiceProviderBeingBuilt<Authenticator>, GatewayError> {
let ecash_manager = self.ecash_manager().await?;
let used_private_network_ips = self.get_wireguard_networks().await?;
let Some(opts) = &self.authenticator_opts else {
return Err(GatewayError::UnspecifiedAuthenticatorConfig);
@@ -526,7 +521,6 @@ impl GatewayTasksBuilder {
opts.config.clone(),
upgrade_mode_common,
wireguard_data.inner.clone(),
used_private_network_ips,
ecash_manager,
self.shutdown_tracker.clone(),
)
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GatewayWireguardError {
#[error("internal error: {0}")]
InternalError(String),
#[error("peers can't be interacted with anymore")]
PeerInteractionStopped,
}
impl GatewayWireguardError {
pub fn internal(message: impl Into<String>) -> Self {
GatewayWireguardError::InternalError(message.into())
}
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod peer_manager;
pub use error::GatewayWireguardError;
pub use peer_manager::PeerManager;
@@ -1,14 +1,17 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node::internal_service_providers::authenticator::error::AuthenticatorError;
use crate::node::wireguard::GatewayWireguardError;
use defguard_wireguard_rs::{host::Peer, key::Key};
use futures::channel::oneshot;
use nym_credential_verification::{ClientBandwidth, TicketVerifier};
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard::peer_controller::IpPair;
use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData};
use nym_wireguard_types::PeerPublicKey;
use tracing::error;
#[derive(Clone)]
pub struct PeerManager {
pub(crate) wireguard_gateway_data: WireguardGatewayData,
}
@@ -19,26 +22,73 @@ impl PeerManager {
wireguard_gateway_data,
}
}
pub async fn add_peer(&self, peer: Peer) -> Result<(), AuthenticatorError> {
pub async fn allocate_peer_ip_pair(&self) -> Result<IpPair, GatewayWireguardError> {
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::AllocatePeerIpPair { response_tx };
self.wireguard_gateway_data
.peer_tx()
.send(msg)
.await
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|e| {
GatewayWireguardError::InternalError(format!(
"Failed to receive IP allocation: {e}"
))
})?
.map_err(|e| {
error!("Failed to allocate IPs from pool: {e}");
GatewayWireguardError::InternalError(format!("Failed to allocate IPs: {e}"))
})
}
pub async fn release_ip_pair(&self, ip_pair: IpPair) -> Result<(), GatewayWireguardError> {
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::ReleaseIpPair {
response_tx,
ip_pair,
};
self.wireguard_gateway_data
.peer_tx()
.send(msg)
.await
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| GatewayWireguardError::internal("no response for release ip allocation"))?
.map_err(|err| {
GatewayWireguardError::InternalError(format!(
"releasing ip pair not be performed: {err:?}"
))
})?;
Ok(())
}
pub async fn add_peer(&self, peer: Peer) -> Result<(), GatewayWireguardError> {
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::AddPeer { peer, response_tx };
self.wireguard_gateway_data
.peer_tx()
.send(msg)
.await
.map_err(|_| AuthenticatorError::PeerInteractionStopped)?;
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| AuthenticatorError::InternalError("no response for add peer".to_string()))?
.map_err(|_| GatewayWireguardError::internal("no response for add peer".to_string()))?
.map_err(|err| {
AuthenticatorError::InternalError(format!(
GatewayWireguardError::internal(format!(
"adding peer could not be performed: {err:?}"
))
})
}
pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> {
pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), GatewayWireguardError> {
let key = Key::new(pub_key.to_bytes());
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::RemovePeer { key, response_tx };
@@ -46,15 +96,13 @@ impl PeerManager {
.peer_tx()
.send(msg)
.await
.map_err(|_| AuthenticatorError::PeerInteractionStopped)?;
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| {
AuthenticatorError::InternalError("no response for remove peer".to_string())
})?
.map_err(|_| GatewayWireguardError::internal("no response for remove peer"))?
.map_err(|err| {
AuthenticatorError::InternalError(format!(
GatewayWireguardError::InternalError(format!(
"removing peer could not be performed: {err:?}"
))
})
@@ -63,7 +111,7 @@ impl PeerManager {
pub async fn query_peer(
&self,
public_key: PeerPublicKey,
) -> Result<Option<Peer>, AuthenticatorError> {
) -> Result<Option<Peer>, GatewayWireguardError> {
let key = Key::new(public_key.to_bytes());
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::QueryPeer { key, response_tx };
@@ -71,15 +119,13 @@ impl PeerManager {
.peer_tx()
.send(msg)
.await
.map_err(|_| AuthenticatorError::PeerInteractionStopped)?;
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| {
AuthenticatorError::InternalError("no response for query peer".to_string())
})?
.map_err(|_| GatewayWireguardError::internal("no response for query peer".to_string()))?
.map_err(|err| {
AuthenticatorError::InternalError(format!(
GatewayWireguardError::internal(format!(
"querying peer could not be performed: {err:?}"
))
})
@@ -88,7 +134,7 @@ impl PeerManager {
pub async fn query_bandwidth(
&self,
public_key: PeerPublicKey,
) -> Result<i64, AuthenticatorError> {
) -> Result<i64, GatewayWireguardError> {
let client_bandwidth = self.query_client_bandwidth(public_key).await?;
Ok(client_bandwidth.available().await)
}
@@ -96,7 +142,7 @@ impl PeerManager {
pub async fn query_client_bandwidth(
&self,
key: PeerPublicKey,
) -> Result<ClientBandwidth, AuthenticatorError> {
) -> Result<ClientBandwidth, GatewayWireguardError> {
let key = Key::new(key.to_bytes());
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::GetClientBandwidthByKey { key, response_tx };
@@ -104,17 +150,13 @@ impl PeerManager {
.peer_tx()
.send(msg)
.await
.map_err(|_| AuthenticatorError::PeerInteractionStopped)?;
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| {
AuthenticatorError::InternalError(
"no response for query client bandwidth".to_string(),
)
})?
.map_err(|_| GatewayWireguardError::internal("no response for query client bandwidth"))?
.map_err(|err| {
AuthenticatorError::InternalError(format!(
GatewayWireguardError::internal(format!(
"querying client bandwidth could not be performed: {err:?}"
))
})
@@ -124,7 +166,7 @@ impl PeerManager {
&self,
key: PeerPublicKey,
credential: CredentialSpendingData,
) -> Result<Box<dyn TicketVerifier + Send + Sync>, AuthenticatorError> {
) -> Result<Box<dyn TicketVerifier + Send + Sync>, GatewayWireguardError> {
let key = Key::new(key.to_bytes());
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::GetVerifierByKey {
@@ -136,15 +178,15 @@ impl PeerManager {
.peer_tx()
.send(msg)
.await
.map_err(|_| AuthenticatorError::PeerInteractionStopped)?;
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
response_rx
.await
.map_err(|_| {
AuthenticatorError::InternalError("no response for query verifier".to_string())
GatewayWireguardError::internal("no response for query verifier".to_string())
})?
.map_err(|err| {
AuthenticatorError::InternalError(format!(
GatewayWireguardError::internal(format!(
"querying verifier could not be performed: {err:?}"
))
})
+1 -1
View File
@@ -26,7 +26,7 @@ nym-lp-transport = { path = "../common/nym-lp-transport", features = ["io-mocks"
nym-gateway = { path = "../gateway" }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] }
tracing = { workspace = true }
futures = { workspace = true }
[lints]
workspace = true
+82 -17
View File
@@ -5,7 +5,11 @@
mod tests {
use anyhow::Context;
use nym_bandwidth_controller::mock::MockBandwidthController;
use nym_credential_verification::UpgradeModeState;
use nym_credential_verification::ecash::MockEcashManager;
use nym_credential_verification::upgrade_mode::{
UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_gateway::GatewayError;
@@ -14,6 +18,7 @@ mod tests {
LpDebug, LpHandlerState, LpLocalPeer, MixForwardingReceiver, PeerControlRequest,
WireguardGatewayData, mix_forwarding_channels,
};
use nym_gateway::node::wireguard::PeerManager;
use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig};
use nym_registration_client::{LpClientError, LpRegistrationClient};
use nym_test_utils::helpers::{CryptoRng, RngCore, u64_seeded_rng};
@@ -49,6 +54,7 @@ mod tests {
peer: LpLocalPeer,
x25519_wg_keys: Arc<x25519::KeyPair>,
socket_addr: SocketAddr,
lp_version: u8,
}
impl Party {
@@ -68,6 +74,7 @@ mod tests {
.with_kem_psq_key(lp_x25519_keys),
x25519_wg_keys,
socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))),
lp_version: 1,
}
}
}
@@ -174,6 +181,17 @@ mod tests {
Ok(GatewayStorage::from_connection_pool(conn_pool, 100).await?)
}
const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
];
pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey {
let private_key =
ed25519::PrivateKey::from_bytes(&Self::DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap();
private_key.public_key()
}
async fn mock(rng: &mut (impl RngCore + CryptoRng)) -> anyhow::Result<Self> {
let base = Party::generate(rng);
@@ -199,11 +217,27 @@ mod tests {
// create wireguard data
let (wireguard_data, peer_request_rx) = Self::wireguard_data(&base);
let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded();
// TODO: use it if we ever want to test UM
let _ = um_recheck_rx;
// mock the wg peer controller
let (mock_peer_controller, peer_controller_state) =
mock_peer_controller(peer_request_rx);
let upgrade_mode_state = UpgradeModeState::new(Self::dummy_attester_public_key());
let upgrade_mode_details = UpgradeModeDetails::new(
UpgradeModeCheckConfig {
// essentially we never want to trigger this in our tests
min_staleness_recheck: Duration::from_nanos(1),
},
UpgradeModeCheckRequestSender::new(um_recheck_tx),
upgrade_mode_state.clone(),
);
// registering particular responses for peer controller is up to given test
let peer_manager = Arc::new(PeerManager::new(wireguard_data));
let lp_state = LpHandlerState {
// use mock instance of ecash verifier
@@ -220,9 +254,8 @@ mod tests {
active_clients_store: ActiveClientsStore::new(),
// handles required for wg registration
wg_peer_controller: Some(wireguard_data.peer_tx().clone()),
wireguard_data: Some(wireguard_data),
upgrade_mode: upgrade_mode_details,
peer_manager,
// use default lp config (with enabled flag)
lp_config,
@@ -237,6 +270,7 @@ mod tests {
session_states: Arc::new(Default::default()),
// sensible default value for tests
registrations_in_progress: Default::default(),
forward_semaphore,
};
@@ -376,6 +410,7 @@ mod tests {
mod using_lp_registration_client {
use super::*;
use nym_registration_client::NestedLpSession;
use nym_wireguard::DefguardPeer;
#[tokio::test]
async fn test_basic_lp_entry_registration() -> anyhow::Result<()> {
@@ -392,6 +427,7 @@ mod tests {
client_data.base.peer.ed25519().clone(),
entry.base.peer.as_remote(),
entry.base.socket_addr,
entry.base.lp_version,
);
// 1. establish mock connection between client and gateway and retrieve gateway's handle
@@ -410,11 +446,10 @@ mod tests {
// 1) peer registration - ip pair allocation
let ip_pair = entry.allocate_ip_pair().await;
let reg_res = Ok::<_, nym_wireguard::Error>(ip_pair);
let public_key = client_key.to_wg_key();
entry
.register_peer_controller_response(
PeerControlRequestType::RegisterPeer { public_key },
PeerControlRequestType::AllocatePeerIpPair {},
reg_res,
)
.await;
@@ -430,6 +465,16 @@ mod tests {
)
.await;
// 3) peer query - check for prior registrations
let query_res = Ok::<_, nym_wireguard::Error>(Option::<DefguardPeer>::None);
let key = client_key.to_wg_key();
entry
.register_peer_controller_response(
PeerControlRequestType::QueryPeer { key },
query_res,
)
.await;
// 4. spawn peer controller to be able to handle dvpn registration requests
entry.spawn_peer_controller();
@@ -440,7 +485,7 @@ mod tests {
let wg_keypair = client_data.base.x25519_wg_keys;
let gateway_identity = entry.base.peer.ed25519().public_key();
let registration_result = client
.register(
.register_dvpn(
&mut client_rng,
&wg_keypair,
gateway_identity,
@@ -454,7 +499,6 @@ mod tests {
let peers_guard = entry.mock_peer_controller_state.peers.read().await;
let peer = peers_guard.get_by_x25519_key(&client_key).unwrap().clone();
drop(peers_guard);
assert!(peer.register_success);
assert!(peer.add_success);
assert_eq!(registration_result.private_ipv4, ip_pair.ipv4);
@@ -483,6 +527,7 @@ mod tests {
client_data.base.peer.ed25519().clone(),
entry.base.peer.as_remote(),
entry.base.socket_addr,
entry.base.lp_version,
);
// 1. establish mock connection between client and gateway and retrieve gateway's handle
@@ -506,7 +551,7 @@ mod tests {
let wg_keypair = client_data.base.x25519_wg_keys;
let gateway_identity = entry.base.peer.ed25519().public_key();
let registration_result = client
.register(
.register_dvpn(
&mut client_rng,
&wg_keypair,
gateway_identity,
@@ -520,7 +565,10 @@ mod tests {
let LpClientError::Transport(err) = registration_result else {
panic!("unexpected error");
};
assert_eq!(err, "Cannot register: handshake not completed");
assert_eq!(
err,
"State machine not available - has the handshake been completed?"
);
// 5. stop the gateway task and finish the test
entry.stop_tasks().await?;
@@ -544,6 +592,7 @@ mod tests {
client_data.base.peer.ed25519().clone(),
entry.base.peer.as_remote(),
entry.base.socket_addr,
entry.base.lp_version,
);
// START: ENTRY SETUP
@@ -570,11 +619,10 @@ mod tests {
// 1) peer registration - ip pair allocation
let entry_ip_pair = entry.allocate_ip_pair().await;
let reg_res = Ok::<_, nym_wireguard::Error>(entry_ip_pair);
let public_key = client_key.to_wg_key();
entry
.register_peer_controller_response(
PeerControlRequestType::RegisterPeer { public_key },
PeerControlRequestType::AllocatePeerIpPair {},
reg_res,
)
.await;
@@ -590,6 +638,16 @@ mod tests {
)
.await;
// 3) peer query - check for prior registrations
let query_res = Ok::<_, nym_wireguard::Error>(Option::<DefguardPeer>::None);
let key = client_key.to_wg_key();
entry
.register_peer_controller_response(
PeerControlRequestType::QueryPeer { key },
query_res,
)
.await;
// 5. spawn peer controller to be able to handle dvpn registration requests
entry.spawn_peer_controller();
@@ -612,10 +670,9 @@ mod tests {
// 1) peer registration - ip pair allocation
let exit_ip_pair = exit.allocate_ip_pair().await;
let reg_res = Ok::<_, nym_wireguard::Error>(exit_ip_pair);
let public_key = client_key.to_wg_key();
exit.register_peer_controller_response(
PeerControlRequestType::RegisterPeer { public_key },
PeerControlRequestType::AllocatePeerIpPair {},
reg_res,
)
.await;
@@ -630,6 +687,15 @@ mod tests {
)
.await;
// 3) peer query - check for prior registrations
let query_res = Ok::<_, nym_wireguard::Error>(Option::<DefguardPeer>::None);
let key = client_key.to_wg_key();
exit.register_peer_controller_response(
PeerControlRequestType::QueryPeer { key },
query_res,
)
.await;
// 11. spawn peer controller to be able to handle dvpn registration requests
exit.spawn_peer_controller();
@@ -642,11 +708,12 @@ mod tests {
exit.base.socket_addr.to_string(),
client_data.base.peer.ed25519().clone(),
exit.base.peer.as_remote(),
exit.base.lp_version,
);
// 13. Perform handshake and registration with exit gateway (all via entry forwarding)
let exit_registration_result = nested_session
.handshake_and_register(
.handshake_and_register_dvpn(
&mut entry_client,
&mut client_rng,
&client_data.base.x25519_wg_keys,
@@ -659,7 +726,7 @@ mod tests {
// 14. complete registration with the entry
let entry_registration_result = entry_client
.register(
.register_dvpn(
&mut client_rng,
&client_data.base.x25519_wg_keys,
entry.base.peer.ed25519().public_key(),
@@ -673,13 +740,11 @@ mod tests {
let peers_guard = entry.mock_peer_controller_state.peers.read().await;
let entry_peer = peers_guard.get_by_x25519_key(&client_key).unwrap().clone();
drop(peers_guard);
assert!(entry_peer.register_success);
assert!(entry_peer.add_success);
let peers_guard = exit.mock_peer_controller_state.peers.read().await;
let exit_peer = peers_guard.get_by_x25519_key(&client_key).unwrap().clone();
drop(peers_guard);
assert!(exit_peer.register_success);
assert!(exit_peer.add_success);
assert_eq!(entry_registration_result.private_ipv4, entry_ip_pair.ipv4);
@@ -1,32 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ?\n ",
"describe": {
"columns": [
{
"name": "epoch_id: u32",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "serialised_signatures",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "serialization_revision: u8",
"ordinal": 2,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM gateway_status\n JOIN gateway_details\n ON gateway_status.gateway_details_id = gateway_details.id\n WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?;\n ",
"describe": {
"columns": [
{
"name": "timestamp",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "reliability: u8",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true
]
},
"hash": "1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM emergency_credential\n WHERE id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "2f3c12cb0c48084b569e12ecb0315212a6f526f78258e173c96ec177988696ef"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM emergency_credential\n WHERE type = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "4a4f3b32b313f7fbc6eb579659e7cec1442967e53764b83ba0a66cd9a72494f9"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
"describe": {
"columns": [
{
@@ -22,5 +22,5 @@
false
]
},
"hash": "3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347"
"hash": "5d3b8ad051ab6f46c702308c2fc751a5ca340ac9c6dd86da1a5e9a3e65ea589f"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n d.node_id as \"node_id: NodeId\",\n CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as \"value: f32\"\n FROM\n gateway_details d\n JOIN\n gateway_status s on d.id = s.gateway_details_id\n WHERE\n timestamp >= ? AND\n timestamp <= ?\n GROUP BY 1\n ",
"describe": {
"columns": [
{
"name": "node_id: NodeId",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "value: f32",
"ordinal": 1,
"type_info": "Null"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
null
]
},
"hash": "676299beb2004ab89f7b38cf21ffb84ab5e7d7435297573523e2532560c2e302"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM global_expiration_date_signatures\n WHERE expiration_date = ?\n ",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
"describe": {
"columns": [
{
@@ -15,12 +15,12 @@
}
],
"parameters": {
"Right": 1
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be"
"hash": "697e6d738aecf115a3608139579b2d1d937dd4cc4778dfb42709adf08c1497f2"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM mixnode_status\n JOIN mixnode_details\n ON mixnode_status.mixnode_details_id = mixnode_details.id\n WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?;\n ",
"describe": {
"columns": [
{
"name": "timestamp",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "reliability: u8",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true
]
},
"hash": "73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e"
}
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM partial_expiration_date_signatures\n WHERE expiration_date = ?\n ",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM partial_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
"describe": {
"columns": [
{
@@ -15,12 +15,12 @@
}
],
"parameters": {
"Right": 1
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055"
"hash": "7e1829a17b81d7ee3dd8134b7a2be38ffdcaa672afbae44993365ca6910f68f6"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO emergency_credential\n (type, content, expiration)\n VALUES (?, ?, ?)\n ON CONFLICT(type, content) DO NOTHING;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "805ad4f26e0234d7f482a263e186156311713d2e9f69d39c868cd16296b56326"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT identity FROM gateway_details WHERE node_id = ?",
"describe": {
"columns": [
{
"name": "identity",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n d.mix_id as \"mix_id: NodeId\",\n AVG(s.reliability) as \"value: f32\"\n FROM\n mixnode_details d\n JOIN\n mixnode_status s on d.id = s.mixnode_details_id\n WHERE\n timestamp >= ? AND\n timestamp <= ?\n GROUP BY 1\n ",
"describe": {
"columns": [
{
"name": "mix_id: NodeId",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "value: f32",
"ordinal": 1,
"type_info": "Null"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
null
]
},
"hash": "c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166"
}
@@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use strum_macros::{Display, EnumString};
use thiserror::Error;
use utoipa::ToSchema;
// to whoever is thinking of modifying this struct.
@@ -211,17 +212,57 @@ pub struct LewesProtocolDetailsV1 {
/// Digests of the KEM keys available to this node alongside hashing algorithms used
/// for their computation.
pub kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, Vec<u8>>>,
/// note: digests are hex encoded
pub kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, String>>,
/// Digests of the signing keys available to this node alongside hashing algorithms used
/// for their computation.
pub signing_keys: HashMap<LPSignatureScheme, HashMap<LPHashFunction, Vec<u8>>>,
/// note: digests are hex encoded
pub signing_keys: HashMap<LPSignatureScheme, HashMap<LPHashFunction, String>>,
}
impl LewesProtocolDetailsV1 {
fn decode_digests(
digests: &HashMap<LPHashFunction, String>,
) -> Result<HashMap<HashFunction, Vec<u8>>, MalformedLPData> {
let mut kem_digests = HashMap::new();
for (hash_function, digest) in digests {
let digest = hex::decode(digest).map_err(|source| MalformedLPData::MalformedHash {
value: digest.to_string(),
source,
})?;
kem_digests.insert((*hash_function).try_into()?, digest);
}
Ok(kem_digests)
}
pub fn kem_keys(
&self,
) -> Result<HashMap<KEM, HashMap<HashFunction, Vec<u8>>>, MalformedLPData> {
let mut kem_keys = HashMap::new();
for (kem, digests) in &self.kem_keys {
let kem_digests = Self::decode_digests(digests)?;
kem_keys.insert((*kem).try_into()?, kem_digests);
}
Ok(kem_keys)
}
pub fn signing_keys(
&self,
) -> Result<HashMap<SignatureScheme, HashMap<HashFunction, Vec<u8>>>, MalformedLPData> {
let mut signing_keys = HashMap::new();
for (signature_scheme, digests) in &self.signing_keys {
let kem_digests = Self::decode_digests(digests)?;
signing_keys.insert((*signature_scheme).try_into()?, kem_digests);
}
Ok(signing_keys)
}
}
/// Convert map of digests from `nym_node_requests` types into `nym-api-requests` types
fn translate_digests(
digests: HashMap<nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction, Vec<u8>>,
) -> HashMap<LPHashFunction, Vec<u8>> {
digests: HashMap<nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction, String>,
) -> HashMap<LPHashFunction, String> {
digests
.into_iter()
.map(|(hash_fn, digest)| (hash_fn.into(), digest))
@@ -244,6 +285,7 @@ fn translate_digests(
ToSchema,
)]
#[strum(serialize_all = "lowercase")]
#[non_exhaustive]
pub enum LPKEM {
MlKem768,
XWing,
@@ -267,6 +309,7 @@ pub enum LPKEM {
ToSchema,
)]
#[strum(serialize_all = "lowercase")]
#[non_exhaustive]
pub enum LPHashFunction {
Blake3,
Shake128,
@@ -290,6 +333,7 @@ pub enum LPHashFunction {
ToSchema,
)]
#[strum(serialize_all = "lowercase")]
#[non_exhaustive]
pub enum LPSignatureScheme {
Ed25519,
}
@@ -469,32 +513,59 @@ impl From<nym_node_requests::api::v1::lewes_protocol::models::LPSignatureScheme>
}
}
impl From<LPKEM> for KEM {
fn from(value: LPKEM) -> Self {
#[derive(Debug, Error)]
pub enum MalformedLPData {
#[error("{value} does not correspond to any valid LP KEM")]
UnknownLpKEM { value: LPKEM },
#[error("{value} does not correspond to any valid LP Signature Scheme")]
UnknownLpSignatureScheme { value: LPSignatureScheme },
#[error("{value} does not correspond to any valid LP Hash Function")]
UnknownLpHashFunction { value: LPHashFunction },
#[error("{value} is not a valid hex encoded hash: {source}")]
MalformedHash {
value: String,
source: hex::FromHexError,
},
}
impl TryFrom<LPKEM> for KEM {
type Error = MalformedLPData;
fn try_from(value: LPKEM) -> Result<Self, Self::Error> {
match value {
LPKEM::MlKem768 => KEM::MlKem768,
LPKEM::XWing => KEM::XWing,
LPKEM::X25519 => KEM::X25519,
LPKEM::McEliece => KEM::McEliece,
LPKEM::MlKem768 => Ok(KEM::MlKem768),
LPKEM::XWing => Ok(KEM::XWing),
LPKEM::X25519 => Ok(KEM::X25519),
LPKEM::McEliece => Ok(KEM::McEliece),
// TODO: for backwards compatibility once variants within the LP crate change
// other => Err(MalformedLPData::UnknownLpKEM { value: other }),
}
}
}
impl From<LPHashFunction> for HashFunction {
fn from(value: LPHashFunction) -> Self {
impl TryFrom<LPHashFunction> for HashFunction {
type Error = MalformedLPData;
fn try_from(value: LPHashFunction) -> Result<Self, Self::Error> {
match value {
LPHashFunction::Blake3 => HashFunction::Blake3,
LPHashFunction::Shake128 => HashFunction::Shake128,
LPHashFunction::Shake256 => HashFunction::Shake256,
LPHashFunction::Sha256 => HashFunction::SHA256,
LPHashFunction::Blake3 => Ok(HashFunction::Blake3),
LPHashFunction::Shake128 => Ok(HashFunction::Shake128),
LPHashFunction::Shake256 => Ok(HashFunction::Shake256),
LPHashFunction::Sha256 => Ok(HashFunction::SHA256),
// TODO: for backwards compatibility once variants within the LP crate change
// other => Err(MalformedLPData::UnknownLpHashFunction { value: other }),
}
}
}
impl From<LPSignatureScheme> for SignatureScheme {
fn from(value: LPSignatureScheme) -> Self {
impl TryFrom<LPSignatureScheme> for SignatureScheme {
type Error = MalformedLPData;
fn try_from(value: LPSignatureScheme) -> Result<Self, Self::Error> {
match value {
LPSignatureScheme::Ed25519 => SignatureScheme::Ed25519,
LPSignatureScheme::Ed25519 => Ok(SignatureScheme::Ed25519),
// TODO: for backwards compatibility once variants within the LP crate change
// other => Err(MalformedLPData::UnknownLpSignatureScheme { value: other }),
}
}
}
@@ -262,10 +262,16 @@ pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 {
let mut kem_hashes = std::collections::HashMap::new();
let mut signing_keys_hashes = std::collections::HashMap::new();
kem_hashes.insert(LPHashFunction::Sha256, vec![(seed % 256) as u8; 32]);
kem_hashes.insert(
LPHashFunction::Sha256,
hex::encode([(seed % 256) as u8; 32]),
);
kem_hashes_wrapper.insert(LPKEM::X25519, kem_hashes);
signing_keys_hashes.insert(LPHashFunction::Sha256, vec![(seed % 256) as u8; 32]);
signing_keys_hashes.insert(
LPHashFunction::Sha256,
hex::encode([(seed % 256) as u8; 32]),
);
signing_keys_hashes_wrapper.insert(LPSignatureScheme::Ed25519, signing_keys_hashes);
NymNodeDescriptionV2 {
@@ -1,27 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO transaction\n (hash, height, index, success, messages, memo, signatures, signer_infos, fee, gas_wanted, gas_used, raw_log, logs, events)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT (hash) DO UPDATE\n SET height = excluded.height,\n index = excluded.index,\n success = excluded.success,\n messages = excluded.messages,\n memo = excluded.memo,\n signatures = excluded.signatures,\n signer_infos = excluded.signer_infos,\n fee = excluded.fee,\n gas_wanted = excluded.gas_wanted,\n gas_used = excluded.gas_used,\n raw_log = excluded.raw_log,\n logs = excluded.logs,\n events = excluded.events\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Int4",
"Bool",
"Jsonb",
"Text",
"TextArray",
"Jsonb",
"Jsonb",
"Int8",
"Int8",
"Text",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO validator (consensus_address, consensus_pubkey)\n VALUES ($1, $2)\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM pre_commit WHERE height < $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT height\n FROM block\n ORDER BY height ASC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "height",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE metadata SET last_processed_height = GREATEST(last_processed_height, $1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f"
}

Some files were not shown because too many files have changed in this diff Show More