Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49faa13855 | |||
| 51e5e7825d | |||
| ded23a6271 | |||
| e2d29f184d | |||
| a151a03181 | |||
| b19e82d4f7 | |||
| 88a4633bc4 | |||
| 660eff45dc | |||
| d4882ca276 | |||
| cfcf804b47 | |||
| b6d22abc01 | |||
| bd755385ed | |||
| 940fb09ae4 | |||
| 47af0b24f0 | |||
| 52edfdcc2f | |||
| af04afbe5e | |||
| 63f158cccb | |||
| b4aee7a1d9 | |||
| c55b215b65 | |||
| 7e8faf0ec6 | |||
| 0082b9fc50 | |||
| e16a337354 | |||
| cd0881462b |
@@ -10,6 +10,7 @@ on:
|
||||
- 'nym-api/**'
|
||||
- 'nym-authenticator-client/**'
|
||||
- 'nym-credential-proxy/**'
|
||||
- 'nym-gateway-probe/**'
|
||||
- 'nym-ip-packet-client/**'
|
||||
- 'nym-network-monitor/**'
|
||||
- 'nym-node/**'
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--allow-branch ${{ github.ref_name }} \
|
||||
--no-git-commit \
|
||||
|
||||
# Dry run may show cascading dependency errors because packages aren't
|
||||
# actually uploaded - these are expected and ignored. We check for real
|
||||
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
|
||||
- name: Publish (dry run)
|
||||
run: |
|
||||
output=$(cargo workspaces publish --dry-run --allow-dirty 2>&1) || true
|
||||
echo "$output"
|
||||
|
||||
# Check for real errors (not cascading dependency errors)
|
||||
# Cascading errors mention "crates.io index", real errors mention "Cargo.toml"
|
||||
echo "$output" | grep -i "Cargo.toml" && exit 1 || true
|
||||
|
||||
# Show the list of packages published
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,59 @@
|
||||
# This is in case, for whatever reason, a publication run fails, and we need to restart halfway down the list, of unbumped/unpublished crates.
|
||||
name: Resume crates.io publish
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
CRATES=$(cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2)
|
||||
|
||||
if [ -z "$CRATES" ]; then
|
||||
echo "Error: No crates found after '${{ inputs.resume_after }}'"
|
||||
echo "Check the crate name matches exactly from 'cargo workspaces plan'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Will publish the following crates:"
|
||||
echo "$CRATES"
|
||||
echo ""
|
||||
|
||||
echo "$CRATES" | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting ${{ inputs.publish_interval }}s before next publish..."
|
||||
sleep ${{ inputs.publish_interval }}
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Publish crates to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_interval:
|
||||
description: "Seconds to wait between publishes (600 for first publish, 60 after)"
|
||||
required: false
|
||||
default: "600"
|
||||
type: string
|
||||
backup_author:
|
||||
description: "Second team member added as owner of the crate"
|
||||
required: false
|
||||
default: "jstuczyn"
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
cargo workspaces publish \
|
||||
--publish-as-is \
|
||||
--publish-interval ${{ inputs.publish_interval }}
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
|
||||
- name: Add team as crate owners
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM="github:nymtech:core"
|
||||
echo "Checking and adding $TEAM as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM"; then
|
||||
echo " $TEAM already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM as owner of $crate..."
|
||||
cargo owner --add "$TEAM" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
|
||||
- name: Add secondary member as crate owner
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
TEAM_MEMBER="${{ inputs.backup_author }}"
|
||||
echo "Checking and adding $TEAM_MEMBER as owner to workspace crates..."
|
||||
|
||||
cargo workspaces list | while read crate; do
|
||||
echo "Checking $crate..."
|
||||
|
||||
if cargo owner --list "$crate" 2>/dev/null | grep -q "$TEAM_MEMBER"; then
|
||||
echo " $TEAM_MEMBER already owns $crate, skipping"
|
||||
else
|
||||
echo " Adding $TEAM_MEMBER as owner of $crate..."
|
||||
cargo owner --add "$TEAM_MEMBER" "$crate"
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Bump crate versions
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to set (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CI_BOT_AUTHOR: "Nym bot"
|
||||
CI_BOT_EMAIL: "nym-bot@users.noreply.github.com"
|
||||
|
||||
jobs:
|
||||
version-bump:
|
||||
runs-on: arc-linux-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.name "${{ env.CI_BOT_AUTHOR }}"
|
||||
git config --global user.email "${{ env.CI_BOT_EMAIL }}"
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
if ! npx semver "${{ inputs.version }}"; then
|
||||
echo "Error: '${{ inputs.version }}' is not valid semver"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
VERSION=$(grep -oP '^\s*version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+' Cargo.toml | head -1)
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version custom ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--yes
|
||||
|
||||
- name: Commit and push version bump
|
||||
run: |
|
||||
git add -A
|
||||
git commit -m "crates release: bump version to ${{ inputs.version }}"
|
||||
git push
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
@@ -1,43 +0,0 @@
|
||||
name: Publish to crates.io (dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-dry-run:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-commit \
|
||||
--no-git-tag \
|
||||
--no-git-push \
|
||||
--yes
|
||||
|
||||
# Note: Dry run may show cascading dependency errors because packages
|
||||
# aren't actually uploaded. Check if the missing dependency has an
|
||||
# "aborting upload due to dry run" message earlier in the output - if so,
|
||||
# it would succeed in a real publish since cargo-workspaces publishes in
|
||||
# dependency order. cargo-workspaces doesn't fail on err, so there isn't
|
||||
# a good way to check this at the moment.
|
||||
- name: Publish (dry run)
|
||||
run: cargo workspaces publish --from-git --dry-run --allow-dirty
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 1.21.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# - name: Configure git
|
||||
# run: |
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
cargo workspaces version ${{ inputs.version }} \
|
||||
--no-git-push \
|
||||
--no-git-tag \
|
||||
--yes
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo workspaces publish --from-git --no-git-commit
|
||||
|
||||
# - name: Push version commit
|
||||
# run: |
|
||||
# git push origin HEAD
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Resume publish to crates.io
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
resume_after:
|
||||
description: "Last successfully published crate (will start from the next one)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: arc-linux-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
- name: Publish remaining crates
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
# Get crates in publish order, skip up to and including resume_after
|
||||
cargo workspaces plan 2>/dev/null | sed -n '/^${{ inputs.resume_after }}$/,$p' | tail -n +2 | while read crate; do
|
||||
echo "Publishing $crate..."
|
||||
cargo publish -p "$crate" --allow-dirty
|
||||
echo "Waiting 600s before next publish..."
|
||||
sleep 600
|
||||
done
|
||||
|
||||
- name: Show package versions
|
||||
run: cargo workspaces list --long
|
||||
Generated
+327
-171
File diff suppressed because it is too large
Load Diff
+102
-101
@@ -205,7 +205,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.85"
|
||||
readme = "README.md"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -303,6 +303,7 @@ ledger-transport = "0.10.0"
|
||||
ledger-transport-hid = "0.10.0"
|
||||
log = "0.4"
|
||||
mime = "0.3.17"
|
||||
mock_instant = "0.6.0"
|
||||
moka = { version = "0.12", features = ["future"] }
|
||||
nix = "0.30.1"
|
||||
notify = "5.1.0"
|
||||
@@ -324,7 +325,7 @@ rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
@@ -380,7 +381,7 @@ url = "2.5"
|
||||
utoipa = "5.2"
|
||||
utoipa-swagger-ui = "8.1"
|
||||
utoipauto = "0.2"
|
||||
uuid = "*"
|
||||
uuid = "1.19.0"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
@@ -390,106 +391,106 @@ zeroize = "1.7.0"
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.1", path = "common/nym-id" }
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.20.4", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
|
||||
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.1", path = "common/task" }
|
||||
nym-tun = { version = "1.20.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.1", path = "common/topology" }
|
||||
nym-types = { version = "1.20.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
||||
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.20.4", path = "common/task" }
|
||||
nym-tun = { version = "1.20.4", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.20.4", path = "common/topology" }
|
||||
nym-types = { version = "1.20.4", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.20.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
|
||||
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.69"
|
||||
version = "1.1.70"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.69"
|
||||
version = "1.1.70"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -18,6 +18,7 @@ mod util;
|
||||
mod version;
|
||||
|
||||
pub use error::Error;
|
||||
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
pub use v6 as latest;
|
||||
pub use version::AuthenticatorVersion;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::traits::{
|
||||
TopUpBandwidthResponse, UpgradeModeStatus,
|
||||
};
|
||||
use crate::{v2, v3, v4, v5, v6};
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthenticatorResponse {
|
||||
@@ -17,6 +18,17 @@ pub enum AuthenticatorResponse {
|
||||
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
pub struct SerialisedResponse {
|
||||
pub bytes: Vec<u8>,
|
||||
pub reply_to: Option<Recipient>,
|
||||
}
|
||||
|
||||
impl SerialisedResponse {
|
||||
pub fn new(bytes: Vec<u8>, reply_to: Option<Recipient>) -> Self {
|
||||
Self { bytes, reply_to }
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeModeStatus for AuthenticatorResponse {
|
||||
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
|
||||
match self {
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_network_defaults::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
pub fn authenticator_ipv6_to_ipv4(addr: Ipv6Addr) -> Ipv4Addr {
|
||||
let before_last_byte = addr.octets()[14];
|
||||
let last_byte = addr.octets()[15];
|
||||
|
||||
Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authenticator_ipv4_to_ipv6(addr: Ipv4Addr) -> Ipv6Addr {
|
||||
let before_last_byte = addr.octets()[2];
|
||||
let last_byte = addr.octets()[3];
|
||||
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -56,27 +56,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -54,27 +54,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::BandwidthClaim;
|
||||
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::time::SystemTime;
|
||||
use std::{fmt, ops::Deref, str::FromStr};
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
@@ -20,7 +19,6 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
|
||||
use sha2::Sha256;
|
||||
|
||||
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
|
||||
pub type PrivateIPs = HashMap<IpPair, SystemTime>;
|
||||
|
||||
#[cfg(feature = "verify")]
|
||||
pub type HmacSha256 = Hmac<Sha256>;
|
||||
@@ -53,27 +51,11 @@ impl fmt::Display for IpPair {
|
||||
|
||||
impl From<IpAddr> for IpPair {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
let (before_last_byte, last_byte) = match value {
|
||||
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
||||
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
||||
let (ipv4, ipv6) = match value {
|
||||
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
|
||||
};
|
||||
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
|
||||
let ipv4 = Ipv4Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
|
||||
before_last_byte,
|
||||
last_byte,
|
||||
);
|
||||
let ipv6 = Ipv6Addr::new(
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
|
||||
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
|
||||
last_bytes,
|
||||
);
|
||||
|
||||
IpPair::new(ipv4, ipv6)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ features = ["json"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
|
||||
workspace = true
|
||||
features = ["json", "rustls-tls"]
|
||||
features = ["json", "rustls"]
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -22,6 +22,8 @@ pub mod ecash;
|
||||
pub mod error;
|
||||
pub mod upgrade_mode;
|
||||
|
||||
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
|
||||
|
||||
// Histogram buckets for ecash verification duration (in seconds)
|
||||
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
|
||||
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0];
|
||||
@@ -111,6 +113,13 @@ impl CredentialVerifier {
|
||||
}
|
||||
|
||||
pub async fn verify(&mut self) -> Result<i64> {
|
||||
if self.ecash_verifier.is_mock() {
|
||||
// if we're in the mock mode (local testing), skip cryptographic verification
|
||||
// and just return a dummy bandwidth value since we don't have blockchain access
|
||||
// Return a reasonable test bandwidth value (e.g., 1GB in bytes)
|
||||
return Ok(MOCK_BANDWIDTH);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
nym_metrics::inc!("ecash_verification_attempts");
|
||||
|
||||
|
||||
@@ -291,3 +291,40 @@ struct UpgradeModeStateInner {
|
||||
// (and dealing with the async consequences of that)
|
||||
status: UpgradeModeStatus,
|
||||
}
|
||||
|
||||
pub mod testing {
|
||||
use crate::UpgradeModeState;
|
||||
use crate::upgrade_mode::{
|
||||
CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
|
||||
};
|
||||
use futures::channel::mpsc::UnboundedReceiver;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn mock_dummy_upgrade_mode_details() -> (UpgradeModeDetails, UnboundedReceiver<CheckRequest>)
|
||||
{
|
||||
let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded();
|
||||
|
||||
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(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap();
|
||||
private_key.public_key()
|
||||
}
|
||||
|
||||
let upgrade_mode_state = UpgradeModeState::new(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(),
|
||||
);
|
||||
(upgrade_mode_details, um_recheck_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
|
||||
async-trait = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
cfg-if = { workspace = true}
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls"] }
|
||||
http.workspace = true
|
||||
url = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
@@ -552,7 +552,7 @@ mod test {
|
||||
let var_name = HickoryDnsResolver::default();
|
||||
let resolver = var_name;
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.dns_resolver(resolver.into())
|
||||
.dns_resolver(resolver)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
# of env files and requires workspace context
|
||||
exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "nym_kcp"
|
||||
|
||||
@@ -757,12 +757,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forward_packet_encode_decode_roundtrip() {
|
||||
fn test_forward_packet_encode_decode_roundtrip_v4() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let forward_data = crate::message::ForwardPacketData {
|
||||
target_gateway_identity: [77u8; 32],
|
||||
target_lp_address: "1.2.3.4:41264".to_string(),
|
||||
target_lp_address: "1.2.3.4:41264".parse().unwrap(),
|
||||
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
|
||||
};
|
||||
|
||||
@@ -789,7 +789,50 @@ mod tests {
|
||||
|
||||
if let LpMessage::ForwardPacket(data) = decoded.message {
|
||||
assert_eq!(data.target_gateway_identity, [77u8; 32]);
|
||||
assert_eq!(data.target_lp_address, "1.2.3.4:41264");
|
||||
assert_eq!(data.target_lp_address, "1.2.3.4:41264".parse().unwrap());
|
||||
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
|
||||
} else {
|
||||
panic!("Expected ForwardPacket message");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forward_packet_encode_decode_roundtrip_v6() {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let forward_data = crate::message::ForwardPacketData {
|
||||
target_gateway_identity: [77u8; 32],
|
||||
target_lp_address: "[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap(),
|
||||
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 999,
|
||||
counter: 555,
|
||||
},
|
||||
message: LpMessage::ForwardPacket(forward_data),
|
||||
trailer: [0xff; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize
|
||||
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||
|
||||
// Parse back
|
||||
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||
|
||||
// Verify LP protocol handling works correctly
|
||||
assert_eq!(decoded.header.receiver_idx, 999);
|
||||
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
|
||||
|
||||
if let LpMessage::ForwardPacket(data) = decoded.message {
|
||||
assert_eq!(data.target_gateway_identity, [77u8; 32]);
|
||||
assert_eq!(
|
||||
data.target_lp_address,
|
||||
"[dead:beef:4242:c0ff:ee00::1111]:41264".parse().unwrap()
|
||||
);
|
||||
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
|
||||
} else {
|
||||
panic!("Expected ForwardPacket message");
|
||||
|
||||
@@ -9,6 +9,7 @@ use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -229,9 +230,8 @@ pub struct ForwardPacketData {
|
||||
/// Target gateway's Ed25519 identity (32 bytes)
|
||||
pub target_gateway_identity: [u8; 32],
|
||||
|
||||
// TODO: replace it with `SocketAddr`
|
||||
/// Target gateway's LP address (IP:port string)
|
||||
pub target_lp_address: String,
|
||||
pub target_lp_address: SocketAddr,
|
||||
|
||||
/// Complete inner LP packet bytes (serialized LpPacket)
|
||||
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
||||
@@ -241,7 +241,7 @@ pub struct ForwardPacketData {
|
||||
impl ForwardPacketData {
|
||||
pub fn new(
|
||||
target_gateway_identity: ed25519::PublicKey,
|
||||
target_lp_address: String,
|
||||
target_lp_address: SocketAddr,
|
||||
inner_packet_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
ForwardPacketData {
|
||||
@@ -254,20 +254,31 @@ impl ForwardPacketData {
|
||||
fn len(&self) -> usize {
|
||||
// 32 bytes target gateway identity
|
||||
// +
|
||||
// 4 bytes length of target lp address
|
||||
// 1 byte length of target lp address type
|
||||
// +
|
||||
// target_lp_address.len()
|
||||
// {4,16} target_lp_address IPv{4,6}
|
||||
// +
|
||||
// 2 bytes target_lp_address port
|
||||
// +
|
||||
// 4 bytes of length of inner packet bytes
|
||||
// +
|
||||
// inner_packet_bytes.len()
|
||||
32 + 4 + self.target_lp_address.len() + 4 + self.inner_packet_bytes.len()
|
||||
match self.target_lp_address {
|
||||
SocketAddr::V4(_) => 32 + 1 + 4 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
SocketAddr::V6(_) => 32 + 1 + 16 + 2 + 4 + self.inner_packet_bytes.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
|
||||
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
|
||||
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
|
||||
};
|
||||
|
||||
dst.put_slice(&self.target_gateway_identity);
|
||||
dst.put_u16_le(self.target_lp_address.len() as u16);
|
||||
dst.put_slice(self.target_lp_address.as_bytes());
|
||||
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
|
||||
dst.put_slice(&ip_bytes); // IP bytes
|
||||
dst.put_u16_le(self.target_lp_address.port()); // Port
|
||||
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
|
||||
dst.put_slice(&self.inner_packet_bytes);
|
||||
}
|
||||
@@ -279,42 +290,56 @@ impl ForwardPacketData {
|
||||
}
|
||||
|
||||
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
// smallest possible packet with empty address and empty data
|
||||
if bytes.len() < 38 {
|
||||
// smallest possible packet with ipv4 and empty data
|
||||
if bytes.len() < 43 {
|
||||
// 32 + 1 + 4 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData[1]. got {}",
|
||||
"Too few bytes to deserialise ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let target_gateway_identity = bytes[0..32].try_into().unwrap();
|
||||
let target_lp_address_len = u16::from_le_bytes([bytes[32], bytes[33]]);
|
||||
let target_lp_address_is_ipv6 = bytes[32] != 0;
|
||||
|
||||
// smallest possible packet with empty data
|
||||
if bytes[34..].len() < 4 + target_lp_address_len as usize {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ForwardPacketData[2]. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
|
||||
// IPv6, first check we have actually enough bytes
|
||||
// smallest possible packet with ipv6 and empty data
|
||||
if bytes.len() < 55 {
|
||||
// 32 + 1 + 16 + 2 + 4 + 0
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[49], bytes[50]]);
|
||||
(SocketAddr::new(ipv6, port), 51)
|
||||
} else {
|
||||
// IPv4. Length check done at the start
|
||||
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap()));
|
||||
let port = u16::from_le_bytes([bytes[37], bytes[38]]);
|
||||
(SocketAddr::new(ipv4, port), 39)
|
||||
};
|
||||
|
||||
let target_lp_address =
|
||||
String::from_utf8_lossy(&bytes[34..34 + target_lp_address_len as usize]).to_string();
|
||||
let inner_packet_bytes_len = u32::from_le_bytes([
|
||||
bytes[34 + target_lp_address_len as usize],
|
||||
bytes[34 + target_lp_address_len as usize + 1],
|
||||
bytes[34 + target_lp_address_len as usize + 2],
|
||||
bytes[34 + target_lp_address_len as usize + 3],
|
||||
bytes[next_index],
|
||||
bytes[next_index + 1],
|
||||
bytes[next_index + 2],
|
||||
bytes[next_index + 3],
|
||||
]);
|
||||
if bytes[34 + target_lp_address_len as usize + 4..].len() != inner_packet_bytes_len as usize
|
||||
{
|
||||
if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
|
||||
bytes[34 + target_lp_address_len as usize + 4..].len()
|
||||
bytes[next_index + 4..].len()
|
||||
)));
|
||||
}
|
||||
let inner_packet_bytes = bytes[34 + target_lp_address_len as usize + 4..].to_vec();
|
||||
let inner_packet_bytes = bytes[next_index + 4..].to_vec();
|
||||
|
||||
Ok(ForwardPacketData {
|
||||
target_gateway_identity,
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::{
|
||||
};
|
||||
use bytes::{Buf, Bytes};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
use std::mem;
|
||||
use tracing::debug;
|
||||
|
||||
@@ -364,33 +365,44 @@ impl LpStateMachine {
|
||||
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
|
||||
LpState::KKTExchange { session }
|
||||
} else {
|
||||
use crate::message::LpMessage;
|
||||
|
||||
// Packet message is already parsed, match on it directly
|
||||
match &packet.message {
|
||||
LpMessage::KKTRequest(kkt_request) if !session.is_initiator() => {
|
||||
// Responder processes KKT request
|
||||
// Convert X25519 public key to KEM format for KKT response
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
|
||||
// Get local X25519 public key by deriving from private key
|
||||
let local_x25519_public = session.local_x25519_public();
|
||||
// Get local KEM key
|
||||
match session.get_kem_key_handle() {
|
||||
Err(err) => {
|
||||
let reason = err.to_string();
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
Ok(local_kem_psq_public) => {
|
||||
// Convert to libcrux KEM public key
|
||||
// V1 is X255519
|
||||
match libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
local_kem_psq_public.as_bytes(),
|
||||
) {
|
||||
Ok(libcrux_public_key) => {
|
||||
let responder_kem_pk = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
// Convert to libcrux KEM public key
|
||||
match libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
local_x25519_public.as_bytes(),
|
||||
) {
|
||||
Ok(libcrux_public_key) => {
|
||||
let responder_kem_pk = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
match session.process_kkt_request(&kkt_request.0, &responder_kem_pk) {
|
||||
Ok(kkt_response_message) => {
|
||||
match session.next_packet(kkt_response_message) {
|
||||
Ok(response_packet) => {
|
||||
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
|
||||
// After KKT exchange, move to Handshaking
|
||||
LpState::Handshaking { session }
|
||||
match session.process_kkt_request(&kkt_request.0, &responder_kem_pk) {
|
||||
Ok(kkt_response_message) => {
|
||||
match session.next_packet(kkt_response_message) {
|
||||
Ok(response_packet) => {
|
||||
result_action = Some(Ok(LpAction::SendPacket(response_packet)));
|
||||
// After KKT exchange, move to Handshaking
|
||||
LpState::Handshaking { session }
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
@@ -400,18 +412,13 @@ impl LpStateMachine {
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
result_action = Some(Err(e));
|
||||
let reason = format!("Failed to convert X25519 to KEM: {:?}", e);
|
||||
let err = LpError::Internal(reason.clone());
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let reason = format!("Failed to convert X25519 to KEM: {:?}", e);
|
||||
let err = LpError::Internal(reason.clone());
|
||||
result_action = Some(Err(err));
|
||||
LpState::Closed { reason }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
LpMessage::KKTResponse(kkt_response) if session.is_initiator() => {
|
||||
|
||||
@@ -27,10 +27,27 @@ pub struct NymNodeInformation {
|
||||
pub version: AuthenticatorVersion,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WireguardRegistrationData {
|
||||
/// Public x25519 key of this gateway
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
pub public_key: x25519::PublicKey,
|
||||
|
||||
/// Port at which this gateway is accessible for wireguard
|
||||
pub port: u16,
|
||||
|
||||
/// Ipv4 address assigned to this peer
|
||||
pub private_ipv4: Ipv4Addr,
|
||||
|
||||
/// Ipv6 address assigned to this peer
|
||||
pub private_ipv6: Ipv6Addr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WireguardConfiguration {
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
pub public_key: x25519::PublicKey,
|
||||
pub psk: Option<[u8; 32]>,
|
||||
pub endpoint: SocketAddr,
|
||||
pub private_ipv4: Ipv4Addr,
|
||||
pub private_ipv6: Ipv6Addr,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//! LP (Lewes Protocol) registration message types shared between client and gateway.
|
||||
|
||||
use crate::WireguardConfiguration;
|
||||
use crate::WireguardRegistrationData;
|
||||
use crate::dvpn::{
|
||||
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
|
||||
LpDvpnRegistrationRequestMessage, LpDvpnRegistrationRequestMessageContent,
|
||||
@@ -16,8 +16,6 @@ use crate::mixnet::{
|
||||
};
|
||||
use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer};
|
||||
use nym_authenticator_requests::models::BandwidthClaim;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
|
||||
@@ -133,25 +131,14 @@ impl LpRegistrationRequest {
|
||||
}
|
||||
|
||||
/// Create new dVPN registration initialisation request
|
||||
pub fn new_initial_dvpn<R>(
|
||||
rng: &mut R,
|
||||
pub fn new_initial_dvpn(
|
||||
wg_public_key: nym_wireguard_types::PeerPublicKey,
|
||||
ticket_type: TicketType,
|
||||
) -> Self
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let mut psk = [0u8; 32];
|
||||
rng.fill_bytes(&mut psk);
|
||||
|
||||
psk: [u8; 32],
|
||||
) -> Self {
|
||||
Self::new(LpRegistrationRequestData::Dvpn {
|
||||
data: Box::new(LpDvpnRegistrationRequestMessage {
|
||||
content: LpDvpnRegistrationRequestMessageContent::InitialRequest(
|
||||
LpDvpnRegistrationInitialRequest {
|
||||
wg_public_key,
|
||||
psk,
|
||||
ticket_type,
|
||||
},
|
||||
LpDvpnRegistrationInitialRequest { wg_public_key, psk },
|
||||
),
|
||||
}),
|
||||
})
|
||||
@@ -187,7 +174,7 @@ impl LpRegistrationRequest {
|
||||
|
||||
impl LpRegistrationResponse {
|
||||
/// Create a success response with GatewayData (for dVPN mode)
|
||||
pub fn success_dvpn(config: WireguardConfiguration, available_bandwidth: i64) -> Self {
|
||||
pub fn success_dvpn(config: WireguardRegistrationData, upgrade_mode: bool) -> Self {
|
||||
Self {
|
||||
status: RegistrationStatus::Completed,
|
||||
response_data: LpRegistrationResponseData::Dvpn {
|
||||
@@ -195,7 +182,7 @@ impl LpRegistrationResponse {
|
||||
content: LpDvpnRegistrationResponseMessageContent::CompletedRegistration(
|
||||
dvpn::CompletedRegistrationResponse {
|
||||
config,
|
||||
available_bandwidth,
|
||||
upgrade_mode,
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -203,16 +190,13 @@ impl LpRegistrationResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success_mixnet(config: LpMixnetGatewayData, available_bandwidth: i64) -> Self {
|
||||
pub fn success_mixnet(config: LpMixnetGatewayData) -> Self {
|
||||
Self {
|
||||
status: RegistrationStatus::Completed,
|
||||
response_data: LpRegistrationResponseData::Mixnet {
|
||||
data: LpMixnetRegistrationResponseMessage {
|
||||
content: LpMixnetRegistrationResponseMessageContent::CompletedRegistration(
|
||||
mixnet::CompletedRegistrationResponse {
|
||||
config,
|
||||
available_bandwidth,
|
||||
},
|
||||
mixnet::CompletedRegistrationResponse { config },
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -291,9 +275,8 @@ impl LpRegistrationResponse {
|
||||
}
|
||||
|
||||
pub mod dvpn {
|
||||
use crate::WireguardConfiguration;
|
||||
use crate::WireguardRegistrationData;
|
||||
use nym_authenticator_requests::models::BandwidthClaim;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// client
|
||||
@@ -317,9 +300,6 @@ pub mod dvpn {
|
||||
|
||||
/// 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)]
|
||||
@@ -357,10 +337,11 @@ pub mod dvpn {
|
||||
pub struct CompletedRegistrationResponse {
|
||||
/// Gateway configuration data for dVPN mode (WireGuard)
|
||||
/// This matches what WireguardRegistrationResult expects
|
||||
pub config: WireguardConfiguration,
|
||||
pub config: WireguardRegistrationData,
|
||||
|
||||
/// The bandwidth available to this client,
|
||||
pub available_bandwidth: i64,
|
||||
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
|
||||
/// (thus it will not meter bandwidth)
|
||||
pub upgrade_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
@@ -434,9 +415,6 @@ pub mod mixnet {
|
||||
///
|
||||
/// 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)]
|
||||
@@ -453,14 +431,14 @@ mod tests {
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
// ==================== Helper Functions ====================
|
||||
|
||||
fn create_test_gateway_data() -> WireguardConfiguration {
|
||||
WireguardConfiguration {
|
||||
fn create_test_wg_config() -> WireguardRegistrationData {
|
||||
WireguardRegistrationData {
|
||||
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(
|
||||
nym_sphinx::PublicKey::from([1u8; 32]),
|
||||
),
|
||||
port: 1234,
|
||||
private_ipv4: Ipv4Addr::new(10, 0, 0, 1),
|
||||
private_ipv6: Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1),
|
||||
endpoint: "192.168.1.1:8080".parse().expect("Valid test endpoint"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,10 +483,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_success_dvpn() {
|
||||
let cfg = create_test_gateway_data();
|
||||
let allocated_bandwidth = 500_000_000;
|
||||
let cfg = create_test_wg_config();
|
||||
|
||||
let response = LpRegistrationResponse::success_dvpn(cfg, allocated_bandwidth);
|
||||
let response = LpRegistrationResponse::success_dvpn(cfg, false);
|
||||
assert!(response.status.is_successful());
|
||||
|
||||
let LpRegistrationResponseData::Dvpn { data } = response.response_data else {
|
||||
@@ -521,7 +498,7 @@ mod tests {
|
||||
panic!("unexpected response")
|
||||
};
|
||||
assert_eq!(complete.config, cfg);
|
||||
assert_eq!(complete.available_bandwidth, allocated_bandwidth);
|
||||
assert!(!complete.upgrade_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -532,10 +509,7 @@ mod tests {
|
||||
let lp_gateway_data = LpMixnetGatewayData {
|
||||
gateway_identity: *valid_key.public_key(),
|
||||
};
|
||||
let allocated_bandwidth = 500_000_000;
|
||||
|
||||
let response =
|
||||
LpRegistrationResponse::success_mixnet(lp_gateway_data.clone(), allocated_bandwidth);
|
||||
let response = LpRegistrationResponse::success_mixnet(lp_gateway_data.clone());
|
||||
assert!(response.status.is_successful());
|
||||
|
||||
let LpRegistrationResponseData::Mixnet { data } = response.response_data else {
|
||||
@@ -548,6 +522,5 @@ mod tests {
|
||||
panic!("unexpected response")
|
||||
};
|
||||
assert_eq!(complete.config, lp_gateway_data);
|
||||
assert_eq!(complete.available_bandwidth, allocated_bandwidth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub enum ProtocolError {
|
||||
InvalidServiceProviderType(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum ServiceProviderType {
|
||||
NetworkRequester = 0,
|
||||
@@ -76,7 +76,7 @@ impl ServiceProviderTypeExt for u8 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Protocol {
|
||||
pub version: u8,
|
||||
pub service_provider_type: ServiceProviderType,
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::traits::Timeboxed;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use nym_bin_common::logging::tracing_subscriber::EnvFilter;
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
use nym_bin_common::logging::{default_tracing_fmt_layer, tracing_subscriber};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use std::future::Future;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::error::Elapsed;
|
||||
|
||||
use nym_bin_common::logging::tracing_subscriber::EnvFilter;
|
||||
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
|
||||
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
|
||||
use nym_bin_common::logging::{default_tracing_fmt_layer, tracing_subscriber};
|
||||
pub use rand_chacha::ChaCha20Rng as DeterministicRng;
|
||||
pub use rand_chacha::rand_core::{CryptoRng, RngCore};
|
||||
|
||||
pub fn leak<T>(val: T) -> &'static mut T {
|
||||
@@ -26,16 +26,16 @@ where
|
||||
tokio::spawn(async move { fut.timeboxed().await })
|
||||
}
|
||||
|
||||
pub fn deterministic_rng() -> ChaCha20Rng {
|
||||
pub fn deterministic_rng() -> DeterministicRng {
|
||||
seeded_rng([42u8; 32])
|
||||
}
|
||||
|
||||
pub fn seeded_rng(seed: [u8; 32]) -> ChaCha20Rng {
|
||||
ChaCha20Rng::from_seed(seed)
|
||||
pub fn seeded_rng(seed: [u8; 32]) -> DeterministicRng {
|
||||
DeterministicRng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn u64_seeded_rng(seed: u64) -> ChaCha20Rng {
|
||||
ChaCha20Rng::seed_from_u64(seed)
|
||||
pub fn u64_seeded_rng(seed: u64) -> DeterministicRng {
|
||||
DeterministicRng::seed_from_u64(seed)
|
||||
}
|
||||
|
||||
// test logger to use during debugging
|
||||
|
||||
@@ -13,7 +13,7 @@ description = "Functions and tests for checking Nym's Credential Proxy is being
|
||||
|
||||
[dependencies]
|
||||
jwt-simple = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
|
||||
@@ -26,7 +26,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
|
||||
fn from(req: &PeerControlRequest) -> Self {
|
||||
match req {
|
||||
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||
PeerControlRequest::AllocatePeerIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||
PeerControlRequest::PreAllocateIpPair { .. } => 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::CheckActivePeer { .. } => unreachable!(),
|
||||
PeerControlRequest::ReleaseIpPair { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -114,7 +115,7 @@ impl MockPeerControllerV2 {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
PeerControlRequest::AllocatePeerIpPair { response_tx, .. } => {
|
||||
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
|
||||
response_tx
|
||||
.send(
|
||||
*response
|
||||
@@ -186,6 +187,15 @@ impl MockPeerControllerV2 {
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
PeerControlRequest::CheckActivePeer { response_tx, .. } => {
|
||||
response_tx
|
||||
.send(
|
||||
*response
|
||||
.downcast()
|
||||
.expect("registered response has mismatched type"),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,10 @@ nym-wireguard-types = { workspace = true }
|
||||
nym-node-metrics = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
nym-gateway-storage = { workspace = true, features = ["mock"] }
|
||||
mock_instant = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::IpPoolError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
@@ -22,7 +24,7 @@ pub enum Error {
|
||||
SystemTime(#[from] std::time::SystemTimeError),
|
||||
|
||||
#[error("IP pool error: {0}")]
|
||||
IpPool(String),
|
||||
IpPool(#[from] IpPoolError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
// 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 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::{Duration, SystemTime};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::Instant;
|
||||
use std::time::Duration;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
mod compat;
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::Instant;
|
||||
#[cfg(not(test))]
|
||||
use std::time::Instant;
|
||||
|
||||
// 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 {
|
||||
@@ -57,17 +59,38 @@ impl Display for IpPair {
|
||||
pub enum AllocationState {
|
||||
/// IP is available for allocation
|
||||
Free,
|
||||
/// IP is allocated and in use, with timestamp of allocation
|
||||
Allocated(SystemTime),
|
||||
|
||||
/// The IP has been pre-allocated for a peer, but the corresponding registration has not yet been finalised
|
||||
PreAllocated { allocated_at: Instant },
|
||||
|
||||
/// IP is allocated and in use, with timestamp
|
||||
Allocated { allocated_at: Instant },
|
||||
}
|
||||
|
||||
impl AllocationState {
|
||||
pub fn is_free(&self) -> bool {
|
||||
matches!(self, AllocationState::Free)
|
||||
}
|
||||
|
||||
pub fn new_pre_allocated() -> Self {
|
||||
AllocationState::PreAllocated {
|
||||
allocated_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_allocated() -> Self {
|
||||
AllocationState::Allocated {
|
||||
allocated_at: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe IP address pool manager
|
||||
///
|
||||
/// Manages allocation of IPv4/IPv6 address pairs from configured CIDR ranges.
|
||||
/// Ensures collision-free allocation and supports stale cleanup.
|
||||
#[derive(Clone)]
|
||||
pub struct IpPool {
|
||||
allocations: Arc<RwLock<HashMap<IpPair, AllocationState>>>,
|
||||
allocations: HashMap<IpPair, AllocationState>,
|
||||
}
|
||||
|
||||
impl IpPool {
|
||||
@@ -98,7 +121,7 @@ impl IpPool {
|
||||
.iter()
|
||||
.filter_map(|ip| {
|
||||
if let IpAddr::V4(v4) = ip {
|
||||
Some(v4)
|
||||
if v4 != ipv4_network { Some(v4) } else { None }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -109,7 +132,7 @@ impl IpPool {
|
||||
.iter()
|
||||
.filter_map(|ip| {
|
||||
if let IpAddr::V6(v6) = ip {
|
||||
Some(v6)
|
||||
if v6 != ipv6_network { Some(v6) } else { None }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -129,21 +152,18 @@ impl IpPool {
|
||||
allocations.len(),
|
||||
);
|
||||
|
||||
Ok(IpPool {
|
||||
allocations: Arc::new(RwLock::new(allocations)),
|
||||
})
|
||||
Ok(IpPool { allocations })
|
||||
}
|
||||
|
||||
/// Allocate a free IP pair from the pool
|
||||
/// Preallocate a free IP pair from the pool
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IpPoolError::NoFreeIp` if no IPs are available
|
||||
pub async fn allocate(&self) -> Result<IpPair, IpPoolError> {
|
||||
let mut pool = self.allocations.write().await;
|
||||
|
||||
pub fn pre_allocate(&mut self) -> Result<IpPair, IpPoolError> {
|
||||
// Find a free IP and allocate it
|
||||
let assignment_start = Instant::now();
|
||||
let free_ip = pool
|
||||
let free_ip = self
|
||||
.allocations
|
||||
.iter_mut()
|
||||
.filter(|(_, state)| matches!(state, AllocationState::Free))
|
||||
.choose(&mut rand::thread_rng())
|
||||
@@ -155,18 +175,42 @@ impl IpPool {
|
||||
}
|
||||
|
||||
let ip_pair = *free_ip.0;
|
||||
*free_ip.1 = AllocationState::Allocated(SystemTime::now());
|
||||
*free_ip.1 = AllocationState::new_pre_allocated();
|
||||
|
||||
tracing::debug!("Allocated IP pair: {ip_pair}");
|
||||
Ok(ip_pair)
|
||||
}
|
||||
|
||||
pub fn confirm_allocation(&mut self, ip_pair: IpPair) -> Result<(), IpPoolError> {
|
||||
let Some(allocation) = self.allocations.get_mut(&ip_pair) else {
|
||||
return Err(IpPoolError::UnknownIpPair { ip_pair });
|
||||
};
|
||||
match allocation {
|
||||
AllocationState::Free => {
|
||||
// seems the IpPair has been released before the confirmation, but it has not yet been re-allocated
|
||||
warn!(
|
||||
"{ip_pair} seems to have already been released, but has not been allocated to a new peer yet"
|
||||
);
|
||||
*allocation = AllocationState::Allocated {
|
||||
allocated_at: Instant::now(),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
AllocationState::PreAllocated { allocated_at } => {
|
||||
*allocation = AllocationState::Allocated {
|
||||
allocated_at: *allocated_at,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
AllocationState::Allocated { .. } => Err(IpPoolError::AlreadyUsed { ip_pair }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Release an IP pair back to the pool
|
||||
///
|
||||
/// Marks the IP as free for future allocations.
|
||||
pub async fn release(&self, ip_pair: IpPair) {
|
||||
let mut pool = self.allocations.write().await;
|
||||
if let Some(state) = pool.get_mut(&ip_pair) {
|
||||
pub fn release(&mut self, ip_pair: IpPair) {
|
||||
if let Some(state) = self.allocations.get_mut(&ip_pair) {
|
||||
*state = AllocationState::Free;
|
||||
tracing::debug!("Released IP pair: {ip_pair}");
|
||||
}
|
||||
@@ -175,58 +219,67 @@ impl IpPool {
|
||||
/// Mark an IP pair as allocated (used during initialization from database)
|
||||
///
|
||||
/// This is used when restoring state from the database on gateway startup.
|
||||
pub async fn mark_used(&self, ip_pair: IpPair) {
|
||||
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}");
|
||||
} else {
|
||||
tracing::warn!("Attempted to mark unknown IP pair as used: {ip_pair}");
|
||||
pub fn mark_used(&mut self, ip_pair: IpPair) -> Result<(), IpPoolError> {
|
||||
let Some(state) = self.allocations.get_mut(&ip_pair) else {
|
||||
return Err(IpPoolError::UnknownIpPair { ip_pair });
|
||||
};
|
||||
|
||||
if !state.is_free() {
|
||||
return Err(IpPoolError::AlreadyUsed { ip_pair });
|
||||
}
|
||||
tracing::debug!("Marked IP pair as used: {ip_pair}");
|
||||
*state = AllocationState::new_allocated();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the number of free IPs in the pool
|
||||
pub async fn free_count(&self) -> usize {
|
||||
let pool = self.allocations.read().await;
|
||||
pool.iter()
|
||||
pub fn free_count(&self) -> usize {
|
||||
self.allocations
|
||||
.iter()
|
||||
.filter(|(_, state)| matches!(state, AllocationState::Free))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get the number of allocated IPs in the pool
|
||||
pub async fn allocated_count(&self) -> usize {
|
||||
let pool = self.allocations.read().await;
|
||||
pool.iter()
|
||||
.filter(|(_, state)| matches!(state, AllocationState::Allocated(_)))
|
||||
pub fn pre_allocated_count(&self) -> usize {
|
||||
self.allocations
|
||||
.iter()
|
||||
.filter(|(_, state)| matches!(state, AllocationState::PreAllocated { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get the number of allocated IPs in the pool
|
||||
pub fn allocated_count(&self) -> usize {
|
||||
self.allocations
|
||||
.iter()
|
||||
.filter(|(_, state)| matches!(state, AllocationState::Allocated { .. }))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get the total pool size
|
||||
pub async fn total_count(&self) -> usize {
|
||||
let pool = self.allocations.read().await;
|
||||
pool.len()
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.allocations.len()
|
||||
}
|
||||
|
||||
/// Clean up stale allocations older than the specified duration
|
||||
///
|
||||
/// Returns the number of IPs that were freed
|
||||
pub async fn cleanup_stale(&self, max_age: std::time::Duration) -> usize {
|
||||
let mut pool = self.allocations.write().await;
|
||||
let now = SystemTime::now();
|
||||
pub fn cleanup_stale(&mut self, max_age: Duration) -> usize {
|
||||
let now = Instant::now();
|
||||
let mut freed = 0;
|
||||
|
||||
for (_ip, state) in pool.iter_mut() {
|
||||
if let AllocationState::Allocated(allocated_at) = state
|
||||
&& let Ok(age) = now.duration_since(*allocated_at)
|
||||
&& age > max_age
|
||||
{
|
||||
*state = AllocationState::Free;
|
||||
freed += 1;
|
||||
for state in self.allocations.values_mut() {
|
||||
if let AllocationState::PreAllocated { allocated_at, .. } = state {
|
||||
let age = now.duration_since(*allocated_at);
|
||||
if age > max_age {
|
||||
*state = AllocationState::Free;
|
||||
freed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if freed > 0 {
|
||||
tracing::info!("Cleaned up {} stale IP allocations", freed);
|
||||
tracing::info!("Cleaned up {freed} stale IP allocations");
|
||||
}
|
||||
|
||||
freed
|
||||
@@ -234,11 +287,243 @@ impl IpPool {
|
||||
}
|
||||
|
||||
/// Errors that can occur during IP pool operations
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum IpPoolError {
|
||||
#[error("No free IP addresses available in pool")]
|
||||
NoFreeIp,
|
||||
|
||||
#[error("Attempted to mark an IpPair that is already in used: {ip_pair}")]
|
||||
AlreadyUsed { ip_pair: IpPair },
|
||||
|
||||
#[error("Attempted to mark an unknown ip pair: {ip_pair}")]
|
||||
UnknownIpPair { ip_pair: IpPair },
|
||||
|
||||
#[error("Invalid IP network configuration: {0}")]
|
||||
InvalidNetwork(#[from] ipnetwork::IpNetworkError),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::bail;
|
||||
use mock_instant::thread_local::MockClock;
|
||||
|
||||
// 3 addresses in each pool
|
||||
fn small_ip_pool() -> IpPool {
|
||||
IpPool::new(
|
||||
Ipv4Addr::new(10, 0, 0, 0),
|
||||
30,
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0),
|
||||
126,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_pool_initial_allocation() -> anyhow::Result<()> {
|
||||
let base_ipv4_network = Ipv4Addr::new(10, 0, 0, 0);
|
||||
let base_ipv4_prefix = 24; // 255 addresses
|
||||
let base_ipv6_network = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0);
|
||||
let base_ipv6_prefix = 112; // 65535 addresses
|
||||
|
||||
// ipv4 pool size < ipv6 pool size
|
||||
let base_ip_pool = IpPool::new(
|
||||
base_ipv4_network,
|
||||
base_ipv4_prefix,
|
||||
base_ipv6_network,
|
||||
base_ipv6_prefix,
|
||||
)?;
|
||||
let inner = base_ip_pool.allocations;
|
||||
// minimum of ipv4 and ipv6 allocations
|
||||
assert_eq!(inner.len(), 255);
|
||||
|
||||
// no ipv4 addresses
|
||||
let base_ip_pool = IpPool::new(base_ipv4_network, 32, base_ipv6_network, base_ipv6_prefix)?;
|
||||
let inner = base_ip_pool.allocations;
|
||||
assert_eq!(inner.len(), 0);
|
||||
|
||||
// no ipv6 addresses
|
||||
let base_ip_pool =
|
||||
IpPool::new(base_ipv4_network, base_ipv4_prefix, base_ipv6_network, 128)?;
|
||||
let inner = base_ip_pool.allocations;
|
||||
assert_eq!(inner.len(), 0);
|
||||
|
||||
// ipv4 pool size == ipv6 pool size
|
||||
let base_ip_pool = IpPool::new(base_ipv4_network, 16, base_ipv6_network, base_ipv6_prefix)?;
|
||||
let inner = base_ip_pool.allocations;
|
||||
assert_eq!(inner.len(), 65535);
|
||||
|
||||
// ipv4 pool size > ipv6 pool size
|
||||
let base_ip_pool = IpPool::new(base_ipv4_network, 12, base_ipv6_network, base_ipv6_prefix)?;
|
||||
let inner = base_ip_pool.allocations;
|
||||
assert_eq!(inner.len(), 65535);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_different_allocation(left: IpPair, right: IpPair) -> anyhow::Result<()> {
|
||||
if left.ipv4 == right.ipv4 || left.ipv6 == right.ipv6 {
|
||||
bail!("ip allocation overlap")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_pool_allocation() -> anyhow::Result<()> {
|
||||
let mut pool = small_ip_pool();
|
||||
assert_eq!(pool.allocations.len(), 3);
|
||||
|
||||
let gateway_pair = IpPair {
|
||||
ipv4: Ipv4Addr::new(10, 0, 0, 0),
|
||||
ipv6: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0),
|
||||
};
|
||||
|
||||
assert_eq!(pool.free_count(), 3);
|
||||
assert_eq!(pool.pre_allocated_count(), 0);
|
||||
|
||||
let allocation1 = pool.pre_allocate()?;
|
||||
assert_eq!(pool.free_count(), 2);
|
||||
assert_eq!(pool.pre_allocated_count(), 1);
|
||||
|
||||
let allocation2 = pool.pre_allocate()?;
|
||||
assert_eq!(pool.free_count(), 1);
|
||||
assert_eq!(pool.pre_allocated_count(), 2);
|
||||
|
||||
pool.confirm_allocation(allocation1)?;
|
||||
assert_eq!(pool.free_count(), 1);
|
||||
assert_eq!(pool.pre_allocated_count(), 1);
|
||||
assert_eq!(pool.allocated_count(), 1);
|
||||
|
||||
let allocation3 = pool.pre_allocate()?;
|
||||
assert_eq!(pool.free_count(), 0);
|
||||
assert_eq!(pool.pre_allocated_count(), 2);
|
||||
assert_eq!(pool.allocated_count(), 1);
|
||||
|
||||
// make sure each was unique and different from the gateway
|
||||
ensure_different_allocation(allocation1, allocation2)?;
|
||||
ensure_different_allocation(allocation1, allocation3)?;
|
||||
ensure_different_allocation(allocation2, allocation3)?;
|
||||
|
||||
ensure_different_allocation(allocation1, gateway_pair)?;
|
||||
ensure_different_allocation(allocation2, gateway_pair)?;
|
||||
ensure_different_allocation(allocation3, gateway_pair)?;
|
||||
|
||||
// allocation 4 will fail as we have run out of addresses
|
||||
assert_eq!(pool.pre_allocate().unwrap_err(), IpPoolError::NoFreeIp);
|
||||
|
||||
// if pair gets released, it's eligible for allocation again
|
||||
pool.release(allocation2);
|
||||
assert_eq!(pool.free_count(), 1);
|
||||
assert_eq!(pool.pre_allocated_count(), 1);
|
||||
assert_eq!(pool.allocated_count(), 1);
|
||||
|
||||
let reallocation = pool.pre_allocate()?;
|
||||
assert_eq!(reallocation, allocation2);
|
||||
|
||||
assert_eq!(pool.free_count(), 0);
|
||||
assert_eq!(pool.pre_allocated_count(), 2);
|
||||
assert_eq!(pool.allocated_count(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_pool_mark_used() -> anyhow::Result<()> {
|
||||
let mut pool = small_ip_pool();
|
||||
|
||||
let pair1 = IpPair::new(
|
||||
Ipv4Addr::new(10, 0, 0, 1),
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1),
|
||||
);
|
||||
let pair2 = IpPair::new(
|
||||
Ipv4Addr::new(10, 0, 0, 2),
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 2),
|
||||
);
|
||||
let pair3 = IpPair::new(
|
||||
Ipv4Addr::new(10, 0, 0, 3),
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 3),
|
||||
);
|
||||
|
||||
let bad_pair1 = IpPair::new(
|
||||
Ipv4Addr::new(10, 0, 0, 1),
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 4),
|
||||
);
|
||||
let bad_pair2 = IpPair::new(
|
||||
Ipv4Addr::new(10, 0, 0, 4),
|
||||
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1),
|
||||
);
|
||||
|
||||
assert!(pool.mark_used(pair1,).is_ok());
|
||||
assert_eq!(
|
||||
pool.mark_used(pair1).unwrap_err(),
|
||||
IpPoolError::AlreadyUsed { ip_pair: pair1 }
|
||||
);
|
||||
|
||||
assert!(pool.mark_used(pair2).is_ok());
|
||||
assert!(pool.mark_used(pair3).is_ok());
|
||||
|
||||
assert_eq!(
|
||||
pool.mark_used(bad_pair1).unwrap_err(),
|
||||
IpPoolError::UnknownIpPair { ip_pair: bad_pair1 }
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pool.mark_used(bad_pair2,).unwrap_err(),
|
||||
IpPoolError::UnknownIpPair { ip_pair: bad_pair2 }
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_pool_cleanup() -> anyhow::Result<()> {
|
||||
MockClock::set_time(Duration::ZERO);
|
||||
|
||||
let mut pool = small_ip_pool();
|
||||
|
||||
let age_threshold = Duration::from_secs(1);
|
||||
|
||||
// nothing to cleanup
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 0);
|
||||
|
||||
// just allocated
|
||||
let pair1 = pool.pre_allocate()?;
|
||||
let pair2 = pool.pre_allocate()?;
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 0);
|
||||
|
||||
// advance time to go beyond the allocation threshold
|
||||
MockClock::advance(Duration::from_millis(1001));
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 2);
|
||||
|
||||
// ensure those pairs are now marked as free
|
||||
assert!(pool.allocations.get(&pair1).unwrap().is_free());
|
||||
assert!(pool.allocations.get(&pair2).unwrap().is_free());
|
||||
|
||||
pool.pre_allocate()?;
|
||||
MockClock::advance(Duration::from_millis(500));
|
||||
pool.pre_allocate()?;
|
||||
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 0);
|
||||
MockClock::advance(Duration::from_millis(501));
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 1);
|
||||
|
||||
MockClock::advance(Duration::from_millis(500));
|
||||
assert_eq!(pool.cleanup_stale(age_threshold), 1);
|
||||
|
||||
let mut new_pool = small_ip_pool();
|
||||
let pair1 = new_pool.pre_allocate()?;
|
||||
let pair2 = new_pool.pre_allocate()?;
|
||||
|
||||
// complete allocation for pair2
|
||||
new_pool.confirm_allocation(pair2)?;
|
||||
MockClock::advance(Duration::from_millis(2000));
|
||||
|
||||
// only pair1 should have got cleaned up
|
||||
assert_eq!(new_pool.cleanup_stale(age_threshold), 1);
|
||||
assert!(new_pool.allocations.get(&pair1).unwrap().is_free());
|
||||
assert!(!new_pool.allocations.get(&pair2).unwrap().is_free());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ pub async fn start_wireguard(
|
||||
|
||||
// Initialize IP pool from configuration
|
||||
info!("Initializing IP pool for WireGuard peer allocation");
|
||||
let ip_pool = IpPool::new(
|
||||
let mut ip_pool = IpPool::new(
|
||||
wireguard_data.inner.config().private_ipv4,
|
||||
wireguard_data.inner.config().private_network_prefix_v4,
|
||||
wireguard_data.inner.config().private_ipv6,
|
||||
@@ -294,7 +294,7 @@ pub async fn start_wireguard(
|
||||
// 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;
|
||||
ip_pool.mark_used(ip_pair)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ pub enum PeerControlRequestType {
|
||||
ReleaseIpPair { ip_pair: IpPair },
|
||||
RemovePeer { key: KeyWrapper },
|
||||
QueryPeer { key: KeyWrapper },
|
||||
CheckActivePeer { key: KeyWrapper },
|
||||
GetClientBandwidthByKey { key: KeyWrapper },
|
||||
GetClientBandwidthByIp { ip: IpAddr },
|
||||
GetVerifierByKey { key: KeyWrapper },
|
||||
@@ -93,6 +94,7 @@ impl PeerControlRequestType {
|
||||
PeerControlRequestType::GetClientBandwidthByIp { .. } => None,
|
||||
PeerControlRequestType::GetVerifierByKey { key } => Some(key.clone()),
|
||||
PeerControlRequestType::GetVerifierByIp { .. } => None,
|
||||
PeerControlRequestType::CheckActivePeer { key } => Some(key.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +109,7 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
|
||||
PeerControlRequest::AddPeer { peer, .. } => PeerControlRequestType::AddPeer {
|
||||
public_key: (&peer.public_key).into(),
|
||||
},
|
||||
PeerControlRequest::AllocatePeerIpPair { .. } => {
|
||||
PeerControlRequest::PreAllocateIpPair { .. } => {
|
||||
PeerControlRequestType::AllocatePeerIpPair {}
|
||||
}
|
||||
PeerControlRequest::ReleaseIpPair { ip_pair, .. } => {
|
||||
@@ -131,6 +133,9 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
|
||||
PeerControlRequest::GetVerifierByIp { ip, .. } => {
|
||||
PeerControlRequestType::GetVerifierByIp { ip: *ip }
|
||||
}
|
||||
PeerControlRequest::CheckActivePeer { key, .. } => {
|
||||
PeerControlRequestType::CheckActivePeer { key: key.into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,7 +271,7 @@ impl MockPeerController {
|
||||
}
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
PeerControlRequest::AllocatePeerIpPair { response_tx, .. } => {
|
||||
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
PeerControlRequest::ReleaseIpPair {
|
||||
@@ -295,6 +300,9 @@ impl MockPeerController {
|
||||
PeerControlRequest::GetVerifierByIp { response_tx, .. } => {
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
PeerControlRequest::CheckActivePeer { response_tx, .. } => {
|
||||
response_tx.send_downcasted(response.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ pub enum PeerControlRequest {
|
||||
response_tx: oneshot::Sender<AddPeerControlResponse>,
|
||||
},
|
||||
/// Attempt to allocate an IP pair from the pool
|
||||
AllocatePeerIpPair {
|
||||
PreAllocateIpPair {
|
||||
response_tx: oneshot::Sender<AllocatePeerControlResponse>,
|
||||
},
|
||||
/// Attempt to return an IP pair back to the pool
|
||||
@@ -93,6 +93,10 @@ pub enum PeerControlRequest {
|
||||
key: Key,
|
||||
response_tx: oneshot::Sender<QueryPeerControlResponse>,
|
||||
},
|
||||
CheckActivePeer {
|
||||
key: Key,
|
||||
response_tx: oneshot::Sender<CheckActivePeerResponse>,
|
||||
},
|
||||
GetClientBandwidthByKey {
|
||||
key: Key,
|
||||
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
|
||||
@@ -118,6 +122,7 @@ pub type AllocatePeerControlResponse = Result<IpPair>;
|
||||
pub type ReleaseIpPairControlResponse = Result<()>;
|
||||
pub type RemovePeerControlResponse = Result<()>;
|
||||
pub type QueryPeerControlResponse = Result<Option<Peer>>;
|
||||
pub type CheckActivePeerResponse = Result<bool>;
|
||||
pub type GetClientBandwidthControlResponse = Result<ClientBandwidth>;
|
||||
pub type QueryVerifierControlResponse = Result<Box<dyn TicketVerifier + Send + Sync>>;
|
||||
|
||||
@@ -216,7 +221,7 @@ impl PeerController {
|
||||
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
|
||||
self.ip_pool.release(ip_pair)
|
||||
}
|
||||
|
||||
let ret = self.wg_api.remove_peer(key);
|
||||
@@ -258,6 +263,13 @@ impl PeerController {
|
||||
async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> {
|
||||
nym_metrics::inc!("wg_peer_addition_attempts");
|
||||
|
||||
// confirm ip allocation so that it wouldn't be released for as long as the peer exists
|
||||
let Some(ip_pair) = allocated_ip_pair(peer) else {
|
||||
return Err(Error::Internal(
|
||||
"could not determine ip pair allocated to the peer".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
// Try to configure WireGuard peer
|
||||
if let Err(e) = self.wg_api.configure_peer(peer) {
|
||||
nym_metrics::inc!("wg_peer_addition_failed");
|
||||
@@ -289,6 +301,9 @@ impl PeerController {
|
||||
*self.host_information.write().await = host_information;
|
||||
}
|
||||
let public_key = peer.public_key.clone();
|
||||
|
||||
self.ip_pool.confirm_allocation(ip_pair)?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
handle.run().await;
|
||||
debug!("Peer handle shut down for {public_key}");
|
||||
@@ -302,15 +317,11 @@ impl PeerController {
|
||||
///
|
||||
/// This only allocates IPs - the caller must handle database storage and
|
||||
/// then call AddPeer with a complete Peer struct.
|
||||
async fn handle_ip_allocation_request(&mut self) -> Result<IpPair> {
|
||||
fn handle_ip_allocation_request(&mut self) -> Result<IpPair> {
|
||||
nym_metrics::inc!("wg_ip_allocation_attempts");
|
||||
|
||||
// Allocate IP pair from pool
|
||||
let ip_pair = self
|
||||
.ip_pool
|
||||
.allocate()
|
||||
.await
|
||||
.map_err(|e| Error::IpPool(e.to_string()))?;
|
||||
let ip_pair = self.ip_pool.pre_allocate()?;
|
||||
|
||||
nym_metrics::inc!("wg_ip_allocation_success");
|
||||
tracing::debug!("Allocated IP pair: {ip_pair}");
|
||||
@@ -319,8 +330,8 @@ impl PeerController {
|
||||
}
|
||||
|
||||
/// Return IP pair back to the pool
|
||||
async fn handle_ip_release(&mut self, ip_pair: IpPair) {
|
||||
self.ip_pool.release(ip_pair).await
|
||||
fn handle_ip_release(&mut self, ip_pair: IpPair) {
|
||||
self.ip_pool.release(ip_pair)
|
||||
}
|
||||
|
||||
async fn ip_to_key(&self, ip: IpAddr) -> Result<Option<Key>> {
|
||||
@@ -359,6 +370,12 @@ impl PeerController {
|
||||
.client_bandwidth())
|
||||
}
|
||||
|
||||
fn check_active_peer(&self, key: Key) -> Result<bool> {
|
||||
// peer is active as long as we still have an entry inside the bandwidth storage manager,
|
||||
// as it is removed upon peer removal
|
||||
Ok(self.bw_storage_managers.contains_key(&key))
|
||||
}
|
||||
|
||||
async fn handle_get_client_bandwidth_by_ip(&self, ip: IpAddr) -> Result<ClientBandwidth> {
|
||||
let Some(key) = self.ip_to_key(ip).await? else {
|
||||
return Err(Error::MissingClientKernelEntry(ip.to_string()));
|
||||
@@ -492,16 +509,14 @@ impl PeerController {
|
||||
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::PreAllocateIpPair { response_tx } => {
|
||||
response_tx.send(self.handle_ip_allocation_request()).ok();
|
||||
}
|
||||
PeerControlRequest::ReleaseIpPair {
|
||||
response_tx,
|
||||
ip_pair,
|
||||
} => {
|
||||
self.handle_ip_release(ip_pair).await;
|
||||
self.handle_ip_release(ip_pair);
|
||||
response_tx.send(Ok(())).ok();
|
||||
}
|
||||
PeerControlRequest::RemovePeer { key, response_tx } => {
|
||||
@@ -540,6 +555,9 @@ impl PeerController {
|
||||
.send(self.handle_query_verifier_by_ip(ip, *credential).await)
|
||||
.ok();
|
||||
}
|
||||
PeerControlRequest::CheckActivePeer { key, response_tx } => {
|
||||
response_tx.send(self.check_active_peer(key)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +576,7 @@ impl PeerController {
|
||||
}
|
||||
_ = self.ip_cleanup_interval.next() => {
|
||||
// Periodically cleanup stale IP allocations
|
||||
let freed = self.ip_pool.cleanup_stale(DEFAULT_IP_STALE_AGE).await;
|
||||
let freed = self.ip_pool.cleanup_stale(DEFAULT_IP_STALE_AGE);
|
||||
if freed > 0 {
|
||||
nym_metrics::inc_by!("wg_stale_ips_cleaned", freed as u64);
|
||||
info!("Cleaned up {} stale IP allocations", freed);
|
||||
|
||||
@@ -21,7 +21,7 @@ zeroize = { workspace = true }
|
||||
|
||||
nym-bin-common = { workspace = true }
|
||||
nym-http-api-client = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["form"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
Generated
+43
-15
@@ -825,7 +825,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "easy-addr"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"quote",
|
||||
@@ -1248,7 +1248,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a95dc43ef8954a4f79846e3224434cf389d4a9c14b77f526dae3cfd2221c6141"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1261,7 +1263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47bb3e8427c193cd500c802274b11879086863c3643525b6ece3e9ab1c77bddc"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1275,7 +1279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3628aac6715e844f3ab20e3b8ae8c4684f144ccb78e205f002c1c3ae375e956"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
@@ -1289,7 +1295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b710addc28c9950dd961e7dd3837ef3b479492d2b21b5f2437eb7d2899403027"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
@@ -1335,7 +1343,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d022e85291bf51877fbbf4688bc3762c605fdaee3b98c6407414f7c358bc5610"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1349,7 +1359,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb13102740426a4a2b683f54bbd6614fe9ecd745f5117bcf197c49c300b15edf"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -1384,7 +1396,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c21bceb3bb8ee2789851b3f381fc035485af825bf7290b7c99a5af4e8f6ba1"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -1404,7 +1418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a20b931ee849f6179ce2b387accd058720017f644ffbc8c2422f3e9ac3ff54"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1419,7 +1435,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9834193b4641acdf9f360aea684a6bd841cad287930bc0d7c3241a133756464"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"regex",
|
||||
@@ -1427,7 +1445,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03077f9ebeb40caf8aa8e6f7bf8728449f73733e7a246986e492fa34ad3e70ab"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
@@ -1455,7 +1475,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42129a72f4b0dc0304a48b0ca1769b27694d913687ace5692d4c6924ca9f2a13"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1483,7 +1505,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70239cc26beda3ad19289188c50d554522af29646d7d3f855bda6fc8ed332fe7"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -1495,7 +1519,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba662d39fd6da9e13166fa1162ff41c2cfaed78a77c70df72fbda6fef5eb4f5"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
@@ -1524,7 +1550,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "1.20.1"
|
||||
version = "1.20.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "676c2793efbf9ccdf86bb788c903f778a2d5993a5174729303f9511a297f4ca8"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
|
||||
+20
-25
@@ -57,34 +57,25 @@ schemars = "0.8.16"
|
||||
|
||||
thiserror = "2.0.11"
|
||||
|
||||
# Common crates from parent workspace (paths relative to contracts/)
|
||||
#
|
||||
# TODO: Once these crates are published to crates.io, switch from path dependencies
|
||||
# to crates.io versions.
|
||||
#
|
||||
# TODO add a [patch.crates-io] section at the bottom for local development if you need to use a modded version of common import instead e.g.:
|
||||
#
|
||||
# [patch.crates-io]
|
||||
# nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
#
|
||||
easy-addr = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
nym-coconut-dkg-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-contracts-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
nym-crypto = { version = "1.20.1", path = "../common/crypto", default-features = false }
|
||||
nym-ecash-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-group-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-mixnet-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.20.1", path = "../common/network-defaults", default-features = false }
|
||||
nym-performance-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-pool-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/nym-pool-contract" }
|
||||
nym-vesting-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
# For local development with modifications, add a [patch.crates-io] section (see bottom of file)
|
||||
nym-coconut-dkg-common = "1.20.4"
|
||||
nym-contracts-common = "1.20.4"
|
||||
nym-contracts-common-testing = "1.20.4"
|
||||
nym-crypto = { version = "1.20.4", default-features = false }
|
||||
nym-ecash-contract-common = "1.20.4"
|
||||
nym-group-contract-common = "1.20.4"
|
||||
nym-mixnet-contract-common = "1.20.4"
|
||||
nym-multisig-contract-common = "1.20.4"
|
||||
nym-network-defaults = { version = "1.20.4", default-features = false }
|
||||
nym-performance-contract-common = "1.20.4"
|
||||
nym-pool-contract-common = "1.20.4"
|
||||
nym-vesting-contract-common = "1.20.4"
|
||||
|
||||
# Aliases for crates that some contracts import under different names
|
||||
contracts-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/contracts-common", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
|
||||
contracts-common = { version = "1.20.4", package = "nym-contracts-common" }
|
||||
mixnet-contract-common = { version = "1.20.4", package = "nym-mixnet-contract-common" }
|
||||
vesting-contract-common = { version = "1.20.4", package = "nym-vesting-contract-common" }
|
||||
|
||||
# Internal contract workspace members (for cross-contract testing)
|
||||
cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
|
||||
@@ -101,3 +92,7 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
# For local development, import via path instead of crates.io, e.g.
|
||||
# [patch.crates-io]
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
+17
-5
@@ -9,14 +9,26 @@ This version is defined in the `[workspace.package]` section of the root monorep
|
||||
## When Developing
|
||||
If you add a workspace dependency to the SDK when developing, make sure to add this to the workspace dependencies in the root monorepo `Cargo.toml`.
|
||||
|
||||
## Publishing
|
||||
## Check local publication
|
||||
```
|
||||
# List crates to publish
|
||||
cargo workspaces list
|
||||
|
||||
# Dry run - check for compilation or other problems
|
||||
# Dry run locally - check for compilation or other problems
|
||||
cargo workspaces publish --no-git-commit --dry-run
|
||||
|
||||
# Publish
|
||||
TODO
|
||||
```
|
||||
|
||||
## CI
|
||||
There are several workflows that should be run in the following order:
|
||||
- `ci-crates-publish-dry-run`: **run this first!** This is a remote dry-run on a runner. This greps for any errors that would be a problem when we're not dry-running. It doesn't catch all errors, as `dry-run` has a known issue where, assuming that 2 new crates are being uploaded, and crate B relies on crate A, if crate A isn't on crates.io (which it won't be, since you're dry-running publication), then since `cargo workspaces publish` only checks for available versions on crates.io, it will error. We don't want the CI to fail in that case.
|
||||
- `ci-crates-version-bump`: this bumps the versions of the workspace + dependencies to the passed version, and then commits the change. This is its own CI job so that we don't get into sticky situations whereby the version bump + commit happens, but the publication step fails.
|
||||
- `ci-crates-publish`: this publishes the crates. So long as you're not uploading more than 5 new crates, pass `60` as the `publish_interval`. This is to get around [crates.io rate limiting](https://github.com/rust-lang/crates.io/blob/ad7e58e1afd65b9137e58a7bca3e1fb7f5546682/src/rate_limiter.rs#L24). Pass the Github handle of whoever should be the backup author of the crate for security redundency (see the section below) as the second arg.
|
||||
|
||||
> There is also `ci-crates-publish-resume` which is there in case a publication run fails and needs to be restarted part way through the list of unbumped/unpublished crates. Pass the previously bumped/published crate in the list output of `cargo workspaces list`
|
||||
|
||||
## Crates.io Authors
|
||||
Since Github teams have [limited ownership / mod rights](https://doc.rust-lang.org/cargo/reference/publishing.html#cargo-owner) of crates, and we cannot create a `CARGO_REGISTRY_TOKEN` on behalf of the Nym Github org. As such, we are currently using personal cargo tokens generated by team members (currently Max), and adding the Nym Github org as an owner in the CI job.
|
||||
|
||||
However, since the Github org cannot add or modify owners, are also adding a second user author as a redundency, on the offchance that Max loses access to his Crates.io / Github account, gets struck by lightning, etc. This is the author passed as the second argument to the `ci-crates-publish` CI, and if none is passed, defaults to [https://github.com/jstuczyn](https://github.com/jstuczyn) since he is the Github org owner.
|
||||
|
||||
Authors can also be changed by running `scripts/add-crates-owners.sh`.
|
||||
|
||||
+3
-1
@@ -89,9 +89,11 @@ bytes = { workspace = true }
|
||||
defguard_wireguard_rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
nym-test-utils = { workspace = true }
|
||||
nym-gateway-storage = { workspace = true, features = ["mock"] }
|
||||
nym-wireguard = { workspace = true, features = ["mock"] }
|
||||
mock_instant = "0.6.0"
|
||||
mock_instant = { workspace = true }
|
||||
time = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -4,29 +4,15 @@
|
||||
use crate::node::internal_service_providers::authenticator::{
|
||||
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 crate::node::wireguard::{PeerManager, PeerRegistrator};
|
||||
use futures::StreamExt;
|
||||
use nym_authenticator_requests::models::BandwidthClaim;
|
||||
use nym_authenticator_requests::traits::UpgradeModeMessage;
|
||||
use nym_authenticator_requests::{latest, v4::registration::IpPair};
|
||||
use nym_authenticator_requests::{
|
||||
latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs},
|
||||
request::AuthenticatorRequest,
|
||||
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage},
|
||||
v1, v2, v3, v4, v5, v6, AuthenticatorVersion, CURRENT_VERSION,
|
||||
};
|
||||
use nym_credential_verification::ecash::traits::EcashManager;
|
||||
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
|
||||
use nym_credential_verification::{
|
||||
bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig,
|
||||
ClientBandwidth, CredentialVerifier,
|
||||
};
|
||||
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData};
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_gateway_requests::models::CredentialSpendingRequest;
|
||||
use nym_gateway_storage::models::PersistedBandwidth;
|
||||
use nym_sdk::mixnet::{
|
||||
AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane,
|
||||
};
|
||||
@@ -36,50 +22,28 @@ use nym_task::ShutdownToken;
|
||||
use nym_wireguard::WireguardGatewayData;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::cmp::max;
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use tokio_stream::wrappers::IntervalStream;
|
||||
|
||||
type AuthenticatorHandleResult = Result<(Vec<u8>, Option<Recipient>), AuthenticatorError>;
|
||||
const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute
|
||||
const DEFAULT_CREDENTIAL_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute
|
||||
|
||||
// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client
|
||||
// its depletion is low enough to not require sending new tickets
|
||||
const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024;
|
||||
|
||||
pub(crate) struct RegisteredAndFree {
|
||||
registration_in_progress: PendingRegistrations,
|
||||
taken_private_network_ips: PrivateIPs,
|
||||
}
|
||||
|
||||
impl RegisteredAndFree {
|
||||
pub(crate) fn new() -> Self {
|
||||
RegisteredAndFree {
|
||||
registration_in_progress: Default::default(),
|
||||
taken_private_network_ips: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MixnetListener {
|
||||
// The configuration for the mixnet listener
|
||||
pub(crate) config: Config,
|
||||
pub(crate) _config: Config,
|
||||
|
||||
// The mixnet client that we use to send and receive packets from the mixnet
|
||||
pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient,
|
||||
|
||||
// Registrations awaiting confirmation
|
||||
pub(crate) registered_and_free: RwLock<RegisteredAndFree>,
|
||||
|
||||
pub(crate) peer_manager: PeerManager,
|
||||
|
||||
pub(crate) upgrade_mode: UpgradeModeDetails,
|
||||
|
||||
pub(crate) ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
pub(crate) peer_registrator: PeerRegistrator,
|
||||
|
||||
pub(crate) timeout_check_interval: IntervalStream,
|
||||
|
||||
@@ -91,18 +55,17 @@ impl MixnetListener {
|
||||
config: Config,
|
||||
wireguard_gateway_data: WireguardGatewayData,
|
||||
mixnet_client: nym_sdk::mixnet::MixnetClient,
|
||||
peer_registrator: PeerRegistrator,
|
||||
upgrade_mode: UpgradeModeDetails,
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
) -> Self {
|
||||
let timeout_check_interval =
|
||||
IntervalStream::new(tokio::time::interval(DEFAULT_REGISTRATION_TIMEOUT_CHECK));
|
||||
IntervalStream::new(tokio::time::interval(DEFAULT_CREDENTIAL_TIMEOUT_CHECK));
|
||||
MixnetListener {
|
||||
config,
|
||||
_config: config,
|
||||
mixnet_client,
|
||||
registered_and_free: RwLock::new(RegisteredAndFree::new()),
|
||||
peer_manager: PeerManager::new(wireguard_gateway_data),
|
||||
upgrade_mode,
|
||||
ecash_verifier,
|
||||
peer_registrator,
|
||||
timeout_check_interval,
|
||||
seen_credential_cache: SeenCredentialCache::new(),
|
||||
}
|
||||
@@ -112,10 +75,6 @@ impl MixnetListener {
|
||||
self.upgrade_mode.enabled()
|
||||
}
|
||||
|
||||
fn keypair(&self) -> &Arc<KeyPair> {
|
||||
self.peer_manager.wireguard_gateway_data.keypair()
|
||||
}
|
||||
|
||||
async fn upgrade_mode_bandwidth(&self, peer: PeerPublicKey) -> Result<i64, AuthenticatorError> {
|
||||
// if we're undergoing upgrade mode, we don't meter bandwidth,
|
||||
// we simply return MAX of clients current bandwidth and minimum bandwidth before default
|
||||
@@ -129,47 +88,6 @@ 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_progress
|
||||
.values()
|
||||
.cloned()
|
||||
.collect();
|
||||
for reg in registered_values {
|
||||
let timestamp = registered_and_free
|
||||
.taken_private_network_ips
|
||||
.get_mut(®.gateway_data.private_ips)
|
||||
.ok_or(AuthenticatorError::InternalDataCorruption(format!(
|
||||
"IPs {} should be present",
|
||||
reg.gateway_data.private_ips
|
||||
)))?;
|
||||
|
||||
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 {
|
||||
registered_and_free
|
||||
.registration_in_progress
|
||||
.remove(®.gateway_data.pub_key());
|
||||
registered_and_free
|
||||
.taken_private_network_ips
|
||||
.remove(®.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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_initial_request(
|
||||
&mut self,
|
||||
init_message: Box<dyn InitMessage + Send + Sync + 'static>,
|
||||
@@ -177,352 +95,12 @@ impl MixnetListener {
|
||||
request_id: u64,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> AuthenticatorHandleResult {
|
||||
let remote_public = init_message.pub_key();
|
||||
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_progress
|
||||
.get(&remote_public)
|
||||
{
|
||||
let gateway_data = registration_data.gateway_data.clone();
|
||||
let bytes = match AuthenticatorVersion::from(protocol) {
|
||||
AuthenticatorVersion::V1 => {
|
||||
v1::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v1::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v1::GatewayClient {
|
||||
pub_key: gateway_data.pub_key,
|
||||
private_ip: gateway_data.private_ips.ipv4.into(),
|
||||
mac: v1::ClientMac::new(gateway_data.mac.to_vec()),
|
||||
},
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V2 => {
|
||||
v2::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v2::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v2::registration::GatewayClient::new(
|
||||
self.keypair().private_key(),
|
||||
remote_public.inner(),
|
||||
registration_data.gateway_data.private_ips.ipv4.into(),
|
||||
registration_data.nonce,
|
||||
),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V3 => {
|
||||
v3::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v3::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v3::registration::GatewayClient::new(
|
||||
self.keypair().private_key(),
|
||||
remote_public.inner(),
|
||||
registration_data.gateway_data.private_ips.ipv4.into(),
|
||||
registration_data.nonce,
|
||||
),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V4 => {
|
||||
v4::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v4::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
// convert current to v5 and then v5 to v4 (current as of 28.08.25)
|
||||
gateway_data: v5::registration::GatewayClient::from(
|
||||
registration_data.gateway_data.clone(),
|
||||
)
|
||||
.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V5 => {
|
||||
v5::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v5::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: registration_data.gateway_data.clone().into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V6 => {
|
||||
v6::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v6::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: registration_data.gateway_data.clone(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
self.upgrade_mode_enabled(),
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion),
|
||||
};
|
||||
return Ok((bytes, reply_to));
|
||||
}
|
||||
let response = self
|
||||
.peer_registrator
|
||||
.on_initial_authenticator_request(init_message, protocol, request_id, reply_to)
|
||||
.await?;
|
||||
|
||||
let peer = self.peer_manager.query_peer(remote_public).await?;
|
||||
if let Some(peer) = peer {
|
||||
let allowed_ipv4 = peer
|
||||
.allowed_ips
|
||||
.iter()
|
||||
.find_map(|ip_mask| match ip_mask.address {
|
||||
IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(AuthenticatorError::InternalError(
|
||||
"there should be one private IPv4 in the list".to_string(),
|
||||
))?;
|
||||
let allowed_ipv6 = peer
|
||||
.allowed_ips
|
||||
.iter()
|
||||
.find_map(|ip_mask| match ip_mask.address {
|
||||
IpAddr::V6(ipv6_addr) => Some(ipv6_addr),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(IpPair::from(IpAddr::from(allowed_ipv4)).ipv6);
|
||||
let bytes = match AuthenticatorVersion::from(protocol) {
|
||||
AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered(
|
||||
v1::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ip: allowed_ipv4.into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered(
|
||||
v2::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ip: allowed_ipv4.into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered(
|
||||
v3::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ip: allowed_ipv4.into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered(
|
||||
v4::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ips: (allowed_ipv4, allowed_ipv6).into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered(
|
||||
v5::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ips: (allowed_ipv4, allowed_ipv6).into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered(
|
||||
v6::registration::RegisteredData {
|
||||
pub_key: self.keypair().public_key().into(),
|
||||
private_ips: (allowed_ipv4, allowed_ipv6).into(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
},
|
||||
request_id,
|
||||
self.upgrade_mode_enabled(),
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion),
|
||||
};
|
||||
return Ok((bytes, reply_to));
|
||||
}
|
||||
|
||||
// mark it as used, even though it's not final
|
||||
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(),
|
||||
ip_allocation.into(),
|
||||
nonce,
|
||||
);
|
||||
let registration_data = latest::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: gateway_data.clone(),
|
||||
wg_port: self.config.authenticator.tunnel_announced_port,
|
||||
};
|
||||
registered_and_free
|
||||
.registration_in_progress
|
||||
.insert(remote_public, registration_data.clone());
|
||||
let bytes = match AuthenticatorVersion::from(protocol) {
|
||||
AuthenticatorVersion::V1 => {
|
||||
v1::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v1::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v1::registration::GatewayClient::new(
|
||||
self.keypair().private_key(),
|
||||
remote_public.inner(),
|
||||
ip_allocation.ipv4.into(),
|
||||
nonce,
|
||||
),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V2 => {
|
||||
v2::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v2::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v2::registration::GatewayClient::new(
|
||||
self.keypair().private_key(),
|
||||
remote_public.inner(),
|
||||
ip_allocation.ipv4.into(),
|
||||
nonce,
|
||||
),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V3 => {
|
||||
v3::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v3::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: v3::registration::GatewayClient::new(
|
||||
self.keypair().private_key(),
|
||||
remote_public.inner(),
|
||||
ip_allocation.ipv4.into(),
|
||||
nonce,
|
||||
),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V4 => {
|
||||
v4::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v4::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
// convert current to v5 and then v5 to v4 (current as of 28.08.25)
|
||||
gateway_data: v5::registration::GatewayClient::from(
|
||||
registration_data.gateway_data.clone(),
|
||||
)
|
||||
.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V5 => {
|
||||
v5::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v5::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: registration_data.gateway_data.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::V6 => {
|
||||
v6::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v6::registration::RegistrationData {
|
||||
nonce: registration_data.nonce,
|
||||
gateway_data: registration_data.gateway_data,
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
self.upgrade_mode_enabled(),
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?
|
||||
}
|
||||
AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion),
|
||||
};
|
||||
|
||||
Ok((bytes, reply_to))
|
||||
}
|
||||
|
||||
async fn handle_final_credential_claim(
|
||||
&self,
|
||||
claim: BandwidthClaim,
|
||||
client_id: i64,
|
||||
) -> Result<(), AuthenticatorError> {
|
||||
match claim.credential {
|
||||
BandwidthCredential::ZkNym(zk_nym) => {
|
||||
// if we got zk-nym, we just try to verify it
|
||||
credential_verification(self.ecash_verifier.clone(), *zk_nym, client_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
BandwidthCredential::UpgradeModeJWT { token } => {
|
||||
// if we're already in the upgrade mode, don't bother validating the token
|
||||
if self.upgrade_mode_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.upgrade_mode.try_enable_via_received_jwt(token).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Ok((response.bytes, response.reply_to))
|
||||
}
|
||||
|
||||
async fn on_final_request(
|
||||
@@ -532,139 +110,12 @@ impl MixnetListener {
|
||||
request_id: u64,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> AuthenticatorHandleResult {
|
||||
let mut registered_and_free = self.registered_and_free.write().await;
|
||||
let registration_data = registered_and_free
|
||||
.registration_in_progress
|
||||
.get(&final_message.gateway_client_pub_key())
|
||||
.ok_or(AuthenticatorError::RegistrationNotInProgress)?
|
||||
.clone();
|
||||
|
||||
if final_message
|
||||
.verify(self.keypair().private_key(), registration_data.nonce)
|
||||
.is_err()
|
||||
{
|
||||
return Err(AuthenticatorError::MacVerificationFailure);
|
||||
}
|
||||
|
||||
let mut peer = Peer::new(Key::new(final_message.gateway_client_pub_key().to_bytes()));
|
||||
peer.allowed_ips
|
||||
.push(IpAddrMask::new(final_message.private_ips().ipv4.into(), 32));
|
||||
peer.allowed_ips.push(IpAddrMask::new(
|
||||
final_message.private_ips().ipv6.into(),
|
||||
128,
|
||||
));
|
||||
|
||||
// ideally credential wouldn't have been required in upgrade mode,
|
||||
// however, we need some basic information to insert valid wg peer
|
||||
let Some(credential) = final_message.credential() else {
|
||||
return Err(AuthenticatorError::NoCredentialReceived);
|
||||
};
|
||||
|
||||
let typ = credential.kind;
|
||||
|
||||
let client_id = self
|
||||
.ecash_verifier
|
||||
.storage()
|
||||
.insert_wireguard_peer(&peer, typ.into())
|
||||
let response = self
|
||||
.peer_registrator
|
||||
.on_final_authenticator_request(final_message, protocol, request_id, reply_to)
|
||||
.await?;
|
||||
|
||||
if let Err(err) = self
|
||||
.handle_final_credential_claim(credential, client_id)
|
||||
.await
|
||||
{
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.remove_wireguard_peer(&peer.public_key.to_string())
|
||||
.await?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let public_key = peer.public_key.to_string();
|
||||
if let Err(e) = self.peer_manager.add_peer(peer).await {
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.remove_wireguard_peer(&public_key)
|
||||
.await?;
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
registered_and_free
|
||||
.registration_in_progress
|
||||
.remove(&final_message.gateway_client_pub_key());
|
||||
|
||||
let bytes = match AuthenticatorVersion::from(protocol) {
|
||||
AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered(
|
||||
v1::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
private_ip: registration_data.gateway_data.private_ips.ipv4.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered(
|
||||
v2::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
private_ip: registration_data.gateway_data.private_ips.ipv4.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered(
|
||||
v3::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
private_ip: registration_data.gateway_data.private_ips.ipv4.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered(
|
||||
v4::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
// convert current to v5 and then v5 to v4 (current as of 28.08.25)
|
||||
private_ips: v5::registration::IpPair::from(
|
||||
registration_data.gateway_data.private_ips,
|
||||
)
|
||||
.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered(
|
||||
v5::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
private_ips: registration_data.gateway_data.private_ips.into(),
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered(
|
||||
v6::registration::RegisteredData {
|
||||
pub_key: registration_data.gateway_data.pub_key,
|
||||
private_ips: registration_data.gateway_data.private_ips,
|
||||
wg_port: registration_data.wg_port,
|
||||
},
|
||||
request_id,
|
||||
self.upgrade_mode_enabled(),
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(AuthenticatorError::response_serialisation)?,
|
||||
AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion),
|
||||
};
|
||||
Ok((bytes, reply_to))
|
||||
Ok((response.bytes, response.reply_to))
|
||||
}
|
||||
|
||||
async fn on_query_bandwidth_request(
|
||||
@@ -955,9 +406,6 @@ impl MixnetListener {
|
||||
break;
|
||||
},
|
||||
_ = self.timeout_check_interval.next() => {
|
||||
if let Err(e) = self.remove_stale_registrations().await {
|
||||
tracing::error!("Could not clear stale registrations. The registration process might get jammed soon - {e:?}");
|
||||
}
|
||||
self.seen_credential_cache.remove_stale();
|
||||
}
|
||||
msg = self.mixnet_client.next() => {
|
||||
@@ -987,45 +435,6 @@ impl MixnetListener {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn credential_storage_preparation(
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
client_id: i64,
|
||||
) -> Result<PersistedBandwidth, AuthenticatorError> {
|
||||
ecash_verifier
|
||||
.storage()
|
||||
.create_bandwidth_entry(client_id)
|
||||
.await?;
|
||||
let bandwidth = ecash_verifier
|
||||
.storage()
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.ok_or(AuthenticatorError::InternalError(
|
||||
"bandwidth entry should have just been created".to_string(),
|
||||
))?;
|
||||
Ok(bandwidth)
|
||||
}
|
||||
|
||||
async fn credential_verification(
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
credential: CredentialSpendingData,
|
||||
client_id: i64,
|
||||
) -> Result<i64, AuthenticatorError> {
|
||||
let bandwidth = credential_storage_preparation(ecash_verifier.clone(), client_id).await?;
|
||||
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
|
||||
let mut verifier = CredentialVerifier::new(
|
||||
CredentialSpendingRequest::new(credential),
|
||||
ecash_verifier.clone(),
|
||||
BandwidthStorageManager::new(
|
||||
ecash_verifier.storage(),
|
||||
client_bandwidth,
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
Ok(verifier.verify().await?)
|
||||
}
|
||||
|
||||
fn deserialize_request(
|
||||
reconstructed: &ReconstructedMessage,
|
||||
) -> Result<AuthenticatorRequest, AuthenticatorError> {
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::internal_service_providers::authenticator::error::AuthenticatorError;
|
||||
use crate::node::wireguard::PeerRegistrator;
|
||||
use futures::channel::oneshot;
|
||||
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::{path::Path, sync::Arc};
|
||||
use std::path::Path;
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
@@ -32,12 +33,12 @@ impl OnStartData {
|
||||
pub struct Authenticator {
|
||||
#[allow(unused)]
|
||||
config: Config,
|
||||
peer_registrator: PeerRegistrator,
|
||||
upgrade_mode_state: UpgradeModeDetails,
|
||||
wait_for_gateway: bool,
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||
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>,
|
||||
shutdown: ShutdownTracker,
|
||||
on_start: Option<oneshot::Sender<OnStartData>>,
|
||||
}
|
||||
@@ -45,20 +46,18 @@ pub struct Authenticator {
|
||||
impl Authenticator {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
peer_registrator: PeerRegistrator,
|
||||
upgrade_mode_state: UpgradeModeDetails,
|
||||
wireguard_gateway_data: WireguardGatewayData,
|
||||
ecash_verifier: Arc<
|
||||
dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync,
|
||||
>,
|
||||
shutdown: ShutdownTracker,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
peer_registrator,
|
||||
upgrade_mode_state,
|
||||
wait_for_gateway: false,
|
||||
custom_topology_provider: None,
|
||||
custom_gateway_transceiver: None,
|
||||
ecash_verifier,
|
||||
wireguard_gateway_data,
|
||||
shutdown,
|
||||
on_start: None,
|
||||
@@ -134,8 +133,8 @@ impl Authenticator {
|
||||
self.config,
|
||||
self.wireguard_gateway_data,
|
||||
mixnet_client,
|
||||
self.peer_registrator,
|
||||
self.upgrade_mode_state,
|
||||
self.ecash_verifier,
|
||||
);
|
||||
|
||||
tracing::info!("The address of this client is: {self_address}");
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::SystemTime;
|
||||
use mock_instant::thread_local::Instant;
|
||||
#[cfg(not(test))]
|
||||
use std::time::SystemTime;
|
||||
use std::time::Instant;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
@@ -15,7 +15,7 @@ const SEEN_CREDENTIAL_CACHE_TIME: Duration = Duration::from_secs(60 * 60); // 1
|
||||
#[derive(Eq, Hash, PartialEq)]
|
||||
struct TimestampedPeerPubKey {
|
||||
peer_pub_key: PeerPublicKey,
|
||||
timestamp: SystemTime,
|
||||
timestamp: Instant,
|
||||
}
|
||||
|
||||
pub(crate) struct SeenCredentialCache {
|
||||
@@ -36,7 +36,7 @@ impl SeenCredentialCache {
|
||||
) {
|
||||
let value = TimestampedPeerPubKey {
|
||||
peer_pub_key,
|
||||
timestamp: SystemTime::now(),
|
||||
timestamp: Instant::now(),
|
||||
};
|
||||
self.cached_credentials
|
||||
.insert(credential.serial_number_b58(), value);
|
||||
@@ -52,12 +52,9 @@ impl SeenCredentialCache {
|
||||
}
|
||||
|
||||
pub(crate) fn remove_stale(&mut self) {
|
||||
let now = SystemTime::now();
|
||||
let now = Instant::now();
|
||||
self.cached_credentials.retain(|_, value| {
|
||||
let Ok(cache_time) = now.duration_since(value.timestamp) else {
|
||||
tracing::warn!("Got decreasing consecutive system timestamps");
|
||||
return false;
|
||||
};
|
||||
let cache_time = now.duration_since(value.timestamp);
|
||||
cache_time < SEEN_CREDENTIAL_CACHE_TIME
|
||||
});
|
||||
}
|
||||
@@ -159,30 +156,9 @@ mod test {
|
||||
cache.remove_stale();
|
||||
assert!(cache.get_peer_pub_key(&credential).is_some());
|
||||
|
||||
MockClock::advance_system_time(SEEN_CREDENTIAL_CACHE_TIME * 2);
|
||||
MockClock::advance(SEEN_CREDENTIAL_CACHE_TIME * 2);
|
||||
|
||||
cache.remove_stale();
|
||||
assert!(cache.get_peer_pub_key(&credential).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_time() {
|
||||
assert!(MockClock::is_thread_local());
|
||||
assert!(SystemTime::now().is_thread_local());
|
||||
|
||||
let mut cache = SeenCredentialCache::new();
|
||||
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
|
||||
let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap();
|
||||
|
||||
// set some value for time
|
||||
MockClock::set_system_time(Duration::from_secs(10));
|
||||
cache.insert_credential(credential.clone(), peer_pub_key);
|
||||
|
||||
// then set the time in the past
|
||||
MockClock::set_system_time(Duration::ZERO);
|
||||
cache.remove_stale();
|
||||
|
||||
// invalid time should remove the credential, just in case
|
||||
assert!(cache.get_peer_pub_key(&credential).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,10 +1002,7 @@ where
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Parse target gateway address
|
||||
let target_addr: SocketAddr = forward_data.target_lp_address.parse().map_err(|e| {
|
||||
inc!("lp_forward_failed");
|
||||
GatewayError::LpProtocolError(format!("Invalid target address: {}", e))
|
||||
})?;
|
||||
let target_addr = forward_data.target_lp_address;
|
||||
|
||||
// Check if we need to open a new connection
|
||||
let need_new_connection = match &self.exit_stream {
|
||||
@@ -1244,23 +1241,15 @@ 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
|
||||
@@ -1268,23 +1257,6 @@ 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
|
||||
@@ -1311,20 +1283,6 @@ 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 {
|
||||
@@ -1335,13 +1293,11 @@ mod tests {
|
||||
local_lp_peer: lp_peer,
|
||||
metrics: nym_node_metrics::NymNodeMetrics::default(),
|
||||
active_clients_store: ActiveClientsStore::new(),
|
||||
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)),
|
||||
peer_registrator: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,12 +68,11 @@
|
||||
// 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::wireguard::PeerRegistrator;
|
||||
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;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
@@ -85,7 +84,6 @@ use tokio::net::TcpListener;
|
||||
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,
|
||||
@@ -184,10 +182,6 @@ 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.
|
||||
@@ -257,9 +251,6 @@ 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;
|
||||
}
|
||||
@@ -273,7 +264,6 @@ 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,
|
||||
}
|
||||
@@ -360,12 +350,8 @@ pub struct LpHandlerState {
|
||||
/// Active clients tracking
|
||||
pub active_clients_store: ActiveClientsStore,
|
||||
|
||||
/// Current state of the Upgrade Mode as perceived by this gateway
|
||||
pub upgrade_mode: UpgradeModeDetails,
|
||||
|
||||
/// WireGuard gateway data (contains keypair and config)
|
||||
/// alongside helpers for managing peers
|
||||
pub peer_manager: Arc<PeerManager>,
|
||||
/// Handle registering new wireguard peers
|
||||
pub peer_registrator: Option<PeerRegistrator>,
|
||||
|
||||
/// LP configuration (for timestamp validation, etc.)
|
||||
pub lp_config: LpConfig,
|
||||
@@ -399,10 +385,6 @@ pub struct LpHandlerState {
|
||||
/// to rekey without re-authentication.
|
||||
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
|
||||
///
|
||||
/// Prevents file descriptor exhaustion when forwarding LP packets during
|
||||
@@ -577,31 +559,26 @@ 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, 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()
|
||||
"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()
|
||||
);
|
||||
|
||||
self.shutdown.try_spawn_named(
|
||||
cleanup_task::cleanup_loop(
|
||||
peer_manager,
|
||||
handshake_states,
|
||||
session_states,
|
||||
pending_registrations,
|
||||
dbg_cfg,
|
||||
shutdown,
|
||||
metrics,
|
||||
@@ -619,33 +596,27 @@ impl LpListener {
|
||||
}
|
||||
|
||||
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};
|
||||
use tracing::{debug, 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)
|
||||
@@ -680,33 +651,10 @@ pub(crate) mod cleanup_task {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
if hs_removed > 0 || ss_removed > 0 || demoted_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)",
|
||||
"LP state cleanup: removed {hs_removed} handshakes, {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
|
||||
duration.as_secs_f64()
|
||||
);
|
||||
|
||||
@@ -720,12 +668,6 @@ pub(crate) mod cleanup_task {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,10 +679,8 @@ pub(crate) mod cleanup_task {
|
||||
/// 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,
|
||||
@@ -757,7 +697,7 @@ pub(crate) mod cleanup_task {
|
||||
break;
|
||||
}
|
||||
_ = cleanup_interval.tick() => {
|
||||
perform_cleanup(&peer_manager, &handshake_states, &session_states, ®istrations_in_progress, cfg).await;
|
||||
perform_cleanup(&handshake_states, &session_states, cfg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::{LpHandlerState, ReceiverIndex, TimestampedState};
|
||||
use crate::error::GatewayError;
|
||||
use defguard_wireguard_rs::host::Peer;
|
||||
use defguard_wireguard_rs::key::Key;
|
||||
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::{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 crate::node::lp_listener::{LpHandlerState, ReceiverIndex};
|
||||
use nym_metrics::{add_histogram_obs, inc};
|
||||
use nym_registration_common::dvpn::{
|
||||
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
|
||||
LpDvpnRegistrationRequestMessage, LpDvpnRegistrationRequestMessageContent,
|
||||
@@ -24,15 +10,8 @@ use nym_registration_common::dvpn::{
|
||||
use nym_registration_common::mixnet::LpMixnetRegistrationRequestMessage;
|
||||
use nym_registration_common::{
|
||||
LpRegistrationRequest, LpRegistrationRequestData, LpRegistrationResponse, RegistrationMode,
|
||||
RegistrationStatus, WireguardConfiguration,
|
||||
RegistrationStatus,
|
||||
};
|
||||
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 tokio::sync::{Mutex, MutexGuard};
|
||||
use tracing::*;
|
||||
|
||||
// Histogram buckets for LP registration duration tracking
|
||||
@@ -49,224 +28,28 @@ const LP_REGISTRATION_DURATION_BUCKETS: &[f64] = &[
|
||||
30.0, // 30s
|
||||
];
|
||||
|
||||
// Histogram buckets for WireGuard peer controller channel latency
|
||||
// Measures time to send request and receive response from peer controller
|
||||
// Expected: 1ms-100ms for normal operations, up to 2s for slow conditions
|
||||
const WG_CONTROLLER_LATENCY_BUCKETS: &[f64] = &[
|
||||
0.001, // 1ms
|
||||
0.005, // 5ms
|
||||
0.01, // 10ms
|
||||
0.05, // 50ms
|
||||
0.1, // 100ms
|
||||
0.25, // 250ms
|
||||
0.5, // 500ms
|
||||
1.0, // 1s
|
||||
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
|
||||
{
|
||||
let Some(registrator) = self.peer_registrator.as_ref() else {
|
||||
return LpRegistrationResponse::error(
|
||||
format!("WireGuard peer IP allocation failed: {err}"),
|
||||
"dVPN via LP is not enabled on this node",
|
||||
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)
|
||||
}
|
||||
}
|
||||
registrator
|
||||
.on_initial_lp_request(request, sender)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
LpRegistrationResponse::error(
|
||||
format!("LP registration has failed: {err}"),
|
||||
RegistrationMode::Dvpn,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_dvpn_registration_finalisation(
|
||||
@@ -274,63 +57,22 @@ impl LpHandlerState {
|
||||
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 {
|
||||
let Some(registrator) = self.peer_registrator.as_ref() else {
|
||||
return LpRegistrationResponse::error(
|
||||
"no pending registration",
|
||||
"dVPN via LP is not enabled on this node",
|
||||
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)
|
||||
registrator
|
||||
.on_final_lp_request(request, sender)
|
||||
.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}"),
|
||||
.unwrap_or_else(|err| {
|
||||
LpRegistrationResponse::error(
|
||||
format!("LP registration has 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(
|
||||
@@ -414,162 +156,4 @@ impl LpHandlerState {
|
||||
|
||||
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>,
|
||||
client_id: i64,
|
||||
) -> Result<PersistedBandwidth, GatewayError> {
|
||||
// Check if bandwidth entry already exists (idempotent)
|
||||
let existing_bandwidth = ecash_verifier
|
||||
.storage()
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?;
|
||||
|
||||
// Only create if it doesn't exist
|
||||
if existing_bandwidth.is_none() {
|
||||
ecash_verifier
|
||||
.storage()
|
||||
.create_bandwidth_entry(client_id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let bandwidth = ecash_verifier
|
||||
.storage()
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.ok_or_else(|| GatewayError::InternalError("bandwidth entry should exist".to_string()))?;
|
||||
Ok(bandwidth)
|
||||
}
|
||||
|
||||
// TODO: dedup
|
||||
/// Verify credential and allocate bandwidth using CredentialVerifier
|
||||
async fn credential_verification(
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
credential: CredentialSpendingData,
|
||||
client_id: i64,
|
||||
) -> Result<i64, GatewayError> {
|
||||
let bandwidth = credential_storage_preparation(ecash_verifier.clone(), client_id).await?;
|
||||
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
|
||||
let mut verifier = CredentialVerifier::new(
|
||||
CredentialSpendingRequest::new(credential),
|
||||
ecash_verifier.clone(),
|
||||
BandwidthStorageManager::new(
|
||||
ecash_verifier.storage(),
|
||||
client_bandwidth,
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
// Track credential verification attempts
|
||||
inc!("lp_credential_verification_attempts");
|
||||
|
||||
// For mock ecash mode (local testing), skip cryptographic verification
|
||||
// and just return a dummy bandwidth value since we don't have blockchain access
|
||||
let allocated = if ecash_verifier.is_mock() {
|
||||
// Return a reasonable test bandwidth value (e.g., 1GB in bytes)
|
||||
const MOCK_BANDWIDTH: i64 = 1024 * 1024 * 1024;
|
||||
inc!("lp_credential_verification_success");
|
||||
inc_by!("lp_bandwidth_allocated_bytes_total", MOCK_BANDWIDTH);
|
||||
Ok::<i64, GatewayError>(MOCK_BANDWIDTH)
|
||||
} else {
|
||||
match verifier.verify().await {
|
||||
Ok(allocated) => {
|
||||
inc!("lp_credential_verification_success");
|
||||
// Track allocated bandwidth
|
||||
inc_by!("lp_bandwidth_allocated_bytes_total", allocated);
|
||||
Ok(allocated)
|
||||
}
|
||||
Err(e) => {
|
||||
inc!("lp_credential_verification_failed");
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(allocated)
|
||||
}
|
||||
|
||||
+21
-18
@@ -38,7 +38,7 @@ use tracing::*;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher;
|
||||
use crate::node::wireguard::PeerManager;
|
||||
use crate::node::wireguard::{PeerManager, PeerRegistrator};
|
||||
pub use client_handling::active_clients::ActiveClientsStore;
|
||||
pub use lp_listener::LpConfig;
|
||||
pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender;
|
||||
@@ -299,6 +299,22 @@ impl GatewayTasksBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_peer_registrator(
|
||||
&mut self,
|
||||
upgrade_mode_details: UpgradeModeDetails,
|
||||
) -> Result<Option<PeerRegistrator>, GatewayError> {
|
||||
let Some(wireguard_data) = &self.wireguard_data else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let peer_manager = PeerManager::new(wireguard_data.inner.clone());
|
||||
Ok(Some(PeerRegistrator::new(
|
||||
self.ecash_manager().await?,
|
||||
peer_manager,
|
||||
upgrade_mode_details,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn build_websocket_listener(
|
||||
&mut self,
|
||||
active_clients_store: ActiveClientsStore,
|
||||
@@ -330,19 +346,9 @@ impl GatewayTasksBuilder {
|
||||
|
||||
pub async fn build_lp_listener(
|
||||
&mut self,
|
||||
upgrade_mode_common_state: UpgradeModeDetails,
|
||||
peer_registrator: Option<PeerRegistrator>,
|
||||
active_clients_store: ActiveClientsStore,
|
||||
) -> Result<lp_listener::LpListener, GatewayError> {
|
||||
// Get WireGuard peer controller if available
|
||||
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?,
|
||||
storage: self.storage.clone(),
|
||||
@@ -353,13 +359,11 @@ impl GatewayTasksBuilder {
|
||||
.with_kem_psq_key(self.kem_psq_keys.clone()),
|
||||
metrics: self.metrics.clone(),
|
||||
active_clients_store,
|
||||
upgrade_mode: upgrade_mode_common_state,
|
||||
peer_manager,
|
||||
peer_registrator,
|
||||
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,
|
||||
)),
|
||||
@@ -495,11 +499,10 @@ impl GatewayTasksBuilder {
|
||||
|
||||
pub async fn build_wireguard_authenticator(
|
||||
&mut self,
|
||||
peer_registrator: PeerRegistrator,
|
||||
upgrade_mode_common: UpgradeModeDetails,
|
||||
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||
) -> Result<ServiceProviderBeingBuilt<Authenticator>, GatewayError> {
|
||||
let ecash_manager = self.ecash_manager().await?;
|
||||
|
||||
let Some(opts) = &self.authenticator_opts else {
|
||||
return Err(GatewayError::UnspecifiedAuthenticatorConfig);
|
||||
};
|
||||
@@ -519,9 +522,9 @@ impl GatewayTasksBuilder {
|
||||
|
||||
let mut authenticator_server = Authenticator::new(
|
||||
opts.config.clone(),
|
||||
peer_registrator,
|
||||
upgrade_mode_common,
|
||||
wireguard_data.inner.clone(),
|
||||
ecash_manager,
|
||||
self.shutdown_tracker.clone(),
|
||||
)
|
||||
.with_custom_gateway_transceiver(transceiver)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_credential_verification::upgrade_mode::UpgradeModeEnableError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -10,10 +11,48 @@ pub enum GatewayWireguardError {
|
||||
|
||||
#[error("peers can't be interacted with anymore")]
|
||||
PeerInteractionStopped,
|
||||
|
||||
#[error("registration is not in progress for the provided peer key")]
|
||||
RegistrationNotInProgress,
|
||||
|
||||
#[error("missing reply_to for old client")]
|
||||
MissingReplyToForOldClient,
|
||||
|
||||
#[error("unknown version number")]
|
||||
UnknownAuthenticatorVersion,
|
||||
|
||||
#[error("unsupported authenticator version")]
|
||||
UnsupportedAuthenticatorVersion,
|
||||
|
||||
#[error("mac does not verify")]
|
||||
AuthenticatorMacVerificationFailure,
|
||||
|
||||
#[error("no credential received")]
|
||||
MissingAuthenticatorCredential,
|
||||
|
||||
#[error(transparent)]
|
||||
UpgradeModeEnable(#[from] UpgradeModeEnableError),
|
||||
|
||||
#[error("credential verification failed: {0}")]
|
||||
CredentialVerificationError(#[from] nym_credential_verification::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
GatewayStorageError(#[from] nym_gateway_storage::error::GatewayStorageError),
|
||||
|
||||
#[error("failed to serialise authenticator response packet: {source}")]
|
||||
AuthenticatorResponseSerialisationFailure { source: Box<bincode::ErrorKind> },
|
||||
}
|
||||
|
||||
impl GatewayWireguardError {
|
||||
pub fn internal(message: impl Into<String>) -> Self {
|
||||
GatewayWireguardError::InternalError(message.into())
|
||||
}
|
||||
|
||||
pub fn authenticator_response_serialisation(
|
||||
source: impl Into<Box<bincode::ErrorKind>>,
|
||||
) -> Self {
|
||||
GatewayWireguardError::AuthenticatorResponseSerialisationFailure {
|
||||
source: source.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod error;
|
||||
pub mod new_peer_registration;
|
||||
pub mod peer_manager;
|
||||
|
||||
pub use error::GatewayWireguardError;
|
||||
pub use new_peer_registration::PeerRegistrator;
|
||||
pub use peer_manager::PeerManager;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::wireguard::new_peer_registration::helpers::build_final_authenticator_response;
|
||||
use crate::node::wireguard::new_peer_registration::pending::{
|
||||
PendingRegistration, PendingRegistrationData,
|
||||
};
|
||||
use crate::node::wireguard::{GatewayWireguardError, PeerRegistrator};
|
||||
use defguard_wireguard_rs::host::Peer;
|
||||
use nym_authenticator_requests::authenticator_ipv4_to_ipv6;
|
||||
use nym_authenticator_requests::response::SerialisedResponse;
|
||||
use nym_registration_common::WireguardRegistrationData;
|
||||
use nym_sdk::mixnet::Recipient;
|
||||
use nym_service_provider_requests_common::Protocol;
|
||||
use nym_wireguard::peer_controller::IpPair;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Instant;
|
||||
|
||||
impl PeerRegistrator {
|
||||
fn authenticator_peer_to_final_response(
|
||||
&self,
|
||||
peer: Peer,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
let allowed_ipv4 = peer
|
||||
.allowed_ips
|
||||
.iter()
|
||||
.find_map(|ip_mask| match ip_mask.address {
|
||||
IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or(GatewayWireguardError::internal(
|
||||
"there should be one private IPv4 in the list",
|
||||
))?;
|
||||
let allowed_ipv6 = peer
|
||||
.allowed_ips
|
||||
.iter()
|
||||
.find_map(|ip_mask| match ip_mask.address {
|
||||
IpAddr::V6(ipv6_addr) => Some(ipv6_addr),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(authenticator_ipv4_to_ipv6(allowed_ipv4));
|
||||
|
||||
let ip_allocation = IpPair::new(allowed_ipv4, allowed_ipv6);
|
||||
let wg_port = self.wg_port();
|
||||
let local_pub_key = (*self.keypair().public_key()).into();
|
||||
let upgrade_mode_enabled = self.upgrade_mode_enabled();
|
||||
|
||||
build_final_authenticator_response(
|
||||
ip_allocation,
|
||||
wg_port,
|
||||
local_pub_key,
|
||||
upgrade_mode_enabled,
|
||||
request_id,
|
||||
protocol.into(),
|
||||
reply_to,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn check_pending_authenticator_registration(
|
||||
&self,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
remote_public: PeerPublicKey,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<Option<SerialisedResponse>, GatewayWireguardError> {
|
||||
let Some(pending_registration) = self
|
||||
.pending_registrations
|
||||
.check_authenticator(&remote_public)
|
||||
.await
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
pending_registration.to_pending_authenticator_response(
|
||||
self.keypair().private_key(),
|
||||
self.upgrade_mode_enabled(),
|
||||
request_id,
|
||||
protocol.into(),
|
||||
reply_to,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn check_existing_authenticator_peer(
|
||||
&self,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
remote_public: PeerPublicKey,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<Option<SerialisedResponse>, GatewayWireguardError> {
|
||||
let Some(peer) = self.peer_manager.query_peer(remote_public).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(self.authenticator_peer_to_final_response(
|
||||
peer, protocol, request_id, reply_to,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(super) fn new_pending_authenticator(
|
||||
&self,
|
||||
peer: PeerPublicKey,
|
||||
ip_allocation: IpPair,
|
||||
) -> PendingRegistration {
|
||||
let nonce: u64 = fastrand::u64(..);
|
||||
|
||||
PendingRegistration {
|
||||
requested_on: Instant::now(),
|
||||
data: PendingRegistrationData {
|
||||
nonce,
|
||||
peer_key: peer,
|
||||
psk: None,
|
||||
wireguard_config: WireguardRegistrationData {
|
||||
public_key: *self.keypair().public_key(),
|
||||
port: self.wg_port(),
|
||||
private_ipv4: ip_allocation.ipv4,
|
||||
private_ipv6: ip_allocation.ipv6,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn process_fresh_initial_authenticator_registration(
|
||||
&self,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
remote_public: PeerPublicKey,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
// 1. allocate ip pair
|
||||
let ip_allocation = self.peer_manager.preallocate_peer_ip_pair().await?;
|
||||
|
||||
let pending = self.new_pending_authenticator(remote_public, ip_allocation);
|
||||
|
||||
// 2. construct response
|
||||
let response = pending.to_pending_authenticator_response(
|
||||
self.keypair().private_key(),
|
||||
self.upgrade_mode_enabled(),
|
||||
request_id,
|
||||
protocol.into(),
|
||||
reply_to,
|
||||
)?;
|
||||
|
||||
// 3. insert pending data into cache
|
||||
self.pending_registrations
|
||||
.authenticator
|
||||
.write()
|
||||
.await
|
||||
.insert(remote_public, pending);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::wireguard::GatewayWireguardError;
|
||||
use nym_authenticator_requests::response::SerialisedResponse;
|
||||
use nym_authenticator_requests::{v1, v2, v3, v4, v5, v6, AuthenticatorVersion};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_sdk::mixnet::Recipient;
|
||||
use nym_wireguard::ip_pool::IpPair;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn build_pending_authenticator_response(
|
||||
ip_allocation: IpPair,
|
||||
wg_port: u16,
|
||||
local_key: &x25519::PrivateKey,
|
||||
peer_key: PeerPublicKey,
|
||||
upgrade_mode_enabled: bool,
|
||||
nonce: u64,
|
||||
request_id: u64,
|
||||
version: AuthenticatorVersion,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
let private_ipv4 = ip_allocation.ipv4;
|
||||
let private_ipv6 = ip_allocation.ipv6;
|
||||
|
||||
let bytes = match version {
|
||||
AuthenticatorVersion::V1 => Err(GatewayWireguardError::UnsupportedAuthenticatorVersion),
|
||||
AuthenticatorVersion::V2 => {
|
||||
v2::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v2::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: v2::registration::GatewayClient::new(
|
||||
local_key,
|
||||
peer_key.inner(),
|
||||
private_ipv4.into(),
|
||||
nonce,
|
||||
),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)
|
||||
}
|
||||
AuthenticatorVersion::V3 => {
|
||||
v3::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v3::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: v3::registration::GatewayClient::new(
|
||||
local_key,
|
||||
peer_key.inner(),
|
||||
private_ipv4.into(),
|
||||
nonce,
|
||||
),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)
|
||||
}
|
||||
AuthenticatorVersion::V4 => {
|
||||
v4::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v4::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: v4::registration::GatewayClient::new(
|
||||
local_key,
|
||||
peer_key.inner(),
|
||||
v4::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
nonce,
|
||||
),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)
|
||||
}
|
||||
AuthenticatorVersion::V5 => {
|
||||
v5::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v5::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: v5::registration::GatewayClient::new(
|
||||
local_key,
|
||||
peer_key.inner(),
|
||||
v5::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
nonce,
|
||||
),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)
|
||||
}
|
||||
AuthenticatorVersion::V6 => {
|
||||
v6::response::AuthenticatorResponse::new_pending_registration_success(
|
||||
v6::registration::RegistrationData {
|
||||
nonce,
|
||||
gateway_data: v6::registration::GatewayClient::new(
|
||||
local_key,
|
||||
peer_key.inner(),
|
||||
v6::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
nonce,
|
||||
),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
upgrade_mode_enabled,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)
|
||||
}
|
||||
AuthenticatorVersion::UNKNOWN => {
|
||||
return Err(GatewayWireguardError::UnknownAuthenticatorVersion)
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(nym_authenticator_requests::response::SerialisedResponse::new(bytes, reply_to))
|
||||
}
|
||||
|
||||
pub(crate) fn build_final_authenticator_response(
|
||||
ip_allocation: IpPair,
|
||||
wg_port: u16,
|
||||
pub_key: PeerPublicKey,
|
||||
upgrade_mode_enabled: bool,
|
||||
request_id: u64,
|
||||
version: AuthenticatorVersion,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
let private_ipv4 = ip_allocation.ipv4;
|
||||
let private_ipv6 = ip_allocation.ipv6;
|
||||
|
||||
let bytes = match version {
|
||||
AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered(
|
||||
v1::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ip: private_ipv4.into(),
|
||||
wg_port,
|
||||
},
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered(
|
||||
v2::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ip: private_ipv4.into(),
|
||||
wg_port,
|
||||
},
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered(
|
||||
v3::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ip: private_ipv4.into(),
|
||||
wg_port,
|
||||
},
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered(
|
||||
v4::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ips: v4::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
wg_port,
|
||||
},
|
||||
reply_to.ok_or(GatewayWireguardError::MissingReplyToForOldClient)?,
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered(
|
||||
v5::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ips: v5::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered(
|
||||
v6::registration::RegisteredData {
|
||||
pub_key,
|
||||
private_ips: v6::registration::IpPair::new(private_ipv4, private_ipv6),
|
||||
wg_port,
|
||||
},
|
||||
request_id,
|
||||
upgrade_mode_enabled,
|
||||
)
|
||||
.to_bytes()
|
||||
.map_err(GatewayWireguardError::authenticator_response_serialisation)?,
|
||||
AuthenticatorVersion::UNKNOWN => {
|
||||
return Err(GatewayWireguardError::UnknownAuthenticatorVersion)
|
||||
}
|
||||
};
|
||||
Ok(SerialisedResponse::new(bytes, reply_to))
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::lp_listener::ReceiverIndex;
|
||||
use crate::node::wireguard::new_peer_registration::pending::{
|
||||
PendingRegistration, PendingRegistrationData,
|
||||
};
|
||||
use crate::node::wireguard::{GatewayWireguardError, PeerRegistrator};
|
||||
use defguard_wireguard_rs::host::Peer;
|
||||
use defguard_wireguard_rs::key::Key;
|
||||
use nym_registration_common::{LpRegistrationResponse, WireguardRegistrationData};
|
||||
use nym_wireguard::ip_pool::{allocated_ip_pair, IpPair};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::time::Instant;
|
||||
|
||||
impl PeerRegistrator {
|
||||
/// In the case of an already registered WG peer, update its PSK.
|
||||
pub(super) async fn update_peer_psk(
|
||||
&self,
|
||||
peer: PeerPublicKey,
|
||||
psk: Key,
|
||||
) -> Result<(), GatewayWireguardError> {
|
||||
// 1. check if the peer is currently being handled
|
||||
if self.peer_manager.check_active_peer(peer).await? {
|
||||
// 2. if so, force disconnect it (as we're handling new request from the same peer)
|
||||
self.peer_manager.remove_peer(peer).await?;
|
||||
}
|
||||
|
||||
// 3. update the on-disk PSK
|
||||
let encoded_psk = psk.to_lower_hex();
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lp_peer_to_final_response(
|
||||
&self,
|
||||
peer: Peer,
|
||||
) -> Result<Option<LpRegistrationResponse>, GatewayWireguardError> {
|
||||
// Incomplete data, treat as new registration
|
||||
let Some(allocated_ips) = allocated_ip_pair(&peer) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(LpRegistrationResponse::success_dvpn(
|
||||
WireguardRegistrationData {
|
||||
public_key: *self.keypair().public_key(),
|
||||
port: self.wg_port(),
|
||||
private_ipv4: allocated_ips.ipv4,
|
||||
private_ipv6: allocated_ips.ipv6,
|
||||
},
|
||||
self.upgrade_mode_enabled(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub(super) async fn check_pending_lp_registration(
|
||||
&self,
|
||||
sender: ReceiverIndex,
|
||||
) -> Result<Option<LpRegistrationResponse>, GatewayWireguardError> {
|
||||
let Some(pending_registration) = self.pending_registrations.check_lp(sender).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(pending_registration.to_pending_lp_response()))
|
||||
}
|
||||
|
||||
pub(super) async fn check_existing_lp_peer(
|
||||
&self,
|
||||
remote_public: PeerPublicKey,
|
||||
) -> Result<Option<LpRegistrationResponse>, GatewayWireguardError> {
|
||||
let Some(peer) = self.peer_manager.query_peer(remote_public).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
self.lp_peer_to_final_response(peer)
|
||||
}
|
||||
|
||||
pub(super) fn new_pending_lp(
|
||||
&self,
|
||||
peer: PeerPublicKey,
|
||||
psk: Key,
|
||||
ip_allocation: IpPair,
|
||||
) -> PendingRegistration {
|
||||
let nonce: u64 = fastrand::u64(..);
|
||||
|
||||
PendingRegistration {
|
||||
requested_on: Instant::now(),
|
||||
data: PendingRegistrationData {
|
||||
nonce,
|
||||
peer_key: peer,
|
||||
psk: Some(psk),
|
||||
wireguard_config: WireguardRegistrationData {
|
||||
public_key: *self.keypair().public_key(),
|
||||
port: self.wg_port(),
|
||||
private_ipv4: ip_allocation.ipv4,
|
||||
private_ipv6: ip_allocation.ipv6,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn process_fresh_initial_lp_registration(
|
||||
&self,
|
||||
sender: ReceiverIndex,
|
||||
remote_public: PeerPublicKey,
|
||||
psk: Key,
|
||||
) -> Result<LpRegistrationResponse, GatewayWireguardError> {
|
||||
// 1. allocate ip pair
|
||||
let ip_allocation = self.peer_manager.preallocate_peer_ip_pair().await?;
|
||||
|
||||
let pending = self.new_pending_lp(remote_public, psk, ip_allocation);
|
||||
|
||||
// 2. construct response
|
||||
let response = pending.to_pending_lp_response();
|
||||
|
||||
// 3. insert pending data into cache
|
||||
self.pending_registrations
|
||||
.lp
|
||||
.write()
|
||||
.await
|
||||
.insert(sender, pending);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Unification of Nym registration flow
|
||||
//! In general the registration has the following structure:
|
||||
//! 1. Initial request message is received
|
||||
//! 1.1. We check if the peer has already registered before -> if so, we returned the past information
|
||||
//! 1.2. We check if the peer already has a pending registration -> if so, we return the past information
|
||||
//! 1.3. We pre-allocated [`nym_wireguard::ip_pool::IpPair`] and save time-sensitive pending registration.
|
||||
//! If it does not complete within specified time interval, the information is going to get removed.
|
||||
//! 2. Finalisation request message is received, where credential has to be attached is verified.
|
||||
//! Upon successful completion, pending registration is transformed into a properly inserted peer.
|
||||
|
||||
use crate::node::lp_listener::ReceiverIndex;
|
||||
use crate::node::wireguard::new_peer_registration::pending::{
|
||||
PendingRegistration, PendingRegistrations,
|
||||
};
|
||||
use crate::node::wireguard::{GatewayWireguardError, PeerManager};
|
||||
use defguard_wireguard_rs::host::Peer;
|
||||
use defguard_wireguard_rs::key::Key;
|
||||
use defguard_wireguard_rs::net::IpAddrMask;
|
||||
use nym_authenticator_requests::models::BandwidthClaim;
|
||||
use nym_authenticator_requests::response::SerialisedResponse;
|
||||
use nym_authenticator_requests::traits::{FinalMessage, InitMessage};
|
||||
use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager;
|
||||
use nym_credential_verification::ecash::traits::EcashManager;
|
||||
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
|
||||
use nym_credential_verification::{
|
||||
BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier,
|
||||
};
|
||||
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_gateway_requests::models::CredentialSpendingRequest;
|
||||
use nym_gateway_storage::models::PersistedBandwidth;
|
||||
use nym_registration_common::dvpn::{
|
||||
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
|
||||
};
|
||||
use nym_registration_common::LpRegistrationResponse;
|
||||
use nym_sdk::mixnet::Recipient;
|
||||
use nym_service_provider_requests_common::Protocol;
|
||||
use nym_task::ShutdownToken;
|
||||
use nym_wireguard::WireguardConfig;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::time::{interval_at, Instant};
|
||||
use tracing::trace;
|
||||
|
||||
mod authenticator;
|
||||
mod helpers;
|
||||
mod lp;
|
||||
mod pending;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PeerRegistrator {
|
||||
/// Handle for the structure managing verification of the ecash credentials for the bandwidth control
|
||||
pub(crate) ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
|
||||
/// Handle for communication with the [`nym_wireguard::peer_controller::PeerController`]
|
||||
pub(crate) peer_manager: PeerManager,
|
||||
|
||||
/// Information about the current state of the upgrade mode as well as a handle
|
||||
/// to remotely trigger the recheck
|
||||
pub(crate) upgrade_mode: UpgradeModeDetails,
|
||||
|
||||
/// Registrations in progress
|
||||
pub(crate) pending_registrations: PendingRegistrations,
|
||||
}
|
||||
|
||||
impl PeerRegistrator {
|
||||
pub fn new(
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
peer_manager: PeerManager,
|
||||
upgrade_mode: UpgradeModeDetails,
|
||||
) -> Self {
|
||||
PeerRegistrator {
|
||||
ecash_verifier,
|
||||
peer_manager,
|
||||
upgrade_mode,
|
||||
pending_registrations: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup_task(&self, shutdown_token: ShutdownToken) -> StaleRegistrationRemover {
|
||||
StaleRegistrationRemover {
|
||||
pending_registrations: self.pending_registrations.clone(),
|
||||
shutdown_token,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_mode_enabled(&self) -> bool {
|
||||
self.upgrade_mode.enabled()
|
||||
}
|
||||
|
||||
fn keypair(&self) -> &Arc<x25519::KeyPair> {
|
||||
self.peer_manager.wireguard_gateway_data.keypair()
|
||||
}
|
||||
|
||||
fn wireguard_config(&self) -> WireguardConfig {
|
||||
self.peer_manager.wireguard_gateway_data.config()
|
||||
}
|
||||
|
||||
fn wg_port(&self) -> u16 {
|
||||
self.wireguard_config().announced_tunnel_port
|
||||
}
|
||||
|
||||
pub async fn credential_storage_preparation(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<PersistedBandwidth, GatewayWireguardError> {
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.create_bandwidth_entry(client_id)
|
||||
.await?;
|
||||
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.ok_or(GatewayWireguardError::internal(
|
||||
"missing bandwidth entry after it has just been created",
|
||||
))
|
||||
}
|
||||
|
||||
async fn credential_verification(
|
||||
&self,
|
||||
credential: CredentialSpendingData,
|
||||
client_id: i64,
|
||||
) -> Result<i64, GatewayWireguardError> {
|
||||
let bandwidth = self.credential_storage_preparation(client_id).await?;
|
||||
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
|
||||
let mut verifier = CredentialVerifier::new(
|
||||
CredentialSpendingRequest::new(credential),
|
||||
self.ecash_verifier.clone(),
|
||||
BandwidthStorageManager::new(
|
||||
self.ecash_verifier.storage(),
|
||||
client_bandwidth,
|
||||
client_id,
|
||||
BandwidthFlushingBehaviourConfig::default(),
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
Ok(verifier.verify().await?)
|
||||
}
|
||||
|
||||
async fn handle_final_credential_claim(
|
||||
&self,
|
||||
claim: BandwidthClaim,
|
||||
client_id: i64,
|
||||
) -> Result<(), GatewayWireguardError> {
|
||||
match claim.credential {
|
||||
BandwidthCredential::ZkNym(zk_nym) => {
|
||||
// if we got zk-nym, we just try to verify it
|
||||
self.credential_verification(*zk_nym, client_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
BandwidthCredential::UpgradeModeJWT { token } => {
|
||||
// if we're already in the upgrade mode, don't bother validating the token
|
||||
if self.upgrade_mode_enabled() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.upgrade_mode.try_enable_via_received_jwt(token).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to process new peer by:
|
||||
/// 1. retrieving previous IP allocation
|
||||
/// 2. inserting it into the storage
|
||||
/// 3. verifying bandwidth claim and increasing the allowance
|
||||
/// 4. spawning the peer handler
|
||||
async fn process_new_peer(
|
||||
&self,
|
||||
pending: PendingRegistration,
|
||||
credential: BandwidthClaim,
|
||||
) -> Result<(), GatewayWireguardError> {
|
||||
// 1. create peer based on the cached registration information
|
||||
let defguard_key = Key::new(pending.data.peer_key.to_bytes());
|
||||
let mut peer = Peer::new(defguard_key);
|
||||
if let Some(psk) = pending.data.psk {
|
||||
peer.preshared_key = Some(psk);
|
||||
}
|
||||
let private_ipv4 = pending.data.wireguard_config.private_ipv4;
|
||||
let private_ipv6 = pending.data.wireguard_config.private_ipv6;
|
||||
peer.allowed_ips = vec![
|
||||
IpAddrMask::new(private_ipv4.into(), 32),
|
||||
IpAddrMask::new(private_ipv6.into(), 128),
|
||||
];
|
||||
|
||||
let typ = credential.kind;
|
||||
|
||||
// 2. attempt to pre-insert peer into the storage
|
||||
let client_id = self
|
||||
.ecash_verifier
|
||||
.storage()
|
||||
.insert_wireguard_peer(&peer, typ.into())
|
||||
.await?;
|
||||
|
||||
// 3. verify the credential
|
||||
if let Err(err) = self
|
||||
.handle_final_credential_claim(credential, client_id)
|
||||
.await
|
||||
{
|
||||
// 3.1. on failure -> remove the inserted peer
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.remove_wireguard_peer(&peer.public_key.to_string())
|
||||
.await?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// 4. attempt to start the actual handle for the peer
|
||||
let public_key = peer.public_key.to_string();
|
||||
if let Err(err) = self.peer_manager.add_peer(peer).await {
|
||||
// 4.1. on failure -> remove the inserted peer (from the storage)
|
||||
self.ecash_verifier
|
||||
.storage()
|
||||
.remove_wireguard_peer(&public_key)
|
||||
.await?;
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn on_initial_authenticator_request(
|
||||
&mut self,
|
||||
init_message: Box<dyn InitMessage + Send + Sync + 'static>,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
let remote_public = init_message.pub_key();
|
||||
|
||||
// 1. check if there's any pending registration already in progress,
|
||||
// if so, return the same data again without additional processing
|
||||
if let Some(pending_registration) = self
|
||||
.check_pending_authenticator_registration(protocol, request_id, remote_public, reply_to)
|
||||
.await?
|
||||
{
|
||||
return Ok(pending_registration);
|
||||
}
|
||||
|
||||
// 2. check if there is already a peer associated with this sender,
|
||||
// if so, retrieve the "final" data without additional processing
|
||||
if let Some(existing_registration) = self
|
||||
.check_existing_authenticator_peer(protocol, request_id, remote_public, reply_to)
|
||||
.await?
|
||||
{
|
||||
return Ok(existing_registration);
|
||||
}
|
||||
|
||||
// 3. process fresh registration request
|
||||
self.process_fresh_initial_authenticator_registration(
|
||||
protocol,
|
||||
request_id,
|
||||
remote_public,
|
||||
reply_to,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn on_final_authenticator_request(
|
||||
&mut self,
|
||||
final_message: Box<dyn FinalMessage + Send + Sync + 'static>,
|
||||
protocol: Protocol,
|
||||
request_id: u64,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<SerialisedResponse, GatewayWireguardError> {
|
||||
let peer = final_message.gateway_client_pub_key();
|
||||
// 1. check if there's any pending registration associated with this peer
|
||||
let pending_data = self
|
||||
.pending_registrations
|
||||
.check_authenticator(&peer)
|
||||
.await
|
||||
.ok_or(GatewayWireguardError::RegistrationNotInProgress)?
|
||||
.clone();
|
||||
|
||||
// 2. verify the correctness of the received request based on the prior nonce
|
||||
if final_message
|
||||
.verify(self.keypair().private_key(), pending_data.data.nonce)
|
||||
.is_err()
|
||||
{
|
||||
return Err(GatewayWireguardError::AuthenticatorMacVerificationFailure);
|
||||
}
|
||||
|
||||
// 3. ensure we have received a credential
|
||||
let Some(credential) = final_message.credential() else {
|
||||
return Err(GatewayWireguardError::MissingAuthenticatorCredential);
|
||||
};
|
||||
|
||||
// 4. prepare new peer information and verify the credential
|
||||
self.process_new_peer(pending_data.clone(), credential)
|
||||
.await?;
|
||||
|
||||
// 5. remove pending registration
|
||||
self.pending_registrations.remove_authenticator(&peer).await;
|
||||
|
||||
// 6. construct and return the response
|
||||
pending_data.to_registered_authenticator_response(
|
||||
self.upgrade_mode_enabled(),
|
||||
request_id,
|
||||
protocol.into(),
|
||||
reply_to,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn on_initial_lp_request(
|
||||
&self,
|
||||
init_msg: LpDvpnRegistrationInitialRequest,
|
||||
sender: ReceiverIndex,
|
||||
) -> Result<LpRegistrationResponse, GatewayWireguardError> {
|
||||
let remote_public = init_msg.wg_public_key;
|
||||
let psk = Key::new(init_msg.psk);
|
||||
|
||||
// 1. check if there's any pending registration already in progress,
|
||||
// if so, return the same data again without additional processing,
|
||||
// but update stored PSK
|
||||
if let Some(pending_registration) = self.check_pending_lp_registration(sender).await? {
|
||||
self.update_peer_psk(remote_public, psk).await?;
|
||||
return Ok(pending_registration);
|
||||
}
|
||||
|
||||
// 2. check if there is already a peer associated with this sender,
|
||||
// if so, retrieve the "final" data without additional processing,
|
||||
// but do update stored PSK
|
||||
if let Some(existing_registration) = self.check_existing_lp_peer(remote_public).await? {
|
||||
self.update_peer_psk(remote_public, psk).await?;
|
||||
return Ok(existing_registration);
|
||||
}
|
||||
|
||||
// 3. process fresh registration request
|
||||
self.process_fresh_initial_lp_registration(sender, remote_public, psk)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn on_final_lp_request(
|
||||
&self,
|
||||
final_msg: LpDvpnRegistrationFinalisation,
|
||||
sender: ReceiverIndex,
|
||||
) -> Result<LpRegistrationResponse, GatewayWireguardError> {
|
||||
// 1. check if there's any pending registration associated with this peer
|
||||
let pending_data = self
|
||||
.pending_registrations
|
||||
.check_lp(sender)
|
||||
.await
|
||||
.ok_or(GatewayWireguardError::RegistrationNotInProgress)?
|
||||
.clone();
|
||||
|
||||
let credential = final_msg.credential;
|
||||
|
||||
// 2. prepare new peer information and verify the credential
|
||||
self.process_new_peer(pending_data.clone(), credential)
|
||||
.await?;
|
||||
|
||||
// 3 remove pending registration
|
||||
self.pending_registrations.remove_lp(sender).await;
|
||||
|
||||
// 4. construct and return the response
|
||||
Ok(pending_data.to_registered_lp_response(self.upgrade_mode_enabled()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StaleRegistrationRemover {
|
||||
pending_registrations: PendingRegistrations,
|
||||
shutdown_token: ShutdownToken,
|
||||
}
|
||||
|
||||
impl StaleRegistrationRemover {
|
||||
// TODO: make it configurable
|
||||
const STALE_REG_CHECK_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub async fn run(&self) {
|
||||
let start = Instant::now() + Self::STALE_REG_CHECK_INTERVAL;
|
||||
let mut interval = interval_at(start, Self::STALE_REG_CHECK_INTERVAL);
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.shutdown_token.cancelled() => {
|
||||
trace!("StaleRegistrationRemover: received shutdown");
|
||||
break
|
||||
}
|
||||
_ = interval.tick() => {
|
||||
self.pending_registrations.remove_stale_registrations().await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::node::lp_listener::ReceiverIndex;
|
||||
use crate::node::wireguard::new_peer_registration::helpers::{
|
||||
build_final_authenticator_response, build_pending_authenticator_response,
|
||||
};
|
||||
use crate::node::wireguard::GatewayWireguardError;
|
||||
use defguard_wireguard_rs::key::Key;
|
||||
use nym_authenticator_requests::AuthenticatorVersion;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_registration_common::{LpRegistrationResponse, WireguardRegistrationData};
|
||||
use nym_sdk::mixnet::Recipient;
|
||||
use nym_wireguard::ip_pool::IpPair;
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
const DEFAULT_PENDING_REGISTRATION_TTL: Duration = Duration::from_secs(120); // 2 minutes
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PendingRegistration {
|
||||
pub(super) requested_on: Instant,
|
||||
pub(super) data: PendingRegistrationData,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PendingRegistrationData {
|
||||
pub(super) nonce: u64,
|
||||
|
||||
pub(super) peer_key: PeerPublicKey,
|
||||
|
||||
// will not be set if registering via the Authenticator
|
||||
pub(super) psk: Option<Key>,
|
||||
|
||||
pub(super) wireguard_config: WireguardRegistrationData,
|
||||
}
|
||||
|
||||
impl PendingRegistration {
|
||||
pub(crate) fn to_pending_authenticator_response(
|
||||
&self,
|
||||
local_key: &x25519::PrivateKey,
|
||||
upgrade_mode_enabled: bool,
|
||||
request_id: u64,
|
||||
version: AuthenticatorVersion,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<nym_authenticator_requests::response::SerialisedResponse, GatewayWireguardError>
|
||||
{
|
||||
let nonce = self.data.nonce;
|
||||
let remote_public = self.data.peer_key;
|
||||
let wg_port = self.data.wireguard_config.port;
|
||||
let ip_allocation = IpPair::new(
|
||||
self.data.wireguard_config.private_ipv4,
|
||||
self.data.wireguard_config.private_ipv6,
|
||||
);
|
||||
|
||||
build_pending_authenticator_response(
|
||||
ip_allocation,
|
||||
wg_port,
|
||||
local_key,
|
||||
remote_public,
|
||||
upgrade_mode_enabled,
|
||||
nonce,
|
||||
request_id,
|
||||
version,
|
||||
reply_to,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_registered_authenticator_response(
|
||||
&self,
|
||||
upgrade_mode_enabled: bool,
|
||||
request_id: u64,
|
||||
version: AuthenticatorVersion,
|
||||
reply_to: Option<Recipient>,
|
||||
) -> Result<nym_authenticator_requests::response::SerialisedResponse, GatewayWireguardError>
|
||||
{
|
||||
let wg_port = self.data.wireguard_config.port;
|
||||
let local_pub_key = self.data.wireguard_config.public_key.into();
|
||||
|
||||
let ip_allocation = IpPair::new(
|
||||
self.data.wireguard_config.private_ipv4,
|
||||
self.data.wireguard_config.private_ipv6,
|
||||
);
|
||||
|
||||
build_final_authenticator_response(
|
||||
ip_allocation,
|
||||
wg_port,
|
||||
local_pub_key,
|
||||
upgrade_mode_enabled,
|
||||
request_id,
|
||||
version,
|
||||
reply_to,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn to_pending_lp_response(&self) -> LpRegistrationResponse {
|
||||
LpRegistrationResponse::request_dvpn_credential()
|
||||
}
|
||||
|
||||
pub(crate) fn to_registered_lp_response(
|
||||
&self,
|
||||
upgrade_mode_enabled: bool,
|
||||
) -> LpRegistrationResponse {
|
||||
LpRegistrationResponse::success_dvpn(self.data.wireguard_config, upgrade_mode_enabled)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct PendingRegistrations {
|
||||
// TODO: unify those, somehow, later
|
||||
/// Registrations in progress received from the Authenticator service provider via the
|
||||
/// [`crate::node::internal_service_providers::authenticator::mixnet_listener::MixnetListener`]
|
||||
pub(crate) authenticator: Arc<RwLock<HashMap<PeerPublicKey, PendingRegistration>>>,
|
||||
|
||||
/// Registrations in progress received from the LP Listener via the
|
||||
/// [`crate::node::lp_listener::handler::LpConnectionHandler`] and handle through
|
||||
/// [`crate::node::lp_listener::registration::LpHandlerState`]
|
||||
pub(crate) lp: Arc<RwLock<HashMap<ReceiverIndex, PendingRegistration>>>,
|
||||
}
|
||||
|
||||
impl PendingRegistrations {
|
||||
pub(crate) async fn check_authenticator(
|
||||
&self,
|
||||
peer: &PeerPublicKey,
|
||||
) -> Option<PendingRegistration> {
|
||||
self.authenticator.read().await.get(peer).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_authenticator(&self, peer: &PeerPublicKey) {
|
||||
self.authenticator.write().await.remove(peer);
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_lp(&self, receiver_index: ReceiverIndex) {
|
||||
self.lp.write().await.remove(&receiver_index);
|
||||
}
|
||||
|
||||
pub(crate) async fn check_lp(
|
||||
&self,
|
||||
receiver_index: ReceiverIndex,
|
||||
) -> Option<PendingRegistration> {
|
||||
self.lp.read().await.get(&receiver_index).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_stale_registrations(&self) {
|
||||
// note: `IpPool` will release stale pre-allocated addresses by itself during the cleanup,
|
||||
// so there's no need to send explicit messages over
|
||||
let now = Instant::now();
|
||||
self.authenticator.write().await.retain(|_, pending| {
|
||||
now.duration_since(pending.requested_on) < DEFAULT_PENDING_REGISTRATION_TTL
|
||||
});
|
||||
self.lp.write().await.retain(|_, pending| {
|
||||
now.duration_since(pending.requested_on) < DEFAULT_PENDING_REGISTRATION_TTL
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,28 @@ 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_metrics::add_histogram_obs;
|
||||
use nym_wireguard::peer_controller::IpPair;
|
||||
use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use std::time::Instant;
|
||||
use tracing::error;
|
||||
|
||||
// Histogram buckets for WireGuard peer controller channel latency
|
||||
// Measures time to send request and receive response from peer controller
|
||||
// Expected: 1ms-100ms for normal operations, up to 2s for slow conditions
|
||||
const WG_CONTROLLER_LATENCY_BUCKETS: &[f64] = &[
|
||||
0.001, // 1ms
|
||||
0.005, // 5ms
|
||||
0.01, // 10ms
|
||||
0.05, // 50ms
|
||||
0.1, // 100ms
|
||||
0.25, // 250ms
|
||||
0.5, // 500ms
|
||||
1.0, // 1s
|
||||
2.0, // 2s
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PeerManager {
|
||||
pub(crate) wireguard_gateway_data: WireguardGatewayData,
|
||||
@@ -23,16 +40,17 @@ impl PeerManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn allocate_peer_ip_pair(&self) -> Result<IpPair, GatewayWireguardError> {
|
||||
pub async fn preallocate_peer_ip_pair(&self) -> Result<IpPair, GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::AllocatePeerIpPair { response_tx };
|
||||
let msg = PeerControlRequest::PreAllocateIpPair { response_tx };
|
||||
self.wireguard_gateway_data
|
||||
.peer_tx()
|
||||
.send(msg)
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|e| {
|
||||
GatewayWireguardError::InternalError(format!(
|
||||
@@ -42,7 +60,16 @@ impl PeerManager {
|
||||
.map_err(|e| {
|
||||
error!("Failed to allocate IPs from pool: {e}");
|
||||
GatewayWireguardError::InternalError(format!("Failed to allocate IPs: {e}"))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn release_ip_pair(&self, ip_pair: IpPair) -> Result<(), GatewayWireguardError> {
|
||||
@@ -70,6 +97,7 @@ impl PeerManager {
|
||||
}
|
||||
|
||||
pub async fn add_peer(&self, peer: Peer) -> Result<(), GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::AddPeer { peer, response_tx };
|
||||
self.wireguard_gateway_data
|
||||
@@ -78,17 +106,27 @@ impl PeerManager {
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for add peer".to_string()))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::internal(format!(
|
||||
"adding peer could not be performed: {err:?}"
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), GatewayWireguardError> {
|
||||
pub async fn remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(pub_key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::RemovePeer { key, response_tx };
|
||||
@@ -98,20 +136,63 @@ impl PeerManager {
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for remove peer"))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::InternalError(format!(
|
||||
"removing peer could not be performed: {err:?}"
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn check_active_peer(
|
||||
&self,
|
||||
pub_key: PeerPublicKey,
|
||||
) -> Result<bool, GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(pub_key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::CheckActivePeer { key, response_tx };
|
||||
self.wireguard_gateway_data
|
||||
.peer_tx()
|
||||
.send(msg)
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for check active peer"))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::InternalError(format!(
|
||||
"check active peer could not be performed: {err:?}"
|
||||
))
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn query_peer(
|
||||
&self,
|
||||
public_key: PeerPublicKey,
|
||||
) -> Result<Option<Peer>, GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(public_key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::QueryPeer { key, response_tx };
|
||||
@@ -121,14 +202,23 @@ impl PeerManager {
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for query peer".to_string()))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::internal(format!(
|
||||
"querying peer could not be performed: {err:?}"
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn query_bandwidth(
|
||||
@@ -143,6 +233,7 @@ impl PeerManager {
|
||||
&self,
|
||||
key: PeerPublicKey,
|
||||
) -> Result<ClientBandwidth, GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::GetClientBandwidthByKey { key, response_tx };
|
||||
@@ -152,14 +243,23 @@ impl PeerManager {
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::internal("no response for query client bandwidth"))?
|
||||
.map_err(|err| {
|
||||
GatewayWireguardError::internal(format!(
|
||||
"querying client bandwidth could not be performed: {err:?}"
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn query_verifier_by_key(
|
||||
@@ -167,6 +267,7 @@ impl PeerManager {
|
||||
key: PeerPublicKey,
|
||||
credential: CredentialSpendingData,
|
||||
) -> Result<Box<dyn TicketVerifier + Send + Sync>, GatewayWireguardError> {
|
||||
let controller_start = Instant::now();
|
||||
let key = Key::new(key.to_bytes());
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let msg = PeerControlRequest::GetVerifierByKey {
|
||||
@@ -180,7 +281,7 @@ impl PeerManager {
|
||||
.await
|
||||
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
|
||||
|
||||
response_rx
|
||||
let res = response_rx
|
||||
.await
|
||||
.map_err(|_| {
|
||||
GatewayWireguardError::internal("no response for query verifier".to_string())
|
||||
@@ -189,31 +290,39 @@ impl PeerManager {
|
||||
GatewayWireguardError::internal(format!(
|
||||
"querying verifier could not be performed: {err:?}"
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let latency = controller_start.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
"wg_peer_controller_channel_latency_seconds",
|
||||
latency,
|
||||
WG_CONTROLLER_LATENCY_BUCKETS
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use super::*;
|
||||
use crate::node::wireguard::PeerRegistrator;
|
||||
use crate::nym_authenticator::config::Authenticator;
|
||||
use defguard_wireguard_rs::net::IpAddrMask;
|
||||
use nym_credential_verification::upgrade_mode::testing::mock_dummy_upgrade_mode_details;
|
||||
use nym_credential_verification::{
|
||||
bandwidth_storage_manager::BandwidthStorageManager, ecash::MockEcashManager,
|
||||
};
|
||||
use nym_credentials_interface::Bandwidth;
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_gateway_storage::traits::{mock::MockGatewayStorage, BandwidthGatewayStorage};
|
||||
use nym_task::ShutdownManager;
|
||||
use nym_test_utils::helpers::{deterministic_rng, DeterministicRng, RngCore};
|
||||
use nym_wireguard::peer_controller::{start_controller, stop_controller};
|
||||
use rand::rngs::OsRng;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::nym_authenticator::{
|
||||
config::Authenticator, mixnet_listener::credential_storage_preparation,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
const CREDENTIAL_BYTES: [u8; 1245] = [
|
||||
0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254,
|
||||
16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139,
|
||||
@@ -279,141 +388,202 @@ mod tests {
|
||||
0, 0, 0, 0, 0, 1,
|
||||
];
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_peer() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let peer_manager = PeerManager::new(wireguard_data);
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let peer = Peer::default();
|
||||
let ecash_manager = MockEcashManager::new(Box::new(storage.clone()));
|
||||
|
||||
assert!(peer_manager.add_peer(peer.clone()).await.is_err());
|
||||
|
||||
let client_id = storage
|
||||
.insert_wireguard_peer(&peer, FromStr::from_str("entry_wireguard").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(peer_manager.add_peer(peer.clone()).await.is_err());
|
||||
|
||||
credential_storage_preparation(Arc::new(ecash_manager), client_id)
|
||||
.await
|
||||
.unwrap();
|
||||
peer_manager.add_peer(peer.clone()).await.unwrap();
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
struct TestSetup {
|
||||
rng: DeterministicRng,
|
||||
_ecash_manager: Arc<MockEcashManager>,
|
||||
storage: Arc<RwLock<MockGatewayStorage>>,
|
||||
peer_registrator: PeerRegistrator,
|
||||
peer_manager: PeerManager,
|
||||
task_manager: ShutdownManager,
|
||||
}
|
||||
|
||||
async fn helper_add_peer(
|
||||
storage: &Arc<RwLock<MockGatewayStorage>>,
|
||||
peer_manager: &mut PeerManager,
|
||||
) -> i64 {
|
||||
let peer = Peer::default();
|
||||
let ecash_manager = MockEcashManager::new(Box::new(storage.clone()));
|
||||
let client_id = storage
|
||||
struct GeneratedPeer {
|
||||
peer: Peer,
|
||||
client_id: i64,
|
||||
}
|
||||
|
||||
impl GeneratedPeer {
|
||||
fn key(&self) -> PeerPublicKey {
|
||||
PeerPublicKey::from_str(self.peer.public_key.to_string().as_str()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSetup {
|
||||
fn new() -> TestSetup {
|
||||
let mut rng = deterministic_rng();
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut rng)),
|
||||
);
|
||||
|
||||
let (upgrade_mode_details, _) = mock_dummy_upgrade_mode_details();
|
||||
let peer_manager = PeerManager::new(wireguard_data);
|
||||
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
|
||||
let ecash_manager = Arc::new(MockEcashManager::new(Box::new(storage.clone())));
|
||||
let peer_registrator = PeerRegistrator::new(
|
||||
ecash_manager.clone(),
|
||||
peer_manager.clone(),
|
||||
upgrade_mode_details,
|
||||
);
|
||||
|
||||
TestSetup {
|
||||
rng,
|
||||
_ecash_manager: ecash_manager,
|
||||
storage,
|
||||
peer_registrator,
|
||||
peer_manager,
|
||||
task_manager,
|
||||
}
|
||||
}
|
||||
|
||||
async fn peer_with_pre_allocated_ip(&mut self) -> Peer {
|
||||
let mut peer = Peer::default();
|
||||
let mut key = [0u8; 32];
|
||||
self.rng.fill_bytes(&mut key);
|
||||
peer.public_key = Key::new(key);
|
||||
|
||||
let allocation = self.peer_manager.preallocate_peer_ip_pair().await.unwrap();
|
||||
peer.allowed_ips = vec![
|
||||
IpAddrMask::new(allocation.ipv4.into(), 32),
|
||||
IpAddrMask::new(allocation.ipv6.into(), 128),
|
||||
];
|
||||
|
||||
peer
|
||||
}
|
||||
|
||||
async fn _add_peer(&self, peer: &Peer) -> i64 {
|
||||
let client_id = self
|
||||
.storage
|
||||
.insert_wireguard_peer(peer, FromStr::from_str("entry_wireguard").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
self.peer_registrator
|
||||
.credential_storage_preparation(client_id)
|
||||
.await
|
||||
.unwrap();
|
||||
self.peer_manager.add_peer(peer.clone()).await.unwrap();
|
||||
client_id
|
||||
}
|
||||
|
||||
async fn add_peer(&mut self) -> GeneratedPeer {
|
||||
let peer = self.peer_with_pre_allocated_ip().await;
|
||||
let client_id = self._add_peer(&peer).await;
|
||||
|
||||
GeneratedPeer { peer, client_id }
|
||||
}
|
||||
|
||||
async fn finish(self) {
|
||||
stop_controller(self.task_manager).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn assign_peer_ip() -> anyhow::Result<()> {
|
||||
let test = TestSetup::new();
|
||||
|
||||
let ip_pair1 = test.peer_manager.preallocate_peer_ip_pair().await?;
|
||||
let ip_pair2 = test.peer_manager.preallocate_peer_ip_pair().await?;
|
||||
assert_ne!(ip_pair1, ip_pair2);
|
||||
|
||||
test.finish().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_peer() {
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.peer_with_pre_allocated_ip().await;
|
||||
|
||||
assert!(test.peer_manager.add_peer(peer.clone()).await.is_err());
|
||||
|
||||
let client_id = test
|
||||
.storage
|
||||
.insert_wireguard_peer(&peer, FromStr::from_str("entry_wireguard").unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
credential_storage_preparation(Arc::new(ecash_manager), client_id)
|
||||
assert!(test.peer_manager.add_peer(peer.clone()).await.is_err());
|
||||
|
||||
test.peer_registrator
|
||||
.credential_storage_preparation(client_id)
|
||||
.await
|
||||
.unwrap();
|
||||
peer_manager.add_peer(peer.clone()).await.unwrap();
|
||||
test.peer_manager.add_peer(peer.clone()).await.unwrap();
|
||||
|
||||
client_id
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_peer() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.add_peer().await;
|
||||
let public_key = peer.key();
|
||||
|
||||
helper_add_peer(&storage, &mut peer_manager).await;
|
||||
peer_manager._remove_peer(public_key).await.unwrap();
|
||||
test.peer_manager.remove_peer(public_key).await.unwrap();
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_peer() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.peer_with_pre_allocated_ip().await;
|
||||
let public_key = PeerPublicKey::from_str(peer.public_key.to_string().as_str()).unwrap();
|
||||
|
||||
assert!(peer_manager.query_peer(public_key).await.unwrap().is_none());
|
||||
assert!(test
|
||||
.peer_manager
|
||||
.query_peer(public_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
helper_add_peer(&storage, &mut peer_manager).await;
|
||||
let peer = peer_manager.query_peer(public_key).await.unwrap().unwrap();
|
||||
assert_eq!(peer.public_key, key);
|
||||
test._add_peer(&peer).await;
|
||||
let peer_query = test
|
||||
.peer_manager
|
||||
.query_peer(public_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(peer.public_key, peer_query.public_key);
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_bandwidth() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.peer_with_pre_allocated_ip().await;
|
||||
let public_key = PeerPublicKey::from_str(peer.public_key.to_string().as_str()).unwrap();
|
||||
|
||||
assert!(peer_manager.query_bandwidth(public_key).await.is_err());
|
||||
assert!(test.peer_manager.query_bandwidth(public_key).await.is_err());
|
||||
|
||||
helper_add_peer(&storage, &mut peer_manager).await;
|
||||
let available_bandwidth = peer_manager.query_bandwidth(public_key).await.unwrap();
|
||||
test._add_peer(&peer).await;
|
||||
let available_bandwidth = test.peer_manager.query_bandwidth(public_key).await.unwrap();
|
||||
assert_eq!(available_bandwidth, 0);
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_client_bandwidth() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.peer_with_pre_allocated_ip().await;
|
||||
let public_key = PeerPublicKey::from_str(peer.public_key.to_string().as_str()).unwrap();
|
||||
|
||||
assert!(peer_manager
|
||||
assert!(test
|
||||
.peer_manager
|
||||
.query_client_bandwidth(public_key)
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
helper_add_peer(&storage, &mut peer_manager).await;
|
||||
let available_bandwidth = peer_manager
|
||||
test._add_peer(&peer).await;
|
||||
let available_bandwidth = test
|
||||
.peer_manager
|
||||
.query_client_bandwidth(public_key)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -421,64 +591,51 @@ mod tests {
|
||||
.await;
|
||||
assert_eq!(available_bandwidth, 0);
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_verifier() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.peer_with_pre_allocated_ip().await;
|
||||
let public_key = PeerPublicKey::from_str(peer.public_key.to_string().as_str()).unwrap();
|
||||
|
||||
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
|
||||
|
||||
assert!(peer_manager
|
||||
assert!(test
|
||||
.peer_manager
|
||||
.query_verifier_by_key(public_key, credential.clone())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
helper_add_peer(&storage, &mut peer_manager).await;
|
||||
peer_manager
|
||||
test._add_peer(&peer).await;
|
||||
test.peer_manager
|
||||
.query_verifier_by_key(public_key, credential)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn increase_decrease_bandwidth() {
|
||||
let (wireguard_data, request_rx) = WireguardGatewayData::new(
|
||||
Authenticator::default().into(),
|
||||
Arc::new(KeyPair::new(&mut OsRng)),
|
||||
);
|
||||
let mut peer_manager = PeerManager::new(wireguard_data);
|
||||
let key = Key::default();
|
||||
let public_key = PeerPublicKey::from_str(&key.to_string()).unwrap();
|
||||
let mut test = TestSetup::new();
|
||||
let peer = test.add_peer().await;
|
||||
let public_key = peer.key();
|
||||
|
||||
let top_up = 42;
|
||||
let consume = 4;
|
||||
let (storage, task_manager) = start_controller(
|
||||
peer_manager.wireguard_gateway_data.peer_tx().clone(),
|
||||
request_rx,
|
||||
);
|
||||
|
||||
let client_id = helper_add_peer(&storage, &mut peer_manager).await;
|
||||
let client_bandwidth = peer_manager
|
||||
.query_client_bandwidth(public_key)
|
||||
let client_bandwidth = test
|
||||
.peer_manager
|
||||
.query_client_bandwidth(peer.key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut bw_manager = BandwidthStorageManager::new(
|
||||
Box::new(storage),
|
||||
Box::new(test.storage.clone()),
|
||||
client_bandwidth.clone(),
|
||||
client_id,
|
||||
peer.client_id,
|
||||
Default::default(),
|
||||
true,
|
||||
);
|
||||
@@ -494,7 +651,7 @@ mod tests {
|
||||
|
||||
assert_eq!(client_bandwidth.available().await, top_up);
|
||||
assert_eq!(
|
||||
peer_manager.query_bandwidth(public_key).await.unwrap(),
|
||||
test.peer_manager.query_bandwidth(public_key).await.unwrap(),
|
||||
top_up
|
||||
);
|
||||
|
||||
@@ -502,10 +659,10 @@ mod tests {
|
||||
let remaining = top_up - consume;
|
||||
assert_eq!(client_bandwidth.available().await, remaining);
|
||||
assert_eq!(
|
||||
peer_manager.query_bandwidth(public_key).await.unwrap(),
|
||||
test.peer_manager.query_bandwidth(public_key).await.unwrap(),
|
||||
remaining
|
||||
);
|
||||
|
||||
stop_controller(task_manager).await;
|
||||
test.finish().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
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_credential_verification::upgrade_mode::testing::mock_dummy_upgrade_mode_details;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_gateway::GatewayError;
|
||||
@@ -18,7 +15,7 @@ mod tests {
|
||||
LpDebug, LpHandlerState, LpLocalPeer, MixForwardingReceiver, PeerControlRequest,
|
||||
WireguardGatewayData, mix_forwarding_channels,
|
||||
};
|
||||
use nym_gateway::node::wireguard::PeerManager;
|
||||
use nym_gateway::node::wireguard::{PeerManager, PeerRegistrator};
|
||||
use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig};
|
||||
use nym_registration_client::{LpClientError, LpRegistrationClient};
|
||||
use nym_test_utils::helpers::{CryptoRng, RngCore, u64_seeded_rng};
|
||||
@@ -166,10 +163,9 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn allocate_ip_pair(&mut self) -> IpPair {
|
||||
fn pre_allocate_ip_pair(&mut self) -> IpPair {
|
||||
self.ip_pool
|
||||
.allocate()
|
||||
.await
|
||||
.pre_allocate()
|
||||
.expect("unexpected ip allocation failure!")
|
||||
}
|
||||
|
||||
@@ -181,17 +177,6 @@ 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);
|
||||
|
||||
@@ -217,31 +202,24 @@ 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;
|
||||
let (upgrade_mode_details, _) = mock_dummy_upgrade_mode_details();
|
||||
|
||||
// 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 ecash_verifier = Arc::new(ecash_verifier);
|
||||
|
||||
let peer_registrator = PeerRegistrator::new(
|
||||
ecash_verifier.clone(),
|
||||
PeerManager::new(wireguard_data),
|
||||
upgrade_mode_details,
|
||||
);
|
||||
|
||||
let lp_state = LpHandlerState {
|
||||
// use mock instance of ecash verifier
|
||||
ecash_verifier: Arc::new(ecash_verifier),
|
||||
ecash_verifier,
|
||||
|
||||
// use in-memory database (no need for persistency)
|
||||
storage,
|
||||
@@ -253,10 +231,6 @@ mod tests {
|
||||
// no clients at the beginning
|
||||
active_clients_store: ActiveClientsStore::new(),
|
||||
|
||||
// handles required for wg registration
|
||||
upgrade_mode: upgrade_mode_details,
|
||||
peer_manager,
|
||||
|
||||
// use default lp config (with enabled flag)
|
||||
lp_config,
|
||||
|
||||
@@ -269,9 +243,10 @@ mod tests {
|
||||
// we start with empty state
|
||||
session_states: Arc::new(Default::default()),
|
||||
|
||||
// sensible default value for tests
|
||||
registrations_in_progress: Default::default(),
|
||||
forward_semaphore,
|
||||
|
||||
// handles for dealing with new peers
|
||||
peer_registrator: Some(peer_registrator),
|
||||
};
|
||||
|
||||
Ok(Gateway {
|
||||
@@ -444,7 +419,7 @@ mod tests {
|
||||
|
||||
// 3. register all needed responses for the dvpn registration that will reach the peer controller
|
||||
// 1) peer registration - ip pair allocation
|
||||
let ip_pair = entry.allocate_ip_pair().await;
|
||||
let ip_pair = entry.pre_allocate_ip_pair();
|
||||
let reg_res = Ok::<_, nym_wireguard::Error>(ip_pair);
|
||||
|
||||
entry
|
||||
@@ -617,7 +592,7 @@ mod tests {
|
||||
|
||||
// 4. register all needed responses for the dvpn registration that will reach the peer controller
|
||||
// 1) peer registration - ip pair allocation
|
||||
let entry_ip_pair = entry.allocate_ip_pair().await;
|
||||
let entry_ip_pair = entry.pre_allocate_ip_pair();
|
||||
let reg_res = Ok::<_, nym_wireguard::Error>(entry_ip_pair);
|
||||
|
||||
entry
|
||||
@@ -668,7 +643,7 @@ mod tests {
|
||||
|
||||
// 10. register all needed responses for the dvpn registration that will reach the peer controller
|
||||
// 1) peer registration - ip pair allocation
|
||||
let exit_ip_pair = exit.allocate_ip_pair().await;
|
||||
let exit_ip_pair = exit.pre_allocate_ip_pair();
|
||||
let reg_res = Ok::<_, nym_wireguard::Error>(exit_ip_pair);
|
||||
|
||||
exit.register_peer_controller_response(
|
||||
@@ -705,7 +680,7 @@ mod tests {
|
||||
// technically we should use different ephemeral keys than we had for the entry
|
||||
// but crypto is going to work the same
|
||||
let mut nested_session = NestedLpSession::new(
|
||||
exit.base.socket_addr.to_string(),
|
||||
exit.base.socket_addr,
|
||||
client_data.base.peer.ed25519().clone(),
|
||||
exit.base.peer.as_remote(),
|
||||
exit.base.lp_version,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.72"
|
||||
version = "1.1.73"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
@@ -27,7 +27,6 @@ moka = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -124,6 +123,7 @@ sqlx = { workspace = true, features = [
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json", "query"] }
|
||||
tempfile = { workspace = true }
|
||||
cw3 = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
@@ -210,6 +210,12 @@ pub struct LewesProtocolDetailsV1 {
|
||||
/// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP
|
||||
pub data_port: u16,
|
||||
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
/// LP public key
|
||||
pub x25519: x25519::PublicKey,
|
||||
|
||||
/// Digests of the KEM keys available to this node alongside hashing algorithms used
|
||||
/// for their computation.
|
||||
/// note: digests are hex encoded
|
||||
@@ -457,6 +463,7 @@ impl From<nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol>
|
||||
enabled: value.enabled,
|
||||
control_port: value.control_port,
|
||||
data_port: value.data_port,
|
||||
x25519: value.x25519,
|
||||
kem_keys: value
|
||||
.kem_keys
|
||||
.into_iter()
|
||||
|
||||
@@ -343,6 +343,7 @@ pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 {
|
||||
enabled: true,
|
||||
control_port: 1234,
|
||||
data_port: 2345,
|
||||
x25519: *x25519.public_key(),
|
||||
kem_keys: kem_hashes_wrapper,
|
||||
signing_keys: signing_keys_hashes_wrapper,
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ecash::error::{EcashError, RedemptionError};
|
||||
use crate::node_status_api::utils::NodeUptimes;
|
||||
use crate::storage::models::NodeStatus;
|
||||
use crate::support::caching::cache::UninitialisedCache;
|
||||
use axum::http::StatusCode;
|
||||
use nym_api_requests::ecash::models::DepositId;
|
||||
use nym_api_requests::models::{
|
||||
HistoricalPerformanceResponse, HistoricalUptimeResponse, NodePerformance,
|
||||
@@ -15,7 +16,6 @@ use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{IdentityKey, NodeId};
|
||||
use nym_serde_helpers::date::DATE_FORMAT;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use reqwest::StatusCode;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Error;
|
||||
|
||||
@@ -350,6 +350,7 @@ impl AuthenticatorClient {
|
||||
|
||||
let gateway_data = WireguardConfiguration {
|
||||
public_key: registered_data.pub_key().inner().into(),
|
||||
psk: None, // Mixnet-based regsitration does not have psk
|
||||
endpoint: SocketAddr::new(self.ip_addr, registered_data.wg_port()),
|
||||
private_ipv4: registered_data.private_ips().ipv4,
|
||||
private_ipv6: registered_data.private_ips().ipv6,
|
||||
|
||||
@@ -20,7 +20,7 @@ serde_json.workspace = true
|
||||
serde_with = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||
tsify = { workspace = true, optional = true }
|
||||
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["json", "rustls"] }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
|
||||
## openapi:
|
||||
|
||||
@@ -22,7 +22,7 @@ clap = { workspace = true, features = ["derive", "env"] }
|
||||
futures.workspace = true
|
||||
humantime.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
sqlx = { workspace = true, features = [
|
||||
|
||||
@@ -27,7 +27,7 @@ nym-task = { workspace = true }
|
||||
nym-validator-client = { workspace = true }
|
||||
nyxd-scraper-psql = { path = "../common/nyxd-scraper-psql" }
|
||||
nyxd-scraper-shared = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
reqwest = { workspace = true, features = ["rustls"] }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -15,8 +15,6 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
bs58.workspace = true
|
||||
bincode.workspace = true
|
||||
bytes.workspace = true
|
||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
futures.workspace = true
|
||||
@@ -27,7 +25,7 @@ rand.workspace = true
|
||||
reqwest = { workspace = true, features = ["socks"] }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
@@ -39,43 +37,34 @@ tokio-util.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
url = { workspace = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = [
|
||||
"reusable_secrets",
|
||||
"static_secrets",
|
||||
] }
|
||||
|
||||
|
||||
nym-api-requests = { path = "../nym-api/nym-api-requests" }
|
||||
nym-authenticator-client = { workspace = true }
|
||||
nym-authenticator-requests = { workspace = true }
|
||||
nym-bandwidth-controller = { workspace = true }
|
||||
nym-bin-common = { workspace = true }
|
||||
nym-client-core = { workspace = true }
|
||||
nym-crypto = { workspace = true }
|
||||
nym-config = { workspace = true }
|
||||
nym-connection-monitor = { path = "../common/nym-connection-monitor" }
|
||||
nym-credentials = { workspace = true }
|
||||
nym-credentials-interface = { workspace = true }
|
||||
nym-credential-utils = { workspace = true }
|
||||
nym-ip-packet-client = { workspace = true }
|
||||
nym-authenticator-client = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-sdk = { workspace = true }
|
||||
nym-validator-client = { workspace = true }
|
||||
nym-credentials = { workspace = true }
|
||||
nym-http-api-client-macro = { path = "../common/http-api-client-macro" }
|
||||
nym-crypto = { workspace = true }
|
||||
nym-http-api-client = { path = "../common/http-api-client" }
|
||||
nym-node-status-client = { path = "../nym-node-status-api/nym-node-status-client" }
|
||||
nym-node-requests = { path = "../nym-node/nym-node-requests" }
|
||||
nym-registration-client = { path = "../nym-registration-client" }
|
||||
nym-lp = { path = "../common/nym-lp" }
|
||||
nym-http-api-client-macro = { path = "../common/http-api-client-macro" }
|
||||
nym-ip-packet-client = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-lp = { path = "../common/nym-lp" }
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-node-requests = { path = "../nym-node/nym-node-requests" }
|
||||
nym-node-status-client = { path = "../nym-node-status-api/nym-node-status-client" }
|
||||
nym-registration-client = { path = "../nym-registration-client" }
|
||||
nym-registration-common = { path = "../common/registration" }
|
||||
time = { workspace = true }
|
||||
|
||||
# TEMP: REMOVE BEFORE PR
|
||||
nym-sdk = { workspace = true }
|
||||
nym-topology = { workspace = true }
|
||||
nym-validator-client = { workspace = true }
|
||||
|
||||
[features]
|
||||
utoipa = ["dep:utoipa"]
|
||||
|
||||
@@ -8,29 +8,38 @@ use nym_bandwidth_controller::mock::MockBandwidthController;
|
||||
use nym_client_core::client::base_client::storage::OnDiskPersistent;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_node_status_client::models::AttachedTicketMaterials;
|
||||
use nym_sdk::NymNetworkDetails;
|
||||
use nym_sdk::bandwidth::BandwidthImporter;
|
||||
use nym_sdk::mixnet::{CredentialStorage, DisconnectedMixnetClient, EphemeralCredentialStorage};
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub(crate) fn build_bandwidth_controller<S>(
|
||||
rpc_client: QueryHttpRpcNyxdClient,
|
||||
on_disk_storage: S,
|
||||
network: &NymNetworkDetails,
|
||||
storage: S,
|
||||
use_mock_ecash: bool,
|
||||
) -> Box<dyn BandwidthTicketProvider>
|
||||
) -> anyhow::Result<Box<dyn BandwidthTicketProvider>>
|
||||
where
|
||||
S: CredentialStorage + 'static,
|
||||
S::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
if !use_mock_ecash {
|
||||
Box::new(nym_bandwidth_controller::BandwidthController::new(
|
||||
on_disk_storage,
|
||||
rpc_client,
|
||||
let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network)?;
|
||||
|
||||
let nyxd_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.map(|ep| ep.nyxd_url())
|
||||
.ok_or(anyhow::anyhow!("missing nyxd url"))?;
|
||||
let rpc_client =
|
||||
nym_validator_client::nyxd::NyxdClient::connect(config, nyxd_url.as_str())?;
|
||||
|
||||
Ok(Box::new(
|
||||
nym_bandwidth_controller::BandwidthController::new(storage, rpc_client),
|
||||
))
|
||||
} else {
|
||||
Box::new(MockBandwidthController::default())
|
||||
Ok(Box::new(MockBandwidthController::default()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ use nym_ip_packet_requests::v8::response::{
|
||||
ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
|
||||
};
|
||||
use nym_lp::peer::LpRemotePeer;
|
||||
use nym_sdk::mixnet::ReconstructedMessage;
|
||||
use nym_sdk::{
|
||||
DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails,
|
||||
TopologyProvider, mixnet::ReconstructedMessage,
|
||||
};
|
||||
use nym_topology::NymTopology;
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
pub fn to_lp_remote_peer(identity: ed25519::PublicKey, data: TestedNodeLpDetails) -> LpRemotePeer {
|
||||
LpRemotePeer::new(identity, data.x25519).with_key_digests(
|
||||
@@ -62,3 +67,45 @@ pub fn unpack_data_response(reconstructed_message: &ReconstructedMessage) -> Opt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_topology(
|
||||
network_details: &NymNetworkDetails,
|
||||
debug_config: &DebugConfig,
|
||||
) -> Result<NymTopology, String> {
|
||||
// get Nym API URLs from network_details
|
||||
let nym_api_urls: Vec<Url> = network_details
|
||||
.nym_api_urls
|
||||
.as_ref()
|
||||
.map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect())
|
||||
.or_else(|| {
|
||||
network_details
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|e| e.api_url())
|
||||
.map(|url| vec![url])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if nym_api_urls.is_empty() {
|
||||
return Err(String::from("No nym-api URLs available to fetch topology"));
|
||||
}
|
||||
|
||||
let topology_config = NymApiTopologyProviderConfig {
|
||||
min_mixnode_performance: debug_config.topology.minimum_mixnode_performance,
|
||||
min_gateway_performance: debug_config.topology.minimum_gateway_performance,
|
||||
use_extended_topology: debug_config.topology.use_extended_topology,
|
||||
ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role,
|
||||
};
|
||||
|
||||
let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client);
|
||||
|
||||
match provider.get_new_topology().await {
|
||||
Some(topology) => {
|
||||
info!("Fetched network topology");
|
||||
Ok(topology)
|
||||
}
|
||||
None => Err(String::from("Failed to fetch network topology")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::NetstackArgs;
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
mod sys {
|
||||
use std::ffi::{c_char, c_void};
|
||||
@@ -15,8 +17,6 @@ mod sys {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::config::NetstackArgs;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct NetstackRequest {
|
||||
private_key: String,
|
||||
@@ -224,14 +224,14 @@ pub struct TwoHopNetstackRequestGo {
|
||||
pub entry_wg_ip: String,
|
||||
pub entry_private_key: String,
|
||||
pub entry_public_key: String,
|
||||
pub entry_endpoint: String,
|
||||
pub entry_endpoint: SocketAddr,
|
||||
pub entry_awg_args: String,
|
||||
|
||||
// Exit tunnel configuration (connects via forwarder through entry)
|
||||
pub exit_wg_ip: String,
|
||||
pub exit_private_key: String,
|
||||
pub exit_public_key: String,
|
||||
pub exit_endpoint: String,
|
||||
pub exit_endpoint: SocketAddr,
|
||||
pub exit_awg_args: String,
|
||||
|
||||
// Test parameters
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
use anyhow::{Context, anyhow, bail};
|
||||
use nym_api_requests::models::{
|
||||
AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2,
|
||||
IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV1,
|
||||
NetworkRequesterDetailsV2, NymNodeDataV2, OffsetDateTimeJsonSchemaWrapper, WebSocketsV2,
|
||||
WireguardDetailsV2,
|
||||
IpPacketRouterDetailsV2, NetworkRequesterDetailsV2, NymNodeDataV2,
|
||||
OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2,
|
||||
};
|
||||
use nym_authenticator_requests::AuthenticatorVersion;
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
@@ -27,7 +26,7 @@ use std::collections::HashMap;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use url::Url;
|
||||
// in the old behaviour we were getting all skimmed nodes to retrieve performance
|
||||
// that was ultimately unused
|
||||
@@ -108,6 +107,11 @@ impl DirectoryNode {
|
||||
.as_ref()
|
||||
.map(|ipr| ipr.address.parse().context("malformed ipr address"))
|
||||
.transpose()?;
|
||||
let network_requester_address = description
|
||||
.network_requester
|
||||
.as_ref()
|
||||
.map(|nr| nr.address.parse().context("malformed nr address"))
|
||||
.transpose()?;
|
||||
let authenticator_address = description
|
||||
.authenticator
|
||||
.as_ref()
|
||||
@@ -126,26 +130,21 @@ impl DirectoryNode {
|
||||
.copied()
|
||||
.ok_or_else(|| anyhow!("no ip address known"))?;
|
||||
|
||||
let lp_data = match (
|
||||
description.lewes_protocol.clone(),
|
||||
description.host_information.keys.x25519_versioned_noise,
|
||||
) {
|
||||
(Some(lp_data), Some(noise_key)) => Some(TestedNodeLpDetails {
|
||||
let lp_data = description.lewes_protocol.as_ref().and_then(|lp_data| {
|
||||
Some(TestedNodeLpDetails {
|
||||
address: SocketAddr::new(ip_address, lp_data.control_port),
|
||||
expected_kem_key_hashes: lp_data.kem_keys()?,
|
||||
expected_signing_key_hashes: lp_data.signing_keys()?,
|
||||
x25519: noise_key.x25519_pubkey,
|
||||
expected_kem_key_hashes: lp_data.kem_keys().ok()?,
|
||||
expected_signing_key_hashes: lp_data.signing_keys().ok()?,
|
||||
x25519: lp_data.x25519,
|
||||
// \/ TODO: proper derivation from build version
|
||||
lp_version: version::CURRENT,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
let network_requester_details = self.described.description.network_requester.clone();
|
||||
})
|
||||
});
|
||||
|
||||
Ok(TestedNodeDetails {
|
||||
identity: self.identity(),
|
||||
exit_router_address,
|
||||
network_requester_details,
|
||||
network_requester_address,
|
||||
authenticator_address,
|
||||
authenticator_version,
|
||||
ip_address: Some(ip_address),
|
||||
@@ -217,12 +216,33 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
let build_info_result = client.get_build_information().await;
|
||||
let aux_details_result = client.get_auxiliary_details().await;
|
||||
let websockets_result = client.get_mixnet_websockets().await;
|
||||
let lp_result = client.get_lewes_protocol().await;
|
||||
|
||||
// These are optional, so we use ok() to ignore errors
|
||||
let ipr_result = client.get_ip_packet_router().await.ok();
|
||||
let authenticator_result = client.get_authenticator().await.ok();
|
||||
let wireguard_result = client.get_wireguard().await.ok();
|
||||
let ipr_result = client
|
||||
.get_ip_packet_router()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get ipr information : {e}"))
|
||||
.ok();
|
||||
let nr_result = client
|
||||
.get_network_requester()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get nr information : {e}"))
|
||||
.ok();
|
||||
let authenticator_result = client
|
||||
.get_authenticator()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get authenticator information : {e}"))
|
||||
.ok();
|
||||
let wireguard_result = client
|
||||
.get_wireguard()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get wireguard information : {e}"))
|
||||
.ok();
|
||||
let lp_result = client
|
||||
.get_lewes_protocol()
|
||||
.await
|
||||
.inspect_err(|e| error!("Failed to get LP information : {e}"))
|
||||
.ok();
|
||||
|
||||
// Check required fields
|
||||
let host_info = host_info_result.context("Failed to get host information")?;
|
||||
@@ -242,7 +262,11 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
}
|
||||
|
||||
// Convert to our internal types
|
||||
let network_requester: Option<NetworkRequesterDetailsV2> = None; // Not needed for LP testing
|
||||
let network_requester: Option<NetworkRequesterDetailsV2> =
|
||||
nr_result.map(|nr| NetworkRequesterDetailsV2 {
|
||||
address: nr.address,
|
||||
uses_exit_policy: false, // Field not availabe, to change if it becomes useful here
|
||||
});
|
||||
let ip_packet_router: Option<IpPacketRouterDetailsV2> =
|
||||
ipr_result.map(|ipr| IpPacketRouterDetailsV2 {
|
||||
address: ipr.address,
|
||||
@@ -260,8 +284,6 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
public_key: wg.public_key,
|
||||
});
|
||||
|
||||
let lp: Option<LewesProtocolDetailsV1> = lp_result.ok().map(Into::into);
|
||||
|
||||
// Construct NymNodeData
|
||||
let node_data = NymNodeDataV2 {
|
||||
last_polled: OffsetDateTimeJsonSchemaWrapper(OffsetDateTime::now_utc()),
|
||||
@@ -293,7 +315,7 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
|
||||
ip_packet_router,
|
||||
authenticator,
|
||||
wireguard,
|
||||
lewes_protocol: lp,
|
||||
lewes_protocol: lp_result.map(Into::into),
|
||||
mixnet_websockets: WebSocketsV2 {
|
||||
ws_port: websockets.ws_port,
|
||||
wss_port: websockets.wss_port,
|
||||
@@ -431,45 +453,27 @@ impl NymApiDirectory {
|
||||
Ok(maybe_entry)
|
||||
}
|
||||
|
||||
pub fn exit_gateway_nr(&self, identity: &NodeIdentity) -> anyhow::Result<DirectoryNode> {
|
||||
pub fn exit_gateway(&self, identity: &NodeIdentity) -> anyhow::Result<DirectoryNode> {
|
||||
let Some(maybe_entry) = self.nodes.get(identity).cloned() else {
|
||||
bail!("{identity} not found in directory")
|
||||
bail!("{identity} does not exist")
|
||||
};
|
||||
if !maybe_entry.described.description.declared_role.exit_nr {
|
||||
bail!("{identity} doesn't support exit NR mode")
|
||||
if !maybe_entry
|
||||
.described
|
||||
.description
|
||||
.declared_role
|
||||
.can_operate_exit_gateway()
|
||||
{
|
||||
bail!("{identity} is not an exit node")
|
||||
};
|
||||
Ok(maybe_entry)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub enum TestedNode {
|
||||
#[default]
|
||||
SameAsEntry,
|
||||
Custom {
|
||||
identity: NodeIdentity,
|
||||
shares_entry: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl TestedNode {
|
||||
pub fn is_same_as_entry(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestedNode::SameAsEntry
|
||||
| TestedNode::Custom {
|
||||
shares_entry: true,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestedNodeDetails {
|
||||
pub identity: NodeIdentity,
|
||||
pub exit_router_address: Option<Recipient>,
|
||||
pub network_requester_details: Option<NetworkRequesterDetailsV1>,
|
||||
pub network_requester_address: Option<Recipient>,
|
||||
pub authenticator_address: Option<Recipient>,
|
||||
pub authenticator_version: AuthenticatorVersion,
|
||||
pub ip_address: Option<IpAddr>,
|
||||
@@ -484,29 +488,3 @@ pub struct TestedNodeLpDetails {
|
||||
pub x25519: x25519::PublicKey,
|
||||
pub lp_version: u8,
|
||||
}
|
||||
|
||||
impl TestedNodeDetails {
|
||||
/// Create from CLI args (localnet mode - no HTTP query needed)
|
||||
pub fn from_cli(identity: NodeIdentity, lp_data: TestedNodeLpDetails) -> Self {
|
||||
Self {
|
||||
identity,
|
||||
ip_address: Some(lp_data.address.ip()),
|
||||
lp_data: Some(lp_data),
|
||||
network_requester_details: None,
|
||||
// These are None in localnet mode - only needed for mixnet/authenticator
|
||||
exit_router_address: None,
|
||||
authenticator_address: None,
|
||||
authenticator_version: AuthenticatorVersion::UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this node has sufficient info for LP testing
|
||||
pub fn can_test_lp(&self) -> bool {
|
||||
self.lp_data.is_some()
|
||||
}
|
||||
|
||||
/// Check if this node has sufficient info for mixnet testing
|
||||
pub fn can_test_mixnet(&self) -> bool {
|
||||
self.exit_router_address.is_some() || self.authenticator_address.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymApiDirectory;
|
||||
use crate::common::helpers::mixnet_debug_config;
|
||||
use crate::common::nodes::{TestedNodeDetails, TestedNodeLpDetails};
|
||||
use crate::common::socks5_test::HttpsConnectivityTest;
|
||||
@@ -12,7 +11,7 @@ use crate::common::wireguard::{
|
||||
TwoHopWgTunnelConfig, WgTunnelConfig, run_tunnel_tests, run_two_hop_tunnel_tests,
|
||||
};
|
||||
use crate::common::{helpers, icmp};
|
||||
use crate::config::NetstackArgs;
|
||||
use crate::config::{NetstackArgs, Socks5Args};
|
||||
use anyhow::bail;
|
||||
use base64::{Engine, engine::general_purpose};
|
||||
use bytes::BytesMut;
|
||||
@@ -30,11 +29,8 @@ use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_ip_packet_requests::{IpPair, codec::MultiIpPacketCodec};
|
||||
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
|
||||
use nym_sdk::NymNetworkDetails;
|
||||
use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5};
|
||||
use nym_sdk::{
|
||||
DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails,
|
||||
TopologyProvider,
|
||||
};
|
||||
use nym_topology::{HardcodedTopologyProvider, NymTopology};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
@@ -44,13 +40,12 @@ use std::{
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::{codec::Decoder, sync::CancellationToken};
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
pub async fn wg_probe(
|
||||
mut auth_client: AuthenticatorClient,
|
||||
gateway_ip: IpAddr,
|
||||
auth_version: AuthenticatorVersion,
|
||||
awg_args: String,
|
||||
awg_args: Option<String>,
|
||||
netstack_args: NetstackArgs,
|
||||
// TODO: update type
|
||||
credential: CredentialSpendingData,
|
||||
@@ -123,9 +118,8 @@ pub async fn wg_probe(
|
||||
};
|
||||
|
||||
let peer_public = registered_data.pub_key().inner();
|
||||
let static_private = x25519_dalek::StaticSecret::from(private_key.to_bytes());
|
||||
let public_key_bs64 = general_purpose::STANDARD.encode(peer_public.as_bytes());
|
||||
let private_key_hex = hex::encode(static_private.to_bytes());
|
||||
let private_key_hex = hex::encode(private_key.to_bytes());
|
||||
let public_key_hex = hex::encode(peer_public.as_bytes());
|
||||
|
||||
info!("WG connection details");
|
||||
@@ -152,7 +146,12 @@ pub async fn wg_probe(
|
||||
wg_endpoint,
|
||||
);
|
||||
|
||||
run_tunnel_tests(&tunnel_config, &netstack_args, &awg_args, &mut wg_outcome);
|
||||
run_tunnel_tests(
|
||||
&tunnel_config,
|
||||
&netstack_args,
|
||||
&awg_args.unwrap_or_default(),
|
||||
&mut wg_outcome,
|
||||
);
|
||||
|
||||
Ok(wg_outcome)
|
||||
}
|
||||
@@ -166,7 +165,7 @@ pub async fn lp_registration_probe(
|
||||
let lp_version = gateway_lp_data.lp_version;
|
||||
let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data);
|
||||
|
||||
info!("Starting LP registration probe for gateway at {lp_address}",);
|
||||
info!("Starting LP registration probe for gateway at {lp_address}");
|
||||
|
||||
let mut lp_outcome = LpProbeResults::default();
|
||||
|
||||
@@ -245,6 +244,12 @@ pub async fn lp_registration_probe(
|
||||
|
||||
info!("LP registration successful! Received gateway data:");
|
||||
info!(" - Gateway public key: {:?}", gateway_data.public_key);
|
||||
info!(
|
||||
" - PSK: {:?}",
|
||||
gateway_data
|
||||
.psk
|
||||
.map(|k| general_purpose::STANDARD.encode(k))
|
||||
);
|
||||
info!(" - Private IPv4: {}", gateway_data.private_ipv4);
|
||||
info!(" - Private IPv6: {}", gateway_data.private_ipv6);
|
||||
info!(" - Endpoint: {}", gateway_data.endpoint);
|
||||
@@ -274,7 +279,7 @@ pub async fn wg_probe_lp(
|
||||
entry_gateway: &TestedNodeDetails,
|
||||
exit_gateway: &TestedNodeDetails,
|
||||
bandwidth_controller: &dyn BandwidthTicketProvider,
|
||||
awg_args: String,
|
||||
awg_args: Option<String>,
|
||||
netstack_args: NetstackArgs,
|
||||
) -> anyhow::Result<WgProbeResults> {
|
||||
// Validate that both gateways have required information
|
||||
@@ -294,9 +299,6 @@ pub async fn wg_probe_lp(
|
||||
let entry_lp_version = entry_lp_data.lp_version;
|
||||
let exit_lp_version = exit_lp_data.lp_version;
|
||||
|
||||
let entry_ip = entry_address.ip();
|
||||
let exit_ip = exit_address.ip();
|
||||
|
||||
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
|
||||
|
||||
let mut wg_outcome = WgProbeResults::default();
|
||||
@@ -333,16 +335,10 @@ pub async fn wg_probe_lp(
|
||||
|
||||
// STEP 2: Use nested session to register with exit gateway via forwarding
|
||||
info!("Registering with exit gateway via entry forwarding...");
|
||||
let mut nested_session = NestedLpSession::new(
|
||||
exit_address.to_string(),
|
||||
exit_lp_keypair,
|
||||
exit_peer,
|
||||
exit_lp_version,
|
||||
);
|
||||
let mut nested_session =
|
||||
NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_version);
|
||||
|
||||
// Convert exit gateway identity to ed25519 public key for registration
|
||||
let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?;
|
||||
let exit_gateway_pubkey = exit_gateway.identity;
|
||||
|
||||
// Perform handshake and registration with exit gateway via forwarding
|
||||
let exit_gateway_data = match nested_session
|
||||
@@ -406,9 +402,9 @@ pub async fn wg_probe_lp(
|
||||
|
||||
// Build WireGuard endpoint addresses
|
||||
// Entry endpoint uses entry_ip (host-reachable) + port from registration
|
||||
let entry_wg_endpoint = format!("{}:{}", entry_ip, entry_gateway_data.endpoint.port());
|
||||
let entry_wg_endpoint = entry_gateway_data.endpoint;
|
||||
// Exit endpoint uses exit_ip + port from registration (forwarded via entry)
|
||||
let exit_wg_endpoint = format!("{}:{}", exit_ip, exit_gateway_data.endpoint.port());
|
||||
let exit_wg_endpoint = exit_gateway_data.endpoint;
|
||||
|
||||
info!("Two-hop WireGuard configuration:");
|
||||
info!(" Entry gateway:");
|
||||
@@ -424,12 +420,12 @@ pub async fn wg_probe_lp(
|
||||
entry_private_key_hex,
|
||||
entry_public_key_hex,
|
||||
entry_wg_endpoint,
|
||||
awg_args.clone(), // Entry AWG args
|
||||
awg_args.clone().unwrap_or_default(), // Entry AWG args
|
||||
exit_gateway_data.private_ipv4.to_string(),
|
||||
exit_private_key_hex,
|
||||
exit_public_key_hex,
|
||||
exit_wg_endpoint,
|
||||
awg_args, // Exit AWG args
|
||||
awg_args.unwrap_or_default(), // Exit AWG args
|
||||
);
|
||||
|
||||
// Run two-hop tunnel connectivity tests
|
||||
@@ -632,39 +628,23 @@ pub async fn listen_for_icmp_ping_replies(
|
||||
|
||||
/// Creates a SOCKS5 proxy connection through the mixnet to the exit GW
|
||||
/// and performs necessary tests.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(level = "info", name = "socks5_test", skip_all)]
|
||||
pub(crate) async fn do_socks5_connectivity_test(
|
||||
network_requester_address: &str,
|
||||
nr_recipient: &Recipient,
|
||||
entry_gateway_id: NodeIdentity,
|
||||
network_details: NymNetworkDetails,
|
||||
directory: &NymApiDirectory,
|
||||
json_rpc_endpoints: Vec<String>,
|
||||
mixnet_client_timeout: u64,
|
||||
test_run_count: u64,
|
||||
failure_count_cutoff: usize,
|
||||
topology: Option<NymTopology>,
|
||||
min_gw_performance: Option<u8>,
|
||||
socks5_args: Socks5Args,
|
||||
maybe_topology: Option<NymTopology>,
|
||||
) -> anyhow::Result<Socks5ProbeResults> {
|
||||
info!(
|
||||
"Starting SOCKS5 test through Network Requester: {}",
|
||||
network_requester_address
|
||||
nr_recipient
|
||||
);
|
||||
if json_rpc_endpoints.is_empty() {
|
||||
if socks5_args.socks5_json_rpc_url_list.is_empty() {
|
||||
bail!("You need to define JSON RPC URLs in order to test SOCKS5")
|
||||
}
|
||||
|
||||
// parse the network requester address
|
||||
let nr_recipient = match network_requester_address.parse::<Recipient>() {
|
||||
Ok(addr) => addr,
|
||||
Err(e) => {
|
||||
error!("Invalid Network Requester address: {}", e);
|
||||
|
||||
return Ok(Socks5ProbeResults::error_before_connecting(format!(
|
||||
"Invalid NR address: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Network Requester gateway: {}",
|
||||
nr_recipient.gateway().to_base58_string()
|
||||
@@ -675,53 +655,30 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
);
|
||||
|
||||
// create ephemeral SOCKS5 client
|
||||
let socks5_config = Socks5::new(network_requester_address.to_string());
|
||||
|
||||
// since we define both entry & exit gateways to be the same tested GW,
|
||||
// this shouldn't negatively affect mixnet layers but it will force route
|
||||
// construction in case GW would get filtered out of topology
|
||||
let min_gw_performance = Some(0);
|
||||
let socks5_config = Socks5::new(nr_recipient.to_string());
|
||||
|
||||
// debug config similar to main probe
|
||||
let debug_config = mixnet_debug_config(min_gw_performance, true);
|
||||
|
||||
// Verify the NR gateway exists in the directory with exit_nr role
|
||||
let nr_gateway_id = nr_recipient.gateway();
|
||||
if let Err(e) = directory.exit_gateway_nr(&nr_gateway_id) {
|
||||
return Ok(Socks5ProbeResults::error_before_connecting(e.to_string()));
|
||||
} else {
|
||||
info!("✔️ Network Requester gateway found in directory with exit_nr role");
|
||||
}
|
||||
|
||||
// use intended exit as entry as well
|
||||
let entry_gateway = nr_gateway_id;
|
||||
|
||||
// use existing topology if available, otherwise fetch it
|
||||
let topology_provider: Box<HardcodedTopologyProvider> = match topology {
|
||||
Some(t) => {
|
||||
info!("✔️ Reusing topology from main mixnet client");
|
||||
Box::new(HardcodedTopologyProvider::new(t))
|
||||
}
|
||||
None => {
|
||||
info!("Fetching topology for SOCKS5 client...");
|
||||
match hardcoded_topology(&network_details, &debug_config).await {
|
||||
Ok(provider) => provider,
|
||||
Err(e) => return Ok(Socks5ProbeResults::error_before_connecting(e)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let socks5_client_builder = MixnetClientBuilder::new_ephemeral()
|
||||
let mut socks5_client_builder = MixnetClientBuilder::new_ephemeral()
|
||||
// Specify entry gateway explicitly
|
||||
.request_gateway(entry_gateway.to_base58_string())
|
||||
.request_gateway(entry_gateway_id.to_base58_string())
|
||||
.socks5_config(socks5_config)
|
||||
.network_details(network_details)
|
||||
.debug_config(debug_config)
|
||||
.custom_topology_provider(topology_provider)
|
||||
.build()?;
|
||||
.debug_config(debug_config);
|
||||
|
||||
if let Some(topology) = maybe_topology {
|
||||
socks5_client_builder = socks5_client_builder
|
||||
.custom_topology_provider(Box::new(HardcodedTopologyProvider::new(topology)));
|
||||
}
|
||||
|
||||
let disconnected_socks5_client = socks5_client_builder.build()?;
|
||||
|
||||
// connect to mixnet via SOCKS5
|
||||
let socks5_client = match socks5_client_builder.connect_to_mixnet_via_socks5().await {
|
||||
let socks5_client = match disconnected_socks5_client
|
||||
.connect_to_mixnet_via_socks5()
|
||||
.await
|
||||
{
|
||||
Ok(client) => {
|
||||
info!("🌐 Successfully connected to mixnet via SOCKS5 proxy");
|
||||
info!(
|
||||
@@ -740,10 +697,10 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
};
|
||||
|
||||
let test = match HttpsConnectivityTest::new(
|
||||
test_run_count,
|
||||
mixnet_client_timeout,
|
||||
failure_count_cutoff,
|
||||
json_rpc_endpoints,
|
||||
socks5_args.test_count,
|
||||
socks5_args.mixnet_client_timeout_sec,
|
||||
socks5_args.failure_count_cutoff,
|
||||
socks5_args.socks5_json_rpc_url_list,
|
||||
socks5_client.socks5_url(),
|
||||
) {
|
||||
Ok(test) => test,
|
||||
@@ -762,45 +719,3 @@ pub(crate) async fn do_socks5_connectivity_test(
|
||||
|
||||
Ok(Socks5ProbeResults::with_http_result(result))
|
||||
}
|
||||
|
||||
async fn hardcoded_topology(
|
||||
network_details: &NymNetworkDetails,
|
||||
debug_config: &DebugConfig,
|
||||
) -> Result<Box<HardcodedTopologyProvider>, String> {
|
||||
// get Nym API URLs from network_details
|
||||
let nym_api_urls: Vec<Url> = network_details
|
||||
.nym_api_urls
|
||||
.as_ref()
|
||||
.map(|urls| urls.iter().filter_map(|u| u.url.parse().ok()).collect())
|
||||
.or_else(|| {
|
||||
network_details
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|e| e.api_url())
|
||||
.map(|url| vec![url])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if nym_api_urls.is_empty() {
|
||||
return Err(String::from("No nym-api URLs available to fetch topology"));
|
||||
}
|
||||
|
||||
let topology_config = NymApiTopologyProviderConfig {
|
||||
min_mixnode_performance: debug_config.topology.minimum_mixnode_performance,
|
||||
min_gateway_performance: debug_config.topology.minimum_gateway_performance,
|
||||
use_extended_topology: debug_config.topology.use_extended_topology,
|
||||
ignore_egress_epoch_role: debug_config.topology.ignore_egress_epoch_role,
|
||||
};
|
||||
|
||||
let api_client = nym_http_api_client::Client::new_url(nym_api_urls[0].clone(), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut provider = NymApiTopologyProvider::new(topology_config, nym_api_urls, api_client);
|
||||
|
||||
match provider.get_new_topology().await {
|
||||
Some(topology) => {
|
||||
info!("Fetched network topology");
|
||||
Ok(Box::new(HardcodedTopologyProvider::new(topology)))
|
||||
}
|
||||
None => Err(String::from("Failed to fetch network topology")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
//! that is shared between different test modes (authenticator-based and LP-based).
|
||||
|
||||
use nym_config::defaults::{WG_METADATA_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4};
|
||||
use std::net::SocketAddr;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::NetstackArgs;
|
||||
use crate::common::netstack::{
|
||||
NetstackRequest, NetstackRequestGo, NetstackResult, TwoHopNetstackRequestGo,
|
||||
};
|
||||
use crate::common::types::WgProbeResults;
|
||||
use crate::config::NetstackArgs;
|
||||
|
||||
/// Safe division that returns 0.0 when divisor is 0 (instead of NaN/Inf)
|
||||
fn safe_ratio(received: u16, sent: u16) -> f32 {
|
||||
@@ -185,7 +186,7 @@ pub struct TwoHopWgTunnelConfig {
|
||||
/// Entry gateway's WireGuard public key (hex encoded)
|
||||
pub entry_public_key_hex: String,
|
||||
/// Entry WireGuard endpoint address (entry_gateway_ip:port)
|
||||
pub entry_endpoint: String,
|
||||
pub entry_endpoint: SocketAddr,
|
||||
/// Entry Amnezia WG args (empty for standard WG)
|
||||
pub entry_awg_args: String,
|
||||
|
||||
@@ -197,7 +198,7 @@ pub struct TwoHopWgTunnelConfig {
|
||||
/// Exit gateway's WireGuard public key (hex encoded)
|
||||
pub exit_public_key_hex: String,
|
||||
/// Exit WireGuard endpoint address (exit_gateway_ip:port, forwarded via entry)
|
||||
pub exit_endpoint: String,
|
||||
pub exit_endpoint: SocketAddr,
|
||||
/// Exit Amnezia WG args (empty for standard WG)
|
||||
pub exit_awg_args: String,
|
||||
}
|
||||
@@ -209,24 +210,24 @@ impl TwoHopWgTunnelConfig {
|
||||
entry_private_ipv4: impl Into<String>,
|
||||
entry_private_key_hex: impl Into<String>,
|
||||
entry_public_key_hex: impl Into<String>,
|
||||
entry_endpoint: impl Into<String>,
|
||||
entry_endpoint: SocketAddr,
|
||||
entry_awg_args: impl Into<String>,
|
||||
exit_private_ipv4: impl Into<String>,
|
||||
exit_private_key_hex: impl Into<String>,
|
||||
exit_public_key_hex: impl Into<String>,
|
||||
exit_endpoint: impl Into<String>,
|
||||
exit_endpoint: SocketAddr,
|
||||
exit_awg_args: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
entry_private_ipv4: entry_private_ipv4.into(),
|
||||
entry_private_key_hex: entry_private_key_hex.into(),
|
||||
entry_public_key_hex: entry_public_key_hex.into(),
|
||||
entry_endpoint: entry_endpoint.into(),
|
||||
entry_endpoint,
|
||||
entry_awg_args: entry_awg_args.into(),
|
||||
exit_private_ipv4: exit_private_ipv4.into(),
|
||||
exit_private_key_hex: exit_private_key_hex.into(),
|
||||
exit_public_key_hex: exit_public_key_hex.into(),
|
||||
exit_endpoint: exit_endpoint.into(),
|
||||
exit_endpoint,
|
||||
exit_awg_args: exit_awg_args.into(),
|
||||
}
|
||||
}
|
||||
@@ -256,14 +257,14 @@ pub fn run_two_hop_tunnel_tests(
|
||||
entry_wg_ip: config.entry_private_ipv4.clone(),
|
||||
entry_private_key: config.entry_private_key_hex.clone(),
|
||||
entry_public_key: config.entry_public_key_hex.clone(),
|
||||
entry_endpoint: config.entry_endpoint.clone(),
|
||||
entry_endpoint: config.entry_endpoint,
|
||||
entry_awg_args: config.entry_awg_args.clone(),
|
||||
|
||||
// Exit tunnel config
|
||||
exit_wg_ip: config.exit_private_ipv4.clone(),
|
||||
exit_private_key: config.exit_private_key_hex.clone(),
|
||||
exit_public_key: config.exit_public_key_hex.clone(),
|
||||
exit_endpoint: config.exit_endpoint.clone(),
|
||||
exit_endpoint: config.exit_endpoint,
|
||||
exit_awg_args: config.exit_awg_args.clone(),
|
||||
|
||||
// Test parameters (use IPv4 config)
|
||||
|
||||
@@ -1,29 +1,94 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Args;
|
||||
use clap::{ArgGroup, Args};
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_node_status_client::models::AttachedTicketMaterials;
|
||||
use nym_sdk::mixnet::{
|
||||
CredentialStorage, DisconnectedMixnetClient, Ephemeral, MixnetClientStorage, OnDiskPersistent,
|
||||
};
|
||||
|
||||
#[derive(Args)]
|
||||
use crate::common::bandwidth_helpers::{acquire_bandwidth, import_bandwidth};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct CredentialArgs {
|
||||
/// Serialized credential data
|
||||
#[arg(long)]
|
||||
ticket_materials: Option<String>,
|
||||
pub ticket_materials: String,
|
||||
|
||||
/// Version of the serialized credential
|
||||
#[arg(long, default_value_t = 1)]
|
||||
ticket_materials_revision: u8,
|
||||
pub ticket_materials_revision: u8,
|
||||
}
|
||||
|
||||
impl CredentialArgs {
|
||||
pub fn decode_attached_ticket_materials(&self) -> anyhow::Result<AttachedTicketMaterials> {
|
||||
let ticket_materials = self
|
||||
.ticket_materials
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("ticket_materials is required"))?
|
||||
.clone();
|
||||
|
||||
Ok(AttachedTicketMaterials::from_serialised_string(
|
||||
ticket_materials,
|
||||
pub async fn import_credential(
|
||||
self,
|
||||
mixnet_client: &DisconnectedMixnetClient<Ephemeral>,
|
||||
) -> anyhow::Result<()> {
|
||||
let tickets_materials = AttachedTicketMaterials::from_serialised_string(
|
||||
self.ticket_materials,
|
||||
self.ticket_materials_revision,
|
||||
)?)
|
||||
)?;
|
||||
let bandwidth_import = mixnet_client.begin_bandwidth_import();
|
||||
import_bandwidth(bandwidth_import, tickets_materials).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Two ways to inject credentials when not running as agent
|
||||
/// 1. Mnemonic : expected to be used on lower envs
|
||||
/// - mnemonic
|
||||
/// 2. Mock ecash : expected to be used for local setups
|
||||
/// - use_mock_ecash
|
||||
#[derive(Debug, Args)]
|
||||
#[command(group(
|
||||
ArgGroup::new("credential_mode")
|
||||
.args(["use_mock_ecash","mnemonic"])
|
||||
.required(true)
|
||||
.multiple(false)
|
||||
))]
|
||||
pub struct CredentialMode {
|
||||
/// Use mock ecash credentials for testing (requires gateway with --lp-use-mock-ecash)
|
||||
#[arg(long, action = clap::ArgAction::SetTrue)]
|
||||
pub use_mock_ecash: bool,
|
||||
|
||||
/// Mnemonic to get credentials from the blockchain. It needs NYMs.
|
||||
#[arg(long)]
|
||||
pub mnemonic: Option<String>,
|
||||
}
|
||||
|
||||
impl CredentialMode {
|
||||
pub async fn acquire(
|
||||
&self,
|
||||
disconnected_mixnet_client: &DisconnectedMixnetClient<OnDiskPersistent>,
|
||||
storage: &OnDiskPersistent,
|
||||
) -> anyhow::Result<()> {
|
||||
// Return immediately as there is nothing to do
|
||||
if self.use_mock_ecash {
|
||||
return Ok(());
|
||||
}
|
||||
let ticketbook_count = storage
|
||||
.credential_store()
|
||||
.get_ticketbooks_info()
|
||||
.await?
|
||||
.len();
|
||||
tracing::info!("Credential store contains {} ticketbooks", ticketbook_count);
|
||||
|
||||
if ticketbook_count < 1 {
|
||||
let mnemonic = self.mnemonic.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"We are not using mock ecash and mnemonic is not set, this should not happen"
|
||||
)
|
||||
})?;
|
||||
for ticketbook_type in [
|
||||
TicketType::V1MixnetEntry,
|
||||
TicketType::V1WireguardEntry,
|
||||
TicketType::V1WireguardExit,
|
||||
] {
|
||||
acquire_bandwidth(mnemonic, disconnected_mixnet_client, ticketbook_type).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,49 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Args;
|
||||
|
||||
mod credentials;
|
||||
mod netstack;
|
||||
mod socks5;
|
||||
mod test_mode;
|
||||
|
||||
pub use credentials::CredentialArgs;
|
||||
pub use credentials::{CredentialArgs, CredentialMode};
|
||||
pub use netstack::NetstackArgs;
|
||||
pub use socks5::Socks5Args;
|
||||
pub use test_mode::TestMode;
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct ProbeConfig {
|
||||
/// Only choose gateway with that minimum performance
|
||||
#[arg(long)]
|
||||
pub min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
/// Test mode - explicitly specify which tests to run
|
||||
///
|
||||
/// Modes:
|
||||
/// core. - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// wg-mix - Wireguard via authenticator
|
||||
/// wg-lp - Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
/// lp-only - LP registration only (no WireGuard)
|
||||
/// socks5-only - Socks5 network requester test
|
||||
/// all - Mixnet, wireguard over authenticator and LP registration
|
||||
///
|
||||
#[arg(long, default_value_t = TestMode::default(), verbatim_doc_comment)]
|
||||
pub test_mode: TestMode,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
pub ignore_egress_epoch_role: bool,
|
||||
|
||||
/// Arguments to be appended to the wireguard config enabling amnezia-wg configuration
|
||||
#[arg(long)]
|
||||
pub amnezia_args: Option<String>,
|
||||
|
||||
/// Arguments to manage netstack downloads
|
||||
#[command(flatten)]
|
||||
pub netstack_args: NetstackArgs,
|
||||
|
||||
/// Arguments to configure socks5 probe
|
||||
#[command(flatten)]
|
||||
pub socks5_args: Socks5Args,
|
||||
}
|
||||
|
||||
@@ -3,39 +3,39 @@
|
||||
|
||||
use clap::Args;
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
#[derive(Args, Clone, Debug)]
|
||||
pub struct NetstackArgs {
|
||||
#[arg(long, default_value_t = 180)]
|
||||
#[arg(long, hide = true, default_value_t = 180)]
|
||||
pub netstack_download_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 30)]
|
||||
#[arg(long, hide = true, default_value_t = 30)]
|
||||
pub metadata_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value = "1.1.1.1")]
|
||||
#[arg(long, hide = true, default_value = "1.1.1.1")]
|
||||
pub netstack_v4_dns: String,
|
||||
|
||||
#[arg(long, default_value = "2606:4700:4700::1111")]
|
||||
#[arg(long, hide = true, default_value = "2606:4700:4700::1111")]
|
||||
pub netstack_v6_dns: String,
|
||||
|
||||
#[arg(long, default_value_t = 5)]
|
||||
#[arg(long, hide = true, default_value_t = 5)]
|
||||
pub netstack_num_ping: u8,
|
||||
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub netstack_send_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub netstack_recv_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_values_t = vec!["nym.com".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["nym.com".to_string()])]
|
||||
pub netstack_ping_hosts_v4: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["1.1.1.1".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["1.1.1.1".to_string()])]
|
||||
pub netstack_ping_ips_v4: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["cloudflare.com".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["cloudflare.com".to_string()])]
|
||||
pub netstack_ping_hosts_v6: Vec<String>,
|
||||
|
||||
#[arg(long, default_values_t = vec!["2001:4860:4860::8888".to_string(), "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string()])]
|
||||
#[arg(long, hide= true, default_values_t = vec!["2001:4860:4860::8888".to_string(), "2606:4700:4700::1111".to_string(), "2620:fe::fe".to_string()])]
|
||||
pub netstack_ping_ips_v6: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,21 @@ use clap::Args;
|
||||
|
||||
use crate::common::socks5_test::JsonRpcClient;
|
||||
|
||||
#[derive(Args)]
|
||||
const DEFAULT_RPC_ENDPOINT: &str = "https://cloudflare-eth.com";
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct Socks5Args {
|
||||
#[arg(long, value_delimiter = ';')]
|
||||
#[arg(long, hide = true, value_delimiter = ';', default_value = DEFAULT_RPC_ENDPOINT)]
|
||||
pub socks5_json_rpc_url_list: Vec<String>,
|
||||
|
||||
#[arg(long, default_value_t = 30)]
|
||||
#[arg(long, hide = true, default_value_t = 30)]
|
||||
pub mixnet_client_timeout_sec: u64,
|
||||
|
||||
#[arg(long, default_value_t = 10)]
|
||||
#[arg(long, hide = true, default_value_t = 10)]
|
||||
pub test_count: u64,
|
||||
|
||||
/// stops socks5 test early after this many failed attempts
|
||||
#[arg(long, default_value_t = 3)]
|
||||
#[arg(long, hide = true, default_value_t = 3)]
|
||||
pub failure_count_cutoff: usize,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,87 +4,72 @@
|
||||
//! Test mode definitions for gateway probe.
|
||||
//!
|
||||
//! This module defines the different test modes supported by the gateway probe:
|
||||
//! - Mixnet: Traditional mixnet path testing
|
||||
//! - SingleHop: LP registration + WireGuard on single gateway
|
||||
//! - TwoHop: Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
//! - Core: Traditional mixnet path testing and Wireguard via authenticator
|
||||
//! - WgMix: Wireguard via authenticator
|
||||
//! - WgLp: Entry LP + Exit LP (nested forwarding) + WireGuard
|
||||
//! - LpOnly: LP registration only, no WireGuard
|
||||
//! - Socks5Only: Socks5 test
|
||||
//! - All: Mixnet, wireguard over authenticator and LP registration
|
||||
|
||||
/// Test mode for the gateway probe.
|
||||
///
|
||||
/// Determines which tests are performed and how connections are established.
|
||||
// This enum replaces the scattered boolean flags (only_wireguard,
|
||||
// only_lp_registration, test_lp_wg) with explicit, named modes for clarity.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum TestMode {
|
||||
/// Traditional mixnet testing - connects via mixnet, tests entry/exit pings + WireGuard via authenticator
|
||||
/// Mixnet tests + WireGuard via authenticator
|
||||
#[default]
|
||||
Mixnet,
|
||||
/// LP registration + WireGuard on single gateway (no mixnet, no forwarding)
|
||||
SingleHop,
|
||||
/// Entry LP + Exit LP (nested session forwarding) + WireGuard tunnel
|
||||
TwoHop,
|
||||
/// LP registration only - test handshake and registration, skip WireGuard
|
||||
Core,
|
||||
/// Wireguard via authenticator
|
||||
WgMix,
|
||||
/// Wireguard over LP
|
||||
WgLp,
|
||||
/// LP registration only - test handshake and registration
|
||||
LpOnly,
|
||||
/// Socks5 test only
|
||||
Socks5Only,
|
||||
/// Mixnet tests, Wireguard tests, LP tests, Socks5 test
|
||||
All,
|
||||
}
|
||||
|
||||
impl TestMode {
|
||||
/// Infer test mode from legacy boolean flags (backward compatibility)
|
||||
pub fn from_flags(
|
||||
only_wireguard: bool,
|
||||
only_lp_registration: bool,
|
||||
test_lp_wg: bool,
|
||||
has_exit_gateway: bool,
|
||||
) -> Self {
|
||||
if only_lp_registration {
|
||||
TestMode::LpOnly
|
||||
} else if test_lp_wg {
|
||||
if has_exit_gateway {
|
||||
TestMode::TwoHop
|
||||
} else {
|
||||
TestMode::SingleHop
|
||||
}
|
||||
} else if only_wireguard {
|
||||
// WireGuard via authenticator (still uses mixnet path)
|
||||
TestMode::Mixnet
|
||||
} else {
|
||||
TestMode::Mixnet
|
||||
}
|
||||
// Wether we need to run mixnet tests
|
||||
pub fn mixnet_tests(&self) -> bool {
|
||||
matches!(self, TestMode::Core | TestMode::All)
|
||||
}
|
||||
|
||||
// Wether we need to run Wiregurd tests
|
||||
pub fn wireguard_tests(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::Core | TestMode::WgMix | TestMode::WgLp | TestMode::All
|
||||
)
|
||||
}
|
||||
|
||||
// Wether we need to run Lp tests
|
||||
pub fn lp_tests(&self) -> bool {
|
||||
matches!(self, TestMode::WgLp | TestMode::LpOnly | TestMode::All)
|
||||
}
|
||||
|
||||
// Wether we need to run socks5 tests
|
||||
pub fn socks5_tests(&self) -> bool {
|
||||
matches!(self, TestMode::Socks5Only | TestMode::All)
|
||||
}
|
||||
|
||||
/// Whether this mode requires a mixnet client
|
||||
pub fn needs_mixnet(&self) -> bool {
|
||||
matches!(self, TestMode::Mixnet)
|
||||
}
|
||||
|
||||
/// Whether this mode uses LP registration
|
||||
pub fn uses_lp(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::SingleHop | TestMode::TwoHop | TestMode::LpOnly
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this mode tests WireGuard tunnels
|
||||
pub fn tests_wireguard(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TestMode::Mixnet | TestMode::SingleHop | TestMode::TwoHop
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this mode requires an exit gateway
|
||||
pub fn needs_exit_gateway(&self) -> bool {
|
||||
matches!(self, TestMode::TwoHop)
|
||||
matches!(self, TestMode::Core | TestMode::All | TestMode::WgMix)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TestMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TestMode::Mixnet => write!(f, "mixnet"),
|
||||
TestMode::SingleHop => write!(f, "single-hop"),
|
||||
TestMode::TwoHop => write!(f, "two-hop"),
|
||||
TestMode::Core => write!(f, "core"),
|
||||
TestMode::WgMix => write!(f, "wg-mix"),
|
||||
TestMode::WgLp => write!(f, "wg-lp"),
|
||||
TestMode::LpOnly => write!(f, "lp-only"),
|
||||
TestMode::Socks5Only => write!(f, "socks5-only"),
|
||||
TestMode::All => write!(f, "all"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,12 +79,14 @@ impl std::str::FromStr for TestMode {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"mixnet" => Ok(TestMode::Mixnet),
|
||||
"single-hop" | "singlehop" | "single_hop" => Ok(TestMode::SingleHop),
|
||||
"two-hop" | "twohop" | "two_hop" => Ok(TestMode::TwoHop),
|
||||
"mixnet" | "core" => Ok(TestMode::Core),
|
||||
"wg-mix" | "wgmix" | "wg_mix" => Ok(TestMode::WgMix),
|
||||
"wg-lp" | "wglp" | "wg_lp" => Ok(TestMode::WgLp),
|
||||
"lp-only" | "lponly" | "lp_only" => Ok(TestMode::LpOnly),
|
||||
"socks5-only" | "socks5only" | "socks5_only" => Ok(TestMode::Socks5Only),
|
||||
"all" => Ok(TestMode::All),
|
||||
_ => Err(format!(
|
||||
"Unknown test mode: '{}'. Valid modes: mixnet, single-hop, two-hop, lp-only",
|
||||
"Unknown test mode: '{}'. Valid modes: core, wg-mix, wg-lp, lp-only, socks5-only, all",
|
||||
s
|
||||
)),
|
||||
}
|
||||
@@ -110,152 +97,80 @@ impl std::str::FromStr for TestMode {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ============ from_flags() tests ============
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_default_is_mixnet() {
|
||||
// All flags false -> Mixnet (default)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, false, false),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_only_wireguard_is_mixnet() {
|
||||
// only_wireguard still uses mixnet path (WG via authenticator)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(true, false, false, false),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_only_lp_registration() {
|
||||
// only_lp_registration -> LpOnly (takes priority)
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, true, false, false),
|
||||
TestMode::LpOnly
|
||||
);
|
||||
// Even with other flags set, only_lp_registration wins
|
||||
assert_eq!(
|
||||
TestMode::from_flags(true, true, true, true),
|
||||
TestMode::LpOnly
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_test_lp_wg_single_hop() {
|
||||
// test_lp_wg without exit gateway -> SingleHop
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, true, false),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_test_lp_wg_two_hop() {
|
||||
// test_lp_wg with exit gateway -> TwoHop
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, true, true),
|
||||
TestMode::TwoHop
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_flags_has_exit_gateway_alone_is_mixnet() {
|
||||
// has_exit_gateway alone doesn't change mode
|
||||
assert_eq!(
|
||||
TestMode::from_flags(false, false, false, true),
|
||||
TestMode::Mixnet
|
||||
);
|
||||
}
|
||||
|
||||
// ============ Helper method tests ============
|
||||
|
||||
#[test]
|
||||
fn test_needs_mixnet() {
|
||||
assert!(TestMode::Mixnet.needs_mixnet());
|
||||
assert!(!TestMode::SingleHop.needs_mixnet());
|
||||
assert!(!TestMode::TwoHop.needs_mixnet());
|
||||
assert!(TestMode::Core.needs_mixnet());
|
||||
assert!(TestMode::WgMix.needs_mixnet());
|
||||
assert!(!TestMode::WgLp.needs_mixnet());
|
||||
assert!(!TestMode::LpOnly.needs_mixnet());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uses_lp() {
|
||||
assert!(!TestMode::Mixnet.uses_lp());
|
||||
assert!(TestMode::SingleHop.uses_lp());
|
||||
assert!(TestMode::TwoHop.uses_lp());
|
||||
assert!(TestMode::LpOnly.uses_lp());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tests_wireguard() {
|
||||
assert!(TestMode::Mixnet.tests_wireguard());
|
||||
assert!(TestMode::SingleHop.tests_wireguard());
|
||||
assert!(TestMode::TwoHop.tests_wireguard());
|
||||
assert!(!TestMode::LpOnly.tests_wireguard());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_needs_exit_gateway() {
|
||||
assert!(!TestMode::Mixnet.needs_exit_gateway());
|
||||
assert!(!TestMode::SingleHop.needs_exit_gateway());
|
||||
assert!(TestMode::TwoHop.needs_exit_gateway());
|
||||
assert!(!TestMode::LpOnly.needs_exit_gateway());
|
||||
assert!(!TestMode::Socks5Only.needs_mixnet());
|
||||
assert!(TestMode::All.needs_mixnet());
|
||||
}
|
||||
|
||||
// ============ Display tests ============
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(TestMode::Mixnet.to_string(), "mixnet");
|
||||
assert_eq!(TestMode::SingleHop.to_string(), "single-hop");
|
||||
assert_eq!(TestMode::TwoHop.to_string(), "two-hop");
|
||||
assert_eq!(TestMode::Core.to_string(), "core");
|
||||
assert_eq!(TestMode::WgMix.to_string(), "wg-mix");
|
||||
assert_eq!(TestMode::WgLp.to_string(), "wg-lp");
|
||||
assert_eq!(TestMode::LpOnly.to_string(), "lp-only");
|
||||
assert_eq!(TestMode::Socks5Only.to_string(), "socks5-only");
|
||||
assert_eq!(TestMode::All.to_string(), "all");
|
||||
}
|
||||
|
||||
// ============ FromStr tests ============
|
||||
|
||||
#[test]
|
||||
fn test_from_str_canonical() {
|
||||
assert_eq!("mixnet".parse::<TestMode>().unwrap(), TestMode::Mixnet);
|
||||
assert_eq!(
|
||||
"single-hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("two-hop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("core".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
assert_eq!("wg-mix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wg-lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lp-only".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5-only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
assert_eq!("all".parse::<TestMode>().unwrap(), TestMode::All);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_alternate_formats() {
|
||||
// Default aliases
|
||||
assert_eq!("mixnet".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
|
||||
// snake_case
|
||||
assert_eq!(
|
||||
"single_hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("two_hop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("wg_mix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wg_lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lp_only".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5_only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
|
||||
// no separator
|
||||
assert_eq!(
|
||||
"singlehop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("twohop".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("wgmix".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("wglp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("lponly".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"socks5only".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str_case_insensitive() {
|
||||
assert_eq!("MIXNET".parse::<TestMode>().unwrap(), TestMode::Mixnet);
|
||||
assert_eq!(
|
||||
"Single-Hop".parse::<TestMode>().unwrap(),
|
||||
TestMode::SingleHop
|
||||
);
|
||||
assert_eq!("TWO_HOP".parse::<TestMode>().unwrap(), TestMode::TwoHop);
|
||||
assert_eq!("cOrE".parse::<TestMode>().unwrap(), TestMode::Core);
|
||||
assert_eq!("WG-MIX".parse::<TestMode>().unwrap(), TestMode::WgMix);
|
||||
assert_eq!("Wg_Lp".parse::<TestMode>().unwrap(), TestMode::WgLp);
|
||||
assert_eq!("LpOnly".parse::<TestMode>().unwrap(), TestMode::LpOnly);
|
||||
assert_eq!(
|
||||
"soCkS5-oNlY".parse::<TestMode>().unwrap(),
|
||||
TestMode::Socks5Only
|
||||
);
|
||||
assert_eq!("ALL".parse::<TestMode>().unwrap(), TestMode::All);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -270,10 +185,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_display_fromstr_roundtrip() {
|
||||
for mode in [
|
||||
TestMode::Mixnet,
|
||||
TestMode::SingleHop,
|
||||
TestMode::TwoHop,
|
||||
TestMode::Core,
|
||||
TestMode::WgMix,
|
||||
TestMode::WgLp,
|
||||
TestMode::LpOnly,
|
||||
TestMode::Socks5Only,
|
||||
TestMode::All,
|
||||
] {
|
||||
let s = mode.to_string();
|
||||
let parsed: TestMode = s.parse().unwrap();
|
||||
|
||||
+346
-794
File diff suppressed because it is too large
Load Diff
+145
-511
@@ -1,20 +1,12 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use anyhow::bail;
|
||||
use clap::{Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::setup_env;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_gateway_probe::config::Socks5Args;
|
||||
use nym_gateway_probe::{
|
||||
CredentialArgs, NetstackArgs, NymApiDirectory, ProbeResult, TestMode, TestedNode,
|
||||
TestedNodeDetails, TestedNodeLpDetails, query_gateway_by_ip,
|
||||
};
|
||||
use nym_kkt_ciphersuite::{HashFunction, KEM};
|
||||
use nym_gateway_probe::config::{CredentialArgs, CredentialMode, ProbeConfig};
|
||||
use nym_gateway_probe::{NymApiDirectory, ProbeResult, query_gateway_by_ip};
|
||||
use nym_sdk::mixnet::NodeIdentity;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use tracing::*;
|
||||
@@ -23,178 +15,84 @@ fn pretty_build_info_static() -> &'static str {
|
||||
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
fn validate_node_identity(s: &str) -> Result<NodeIdentity, String> {
|
||||
match s.parse() {
|
||||
Ok(cg) => Ok(cg),
|
||||
Err(_) => Err(format!("failed to parse country group: {s}")),
|
||||
}
|
||||
}
|
||||
const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||
struct CliArgs {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
command: Commands,
|
||||
|
||||
/// Path pointing to an env file describing the network.
|
||||
#[arg(short, long, global = true)]
|
||||
#[arg(short, long)]
|
||||
config_env_file: Option<PathBuf>,
|
||||
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway", global = true)]
|
||||
entry_gateway: Option<String>,
|
||||
|
||||
/// The address of the gateway to probe directly (bypasses directory lookup)
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long, global = true)]
|
||||
gateway_ip: Option<String>,
|
||||
|
||||
// ##########
|
||||
// ENTRY
|
||||
// ##########
|
||||
/// Ed25519 identity of the entry gateway (base58 encoded)
|
||||
/// When provided, skips HTTP API query - use for localnet testing
|
||||
#[arg(long, global = true)]
|
||||
entry_gateway_identity: Option<String>,
|
||||
|
||||
/// x25519 key of the entry gateway used for KKT exchange (base58 encoded)
|
||||
#[arg(long, global = true, requires = "entry_gateway_identity")]
|
||||
entry_gateway_x25519_key: Option<String>,
|
||||
|
||||
/// expected kem type of the entry gateway used during KKT exchange
|
||||
#[arg(long, global = true, requires = "entry_gateway_x25519_key")]
|
||||
entry_gateway_kem_key_type: Option<String>,
|
||||
|
||||
/// expected hash function used for the entry gateway kem key digest
|
||||
#[arg(long, global = true, requires = "entry_gateway_kem_key_type")]
|
||||
entry_gateway_kem_key_hash_function: Option<String>,
|
||||
|
||||
/// expected entry gateway kem key digest (base58 encoded)
|
||||
#[arg(long, global = true, requires = "entry_gateway_kem_key_hash_function")]
|
||||
entry_gateway_kem_hey_hash_bs58: Option<String>,
|
||||
|
||||
/// LP listener address for entry gateway (e.g., "192.168.66.6:41264")
|
||||
/// Used with --entry-gateway-identity for localnet mode
|
||||
#[arg(long, global = true)]
|
||||
entry_lp_address: Option<SocketAddr>,
|
||||
|
||||
// ##########
|
||||
// EXIT
|
||||
// ##########
|
||||
/// The address of the exit gateway for LP forwarding tests (used with --test-lp-wg)
|
||||
/// When specified, --gateway-ip becomes the entry gateway and this becomes the exit gateway
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long, global = true)]
|
||||
exit_gateway_ip: Option<String>,
|
||||
|
||||
/// Ed25519 identity of the exit gateway (base58 encoded)
|
||||
/// When provided, skips HTTP API query - use for localnet testing
|
||||
#[arg(long, global = true)]
|
||||
exit_gateway_identity: Option<String>,
|
||||
|
||||
/// x25519 key of the exit gateway used for KKT exchange (base58 encoded)
|
||||
#[arg(long, global = true, requires = "exit_gateway_identity")]
|
||||
exit_gateway_x25519_key: Option<String>,
|
||||
|
||||
/// expected kem type of the exit gateway used during KKT exchange
|
||||
#[arg(long, global = true, requires = "exit_gateway_x25519_key")]
|
||||
exit_gateway_kem_key_type: Option<String>,
|
||||
|
||||
/// expected hash function used for the exit gateway kem key digest
|
||||
#[arg(long, global = true, requires = "exit_gateway_kem_key_type")]
|
||||
exit_gateway_kem_key_hash_function: Option<String>,
|
||||
|
||||
/// expected exit gateway kem key digest (base58 encoded)
|
||||
#[arg(long, global = true, requires = "exit_gateway_kem_key_hash_function")]
|
||||
exit_gateway_kem_hey_hash_bs58: Option<String>,
|
||||
|
||||
/// LP listener address for exit gateway (e.g., "172.18.0.5:41264")
|
||||
/// This is the address the entry gateway uses to reach exit (for forwarding)
|
||||
/// Used with --exit-gateway-identity for localnet mode
|
||||
#[arg(long, global = true)]
|
||||
exit_lp_address: Option<SocketAddr>,
|
||||
|
||||
/// Default LP control port when deriving LP address from gateway IP
|
||||
#[arg(long, global = true, default_value = "41264")]
|
||||
lp_port: u16,
|
||||
|
||||
/// Identity of the node to test
|
||||
#[arg(long, short, value_parser = validate_node_identity, global = true)]
|
||||
node: Option<NodeIdentity>,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
min_gateway_mixnet_performance: Option<u8>,
|
||||
|
||||
// this was a dead field
|
||||
// #[arg(long, global = true)]
|
||||
// min_gateway_vpn_performance: Option<u8>,
|
||||
#[arg(long, global = true)]
|
||||
only_wireguard: bool,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
only_lp_registration: bool,
|
||||
|
||||
/// Test WireGuard via LP registration (no mixnet) - uses nested session forwarding
|
||||
#[arg(long, global = true)]
|
||||
test_lp_wg: bool,
|
||||
|
||||
/// Test mode - explicitly specify which tests to run
|
||||
///
|
||||
/// Modes:
|
||||
/// mixnet - Traditional mixnet testing (entry/exit pings + WireGuard via authenticator)
|
||||
/// single-hop - LP registration + WireGuard on single gateway (no mixnet)
|
||||
/// two-hop - Entry LP + Exit LP (nested forwarding) + WireGuard tunnel
|
||||
/// lp-only - LP registration only (no WireGuard)
|
||||
///
|
||||
/// If not specified, mode is inferred from other flags:
|
||||
/// --only-lp-registration → lp-only
|
||||
/// --test-lp-wg with exit gateway → two-hop
|
||||
/// --test-lp-wg without exit → single-hop
|
||||
/// otherwise → mixnet
|
||||
#[arg(long, global = true, value_name = "MODE")]
|
||||
mode: Option<String>,
|
||||
|
||||
/// Disable logging during probe
|
||||
#[arg(long, global = true)]
|
||||
ignore_egress_epoch_role: bool,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
#[arg(long)]
|
||||
no_log: bool,
|
||||
|
||||
/// Arguments to be appended to the wireguard config enabling amnezia-wg configuration
|
||||
#[arg(long, short, global = true)]
|
||||
amnezia_args: Option<String>,
|
||||
|
||||
/// Arguments to manage netstack downloads
|
||||
#[command(flatten)]
|
||||
netstack_args: NetstackArgs,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_args: CredentialArgs,
|
||||
|
||||
/// Arguments to configure socks5 probe
|
||||
#[command(flatten)]
|
||||
socks5_args: Socks5Args,
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG_DIR: &str = "/tmp/nym-gateway-probe/config/";
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Run the probe locally
|
||||
/// Run the probe on an unannounced gateway. IP must be provided. Bypasses directory lookup
|
||||
RunLocal {
|
||||
/// Provide a mnemonic to get credentials (optional when using --use-mock-ecash)
|
||||
#[arg(long)]
|
||||
mnemonic: Option<String>,
|
||||
|
||||
/// Directory for credential and mixnet storage
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// Use mock ecash credentials for testing (requires gateway with --lp-use-mock-ecash)
|
||||
/// The address of the gateway
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long)]
|
||||
use_mock_ecash: bool,
|
||||
entry_gateway_ip: String,
|
||||
|
||||
/// The address of the exit gateway. If not provided, entry acts as exit
|
||||
/// Supports formats: IP (192.168.66.5), IP:PORT (192.168.66.5:8080), HOST:PORT (localhost:30004)
|
||||
#[arg(long)]
|
||||
exit_gateway_ip: Option<String>,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_mode: CredentialMode,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
|
||||
/// Run the probe on a bonded gateway. Uses directory lookup
|
||||
Run {
|
||||
/// Directory for credential and mixnet storage
|
||||
#[arg(long)]
|
||||
config_dir: Option<PathBuf>,
|
||||
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway")]
|
||||
entry_gateway: NodeIdentity,
|
||||
|
||||
/// Optional identity of the exit node to test, if not provided, entry_gateway is used
|
||||
#[arg(long)]
|
||||
exit_gateway: Option<NodeIdentity>,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_mode: CredentialMode,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
|
||||
/// Run the probe by NS agents
|
||||
RunAgent {
|
||||
/// The specific gateway specified by ID.
|
||||
#[arg(long, short = 'g', alias = "gateway")]
|
||||
entry_gateway: NodeIdentity,
|
||||
|
||||
/// Arguments to manage credentials
|
||||
#[command(flatten)]
|
||||
credential_args: CredentialArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
probe_config: ProbeConfig,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -214,266 +112,66 @@ fn setup_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
/// Resolve the test mode from explicit --mode arg or infer from legacy flags
|
||||
fn resolve_test_mode(
|
||||
mode_arg: Option<&str>,
|
||||
only_wireguard: bool,
|
||||
only_lp_registration: bool,
|
||||
test_lp_wg: bool,
|
||||
has_exit_gateway: bool,
|
||||
) -> anyhow::Result<TestMode> {
|
||||
if let Some(mode_str) = mode_arg {
|
||||
// Explicit --mode takes priority
|
||||
mode_str
|
||||
.parse::<TestMode>()
|
||||
.map_err(|e| anyhow::anyhow!("{}", e))
|
||||
} else {
|
||||
// Infer from legacy flags
|
||||
Ok(TestMode::from_flags(
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
has_exit_gateway,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert TestMode back to legacy boolean flags for backward compatibility
|
||||
fn mode_to_flags(mode: TestMode) -> (bool, bool, bool) {
|
||||
match mode {
|
||||
TestMode::Mixnet => (false, false, false), // only_wireguard handled separately
|
||||
TestMode::SingleHop => (false, false, true),
|
||||
TestMode::TwoHop => (false, false, true),
|
||||
TestMode::LpOnly => (false, true, false),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::todo)]
|
||||
#[allow(unreachable_code, unused)]
|
||||
// ^^^^ // NOTE: to be changed by @SW
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
let args = CliArgs::parse();
|
||||
if !args.no_log {
|
||||
setup_logging();
|
||||
}
|
||||
debug!("{:?}", nym_bin_common::bin_info_local_vergen!());
|
||||
setup_env(args.config_env_file.as_ref());
|
||||
|
||||
setup_env(args.config_env_file.as_ref());
|
||||
let network = nym_sdk::NymNetworkDetails::new_from_env();
|
||||
|
||||
let nyxd_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.map(|ep| ep.nyxd_url())
|
||||
.ok_or(anyhow::anyhow!("missing nyxd url"))?;
|
||||
match args.command {
|
||||
Commands::RunLocal {
|
||||
config_dir,
|
||||
entry_gateway_ip,
|
||||
exit_gateway_ip,
|
||||
credential_mode,
|
||||
probe_config,
|
||||
} => {
|
||||
info!("Using direct IP query mode for gateway: {entry_gateway_ip}");
|
||||
let entry_details = query_gateway_by_ip(entry_gateway_ip)
|
||||
.await?
|
||||
.to_testable_node()?;
|
||||
|
||||
args.socks5_args.validate_socks5_endpoints().await?;
|
||||
|
||||
// Three resolution modes in priority order:
|
||||
// 1. Localnet mode: --entry-gateway-identity provided (no HTTP query)
|
||||
// 2. Direct IP mode: --gateway-ip provided (queries HTTP API)
|
||||
// 3. Directory mode: uses nym-api directory service
|
||||
|
||||
// Localnet mode: identity provided via CLI, skip HTTP queries entirely
|
||||
if let Some(kem_key_digest) = &args.entry_gateway_kem_hey_hash_bs58 {
|
||||
info!("Using localnet mode with CLI-provided gateway identity");
|
||||
|
||||
// SAFETY: if kem key digest is provided, all other LP data must also be present
|
||||
// (enforced by clap)
|
||||
let hash_fn: HashFunction = args
|
||||
.entry_gateway_kem_key_hash_function
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
let kem_type: KEM = args.entry_gateway_kem_key_type.as_ref().unwrap().parse()?;
|
||||
let x25519_key: x25519::PublicKey =
|
||||
args.entry_gateway_x25519_key.as_ref().unwrap().parse()?;
|
||||
let identity: ed25519::PublicKey = args.entry_gateway_identity.as_ref().unwrap().parse()?;
|
||||
let digest = bs58::decode(&kem_key_digest).into_vec()?;
|
||||
|
||||
let mut expected_kem_key_hashes = HashMap::new();
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(hash_fn, digest);
|
||||
expected_kem_key_hashes.insert(kem_type, digests);
|
||||
|
||||
// Entry LP address: explicit or derived from gateway_ip + lp_port
|
||||
let entry_lp_addr: SocketAddr = if let Some(lp_addr) = args.entry_lp_address {
|
||||
lp_addr
|
||||
} else if let Some(gw_ip) = &args.gateway_ip {
|
||||
// Derive LP address from gateway IP
|
||||
let ip: std::net::IpAddr = gw_ip
|
||||
.parse()
|
||||
.map_err(|e| anyhow::anyhow!("Invalid gateway-ip '{gw_ip}': {e}"))?;
|
||||
SocketAddr::new(ip, args.lp_port)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"--entry-lp-address or --gateway-ip required with --entry-gateway-identity"
|
||||
);
|
||||
};
|
||||
|
||||
let entry_lp_node = TestedNodeLpDetails {
|
||||
address: entry_lp_addr,
|
||||
expected_kem_key_hashes,
|
||||
expected_signing_key_hashes: todo!(),
|
||||
x25519: x25519_key,
|
||||
lp_version: todo!(),
|
||||
};
|
||||
let entry_details = TestedNodeDetails::from_cli(identity, entry_lp_node);
|
||||
|
||||
// Parse exit gateway if provided
|
||||
let exit_details = if let Some(kem_key_digest) = &args.exit_gateway_kem_hey_hash_bs58 {
|
||||
let exit_lp_addr = *args.exit_lp_address.as_ref().ok_or_else(|| {
|
||||
anyhow::anyhow!("--exit-lp-address required with --exit-gateway-identity")
|
||||
})?;
|
||||
|
||||
// SAFETY: if kem key digest is provided, all other LP data must also be present
|
||||
// (enforced by clap)
|
||||
let hash_fn: HashFunction = args
|
||||
.exit_gateway_kem_key_hash_function
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
let kem_type: KEM = args.exit_gateway_kem_key_type.as_ref().unwrap().parse()?;
|
||||
let x25519_key: x25519::PublicKey =
|
||||
args.exit_gateway_x25519_key.as_ref().unwrap().parse()?;
|
||||
let identity: ed25519::PublicKey =
|
||||
args.exit_gateway_identity.as_ref().unwrap().parse()?;
|
||||
let digest = bs58::decode(&kem_key_digest).into_vec()?;
|
||||
|
||||
let mut expected_kem_key_hashes = HashMap::new();
|
||||
let mut digests = HashMap::new();
|
||||
digests.insert(hash_fn, digest);
|
||||
expected_kem_key_hashes.insert(kem_type, digests);
|
||||
|
||||
let exit_lp_node = TestedNodeLpDetails {
|
||||
address: exit_lp_addr,
|
||||
expected_kem_key_hashes,
|
||||
expected_signing_key_hashes: todo!(),
|
||||
x25519: x25519_key,
|
||||
lp_version: todo!(),
|
||||
};
|
||||
|
||||
Some(TestedNodeDetails::from_cli(identity, exit_lp_node))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Resolve test mode from --mode arg or infer from flags
|
||||
let has_exit = exit_details.is_some();
|
||||
let test_mode = resolve_test_mode(
|
||||
args.mode.as_deref(),
|
||||
args.only_wireguard,
|
||||
args.only_lp_registration,
|
||||
args.test_lp_wg,
|
||||
has_exit,
|
||||
)?;
|
||||
|
||||
// Validate that two-hop mode has required exit gateway
|
||||
if test_mode.needs_exit_gateway() && !has_exit {
|
||||
bail!(
|
||||
"--mode two-hop requires exit gateway \
|
||||
(use --exit-gateway-identity and --exit-lp-address)"
|
||||
);
|
||||
}
|
||||
|
||||
info!("Test mode: {}", test_mode);
|
||||
|
||||
// Convert back to flags for backward compatibility with existing probe methods
|
||||
// only_wireguard is preserved from args since it's orthogonal to mode
|
||||
// (it means "skip ping tests" in mixnet mode, irrelevant for LP modes)
|
||||
let (_, only_lp_registration, test_lp_wg) = mode_to_flags(test_mode);
|
||||
let only_wireguard = args.only_wireguard;
|
||||
|
||||
let mut trial = nym_gateway_probe::Probe::new_localnet(
|
||||
entry_details,
|
||||
exit_details,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
args.socks5_args,
|
||||
);
|
||||
|
||||
if let Some(awg_args) = args.amnezia_args {
|
||||
trial.with_amnezia(&awg_args);
|
||||
}
|
||||
|
||||
// Localnet mode doesn't need directory, but nyxd_url is still used for credentials
|
||||
return match &args.command {
|
||||
Some(Commands::RunLocal {
|
||||
mnemonic,
|
||||
config_dir,
|
||||
use_mock_ecash,
|
||||
}) => {
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
Box::pin(trial.probe_run_locally(
|
||||
&config_dir,
|
||||
mnemonic.as_deref(),
|
||||
None, // No directory in localnet mode
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
*use_mock_ecash,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
Box::pin(trial.probe(
|
||||
None, // No directory in localnet mode
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// If gateway IP is provided, query it directly without using the directory
|
||||
let (entry, directory, gateway_node, exit_gateway_node) =
|
||||
if let Some(gateway_ip) = args.gateway_ip.clone() {
|
||||
info!("Using direct IP query mode for gateway: {}", gateway_ip);
|
||||
let gateway_node = query_gateway_by_ip(gateway_ip).await?;
|
||||
let identity = gateway_node.identity();
|
||||
|
||||
// Query exit gateway if provided (for LP forwarding tests)
|
||||
let exit_node = if let Some(exit_gateway_ip) = args.exit_gateway_ip {
|
||||
info!(
|
||||
"Using direct IP query mode for exit gateway: {}",
|
||||
exit_gateway_ip
|
||||
);
|
||||
Some(query_gateway_by_ip(exit_gateway_ip).await?)
|
||||
// Parse exit gateway if provided
|
||||
let exit_details = if let Some(ip_address) = exit_gateway_ip {
|
||||
info!("Using direct IP query mode for exit gateway: {ip_address}");
|
||||
Some(query_gateway_by_ip(ip_address).await?.to_testable_node()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Still create the directory for potential secondary lookups,
|
||||
// but only if API URL is available
|
||||
let directory =
|
||||
if let Some(api_url) = network.endpoints.first().and_then(|ep| ep.api_url()) {
|
||||
Some(NymApiDirectory::new(api_url).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
(identity, directory, Some(gateway_node), exit_node)
|
||||
} else {
|
||||
// Original behavior: use directory service
|
||||
if config_dir.is_file() {
|
||||
anyhow::bail!("provided configuration directory is a file");
|
||||
}
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir.clone())?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
|
||||
Box::pin(trial.probe_run_locally(&config_dir, credential_mode)).await
|
||||
}
|
||||
Commands::Run {
|
||||
entry_gateway,
|
||||
exit_gateway,
|
||||
config_dir,
|
||||
credential_mode,
|
||||
probe_config,
|
||||
} => {
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
@@ -481,121 +179,57 @@ pub(crate) async fn run() -> anyhow::Result<ProbeResult> {
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
let entry_details = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
let exit_details = exit_gateway
|
||||
.map(|id_key| directory.exit_gateway(&id_key))
|
||||
.transpose()?
|
||||
.map(|node| node.to_testable_node())
|
||||
.transpose()?;
|
||||
|
||||
let entry = if let Some(gateway) = &args.entry_gateway {
|
||||
NodeIdentity::from_base58_string(gateway)?
|
||||
} else {
|
||||
directory.random_exit_with_ipr()?
|
||||
};
|
||||
|
||||
(entry, Some(directory), None, None)
|
||||
};
|
||||
|
||||
let test_point = if let Some(node) = args.node {
|
||||
TestedNode::Custom {
|
||||
identity: node,
|
||||
shares_entry: false,
|
||||
}
|
||||
} else {
|
||||
TestedNode::SameAsEntry
|
||||
};
|
||||
|
||||
// Resolve test mode from --mode arg or infer from flags
|
||||
let has_exit = exit_gateway_node.is_some();
|
||||
let test_mode = resolve_test_mode(
|
||||
args.mode.as_deref(),
|
||||
args.only_wireguard,
|
||||
args.only_lp_registration,
|
||||
args.test_lp_wg,
|
||||
has_exit,
|
||||
)?;
|
||||
info!("Test mode: {}", test_mode);
|
||||
|
||||
// Convert back to flags for backward compatibility with existing probe methods
|
||||
// only_wireguard is preserved from args since it's orthogonal to mode
|
||||
let (_, only_lp_registration, test_lp_wg) = mode_to_flags(test_mode);
|
||||
let only_wireguard = args.only_wireguard;
|
||||
|
||||
let mut trial = if let (Some(entry_node), Some(exit_node)) = (&gateway_node, &exit_gateway_node)
|
||||
{
|
||||
// Both entry and exit gateways provided (for LP telescoping tests)
|
||||
info!("Using both entry and exit gateways for LP forwarding test");
|
||||
nym_gateway_probe::Probe::new_with_gateways(
|
||||
entry,
|
||||
test_point,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
entry_node.clone(),
|
||||
exit_node.clone(),
|
||||
args.socks5_args,
|
||||
)
|
||||
} else if let Some(gw_node) = gateway_node {
|
||||
// Only entry gateway provided
|
||||
nym_gateway_probe::Probe::new_with_gateway(
|
||||
entry,
|
||||
test_point,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
gw_node,
|
||||
args.socks5_args,
|
||||
)
|
||||
} else {
|
||||
// No direct gateways, use directory lookup
|
||||
nym_gateway_probe::Probe::new(
|
||||
entry,
|
||||
test_point,
|
||||
args.netstack_args,
|
||||
args.credential_args,
|
||||
args.socks5_args,
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(awg_args) = args.amnezia_args {
|
||||
trial.with_amnezia(&awg_args);
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(Commands::RunLocal {
|
||||
mnemonic,
|
||||
config_dir,
|
||||
use_mock_ecash,
|
||||
}) => {
|
||||
let config_dir = config_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| Path::new(DEFAULT_CONFIG_DIR).join(&network.network_name));
|
||||
|
||||
if config_dir.is_file() {
|
||||
anyhow::bail!("provided configuration directory is a file");
|
||||
}
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir.clone())?;
|
||||
}
|
||||
|
||||
info!(
|
||||
"using the following directory for the probe config: {}",
|
||||
config_dir.display()
|
||||
);
|
||||
|
||||
Box::pin(trial.probe_run_locally(
|
||||
&config_dir,
|
||||
mnemonic.as_deref(),
|
||||
directory,
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
use_mock_ecash,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
let trial =
|
||||
nym_gateway_probe::Probe::new(entry_details, exit_details, network, probe_config);
|
||||
Box::pin(trial.probe_run(&config_dir, credential_mode)).await
|
||||
}
|
||||
None => {
|
||||
Box::pin(trial.probe(
|
||||
directory,
|
||||
nyxd_url,
|
||||
args.ignore_egress_epoch_role,
|
||||
only_wireguard,
|
||||
only_lp_registration,
|
||||
test_lp_wg,
|
||||
args.min_gateway_mixnet_performance,
|
||||
network,
|
||||
))
|
||||
.await
|
||||
Commands::RunAgent {
|
||||
entry_gateway,
|
||||
credential_args,
|
||||
mut probe_config,
|
||||
} => {
|
||||
let api_url = network
|
||||
.endpoints
|
||||
.first()
|
||||
.and_then(|ep| ep.api_url())
|
||||
.ok_or(anyhow::anyhow!("missing api url"))?;
|
||||
|
||||
let directory = NymApiDirectory::new(api_url).await?;
|
||||
let entry_details = directory
|
||||
.entry_gateway(&entry_gateway)?
|
||||
.to_testable_node()?;
|
||||
|
||||
// Agents run everything
|
||||
probe_config.test_mode = nym_gateway_probe::config::TestMode::All;
|
||||
|
||||
let trial = nym_gateway_probe::Probe::new(entry_details, None, network, probe_config);
|
||||
Box::pin(trial.probe_run_agent(credential_args)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-agent"
|
||||
version = "1.1.1"
|
||||
version = "1.1.2"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) async fn run_probe(
|
||||
|
||||
// Run the probe
|
||||
let log = probe.run_and_get_log(
|
||||
&Some(gateway_identity_key.clone()),
|
||||
gateway_identity_key.clone(),
|
||||
probe_extra_args,
|
||||
testrun.ticket_materials,
|
||||
);
|
||||
|
||||
@@ -73,16 +73,15 @@ impl GwProbe {
|
||||
|
||||
pub(crate) fn run_and_get_log(
|
||||
&self,
|
||||
gateway_key: &Option<String>,
|
||||
gateway_key: String,
|
||||
probe_extra_args: &Vec<String>,
|
||||
ticket_materials: AttachedTicketMaterials,
|
||||
) -> String {
|
||||
let mut command = std::process::Command::new(&self.path);
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
|
||||
if let Some(gateway_id) = gateway_key {
|
||||
command.arg("--gateway").arg(gateway_id);
|
||||
}
|
||||
command.arg("run-agent");
|
||||
command.arg("--gateway").arg(gateway_key);
|
||||
|
||||
tracing::info!("Extra args for the probe:");
|
||||
for arg in probe_extra_args {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.0.14"
|
||||
version = "4.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) fn routes() -> Router<AppState> {
|
||||
"/exit/countries",
|
||||
axum::routing::get(get_entry_gateway_countries),
|
||||
)
|
||||
.route("/exit/ips", axum::routing::get(get_exit_gateway_ips))
|
||||
.route(
|
||||
"/exit/country/:two_letter_country_code",
|
||||
axum::routing::get(get_exit_gateways_by_country),
|
||||
@@ -70,6 +71,27 @@ pub async fn get_entry_gateway_countries(state: State<AppState>) -> HttpResult<J
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
path = "/exit/ips",
|
||||
summary = "Gets IP addresses of all exit gateways currently on the network",
|
||||
context_path = "/dvpn/v1/directory/gateways",
|
||||
operation_id = "getExitGatewayIps",
|
||||
responses(
|
||||
(status = 200, body = Vec<String>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state))]
|
||||
pub async fn get_exit_gateway_ips(state: State<AppState>) -> HttpResult<Json<Vec<String>>> {
|
||||
Ok(Json(
|
||||
state
|
||||
.cache()
|
||||
.get_exit_gateway_ips(state.db_pool(), &MIN_SUPPORTED_VERSION)
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
|
||||
@@ -9,10 +9,10 @@ use nym_node_status_client::auth::VerifiableRequest;
|
||||
use nym_validator_client::nym_api::SkimmedNode;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::{collections::HashMap, net::IpAddr, sync::Arc, time::Duration};
|
||||
use time::UtcDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{error, instrument, warn};
|
||||
use tracing::{error, instrument, trace, warn};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use super::models::SessionStats;
|
||||
@@ -153,6 +153,7 @@ impl AppState {
|
||||
|
||||
static GATEWAYS_LIST_KEY: &str = "gateways";
|
||||
static DVPN_GATEWAYS_LIST_KEY: &str = "dvpn_gateways";
|
||||
static DVPN_EXIT_GATEWAY_IPS: &str = "dvpn_exit_gateway_ips";
|
||||
static NYM_NODES_LIST_KEY: &str = "nym_nodes";
|
||||
static MIXSTATS_LIST_KEY: &str = "mixstats";
|
||||
static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history";
|
||||
@@ -164,6 +165,7 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30;
|
||||
pub(crate) struct HttpCache {
|
||||
gateways: Cache<String, Arc<RwLock<Vec<Gateway>>>>,
|
||||
dvpn_gateways: Cache<String, Arc<RwLock<Vec<DVpnGateway>>>>,
|
||||
exit_gateway_ips: Cache<String, Arc<RwLock<Vec<String>>>>,
|
||||
nym_nodes: Cache<String, Arc<RwLock<Vec<ExtendedNymNode>>>>,
|
||||
mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>,
|
||||
history: Cache<String, Arc<RwLock<Vec<SummaryHistory>>>>,
|
||||
@@ -174,27 +176,31 @@ impl HttpCache {
|
||||
pub async fn new(ttl_seconds: u64) -> Self {
|
||||
HttpCache {
|
||||
gateways: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
dvpn_gateways: Cache::builder()
|
||||
.max_capacity(6)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
exit_gateway_ips: Cache::builder()
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
nym_nodes: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
mixstats: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
history: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
session_stats: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.max_capacity(1)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
}
|
||||
@@ -436,6 +442,63 @@ impl HttpCache {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn get_exit_gateway_ips(
|
||||
&self,
|
||||
db: &DbPool,
|
||||
min_node_version: &Version,
|
||||
) -> Vec<String> {
|
||||
match self.exit_gateway_ips.get(DVPN_EXIT_GATEWAY_IPS).await {
|
||||
Some(guard) => {
|
||||
let read_lock = guard.read().await;
|
||||
read_lock.clone()
|
||||
}
|
||||
None => {
|
||||
trace!("No exit gateway IPs in cache, refreshing...");
|
||||
|
||||
let ips: Vec<String> = self
|
||||
.get_dvpn_gateway_list(db, min_node_version)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|gw| {
|
||||
if gw.can_route_exit() {
|
||||
Some(gw.ip_addresses)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.filter(IpAddr::is_ipv4)
|
||||
.map(|ip| ip.to_string())
|
||||
.sorted()
|
||||
.dedup()
|
||||
.collect();
|
||||
|
||||
self.upsert_exit_gateway_ips(ips.clone()).await;
|
||||
|
||||
ips
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_exit_gateway_ips(
|
||||
&self,
|
||||
ip_list: Vec<String>,
|
||||
) -> Entry<String, Arc<RwLock<Vec<String>>>> {
|
||||
self.exit_gateway_ips
|
||||
.entry_by_ref(DVPN_EXIT_GATEWAY_IPS)
|
||||
.and_upsert_with(|maybe_entry| async {
|
||||
if let Some(entry) = maybe_entry {
|
||||
let v = entry.into_value();
|
||||
let mut guard = v.write().await;
|
||||
*guard = ip_list;
|
||||
v.clone()
|
||||
} else {
|
||||
Arc::new(RwLock::new(ip_list))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upsert_nym_node_list(
|
||||
&self,
|
||||
nym_node_list: Vec<ExtendedNymNode>,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.24.0"
|
||||
version = "1.25.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -19,6 +21,12 @@ pub struct LewesProtocol {
|
||||
/// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP
|
||||
pub data_port: u16,
|
||||
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
/// LP public key
|
||||
pub x25519: x25519::PublicKey,
|
||||
|
||||
/// Digests of the KEM keys available to this node alongside hashing algorithms used
|
||||
/// for their computation.
|
||||
/// note: digests are hex encoded
|
||||
@@ -35,6 +43,7 @@ impl LewesProtocol {
|
||||
enabled: bool,
|
||||
control_port: u16,
|
||||
data_port: u16,
|
||||
x25519: x25519::PublicKey,
|
||||
kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, String>>,
|
||||
signing_keys: HashMap<LPSignatureScheme, HashMap<LPHashFunction, String>>,
|
||||
) -> Self {
|
||||
@@ -42,6 +51,7 @@ impl LewesProtocol {
|
||||
enabled,
|
||||
control_port,
|
||||
data_port,
|
||||
x25519,
|
||||
kem_keys,
|
||||
signing_keys,
|
||||
}
|
||||
|
||||
+31
-14
@@ -673,6 +673,26 @@ impl NymNode {
|
||||
let upgrade_mode_common_state =
|
||||
gateway_tasks_builder.build_upgrade_mode_common_state(upgrade_check_request_sender);
|
||||
|
||||
// Set WireGuard data early so other builders can access it
|
||||
if self.config.wireguard.enabled {
|
||||
let Some(wg_data) = self.wireguard.take() else {
|
||||
return Err(NymNodeError::WireguardDataUnavailable);
|
||||
};
|
||||
gateway_tasks_builder.set_wireguard_data(wg_data.into());
|
||||
}
|
||||
|
||||
let wg_peer_registrator = gateway_tasks_builder
|
||||
.build_peer_registrator(upgrade_mode_common_state.clone())
|
||||
.await?;
|
||||
|
||||
if let Some(wg_peer_registrator) = wg_peer_registrator.as_ref() {
|
||||
let cleanup_task = wg_peer_registrator.cleanup_task(self.shutdown_token());
|
||||
self.shutdown_tracker().try_spawn_named(
|
||||
async move { cleanup_task.run().await },
|
||||
"StaleRegistrationRemover",
|
||||
);
|
||||
};
|
||||
|
||||
// if we're running in entry mode, start the websocket
|
||||
if self.modes().entry {
|
||||
info!(
|
||||
@@ -688,15 +708,6 @@ impl NymNode {
|
||||
self.shutdown_tracker()
|
||||
.try_spawn_named(async move { websocket.run().await }, "EntryWebsocket");
|
||||
|
||||
// Set WireGuard data early so LP listener can access it
|
||||
// (LP listener needs wg_peer_controller for dVPN registrations)
|
||||
if self.config.wireguard.enabled {
|
||||
let Some(wg_data) = self.wireguard.take() else {
|
||||
return Err(NymNodeError::WireguardDataUnavailable);
|
||||
};
|
||||
gateway_tasks_builder.set_wireguard_data(wg_data.into());
|
||||
}
|
||||
|
||||
// Start LP listener if enabled
|
||||
info!(
|
||||
"starting the LP listener on {} (data handler on: {})",
|
||||
@@ -704,10 +715,7 @@ impl NymNode {
|
||||
self.config.gateway_tasks.lp.data_bind_address,
|
||||
);
|
||||
let mut lp_listener = gateway_tasks_builder
|
||||
.build_lp_listener(
|
||||
upgrade_mode_common_state.clone(),
|
||||
active_clients_store.clone(),
|
||||
)
|
||||
.build_lp_listener(wg_peer_registrator.clone(), active_clients_store.clone())
|
||||
.await?;
|
||||
self.shutdown_tracker()
|
||||
.try_spawn_named(async move { lp_listener.run().await }, "LpListener");
|
||||
@@ -747,8 +755,16 @@ impl NymNode {
|
||||
|
||||
gateway_tasks_builder.set_authenticator_opts(config.auth_opts);
|
||||
|
||||
let Some(peer_registrator) = wg_peer_registrator else {
|
||||
return Err(NymNodeError::WireguardDataUnavailable);
|
||||
};
|
||||
|
||||
let authenticator = gateway_tasks_builder
|
||||
.build_wireguard_authenticator(upgrade_mode_common_state.clone(), topology_provider)
|
||||
.build_wireguard_authenticator(
|
||||
peer_registrator,
|
||||
upgrade_mode_common_state.clone(),
|
||||
topology_provider,
|
||||
)
|
||||
.await?;
|
||||
let started_authenticator = authenticator.start_service_provider().await?;
|
||||
active_clients_store.insert_embedded(started_authenticator.handle);
|
||||
@@ -890,6 +906,7 @@ impl NymNode {
|
||||
self.modes().entry,
|
||||
self.config.gateway_tasks.lp.announced_control_port(),
|
||||
self.config.gateway_tasks.lp.announced_data_port(),
|
||||
*self.x25519_noise_keys.public_key(),
|
||||
self.compute_kem_key_hashes(),
|
||||
self.compute_signing_key_hashes(),
|
||||
);
|
||||
|
||||
@@ -17,8 +17,8 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::config::RegistrationMode;
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::{LpRegistrationConfig, config::RegistrationMode};
|
||||
use crate::{config::RegistrationClientConfig, error::RegistrationClientError};
|
||||
|
||||
const VPN_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15);
|
||||
const MIXNET_CLIENT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
@@ -31,19 +31,31 @@ pub struct NymNodeWithKeys {
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct BuilderConfig {
|
||||
// For future reference
|
||||
// Common options
|
||||
pub entry_node: NymNodeWithKeys,
|
||||
pub exit_node: NymNodeWithKeys,
|
||||
pub data_path: Option<PathBuf>,
|
||||
pub mode: RegistrationMode,
|
||||
pub cancel_token: CancellationToken,
|
||||
|
||||
// Toggle
|
||||
#[builder(default)]
|
||||
pub enable_lp_regitration: bool,
|
||||
|
||||
// Mixnet based only option
|
||||
pub mixnet_client_config: MixnetClientConfig,
|
||||
#[builder(default = MIXNET_CLIENT_STARTUP_TIMEOUT)]
|
||||
pub mixnet_client_startup_timeout: Duration,
|
||||
pub mode: RegistrationMode,
|
||||
pub user_agent: UserAgent,
|
||||
pub custom_topology_provider: Box<dyn TopologyProvider + Send + Sync>,
|
||||
pub network_env: NymNetworkDetails,
|
||||
pub cancel_token: CancellationToken,
|
||||
#[cfg(unix)]
|
||||
pub connection_fd_callback: Arc<dyn Fn(RawFd) + Send + Sync>,
|
||||
|
||||
// LP based only option
|
||||
#[builder(default)]
|
||||
pub lp_registration_config: LpRegistrationConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
@@ -75,29 +87,36 @@ impl BuilderConfig {
|
||||
// Mixnet mode uses 5-hop configuration
|
||||
RegistrationMode::Mixnet => mixnet_debug_config(&self.mixnet_client_config),
|
||||
// Wireguard and LP both use 2-hop configuration
|
||||
RegistrationMode::Wireguard | RegistrationMode::Lp => {
|
||||
two_hop_debug_config(&self.mixnet_client_config)
|
||||
}
|
||||
RegistrationMode::Wireguard => two_hop_debug_config(&self.mixnet_client_config),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_storage(
|
||||
pub fn registration_client_config(&self) -> RegistrationClientConfig {
|
||||
RegistrationClientConfig {
|
||||
entry: self.entry_node.clone(),
|
||||
exit: self.exit_node.clone(),
|
||||
mode: self.mode,
|
||||
lp_registration_config: self.lp_registration_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_mixnet_client_storage(
|
||||
&self,
|
||||
) -> Result<Option<(OnDiskPersistent, PersistentStorage)>, RegistrationClientError> {
|
||||
if let Some(path) = &self.data_path {
|
||||
tracing::debug!("Using custom key storage path: {}", path.display());
|
||||
|
||||
let storage_paths = StoragePaths::new_from_dir(path)
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
let mixnet_client_storage = storage_paths
|
||||
.initialise_persistent_storage(&self.mixnet_client_debug_config())
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
let credential_storage = storage_paths
|
||||
.persistent_credential_storage()
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))?;
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
Ok(Some((mixnet_client_storage, credential_storage)))
|
||||
} else {
|
||||
@@ -105,6 +124,26 @@ impl BuilderConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_credential_storage(
|
||||
&self,
|
||||
) -> Result<Option<PersistentStorage>, RegistrationClientError> {
|
||||
if let Some(path) = &self.data_path {
|
||||
tracing::debug!("Using custom credential storage path: {}", path.display());
|
||||
|
||||
let storage_paths = StoragePaths::new_from_dir(path)
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
let credential_storage = storage_paths
|
||||
.persistent_credential_storage()
|
||||
.await
|
||||
.map_err(|err| RegistrationClientError::StorageInitialization(Box::new(err)))?;
|
||||
|
||||
Ok(Some(credential_storage))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_and_connect_mixnet_client<S>(
|
||||
self,
|
||||
builder: MixnetClientBuilder<S>,
|
||||
@@ -121,7 +160,7 @@ impl BuilderConfig {
|
||||
let debug_config = self.mixnet_client_debug_config();
|
||||
let remember_me = match self.mode {
|
||||
RegistrationMode::Mixnet => RememberMe::new_mixnet(),
|
||||
RegistrationMode::Wireguard | RegistrationMode::Lp => RememberMe::new_vpn(),
|
||||
RegistrationMode::Wireguard => RememberMe::new_vpn(),
|
||||
};
|
||||
|
||||
let identity = self.entry_node.node.identity.to_string();
|
||||
|
||||
@@ -13,7 +13,12 @@ use nym_validator_client::{
|
||||
nyxd::{Config as NyxdClientConfig, NyxdClient},
|
||||
};
|
||||
|
||||
use crate::{RegistrationClient, config::RegistrationClientConfig, error::RegistrationClientError};
|
||||
use crate::{
|
||||
RegistrationClient,
|
||||
clients::{LpBasedRegistrationClient, MixnetBasedRegistrationClient},
|
||||
config::RegistrationMode,
|
||||
error::RegistrationClientError,
|
||||
};
|
||||
use config::BuilderConfig;
|
||||
|
||||
pub(crate) mod config;
|
||||
@@ -27,13 +32,38 @@ impl RegistrationClientBuilder {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn use_lp(&self) -> bool {
|
||||
let lp_enabled = self.config.enable_lp_regitration;
|
||||
let lp_info_available = self.config.entry_node.node.lp_data.is_some()
|
||||
&& self.config.exit_node.node.lp_data.is_some();
|
||||
// To remove when LP supports Mixnet registration
|
||||
let wireguard_mode = self.config.mode == RegistrationMode::Wireguard;
|
||||
let use_lp = lp_enabled && lp_info_available && wireguard_mode;
|
||||
if !use_lp && lp_enabled {
|
||||
tracing::warn!(
|
||||
"LP is enabled but can't be used: Missing LP information: {lp_info_available}, wireguard mode: {wireguard_mode}"
|
||||
);
|
||||
}
|
||||
use_lp
|
||||
}
|
||||
|
||||
pub async fn build(self) -> Result<RegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_storage().await?;
|
||||
let config = RegistrationClientConfig {
|
||||
entry: self.config.entry_node.clone(),
|
||||
exit: self.config.exit_node.clone(),
|
||||
mode: self.config.mode,
|
||||
};
|
||||
if self.use_lp() {
|
||||
tracing::debug!("Using LP for registration");
|
||||
Ok(RegistrationClient::Lp(Box::new(self.build_lp().await?)))
|
||||
} else {
|
||||
tracing::debug!("Using Mixnet for registration");
|
||||
Ok(RegistrationClient::Mixnet(Box::new(
|
||||
self.build_mixnet().await?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_mixnet(
|
||||
self,
|
||||
) -> Result<MixnetBasedRegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_mixnet_client_storage().await?;
|
||||
let config = self.config.registration_client_config();
|
||||
let cancel_token = self.config.cancel_token.clone();
|
||||
let (event_tx, event_rx) = mpsc::unbounded();
|
||||
|
||||
@@ -83,7 +113,7 @@ impl RegistrationClientBuilder {
|
||||
};
|
||||
let mixnet_client_address = *mixnet_client.nym_address();
|
||||
|
||||
Ok(RegistrationClient {
|
||||
Ok(MixnetBasedRegistrationClient {
|
||||
mixnet_client,
|
||||
config,
|
||||
cancel_token,
|
||||
@@ -92,6 +122,30 @@ impl RegistrationClientBuilder {
|
||||
event_rx,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_lp(self) -> Result<LpBasedRegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_credential_storage().await?;
|
||||
let config = self.config.registration_client_config();
|
||||
|
||||
let nyxd_client = get_nyxd_client(&self.config.network_env)?;
|
||||
|
||||
let bandwidth_controller: Box<dyn BandwidthTicketProvider> =
|
||||
if let Some(credential_storage) = storage {
|
||||
Box::new(BandwidthController::new(credential_storage, nyxd_client))
|
||||
} else {
|
||||
Box::new(BandwidthController::new(
|
||||
EphemeralCredentialStorage::default(),
|
||||
nyxd_client,
|
||||
))
|
||||
};
|
||||
|
||||
Ok(LpBasedRegistrationClient {
|
||||
config,
|
||||
bandwidth_controller,
|
||||
cancel_token: self.config.cancel_token.clone(),
|
||||
fallback_client_builder: Some(self),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// temporary while we use the legacy bandwidth-controller
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::builder::RegistrationClientBuilder;
|
||||
use crate::config::RegistrationClientConfig;
|
||||
use crate::config::RegistrationMode;
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::lp_client::helpers::to_lp_remote_peer;
|
||||
use crate::lp_client::{LpRegistrationClient, NestedLpSession};
|
||||
use crate::types::{LpRegistrationResult, RegistrationResult};
|
||||
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct LpBasedRegistrationClient {
|
||||
pub(crate) config: RegistrationClientConfig,
|
||||
pub(crate) bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
// While we allow a fallback, we need to be able to build it
|
||||
pub(crate) fallback_client_builder: Option<RegistrationClientBuilder>,
|
||||
}
|
||||
|
||||
impl LpBasedRegistrationClient {
|
||||
// create dedicated method taking RNG instance for tests
|
||||
async fn register_wg_with_rng<R>(
|
||||
self,
|
||||
rng: &mut R,
|
||||
) -> Result<RegistrationResult, RegistrationClientError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
// Extract and validate LP data
|
||||
let entry_lp_data = self.config.entry.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_lp_data = self.config.exit.node.lp_data.ok_or(
|
||||
RegistrationClientError::LpRegistrationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let entry_lp_protocol = entry_lp_data.lp_protocol_version;
|
||||
let exit_lp_protocol = exit_lp_data.lp_protocol_version;
|
||||
|
||||
let entry_address = entry_lp_data.address;
|
||||
let exit_address = exit_lp_data.address;
|
||||
|
||||
tracing::debug!("Entry gateway LP address: {entry_address}");
|
||||
tracing::debug!("Exit gateway LP address: {exit_address}");
|
||||
|
||||
// Generate fresh Ed25519 keypairs for LP registration
|
||||
// These are ephemeral and used only for the LP handshake protocol
|
||||
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng));
|
||||
|
||||
let entry_peer = to_lp_remote_peer(self.config.entry.node.identity, entry_lp_data);
|
||||
let exit_peer = to_lp_remote_peer(self.config.exit.node.identity, exit_lp_data);
|
||||
|
||||
// STEP 1: Establish outer session with entry gateway
|
||||
// This creates the LP session that will be used to forward packets to exit.
|
||||
// Uses packet-per-connection model: each handshake packet on new TCP connection.
|
||||
tracing::info!("Establishing outer session with entry gateway");
|
||||
let mut entry_client = LpRegistrationClient::new(
|
||||
entry_lp_keypair.clone(),
|
||||
entry_peer,
|
||||
entry_address,
|
||||
entry_lp_protocol,
|
||||
self.config.lp_registration_config,
|
||||
);
|
||||
|
||||
// Perform handshake with entry gateway (outer session now established)
|
||||
entry_client.perform_handshake().await.map_err(|source| {
|
||||
RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!("Outer session with entry gateway established");
|
||||
|
||||
// STEP 2: Use nested session to register with exit gateway via forwarding
|
||||
// This hides the client's IP address from the exit gateway
|
||||
tracing::info!("Registering with exit gateway via entry forwarding");
|
||||
let mut nested_session =
|
||||
NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_protocol);
|
||||
|
||||
// Perform handshake and registration with exit gateway (all via entry forwarding)
|
||||
let exit_gateway_data = nested_session
|
||||
.handshake_and_register_dvpn::<TcpStream, _>(
|
||||
&mut entry_client,
|
||||
rng,
|
||||
&self.config.exit.keys,
|
||||
&self.config.exit.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardExit,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::ExitGatewayRegisterLp {
|
||||
gateway_id: self.config.exit.node.identity.to_base58_string(),
|
||||
lp_address: exit_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Exit gateway registration completed via forwarding");
|
||||
|
||||
// STEP 3: Register with entry gateway (packet-per-connection)
|
||||
tracing::info!("Registering with entry gateway");
|
||||
let entry_gateway_data = entry_client
|
||||
.register_dvpn(
|
||||
rng,
|
||||
&self.config.entry.keys,
|
||||
&self.config.entry.node.identity,
|
||||
&*self.bandwidth_controller,
|
||||
TicketType::V1WireguardEntry,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| RegistrationClientError::EntryGatewayRegisterLp {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
lp_address: entry_address,
|
||||
source: Box::new(source),
|
||||
})?;
|
||||
|
||||
tracing::info!("Entry gateway registration successful");
|
||||
|
||||
tracing::info!("LP registration successful for both gateways");
|
||||
|
||||
// LP is registration-only (packet-per-connection model).
|
||||
// All data flows through WireGuard after this point.
|
||||
// Each LP packet used its own TCP connection which was closed after the exchange.
|
||||
// Exit registration was completed via forwarding through entry gateway.
|
||||
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
|
||||
entry_gateway_data,
|
||||
exit_gateway_data,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
|
||||
self.register_wg_with_rng(&mut rng).await
|
||||
}
|
||||
|
||||
pub(crate) async fn register(mut self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let fallback = self.fallback_client_builder.take();
|
||||
match &self.config.mode {
|
||||
RegistrationMode::Mixnet => {
|
||||
if let Some(fallback) = fallback {
|
||||
register_with_fallback(fallback).await
|
||||
} else {
|
||||
Err(RegistrationClientError::UnsupportedMode)
|
||||
}
|
||||
}
|
||||
RegistrationMode::Wireguard => {
|
||||
let lp_registration_result = self
|
||||
.cancel_token
|
||||
.clone()
|
||||
.run_until_cancelled(self.register_wg())
|
||||
.await;
|
||||
match lp_registration_result {
|
||||
// Everything went fine
|
||||
Some(Ok(res)) => Ok(res),
|
||||
|
||||
// LP reg failed, try fallback if we have one
|
||||
Some(Err(e)) => {
|
||||
tracing::error!("LP registration failed : {e}");
|
||||
if let Some(fallback) = fallback {
|
||||
tracing::info!("Registering with fallback");
|
||||
register_with_fallback(fallback).await
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancelled registration
|
||||
None => Err(RegistrationClientError::Cancelled),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_with_fallback(
|
||||
client_builder: RegistrationClientBuilder,
|
||||
) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
// This is forcefully building a mixnet based client
|
||||
let fallback_client = client_builder.build_mixnet().await?;
|
||||
fallback_client.register().await
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::RegistrationClientConfig;
|
||||
use crate::config::RegistrationMode;
|
||||
use crate::error::RegistrationClientError;
|
||||
use crate::types::{MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult};
|
||||
use nym_authenticator_client::AuthClientMixnetListenerHandle;
|
||||
use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_registration_common::AssignedAddresses;
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub struct MixnetBasedRegistrationClient {
|
||||
pub(crate) mixnet_client: MixnetClient,
|
||||
pub(crate) config: RegistrationClientConfig,
|
||||
pub(crate) mixnet_client_address: Recipient,
|
||||
pub(crate) bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
pub(crate) cancel_token: CancellationToken,
|
||||
pub(crate) event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
enum MixnetClientHandle {
|
||||
Authenticator(AuthClientMixnetListenerHandle),
|
||||
Sdk(Box<MixnetClient>),
|
||||
}
|
||||
|
||||
impl MixnetClientHandle {
|
||||
async fn stop(self) {
|
||||
match self {
|
||||
Self::Authenticator(handle) => handle.stop().await,
|
||||
Self::Sdk(handle) => handle.disconnect().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bundle of an actual error and the underlying mixnet client so it can be shutdown correctly if needed
|
||||
struct RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle,
|
||||
source: crate::RegistrationClientError,
|
||||
}
|
||||
|
||||
impl MixnetBasedRegistrationClient {
|
||||
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationError> {
|
||||
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
|
||||
|
||||
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
|
||||
|
||||
let Some(ipr_address) = self.config.exit.node.ipr_address else {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::NoIpPacketRouterAddress {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let mut ipr_client =
|
||||
IprClientConnect::new(self.mixnet_client, self.cancel_token.child_token());
|
||||
|
||||
let interface_addresses = match self
|
||||
.cancel_token
|
||||
.run_until_cancelled(ipr_client.connect(ipr_address))
|
||||
.await
|
||||
{
|
||||
Some(Ok(addr)) => addr,
|
||||
Some(Err(e)) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(
|
||||
ipr_client.into_mixnet_client(),
|
||||
)),
|
||||
source: RegistrationClientError::ConnectToIpPacketRouter(e),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(
|
||||
ipr_client.into_mixnet_client(),
|
||||
)),
|
||||
source: RegistrationClientError::Cancelled,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RegistrationResult::Mixnet(Box::new(
|
||||
MixnetRegistrationResult {
|
||||
mixnet_client: ipr_client.into_mixnet_client(),
|
||||
assigned_addresses: AssignedAddresses {
|
||||
interface_addresses,
|
||||
exit_mix_address: ipr_address,
|
||||
mixnet_client_address: self.mixnet_client_address,
|
||||
entry_mixnet_gateway_ip,
|
||||
exit_mixnet_gateway_ip,
|
||||
},
|
||||
event_rx: self.event_rx,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationError> {
|
||||
let Some(entry_auth_address) = self.config.entry.node.authenticator_address else {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let Some(exit_auth_address) = self.config.exit.node.authenticator_address else {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Sdk(Box::new(self.mixnet_client)),
|
||||
source: RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let entry_version = self.config.entry.node.version;
|
||||
tracing::debug!("Entry gateway version: {entry_version}");
|
||||
let exit_version = self.config.exit.node.version;
|
||||
tracing::debug!("Exit gateway version: {exit_version}");
|
||||
|
||||
// Start the auth client mixnet listener, which will listen for incoming messages from the
|
||||
// mixnet and rebroadcast them to the auth clients.
|
||||
let mixnet_listener =
|
||||
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.child_token())
|
||||
.start();
|
||||
|
||||
let mut entry_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
entry_auth_address,
|
||||
entry_version,
|
||||
self.config.entry.keys,
|
||||
self.config.entry.node.ip_address,
|
||||
);
|
||||
|
||||
let mut exit_auth_client = AuthenticatorClient::new(
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
exit_auth_address,
|
||||
exit_version,
|
||||
self.config.exit.keys,
|
||||
self.config.exit.node.ip_address,
|
||||
);
|
||||
|
||||
let entry_fut = entry_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardEntry);
|
||||
let exit_fut = exit_auth_client
|
||||
.register_wireguard(&*self.bandwidth_controller, TicketType::V1WireguardExit);
|
||||
|
||||
let (entry, exit) = match Box::pin(
|
||||
self.cancel_token
|
||||
.run_until_cancelled(async { tokio::join!(entry_fut, exit_fut) }),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some((entry, exit)) => (entry, exit),
|
||||
None => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
source: RegistrationClientError::Cancelled,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(source) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
source: RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.entry.node.identity.to_base58_string(),
|
||||
entry_auth_address,
|
||||
true,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let exit = match exit {
|
||||
Ok(exit) => exit,
|
||||
Err(source) => {
|
||||
return Err(RegistrationError {
|
||||
mixnet_client_handle: MixnetClientHandle::Authenticator(mixnet_listener),
|
||||
source: RegistrationClientError::from_authenticator_error(
|
||||
source,
|
||||
self.config.exit.node.identity.to_base58_string(),
|
||||
exit_auth_address,
|
||||
false,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(RegistrationResult::Wireguard(Box::new(
|
||||
WireguardRegistrationResult {
|
||||
entry_gateway_client: entry_auth_client,
|
||||
exit_gateway_client: exit_auth_client,
|
||||
entry_gateway_data: entry,
|
||||
exit_gateway_data: exit,
|
||||
authenticator_listener_handle: mixnet_listener,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn register(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let registration_result = match self.config.mode {
|
||||
RegistrationMode::Mixnet => self.register_mix_exit().await,
|
||||
RegistrationMode::Wireguard => self.register_wg().await,
|
||||
};
|
||||
|
||||
// If we failed to register, shut down the mixnet client and wait for it to exit
|
||||
match registration_result {
|
||||
Ok(result) => Ok(result),
|
||||
Err(error) => {
|
||||
tracing::debug!("Registration failed");
|
||||
tracing::debug!("Shutting down mixnet client");
|
||||
error.mixnet_client_handle.stop().await;
|
||||
tracing::debug!("Mixnet client stopped");
|
||||
Err(error.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod lp;
|
||||
pub(crate) mod mixnet;
|
||||
|
||||
pub use lp::LpBasedRegistrationClient;
|
||||
pub use mixnet::MixnetBasedRegistrationClient;
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::LpRegistrationConfig;
|
||||
|
||||
use crate::builder::config::NymNodeWithKeys;
|
||||
|
||||
/// Registration mode for the client
|
||||
@@ -8,26 +10,13 @@ use crate::builder::config::NymNodeWithKeys;
|
||||
pub enum RegistrationMode {
|
||||
/// 5-hop mixnet with IPR (IP Packet Router)
|
||||
Mixnet,
|
||||
/// 2-hop WireGuard with authenticator
|
||||
/// 2-hop WireGuard
|
||||
Wireguard,
|
||||
/// 2-hop WireGuard with LP (Lewes Protocol)
|
||||
Lp,
|
||||
}
|
||||
|
||||
impl RegistrationMode {
|
||||
/// Legacy method for backward compatibility
|
||||
#[deprecated(note = "use explicit enum variant instead")]
|
||||
pub fn legacy_two_hop(use_two_hop: bool) -> RegistrationMode {
|
||||
if use_two_hop {
|
||||
Self::Wireguard
|
||||
} else {
|
||||
Self::Mixnet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegistrationClientConfig {
|
||||
pub(crate) entry: NymNodeWithKeys,
|
||||
pub(crate) exit: NymNodeWithKeys,
|
||||
pub(crate) mode: RegistrationMode,
|
||||
pub(crate) lp_registration_config: LpRegistrationConfig,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user