Compare commits

..

1 Commits

Author SHA1 Message Date
benedetta davico 09fa612a82 Update sandbox.env 2026-01-27 17:15:48 +01:00
431 changed files with 20511 additions and 25061 deletions
-1
View File
@@ -3,5 +3,4 @@
.gitignore
**/node_modules
**/target
target-otel
dist
-4
View File
@@ -6,8 +6,6 @@ on:
jobs:
build:
runs-on: arc-ubuntu-22.04
env:
NEXT_PUBLIC_SITE_URL: https://nymtech.net/docs
defaults:
run:
working-directory: documentation/docs
@@ -43,8 +41,6 @@ jobs:
run: pnpm i
- name: Build project
run: pnpm run build
- name: Generate sitemap
run: npx next-sitemap
- name: Move files to /dist/
run: ../scripts/move-to-dist.sh
+32 -69
View File
@@ -3,28 +3,13 @@ name: ci-build-upload-binaries
on:
workflow_dispatch:
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:
description: 'Force RUSTFLAGS="--cfg tokio_unstable" (auto-set when tokio-console is selected)'
required: false
description: 'True to add RUSTFLAGS="--cfg tokio_unstable"'
required: true
default: false
type: boolean
enable_deb:
description: "Enable cargo-deb installation and .deb package building"
description: "True to enable cargo-deb installation and .deb package building"
required: false
default: false
type: boolean
@@ -36,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [arc-linux-latest]
platform: [ arc-linux-latest ]
runs-on: ${{ matrix.platform }}
env:
@@ -51,62 +36,38 @@ jobs:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
rm -rf ci-builds || true
mkdir -p "$OUTPUT_DIR"
echo "$OUTPUT_DIR"
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Resolve cargo features and RUSTFLAGS
if: github.event_name == 'workflow_dispatch'
shell: bash
- name: Sets env vars for tokio if set in manual dispatch inputs
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
run: |
FEATURES=""
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
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Build all binaries
shell: bash
run: cargo build --workspace --release ${{ env.CARGO_FEATURES }}
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --release ${{ env.CARGO_FEATURES }}
- 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
shell: bash
run: cargo install cargo-deb
- name: Build deb packages
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
shell: bash
run: make deb
if: github.event_name == 'workflow_dispatch' && inputs.enable_deb == true
- name: Upload Artifact
if: github.event_name == 'workflow_dispatch'
@@ -123,22 +84,24 @@ jobs:
target/release/nym-node
retention-days: 30
# If this was a pull_request or nightly, upload to build server
- name: Prepare build output
# if: github.event_name == 'schedule' || github.event_name == 'pull_request'
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
cp target/release/nym-client "$OUTPUT_DIR"
cp target/release/nym-socks5-client "$OUTPUT_DIR"
cp target/release/nym-api "$OUTPUT_DIR"
cp target/release/nym-network-requester "$OUTPUT_DIR"
cp target/release/nymvisor "$OUTPUT_DIR"
cp target/release/nym-node "$OUTPUT_DIR"
cp target/release/nym-cli "$OUTPUT_DIR"
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.enable_deb }}" == "true" ]]; then
cp target/debian/*.deb "$OUTPUT_DIR"
cp target/release/nym-client $OUTPUT_DIR
cp target/release/nym-socks5-client $OUTPUT_DIR
cp target/release/nym-api $OUTPUT_DIR
cp target/release/nym-network-requester $OUTPUT_DIR
cp target/release/nymvisor $OUTPUT_DIR
cp target/release/nym-node $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR
fi
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
+1 -10
View File
@@ -10,7 +10,6 @@ on:
- 'nym-api/**'
- 'nym-authenticator-client/**'
- 'nym-credential-proxy/**'
- 'nym-gateway-probe/**'
- 'nym-ip-packet-client/**'
- 'nym-network-monitor/**'
- 'nym-node/**'
@@ -90,7 +89,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
args: --workspace --all-targets --exclude nym-gateway-probe -- -D warnings
- name: Clippy (non-macos)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
@@ -105,14 +104,6 @@ jobs:
with:
command: build
# only build on linux because of wg FFI bindings of its dependency (network probe)
- name: Build nym-node-status-api (linux only)
if: runner.os == 'Linux'
uses: actions-rs/cargo@v1
with:
command: build
args: -p nym-node-status-api
- name: Build all examples
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
@@ -3,7 +3,7 @@ name: ci-check-ns-api-version
on:
pull_request:
paths:
- "nym-node-status-api/nym-node-status-api/**"
- "nym-node-status-api/**"
env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -1,79 +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
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
@@ -1,59 +0,0 @@
# 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
-86
View File
@@ -1,86 +0,0 @@
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!"
@@ -1,74 +0,0 @@
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
-21
View File
@@ -1,21 +0,0 @@
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
@@ -51,3 +51,25 @@ jobs:
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-wallet
NYM_PROJECT_NAME: "nym-wallet"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ job.status == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+37 -2
View File
@@ -10,8 +10,8 @@ jobs:
strategy:
fail-fast: false
matrix:
rust: [ stable, beta ]
os: [ ubuntu-22.04, windows-latest, macos-latest ]
rust: [stable, beta]
os: [ubuntu-22.04, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -93,3 +93,38 @@ jobs:
with:
command: clippy
args: --workspace --all-targets -- -D warnings
notification:
needs: build
runs-on: custom-linux
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v3
- name: Check out repository code
uses: actions/checkout@v6
- name: install npm
uses: actions/setup-node@v4
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 20
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym nightly build"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+36 -1
View File
@@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-22.04, macos-latest, windows-latest ]
os: [ubuntu-22.04, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -55,3 +55,38 @@ jobs:
with:
command: clippy
args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings
notification:
needs: build
runs-on: custom-linux
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v3
- name: Check out repository code
uses: actions/checkout@v6
- name: install npm
uses: actions/setup-node@v4
if: env.WORKFLOW_CONCLUSION == 'failure'
with:
node-version: 20
- name: Matrix - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "nym-wallet-nightly-build"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_NIGHTLY }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -24,3 +24,34 @@ jobs:
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
notification:
needs: cargo-deny
runs-on: custom-linux
steps:
- name: Check out repository code
uses: actions/checkout@v6
- name: Download report from previous job
uses: actions/download-artifact@v7
with:
name: report
path: .github/workflows/support-files/notifications
- name: install npm
uses: actions/setup-node@v4
with:
node-version: 20
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: security
NYM_PROJECT_NAME: "Daily security report"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_AUDIT }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -0,0 +1,43 @@
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
+47
View File
@@ -0,0 +1,47 @@
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
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -8,7 +8,7 @@ env:
jobs:
build-container:
runs-on: ubuntu-latest
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.52.4
uses: mikefarah/yq@v4.50.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -1,41 +0,0 @@
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
+35 -7
View File
@@ -4,23 +4,51 @@ This is a collection of scripts and files to support GitHub Actions.
## Sending Notifications
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub
Actions.
These scripts send CI notifications to Matrix by creating messages from templates and env vars passed from GitHub Actions.
### Adding notifications to a GitHub Action
```
jobs:
build:
...
- name: Notifications - Node Install
run: npm install
working-directory: .github/workflows/support-files/notifications
- name: Notifications - Send
env:
NYM_NOTIFICATION_KIND: "my-component"
GIT_BRANCH: "${GITHUB_REF##*/}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
IS_SUCCESS: "${{ job.status == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
```
Notifications are run by adding the snippet above to a GitHub Action, and:
1. Installing node packages needed at run time
2. Set the env vars as required:
- `NYM_NOTIFICATION_KIND` matches the directory in `.github/workflows/support-files/${NYM_NOTIFICATION_KIND}` to provide the templates and extra scripting in `index.js`
- Matrix credentials, room and other env vars for the status of the build and repo
3. Replacing the default entry point shell script on the `keybaseio/client:stable-node` docker image to run `.github/workflows/support-files/notifications/entry_point.sh`
### Running locally
You will need:
- Node 16 LTS
- npm
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix
credentials.
Copy `.github/workflows/support-files/.env.example` to `.github/workflows/support-files/.env` and valid Matrix credentials.
Then run `npm install` to get dependencies.
Start development mode for the notification type you want either by passing the value as an env var called
`NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
Start development mode for the notification type you want either by passing the value as an env var called `NYM_NOTIFICATION_KIND` or set the `.env` file values correctly.
```bash
cd .github/workflows/support-files
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# pass exit codes out to GitHub Actions
set -euxo pipefail
# change to the directory that contains this script
cd "${0%/*}"
# run the node script
node send_message.js
@@ -0,0 +1,126 @@
require('dotenv').config();
const { sendMatrixMessage } = require('./send_message_to_matrix');
let context = {
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security','ci-docs','cd-docs','ci-dev','cd-dev'],
};
/**
* Validate that all required env and context vars are available
*/
function validateContext() {
if (!context.env.NYM_NOTIFICATION_KIND) {
throw new Error(
'Please set env var NYM_NOTIFICATION_KIND with the project kind that matches a directory in ".github/workflows/support-files"',
);
}
if (!context.kinds.includes(context.env.NYM_NOTIFICATION_KIND)) {
throw new Error(`Env var NYM_NOTIFICATION_KIND is not in ${context.kinds}`);
}
if (!context.env.NYM_PROJECT_NAME) {
throw new Error(
'Please set env var NYM_PROJECT_NAME with the project name for displaying in notification messages',
);
}
if (context.env.MATRIX_ROOM) {
if (!context.env.MATRIX_SERVER) {
throw new Error(
'Matrix server is not defined. Please set env var MATRIX_SERVER',
);
}
if (!context.env.MATRIX_USER_ID) {
throw new Error(
'Matrix user id is not defined. Please set env var MATRIX_USER_ID',
);
}
if (!context.env.MATRIX_TOKEN) {
throw new Error(
'Matrix token is not defined. Please set env var MATRIX_TOKEN',
);
}
if (!context.env.MATRIX_DEVICE_ID) {
throw new Error(
'Matrix device id is not defined. Please set env var MATRIX_DEVICE_ID',
);
}
}
}
/**
* Creates a context that will be available in the templates for rendering notifications
*/
function createTemplateContext() {
const options = { dateStyle: 'full', timeStyle: 'long' };
context.timestamp = new Date().toLocaleString(undefined, options);
// add environment to template context and validate
context.env = process.env;
try {
validateContext();
} catch (e) {
if(process.env.SHOW_DEBUG) {
// recursively print the context for easy debugging and rethrow the error
console.dir({ context }, { depth: null });
}
throw e;
}
context.kind = context.env.NYM_NOTIFICATION_KIND;
if (!context.env.GIT_BRANCH_NAME) {
context.env.GIT_BRANCH_NAME = context.env.GITHUB_REF.split('/')
.slice(2)
.join('/');
}
context.status = process.env.IS_SUCCESS === 'true' ? 'success' : 'failure';
}
/**
* Uses the `kind` set in the context to process the context and generate a notification message
* @returns {Promise<string>} A string notification message body
*/
async function processKindScript() {
const script = require(`../${context.kind}`);
if (!script.addToContextAndValidate) {
throw new Error(
`"./${context.kind}/index.js" does not export a method called "async addToContextAndValidate(context)"`,
);
}
if (!script.getMessageBody) {
throw new Error(
`"./${context.kind}/index.js" does not export a method called "async getMessageBody(context)"`,
);
}
// call the script to modify and validate the context
await script.addToContextAndValidate(context);
// let the script create a message body and return the result as a string for sending
return await script.getMessageBody(context);
}
/**
* The main function, as async so that await syntax is available
*/
async function main() {
createTemplateContext();
console.log(`Sending notification for kind "${context.kind}"...`);
const messageBody = await processKindScript();
if(process.env.SHOW_DEBUG) {
console.log('-----------------------------------------');
console.log(messageBody);
console.log('-----------------------------------------');
}
if(context.env.MATRIX_ROOM) {
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM)
}
if(context.env.MATRIX_ROOM_OF_SHAME && context.env.IS_SUCCESS !== 'true') {
// when a job fails
await sendMatrixMessage(context, messageBody, context.env.MATRIX_ROOM_OF_SHAME)
}
}
// call main function and let NodeJS handle the promise
main();
@@ -0,0 +1,67 @@
const sdk = require('matrix-js-sdk');
global.Olm = require('olm');
const { LocalStorage } = require('node-localstorage');
const localStorage = new LocalStorage('./scratch');
const {
LocalStorageCryptoStore,
} = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
var showdown = require('showdown');
// hide all matrix client output
console.error = (error) => console.log('❌ error: ', error);
process.stderr.write = () => {};
process.stdout.write = () => {};
function createClient(context, room, message) {
const server = context.env.MATRIX_SERVER;
const token = context.env.MATRIX_TOKEN;
const deviceId = context.env.MATRIX_DEVICE_ID;
const userId = context.env.MATRIX_USER_ID;
const client = sdk.createClient({
baseUrl: server,
accessToken: token,
userId,
deviceId,
sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage),
});
client.on('sync', async function(state, prevState, res) {
if (state !== 'PREPARED') return;
client.setGlobalErrorOnUnknownDevices(false);
try {
await client.joinRoom(room);
await client.sendEvent(
room,
'm.room.message',
{
msgtype: 'm.text',
format: 'org.matrix.custom.html',
body: message,
formatted_body: message,
},
'',
);
} catch (error) {
console.error('Job failed: ' + error.message);
}
client.stopClient();
process.exit(0);
});
return client;
}
async function sendMatrixMessage(contextArg, messageAsMarkdown, roomId) {
const converter = new showdown.Converter();
const messageAsHtml = converter.makeHtml(messageAsMarkdown);
const client = createClient(contextArg, roomId, messageAsHtml);
await client.initCrypto();
await client.startClient({ initialSyncLimit: 1 });
}
module.exports = {
sendMatrixMessage,
};
+1 -1
View File
@@ -67,6 +67,7 @@ nym-api/redocly/formatted-openapi.json
*.profraw
.beads
CLAUDE.md
docs
.claude
.superego
@@ -76,4 +77,3 @@ CLAUDE.md
.claude/settings.json
/notes
/target-otel
-156
View File
@@ -4,162 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.4-quark] (2026-02-24)
- Enhance CI workflow with feature inputs ([#6462])
- Chore/revert 6433 ([#6445])
- Lp/stateless handshake ([#6437])
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/client/internal-dev ([#6435])
- build(deps-dev): bump webpack from 5.102.1 to 5.104.1 ([#6432])
- build(deps-dev): bump webpack from 5.98.0 to 5.105.0 in /wasm/mix-fetch/internal-dev ([#6431])
- build(deps-dev): bump webpack from 5.94.0 to 5.104.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#6430])
- build(deps-dev): bump webpack from 5.77.0 to 5.104.1 in /wasm/zknym-lib/internal-dev ([#6429])
- build(deps-dev): bump webpack from 5.76.0 to 5.105.0 in /clients/native/examples/js-examples/websocket ([#6428])
- HTTP & DNS Improvements ([#6423])
- Endpoint for exit GW IPs ([#6418])
- build(deps): bump bytes from 1.6.0 to 1.11.1 in /contracts ([#6416])
- build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 ([#6415])
- build(deps): bump bytes from 1.11.0 to 1.11.1 ([#6414])
- build(deps): bump mikefarah/yq from 4.50.1 to 4.52.2 ([#6407])
- build(deps-dev): bump eslint from 8.57.1 to 9.26.0 ([#6405])
- Update reqwest to v0.13.1 ([#6401])
- build(deps): bump next from 15.5.9 to 16.1.5 in /documentation/docs ([#6387])
- build(deps): bump next from 15.4.10 to 16.1.5 in /nym-node-status-api/nym-node-status-ui ([#6385])
- build(deps): bump lodash from 4.17.21 to 4.17.23 ([#6369])
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 ([#6360])
- build(deps-dev): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/codegen/contract-clients ([#6359])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /sdk/typescript/packages/nodejs-client ([#6354])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /documentation/docs ([#6353])
- build(deps): bump lodash from 4.17.21 to 4.17.23 in /clients/native/examples/js-examples/websocket ([#6351])
- build(deps): bump lodash-es from 4.17.21 to 4.17.23 in /documentation/docs ([#6350])
- build(deps): bump diff from 5.2.0 to 5.2.2 in /documentation/docs ([#6345])
- Max/crates publishing tweaks ([#6343])
- build(deps): bump h3 from 1.15.4 to 1.15.5 ([#6339])
- build(deps): bump h3 from 1.15.4 to 1.15.5 in /documentation/docs ([#6332])
- build(deps): bump undici from 6.21.3 to 6.23.0 in /documentation/docs ([#6325])
- build(deps): bump rsa from 0.9.8 to 0.9.10 ([#6311])
- build(deps): bump qs and express in /wasm/mix-fetch/internal-dev ([#6308])
- build(deps): bump qs and express in /clients/native/examples/js-examples/websocket ([#6307])
- feat: introduce on-disk cache persistance for major nym-api caches ([#6302])
- Fix migrations in the Data Observatory ([#6271])
[#6462]: https://github.com/nymtech/nym/pull/6462
[#6445]: https://github.com/nymtech/nym/pull/6445
[#6437]: https://github.com/nymtech/nym/pull/6437
[#6435]: https://github.com/nymtech/nym/pull/6435
[#6432]: https://github.com/nymtech/nym/pull/6432
[#6431]: https://github.com/nymtech/nym/pull/6431
[#6430]: https://github.com/nymtech/nym/pull/6430
[#6429]: https://github.com/nymtech/nym/pull/6429
[#6428]: https://github.com/nymtech/nym/pull/6428
[#6423]: https://github.com/nymtech/nym/pull/6423
[#6418]: https://github.com/nymtech/nym/pull/6418
[#6416]: https://github.com/nymtech/nym/pull/6416
[#6415]: https://github.com/nymtech/nym/pull/6415
[#6414]: https://github.com/nymtech/nym/pull/6414
[#6407]: https://github.com/nymtech/nym/pull/6407
[#6405]: https://github.com/nymtech/nym/pull/6405
[#6401]: https://github.com/nymtech/nym/pull/6401
[#6387]: https://github.com/nymtech/nym/pull/6387
[#6385]: https://github.com/nymtech/nym/pull/6385
[#6369]: https://github.com/nymtech/nym/pull/6369
[#6360]: https://github.com/nymtech/nym/pull/6360
[#6359]: https://github.com/nymtech/nym/pull/6359
[#6354]: https://github.com/nymtech/nym/pull/6354
[#6353]: https://github.com/nymtech/nym/pull/6353
[#6351]: https://github.com/nymtech/nym/pull/6351
[#6350]: https://github.com/nymtech/nym/pull/6350
[#6345]: https://github.com/nymtech/nym/pull/6345
[#6343]: https://github.com/nymtech/nym/pull/6343
[#6339]: https://github.com/nymtech/nym/pull/6339
[#6332]: https://github.com/nymtech/nym/pull/6332
[#6325]: https://github.com/nymtech/nym/pull/6325
[#6311]: https://github.com/nymtech/nym/pull/6311
[#6308]: https://github.com/nymtech/nym/pull/6308
[#6307]: https://github.com/nymtech/nym/pull/6307
[#6302]: https://github.com/nymtech/nym/pull/6302
[#6271]: https://github.com/nymtech/nym/pull/6271
## [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)
- bugfix: downgrade gateway protocol to clients proposed version ([#6377])
Generated
+1781 -1951
View File
File diff suppressed because it is too large Load Diff
+107 -109
View File
@@ -185,6 +185,7 @@ default-members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -205,7 +206,7 @@ edition = "2024"
license = "Apache-2.0"
rust-version = "1.85"
readme = "README.md"
version = "1.20.4"
version = "1.20.1"
[workspace.dependencies]
addr = "0.15.6"
@@ -232,7 +233,7 @@ blake3 = "1.7.0"
bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.11.1"
bytes = "1.10.1"
cargo_metadata = "0.19.2"
celes = "2.6.0"
cfg-if = "1.0.0"
@@ -303,16 +304,13 @@ ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
mime = "0.3.17"
mock_instant = "0.6.0"
moka = { version = "0.12", features = ["future"] }
nix = "0.30.1"
notify = "5.1.0"
num_enum = "0.7.5"
once_cell = "1.21.3"
opentelemetry = "0.31.0"
opentelemetry_sdk = "0.31.0"
opentelemetry-otlp = "0.31.0"
tonic = "0.14.4"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
parking_lot = "0.12.3"
pem = "0.8"
petgraph = "0.6.5"
@@ -322,13 +320,12 @@ publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.13.1", default-features = false }
reqwest = { version = "0.12.15", default-features = false }
rs_merkle = "1.5.0"
schemars = "0.8.22"
semver = "1.0.26"
@@ -370,8 +367,9 @@ tower = "0.5.2"
tower-http = "0.6.6"
tracing = "0.1.41"
tracing-log = "0.2"
tracing-opentelemetry = "0.32.1"
tracing-opentelemetry = "0.19.0"
tracing-subscriber = "0.3.20"
tracing-tree = "0.2.2"
tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
ts-rs = "10.1.0"
@@ -383,7 +381,7 @@ url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "8.1"
utoipauto = "0.2"
uuid = "1.19.0"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
walkdir = "2"
@@ -393,106 +391,106 @@ zeroize = "1.7.0"
prometheus = { version = "0.14.0" }
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.4", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.4", path = "common/config" }
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.4", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.4", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-requests = { version = "1.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-api-requests = { version = "1.20.1", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.1", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.1", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.1", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.1", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.1", path = "common/bin-common" }
nym-cache = { version = "1.20.1", path = "common/nym-cache" }
nym-client-core = { version = "1.20.1", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.1", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.1", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.1", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.1", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.1", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.1", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.1", path = "common/config" }
nym-contracts-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.1", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.1", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.1", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.1", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.1", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.1", path = "common/credential-verification" }
nym-crypto = { version = "1.20.1", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.1", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.1", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.1", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.1", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.1", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.1", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.1", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-requests = { version = "1.20.1", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.1", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.1", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.1", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.1", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.1", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.1", path = "common/nym-id" }
nym-kkt-ciphersuite = { path = "common/nym-kkt-ciphersuite" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
nym-registration-common = { version = "1.20.4", path = "common/registration" }
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
nym-task = { version = "1.20.4", path = "common/task" }
nym-tun = { version = "1.20.4", path = "common/tun" }
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.4", path = "common/topology" }
nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
nym-ip-packet-client = { version = "1.20.1", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.1", path = "common/ip-packet-requests" }
nym-metrics = { version = "1.20.1", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.1", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.1", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.1", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.1", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.1", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.1", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.1", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.1", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.1", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.1", path = "nym-outfox" }
nym-registration-common = { version = "1.20.1", path = "common/registration" }
nym-pemstore = { version = "1.20.1", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.1", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.1", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.1", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.1", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.1", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.1", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.1", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.1", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.1", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.1", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.1", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.1", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.1", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.1", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.1", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.1", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.1", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.1", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.1", path = "common/statistics" }
nym-store-cipher = { version = "1.20.1", path = "common/store-cipher" }
nym-task = { version = "1.20.1", path = "common/task" }
nym-tun = { version = "1.20.1", path = "common/tun" }
nym-test-utils = { version = "1.20.1", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.1", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.1", path = "common/topology" }
nym-types = { version = "1.20.1", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.1", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.1", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.20.1", path = "common/verloc" }
nym-wireguard = { version = "1.20.1", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.1", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.1", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.1", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.1", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
nym-wasm-client-core = { version = "1.20.1", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.1", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.1", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.1", path = "common/nyxd-scraper-shared" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.71"
version = "1.1.69"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
File diff suppressed because it is too large Load Diff
@@ -19,7 +19,7 @@
"license": "Apache-2.0",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"webpack": "^5.105.0",
"webpack": "^5.76.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
},
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.71"
version = "1.1.69"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
-1
View File
@@ -18,7 +18,6 @@ mod util;
mod version;
pub use error::Error;
pub use util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
pub use v6 as latest;
pub use version::AuthenticatorVersion;
@@ -7,7 +7,6 @@ use crate::traits::{
TopUpBandwidthResponse, UpgradeModeStatus,
};
use crate::{v2, v3, v4, v5, v6};
use nym_sphinx::addressing::Recipient;
#[derive(Debug)]
pub enum AuthenticatorResponse {
@@ -18,17 +17,6 @@ pub enum AuthenticatorResponse {
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
}
pub struct SerialisedResponse {
pub bytes: Vec<u8>,
pub reply_to: Option<Recipient>,
}
impl SerialisedResponse {
pub fn new(bytes: Vec<u8>, reply_to: Option<Recipient>) -> Self {
Self { bytes, reply_to }
}
}
impl UpgradeModeStatus for AuthenticatorResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
match self {
-32
View File
@@ -1,38 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use std::net::{Ipv4Addr, Ipv6Addr};
pub fn authenticator_ipv6_to_ipv4(addr: Ipv6Addr) -> Ipv4Addr {
let before_last_byte = addr.octets()[14];
let last_byte = addr.octets()[15];
Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
)
}
pub fn authenticator_ipv4_to_ipv6(addr: Ipv4Addr) -> Ipv6Addr {
let before_last_byte = addr.octets()[2];
let last_byte = addr.octets()[3];
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
)
}
#[cfg(test)]
pub(crate) mod tests {
pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData;
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -56,11 +56,27 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
let (before_last_byte, last_byte) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_credentials_interface::CredentialSpendingData;
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -54,11 +54,27 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
let (before_last_byte, last_byte) = match value {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
@@ -3,12 +3,13 @@
use crate::error::Error;
use crate::models::BandwidthClaim;
use crate::util::{authenticator_ipv4_to_ipv6, authenticator_ipv6_to_ipv4};
use base64::{Engine, engine::general_purpose};
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
@@ -19,11 +20,13 @@ use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
@@ -51,11 +54,27 @@ impl fmt::Display for IpPair {
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (ipv4, ipv6) = match value {
IpAddr::V4(ipv4) => (ipv4, authenticator_ipv4_to_ipv6(ipv4)),
IpAddr::V6(ipv6_addr) => (authenticator_ipv6_to_ipv4(ipv6_addr), ipv6_addr),
let (before_last_byte, last_byte) = match value {
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let 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)
}
}
+1 -5
View File
@@ -21,7 +21,7 @@ pub struct MockBandwidthController {
impl BandwidthTicketProvider for MockBandwidthController {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
_ticket_type: TicketType,
_gateway_id: PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
@@ -100,10 +100,6 @@ impl BandwidthTicketProvider for MockBandwidthController {
let mut credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES)
.expect("Failed to deserialize test credential - this is a bug in the test harness");
// change the ticket type to the requested ticket
// note that verification outside mocks is going to fail
credential.payment.t_type = ticket_type.to_repr() as u8;
// Update spend_date to today to pass validation
credential.spend_date = OffsetDateTime::now_utc().date();
-19
View File
@@ -57,22 +57,3 @@ where
Ok(Some(token))
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box<T> {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
(**self)
.get_ecash_ticket(ticket_type, gateway_id, tickets_to_spend)
.await
}
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
}
+9 -13
View File
@@ -19,15 +19,12 @@ serde_json = { workspace = true, optional = true }
## tracing
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
tracing-tree = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
tracing-opentelemetry = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true }
opentelemetry = { workspace = true, features = ["trace"], optional = true }
## otel-otlp (modern OTLP export to SigNoz/any OTLP collector)
opentelemetry_sdk = { workspace = true, features = ["trace"], optional = true }
opentelemetry-otlp = { workspace = true, features = ["grpc-tonic", "trace", "tls-roots"], optional = true }
tonic = { workspace = true, optional = true }
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
[build-dependencies]
@@ -38,14 +35,13 @@ default = []
openapi = ["utoipa"]
output_format = ["serde_json", "dep:clap"]
bin_info_schema = ["schemars"]
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
otel-otlp = [
basic_tracing = ["dep:tracing", "tracing-subscriber"]
tracing = [
"basic_tracing",
"dep:opentelemetry",
"dep:opentelemetry_sdk",
"dep:opentelemetry-otlp",
"dep:tracing-opentelemetry",
"dep:tonic",
"tracing-tree",
"opentelemetry-jaeger",
"tracing-opentelemetry",
"opentelemetry",
]
clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"]
models = []
+39 -98
View File
@@ -4,9 +4,16 @@
use serde::{Deserialize, Serialize};
use std::io::IsTerminal;
// Re-export tracing_subscriber for consumers that need to compose layers
#[cfg(feature = "basic_tracing")]
#[cfg(feature = "tracing")]
pub use opentelemetry;
#[cfg(feature = "tracing")]
pub use opentelemetry_jaeger;
#[cfg(feature = "tracing")]
pub use tracing_opentelemetry;
#[cfg(feature = "tracing")]
pub use tracing_subscriber;
#[cfg(feature = "tracing")]
pub use tracing_tree;
#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
@@ -62,106 +69,40 @@ pub fn setup_tracing_logger() {
build_tracing_logger().init()
}
/// Initialize an OpenTelemetry tracing layer that exports spans via OTLP/gRPC.
///
/// This produces a layer compatible with `tracing_subscriber::registry()` that
/// sends traces to any OTLP-compatible collector (SigNoz, Grafana Tempo, etc).
///
/// Returns both the tracing layer and the [`SdkTracerProvider`] so the caller
/// can invoke [`SdkTracerProvider::shutdown`] for graceful flush on exit.
///
/// # Arguments
/// * `service_name` - The service name reported to the collector (e.g. "nym-node")
/// * `endpoint` - The OTLP/gRPC collector endpoint (e.g. "http://localhost:4317"
/// or "https://ingest.eu.signoz.cloud:443" for SigNoz Cloud)
/// * `ingestion_key` - Optional SigNoz Cloud ingestion key. When provided, it is
/// sent as the `signoz-ingestion-key` gRPC metadata header on every export.
/// * `environment` - Deployment environment label (e.g. "sandbox", "mainnet", "canary").
/// Attached as the `deployment.environment` OTel resource attribute.
/// * `sample_ratio` - Trace sampling ratio in 0.0..=1.0 (e.g. 0.1 = 10% of traces).
/// Used to limit cost when exporting from many nodes; clamped to [0.0, 1.0].
/// * `export_timeout_secs` - Timeout in seconds for each OTLP export batch. Prevents
/// unbounded blocking if the collector is slow or unreachable.
#[cfg(feature = "otel-otlp")]
pub fn init_otel_layer<S>(
service_name: &str,
endpoint: &str,
ingestion_key: Option<&str>,
environment: &str,
sample_ratio: f64,
export_timeout_secs: u64,
) -> Result<
(
tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>,
opentelemetry_sdk::trace::SdkTracerProvider,
),
Box<dyn std::error::Error + Send + Sync>,
>
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_otlp::WithTonicConfig;
use opentelemetry_sdk::trace::Sampler;
use std::time::Duration;
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason
#[cfg(feature = "tracing")]
#[macro_export]
macro_rules! setup_tracing {
($service_name: expr) => {
use nym_bin_common::logging::tracing_subscriber::layer::SubscriberExt;
use nym_bin_common::logging::tracing_subscriber::util::SubscriberInitExt;
// Validate endpoint URI early to fail with a clear message
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
return Err(format!(
"invalid OTLP endpoint URI: {endpoint} (must start with http:// or https://)"
)
.into());
}
let registry = nym_bin_common::logging::tracing_subscriber::Registry::default()
.with(nym_bin_common::logging::tracing_subscriber::EnvFilter::from_default_env())
.with(
nym_bin_common::logging::tracing_tree::HierarchicalLayer::new(4)
.with_targets(true)
.with_bracketed_fields(true),
);
let sample_ratio_clamped = sample_ratio.clamp(0.0, 1.0);
let tracer = nym_bin_common::logging::opentelemetry_jaeger::new_collector_pipeline()
.with_endpoint("http://44.199.230.10:14268/api/traces")
.with_service_name($service_name)
.with_isahc()
.with_trace_config(
nym_bin_common::logging::opentelemetry::sdk::trace::config().with_sampler(
nym_bin_common::logging::opentelemetry::sdk::trace::Sampler::TraceIdRatioBased(
0.1,
),
),
)
.install_batch(nym_bin_common::logging::opentelemetry::runtime::Tokio)
.expect("Could not init tracer");
let mut builder = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.with_timeout(Duration::from_secs(export_timeout_secs));
let telemetry = nym_bin_common::logging::tracing_opentelemetry::layer().with_tracer(tracer);
// Explicitly configure TLS when the endpoint uses HTTPS
if endpoint.starts_with("https://") {
builder =
builder.with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots());
}
if let Some(key) = ingestion_key {
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
"signoz-ingestion-key",
key.parse()
.map_err(|_| "invalid ingestion key format (value redacted)")?,
);
builder = builder.with_metadata(metadata);
}
let exporter = builder
.build()
.map_err(|e| format!("failed to build OTLP exporter for endpoint {endpoint}: {e}"))?;
let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_sampler(Sampler::TraceIdRatioBased(sample_ratio_clamped))
.with_batch_exporter(exporter)
.with_resource(
opentelemetry_sdk::Resource::builder()
.with_service_name(service_name.to_owned())
.with_attribute(opentelemetry::KeyValue::new(
"deployment.environment",
environment.to_owned(),
))
.build(),
)
.build();
opentelemetry::global::set_tracer_provider(tracer_provider.clone());
let tracer = tracer_provider.tracer(service_name.to_owned());
Ok((
tracing_opentelemetry::layer().with_tracer(tracer),
tracer_provider,
))
registry.with(telemetry).init();
};
}
pub fn banner(crate_name: &str, crate_version: &str) -> String {
@@ -26,7 +26,7 @@ use crate::{
error::ClientCoreError,
};
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))]
pub use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage;
pub use nym_client_core_gateways_storage as gateways_storage;
pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails};
+26 -80
View File
@@ -128,95 +128,54 @@ impl ManagedConnection {
async fn run(self) {
let address = self.address;
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
let connect_start = tokio::time::Instant::now();
let connection_fut = TcpStream::connect(address);
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
Ok(stream_res) => match stream_res {
Ok(stream) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
debug!("Managed to establish connection to {}", self.address);
let noise_start = tokio::time::Instant::now();
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
error!("Failed to perform Noise handshake with {address} - {err}");
// we failed to finish the noise handshake - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
debug!("Noise initiator handshake completed for {:?}", address);
Framed::new(noise_stream, NymCodec)
}
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
debug!("failed to establish connection to {address} (err: {err})",);
return;
}
},
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
connect_ms,
reconnection_attempt,
exit_reason = "timeout",
debug!(
"failed to connect to {address} within {:?}",
self.connection_timeout
);
// we failed to connect - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
// Take whatever the receiver channel produces and put it on the connection.
// We could have as well used conn.send_all(receiver.map(Ok)), but considering we don't care
// about neither receiver nor the connection, it doesn't matter which one gets consumed
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
warn!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packets to {address}: {err}"
);
warn!("Failed to forward packets to {address}: {err}");
}
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
"connection manager to {address} is finished. Either the connection failed or mixnet client got dropped",
);
}
}
@@ -313,18 +272,16 @@ impl SendWithoutResponse for Client {
trace!("Sending packet to {address}");
// TODO: optimisation for the future: rather than constantly using legacy encoding,
// use the mix packet type / flags to pick encoding per packet
// once we're addressing by node_id (and thus have full node info here),
// we could simply infer supported encoding based on their version
let framed_packet =
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
debug!(
event = "mixclient.try_send",
peer = %address,
result = "not_connected",
"establishing initial connection to {address}"
);
debug!("establishing initial connection to {address}");
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
// for sending for as soon as the connection is created
self.make_connection(address, framed_packet);
return Err(io::Error::new(
io::ErrorKind::NotConnected,
@@ -332,24 +289,15 @@ impl SendWithoutResponse for Client {
));
};
let channel_capacity = sender.channel.max_capacity();
let channel_available = sender.channel.capacity();
let channel_used = channel_capacity - channel_available;
let sending_res = sender.channel.try_send(framed_packet);
drop(sender);
sending_res.map_err(|err| {
match err {
TrySendError::Full(_) => {
warn!(
event = "mixclient.try_send",
peer = %address,
result = "full_dropped",
channel_capacity,
channel_used,
"dropping packet: connection buffer to {address} is full ({channel_used}/{channel_capacity})"
);
debug!("Connection to {address} seems to not be able to handle all the traffic - dropping the current packet");
// it's not a 'big' error, but we did not manage to send the packet
// if the queue is full, we can't really do anything but to drop the packet
io::Error::new(
io::ErrorKind::WouldBlock,
"connection queue is full",
@@ -357,13 +305,11 @@ impl SendWithoutResponse for Client {
}
TrySendError::Closed(dropped) => {
debug!(
event = "mixclient.try_send",
peer = %address,
result = "closed_reconnecting",
channel_capacity,
channel_used,
"connection to {address} dead, attempting re-establishment"
"Connection to {address} seems to be dead. attempting to re-establish it...",
);
// it's not a 'big' error, but we did not manage to send the packet, but queue
// it up to send it as soon as the connection is re-established
self.make_connection(address, dropped);
io::Error::new(
io::ErrorKind::ConnectionAborted,
@@ -76,7 +76,7 @@ features = ["json"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.reqwest]
workspace = true
features = ["json", "rustls"]
features = ["json", "rustls-tls"]
[dev-dependencies]
anyhow = { workspace = true }
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
};
use nym_api_requests::models::{
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
@@ -273,18 +273,18 @@ impl<C, S> Client<C, S> {
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(
&self,
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
Ok(self.nym_api.get_all_described_nodes().await?)
}
// pub async fn get_all_cached_described_nodes_v2(
// &self,
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// Ok(self.nym_api.get_all_described_nodes_v2().await?)
// }
pub async fn get_all_cached_described_nodes_v2(
&self,
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
Ok(self.nym_api.get_all_described_nodes_v2().await?)
}
pub async fn get_all_cached_bonded_nym_nodes(
&self,
@@ -473,7 +473,7 @@ impl NymApiClient {
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(
&self,
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
@@ -495,29 +495,29 @@ impl NymApiClient {
Ok(descriptions)
}
// pub async fn get_all_described_nodes_v2(
// &self,
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// // 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 descriptions = Vec::new();
//
// loop {
// let mut res = self
// .nym_api
// .get_nodes_described_v2(Some(page), None)
// .await?;
//
// descriptions.append(&mut res.data);
// if descriptions.len() < res.pagination.total {
// page += 1
// } else {
// break;
// }
// }
//
// Ok(descriptions)
// }
pub async fn get_all_described_nodes_v2(
&self,
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// 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 descriptions = Vec::new();
loop {
let mut res = self
.nym_api
.get_nodes_described_v2(Some(page), None)
.await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
@@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::nym_nodes::{
@@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient {
}
#[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(
&self,
page: Option<u32>,
@@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient {
.await
}
// #[tracing::instrument(level = "debug", skip_all)]
// async fn get_nodes_described_v2(
// &self,
// page: Option<u32>,
// per_page: Option<u32>,
// ) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
// let mut params = Vec::new();
//
// if let Some(page) = page {
// params.push(("page", page.to_string()))
// }
//
// if let Some(per_page) = per_page {
// params.push(("per_page", per_page.to_string()))
// }
//
// self.get_json(
// &[
// routes::V2_API_VERSION,
// routes::NYM_NODES_ROUTES,
// routes::NYM_NODES_DESCRIBED,
// ],
// &params,
// )
// .await
// }
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nodes_described_v2(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[
routes::V2_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_DESCRIBED,
],
&params,
)
.await
}
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
self.get_rewarded_set().await
@@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient {
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
// #[allow(deprecated)]
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
#[allow(deprecated)]
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
let mut page = 0;
@@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient {
Ok(descriptions)
}
// 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
// let mut page = 0;
// let mut descriptions = Vec::new();
//
// loop {
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
//
// descriptions.append(&mut res.data);
// if descriptions.len() < res.pagination.total {
// page += 1
// } else {
// break;
// }
// }
//
// Ok(descriptions)
// }
async fn get_all_described_nodes_v2(&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
// 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 descriptions = Vec::new();
loop {
let mut res = self.get_nodes_described_v2(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nym_nodes(
@@ -14,7 +14,7 @@ pub struct Args {
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.get_all_cached_described_nodes().await {
match client.get_all_cached_described_nodes_v2().await {
Ok(res) => match args.identity_key {
Some(identity_key) => {
let node = res.iter().find(|node| {
@@ -14,7 +14,7 @@ pub struct Args {
}
pub async fn query(args: Args, client: &QueryClientWithNyxd) {
match client.get_all_cached_described_nodes().await {
match client.get_all_cached_described_nodes_v2().await {
Ok(res) => match args.identity_key {
Some(identity_key) => {
let node = res.iter().find(|node| {
+1 -1
View File
@@ -19,7 +19,7 @@ bs58 = { workspace = true }
futures = { workspace = true }
humantime = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["rustls"] }
reqwest = { workspace = true, features = ["rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true, features = ["derive"] }
@@ -22,8 +22,6 @@ pub mod ecash;
pub mod error;
pub mod upgrade_mode;
const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024;
// Histogram buckets for ecash verification duration (in seconds)
const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0];
@@ -113,13 +111,6 @@ impl CredentialVerifier {
}
pub async fn verify(&mut self) -> Result<i64> {
if self.ecash_verifier.is_mock() {
// if we're in the mock mode (local testing), skip cryptographic verification
// and just return a dummy bandwidth value since we don't have blockchain access
// Return a reasonable test bandwidth value (e.g., 1GB in bytes)
return Ok(MOCK_BANDWIDTH);
}
let start = Instant::now();
nym_metrics::inc!("ecash_verification_attempts");
@@ -291,40 +291,3 @@ struct UpgradeModeStateInner {
// (and dealing with the async consequences of that)
status: UpgradeModeStatus,
}
pub mod testing {
use crate::UpgradeModeState;
use crate::upgrade_mode::{
CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestSender, UpgradeModeDetails,
};
use futures::channel::mpsc::UnboundedReceiver;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
pub fn mock_dummy_upgrade_mode_details() -> (UpgradeModeDetails, UnboundedReceiver<CheckRequest>)
{
let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded();
const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
];
pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey {
let private_key =
ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap();
private_key.public_key()
}
let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key());
let upgrade_mode_details = UpgradeModeDetails::new(
UpgradeModeCheckConfig {
// essentially we never want to trigger this in our tests
min_staleness_recheck: Duration::from_nanos(1),
},
UpgradeModeCheckRequestSender::new(um_recheck_tx),
upgrade_mode_state.clone(),
);
(upgrade_mode_details, um_recheck_rx)
}
}
+2 -2
View File
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
# internal
nym-sphinx-types = { workspace = true, optional = true }
nym-sphinx-types = { workspace = true }
nym-pemstore = { workspace = true }
[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"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
sphinx = ["nym-sphinx-types/sphinx"]
[lints]
workspace = true
-1
View File
@@ -15,7 +15,6 @@ description = "Functions to interact with zknym signers, checking their status a
futures = { workspace = true }
thiserror = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
url = { workspace = true }
+6 -8
View File
@@ -3,9 +3,14 @@
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_network_defaults::NymNetworkDetails;
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::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
@@ -13,12 +18,6 @@ use nym_validator_client::models::{
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
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;
pub mod error;
@@ -32,7 +31,6 @@ pub type TypedSignerResult = SignerResult<
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
#[derive(Serialize, Deserialize)]
pub struct SignersTestResult {
pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>,
+1 -1
View File
@@ -21,7 +21,7 @@ debug-inventory = ["nym-http-api-client-macro/debug-inventory"]
async-trait = { workspace = true }
bincode = { workspace = true }
cfg-if = { workspace = true}
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls"] }
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
http.workspace = true
url = { workspace = true }
once_cell = { workspace = true }
+83 -215
View File
@@ -46,10 +46,7 @@ use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
str::FromStr,
sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering::Relaxed},
},
sync::{Arc, LazyLock},
time::Duration,
};
@@ -73,23 +70,14 @@ pub(crate) const DEFAULT_QUERY_TIMEOUT: Duration = Duration::from_secs(5);
impl ClientBuilder {
/// 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 {
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.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver);
self.use_secure_dns = false;
self
}
/// 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`]).
/// Override the DNS resolver implementation used by the underlying http client.
pub fn no_hickory_dns(mut self) -> Self {
self = self.non_shared();
self.use_secure_dns = false;
self
}
@@ -141,8 +129,7 @@ pub struct HickoryDnsResolver {
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
use_system: Arc<AtomicBool>,
system_resolver: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
static_base: Option<Arc<OnceCell<StaticResolver>>>,
use_shared: bool,
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
@@ -154,8 +141,7 @@ impl Default for HickoryDnsResolver {
fn default() -> Self {
Self {
state: Default::default(),
use_system: Arc::new(AtomicBool::new(false)),
system_resolver: Default::default(),
fallback: Default::default(),
static_base: Some(Default::default()),
use_shared: true,
overall_dns_timeout: DEFAULT_OVERALL_LOOKUP_TIMEOUT,
@@ -165,28 +151,16 @@ impl Default for HickoryDnsResolver {
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
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 resolver = self.state.clone();
let maybe_fallback = self.fallback.clone();
let maybe_static = self.static_base.clone();
let use_shared = self.use_shared;
let overall_dns_timeout = self.overall_dns_timeout;
Box::pin(async move {
resolve(
name,
resolver,
maybe_fallback,
maybe_static,
use_shared,
overall_dns_timeout,
@@ -197,17 +171,16 @@ 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(
name: Name,
resolver: TokioResolver,
resolver: Arc<OnceCell<TokioResolver>>,
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
independent: bool,
overall_dns_timeout: Duration,
) -> 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
// looked up previously within the timeout to where we are not yet ready to try the
// default resolver yet again.
@@ -241,6 +214,22 @@ 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
// check the table for our entry
if let Some(ref static_resolver) = maybe_static {
@@ -269,11 +258,6 @@ impl Iterator for SocketAddrs {
}
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
pub async fn resolve_str(
&self,
@@ -281,20 +265,10 @@ impl HickoryDnsResolver {
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
let n =
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(
n,
resolver,
self.state.clone(),
self.fallback.clone(),
self.static_base.clone(),
self.use_shared,
self.overall_dns_timeout,
@@ -324,11 +298,13 @@ impl HickoryDnsResolver {
fn new_resolver_system(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// 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
if !use_shared {
if !use_shared || SHARED_RESOLVER.fallback.is_none() {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.system_resolver
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?
.clone())
}
@@ -344,80 +320,45 @@ impl HickoryDnsResolver {
}
}
/// Swap the primary internal resolver to the system resolver rather than the
/// configured custom resolver.
pub fn use_system_resolver(&self) {
self.use_system.store(true, Relaxed);
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?;
if self.use_shared {
SHARED_RESOLVER.use_system_resolver();
}
// 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 {
// SHARED_RESOLVER.enable_system_fallback()?;
// }
Ok(())
}
/// Swap the primary internal resolver to the configured custom resolver rather than the
/// system resolver.
pub fn use_configured_resolver(&self) {
self.use_system.store(false, Relaxed);
/// Disable fallback resolution. If the primary resolver fails the error is
/// returned immediately
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
if self.use_shared {
SHARED_RESOLVER.use_configured_resolver();
}
// // 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 {
// SHARED_RESOLVER.fallback = None;
// }
}
/// 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
/// Get the current map of hostname to address in use by the fallback static lookup if one
/// exists.
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
Some(self.static_base.as_ref()?.get()?.get_fallback_addrs())
Some(self.static_base.as_ref()?.get()?.get_addrs())
}
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
pub fn set_fallback_addrs(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
debug!("setting fallback 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_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);
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
let cell = OnceCell::new();
cell.set(StaticResolver::new(addrs))
.expect("infallible assign");
self.static_base = Some(Arc::new(cell));
}
/// Successfully resolved addresses are cached for a minimum of 30 minutes
@@ -554,7 +495,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
}
fn new_default_static_fallback() -> StaticResolver {
StaticResolver::new().with_fallback(constants::default_static_addrs())
StaticResolver::new(constants::default_static_addrs())
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -591,7 +532,10 @@ mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::Instant,
};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -608,7 +552,7 @@ mod test {
let var_name = HickoryDnsResolver::default();
let resolver = var_name;
let client = reqwest::ClientBuilder::new()
.dns_resolver(resolver)
.dns_resolver(resolver.into())
.build()
.unwrap();
@@ -653,7 +597,7 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
resolver.set_fallback_addrs(addr_map);
resolver.set_static_fallbacks(addr_map);
let mut addrs = resolver.resolve_str(example_domain).await?;
assert!(addrs.contains(&example_ip4));
@@ -794,19 +738,18 @@ mod test {
}
#[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.
//
// 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
#[ignore]
// 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> {
let resolver1 = HickoryDnsResolver::shared();
let time_start = Instant::now();
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
let time_start = std::time::Instant::now();
// create a new resolver that uses the shared resolver
let resolver = HickoryDnsResolver::shared();
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver::default();
// successful lookup using fallback to static resolver
let domain = "rpc.nymtech.net";
@@ -815,27 +758,9 @@ mod test {
.await
.expect("failed to resolve address in static lookup");
let lookup_dur = Instant::now() - time_start;
assert!(
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()
println!(
"{}ms resolved {domain}",
(Instant::now() - time_start).as_millis()
);
// unsuccessful lookup - primary times out, and not in static table
@@ -846,62 +771,5 @@ mod test {
// assert!(result.is_err_and(|e| matches!(e, ResolveError::ResolveError(e) if e.is_nx_domain())));
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,9 +51,6 @@ pub const VERCEL_COM_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
];
pub const NYM_API_CDN: &str = "cdn1.media-platform.net";
pub const NYM_API_CDN_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(172, 104, 178, 252))];
pub const NYM_COM_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
@@ -72,12 +69,6 @@ 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>> {
let mut m = HashMap::new();
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
@@ -97,7 +88,6 @@ pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
m.insert(NYM_API_CDN.to_string(), NYM_API_CDN_IPS.to_vec());
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
+52 -279
View File
@@ -14,78 +14,42 @@ const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CAC
#[derive(Debug, Default, Clone)]
pub struct StaticResolver {
fallback_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
preresolve_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
pre_resolve_timeout: Option<Duration>,
}
#[derive(Debug, Clone, Default)]
enum PreResolveStatus {
#[default]
Valid,
ValidUntil(Instant),
}
#[derive(Debug, Clone, Default)]
struct Entry {
status: PreResolveStatus,
valid_for_pre_resolve_until: Option<Instant>,
addrs: Vec<IpAddr>,
}
impl Entry {
fn new(addrs: Vec<IpAddr>) -> Self {
Self {
status: PreResolveStatus::Valid,
valid_for_pre_resolve_until: None,
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 {
pub fn new() -> StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
debug!("building static resolver");
let static_entries = static_entries
.into_iter()
.map(|(name, ips)| (name, Entry::new(ips)))
.collect();
Self {
fallback_addr_map: Arc::new(Mutex::new(HashMap::new())),
preresolve_addr_map: Arc::new(Mutex::new(HashMap::new())),
static_addr_map: Arc::new(Mutex::new(static_entries)),
pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT),
}
}
/// Initialize the contents of the pre-resolve table for this instance of the static resolver
#[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>> {
/// Return the full set of domain names and associated addresses stored in this static lookup table
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
let mut out = HashMap::new();
self.preresolve_addr_map
self.static_addr_map
.lock()
.unwrap()
.iter()
@@ -95,38 +59,6 @@ impl StaticResolver {
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
/// static lookup table.
#[allow(unused)]
@@ -139,58 +71,44 @@ impl StaticResolver {
/// recently (within the configured timeout) looked it up previously in this static table using
/// a regular resolve.
pub fn pre_resolve(&self, name: &str) -> Option<Vec<IpAddr>> {
self.preresolve_addr_map
debug!("found {name:?} in pre-resolve static table resolver");
self.pre_resolve_timeout?;
self.static_addr_map
.lock()
.unwrap()
.get(name)
.filter(|entry| entry.is_valid())
.map(|entry| {
debug!("pre-resolve lookup hit for \"{name:?}\" in static table resolver");
entry.addrs.clone()
.filter(|e| {
e.valid_for_pre_resolve_until
.is_some_and(|t| t > Instant::now())
})
.map(|e| e.addrs.clone())
}
#[allow(unused)]
pub fn resolve_str(&self, name: &str) -> Option<Vec<IpAddr>> {
Self::resolve_inner(
self.fallback_addr_map.lock().unwrap(),
self.preresolve_addr_map.lock().unwrap(),
self.static_addr_map.lock().unwrap(),
name,
self.pre_resolve_timeout,
)
.map(|e| e.addrs)
}
fn resolve_inner(
fallback_table: MutexGuard<'_, HashMap<String, Vec<IpAddr>>>,
mut preresolve_table: MutexGuard<'_, HashMap<String, Entry>>,
mut table: MutexGuard<'_, HashMap<String, Entry>>,
name: &str,
pre_resolve_cache_timeout: Option<Duration>,
) -> Option<Vec<IpAddr>> {
let resolved = fallback_table.get(name)?;
timeout: Option<Duration>,
) -> Option<Entry> {
let resolved = table.get_mut(name)?;
debug!("lookup hit for \"{name:?}\" in static table resolver");
debug!("found {name:?} in static table resolver");
// 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
// triggering.
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),
);
}
_ => {}
}
if let Some(pre_resolve_timeout) = timeout {
// 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.
resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout);
}
Some(resolved.clone())
}
@@ -199,23 +117,13 @@ impl StaticResolver {
impl Resolve for StaticResolver {
fn resolve(&self, name: Name) -> Resolving {
debug!("looking up {name:?} in static resolver");
// 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 addr_map = self.static_addr_map.clone();
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 {
let fallback_addr_map = fallback_addr_map.lock().unwrap();
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,
) {
let addr_map = addr_map.lock().unwrap();
let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) {
None => return Err(ResolveError::StaticLookupMiss.into()),
Some(addrs) => addrs,
Some(entry) => entry.addrs,
};
let addrs: Addrs = Box::new(
lookup
@@ -234,7 +142,6 @@ mod test {
use super::*;
use std::error::Error as StdError;
use std::net::Ipv4Addr;
use std::str::FromStr;
#[tokio::test]
@@ -242,7 +149,7 @@ mod test {
let example_domain = String::from("static.nymvpn.com");
// lookup for domain for which there is no entry
let resolver = StaticResolver::new();
let resolver = StaticResolver::new(HashMap::new());
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let result = resolver.resolve(url).await;
@@ -259,7 +166,7 @@ mod test {
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let resolver = StaticResolver::new().with_fallback(addr_map);
let resolver = StaticResolver::new(addr_map);
let mut addrs = resolver.resolve(url).await?;
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
@@ -268,7 +175,7 @@ mod test {
}
#[test]
fn elevate_fallback_to_pre_resolve() {
fn static_lookup_pre_resolve() {
let example_duration = Duration::from_secs(3);
let example_domain = String::from("static.nymvpn.com");
let mut addr_map = HashMap::new();
@@ -276,23 +183,24 @@ mod test {
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let resolver = StaticResolver::new()
.with_fallback(addr_map)
.with_pre_resolve_timeout(example_duration);
let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration);
// ensure that attempting to pre-resolve without first resolving returns none
let result = resolver.pre_resolve(&example_domain);
assert!(result.is_none());
// resolving should now update the pre-resolve validity timeout for the entry
let _addrs = resolver
.resolve_str(&example_domain)
.expect("entry should exist");
assert!(matches!(
resolver.preresolve_status(&example_domain),
Some(PreResolveStatus::ValidUntil(t))
if t < Instant::now() + example_duration
));
let entry = StaticResolver::resolve_inner(
resolver.static_addr_map.lock().unwrap(),
&example_domain,
Some(example_duration),
)
.expect("missing entry???!!!!");
assert!(
entry
.valid_for_pre_resolve_until
.is_some_and(|t| t < Instant::now() + example_duration)
);
// check that pre-resolve now returns the expected record
let addrs = resolver
@@ -306,139 +214,4 @@ mod test {
let result = resolver.pre_resolve(&example_domain);
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)
}
}
}
+16 -217
View File
@@ -3,21 +3,15 @@
//! Utilities for and implementation of request tunneling
use std::sync::{
Arc, LazyLock, RwLock,
atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::warn;
use crate::{Client, ClientBuilder};
static SHARED_FRONTING_POLICY: LazyLock<Arc<RwLock<FrontPolicy>>> =
LazyLock::new(|| Arc::new(RwLock::new(FrontPolicy::Off)));
use crate::ClientBuilder;
// #[cfg(feature = "tunneling")]
#[derive(Debug)]
pub(crate) struct Front {
pub(crate) policy: Arc<RwLock<FrontPolicy>>,
pub(crate) policy: FrontPolicy,
enabled: AtomicBool,
}
@@ -25,7 +19,7 @@ impl Clone for Front {
fn clone(&self) -> Self {
Self {
policy: self.policy.clone(),
enabled: AtomicBool::new(false),
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
}
}
}
@@ -33,30 +27,13 @@ impl Clone for Front {
impl Front {
pub(crate) fn new(policy: FrontPolicy) -> Self {
Self {
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),
enabled: AtomicBool::new(policy == FrontPolicy::Always),
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 {
match *self.policy.read().unwrap() {
match self.policy {
FrontPolicy::Off => false,
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
FrontPolicy::Always => true,
@@ -69,13 +46,14 @@ impl Front {
if self.is_enabled() {
return;
}
if matches!(*self.policy.read().unwrap(), FrontPolicy::OnRetry) {
if matches!(self.policy, FrontPolicy::OnRetry) {
self.enabled.store(true, Ordering::Relaxed);
}
}
}
#[derive(Debug, Default, PartialEq, Clone)]
#[cfg(feature = "tunneling")]
/// Policy for when to use domain fronting for HTTP requests.
pub enum FrontPolicy {
/// Always use domain fronting for all requests.
@@ -88,208 +66,29 @@ pub enum FrontPolicy {
}
impl ClientBuilder {
/// Enable and configure request tunneling for API requests. If no front policy is
/// provided the shared fronting policy will be used.
pub fn with_fronting(mut self, policy: Option<FrontPolicy>) -> Self {
let front = if let Some(p) = policy {
Front::new(p)
} else {
Front::shared()
};
/// Enable and configure request tunneling for API requests.
#[cfg(feature = "tunneling")]
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
let front = Front::new(policy);
// Check if any of the supplied urls even support fronting
if !self.urls.iter().any(|url| url.has_front()) {
warn!(
"fronting is enabled, but none of the supplied urls have configured fronting domains: {:?}",
self.urls
"fronting is enabled, but none of the supplied urls have configured fronting domains"
);
}
self.front = front;
self.front = Some(front);
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)]
mod tests {
use super::*;
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]
async fn nym_api_works() {
let url1 = Url::new(
@@ -305,7 +104,7 @@ mod tests {
let client = ClientBuilder::new(url1)
.expect("bad url")
.with_fronting(Some(FrontPolicy::Always))
.with_fronting(FrontPolicy::Always)
.build()
.expect("failed to build client");
@@ -341,7 +140,7 @@ mod tests {
let client = ClientBuilder::new_with_urls(vec![url1, url2])
.expect("bad url")
.with_fronting(Some(FrontPolicy::Always))
.with_fronting(FrontPolicy::Always)
.build()
.expect("failed to build client");
+102 -191
View File
@@ -136,7 +136,6 @@
//! ```
#![warn(missing_docs)]
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
@@ -148,7 +147,6 @@ pub mod registry;
use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use itertools::Itertools;
@@ -163,7 +161,9 @@ use std::time::Duration;
use thiserror::Error;
use tracing::{debug, instrument, warn};
use std::sync::{Arc, LazyLock};
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
use std::sync::Arc;
#[cfg(feature = "tunneling")]
mod fronted;
@@ -195,8 +195,6 @@ use nym_http_api_client_macro::client_defaults;
/// high and chatty protocols take a while to complete.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const NYM_OUTER_SNI_HEADER: &str = "NYM-ORIGINAL-OUTER-SNI";
#[cfg(not(target_arch = "wasm32"))]
client_defaults!(
priority = -100;
@@ -208,24 +206,6 @@ client_defaults!(
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
pub type PathSegments<'a> = &'a [&'a str];
/// Collection of HTTP Request Parameters
@@ -347,22 +327,16 @@ pub enum HttpClientError {
source: reqwest::Error,
},
#[error("failed to parse header value: {source}")]
InvalidHeaderValue {
#[source]
source: http::Error,
},
#[error("failed to send request for {url}: {source}")]
RequestSendFailure {
url: Box<reqwest::Url>,
url: reqwest::Url,
#[source]
source: ReqwestErrorWrapper,
},
#[error("failed to read response body from {url}: {source}")]
ResponseReadFailure {
url: Box<reqwest::Url>,
url: reqwest::Url,
headers: Box<HeaderMap>,
status: StatusCode,
#[source]
@@ -379,7 +353,7 @@ pub enum HttpClientError {
},
#[error("the requested resource could not be found at {url}")]
NotFound { url: Box<reqwest::Url> },
NotFound { url: reqwest::Url },
#[error("attempted to use domain fronting and clone a request containing stream data")]
AttemptedToCloneStreamRequest,
@@ -391,7 +365,7 @@ pub enum HttpClientError {
"the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}"
)]
RequestFailure {
url: Box<reqwest::Url>,
url: reqwest::Url,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -400,7 +374,7 @@ pub enum HttpClientError {
"the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}"
)]
EmptyResponse {
url: Box<reqwest::Url>,
url: reqwest::Url,
status: StatusCode,
headers: Box<HeaderMap>,
},
@@ -409,7 +383,7 @@ pub enum HttpClientError {
"failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}"
)]
EndpointFailure {
url: Box<reqwest::Url>,
url: reqwest::Url,
status: StatusCode,
headers: Box<HeaderMap>,
error: String,
@@ -479,7 +453,7 @@ impl HttpClientError {
pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self {
HttpClientError::RequestSendFailure {
url: Box::new(url),
url,
source: ReqwestErrorWrapper(source),
}
}
@@ -580,19 +554,6 @@ pub trait ApiClientCore {
let req = self.create_request(method, path, params, json_body)?;
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
@@ -601,18 +562,16 @@ pub struct ClientBuilder {
urls: Vec<Url>,
timeout: Option<Duration>,
custom_user_agent: Option<HeaderValue>,
reqwest_client_builder: Option<reqwest::ClientBuilder>,
custom_user_agent: bool,
reqwest_client_builder: reqwest::ClientBuilder,
#[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool,
#[cfg(feature = "tunneling")]
front: fronted::Front,
front: Option<fronted::Front>,
retry_limit: usize,
serialization: SerializationFormat,
error: Option<HttpClientError>,
}
impl ClientBuilder {
@@ -683,10 +642,10 @@ impl ClientBuilder {
let mut builder = Self::new_with_urls(urls)?;
// Enable domain fronting using the shared fronting policy
// Enable domain fronting by default (on retry)
#[cfg(feature = "tunneling")]
{
builder = builder.with_fronting(None);
builder = builder.with_fronting(FrontPolicy::OnRetry);
}
Ok(builder)
@@ -700,31 +659,26 @@ impl ClientBuilder {
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 {
urls,
timeout: None,
custom_user_agent: None,
reqwest_client_builder: None,
custom_user_agent: false,
reqwest_client_builder,
use_secure_dns: true,
#[cfg(feature = "tunneling")]
front: fronted::Front::off(),
front: None,
retry_limit: 0,
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`
pub fn add_url(mut self, url: Url) -> Self {
self.urls.push(url);
@@ -769,7 +723,7 @@ impl ClientBuilder {
/// Provide a pre-configured [`reqwest::ClientBuilder`]
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
self.reqwest_client_builder = Some(reqwest_builder);
self.reqwest_client_builder = reqwest_builder;
self
}
@@ -779,12 +733,18 @@ impl ClientBuilder {
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
match value.try_into() {
Ok(v) => self.custom_user_agent = Some(v),
Err(err) => {
self.error = Some(HttpClientError::InvalidHeaderValue { source: err.into() })
}
}
self.custom_user_agent = true;
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
self
}
/// 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
}
@@ -801,33 +761,30 @@ impl ClientBuilder {
/// Returns a Client that uses this ClientBuilder configuration.
pub fn build(self) -> Result<Client, HttpClientError> {
if let Some(err) = self.error {
return Err(err);
}
#[cfg(target_arch = "wasm32")]
let reqwest_client = Some(reqwest::ClientBuilder::new().build()?);
let reqwest_client = self.reqwest_client_builder.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"))]
let reqwest_client = self
.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()));
}
let reqwest_client = {
let mut builder = self.reqwest_client_builder;
builder
.build()
.map_err(HttpClientError::reqwest_client_build_error)
})
.transpose()?;
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
builder
.build()
.map_err(HttpClientError::reqwest_client_build_error)?
};
let client = Client {
base_urls: self.urls,
current_idx: Arc::new(AtomicUsize::new(0)),
reqwest_client,
custom_user_agent: self.custom_user_agent,
using_secure_dns: self.use_secure_dns,
#[cfg(feature = "tunneling")]
front: self.front,
@@ -847,11 +804,11 @@ impl ClientBuilder {
pub struct Client {
base_urls: Vec<Url>,
current_idx: Arc<AtomicUsize>,
reqwest_client: Option<reqwest::Client>,
custom_user_agent: Option<HeaderValue>,
reqwest_client: reqwest::Client,
using_secure_dns: bool,
#[cfg(feature = "tunneling")]
front: fronted::Front,
front: Option<fronted::Front>,
#[cfg(target_arch = "wasm32")]
request_timeout: Duration,
@@ -905,8 +862,8 @@ impl Client {
Client {
base_urls: vec![new_url],
current_idx: Arc::new(Default::default()),
reqwest_client: None,
custom_user_agent: None,
reqwest_client: self.reqwest_client.clone(),
using_secure_dns: self.using_secure_dns,
#[cfg(feature = "tunneling")]
front: self.front.clone(),
@@ -940,7 +897,9 @@ impl Client {
#[cfg(feature = "tunneling")]
fn matches_current_host(&self, url: &Url) -> bool {
if self.front.is_enabled() {
if let Some(ref front) = self.front
&& front.is_enabled()
{
url.host_str() == self.current_url().front_str()
} else {
url.host_str() == self.current_url().host_str()
@@ -967,7 +926,9 @@ impl Client {
}
#[cfg(feature = "tunneling")]
if self.front.is_enabled() {
if let Some(ref front) = self.front
&& front.is_enabled()
{
// if we are using fronting, try updating to the next front
let url = self.current_url();
@@ -987,7 +948,9 @@ impl Client {
// if fronting is enabled we want to update to a host that has fronts configured
#[cfg(feature = "tunneling")]
if self.front.is_enabled() {
if let Some(ref front) = self.front
&& front.is_enabled()
{
while next != orig {
if self.base_urls[next].has_front() {
// we have a front for the next host, so we can use it
@@ -1018,12 +981,14 @@ impl Client {
/// 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
/// send.
pub(crate) fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) {
let url = self.current_url();
r.url_mut().set_host(url.host_str()).unwrap();
#[cfg(feature = "tunneling")]
if self.front.is_enabled() {
if let Some(ref front) = self.front
&& front.is_enabled()
{
if let Some(front_host) = url.front_str() {
if let Some(actual_host) = url.host_str() {
tracing::debug!(
@@ -1043,13 +1008,6 @@ impl Client {
.headers_mut()
.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());
} else {
tracing::debug!(
@@ -1090,21 +1048,12 @@ impl ApiClientCore for Client {
self.apply_hosts_to_req(&mut req);
let client = if let Some(client) = &self.reqwest_client {
client.clone()
} else {
SHARED_CLIENT.clone()
};
let mut rb = RequestBuilder::from_parts(client, req);
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
rb = rb
.header(ACCEPT, 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 {
match self.serialization {
SerializationFormat::Json => {
@@ -1147,19 +1096,16 @@ impl ApiClientCore for Client {
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
Ok(
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
Ok(wasmtimer::tokio::timeout(
self.request_timeout,
self.reqwest_client.execute(req),
)
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
};
#[cfg(not(target_arch = "wasm32"))]
let response = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
client.execute(req).await
};
let response = self.reqwest_client.execute(req).await;
match response {
Ok(resp) => return Ok(resp),
@@ -1175,10 +1121,20 @@ impl ApiClientCore for Client {
if is_network_err {
// if we have multiple urls, update to the next
self.maybe_rotate_hosts(Some(url.clone()));
self.update_host(Some(url.clone()));
#[cfg(feature = "tunneling")]
self.maybe_enable_fronting(("network", url.as_str(), &err));
if let Some(ref front) = self.front {
// 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 {
@@ -1202,21 +1158,6 @@ 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.
@@ -1369,35 +1310,6 @@ pub trait ApiClient: ApiClientCore {
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
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T` based on the content type header
@@ -1415,8 +1327,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::GET, path, params, None::<&()>)
.await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
@@ -1438,7 +1349,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::POST, path, params, Some(json_body))
.await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
@@ -1458,7 +1369,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::DELETE, path, params, None::<&()>)
.await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
@@ -1480,7 +1391,7 @@ pub trait ApiClient: ApiClientCore {
let res = self
.send_request(reqwest::Method::PATCH, path, params, Some(json_body))
.await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1492,7 +1403,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?;
let res = self.send(req).await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1509,7 +1420,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?;
let res = self.send(req).await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// `delete` json data from the provided absolute endpoint, e.g.
@@ -1521,7 +1432,7 @@ pub trait ApiClient: ApiClientCore {
{
let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?;
let res = self.send(req).await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
/// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`.
@@ -1539,7 +1450,7 @@ pub trait ApiClient: ApiClientCore {
let req =
self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?;
let res = self.send(req).await?;
self.parse_response(res, false).await
parse_response(res, false).await
}
}
@@ -1606,7 +1517,7 @@ where
if !allow_empty && let Some(0) = res.content_length() {
return Err(HttpClientError::EmptyResponse {
url: Box::new(url),
url,
status,
headers: Box::new(headers),
});
@@ -1619,25 +1530,25 @@ where
.bytes()
.await
.map_err(|source| HttpClientError::ResponseReadFailure {
url: Box::new(url),
url,
headers: Box::new(headers.clone()),
status,
source: ReqwestErrorWrapper(source),
})?;
decode_raw_response(&headers, full)
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound { url: Box::new(url) })
Err(HttpClientError::NotFound { url })
} else {
let Ok(plaintext) = res.text().await else {
return Err(HttpClientError::RequestFailure {
url: Box::new(url),
url,
status,
headers: Box::new(headers),
});
};
Err(HttpClientError::EndpointFailure {
url: Box::new(url),
url,
status,
headers: Box::new(headers),
error: plaintext,
+2 -3
View File
@@ -89,8 +89,7 @@ fn sanitizing_urls() {
// - on error without retries is where we have multiple urls, is the url updated?
#[tokio::test]
#[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.
#[ignore] // test relies on external services being available and behaving in a specific way.
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.test".parse()?, // This should fail because of DNS NXDomain (rotate)
@@ -200,7 +199,7 @@ fn fronted_host_updating() {
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let mut client = ClientBuilder::new(url)
.unwrap()
.with_fronting(Some(crate::fronted::FrontPolicy::Always))
.with_fronting(crate::fronted::FrontPolicy::Always)
.build()
.unwrap();
-10
View File
@@ -123,16 +123,6 @@ 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 {
fn as_ref(&self) -> &url::Url {
&self.url
-3
View File
@@ -6,9 +6,6 @@ edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
# Exclude build.rs from published crate - it's only used for dev-time sync
# of env files and requires workspace context
exclude = ["build.rs"]
[dependencies]
dotenvy = { workspace = true, optional = true }
-4
View File
@@ -51,10 +51,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
ApiUrlConst {
url: "https://cdn1.media-platform.net/api/",
front_hosts: None,
},
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
-1
View File
@@ -6,7 +6,6 @@ edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
publish = false
[lib]
name = "nym_kcp"
+1 -2
View File
@@ -21,8 +21,7 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
rand09 = { workspace = true }
rand = "0.9.2"
zeroize = { workspace = true, features = ["zeroize_derive"] }
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
+22 -22
View File
@@ -18,13 +18,13 @@ use nym_kkt::{
responder_ingest_message, responder_process,
},
};
use rand09::prelude::*;
use rand::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand09::rng().fill_bytes(&mut s);
rand::rng().fill_bytes(&mut s);
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) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
let mut rng = rand09::rng();
let mut rng = rand::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (i_context, i_frame) =
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
c.bench_function(
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (r_context, _) =
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
c.bench_function(
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
}
// Initiator, OneWay
{
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::OneWay,
ciphersuite,
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let i_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
KKTMode::Mutual,
ciphersuite,
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
},
);
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|b| {
b.iter(|| {
initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
);
let obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&r_frame,
&r_frame.context().unwrap(),
responder_ed25519_keypair.public_key(),
+4 -3
View File
@@ -5,7 +5,7 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
use blake3::Hasher;
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
use nym_crypto::asymmetric::x25519;
use rand09::{CryptoRng, RngCore};
use rand::{CryptoRng, RngCore};
use zeroize::Zeroize;
#[derive(Clone, Copy, Zeroize)]
@@ -182,7 +182,8 @@ mod test {
encryption::{KKTSessionSecret, decrypt, encrypt},
key_utils::generate_keypair_x25519,
};
use rand09::{RngCore, SeedableRng, rng};
use rand::{RngCore, SeedableRng, rng};
use rand_chacha::ChaCha20Rng;
#[test]
fn test_keygen() {
@@ -226,7 +227,7 @@ mod test {
#[test]
fn kkt_frame_encryption() -> anyhow::Result<()> {
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
let mut rng = ChaCha20Rng::seed_from_u64(42);
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
let aad = b"my-amazing-aad";
+2 -1
View File
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use classic_mceliece_rust::keypair_boxed;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
use rand09::{CryptoRng, RngCore};
use rand::{CryptoRng, RngCore};
pub fn generate_keypair_ed25519<R>(
rng: &mut R,
@@ -61,6 +61,7 @@ pub fn generate_keypair_mceliece<'a, R>(
classic_mceliece_rust::PublicKey<'a>,
)
where
// this is annoying because mceliece lib uses rand 0.8.5...
R: RngCore + CryptoRng,
{
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
+15 -14
View File
@@ -9,7 +9,7 @@
//! The underlying KKT protocol is implemented in the `session` module.
use nym_crypto::asymmetric::{ed25519, x25519};
use rand09::{CryptoRng, RngCore};
use rand::{CryptoRng, RngCore};
use crate::{
ciphersuite::{Ciphersuite, EncapsulationKey},
@@ -33,7 +33,7 @@ use crate::frame::KKTFrame;
/// The request will be signed with the provided signing key.
///
/// # Arguments
/// * `rng` - random number generator
/// * `rng` - Random number generator
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `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
/// ```ignore
/// let gateway_kem_key = validate_kem_response(
/// &context,
/// &mut context,
/// &session_secret,
/// &gateway_verification_key,
/// &expected_hash_from_directory,
@@ -199,14 +199,14 @@ mod tests {
fn random_x25519_key() -> x25519::PrivateKey {
let mut bytes = [0u8; 32];
let mut rng = rand09::rng();
let mut rng = rand::rng();
rng.fill_bytes(&mut bytes);
x25519::PrivateKey::from_secret(bytes)
}
#[test]
fn test_kkt_wrappers_oneway_authenticated() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
// Generate Ed25519 keypairs for both parties
let mut initiator_secret = [0u8; 32];
@@ -241,7 +241,7 @@ mod tests {
);
// Client: Request KEM key
let (session_key, context, request_frame_ciphertext) = request_kem_key(
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
&mut rng,
ciphersuite,
ed25519_init.private_key(),
@@ -262,7 +262,7 @@ mod tests {
// Client: Validate response
let obtained_key = validate_kem_response(
&context,
&mut context,
&session_key,
ed25519_resp.public_key(),
&key_hash,
@@ -276,7 +276,7 @@ mod tests {
#[test]
fn test_kkt_wrappers_anonymous() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
// Only responder has keys
let mut responder_secret = [0u8; 32];
@@ -304,7 +304,8 @@ mod tests {
);
// Anonymous initiator
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let (mut context, request_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// Generate the session's shared secret and encrypt the Initiator's request
let (session_secret, encrypted_request_bytes) =
@@ -323,7 +324,7 @@ mod tests {
// Initiator: Validate response
let obtained_key = validate_kem_response(
&context,
&mut context,
&session_secret,
responder_keypair.public_key(),
&key_hash,
@@ -336,7 +337,7 @@ mod tests {
#[test]
fn test_invalid_signature_rejected() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
@@ -389,7 +390,7 @@ mod tests {
#[test]
fn test_hash_mismatch_rejected() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
let mut initiator_secret = [0u8; 32];
rng.fill_bytes(&mut initiator_secret);
@@ -416,7 +417,7 @@ mod tests {
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (session_key, context, request_frame) = request_kem_key(
let (session_key, mut context, request_frame) = request_kem_key(
&mut rng,
ciphersuite,
initiator_keypair.private_key(),
@@ -436,7 +437,7 @@ mod tests {
// Client validates with WRONG hash
let result = validate_kem_response(
&context,
&mut context,
&session_key,
responder_keypair.public_key(),
&wrong_hash, // Wrong!
+26 -26
View File
@@ -38,7 +38,7 @@ mod test {
#[test]
fn test_kkt_psq_e2e_clear() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
@@ -106,18 +106,18 @@ mod test {
// Anonymous Initiator, OneWay
{
let (i_context, i_frame) =
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
let i_frame_bytes = i_frame.to_bytes();
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (r_context, _) =
let (mut r_context, _) =
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_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_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -141,7 +141,7 @@ mod test {
}
// Initiator, OneWay
{
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
@@ -154,7 +154,7 @@ mod test {
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -165,7 +165,7 @@ mod test {
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_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_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -190,7 +190,7 @@ mod test {
// Initiator, Mutual
{
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
@@ -203,7 +203,7 @@ mod test {
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -214,7 +214,7 @@ mod test {
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_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_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -241,7 +241,7 @@ mod test {
}
#[test]
fn test_kkt_psq_e2e_encrypted() {
let mut rng = rand09::rng();
let mut rng = rand::rng();
// generate ed25519 keys
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
@@ -312,7 +312,7 @@ mod test {
// Anonymous Initiator, OneWay
{
let (i_context, i_frame) =
let (mut i_context, i_frame) =
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// encryption - initiator frame
@@ -330,11 +330,11 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (r_context, _) =
let (mut r_context, _) =
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -352,7 +352,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -364,7 +364,7 @@ mod test {
}
// Initiator, OneWay
{
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::OneWay,
ciphersuite,
@@ -388,7 +388,7 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&r_context,
Some(initiator_ed25519_keypair.public_key()),
None,
@@ -399,7 +399,7 @@ mod test {
assert!(r_obtained_key.is_none());
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -417,7 +417,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
@@ -430,7 +430,7 @@ mod test {
// Initiator, Mutual
{
let (i_context, i_frame) = initiator_process(
let (mut i_context, i_frame) = initiator_process(
&mut rng,
crate::context::KKTMode::Mutual,
ciphersuite,
@@ -454,7 +454,7 @@ mod test {
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
.unwrap();
let (r_context, r_obtained_key) = responder_ingest_message(
let (mut r_context, r_obtained_key) = responder_ingest_message(
&i_context_r,
Some(initiator_ed25519_keypair.public_key()),
Some(&i_dir_hash),
@@ -465,7 +465,7 @@ mod test {
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
let r_frame = responder_process(
&r_context,
&mut r_context,
i_frame_r.session_id(),
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
@@ -483,7 +483,7 @@ mod test {
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
let i_obtained_key = initiator_ingest_response(
&i_context,
&mut i_context,
&i_frame_r,
&i_context_r,
responder_ed25519_keypair.public_key(),
+3 -3
View File
@@ -1,5 +1,5 @@
use nym_crypto::asymmetric::ed25519::{self, Signature};
use rand09::{CryptoRng, RngCore};
use rand::{CryptoRng, RngCore};
use crate::frame::KKTSessionId;
use crate::{
@@ -73,7 +73,7 @@ where
}
pub fn initiator_ingest_response<'a>(
own_context: &KKTContext,
own_context: &mut KKTContext,
remote_frame: &KKTFrame,
remote_context: &KKTContext,
remote_verification_key: &ed25519::PublicKey,
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
}
pub fn responder_process<'a>(
own_context: &KKTContext,
own_context: &mut KKTContext,
session_id: KKTSessionId,
signing_key: &ed25519::PrivateKey,
encapsulation_key: &EncapsulationKey<'a>,
+1 -2
View File
@@ -12,9 +12,8 @@ readme.workspace = true
publish = false
[dependencies]
tokio = { workspace = true, features = ["net", "io-util"] }
tokio = { workspace = true, features = ["net"] }
nym-test-utils = { path = "../test-utils", optional = true }
tracing = { workspace = true }
[features]
io-mocks = ["nym-test-utils"]
+2 -103
View File
@@ -4,100 +4,15 @@
#[cfg(feature = "io-mocks")]
use nym_test_utils::mocks::async_read_write::MockIOStream;
use std::net::SocketAddr;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tracing::debug;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransport: Sized {
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
/// Sends a serialised (and optionally encrypted) LP packet over the data stream with length-prefixed framing.
///
/// Format: 4-byte big-endian u32 length + packet bytes
///
/// # Arguments
/// * `packet_data` - The serialised LP packet to send
///
/// # Errors
/// Returns an error on network transmission fails.
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
/// 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 {
@@ -109,14 +24,6 @@ impl LpTransport for TcpStream {
// Set TCP_NODELAY for low latency
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")]
@@ -128,12 +35,4 @@ impl LpTransport for MockIOStream {
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
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
}
}
+2 -12
View File
@@ -17,12 +17,11 @@ sha2 = { workspace = true }
tracing = { workspace = true }
rand = { workspace = true }
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
rand09 = { workspace = true }
rand09 = { package = "rand", version = "0.9.2" }
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
nym-kkt = { path = "../nym-kkt" }
nym-lp-common = { path = "../nym-lp-common" }
nym-lp-transport = { path = "../nym-lp-transport" }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
@@ -35,21 +34,12 @@ num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
# needed for the 'mock 'feature
nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
#rand_chacha = "0.3"
mock_instant = { workspace = true }
rand_chacha = "0.3"
nym-crypto = { path = "../crypto", features = ["rand"] }
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]]
name = "replay_protection"
+3 -3
View File
@@ -1,8 +1,8 @@
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
use nym_lp::replay::ReceivingKeyCounterValidator;
use nym_test_utils::helpers::u64_seeded_rng;
use parking_lot::Mutex;
use rand::Rng;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use std::sync::Arc;
fn bench_sequential_counters(c: &mut Criterion) {
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
let validator = ReceivingKeyCounterValidator::default();
// Create random counters within a valid window
let mut rng = u64_seeded_rng(42);
let mut rng = ChaCha8Rng::seed_from_u64(42);
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
b.iter(|| {
+7 -83
View File
@@ -555,7 +555,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
// Need payload and trailer to meet min_size requirement
let payload_size = 10; // Arbitrary
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
@@ -565,7 +565,7 @@ mod tests {
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::InvalidMessageType(231)) => {} // Expected error
Err(LpError::InvalidMessageType(255)) => {} // Expected error
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
Ok(_) => panic!("Expected error, but got Ok"),
}
@@ -628,7 +628,7 @@ mod tests {
receiver_idx: 42,
counter: 123,
},
message: LpMessage::ClientHello(hello_data),
message: LpMessage::ClientHello(hello_data.clone()),
trailer: [0; TRAILER_LEN],
};
@@ -681,7 +681,7 @@ mod tests {
receiver_idx: 100,
counter: 200,
},
message: LpMessage::ClientHello(hello_data),
message: LpMessage::ClientHello(hello_data.clone()),
trailer: [55; TRAILER_LEN],
};
@@ -757,12 +757,12 @@ mod tests {
}
#[test]
fn test_forward_packet_encode_decode_roundtrip_v4() {
fn test_forward_packet_encode_decode_roundtrip() {
let mut dst = BytesMut::new();
let forward_data = crate::message::ForwardPacketData {
target_gateway_identity: [77u8; 32],
target_lp_address: "1.2.3.4:41264".parse().unwrap(),
target_lp_address: "1.2.3.4:41264".to_string(),
inner_packet_bytes: vec![0xa, 0xb, 0xc, 0xd],
};
@@ -789,50 +789,7 @@ mod tests {
if let LpMessage::ForwardPacket(data) = decoded.message {
assert_eq!(data.target_gateway_identity, [77u8; 32]);
assert_eq!(data.target_lp_address, "1.2.3.4:41264".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.target_lp_address, "1.2.3.4:41264");
assert_eq!(data.inner_packet_bytes, vec![0xa, 0xb, 0xc, 0xd]);
} else {
panic!("Expected ForwardPacket message");
@@ -1289,37 +1246,4 @@ mod tests {
_ => 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"),
}
}
}
-23
View File
@@ -1,11 +1,9 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::message::MessageType;
use crate::{noise_protocol::NoiseError, replay::ReplayError};
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_kkt::ciphersuite::{HashFunction, KEM};
use nym_kkt::error::KKTError;
use thiserror::Error;
#[derive(Error, Debug)]
@@ -104,25 +102,4 @@ pub enum LpError {
kem: KEM,
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:?}"
))
}
}
+39 -135
View File
@@ -10,7 +10,6 @@ pub mod noise_protocol;
pub mod packet;
pub mod peer;
pub mod psk;
mod psq;
pub mod replay;
pub mod session;
mod session_integration;
@@ -22,139 +21,31 @@ pub use error::LpError;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
pub use session::LpSession;
pub use session::{LpSession, generate_fresh_salt};
pub use session_manager::SessionManager;
pub use state_machine::LpStateMachine;
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
pub const NOISE_PSK_INDEX: u8 = 3;
#[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))]
#[cfg(test)]
pub fn sessions_for_tests() -> (LpSession, LpSession) {
let sessions = SessionsMock::mock_post_handshake(69);
(sessions.initiator, sessions.responder)
}
let (init, resp) = crate::peer::mock_peers();
#[cfg(any(feature = "mock", test))]
pub fn mock_session_for_test() -> LpSession {
SessionsMock::mock_initiator()
// Use a fixed receiver_index for deterministic tests
let receiver_index: u32 = 12345;
// 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)]
@@ -162,16 +53,17 @@ mod tests {
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::session_manager::SessionManager;
use crate::{LpError, SessionsMock, mock_session_for_test};
use crate::{LpError, sessions_for_tests};
use bytes::BytesMut;
// Import the new standalone functions
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::peer::mock_peers;
#[test]
fn test_replay_protection_integration() {
// Create session
let mut session = mock_session_for_test();
let session = sessions_for_tests().0;
// === Packet 1 (Counter 0 - Should succeed) ===
let packet1 = LpPacket {
@@ -270,20 +162,32 @@ mod tests {
#[test]
fn test_session_manager_integration() {
// Create session manager
let mut local_manager = SessionManager::new();
let mut remote_manager = SessionManager::new();
let local_manager = SessionManager::new();
let remote_manager = SessionManager::new();
// Generate Ed25519 keypairs for PSQ authentication
let (init, resp) = mock_peers();
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
let local_session = sessions.initiator;
let remote_session = sessions.responder;
// Test salt
let salt = [46u8; 32];
// Create a session via manager
let _ = local_manager.create_session_state_machine(local_session);
let _ = remote_manager.create_session_state_machine(remote_session);
let _ = local_manager
.create_session_state_machine(
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) ===
let packet1 = LpPacket {
header: LpHeader {
+34 -210
View File
@@ -1,19 +1,17 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::packet::LpHeader;
use crate::peer::LpRemotePeer;
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError};
use bytes::{BufMut, BytesMut};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use nym_crypto::asymmetric::{ed25519, x25519};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
@@ -30,17 +28,6 @@ impl ClientHelloData {
// 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 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 {
Self::LEN
}
@@ -154,8 +141,6 @@ pub enum MessageType {
SubsessionReady = 0x000C,
/// Subsession abort - race winner tells loser to become responder
SubsessionAbort = 0x000D,
/// General error
Error = 0x00FF,
}
impl MessageType {
@@ -172,9 +157,6 @@ impl MessageType {
pub struct HandshakeData(pub Vec<u8>);
impl HandshakeData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -192,11 +174,6 @@ impl HandshakeData {
pub struct EncryptedDataPayload(pub Vec<u8>);
impl EncryptedDataPayload {
#[allow(dead_code)]
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -215,10 +192,6 @@ impl EncryptedDataPayload {
pub struct KKTRequestData(pub Vec<u8>);
impl KKTRequestData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -237,10 +210,6 @@ impl KKTRequestData {
pub struct KKTResponseData(pub Vec<u8>);
impl KKTResponseData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
@@ -254,60 +223,15 @@ 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
#[derive(Debug, Clone)]
pub struct ForwardPacketData {
/// Target gateway's Ed25519 identity (32 bytes)
pub target_gateway_identity: [u8; 32],
// TODO: replace it with `SocketAddr`
/// Target gateway's LP address (IP:port string)
pub target_lp_address: SocketAddr,
pub target_lp_address: String,
/// Complete inner LP packet bytes (serialized LpPacket)
/// This is the CLIENT→EXIT gateway packet, encrypted for exit
@@ -315,46 +239,23 @@ pub struct 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 {
// 32 bytes target gateway identity
// +
// 1 byte length of target lp address type
// 4 bytes length of target lp address
// +
// {4,16} target_lp_address IPv{4,6}
// +
// 2 bytes target_lp_address port
// target_lp_address.len()
// +
// 4 bytes of length of inner packet bytes
// +
// 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(),
}
32 + 4 + self.target_lp_address.len() + 4 + self.inner_packet_bytes.len()
}
fn encode(&self, dst: &mut BytesMut) {
let (is_ipv6, ip_bytes) = match &self.target_lp_address {
SocketAddr::V4(address) => (false, address.ip().octets().to_vec()),
SocketAddr::V6(address) => (true, address.ip().octets().to_vec()),
};
dst.put_slice(&self.target_gateway_identity);
dst.put_u8(is_ipv6 as u8); // IP type , 0 for ipv4
dst.put_slice(&ip_bytes); // IP bytes
dst.put_u16_le(self.target_lp_address.port()); // Port
dst.put_u16_le(self.target_lp_address.len() as u16);
dst.put_slice(self.target_lp_address.as_bytes());
dst.put_u32_le(self.inner_packet_bytes.len() as u32);
dst.put_slice(&self.inner_packet_bytes);
}
@@ -366,56 +267,42 @@ impl ForwardPacketData {
}
pub fn decode(bytes: &[u8]) -> Result<Self, LpError> {
// smallest possible packet with ipv4 and empty data
if bytes.len() < 43 {
// 32 + 1 + 4 + 2 + 4 + 0
// smallest possible packet with empty address and empty data
if bytes.len() < 38 {
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ForwardPacketData. got {}",
"Too few bytes to deserialise ForwardPacketData[1]. got {}",
bytes.len()
)));
}
// SAFETY: we ensured we have sufficient data
#[allow(clippy::unwrap_used)]
let target_gateway_identity = bytes[0..32].try_into().unwrap();
let target_lp_address_is_ipv6 = bytes[32] != 0;
let target_lp_address_len = u16::from_le_bytes([bytes[32], bytes[33]]);
let (target_lp_address, next_index) = if target_lp_address_is_ipv6 {
// IPv6, first check we have actually enough bytes
// smallest possible packet with ipv6 and empty data
if bytes.len() < 55 {
// 32 + 1 + 16 + 2 + 4 + 0
return Err(LpError::DeserializationError(format!(
"Too few bytes to deserialise ipv6 ForwardPacketData. got {}",
bytes.len()
)));
}
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
#[allow(clippy::unwrap_used)]
let ipv6 = IpAddr::V6(Ipv6Addr::from_octets(bytes[33..49].try_into().unwrap()));
let port = u16::from_le_bytes([bytes[49], bytes[50]]);
(SocketAddr::new(ipv6, port), 51)
} else {
// IPv4. Length check done at the start
// SAFETY: we ensured we have sufficient data, and the length is correct for casting
#[allow(clippy::unwrap_used)]
let ipv4 = IpAddr::V4(Ipv4Addr::from_octets(bytes[33..37].try_into().unwrap()));
let port = u16::from_le_bytes([bytes[37], bytes[38]]);
(SocketAddr::new(ipv4, port), 39)
};
let inner_packet_bytes_len = u32::from_le_bytes([
bytes[next_index],
bytes[next_index + 1],
bytes[next_index + 2],
bytes[next_index + 3],
]);
if bytes[next_index + 4..].len() != inner_packet_bytes_len as usize {
// smallest possible packet with empty data
if bytes[34..].len() < 4 + target_lp_address_len as usize {
return Err(LpError::DeserializationError(format!(
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
bytes[next_index + 4..].len()
"Too few bytes to deserialise ForwardPacketData[2]. got {}",
bytes.len()
)));
}
let inner_packet_bytes = bytes[next_index + 4..].to_vec();
let target_lp_address =
String::from_utf8_lossy(&bytes[34..34 + target_lp_address_len as usize]).to_string();
let inner_packet_bytes_len = u32::from_le_bytes([
bytes[34 + target_lp_address_len as usize],
bytes[34 + target_lp_address_len as usize + 1],
bytes[34 + target_lp_address_len as usize + 2],
bytes[34 + target_lp_address_len as usize + 3],
]);
if bytes[34 + target_lp_address_len as usize + 4..].len() != inner_packet_bytes_len as usize
{
return Err(LpError::DeserializationError(format!(
"Expected {inner_packet_bytes_len} bytes to deserialise inner packet bytes of ForwardPacketData. got {}",
bytes[34 + target_lp_address_len as usize + 4..].len()
)));
}
let inner_packet_bytes = bytes[34 + target_lp_address_len as usize + 4..].to_vec();
Ok(ForwardPacketData {
target_gateway_identity,
@@ -525,62 +412,6 @@ pub enum LpMessage {
SubsessionReady(SubsessionReadyData),
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
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 {
@@ -600,7 +431,6 @@ impl Display for LpMessage {
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
LpMessage::Error(_) => write!(f, "Error"),
}
}
}
@@ -622,7 +452,6 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionAbort => &[],
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
}
}
@@ -642,7 +471,6 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => false, // Always has payload
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
LpMessage::SubsessionAbort => true, // Empty signal
LpMessage::Error(_) => false,
}
}
@@ -662,7 +490,6 @@ impl LpMessage {
LpMessage::SubsessionKK2(payload) => payload.len(),
LpMessage::SubsessionReady(payload) => payload.len(),
LpMessage::SubsessionAbort => 0,
LpMessage::Error(payload) => payload.len(),
}
}
@@ -682,7 +509,6 @@ impl LpMessage {
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
LpMessage::Error(_) => MessageType::Error,
}
}
@@ -702,7 +528,6 @@ impl LpMessage {
LpMessage::SubsessionKK2(data) => data.encode(dst),
LpMessage::SubsessionReady(data) => data.encode(dst),
LpMessage::SubsessionAbort => { /* No content - signal only */ }
LpMessage::Error(data) => data.encode(dst),
}
}
@@ -755,7 +580,6 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::SubsessionAbort)
}
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
}
}
}
+40 -47
View File
@@ -82,13 +82,6 @@ pub enum ReadResult {
// --- Implementation ---
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.
///
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
@@ -98,46 +91,6 @@ 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.
///
/// Assumes the caller handles buffering and framing to provide one full message.
@@ -335,3 +288,43 @@ 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))
}
+4 -8
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::LpError;
use crate::message::{LpMessage, MessageType};
use crate::message::LpMessage;
use crate::replay::ReceivingKeyCounterValidator;
use bytes::{BufMut, BytesMut};
use nym_lp_common::format_debug_bytes;
@@ -53,10 +53,6 @@ impl LpPacket {
}
}
pub fn typ(&self) -> MessageType {
self.message.typ()
}
/// Compute a hash of the message payload
///
/// This can be used for message integrity verification or deduplication
@@ -207,9 +203,9 @@ impl LpHeader {
}
impl LpHeader {
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
pub fn new(receiver_idx: u32, counter: u64) -> Self {
Self {
protocol_version,
protocol_version: version::CURRENT,
reserved: [0u8; 3],
receiver_idx,
counter,
@@ -269,7 +265,7 @@ impl LpHeader {
Ok(LpHeader {
protocol_version,
reserved,
reserved: [0u8; 3],
receiver_idx,
counter,
})
+4 -42
View File
@@ -1,9 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{ClientHelloData, LpError};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use std::collections::HashMap;
use std::sync::Arc;
@@ -30,14 +29,6 @@ impl LpLocalPeer {
}
}
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
ClientHelloData::new_with_fresh_salt(
*self.x25519().public_key(),
*self.ed25519().public_key(),
timestamp,
)
}
#[must_use]
pub fn with_kem_psq_key(mut self, key: Arc<x25519::KeyPair>) -> Self {
self.kem_psq = Some(key);
@@ -52,14 +43,6 @@ impl LpLocalPeer {
&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
#[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer {
@@ -145,37 +128,16 @@ impl LpRemotePeer {
self.expected_signing_key_digests = expected_signing_key_digests;
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(any(feature = "mock", test))]
#[cfg(test)]
pub fn mock_peer() -> LpLocalPeer {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
random_peer(&mut rng)
}
#[cfg(any(feature = "mock", test))]
#[cfg(test)]
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
let x25519 = Arc::new(ed25519.to_x25519());
@@ -188,7 +150,7 @@ pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPe
}
}
#[cfg(any(feature = "mock", test))]
#[cfg(test)]
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
// use deterministic rng
let mut rng = nym_test_utils::helpers::deterministic_rng();
-52
View File
@@ -1,52 +0,0 @@
// 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 {}
-391
View File
@@ -1,391 +0,0 @@
// 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),
}
}
}
-340
View File
@@ -1,340 +0,0 @@
// 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(())
}
}
-461
View File
@@ -1,461 +0,0 @@
// 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),
}
}
}
+1761 -170
View File
File diff suppressed because it is too large Load Diff
+594 -54
View File
@@ -2,7 +2,7 @@
mod tests {
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::{
LpError, SessionsMock,
LpError,
message::LpMessage,
packet::{LpHeader, LpPacket, TRAILER_LEN},
session_manager::SessionManager,
@@ -44,21 +44,200 @@ mod tests {
#[test]
fn test_full_session_flow() {
// 1. Initialize session manager
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
// 2. Create sessions using the pre-built Noise states
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);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 100001;
// 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
assert_eq!(session_manager_1.session_count(), 1);
assert_eq!(session_manager_2.session_count(), 1);
// 3. Simulate Data Transfer (Post-Handshake)
// 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");
// 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...");
let plaintext_a_to_b = b"Hello from A!";
@@ -135,7 +314,7 @@ mod tests {
println!("Data transfer simulation completed.");
// 4. Replay Protection Test (Data Packet)
// 8. Replay Protection Test (Data Packet)
println!("Testing data packet replay protection...");
// Try to replay the last message from B to A
// Need to re-encode because decode consumes the buffer
@@ -166,7 +345,7 @@ mod tests {
);
println!("Data packet replay protection test passed.");
// 5. Test out-of-order packet reception (send counter N+1 before counter N)
// 9. Test out-of-order packet reception (send counter N+1 before counter N)
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_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
@@ -212,7 +391,7 @@ mod tests {
String::from_utf8_lossy(&decrypted_payload)
);
// 6. Now send the skipped counter N message (should still work)
// 10. Now send the skipped counter N message (should still work)
println!("Testing delayed data packet reception...");
// Prepare data for counter_a_next (N)
let plaintext_delayed = b"Delayed message";
@@ -260,7 +439,7 @@ mod tests {
println!("Delayed data packet reception test passed.");
// 7. Try to replay message with counter N (should fail)
// 11. Try to replay message with counter N (should fail)
println!("Testing replay of delayed packet...");
let parsed_delayed_replay =
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
@@ -272,7 +451,7 @@ mod tests {
"Should be a replay protection error"
);
// 8. Session removal
// 12. Session removal
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
@@ -289,21 +468,80 @@ mod tests {
#[test]
fn test_bidirectional_communication() {
// 1. Initialize session manager
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
// 2. Create sessions using the pre-built Noise states
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);
// Use fixed receiver_index for test
let receiver_index: u32 = 100002;
// Counters after handshake
let mut counter_a = 0; // Next counter for A to send
let mut counter_b = 0; // Next counter for B to send
// Test salt
let salt = [43u8; 32];
// 3. Send multiple encrypted messages both ways
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");
// 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;
for i in 0..NUM_MESSAGES {
println!("Bidirectional test: Round {}", i);
@@ -368,30 +606,36 @@ mod tests {
// 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.
assert_eq!(
counter_a, NUM_MESSAGES,
counter_a,
2 + NUM_MESSAGES,
"Peer A final send counter mismatch"
);
assert_eq!(
total_recv_a, NUM_MESSAGES,
total_recv_a,
1 + NUM_MESSAGES,
"Peer A total received count mismatch"
); // Received 5 data
); // Received 1 handshake + 5 data
assert_eq!(
next_recv_a, NUM_MESSAGES,
next_recv_a,
1 + NUM_MESSAGES,
"Peer A next expected receive counter mismatch"
); // Expected counter for msg from B
// 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.
assert_eq!(
counter_b, NUM_MESSAGES,
counter_b,
1 + NUM_MESSAGES,
"Peer B final send counter mismatch"
);
assert_eq!(
total_recv_b, NUM_MESSAGES,
total_recv_b,
2 + NUM_MESSAGES,
"Peer B total received count mismatch"
); // Received 5 data
); // Received 2 handshake + 5 data
assert_eq!(
next_recv_b, NUM_MESSAGES,
next_recv_b,
2 + NUM_MESSAGES,
"Peer B next expected receive counter mismatch"
); // Expected counter for msg from A
@@ -402,14 +646,21 @@ mod tests {
#[test]
fn test_session_error_handling() {
// 1. Initialize session manager
let mut session_manager = SessionManager::new();
let session_manager = SessionManager::new();
let receiver_index = 123;
let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator;
let session2 = SessionsMock::mock_post_handshake(124).initiator;
// Generate Ed25519 keypair for PSQ authentication
let (a, b) = mock_peers();
// 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)
let _session = session_manager.create_session_state_machine(session1);
let _session = session_manager
.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
let result = session_manager.state_machine_exists(999);
@@ -423,10 +674,19 @@ mod tests {
);
// 5. Create and immediately remove a session
let _temp_session = session_manager.create_session_state_machine(session2);
let receiver_index_temp: u32 = 100004;
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!(
session_manager.remove_state_machine(124),
session_manager.remove_state_machine(receiver_index_temp),
"Should remove the session"
);
@@ -473,8 +733,14 @@ mod tests {
}
// Remove unused imports if SessionManager methods are no longer direct dependencies
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
use crate::peer::mock_peers;
use crate::state_machine::LpData;
use crate::state_machine::{LpAction, LpInput, LpStateBare};
use crate::{
// 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
// Keep helper function for creating test packets if needed,
@@ -491,22 +757,295 @@ mod tests {
#[test]
fn test_full_session_flow_with_process_input() {
// 1. Initialize session managers
let mut session_manager_1 = SessionManager::new();
let mut session_manager_2 = SessionManager::new();
let session_manager_1 = SessionManager::new();
let session_manager_2 = SessionManager::new();
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
// 2. Generate Ed25519 keypairs for PSQ authentication
let (a, b) = mock_peers();
// 2. Create sessions state machines
session_manager_1.create_session_state_machine(sessions.initiator);
session_manager_2.create_session_state_machine(sessions.responder);
// Use fixed receiver_index for test
let receiver_index: u32 = 100005;
// 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_2.session_count(), 1);
assert!(session_manager_1.state_machine_exists(receiver_index));
assert!(session_manager_2.state_machine_exists(receiver_index));
// Verify initial states are Transport
// Verify initial states are ReadyToHandshake
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!(
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport
@@ -515,8 +1054,9 @@ mod tests {
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
println!("Handshake simulation completed successfully via process_input.");
// --- 3. Simulate Data Transfer via process_input ---
// --- 5. Simulate Data Transfer 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_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
@@ -594,7 +1134,7 @@ mod tests {
}
println!("Data transfer simulation completed.");
// --- 4. Replay Protection Test ---
// --- 6. Replay Protection Test ---
println!("Testing data packet replay protection via process_input...");
let replay_result = session_manager_1
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
@@ -608,7 +1148,7 @@ mod tests {
);
println!("Data packet replay protection test passed.");
// --- 5. Out-of-Order Test ---
// --- 7. Out-of-Order Test ---
println!("Testing out-of-order reception via process_input...");
// A prepares N+1 then N
@@ -667,7 +1207,7 @@ mod tests {
);
println!("Out-of-order test passed.");
// --- 6. Close Test ---
// --- 8. Close Test ---
println!("Testing close via process_input...");
// A closes
@@ -715,7 +1255,7 @@ mod tests {
));
println!("Close test passed.");
// --- 7. Session Removal ---
// --- 9. Session Removal ---
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
assert!(!session_manager_1.state_machine_exists(receiver_index));
+146 -50
View File
@@ -6,9 +6,11 @@
//! This module implements session lifecycle management functionality, handling
//! creation, retrieval, and storage of sessions.
use crate::state_machine::{LpAction, LpInput, LpStateBare};
use crate::noise_protocol::ReadResult;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
use std::collections::HashMap;
use dashmap::DashMap;
/// Manages the lifecycle of Lewes Protocol sessions.
///
@@ -16,7 +18,7 @@ use std::collections::HashMap;
/// ensuring proper thread-safety for concurrent access.
pub struct SessionManager {
/// Manages state machines directly, keyed by lp_id
state_machines: HashMap<u32, LpStateMachine>,
state_machines: DashMap<u32, LpStateMachine>,
}
impl Default for SessionManager {
@@ -29,18 +31,36 @@ impl SessionManager {
/// Creates a new session manager with empty session storage.
pub fn new() -> Self {
Self {
state_machines: HashMap::new(),
state_machines: DashMap::new(),
}
}
pub fn process_input(
&mut self,
lp_id: u32,
input: LpInput,
) -> Result<Option<LpAction>, LpError> {
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
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> {
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
}
@@ -64,27 +84,38 @@ impl SessionManager {
})?
}
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?.receiving_counter_mark(counter)
})?
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
}
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
self.prepare_handshake_message(lp_id)
}
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
.ok()?
}
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)
.map_err(LpError::NoiseError)
})?
}
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
pub fn encrypt_data(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
self.with_state_machine(lp_id, |sm| {
sm.session()?
.encrypt_data(message)
.map_err(LpError::NoiseError)
})?
@@ -94,6 +125,14 @@ impl SessionManager {
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 {
self.state_machines.len()
}
@@ -107,7 +146,7 @@ impl SessionManager {
F: FnOnce(&LpStateMachine) -> R,
{
if let Some(sm) = self.state_machines.get(&lp_id) {
Ok(f(sm))
Ok(f(&sm))
} else {
Err(LpError::StateMachineNotFound { lp_id })
}
@@ -115,48 +154,74 @@ impl SessionManager {
}
// For mutable access (like running process_input)
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
where
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
{
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
Ok(f(sm))
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
Ok(f(&mut sm))
} else {
Err(LpError::StateMachineNotFound { lp_id })
}
}
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
let receiver_index = lp_session.id();
let sm = LpStateMachine::new(lp_session);
pub fn create_session_state_machine(
&self,
receiver_index: u32,
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);
receiver_index
Ok(receiver_index)
}
/// Method to remove a state machine
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
let removed = self.state_machines.remove(&lp_id);
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)]
mod tests {
use super::*;
use crate::{SessionsMock, mock_session_for_test};
use crate::peer::{mock_peers, random_peer};
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn test_session_manager_get() {
let mut manager = SessionManager::new();
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let local_session = mock_session_for_test();
let id = local_session.id();
let salt = [47u8; 32];
let receiver_index: u32 = 1001;
let sm_1_id = manager.create_session_state_machine(local_session);
assert_eq!(sm_1_id, id);
let sm_1_id = manager
.create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt)
.unwrap();
let retrieved = manager.state_machine_exists(id);
let retrieved = manager.state_machine_exists(sm_1_id);
assert!(retrieved);
let not_found = manager.state_machine_exists(99);
@@ -165,10 +230,17 @@ mod tests {
#[test]
fn test_session_manager_remove() {
let mut manager = SessionManager::new();
let local_session = mock_session_for_test();
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let sm_1_id = manager.create_session_state_machine(local_session);
let salt = [48u8; 32];
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);
assert!(removed);
@@ -180,14 +252,26 @@ mod tests {
#[test]
fn test_multiple_sessions() {
let mut manager = SessionManager::new();
let session1 = SessionsMock::mock_post_handshake(123).initiator;
let session2 = SessionsMock::mock_post_handshake(124).initiator;
let session3 = SessionsMock::mock_post_handshake(125).initiator;
let manager = SessionManager::new();
let mut rng = deterministic_rng();
let local = random_peer(&mut rng);
let peer1 = random_peer(&mut rng);
let peer2 = random_peer(&mut rng);
let peer3 = random_peer(&mut rng);
let sm_1 = manager.create_session_state_machine(session1);
let sm_2 = manager.create_session_state_machine(session2);
let sm_3 = manager.create_session_state_machine(session3);
let salt = [49u8; 32];
let sm_1 = manager
.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);
@@ -202,11 +286,23 @@ mod tests {
#[test]
fn test_session_manager_create_session() {
let mut manager = SessionManager::new();
let manager = SessionManager::new();
let (init, resp) = mock_peers();
let sesion = mock_session_for_test();
let salt = [50u8; 32];
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);
let retrieved = manager.get_state_machine_id(sm);
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 sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
use tokio::time::Instant;
use tracing::{debug, error, info, instrument};
use tracing::{debug, error, info, instrument, warn};
#[derive(Clone)]
pub struct PostgresScraperStorage {
@@ -22,10 +22,7 @@ pub struct PostgresScraperStorage {
impl PostgresScraperStorage {
#[instrument]
pub async fn init(
connection_string: &str,
run_migrations: &bool,
) -> Result<Self, PostgresScraperError> {
pub async fn init(connection_string: &str) -> Result<Self, PostgresScraperError> {
debug!("initialising scraper database with '{connection_string}'",);
let connection_pool = match sqlx::PgPool::connect(connection_string).await {
@@ -36,13 +33,12 @@ impl PostgresScraperStorage {
}
};
if *run_migrations {
if let Err(err) = sqlx::migrate!("./sql_migrations")
.run(&connection_pool)
.await
{
return Err(err.into());
}
if let Err(err) = sqlx::migrate!("./sql_migrations")
.run(&connection_pool)
.await
{
warn!("Failed to initialize SQLx database: {err}");
// return Err(err.into());
}
info!("Database migration finished!");
@@ -196,11 +192,8 @@ impl PostgresScraperStorage {
impl NyxdScraperStorage for PostgresScraperStorage {
type StorageTransaction = PostgresStorageTransaction;
async fn initialise(
storage: &str,
run_migrations: &bool,
) -> Result<Self, NyxdScraperStorageError> {
PostgresScraperStorage::init(storage, run_migrations)
async fn initialise(storage: &str) -> Result<Self, NyxdScraperStorageError> {
PostgresScraperStorage::init(storage)
.await
.map_err(NyxdScraperStorageError::from)
}
@@ -48,8 +48,6 @@ pub struct Config {
pub store_precommits: bool,
pub start_block: StartingBlockOpts,
pub run_migrations: bool,
}
pub struct NyxdScraperBuilder<S> {
@@ -163,7 +161,7 @@ where
pub async fn new(config: Config) -> Result<Self, ScraperError> {
config.pruning_options.validate()?;
let storage = S::initialise(&config.database_storage, &config.run_migrations).await?;
let storage = S::initialise(&config.database_storage).await?;
let rpc_client = RpcClient::new(&config.rpc_url)?;
Ok(NyxdScraper {

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