Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b9fdc918f | |||
| 3c12181da3 | |||
| 801fa8676a | |||
| 9416221361 | |||
| 27e1f8125c | |||
| 5d7f3402c7 | |||
| 2d73ea5c82 | |||
| b8d8ee6109 | |||
| a779b7a266 | |||
| b2d7b54f34 | |||
| d93d25ebae | |||
| ae0ab69bd2 | |||
| 4897cb0ce4 | |||
| 46b9d5374b | |||
| e7fcaa980f | |||
| 5fc2936d3f | |||
| 3d59a72ee8 | |||
| bb694855d5 | |||
| 9cb2655e7d | |||
| 0c3efe67fb | |||
| ca5ad94420 | |||
| 220c64100d | |||
| 505a19e32f | |||
| b75839461b | |||
| 4e4e0df721 | |||
| c3520b575f | |||
| c7a466860e | |||
| 956df22d86 | |||
| 0ca122c56b | |||
| 492eb22d74 | |||
| 9513eb458b | |||
| 8bca0698ee | |||
| 8e278866c7 | |||
| ff93657609 | |||
| d46e967b5b | |||
| 1219dcf874 | |||
| a7068ea421 | |||
| 5dc6546f1c | |||
| 5f2bc60c2c | |||
| 195c75d293 | |||
| f9827f5dd4 | |||
| b92dd2f264 | |||
| 8e792b7b93 | |||
| 061840c47c | |||
| 93834bcf28 | |||
| 89ab2630cd | |||
| fd47ebfad0 | |||
| b0d01ec12a | |||
| 470282612b | |||
| bb24b5e91d | |||
| 4222a7b684 | |||
| 73bc746cd6 | |||
| 32402d64e8 | |||
| 681b0d17b5 | |||
| 068ee7d2b7 | |||
| 39cfd532a8 | |||
| 613d496133 | |||
| 1ecb457c66 | |||
| 49faa13855 | |||
| 51e5e7825d | |||
| ded23a6271 | |||
| 801dcdda1e | |||
| e2d29f184d | |||
| a151a03181 | |||
| b19e82d4f7 | |||
| 88a4633bc4 | |||
| 660eff45dc | |||
| d4882ca276 | |||
| cfcf804b47 | |||
| b6d22abc01 | |||
| bd755385ed | |||
| 940fb09ae4 | |||
| 47af0b24f0 | |||
| 52edfdcc2f | |||
| af04afbe5e | |||
| 63f158cccb | |||
| b4aee7a1d9 | |||
| c55b215b65 | |||
| 7e8faf0ec6 | |||
| 0082b9fc50 | |||
| e16a337354 | |||
| cd0881462b | |||
| 8916b021a9 | |||
| dccdde108c | |||
| 9d661e7a7b | |||
| 76ce1bc0f9 | |||
| d3648f13c5 | |||
| 9a931b9251 | |||
| f4ba8ac2b3 | |||
| c274cc588d | |||
| 7dd1dd1a6c | |||
| 982786b678 | |||
| 561182ce6b | |||
| f4b59158df | |||
| 944b4f5aad | |||
| 6e62e34ac8 | |||
| 18e72c90df | |||
| fd051540aa |
@@ -3,4 +3,5 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/target
|
**/target
|
||||||
|
target-otel
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -3,13 +3,28 @@ name: ci-build-upload-binaries
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
feature_profile:
|
||||||
|
description: "Select a predefined cargo feature profile"
|
||||||
|
required: false
|
||||||
|
default: "none"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- none
|
||||||
|
- tokio-console
|
||||||
|
- otel
|
||||||
|
- otel,tokio-console
|
||||||
|
extra_features:
|
||||||
|
description: "Additional comma-separated cargo features (e.g. feat1,feat2)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
add_tokio_unstable:
|
add_tokio_unstable:
|
||||||
description: 'True to add RUSTFLAGS="--cfg tokio_unstable"'
|
description: 'Force RUSTFLAGS="--cfg tokio_unstable" (auto-set when tokio-console is selected)'
|
||||||
required: true
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
enable_deb:
|
enable_deb:
|
||||||
description: "True to enable cargo-deb installation and .deb package building"
|
description: "Enable cargo-deb installation and .deb package building"
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -21,7 +36,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [ arc-linux-latest ]
|
platform: [arc-linux-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
env:
|
env:
|
||||||
@@ -36,38 +51,62 @@ jobs:
|
|||||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
rm -rf ci-builds || true
|
rm -rf ci-builds || true
|
||||||
mkdir -p $OUTPUT_DIR
|
mkdir -p "$OUTPUT_DIR"
|
||||||
echo $OUTPUT_DIR
|
echo "$OUTPUT_DIR"
|
||||||
|
|
||||||
- name: Install Dependencies (Linux)
|
- name: Install Dependencies (Linux)
|
||||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||||
|
|
||||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
- name: Resolve cargo features and RUSTFLAGS
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
if: github.event_name == 'workflow_dispatch'
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
|
FEATURES=""
|
||||||
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
|
PROFILE="${{ inputs.feature_profile }}"
|
||||||
|
EXTRA="${{ inputs.extra_features }}"
|
||||||
|
|
||||||
|
if [[ "$PROFILE" != "none" && -n "$PROFILE" ]]; then
|
||||||
|
FEATURES="$PROFILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$EXTRA" ]]; then
|
||||||
|
if [[ -n "$FEATURES" ]]; then
|
||||||
|
FEATURES="${FEATURES},${EXTRA}"
|
||||||
|
else
|
||||||
|
FEATURES="$EXTRA"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$FEATURES" ]]; then
|
||||||
|
echo "CARGO_FEATURES=--features ${FEATURES}" >> "$GITHUB_ENV"
|
||||||
|
echo "::notice::Selected cargo features: $FEATURES"
|
||||||
|
else
|
||||||
|
echo "::notice::No additional cargo features selected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FEATURES" == *"tokio-console"* ]] || [[ "${{ inputs.add_tokio_unstable }}" == "true" ]]; then
|
||||||
|
echo "RUSTFLAGS=--cfg tokio_unstable" >> "$GITHUB_ENV"
|
||||||
|
echo "::notice::Enabled RUSTFLAGS --cfg tokio_unstable"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||||
|
|
||||||
- name: Build all binaries
|
- name: Build all binaries
|
||||||
uses: actions-rs/cargo@v1
|
shell: bash
|
||||||
with:
|
run: cargo build --workspace --release ${{ env.CARGO_FEATURES }}
|
||||||
command: build
|
|
||||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
|
||||||
|
|
||||||
- name: Install cargo-deb
|
- name: Install cargo-deb
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-deb
|
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
||||||
|
shell: bash
|
||||||
|
run: cargo install cargo-deb
|
||||||
|
|
||||||
- name: Build deb packages
|
- name: Build deb packages
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
||||||
shell: bash
|
shell: bash
|
||||||
run: make deb
|
run: make deb
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
@@ -84,24 +123,22 @@ jobs:
|
|||||||
target/release/nym-node
|
target/release/nym-node
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
# If this was a pull_request or nightly, upload to build server
|
|
||||||
|
|
||||||
- name: Prepare build output
|
- name: Prepare build output
|
||||||
# if: github.event_name == 'schedule' || github.event_name == 'pull_request'
|
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
cp target/release/nym-client $OUTPUT_DIR
|
cp target/release/nym-client "$OUTPUT_DIR"
|
||||||
cp target/release/nym-socks5-client $OUTPUT_DIR
|
cp target/release/nym-socks5-client "$OUTPUT_DIR"
|
||||||
cp target/release/nym-api $OUTPUT_DIR
|
cp target/release/nym-api "$OUTPUT_DIR"
|
||||||
cp target/release/nym-network-requester $OUTPUT_DIR
|
cp target/release/nym-network-requester "$OUTPUT_DIR"
|
||||||
cp target/release/nymvisor $OUTPUT_DIR
|
cp target/release/nymvisor "$OUTPUT_DIR"
|
||||||
cp target/release/nym-node $OUTPUT_DIR
|
cp target/release/nym-node "$OUTPUT_DIR"
|
||||||
cp target/release/nym-cli $OUTPUT_DIR
|
cp target/release/nym-cli "$OUTPUT_DIR"
|
||||||
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.enable_deb }}" == "true" ]]; then
|
||||||
cp target/debian/*.deb $OUTPUT_DIR
|
cp target/debian/*.deb "$OUTPUT_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Deploy branch to CI www
|
- name: Deploy branch to CI www
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: easingthemes/ssh-deploy@main
|
uses: easingthemes/ssh-deploy@main
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ on:
|
|||||||
- 'nym-api/**'
|
- 'nym-api/**'
|
||||||
- 'nym-authenticator-client/**'
|
- 'nym-authenticator-client/**'
|
||||||
- 'nym-credential-proxy/**'
|
- 'nym-credential-proxy/**'
|
||||||
|
- 'nym-gateway-probe/**'
|
||||||
- 'nym-ip-packet-client/**'
|
- 'nym-ip-packet-client/**'
|
||||||
- 'nym-network-monitor/**'
|
- 'nym-network-monitor/**'
|
||||||
- 'nym-node/**'
|
- 'nym-node/**'
|
||||||
@@ -89,7 +90,7 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: clippy
|
command: clippy
|
||||||
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
|
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
|
||||||
|
|
||||||
- name: Clippy (non-macos)
|
- name: Clippy (non-macos)
|
||||||
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
|
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
|
||||||
@@ -104,6 +105,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
command: build
|
command: build
|
||||||
|
|
||||||
|
# only build on linux because of wg FFI bindings of its dependency (network probe)
|
||||||
|
- name: Build nym-node-status-api (linux only)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: -p nym-node-status-api
|
||||||
|
|
||||||
- name: Build all examples
|
- name: Build all examples
|
||||||
if: contains(matrix.os, 'linux')
|
if: contains(matrix.os, 'linux')
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "nym-node-status-api/**"
|
- "nym-node-status-api/nym-node-status-api/**"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
|
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
|
||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: ci-docs-linkcheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "documentation/docs/**"
|
||||||
|
- ".github/workflows/ci-docs-linkcheck.yml"
|
||||||
|
- "lychee.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linkcheck:
|
||||||
|
runs-on: arc-linux-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Check links
|
||||||
|
uses: lycheeverse/lychee-action@v2
|
||||||
|
with:
|
||||||
|
args: ${{ github.workspace }}/documentation/docs/ --config ${{ github.workspace }}/lychee.toml --root-dir ${{ github.workspace }}/documentation/docs/pages/
|
||||||
|
fail: true
|
||||||
@@ -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
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-container:
|
build-container:
|
||||||
runs-on: arc-ubuntu-22.04-dind
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Harbor
|
- name: Login to Harbor
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
git config --global user.name "Lawrence Stalder"
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
uses: mikefarah/yq@v4.50.1
|
uses: mikefarah/yq@v4.52.2
|
||||||
id: get_version
|
id: get_version
|
||||||
with:
|
with:
|
||||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -67,7 +67,6 @@ nym-api/redocly/formatted-openapi.json
|
|||||||
*.profraw
|
*.profraw
|
||||||
.beads
|
.beads
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
docs
|
|
||||||
.claude
|
.claude
|
||||||
.superego
|
.superego
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,86 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2026.3-parmigiano] (2026-02-10)
|
||||||
|
|
||||||
|
- chore: disable LP on parmigiano branch ([#6422])
|
||||||
|
- revert mixnet-based client fautly changes from LP ([#6420])
|
||||||
|
- [LP fix] Registration client with fallback ([#6419])
|
||||||
|
- Lp/ip pool fixes ([#6412])
|
||||||
|
- [LP-fix] expose wg psk for the vpn-client ([#6411])
|
||||||
|
- LP-fix : configurable LP timeouts ([#6409])
|
||||||
|
- LP-fix : add LP x25519 key to the description ([#6408])
|
||||||
|
- use rng that is Send ([#6404])
|
||||||
|
- use local kem key instead of local x25519 ([#6402])
|
||||||
|
- [LP Gateway Probe] CLI and behavior improvements ([#6400])
|
||||||
|
- lp: attempt to negotiate (and use) protocol version ([#6399])
|
||||||
|
- bugfix: use correct reserved bytes when parsing LpHeader ([#6398])
|
||||||
|
- Lp/bugfix/share ip allocation ([#6395])
|
||||||
|
- feat: use hex-encoding for lp key digests ([#6394])
|
||||||
|
- Add socks5 test to gateway-probe ([#6393])
|
||||||
|
- [LP Gateway probe] Improve file structure ([#6391])
|
||||||
|
- Reduce the size of `HttpClientError` ([#6390])
|
||||||
|
- Lp/two step dvpn reg ([#6386])
|
||||||
|
- Add extra configured nym api url to env ([#6382])
|
||||||
|
- Lp/dvpn psk injection ([#6378])
|
||||||
|
- LP: include signing key digests to LP responses ([#6373])
|
||||||
|
- Lp/use noise x25519 ([#6372])
|
||||||
|
- Topology fallback ([#6363])
|
||||||
|
- NS API socks5 support ([#6361])
|
||||||
|
- LP: modified LPRemotePeer to dynamically choose required KEM key hash ([#6358])
|
||||||
|
- Fix KKT Integration into LP ([#6357])
|
||||||
|
- LP: mixnet reg fixes ([#6356])
|
||||||
|
- LP: announced KEM key hashes ([#6349])
|
||||||
|
- revert faulty drop changes ([#6346])
|
||||||
|
- small qol changes ([#6340])
|
||||||
|
- Apply configured api urls via env ([#6337])
|
||||||
|
- lp chore: make sure to take reserved bytes straight from the header ([#6336])
|
||||||
|
- LP: x25519/ed22519 cleanup round ([#6335])
|
||||||
|
- Lp/encrypted kkt ([#6331])
|
||||||
|
- ensure packets with incompatible versions are rejected ([#6326])
|
||||||
|
- standarise lp serialisation: ([#6324])
|
||||||
|
- Upgrade to def_guard_wireguard v0.8.0 ([#6315])
|
||||||
|
- Max/crates io prep v2 ([#6270])
|
||||||
|
|
||||||
|
[#6422]: https://github.com/nymtech/nym/pull/6422
|
||||||
|
[#6420]: https://github.com/nymtech/nym/pull/6420
|
||||||
|
[#6419]: https://github.com/nymtech/nym/pull/6419
|
||||||
|
[#6412]: https://github.com/nymtech/nym/pull/6412
|
||||||
|
[#6411]: https://github.com/nymtech/nym/pull/6411
|
||||||
|
[#6409]: https://github.com/nymtech/nym/pull/6409
|
||||||
|
[#6408]: https://github.com/nymtech/nym/pull/6408
|
||||||
|
[#6404]: https://github.com/nymtech/nym/pull/6404
|
||||||
|
[#6402]: https://github.com/nymtech/nym/pull/6402
|
||||||
|
[#6400]: https://github.com/nymtech/nym/pull/6400
|
||||||
|
[#6399]: https://github.com/nymtech/nym/pull/6399
|
||||||
|
[#6398]: https://github.com/nymtech/nym/pull/6398
|
||||||
|
[#6395]: https://github.com/nymtech/nym/pull/6395
|
||||||
|
[#6394]: https://github.com/nymtech/nym/pull/6394
|
||||||
|
[#6393]: https://github.com/nymtech/nym/pull/6393
|
||||||
|
[#6391]: https://github.com/nymtech/nym/pull/6391
|
||||||
|
[#6390]: https://github.com/nymtech/nym/pull/6390
|
||||||
|
[#6386]: https://github.com/nymtech/nym/pull/6386
|
||||||
|
[#6382]: https://github.com/nymtech/nym/pull/6382
|
||||||
|
[#6378]: https://github.com/nymtech/nym/pull/6378
|
||||||
|
[#6373]: https://github.com/nymtech/nym/pull/6373
|
||||||
|
[#6372]: https://github.com/nymtech/nym/pull/6372
|
||||||
|
[#6363]: https://github.com/nymtech/nym/pull/6363
|
||||||
|
[#6361]: https://github.com/nymtech/nym/pull/6361
|
||||||
|
[#6358]: https://github.com/nymtech/nym/pull/6358
|
||||||
|
[#6357]: https://github.com/nymtech/nym/pull/6357
|
||||||
|
[#6356]: https://github.com/nymtech/nym/pull/6356
|
||||||
|
[#6349]: https://github.com/nymtech/nym/pull/6349
|
||||||
|
[#6346]: https://github.com/nymtech/nym/pull/6346
|
||||||
|
[#6340]: https://github.com/nymtech/nym/pull/6340
|
||||||
|
[#6337]: https://github.com/nymtech/nym/pull/6337
|
||||||
|
[#6336]: https://github.com/nymtech/nym/pull/6336
|
||||||
|
[#6335]: https://github.com/nymtech/nym/pull/6335
|
||||||
|
[#6331]: https://github.com/nymtech/nym/pull/6331
|
||||||
|
[#6326]: https://github.com/nymtech/nym/pull/6326
|
||||||
|
[#6324]: https://github.com/nymtech/nym/pull/6324
|
||||||
|
[#6315]: https://github.com/nymtech/nym/pull/6315
|
||||||
|
[#6270]: https://github.com/nymtech/nym/pull/6270
|
||||||
|
|
||||||
## [2026.2-oscypek] (2026-01-27)
|
## [2026.2-oscypek] (2026-01-27)
|
||||||
|
|
||||||
- bugfix: downgrade gateway protocol to clients proposed version ([#6377])
|
- bugfix: downgrade gateway protocol to clients proposed version ([#6377])
|
||||||
|
|||||||
Generated
+353
-184
File diff suppressed because it is too large
Load Diff
+104
-103
@@ -185,7 +185,6 @@ default-members = [
|
|||||||
"nym-credential-proxy/nym-credential-proxy",
|
"nym-credential-proxy/nym-credential-proxy",
|
||||||
"nym-node",
|
"nym-node",
|
||||||
"nym-node-status-api/nym-node-status-agent",
|
"nym-node-status-api/nym-node-status-agent",
|
||||||
"nym-node-status-api/nym-node-status-api",
|
|
||||||
"nym-statistics-api",
|
"nym-statistics-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
"nyx-chain-watcher",
|
"nyx-chain-watcher",
|
||||||
@@ -206,7 +205,7 @@ edition = "2024"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
version = "1.20.1"
|
version = "1.20.4"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
addr = "0.15.6"
|
addr = "0.15.6"
|
||||||
@@ -233,7 +232,7 @@ blake3 = "1.7.0"
|
|||||||
bloomfilter = "3.0.1"
|
bloomfilter = "3.0.1"
|
||||||
bs58 = "0.5.1"
|
bs58 = "0.5.1"
|
||||||
bytecodec = "0.4.15"
|
bytecodec = "0.4.15"
|
||||||
bytes = "1.10.1"
|
bytes = "1.11.1"
|
||||||
cargo_metadata = "0.19.2"
|
cargo_metadata = "0.19.2"
|
||||||
celes = "2.6.0"
|
celes = "2.6.0"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
@@ -304,6 +303,7 @@ ledger-transport = "0.10.0"
|
|||||||
ledger-transport-hid = "0.10.0"
|
ledger-transport-hid = "0.10.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
|
mock_instant = "0.6.0"
|
||||||
moka = { version = "0.12", features = ["future"] }
|
moka = { version = "0.12", features = ["future"] }
|
||||||
nix = "0.30.1"
|
nix = "0.30.1"
|
||||||
notify = "5.1.0"
|
notify = "5.1.0"
|
||||||
@@ -320,12 +320,13 @@ publicsuffix = "2.3.0"
|
|||||||
proc_pidinfo = "0.1.3"
|
proc_pidinfo = "0.1.3"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
rand09 = { package = "rand", version = "=0.9.2" }
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.3"
|
||||||
rand_core = "0.6.3"
|
rand_core = "0.6.3"
|
||||||
rand_distr = "0.4"
|
rand_distr = "0.4"
|
||||||
rayon = "1.5.1"
|
rayon = "1.5.1"
|
||||||
regex = "1.10.6"
|
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"
|
rs_merkle = "1.5.0"
|
||||||
schemars = "0.8.22"
|
schemars = "0.8.22"
|
||||||
semver = "1.0.26"
|
semver = "1.0.26"
|
||||||
@@ -381,7 +382,7 @@ url = "2.5"
|
|||||||
utoipa = "5.2"
|
utoipa = "5.2"
|
||||||
utoipa-swagger-ui = "8.1"
|
utoipa-swagger-ui = "8.1"
|
||||||
utoipauto = "0.2"
|
utoipauto = "0.2"
|
||||||
uuid = "*"
|
uuid = "1.19.0"
|
||||||
vergen = { version = "=8.3.1", default-features = false }
|
vergen = { version = "=8.3.1", default-features = false }
|
||||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
@@ -391,106 +392,106 @@ zeroize = "1.7.0"
|
|||||||
prometheus = { version = "0.14.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
|
# 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-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||||
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
|
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
|
||||||
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
|
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
|
||||||
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
|
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
|
||||||
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
|
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
|
||||||
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
|
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
|
||||||
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
|
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
|
||||||
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
|
nym-client-core = { version = "1.20.4", 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-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
|
||||||
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
|
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
|
||||||
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
|
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
|
||||||
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
|
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
|
||||||
nym-common = { version = "1.20.1", path = "common/nym-common" }
|
nym-common = { version = "1.20.4", path = "common/nym-common" }
|
||||||
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
|
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
|
||||||
nym-config = { version = "1.20.1", path = "common/config" }
|
nym-config = { version = "1.20.4", path = "common/config" }
|
||||||
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||||
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||||
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
|
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
|
||||||
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
|
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
|
||||||
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
|
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
|
||||||
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
|
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
|
||||||
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
|
nym-credentials-interface = { version = "1.20.4", 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-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||||
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
|
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
|
||||||
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
|
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
|
||||||
nym-dkg = { version = "1.20.1", path = "common/dkg" }
|
nym-dkg = { version = "1.20.4", path = "common/dkg" }
|
||||||
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||||
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
|
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
|
||||||
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
|
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
|
||||||
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
|
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
|
||||||
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
|
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
|
||||||
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
|
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
|
||||||
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
|
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
|
||||||
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
|
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
|
||||||
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
|
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
|
||||||
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
|
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
|
||||||
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||||
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
|
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
|
||||||
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
|
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
|
||||||
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
|
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
|
||||||
nym-id = { version = "1.20.1", path = "common/nym-id" }
|
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-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
|
||||||
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
|
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||||
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
|
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||||
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
|
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||||
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
|
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||||
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
|
||||||
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
|
||||||
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
|
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
|
||||||
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
|
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
|
||||||
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
|
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
|
||||||
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
|
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
|
||||||
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
|
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
|
||||||
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
|
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
|
||||||
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
|
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
|
||||||
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
|
nym-registration-common = { version = "1.20.4", path = "common/registration" }
|
||||||
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
|
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
|
||||||
nym-registration-common = { version = "1.20.1", path = "common/registration" }
|
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||||
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
|
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
|
||||||
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
|
||||||
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
|
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
|
||||||
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
|
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
|
||||||
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
|
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
|
||||||
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
|
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
|
||||||
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
|
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
|
||||||
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
|
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
|
||||||
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
|
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
|
||||||
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
|
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
|
||||||
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
|
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
|
||||||
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
|
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
|
||||||
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
|
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
|
||||||
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
|
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
|
||||||
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
|
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
|
||||||
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
|
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
|
||||||
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
|
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
|
||||||
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
|
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
|
||||||
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
|
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
|
||||||
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
|
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
|
||||||
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
|
nym-task = { version = "1.20.4", path = "common/task" }
|
||||||
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
|
nym-tun = { version = "1.20.4", path = "common/tun" }
|
||||||
nym-task = { version = "1.20.1", path = "common/task" }
|
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
|
||||||
nym-tun = { version = "1.20.1", path = "common/tun" }
|
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
|
||||||
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
|
nym-topology = { version = "1.20.4", path = "common/topology" }
|
||||||
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
|
nym-types = { version = "1.20.4", path = "common/types" }
|
||||||
nym-topology = { version = "1.20.1", path = "common/topology" }
|
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||||
nym-types = { version = "1.20.1", path = "common/types" }
|
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||||
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
|
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||||
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
|
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||||
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||||
nym-verloc = { version = "1.20.1", path = "common/verloc" }
|
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||||
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
|
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||||
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
|
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||||
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
|
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||||
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-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
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-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||||
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
|
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||||
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
|
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
|
||||||
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
|
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
|
||||||
|
|
||||||
# coconut/DKG related
|
# coconut/DKG related
|
||||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
# 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]
|
[package]
|
||||||
name = "nym-client"
|
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>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||||
description = "Implementation of the Nym Client"
|
description = "Implementation of the Nym Client"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
+1004
-974
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"webpack": "^5.76.0",
|
"webpack": "^5.105.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.7.4"
|
"webpack-dev-server": "^4.7.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.69"
|
version = "1.1.70"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
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"
|
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ mod util;
|
|||||||
mod version;
|
mod version;
|
||||||
|
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||||
pub use v6 as latest;
|
pub use v6 as latest;
|
||||||
pub use version::AuthenticatorVersion;
|
pub use version::AuthenticatorVersion;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use crate::traits::{
|
|||||||
TopUpBandwidthResponse, UpgradeModeStatus,
|
TopUpBandwidthResponse, UpgradeModeStatus,
|
||||||
};
|
};
|
||||||
use crate::{v2, v3, v4, v5, v6};
|
use crate::{v2, v3, v4, v5, v6};
|
||||||
|
use nym_sphinx::addressing::Recipient;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AuthenticatorResponse {
|
pub enum AuthenticatorResponse {
|
||||||
@@ -17,6 +18,17 @@ pub enum AuthenticatorResponse {
|
|||||||
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
|
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 {
|
impl UpgradeModeStatus for AuthenticatorResponse {
|
||||||
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
|
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
|
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||||
use base64::{Engine, engine::general_purpose};
|
use base64::{Engine, engine::general_purpose};
|
||||||
use nym_credentials_interface::CredentialSpendingData;
|
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 nym_wireguard_types::PeerPublicKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -56,27 +56,11 @@ impl fmt::Display for IpPair {
|
|||||||
|
|
||||||
impl From<IpAddr> for IpPair {
|
impl From<IpAddr> for IpPair {
|
||||||
fn from(value: IpAddr) -> Self {
|
fn from(value: IpAddr) -> Self {
|
||||||
let (before_last_byte, last_byte) = match value {
|
let (ipv4, ipv6) = match value {
|
||||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
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)
|
IpPair::new(ipv4, ipv6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||||
use base64::{Engine, engine::general_purpose};
|
use base64::{Engine, engine::general_purpose};
|
||||||
use nym_credentials_interface::CredentialSpendingData;
|
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 nym_wireguard_types::PeerPublicKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -54,27 +54,11 @@ impl fmt::Display for IpPair {
|
|||||||
|
|
||||||
impl From<IpAddr> for IpPair {
|
impl From<IpAddr> for IpPair {
|
||||||
fn from(value: IpAddr) -> Self {
|
fn from(value: IpAddr) -> Self {
|
||||||
let (before_last_byte, last_byte) = match value {
|
let (ipv4, ipv6) = match value {
|
||||||
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||||
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
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)
|
IpPair::new(ipv4, ipv6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::models::BandwidthClaim;
|
use crate::models::BandwidthClaim;
|
||||||
|
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
|
||||||
use base64::{Engine, engine::general_purpose};
|
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 nym_wireguard_types::PeerPublicKey;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
use std::time::SystemTime;
|
|
||||||
use std::{fmt, ops::Deref, str::FromStr};
|
use std::{fmt, ops::Deref, str::FromStr};
|
||||||
|
|
||||||
#[cfg(feature = "verify")]
|
#[cfg(feature = "verify")]
|
||||||
@@ -20,13 +19,11 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
|
|||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
|
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
|
||||||
pub type PrivateIPs = HashMap<IpPair, Taken>;
|
|
||||||
|
|
||||||
#[cfg(feature = "verify")]
|
#[cfg(feature = "verify")]
|
||||||
pub type HmacSha256 = Hmac<Sha256>;
|
pub type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
pub type Nonce = u64;
|
pub type Nonce = u64;
|
||||||
pub type Taken = Option<SystemTime>;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct IpPair {
|
pub struct IpPair {
|
||||||
@@ -54,27 +51,11 @@ impl fmt::Display for IpPair {
|
|||||||
|
|
||||||
impl From<IpAddr> for IpPair {
|
impl From<IpAddr> for IpPair {
|
||||||
fn from(value: IpAddr) -> Self {
|
fn from(value: IpAddr) -> Self {
|
||||||
let (before_last_byte, last_byte) = match value {
|
let (ipv4, ipv6) = match value {
|
||||||
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
|
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
|
||||||
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
|
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)
|
IpPair::new(ipv4, ipv6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
|
|||||||
impl BandwidthTicketProvider for MockBandwidthController {
|
impl BandwidthTicketProvider for MockBandwidthController {
|
||||||
async fn get_ecash_ticket(
|
async fn get_ecash_ticket(
|
||||||
&self,
|
&self,
|
||||||
_ticket_type: TicketType,
|
ticket_type: TicketType,
|
||||||
_gateway_id: PublicKey,
|
_gateway_id: PublicKey,
|
||||||
tickets_to_spend: u32,
|
tickets_to_spend: u32,
|
||||||
) -> Result<PreparedCredential, BandwidthControllerError> {
|
) -> Result<PreparedCredential, BandwidthControllerError> {
|
||||||
@@ -100,6 +100,10 @@ impl BandwidthTicketProvider for MockBandwidthController {
|
|||||||
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
|
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
|
||||||
.expect("Failed to deserialize test credential - this is a bug in the test harness");
|
.expect("Failed to deserialize test credential - this is a bug in the test harness");
|
||||||
|
|
||||||
|
// change the ticket type to the requested ticket
|
||||||
|
// note that verification outside mocks is going to fail
|
||||||
|
credential.payment.t_type = ticket_type.to_repr() as u8;
|
||||||
|
|
||||||
// Update spend_date to today to pass validation
|
// Update spend_date to today to pass validation
|
||||||
credential.spend_date = OffsetDateTime::now_utc().date();
|
credential.spend_date = OffsetDateTime::now_utc().date();
|
||||||
|
|
||||||
|
|||||||
@@ -57,3 +57,22 @@ where
|
|||||||
Ok(Some(token))
|
Ok(Some(token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
|
impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box<T> {
|
||||||
|
async fn get_ecash_ticket(
|
||||||
|
&self,
|
||||||
|
ticket_type: TicketType,
|
||||||
|
gateway_id: ed25519::PublicKey,
|
||||||
|
tickets_to_spend: u32,
|
||||||
|
) -> Result<PreparedCredential, BandwidthControllerError> {
|
||||||
|
(**self)
|
||||||
|
.get_ecash_ticket(ticket_type, gateway_id, tickets_to_spend)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
|
||||||
|
(**self).get_upgrade_mode_token().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub use opentelemetry;
|
|||||||
pub use opentelemetry_jaeger;
|
pub use opentelemetry_jaeger;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
pub use tracing_opentelemetry;
|
pub use tracing_opentelemetry;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "basic_tracing")]
|
||||||
pub use tracing_subscriber;
|
pub use tracing_subscriber;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
pub use tracing_tree;
|
pub use tracing_tree;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use crate::{
|
|||||||
error::ClientCoreError,
|
error::ClientCoreError,
|
||||||
};
|
};
|
||||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
|
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
|
||||||
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
|
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
|
||||||
|
|
||||||
pub use nym_client_core_gateways_storage as gateways_storage;
|
pub use nym_client_core_gateways_storage as gateways_storage;
|
||||||
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
|
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ features = ["json"]
|
|||||||
|
|
||||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
|
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
|
||||||
workspace = true
|
workspace = true
|
||||||
features = ["json", "rustls-tls"]
|
features = ["json", "rustls"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
|
|||||||
};
|
};
|
||||||
use nym_api_requests::models::{
|
use nym_api_requests::models::{
|
||||||
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
|
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
|
||||||
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
|
||||||
};
|
};
|
||||||
use nym_api_requests::nym_nodes::{
|
use nym_api_requests::nym_nodes::{
|
||||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||||
@@ -273,18 +273,18 @@ impl<C, S> Client<C, S> {
|
|||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
|
// #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
|
||||||
pub async fn get_all_cached_described_nodes(
|
pub async fn get_all_cached_described_nodes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||||
Ok(self.nym_api.get_all_described_nodes().await?)
|
Ok(self.nym_api.get_all_described_nodes().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_cached_described_nodes_v2(
|
// pub async fn get_all_cached_described_nodes_v2(
|
||||||
&self,
|
// &self,
|
||||||
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||||
Ok(self.nym_api.get_all_described_nodes_v2().await?)
|
// Ok(self.nym_api.get_all_described_nodes_v2().await?)
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub async fn get_all_cached_bonded_nym_nodes(
|
pub async fn get_all_cached_bonded_nym_nodes(
|
||||||
&self,
|
&self,
|
||||||
@@ -473,7 +473,7 @@ impl NymApiClient {
|
|||||||
Ok(self.nym_api.health().await?)
|
Ok(self.nym_api.health().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||||
pub async fn get_all_described_nodes(
|
pub async fn get_all_described_nodes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
|
||||||
@@ -495,29 +495,29 @@ impl NymApiClient {
|
|||||||
Ok(descriptions)
|
Ok(descriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_described_nodes_v2(
|
// pub async fn get_all_described_nodes_v2(
|
||||||
&self,
|
// &self,
|
||||||
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
|
||||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||||
let mut page = 0;
|
// let mut page = 0;
|
||||||
let mut descriptions = Vec::new();
|
// let mut descriptions = Vec::new();
|
||||||
|
//
|
||||||
loop {
|
// loop {
|
||||||
let mut res = self
|
// let mut res = self
|
||||||
.nym_api
|
// .nym_api
|
||||||
.get_nodes_described_v2(Some(page), None)
|
// .get_nodes_described_v2(Some(page), None)
|
||||||
.await?;
|
// .await?;
|
||||||
|
//
|
||||||
descriptions.append(&mut res.data);
|
// descriptions.append(&mut res.data);
|
||||||
if descriptions.len() < res.pagination.total {
|
// if descriptions.len() < res.pagination.total {
|
||||||
page += 1
|
// page += 1
|
||||||
} else {
|
// } else {
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Ok(descriptions)
|
// Ok(descriptions)
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub async fn get_all_bonded_nym_nodes(
|
pub async fn get_all_bonded_nym_nodes(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse;
|
|||||||
use nym_api_requests::models::{
|
use nym_api_requests::models::{
|
||||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
|
||||||
SignerInformationResponse,
|
SignerInformationResponse,
|
||||||
};
|
};
|
||||||
use nym_api_requests::nym_nodes::{
|
use nym_api_requests::nym_nodes::{
|
||||||
@@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
#[deprecated(note = "use .get_nodes_described_v2 instead")]
|
// #[deprecated(note = "use .get_nodes_described_v2 instead")]
|
||||||
async fn get_nodes_described(
|
async fn get_nodes_described(
|
||||||
&self,
|
&self,
|
||||||
page: Option<u32>,
|
page: Option<u32>,
|
||||||
@@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
// #[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_nodes_described_v2(
|
// async fn get_nodes_described_v2(
|
||||||
&self,
|
// &self,
|
||||||
page: Option<u32>,
|
// page: Option<u32>,
|
||||||
per_page: Option<u32>,
|
// per_page: Option<u32>,
|
||||||
) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
|
// ) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
|
||||||
let mut params = Vec::new();
|
// let mut params = Vec::new();
|
||||||
|
//
|
||||||
if let Some(page) = page {
|
// if let Some(page) = page {
|
||||||
params.push(("page", page.to_string()))
|
// params.push(("page", page.to_string()))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if let Some(per_page) = per_page {
|
// if let Some(per_page) = per_page {
|
||||||
params.push(("per_page", per_page.to_string()))
|
// params.push(("per_page", per_page.to_string()))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
self.get_json(
|
// self.get_json(
|
||||||
&[
|
// &[
|
||||||
routes::V2_API_VERSION,
|
// routes::V2_API_VERSION,
|
||||||
routes::NYM_NODES_ROUTES,
|
// routes::NYM_NODES_ROUTES,
|
||||||
routes::NYM_NODES_DESCRIBED,
|
// routes::NYM_NODES_DESCRIBED,
|
||||||
],
|
// ],
|
||||||
¶ms,
|
// ¶ms,
|
||||||
)
|
// )
|
||||||
.await
|
// .await
|
||||||
}
|
// }
|
||||||
|
|
||||||
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||||
self.get_rewarded_set().await
|
self.get_rewarded_set().await
|
||||||
@@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
|
||||||
#[allow(deprecated)]
|
// #[allow(deprecated)]
|
||||||
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescriptionV1>, NymAPIError> {
|
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescriptionV1>, NymAPIError> {
|
||||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||||
let mut page = 0;
|
let mut page = 0;
|
||||||
@@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
Ok(descriptions)
|
Ok(descriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_described_nodes_v2(&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
|
// async fn (&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
|
||||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||||
let mut page = 0;
|
// let mut page = 0;
|
||||||
let mut descriptions = Vec::new();
|
// let mut descriptions = Vec::new();
|
||||||
|
//
|
||||||
loop {
|
// loop {
|
||||||
let mut res = self.get_nodes_described_v2(Some(page), None).await?;
|
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
|
||||||
|
//
|
||||||
descriptions.append(&mut res.data);
|
// descriptions.append(&mut res.data);
|
||||||
if descriptions.len() < res.pagination.total {
|
// if descriptions.len() < res.pagination.total {
|
||||||
page += 1
|
// page += 1
|
||||||
} else {
|
// } else {
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Ok(descriptions)
|
// Ok(descriptions)
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_nym_nodes(
|
async fn get_nym_nodes(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
||||||
match client.get_all_cached_described_nodes_v2().await {
|
match client.get_all_cached_described_nodes().await {
|
||||||
Ok(res) => match args.identity_key {
|
Ok(res) => match args.identity_key {
|
||||||
Some(identity_key) => {
|
Some(identity_key) => {
|
||||||
let node = res.iter().find(|node| {
|
let node = res.iter().find(|node| {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
|
||||||
match client.get_all_cached_described_nodes_v2().await {
|
match client.get_all_cached_described_nodes().await {
|
||||||
Ok(res) => match args.identity_key {
|
Ok(res) => match args.identity_key {
|
||||||
Some(identity_key) => {
|
Some(identity_key) => {
|
||||||
let node = res.iter().find(|node| {
|
let node = res.iter().find(|node| {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
humantime = { workspace = true }
|
humantime = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
reqwest = { workspace = true, features = ["rustls"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
strum = { workspace = true, features = ["derive"] }
|
strum = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ pub mod ecash;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod upgrade_mode;
|
pub mod upgrade_mode;
|
||||||
|
|
||||||
|
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
|
||||||
|
|
||||||
// Histogram buckets for ecash verification duration (in seconds)
|
// Histogram buckets for ecash verification duration (in seconds)
|
||||||
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
|
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
|
||||||
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0];
|
&[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> {
|
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();
|
let start = Instant::now();
|
||||||
nym_metrics::inc!("ecash_verification_attempts");
|
nym_metrics::inc!("ecash_verification_attempts");
|
||||||
|
|
||||||
|
|||||||
@@ -291,3 +291,40 @@ struct UpgradeModeStateInner {
|
|||||||
// (and dealing with the async consequences of that)
|
// (and dealing with the async consequences of that)
|
||||||
status: UpgradeModeStatus,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
|
|||||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
nym-sphinx-types = { workspace = true }
|
nym-sphinx-types = { workspace = true, optional = true }
|
||||||
nym-pemstore = { workspace = true }
|
nym-pemstore = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -51,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
|
|||||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||||
sphinx = ["nym-sphinx-types/sphinx"]
|
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ description = "Functions to interact with zknym signers, checking their status a
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
semver = { workspace = true }
|
semver = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|||||||
@@ -3,14 +3,9 @@
|
|||||||
|
|
||||||
use crate::client_check::check_client;
|
use crate::client_check::check_client;
|
||||||
use futures::stream::{FuturesUnordered, StreamExt};
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
|
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||||
use nym_network_defaults::NymNetworkDetails;
|
use nym_network_defaults::NymNetworkDetails;
|
||||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
pub use error::SignerCheckError;
|
|
||||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
|
||||||
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
||||||
use nym_validator_client::models::{
|
use nym_validator_client::models::{
|
||||||
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
||||||
@@ -18,6 +13,12 @@ use nym_validator_client::models::{
|
|||||||
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||||
ContractVKShare, DealerDetails, Epoch,
|
ContractVKShare, DealerDetails, Epoch,
|
||||||
};
|
};
|
||||||
|
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub use error::SignerCheckError;
|
||||||
|
|
||||||
mod client_check;
|
mod client_check;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -31,6 +32,7 @@ pub type TypedSignerResult = SignerResult<
|
|||||||
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
||||||
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SignersTestResult {
|
pub struct SignersTestResult {
|
||||||
pub threshold: Option<u64>,
|
pub threshold: Option<u64>,
|
||||||
pub results: Vec<TypedSignerResult>,
|
pub results: Vec<TypedSignerResult>,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
|
|||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
bincode = { workspace = true }
|
bincode = { workspace = true }
|
||||||
cfg-if = { 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
|
http.workspace = true
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
sync::{Arc, LazyLock},
|
sync::{
|
||||||
|
Arc, LazyLock,
|
||||||
|
atomic::{AtomicBool, Ordering::Relaxed},
|
||||||
|
},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,14 +73,23 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
|
|
||||||
impl ClientBuilder {
|
impl ClientBuilder {
|
||||||
/// Override the DNS resolver implementation used by the underlying http client.
|
/// Override the DNS resolver implementation used by the underlying http client.
|
||||||
|
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||||
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
|
pub fn dns_resolver<R: Resolve + 'static>(mut self, resolver: Arc<R>) -> Self {
|
||||||
self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
|
self = self.non_shared();
|
||||||
|
// because of the call to non-shared this conditional should always run.
|
||||||
|
if let Some(rb) = self.reqwest_client_builder {
|
||||||
|
self.reqwest_client_builder = Some(rb.dns_resolver(resolver));
|
||||||
|
}
|
||||||
self.use_secure_dns = false;
|
self.use_secure_dns = false;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Override the DNS resolver implementation used by the underlying http client.
|
/// Override the DNS resolver implementation used by the underlying http client. If
|
||||||
|
/// [`Self::dns_resolver`] is called directly that will take priority over this, there is no
|
||||||
|
/// need to call both.
|
||||||
|
/// This forces the use of an independent request executor (via [`Self::non_shared`]).
|
||||||
pub fn no_hickory_dns(mut self) -> Self {
|
pub fn no_hickory_dns(mut self) -> Self {
|
||||||
|
self = self.non_shared();
|
||||||
self.use_secure_dns = false;
|
self.use_secure_dns = false;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -129,7 +141,8 @@ pub struct HickoryDnsResolver {
|
|||||||
// Tokio Runtime in initialization, so we must delay the actual
|
// Tokio Runtime in initialization, so we must delay the actual
|
||||||
// construction of the resolver.
|
// construction of the resolver.
|
||||||
state: Arc<OnceCell<TokioResolver>>,
|
state: Arc<OnceCell<TokioResolver>>,
|
||||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
use_system: Arc<AtomicBool>,
|
||||||
|
system_resolver: Arc<OnceCell<TokioResolver>>,
|
||||||
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
static_base: Option<Arc<OnceCell<StaticResolver>>>,
|
||||||
use_shared: bool,
|
use_shared: bool,
|
||||||
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
|
||||||
@@ -141,7 +154,8 @@ impl Default for HickoryDnsResolver {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
fallback: Default::default(),
|
use_system: Arc::new(AtomicBool::new(false)),
|
||||||
|
system_resolver: Default::default(),
|
||||||
static_base: Some(Default::default()),
|
static_base: Some(Default::default()),
|
||||||
use_shared: true,
|
use_shared: true,
|
||||||
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
|
||||||
@@ -151,16 +165,28 @@ impl Default for HickoryDnsResolver {
|
|||||||
|
|
||||||
impl Resolve for HickoryDnsResolver {
|
impl Resolve for HickoryDnsResolver {
|
||||||
fn resolve(&self, name: Name) -> Resolving {
|
fn resolve(&self, name: Name) -> Resolving {
|
||||||
let resolver = self.state.clone();
|
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
let maybe_fallback = self.fallback.clone();
|
|
||||||
let maybe_static = self.static_base.clone();
|
|
||||||
let use_shared = self.use_shared;
|
let use_shared = self.use_shared;
|
||||||
|
let resolver = if use_system {
|
||||||
|
match self
|
||||||
|
.system_resolver
|
||||||
|
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
|
||||||
|
{
|
||||||
|
Ok(r) => r.clone(),
|
||||||
|
Err(e) => return Box::pin(return_err(e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.state
|
||||||
|
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let maybe_static = self.static_base.clone();
|
||||||
let overall_dns_timeout = self.overall_dns_timeout;
|
let overall_dns_timeout = self.overall_dns_timeout;
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
resolve(
|
resolve(
|
||||||
name,
|
name,
|
||||||
resolver,
|
resolver,
|
||||||
maybe_fallback,
|
|
||||||
maybe_static,
|
maybe_static,
|
||||||
use_shared,
|
use_shared,
|
||||||
overall_dns_timeout,
|
overall_dns_timeout,
|
||||||
@@ -171,16 +197,17 @@ impl Resolve for HickoryDnsResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn return_err(e: ResolveError) -> Result<Addrs, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve(
|
async fn resolve(
|
||||||
name: Name,
|
name: Name,
|
||||||
resolver: Arc<OnceCell<TokioResolver>>,
|
resolver: TokioResolver,
|
||||||
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
|
||||||
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
|
||||||
independent: bool,
|
independent: bool,
|
||||||
overall_dns_timeout: Duration,
|
overall_dns_timeout: Duration,
|
||||||
) -> Result<Addrs, ResolveError> {
|
) -> Result<Addrs, ResolveError> {
|
||||||
let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent));
|
|
||||||
|
|
||||||
// try checking the static table to see if any of the addresses in the table have been
|
// try checking the static table to see if any of the addresses in the table have been
|
||||||
// looked up previously within the timeout to where we are not yet ready to try the
|
// looked up previously within the timeout to where we are not yet ready to try the
|
||||||
// default resolver yet again.
|
// default resolver yet again.
|
||||||
@@ -214,22 +241,6 @@ async fn resolve(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the primary resolver encountered an error, attempt a lookup using the fallback
|
|
||||||
// resolver if one is configured.
|
|
||||||
if let Some(ref fallback) = maybe_fallback {
|
|
||||||
let resolver =
|
|
||||||
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
|
|
||||||
|
|
||||||
let resolve_fut =
|
|
||||||
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
|
|
||||||
if let Ok(Ok(lookup)) = resolve_fut.await {
|
|
||||||
let addrs: Addrs = Box::new(SocketAddrs {
|
|
||||||
iter: lookup.into_iter(),
|
|
||||||
});
|
|
||||||
return Ok(addrs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no record has been found and a static map of fallback addresses is configured
|
// If no record has been found and a static map of fallback addresses is configured
|
||||||
// check the table for our entry
|
// check the table for our entry
|
||||||
if let Some(ref static_resolver) = maybe_static {
|
if let Some(ref static_resolver) = maybe_static {
|
||||||
@@ -258,6 +269,11 @@ impl Iterator for SocketAddrs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HickoryDnsResolver {
|
impl HickoryDnsResolver {
|
||||||
|
/// Returns an instance of the shared resolver.
|
||||||
|
pub fn shared() -> Self {
|
||||||
|
SHARED_RESOLVER.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
/// Attempt to resolve a domain name to a set of ['IpAddr']s
|
||||||
pub async fn resolve_str(
|
pub async fn resolve_str(
|
||||||
&self,
|
&self,
|
||||||
@@ -265,10 +281,20 @@ impl HickoryDnsResolver {
|
|||||||
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
|
||||||
let n =
|
let n =
|
||||||
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
|
||||||
|
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let resolver = if use_system {
|
||||||
|
self.system_resolver
|
||||||
|
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(self.use_shared))?
|
||||||
|
.clone()
|
||||||
|
} else {
|
||||||
|
self.state
|
||||||
|
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
n,
|
n,
|
||||||
self.state.clone(),
|
resolver,
|
||||||
self.fallback.clone(),
|
|
||||||
self.static_base.clone(),
|
self.static_base.clone(),
|
||||||
self.use_shared,
|
self.use_shared,
|
||||||
self.overall_dns_timeout,
|
self.overall_dns_timeout,
|
||||||
@@ -298,13 +324,11 @@ impl HickoryDnsResolver {
|
|||||||
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||||
// using a closure here is slightly gross, but this makes sure that if the
|
// using a closure here is slightly gross, but this makes sure that if the
|
||||||
// lazy-init returns an error it can be handled by the client
|
// lazy-init returns an error it can be handled by the client
|
||||||
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
|
if !use_shared {
|
||||||
new_resolver_system()
|
new_resolver_system()
|
||||||
} else {
|
} else {
|
||||||
Ok(SHARED_RESOLVER
|
Ok(SHARED_RESOLVER
|
||||||
.fallback
|
.system_resolver
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get_or_try_init(new_resolver_system)?
|
.get_or_try_init(new_resolver_system)?
|
||||||
.clone())
|
.clone())
|
||||||
}
|
}
|
||||||
@@ -320,45 +344,80 @@ impl HickoryDnsResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
/// Swap the primary internal resolver to the system resolver rather than the
|
||||||
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
|
/// configured custom resolver.
|
||||||
self.fallback = Some(Default::default());
|
pub fn use_system_resolver(&self) {
|
||||||
let _ = self
|
self.use_system.store(true, Relaxed);
|
||||||
.fallback
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.get_or_try_init(new_resolver_system)?;
|
|
||||||
|
|
||||||
// IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
if self.use_shared {
|
||||||
// if self.use_shared {
|
SHARED_RESOLVER.use_system_resolver();
|
||||||
// SHARED_RESOLVER.enable_system_fallback()?;
|
}
|
||||||
// }
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable fallback resolution. If the primary resolver fails the error is
|
/// Swap the primary internal resolver to the configured custom resolver rather than the
|
||||||
/// returned immediately
|
/// system resolver.
|
||||||
pub fn disable_system_fallback(&mut self) {
|
pub fn use_configured_resolver(&self) {
|
||||||
self.fallback = None;
|
self.use_system.store(false, Relaxed);
|
||||||
|
|
||||||
// // IF THIS INSTANCE IS A FRONT FOR THE SHARED RESOLVER SHOULDN'T THIS FN ENABLE THE SYSTEM FALLBACK FOR THE SHARED RESOLVER TOO?
|
if self.use_shared {
|
||||||
// if self.use_shared {
|
SHARED_RESOLVER.use_configured_resolver();
|
||||||
// SHARED_RESOLVER.fallback = None;
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current map of hostname to address in use by the fallback static lookup if one
|
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||||
|
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||||
|
/// table is consulted.
|
||||||
|
///
|
||||||
|
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||||
|
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||||
|
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
|
||||||
|
pub fn clear_preresolve(&self) {
|
||||||
|
debug!("clearing pre-resolve table");
|
||||||
|
if let Some(cell) = &self.static_base
|
||||||
|
&& let Some(static_base) = cell.get()
|
||||||
|
{
|
||||||
|
static_base.clear_preresolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current map of hostnames to addresses used in the fallback static lookup stage if one
|
||||||
/// exists.
|
/// exists.
|
||||||
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||||
Some(self.static_base.as_ref()?.get()?.get_addrs())
|
Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
|
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
|
||||||
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||||
let cell = OnceCell::new();
|
debug!("setting fallback entries for {:?}", addrs.keys());
|
||||||
cell.set(StaticResolver::new(addrs))
|
if self.static_base.is_none() {
|
||||||
.expect("infallible assign");
|
let cell = OnceCell::new();
|
||||||
self.static_base = Some(Arc::new(cell));
|
self.static_base = Some(Arc::new(cell));
|
||||||
|
}
|
||||||
|
self.static_base
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||||
|
.set_fallback(addrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current map of hostnames to addresses used in the preresolve static lookup stage
|
||||||
|
/// if one exists.
|
||||||
|
pub fn get_static_preresolve(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
|
||||||
|
Some(self.static_base.as_ref()?.get()?.get_preresolve_addrs())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set (or overwrite) the map of addresses used in the preresolve static hostname lookup
|
||||||
|
pub fn set_static_preresolve(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||||
|
debug!("setting pre-resolve entries for {:?}", addrs.keys());
|
||||||
|
if self.static_base.is_none() {
|
||||||
|
let cell = OnceCell::new();
|
||||||
|
self.static_base = Some(Arc::new(cell));
|
||||||
|
}
|
||||||
|
self.static_base
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get_or_init(|| Self::new_static_fallback(self.use_shared))
|
||||||
|
.set_preresolve(addrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
/// Successfully resolved addresses are cached for a minimum of 30 minutes
|
||||||
@@ -495,7 +554,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_default_static_fallback() -> StaticResolver {
|
fn new_default_static_fallback() -> StaticResolver {
|
||||||
StaticResolver::new(constants::default_static_addrs())
|
StaticResolver::new().with_fallback(constants::default_static_addrs())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||||
@@ -532,10 +591,7 @@ mod test {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::{
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
|
||||||
time::Instant,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// IP addresses guaranteed to fail attempts to resolve
|
/// IP addresses guaranteed to fail attempts to resolve
|
||||||
///
|
///
|
||||||
@@ -552,7 +608,7 @@ mod test {
|
|||||||
let var_name = HickoryDnsResolver::default();
|
let var_name = HickoryDnsResolver::default();
|
||||||
let resolver = var_name;
|
let resolver = var_name;
|
||||||
let client = reqwest::ClientBuilder::new()
|
let client = reqwest::ClientBuilder::new()
|
||||||
.dns_resolver(resolver.into())
|
.dns_resolver(resolver)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -597,7 +653,7 @@ mod test {
|
|||||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||||
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
|
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
|
||||||
|
|
||||||
resolver.set_static_fallbacks(addr_map);
|
resolver.set_fallback_addrs(addr_map);
|
||||||
|
|
||||||
let mut addrs = resolver.resolve_str(example_domain).await?;
|
let mut addrs = resolver.resolve_str(example_domain).await?;
|
||||||
assert!(addrs.contains(&example_ip4));
|
assert!(addrs.contains(&example_ip4));
|
||||||
@@ -738,18 +794,19 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore]
|
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||||
// this test is dependent of external network setup -- i.e. blocking all traffic to the default
|
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||||
// resolvers. Otherwise the default resolvers will succeed without using the static fallback,
|
// interference with other tests.
|
||||||
// making the test pointless
|
//
|
||||||
|
// this test is dependent of external network setup -- i.e. blocking all traffic to the
|
||||||
|
// default resolvers. Otherwise the default resolvers will succeed without using the static
|
||||||
|
// fallback, making the test pointless
|
||||||
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
async fn dns_lookup_failure_on_shared() -> Result<(), ResolveError> {
|
||||||
let time_start = Instant::now();
|
let resolver1 = HickoryDnsResolver::shared();
|
||||||
let r = OnceCell::new();
|
|
||||||
r.set(build_broken_resolver().expect("failed to build resolver"))
|
|
||||||
.expect("broken resolver init error");
|
|
||||||
|
|
||||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
let time_start = std::time::Instant::now();
|
||||||
let resolver = HickoryDnsResolver::default();
|
// create a new resolver that uses the shared resolver
|
||||||
|
let resolver = HickoryDnsResolver::shared();
|
||||||
|
|
||||||
// successful lookup using fallback to static resolver
|
// successful lookup using fallback to static resolver
|
||||||
let domain = "rpc.nymtech.net";
|
let domain = "rpc.nymtech.net";
|
||||||
@@ -758,9 +815,27 @@ mod test {
|
|||||||
.await
|
.await
|
||||||
.expect("failed to resolve address in static lookup");
|
.expect("failed to resolve address in static lookup");
|
||||||
|
|
||||||
println!(
|
let lookup_dur = Instant::now() - time_start;
|
||||||
"{}ms resolved {domain}",
|
assert!(
|
||||||
(Instant::now() - time_start).as_millis()
|
lookup_dur > resolver.overall_dns_timeout,
|
||||||
|
"expected lookup timeout - took {}ms",
|
||||||
|
(lookup_dur).as_millis()
|
||||||
|
);
|
||||||
|
|
||||||
|
let time_start = std::time::Instant::now();
|
||||||
|
// successful lookup using pre-resolve entry promoted from fallback
|
||||||
|
let domain = "rpc.nymtech.net";
|
||||||
|
let _ = resolver1
|
||||||
|
.resolve_str(domain)
|
||||||
|
.await
|
||||||
|
.expect("domain expected to be in pre-resolve");
|
||||||
|
|
||||||
|
// this lookup should basically be instant as we are using pre-resolve
|
||||||
|
let lookup_dur = std::time::Instant::now() - time_start;
|
||||||
|
assert!(
|
||||||
|
lookup_dur < Duration::from_millis(10),
|
||||||
|
"expected instant - took {}ms",
|
||||||
|
(lookup_dur).as_millis()
|
||||||
);
|
);
|
||||||
|
|
||||||
// unsuccessful lookup - primary times out, and not in static table
|
// unsuccessful lookup - primary times out, and not in static table
|
||||||
@@ -771,5 +846,62 @@ mod test {
|
|||||||
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
|
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||||
|
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||||
|
// interference with other tests.
|
||||||
|
async fn setting_dns_fallbacks_with_shared_resolver() -> Result<(), ResolveError> {
|
||||||
|
let resolver1 = HickoryDnsResolver::shared();
|
||||||
|
|
||||||
|
// create a new resolver that uses the shared resolver
|
||||||
|
let mut resolver = HickoryDnsResolver::shared();
|
||||||
|
|
||||||
|
let example_domains = [
|
||||||
|
String::from("static1.nymvpn.com"),
|
||||||
|
String::from("static2.nymvpn.com"),
|
||||||
|
];
|
||||||
|
let mut addr_map1 = HashMap::new();
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[0].clone(),
|
||||||
|
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||||
|
);
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[1].clone(),
|
||||||
|
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||||
|
);
|
||||||
|
|
||||||
|
resolver.set_static_preresolve(addr_map1);
|
||||||
|
|
||||||
|
let time_start = std::time::Instant::now();
|
||||||
|
// successful lookup using pre-resolve entry promoted from fallback
|
||||||
|
let _ = resolver1
|
||||||
|
.resolve_str(&example_domains[0])
|
||||||
|
.await
|
||||||
|
.expect("domain expected to be in pre-resolve");
|
||||||
|
|
||||||
|
// this lookup should basically be instant as we are using pre-resolve
|
||||||
|
let lookup_dur = std::time::Instant::now() - time_start;
|
||||||
|
assert!(
|
||||||
|
lookup_dur < Duration::from_millis(10),
|
||||||
|
"expected instant - took {}ms",
|
||||||
|
(lookup_dur).as_millis()
|
||||||
|
);
|
||||||
|
|
||||||
|
// After clearing the pre-resolve in one instance of the shared resolver ...
|
||||||
|
resolver.clear_preresolve();
|
||||||
|
|
||||||
|
// ... other instances have their pre-resolve entries cleared.
|
||||||
|
let prereslve_lookup = resolver1
|
||||||
|
.static_base
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.get()
|
||||||
|
.unwrap()
|
||||||
|
.pre_resolve(&example_domains[0]);
|
||||||
|
assert!(prereslve_lookup.is_none());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ pub const VERCEL_COM_IPS: &[IpAddr] = &[
|
|||||||
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
|
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pub const NYM_API_CDN: &str = "cdn1.media-platform.net";
|
||||||
|
pub const NYM_API_CDN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(172, 104, 178, 252))];
|
||||||
|
|
||||||
pub const NYM_COM_DOMAIN: &str = "nym.com";
|
pub const NYM_COM_DOMAIN: &str = "nym.com";
|
||||||
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
|
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
|
||||||
|
|
||||||
@@ -69,6 +72,12 @@ pub const NYM_RPC_IPS: &[IpAddr] = &[
|
|||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn empty_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
|
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
|
||||||
@@ -88,6 +97,7 @@ pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
|
|||||||
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
|
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
|
||||||
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
|
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
|
||||||
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
|
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
|
||||||
|
m.insert(NYM_API_CDN.to_string(), NYM_API_CDN_IPS.to_vec());
|
||||||
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
|
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
|
||||||
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
|
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
|
||||||
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
|
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
|
||||||
|
|||||||
@@ -14,42 +14,78 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
|
|||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct StaticResolver {
|
pub struct StaticResolver {
|
||||||
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
|
||||||
|
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
|
||||||
pre_resolve_timeout: Option<Duration>,
|
pre_resolve_timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
enum PreResolveStatus {
|
||||||
|
#[default]
|
||||||
|
Valid,
|
||||||
|
ValidUntil(Instant),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct Entry {
|
struct Entry {
|
||||||
valid_for_pre_resolve_until: Option<Instant>,
|
status: PreResolveStatus,
|
||||||
addrs: Vec<IpAddr>,
|
addrs: Vec<IpAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entry {
|
impl Entry {
|
||||||
fn new(addrs: Vec<IpAddr>) -> Self {
|
fn new(addrs: Vec<IpAddr>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
valid_for_pre_resolve_until: None,
|
status: PreResolveStatus::Valid,
|
||||||
addrs,
|
addrs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_timeout(addrs: Vec<IpAddr>, timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
status: PreResolveStatus::ValidUntil(Instant::now() + timeout),
|
||||||
|
addrs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid(&self) -> bool {
|
||||||
|
match self.status {
|
||||||
|
PreResolveStatus::Valid => true,
|
||||||
|
PreResolveStatus::ValidUntil(t) => t > Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticResolver {
|
impl StaticResolver {
|
||||||
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
|
pub fn new() -> StaticResolver {
|
||||||
debug!("building static resolver");
|
|
||||||
let static_entries = static_entries
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, ips)| (name, Entry::new(ips)))
|
|
||||||
.collect();
|
|
||||||
Self {
|
Self {
|
||||||
static_addr_map: Arc::new(Mutex::new(static_entries)),
|
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
|
||||||
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
|
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the full set of domain names and associated addresses stored in this static lookup table
|
/// Initialize the contents of the pre-resolve table for this instance of the static resolver
|
||||||
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
#[allow(unused)]
|
||||||
|
pub fn with_preresolve(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||||
|
let entries = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, ips)| (name, Entry::new(ips)))
|
||||||
|
.collect();
|
||||||
|
self.preresolve_addr_map = Arc::new(Mutex::new(entries));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the contenes of the fallback table for this instance of the static resolver
|
||||||
|
pub fn with_fallback(mut self, entries: HashMap<String, Vec<IpAddr>>) -> Self {
|
||||||
|
self.fallback_addr_map = Arc::new(Mutex::new(entries));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the set of domain names and associated addresses stored in the pre-resolve static
|
||||||
|
/// lookup table
|
||||||
|
pub fn get_preresolve_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||||
let mut out = HashMap::new();
|
let mut out = HashMap::new();
|
||||||
self.static_addr_map
|
self.preresolve_addr_map
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -59,6 +95,38 @@ impl StaticResolver {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the set of domain names and associated addresses stored in the fallback static lookup
|
||||||
|
/// table
|
||||||
|
pub fn get_fallback_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
|
||||||
|
self.fallback_addr_map.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set (or overwrite) the map of static addresses to be returned only after attempting a lookup
|
||||||
|
/// over the network resolver.
|
||||||
|
pub fn set_fallback(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||||
|
self.fallback_addr_map.lock().unwrap().extend(addrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||||
|
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||||
|
/// table is consulted.
|
||||||
|
///
|
||||||
|
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||||
|
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||||
|
/// pre-resolve (using [`Self::preresolve_to_addrs`]) will be removed.
|
||||||
|
pub fn clear_preresolve(&self) {
|
||||||
|
*self.preresolve_addr_map.lock().unwrap() = HashMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set (or overwrite) the map of static addresses and mark these domains to be returned
|
||||||
|
/// WITHOUT attempting a lookup over the network resolver.
|
||||||
|
pub fn set_preresolve(&self, addrs: HashMap<String, Vec<IpAddr>>) {
|
||||||
|
let mut current_map = self.preresolve_addr_map.lock().unwrap();
|
||||||
|
for (domain, ips) in addrs.into_iter() {
|
||||||
|
_ = current_map.insert(domain, Entry::new(ips))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the timeout for which domains can be pre-resolved after they are looked up in the
|
/// Change the timeout for which domains can be pre-resolved after they are looked up in the
|
||||||
/// static lookup table.
|
/// static lookup table.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -71,44 +139,58 @@ impl StaticResolver {
|
|||||||
/// recently (within the configured timeout) looked it up previously in this static table using
|
/// recently (within the configured timeout) looked it up previously in this static table using
|
||||||
/// a regular resolve.
|
/// a regular resolve.
|
||||||
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
|
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||||
debug!("found {name:?} in pre-resolve static table resolver");
|
self.preresolve_addr_map
|
||||||
|
|
||||||
self.pre_resolve_timeout?;
|
|
||||||
|
|
||||||
self.static_addr_map
|
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get(name)
|
.get(name)
|
||||||
.filter(|e| {
|
.filter(|entry| entry.is_valid())
|
||||||
e.valid_for_pre_resolve_until
|
.map(|entry| {
|
||||||
.is_some_and(|t| t > Instant::now())
|
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
|
||||||
|
entry.addrs.clone()
|
||||||
})
|
})
|
||||||
.map(|e| e.addrs.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
|
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
|
||||||
Self::resolve_inner(
|
Self::resolve_inner(
|
||||||
self.static_addr_map.lock().unwrap(),
|
self.fallback_addr_map.lock().unwrap(),
|
||||||
|
self.preresolve_addr_map.lock().unwrap(),
|
||||||
name,
|
name,
|
||||||
self.pre_resolve_timeout,
|
self.pre_resolve_timeout,
|
||||||
)
|
)
|
||||||
.map(|e| e.addrs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_inner(
|
fn resolve_inner(
|
||||||
mut table: MutexGuard<'_, HashMap<String, Entry>>,
|
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
|
||||||
|
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
|
||||||
name: &str,
|
name: &str,
|
||||||
timeout: Option<Duration>,
|
pre_resolve_cache_timeout: Option<Duration>,
|
||||||
) -> Option<Entry> {
|
) -> Option<Vec<IpAddr>> {
|
||||||
let resolved = table.get_mut(name)?;
|
let resolved = fallback_table.get(name)?;
|
||||||
|
|
||||||
debug!("found {name:?} in static table resolver");
|
debug!("lookup hit for \"{name:?}\" in static table resolver");
|
||||||
|
|
||||||
if let Some(pre_resolve_timeout) = timeout {
|
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
||||||
// We had to look this entry up and a pre-resolve duration is defined, so it will
|
// trigger in pre-resolve lookups for the next _timeout_ window if it wasn't already
|
||||||
// trigger in pre-resolve lookups for the next _timeout_ window.
|
// triggering.
|
||||||
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout);
|
if let Some(pre_resolve_timeout) = pre_resolve_cache_timeout {
|
||||||
|
match preresolve_table.get_mut(name) {
|
||||||
|
None => {
|
||||||
|
_ = preresolve_table.insert(
|
||||||
|
name.to_string(),
|
||||||
|
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Not sure how we would get cases where this is Some( ) -- it requires having a
|
||||||
|
// Valid entry in the preresolve table and still doing a lookup against fallback.
|
||||||
|
Some(entry) if matches!(entry.status, PreResolveStatus::ValidUntil(_)) => {
|
||||||
|
_ = preresolve_table.insert(
|
||||||
|
name.to_string(),
|
||||||
|
Entry::new_timeout(resolved.clone(), pre_resolve_timeout),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(resolved.clone())
|
Some(resolved.clone())
|
||||||
}
|
}
|
||||||
@@ -117,13 +199,23 @@ impl StaticResolver {
|
|||||||
impl Resolve for StaticResolver {
|
impl Resolve for StaticResolver {
|
||||||
fn resolve(&self, name: Name) -> Resolving {
|
fn resolve(&self, name: Name) -> Resolving {
|
||||||
debug!("looking up {name:?} in static resolver");
|
debug!("looking up {name:?} in static resolver");
|
||||||
let addr_map = self.static_addr_map.clone();
|
// these should clone arcs, not the actual tables
|
||||||
|
let fallback_addr_map = self.fallback_addr_map.clone();
|
||||||
|
let presesolve_addr_map = self.preresolve_addr_map.clone();
|
||||||
let timeout = self.pre_resolve_timeout;
|
let timeout = self.pre_resolve_timeout;
|
||||||
|
// Also the returned future doesn't try to take the lock on the tables until the
|
||||||
|
// future is awaited, so no blocking issues.
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let addr_map = addr_map.lock().unwrap();
|
let fallback_addr_map = fallback_addr_map.lock().unwrap();
|
||||||
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
|
let presesolve_addr_map = presesolve_addr_map.lock().unwrap();
|
||||||
|
let lookup = match Self::resolve_inner(
|
||||||
|
fallback_addr_map,
|
||||||
|
presesolve_addr_map,
|
||||||
|
name.as_str(),
|
||||||
|
timeout,
|
||||||
|
) {
|
||||||
None => return Err(ResolveError::StaticLookupMiss.into()),
|
None => return Err(ResolveError::StaticLookupMiss.into()),
|
||||||
Some(entry) => entry.addrs,
|
Some(addrs) => addrs,
|
||||||
};
|
};
|
||||||
let addrs: Addrs = Box::new(
|
let addrs: Addrs = Box::new(
|
||||||
lookup
|
lookup
|
||||||
@@ -142,6 +234,7 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -149,7 +242,7 @@ mod test {
|
|||||||
let example_domain = String::from("static.nymvpn.com");
|
let example_domain = String::from("static.nymvpn.com");
|
||||||
|
|
||||||
// lookup for domain for which there is no entry
|
// lookup for domain for which there is no entry
|
||||||
let resolver = StaticResolver::new(HashMap::new());
|
let resolver = StaticResolver::new();
|
||||||
|
|
||||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||||
let result = resolver.resolve(url).await;
|
let result = resolver.resolve(url).await;
|
||||||
@@ -166,7 +259,7 @@ mod test {
|
|||||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||||
|
|
||||||
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
|
||||||
let resolver = StaticResolver::new(addr_map);
|
let resolver = StaticResolver::new().with_fallback(addr_map);
|
||||||
let mut addrs = resolver.resolve(url).await?;
|
let mut addrs = resolver.resolve(url).await?;
|
||||||
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
|
||||||
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
|
||||||
@@ -175,7 +268,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_lookup_pre_resolve() {
|
fn elevate_fallback_to_pre_resolve() {
|
||||||
let example_duration = Duration::from_secs(3);
|
let example_duration = Duration::from_secs(3);
|
||||||
let example_domain = String::from("static.nymvpn.com");
|
let example_domain = String::from("static.nymvpn.com");
|
||||||
let mut addr_map = HashMap::new();
|
let mut addr_map = HashMap::new();
|
||||||
@@ -183,24 +276,23 @@ mod test {
|
|||||||
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
|
||||||
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
|
||||||
|
|
||||||
let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration);
|
let resolver = StaticResolver::new()
|
||||||
|
.with_fallback(addr_map)
|
||||||
|
.with_pre_resolve_timeout(example_duration);
|
||||||
|
|
||||||
// ensure that attempting to pre-resolve without first resolving returns none
|
// ensure that attempting to pre-resolve without first resolving returns none
|
||||||
let result = resolver.pre_resolve(&example_domain);
|
let result = resolver.pre_resolve(&example_domain);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
|
|
||||||
// resolving should now update the pre-resolve validity timeout for the entry
|
// resolving should now update the pre-resolve validity timeout for the entry
|
||||||
let entry = StaticResolver::resolve_inner(
|
let _addrs = resolver
|
||||||
resolver.static_addr_map.lock().unwrap(),
|
.resolve_str(&example_domain)
|
||||||
&example_domain,
|
.expect("entry should exist");
|
||||||
Some(example_duration),
|
assert!(matches!(
|
||||||
)
|
resolver.preresolve_status(&example_domain),
|
||||||
.expect("missing entry???!!!!");
|
Some(PreResolveStatus::ValidUntil(t))
|
||||||
assert!(
|
if t < Instant::now() + example_duration
|
||||||
entry
|
));
|
||||||
.valid_for_pre_resolve_until
|
|
||||||
.is_some_and(|t| t < Instant::now() + example_duration)
|
|
||||||
);
|
|
||||||
|
|
||||||
// check that pre-resolve now returns the expected record
|
// check that pre-resolve now returns the expected record
|
||||||
let addrs = resolver
|
let addrs = resolver
|
||||||
@@ -214,4 +306,139 @@ mod test {
|
|||||||
let result = resolver.pre_resolve(&example_domain);
|
let result = resolver.pre_resolve(&example_domain);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_and_use_preresolve() {
|
||||||
|
let example_duration = Duration::from_secs(3);
|
||||||
|
let example_domains = [
|
||||||
|
String::from("static1.nymvpn.com"),
|
||||||
|
String::from("static2.nymvpn.com"),
|
||||||
|
String::from("preresolve.nymvpn.com"),
|
||||||
|
];
|
||||||
|
let mut addr_map1 = HashMap::new();
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[0].clone(),
|
||||||
|
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||||
|
);
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[1].clone(),
|
||||||
|
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut addr_map2 = HashMap::new();
|
||||||
|
addr_map2.insert(
|
||||||
|
example_domains[1].clone(),
|
||||||
|
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||||
|
);
|
||||||
|
addr_map2.insert(
|
||||||
|
example_domains[2].clone(),
|
||||||
|
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolver = StaticResolver::new()
|
||||||
|
.with_fallback(addr_map1)
|
||||||
|
.with_pre_resolve_timeout(example_duration);
|
||||||
|
|
||||||
|
// Attempting to pre-resolve without setting the table returns none
|
||||||
|
let result = resolver.pre_resolve(&example_domains[0]);
|
||||||
|
assert!(result.is_none());
|
||||||
|
|
||||||
|
resolver.set_preresolve(addr_map2);
|
||||||
|
|
||||||
|
// After setting the pre-resolve, addresses in the the table are returned
|
||||||
|
let result = resolver.pre_resolve(&example_domains[1]);
|
||||||
|
assert!(result.is_some());
|
||||||
|
|
||||||
|
// If the domain wasn't in the pre-resolve table it returns none.
|
||||||
|
let result = resolver.pre_resolve(&example_domains[0]);
|
||||||
|
assert!(result.is_none());
|
||||||
|
|
||||||
|
resolver.clear_preresolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preresolve_with_fallback() {
|
||||||
|
let example_duration = Duration::from_secs(3);
|
||||||
|
let example_domains = [
|
||||||
|
String::from("static1.nymvpn.com"),
|
||||||
|
String::from("static2.nymvpn.com"),
|
||||||
|
String::from("preresolve.nymvpn.com"),
|
||||||
|
];
|
||||||
|
let mut addr_map1 = HashMap::new();
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[0].clone(),
|
||||||
|
vec![Ipv4Addr::new(10, 10, 10, 10).into()],
|
||||||
|
);
|
||||||
|
addr_map1.insert(
|
||||||
|
example_domains[1].clone(),
|
||||||
|
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut addr_map2 = HashMap::new();
|
||||||
|
addr_map2.insert(
|
||||||
|
example_domains[1].clone(),
|
||||||
|
vec![Ipv4Addr::new(1, 1, 1, 1).into()],
|
||||||
|
);
|
||||||
|
addr_map2.insert(
|
||||||
|
example_domains[2].clone(),
|
||||||
|
vec![Ipv4Addr::new(8, 8, 8, 8).into()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolver = StaticResolver::new()
|
||||||
|
.with_fallback(addr_map1)
|
||||||
|
.with_preresolve(addr_map2)
|
||||||
|
.with_pre_resolve_timeout(example_duration);
|
||||||
|
|
||||||
|
// when using both pre-resolve and fallback elevating entries from fallback to pre-resolve
|
||||||
|
// leaves the entries as `Valid`.
|
||||||
|
assert!(matches!(
|
||||||
|
resolver.preresolve_status(&example_domains[1]),
|
||||||
|
Some(PreResolveStatus::Valid)
|
||||||
|
));
|
||||||
|
let _addrs = resolver
|
||||||
|
.resolve_str(&example_domains[1])
|
||||||
|
.expect("entry should exist");
|
||||||
|
assert!(matches!(
|
||||||
|
resolver.preresolve_status(&example_domains[1]),
|
||||||
|
Some(PreResolveStatus::Valid)
|
||||||
|
));
|
||||||
|
|
||||||
|
// entries not already in pre-resolve get elevated with a timeout.
|
||||||
|
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||||
|
let _addrs = resolver
|
||||||
|
.resolve_str(&example_domains[0])
|
||||||
|
.expect("entry should exist");
|
||||||
|
assert!(resolver.preresolve_contains(&example_domains[0]));
|
||||||
|
assert!(matches!(
|
||||||
|
resolver.preresolve_status(&example_domains[0]),
|
||||||
|
Some(PreResolveStatus::ValidUntil(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// clearing the pre-resolve table doesn't impact the fallback table.
|
||||||
|
resolver.clear_preresolve();
|
||||||
|
assert!(!resolver.preresolve_contains(&example_domains[0]));
|
||||||
|
assert!(!resolver.preresolve_contains(&example_domains[1]));
|
||||||
|
assert!(!resolver.preresolve_contains(&example_domains[2]));
|
||||||
|
assert!(!resolver.fallback_contains(&example_domains[0]));
|
||||||
|
assert!(!resolver.fallback_contains(&example_domains[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convenience functions for testing
|
||||||
|
impl StaticResolver {
|
||||||
|
fn preresolve_status(&self, name: &str) -> Option<PreResolveStatus> {
|
||||||
|
self.preresolve_addr_map
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(name)
|
||||||
|
.map(|e| e.status.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preresolve_contains(&self, name: &str) -> bool {
|
||||||
|
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_contains(&self, name: &str) -> bool {
|
||||||
|
self.preresolve_addr_map.lock().unwrap().contains_key(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,21 @@
|
|||||||
|
|
||||||
//! Utilities for and implementation of request tunneling
|
//! Utilities for and implementation of request tunneling
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::{
|
||||||
|
Arc, LazyLock, RwLock,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::ClientBuilder;
|
use crate::{Client, ClientBuilder};
|
||||||
|
|
||||||
|
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
|
||||||
|
|
||||||
// #[cfg(feature = "tunneling")]
|
// #[cfg(feature = "tunneling")]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Front {
|
pub(crate) struct Front {
|
||||||
pub(crate) policy: FrontPolicy,
|
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
|
||||||
enabled: AtomicBool,
|
enabled: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +25,7 @@ impl Clone for Front {
|
|||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
policy: self.policy.clone(),
|
policy: self.policy.clone(),
|
||||||
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
|
enabled: AtomicBool::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,13 +33,30 @@ impl Clone for Front {
|
|||||||
impl Front {
|
impl Front {
|
||||||
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
pub(crate) fn new(policy: FrontPolicy) -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: AtomicBool::new(policy == FrontPolicy::Always),
|
enabled: AtomicBool::new(false),
|
||||||
|
policy: Arc::new(RwLock::new(policy)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn off() -> Self {
|
||||||
|
Self::new(FrontPolicy::Off)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shared() -> Self {
|
||||||
|
let policy = SHARED_FRONTING_POLICY.clone();
|
||||||
|
Self {
|
||||||
|
enabled: AtomicBool::new(false),
|
||||||
policy,
|
policy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_policy(&self, policy: FrontPolicy) {
|
||||||
|
*self.policy.write().unwrap() = policy;
|
||||||
|
self.enabled.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_enabled(&self) -> bool {
|
pub(crate) fn is_enabled(&self) -> bool {
|
||||||
match self.policy {
|
match *self.policy.read().unwrap() {
|
||||||
FrontPolicy::Off => false,
|
FrontPolicy::Off => false,
|
||||||
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
|
||||||
FrontPolicy::Always => true,
|
FrontPolicy::Always => true,
|
||||||
@@ -46,14 +69,13 @@ impl Front {
|
|||||||
if self.is_enabled() {
|
if self.is_enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if matches!(self.policy, FrontPolicy::OnRetry) {
|
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
|
||||||
self.enabled.store(true, Ordering::Relaxed);
|
self.enabled.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Clone)]
|
#[derive(Debug, Default, PartialEq, Clone)]
|
||||||
#[cfg(feature = "tunneling")]
|
|
||||||
/// Policy for when to use domain fronting for HTTP requests.
|
/// Policy for when to use domain fronting for HTTP requests.
|
||||||
pub enum FrontPolicy {
|
pub enum FrontPolicy {
|
||||||
/// Always use domain fronting for all requests.
|
/// Always use domain fronting for all requests.
|
||||||
@@ -66,29 +88,208 @@ pub enum FrontPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientBuilder {
|
impl ClientBuilder {
|
||||||
/// Enable and configure request tunneling for API requests.
|
/// Enable and configure request tunneling for API requests. If no front policy is
|
||||||
#[cfg(feature = "tunneling")]
|
/// provided the shared fronting policy will be used.
|
||||||
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
|
pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
|
||||||
let front = Front::new(policy);
|
let front = if let Some(p) = policy {
|
||||||
|
Front::new(p)
|
||||||
|
} else {
|
||||||
|
Front::shared()
|
||||||
|
};
|
||||||
|
|
||||||
// Check if any of the supplied urls even support fronting
|
// Check if any of the supplied urls even support fronting
|
||||||
if !self.urls.iter().any(|url| url.has_front()) {
|
if !self.urls.iter().any(|url| url.has_front()) {
|
||||||
warn!(
|
warn!(
|
||||||
"fronting is enabled, but none of the supplied urls have configured fronting domains"
|
"fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
|
||||||
|
self.urls
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.front = Some(front);
|
self.front = front;
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Set the policy for enabling fronting. If fronting was previously unset this will set it, and
|
||||||
|
/// make it possible to enable (i.e [`FrontPolicy::Off`] will not enable it).
|
||||||
|
///
|
||||||
|
/// Calling this function sets a custom policy for this client, disconnecting it from the shared
|
||||||
|
/// fronting policy -- i.e. changes applied through [`Client::set_shared_front_policy`] will not
|
||||||
|
/// be impact this client.
|
||||||
|
pub fn set_front_policy(&mut self, policy: FrontPolicy) {
|
||||||
|
self.front.set_policy(policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the fronting policy for this client to follow the shared policy.
|
||||||
|
pub fn use_shared_front_policy(&mut self) {
|
||||||
|
self.front = Front::shared();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the fronting policy for all clients using the shared policy.
|
||||||
|
//
|
||||||
|
// NOTE: this does not reset the per-instance enabled flag like it will when using
|
||||||
|
// [`Front::set_front_policy`]. So if a client is using shared policy with the `OnRetry` policy
|
||||||
|
// and this function is used to swap that policy away from and then back to `OnRetry` the
|
||||||
|
// fronting will still be enabled. Noting this here just in case this triggers any corner cases
|
||||||
|
// down the road.
|
||||||
|
pub fn set_shared_front_policy(policy: FrontPolicy) {
|
||||||
|
*SHARED_FRONTING_POLICY.write().unwrap() = policy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{ApiClientCore, NO_PARAMS, Url};
|
use crate::{ApiClientCore, NO_PARAMS, Url};
|
||||||
|
|
||||||
|
impl Front {
|
||||||
|
pub(crate) fn policy(&self) -> FrontPolicy {
|
||||||
|
self.policy.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Policy can be set for an independent client and the update is applied properly
|
||||||
|
#[test]
|
||||||
|
fn set_policy_independent_client() {
|
||||||
|
let url1 = Url::new(
|
||||||
|
"https://validator.global.ssl.fastly.net",
|
||||||
|
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut client1 = ClientBuilder::new(url1.clone())
|
||||||
|
.unwrap()
|
||||||
|
.with_fronting(Some(FrontPolicy::Off))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||||
|
|
||||||
|
let client2 = ClientBuilder::new(url1.clone())
|
||||||
|
.unwrap()
|
||||||
|
.with_fronting(Some(FrontPolicy::OnRetry))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure that setting the policy for a client it gets properly applied.
|
||||||
|
client1.set_front_policy(FrontPolicy::Always);
|
||||||
|
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||||
|
|
||||||
|
// ensure that setting the policy in a client NOT using the shared policy does NOT update
|
||||||
|
// the policy used by another client.
|
||||||
|
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||||
|
|
||||||
|
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||||
|
// requests
|
||||||
|
let req = client1
|
||||||
|
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_host = url1.host_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
req.headers()
|
||||||
|
.get(reqwest::header::HOST)
|
||||||
|
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||||
|
"{:?} != {:?}",
|
||||||
|
expected_host,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_front = url1.front_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
req.url()
|
||||||
|
.host()
|
||||||
|
.is_some_and(|url| url.to_string() == expected_front),
|
||||||
|
"{:?} != {:?}",
|
||||||
|
expected_front,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Policy can be set for the shared client and the update is applied properly
|
||||||
|
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH THE SHARED POLICY AND AS SUCH CAN HAVE
|
||||||
|
// AN IMPACT ON OTHER TESTS
|
||||||
|
#[test]
|
||||||
|
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||||
|
fn set_policy_shared_client() {
|
||||||
|
let url1 = Url::new(
|
||||||
|
"https://validator.global.ssl.fastly.net",
|
||||||
|
Some(vec!["https://yelp.global.ssl.fastly.net"]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Client::set_shared_front_policy(FrontPolicy::Off);
|
||||||
|
assert!(*SHARED_FRONTING_POLICY.read().unwrap() == FrontPolicy::Off);
|
||||||
|
|
||||||
|
let client1 = ClientBuilder::new(url1.clone())
|
||||||
|
.unwrap()
|
||||||
|
.with_fronting(None)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
assert!(client1.front.policy() == FrontPolicy::Off);
|
||||||
|
|
||||||
|
let mut client2 = ClientBuilder::new(url1.clone())
|
||||||
|
.unwrap()
|
||||||
|
.with_fronting(Some(FrontPolicy::Off))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure that setting the shared policy gets properly applied
|
||||||
|
Client::set_shared_front_policy(FrontPolicy::Always);
|
||||||
|
assert!(client1.front.policy() == FrontPolicy::Always);
|
||||||
|
|
||||||
|
// Setting the shared policy should NOT update clients NOT using the shared policy.
|
||||||
|
assert!(client2.front.policy() == FrontPolicy::Off);
|
||||||
|
|
||||||
|
// Ensure that the policy takes effect and is applied when setting host headers on outgoing
|
||||||
|
// requests
|
||||||
|
let req = client1
|
||||||
|
.create_request(reqwest::Method::GET, &["/"], NO_PARAMS, None::<&()>)
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_host = url1.host_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
req.headers()
|
||||||
|
.get(reqwest::header::HOST)
|
||||||
|
.is_some_and(|h| h.to_str().unwrap() == expected_host),
|
||||||
|
"{:?} != {:?}",
|
||||||
|
expected_host,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected_front = url1.front_str().unwrap();
|
||||||
|
assert!(
|
||||||
|
req.url()
|
||||||
|
.host()
|
||||||
|
.is_some_and(|url| url.to_string() == expected_front),
|
||||||
|
"{:?} != {:?}",
|
||||||
|
expected_front,
|
||||||
|
req,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ensure that setting to the shared policy works
|
||||||
|
client2.use_shared_front_policy();
|
||||||
|
assert!(client2.front.policy() == FrontPolicy::Always);
|
||||||
|
|
||||||
|
// ensure that if the policy is OnRetry then the `enabled` fields are still independent,
|
||||||
|
// despite the policy being shared.
|
||||||
|
Client::set_shared_front_policy(FrontPolicy::OnRetry);
|
||||||
|
assert!(client1.front.policy() == FrontPolicy::OnRetry);
|
||||||
|
assert!(client2.front.policy() == FrontPolicy::OnRetry);
|
||||||
|
|
||||||
|
assert!(!client1.front.is_enabled());
|
||||||
|
assert!(!client2.front.is_enabled());
|
||||||
|
|
||||||
|
client1.front.retry_enable();
|
||||||
|
assert!(client1.front.is_enabled());
|
||||||
|
assert!(!client2.front.is_enabled());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn nym_api_works() {
|
async fn nym_api_works() {
|
||||||
let url1 = Url::new(
|
let url1 = Url::new(
|
||||||
@@ -104,7 +305,7 @@ mod tests {
|
|||||||
|
|
||||||
let client = ClientBuilder::new(url1)
|
let client = ClientBuilder::new(url1)
|
||||||
.expect("bad url")
|
.expect("bad url")
|
||||||
.with_fronting(FrontPolicy::Always)
|
.with_fronting(Some(FrontPolicy::Always))
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to build client");
|
.expect("failed to build client");
|
||||||
|
|
||||||
@@ -140,7 +341,7 @@ mod tests {
|
|||||||
|
|
||||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||||
.expect("bad url")
|
.expect("bad url")
|
||||||
.with_fronting(FrontPolicy::Always)
|
.with_fronting(Some(FrontPolicy::Always))
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to build client");
|
.expect("failed to build client");
|
||||||
|
|
||||||
|
|||||||
+191
-102
@@ -136,6 +136,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use http::header::USER_AGENT;
|
||||||
pub use inventory;
|
pub use inventory;
|
||||||
pub use reqwest;
|
pub use reqwest;
|
||||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||||
@@ -147,6 +148,7 @@ pub mod registry;
|
|||||||
use crate::path::RequestPath;
|
use crate::path::RequestPath;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -161,9 +163,7 @@ use std::time::Duration;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
mod fronted;
|
mod fronted;
|
||||||
@@ -195,6 +195,8 @@ use nym_http_api_client_macro::client_defaults;
|
|||||||
/// high and chatty protocols take a while to complete.
|
/// high and chatty protocols take a while to complete.
|
||||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
client_defaults!(
|
client_defaults!(
|
||||||
priority = -100;
|
priority = -100;
|
||||||
@@ -206,6 +208,24 @@ client_defaults!(
|
|||||||
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
|
user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
|
tracing::info!("Initializing shared HTTP client");
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(target_arch = "wasm32")] {
|
||||||
|
reqwest::ClientBuilder::new().build()
|
||||||
|
.expect("failed to initialize shared http client")
|
||||||
|
} else {
|
||||||
|
let mut builder = default_builder();
|
||||||
|
|
||||||
|
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.expect("failed to initialize shared http client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/// Collection of URL Path Segments
|
/// Collection of URL Path Segments
|
||||||
pub type PathSegments<'a> = &'a [&'a str];
|
pub type PathSegments<'a> = &'a [&'a str];
|
||||||
/// Collection of HTTP Request Parameters
|
/// Collection of HTTP Request Parameters
|
||||||
@@ -327,16 +347,22 @@ pub enum HttpClientError {
|
|||||||
source: reqwest::Error,
|
source: reqwest::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("failed to parse header value: {source}")]
|
||||||
|
InvalidHeaderValue {
|
||||||
|
#[source]
|
||||||
|
source: http::Error,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("failed to send request for {url}: {source}")]
|
#[error("failed to send request for {url}: {source}")]
|
||||||
RequestSendFailure {
|
RequestSendFailure {
|
||||||
url: reqwest::Url,
|
url: Box<reqwest::Url>,
|
||||||
#[source]
|
#[source]
|
||||||
source: ReqwestErrorWrapper,
|
source: ReqwestErrorWrapper,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("failed to read response body from {url}: {source}")]
|
#[error("failed to read response body from {url}: {source}")]
|
||||||
ResponseReadFailure {
|
ResponseReadFailure {
|
||||||
url: reqwest::Url,
|
url: Box<reqwest::Url>,
|
||||||
headers: Box<HeaderMap>,
|
headers: Box<HeaderMap>,
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
#[source]
|
#[source]
|
||||||
@@ -353,7 +379,7 @@ pub enum HttpClientError {
|
|||||||
},
|
},
|
||||||
|
|
||||||
#[error("the requested resource could not be found at {url}")]
|
#[error("the requested resource could not be found at {url}")]
|
||||||
NotFound { url: reqwest::Url },
|
NotFound { url: Box<reqwest::Url> },
|
||||||
|
|
||||||
#[error("attempted to use domain fronting and clone a request containing stream data")]
|
#[error("attempted to use domain fronting and clone a request containing stream data")]
|
||||||
AttemptedToCloneStreamRequest,
|
AttemptedToCloneStreamRequest,
|
||||||
@@ -365,7 +391,7 @@ pub enum HttpClientError {
|
|||||||
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
|
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
|
||||||
)]
|
)]
|
||||||
RequestFailure {
|
RequestFailure {
|
||||||
url: reqwest::Url,
|
url: Box<reqwest::Url>,
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
headers: Box<HeaderMap>,
|
headers: Box<HeaderMap>,
|
||||||
},
|
},
|
||||||
@@ -374,7 +400,7 @@ pub enum HttpClientError {
|
|||||||
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
|
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
|
||||||
)]
|
)]
|
||||||
EmptyResponse {
|
EmptyResponse {
|
||||||
url: reqwest::Url,
|
url: Box<reqwest::Url>,
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
headers: Box<HeaderMap>,
|
headers: Box<HeaderMap>,
|
||||||
},
|
},
|
||||||
@@ -383,7 +409,7 @@ pub enum HttpClientError {
|
|||||||
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
|
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
|
||||||
)]
|
)]
|
||||||
EndpointFailure {
|
EndpointFailure {
|
||||||
url: reqwest::Url,
|
url: Box<reqwest::Url>,
|
||||||
status: StatusCode,
|
status: StatusCode,
|
||||||
headers: Box<HeaderMap>,
|
headers: Box<HeaderMap>,
|
||||||
error: String,
|
error: String,
|
||||||
@@ -453,7 +479,7 @@ impl HttpClientError {
|
|||||||
|
|
||||||
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
|
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
|
||||||
HttpClientError::RequestSendFailure {
|
HttpClientError::RequestSendFailure {
|
||||||
url,
|
url: Box::new(url),
|
||||||
source: ReqwestErrorWrapper(source),
|
source: ReqwestErrorWrapper(source),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,6 +580,19 @@ pub trait ApiClientCore {
|
|||||||
let req = self.create_request(method, path, params, json_body)?;
|
let req = self.create_request(method, path, params, json_body)?;
|
||||||
self.send(req).await
|
self.send(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
|
||||||
|
///
|
||||||
|
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
|
||||||
|
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
|
||||||
|
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
|
||||||
|
/// multiple times.
|
||||||
|
fn maybe_rotate_hosts(&self, offending_url: Option<Url>);
|
||||||
|
|
||||||
|
/// If the fronting policy for the client is set to `OnRetry` this function will enable the
|
||||||
|
/// fronting if not already enabled.
|
||||||
|
#[cfg(feature = "tunneling")]
|
||||||
|
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
|
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
|
||||||
@@ -562,16 +601,18 @@ pub struct ClientBuilder {
|
|||||||
urls: Vec<Url>,
|
urls: Vec<Url>,
|
||||||
|
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
custom_user_agent: bool,
|
custom_user_agent: Option<HeaderValue>,
|
||||||
reqwest_client_builder: reqwest::ClientBuilder,
|
reqwest_client_builder: Option<reqwest::ClientBuilder>,
|
||||||
#[allow(dead_code)] // not dead code, just unused in wasm
|
#[allow(dead_code)] // not dead code, just unused in wasm
|
||||||
use_secure_dns: bool,
|
use_secure_dns: bool,
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
front: Option<fronted::Front>,
|
front: fronted::Front,
|
||||||
|
|
||||||
retry_limit: usize,
|
retry_limit: usize,
|
||||||
serialization: SerializationFormat,
|
serialization: SerializationFormat,
|
||||||
|
|
||||||
|
error: Option<HttpClientError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientBuilder {
|
impl ClientBuilder {
|
||||||
@@ -642,10 +683,10 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
let mut builder = Self::new_with_urls(urls)?;
|
let mut builder = Self::new_with_urls(urls)?;
|
||||||
|
|
||||||
// Enable domain fronting by default (on retry)
|
// Enable domain fronting using the shared fronting policy
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
{
|
{
|
||||||
builder = builder.with_fronting(FrontPolicy::OnRetry);
|
builder = builder.with_fronting(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(builder)
|
Ok(builder)
|
||||||
@@ -659,26 +700,31 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
let urls = Self::check_urls(urls);
|
let urls = Self::check_urls(urls);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
let reqwest_client_builder = reqwest::ClientBuilder::new();
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let reqwest_client_builder = default_builder();
|
|
||||||
|
|
||||||
Ok(ClientBuilder {
|
Ok(ClientBuilder {
|
||||||
urls,
|
urls,
|
||||||
timeout: None,
|
timeout: None,
|
||||||
custom_user_agent: false,
|
custom_user_agent: None,
|
||||||
reqwest_client_builder,
|
reqwest_client_builder: None,
|
||||||
use_secure_dns: true,
|
use_secure_dns: true,
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
front: None,
|
front: fronted::Front::off(),
|
||||||
|
|
||||||
retry_limit: 0,
|
retry_limit: 0,
|
||||||
serialization: SerializationFormat::Json,
|
serialization: SerializationFormat::Json,
|
||||||
|
error: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configure use of an independent HTTP request executor. This prevents use of beneficial
|
||||||
|
/// features like connection pooling under the hood.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn non_shared(mut self) -> Self {
|
||||||
|
if self.reqwest_client_builder.is_none() {
|
||||||
|
self.reqwest_client_builder = Some(default_builder());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add an additional URL to the set usable by this constructed `Client`
|
/// Add an additional URL to the set usable by this constructed `Client`
|
||||||
pub fn add_url(mut self, url: Url) -> Self {
|
pub fn add_url(mut self, url: Url) -> Self {
|
||||||
self.urls.push(url);
|
self.urls.push(url);
|
||||||
@@ -723,7 +769,7 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
/// Provide a pre-configured [`reqwest::ClientBuilder`]
|
/// Provide a pre-configured [`reqwest::ClientBuilder`]
|
||||||
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
|
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
|
||||||
self.reqwest_client_builder = reqwest_builder;
|
self.reqwest_client_builder = Some(reqwest_builder);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,18 +779,12 @@ impl ClientBuilder {
|
|||||||
V: TryInto<HeaderValue>,
|
V: TryInto<HeaderValue>,
|
||||||
V::Error: Into<http::Error>,
|
V::Error: Into<http::Error>,
|
||||||
{
|
{
|
||||||
self.custom_user_agent = true;
|
match value.try_into() {
|
||||||
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
|
Ok(v) => self.custom_user_agent = Some(v),
|
||||||
self
|
Err(err) => {
|
||||||
}
|
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
|
||||||
|
}
|
||||||
/// Override DNS resolution for specific domains to particular IP addresses.
|
}
|
||||||
///
|
|
||||||
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
|
|
||||||
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
|
|
||||||
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,30 +801,33 @@ impl ClientBuilder {
|
|||||||
|
|
||||||
/// Returns a Client that uses this ClientBuilder configuration.
|
/// Returns a Client that uses this ClientBuilder configuration.
|
||||||
pub fn build(self) -> Result<Client, HttpClientError> {
|
pub fn build(self) -> Result<Client, HttpClientError> {
|
||||||
|
if let Some(err) = self.error {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let reqwest_client = self.reqwest_client_builder.build()?;
|
let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
|
||||||
|
|
||||||
// TODO: we should probably be propagating the error rather than panicking,
|
|
||||||
// but that'd break bunch of things due to type changes
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let reqwest_client = {
|
let reqwest_client = self
|
||||||
let mut builder = self.reqwest_client_builder;
|
.reqwest_client_builder
|
||||||
|
.map(|mut builder| {
|
||||||
|
// unless explicitly disabled use the DoT/DoH enabled resolver
|
||||||
|
if self.use_secure_dns {
|
||||||
|
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
||||||
|
}
|
||||||
|
|
||||||
// unless explicitly disabled use the DoT/DoH enabled resolver
|
builder
|
||||||
if self.use_secure_dns {
|
.build()
|
||||||
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
|
.map_err(HttpClientError::reqwest_client_build_error)
|
||||||
}
|
})
|
||||||
|
.transpose()?;
|
||||||
builder
|
|
||||||
.build()
|
|
||||||
.map_err(HttpClientError::reqwest_client_build_error)?
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = Client {
|
let client = Client {
|
||||||
base_urls: self.urls,
|
base_urls: self.urls,
|
||||||
current_idx: Arc::new(AtomicUsize::new(0)),
|
current_idx: Arc::new(AtomicUsize::new(0)),
|
||||||
reqwest_client,
|
reqwest_client,
|
||||||
using_secure_dns: self.use_secure_dns,
|
custom_user_agent: self.custom_user_agent,
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
front: self.front,
|
front: self.front,
|
||||||
@@ -804,11 +847,11 @@ impl ClientBuilder {
|
|||||||
pub struct Client {
|
pub struct Client {
|
||||||
base_urls: Vec<Url>,
|
base_urls: Vec<Url>,
|
||||||
current_idx: Arc<AtomicUsize>,
|
current_idx: Arc<AtomicUsize>,
|
||||||
reqwest_client: reqwest::Client,
|
reqwest_client: Option<reqwest::Client>,
|
||||||
using_secure_dns: bool,
|
custom_user_agent: Option<HeaderValue>,
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
front: Option<fronted::Front>,
|
front: fronted::Front,
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
request_timeout: Duration,
|
request_timeout: Duration,
|
||||||
@@ -862,8 +905,8 @@ impl Client {
|
|||||||
Client {
|
Client {
|
||||||
base_urls: vec![new_url],
|
base_urls: vec![new_url],
|
||||||
current_idx: Arc::new(Default::default()),
|
current_idx: Arc::new(Default::default()),
|
||||||
reqwest_client: self.reqwest_client.clone(),
|
reqwest_client: None,
|
||||||
using_secure_dns: self.using_secure_dns,
|
custom_user_agent: None,
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
front: self.front.clone(),
|
front: self.front.clone(),
|
||||||
@@ -897,9 +940,7 @@ impl Client {
|
|||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
fn matches_current_host(&self, url: &Url) -> bool {
|
fn matches_current_host(&self, url: &Url) -> bool {
|
||||||
if let Some(ref front) = self.front
|
if self.front.is_enabled() {
|
||||||
&& front.is_enabled()
|
|
||||||
{
|
|
||||||
url.host_str() == self.current_url().front_str()
|
url.host_str() == self.current_url().front_str()
|
||||||
} else {
|
} else {
|
||||||
url.host_str() == self.current_url().host_str()
|
url.host_str() == self.current_url().host_str()
|
||||||
@@ -926,9 +967,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
if let Some(ref front) = self.front
|
if self.front.is_enabled() {
|
||||||
&& front.is_enabled()
|
|
||||||
{
|
|
||||||
// if we are using fronting, try updating to the next front
|
// if we are using fronting, try updating to the next front
|
||||||
let url = self.current_url();
|
let url = self.current_url();
|
||||||
|
|
||||||
@@ -948,9 +987,7 @@ impl Client {
|
|||||||
|
|
||||||
// if fronting is enabled we want to update to a host that has fronts configured
|
// if fronting is enabled we want to update to a host that has fronts configured
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
if let Some(ref front) = self.front
|
if self.front.is_enabled() {
|
||||||
&& front.is_enabled()
|
|
||||||
{
|
|
||||||
while next != orig {
|
while next != orig {
|
||||||
if self.base_urls[next].has_front() {
|
if self.base_urls[next].has_front() {
|
||||||
// we have a front for the next host, so we can use it
|
// we have a front for the next host, so we can use it
|
||||||
@@ -981,14 +1018,12 @@ impl Client {
|
|||||||
/// this method. For example, if the client is configured to rotate hosts after each error, this
|
/// this method. For example, if the client is configured to rotate hosts after each error, this
|
||||||
/// method should be called after the host has been updated -- i.e. as part of the subsequent
|
/// method should be called after the host has been updated -- i.e. as part of the subsequent
|
||||||
/// send.
|
/// send.
|
||||||
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
|
||||||
let url = self.current_url();
|
let url = self.current_url();
|
||||||
r.url_mut().set_host(url.host_str()).unwrap();
|
r.url_mut().set_host(url.host_str()).unwrap();
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
if let Some(ref front) = self.front
|
if self.front.is_enabled() {
|
||||||
&& front.is_enabled()
|
|
||||||
{
|
|
||||||
if let Some(front_host) = url.front_str() {
|
if let Some(front_host) = url.front_str() {
|
||||||
if let Some(actual_host) = url.host_str() {
|
if let Some(actual_host) = url.host_str() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -1008,6 +1043,13 @@ impl Client {
|
|||||||
.headers_mut()
|
.headers_mut()
|
||||||
.insert(reqwest::header::HOST, actual_host_header);
|
.insert(reqwest::header::HOST, actual_host_header);
|
||||||
|
|
||||||
|
// Set a custom header to capture the outer host (used in the SNI) of the request
|
||||||
|
let front_host_header: HeaderValue =
|
||||||
|
front_host.parse().unwrap_or(HeaderValue::from_static(""));
|
||||||
|
_ = r
|
||||||
|
.headers_mut()
|
||||||
|
.insert(NYM_OUTER_SNI_HEADER, front_host_header);
|
||||||
|
|
||||||
return (url.as_str(), url.front_str());
|
return (url.as_str(), url.front_str());
|
||||||
} else {
|
} else {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
@@ -1048,12 +1090,21 @@ impl ApiClientCore for Client {
|
|||||||
|
|
||||||
self.apply_hosts_to_req(&mut req);
|
self.apply_hosts_to_req(&mut req);
|
||||||
|
|
||||||
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
|
let client = if let Some(client) = &self.reqwest_client {
|
||||||
|
client.clone()
|
||||||
|
} else {
|
||||||
|
SHARED_CLIENT.clone()
|
||||||
|
};
|
||||||
|
let mut rb = RequestBuilder::from_parts(client, req);
|
||||||
|
|
||||||
rb = rb
|
rb = rb
|
||||||
.header(ACCEPT, self.serialization.content_type())
|
.header(ACCEPT, self.serialization.content_type())
|
||||||
.header(CONTENT_TYPE, self.serialization.content_type());
|
.header(CONTENT_TYPE, self.serialization.content_type());
|
||||||
|
|
||||||
|
if let Some(user_agent) = &self.custom_user_agent {
|
||||||
|
rb = rb.header(USER_AGENT, user_agent.clone());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(body) = body {
|
if let Some(body) = body {
|
||||||
match self.serialization {
|
match self.serialization {
|
||||||
SerializationFormat::Json => {
|
SerializationFormat::Json => {
|
||||||
@@ -1096,16 +1147,19 @@ impl ApiClientCore for Client {
|
|||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let response: Result<Response, HttpClientError> = {
|
let response: Result<Response, HttpClientError> = {
|
||||||
Ok(wasmtimer::tokio::timeout(
|
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||||
self.request_timeout,
|
Ok(
|
||||||
self.reqwest_client.execute(req),
|
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
|
||||||
|
.await
|
||||||
|
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
|
||||||
)
|
)
|
||||||
.await
|
|
||||||
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let response = self.reqwest_client.execute(req).await;
|
let response = {
|
||||||
|
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||||
|
client.execute(req).await
|
||||||
|
};
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(resp) => return Ok(resp),
|
Ok(resp) => return Ok(resp),
|
||||||
@@ -1121,20 +1175,10 @@ impl ApiClientCore for Client {
|
|||||||
|
|
||||||
if is_network_err {
|
if is_network_err {
|
||||||
// if we have multiple urls, update to the next
|
// if we have multiple urls, update to the next
|
||||||
self.update_host(Some(url.clone()));
|
self.maybe_rotate_hosts(Some(url.clone()));
|
||||||
|
|
||||||
#[cfg(feature = "tunneling")]
|
#[cfg(feature = "tunneling")]
|
||||||
if let Some(ref front) = self.front {
|
self.maybe_enable_fronting(("network", url.as_str(), &err));
|
||||||
// If fronting is set to be enabled on error, enable domain fronting as we
|
|
||||||
// have encountered an error.
|
|
||||||
let was_enabled = front.is_enabled();
|
|
||||||
front.retry_enable();
|
|
||||||
if !was_enabled && front.is_enabled() {
|
|
||||||
tracing::info!(
|
|
||||||
"Domain fronting activated after connection failure: {err}",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if attempts < self.retry_limit {
|
if attempts < self.retry_limit {
|
||||||
@@ -1158,6 +1202,21 @@ impl ApiClientCore for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_rotate_hosts(&self, offending: Option<Url>) {
|
||||||
|
self.update_host(offending);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tunneling")]
|
||||||
|
fn maybe_enable_fronting(&self, context: impl std::fmt::Debug) {
|
||||||
|
// If fronting is set to be OnRetry, enable domain fronting as we
|
||||||
|
// have encountered an error.
|
||||||
|
let was_enabled = self.front.is_enabled();
|
||||||
|
self.front.retry_enable();
|
||||||
|
if !was_enabled && self.front.is_enabled() {
|
||||||
|
tracing::debug!("Domain fronting activated after failure: {context:?}",);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Common usage functionality for the http client.
|
/// Common usage functionality for the http client.
|
||||||
@@ -1310,6 +1369,35 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
self.get_response(path, params).await
|
self.get_response(path, params).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt to parse a response object from an HTTP response
|
||||||
|
async fn parse_response<T>(
|
||||||
|
&self,
|
||||||
|
res: Response,
|
||||||
|
allow_empty: bool,
|
||||||
|
) -> Result<T, HttpClientError>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let url = Url::from(res.url());
|
||||||
|
parse_response(res, allow_empty).await.inspect_err(|e| {
|
||||||
|
if matches!(
|
||||||
|
// if we encounter a read error while we attempt to parse it could be caused by censorship and we should
|
||||||
|
// rotate hosts / enable fronting.
|
||||||
|
e,
|
||||||
|
HttpClientError::ResponseReadFailure {
|
||||||
|
url: _,
|
||||||
|
headers: _,
|
||||||
|
status: _,
|
||||||
|
source: _,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
self.maybe_rotate_hosts(Some(url.clone()));
|
||||||
|
#[cfg(feature = "tunneling")]
|
||||||
|
self.maybe_enable_fronting(("parse/read", url.as_str(), e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||||
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
|
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
|
||||||
/// into the provided type `T` based on the content type header
|
/// into the provided type `T` based on the content type header
|
||||||
@@ -1327,7 +1415,8 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
let res = self
|
let res = self
|
||||||
.send_request(reqwest::Method::GET, path, params, None::<&()>)
|
.send_request(reqwest::Method::GET, path, params, None::<&()>)
|
||||||
.await?;
|
.await?;
|
||||||
parse_response(res, false).await
|
|
||||||
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||||
@@ -1349,7 +1438,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
let res = self
|
let res = self
|
||||||
.send_request(reqwest::Method::POST, path, params, Some(json_body))
|
.send_request(reqwest::Method::POST, path, params, Some(json_body))
|
||||||
.await?;
|
.await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
|
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
|
||||||
@@ -1369,7 +1458,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
let res = self
|
let res = self
|
||||||
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
|
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
|
||||||
.await?;
|
.await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
|
||||||
@@ -1391,7 +1480,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
let res = self
|
let res = self
|
||||||
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
|
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
|
||||||
.await?;
|
.await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||||
@@ -1403,7 +1492,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
{
|
{
|
||||||
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
|
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
|
||||||
let res = self.send(req).await?;
|
let res = self.send(req).await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||||
@@ -1420,7 +1509,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
{
|
{
|
||||||
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
|
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
|
||||||
let res = self.send(req).await?;
|
let res = self.send(req).await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `delete` json data from the provided absolute endpoint, e.g.
|
/// `delete` json data from the provided absolute endpoint, e.g.
|
||||||
@@ -1432,7 +1521,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
{
|
{
|
||||||
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
|
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
|
||||||
let res = self.send(req).await?;
|
let res = self.send(req).await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
|
||||||
@@ -1450,7 +1539,7 @@ pub trait ApiClient: ApiClientCore {
|
|||||||
let req =
|
let req =
|
||||||
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
|
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
|
||||||
let res = self.send(req).await?;
|
let res = self.send(req).await?;
|
||||||
parse_response(res, false).await
|
self.parse_response(res, false).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1517,7 +1606,7 @@ where
|
|||||||
|
|
||||||
if !allow_empty && let Some(0) = res.content_length() {
|
if !allow_empty && let Some(0) = res.content_length() {
|
||||||
return Err(HttpClientError::EmptyResponse {
|
return Err(HttpClientError::EmptyResponse {
|
||||||
url,
|
url: Box::new(url),
|
||||||
status,
|
status,
|
||||||
headers: Box::new(headers),
|
headers: Box::new(headers),
|
||||||
});
|
});
|
||||||
@@ -1530,25 +1619,25 @@ where
|
|||||||
.bytes()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.map_err(|source| HttpClientError::ResponseReadFailure {
|
.map_err(|source| HttpClientError::ResponseReadFailure {
|
||||||
url,
|
url: Box::new(url),
|
||||||
headers: Box::new(headers.clone()),
|
headers: Box::new(headers.clone()),
|
||||||
status,
|
status,
|
||||||
source: ReqwestErrorWrapper(source),
|
source: ReqwestErrorWrapper(source),
|
||||||
})?;
|
})?;
|
||||||
decode_raw_response(&headers, full)
|
decode_raw_response(&headers, full)
|
||||||
} else if res.status() == StatusCode::NOT_FOUND {
|
} else if res.status() == StatusCode::NOT_FOUND {
|
||||||
Err(HttpClientError::NotFound { url })
|
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||||
} else {
|
} else {
|
||||||
let Ok(plaintext) = res.text().await else {
|
let Ok(plaintext) = res.text().await else {
|
||||||
return Err(HttpClientError::RequestFailure {
|
return Err(HttpClientError::RequestFailure {
|
||||||
url,
|
url: Box::new(url),
|
||||||
status,
|
status,
|
||||||
headers: Box::new(headers),
|
headers: Box::new(headers),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Err(HttpClientError::EndpointFailure {
|
Err(HttpClientError::EndpointFailure {
|
||||||
url,
|
url: Box::new(url),
|
||||||
status,
|
status,
|
||||||
headers: Box::new(headers),
|
headers: Box::new(headers),
|
||||||
error: plaintext,
|
error: plaintext,
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ fn sanitizing_urls() {
|
|||||||
// - on error without retries is where we have multiple urls, is the url updated?
|
// - on error without retries is where we have multiple urls, is the url updated?
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // test relies on external services being available and behaving in a specific way.
|
#[cfg(any())] // #[ignore] we run ignore assuming it just means slow in Ci/CD -_-
|
||||||
|
// test relies on external services being available and behaving in a specific way.
|
||||||
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let client = ClientBuilder::new_with_urls(vec![
|
let client = ClientBuilder::new_with_urls(vec![
|
||||||
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
|
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
|
||||||
@@ -199,7 +200,7 @@ fn fronted_host_updating() {
|
|||||||
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
|
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
|
||||||
let mut client = ClientBuilder::new(url)
|
let mut client = ClientBuilder::new(url)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_fronting(crate::fronted::FrontPolicy::Always)
|
.with_fronting(Some(crate::fronted::FrontPolicy::Always))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,16 @@ impl From<reqwest::Url> for Url {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&reqwest::Url> for Url {
|
||||||
|
fn from(url: &url::Url) -> Self {
|
||||||
|
Self {
|
||||||
|
url: url.clone(),
|
||||||
|
fronts: None,
|
||||||
|
current_front: Arc::new(AtomicUsize::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AsRef<url::Url> for Url {
|
impl AsRef<url::Url> for Url {
|
||||||
fn as_ref(&self) -> &url::Url {
|
fn as_ref(&self) -> &url::Url {
|
||||||
&self.url
|
&self.url
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ edition.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.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]
|
[dependencies]
|
||||||
dotenvy = { workspace = true, optional = true }
|
dotenvy = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
|||||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||||
},
|
},
|
||||||
|
ApiUrlConst {
|
||||||
|
url: "https://cdn1.media-platform.net/api/",
|
||||||
|
front_hosts: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = { workspace = true }
|
|||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "nym_kcp"
|
name = "nym_kcp"
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
|||||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||||
|
|
||||||
rand = "0.9.2"
|
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
|
||||||
|
rand09 = { workspace = true }
|
||||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ use nym_kkt::{
|
|||||||
responder_ingest_message, responder_process,
|
responder_ingest_message, responder_process,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand09::prelude::*;
|
||||||
|
|
||||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut s: [u8; 32] = [0u8; 32];
|
let mut s: [u8; 32] = [0u8; 32];
|
||||||
rand::rng().fill_bytes(&mut s);
|
rand09::rng().fill_bytes(&mut s);
|
||||||
ed25519::KeyPair::from_secret(s, 0)
|
ed25519::KeyPair::from_secret(s, 0)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -33,13 +33,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
|||||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
// generate ed25519 keys
|
// generate ed25519 keys
|
||||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||||
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut i_context, i_frame) =
|
let (i_context, i_frame) =
|
||||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||||
|
|
||||||
c.bench_function(
|
c.bench_function(
|
||||||
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut r_context, _) =
|
let (r_context, _) =
|
||||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||||
|
|
||||||
c.bench_function(
|
c.bench_function(
|
||||||
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
responder_process(
|
responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
initiator_ingest_response(
|
initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let obtained_key = initiator_ingest_response(
|
let obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
}
|
}
|
||||||
// Initiator, OneWay
|
// Initiator, OneWay
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
KKTMode::OneWay,
|
KKTMode::OneWay,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&r_context,
|
&r_context,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
None,
|
None,
|
||||||
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
responder_process(
|
responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
initiator_ingest_response(
|
initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
KKTMode::Mutual,
|
KKTMode::Mutual,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&r_context,
|
&r_context,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
Some(&i_dir_hash),
|
Some(&i_dir_hash),
|
||||||
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
responder_process(
|
responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
initiator_ingest_response(
|
initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let obtained_key = initiator_ingest_response(
|
let obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&r_frame,
|
&r_frame,
|
||||||
&r_frame.context().unwrap(),
|
&r_frame.context().unwrap(),
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
|
|||||||
use blake3::Hasher;
|
use blake3::Hasher;
|
||||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||||
use nym_crypto::asymmetric::x25519;
|
use nym_crypto::asymmetric::x25519;
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand09::{CryptoRng, RngCore};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Zeroize)]
|
#[derive(Clone, Copy, Zeroize)]
|
||||||
@@ -182,8 +182,7 @@ mod test {
|
|||||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||||
key_utils::generate_keypair_x25519,
|
key_utils::generate_keypair_x25519,
|
||||||
};
|
};
|
||||||
use rand::{RngCore, SeedableRng, rng};
|
use rand09::{RngCore, SeedableRng, rng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keygen() {
|
fn test_keygen() {
|
||||||
@@ -227,7 +226,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
|
||||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||||
let aad = b"my-amazing-aad";
|
let aad = b"my-amazing-aad";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||||||
use classic_mceliece_rust::keypair_boxed;
|
use classic_mceliece_rust::keypair_boxed;
|
||||||
|
|
||||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand09::{CryptoRng, RngCore};
|
||||||
|
|
||||||
pub fn generate_keypair_ed25519<R>(
|
pub fn generate_keypair_ed25519<R>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
@@ -61,7 +61,6 @@ pub fn generate_keypair_mceliece<'a, R>(
|
|||||||
classic_mceliece_rust::PublicKey<'a>,
|
classic_mceliece_rust::PublicKey<'a>,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
// this is annoying because mceliece lib uses rand 0.8.5...
|
|
||||||
R: RngCore + CryptoRng,
|
R: RngCore + CryptoRng,
|
||||||
{
|
{
|
||||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||||
|
|||||||
+14
-15
@@ -9,7 +9,7 @@
|
|||||||
//! The underlying KKT protocol is implemented in the `session` module.
|
//! The underlying KKT protocol is implemented in the `session` module.
|
||||||
|
|
||||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand09::{CryptoRng, RngCore};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||||
@@ -33,7 +33,7 @@ use crate::frame::KKTFrame;
|
|||||||
/// The request will be signed with the provided signing key.
|
/// The request will be signed with the provided signing key.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `rng` - Random number generator
|
/// * `rng` - random number generator
|
||||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||||
@@ -90,7 +90,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
|
|||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let gateway_kem_key = validate_kem_response(
|
/// let gateway_kem_key = validate_kem_response(
|
||||||
/// &mut context,
|
/// &context,
|
||||||
/// &session_secret,
|
/// &session_secret,
|
||||||
/// &gateway_verification_key,
|
/// &gateway_verification_key,
|
||||||
/// &expected_hash_from_directory,
|
/// &expected_hash_from_directory,
|
||||||
@@ -199,14 +199,14 @@ mod tests {
|
|||||||
|
|
||||||
fn random_x25519_key() -> x25519::PrivateKey {
|
fn random_x25519_key() -> x25519::PrivateKey {
|
||||||
let mut bytes = [0u8; 32];
|
let mut bytes = [0u8; 32];
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
rng.fill_bytes(&mut bytes);
|
rng.fill_bytes(&mut bytes);
|
||||||
x25519::PrivateKey::from_secret(bytes)
|
x25519::PrivateKey::from_secret(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kkt_wrappers_oneway_authenticated() {
|
fn test_kkt_wrappers_oneway_authenticated() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
// Generate Ed25519 keypairs for both parties
|
// Generate Ed25519 keypairs for both parties
|
||||||
let mut initiator_secret = [0u8; 32];
|
let mut initiator_secret = [0u8; 32];
|
||||||
@@ -241,7 +241,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Client: Request KEM key
|
// Client: Request KEM key
|
||||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
let (session_key, context, request_frame_ciphertext) = request_kem_key(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
ed25519_init.private_key(),
|
ed25519_init.private_key(),
|
||||||
@@ -262,7 +262,7 @@ mod tests {
|
|||||||
|
|
||||||
// Client: Validate response
|
// Client: Validate response
|
||||||
let obtained_key = validate_kem_response(
|
let obtained_key = validate_kem_response(
|
||||||
&mut context,
|
&context,
|
||||||
&session_key,
|
&session_key,
|
||||||
ed25519_resp.public_key(),
|
ed25519_resp.public_key(),
|
||||||
&key_hash,
|
&key_hash,
|
||||||
@@ -276,7 +276,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kkt_wrappers_anonymous() {
|
fn test_kkt_wrappers_anonymous() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
// Only responder has keys
|
// Only responder has keys
|
||||||
let mut responder_secret = [0u8; 32];
|
let mut responder_secret = [0u8; 32];
|
||||||
@@ -304,8 +304,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Anonymous initiator
|
// Anonymous initiator
|
||||||
let (mut context, request_frame) =
|
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
|
||||||
|
|
||||||
// Generate the session's shared secret and encrypt the Initiator's request
|
// Generate the session's shared secret and encrypt the Initiator's request
|
||||||
let (session_secret, encrypted_request_bytes) =
|
let (session_secret, encrypted_request_bytes) =
|
||||||
@@ -324,7 +323,7 @@ mod tests {
|
|||||||
|
|
||||||
// Initiator: Validate response
|
// Initiator: Validate response
|
||||||
let obtained_key = validate_kem_response(
|
let obtained_key = validate_kem_response(
|
||||||
&mut context,
|
&context,
|
||||||
&session_secret,
|
&session_secret,
|
||||||
responder_keypair.public_key(),
|
responder_keypair.public_key(),
|
||||||
&key_hash,
|
&key_hash,
|
||||||
@@ -337,7 +336,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_signature_rejected() {
|
fn test_invalid_signature_rejected() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
let mut initiator_secret = [0u8; 32];
|
let mut initiator_secret = [0u8; 32];
|
||||||
rng.fill_bytes(&mut initiator_secret);
|
rng.fill_bytes(&mut initiator_secret);
|
||||||
@@ -390,7 +389,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_hash_mismatch_rejected() {
|
fn test_hash_mismatch_rejected() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
let mut initiator_secret = [0u8; 32];
|
let mut initiator_secret = [0u8; 32];
|
||||||
rng.fill_bytes(&mut initiator_secret);
|
rng.fill_bytes(&mut initiator_secret);
|
||||||
@@ -417,7 +416,7 @@ mod tests {
|
|||||||
// Use WRONG hash
|
// Use WRONG hash
|
||||||
let wrong_hash = [0u8; 32];
|
let wrong_hash = [0u8; 32];
|
||||||
|
|
||||||
let (session_key, mut context, request_frame) = request_kem_key(
|
let (session_key, context, request_frame) = request_kem_key(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
initiator_keypair.private_key(),
|
initiator_keypair.private_key(),
|
||||||
@@ -437,7 +436,7 @@ mod tests {
|
|||||||
|
|
||||||
// Client validates with WRONG hash
|
// Client validates with WRONG hash
|
||||||
let result = validate_kem_response(
|
let result = validate_kem_response(
|
||||||
&mut context,
|
&context,
|
||||||
&session_key,
|
&session_key,
|
||||||
responder_keypair.public_key(),
|
responder_keypair.public_key(),
|
||||||
&wrong_hash, // Wrong!
|
&wrong_hash, // Wrong!
|
||||||
|
|||||||
+26
-26
@@ -38,7 +38,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kkt_psq_e2e_clear() {
|
fn test_kkt_psq_e2e_clear() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
// generate ed25519 keys
|
// generate ed25519 keys
|
||||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||||
@@ -106,18 +106,18 @@ mod test {
|
|||||||
|
|
||||||
// Anonymous Initiator, OneWay
|
// Anonymous Initiator, OneWay
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) =
|
let (i_context, i_frame) =
|
||||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||||
|
|
||||||
let i_frame_bytes = i_frame.to_bytes();
|
let i_frame_bytes = i_frame.to_bytes();
|
||||||
|
|
||||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||||
|
|
||||||
let (mut r_context, _) =
|
let (r_context, _) =
|
||||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -129,7 +129,7 @@ mod test {
|
|||||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -141,7 +141,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
// Initiator, OneWay
|
// Initiator, OneWay
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
crate::context::KKTMode::OneWay,
|
crate::context::KKTMode::OneWay,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -154,7 +154,7 @@ mod test {
|
|||||||
|
|
||||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&r_context,
|
&r_context,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
None,
|
None,
|
||||||
@@ -165,7 +165,7 @@ mod test {
|
|||||||
assert!(r_obtained_key.is_none());
|
assert!(r_obtained_key.is_none());
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -177,7 +177,7 @@ mod test {
|
|||||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -190,7 +190,7 @@ mod test {
|
|||||||
|
|
||||||
// Initiator, Mutual
|
// Initiator, Mutual
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
crate::context::KKTMode::Mutual,
|
crate::context::KKTMode::Mutual,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -203,7 +203,7 @@ mod test {
|
|||||||
|
|
||||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&r_context,
|
&r_context,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
Some(&i_dir_hash),
|
Some(&i_dir_hash),
|
||||||
@@ -214,7 +214,7 @@ mod test {
|
|||||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -226,7 +226,7 @@ mod test {
|
|||||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -241,7 +241,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kkt_psq_e2e_encrypted() {
|
fn test_kkt_psq_e2e_encrypted() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand09::rng();
|
||||||
|
|
||||||
// generate ed25519 keys
|
// generate ed25519 keys
|
||||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||||
@@ -312,7 +312,7 @@ mod test {
|
|||||||
|
|
||||||
// Anonymous Initiator, OneWay
|
// Anonymous Initiator, OneWay
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) =
|
let (i_context, i_frame) =
|
||||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||||
|
|
||||||
// encryption - initiator frame
|
// encryption - initiator frame
|
||||||
@@ -330,11 +330,11 @@ mod test {
|
|||||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut r_context, _) =
|
let (r_context, _) =
|
||||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -352,7 +352,7 @@ mod test {
|
|||||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -364,7 +364,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
// Initiator, OneWay
|
// Initiator, OneWay
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
crate::context::KKTMode::OneWay,
|
crate::context::KKTMode::OneWay,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -388,7 +388,7 @@ mod test {
|
|||||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&r_context,
|
&r_context,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
None,
|
None,
|
||||||
@@ -399,7 +399,7 @@ mod test {
|
|||||||
assert!(r_obtained_key.is_none());
|
assert!(r_obtained_key.is_none());
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -417,7 +417,7 @@ mod test {
|
|||||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
@@ -430,7 +430,7 @@ mod test {
|
|||||||
|
|
||||||
// Initiator, Mutual
|
// Initiator, Mutual
|
||||||
{
|
{
|
||||||
let (mut i_context, i_frame) = initiator_process(
|
let (i_context, i_frame) = initiator_process(
|
||||||
&mut rng,
|
&mut rng,
|
||||||
crate::context::KKTMode::Mutual,
|
crate::context::KKTMode::Mutual,
|
||||||
ciphersuite,
|
ciphersuite,
|
||||||
@@ -454,7 +454,7 @@ mod test {
|
|||||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
Some(initiator_ed25519_keypair.public_key()),
|
Some(initiator_ed25519_keypair.public_key()),
|
||||||
Some(&i_dir_hash),
|
Some(&i_dir_hash),
|
||||||
@@ -465,7 +465,7 @@ mod test {
|
|||||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||||
|
|
||||||
let r_frame = responder_process(
|
let r_frame = responder_process(
|
||||||
&mut r_context,
|
&r_context,
|
||||||
i_frame_r.session_id(),
|
i_frame_r.session_id(),
|
||||||
responder_ed25519_keypair.private_key(),
|
responder_ed25519_keypair.private_key(),
|
||||||
&responder_kem_public_key,
|
&responder_kem_public_key,
|
||||||
@@ -483,7 +483,7 @@ mod test {
|
|||||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||||
|
|
||||||
let i_obtained_key = initiator_ingest_response(
|
let i_obtained_key = initiator_ingest_response(
|
||||||
&mut i_context,
|
&i_context,
|
||||||
&i_frame_r,
|
&i_frame_r,
|
||||||
&i_context_r,
|
&i_context_r,
|
||||||
responder_ed25519_keypair.public_key(),
|
responder_ed25519_keypair.public_key(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand09::{CryptoRng, RngCore};
|
||||||
|
|
||||||
use crate::frame::KKTSessionId;
|
use crate::frame::KKTSessionId;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -73,7 +73,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn initiator_ingest_response<'a>(
|
pub fn initiator_ingest_response<'a>(
|
||||||
own_context: &mut KKTContext,
|
own_context: &KKTContext,
|
||||||
remote_frame: &KKTFrame,
|
remote_frame: &KKTFrame,
|
||||||
remote_context: &KKTContext,
|
remote_context: &KKTContext,
|
||||||
remote_verification_key: &ed25519::PublicKey,
|
remote_verification_key: &ed25519::PublicKey,
|
||||||
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn responder_process<'a>(
|
pub fn responder_process<'a>(
|
||||||
own_context: &mut KKTContext,
|
own_context: &KKTContext,
|
||||||
session_id: KKTSessionId,
|
session_id: KKTSessionId,
|
||||||
signing_key: &ed25519::PrivateKey,
|
signing_key: &ed25519::PrivateKey,
|
||||||
encapsulation_key: &EncapsulationKey<'a>,
|
encapsulation_key: &EncapsulationKey<'a>,
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ readme.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { workspace = true, features = ["net"] }
|
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||||
nym-test-utils = { path = "../test-utils", optional = true }
|
nym-test-utils = { path = "../test-utils", optional = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
io-mocks = ["nym-test-utils"]
|
io-mocks = ["nym-test-utils"]
|
||||||
|
|||||||
@@ -4,15 +4,100 @@
|
|||||||
#[cfg(feature = "io-mocks")]
|
#[cfg(feature = "io-mocks")]
|
||||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
// only used in internal code (and tests)
|
// only used in internal code (and tests)
|
||||||
#[allow(async_fn_in_trait)]
|
#[allow(async_fn_in_trait)]
|
||||||
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
pub trait LpTransport: Sized {
|
||||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
||||||
|
|
||||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
||||||
|
|
||||||
|
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
|
||||||
|
///
|
||||||
|
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `packet_data` - The serialised LP packet to send
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error on network transmission fails.
|
||||||
|
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
|
||||||
|
|
||||||
|
/// Receives an LP packet from a TCP stream with length-prefixed framing.
|
||||||
|
///
|
||||||
|
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error on network transmission fails.
|
||||||
|
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_serialised_packet_async_write<W>(
|
||||||
|
writer: &mut W,
|
||||||
|
packet_data: &[u8],
|
||||||
|
) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
// Send 4-byte length prefix (u32 big-endian)
|
||||||
|
let len = packet_data.len() as u32;
|
||||||
|
writer
|
||||||
|
.write_all(&len.to_be_bytes())
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||||
|
|
||||||
|
// Send the actual packet data
|
||||||
|
writer
|
||||||
|
.write_all(packet_data)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||||
|
|
||||||
|
// Flush to ensure data is sent immediately
|
||||||
|
writer
|
||||||
|
.flush()
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
"Sent LP packet ({} bytes + 4 byte header)",
|
||||||
|
packet_data.len()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin,
|
||||||
|
{
|
||||||
|
// Read 4-byte length prefix (u32 big-endian)
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
reader
|
||||||
|
.read_exact(&mut len_buf)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||||
|
|
||||||
|
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
|
||||||
|
// Sanity check to prevent huge allocations
|
||||||
|
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||||
|
if packet_len > MAX_PACKET_SIZE {
|
||||||
|
return Err(std::io::Error::other(format!(
|
||||||
|
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the actual packet data
|
||||||
|
let mut packet_buf = vec![0u8; packet_len];
|
||||||
|
reader
|
||||||
|
.read_exact(&mut packet_buf)
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||||
|
|
||||||
|
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||||
|
Ok(packet_buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LpTransport for TcpStream {
|
impl LpTransport for TcpStream {
|
||||||
@@ -24,6 +109,14 @@ impl LpTransport for TcpStream {
|
|||||||
// Set TCP_NODELAY for low latency
|
// Set TCP_NODELAY for low latency
|
||||||
self.set_nodelay(nodelay)
|
self.set_nodelay(nodelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||||
|
send_serialised_packet_async_write(self, packet_data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||||
|
receive_raw_packet_async_read(self).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "io-mocks")]
|
#[cfg(feature = "io-mocks")]
|
||||||
@@ -35,4 +128,12 @@ impl LpTransport for MockIOStream {
|
|||||||
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||||
|
send_serialised_packet_async_write(self, packet_data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||||
|
receive_raw_packet_async_read(self).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ sha2 = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||||
rand09 = { package = "rand", version = "0.9.2" }
|
rand09 = { workspace = true }
|
||||||
|
|
||||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||||
nym-kkt = { path = "../nym-kkt" }
|
nym-kkt = { path = "../nym-kkt" }
|
||||||
nym-lp-common = { path = "../nym-lp-common" }
|
nym-lp-common = { path = "../nym-lp-common" }
|
||||||
|
nym-lp-transport = { path = "../nym-lp-transport" }
|
||||||
|
|
||||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||||
@@ -34,12 +35,21 @@ num_enum = { workspace = true }
|
|||||||
chacha20poly1305 = { workspace = true }
|
chacha20poly1305 = { workspace = true }
|
||||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||||
|
|
||||||
|
# needed for the 'mock 'feature
|
||||||
|
nym-test-utils = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
rand_chacha = "0.3"
|
#rand_chacha = "0.3"
|
||||||
|
mock_instant = { workspace = true }
|
||||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||||
nym-test-utils = { workspace = true }
|
nym-test-utils = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
|
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
mock = ["nym-test-utils", "nym-crypto/rand"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "replay_protection"
|
name = "replay_protection"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||||
|
use nym_test_utils::helpers::u64_seeded_rng;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::Rng;
|
||||||
use rand_chacha::ChaCha8Rng;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
fn bench_sequential_counters(c: &mut Criterion) {
|
fn bench_sequential_counters(c: &mut Criterion) {
|
||||||
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
|
|||||||
let validator = ReceivingKeyCounterValidator::default();
|
let validator = ReceivingKeyCounterValidator::default();
|
||||||
|
|
||||||
// Create random counters within a valid window
|
// Create random counters within a valid window
|
||||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
let mut rng = u64_seeded_rng(42);
|
||||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ mod tests {
|
|||||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||||
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
|
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
|
||||||
// Need payload and trailer to meet min_size requirement
|
// Need payload and trailer to meet min_size requirement
|
||||||
let payload_size = 10; // Arbitrary
|
let payload_size = 10; // Arbitrary
|
||||||
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
||||||
@@ -565,7 +565,7 @@ mod tests {
|
|||||||
let result = parse_lp_packet(&buf, None);
|
let result = parse_lp_packet(&buf, None);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
match result {
|
match result {
|
||||||
Err(LpError::InvalidMessageType(255)) => {} // Expected error
|
Err(LpError::InvalidMessageType(231)) => {} // Expected error
|
||||||
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
||||||
Ok(_) => panic!("Expected error, but got Ok"),
|
Ok(_) => panic!("Expected error, but got Ok"),
|
||||||
}
|
}
|
||||||
@@ -628,7 +628,7 @@ mod tests {
|
|||||||
receiver_idx: 42,
|
receiver_idx: 42,
|
||||||
counter: 123,
|
counter: 123,
|
||||||
},
|
},
|
||||||
message: LpMessage::ClientHello(hello_data.clone()),
|
message: LpMessage::ClientHello(hello_data),
|
||||||
trailer: [0; TRAILER_LEN],
|
trailer: [0; TRAILER_LEN],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -681,7 +681,7 @@ mod tests {
|
|||||||
receiver_idx: 100,
|
receiver_idx: 100,
|
||||||
counter: 200,
|
counter: 200,
|
||||||
},
|
},
|
||||||
message: LpMessage::ClientHello(hello_data.clone()),
|
message: LpMessage::ClientHello(hello_data),
|
||||||
trailer: [55; TRAILER_LEN],
|
trailer: [55; TRAILER_LEN],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -757,12 +757,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_forward_packet_encode_decode_roundtrip() {
|
fn test_forward_packet_encode_decode_roundtrip_v4() {
|
||||||
let mut dst = BytesMut::new();
|
let mut dst = BytesMut::new();
|
||||||
|
|
||||||
let forward_data = crate::message::ForwardPacketData {
|
let forward_data = crate::message::ForwardPacketData {
|
||||||
target_gateway_identity: [77u8; 32],
|
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],
|
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -789,7 +789,50 @@ mod tests {
|
|||||||
|
|
||||||
if let LpMessage::ForwardPacket(data) = decoded.message {
|
if let LpMessage::ForwardPacket(data) = decoded.message {
|
||||||
assert_eq!(data.target_gateway_identity, [77u8; 32]);
|
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]);
|
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected ForwardPacket message");
|
panic!("Expected ForwardPacket message");
|
||||||
@@ -1246,4 +1289,37 @@ mod tests {
|
|||||||
_ => panic!("Expected SubsessionKK1 message"),
|
_ => panic!("Expected SubsessionKK1 message"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_parse_error() {
|
||||||
|
use crate::message::ErrorPacketData;
|
||||||
|
|
||||||
|
let mut dst = BytesMut::new();
|
||||||
|
|
||||||
|
let error_data = ErrorPacketData {
|
||||||
|
message: "this is an error".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet = LpPacket {
|
||||||
|
header: LpHeader {
|
||||||
|
protocol_version: 1,
|
||||||
|
reserved: [0u8; 3],
|
||||||
|
receiver_idx: 42,
|
||||||
|
counter: 200,
|
||||||
|
},
|
||||||
|
message: LpMessage::Error(error_data.clone()),
|
||||||
|
trailer: [0; TRAILER_LEN],
|
||||||
|
};
|
||||||
|
|
||||||
|
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||||
|
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoded.header.receiver_idx, 42);
|
||||||
|
match decoded.message {
|
||||||
|
LpMessage::Error(data) => {
|
||||||
|
assert_eq!(data.message, "this is an error");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Error message"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::message::MessageType;
|
||||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||||
|
use nym_kkt::error::KKTError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
@@ -102,4 +104,25 @@ pub enum LpError {
|
|||||||
kem: KEM,
|
kem: KEM,
|
||||||
hash_function: HashFunction,
|
hash_function: HashFunction,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("failed to complete KKT/PSQ handshake: {0}")]
|
||||||
|
KKTPSQHandshake(String),
|
||||||
|
|
||||||
|
#[error("failed to complete the KKT exchange: {source}")]
|
||||||
|
KKTFailure {
|
||||||
|
#[from]
|
||||||
|
source: KKTError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpError {
|
||||||
|
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
|
||||||
|
Self::KKTPSQHandshake(msg.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
|
||||||
|
Self::KKTPSQHandshake(format!(
|
||||||
|
"received unexpected response, got: {got:?}, expected: {expected:?}"
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-39
@@ -10,6 +10,7 @@ pub mod noise_protocol;
|
|||||||
pub mod packet;
|
pub mod packet;
|
||||||
pub mod peer;
|
pub mod peer;
|
||||||
pub mod psk;
|
pub mod psk;
|
||||||
|
mod psq;
|
||||||
pub mod replay;
|
pub mod replay;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
mod session_integration;
|
mod session_integration;
|
||||||
@@ -21,31 +22,139 @@ pub use error::LpError;
|
|||||||
pub use message::{ClientHelloData, LpMessage};
|
pub use message::{ClientHelloData, LpMessage};
|
||||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||||
pub use session::{LpSession, generate_fresh_salt};
|
pub use session::LpSession;
|
||||||
pub use session_manager::SessionManager;
|
pub use session_manager::SessionManager;
|
||||||
pub use state_machine::LpStateMachine;
|
pub use state_machine::LpStateMachine;
|
||||||
|
|
||||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(feature = "mock", test))]
|
||||||
|
pub struct SessionsMock {
|
||||||
|
pub initiator: LpSession,
|
||||||
|
pub responder: LpSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "mock", test))]
|
||||||
|
impl SessionsMock {
|
||||||
|
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
|
||||||
|
use crate::peer::mock_peers;
|
||||||
|
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||||
|
|
||||||
|
let (init, resp) = mock_peers();
|
||||||
|
let resp_remote = resp.as_remote();
|
||||||
|
let init_remote = init.as_remote();
|
||||||
|
let salt = [42u8; 32];
|
||||||
|
let session_id_bytes = session_id.to_le_bytes();
|
||||||
|
|
||||||
|
// skip KKT by just deriving the kem key locally
|
||||||
|
let kem_keys = resp.kem_psq.as_ref().unwrap();
|
||||||
|
|
||||||
|
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||||
|
libcrux_kem::Algorithm::X25519,
|
||||||
|
kem_keys.private_key().as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||||
|
|
||||||
|
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||||
|
libcrux_kem::Algorithm::X25519,
|
||||||
|
kem_keys.public_key().as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||||
|
|
||||||
|
// INIT -> RESP: PSQ MSG1
|
||||||
|
let psq_initiator = crate::psk::psq_initiator_create_message(
|
||||||
|
init.x25519.private_key(),
|
||||||
|
&resp_remote.x25519_public,
|
||||||
|
&encapsulation_key,
|
||||||
|
init.ed25519.private_key(),
|
||||||
|
init.ed25519.public_key(),
|
||||||
|
&salt,
|
||||||
|
&session_id_bytes,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let psk = psq_initiator.psk;
|
||||||
|
let psq_payload = psq_initiator.payload;
|
||||||
|
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
|
||||||
|
|
||||||
|
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||||
|
.local_private_key(init.x25519().private_key().as_bytes())
|
||||||
|
.remote_public_key(resp_remote.x25519_public.as_bytes())
|
||||||
|
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||||
|
.build_initiator()
|
||||||
|
.unwrap();
|
||||||
|
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
|
||||||
|
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||||
|
|
||||||
|
let psq_responder = crate::psk::psq_responder_process_message(
|
||||||
|
resp.x25519.private_key(),
|
||||||
|
&init_remote.x25519_public,
|
||||||
|
(&decapsulation_key, &encapsulation_key),
|
||||||
|
&init_remote.ed25519_public,
|
||||||
|
&psq_payload,
|
||||||
|
&salt,
|
||||||
|
&session_id_bytes,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||||
|
.local_private_key(resp.x25519().private_key().as_bytes())
|
||||||
|
.remote_public_key(init_remote.x25519_public.as_bytes())
|
||||||
|
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||||
|
.build_responder()
|
||||||
|
.unwrap();
|
||||||
|
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
|
||||||
|
noise_protocol_resp.read_message(&noise_msg1).unwrap();
|
||||||
|
|
||||||
|
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
|
||||||
|
noise_protocol_init.read_message(&noise_msg2).unwrap();
|
||||||
|
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||||
|
|
||||||
|
assert!(noise_protocol_init.is_handshake_finished());
|
||||||
|
|
||||||
|
noise_protocol_resp.read_message(&noise_msg3).unwrap();
|
||||||
|
assert!(noise_protocol_resp.is_handshake_finished());
|
||||||
|
|
||||||
|
SessionsMock {
|
||||||
|
initiator: LpSession::new(
|
||||||
|
session_id,
|
||||||
|
1,
|
||||||
|
outer_aead_key.clone(),
|
||||||
|
init,
|
||||||
|
resp_remote,
|
||||||
|
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||||
|
noise_protocol_init,
|
||||||
|
),
|
||||||
|
responder: LpSession::new(
|
||||||
|
session_id,
|
||||||
|
1,
|
||||||
|
outer_aead_key,
|
||||||
|
resp,
|
||||||
|
init_remote,
|
||||||
|
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||||
|
noise_protocol_resp,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we just need a dummy 'valid' session for simpler tests
|
||||||
|
pub fn mock_initiator() -> LpSession {
|
||||||
|
Self::mock_post_handshake(1234).initiator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "mock", test))]
|
||||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||||
let (init, resp) = crate::peer::mock_peers();
|
let sessions = SessionsMock::mock_post_handshake(69);
|
||||||
|
(sessions.initiator, sessions.responder)
|
||||||
|
}
|
||||||
|
|
||||||
// Use a fixed receiver_index for deterministic tests
|
#[cfg(any(feature = "mock", test))]
|
||||||
let receiver_index: u32 = 12345;
|
pub fn mock_session_for_test() -> LpSession {
|
||||||
|
SessionsMock::mock_initiator()
|
||||||
// Use consistent salt for deterministic tests
|
|
||||||
let salt = [1u8; 32];
|
|
||||||
|
|
||||||
let initiator_session =
|
|
||||||
LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt)
|
|
||||||
.expect("Test session creation failed");
|
|
||||||
|
|
||||||
let responder_session = LpSession::new(receiver_index, false, resp, init.as_remote(), &salt)
|
|
||||||
.expect("Test session creation failed");
|
|
||||||
|
|
||||||
(initiator_session, responder_session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -53,17 +162,16 @@ mod tests {
|
|||||||
use crate::message::LpMessage;
|
use crate::message::LpMessage;
|
||||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||||
use crate::session_manager::SessionManager;
|
use crate::session_manager::SessionManager;
|
||||||
use crate::{LpError, sessions_for_tests};
|
use crate::{LpError, SessionsMock, mock_session_for_test};
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
|
|
||||||
// Import the new standalone functions
|
// Import the new standalone functions
|
||||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||||
use crate::peer::mock_peers;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_replay_protection_integration() {
|
fn test_replay_protection_integration() {
|
||||||
// Create session
|
// Create session
|
||||||
let session = sessions_for_tests().0;
|
let mut session = mock_session_for_test();
|
||||||
|
|
||||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||||
let packet1 = LpPacket {
|
let packet1 = LpPacket {
|
||||||
@@ -162,32 +270,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_session_manager_integration() {
|
fn test_session_manager_integration() {
|
||||||
// Create session manager
|
// Create session manager
|
||||||
let local_manager = SessionManager::new();
|
let mut local_manager = SessionManager::new();
|
||||||
let remote_manager = SessionManager::new();
|
let mut remote_manager = SessionManager::new();
|
||||||
|
|
||||||
// Generate Ed25519 keypairs for PSQ authentication
|
|
||||||
let (init, resp) = mock_peers();
|
|
||||||
|
|
||||||
// Use fixed receiver_index for deterministic test
|
// Use fixed receiver_index for deterministic test
|
||||||
let receiver_index: u32 = 54321;
|
let receiver_index: u32 = 54321;
|
||||||
|
|
||||||
// Test salt
|
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||||
let salt = [46u8; 32];
|
let local_session = sessions.initiator;
|
||||||
|
let remote_session = sessions.responder;
|
||||||
|
|
||||||
// Create a session via manager
|
// Create a session via manager
|
||||||
let _ = local_manager
|
let _ = local_manager.create_session_state_machine(local_session);
|
||||||
.create_session_state_machine(
|
let _ = remote_manager.create_session_state_machine(remote_session);
|
||||||
receiver_index,
|
|
||||||
true,
|
|
||||||
init.clone(),
|
|
||||||
resp.as_remote(),
|
|
||||||
&salt,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let _ = remote_manager
|
|
||||||
.create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt)
|
|
||||||
.unwrap();
|
|
||||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||||
let packet1 = LpPacket {
|
let packet1 = LpPacket {
|
||||||
header: LpHeader {
|
header: LpHeader {
|
||||||
|
|||||||
+206
-30
@@ -1,17 +1,19 @@
|
|||||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::packet::LpHeader;
|
||||||
use crate::peer::LpRemotePeer;
|
use crate::peer::LpRemotePeer;
|
||||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError};
|
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
|
||||||
/// Data structure for the ClientHello message
|
/// Data structure for the ClientHello message
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct ClientHelloData {
|
pub struct ClientHelloData {
|
||||||
/// Client-proposed receiver index for session identification (4 bytes)
|
/// Client-proposed receiver index for session identification (4 bytes)
|
||||||
/// Auto-generated randomly by the client
|
/// Auto-generated randomly by the client
|
||||||
@@ -28,6 +30,17 @@ impl ClientHelloData {
|
|||||||
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
|
||||||
pub const LEN: usize = 100;
|
pub const LEN: usize = 100;
|
||||||
|
|
||||||
|
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
|
||||||
|
LpPacket::new(
|
||||||
|
LpHeader::new(
|
||||||
|
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
|
||||||
|
0, // counter starts at 0
|
||||||
|
protocol_version,
|
||||||
|
),
|
||||||
|
LpMessage::ClientHello(self),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
Self::LEN
|
Self::LEN
|
||||||
}
|
}
|
||||||
@@ -141,6 +154,8 @@ pub enum MessageType {
|
|||||||
SubsessionReady = 0x000C,
|
SubsessionReady = 0x000C,
|
||||||
/// Subsession abort - race winner tells loser to become responder
|
/// Subsession abort - race winner tells loser to become responder
|
||||||
SubsessionAbort = 0x000D,
|
SubsessionAbort = 0x000D,
|
||||||
|
/// General error
|
||||||
|
Error = 0x00FF,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageType {
|
impl MessageType {
|
||||||
@@ -157,6 +172,9 @@ impl MessageType {
|
|||||||
pub struct HandshakeData(pub Vec<u8>);
|
pub struct HandshakeData(pub Vec<u8>);
|
||||||
|
|
||||||
impl HandshakeData {
|
impl HandshakeData {
|
||||||
|
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
@@ -174,6 +192,11 @@ impl HandshakeData {
|
|||||||
pub struct EncryptedDataPayload(pub Vec<u8>);
|
pub struct EncryptedDataPayload(pub Vec<u8>);
|
||||||
|
|
||||||
impl EncryptedDataPayload {
|
impl EncryptedDataPayload {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
@@ -192,6 +215,10 @@ impl EncryptedDataPayload {
|
|||||||
pub struct KKTRequestData(pub Vec<u8>);
|
pub struct KKTRequestData(pub Vec<u8>);
|
||||||
|
|
||||||
impl KKTRequestData {
|
impl KKTRequestData {
|
||||||
|
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
@@ -210,6 +237,10 @@ impl KKTRequestData {
|
|||||||
pub struct KKTResponseData(pub Vec<u8>);
|
pub struct KKTResponseData(pub Vec<u8>);
|
||||||
|
|
||||||
impl KKTResponseData {
|
impl KKTResponseData {
|
||||||
|
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||||
|
Self(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
self.0.len()
|
self.0.len()
|
||||||
}
|
}
|
||||||
@@ -223,15 +254,60 @@ impl KKTResponseData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// General human-readable error message
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ErrorPacketData {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorPacketData {
|
||||||
|
pub(crate) fn new(message: impl Into<String>) -> Self {
|
||||||
|
ErrorPacketData {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
// length-encoding + message
|
||||||
|
4 + self.message.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(&self, dst: &mut BytesMut) {
|
||||||
|
dst.put_u32_le(self.message.len() as u32);
|
||||||
|
dst.put_slice(self.message.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||||
|
if bytes.len() < 4 {
|
||||||
|
return Err(LpError::DeserializationError(format!(
|
||||||
|
"Too few bytes to deserialise ErrorPacketData. got {}",
|
||||||
|
bytes.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||||
|
if bytes[4..].len() != message_len {
|
||||||
|
return Err(LpError::DeserializationError(format!(
|
||||||
|
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
|
||||||
|
bytes.len(),
|
||||||
|
4 + message_len
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
|
||||||
|
|
||||||
|
Ok(ErrorPacketData { message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Packet forwarding request with embedded inner LP packet
|
/// Packet forwarding request with embedded inner LP packet
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ForwardPacketData {
|
pub struct ForwardPacketData {
|
||||||
/// Target gateway's Ed25519 identity (32 bytes)
|
/// Target gateway's Ed25519 identity (32 bytes)
|
||||||
pub target_gateway_identity: [u8; 32],
|
pub target_gateway_identity: [u8; 32],
|
||||||
|
|
||||||
// TODO: replace it with `SocketAddr`
|
|
||||||
/// Target gateway's LP address (IP:port string)
|
/// 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)
|
/// Complete inner LP packet bytes (serialized LpPacket)
|
||||||
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
|
||||||
@@ -239,23 +315,46 @@ pub struct ForwardPacketData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ForwardPacketData {
|
impl ForwardPacketData {
|
||||||
|
pub fn new(
|
||||||
|
target_gateway_identity: ed25519::PublicKey,
|
||||||
|
target_lp_address: SocketAddr,
|
||||||
|
inner_packet_bytes: Vec<u8>,
|
||||||
|
) -> Self {
|
||||||
|
ForwardPacketData {
|
||||||
|
target_gateway_identity: target_gateway_identity.to_bytes(),
|
||||||
|
target_lp_address,
|
||||||
|
inner_packet_bytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
// 32 bytes target gateway identity
|
// 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
|
// 4 bytes of length of inner packet bytes
|
||||||
// +
|
// +
|
||||||
// inner_packet_bytes.len()
|
// 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) {
|
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_slice(&self.target_gateway_identity);
|
||||||
dst.put_u16_le(self.target_lp_address.len() as u16);
|
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
|
||||||
dst.put_slice(self.target_lp_address.as_bytes());
|
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_u32_le(self.inner_packet_bytes.len() as u32);
|
||||||
dst.put_slice(&self.inner_packet_bytes);
|
dst.put_slice(&self.inner_packet_bytes);
|
||||||
}
|
}
|
||||||
@@ -267,42 +366,56 @@ impl ForwardPacketData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||||
// smallest possible packet with empty address and empty data
|
// smallest possible packet with ipv4 and empty data
|
||||||
if bytes.len() < 38 {
|
if bytes.len() < 43 {
|
||||||
|
// 32 + 1 + 4 + 2 + 4 + 0
|
||||||
return Err(LpError::DeserializationError(format!(
|
return Err(LpError::DeserializationError(format!(
|
||||||
"Too few bytes to deserialise ForwardPacketData[1]. got {}",
|
"Too few bytes to deserialise ForwardPacketData. got {}",
|
||||||
bytes.len()
|
bytes.len()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// SAFETY: we ensured we have sufficient data
|
// SAFETY: we ensured we have sufficient data
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let target_gateway_identity = bytes[0..32].try_into().unwrap();
|
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
|
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
|
||||||
if bytes[34..].len() < 4 + target_lp_address_len as usize {
|
// IPv6, first check we have actually enough bytes
|
||||||
return Err(LpError::DeserializationError(format!(
|
// smallest possible packet with ipv6 and empty data
|
||||||
"Too few bytes to deserialise ForwardPacketData[2]. got {}",
|
if bytes.len() < 55 {
|
||||||
bytes.len()
|
// 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([
|
let inner_packet_bytes_len = u32::from_le_bytes([
|
||||||
bytes[34 + target_lp_address_len as usize],
|
bytes[next_index],
|
||||||
bytes[34 + target_lp_address_len as usize + 1],
|
bytes[next_index + 1],
|
||||||
bytes[34 + target_lp_address_len as usize + 2],
|
bytes[next_index + 2],
|
||||||
bytes[34 + target_lp_address_len as usize + 3],
|
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!(
|
return Err(LpError::DeserializationError(format!(
|
||||||
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
|
"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 {
|
Ok(ForwardPacketData {
|
||||||
target_gateway_identity,
|
target_gateway_identity,
|
||||||
@@ -412,6 +525,62 @@ pub enum LpMessage {
|
|||||||
SubsessionReady(SubsessionReadyData),
|
SubsessionReady(SubsessionReadyData),
|
||||||
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
||||||
SubsessionAbort,
|
SubsessionAbort,
|
||||||
|
/// An error has occurred
|
||||||
|
Error(ErrorPacketData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HandshakeData> for LpMessage {
|
||||||
|
fn from(value: HandshakeData) -> Self {
|
||||||
|
LpMessage::Handshake(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<EncryptedDataPayload> for LpMessage {
|
||||||
|
fn from(value: EncryptedDataPayload) -> Self {
|
||||||
|
LpMessage::EncryptedData(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ClientHelloData> for LpMessage {
|
||||||
|
fn from(value: ClientHelloData) -> Self {
|
||||||
|
LpMessage::ClientHello(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KKTRequestData> for LpMessage {
|
||||||
|
fn from(value: KKTRequestData) -> Self {
|
||||||
|
LpMessage::KKTRequest(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KKTResponseData> for LpMessage {
|
||||||
|
fn from(value: KKTResponseData) -> Self {
|
||||||
|
LpMessage::KKTResponse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ForwardPacketData> for LpMessage {
|
||||||
|
fn from(value: ForwardPacketData) -> Self {
|
||||||
|
LpMessage::ForwardPacket(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubsessionKK1Data> for LpMessage {
|
||||||
|
fn from(value: SubsessionKK1Data) -> Self {
|
||||||
|
LpMessage::SubsessionKK1(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubsessionKK2Data> for LpMessage {
|
||||||
|
fn from(value: SubsessionKK2Data) -> Self {
|
||||||
|
LpMessage::SubsessionKK2(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubsessionReadyData> for LpMessage {
|
||||||
|
fn from(value: SubsessionReadyData) -> Self {
|
||||||
|
LpMessage::SubsessionReady(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for LpMessage {
|
impl Display for LpMessage {
|
||||||
@@ -431,6 +600,7 @@ impl Display for LpMessage {
|
|||||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||||
|
LpMessage::Error(_) => write!(f, "Error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,6 +622,7 @@ impl LpMessage {
|
|||||||
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
|
||||||
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
|
||||||
LpMessage::SubsessionAbort => &[],
|
LpMessage::SubsessionAbort => &[],
|
||||||
|
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,6 +642,7 @@ impl LpMessage {
|
|||||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||||
LpMessage::SubsessionAbort => true, // Empty signal
|
LpMessage::SubsessionAbort => true, // Empty signal
|
||||||
|
LpMessage::Error(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,6 +662,7 @@ impl LpMessage {
|
|||||||
LpMessage::SubsessionKK2(payload) => payload.len(),
|
LpMessage::SubsessionKK2(payload) => payload.len(),
|
||||||
LpMessage::SubsessionReady(payload) => payload.len(),
|
LpMessage::SubsessionReady(payload) => payload.len(),
|
||||||
LpMessage::SubsessionAbort => 0,
|
LpMessage::SubsessionAbort => 0,
|
||||||
|
LpMessage::Error(payload) => payload.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +682,7 @@ impl LpMessage {
|
|||||||
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
||||||
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
||||||
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
||||||
|
LpMessage::Error(_) => MessageType::Error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +702,7 @@ impl LpMessage {
|
|||||||
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
||||||
LpMessage::SubsessionReady(data) => data.encode(dst),
|
LpMessage::SubsessionReady(data) => data.encode(dst),
|
||||||
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
||||||
|
LpMessage::Error(data) => data.encode(dst),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +755,7 @@ impl LpMessage {
|
|||||||
content.ensure_empty()?;
|
content.ensure_empty()?;
|
||||||
Ok(LpMessage::SubsessionAbort)
|
Ok(LpMessage::SubsessionAbort)
|
||||||
}
|
}
|
||||||
|
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ pub enum ReadResult {
|
|||||||
// --- Implementation ---
|
// --- Implementation ---
|
||||||
|
|
||||||
impl NoiseProtocol {
|
impl NoiseProtocol {
|
||||||
|
pub fn params() -> NoiseParams {
|
||||||
|
// SAFETY: the hardcoded pattern must be valid
|
||||||
|
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
crate::NOISE_PATTERN.parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
|
||||||
///
|
///
|
||||||
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
|
||||||
@@ -91,6 +98,46 @@ impl NoiseProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_handshake_state<'a>(
|
||||||
|
local_private_key: &'a [u8],
|
||||||
|
remote_public_key: &'a [u8],
|
||||||
|
psk: &'a [u8],
|
||||||
|
) -> snow::Builder<'a> {
|
||||||
|
let psk_index = crate::NOISE_PSK_INDEX;
|
||||||
|
let noise_params = NoiseProtocol::params();
|
||||||
|
|
||||||
|
snow::Builder::new(noise_params)
|
||||||
|
.local_private_key(local_private_key)
|
||||||
|
.remote_public_key(remote_public_key)
|
||||||
|
.psk(psk_index, psk)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
|
||||||
|
/// remote public key and psk
|
||||||
|
pub fn build_new_initiator(
|
||||||
|
local_private_key: &[u8],
|
||||||
|
remote_public_key: &[u8],
|
||||||
|
psk: &[u8],
|
||||||
|
) -> Result<Self, NoiseError> {
|
||||||
|
let handshake_state =
|
||||||
|
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||||
|
.build_initiator()?;
|
||||||
|
Ok(Self::new(handshake_state))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
|
||||||
|
/// remote public key and psk
|
||||||
|
pub fn build_new_responder(
|
||||||
|
local_private_key: &[u8],
|
||||||
|
remote_public_key: &[u8],
|
||||||
|
psk: &[u8],
|
||||||
|
) -> Result<Self, NoiseError> {
|
||||||
|
let handshake_state =
|
||||||
|
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
|
||||||
|
.build_responder()?;
|
||||||
|
Ok(Self::new(handshake_state))
|
||||||
|
}
|
||||||
|
|
||||||
/// Processes a single, complete incoming Noise message frame.
|
/// Processes a single, complete incoming Noise message frame.
|
||||||
///
|
///
|
||||||
/// Assumes the caller handles buffering and framing to provide one full message.
|
/// Assumes the caller handles buffering and framing to provide one full message.
|
||||||
@@ -288,43 +335,3 @@ impl NoiseProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_noise_state(
|
|
||||||
local_private_key: &[u8],
|
|
||||||
remote_public_key: &[u8],
|
|
||||||
psk: &[u8],
|
|
||||||
) -> Result<NoiseProtocol, NoiseError> {
|
|
||||||
let pattern_name = crate::NOISE_PATTERN;
|
|
||||||
let psk_index = crate::NOISE_PSK_INDEX;
|
|
||||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
|
||||||
|
|
||||||
let builder = snow::Builder::new(noise_params.clone());
|
|
||||||
// Using dummy remote key as it's not needed for state creation itself
|
|
||||||
// In a real scenario, the key would depend on initiator/responder role
|
|
||||||
let handshake_state = builder
|
|
||||||
.local_private_key(local_private_key)
|
|
||||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
|
||||||
.psk(psk_index, psk)
|
|
||||||
.build_initiator()?;
|
|
||||||
Ok(NoiseProtocol::new(handshake_state))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_noise_state_responder(
|
|
||||||
local_private_key: &[u8],
|
|
||||||
remote_public_key: &[u8],
|
|
||||||
psk: &[u8],
|
|
||||||
) -> Result<NoiseProtocol, NoiseError> {
|
|
||||||
let pattern_name = crate::NOISE_PATTERN;
|
|
||||||
let psk_index = crate::NOISE_PSK_INDEX;
|
|
||||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
|
||||||
|
|
||||||
let builder = snow::Builder::new(noise_params.clone());
|
|
||||||
// Using dummy remote key as it's not needed for state creation itself
|
|
||||||
// In a real scenario, the key would depend on initiator/responder role
|
|
||||||
let handshake_state = builder
|
|
||||||
.local_private_key(local_private_key)
|
|
||||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
|
||||||
.psk(psk_index, psk)
|
|
||||||
.build_responder()?;
|
|
||||||
Ok(NoiseProtocol::new(handshake_state))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::LpError;
|
use crate::LpError;
|
||||||
use crate::message::LpMessage;
|
use crate::message::{LpMessage, MessageType};
|
||||||
use crate::replay::ReceivingKeyCounterValidator;
|
use crate::replay::ReceivingKeyCounterValidator;
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use nym_lp_common::format_debug_bytes;
|
use nym_lp_common::format_debug_bytes;
|
||||||
@@ -53,6 +53,10 @@ impl LpPacket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn typ(&self) -> MessageType {
|
||||||
|
self.message.typ()
|
||||||
|
}
|
||||||
|
|
||||||
/// Compute a hash of the message payload
|
/// Compute a hash of the message payload
|
||||||
///
|
///
|
||||||
/// This can be used for message integrity verification or deduplication
|
/// This can be used for message integrity verification or deduplication
|
||||||
@@ -203,9 +207,9 @@ impl LpHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LpHeader {
|
impl LpHeader {
|
||||||
pub fn new(receiver_idx: u32, counter: u64) -> Self {
|
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
protocol_version: version::CURRENT,
|
protocol_version,
|
||||||
reserved: [0u8; 3],
|
reserved: [0u8; 3],
|
||||||
receiver_idx,
|
receiver_idx,
|
||||||
counter,
|
counter,
|
||||||
@@ -265,7 +269,7 @@ impl LpHeader {
|
|||||||
|
|
||||||
Ok(LpHeader {
|
Ok(LpHeader {
|
||||||
protocol_version,
|
protocol_version,
|
||||||
reserved: [0u8; 3],
|
reserved,
|
||||||
receiver_idx,
|
receiver_idx,
|
||||||
counter,
|
counter,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::{ClientHelloData, LpError};
|
||||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||||
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -29,6 +30,14 @@ impl LpLocalPeer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
|
||||||
|
ClientHelloData::new_with_fresh_salt(
|
||||||
|
*self.x25519().public_key(),
|
||||||
|
*self.ed25519().public_key(),
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
|
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
|
||||||
self.kem_psq = Some(key);
|
self.kem_psq = Some(key);
|
||||||
@@ -43,6 +52,14 @@ impl LpLocalPeer {
|
|||||||
&self.x25519
|
&self.x25519
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the reference to the KEM Public key of the peer (if available).
|
||||||
|
pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
|
||||||
|
self.kem_psq
|
||||||
|
.as_ref()
|
||||||
|
.map(|kp| kp.public_key())
|
||||||
|
.ok_or(LpError::ResponderWithMissingKEMKey)
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
|
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn as_remote(&self) -> LpRemotePeer {
|
pub fn as_remote(&self) -> LpRemotePeer {
|
||||||
@@ -128,16 +145,37 @@ impl LpRemotePeer {
|
|||||||
self.expected_signing_key_digests = expected_signing_key_digests;
|
self.expected_signing_key_digests = expected_signing_key_digests;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempt to retrieve expected KEM key hash of the remote
|
||||||
|
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
|
||||||
|
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
|
||||||
|
pub(crate) fn expected_kem_key_hash(
|
||||||
|
&self,
|
||||||
|
ciphersuite: Ciphersuite,
|
||||||
|
) -> Result<Vec<u8>, LpError> {
|
||||||
|
let kem = ciphersuite.kem();
|
||||||
|
let hash_function = ciphersuite.hash_function();
|
||||||
|
|
||||||
|
let digests = self
|
||||||
|
.expected_kem_key_digests
|
||||||
|
.get(&kem)
|
||||||
|
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
|
||||||
|
|
||||||
|
digests
|
||||||
|
.get(&hash_function)
|
||||||
|
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(feature = "mock", test))]
|
||||||
pub fn mock_peer() -> LpLocalPeer {
|
pub fn mock_peer() -> LpLocalPeer {
|
||||||
// use deterministic rng
|
// use deterministic rng
|
||||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||||
random_peer(&mut rng)
|
random_peer(&mut rng)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(feature = "mock", test))]
|
||||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||||
let x25519 = Arc::new(ed25519.to_x25519());
|
let x25519 = Arc::new(ed25519.to_x25519());
|
||||||
@@ -150,7 +188,7 @@ pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(feature = "mock", test))]
|
||||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||||
// use deterministic rng
|
// use deterministic rng
|
||||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||||
|
use crate::{LpError, LpPacket};
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use nym_lp_transport::traits::LpTransport;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
|
||||||
|
#[cfg(not(test))]
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// only used in internal code (and tests)
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait LpTransportHandshakeExt: LpTransport {
|
||||||
|
// the outer key is temporary until the algorithm is changed with psqv2
|
||||||
|
async fn receive_packet(
|
||||||
|
&mut self,
|
||||||
|
outer_key: Option<&OuterAeadKey>,
|
||||||
|
) -> Result<LpPacket, LpError>
|
||||||
|
where
|
||||||
|
Self: Unpin,
|
||||||
|
{
|
||||||
|
let raw = self.receive_raw_packet().await?;
|
||||||
|
parse_lp_packet(&raw, outer_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_packet(
|
||||||
|
&mut self,
|
||||||
|
packet: LpPacket,
|
||||||
|
outer_key: Option<&OuterAeadKey>,
|
||||||
|
) -> Result<(), LpError>
|
||||||
|
where
|
||||||
|
Self: Unpin,
|
||||||
|
{
|
||||||
|
let mut packet_buf = BytesMut::new();
|
||||||
|
|
||||||
|
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
|
||||||
|
self.send_serialised_packet(&packet_buf).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::codec::OuterAeadKey;
|
||||||
|
use crate::message::{HandshakeData, KKTRequestData, MessageType};
|
||||||
|
use crate::noise_protocol::NoiseProtocol;
|
||||||
|
use crate::peer::LpRemotePeer;
|
||||||
|
use crate::psk::psq_initiator_create_message;
|
||||||
|
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||||
|
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||||
|
use crate::session::PqSharedSecret;
|
||||||
|
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||||
|
use nym_kkt::KKT_RESPONSE_AAD;
|
||||||
|
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||||
|
use nym_kkt::context::KKTContext;
|
||||||
|
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
|
||||||
|
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
|
||||||
|
use nym_lp_transport::traits::LpTransport;
|
||||||
|
use rand09::rng;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
impl<'a, S> PSQHandshakeState<'a, S>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
/// Generate and send client hello to the responder
|
||||||
|
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||||
|
let timestamp = current_timestamp()?;
|
||||||
|
|
||||||
|
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
|
||||||
|
self.connection
|
||||||
|
.send_packet(client_hello_data.into_lp_packet(protocol), None)
|
||||||
|
.await?;
|
||||||
|
Ok(client_hello_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
|
||||||
|
/// whether the request has been successful or whether there has been a collision in receiver
|
||||||
|
/// index requiring a retry
|
||||||
|
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
|
||||||
|
match self.receive_non_error(None).await?.message {
|
||||||
|
LpMessage::Ack => Ok(true),
|
||||||
|
LpMessage::Collision => Ok(false),
|
||||||
|
other => {
|
||||||
|
// TODO: retry on collision
|
||||||
|
Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::Ack,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to send KKT request to begin the handshake
|
||||||
|
pub(crate) async fn send_kkt_request(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
remote_peer: &LpRemotePeer,
|
||||||
|
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
|
||||||
|
let (session_secret, encrypted_frame) =
|
||||||
|
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
|
||||||
|
let lp_message = KKTRequestData::new(encrypted_frame).into();
|
||||||
|
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||||
|
self.connection.send_packet(lp_packet, None).await?;
|
||||||
|
Ok((kkt_context, session_secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
|
||||||
|
/// the received encapsulation key
|
||||||
|
pub(crate) async fn receive_kkt_response(
|
||||||
|
&mut self,
|
||||||
|
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
|
||||||
|
remote_peer: &LpRemotePeer,
|
||||||
|
) -> Result<EncapsulationKey<'static>, LpError> {
|
||||||
|
let kkt_response = match self.receive_non_error(None).await?.message {
|
||||||
|
LpMessage::KKTResponse(response) => response,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::KKTResponse,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("received KKT response");
|
||||||
|
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
|
||||||
|
|
||||||
|
let (response_frame, remote_context) =
|
||||||
|
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
|
||||||
|
let encapsulation_key = initiator_ingest_response(
|
||||||
|
&kkt_context,
|
||||||
|
&response_frame,
|
||||||
|
&remote_context,
|
||||||
|
&remote_peer.ed25519_public,
|
||||||
|
&expected_kem_key_digest,
|
||||||
|
)?;
|
||||||
|
Ok(encapsulation_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to prepare and send initial PSQ msg1
|
||||||
|
pub(crate) async fn send_psq_initiator_message(
|
||||||
|
&mut self,
|
||||||
|
remote_peer: &LpRemotePeer,
|
||||||
|
encapsulation_key: &EncapsulationKey<'_>,
|
||||||
|
salt: &[u8; 32],
|
||||||
|
session_id_bytes: &[u8; 4],
|
||||||
|
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
let session_id = u32::from_le_bytes(*session_id_bytes);
|
||||||
|
|
||||||
|
let psq_initiator = psq_initiator_create_message(
|
||||||
|
self.local_peer.x25519.private_key(),
|
||||||
|
&remote_peer.x25519_public,
|
||||||
|
encapsulation_key,
|
||||||
|
self.local_peer.ed25519.private_key(),
|
||||||
|
self.local_peer.ed25519.public_key(),
|
||||||
|
salt,
|
||||||
|
session_id_bytes,
|
||||||
|
)?;
|
||||||
|
let psk = psq_initiator.psk;
|
||||||
|
let psq_payload = psq_initiator.payload;
|
||||||
|
|
||||||
|
// TEMP \/
|
||||||
|
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||||
|
// TEMP /\
|
||||||
|
|
||||||
|
// prepare noise state and msg1
|
||||||
|
let mut noise_protocol = NoiseProtocol::build_new_initiator(
|
||||||
|
self.local_peer.x25519().private_key().as_bytes(),
|
||||||
|
remote_peer.x25519_public.as_bytes(),
|
||||||
|
&psk,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// prepare noise msg1
|
||||||
|
let noise_msg1 = noise_protocol
|
||||||
|
.get_bytes_to_send()
|
||||||
|
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
|
||||||
|
let psq_len = psq_payload.len() as u16;
|
||||||
|
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
|
||||||
|
combined.extend_from_slice(&psq_len.to_le_bytes());
|
||||||
|
combined.extend_from_slice(&psq_payload);
|
||||||
|
combined.extend_from_slice(&noise_msg1);
|
||||||
|
|
||||||
|
let lp_message = HandshakeData::new(combined).into();
|
||||||
|
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||||
|
|
||||||
|
self.connection.send_packet(lp_packet, None).await?;
|
||||||
|
Ok((
|
||||||
|
outer_aead_key,
|
||||||
|
noise_protocol,
|
||||||
|
PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive and validate received PSQ msg2
|
||||||
|
pub(crate) async fn receive_psq_responder_message(
|
||||||
|
&mut self,
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
noise_protocol: &mut NoiseProtocol,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let psq_msg2 = match self
|
||||||
|
.connection
|
||||||
|
.receive_packet(Some(outer_aead_key))
|
||||||
|
.await?
|
||||||
|
.message
|
||||||
|
{
|
||||||
|
LpMessage::Handshake(response) => response.0,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::Handshake,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
|
||||||
|
if psq_msg2.len() < 2 {
|
||||||
|
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||||
|
}
|
||||||
|
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
|
||||||
|
if psq_msg2.len() < 2 + handle_len {
|
||||||
|
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||||
|
}
|
||||||
|
// Extract and "store" the PSK handle
|
||||||
|
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
|
||||||
|
let noise_payload = &psq_msg2[2 + handle_len..];
|
||||||
|
|
||||||
|
// *sigh* ignore the message
|
||||||
|
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to prepare and send final PSQ msg3
|
||||||
|
pub(crate) async fn send_final_psq_message(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
noise_protocol: &mut NoiseProtocol,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let noise_msg3 = noise_protocol
|
||||||
|
.get_bytes_to_send()
|
||||||
|
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
|
||||||
|
|
||||||
|
let lp_message = HandshakeData::new(noise_msg3).into();
|
||||||
|
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||||
|
self.connection
|
||||||
|
.send_packet(lp_packet, Some(outer_aead_key))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !noise_protocol.is_handshake_finished() {
|
||||||
|
return Err(LpError::kkt_psq_handshake(
|
||||||
|
"noise handshake not finished after msg3",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receive final ACK that indicates finalisation of the handshake
|
||||||
|
pub(crate) async fn receive_final_ack(
|
||||||
|
&mut self,
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
match self
|
||||||
|
.connection
|
||||||
|
.receive_packet(Some(outer_aead_key))
|
||||||
|
.await?
|
||||||
|
.message
|
||||||
|
{
|
||||||
|
LpMessage::Ack => Ok(()),
|
||||||
|
other => Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::Ack,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn complete_as_initiator_inner(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
// 0. retrieve the expected kem key hash. if we don't know it,
|
||||||
|
// there's no point in even trying to start the handshake
|
||||||
|
let Some(remote_peer) = self.remote_peer.take() else {
|
||||||
|
return Err(IntermediateHandshakeFailure::plain(
|
||||||
|
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||||
|
// and keep retrying until we manage to establish a receiver index without collisions
|
||||||
|
let mut attempt = 0;
|
||||||
|
let client_hello_data = loop {
|
||||||
|
attempt += 1;
|
||||||
|
|
||||||
|
debug!("sending client hello");
|
||||||
|
let client_hello = self
|
||||||
|
.send_client_hello()
|
||||||
|
.await
|
||||||
|
.map_err(IntermediateHandshakeFailure::plain)?;
|
||||||
|
if self
|
||||||
|
.receive_client_hello_ack()
|
||||||
|
.await
|
||||||
|
.map_err(IntermediateHandshakeFailure::plain)?
|
||||||
|
{
|
||||||
|
debug!("received client hello ACK");
|
||||||
|
break client_hello;
|
||||||
|
}
|
||||||
|
debug!("received client hello collision");
|
||||||
|
|
||||||
|
// TODO: make it configurable
|
||||||
|
if attempt > 3 {
|
||||||
|
return Err(IntermediateHandshakeFailure::plain(
|
||||||
|
LpError::kkt_psq_handshake(
|
||||||
|
"failed to establish receiver index without collision",
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let session_id = client_hello_data.receiver_index;
|
||||||
|
let session_id_bytes = session_id.to_le_bytes();
|
||||||
|
let salt = client_hello_data.salt;
|
||||||
|
|
||||||
|
// 3. prepare and send KKT request
|
||||||
|
debug!("sending KKT request");
|
||||||
|
let kkt_data = self
|
||||||
|
.send_kkt_request(session_id, &remote_peer)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 4. receive and process KKT response
|
||||||
|
let encapsulation_key = self
|
||||||
|
.receive_kkt_response(kkt_data, &remote_peer)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
debug!("received KKT response");
|
||||||
|
|
||||||
|
// 5. prepare and send PSQ msg1
|
||||||
|
debug!("sending PSQ msg1");
|
||||||
|
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
|
||||||
|
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 6. receive and process PSQ msg2
|
||||||
|
debug!("received PSQ msg2");
|
||||||
|
if let Err(source) = self
|
||||||
|
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. prepare and send PSQ msg3
|
||||||
|
debug!("sending PSQ msg3");
|
||||||
|
if let Err(source) = self
|
||||||
|
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. receive final ACK and finalise
|
||||||
|
debug!("received final ACK");
|
||||||
|
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Ok(LpSession::new(
|
||||||
|
session_id,
|
||||||
|
self.protocol_version()
|
||||||
|
.expect("protocol version is known at this point"),
|
||||||
|
outer_aead_key,
|
||||||
|
self.local_peer.clone(),
|
||||||
|
remote_peer,
|
||||||
|
pq_shared_secret,
|
||||||
|
noise_protocol,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: missing: receive counter check
|
||||||
|
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
match self.complete_as_initiator_inner().await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::codec::OuterAeadKey;
|
||||||
|
use crate::message::ErrorPacketData;
|
||||||
|
use crate::packet::LpHeader;
|
||||||
|
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||||
|
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||||
|
use crate::{LpError, LpMessage, LpPacket};
|
||||||
|
use nym_kkt::ciphersuite::Ciphersuite;
|
||||||
|
use nym_lp_transport::traits::LpTransport;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
mod initiator;
|
||||||
|
mod responder;
|
||||||
|
|
||||||
|
pub(crate) struct IntermediateHandshakeFailure {
|
||||||
|
/// Session id established during exchange if we managed to derive it
|
||||||
|
session_id: Option<u32>,
|
||||||
|
|
||||||
|
/// Protocol version established during the exchange
|
||||||
|
protocol_version: Option<u8>,
|
||||||
|
|
||||||
|
/// Outer aead key established during exchange if we managed to derive it
|
||||||
|
outer_aead_key: Option<OuterAeadKey>,
|
||||||
|
|
||||||
|
/// The error source
|
||||||
|
source: LpError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntermediateHandshakeFailure {
|
||||||
|
fn plain(source: LpError) -> IntermediateHandshakeFailure {
|
||||||
|
IntermediateHandshakeFailure {
|
||||||
|
session_id: None,
|
||||||
|
protocol_version: None,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PSQHandshakeState<'a, S> {
|
||||||
|
/// The underlying connection established for the handshake
|
||||||
|
connection: &'a mut S,
|
||||||
|
|
||||||
|
/// Protocol version used for the exchange.
|
||||||
|
/// either known implicitly through the directory (initiator)
|
||||||
|
/// or established through client hello (responder)
|
||||||
|
protocol_version: Option<u8>,
|
||||||
|
|
||||||
|
/// Ciphersuite selected for the KKT/PSQ exchange
|
||||||
|
ciphersuite: Ciphersuite,
|
||||||
|
|
||||||
|
/// Representation of a local Lewes Protocol peer
|
||||||
|
/// encapsulating all the known information and keys.
|
||||||
|
local_peer: LpLocalPeer,
|
||||||
|
|
||||||
|
/// Representation of a remote Lewes Protocol peer
|
||||||
|
/// encapsulating all the known information and keys.
|
||||||
|
remote_peer: Option<LpRemotePeer>,
|
||||||
|
|
||||||
|
/// Counter for outgoing packets
|
||||||
|
sending_counter: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S> PSQHandshakeState<'a, S>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
|
||||||
|
PSQHandshakeState {
|
||||||
|
connection,
|
||||||
|
protocol_version: None,
|
||||||
|
ciphersuite,
|
||||||
|
local_peer,
|
||||||
|
remote_peer: None,
|
||||||
|
sending_counter: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
|
||||||
|
self.protocol_version = Some(protocol_version);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
|
||||||
|
self.remote_peer = Some(remote_peer);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn protocol_version(&self) -> Result<u8, LpError> {
|
||||||
|
self.protocol_version
|
||||||
|
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the next counter value for outgoing packets.
|
||||||
|
pub fn next_counter(&mut self) -> u64 {
|
||||||
|
let counter = self.sending_counter;
|
||||||
|
self.sending_counter += 1;
|
||||||
|
counter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_packet(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
protocol_version: u8,
|
||||||
|
message: LpMessage,
|
||||||
|
) -> LpPacket {
|
||||||
|
let counter = self.next_counter();
|
||||||
|
let header = LpHeader::new(session_id, counter, protocol_version);
|
||||||
|
LpPacket::new(header, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn try_send_error_packet(
|
||||||
|
&mut self,
|
||||||
|
err: IntermediateHandshakeFailure,
|
||||||
|
) -> LpError {
|
||||||
|
// if session_id is not known, we can't send the packet back (with the current design)
|
||||||
|
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
|
||||||
|
return err.source;
|
||||||
|
};
|
||||||
|
if let Err(err) = self
|
||||||
|
.send_error_packet(
|
||||||
|
session_id,
|
||||||
|
protocol,
|
||||||
|
err.source.to_string(),
|
||||||
|
err.outer_aead_key.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
debug!("failed to send back error response: {err}")
|
||||||
|
}
|
||||||
|
err.source
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to send an error packet
|
||||||
|
pub(crate) async fn send_error_packet(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
protocol_version: u8,
|
||||||
|
msg: impl Into<String>,
|
||||||
|
outer_aead_key: Option<&OuterAeadKey>,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let packet = self.next_packet(
|
||||||
|
session_id,
|
||||||
|
protocol_version,
|
||||||
|
LpMessage::Error(ErrorPacketData::new(msg)),
|
||||||
|
);
|
||||||
|
self.connection.send_packet(packet, outer_aead_key).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive a packet from connection, explicitly checking for an error response
|
||||||
|
/// and returning corresponding message if received
|
||||||
|
pub(crate) async fn receive_non_error(
|
||||||
|
&mut self,
|
||||||
|
outer_aead_key: Option<&OuterAeadKey>,
|
||||||
|
) -> Result<LpPacket, LpError> {
|
||||||
|
let packet = self.connection.receive_packet(outer_aead_key).await?;
|
||||||
|
|
||||||
|
match &packet.message {
|
||||||
|
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
|
||||||
|
"remote error: {}",
|
||||||
|
error_packet.message
|
||||||
|
))),
|
||||||
|
_ => Ok(packet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::peer::mock_peers;
|
||||||
|
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||||
|
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
|
||||||
|
use mock_instant::thread_local::MockClock;
|
||||||
|
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
|
||||||
|
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||||
|
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::join;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn extract_error(conn: &mut MockIOStream) -> String {
|
||||||
|
let packet = conn.receive_packet(None).await.unwrap();
|
||||||
|
match packet.message {
|
||||||
|
LpMessage::Error(error) => error.message,
|
||||||
|
_ => panic!("non error packet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn e2e_psq_handshake() -> anyhow::Result<()> {
|
||||||
|
let conn_init = MockIOStream::default();
|
||||||
|
let conn_resp = conn_init.try_get_remote_handle();
|
||||||
|
|
||||||
|
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||||
|
// so they'd get 'static lifetime
|
||||||
|
let conn_init = conn_init.leak();
|
||||||
|
let conn_resp = conn_resp.leak();
|
||||||
|
|
||||||
|
let ciphersuite = Ciphersuite::new(
|
||||||
|
KEM::X25519,
|
||||||
|
HashFunction::Blake3,
|
||||||
|
SignatureScheme::Ed25519,
|
||||||
|
HashLength::Default,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (init, resp) = mock_peers();
|
||||||
|
let resp_remote = resp.as_remote();
|
||||||
|
|
||||||
|
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
|
||||||
|
.with_protocol_version(1)
|
||||||
|
.with_remote_peer(resp_remote);
|
||||||
|
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
|
||||||
|
|
||||||
|
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
|
||||||
|
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
|
||||||
|
|
||||||
|
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||||
|
|
||||||
|
let session_init = session_init???;
|
||||||
|
let session_resp = session_resp???;
|
||||||
|
|
||||||
|
assert_eq!(session_init.id(), session_resp.id());
|
||||||
|
assert_eq!(
|
||||||
|
session_init.outer_aead_key().as_bytes(),
|
||||||
|
session_resp.outer_aead_key().as_bytes()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
session_init.pq_shared_secret().as_bytes(),
|
||||||
|
session_resp.pq_shared_secret().as_bytes()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
|
||||||
|
let mut conn_init = MockIOStream::default();
|
||||||
|
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||||
|
|
||||||
|
let ciphersuite = Ciphersuite::new(
|
||||||
|
KEM::X25519,
|
||||||
|
HashFunction::Blake3,
|
||||||
|
SignatureScheme::Ed25519,
|
||||||
|
HashLength::Default,
|
||||||
|
);
|
||||||
|
let (init, resp) = mock_peers();
|
||||||
|
let resp_remote = resp.as_remote();
|
||||||
|
|
||||||
|
// as initiator
|
||||||
|
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
|
||||||
|
.with_protocol_version(1)
|
||||||
|
.with_remote_peer(resp_remote);
|
||||||
|
|
||||||
|
// you can generate and send (valid) client hello as initiator
|
||||||
|
let client_hello = handshake_init.send_client_hello().await?;
|
||||||
|
let LpMessage::ClientHello(received_client_hello) =
|
||||||
|
conn_resp.receive_packet(None).await?.message
|
||||||
|
else {
|
||||||
|
panic!("wrong message type");
|
||||||
|
};
|
||||||
|
assert_eq!(client_hello, received_client_hello);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// essentially make sure you can't accidentally trigger the handshake as the responder
|
||||||
|
#[tokio::test]
|
||||||
|
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
|
||||||
|
let conn_init = MockIOStream::default();
|
||||||
|
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||||
|
|
||||||
|
let ciphersuite = Ciphersuite::new(
|
||||||
|
KEM::X25519,
|
||||||
|
HashFunction::Blake3,
|
||||||
|
SignatureScheme::Ed25519,
|
||||||
|
HashLength::Default,
|
||||||
|
);
|
||||||
|
let (_, resp) = mock_peers();
|
||||||
|
|
||||||
|
// as initiator
|
||||||
|
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||||
|
|
||||||
|
// you can generate and send (valid) client hello as initiator
|
||||||
|
let sending_res = handshake_resp.send_client_hello().await;
|
||||||
|
assert!(sending_res.is_err());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
|
||||||
|
let current_time = Duration::from_secs(10000);
|
||||||
|
MockClock::set_system_time(current_time);
|
||||||
|
|
||||||
|
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
|
||||||
|
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
|
||||||
|
|
||||||
|
let ciphersuite = Ciphersuite::new(
|
||||||
|
KEM::X25519,
|
||||||
|
HashFunction::Blake3,
|
||||||
|
SignatureScheme::Ed25519,
|
||||||
|
HashLength::Default,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TOO OLD
|
||||||
|
let mut conn_init = MockIOStream::default();
|
||||||
|
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||||
|
let (init, resp) = mock_peers();
|
||||||
|
|
||||||
|
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||||
|
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
|
||||||
|
|
||||||
|
conn_init
|
||||||
|
.send_packet(client_hello_too_old.into_lp_packet(1), None)
|
||||||
|
.await?;
|
||||||
|
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("too old"));
|
||||||
|
|
||||||
|
// TOO RECENT
|
||||||
|
let mut conn_init = MockIOStream::default();
|
||||||
|
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||||
|
let (init, resp) = mock_peers();
|
||||||
|
|
||||||
|
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||||
|
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
|
||||||
|
|
||||||
|
conn_init
|
||||||
|
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
|
||||||
|
.await?;
|
||||||
|
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("too future"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::codec::OuterAeadKey;
|
||||||
|
use crate::message::{HandshakeData, KKTResponseData, MessageType};
|
||||||
|
use crate::noise_protocol::NoiseProtocol;
|
||||||
|
use crate::peer::LpRemotePeer;
|
||||||
|
use crate::psk::psq_responder_process_message;
|
||||||
|
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||||
|
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||||
|
use crate::session::PqSharedSecret;
|
||||||
|
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||||
|
use nym_kkt::KKT_RESPONSE_AAD;
|
||||||
|
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||||
|
use nym_kkt::context::KKTContext;
|
||||||
|
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
|
||||||
|
use nym_kkt::frame::KKTSessionId;
|
||||||
|
use nym_kkt::session::{responder_ingest_message, responder_process};
|
||||||
|
use nym_lp_transport::traits::LpTransport;
|
||||||
|
use rand09::rng;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
|
||||||
|
fn validate_client_hello_timestamp(
|
||||||
|
client_timestamp: u64,
|
||||||
|
tolerance: Duration,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let now = current_timestamp()?;
|
||||||
|
|
||||||
|
let age = now.abs_diff(client_timestamp);
|
||||||
|
if age > tolerance.as_secs() {
|
||||||
|
let direction = if now >= client_timestamp {
|
||||||
|
"old"
|
||||||
|
} else {
|
||||||
|
"future"
|
||||||
|
};
|
||||||
|
|
||||||
|
return Err(LpError::kkt_psq_handshake(format!(
|
||||||
|
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
|
||||||
|
tolerance.as_secs()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, S> PSQHandshakeState<'a, S>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
pub(crate) fn encapsulated_kem_keys(
|
||||||
|
&self,
|
||||||
|
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
|
||||||
|
let kem_keys = self
|
||||||
|
.local_peer
|
||||||
|
.kem_psq
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(LpError::ResponderWithMissingKEMKey)?;
|
||||||
|
|
||||||
|
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||||
|
libcrux_kem::Algorithm::X25519,
|
||||||
|
kem_keys.private_key().as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
LpError::KKTError(format!(
|
||||||
|
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||||
|
|
||||||
|
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||||
|
libcrux_kem::Algorithm::X25519,
|
||||||
|
kem_keys.public_key().as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
LpError::KKTError(format!(
|
||||||
|
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||||
|
Ok((dec_key, enc_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive and validate ClientHello
|
||||||
|
pub(crate) async fn receive_client_hello(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
|
||||||
|
let client_hello_packet = self.receive_non_error(None).await?;
|
||||||
|
let client_hello = match client_hello_packet.message {
|
||||||
|
LpMessage::ClientHello(client_hello) => client_hello,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::ClientHello,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_client_hello_timestamp(
|
||||||
|
client_hello.extract_timestamp(),
|
||||||
|
DEFAULT_TIMESTAMP_TOLERANCE,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// TODO: somehow check for collision
|
||||||
|
|
||||||
|
// set version and remote peer information
|
||||||
|
self.protocol_version = Some(client_hello_packet.header.protocol_version);
|
||||||
|
let remote_peer = LpRemotePeer::new(
|
||||||
|
client_hello.client_ed25519_public_key,
|
||||||
|
client_hello.client_lp_public_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((client_hello, remote_peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send client hello ACK
|
||||||
|
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||||
|
self.connection.send_packet(ack, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive and process a KKT request
|
||||||
|
pub(crate) async fn receive_kkt_request(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
|
||||||
|
let kkt_request = match self.receive_non_error(None).await?.message {
|
||||||
|
LpMessage::KKTRequest(request) => request.0,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::KKTRequest,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (session_secret, request_frame, remote_context) =
|
||||||
|
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
|
||||||
|
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
|
||||||
|
|
||||||
|
Ok((context, session_secret, request_frame.session_id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to send KKT response to the previously received request
|
||||||
|
pub(crate) async fn send_kkt_response(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
|
||||||
|
encapsulation_key: &EncapsulationKey<'_>,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let response_frame = responder_process(
|
||||||
|
&kkt_context,
|
||||||
|
kkt_session_id,
|
||||||
|
self.local_peer.ed25519().private_key(),
|
||||||
|
encapsulation_key,
|
||||||
|
)?;
|
||||||
|
let encrypted_frame = encrypt_kkt_frame(
|
||||||
|
&mut rng(),
|
||||||
|
&session_secret,
|
||||||
|
&response_frame,
|
||||||
|
KKT_RESPONSE_AAD,
|
||||||
|
)?;
|
||||||
|
let lp_message = KKTResponseData::new(encrypted_frame).into();
|
||||||
|
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||||
|
|
||||||
|
self.connection.send_packet(lp_packet, None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive and process a PSQ msg1 request
|
||||||
|
pub(crate) async fn receive_psq_initiator_message(
|
||||||
|
&mut self,
|
||||||
|
remote_peer: &LpRemotePeer,
|
||||||
|
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
|
||||||
|
salt: &[u8; 32],
|
||||||
|
session_id_bytes: &[u8; 4],
|
||||||
|
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
|
||||||
|
let psq_msg1 = match self.receive_non_error(None).await?.message {
|
||||||
|
LpMessage::Handshake(response) => response.0,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::Handshake,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
|
||||||
|
if psq_msg1.len() < 2 {
|
||||||
|
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||||
|
}
|
||||||
|
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
|
||||||
|
if psq_msg1.len() < 2 + handle_len {
|
||||||
|
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||||
|
}
|
||||||
|
let psq_payload = &psq_msg1[2..2 + handle_len];
|
||||||
|
let noise_payload = &psq_msg1[2 + handle_len..];
|
||||||
|
|
||||||
|
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
|
||||||
|
let psq_responder = psq_responder_process_message(
|
||||||
|
self.local_peer.x25519.private_key(),
|
||||||
|
&remote_peer.x25519_public,
|
||||||
|
local_kem_keypair,
|
||||||
|
&remote_peer.ed25519_public,
|
||||||
|
psq_payload,
|
||||||
|
salt,
|
||||||
|
session_id_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let psk = psq_responder.psk;
|
||||||
|
let psk_handle = psq_responder.psk_handle;
|
||||||
|
|
||||||
|
// TEMP \/
|
||||||
|
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||||
|
// TEMP /\
|
||||||
|
|
||||||
|
let mut noise_protocol = NoiseProtocol::build_new_responder(
|
||||||
|
self.local_peer.x25519().private_key().as_bytes(),
|
||||||
|
remote_peer.x25519_public.as_bytes(),
|
||||||
|
&psk,
|
||||||
|
)?;
|
||||||
|
noise_protocol.read_message(noise_payload)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
outer_aead_key,
|
||||||
|
noise_protocol,
|
||||||
|
PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||||
|
psk_handle,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to prepare and generate a responder PSQ msg2
|
||||||
|
pub(crate) async fn send_psq_responder_message(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
psk_handle: &[u8],
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
noise_protocol: &mut NoiseProtocol,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let msg2 = noise_protocol
|
||||||
|
.get_bytes_to_send()
|
||||||
|
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
|
||||||
|
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
|
||||||
|
let handle_len = psk_handle.len() as u16;
|
||||||
|
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
|
||||||
|
combined.extend_from_slice(&handle_len.to_le_bytes());
|
||||||
|
combined.extend_from_slice(psk_handle);
|
||||||
|
combined.extend_from_slice(&msg2);
|
||||||
|
|
||||||
|
let lp_message = HandshakeData::new(combined).into();
|
||||||
|
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||||
|
self.connection
|
||||||
|
.send_packet(lp_packet, Some(outer_aead_key))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive and process final PSQ msg3
|
||||||
|
pub(crate) async fn receive_final_psq_message(
|
||||||
|
&mut self,
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
noise_protocol: &mut NoiseProtocol,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let psq_msg3 = match self
|
||||||
|
.connection
|
||||||
|
.receive_packet(Some(outer_aead_key))
|
||||||
|
.await?
|
||||||
|
.message
|
||||||
|
{
|
||||||
|
LpMessage::Handshake(response) => response.0,
|
||||||
|
other => {
|
||||||
|
return Err(LpError::unexpected_handshake_response(
|
||||||
|
other.typ(),
|
||||||
|
MessageType::Handshake,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
noise_protocol.read_message(&psq_msg3)?;
|
||||||
|
if !noise_protocol.is_handshake_finished() {
|
||||||
|
return Err(LpError::kkt_psq_handshake(
|
||||||
|
"noise handshake not finished after msg3",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send final ACK to indicate finalisation of the handshake
|
||||||
|
pub(crate) async fn send_final_ack(
|
||||||
|
&mut self,
|
||||||
|
session_id: u32,
|
||||||
|
outer_aead_key: &OuterAeadKey,
|
||||||
|
) -> Result<(), LpError> {
|
||||||
|
let protocol = self.protocol_version()?;
|
||||||
|
|
||||||
|
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||||
|
self.connection
|
||||||
|
.send_packet(ack, Some(outer_aead_key))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn complete_as_responder_inner(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
// 1. receive and validate ClientHello
|
||||||
|
let (client_hello_data, remote_peer) =
|
||||||
|
self.receive_client_hello()
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: None,
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
debug!("received client hello");
|
||||||
|
|
||||||
|
let session_id = client_hello_data.receiver_index;
|
||||||
|
let session_id_bytes = session_id.to_le_bytes();
|
||||||
|
let salt = client_hello_data.salt;
|
||||||
|
|
||||||
|
// 2. send ack
|
||||||
|
debug!("sending client hello ACK");
|
||||||
|
self.send_client_hello_ack(session_id)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 3. receive and process KKT request
|
||||||
|
let kkt_data =
|
||||||
|
self.receive_kkt_request()
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
debug!("received KKT request");
|
||||||
|
|
||||||
|
// TEMP: 'derive' KEM keys
|
||||||
|
let (dec_key, enc_key) =
|
||||||
|
self.encapsulated_kem_keys()
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 4. prepare and send KKT response
|
||||||
|
debug!("sending KKT response");
|
||||||
|
self.send_kkt_response(session_id, kkt_data, &enc_key)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 5. receive and process PSQ msg1
|
||||||
|
debug!("received PSQ msg1");
|
||||||
|
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
|
||||||
|
.receive_psq_initiator_message(
|
||||||
|
&remote_peer,
|
||||||
|
(&dec_key, &enc_key),
|
||||||
|
&salt,
|
||||||
|
&session_id_bytes,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|source| IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: None,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 6. prepare and send PSQ msg2
|
||||||
|
debug!("sending PSQ msg2");
|
||||||
|
if let Err(source) = self
|
||||||
|
.send_psq_responder_message(
|
||||||
|
session_id,
|
||||||
|
&psk_handle,
|
||||||
|
&outer_aead_key,
|
||||||
|
&mut noise_protocol,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. receive and process PSQ msg3
|
||||||
|
debug!("received PSQ msg3");
|
||||||
|
if let Err(source) = self
|
||||||
|
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. [optionally] send ACK to finalise
|
||||||
|
debug!("sending final ACK");
|
||||||
|
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
|
||||||
|
return Err(IntermediateHandshakeFailure {
|
||||||
|
session_id: Some(session_id),
|
||||||
|
protocol_version: self.protocol_version,
|
||||||
|
outer_aead_key: Some(outer_aead_key),
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Ok(LpSession::new(
|
||||||
|
session_id,
|
||||||
|
self.protocol_version()
|
||||||
|
.expect("protocol version is known at this point"),
|
||||||
|
outer_aead_key,
|
||||||
|
self.local_peer.clone(),
|
||||||
|
remote_peer,
|
||||||
|
pq_shared_secret,
|
||||||
|
noise_protocol,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
|
||||||
|
where
|
||||||
|
S: LpTransport + Unpin,
|
||||||
|
{
|
||||||
|
match self.complete_as_responder_inner().await {
|
||||||
|
Ok(res) => Ok(res),
|
||||||
|
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+170
-1761
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||||
use crate::{
|
use crate::{
|
||||||
LpError,
|
LpError, SessionsMock,
|
||||||
message::LpMessage,
|
message::LpMessage,
|
||||||
packet::{LpHeader, LpPacket, TRAILER_LEN},
|
packet::{LpHeader, LpPacket, TRAILER_LEN},
|
||||||
session_manager::SessionManager,
|
session_manager::SessionManager,
|
||||||
@@ -44,200 +44,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_full_session_flow() {
|
fn test_full_session_flow() {
|
||||||
// 1. Initialize session manager
|
// 1. Initialize session manager
|
||||||
let session_manager_1 = SessionManager::new();
|
let mut session_manager_1 = SessionManager::new();
|
||||||
let session_manager_2 = SessionManager::new();
|
let mut session_manager_2 = SessionManager::new();
|
||||||
|
|
||||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
let receiver_index = 12345;
|
||||||
let (a, b) = mock_peers();
|
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||||
|
|
||||||
// Use fixed receiver_index for deterministic test
|
// 2. Create sessions using the pre-built Noise states
|
||||||
let receiver_index: u32 = 100001;
|
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||||
|
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||||
// Test salt
|
|
||||||
let salt = [42u8; 32];
|
|
||||||
|
|
||||||
// 4. Create sessions using the pre-built Noise states
|
|
||||||
let peer_a_sm = session_manager_1
|
|
||||||
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
|
|
||||||
.expect("Failed to create session A");
|
|
||||||
|
|
||||||
let peer_b_sm = session_manager_2
|
|
||||||
.create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt)
|
|
||||||
.expect("Failed to create session B");
|
|
||||||
|
|
||||||
// Verify session count
|
// Verify session count
|
||||||
assert_eq!(session_manager_1.session_count(), 1);
|
assert_eq!(session_manager_1.session_count(), 1);
|
||||||
assert_eq!(session_manager_2.session_count(), 1);
|
assert_eq!(session_manager_2.session_count(), 1);
|
||||||
|
|
||||||
// Initialize KKT state for both sessions (test bypass)
|
// 3. Simulate Data Transfer (Post-Handshake)
|
||||||
session_manager_1
|
|
||||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
|
||||||
.expect("Failed to init KKT for peer A");
|
|
||||||
session_manager_2
|
|
||||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
|
||||||
.expect("Failed to init KKT for peer B");
|
|
||||||
|
|
||||||
// 5. Simulate Noise Handshake (Sans-IO)
|
|
||||||
println!("Starting handshake simulation...");
|
|
||||||
let mut i_msg_payload;
|
|
||||||
let mut r_msg_payload = None;
|
|
||||||
let mut rounds = 0;
|
|
||||||
const MAX_ROUNDS: usize = 10;
|
|
||||||
|
|
||||||
// Prime initiator's first message
|
|
||||||
i_msg_payload = session_manager_1
|
|
||||||
.prepare_handshake_message(peer_a_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
i_msg_payload.is_some(),
|
|
||||||
"Initiator did not produce initial message"
|
|
||||||
);
|
|
||||||
|
|
||||||
while rounds < MAX_ROUNDS {
|
|
||||||
rounds += 1;
|
|
||||||
let mut did_exchange = false;
|
|
||||||
|
|
||||||
// === Initiator -> Responder ===
|
|
||||||
if let Some(payload) = i_msg_payload.take() {
|
|
||||||
did_exchange = true;
|
|
||||||
println!(
|
|
||||||
" Round {}: Initiator -> Responder ({} bytes)",
|
|
||||||
rounds,
|
|
||||||
payload.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
// A prepares packet
|
|
||||||
let counter = session_manager_1.next_counter(receiver_index).unwrap();
|
|
||||||
let message_a_to_b = create_test_packet(1, receiver_index, counter, payload);
|
|
||||||
let mut encoded_msg = BytesMut::new();
|
|
||||||
serialize_lp_packet(&message_a_to_b, &mut encoded_msg, None)
|
|
||||||
.expect("A serialize failed");
|
|
||||||
|
|
||||||
// B parses packet and checks replay
|
|
||||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("B parse failed");
|
|
||||||
assert_eq!(decoded_packet.header.counter, counter);
|
|
||||||
|
|
||||||
// Check replay before processing handshake
|
|
||||||
session_manager_2
|
|
||||||
.receiving_counter_quick_check(peer_b_sm, decoded_packet.header.counter)
|
|
||||||
.expect("B replay check failed (A->B)");
|
|
||||||
|
|
||||||
match session_manager_2
|
|
||||||
.process_handshake_message(peer_b_sm, &decoded_packet.message)
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Mark counter only after successful processing
|
|
||||||
session_manager_2
|
|
||||||
.receiving_counter_mark(peer_b_sm, decoded_packet.header.counter)
|
|
||||||
.expect("B mark counter failed");
|
|
||||||
}
|
|
||||||
Err(e) => panic!("Responder processing failed: {:?}", e),
|
|
||||||
}
|
|
||||||
// Check if responder needs to send a reply
|
|
||||||
r_msg_payload = session_manager_2
|
|
||||||
.prepare_handshake_message(peer_b_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap();
|
|
||||||
println!("{:?}", r_msg_payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check completion
|
|
||||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
|
||||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
|
||||||
{
|
|
||||||
println!("Handshake completed after Initiator->Responder message.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Responder -> Initiator ===
|
|
||||||
if let Some(payload) = r_msg_payload.take() {
|
|
||||||
did_exchange = true;
|
|
||||||
println!(
|
|
||||||
" Round {}: Responder -> Initiator ({} bytes)",
|
|
||||||
rounds,
|
|
||||||
payload.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
// B prepares packet
|
|
||||||
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
|
|
||||||
let message_b_to_a = create_test_packet(1, receiver_index, counter, payload);
|
|
||||||
let mut encoded_msg = BytesMut::new();
|
|
||||||
serialize_lp_packet(&message_b_to_a, &mut encoded_msg, None)
|
|
||||||
.expect("B serialize failed");
|
|
||||||
|
|
||||||
// A parses packet and checks replay
|
|
||||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("A parse failed");
|
|
||||||
assert_eq!(decoded_packet.header.counter, counter);
|
|
||||||
|
|
||||||
// Check replay before processing handshake
|
|
||||||
session_manager_1
|
|
||||||
.receiving_counter_quick_check(peer_a_sm, decoded_packet.header.counter)
|
|
||||||
.expect("A replay check failed (B->A)");
|
|
||||||
|
|
||||||
match session_manager_1
|
|
||||||
.process_handshake_message(peer_a_sm, &decoded_packet.message)
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
// Mark counter only after successful processing
|
|
||||||
session_manager_1
|
|
||||||
.receiving_counter_mark(peer_a_sm, decoded_packet.header.counter)
|
|
||||||
.expect("A mark counter failed");
|
|
||||||
}
|
|
||||||
Err(e) => panic!("Initiator processing failed: {:?}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if initiator needs to send a reply
|
|
||||||
i_msg_payload = session_manager_1
|
|
||||||
.prepare_handshake_message(peer_a_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// println!("Initiator state: {}", session_manager_1.get_state(peer_a_sm).unwrap());
|
|
||||||
// println!("Responder state: {}", session_manager_2.get_state(peer_b_sm).unwrap());
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Initiator state: {}",
|
|
||||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Responder state: {}",
|
|
||||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check completion again
|
|
||||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
|
||||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
|
||||||
{
|
|
||||||
println!("Handshake completed after Responder->Initiator message.");
|
|
||||||
|
|
||||||
// Safety break if no messages were exchanged in a round
|
|
||||||
if !did_exchange {
|
|
||||||
println!("No messages exchanged in round {}, breaking.", rounds);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(rounds < MAX_ROUNDS, "Handshake loop exceeded max rounds");
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap(),
|
|
||||||
"Initiator handshake did not complete"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap(),
|
|
||||||
"Responder handshake did not complete"
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Handshake simulation completed successfully in {} rounds.",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Handshake Complete ---
|
|
||||||
|
|
||||||
// 7. Simulate Data Transfer (Post-Handshake)
|
|
||||||
println!("Starting data transfer simulation...");
|
println!("Starting data transfer simulation...");
|
||||||
let plaintext_a_to_b = b"Hello from A!";
|
let plaintext_a_to_b = b"Hello from A!";
|
||||||
|
|
||||||
@@ -314,7 +135,7 @@ mod tests {
|
|||||||
|
|
||||||
println!("Data transfer simulation completed.");
|
println!("Data transfer simulation completed.");
|
||||||
|
|
||||||
// 8. Replay Protection Test (Data Packet)
|
// 4. Replay Protection Test (Data Packet)
|
||||||
println!("Testing data packet replay protection...");
|
println!("Testing data packet replay protection...");
|
||||||
// Try to replay the last message from B to A
|
// Try to replay the last message from B to A
|
||||||
// Need to re-encode because decode consumes the buffer
|
// Need to re-encode because decode consumes the buffer
|
||||||
@@ -345,7 +166,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
println!("Data packet replay protection test passed.");
|
println!("Data packet replay protection test passed.");
|
||||||
|
|
||||||
// 9. Test out-of-order packet reception (send counter N+1 before counter N)
|
// 5. Test out-of-order packet reception (send counter N+1 before counter N)
|
||||||
println!("Testing out-of-order data packet reception...");
|
println!("Testing out-of-order data packet reception...");
|
||||||
let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1
|
let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1
|
||||||
let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
|
let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
|
||||||
@@ -391,7 +212,7 @@ mod tests {
|
|||||||
String::from_utf8_lossy(&decrypted_payload)
|
String::from_utf8_lossy(&decrypted_payload)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 10. Now send the skipped counter N message (should still work)
|
// 6. Now send the skipped counter N message (should still work)
|
||||||
println!("Testing delayed data packet reception...");
|
println!("Testing delayed data packet reception...");
|
||||||
// Prepare data for counter_a_next (N)
|
// Prepare data for counter_a_next (N)
|
||||||
let plaintext_delayed = b"Delayed message";
|
let plaintext_delayed = b"Delayed message";
|
||||||
@@ -439,7 +260,7 @@ mod tests {
|
|||||||
|
|
||||||
println!("Delayed data packet reception test passed.");
|
println!("Delayed data packet reception test passed.");
|
||||||
|
|
||||||
// 11. Try to replay message with counter N (should fail)
|
// 7. Try to replay message with counter N (should fail)
|
||||||
println!("Testing replay of delayed packet...");
|
println!("Testing replay of delayed packet...");
|
||||||
let parsed_delayed_replay =
|
let parsed_delayed_replay =
|
||||||
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
|
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
|
||||||
@@ -451,7 +272,7 @@ mod tests {
|
|||||||
"Should be a replay protection error"
|
"Should be a replay protection error"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 12. Session removal
|
// 8. Session removal
|
||||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||||
assert_eq!(session_manager_1.session_count(), 0);
|
assert_eq!(session_manager_1.session_count(), 0);
|
||||||
|
|
||||||
@@ -468,80 +289,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_bidirectional_communication() {
|
fn test_bidirectional_communication() {
|
||||||
// 1. Initialize session manager
|
// 1. Initialize session manager
|
||||||
let session_manager_1 = SessionManager::new();
|
let mut session_manager_1 = SessionManager::new();
|
||||||
let session_manager_2 = SessionManager::new();
|
let mut session_manager_2 = SessionManager::new();
|
||||||
|
|
||||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
let receiver_index = 12345;
|
||||||
let (a, b) = mock_peers();
|
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||||
|
|
||||||
// Use fixed receiver_index for test
|
// 2. Create sessions using the pre-built Noise states
|
||||||
let receiver_index: u32 = 100002;
|
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||||
|
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||||
|
|
||||||
// Test salt
|
// Counters after handshake
|
||||||
let salt = [43u8; 32];
|
let mut counter_a = 0; // Next counter for A to send
|
||||||
|
let mut counter_b = 0; // Next counter for B to send
|
||||||
|
|
||||||
let peer_a_sm = session_manager_1
|
// 3. Send multiple encrypted messages both ways
|
||||||
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
|
|
||||||
.expect("Failed to create session A");
|
|
||||||
|
|
||||||
let peer_b_sm = session_manager_2
|
|
||||||
.create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt)
|
|
||||||
.expect("Failed to create session B");
|
|
||||||
|
|
||||||
// Initialize KKT state for both sessions (test bypass)
|
|
||||||
session_manager_1
|
|
||||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
|
||||||
.expect("Failed to init KKT for peer A");
|
|
||||||
session_manager_2
|
|
||||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
|
||||||
.expect("Failed to init KKT for peer B");
|
|
||||||
|
|
||||||
// Drive handshake to completion (simplified)
|
|
||||||
let mut i_msg = session_manager_1
|
|
||||||
.prepare_handshake_message(peer_a_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
session_manager_2
|
|
||||||
.process_handshake_message(peer_b_sm, &i_msg)
|
|
||||||
.unwrap();
|
|
||||||
session_manager_2
|
|
||||||
.receiving_counter_mark(peer_b_sm, 0)
|
|
||||||
.unwrap(); // Assume counter 0 for first msg
|
|
||||||
let r_msg = session_manager_2
|
|
||||||
.prepare_handshake_message(peer_b_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
session_manager_1
|
|
||||||
.process_handshake_message(peer_a_sm, &r_msg)
|
|
||||||
.unwrap();
|
|
||||||
session_manager_1
|
|
||||||
.receiving_counter_mark(peer_a_sm, 0)
|
|
||||||
.unwrap(); // Assume counter 0 for first msg
|
|
||||||
i_msg = session_manager_1
|
|
||||||
.prepare_handshake_message(peer_a_sm)
|
|
||||||
.transpose()
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
session_manager_2
|
|
||||||
.process_handshake_message(peer_b_sm, &i_msg)
|
|
||||||
.unwrap();
|
|
||||||
session_manager_2
|
|
||||||
.receiving_counter_mark(peer_b_sm, 1)
|
|
||||||
.unwrap(); // Assume counter 1 for second msg from A
|
|
||||||
|
|
||||||
assert!(session_manager_1.is_handshake_complete(peer_a_sm).unwrap());
|
|
||||||
assert!(session_manager_2.is_handshake_complete(peer_b_sm).unwrap());
|
|
||||||
println!("Bidirectional test: Handshake complete.");
|
|
||||||
|
|
||||||
// Counters after handshake (A sent 2, B sent 1)
|
|
||||||
let mut counter_a = 2; // Next counter for A to send
|
|
||||||
let mut counter_b = 1; // Next counter for B to send
|
|
||||||
|
|
||||||
// 4. Send multiple encrypted messages both ways
|
|
||||||
const NUM_MESSAGES: u64 = 5;
|
const NUM_MESSAGES: u64 = 5;
|
||||||
for i in 0..NUM_MESSAGES {
|
for i in 0..NUM_MESSAGES {
|
||||||
println!("Bidirectional test: Round {}", i);
|
println!("Bidirectional test: Round {}", i);
|
||||||
@@ -606,36 +368,30 @@ mod tests {
|
|||||||
// Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7.
|
// Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7.
|
||||||
// Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6.
|
// Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
counter_a,
|
counter_a, NUM_MESSAGES,
|
||||||
2 + NUM_MESSAGES,
|
|
||||||
"Peer A final send counter mismatch"
|
"Peer A final send counter mismatch"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
total_recv_a,
|
total_recv_a, NUM_MESSAGES,
|
||||||
1 + NUM_MESSAGES,
|
|
||||||
"Peer A total received count mismatch"
|
"Peer A total received count mismatch"
|
||||||
); // Received 1 handshake + 5 data
|
); // Received 5 data
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_recv_a,
|
next_recv_a, NUM_MESSAGES,
|
||||||
1 + NUM_MESSAGES,
|
|
||||||
"Peer A next expected receive counter mismatch"
|
"Peer A next expected receive counter mismatch"
|
||||||
); // Expected counter for msg from B
|
); // Expected counter for msg from B
|
||||||
|
|
||||||
// Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6.
|
// Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6.
|
||||||
// Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7.
|
// Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
counter_b,
|
counter_b, NUM_MESSAGES,
|
||||||
1 + NUM_MESSAGES,
|
|
||||||
"Peer B final send counter mismatch"
|
"Peer B final send counter mismatch"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
total_recv_b,
|
total_recv_b, NUM_MESSAGES,
|
||||||
2 + NUM_MESSAGES,
|
|
||||||
"Peer B total received count mismatch"
|
"Peer B total received count mismatch"
|
||||||
); // Received 2 handshake + 5 data
|
); // Received 5 data
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
next_recv_b,
|
next_recv_b, NUM_MESSAGES,
|
||||||
2 + NUM_MESSAGES,
|
|
||||||
"Peer B next expected receive counter mismatch"
|
"Peer B next expected receive counter mismatch"
|
||||||
); // Expected counter for msg from A
|
); // Expected counter for msg from A
|
||||||
|
|
||||||
@@ -646,21 +402,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_session_error_handling() {
|
fn test_session_error_handling() {
|
||||||
// 1. Initialize session manager
|
// 1. Initialize session manager
|
||||||
let session_manager = SessionManager::new();
|
let mut session_manager = SessionManager::new();
|
||||||
|
|
||||||
// Generate Ed25519 keypair for PSQ authentication
|
let receiver_index = 123;
|
||||||
let (a, b) = mock_peers();
|
let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator;
|
||||||
|
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||||
// Use fixed receiver_index for test
|
|
||||||
let receiver_index: u32 = 100003;
|
|
||||||
|
|
||||||
// Test salt
|
|
||||||
let salt = [44u8; 32];
|
|
||||||
|
|
||||||
// 2. Create a session (using real noise state)
|
// 2. Create a session (using real noise state)
|
||||||
let _session = session_manager
|
let _session = session_manager.create_session_state_machine(session1);
|
||||||
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt)
|
|
||||||
.expect("Failed to create session");
|
|
||||||
|
|
||||||
// 3. Try to get a non-existent session
|
// 3. Try to get a non-existent session
|
||||||
let result = session_manager.state_machine_exists(999);
|
let result = session_manager.state_machine_exists(999);
|
||||||
@@ -674,19 +423,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 5. Create and immediately remove a session
|
// 5. Create and immediately remove a session
|
||||||
let receiver_index_temp: u32 = 100004;
|
let _temp_session = session_manager.create_session_state_machine(session2);
|
||||||
let _temp_session = session_manager
|
|
||||||
.create_session_state_machine(
|
|
||||||
receiver_index_temp,
|
|
||||||
true,
|
|
||||||
a.clone(),
|
|
||||||
b.as_remote(),
|
|
||||||
&salt,
|
|
||||||
)
|
|
||||||
.expect("Failed to create temp session");
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
session_manager.remove_state_machine(receiver_index_temp),
|
session_manager.remove_state_machine(124),
|
||||||
"Should remove the session"
|
"Should remove the session"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -733,14 +473,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
// Remove unused imports if SessionManager methods are no longer direct dependencies
|
// Remove unused imports if SessionManager methods are no longer direct dependencies
|
||||||
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
|
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
|
||||||
use crate::peer::mock_peers;
|
|
||||||
use crate::state_machine::LpData;
|
use crate::state_machine::LpData;
|
||||||
use crate::{
|
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||||
// Bring in state machine types
|
|
||||||
state_machine::{LpAction, LpInput, LpStateBare},
|
|
||||||
// message::LpMessage, // LpMessage likely still needed for LpInput/LpAction
|
|
||||||
// packet::{LpHeader, LpPacket, TRAILER_LEN}, // LpPacket needed for LpAction/LpInput
|
|
||||||
};
|
|
||||||
// Use Bytes for SendData input
|
// Use Bytes for SendData input
|
||||||
|
|
||||||
// Keep helper function for creating test packets if needed,
|
// Keep helper function for creating test packets if needed,
|
||||||
@@ -757,295 +491,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_full_session_flow_with_process_input() {
|
fn test_full_session_flow_with_process_input() {
|
||||||
// 1. Initialize session managers
|
// 1. Initialize session managers
|
||||||
let session_manager_1 = SessionManager::new();
|
let mut session_manager_1 = SessionManager::new();
|
||||||
let session_manager_2 = SessionManager::new();
|
let mut session_manager_2 = SessionManager::new();
|
||||||
|
|
||||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
let receiver_index = 12345;
|
||||||
let (a, b) = mock_peers();
|
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||||
|
|
||||||
// Use fixed receiver_index for test
|
// 2. Create sessions state machines
|
||||||
let receiver_index: u32 = 100005;
|
session_manager_1.create_session_state_machine(sessions.initiator);
|
||||||
|
session_manager_2.create_session_state_machine(sessions.responder);
|
||||||
// Test salt
|
|
||||||
let salt = [45u8; 32];
|
|
||||||
|
|
||||||
// 3. Create sessions state machines
|
|
||||||
assert!(
|
|
||||||
session_manager_1
|
|
||||||
.create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt,) // Initiator
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
session_manager_2
|
|
||||||
.create_session_state_machine(receiver_index, false, b, a.as_remote(), &salt,) // Responder
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(session_manager_1.session_count(), 1);
|
assert_eq!(session_manager_1.session_count(), 1);
|
||||||
assert_eq!(session_manager_2.session_count(), 1);
|
assert_eq!(session_manager_2.session_count(), 1);
|
||||||
assert!(session_manager_1.state_machine_exists(receiver_index));
|
assert!(session_manager_1.state_machine_exists(receiver_index));
|
||||||
assert!(session_manager_2.state_machine_exists(receiver_index));
|
assert!(session_manager_2.state_machine_exists(receiver_index));
|
||||||
|
|
||||||
// Verify initial states are ReadyToHandshake
|
// Verify initial states are Transport
|
||||||
assert_eq!(
|
|
||||||
session_manager_1.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::ReadyToHandshake
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::ReadyToHandshake
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- 4. Simulate Noise Handshake via process_input ---
|
|
||||||
println!("Starting handshake simulation via process_input...");
|
|
||||||
|
|
||||||
let mut packet_a_to_b: Option<LpPacket>;
|
|
||||||
let mut packet_b_to_a: Option<LpPacket>;
|
|
||||||
let mut rounds = 0;
|
|
||||||
const MAX_ROUNDS: usize = 10; // KKT (2 messages) + XK handshake (3 messages) + PSQ = 6 rounds total
|
|
||||||
|
|
||||||
// --- Round 1: Initiator Starts ---
|
|
||||||
println!(" Round {}: Initiator starts handshake", rounds);
|
|
||||||
let action_a1 = session_manager_1
|
|
||||||
.process_input(receiver_index, LpInput::StartHandshake)
|
|
||||||
.expect("Initiator StartHandshake should produce an action")
|
|
||||||
.expect("Initiator StartHandshake failed");
|
|
||||||
|
|
||||||
if let LpAction::SendPacket(packet) = action_a1 {
|
|
||||||
println!(" Initiator produced SendPacket (KKT request)");
|
|
||||||
packet_a_to_b = Some(packet);
|
|
||||||
} else {
|
|
||||||
panic!("Initiator StartHandshake did not produce SendPacket");
|
|
||||||
}
|
|
||||||
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_1.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::KKTExchange,
|
|
||||||
"Initiator state wrong after StartHandshake (should be KKTExchange)"
|
|
||||||
);
|
|
||||||
|
|
||||||
// *** ADD THIS BLOCK for Responder StartHandshake ***
|
|
||||||
println!(
|
|
||||||
" Round {}: Responder explicitly enters KKTExchange state",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let action_b_start =
|
|
||||||
session_manager_2.process_input(receiver_index, LpInput::StartHandshake);
|
|
||||||
// Responder's StartHandshake should not produce an action to send
|
|
||||||
assert!(
|
|
||||||
action_b_start.as_ref().unwrap().is_none(),
|
|
||||||
"Responder StartHandshake should produce None action, got {:?}",
|
|
||||||
action_b_start
|
|
||||||
);
|
|
||||||
// Verify responder transitions to KKTExchange state (not Handshaking yet)
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
|
|
||||||
"Responder state should be KKTExchange after its StartHandshake"
|
|
||||||
);
|
|
||||||
// *** END OF ADDED BLOCK ***
|
|
||||||
|
|
||||||
// --- Round 2: Responder Receives KKT Request, Sends KKT Response ---
|
|
||||||
rounds += 1;
|
|
||||||
println!(
|
|
||||||
" Round {}: Responder receives KKT request, sends KKT response",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let packet_to_process = packet_a_to_b
|
|
||||||
.take()
|
|
||||||
.expect("KKT request from A was missing");
|
|
||||||
|
|
||||||
// Simulate network: serialize -> parse (optional but good practice)
|
|
||||||
let mut buf_a = BytesMut::new();
|
|
||||||
serialize_lp_packet(&packet_to_process, &mut buf_a, None).unwrap();
|
|
||||||
let parsed_packet_a = parse_lp_packet(&buf_a, None).unwrap();
|
|
||||||
|
|
||||||
// Responder processes KKT request
|
|
||||||
let action_b1 = session_manager_2
|
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
|
|
||||||
.expect("Responder ReceivePacket should produce an action")
|
|
||||||
.expect("Responder ReceivePacket failed");
|
|
||||||
|
|
||||||
if let LpAction::SendPacket(packet) = action_b1 {
|
|
||||||
println!(" Responder received KKT request, produced KKT response");
|
|
||||||
packet_b_to_a = Some(packet);
|
|
||||||
} else {
|
|
||||||
panic!("Responder ReceivePacket did not produce SendPacket for KKT response");
|
|
||||||
}
|
|
||||||
// Responder transitions to Handshaking after KKT completes
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::Handshaking,
|
|
||||||
"Responder state should be Handshaking after KKT exchange"
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Round 3: Initiator Receives KKT Response, Sends First Noise Message (with PSQ) ---
|
|
||||||
rounds += 1;
|
|
||||||
println!(
|
|
||||||
" Round {}: Initiator receives KKT response, sends first Noise message (with PSQ)",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let packet_to_process = packet_b_to_a
|
|
||||||
.take()
|
|
||||||
.expect("KKT response from B was missing");
|
|
||||||
|
|
||||||
// Simulate network
|
|
||||||
let mut buf_b = BytesMut::new();
|
|
||||||
serialize_lp_packet(&packet_to_process, &mut buf_b, None).unwrap();
|
|
||||||
let parsed_packet_b = parse_lp_packet(&buf_b, None).unwrap();
|
|
||||||
|
|
||||||
// Initiator processes KKT response
|
|
||||||
let action_a2 = session_manager_1
|
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
|
|
||||||
.expect("Initiator ReceivePacket should produce an action")
|
|
||||||
.expect("Initiator ReceivePacket failed");
|
|
||||||
|
|
||||||
match action_a2 {
|
|
||||||
LpAction::SendPacket(packet) => {
|
|
||||||
println!(
|
|
||||||
" Initiator received KKT response, produced first Noise message (-> e)"
|
|
||||||
);
|
|
||||||
packet_a_to_b = Some(packet);
|
|
||||||
// Initiator transitions to Handshaking after KKT completes
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_1.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::Handshaking,
|
|
||||||
"Initiator state should be Handshaking after receiving KKT response"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
LpAction::KKTComplete => {
|
|
||||||
println!(
|
|
||||||
" Initiator received KKT response, produced KKTComplete (will send Noise in next step)"
|
|
||||||
);
|
|
||||||
// KKT completed, now need to explicitly trigger handshake message
|
|
||||||
// This might be the case if KKT completion doesn't automatically send the first Noise message
|
|
||||||
// Let's try to prepare the handshake message
|
|
||||||
if let Some(msg_result) =
|
|
||||||
session_manager_1.prepare_handshake_message(receiver_index)
|
|
||||||
{
|
|
||||||
let msg = msg_result.expect("Failed to prepare handshake message after KKT");
|
|
||||||
// Create a packet from the message
|
|
||||||
let packet = create_test_packet(1, receiver_index, 0, msg);
|
|
||||||
packet_a_to_b = Some(packet);
|
|
||||||
println!(" Prepared first Noise message after KKTComplete");
|
|
||||||
} else {
|
|
||||||
panic!("No handshake message available after KKT complete");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
panic!(
|
|
||||||
"Initiator ReceivePacket produced unexpected action after KKT response: {:?}",
|
|
||||||
other
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Round 4: Responder Receives First Noise Message, Sends Second ---
|
|
||||||
rounds += 1;
|
|
||||||
println!(
|
|
||||||
" Round {}: Responder receives first Noise message, sends second",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let packet_to_process = packet_a_to_b
|
|
||||||
.take()
|
|
||||||
.expect("First Noise packet from A was missing");
|
|
||||||
|
|
||||||
// Simulate network
|
|
||||||
let mut buf_a2 = BytesMut::new();
|
|
||||||
serialize_lp_packet(&packet_to_process, &mut buf_a2, None).unwrap();
|
|
||||||
let parsed_packet_a2 = parse_lp_packet(&buf_a2, None).unwrap();
|
|
||||||
|
|
||||||
// Responder processes first Noise message and sends second Noise message
|
|
||||||
let action_b2 = session_manager_2
|
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
|
|
||||||
.expect("Responder ReceivePacket should produce an action")
|
|
||||||
.expect("Responder ReceivePacket failed");
|
|
||||||
|
|
||||||
if let LpAction::SendPacket(packet) = action_b2 {
|
|
||||||
println!(
|
|
||||||
" Responder received first Noise message, produced second Noise message (<- e, ee, s, es)"
|
|
||||||
);
|
|
||||||
packet_b_to_a = Some(packet);
|
|
||||||
} else {
|
|
||||||
panic!("Responder did not produce SendPacket for second Noise message");
|
|
||||||
}
|
|
||||||
// Responder still in Handshaking, waiting for final message
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::Handshaking,
|
|
||||||
"Responder state should still be Handshaking after sending second message"
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Round 5: Initiator Receives Second Noise Message, Sends Third, Completes ---
|
|
||||||
rounds += 1;
|
|
||||||
println!(
|
|
||||||
" Round {}: Initiator receives second Noise message, sends third, completes",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let packet_to_process = packet_b_to_a
|
|
||||||
.take()
|
|
||||||
.expect("Second Noise packet from B was missing");
|
|
||||||
|
|
||||||
let mut buf_b2 = BytesMut::new();
|
|
||||||
serialize_lp_packet(&packet_to_process, &mut buf_b2, None).unwrap();
|
|
||||||
let parsed_packet_b2 = parse_lp_packet(&buf_b2, None).unwrap();
|
|
||||||
|
|
||||||
let action_a3 = session_manager_1
|
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
|
|
||||||
.expect("Initiator ReceivePacket should produce an action")
|
|
||||||
.expect("Initiator ReceivePacket failed");
|
|
||||||
|
|
||||||
if let LpAction::SendPacket(packet) = action_a3 {
|
|
||||||
println!(
|
|
||||||
" Initiator received second Noise message, produced third Noise message (-> s, se)"
|
|
||||||
);
|
|
||||||
packet_a_to_b = Some(packet);
|
|
||||||
} else {
|
|
||||||
panic!("Initiator did not produce SendPacket for third Noise message");
|
|
||||||
}
|
|
||||||
// Initiator transitions to Transport after sending third message
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_1.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::Transport,
|
|
||||||
"Initiator state should be Transport after sending third message"
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Round 6: Responder Receives Third Noise Message, Completes ---
|
|
||||||
rounds += 1;
|
|
||||||
println!(
|
|
||||||
" Round {}: Responder receives third Noise message, completes",
|
|
||||||
rounds
|
|
||||||
);
|
|
||||||
let packet_to_process = packet_a_to_b
|
|
||||||
.take()
|
|
||||||
.expect("Third Noise packet from A was missing");
|
|
||||||
|
|
||||||
let mut buf_a3 = BytesMut::new();
|
|
||||||
serialize_lp_packet(&packet_to_process, &mut buf_a3, None).unwrap();
|
|
||||||
let parsed_packet_a3 = parse_lp_packet(&buf_a3, None).unwrap();
|
|
||||||
|
|
||||||
let action_b3 = session_manager_2
|
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
|
|
||||||
.expect("Responder final ReceivePacket should produce an action")
|
|
||||||
.expect("Responder final ReceivePacket failed");
|
|
||||||
|
|
||||||
// Responder completes handshake
|
|
||||||
if let LpAction::HandshakeComplete = action_b3 {
|
|
||||||
println!(" Responder received third Noise message, produced HandshakeComplete");
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
" Responder received third Noise message (Action: {:?})",
|
|
||||||
action_b3
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
|
||||||
LpStateBare::Transport,
|
|
||||||
"Responder state should be Transport after processing third message"
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Verification ---
|
|
||||||
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
session_manager_1.get_state(receiver_index).unwrap(),
|
session_manager_1.get_state(receiver_index).unwrap(),
|
||||||
LpStateBare::Transport
|
LpStateBare::Transport
|
||||||
@@ -1054,9 +515,8 @@ mod tests {
|
|||||||
session_manager_2.get_state(receiver_index).unwrap(),
|
session_manager_2.get_state(receiver_index).unwrap(),
|
||||||
LpStateBare::Transport
|
LpStateBare::Transport
|
||||||
);
|
);
|
||||||
println!("Handshake simulation completed successfully via process_input.");
|
|
||||||
|
|
||||||
// --- 5. Simulate Data Transfer via process_input ---
|
// --- 3. Simulate Data Transfer via process_input ---
|
||||||
println!("Starting data transfer simulation via process_input...");
|
println!("Starting data transfer simulation via process_input...");
|
||||||
let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec());
|
let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec());
|
||||||
let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
|
let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
|
||||||
@@ -1134,7 +594,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
println!("Data transfer simulation completed.");
|
println!("Data transfer simulation completed.");
|
||||||
|
|
||||||
// --- 6. Replay Protection Test ---
|
// --- 4. Replay Protection Test ---
|
||||||
println!("Testing data packet replay protection via process_input...");
|
println!("Testing data packet replay protection via process_input...");
|
||||||
let replay_result = session_manager_1
|
let replay_result = session_manager_1
|
||||||
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
|
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
|
||||||
@@ -1148,7 +608,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
println!("Data packet replay protection test passed.");
|
println!("Data packet replay protection test passed.");
|
||||||
|
|
||||||
// --- 7. Out-of-Order Test ---
|
// --- 5. Out-of-Order Test ---
|
||||||
println!("Testing out-of-order reception via process_input...");
|
println!("Testing out-of-order reception via process_input...");
|
||||||
|
|
||||||
// A prepares N+1 then N
|
// A prepares N+1 then N
|
||||||
@@ -1207,7 +667,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
println!("Out-of-order test passed.");
|
println!("Out-of-order test passed.");
|
||||||
|
|
||||||
// --- 8. Close Test ---
|
// --- 6. Close Test ---
|
||||||
println!("Testing close via process_input...");
|
println!("Testing close via process_input...");
|
||||||
|
|
||||||
// A closes
|
// A closes
|
||||||
@@ -1255,7 +715,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
println!("Close test passed.");
|
println!("Close test passed.");
|
||||||
|
|
||||||
// --- 9. Session Removal ---
|
// --- 7. Session Removal ---
|
||||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||||
assert_eq!(session_manager_1.session_count(), 0);
|
assert_eq!(session_manager_1.session_count(), 0);
|
||||||
assert!(!session_manager_1.state_machine_exists(receiver_index));
|
assert!(!session_manager_1.state_machine_exists(receiver_index));
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
//! This module implements session lifecycle management functionality, handling
|
//! This module implements session lifecycle management functionality, handling
|
||||||
//! creation, retrieval, and storage of sessions.
|
//! creation, retrieval, and storage of sessions.
|
||||||
|
|
||||||
use crate::noise_protocol::ReadResult;
|
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
|
||||||
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
|
|
||||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||||
use dashmap::DashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||||
///
|
///
|
||||||
@@ -18,7 +16,7 @@ use dashmap::DashMap;
|
|||||||
/// ensuring proper thread-safety for concurrent access.
|
/// ensuring proper thread-safety for concurrent access.
|
||||||
pub struct SessionManager {
|
pub struct SessionManager {
|
||||||
/// Manages state machines directly, keyed by lp_id
|
/// Manages state machines directly, keyed by lp_id
|
||||||
state_machines: DashMap<u32, LpStateMachine>,
|
state_machines: HashMap<u32, LpStateMachine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SessionManager {
|
impl Default for SessionManager {
|
||||||
@@ -31,36 +29,18 @@ impl SessionManager {
|
|||||||
/// Creates a new session manager with empty session storage.
|
/// Creates a new session manager with empty session storage.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
state_machines: DashMap::new(),
|
state_machines: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
|
pub fn process_input(
|
||||||
|
&mut self,
|
||||||
|
lp_id: u32,
|
||||||
|
input: LpInput,
|
||||||
|
) -> Result<Option<LpAction>, LpError> {
|
||||||
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&self, session: LpSession) -> Result<(), LpError> {
|
|
||||||
let sm = LpStateMachine {
|
|
||||||
state: LpState::ReadyToHandshake {
|
|
||||||
session: Box::new(session),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.state_machines.insert(sm.id()?, sm);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handshaking(&self, lp_id: u32) -> Result<bool, LpError> {
|
|
||||||
Ok(self.get_state(lp_id)? == LpStateBare::Handshaking)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_initiate_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
|
||||||
Ok(self.ready_to_handshake(lp_id)? || self.closed(lp_id)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ready_to_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
|
||||||
Ok(self.get_state(lp_id)? == LpStateBare::ReadyToHandshake)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
|
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||||
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
|
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
|
||||||
}
|
}
|
||||||
@@ -84,38 +64,27 @@ impl SessionManager {
|
|||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||||
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
|
self.with_state_machine_mut(lp_id, |sm| {
|
||||||
|
sm.session_mut()?.receiving_counter_mark(counter)
|
||||||
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
|
||||||
self.prepare_handshake_message(lp_id)
|
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||||
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
|
self.with_state_machine_mut(lp_id, |sm| {
|
||||||
.ok()?
|
sm.session_mut()?
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_handshake_complete(&self, lp_id: u32) -> Result<bool, LpError> {
|
|
||||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))?
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_counter(&self, lp_id: u32) -> Result<u64, LpError> {
|
|
||||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))?
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
|
||||||
self.with_state_machine(lp_id, |sm| {
|
|
||||||
sm.session()?
|
|
||||||
.decrypt_data(message)
|
.decrypt_data(message)
|
||||||
.map_err(LpError::NoiseError)
|
.map_err(LpError::NoiseError)
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt_data(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||||
self.with_state_machine(lp_id, |sm| {
|
self.with_state_machine_mut(lp_id, |sm| {
|
||||||
sm.session()?
|
sm.session_mut()?
|
||||||
.encrypt_data(message)
|
.encrypt_data(message)
|
||||||
.map_err(LpError::NoiseError)
|
.map_err(LpError::NoiseError)
|
||||||
})?
|
})?
|
||||||
@@ -125,14 +94,6 @@ impl SessionManager {
|
|||||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_handshake_message(
|
|
||||||
&self,
|
|
||||||
lp_id: u32,
|
|
||||||
message: &LpMessage,
|
|
||||||
) -> Result<ReadResult, LpError> {
|
|
||||||
self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))?
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_count(&self) -> usize {
|
pub fn session_count(&self) -> usize {
|
||||||
self.state_machines.len()
|
self.state_machines.len()
|
||||||
}
|
}
|
||||||
@@ -146,7 +107,7 @@ impl SessionManager {
|
|||||||
F: FnOnce(&LpStateMachine) -> R,
|
F: FnOnce(&LpStateMachine) -> R,
|
||||||
{
|
{
|
||||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||||
Ok(f(&sm))
|
Ok(f(sm))
|
||||||
} else {
|
} else {
|
||||||
Err(LpError::StateMachineNotFound { lp_id })
|
Err(LpError::StateMachineNotFound { lp_id })
|
||||||
}
|
}
|
||||||
@@ -154,74 +115,48 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For mutable access (like running process_input)
|
// For mutable access (like running process_input)
|
||||||
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||||
{
|
{
|
||||||
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
|
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
|
||||||
Ok(f(&mut sm))
|
Ok(f(sm))
|
||||||
} else {
|
} else {
|
||||||
Err(LpError::StateMachineNotFound { lp_id })
|
Err(LpError::StateMachineNotFound { lp_id })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_session_state_machine(
|
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
|
||||||
&self,
|
let receiver_index = lp_session.id();
|
||||||
receiver_index: u32,
|
let sm = LpStateMachine::new(lp_session);
|
||||||
is_initiator: bool,
|
|
||||||
local_peer: LpLocalPeer,
|
|
||||||
remote_peer: LpRemotePeer,
|
|
||||||
salt: &[u8; 32],
|
|
||||||
) -> Result<u32, LpError> {
|
|
||||||
let sm = LpStateMachine::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?;
|
|
||||||
|
|
||||||
self.state_machines.insert(receiver_index, sm);
|
self.state_machines.insert(receiver_index, sm);
|
||||||
Ok(receiver_index)
|
receiver_index
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Method to remove a state machine
|
/// Method to remove a state machine
|
||||||
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
|
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
|
||||||
let removed = self.state_machines.remove(&lp_id);
|
let removed = self.state_machines.remove(&lp_id);
|
||||||
|
|
||||||
removed.is_some()
|
removed.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test-only method to initialize KKT state to Completed for a session.
|
|
||||||
/// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn init_kkt_for_test(
|
|
||||||
&self,
|
|
||||||
lp_id: u32,
|
|
||||||
remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey,
|
|
||||||
) -> Result<(), LpError> {
|
|
||||||
self.with_state_machine(lp_id, |sm| {
|
|
||||||
sm.session()?.set_kkt_completed_for_test(remote_x25519_pub);
|
|
||||||
Ok(())
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::peer::{mock_peers, random_peer};
|
use crate::{SessionsMock, mock_session_for_test};
|
||||||
use nym_test_utils::helpers::deterministic_rng;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_manager_get() {
|
fn test_session_manager_get() {
|
||||||
let manager = SessionManager::new();
|
let mut manager = SessionManager::new();
|
||||||
let mut rng = deterministic_rng();
|
|
||||||
let local = random_peer(&mut rng);
|
|
||||||
let peer1 = random_peer(&mut rng);
|
|
||||||
|
|
||||||
let salt = [47u8; 32];
|
let local_session = mock_session_for_test();
|
||||||
let receiver_index: u32 = 1001;
|
let id = local_session.id();
|
||||||
|
|
||||||
let sm_1_id = manager
|
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
assert_eq!(sm_1_id, id);
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
let retrieved = manager.state_machine_exists(id);
|
||||||
assert!(retrieved);
|
assert!(retrieved);
|
||||||
|
|
||||||
let not_found = manager.state_machine_exists(99);
|
let not_found = manager.state_machine_exists(99);
|
||||||
@@ -230,17 +165,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_manager_remove() {
|
fn test_session_manager_remove() {
|
||||||
let manager = SessionManager::new();
|
let mut manager = SessionManager::new();
|
||||||
let mut rng = deterministic_rng();
|
let local_session = mock_session_for_test();
|
||||||
let local = random_peer(&mut rng);
|
|
||||||
let peer1 = random_peer(&mut rng);
|
|
||||||
|
|
||||||
let salt = [48u8; 32];
|
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||||
let receiver_index: u32 = 2002;
|
|
||||||
|
|
||||||
let sm_1_id = manager
|
|
||||||
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let removed = manager.remove_state_machine(sm_1_id);
|
let removed = manager.remove_state_machine(sm_1_id);
|
||||||
assert!(removed);
|
assert!(removed);
|
||||||
@@ -252,26 +180,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_sessions() {
|
fn test_multiple_sessions() {
|
||||||
let manager = SessionManager::new();
|
let mut manager = SessionManager::new();
|
||||||
let mut rng = deterministic_rng();
|
let session1 = SessionsMock::mock_post_handshake(123).initiator;
|
||||||
let local = random_peer(&mut rng);
|
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||||
let peer1 = random_peer(&mut rng);
|
let session3 = SessionsMock::mock_post_handshake(125).initiator;
|
||||||
let peer2 = random_peer(&mut rng);
|
|
||||||
let peer3 = random_peer(&mut rng);
|
|
||||||
|
|
||||||
let salt = [49u8; 32];
|
let sm_1 = manager.create_session_state_machine(session1);
|
||||||
|
let sm_2 = manager.create_session_state_machine(session2);
|
||||||
let sm_1 = manager
|
let sm_3 = manager.create_session_state_machine(session3);
|
||||||
.create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sm_2 = manager
|
|
||||||
.create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let sm_3 = manager
|
|
||||||
.create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(manager.session_count(), 3);
|
assert_eq!(manager.session_count(), 3);
|
||||||
|
|
||||||
@@ -286,23 +202,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_manager_create_session() {
|
fn test_session_manager_create_session() {
|
||||||
let manager = SessionManager::new();
|
let mut manager = SessionManager::new();
|
||||||
let (init, resp) = mock_peers();
|
|
||||||
|
|
||||||
let salt = [50u8; 32];
|
let sesion = mock_session_for_test();
|
||||||
let receiver_index: u32 = 4004;
|
|
||||||
|
|
||||||
let sm = manager.create_session_state_machine(
|
|
||||||
receiver_index,
|
|
||||||
true,
|
|
||||||
init,
|
|
||||||
resp.as_remote(),
|
|
||||||
&salt,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(sm.is_ok());
|
|
||||||
let sm = sm.unwrap();
|
|
||||||
|
|
||||||
|
let sm = manager.create_session_state_machine(sesion);
|
||||||
assert_eq!(manager.session_count(), 1);
|
assert_eq!(manager.session_count(), 1);
|
||||||
|
|
||||||
let retrieved = manager.get_state_machine_id(sm);
|
let retrieved = manager.get_state_machine_id(sm);
|
||||||
|
|||||||
+196
-763
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ use nyxd_scraper_shared::storage::helpers::log_db_operation_time;
|
|||||||
use nyxd_scraper_shared::storage::{NyxdScraperStorage, NyxdScraperStorageError};
|
use nyxd_scraper_shared::storage::{NyxdScraperStorage, NyxdScraperStorageError};
|
||||||
use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
|
use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tracing::{debug, error, info, instrument, warn};
|
use tracing::{debug, error, info, instrument};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PostgresScraperStorage {
|
pub struct PostgresScraperStorage {
|
||||||
@@ -22,7 +22,10 @@ pub struct PostgresScraperStorage {
|
|||||||
|
|
||||||
impl PostgresScraperStorage {
|
impl PostgresScraperStorage {
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub async fn init(connection_string: &str) -> Result<Self, PostgresScraperError> {
|
pub async fn init(
|
||||||
|
connection_string: &str,
|
||||||
|
run_migrations: &bool,
|
||||||
|
) -> Result<Self, PostgresScraperError> {
|
||||||
debug!("initialising scraper database with '{connection_string}'",);
|
debug!("initialising scraper database with '{connection_string}'",);
|
||||||
|
|
||||||
let connection_pool = match sqlx::PgPool::connect(connection_string).await {
|
let connection_pool = match sqlx::PgPool::connect(connection_string).await {
|
||||||
@@ -33,12 +36,13 @@ impl PostgresScraperStorage {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = sqlx::migrate!("./sql_migrations")
|
if *run_migrations {
|
||||||
.run(&connection_pool)
|
if let Err(err) = sqlx::migrate!("./sql_migrations")
|
||||||
.await
|
.run(&connection_pool)
|
||||||
{
|
.await
|
||||||
warn!("Failed to initialize SQLx database: {err}");
|
{
|
||||||
// return Err(err.into());
|
return Err(err.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Database migration finished!");
|
info!("Database migration finished!");
|
||||||
@@ -192,8 +196,11 @@ impl PostgresScraperStorage {
|
|||||||
impl NyxdScraperStorage for PostgresScraperStorage {
|
impl NyxdScraperStorage for PostgresScraperStorage {
|
||||||
type StorageTransaction = PostgresStorageTransaction;
|
type StorageTransaction = PostgresStorageTransaction;
|
||||||
|
|
||||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError> {
|
async fn initialise(
|
||||||
PostgresScraperStorage::init(storage)
|
storage: &str,
|
||||||
|
run_migrations: &bool,
|
||||||
|
) -> Result<Self, NyxdScraperStorageError> {
|
||||||
|
PostgresScraperStorage::init(storage, run_migrations)
|
||||||
.await
|
.await
|
||||||
.map_err(NyxdScraperStorageError::from)
|
.map_err(NyxdScraperStorageError::from)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ pub struct Config {
|
|||||||
pub store_precommits: bool,
|
pub store_precommits: bool,
|
||||||
|
|
||||||
pub start_block: StartingBlockOpts,
|
pub start_block: StartingBlockOpts,
|
||||||
|
|
||||||
|
pub run_migrations: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NyxdScraperBuilder<S> {
|
pub struct NyxdScraperBuilder<S> {
|
||||||
@@ -161,7 +163,7 @@ where
|
|||||||
|
|
||||||
pub async fn new(config: Config) -> Result<Self, ScraperError> {
|
pub async fn new(config: Config) -> Result<Self, ScraperError> {
|
||||||
config.pruning_options.validate()?;
|
config.pruning_options.validate()?;
|
||||||
let storage = S::initialise(&config.database_storage).await?;
|
let storage = S::initialise(&config.database_storage, &config.run_migrations).await?;
|
||||||
let rpc_client = RpcClient::new(&config.rpc_url)?;
|
let rpc_client = RpcClient::new(&config.rpc_url)?;
|
||||||
|
|
||||||
Ok(NyxdScraper {
|
Ok(NyxdScraper {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ pub trait NyxdScraperStorage: Clone + Sized {
|
|||||||
type StorageTransaction: NyxdScraperTransaction;
|
type StorageTransaction: NyxdScraperTransaction;
|
||||||
|
|
||||||
/// Either connection string (postgres) or storage path (sqlite)
|
/// Either connection string (postgres) or storage path (sqlite)
|
||||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError>;
|
async fn initialise(
|
||||||
|
storage: &str,
|
||||||
|
run_migrations: &bool,
|
||||||
|
) -> Result<Self, NyxdScraperStorageError>;
|
||||||
|
|
||||||
async fn begin_processing_tx(
|
async fn begin_processing_tx(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -207,7 +207,10 @@ impl SqliteScraperStorage {
|
|||||||
impl NyxdScraperStorage for SqliteScraperStorage {
|
impl NyxdScraperStorage for SqliteScraperStorage {
|
||||||
type StorageTransaction = SqliteStorageTransaction;
|
type StorageTransaction = SqliteStorageTransaction;
|
||||||
|
|
||||||
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError> {
|
async fn initialise(
|
||||||
|
storage: &str,
|
||||||
|
_run_migrations: &bool,
|
||||||
|
) -> Result<Self, NyxdScraperStorageError> {
|
||||||
SqliteScraperStorage::init(storage)
|
SqliteScraperStorage::init(storage)
|
||||||
.await
|
.await
|
||||||
.map_err(NyxdScraperStorageError::from)
|
.map_err(NyxdScraperStorageError::from)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bincode = { workspace = true }
|
bincode = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
nym-authenticator-requests = { workspace = true }
|
nym-authenticator-requests = { workspace = true }
|
||||||
nym-credentials-interface = { workspace = true }
|
nym-credentials-interface = { workspace = true }
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
|
||||||
pub use lp_messages::{
|
pub use lp_messages::*;
|
||||||
LpDvpnRegistrationRequest, LpMixnetGatewayData, LpMixnetRegistrationRequest,
|
|
||||||
LpRegistrationData, LpRegistrationRequest, LpRegistrationResponse, RegistrationMode,
|
|
||||||
};
|
|
||||||
pub use serialisation::BincodeError;
|
pub use serialisation::BincodeError;
|
||||||
|
|
||||||
mod lp_messages;
|
mod lp_messages;
|
||||||
@@ -30,10 +27,27 @@ pub struct NymNodeInformation {
|
|||||||
pub version: AuthenticatorVersion,
|
pub version: AuthenticatorVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[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 {
|
pub struct WireguardConfiguration {
|
||||||
#[serde(with = "bs58_x25519_pubkey")]
|
#[serde(with = "bs58_x25519_pubkey")]
|
||||||
pub public_key: x25519::PublicKey,
|
pub public_key: x25519::PublicKey,
|
||||||
|
pub psk: Option<[u8; 32]>,
|
||||||
pub endpoint: SocketAddr,
|
pub endpoint: SocketAddr,
|
||||||
pub private_ipv4: Ipv4Addr,
|
pub private_ipv4: Ipv4Addr,
|
||||||
pub private_ipv6: Ipv6Addr,
|
pub private_ipv6: Ipv6Addr,
|
||||||
@@ -45,6 +59,10 @@ pub struct NymNodeLPInformation {
|
|||||||
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
|
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
|
||||||
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
|
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
|
||||||
pub x25519: x25519::PublicKey,
|
pub x25519: x25519::PublicKey,
|
||||||
|
|
||||||
|
/// Supported protocol version of the remote gateway.
|
||||||
|
/// Included in case we have to downgrade our version.
|
||||||
|
pub lp_protocol_version: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
|||||||
@@ -3,66 +3,21 @@
|
|||||||
|
|
||||||
//! LP (Lewes Protocol) registration message types shared between client and gateway.
|
//! LP (Lewes Protocol) registration message types shared between client and gateway.
|
||||||
|
|
||||||
use crate::WireguardConfiguration;
|
use crate::WireguardRegistrationData;
|
||||||
|
use crate::dvpn::{
|
||||||
|
LpDvpnRegistrationFinalisation, LpDvpnRegistrationInitialRequest,
|
||||||
|
LpDvpnRegistrationRequestMessage, LpDvpnRegistrationRequestMessageContent,
|
||||||
|
LpDvpnRegistrationResponseMessage, LpDvpnRegistrationResponseMessageContent,
|
||||||
|
RequiresCredentialResponse,
|
||||||
|
};
|
||||||
|
use crate::mixnet::{
|
||||||
|
LpMixnetGatewayData, LpMixnetRegistrationRequestMessage, LpMixnetRegistrationResponseMessage,
|
||||||
|
LpMixnetRegistrationResponseMessageContent,
|
||||||
|
};
|
||||||
use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer};
|
use crate::serialisation::{BincodeError, BincodeOptions, lp_bincode_serializer};
|
||||||
use nym_credentials_interface::{CredentialSpendingData, TicketType};
|
use nym_authenticator_requests::models::BandwidthClaim;
|
||||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
|
||||||
use nym_crypto::asymmetric::ed25519;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::error;
|
||||||
/// Registration request sent by client after LP handshake
|
|
||||||
/// Aligned with existing authenticator registration flow
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LpRegistrationRequest {
|
|
||||||
/// Mode specific registration data
|
|
||||||
pub registration_data: LpRegistrationData,
|
|
||||||
|
|
||||||
/// Unix timestamp for replay protection
|
|
||||||
pub timestamp: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LpRegistrationRequest {
|
|
||||||
pub fn mode(&self) -> RegistrationMode {
|
|
||||||
match self.registration_data {
|
|
||||||
LpRegistrationData::Dvpn { .. } => RegistrationMode::Dvpn,
|
|
||||||
LpRegistrationData::Mixnet { .. } => RegistrationMode::Mixnet,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum LpRegistrationData {
|
|
||||||
/// dVPN mode - register as WireGuard peer (most common)
|
|
||||||
Dvpn {
|
|
||||||
data: Box<LpDvpnRegistrationRequest>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Mixnet mode - register for mixnet routing via IPR
|
|
||||||
Mixnet { data: LpMixnetRegistrationRequest },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LpDvpnRegistrationRequest {
|
|
||||||
/// Client's WireGuard public key (for dVPN mode)
|
|
||||||
pub wg_public_key: nym_wireguard_types::PeerPublicKey,
|
|
||||||
|
|
||||||
/// Bandwidth credential for payment
|
|
||||||
pub credential: CredentialSpendingData,
|
|
||||||
|
|
||||||
/// Ticket type for bandwidth allocation
|
|
||||||
pub ticket_type: TicketType,
|
|
||||||
|
|
||||||
/// Preshared key to be used for the connection
|
|
||||||
pub psk: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LpMixnetRegistrationRequest {
|
|
||||||
/// Client's ed25519 public key (identity)
|
|
||||||
///
|
|
||||||
/// Used to derive DestinationAddressBytes for ActiveClientsStore lookup.
|
|
||||||
pub client_ed25519_pubkey: ed25519::PublicKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum RegistrationMode {
|
pub enum RegistrationMode {
|
||||||
@@ -73,80 +28,135 @@ pub enum RegistrationMode {
|
|||||||
Mixnet,
|
Mixnet,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gateway data for mixnet mode registration
|
/// Registration request sent by client after LP handshake
|
||||||
///
|
/// Aligned with existing authenticator registration flow
|
||||||
/// Contains the gateway's identity and sphinx key needed for the client
|
|
||||||
/// to construct its full nym Recipient address.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LpMixnetGatewayData {
|
pub struct LpRegistrationRequest {
|
||||||
/// Gateway's ed25519 identity public key
|
/// Mode specific registration data
|
||||||
///
|
pub registration_data: LpRegistrationRequestData,
|
||||||
/// Forms part of the client's nym Recipient address.
|
|
||||||
pub gateway_identity: ed25519::PublicKey,
|
/// Unix timestamp for replay protection
|
||||||
// TODO: what we really need in here is the address of internal IPR
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpRegistrationRequest {
|
||||||
|
pub fn mode(&self) -> RegistrationMode {
|
||||||
|
match self.registration_data {
|
||||||
|
LpRegistrationRequestData::Dvpn { .. } => RegistrationMode::Dvpn,
|
||||||
|
LpRegistrationRequestData::Mixnet { .. } => RegistrationMode::Mixnet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LpRegistrationRequestData {
|
||||||
|
/// dVPN mode - register as WireGuard peer (most common)
|
||||||
|
Dvpn {
|
||||||
|
data: Box<LpDvpnRegistrationRequestMessage>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Mixnet mode - register for mixnet routing via IPR
|
||||||
|
Mixnet {
|
||||||
|
data: LpMixnetRegistrationRequestMessage,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registration response from gateway
|
/// Registration response from gateway
|
||||||
/// Contains GatewayData for compatibility with existing client code
|
/// Contains GatewayData for compatibility with existing client code
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct LpRegistrationResponse {
|
pub struct LpRegistrationResponse {
|
||||||
/// Whether registration succeeded
|
/// The status of this registration after the last received client message
|
||||||
pub success: bool,
|
pub status: RegistrationStatus,
|
||||||
|
|
||||||
/// Error message if registration failed
|
/// Mode specific registration response
|
||||||
pub error: Option<String>,
|
pub response_data: LpRegistrationResponseData,
|
||||||
|
}
|
||||||
|
|
||||||
/// Gateway configuration data for dVPN mode (WireGuard)
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
/// This matches what WireguardRegistrationResult expects
|
pub enum LpRegistrationResponseData {
|
||||||
pub gateway_data: Option<WireguardConfiguration>,
|
/// dVPN mode - register as WireGuard peer (most common)
|
||||||
|
Dvpn {
|
||||||
|
data: LpDvpnRegistrationResponseMessage,
|
||||||
|
},
|
||||||
|
|
||||||
/// Gateway data for mixnet mode
|
/// Mixnet mode - register for mixnet routing via IPR
|
||||||
///
|
Mixnet {
|
||||||
/// Contains gateway identity and sphinx key needed for nym address construction.
|
data: LpMixnetRegistrationResponseMessage,
|
||||||
/// Only populated for Mixnet mode registrations.
|
},
|
||||||
pub lp_gateway_data: Option<LpMixnetGatewayData>,
|
}
|
||||||
|
|
||||||
/// Allocated bandwidth in bytes
|
/// Represents the registration status after the last received client message.
|
||||||
pub allocated_bandwidth: i64,
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum RegistrationStatus {
|
||||||
|
/// The registration has been completed successfully
|
||||||
|
Completed,
|
||||||
|
|
||||||
|
/// The registration has failed
|
||||||
|
Failed,
|
||||||
|
|
||||||
|
/// To complete registration the client needs to send additional data,
|
||||||
|
/// e.g. a credential. it is context dependent.
|
||||||
|
PendingMoreData,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistrationStatus {
|
||||||
|
pub fn is_successful(&self) -> bool {
|
||||||
|
matches!(self, RegistrationStatus::Completed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_failed(&self) -> bool {
|
||||||
|
matches!(self, RegistrationStatus::Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_pending(&self) -> bool {
|
||||||
|
matches!(self, RegistrationStatus::PendingMoreData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_timestamp() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.inspect_err(|_| error!("the current timestamp predates unix epoch!"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LpRegistrationRequest {
|
impl LpRegistrationRequest {
|
||||||
/// Create a new dVPN registration request
|
/// Helper wrapping timestamp extraction
|
||||||
pub fn new_dvpn<R>(
|
fn new(registration_data: LpRegistrationRequestData) -> LpRegistrationRequest {
|
||||||
rng: &mut R,
|
|
||||||
wg_public_key: nym_wireguard_types::PeerPublicKey,
|
|
||||||
credential: CredentialSpendingData,
|
|
||||||
ticket_type: TicketType,
|
|
||||||
) -> Self
|
|
||||||
where
|
|
||||||
R: RngCore + CryptoRng,
|
|
||||||
{
|
|
||||||
let mut psk = [0u8; 32];
|
|
||||||
rng.fill_bytes(&mut psk);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
registration_data: LpRegistrationData::Dvpn {
|
registration_data,
|
||||||
data: Box::new(LpDvpnRegistrationRequest {
|
timestamp: current_timestamp(),
|
||||||
wg_public_key,
|
|
||||||
credential,
|
|
||||||
ticket_type,
|
|
||||||
psk,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
timestamp: std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.expect("System time before UNIX epoch")
|
|
||||||
.as_secs(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create new dVPN registration initialisation request
|
||||||
|
pub fn new_initial_dvpn(
|
||||||
|
wg_public_key: nym_wireguard_types::PeerPublicKey,
|
||||||
|
psk: [u8; 32],
|
||||||
|
) -> Self {
|
||||||
|
Self::new(LpRegistrationRequestData::Dvpn {
|
||||||
|
data: Box::new(LpDvpnRegistrationRequestMessage {
|
||||||
|
content: LpDvpnRegistrationRequestMessageContent::InitialRequest(
|
||||||
|
LpDvpnRegistrationInitialRequest { wg_public_key, psk },
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_finalise_dvpn(credential: BandwidthClaim) -> Self {
|
||||||
|
Self::new(LpRegistrationRequestData::Dvpn {
|
||||||
|
data: Box::new(LpDvpnRegistrationRequestMessage {
|
||||||
|
content: LpDvpnRegistrationRequestMessageContent::Finalisation(
|
||||||
|
LpDvpnRegistrationFinalisation { credential },
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate the request timestamp is within acceptable bounds
|
/// Validate the request timestamp is within acceptable bounds
|
||||||
pub fn validate_timestamp(&self, max_skew_secs: u64) -> bool {
|
pub fn validate_timestamp(&self, max_skew_secs: u64) -> bool {
|
||||||
let now = std::time::SystemTime::now()
|
let now = current_timestamp();
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
(now as i64 - self.timestamp as i64).abs() <= max_skew_secs as i64
|
(now as i64 - self.timestamp as i64).abs() <= max_skew_secs as i64
|
||||||
}
|
}
|
||||||
@@ -164,35 +174,92 @@ impl LpRegistrationRequest {
|
|||||||
|
|
||||||
impl LpRegistrationResponse {
|
impl LpRegistrationResponse {
|
||||||
/// Create a success response with GatewayData (for dVPN mode)
|
/// Create a success response with GatewayData (for dVPN mode)
|
||||||
pub fn success(allocated_bandwidth: i64, gateway_data: WireguardConfiguration) -> Self {
|
pub fn success_dvpn(config: WireguardRegistrationData, upgrade_mode: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
success: true,
|
status: RegistrationStatus::Completed,
|
||||||
error: None,
|
response_data: LpRegistrationResponseData::Dvpn {
|
||||||
gateway_data: Some(gateway_data),
|
data: LpDvpnRegistrationResponseMessage {
|
||||||
lp_gateway_data: None,
|
content: LpDvpnRegistrationResponseMessageContent::CompletedRegistration(
|
||||||
allocated_bandwidth,
|
dvpn::CompletedRegistrationResponse {
|
||||||
|
config,
|
||||||
|
upgrade_mode,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a success response for mixnet mode with LpGatewayData
|
pub fn success_mixnet(config: LpMixnetGatewayData) -> Self {
|
||||||
pub fn success_mixnet(allocated_bandwidth: i64, lp_gateway_data: LpMixnetGatewayData) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
success: true,
|
status: RegistrationStatus::Completed,
|
||||||
error: None,
|
response_data: LpRegistrationResponseData::Mixnet {
|
||||||
gateway_data: None,
|
data: LpMixnetRegistrationResponseMessage {
|
||||||
lp_gateway_data: Some(lp_gateway_data),
|
content: LpMixnetRegistrationResponseMessageContent::CompletedRegistration(
|
||||||
allocated_bandwidth,
|
mixnet::CompletedRegistrationResponse { config },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an error response
|
/// Create an error response
|
||||||
pub fn error(error: String) -> Self {
|
pub fn error(error: impl Into<String>, mode: RegistrationMode) -> Self {
|
||||||
Self {
|
let response_data = match mode {
|
||||||
success: false,
|
RegistrationMode::Dvpn => LpRegistrationResponseData::Dvpn {
|
||||||
error: Some(error),
|
data: LpDvpnRegistrationResponseMessage::error(error),
|
||||||
gateway_data: None,
|
},
|
||||||
lp_gateway_data: None,
|
RegistrationMode::Mixnet => LpRegistrationResponseData::Mixnet {
|
||||||
allocated_bandwidth: 0,
|
data: LpMixnetRegistrationResponseMessage::error(error),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
LpRegistrationResponse {
|
||||||
|
status: RegistrationStatus::Failed,
|
||||||
|
response_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_dvpn_credential() -> Self {
|
||||||
|
LpRegistrationResponse {
|
||||||
|
status: RegistrationStatus::PendingMoreData,
|
||||||
|
response_data: LpRegistrationResponseData::Dvpn {
|
||||||
|
data: LpDvpnRegistrationResponseMessage {
|
||||||
|
content: LpDvpnRegistrationResponseMessageContent::RequiresCredential(
|
||||||
|
RequiresCredentialResponse,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_dvpn_response(self) -> Option<LpDvpnRegistrationResponseMessage> {
|
||||||
|
match self.response_data {
|
||||||
|
LpRegistrationResponseData::Dvpn { data } => Some(data),
|
||||||
|
LpRegistrationResponseData::Mixnet { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_mixnet_response(self) -> Option<LpMixnetRegistrationResponseMessage> {
|
||||||
|
match self.response_data {
|
||||||
|
LpRegistrationResponseData::Mixnet { data } => Some(data),
|
||||||
|
LpRegistrationResponseData::Dvpn { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_message(&self) -> Option<&str> {
|
||||||
|
match &self.response_data {
|
||||||
|
LpRegistrationResponseData::Dvpn { data } => match &data.content {
|
||||||
|
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(response) => {
|
||||||
|
Some(&response.error)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
LpRegistrationResponseData::Mixnet { data } => match &data.content {
|
||||||
|
LpMixnetRegistrationResponseMessageContent::RegistrationFailure(response) => {
|
||||||
|
Some(&response.error)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,23 +274,171 @@ impl LpRegistrationResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod dvpn {
|
||||||
|
use crate::WireguardRegistrationData;
|
||||||
|
use nym_authenticator_requests::models::BandwidthClaim;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// client
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpDvpnRegistrationRequestMessage {
|
||||||
|
pub content: LpDvpnRegistrationRequestMessageContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LpDvpnRegistrationRequestMessageContent {
|
||||||
|
InitialRequest(LpDvpnRegistrationInitialRequest),
|
||||||
|
Finalisation(LpDvpnRegistrationFinalisation),
|
||||||
|
// in theory, we could also extend it with Bandwidth-related messages,
|
||||||
|
// but that shouldn't really be the responsibility of a Registration client.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpDvpnRegistrationInitialRequest {
|
||||||
|
/// Client's WireGuard public key (for dVPN mode)
|
||||||
|
pub wg_public_key: nym_wireguard_types::PeerPublicKey,
|
||||||
|
|
||||||
|
/// Preshared key to be used for the connection
|
||||||
|
pub psk: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpDvpnRegistrationFinalisation {
|
||||||
|
/// Ecash credential
|
||||||
|
pub credential: BandwidthClaim,
|
||||||
|
}
|
||||||
|
|
||||||
|
// gateway
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpDvpnRegistrationResponseMessage {
|
||||||
|
pub content: LpDvpnRegistrationResponseMessageContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpDvpnRegistrationResponseMessage {
|
||||||
|
pub fn error(error: impl Into<String>) -> Self {
|
||||||
|
LpDvpnRegistrationResponseMessage {
|
||||||
|
content: LpDvpnRegistrationResponseMessageContent::RegistrationFailure(
|
||||||
|
RegistrationFailureResponse {
|
||||||
|
error: error.into(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LpDvpnRegistrationResponseMessageContent {
|
||||||
|
RequiresCredential(RequiresCredentialResponse),
|
||||||
|
CompletedRegistration(CompletedRegistrationResponse),
|
||||||
|
RegistrationFailure(RegistrationFailureResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct CompletedRegistrationResponse {
|
||||||
|
/// Gateway configuration data for dVPN mode (WireGuard)
|
||||||
|
/// This matches what WireguardRegistrationResult expects
|
||||||
|
pub config: WireguardRegistrationData,
|
||||||
|
|
||||||
|
/// 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)]
|
||||||
|
pub struct RequiresCredentialResponse;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RegistrationFailureResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mixnet {
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// client
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpMixnetRegistrationRequestMessage {
|
||||||
|
pub content: LpMixnetRegistrationRequestContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpMixnetRegistrationRequestContent {
|
||||||
|
/// Client's ed25519 public key (identity)
|
||||||
|
///
|
||||||
|
/// Used to derive DestinationAddressBytes for ActiveClientsStore lookup.
|
||||||
|
pub client_ed25519_pubkey: ed25519::PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// gateway
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LpMixnetRegistrationResponseMessage {
|
||||||
|
pub content: LpMixnetRegistrationResponseMessageContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LpMixnetRegistrationResponseMessage {
|
||||||
|
pub fn error(error: impl Into<String>) -> Self {
|
||||||
|
LpMixnetRegistrationResponseMessage {
|
||||||
|
content: LpMixnetRegistrationResponseMessageContent::RegistrationFailure(
|
||||||
|
RegistrationFailureResponse {
|
||||||
|
error: error.into(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum LpMixnetRegistrationResponseMessageContent {
|
||||||
|
CompletedRegistration(CompletedRegistrationResponse),
|
||||||
|
RegistrationFailure(RegistrationFailureResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gateway data for mixnet mode registration
|
||||||
|
///
|
||||||
|
/// Contains the gateway's identity and sphinx key needed for the client
|
||||||
|
/// to construct its full nym Recipient address.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct LpMixnetGatewayData {
|
||||||
|
/// Gateway's ed25519 identity public key
|
||||||
|
///
|
||||||
|
/// Forms part of the client's nym Recipient address.
|
||||||
|
pub gateway_identity: ed25519::PublicKey,
|
||||||
|
// TODO: what we really need in here is the address of internal IPR
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CompletedRegistrationResponse {
|
||||||
|
/// Gateway data for mixnet mode
|
||||||
|
///
|
||||||
|
/// Contains gateway identity and sphinx key needed for nym address construction.
|
||||||
|
pub config: LpMixnetGatewayData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RegistrationFailureResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
use nym_test_utils::helpers::deterministic_rng;
|
use nym_test_utils::helpers::deterministic_rng;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
// ==================== Helper Functions ====================
|
// ==================== Helper Functions ====================
|
||||||
|
|
||||||
fn create_test_gateway_data() -> WireguardConfiguration {
|
fn create_test_wg_config() -> WireguardRegistrationData {
|
||||||
use std::net::Ipv6Addr;
|
WireguardRegistrationData {
|
||||||
|
|
||||||
WireguardConfiguration {
|
|
||||||
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(
|
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(
|
||||||
nym_sphinx::PublicKey::from([1u8; 32]),
|
nym_sphinx::PublicKey::from([1u8; 32]),
|
||||||
),
|
),
|
||||||
|
port: 1234,
|
||||||
private_ipv4: Ipv4Addr::new(10, 0, 0, 1),
|
private_ipv4: Ipv4Addr::new(10, 0, 0, 1),
|
||||||
private_ipv6: Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 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"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,37 +446,59 @@ mod tests {
|
|||||||
|
|
||||||
// ==================== LpRegistrationResponse Tests ====================
|
// ==================== LpRegistrationResponse Tests ====================
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lp_registration_response_success() {
|
|
||||||
let gateway_data = create_test_gateway_data();
|
|
||||||
let allocated_bandwidth = 1_000_000_000;
|
|
||||||
|
|
||||||
let response = LpRegistrationResponse::success(allocated_bandwidth, gateway_data.clone());
|
|
||||||
|
|
||||||
assert!(response.success);
|
|
||||||
assert!(response.error.is_none());
|
|
||||||
assert!(response.gateway_data.is_some());
|
|
||||||
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
|
|
||||||
|
|
||||||
let returned_gw_data = response
|
|
||||||
.gateway_data
|
|
||||||
.expect("Gateway data should be present in success response");
|
|
||||||
assert_eq!(returned_gw_data.public_key, gateway_data.public_key);
|
|
||||||
assert_eq!(returned_gw_data.private_ipv4, gateway_data.private_ipv4);
|
|
||||||
assert_eq!(returned_gw_data.private_ipv6, gateway_data.private_ipv6);
|
|
||||||
assert_eq!(returned_gw_data.endpoint, gateway_data.endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lp_registration_response_error() {
|
fn test_lp_registration_response_error() {
|
||||||
let error_msg = String::from("Insufficient bandwidth");
|
let error_msg = String::from("Insufficient bandwidth");
|
||||||
|
|
||||||
let response = LpRegistrationResponse::error(error_msg.clone());
|
let response_mixnet =
|
||||||
|
LpRegistrationResponse::error(error_msg.clone(), RegistrationMode::Mixnet);
|
||||||
|
let response_dvpn =
|
||||||
|
LpRegistrationResponse::error(error_msg.clone(), RegistrationMode::Dvpn);
|
||||||
|
|
||||||
assert!(!response.success);
|
assert!(response_mixnet.status.is_failed());
|
||||||
assert_eq!(response.error, Some(error_msg));
|
assert!(response_dvpn.status.is_failed());
|
||||||
assert!(response.gateway_data.is_none());
|
|
||||||
assert_eq!(response.allocated_bandwidth, 0);
|
// check mixnet
|
||||||
|
let LpRegistrationResponseData::Mixnet { data } = response_mixnet.response_data else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
|
||||||
|
let LpMixnetRegistrationResponseMessageContent::RegistrationFailure(failure) = data.content
|
||||||
|
else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
assert_eq!(failure.error, error_msg);
|
||||||
|
|
||||||
|
// check dvpn
|
||||||
|
let LpRegistrationResponseData::Dvpn { data } = response_dvpn.response_data else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
|
||||||
|
let LpDvpnRegistrationResponseMessageContent::RegistrationFailure(failure) = data.content
|
||||||
|
else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
assert_eq!(failure.error, error_msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lp_registration_response_success_dvpn() {
|
||||||
|
let cfg = create_test_wg_config();
|
||||||
|
|
||||||
|
let response = LpRegistrationResponse::success_dvpn(cfg, false);
|
||||||
|
assert!(response.status.is_successful());
|
||||||
|
|
||||||
|
let LpRegistrationResponseData::Dvpn { data } = response.response_data else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
|
||||||
|
let LpDvpnRegistrationResponseMessageContent::CompletedRegistration(complete) =
|
||||||
|
data.content
|
||||||
|
else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
assert_eq!(complete.config, cfg);
|
||||||
|
assert!(!complete.upgrade_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -272,19 +509,18 @@ mod tests {
|
|||||||
let lp_gateway_data = LpMixnetGatewayData {
|
let lp_gateway_data = LpMixnetGatewayData {
|
||||||
gateway_identity: *valid_key.public_key(),
|
gateway_identity: *valid_key.public_key(),
|
||||||
};
|
};
|
||||||
let allocated_bandwidth = 500_000_000;
|
let response = LpRegistrationResponse::success_mixnet(lp_gateway_data.clone());
|
||||||
|
assert!(response.status.is_successful());
|
||||||
|
|
||||||
let response = LpRegistrationResponse::success_mixnet(allocated_bandwidth, lp_gateway_data);
|
let LpRegistrationResponseData::Mixnet { data } = response.response_data else {
|
||||||
|
panic!("unexpected response")
|
||||||
|
};
|
||||||
|
|
||||||
assert!(response.success);
|
let LpMixnetRegistrationResponseMessageContent::CompletedRegistration(complete) =
|
||||||
assert!(response.error.is_none());
|
data.content
|
||||||
assert!(response.gateway_data.is_none());
|
else {
|
||||||
assert!(response.lp_gateway_data.is_some());
|
panic!("unexpected response")
|
||||||
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
|
};
|
||||||
|
assert_eq!(complete.config, lp_gateway_data);
|
||||||
let gw_data = response
|
|
||||||
.lp_gateway_data
|
|
||||||
.expect("LpGatewayData should be present");
|
|
||||||
assert_eq!(gw_data.gateway_identity, *valid_key.public_key());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub enum ProtocolError {
|
|||||||
InvalidServiceProviderType(u8),
|
InvalidServiceProviderType(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum ServiceProviderType {
|
pub enum ServiceProviderType {
|
||||||
NetworkRequester = 0,
|
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 struct Protocol {
|
||||||
pub version: u8,
|
pub version: u8,
|
||||||
pub service_provider_type: ServiceProviderType,
|
pub service_provider_type: ServiceProviderType,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ rand_chacha = { workspace = true }
|
|||||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
||||||
nym-bin-common = { workspace = true, features = ["tracing"] }
|
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::traits::Timeboxed;
|
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 rand_chacha::rand_core::SeedableRng;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::error::Elapsed;
|
use tokio::time::error::Elapsed;
|
||||||
|
|
||||||
use nym_bin_common::logging::tracing_subscriber::EnvFilter;
|
pub use rand_chacha::ChaCha20Rng as DeterministicRng;
|
||||||
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::rand_core::{CryptoRng, RngCore};
|
pub use rand_chacha::rand_core::{CryptoRng, RngCore};
|
||||||
|
|
||||||
pub fn leak<T>(val: T) -> &'static mut T {
|
pub fn leak<T>(val: T) -> &'static mut T {
|
||||||
@@ -26,16 +26,16 @@ where
|
|||||||
tokio::spawn(async move { fut.timeboxed().await })
|
tokio::spawn(async move { fut.timeboxed().await })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deterministic_rng() -> ChaCha20Rng {
|
pub fn deterministic_rng() -> DeterministicRng {
|
||||||
seeded_rng([42u8; 32])
|
seeded_rng([42u8; 32])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seeded_rng(seed: [u8; 32]) -> ChaCha20Rng {
|
pub fn seeded_rng(seed: [u8; 32]) -> DeterministicRng {
|
||||||
ChaCha20Rng::from_seed(seed)
|
DeterministicRng::from_seed(seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn u64_seeded_rng(seed: u64) -> ChaCha20Rng {
|
pub fn u64_seeded_rng(seed: u64) -> DeterministicRng {
|
||||||
ChaCha20Rng::seed_from_u64(seed)
|
DeterministicRng::seed_from_u64(seed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test logger to use during debugging
|
// test logger to use during debugging
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ description = "Functions and tests for checking Nym's Credential Proxy is being
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
jwt-simple = { workspace = true }
|
jwt-simple = { workspace = true }
|
||||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
reqwest = { workspace = true, features = ["rustls"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
|
|||||||
fn from(req: &PeerControlRequest) -> Self {
|
fn from(req: &PeerControlRequest) -> Self {
|
||||||
match req {
|
match req {
|
||||||
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
|
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||||
PeerControlRequest::RegisterPeer { .. } => PeerControlRequestTypeV2::AddPeer,
|
PeerControlRequest::PreAllocateIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
|
||||||
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
|
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
|
||||||
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
|
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
|
||||||
PeerControlRequest::GetClientBandwidthByKey { .. } => {
|
PeerControlRequest::GetClientBandwidthByKey { .. } => {
|
||||||
@@ -41,6 +41,8 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
|
|||||||
PeerControlRequest::GetVerifierByIp { ip, .. } => {
|
PeerControlRequest::GetVerifierByIp { ip, .. } => {
|
||||||
PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip }
|
PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip }
|
||||||
}
|
}
|
||||||
|
PeerControlRequest::CheckActivePeer { .. } => unreachable!(),
|
||||||
|
PeerControlRequest::ReleaseIpPair { .. } => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +115,7 @@ impl MockPeerControllerV2 {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
PeerControlRequest::RegisterPeer { response_tx, .. } => {
|
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
|
||||||
response_tx
|
response_tx
|
||||||
.send(
|
.send(
|
||||||
*response
|
*response
|
||||||
@@ -176,6 +178,24 @@ impl MockPeerControllerV2 {
|
|||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
PeerControlRequest::ReleaseIpPair { response_tx, .. } => {
|
||||||
|
response_tx
|
||||||
|
.send(
|
||||||
|
*response
|
||||||
|
.downcast()
|
||||||
|
.expect("registered response has mismatched type"),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
PeerControlRequest::CheckActivePeer { response_tx, .. } => {
|
||||||
|
response_tx
|
||||||
|
.send(
|
||||||
|
*response
|
||||||
|
.downcast()
|
||||||
|
.expect("registered response has mismatched type"),
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ nym-credential-verification = { workspace = true }
|
|||||||
nym-crypto = { workspace = true, features = ["asymmetric"] }
|
nym-crypto = { workspace = true, features = ["asymmetric"] }
|
||||||
nym-gateway-storage = { workspace = true }
|
nym-gateway-storage = { workspace = true }
|
||||||
nym-gateway-requests = { workspace = true }
|
nym-gateway-requests = { workspace = true }
|
||||||
nym-ip-packet-requests = { workspace = true }
|
nym-authenticator-requests = { workspace = true }
|
||||||
nym-metrics = { workspace = true }
|
nym-metrics = { workspace = true }
|
||||||
nym-network-defaults = { workspace = true }
|
nym-network-defaults = { workspace = true }
|
||||||
nym-task = { workspace = true }
|
nym-task = { workspace = true }
|
||||||
@@ -36,7 +36,10 @@ nym-wireguard-types = { workspace = true }
|
|||||||
nym-node-metrics = { workspace = true }
|
nym-node-metrics = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
nym-gateway-storage = { workspace = true, features = ["mock"] }
|
nym-gateway-storage = { workspace = true, features = ["mock"] }
|
||||||
|
mock_instant = { workspace = true }
|
||||||
|
nym-test-utils = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user