Compare commits

..

7 Commits

Author SHA1 Message Date
benedettadavico cbc37ba6d6 Merge branch 'develop' of https://github.com/nymtech/nym into feature/wallet-ui-tests 2022-10-02 13:12:42 +02:00
benedettadavico d5f3f80a14 Merge branch 'develop' into feature/wallet-ui-tests 2022-09-02 11:46:47 +02:00
Benedetta 69dabdc785 fixing more issues 2022-09-02 10:55:35 +02:00
Benedetta 907c38ac7f fixing webdriverio 2022-09-02 10:27:06 +02:00
Benedetta a9522a00b9 fixing webdriverio 2022-09-02 10:16:05 +02:00
benedettadavico 7a921dc02f new testing branch 2022-09-01 11:12:09 +02:00
benedettadavico 8e49cc7f73 new testing branch 2022-08-22 11:34:20 +02:00
768 changed files with 16435 additions and 35274 deletions
-18
View File
@@ -3,21 +3,3 @@
RUST_LOG=info
RUST_BACKTRACE=1
#########################################
# geoipupdate (needed for explorer-api) #
#########################################
# MaxMind account ID (change it to a valid account ID)
GEOIPUPDATE_ACCOUNT_ID=xxx
# MaxMind license key (change it to a valid license key)
GEOIPUPDATE_LICENSE_KEY=xxx
# List of space-separated database edition IDs. Edition IDs may
# consist of letters, digits, and dashes. For example, GeoIP2-City
# would download the GeoIP2 City database (GeoIP2-City).
GEOIPUPDATE_EDITION_IDS=GeoLite2-Country
# The number of hours between geoipupdate runs. If this is not set
# or is set to 0, geoipupdate will run once and exit.
GEOIPUPDATE_FREQUENCY=72
# The path to the directory where geoipupdate will download the
# database.
GEOIP_DB_DIRECTORY=./explorer-api/geo_ip
+14 -30
View File
@@ -1,52 +1,36 @@
name: Daily security audit
on:
schedule:
- cron: '5 9 * * *'
on: workflow_dispatch
jobs:
cargo-deny:
security_audit:
runs-on: ubuntu-latest
steps:
- name: Checkout repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
- uses: actions/checkout@v1
- uses: actions-rs/audit-check@v1
with:
toolchain: stable
- name: Install cargo deny
run: cargo install --locked cargo-deny
- name: Run cargo deny
run: |
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
>(uniq &> .github/workflows/support-files/notifications/deny.message )
- uses: actions/upload-artifact@v3
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
token: ${{ secrets.GITHUB_TOKEN }}
notification:
needs: cargo-deny
if: ${{ failure() }}
needs: security_audit
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Download report from previous job
uses: actions/download-artifact@v3
with:
name: report
path: .github/workflows/support-files/notifications
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
env:
NYM_NOTIFICATION_KIND: security
NYM_PROJECT_NAME: "Daily security report"
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym daily audit"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "security"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBTECH_TEAM }}"
KEYBASE_NYM_CHANNEL: "test"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+7 -9
View File
@@ -29,12 +29,6 @@ jobs:
override: true
components: rustfmt, clippy
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
@@ -54,6 +48,12 @@ jobs:
command: test
args: --workspace --all-features -- --ignored
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
@@ -66,8 +66,6 @@ jobs:
command: clippy
args: --workspace -- -D warnings
# COCONUT stuff
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
with:
@@ -84,4 +82,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --features=coconut -- -D warnings
args: --features=coconut -- -D warnings
+72
View File
@@ -0,0 +1,72 @@
name: Continuous integration on dispatch
on: workflow_dispatch
jobs:
build:
runs-on: [ self-hosted, custom-linux ]
# Enable sccache via environment variable
env:
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --all-features
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace -- -D warnings
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --features=coconut
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --features=coconut
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
with:
command: clippy
args: --features=coconut -- -D warnings
+5 -18
View File
@@ -1,21 +1,16 @@
name: Build release of Nym smart contracts
on:
workflow_dispatch:
release:
types: [created]
defaults:
run:
working-directory: contracts
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check the release tag starts with `nym-contracts-`
if: startsWith(github.ref, 'refs/tags/nym-contracts-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-contracts-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
@@ -26,7 +21,7 @@ jobs:
components: rustfmt, clippy
- name: Build release contracts
run: make wasm
run: RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
- name: Upload Mixnet Contract Artifact
uses: actions/upload-artifact@v3
@@ -41,11 +36,3 @@ jobs:
name: vesting_contract.wasm
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
retention-days: 5
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
@@ -1,56 +0,0 @@
name: CI for Network Explorer API
on:
workflow_dispatch:
release:
types: [created]
env:
NETWORK: mainnet
jobs:
publish-nym:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
- name: Check the release tag starts with `nym-explorer-api-`
if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-explorer-api-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all explorer-api
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path explorer-api/Cargo.toml --workspace --release
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: my-artifact
path: |
target/release/explorer-api
retention-days: 30
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
@@ -0,0 +1,50 @@
[
{
"os":"ubuntu-latest",
"rust":"stable",
"runOnEvent":"workflow_dispatch"
},
{
"os":"windows-latest",
"rust":"stable",
"runOnEvent":"workflow_dispatch"
},
{
"os":"macos-latest",
"rust":"stable",
"runOnEvent":"workflow_dispatch"
},
{
"os":"ubuntu-latest",
"rust":"beta",
"runOnEvent":"workflow_dispatch"
},
{
"os":"windows-latest",
"rust":"beta",
"runOnEvent":"workflow_dispatch"
},
{
"os":"macos-latest",
"rust":"beta",
"runOnEvent":"workflow_dispatch"
},
{
"os":"ubuntu-latest",
"rust":"nightly",
"runOnEvent":"workflow_dispatch"
},
{
"os":"windows-latest",
"rust":"nightly",
"runOnEvent":"workflow_dispatch"
},
{
"os":"macos-latest",
"rust":"nightly",
"runOnEvent":"workflow_dispatch"
}
]
@@ -1,8 +1,6 @@
name: Nightly builds on latest release
name: Nightly builds on dispatch
on:
schedule:
- cron: '14 2 * * *'
on: workflow_dispatch
jobs:
matrix_prep:
runs-on: ubuntu-latest
@@ -10,40 +8,25 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
# creates the matrix strategy from nightly_build_matrix_includes.json
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- id: set-matrix
uses: JoshuaTheMiller/conditional-build-matrix@main
with:
inputFile: '.github/workflows/nightly_build_matrix_includes.json'
inputFile: '.github/workflows/nightly_build_matrix_on_dispatch.json'
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
get_release:
runs-on: ubuntu-latest
needs: matrix_prep
outputs:
output1: ${{ steps.step2.outputs.latest_release }}
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Fetch all branches
run: git fetch --all
- name: Set output variable to latest release branch
id: step2
run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT
build:
needs: [get_release,matrix_prep]
needs: matrix_prep
strategy:
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }}
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
if: matrix.os == 'ubuntu-latest'
- name: Check out latest release branch
uses: actions/checkout@v3
with:
ref: ${{needs.get_release.outputs.output1}}
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
@@ -59,12 +42,6 @@ jobs:
command: build
args: --workspace
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests
uses: actions-rs/cargo@v1
with:
@@ -122,12 +99,6 @@ jobs:
command: build
args: --workspace --features=coconut
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
@@ -174,13 +145,13 @@ jobs:
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings
notification:
needs: [build,get_release]
needs: build
runs-on: ubuntu-latest
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v2
- name: Check out repository code
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Keybase - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
@@ -189,14 +160,14 @@ jobs:
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym nightly build on latest release"
NYM_PROJECT_NAME: "Nym nightly build"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "ci-nightly-release"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMTECH_TEAM }}"
KEYBASE_NYM_CHANNEL: "${{ secrets.KEYBASE_CHANNEL_DEV_CORE_ID }}"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
@@ -1,203 +0,0 @@
name: Nightly builds on second latest release
on:
schedule:
- cron: '24 2 * * *'
jobs:
matrix_prep:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
# creates the matrix strategy from nightly_build_matrix_includes.json
- uses: actions/checkout@v3
- id: set-matrix
uses: JoshuaTheMiller/conditional-build-matrix@main
with:
inputFile: '.github/workflows/nightly_build_matrix_includes.json'
filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]'
get_release:
runs-on: ubuntu-latest
needs: matrix_prep
outputs:
output1: ${{ steps.step2.outputs.latest_release }}
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Fetch all branches
run: git fetch --all
- name: Set output variable to latest release branch
id: step2
run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 2 | head -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT
build:
needs: [get_release,matrix_prep]
strategy:
matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}}
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }}
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
if: matrix.os == 'ubuntu-latest'
- name: Check out latest release branch
uses: actions/checkout@v3
with:
ref: ${{needs.get_release.outputs.output1}}
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run expensive tests
if: github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master'
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --all-features -- --ignored
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --workspace --all-targets -- -D warnings
- name: Reclaim some disk space
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }}
with:
command: clean
# COCONUT stuff
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --features=coconut
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --features=coconut
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --workspace --all-targets --features=coconut -- -D warnings
# nym-wallet (the rust part)
- name: Build nym-wallet rust code
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Run nym-wallet tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Check nym-wallet formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
- name: Run clippy for nym-wallet
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings
notification:
needs: [build,get_release]
runs-on: ubuntu-latest
steps:
- name: Collect jobs status
uses: technote-space/workflow-conclusion-action@v2
- name: Check out repository code
uses: actions/checkout@v3
- name: Keybase - Node Install
if: env.WORKFLOW_CONCLUSION == 'failure'
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
if: env.WORKFLOW_CONCLUSION == 'failure'
env:
NYM_NOTIFICATION_KIND: nightly
NYM_PROJECT_NAME: "Nym nightly build on latest release"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "ci-nightly-release"
IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+16 -16
View File
@@ -41,19 +41,19 @@ jobs:
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-connect
NYM_PROJECT_NAME: "nym-connect"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "ci-nym-connect"
IS_SUCCESS: "${{ job.status == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
# - name: Keybase - Send Notification
# env:
# NYM_NOTIFICATION_KIND: nym-connect
# NYM_PROJECT_NAME: "nym-connect"
# NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
# NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}"
# GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
# GIT_BRANCH: "${GITHUB_REF##*/}"
# KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
# KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
# KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
# KEYBASE_NYM_CHANNEL: "ci-nym-connect"
# IS_SUCCESS: "${{ job.status == 'success' }}"
# uses: docker://keybaseio/client:stable-node
# with:
# args: .github/workflows/support-files/notifications/entry_point.sh
@@ -25,9 +25,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools
- name: Check the release tag starts with `nym-binaries-`
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v3
@@ -3,7 +3,7 @@ require('dotenv').config();
const Bot = require('keybase-bot');
let context = {
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect','security'],
kinds: ['nym-wallet', 'ts-packages', 'network-explorer', 'nightly', 'nym-connect'],
};
/**
@@ -1,24 +0,0 @@
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const { Octokit, App } = require('octokit');
async function addToContextAndValidate(context) {
return
}
async function getMessageBody(context) {
try {
const source = fs
.readFileSync("deny.message").toString();
return source;
} catch (error) {
console.error(error);
}
}
module.exports = {
addToContextAndValidate,
getMessageBody,
};
+1 -2
View File
@@ -37,5 +37,4 @@ validator-config
*.patch
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
storybook-static
+5 -87
View File
@@ -2,95 +2,25 @@
Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased (v1.2.0 breaking)
### Added
-clients: ability to use multi-reply SURBs to send arbitrarily long messages fully anonymously whilst requesting additional reply blocks whenever they're about to run out ([#1796])
### Changed
- clients: improve message logging when received message fails to get reconstructed ([#1803])
[#1796]: https://github.com/nymtech/nym/pull/1796
[#1803]: https://github.com/nymtech/nym/pull/1803
## Unreleased
### Added
- socks5-client/network-requester: add support for socks4a protocol
## [v1.1.1](https://github.com/nymtech/nym/tree/v1.1.1) (2022-11-29)
### Added
- binaries: add `-c` shortform for `--config-env-file`
- websocket-requests: add server response signalling current packet queue length in the client
- contracts: DKG contract that handles coconut key generation ([#1678][#1708][#1747])
- validator-api: generate coconut keys interactively, using DKG and multisig contracts ([#1678][#1708][#1747])
### Changed
- clients: add concept of transmission lanes to better handle multiple data streams ([#1720])
- clients,validator-api: take coconut signers from the chain instead of specifying them via CLI ([#1747])
- multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747])
- socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783])
- nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client
### Fixed
- gateway-client: fix decrypting stored messages on reconnect ([#1786])
### Fixed
- gateway-client: fix decrypting stored messages on reconnect ([#1786])
- socks5-client: fix shutting down all tasks if anyone of them panics or errors out ([#1805])
[#1678]: https://github.com/nymtech/nym/pull/1678
[#1708]: https://github.com/nymtech/nym/pull/1708
[#1720]: https://github.com/nymtech/nym/pull/1720
[#1747]: https://github.com/nymtech/nym/pull/1747
[#1783]: https://github.com/nymtech/nym/pull/1783
[#1786]: https://github.com/nymtech/nym/pull/1786
[#1805]: https://github.com/nymtech/nym/pull/1805
## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09)
### Added
- clients: add testing-only support for two more extended packet sizes (8kb and 16kb).
- common/ledger: new library for communicating with a Ledger device ([#1640])
- native-client/socks5-client/wasm-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666])
- native-client/socks5-client/wasm-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664])
- native-client/socks5-client/wasm-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671])
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611])
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to compute reward estimation endpoint
- nym-cli: added CLI tool for interacting with the Nyx blockchain and Nym mixnet smart contracts ([#1577])
- validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558])
- wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673])
- network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611](https://github.com/nymtech/nym/pull/1611))
- common/ledger: new library for communicating with a Ledger device ([#1640])
### Fixed
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724])
- validator-api: mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
- validator-api: should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721])
- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
### Changed
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725])
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
- moved `Percent` struct to `contracts-common`, change affects explorer-api
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681])
- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541])
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
- validator-api: changes to internal SQL schema due to the mixnet contract revamp ([#1472])
- validator-api: changes to internal data structures due to the mixnet contract revamp ([#1472])
- validator-api: split epoch-operations into multiple separate transactions ([#1472])
[#1472]: https://github.com/nymtech/nym/pull/1472
[#1541]: https://github.com/nymtech/nym/pull/1541
[#1558]: https://github.com/nymtech/nym/pull/1558
[#1577]: https://github.com/nymtech/nym/pull/1577
@@ -98,18 +28,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1591]: https://github.com/nymtech/nym/pull/1591
[#1640]: https://github.com/nymtech/nym/pull/1640
[#1645]: https://github.com/nymtech/nym/pull/1645
[#1611]: https://github.com/nymtech/nym/pull/1611
[#1664]: https://github.com/nymtech/nym/pull/1664
[#1666]: https://github.com/nymtech/nym/pull/1645
[#1669]: https://github.com/nymtech/nym/pull/1669
[#1671]: https://github.com/nymtech/nym/pull/1671
[#1673]: https://github.com/nymtech/nym/pull/1673
[#1681]: https://github.com/nymtech/nym/pull/1681
[#1702]: https://github.com/nymtech/nym/pull/1702
[#1703]: https://github.com/nymtech/nym/pull/1703
[#1721]: https://github.com/nymtech/nym/pull/1721
[#1724]: https://github.com/nymtech/nym/pull/1724
[#1725]: https://github.com/nymtech/nym/pull/1725
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
Generated
+470 -352
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -26,12 +26,10 @@ members = [
"common/client-libs/gateway-client",
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/client-connections",
"common/coconut-interface",
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
@@ -43,7 +41,6 @@ members = [
"common/execute",
"common/inclusion-probability",
"common/ledger",
"common/logging",
"common/mixnode-common",
"common/network-defaults",
"common/nonexhaustive-delayqueue",
@@ -70,7 +67,6 @@ members = [
"explorer-api",
"gateway",
"gateway/gateway-requests",
"integrations/bity",
"mixnode",
"service-providers/network-requester",
"service-providers/network-statistics",
+4 -22
View File
@@ -2,12 +2,12 @@ test: clippy-all cargo-test wasm fmt
test-all: test cargo-test-expensive
no-clippy: build cargo-test wasm fmt
happy: fmt clippy-happy test
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-wasm-client
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect
cargo-test: test-main test-contracts test-wallet test-connect test-coconut test-wasm-client
cargo-test: test-main test-contracts test-wallet test-connect test-coconut
cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive test-coconut-expensive
build: build-contracts build-wallet build-main build-connect build-wasm-client
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client
build: build-contracts build-wallet build-main build-connect
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect
clippy-happy-main:
cargo clippy
@@ -40,9 +40,6 @@ clippy-all-wallet:
clippy-all-connect:
cargo clippy --workspace --manifest-path nym-connect/Cargo.toml --all-features -- -D warnings
clippy-all-wasm-client:
cargo clippy --workspace --manifest-path clients/webassembly/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
test-main:
cargo test --workspace
@@ -71,9 +68,6 @@ test-wallet:
test-wallet-expensive:
cargo test --manifest-path nym-wallet/Cargo.toml --all-features -- --ignored
test-wasm-client:
cargo test --workspace --manifest-path clients/webassembly/Cargo.toml --all-features
test-connect:
cargo test --manifest-path nym-connect/Cargo.toml --all-features
@@ -92,12 +86,6 @@ build-wallet:
build-connect:
cargo build --manifest-path nym-connect/Cargo.toml --workspace
build-explorer-api:
cargo build --manifest-path explorer-api/Cargo.toml --workspace
build-wasm-client:
cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown
build-nym-cli:
cargo build --release --manifest-path tools/nym-cli/Cargo.toml
@@ -113,15 +101,9 @@ fmt-wallet:
fmt-connect:
cargo fmt --manifest-path nym-connect/Cargo.toml --all
fmt-wasm-client:
cargo fmt --manifest-path clients/webassembly/Cargo.toml --all
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
mixnet-opt: wasm
cd contracts/mixnet && make opt
generate-typescript:
cd tools/ts-rs-cli && cargo run && cd ../..
yarn types:lint:fix
+6 -52
View File
@@ -1,84 +1,38 @@
[package]
name = "client-core"
version = "1.1.1"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { version = "0.1.58" }
dirs = "4.0"
dashmap = "5.4.0"
futures = "0.3"
humantime-serde = "1.0"
log = "0.4"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { version = "1.0", features = ["derive"] }
tap = "1.0.1"
sled = "0.34"
thiserror = "1.0.34"
tokio = { version = "1.19.1", features = ["macros"] }
url = { version ="2.2", features = ["serde"] }
tokio = { version = "1.21.2", features = ["macros"]}
time = "0.3.17"
# internal
config = { path = "../../common/config" }
client-connections = { path = "../../common/client-connections" }
crypto = { path = "../../common/crypto" }
gateway-client = { path = "../../common/client-libs/gateway-client" }
#gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
gateway-requests = { path = "../../gateway/gateway-requests" }
nonexhaustive-delayqueue = { path = "../../common/nonexhaustive-delayqueue" }
nymsphinx = { path = "../../common/nymsphinx" }
pemstore = { path = "../../common/pemstore" }
topology = { path = "../../common/topology" }
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
task = { path = "../../common/task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-stream]
version = "0.1.9"
features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
version = "1.21.2"
features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
version = "0.6.2"
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures]
version = "0.4"
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
version = "0.2.83"
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer]
git = "https://github.com/mmsinclair/wasm-timer"
rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6"
[target."cfg(target_arch = \"wasm32\")".dependencies.gloo-timers]
version = "0.2.4"
features = ["futures"]
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils]
path = "../../common/wasm-utils"
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
version = "0.3.17"
features = ["wasm-bindgen"]
topology = { path = "../../common/topology" }
validator-client = { path = "../../common/client-libs/validator-client" }
tap = "1.0.1"
[dev-dependencies]
tempfile = "3.1.0"
[build-dependencies]
tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] }
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[features]
default = []
fs-surb-storage = ["sqlx"]
wasm = ["gateway-client/wasm"]
coconut = ["gateway-client/coconut", "gateway-requests/coconut"]
-31
View File
@@ -1,31 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[tokio::main]
async fn main() {
#[cfg(feature = "fs-surb-storage")]
{
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let database_path = format!("{}/fs-surbs-example.sqlite", out_dir);
let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path))
.await
.expect("Failed to create SQLx database connection");
sqlx::migrate!("./fs_surbs_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
#[cfg(target_family = "windows")]
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
}
@@ -1,40 +0,0 @@
CREATE TABLE status
(
flush_in_progress INTEGER NOT NULL,
previous_flush_timestamp INTEGER NOT NULL,
client_in_use INTEGER NOT NULL
);
CREATE TABLE reply_surb_storage_metadata
(
min_reply_surb_threshold INTEGER NOT NULL,
max_reply_surb_threshold INTEGER NOT NULL
);
CREATE TABLE sender_tag
(
recipient BLOB NOT NULL UNIQUE,
tag BLOB NOT NULL UNIQUE
);
CREATE TABLE reply_key
(
key_digest BLOB NOT NULL UNIQUE,
reply_key BLOB NOT NULL UNIQUE,
sent_at_timestamp INTEGER NOT NULL
);
CREATE TABLE reply_surb_sender
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
last_sent_timestamp INTEGER NOT NULL,
tag BLOB NOT NULL UNIQUE
);
CREATE TABLE reply_surb
(
reply_surb_sender_id INTEGER NOT NULL,
reply_surb BLOB NOT NULL,
FOREIGN KEY (reply_surb_sender_id) REFERENCES reply_surb_sender (id)
);
@@ -1,480 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::cover_traffic_stream::LoopCoverTrafficStream;
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
use crate::client::key_manager::KeyManager;
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use crate::client::real_messages_control;
use crate::client::real_messages_control::RealMessagesController;
use crate::client::received_buffer::{
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
};
use crate::client::replies::reply_controller;
use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender};
use crate::client::replies::reply_storage::{
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
};
use crate::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
use crate::config::{Config, DebugConfig, GatewayEndpointConfig};
use crate::error::ClientCoreError;
use crate::spawn_future;
use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::{
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
MixnetMessageSender,
};
use log::info;
use nymsphinx::acknowledgements::AckKey;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use std::sync::Arc;
use std::time::Duration;
use task::{ShutdownListener, ShutdownNotifier};
use url::Url;
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub mod non_wasm_helpers;
pub struct ClientInput {
pub shared_lane_queue_lengths: LaneQueueLengths,
pub connection_command_sender: ConnectionCommandSender,
pub input_sender: InputMessageSender,
}
pub struct ClientOutput {
pub received_buffer_request_sender: ReceivedBufferRequestSender,
}
pub enum ClientInputStatus {
AwaitingProducer { client_input: ClientInput },
Connected,
}
impl ClientInputStatus {
pub fn register_producer(&mut self) -> ClientInput {
match std::mem::replace(self, ClientInputStatus::Connected) {
ClientInputStatus::AwaitingProducer { client_input } => client_input,
ClientInputStatus::Connected => panic!("producer was already registered before"),
}
}
}
pub enum ClientOutputStatus {
AwaitingConsumer { client_output: ClientOutput },
Connected,
}
impl ClientOutputStatus {
pub fn register_consumer(&mut self) -> ClientOutput {
match std::mem::replace(self, ClientOutputStatus::Connected) {
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
}
}
}
pub struct BaseClientBuilder<'a, B> {
// due to wasm limitations I had to split it like this : (
gateway_config: &'a GatewayEndpointConfig,
debug_config: &'a DebugConfig,
disabled_credentials: bool,
validator_api_endpoints: Vec<Url>,
reply_storage_backend: B,
bandwidth_controller: Option<BandwidthController>,
key_manager: KeyManager,
}
impl<'a, B> BaseClientBuilder<'a, B>
where
B: ReplyStorageBackend + Send + Sync + 'static,
{
pub fn new_from_base_config<T>(
base_config: &'a Config<T>,
key_manager: KeyManager,
bandwidth_controller: Option<BandwidthController>,
reply_storage_backend: B,
) -> BaseClientBuilder<'a, B> {
BaseClientBuilder {
gateway_config: base_config.get_gateway_endpoint_config(),
debug_config: base_config.get_debug_config(),
disabled_credentials: base_config.get_disabled_credentials_mode(),
validator_api_endpoints: base_config.get_validator_api_endpoints(),
bandwidth_controller,
reply_storage_backend,
key_manager,
}
}
pub fn new(
gateway_config: &'a GatewayEndpointConfig,
debug_config: &'a DebugConfig,
key_manager: KeyManager,
bandwidth_controller: Option<BandwidthController>,
reply_storage_backend: B,
disabled_credentials: bool,
validator_api_endpoints: Vec<Url>,
) -> BaseClientBuilder<'a, B> {
BaseClientBuilder {
gateway_config,
debug_config,
disabled_credentials,
validator_api_endpoints,
bandwidth_controller,
reply_storage_backend,
key_manager,
}
}
pub fn as_mix_recipient(&self) -> Recipient {
Recipient::new(
*self.key_manager.identity_keypair().public_key(),
*self.key_manager.encryption_keypair().public_key(),
// TODO: below only works under assumption that gateway address == gateway id
// (which currently is true)
NodeIdentity::from_base58_string(&self.gateway_config.gateway_id).unwrap(),
)
}
// future constantly pumping loop cover traffic at some specified average rate
// the pumped traffic goes to the MixTrafficController
fn start_cover_traffic_stream(
debug_config: &DebugConfig,
ack_key: Arc<AckKey>,
self_address: Recipient,
topology_accessor: TopologyAccessor,
mix_tx: BatchMixMessageSender,
shutdown: ShutdownListener,
) {
info!("Starting loop cover traffic stream...");
let mut stream = LoopCoverTrafficStream::new(
ack_key,
debug_config.average_ack_delay,
debug_config.average_packet_delay,
debug_config.loop_cover_traffic_average_delay,
mix_tx,
self_address,
topology_accessor,
);
if let Some(size) = debug_config.use_extended_packet_size {
log::debug!("Setting extended packet size: {:?}", size);
stream.set_custom_packet_size(size.into());
}
stream.start_with_shutdown(shutdown);
}
#[allow(clippy::too_many_arguments)]
fn start_real_traffic_controller(
controller_config: real_messages_control::Config,
topology_accessor: TopologyAccessor,
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
reply_storage: CombinedReplyStorage,
reply_controller_sender: ReplyControllerSender,
reply_controller_receiver: ReplyControllerReceiver,
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
shutdown: ShutdownListener,
) {
info!("Starting real traffic stream...");
RealMessagesController::new(
controller_config,
ack_receiver,
input_receiver,
mix_sender,
topology_accessor,
reply_storage,
reply_controller_sender,
reply_controller_receiver,
lane_queue_lengths,
client_connection_rx,
)
.start_with_shutdown(shutdown);
}
// buffer controlling all messages fetched from provider
// required so that other components would be able to use them (say the websocket)
fn start_received_messages_buffer_controller(
local_encryption_keypair: Arc<encryption::KeyPair>,
query_receiver: ReceivedBufferRequestReceiver,
mixnet_receiver: MixnetMessageReceiver,
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
shutdown: ShutdownListener,
) {
info!("Starting received messages buffer controller...");
ReceivedMessagesBufferController::new(
local_encryption_keypair,
query_receiver,
mixnet_receiver,
reply_key_storage,
reply_controller_sender,
)
.start_with_shutdown(shutdown)
}
async fn start_gateway_client(
&mut self,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
shutdown: ShutdownListener,
) -> GatewayClient {
let gateway_id = self.gateway_config.gateway_id.clone();
if gateway_id.is_empty() {
panic!("The identity of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_owner = self.gateway_config.gateway_owner.clone();
if gateway_owner.is_empty() {
panic!("The owner of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_address = self.gateway_config.gateway_listener.clone();
if gateway_address.is_empty() {
panic!("The address of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
.expect("provided gateway id is invalid!");
// disgusting wasm workaround since there's no key persistence there (nor `client init`)
let shared_key = if self.key_manager.gateway_key_set() {
Some(self.key_manager.gateway_shared_key())
} else {
None
};
let mut gateway_client = GatewayClient::new(
gateway_address,
self.key_manager.identity_keypair(),
gateway_identity,
gateway_owner,
shared_key,
mixnet_message_sender,
ack_sender,
self.debug_config.gateway_response_timeout,
self.bandwidth_controller.take(),
shutdown,
);
gateway_client.set_disabled_credentials_mode(self.disabled_credentials);
gateway_client
.authenticate_and_start()
.await
.expect("could not authenticate and start up the gateway connection");
gateway_client
}
// future responsible for periodically polling directory server and updating
// the current global view of topology
async fn start_topology_refresher(
validator_api_urls: Vec<Url>,
refresh_rate: Duration,
topology_accessor: TopologyAccessor,
shutdown: ShutdownListener,
) -> Result<(), ClientCoreError<B>> {
let topology_refresher_config = TopologyRefresherConfig::new(
validator_api_urls,
refresh_rate,
env!("CARGO_PKG_VERSION").to_string(),
);
let mut topology_refresher =
TopologyRefresher::new(topology_refresher_config, topology_accessor);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
info!("Obtaining initial network topology");
topology_refresher.refresh().await;
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
log::error!(
"The current network topology seem to be insufficient to route any packets through \
- check if enough nodes and a gateway are online - source: {err}"
);
return Err(ClientCoreError::InsufficientNetworkTopology(err));
}
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
Ok(())
}
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) -> BatchMixMessageSender {
info!("Starting mix traffic controller...");
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start_with_shutdown(shutdown);
mix_tx
}
async fn setup_persistent_reply_storage(
backend: B,
shutdown: ShutdownListener,
) -> Result<CombinedReplyStorage, ClientCoreError<B>> {
let persistent_storage = PersistentReplyStorage::new(backend);
let mem_store = persistent_storage
.load_state_from_backend()
.await
.map_err(|err| ClientCoreError::SurbStorageError { source: err })?;
let store_clone = mem_store.clone();
spawn_future(async move {
persistent_storage
.flush_on_shutdown(store_clone, shutdown)
.await
});
Ok(mem_store)
}
pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError<B>> {
info!("Starting nym client");
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
// used for announcing connection or disconnection of a channel for pushing re-assembled messages to
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
// channels responsible for controlling real messages
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
// channels responsible for controlling ack messages
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor = TopologyAccessor::new();
// Shutdown notifier for signalling tasks to stop
let shutdown = ShutdownNotifier::default();
// channels responsible for dealing with reply-related fun
let (reply_controller_sender, reply_controller_receiver) =
reply_controller::new_control_channels();
let self_address = self.as_mix_recipient();
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
let gateway_client = self
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
let reply_storage =
Self::setup_persistent_reply_storage(self.reply_storage_backend, shutdown.subscribe())
.await?;
Self::start_topology_refresher(
self.validator_api_endpoints.clone(),
self.debug_config.topology_refresh_rate,
shared_topology_accessor.clone(),
shutdown.subscribe(),
)
.await?;
Self::start_received_messages_buffer_controller(
self.key_manager.encryption_keypair(),
received_buffer_request_receiver,
mixnet_messages_receiver,
reply_storage.key_storage(),
reply_controller_sender.clone(),
shutdown.subscribe(),
);
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender =
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
// Channels that the websocket listener can use to signal downstream to the real traffic
// controller that connections are closed.
let (client_connection_tx, client_connection_rx) = mpsc::unbounded();
// Shared queue length data. Published by the `OutQueueController` in the client, and used
// primarily to throttle incoming connections (e.g socks5 for attached network-requesters)
let shared_lane_queue_lengths = LaneQueueLengths::new();
let mut controller_config = real_messages_control::Config::new(
self.debug_config,
self.key_manager.ack_key(),
self_address,
);
if let Some(size) = self.debug_config.use_extended_packet_size {
log::debug!("Setting extended packet size: {:?}", size);
controller_config.set_custom_packet_size(size.into());
}
Self::start_real_traffic_controller(
controller_config,
shared_topology_accessor.clone(),
ack_receiver,
input_receiver,
sphinx_message_sender.clone(),
reply_storage,
reply_controller_sender,
reply_controller_receiver,
shared_lane_queue_lengths.clone(),
client_connection_rx,
shutdown.subscribe(),
);
if !self.debug_config.disable_loop_cover_traffic_stream {
Self::start_cover_traffic_stream(
self.debug_config,
self.key_manager.ack_key(),
self_address,
shared_topology_accessor,
sphinx_message_sender,
shutdown.subscribe(),
);
}
info!("Client startup finished!");
info!("The address of this client is: {self_address}");
Ok(BaseClient {
client_input: ClientInputStatus::AwaitingProducer {
client_input: ClientInput {
shared_lane_queue_lengths,
connection_command_sender: client_connection_tx,
input_sender,
},
},
client_output: ClientOutputStatus::AwaitingConsumer {
client_output: ClientOutput {
received_buffer_request_sender,
},
},
shutdown_notifier: shutdown,
})
}
}
pub struct BaseClient {
pub client_input: ClientInputStatus,
pub client_output: ClientOutputStatus,
pub shutdown_notifier: ShutdownNotifier,
}
@@ -1,51 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
};
use crate::config::DebugConfig;
use crate::error::ClientCoreError;
use log::{error, info};
use std::path::Path;
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
db_path: P,
debug_config: &DebugConfig,
) -> Result<fs_backend::Backend, ClientCoreError<fs_backend::Backend>> {
// if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}");
Err(ClientCoreError::SurbStorageError { source: err })
}
}
} else {
info!("creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError { source: err });
}
};
// while I kinda hate that we're going to be creating `CombinedReplyStorage` twice,
// it will only be happening on the very first run and in practice won't incur huge
// costs since the storage is going to be empty
let mem_store = CombinedReplyStorage::new(
debug_config.minimum_reply_surb_storage_threshold,
debug_config.maximum_reply_surb_storage_threshold,
);
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError { source: err })?;
Ok(storage_backend)
}
}
@@ -3,27 +3,20 @@
use crate::client::mix_traffic::BatchMixMessageSender;
use crate::client::topology_control::TopologyAccessor;
use crate::spawn_future;
use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt};
use log::*;
use nymsphinx::acknowledgements::AckKey;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::cover::generate_loop_cover_packet;
use nymsphinx::params::PacketSize;
use nymsphinx::utils::sample_poisson_duration;
use rand::{rngs::OsRng, CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::error::TrySendError;
#[cfg(not(target_arch = "wasm32"))]
use task::ShutdownListener;
use tokio::task::JoinHandle;
use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
pub struct LoopCoverTrafficStream<R>
where
R: CryptoRng + Rng,
@@ -32,22 +25,18 @@ where
ack_key: Arc<AckKey>,
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
average_ack_delay: Duration,
average_ack_delay: time::Duration,
/// Average delay a data packet is going to get delay at a single mixnode.
average_packet_delay: Duration,
average_packet_delay: time::Duration,
/// Average delay between sending subsequent cover packets.
average_cover_message_sending_delay: Duration,
average_cover_message_sending_delay: time::Duration,
/// Internal state, determined by `average_message_sending_delay`,
/// used to keep track of when a next packet should be sent out.
#[cfg(not(target_arch = "wasm32"))]
next_delay: Pin<Box<time::Sleep>>,
#[cfg(target_arch = "wasm32")]
next_delay: Pin<Box<wasm_timer::Delay>>,
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
/// out to the network without any further delays.
mix_tx: BatchMixMessageSender,
@@ -61,8 +50,8 @@ where
/// Accessor to the common instance of network topology.
topology_access: TopologyAccessor,
/// Predefined packet size used for the loop cover messages.
packet_size: PacketSize,
/// Listen to shutdown signals.
shutdown: ShutdownListener,
}
impl<R> Stream for LoopCoverTrafficStream<R>
@@ -84,21 +73,13 @@ where
// we know it's time to send a message, so let's prepare delay for the next one
// Get the `now` by looking at the current `delay` deadline
let avg_delay = self.average_cover_message_sending_delay;
let now = self.next_delay.deadline();
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
// The next interval value is `next_poisson_delay` after the one that just
// yielded.
#[cfg(not(target_arch = "wasm32"))]
{
let now = self.next_delay.deadline();
let next = now + next_poisson_delay;
self.next_delay.as_mut().reset(next);
}
#[cfg(target_arch = "wasm32")]
{
self.next_delay.as_mut().reset(next_poisson_delay);
}
let next = now + next_poisson_delay;
self.next_delay.as_mut().reset(next);
Poll::Ready(Some(()))
}
@@ -110,49 +91,30 @@ impl LoopCoverTrafficStream<OsRng> {
#[allow(clippy::too_many_arguments)]
pub fn new(
ack_key: Arc<AckKey>,
average_ack_delay: Duration,
average_packet_delay: Duration,
average_cover_message_sending_delay: Duration,
average_ack_delay: time::Duration,
average_packet_delay: time::Duration,
average_cover_message_sending_delay: time::Duration,
mix_tx: BatchMixMessageSender,
our_full_destination: Recipient,
topology_access: TopologyAccessor,
shutdown: ShutdownListener,
) -> Self {
let rng = OsRng;
#[cfg(not(target_arch = "wasm32"))]
let next_delay = Box::pin(time::sleep(Default::default()));
#[cfg(target_arch = "wasm32")]
let next_delay = Box::pin(wasm_timer::Delay::new(Default::default()));
LoopCoverTrafficStream {
ack_key,
average_ack_delay,
average_packet_delay,
average_cover_message_sending_delay,
next_delay,
next_delay: Box::pin(time::sleep(Default::default())),
mix_tx,
our_full_destination,
rng,
topology_access,
packet_size: Default::default(),
shutdown,
}
}
pub fn set_custom_packet_size(&mut self, packet_size: PacketSize) {
self.packet_size = packet_size;
}
fn set_next_delay(&mut self, amount: Duration) {
#[cfg(not(target_arch = "wasm32"))]
let next_delay = Box::pin(time::sleep(amount));
#[cfg(target_arch = "wasm32")]
let next_delay = Box::pin(wasm_timer::Delay::new(amount));
self.next_delay = next_delay;
}
async fn on_new_message(&mut self) {
trace!("next cover message!");
@@ -161,16 +123,15 @@ impl LoopCoverTrafficStream<OsRng> {
// poisson delay, but is it really a problem?
let topology_permit = self.topology_access.get_read_permit().await;
// the ack is sent back to ourselves (and then ignored)
let topology_ref = match topology_permit.try_get_valid_topology_ref(
let topology_ref_option = topology_permit.try_get_valid_topology_ref(
&self.our_full_destination,
Some(&self.our_full_destination),
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
return;
}
};
);
if topology_ref_option.is_none() {
warn!("No valid topology detected - won't send any loop cover message this time");
return;
}
let topology_ref = topology_ref_option.unwrap();
let cover_message = generate_loop_cover_packet(
&mut self.rng,
@@ -179,26 +140,14 @@ impl LoopCoverTrafficStream<OsRng> {
&self.our_full_destination,
self.average_ack_delay,
self.average_packet_delay,
self.packet_size,
)
.expect("Somehow failed to generate a loop cover message with a valid topology");
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
match err {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
log::debug!("Failed to send cover message - channel full");
// However it's still useful to alert the user that the gateway or the link to
// the gateway can't keep up. Either due to insufficient bandwidth on the
// client side, or that the gateway is overloaded.
log::warn!("Failed to send: gateway appears to not keep up");
}
TrySendError::Closed(_) => {
log::warn!("Failed to send cover message - channel closed");
}
}
}
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
self.mix_tx.unbounded_send(vec![cover_message]).unwrap();
// TODO: I'm not entirely sure whether this is really required, because I'm not 100%
// sure how `yield_now()` works - whether it just notifies the scheduler or whether it
@@ -207,39 +156,40 @@ impl LoopCoverTrafficStream<OsRng> {
// JS: due to identical logical structure to OutQueueControl::on_message(), this is also
// presumably required to prevent bugs in the future. Exact reason is still unknown to me.
// TODO: temporary and BAD workaround for wasm (we should find a way to yield here in wasm)
#[cfg(not(target_arch = "wasm32"))]
tokio::task::yield_now().await;
}
pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) {
async fn run(&mut self) {
// we should set initial delay only when we actually start the stream
let sampled =
sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay);
self.set_next_delay(sampled);
self.next_delay = Box::pin(time::sleep(sample_poisson_duration(
&mut self.rng,
self.average_cover_message_sending_delay,
)));
spawn_future(async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
log::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
}
let mut shutdown = self.shutdown.clone();
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
log::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
}
}
}
shutdown.recv_timeout().await;
log::debug!("LoopCoverTrafficStream: Exiting");
}
assert!(self.shutdown.is_shutdown_poll());
log::debug!("LoopCoverTrafficStream: Exiting");
}
pub fn start(mut self) -> JoinHandle<()> {
tokio::spawn(async move {
self.run().await;
})
}
}
@@ -1,80 +1,33 @@
use client_connections::TransmissionLane;
use futures::channel::mpsc;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
pub type InputMessageSender = mpsc::UnboundedSender<InputMessage>;
pub type InputMessageReceiver = mpsc::UnboundedReceiver<InputMessage>;
#[derive(Debug)]
pub enum InputMessage {
/// The simplest message variant where no additional information is attached.
/// You're simply sending your `data` to specified `recipient` without any tagging.
///
/// Ends up with `NymMessage::Plain` variant
Regular {
Fresh {
recipient: Recipient,
data: Vec<u8>,
lane: TransmissionLane,
with_reply_surb: bool,
},
/// Creates a message used for a duplex anonymous communication where the recipient
/// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`.
///
/// Note that if reply_surbs is set to zero then
/// this variant requires the client having sent some reply_surbs in the past
/// (and thus the recipient also knowing our sender tag).
///
/// Ends up with `NymMessage::Repliable` variant
Anonymous {
recipient: Recipient,
data: Vec<u8>,
reply_surbs: u32,
lane: TransmissionLane,
},
/// Attempt to use our internally received and stored `ReplySurb` to send the message back
/// to specified recipient whilst not knowing its full identity (or even gateway).
///
/// Ends up with `NymMessage::Reply` variant
Reply {
recipient_tag: AnonymousSenderTag,
reply_surb: ReplySurb,
data: Vec<u8>,
lane: TransmissionLane,
},
}
impl InputMessage {
pub fn new_regular(recipient: Recipient, data: Vec<u8>, lane: TransmissionLane) -> Self {
InputMessage::Regular {
pub fn new_fresh(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Self {
InputMessage::Fresh {
recipient,
data,
lane,
with_reply_surb,
}
}
pub fn new_anonymous(
recipient: Recipient,
data: Vec<u8>,
reply_surbs: u32,
lane: TransmissionLane,
) -> Self {
InputMessage::Anonymous {
recipient,
data,
reply_surbs,
lane,
}
}
pub fn new_reply(
recipient_tag: AnonymousSenderTag,
data: Vec<u8>,
lane: TransmissionLane,
) -> Self {
InputMessage::Reply {
recipient_tag,
data,
lane,
}
pub fn new_reply(reply_surb: ReplySurb, data: Vec<u8>) -> Self {
InputMessage::Reply { reply_surb, data }
}
}
@@ -149,10 +149,6 @@ impl KeyManager {
)
}
pub fn gateway_key_set(&self) -> bool {
self.gateway_shared_key.is_some()
}
/// Gets an atomically reference counted pointer to [`AckKey`].
pub fn ack_key(&self) -> Arc<AckKey> {
Arc::clone(&self.ack_key)
+38 -44
View File
@@ -1,16 +1,17 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::GatewayClient;
use log::*;
use nymsphinx::forwarding::packet::MixPacket;
use task::ShutdownListener;
use tokio::task::JoinHandle;
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>;
pub type BatchMixMessageSender = mpsc::UnboundedSender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver<Vec<MixPacket>>;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
pub struct MixTrafficController {
@@ -18,6 +19,7 @@ pub struct MixTrafficController {
// later on gateway_client will need to be accessible by other entities
gateway_client: GatewayClient,
mix_rx: BatchMixMessageReceiver,
shutdown: ShutdownListener,
// TODO: this is temporary work-around.
// in long run `gateway_client` will be moved away from `MixTrafficController` anyway.
@@ -25,32 +27,22 @@ pub struct MixTrafficController {
}
impl MixTrafficController {
pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) {
let (sphinx_message_sender, sphinx_message_receiver) =
tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE);
(
MixTrafficController {
gateway_client,
mix_rx: sphinx_message_receiver,
consecutive_gateway_failure_count: 0,
},
sphinx_message_sender,
)
pub fn new(
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) -> MixTrafficController {
MixTrafficController {
gateway_client,
mix_rx,
shutdown,
consecutive_gateway_failure_count: 0,
}
}
async fn on_messages(&mut self, mut mix_packets: Vec<MixPacket>) {
debug_assert!(!mix_packets.is_empty());
// // simulate some dropped packets
// use rand::rngs::OsRng;
// use rand::Rng;
// let mut rng = OsRng;
// let number = rng.gen_range(0, 100);
// if number > 95 {
// error!("simulating dropped packet");
// return;
// }
let result = if mix_packets.len() == 1 {
let mix_packet = mix_packets.pop().unwrap();
self.gateway_client.send_mix_packet(mix_packet).await
@@ -77,28 +69,30 @@ impl MixTrafficController {
}
}
pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) {
spawn_future(async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
self.on_messages(mix_packets).await;
},
None => {
log::trace!("MixTrafficController: Stopping since channel closed");
break;
}
pub async fn run(&mut self) {
while !self.shutdown.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.next() => match mix_packets {
Some(mix_packets) => {
self.on_messages(mix_packets).await;
},
_ = shutdown.recv() => {
log::trace!("MixTrafficController: Received shutdown");
None => {
log::trace!("MixTrafficController: Stopping since channel closed");
break;
}
},
_ = self.shutdown.recv() => {
log::trace!("MixTrafficController: Received shutdown");
}
}
shutdown.recv_timeout().await;
log::debug!("MixTrafficController: Exiting");
}
assert!(self.shutdown.is_shutdown_poll());
log::debug!("MixTrafficController: Exiting");
}
pub fn start(mut self) -> JoinHandle<()> {
tokio::spawn(async move {
self.run().await;
})
}
}
+9 -4
View File
@@ -1,12 +1,17 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::atomic::AtomicBool;
pub mod base_client;
pub mod cover_traffic_stream;
pub mod inbound_messages;
pub mod key_manager;
pub mod mix_traffic;
pub mod real_messages_control;
pub mod received_buffer;
pub mod replies;
pub mod reply_key_storage;
pub mod topology_control;
// This is *NOT* used to signal shutdown.
// It's critical that we don't have any tasks finishing early, this is an additional safety check
// that tasks exiting are doing so because shutdown has been signalled, and no other reason.
// In particular for tasks that rely on their associated channel being closed to signal shutdown,
// and don't have access to a shutdown listener channel.
pub static SHUTDOWN_HAS_BEEN_SIGNALLED: AtomicBool = AtomicBool::new(false);
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use super::action_controller::{Action, ActionSender};
use futures::StreamExt;
use gateway_client::AcknowledgementReceiver;
use log::*;
@@ -10,30 +10,34 @@ use nymsphinx::{
chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID},
};
use std::sync::Arc;
use task::ShutdownListener;
/// Module responsible for listening for any data resembling acknowledgements from the network
/// and firing actions to remove them from the 'Pending' state.
pub(super) struct AcknowledgementListener {
ack_key: Arc<AckKey>,
ack_receiver: AcknowledgementReceiver,
action_sender: AckActionSender,
action_sender: ActionSender,
shutdown: ShutdownListener,
}
impl AcknowledgementListener {
pub(super) fn new(
ack_key: Arc<AckKey>,
ack_receiver: AcknowledgementReceiver,
action_sender: AckActionSender,
action_sender: ActionSender,
shutdown: ShutdownListener,
) -> Self {
AcknowledgementListener {
ack_key,
ack_receiver,
action_sender,
shutdown,
}
}
async fn on_ack(&mut self, ack_content: Vec<u8>) {
trace!("Received an ack");
debug!("Received an ack");
let frag_id = match recover_identifier(&self.ack_key, &ack_content)
.map(FragmentIdentifier::try_from_bytes)
{
@@ -49,6 +53,11 @@ impl AcknowledgementListener {
if frag_id == COVER_FRAG_ID {
trace!("Received an ack for a cover message - no need to do anything");
return;
} else if frag_id.is_reply() {
info!("Received an ack for a reply message - no need to do anything! (don't know what to do!)");
// TODO: probably there will need to be some extra procedure here, something to notify
// user that his reply reached the recipient (since we got an ack)
return;
}
trace!("Received {} from the mix network", frag_id);
@@ -58,31 +67,28 @@ impl AcknowledgementListener {
.unwrap();
}
async fn handle_ack_receiver_item(&mut self, item: Vec<Vec<u8>>) {
// realistically we would only be getting one ack at the time
for ack in item {
self.on_ack(ack).await;
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started AcknowledgementListener with graceful shutdown support");
while !shutdown.is_shutdown() {
pub(super) async fn run(&mut self) {
debug!("Started AcknowledgementListener");
while !self.shutdown.is_shutdown() {
tokio::select! {
acks = self.ack_receiver.next() => match acks {
Some(acks) => self.handle_ack_receiver_item(acks).await,
Some(acks) => {
// realistically we would only be getting one ack at the time
for ack in acks {
self.on_ack(ack).await;
}
},
None => {
log::trace!("AcknowledgementListener: Stopping since channel closed");
break;
}
},
_ = shutdown.recv() => {
_ = self.shutdown.recv() => {
log::trace!("AcknowledgementListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
assert!(self.shutdown.is_shutdown_poll());
log::debug!("AcknowledgementListener: Exiting");
}
}
@@ -3,7 +3,7 @@
use super::PendingAcknowledgement;
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
use futures::channel::mpsc;
use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender};
use futures::StreamExt;
use log::*;
use nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
@@ -12,9 +12,9 @@ use nymsphinx::Delay as SphinxDelay;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use task::ShutdownListener;
pub(crate) type AckActionSender = mpsc::UnboundedSender<Action>;
pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver<Action>;
pub(crate) type ActionSender = UnboundedSender<Action>;
// The actual data being sent off as well as potential key to the delay queue
type PendingAckEntry = (Arc<PendingAcknowledgement>, Option<QueueKey>);
@@ -96,25 +96,33 @@ pub(super) struct ActionController {
pending_acks_timers: NonExhaustiveDelayQueue<FragmentIdentifier>,
/// Channel for receiving `Action`s from other modules.
incoming_actions: AckActionReceiver,
incoming_actions: UnboundedReceiver<Action>,
/// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements.
retransmission_sender: RetransmissionRequestSender,
/// Listen for shutdown notifications
shutdown: ShutdownListener,
}
impl ActionController {
pub(super) fn new(
config: Config,
retransmission_sender: RetransmissionRequestSender,
incoming_actions: AckActionReceiver,
) -> Self {
ActionController {
config,
pending_acks_data: HashMap::new(),
pending_acks_timers: NonExhaustiveDelayQueue::new(),
incoming_actions,
retransmission_sender,
}
shutdown: ShutdownListener,
) -> (Self, ActionSender) {
let (sender, receiver) = mpsc::unbounded();
(
ActionController {
config,
pending_acks_data: HashMap::new(),
pending_acks_timers: NonExhaustiveDelayQueue::new(),
incoming_actions: receiver,
retransmission_sender,
shutdown,
},
sender,
)
}
fn handle_insert(&mut self, pending_acks: Vec<PendingAcknowledgement>) {
@@ -136,18 +144,13 @@ impl ActionController {
trace!("{} is starting its timer", frag_id);
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) {
// the fact that this branch is now POSSIBLE is a sign of a need to refactor this whole
// retransmission procedure
//
// (it can happen as timer is started when ack expires to make sure it's not stuck in memory
// and the second instance can be fired when we finally get reply surbs for data we failed to retransmit)
// if queue_key.is_some() {
// // this branch should be IMPOSSIBLE under ANY condition. It would imply starting
// // timer TWICE for the SAME PendingAcknowledgement
// panic!("Tried to start an already started ack timer!")
// }
let timeout = (pending_ack_data.delay * self.config.ack_wait_multiplier).to_duration()
if queue_key.is_some() {
// this branch should be IMPOSSIBLE under ANY condition. It would imply starting
// timer TWICE for the SAME PendingAcknowledgement
panic!("Tried to start an already started ack timer!")
}
let timeout = (pending_ack_data.delay.clone() * self.config.ack_wait_multiplier)
.to_duration()
+ self.config.ack_wait_addition;
let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout);
@@ -195,8 +198,7 @@ impl ActionController {
trace!("{} is updating its delay", frag_id);
// TODO: is it possible to solve this without either locking or temporarily removing the value?
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// or `ReplyController` (for 'reply' packets) which held the other potential
// this Action is triggered by `RetransmissionRequestListener` which held the other potential
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
// was dropped hence this unwrap is safe.
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
@@ -249,10 +251,8 @@ impl ActionController {
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started ActionController with graceful shutdown support");
while !shutdown.is_shutdown() {
pub(super) async fn run(&mut self) {
while !self.shutdown.is_shutdown() {
tokio::select! {
action = self.incoming_actions.next() => match action {
Some(action) => self.process_action(action),
@@ -270,15 +270,12 @@ impl ActionController {
break;
}
},
_ = shutdown.recv() => {
_ = self.shutdown.recv() => {
log::trace!("ActionController: Received shutdown");
}
}
}
#[cfg(not(target_arch = "wasm32"))]
tokio::time::timeout(Duration::from_secs(5), shutdown.recv())
.await
.expect("Task stopped without shutdown called");
assert!(self.shutdown.is_shutdown_poll());
log::debug!("ActionController: Exiting");
}
}
@@ -1,14 +1,22 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::replies::reply_controller::ReplyControllerSender;
use client_connections::TransmissionLane;
use super::action_controller::{Action, ActionSender};
use super::PendingAcknowledgement;
use crate::client::reply_key_storage::ReplyKeyStorage;
use crate::client::{
inbound_messages::{InputMessage, InputMessageReceiver},
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
topology_control::TopologyAccessor,
};
use futures::StreamExt;
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::preparer::MessagePreparer;
use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient};
use rand::{CryptoRng, Rng};
use std::sync::Arc;
use task::ShutdownListener;
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
/// putting everything into sphinx packets, etc.
@@ -17,9 +25,15 @@ pub(super) struct InputMessageListener<R>
where
R: CryptoRng + Rng,
{
ack_key: Arc<AckKey>,
ack_recipient: Recipient,
input_receiver: InputMessageReceiver,
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
message_preparer: MessagePreparer<R>,
action_sender: ActionSender,
real_message_sender: BatchRealMessageSender,
topology_access: TopologyAccessor,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
}
impl<R> InputMessageListener<R>
@@ -30,91 +44,151 @@ where
// some considerable refactoring
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
ack_key: Arc<AckKey>,
ack_recipient: Recipient,
input_receiver: InputMessageReceiver,
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
message_preparer: MessagePreparer<R>,
action_sender: ActionSender,
real_message_sender: BatchRealMessageSender,
topology_access: TopologyAccessor,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
) -> Self {
InputMessageListener {
ack_key,
ack_recipient,
input_receiver,
message_handler,
reply_controller_sender,
message_preparer,
action_sender,
real_message_sender,
topology_access,
reply_key_storage,
shutdown,
}
}
async fn handle_reply(
&mut self,
recipient_tag: AnonymousSenderTag,
data: Vec<u8>,
lane: TransmissionLane,
) {
// offload reply handling to the dedicated task
self.reply_controller_sender
.send_reply(recipient_tag, data, lane)
// we require topology for replies to generate surb_acks
async fn handle_reply(&mut self, reply_surb: ReplySurb, data: Vec<u8>) -> Option<RealMessage> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = match topology_permit.try_get_valid_topology_ref(&self.ack_recipient, None) {
Some(topology_ref) => topology_ref,
None => {
warn!("Could not process the message - the network topology is invalid");
return None;
}
};
match self
.message_preparer
.prepare_reply_for_use(data, reply_surb, topology, &self.ack_key)
.await
{
Ok((mix_packet, reply_id)) => {
// TODO: later probably write pending ack here
// and deal with them....
// ... somehow
Some(RealMessage::new(mix_packet, reply_id))
}
Err(err) => {
// TODO: should we have some mechanism to indicate to the user that the `reply_surb`
// could be reused since technically it wasn't used up here?
warn!("failed to deal with received reply surb - {:?}", err);
None
}
}
}
async fn handle_plain_message(
async fn handle_fresh_message(
&mut self,
recipient: Recipient,
content: Vec<u8>,
lane: TransmissionLane,
) {
if let Err(err) = self
.message_handler
.try_send_plain_message(recipient, content, lane)
.await
with_reply_surb: bool,
) -> Option<Vec<RealMessage>> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = match topology_permit
.try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient))
{
warn!("failed to send a plain message - {err}")
}
}
Some(topology_ref) => topology_ref,
None => {
warn!("Could not process the message - the network topology is invalid");
return None;
}
};
async fn handle_repliable_message(
&mut self,
recipient: Recipient,
content: Vec<u8>,
reply_surbs: u32,
lane: TransmissionLane,
) {
if let Err(err) = self
.message_handler
.try_send_message_with_reply_surbs(recipient, content, reply_surbs, lane)
.await
{
warn!("failed to send a repliable message - {err}")
// split the message, attach optional reply surb
let (split_message, reply_key) = self
.message_preparer
.prepare_and_split_message(content, with_reply_surb, topology)
.expect("somehow the topology was invalid after all!");
if let Some(reply_key) = reply_key {
self.reply_key_storage
.insert_encryption_key(reply_key)
.expect("Failed to insert surb reply key to the store!")
}
// encrypt chunks, put them inside sphinx packets and generate acks
let mut pending_acks = Vec::with_capacity(split_message.len());
let mut real_messages = Vec::with_capacity(split_message.len());
for message_chunk in split_message {
// we need to clone it because we need to keep it in memory in case we had to retransmit
// it. And then we'd need to recreate entire ACK again.
let chunk_clone = message_chunk.clone();
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient)
.unwrap();
real_messages.push(RealMessage::new(
prepared_fragment.mix_packet,
message_chunk.fragment_identifier(),
));
pending_acks.push(PendingAcknowledgement::new(
message_chunk,
prepared_fragment.total_delay,
recipient,
));
}
// tells the controller to put this into the hashmap
self.action_sender
.unbounded_send(Action::new_insert(pending_acks))
.unwrap();
Some(real_messages)
}
async fn on_input_message(&mut self, msg: InputMessage) {
match msg {
InputMessage::Regular {
let real_messages = match msg {
InputMessage::Fresh {
recipient,
data,
lane,
} => self.handle_plain_message(recipient, data, lane).await,
InputMessage::Anonymous {
recipient,
data,
reply_surbs,
lane,
with_reply_surb,
} => {
self.handle_repliable_message(recipient, data, reply_surbs, lane)
self.handle_fresh_message(recipient, data, with_reply_surb)
.await
}
InputMessage::Reply {
recipient_tag,
data,
lane,
} => {
self.handle_reply(recipient_tag, data, lane).await;
}
InputMessage::Reply { reply_surb, data } => self
.handle_reply(reply_surb, data)
.await
.map(|message| vec![message]),
};
// there's no point in trying to send nothing
if let Some(real_messages) = real_messages {
// tells real message sender (with the poisson timer) to send this to the mix network
self.real_message_sender
.unbounded_send(real_messages)
.unwrap();
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started InputMessageListener with graceful shutdown support");
while !shutdown.is_shutdown() {
pub(super) async fn run(&mut self) {
debug!("Started InputMessageListener");
while !self.shutdown.is_shutdown() {
tokio::select! {
input_msg = self.input_receiver.recv() => match input_msg {
input_msg = self.input_receiver.next() => match input_msg {
Some(input_msg) => {
self.on_input_message(input_msg).await;
},
@@ -123,12 +197,12 @@ where
break;
}
},
_ = shutdown.recv() => {
_ = self.shutdown.recv() => {
log::trace!("InputMessageListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
assert!(self.shutdown.is_shutdown_poll());
log::debug!("InputMessageListener: Exiting");
}
}
@@ -7,20 +7,17 @@ use self::{
retransmission_request_listener::RetransmissionRequestListener,
sent_notification_listener::SentNotificationListener,
};
use crate::client::inbound_messages::InputMessageReceiver;
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::replies::reply_controller::ReplyControllerSender;
use crate::spawn_future;
use action_controller::AckActionReceiver;
use super::real_traffic_stream::BatchRealMessageSender;
use crate::client::reply_key_storage::ReplyKeyStorage;
use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor};
use futures::channel::mpsc;
use gateway_client::AcknowledgementReceiver;
use log::*;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::params::PacketSize;
use nymsphinx::{
acknowledgements::AckKey,
addressing::clients::Recipient,
chunking::fragment::{Fragment, FragmentIdentifier},
preparer::MessagePreparer,
Delay as SphinxDelay,
};
use rand::{CryptoRng, Rng};
@@ -28,8 +25,8 @@ use std::{
sync::{Arc, Weak},
time::Duration,
};
pub(crate) use action_controller::{AckActionSender, Action};
use task::ShutdownListener;
use tokio::task::JoinHandle;
mod acknowledgement_listener;
mod action_controller;
@@ -51,64 +48,24 @@ pub(super) type SentPacketNotificationSender = mpsc::UnboundedSender<FragmentIde
/// that it is about to be sent to the mix network and its timeout timer should be started.
type SentPacketNotificationReceiver = mpsc::UnboundedReceiver<FragmentIdentifier>;
#[derive(Debug)]
pub(crate) enum PacketDestination {
Anonymous {
recipient_tag: AnonymousSenderTag,
// special flag to indicate whether this was an ack for requesting additional surbs,
// in that case we have to do everything we can to get it through, even if it means going
// below our stored reply surb threshold
extra_surb_request: bool,
},
KnownRecipient(Box<Recipient>),
}
/// Structure representing a data `Fragment` that is on-route to the specified `Recipient`
#[derive(Debug)]
pub(crate) struct PendingAcknowledgement {
message_chunk: Fragment,
delay: SphinxDelay,
destination: PacketDestination,
recipient: Recipient,
}
impl PendingAcknowledgement {
/// Creates new instance of `PendingAcknowledgement` using the provided data.
pub(crate) fn new_known(
message_chunk: Fragment,
delay: SphinxDelay,
recipient: Recipient,
) -> Self {
fn new(message_chunk: Fragment, delay: SphinxDelay, recipient: Recipient) -> Self {
PendingAcknowledgement {
message_chunk,
delay,
destination: PacketDestination::KnownRecipient(recipient.into()),
recipient,
}
}
pub(crate) fn new_anonymous(
message_chunk: Fragment,
delay: SphinxDelay,
recipient_tag: AnonymousSenderTag,
extra_surb_request: bool,
) -> Self {
PendingAcknowledgement {
message_chunk,
delay,
destination: PacketDestination::Anonymous {
recipient_tag,
extra_surb_request,
},
}
}
pub(crate) fn inner_fragment_identifier(&self) -> FragmentIdentifier {
self.message_chunk.fragment_identifier()
}
pub(crate) fn fragment_data(&self) -> Fragment {
self.message_chunk.clone()
}
fn update_delay(&mut self, new_delay: SphinxDelay) {
self.delay = new_delay;
}
@@ -117,6 +74,10 @@ impl PendingAcknowledgement {
/// AcknowledgementControllerConnectors represents set of channels for communication with
/// other parts of the system in order to support acknowledgements and retransmission.
pub(super) struct AcknowledgementControllerConnectors {
/// Channel used for forwarding prepared sphinx messages into the poisson sender
/// to be sent to the mix network.
real_message_sender: BatchRealMessageSender,
/// Channel used for receiving raw messages from a client. The messages need to be put
/// into sphinx packets first.
input_receiver: InputMessageReceiver,
@@ -128,28 +89,20 @@ pub(super) struct AcknowledgementControllerConnectors {
/// Channel used for receiving acknowledgements from the mix network.
ack_receiver: AcknowledgementReceiver,
/// Channel used for sending request to `ActionController` to deal with anything ack-related,
ack_action_sender: AckActionSender,
/// Channel used for receiving request by `ActionController` to deal with anything ack-related,
ack_action_receiver: AckActionReceiver,
}
impl AcknowledgementControllerConnectors {
pub(super) fn new(
real_message_sender: BatchRealMessageSender,
input_receiver: InputMessageReceiver,
sent_notifier: SentPacketNotificationReceiver,
ack_receiver: AcknowledgementReceiver,
ack_action_sender: AckActionSender,
ack_action_receiver: AckActionReceiver,
) -> Self {
AcknowledgementControllerConnectors {
real_message_sender,
input_receiver,
sent_notifier,
ack_receiver,
ack_action_sender,
ack_action_receiver,
}
}
}
@@ -162,135 +115,168 @@ pub(super) struct Config {
/// Given ack timeout in the form a * BASE_DELAY + b, it specifies the multiplier `a`
ack_wait_multiplier: f64,
/// Predefined packet size used for the encapsulated messages.
packet_size: PacketSize,
/// Average delay an acknowledgement packet is going to get delayed at a single mixnode.
average_ack_delay: Duration,
/// Average delay a data packet is going to get delayed at a single mixnode.
average_packet_delay: Duration,
}
impl Config {
pub(super) fn new(ack_wait_addition: Duration, ack_wait_multiplier: f64) -> Self {
pub(super) fn new(
ack_wait_addition: Duration,
ack_wait_multiplier: f64,
average_ack_delay: Duration,
average_packet_delay: Duration,
) -> Self {
Config {
ack_wait_addition,
ack_wait_multiplier,
packet_size: Default::default(),
average_ack_delay,
average_packet_delay,
}
}
pub fn with_custom_packet_size(mut self, packet_size: PacketSize) -> Self {
self.packet_size = packet_size;
self
}
}
pub(super) struct AcknowledgementController<R>
where
R: CryptoRng + Rng,
{
acknowledgement_listener: AcknowledgementListener,
input_message_listener: InputMessageListener<R>,
retransmission_request_listener: RetransmissionRequestListener<R>,
sent_notification_listener: SentNotificationListener,
action_controller: ActionController,
acknowledgement_listener: Option<AcknowledgementListener>,
input_message_listener: Option<InputMessageListener<R>>,
retransmission_request_listener: Option<RetransmissionRequestListener<R>>,
sent_notification_listener: Option<SentNotificationListener>,
action_controller: Option<ActionController>,
}
impl<R> AcknowledgementController<R>
where
R: 'static + CryptoRng + Rng + Clone + Send + Sync,
R: 'static + CryptoRng + Rng + Clone + Send,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
config: Config,
rng: R,
topology_access: TopologyAccessor,
ack_key: Arc<AckKey>,
ack_recipient: Recipient,
reply_key_storage: ReplyKeyStorage,
connectors: AcknowledgementControllerConnectors,
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
shutdown: ShutdownListener,
) -> Self {
let (retransmission_tx, retransmission_rx) = mpsc::unbounded();
let action_config =
action_controller::Config::new(config.ack_wait_addition, config.ack_wait_multiplier);
let action_controller = ActionController::new(
action_config,
retransmission_tx,
connectors.ack_action_receiver,
let (action_controller, action_sender) =
ActionController::new(action_config, retransmission_tx, shutdown.clone());
let message_preparer = MessagePreparer::new(
rng,
ack_recipient,
config.average_packet_delay,
config.average_ack_delay,
);
// will listen for any acks coming from the network
let acknowledgement_listener = AcknowledgementListener::new(
Arc::clone(&ack_key),
connectors.ack_receiver,
connectors.ack_action_sender.clone(),
action_sender.clone(),
shutdown.clone(),
);
// will listen for any new messages from the client
let input_message_listener = InputMessageListener::new(
Arc::clone(&ack_key),
ack_recipient,
connectors.input_receiver,
message_handler.clone(),
reply_controller_sender.clone(),
message_preparer.clone(),
action_sender.clone(),
connectors.real_message_sender.clone(),
topology_access.clone(),
reply_key_storage,
shutdown.clone(),
);
// will listen for any ack timeouts and trigger retransmission
let retransmission_request_listener = RetransmissionRequestListener::new(
connectors.ack_action_sender.clone(),
message_handler,
Arc::clone(&ack_key),
ack_recipient,
message_preparer,
action_sender.clone(),
connectors.real_message_sender,
retransmission_rx,
reply_controller_sender,
topology_access,
shutdown.clone(),
);
// will listen for events indicating the packet was sent through the network so that
// the retransmission timer should be started.
let sent_notification_listener =
SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender);
SentNotificationListener::new(connectors.sent_notifier, action_sender, shutdown);
AcknowledgementController {
acknowledgement_listener,
input_message_listener,
retransmission_request_listener,
sent_notification_listener,
action_controller,
acknowledgement_listener: Some(acknowledgement_listener),
input_message_listener: Some(input_message_listener),
retransmission_request_listener: Some(retransmission_request_listener),
sent_notification_listener: Some(sent_notification_listener),
action_controller: Some(action_controller),
}
}
pub(super) fn start_with_shutdown(self, shutdown: task::ShutdownListener) {
let mut acknowledgement_listener = self.acknowledgement_listener;
let mut input_message_listener = self.input_message_listener;
let mut retransmission_request_listener = self.retransmission_request_listener;
let mut sent_notification_listener = self.sent_notification_listener;
let mut action_controller = self.action_controller;
pub(super) async fn run(&mut self) {
let mut acknowledgement_listener = self.acknowledgement_listener.take().unwrap();
let mut input_message_listener = self.input_message_listener.take().unwrap();
let mut retransmission_request_listener =
self.retransmission_request_listener.take().unwrap();
let mut sent_notification_listener = self.sent_notification_listener.take().unwrap();
let mut action_controller = self.action_controller.take().unwrap();
let shutdown_handle = shutdown.clone();
spawn_future(async move {
acknowledgement_listener
.run_with_shutdown(shutdown_handle)
.await;
// the below are log messages are errors as at the current stage we do not expect any of
// the task to ever finish. This will of course change once we introduce
// graceful shutdowns.
let ack_listener_fut = tokio::spawn(async move {
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
acknowledgement_listener
});
let shutdown_handle = shutdown.clone();
spawn_future(async move {
input_message_listener
.run_with_shutdown(shutdown_handle)
.await;
let input_listener_fut = tokio::spawn(async move {
input_message_listener.run().await;
debug!("The input listener has finished execution!");
input_message_listener
});
let shutdown_handle = shutdown.clone();
spawn_future(async move {
retransmission_request_listener
.run_with_shutdown(shutdown_handle)
.await;
let retransmission_req_fut = tokio::spawn(async move {
retransmission_request_listener.run().await;
debug!("The retransmission request listener has finished execution!");
retransmission_request_listener
});
let shutdown_handle = shutdown.clone();
spawn_future(async move {
sent_notification_listener
.run_with_shutdown(shutdown_handle)
.await;
let sent_notification_fut = tokio::spawn(async move {
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
sent_notification_listener
});
let action_controller_fut = tokio::spawn(async move {
action_controller.run().await;
debug!("The controller has finished execution!");
action_controller
});
spawn_future(async move {
action_controller.run_with_shutdown(shutdown).await;
debug!("The controller has finished execution!");
});
// technically we don't have to bring `AcknowledgementController` back to a valid state
// but we can do it, so why not? Perhaps it might be useful if we wanted to allow
// for restarts of certain modules without killing the entire process.
self.acknowledgement_listener = Some(ack_listener_fut.await.unwrap());
self.input_message_listener = Some(input_listener_fut.await.unwrap());
self.retransmission_request_listener = Some(retransmission_req_fut.await.unwrap());
self.sent_notification_listener = Some(sent_notification_fut.await.unwrap());
self.action_controller = Some(action_controller_fut.await.unwrap());
}
#[allow(dead_code)]
pub(super) fn start(mut self) -> JoinHandle<Self> {
tokio::spawn(async move {
self.run().await;
self
})
}
}
@@ -1,101 +1,82 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{
action_controller::{AckActionSender, Action},
PendingAcknowledgement, RetransmissionRequestReceiver,
use super::action_controller::{Action, ActionSender};
use super::PendingAcknowledgement;
use super::RetransmissionRequestReceiver;
use crate::client::{
real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage},
topology_control::TopologyAccessor,
};
use crate::client::real_messages_control::acknowledgement_control::PacketDestination;
use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError};
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender;
use client_connections::TransmissionLane;
use futures::StreamExt;
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::chunking::fragment::Fragment;
use nymsphinx::preparer::PreparedFragment;
use nymsphinx::preparer::MessagePreparer;
use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient};
use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak};
use task::ShutdownListener;
// responsible for packet retransmission upon fired timer
pub(super) struct RetransmissionRequestListener<R> {
action_sender: AckActionSender,
message_handler: MessageHandler<R>,
pub(super) struct RetransmissionRequestListener<R>
where
R: CryptoRng + Rng,
{
ack_key: Arc<AckKey>,
ack_recipient: Recipient,
message_preparer: MessagePreparer<R>,
action_sender: ActionSender,
real_message_sender: BatchRealMessageSender,
request_receiver: RetransmissionRequestReceiver,
reply_controller_sender: ReplyControllerSender,
topology_access: TopologyAccessor,
shutdown: ShutdownListener,
}
impl<R> RetransmissionRequestListener<R>
where
R: CryptoRng + Rng,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
action_sender: AckActionSender,
message_handler: MessageHandler<R>,
ack_key: Arc<AckKey>,
ack_recipient: Recipient,
message_preparer: MessagePreparer<R>,
action_sender: ActionSender,
real_message_sender: BatchRealMessageSender,
request_receiver: RetransmissionRequestReceiver,
reply_controller_sender: ReplyControllerSender,
topology_access: TopologyAccessor,
shutdown: ShutdownListener,
) -> Self {
RetransmissionRequestListener {
ack_key,
ack_recipient,
message_preparer,
action_sender,
message_handler,
real_message_sender,
request_receiver,
reply_controller_sender,
topology_access,
shutdown,
}
}
async fn prepare_normal_retransmission_chunk(
&mut self,
packet_recipient: Recipient,
chunk_data: Fragment,
) -> Result<PreparedFragment, PreparationError> {
debug!("retransmitting normal packet...");
self.message_handler
.try_prepare_single_chunk_for_sending(packet_recipient, chunk_data)
.await
}
async fn on_retransmission_request(
&mut self,
weak_timed_out_ack: Weak<PendingAcknowledgement>,
) {
let timed_out_ack = match weak_timed_out_ack.upgrade() {
async fn on_retransmission_request(&mut self, timed_out_ack: Weak<PendingAcknowledgement>) {
let timed_out_ack = match timed_out_ack.upgrade() {
Some(timed_out_ack) => timed_out_ack,
None => {
debug!("We received an ack JUST as we were about to retransmit [1]");
return;
}
};
let packet_recipient = &timed_out_ack.recipient;
let chunk_clone = timed_out_ack.message_chunk.clone();
let frag_id = chunk_clone.fragment_identifier();
let maybe_prepared_fragment = match &timed_out_ack.destination {
PacketDestination::Anonymous {
recipient_tag,
extra_surb_request,
} => {
// if this is retransmission for reply, offload it to the dedicated task
// that deals with all the surbs
return self.reply_controller_sender.send_retransmission_data(
*recipient_tag,
weak_timed_out_ack,
*extra_surb_request,
);
}
PacketDestination::KnownRecipient(recipient) => {
self.prepare_normal_retransmission_chunk(
**recipient,
timed_out_ack.message_chunk.clone(),
)
.await
}
};
let frag_id = timed_out_ack.message_chunk.fragment_identifier();
let prepared_fragment = match maybe_prepared_fragment {
Ok(prepared_fragment) => prepared_fragment,
Err(err) => {
warn!("Could not retransmit the packet - {err}");
let topology_permit = self.topology_access.get_read_permit().await;
let topology_ref = match topology_permit
.try_get_valid_topology_ref(&self.ack_recipient, Some(packet_recipient))
{
Some(topology_ref) => topology_ref,
None => {
warn!("Could not retransmit the packet - the network topology is invalid");
// we NEED to start timer here otherwise we will have this guy permanently stuck in memory
self.action_sender
.unbounded_send(Action::new_start_timer(frag_id))
@@ -104,6 +85,11 @@ where
}
};
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient)
.unwrap();
// if we have the ONLY strong reference to the ack data, it means it was removed from the
// pending acks
if Arc::strong_count(&timed_out_ack) == 1 {
@@ -115,6 +101,7 @@ where
// we no longer need the reference - let's drop it so that if somehow `UpdateTimer` action
// reached the controller before this function terminated, the controller would not panic.
drop(timed_out_ack);
let new_delay = prepared_fragment.total_delay;
// We know this update will be reflected by the `StartTimer` Action performed when this
@@ -129,18 +116,18 @@ where
.unwrap();
// send to `OutQueueControl` to eventually send to the mix network
self.message_handler
.forward_messages(
vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)],
TransmissionLane::Retransmission,
)
.await
self.real_message_sender
.unbounded_send(vec![RealMessage::new(
prepared_fragment.mix_packet,
frag_id,
)])
.unwrap();
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started RetransmissionRequestListener with graceful shutdown support");
pub(super) async fn run(&mut self) {
debug!("Started RetransmissionRequestListener");
while !shutdown.is_shutdown() {
while !self.shutdown.is_shutdown() {
tokio::select! {
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack).await,
@@ -149,12 +136,12 @@ where
break;
}
},
_ = shutdown.recv() => {
_ = self.shutdown.recv() => {
log::trace!("RetransmissionRequestListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
assert!(self.shutdown.is_shutdown_poll());
log::debug!("RetransmissionRequestListener: Exiting");
}
}
@@ -1,11 +1,12 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use super::action_controller::{Action, ActionSender};
use super::SentPacketNotificationReceiver;
use futures::StreamExt;
use log::*;
use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
use task::ShutdownListener;
/// Module responsible for starting up retransmission timers.
/// It is required because when we send our packet to the `real traffic stream` controlled
@@ -13,17 +14,20 @@ use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
/// accidentally fire retransmission way quicker than we should have.
pub(super) struct SentNotificationListener {
sent_notifier: SentPacketNotificationReceiver,
action_sender: AckActionSender,
action_sender: ActionSender,
shutdown: ShutdownListener,
}
impl SentNotificationListener {
pub(super) fn new(
sent_notifier: SentPacketNotificationReceiver,
action_sender: AckActionSender,
action_sender: ActionSender,
shutdown: ShutdownListener,
) -> Self {
SentNotificationListener {
sent_notifier,
action_sender,
shutdown,
}
}
@@ -31,16 +35,20 @@ impl SentNotificationListener {
if frag_id == COVER_FRAG_ID {
trace!("sent off a cover message - no need to start retransmission timer!");
return;
} else if frag_id.is_reply() {
debug!("sent off a reply message - no need to start retransmission timer!");
// TODO: probably there will need to be some extra procedure here, like it would
// be nice to know that our reply actually reached the recipient (i.e. we got the ack)
return;
}
self.action_sender
.unbounded_send(Action::new_start_timer(frag_id))
.unwrap();
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started SentNotificationListener with graceful shutdown support");
while !shutdown.is_shutdown() {
pub(super) async fn run(&mut self) {
debug!("Started SentNotificationListener");
while !self.shutdown.is_shutdown() {
tokio::select! {
frag_id = self.sent_notifier.next() => match frag_id {
Some(frag_id) => {
@@ -51,12 +59,12 @@ impl SentNotificationListener {
break;
}
},
_ = shutdown.recv() => {
_ = self.shutdown.recv() => {
log::trace!("SentNotificationListener: Received shutdown");
}
}
}
assert!(shutdown.is_shutdown_poll());
assert!(self.shutdown.is_shutdown_poll());
log::debug!("SentNotificationListener: Exiting");
}
}
@@ -1,548 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use crate::client::real_messages_control::real_traffic_stream::{
BatchRealMessageSender, RealMessage,
};
use crate::client::real_messages_control::{AckActionSender, Action};
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use client_connections::TransmissionLane;
use log::{debug, error, info, trace, warn};
use nymsphinx::acknowledgements::AckKey;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nymsphinx::message::NymMessage;
use nymsphinx::params::{PacketSize, DEFAULT_NUM_MIX_HOPS};
use nymsphinx::preparer::{MessagePreparer, PreparedFragment};
use nymsphinx::Delay;
use rand::{CryptoRng, Rng};
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use topology::{NymTopology, NymTopologyError};
// TODO: move that error elsewhere since it seems to be contaminating different files
#[derive(Debug, Clone, Error)]
pub enum PreparationError {
#[error(transparent)]
NymTopologyError(#[from] NymTopologyError),
#[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")]
MessageTooLongForSingleSurb { fragments: usize },
#[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")]
NotEnoughSurbs { available: usize, required: usize },
}
impl PreparationError {
fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
SurbWrappedPreparationError {
source: self,
returned_surbs: Some(returned_surbs),
}
}
}
#[derive(Debug, Error)]
#[error("Failed to prepare packets - {source}. {} reply surbs will be returned", .returned_surbs.as_ref().map(|s| s.len()).unwrap_or_default())]
pub struct SurbWrappedPreparationError {
#[source]
source: PreparationError,
returned_surbs: Option<Vec<ReplySurb>>,
}
impl<T> From<T> for SurbWrappedPreparationError
where
T: Into<PreparationError>,
{
fn from(err: T) -> Self {
SurbWrappedPreparationError {
source: err.into(),
returned_surbs: None,
}
}
}
impl SurbWrappedPreparationError {
pub(crate) fn return_unused_surbs(
self,
surb_storage: &ReceivedReplySurbsMap,
target: &AnonymousSenderTag,
) -> PreparationError {
if let Some(reply_surbs) = self.returned_surbs {
surb_storage.insert_surbs(target, reply_surbs)
}
self.source
}
}
#[derive(Clone)]
pub(crate) struct Config {
/// Key used to decrypt contents of received SURBAcks
ack_key: Arc<AckKey>,
/// Address of this client which also represent an address to which all acknowledgements
/// and surb-based are going to be sent.
sender_address: Recipient,
/// Average delay a data packet is going to get delay at a single mixnode.
average_packet_delay: Duration,
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
average_ack_delay: Duration,
/// Number of mix hops each packet ('real' message, ack, reply) is expected to take.
/// Note that it does not include gateway hops.
num_mix_hops: u8,
/// Predefined packet size used for the encapsulated messages.
packet_size: PacketSize,
}
impl Config {
pub fn new(
ack_key: Arc<AckKey>,
sender_address: Recipient,
average_packet_delay: Duration,
average_ack_delay: Duration,
) -> Self {
Config {
ack_key,
sender_address,
average_packet_delay,
average_ack_delay,
num_mix_hops: DEFAULT_NUM_MIX_HOPS,
packet_size: PacketSize::default(),
}
}
/// Allows setting non-default number of expected mix hops in the network.
#[allow(dead_code)]
pub fn with_mix_hops(mut self, hops: u8) -> Self {
self.num_mix_hops = hops;
self
}
/// Allows setting non-default size of the sphinx packets sent out.
pub fn with_custom_packet_size(mut self, packet_size: PacketSize) -> Self {
self.packet_size = packet_size;
self
}
}
#[derive(Clone)]
pub(crate) struct MessageHandler<R> {
config: Config,
rng: R,
message_preparer: MessagePreparer<R>,
action_sender: AckActionSender,
real_message_sender: BatchRealMessageSender,
topology_access: TopologyAccessor,
reply_key_storage: SentReplyKeys,
tag_storage: UsedSenderTags,
}
impl<R> MessageHandler<R>
where
R: CryptoRng + Rng,
{
pub(crate) fn new(
config: Config,
rng: R,
action_sender: AckActionSender,
real_message_sender: BatchRealMessageSender,
topology_access: TopologyAccessor,
reply_key_storage: SentReplyKeys,
tag_storage: UsedSenderTags,
) -> Self
where
R: Copy,
{
let message_preparer = MessagePreparer::new(
rng,
config.sender_address,
config.average_packet_delay,
config.average_ack_delay,
)
.with_custom_real_message_packet_size(config.packet_size)
.with_mix_hops(config.num_mix_hops);
MessageHandler {
config,
rng,
message_preparer,
action_sender,
real_message_sender,
topology_access,
reply_key_storage,
tag_storage,
}
}
fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag {
if let Some(existing) = self.tag_storage.try_get_existing(recipient) {
trace!("we already had sender tag for {recipient}");
existing
} else {
info!("creating new sender tag for {recipient}");
let new_tag = AnonymousSenderTag::new_random(&mut self.rng);
self.tag_storage.insert_new(recipient, new_tag);
info!("we'll be using {new_tag} for all anonymous messages sent to {recipient}");
new_tag
}
}
fn get_topology<'a>(
&self,
permit: &'a TopologyReadPermit<'a>,
) -> Result<&'a NymTopology, PreparationError> {
match permit.try_get_valid_topology_ref(&self.config.sender_address, None) {
Ok(topology_ref) => Ok(topology_ref),
Err(err) => {
warn!("Could not process the packet - the network topology is invalid - {err}");
Err(err.into())
}
}
}
async fn generate_reply_surbs_with_keys(
&mut self,
amount: usize,
) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;
let reply_surbs = self
.message_preparer
.generate_reply_surbs(amount, topology)?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
Ok((reply_surbs, reply_keys))
}
pub(crate) async fn try_send_single_surb_message(
&mut self,
target: AnonymousSenderTag,
message: ReplyMessage,
reply_surb: ReplySurb,
is_extra_surb_request: bool,
) -> Result<(), SurbWrappedPreparationError> {
let mut fragment = self
.message_preparer
.pad_and_split_message(NymMessage::new_reply(message));
if fragment.len() > 1 {
// well, it's not a single surb message
return Err(SurbWrappedPreparationError {
source: PreparationError::MessageTooLongForSingleSurb {
fragments: fragment.len(),
},
returned_surbs: Some(vec![reply_surb]),
});
}
let chunk = fragment.pop().unwrap();
let chunk_clone = chunk.clone();
let prepared_fragment = self
.try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone)
.await?;
let real_messages =
RealMessage::new(prepared_fragment.mix_packet, chunk.fragment_identifier());
let delay = prepared_fragment.total_delay;
let pending_ack =
PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request);
let lane = if is_extra_surb_request {
TransmissionLane::ReplySurbRequest
} else {
TransmissionLane::General
};
self.forward_messages(vec![real_messages], lane).await;
self.insert_pending_acks(vec![pending_ack]);
Ok(())
}
pub(crate) async fn try_request_additional_reply_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surb: ReplySurb,
amount: u32,
) -> Result<(), SurbWrappedPreparationError> {
debug!("requesting {amount} reply SURBs from {from:?}");
let surbs_request =
ReplyMessage::new_surb_request_message(self.config.sender_address, amount);
self.try_send_single_surb_message(from, surbs_request, reply_surb, true)
.await
}
// // TODO: this will require additional argument to make it use different variant of `ReplyMessage`
pub(crate) fn split_reply_message(&mut self, message: Vec<u8>) -> Vec<Fragment> {
self.message_preparer
.pad_and_split_message(NymMessage::new_reply(ReplyMessage::new_data_message(
message,
)))
}
// the only difference between this method and `try_send_reply_chunks` is that
// here we are not creating acks as acks are already in memory waiting to get cleared.
// we are only updating their existing delays
pub(crate) async fn try_send_retransmission_reply_chunks(
&mut self,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurb>,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self
.prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs)
.await?;
let mut real_messages = Vec::with_capacity(prepared_fragments.len());
for prepared in prepared_fragments {
self.update_ack_delay(prepared.fragment_identifier, prepared.total_delay);
real_messages.push(prepared.into())
}
self.forward_messages(real_messages, lane).await;
Ok(())
}
pub(crate) async fn try_send_reply_chunks(
&mut self,
target: AnonymousSenderTag,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurb>,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self
.prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs)
.await?;
let mut pending_acks = Vec::with_capacity(fragments.len());
let mut real_messages = Vec::with_capacity(fragments.len());
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) {
let real_message = RealMessage::new(prepared.mix_packet, prepared.fragment_identifier);
let delay = prepared.total_delay;
let pending_ack = PendingAcknowledgement::new_anonymous(raw, delay, target, false);
real_messages.push(real_message);
pending_acks.push(pending_ack);
}
self.forward_messages(real_messages, lane).await;
self.insert_pending_acks(pending_acks);
Ok(())
}
pub(crate) async fn try_send_plain_message(
&mut self,
recipient: Recipient,
message: Vec<u8>,
lane: TransmissionLane,
) -> Result<(), PreparationError> {
let message = NymMessage::new_plain(message);
self.try_split_and_send_non_reply_message(message, recipient, lane)
.await
}
pub(crate) async fn try_split_and_send_non_reply_message(
&mut self,
message: NymMessage,
recipient: Recipient,
lane: TransmissionLane,
) -> Result<(), PreparationError> {
// TODO: I really dislike existence of this assertion, it implies code has to be re-organised
debug_assert!(!matches!(message, NymMessage::Reply(_)));
// TODO2: it's really annoying we have to get topology permit again here due to borrow-checker
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;
let fragments = self.message_preparer.pad_and_split_message(message);
let mut pending_acks = Vec::with_capacity(fragments.len());
let mut real_messages = Vec::with_capacity(fragments.len());
for fragment in fragments {
// we need to clone it because we need to keep it in memory in case we had to retransmit
// it. And then we'd need to recreate entire ACK again.
let chunk_clone = fragment.clone();
let prepared_fragment = self.message_preparer.prepare_chunk_for_sending(
chunk_clone,
topology,
&self.config.ack_key,
&recipient,
)?;
let real_message =
RealMessage::new(prepared_fragment.mix_packet, fragment.fragment_identifier());
let delay = prepared_fragment.total_delay;
let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient);
real_messages.push(real_message);
pending_acks.push(pending_ack);
}
self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await;
Ok(())
}
pub(crate) async fn try_send_additional_reply_surbs(
&mut self,
recipient: Recipient,
amount: u32,
) -> Result<(), PreparationError> {
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) =
self.generate_reply_surbs_with_keys(amount as usize).await?;
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
sender_tag,
reply_surbs,
));
self.try_split_and_send_non_reply_message(
message,
recipient,
TransmissionLane::AdditionalReplySurbs,
)
.await?;
log::trace!("storing {} reply keys", reply_keys.len());
self.reply_key_storage.insert_multiple(reply_keys);
Ok(())
}
pub(crate) async fn try_send_message_with_reply_surbs(
&mut self,
recipient: Recipient,
message: Vec<u8>,
num_reply_surbs: u32,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) = self
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
.await?;
let message =
NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs));
self.try_split_and_send_non_reply_message(message, recipient, lane)
.await?;
log::trace!("storing {} reply keys", reply_keys.len());
self.reply_key_storage.insert_multiple(reply_keys);
Ok(())
}
pub(crate) async fn try_prepare_single_chunk_for_sending(
&mut self,
recipient: Recipient,
chunk: Fragment,
) -> Result<PreparedFragment, PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending(chunk, topology, &self.config.ack_key, &recipient)
.unwrap();
Ok(prepared_fragment)
}
async fn prepare_reply_chunks_for_sending(
&mut self,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurb>,
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
debug_assert_ne!(
fragments.len(),
reply_surbs.len(),
"attempted to send {} fragments with {} reply surbs",
fragments.len(),
reply_surbs.len()
);
let topology_permit = self.topology_access.get_read_permit().await;
let topology = match self.get_topology(&topology_permit) {
Ok(topology) => topology,
Err(err) => return Err(err.return_surbs(reply_surbs)),
};
Ok(fragments
.into_iter()
.zip(reply_surbs.into_iter())
.map(|(fragment, reply_surb)| {
// unwrap here is fine as we know we have a valid topology
self.message_preparer
.prepare_reply_chunk_for_sending(
fragment,
topology,
&self.config.ack_key,
reply_surb,
)
.unwrap()
})
.collect())
}
pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
&mut self,
reply_surb: ReplySurb,
chunk: Fragment,
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = match self.get_topology(&topology_permit) {
Ok(topology) => topology,
Err(err) => return Err(err.return_surbs(vec![reply_surb])),
};
let prepared_fragment = self
.message_preparer
.prepare_reply_chunk_for_sending(chunk, topology, &self.config.ack_key, reply_surb)
.unwrap();
Ok(prepared_fragment)
}
pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) {
self.action_sender
.unbounded_send(Action::UpdateDelay(id, new_delay))
.expect("action control task has died")
}
pub(crate) fn insert_pending_acks(&self, pending_acks: Vec<PendingAcknowledgement>) {
self.action_sender
.unbounded_send(Action::new_insert(pending_acks))
.expect("action control task has died")
}
// tells real message sender (with the poisson timer) to send this to the mix network
pub(crate) async fn forward_messages(
&self,
messages: Vec<RealMessage>,
transmission_lane: TransmissionLane,
) {
self.real_message_sender
.send((messages, transmission_lane))
.await
.expect("real message receiver task (OutQueueControl) has died");
}
}
@@ -8,37 +8,25 @@
use self::{
acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl,
};
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::replies::reply_controller::{
ReplyController, ReplyControllerReceiver, ReplyControllerSender,
use crate::client::real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors;
use crate::client::reply_key_storage::ReplyKeyStorage;
use crate::client::{
inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender,
topology_control::TopologyAccessor,
};
use crate::client::replies::reply_storage::CombinedReplyStorage;
use crate::{
client::{
inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender,
real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors,
topology_control::TopologyAccessor,
},
spawn_future,
};
use client_connections::{ConnectionCommandReceiver, LaneQueueLengths};
use futures::channel::mpsc;
use gateway_client::AcknowledgementReceiver;
use log::*;
use nymsphinx::acknowledgements::AckKey;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::params::PacketSize;
use rand::{rngs::OsRng, CryptoRng, Rng};
use std::sync::Arc;
use std::time::Duration;
use task::ShutdownListener;
use tokio::task::JoinHandle;
use crate::client::replies::reply_controller;
use crate::config;
pub(crate) use acknowledgement_control::{AckActionSender, Action};
pub(crate) mod acknowledgement_control;
pub(crate) mod message_handler;
pub(crate) mod real_traffic_stream;
mod acknowledgement_control;
mod real_traffic_stream;
// TODO: ack_key and self_recipient shouldn't really be part of this config
pub struct Config {
@@ -62,223 +50,133 @@ pub struct Config {
/// Average delay an acknowledgement packet is going to get delayed at a single mixnode.
average_ack_delay_duration: Duration,
/// Controls whether the main packet stream constantly produces packets according to the predefined
/// poisson distribution.
disable_main_poisson_packet_distribution: bool,
/// Predefined packet size used for the encapsulated messages.
packet_size: PacketSize,
/// Defines the minimum number of reply surbs the client would request.
minimum_reply_surb_request_size: u32,
/// Defines the maximum number of reply surbs the client would request.
maximum_reply_surb_request_size: u32,
/// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once.
maximum_allowed_reply_surb_request_size: u32,
/// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking
/// for more even though in theory they wouldn't need to.
maximum_reply_surb_waiting_period: Duration,
/// Defines maximum amount of time given reply surb is going to be valid for.
/// This is going to be superseded by key rotation once implemented.
maximum_reply_surb_age: Duration,
/// Defines maximum amount of time given reply key is going to be valid for.
/// This is going to be superseded by key rotation once implemented.
maximum_reply_key_age: Duration,
}
impl<'a> From<&'a Config> for acknowledgement_control::Config {
fn from(cfg: &'a Config) -> Self {
acknowledgement_control::Config::new(cfg.ack_wait_addition, cfg.ack_wait_multiplier)
.with_custom_packet_size(cfg.packet_size)
}
}
impl<'a> From<&'a Config> for real_traffic_stream::Config {
fn from(cfg: &'a Config) -> Self {
real_traffic_stream::Config::new(
Arc::clone(&cfg.ack_key),
cfg.self_recipient,
cfg.average_ack_delay_duration,
cfg.average_packet_delay_duration,
cfg.average_message_sending_delay,
cfg.disable_main_poisson_packet_distribution,
)
.with_custom_cover_packet_size(cfg.packet_size)
}
}
impl<'a> From<&'a Config> for reply_controller::Config {
fn from(cfg: &'a Config) -> Self {
reply_controller::Config::new(
cfg.minimum_reply_surb_request_size,
cfg.maximum_reply_surb_request_size,
cfg.maximum_allowed_reply_surb_request_size,
cfg.maximum_reply_surb_waiting_period,
cfg.maximum_reply_surb_age,
cfg.maximum_reply_key_age,
)
}
}
impl<'a> From<&'a Config> for message_handler::Config {
fn from(cfg: &'a Config) -> Self {
message_handler::Config::new(
Arc::clone(&cfg.ack_key),
cfg.self_recipient,
cfg.average_packet_delay_duration,
cfg.average_ack_delay_duration,
)
.with_custom_packet_size(cfg.packet_size)
}
}
impl Config {
pub fn new(
base_client_debug_config: &config::DebugConfig,
ack_key: Arc<AckKey>,
ack_wait_multiplier: f64,
ack_wait_addition: Duration,
average_ack_delay_duration: Duration,
average_message_sending_delay: Duration,
average_packet_delay_duration: Duration,
self_recipient: Recipient,
) -> Self {
Config {
ack_key,
ack_wait_addition,
ack_wait_multiplier,
self_recipient,
packet_size: Default::default(),
ack_wait_addition: base_client_debug_config.ack_wait_addition,
ack_wait_multiplier: base_client_debug_config.ack_wait_multiplier,
average_message_sending_delay: base_client_debug_config.message_sending_average_delay,
average_packet_delay_duration: base_client_debug_config.average_packet_delay,
average_ack_delay_duration: base_client_debug_config.average_ack_delay,
disable_main_poisson_packet_distribution: base_client_debug_config
.disable_main_poisson_packet_distribution,
minimum_reply_surb_request_size: base_client_debug_config
.minimum_reply_surb_request_size,
maximum_reply_surb_request_size: base_client_debug_config
.maximum_reply_surb_request_size,
maximum_allowed_reply_surb_request_size: base_client_debug_config
.maximum_allowed_reply_surb_request_size,
maximum_reply_surb_waiting_period: base_client_debug_config
.maximum_reply_surb_waiting_period,
maximum_reply_surb_age: base_client_debug_config.maximum_reply_surb_age,
maximum_reply_key_age: base_client_debug_config.maximum_reply_key_age,
average_message_sending_delay,
average_packet_delay_duration,
average_ack_delay_duration,
}
}
pub fn set_custom_packet_size(&mut self, packet_size: PacketSize) {
self.packet_size = packet_size;
}
}
pub(crate) struct RealMessagesController<R>
pub struct RealMessagesController<R>
where
R: CryptoRng + Rng,
{
out_queue_control: OutQueueControl<R>,
ack_control: AcknowledgementController<R>,
reply_control: ReplyController<R>,
out_queue_control: Option<OutQueueControl<R>>,
ack_control: Option<AcknowledgementController<R>>,
}
// obviously when we finally make shared rng that is on 'higher' level, this should become
// generic `R`
impl RealMessagesController<OsRng> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
pub fn new(
config: Config,
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
topology_access: TopologyAccessor,
reply_storage: CombinedReplyStorage,
// so much refactoring needed, but this is temporary just to test things out
reply_controller_sender: ReplyControllerSender,
reply_controller_receiver: ReplyControllerReceiver,
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
) -> Self {
let rng = OsRng;
// create channels for inter-task communication
let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1);
let (real_message_sender, real_message_receiver) = mpsc::unbounded();
let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded();
let (ack_action_tx, ack_action_rx) = mpsc::unbounded();
let ack_controller_connectors = AcknowledgementControllerConnectors::new(
real_message_sender,
input_receiver,
sent_notifier_rx,
ack_receiver,
ack_action_tx.clone(),
ack_action_rx,
);
// create all configs for the components
let ack_control_config = (&config).into();
let out_queue_config = (&config).into();
let reply_controller_config = (&config).into();
let message_handler_config = (&config).into();
// create the actual components
let message_handler = MessageHandler::new(
message_handler_config,
rng,
ack_action_tx,
real_message_sender,
topology_access.clone(),
reply_storage.key_storage(),
reply_storage.tags_storage(),
let ack_control_config = acknowledgement_control::Config::new(
config.ack_wait_addition,
config.ack_wait_multiplier,
config.average_ack_delay_duration,
config.average_packet_delay_duration,
);
let ack_control = AcknowledgementController::new(
ack_control_config,
rng,
topology_access.clone(),
Arc::clone(&config.ack_key),
config.self_recipient,
reply_key_storage,
ack_controller_connectors,
message_handler.clone(),
reply_controller_sender,
shutdown.clone(),
);
let reply_control = ReplyController::new(
reply_controller_config,
message_handler,
reply_storage,
reply_controller_receiver,
let out_queue_config = real_traffic_stream::Config::new(
config.average_ack_delay_duration,
config.average_packet_delay_duration,
config.average_message_sending_delay,
);
let out_queue_control = OutQueueControl::new(
out_queue_config,
rng,
Arc::clone(&config.ack_key),
sent_notifier_tx,
mix_sender,
real_message_receiver,
rng,
config.self_recipient,
topology_access,
lane_queue_lengths,
client_connection_rx,
shutdown,
);
RealMessagesController {
out_queue_control,
ack_control,
reply_control,
out_queue_control: Some(out_queue_control),
ack_control: Some(ack_control),
}
}
pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) {
let mut out_queue_control = self.out_queue_control;
let ack_control = self.ack_control;
let mut reply_control = self.reply_control;
pub(super) async fn run(&mut self) {
let mut out_queue_control = self.out_queue_control.take().unwrap();
let mut ack_control = self.ack_control.take().unwrap();
let shutdown_handle = shutdown.clone();
spawn_future(async move {
out_queue_control.run_with_shutdown(shutdown_handle).await;
// the below are log messages are errors as at the current stage we do not expect any of
// the task to ever finish. This will of course change once we introduce
// graceful shutdowns.
let out_queue_control_fut = tokio::spawn(async move {
out_queue_control.run_out_queue_control().await;
debug!("The out queue controller has finished execution!");
out_queue_control
});
let shutdown_handle = shutdown.clone();
spawn_future(async move {
reply_control.run_with_shutdown(shutdown_handle).await;
debug!("The reply controller has finished execution!");
let ack_control_fut = tokio::spawn(async move {
ack_control.run().await;
debug!("The acknowledgement controller has finished execution!");
ack_control
});
ack_control.start_with_shutdown(shutdown);
// technically we don't have to bring `RealMessagesController` back to a valid state
// but we can do it, so why not? Perhaps it might be useful if we wanted to allow
// for restarts of certain modules without killing the entire process.
self.out_queue_control = Some(out_queue_control_fut.await.unwrap());
self.ack_control = Some(ack_control_fut.await.unwrap());
}
pub fn start(mut self) -> JoinHandle<Self> {
tokio::spawn(async move {
self.run().await;
self
})
}
}
@@ -4,9 +4,7 @@
use crate::client::mix_traffic::BatchMixMessageSender;
use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender;
use crate::client::topology_control::TopologyAccessor;
use client_connections::{
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt};
use log::*;
@@ -15,45 +13,17 @@ use nymsphinx::addressing::clients::Recipient;
use nymsphinx::chunking::fragment::FragmentIdentifier;
use nymsphinx::cover::generate_loop_cover_packet;
use nymsphinx::forwarding::packet::MixPacket;
use nymsphinx::params::PacketSize;
use nymsphinx::preparer::PreparedFragment;
use nymsphinx::utils::sample_poisson_duration;
use rand::{CryptoRng, Rng};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use task::ShutdownListener;
use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
use self::{
sending_delay_controller::SendingDelayController, transmission_buffer::TransmissionBuffer,
};
mod sending_delay_controller;
mod transmission_buffer;
#[cfg(not(target_arch = "wasm32"))]
fn get_time_now() -> time::Instant {
time::Instant::now()
}
#[cfg(target_arch = "wasm32")]
fn get_time_now() -> wasm_timer::Instant {
wasm_timer::Instant::now()
}
/// Configurable parameters of the `OutQueueControl`
pub(crate) struct Config {
/// Key used to encrypt and decrypt content of an ACK packet.
ack_key: Arc<AckKey>,
/// Represents full address of this client.
our_full_destination: Recipient,
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
average_ack_delay: Duration,
@@ -62,39 +32,20 @@ pub(crate) struct Config {
/// Average delay between sending subsequent packets.
average_message_sending_delay: Duration,
/// Controls whether the stream constantly produces packets according to the predefined
/// poisson distribution.
disable_poisson_packet_distribution: bool,
/// Predefined packet size used for the loop cover messages.
cover_packet_size: PacketSize,
}
impl Config {
pub(crate) fn new(
ack_key: Arc<AckKey>,
our_full_destination: Recipient,
average_ack_delay: Duration,
average_packet_delay: Duration,
average_message_sending_delay: Duration,
disable_poisson_packet_distribution: bool,
) -> Self {
Config {
ack_key,
our_full_destination,
average_ack_delay,
average_packet_delay,
average_message_sending_delay,
disable_poisson_packet_distribution,
cover_packet_size: Default::default(),
}
}
pub fn with_custom_cover_packet_size(mut self, packet_size: PacketSize) -> Self {
self.cover_packet_size = packet_size;
self
}
}
pub(crate) struct OutQueueControl<R>
@@ -104,20 +55,15 @@ where
/// Configurable parameters of the `ActionController`
config: Config,
/// Key used to encrypt and decrypt content of an ACK packet.
ack_key: Arc<AckKey>,
/// Channel used for notifying of a real packet being sent out. Used to start up retransmission timer.
sent_notifier: SentPacketNotificationSender,
/// Internal state, determined by `average_message_sending_delay`,
/// used to keep track of when a next packet should be sent out.
#[cfg(not(target_arch = "wasm32"))]
next_delay: Option<Pin<Box<time::Sleep>>>,
#[cfg(target_arch = "wasm32")]
next_delay: Option<Pin<Box<wasm_timer::Delay>>>,
// To make sure we don't overload the mix_tx channel, we limit the rate we are pushing
// messages.
sending_delay_controller: SendingDelayController,
next_delay: Pin<Box<time::Sleep>>,
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
/// out to the network without any further delays.
@@ -127,38 +73,25 @@ where
/// before being sent out into the network.
real_receiver: BatchRealMessageReceiver,
/// Represents full address of this client.
our_full_destination: Recipient,
/// Instance of a cryptographically secure random number generator.
rng: R,
/// Accessor to the common instance of network topology.
topology_access: TopologyAccessor,
/// Buffer containing all incoming real messages keyed by transmission lane, that we will send
/// out to the mixnet.
transmission_buffer: TransmissionBuffer,
/// Buffer containing all real messages received. It is first exhausted before more are pulled.
received_buffer: VecDeque<RealMessage>,
/// Incoming channel for being notified of closed connections, so that we can close lanes
/// corresponding to connections. To avoid sending traffic unnecessary
client_connection_rx: ConnectionCommandReceiver,
/// Report queue lengths so that upstream can backoff sending data, and keep connections open.
lane_queue_lengths: LaneQueueLengths,
/// Listens for shutdown signals
shutdown: ShutdownListener,
}
#[derive(Debug)]
pub(crate) struct RealMessage {
mix_packet: MixPacket,
fragment_id: FragmentIdentifier,
// TODO: add info about it being constructed with reply-surb
}
impl From<PreparedFragment> for RealMessage {
fn from(fragment: PreparedFragment) -> Self {
RealMessage {
mix_packet: fragment.mix_packet,
fragment_id: fragment.fragment_identifier,
}
}
}
impl RealMessage {
@@ -172,15 +105,63 @@ impl RealMessage {
// messages are already prepared, etc. the real point of it is to forward it to mix_traffic
// after sufficient delay
pub(crate) type BatchRealMessageSender =
tokio::sync::mpsc::Sender<(Vec<RealMessage>, TransmissionLane)>;
type BatchRealMessageReceiver = tokio::sync::mpsc::Receiver<(Vec<RealMessage>, TransmissionLane)>;
pub(crate) type BatchRealMessageSender = mpsc::UnboundedSender<Vec<RealMessage>>;
type BatchRealMessageReceiver = mpsc::UnboundedReceiver<Vec<RealMessage>>;
pub(crate) enum StreamMessage {
Cover,
Real(Box<RealMessage>),
}
impl<R> Stream for OutQueueControl<R>
where
R: CryptoRng + Rng + Unpin,
{
type Item = StreamMessage;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
// it is not yet time to return a message
if self.next_delay.as_mut().poll(cx).is_pending() {
return Poll::Pending;
};
// we know it's time to send a message, so let's prepare delay for the next one
// Get the `now` by looking at the current `delay` deadline
let avg_delay = self.config.average_message_sending_delay;
let now = self.next_delay.deadline();
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
// The next interval value is `next_poisson_delay` after the one that just
// yielded.
let next = now + next_poisson_delay;
self.next_delay.as_mut().reset(next);
// check if we have anything immediately available
if let Some(real_available) = self.received_buffer.pop_front() {
return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available))));
}
// decide what kind of message to send
match Pin::new(&mut self.real_receiver).poll_next(cx) {
// in the case our real message channel stream was closed, we should also indicate we are closed
// (and whoever is using the stream should panic)
Poll::Ready(None) => Poll::Ready(None),
// if there are more messages available, return first one and store the rest
Poll::Ready(Some(real_messages)) => {
self.received_buffer = real_messages.into();
// we MUST HAVE received at least ONE message
Poll::Ready(Some(StreamMessage::Real(Box::new(
self.received_buffer.pop_front().unwrap(),
))))
}
// otherwise construct a dummy one
Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)),
}
}
}
impl<R> OutQueueControl<R>
where
R: CryptoRng + Rng + Unpin,
@@ -190,26 +171,27 @@ where
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
config: Config,
rng: R,
ack_key: Arc<AckKey>,
sent_notifier: SentPacketNotificationSender,
mix_tx: BatchMixMessageSender,
real_receiver: BatchRealMessageReceiver,
rng: R,
our_full_destination: Recipient,
topology_access: TopologyAccessor,
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
shutdown: ShutdownListener,
) -> Self {
OutQueueControl {
config,
ack_key,
sent_notifier,
next_delay: None,
sending_delay_controller: Default::default(),
next_delay: Box::pin(time::sleep(Default::default())),
mix_tx,
real_receiver,
our_full_destination,
rng,
topology_access,
transmission_buffer: Default::default(),
client_connection_rx,
lane_queue_lengths,
received_buffer: VecDeque::with_capacity(0), // we won't be putting any data into this guy directly
shutdown,
}
}
@@ -224,348 +206,95 @@ where
async fn on_message(&mut self, next_message: StreamMessage) {
trace!("created new message");
let (next_message, fragment_id) = match next_message {
let next_message = match next_message {
StreamMessage::Cover => {
// TODO for way down the line: in very rare cases (during topology update) we might have
// to wait a really tiny bit before actually obtaining the permit hence messing with our
// poisson delay, but is it really a problem?
let topology_permit = self.topology_access.get_read_permit().await;
// the ack is sent back to ourselves (and then ignored)
let topology_ref = match topology_permit.try_get_valid_topology_ref(
&self.config.our_full_destination,
Some(&self.config.our_full_destination),
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
return;
}
};
let topology_ref_option = topology_permit.try_get_valid_topology_ref(
&self.our_full_destination,
Some(&self.our_full_destination),
);
if topology_ref_option.is_none() {
warn!(
"No valid topology detected - won't send any loop cover message this time"
);
return;
}
let topology_ref = topology_ref_option.unwrap();
(
generate_loop_cover_packet(
&mut self.rng,
topology_ref,
&self.config.ack_key,
&self.config.our_full_destination,
self.config.average_ack_delay,
self.config.average_packet_delay,
self.config.cover_packet_size,
)
.expect(
"Somehow failed to generate a loop cover message with a valid topology",
),
None,
generate_loop_cover_packet(
&mut self.rng,
topology_ref,
&self.ack_key,
&self.our_full_destination,
self.config.average_ack_delay,
self.config.average_packet_delay,
)
.expect("Somehow failed to generate a loop cover message with a valid topology")
}
StreamMessage::Real(real_message) => {
(real_message.mix_packet, Some(real_message.fragment_id))
self.sent_notify(real_message.fragment_id);
real_message.mix_packet
}
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
log::error!("Failed to send: {}", err);
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) {
if self.shutdown.is_shutdown_poll() {
log::info!("Failed to send (shutdown detected)");
} else {
// We don't try to limp along, panic to avoid continuing in a potentially
// inconsistent state
panic!("{err}");
}
}
// notify ack controller about sending our message only after we actually managed to push it
// through the channel
if let Some(fragment_id) = fragment_id {
self.sent_notify(fragment_id);
}
// In addition to closing connections on receiving messages throught client_connection_rx,
// also close connections when sufficiently stale.
self.transmission_buffer.prune_stale_connections();
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
// the UnboundedReceiver [of mix_rx] will not get a chance to read anything
// JS2: Basically it was the case that with high enough rate, the stream had already a next value
// ready and hence was immediately re-scheduled causing other tasks to be starved;
// yield makes it go back the scheduling queue regardless of its value availability
// TODO: temporary and BAD workaround for wasm (we should find a way to yield here in wasm)
#[cfg(not(target_arch = "wasm32"))]
tokio::task::yield_now().await;
}
fn on_close_connection(&mut self, connection_id: ConnectionId) {
log::debug!("Removing lane for connection: {connection_id}");
self.transmission_buffer
.remove(&TransmissionLane::ConnectionId(connection_id));
}
// Send messages at certain rate and if no real traffic is available, send cover message.
async fn run_normal_out_queue(&mut self) {
// we should set initial delay only when we actually start the stream
self.next_delay = Box::pin(time::sleep(sample_poisson_duration(
&mut self.rng,
self.config.average_message_sending_delay,
)));
fn current_average_message_sending_delay(&self) -> Duration {
self.config.average_message_sending_delay
* self.sending_delay_controller.current_multiplier()
}
fn adjust_current_average_message_sending_delay(&mut self) {
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
log::trace!(
"used_slots: {used_slots}, current_multiplier: {}",
self.sending_delay_controller.current_multiplier()
);
// Even just a single used slot is enough to signal backpressure
if used_slots > 0 {
log::trace!("Backpressure detected");
self.sending_delay_controller.record_backpressure_detected();
}
// If the buffer is running out, slow down the sending rate
if self.mix_tx.capacity() == 0
&& self.sending_delay_controller.not_increased_delay_recently()
{
self.sending_delay_controller.increase_delay_multiplier();
}
// Very carefully step up the sending rate in case it seems like we can solidly handle the
// current rate.
if self.sending_delay_controller.is_sending_reliable() {
self.sending_delay_controller.decrease_delay_multiplier();
}
}
fn pop_next_message(&mut self) -> Option<RealMessage> {
// Pop the next message from the transmission buffer
let (lane, real_next) = self.transmission_buffer.pop_next_message_at_random()?;
// Update the published queue length
let lane_length = self.transmission_buffer.lane_length(&lane);
self.lane_queue_lengths.set(&lane, lane_length);
Some(real_next)
}
fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
// The average delay could change depending on if backpressure in the downstream channel
// (mix_tx) was detected.
self.adjust_current_average_message_sending_delay();
let avg_delay = self.current_average_message_sending_delay();
// Start by checking if we have any incoming messages about closed connections
// NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to
// something simpler.
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
match id {
ConnectionCommand::Close(id) => self.on_close_connection(id),
ConnectionCommand::ActiveConnections(_) => panic!(),
}
}
if let Some(ref mut next_delay) = &mut self.next_delay {
// it is not yet time to return a message
if next_delay.as_mut().poll(cx).is_pending() {
return Poll::Pending;
};
// we know it's time to send a message, so let's prepare delay for the next one
// Get the `now` by looking at the current `delay` deadline
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
// The next interval value is `next_poisson_delay` after the one that just
// yielded.
#[cfg(not(target_arch = "wasm32"))]
{
let now = next_delay.deadline();
let next = now + next_poisson_delay;
next_delay.as_mut().reset(next);
}
#[cfg(target_arch = "wasm32")]
{
next_delay.as_mut().reset(next_poisson_delay);
}
// On every iteration we get new messages from upstream. Given that these come bunched
// in `Vec`, this ensures that on average we will fetch messages faster than we can
// send, which is a condition for being able to multiplex sphinx packets from multiple
// data streams.
match Pin::new(&mut self.real_receiver).poll_recv(cx) {
// in the case our real message channel stream was closed, we should also indicate we are closed
// (and whoever is using the stream should panic)
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some((real_messages, conn_id))) => {
log::trace!("handling real_messages: size: {}", real_messages.len());
self.transmission_buffer.store(&conn_id, real_messages);
let real_next = self.pop_next_message().expect("Just stored one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
let mut shutdown = self.shutdown.clone();
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("OutQueueControl: Received shutdown");
}
Poll::Pending => {
if let Some(real_next) = self.pop_next_message() {
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
} else {
// otherwise construct a dummy one
Poll::Ready(Some(StreamMessage::Cover))
}
}
}
} else {
// we never set an initial delay - let's do it now
cx.waker().wake_by_ref();
let sampled =
sample_poisson_duration(&mut self.rng, self.config.average_message_sending_delay);
#[cfg(not(target_arch = "wasm32"))]
let next_delay = Box::pin(time::sleep(sampled));
#[cfg(target_arch = "wasm32")]
let next_delay = Box::pin(wasm_timer::Delay::new(sampled));
self.next_delay = Some(next_delay);
Poll::Pending
}
}
fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
// Start by checking if we have any incoming messages about closed connections
if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) {
match id {
ConnectionCommand::Close(id) => self.on_close_connection(id),
ConnectionCommand::ActiveConnections(_) => panic!(),
}
}
match Pin::new(&mut self.real_receiver).poll_recv(cx) {
// in the case our real message channel stream was closed, we should also indicate we are closed
// (and whoever is using the stream should panic)
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some((real_messages, conn_id))) => {
log::trace!("handling real_messages: size: {}", real_messages.len());
// First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages);
let real_next = self.pop_next_message().expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
}
Poll::Pending => {
if let Some(real_next) = self.pop_next_message() {
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
} else {
Poll::Pending
}
}
}
}
fn poll_next_message(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<StreamMessage>> {
if self.config.disable_poisson_packet_distribution {
self.poll_immediate(cx)
} else {
self.poll_poisson(cx)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn log_status(&self) {
let packets = self.transmission_buffer.total_size();
let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0;
let lanes = self.transmission_buffer.num_lanes();
let mult = self.sending_delay_controller.current_multiplier();
let delay = self.current_average_message_sending_delay().as_millis();
let status_str = if self.config.disable_poisson_packet_distribution {
format!(
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay",
backlog
)
} else {
format!(
"Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})",
backlog, delay
)
};
if packets > 1000 {
log::warn!("{status_str}");
} else if packets > 0 {
log::info!("{status_str}");
} else {
log::debug!("{status_str}");
}
}
#[cfg(not(target_arch = "wasm32"))]
fn log_status_infrequent(&self) {
if self.sending_delay_controller.current_multiplier() > 1 {
log::warn!(
"Unable to send packets fast enough - sending delay multiplier set to: {}",
self.sending_delay_controller.current_multiplier()
);
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started OutQueueControl with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
{
let mut status_timer = tokio::time::interval(Duration::from_secs(5));
let mut infrequent_status_timer = tokio::time::interval(Duration::from_secs(60));
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("OutQueueControl: Received shutdown");
}
_ = status_timer.tick() => {
self.log_status();
}
_ = infrequent_status_timer.tick() => {
self.log_status_infrequent();
}
next_message = self.next() => if let Some(next_message) = next_message {
next_message = self.next() => match next_message {
Some(next_message) => {
self.on_message(next_message).await;
} else {
log::trace!("OutQueueControl: Stopping since channel closed");
break;
}
}
}
tokio::time::timeout(Duration::from_secs(5), shutdown.recv())
.await
.expect("Task stopped without shutdown called");
}
#[cfg(target_arch = "wasm32")]
{
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("OutQueueControl: Received shutdown");
}
next_message = self.next() => if let Some(next_message) = next_message {
self.on_message(next_message).await;
} else {
},
None => {
log::trace!("OutQueueControl: Stopping since channel closed");
break;
}
}
}
}
assert!(shutdown.is_shutdown_poll());
log::debug!("OutQueueControl: Exiting");
}
}
impl<R> Stream for OutQueueControl<R>
where
R: CryptoRng + Rng + Unpin,
{
type Item = StreamMessage;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.poll_next_message(cx)
pub(crate) async fn run_out_queue_control(&mut self) {
debug!("Starting out queue controller...");
self.run_normal_out_queue().await
}
}
@@ -1,124 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::get_time_now;
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
// the available buffer space we want to take somewhat swift action, but we still need to give a
// short time to give the channel a chance reduce pressure.
const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1;
// The minimum time between decreasing the average delay between packets. We don't want to change
// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to
// wait a little to see the effect before we decrease further.
const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30;
// If we enough time passes without any sign of backpressure in the channel, we can consider
// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out
// bandwidth at all times.
const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30;
// The maximum multiplier we apply to the base average Poisson delay.
const MAX_DELAY_MULTIPLIER: u32 = 6;
// The minium multiplier we apply to the base average Poisson delay.
const MIN_DELAY_MULTIPLIER: u32 = 1;
pub(crate) struct SendingDelayController {
/// Multiply the average sending delay.
/// This is normally set to unity, but if we detect backpressure we increase this
/// multiplier. We use discrete steps.
current_multiplier: u32,
/// Maximum delay multiplier
upper_bound: u32,
/// Minimum delay multiplier
lower_bound: u32,
/// To make sure we don't change the multiplier to fast, we limit a change to some duration
#[cfg(not(target_arch = "wasm32"))]
time_when_changed: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_changed: wasm_timer::Instant,
/// If we have a long enough time without any backpressure detected we try reducing the sending
/// delay multiplier
#[cfg(not(target_arch = "wasm32"))]
time_when_backpressure_detected: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_backpressure_detected: wasm_timer::Instant,
}
impl Default for SendingDelayController {
fn default() -> Self {
SendingDelayController::new(MIN_DELAY_MULTIPLIER, MAX_DELAY_MULTIPLIER)
}
}
impl SendingDelayController {
pub(crate) fn new(lower_bound: u32, upper_bound: u32) -> Self {
assert!(lower_bound <= upper_bound);
let now = get_time_now();
SendingDelayController {
current_multiplier: MIN_DELAY_MULTIPLIER,
upper_bound,
lower_bound,
time_when_changed: now,
time_when_backpressure_detected: now,
}
}
pub(crate) fn current_multiplier(&self) -> u32 {
self.current_multiplier
}
pub(crate) fn increase_delay_multiplier(&mut self) {
if self.current_multiplier < self.upper_bound {
self.current_multiplier =
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::warn!(
"Increasing sending delay multiplier to: {}",
self.current_multiplier
);
} else {
log::warn!("Trying to increase delay multipler higher than allowed");
}
}
pub(crate) fn decrease_delay_multiplier(&mut self) {
if self.current_multiplier > self.lower_bound {
self.current_multiplier =
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::debug!(
"Decreasing sending delay multiplier to: {}",
self.current_multiplier
);
}
}
pub(crate) fn record_backpressure_detected(&mut self) {
self.time_when_backpressure_detected = get_time_now();
}
pub(crate) fn not_increased_delay_recently(&self) -> bool {
get_time_now()
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
}
pub(crate) fn is_sending_reliable(&self) -> bool {
let now = get_time_now();
let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS);
let acceptable_time_without_backpressure =
Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS);
now > self.time_when_backpressure_detected + acceptable_time_without_backpressure
&& now > self.time_when_changed + delay_change_interval
}
}
@@ -1,211 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::TransmissionLane;
use rand::seq::SliceRandom;
use std::{
collections::{HashMap, HashSet, VecDeque},
time::Duration,
};
#[cfg(not(target_arch = "wasm32"))]
use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
use super::{get_time_now, RealMessage};
// The number of lanes included in the oldest set. Used when we need to prioritize traffic.
const OLDEST_LANE_SET_SIZE: usize = 5;
// As a way of prune connections we also check for timeouts.
const MSG_CONSIDERED_STALE_AFTER_SECS: u64 = 10 * 60;
#[derive(Default)]
pub(crate) struct TransmissionBuffer {
buffer: HashMap<TransmissionLane, LaneBufferEntry>,
}
impl TransmissionBuffer {
#[allow(unused)]
pub(crate) fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub(crate) fn remove(&mut self, lane: &TransmissionLane) -> Option<LaneBufferEntry> {
self.buffer.remove(lane)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn num_lanes(&self) -> usize {
self.buffer.keys().count()
}
pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option<usize> {
self.buffer.get(lane).map(LaneBufferEntry::len)
}
#[allow(unused)]
pub(crate) fn connections(&self) -> HashSet<u64> {
self.buffer
.keys()
.filter_map(|lane| match lane {
TransmissionLane::ConnectionId(id) => Some(id),
_ => None,
})
.copied()
.collect()
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn total_size(&self) -> usize {
self.buffer.values().map(LaneBufferEntry::len).sum()
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn total_size_in_bytes(&self) -> usize {
self.buffer
.values()
.map(|lane_buffer_entry| {
lane_buffer_entry
.real_messages
.iter()
.map(|real_message| real_message.mix_packet.sphinx_packet().len())
.sum::<usize>()
})
.sum()
}
fn get_oldest_set(&self) -> Vec<TransmissionLane> {
let mut buffer: Vec<_> = self
.buffer
.iter()
.map(|(k, v)| (k, v.messages_transmitted))
.collect();
buffer.sort_by_key(|v| v.1);
buffer
.iter()
.rev()
.map(|(k, _)| *k)
.take(OLDEST_LANE_SET_SIZE)
.copied()
.collect()
}
pub(crate) fn store(&mut self, lane: &TransmissionLane, real_messages: Vec<RealMessage>) {
if let Some(lane_buffer_entry) = self.buffer.get_mut(lane) {
lane_buffer_entry.append(real_messages);
} else {
self.buffer
.insert(*lane, LaneBufferEntry::new(real_messages));
}
}
fn pick_random_lane(&self) -> Option<&TransmissionLane> {
let lanes: Vec<&TransmissionLane> = self.buffer.keys().collect();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pick_random_small_lane(&self) -> Option<&TransmissionLane> {
let lanes: Vec<&TransmissionLane> = self
.buffer
.iter()
.filter(|(_, v)| v.is_small())
.map(|(k, _)| k)
.collect();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pick_random_old_lane(&self) -> Option<TransmissionLane> {
let lanes = self.get_oldest_set();
lanes.choose(&mut rand::thread_rng()).copied()
}
fn pop_front_from_lane(&mut self, lane: &TransmissionLane) -> Option<RealMessage> {
let real_msgs_queued = self.buffer.get_mut(lane)?;
let real_next = real_msgs_queued.pop_front()?;
real_msgs_queued.messages_transmitted += 1;
if real_msgs_queued.is_empty() {
self.buffer.remove(lane);
}
Some(real_next)
}
pub(crate) fn pop_next_message_at_random(&mut self) -> Option<(TransmissionLane, RealMessage)> {
if self.buffer.is_empty() {
return None;
}
// Very basic heuristic where we prioritize according to small lanes first, the older lanes
// to try to finish lanes when possible, then the rest.
let lane = if let Some(small_lane) = self.pick_random_small_lane() {
*small_lane
} else if let Some(old_lane) = self.pick_random_old_lane() {
old_lane
} else {
*self.pick_random_lane()?
};
let msg = self.pop_front_from_lane(&lane)?;
log::trace!("picking to send from lane: {:?}", lane);
Some((lane, msg))
}
pub(crate) fn prune_stale_connections(&mut self) {
let stale_entries: Vec<_> = self
.buffer
.iter()
.filter_map(|(lane, entry)| if entry.is_stale() { Some(lane) } else { None })
.copied()
.collect();
for lane in stale_entries {
self.remove(&lane);
}
}
}
pub(crate) struct LaneBufferEntry {
pub real_messages: VecDeque<RealMessage>,
pub messages_transmitted: usize,
#[cfg(not(target_arch = "wasm32"))]
pub time_for_last_activity: time::Instant,
#[cfg(target_arch = "wasm32")]
pub time_for_last_activity: wasm_timer::Instant,
}
impl LaneBufferEntry {
fn new(real_messages: Vec<RealMessage>) -> Self {
LaneBufferEntry {
real_messages: real_messages.into(),
messages_transmitted: 0,
time_for_last_activity: get_time_now(),
}
}
fn append(&mut self, real_messages: Vec<RealMessage>) {
self.real_messages.append(&mut real_messages.into());
self.time_for_last_activity = get_time_now();
}
fn pop_front(&mut self) -> Option<RealMessage> {
self.real_messages.pop_front()
}
fn is_small(&self) -> bool {
self.real_messages.len() < 100
}
fn is_stale(&self) -> bool {
get_time_now() - self.time_for_last_activity
> Duration::from_secs(MSG_CONSIDERED_STALE_AFTER_SECS)
}
fn len(&self) -> usize {
self.real_messages.len()
}
fn is_empty(&self) -> bool {
self.real_messages.is_empty()
}
}
+150 -261
View File
@@ -1,25 +1,24 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_controller::ReplyControllerSender;
use crate::client::replies::reply_storage::SentReplyKeys;
use crate::spawn_future;
use crate::client::reply_key_storage::ReplyKeyStorage;
use crate::client::SHUTDOWN_HAS_BEEN_SIGNALLED;
use crypto::asymmetric::encryption;
use crypto::symmetric::stream_cipher;
use crypto::Digest;
use futures::channel::mpsc;
use futures::lock::Mutex;
use futures::StreamExt;
use gateway_client::MixnetMessageReceiver;
use log::*;
use nymsphinx::anonymous_replies::requests::{
RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent,
};
use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey};
use nymsphinx::message::{NymMessage, PlainMessage};
use nymsphinx::params::ReplySurbKeyDigestAlgorithm;
use nymsphinx::params::{ReplySurbEncryptionAlgorithm, ReplySurbKeyDigestAlgorithm};
use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage};
use std::collections::HashSet;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use task::ShutdownListener;
use tokio::task::JoinHandle;
// Buffer Requests to say "hey, send any reconstructed messages to this channel"
// or to say "hey, I'm going offline, don't send anything more to me. Just buffer them instead"
@@ -46,15 +45,26 @@ struct ReceivedMessagesBufferInner {
}
impl ReceivedMessagesBufferInner {
fn recover_from_fragment(&mut self, fragment_data: &[u8]) -> Option<NymMessage> {
if nymsphinx::cover::is_cover(fragment_data) {
fn process_received_fragment(&mut self, raw_fragment: Vec<u8>) -> Option<ReconstructedMessage> {
let fragment_data = match self
.message_receiver
.recover_plaintext(self.local_encryption_keypair.private_key(), raw_fragment)
{
Err(e) => {
warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e);
return None;
}
Ok(frag_data) => frag_data,
};
if nymsphinx::cover::is_cover(&fragment_data) {
trace!("The message was a loop cover message! Skipping it");
return None;
}
let fragment = match self.message_receiver.recover_fragment(fragment_data) {
Err(err) => {
warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!");
let fragment = match self.message_receiver.recover_fragment(&fragment_data) {
Err(e) => {
warn!("failed to recover fragment from raw data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e);
return None;
}
Ok(frag) => frag,
@@ -68,10 +78,9 @@ impl ReceivedMessagesBufferInner {
// if we returned an error the underlying message is malformed in some way
match self.message_receiver.insert_new_fragment(fragment) {
Err(err) => match err {
MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => {
error!("message reconstruction failed - {source}. Attempting to re-use the message sets...");
MessageRecoveryError::MalformedReconstructedMessage(message_sets) => {
// TODO: should we really insert reconstructed sets? could this be abused for some attack?
for set_id in used_sets {
for set_id in message_sets {
if !self.recently_reconstructed.insert(set_id) {
// or perhaps we should even panic at this point?
error!("Reconstructed another message containing already used set id!")
@@ -97,34 +106,6 @@ impl ReceivedMessagesBufferInner {
},
}
}
fn process_received_reply(
&mut self,
reply_ciphertext: &mut [u8],
reply_key: SurbEncryptionKey,
) -> Option<NymMessage> {
// note: this performs decryption IN PLACE without extra allocation
self.message_receiver
.recover_plaintext_from_reply(reply_ciphertext, reply_key);
let fragment_data = reply_ciphertext;
self.recover_from_fragment(fragment_data)
}
fn process_received_regular_packet(&mut self, mut raw_fragment: Vec<u8>) -> Option<NymMessage> {
let fragment_data = match self.message_receiver.recover_plaintext_from_regular_packet(
self.local_encryption_keypair.private_key(),
&mut raw_fragment,
) {
Err(err) => {
warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!");
return None;
}
Ok(frag_data) => frag_data,
};
self.recover_from_fragment(fragment_data)
}
}
#[derive(Debug, Clone)]
@@ -132,15 +113,16 @@ impl ReceivedMessagesBufferInner {
// You should always use .clone() to create additional instances
struct ReceivedMessagesBuffer {
inner: Arc<Mutex<ReceivedMessagesBufferInner>>,
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
/// Storage containing keys to all [`ReplySURB`]s ever sent out that we did not receive back.
// There's no need to put it behind a Mutex since it's already properly concurrent
reply_key_storage: ReplyKeyStorage,
}
impl ReceivedMessagesBuffer {
fn new(
local_encryption_keypair: Arc<encryption::KeyPair>,
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
reply_key_storage: ReplyKeyStorage,
) -> Self {
ReceivedMessagesBuffer {
inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner {
@@ -151,7 +133,6 @@ impl ReceivedMessagesBuffer {
recently_reconstructed: HashSet::new(),
})),
reply_key_storage,
reply_controller_sender,
}
}
@@ -193,143 +174,37 @@ impl ReceivedMessagesBuffer {
guard.message_sender = Some(sender);
}
fn handle_reconstructed_plain_messages(
&mut self,
msgs: Vec<PlainMessage>,
) -> Vec<ReconstructedMessage> {
msgs.into_iter().map(Into::into).collect()
async fn add_reconstructed_messages(&mut self, msgs: Vec<ReconstructedMessage>) {
debug!("Adding {:?} new messages to the buffer!", msgs.len());
trace!("Adding new messages to the buffer! {:?}", msgs);
self.inner.lock().await.messages.extend(msgs)
}
fn handle_reconstructed_repliable_messages(
&mut self,
msgs: Vec<RepliableMessage>,
) -> Vec<ReconstructedMessage> {
let mut reconstructed = Vec::new();
for msg in msgs {
let (reply_surbs, from_surb_request) = match msg.content {
RepliableMessageContent::Data {
message,
reply_surbs,
} => {
trace!(
"received message that also contained additional {} reply surbs from {:?}!",
reply_surbs.len(),
msg.sender_tag
);
fn process_received_reply(
reply_ciphertext: &[u8],
reply_key: SurbEncryptionKey,
) -> Option<ReconstructedMessage> {
let zero_iv = stream_cipher::zero_iv::<ReplySurbEncryptionAlgorithm>();
reconstructed.push(ReconstructedMessage::new(message, msg.sender_tag));
(reply_surbs, false)
}
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
trace!(
"received additional {} reply surbs from {:?}!",
reply_surbs.len(),
msg.sender_tag
);
(reply_surbs, true)
}
RepliableMessageContent::Heartbeat {
additional_reply_surbs,
} => {
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
(additional_reply_surbs, false)
}
};
self.reply_controller_sender.send_additional_surbs(
msg.sender_tag,
reply_surbs,
from_surb_request,
)
}
reconstructed
}
fn handle_reconstructed_reply_messages(
&mut self,
msgs: Vec<ReplyMessage>,
) -> Vec<ReconstructedMessage> {
let mut reconstructed = Vec::new();
for msg in msgs {
match msg.content {
ReplyMessageContent::Data { message } => reconstructed.push(message.into()),
ReplyMessageContent::SurbRequest { recipient, amount } => {
debug!("received request for {amount} additional reply SURBs from {recipient}");
self.reply_controller_sender
.send_additional_surbs_request(*recipient, amount);
}
}
}
reconstructed
}
async fn handle_reconstructed_messages(&mut self, msgs: Vec<NymMessage>) {
if msgs.is_empty() {
return;
}
let mut plain_messages = Vec::new();
let mut repliable_messages = Vec::new();
let mut reply_messages = Vec::new();
for msg in msgs {
match msg {
NymMessage::Plain(plain) => plain_messages.push(plain),
NymMessage::Repliable(repliable) => repliable_messages.push(repliable),
NymMessage::Reply(reply) => reply_messages.push(reply),
}
}
let mut reconstructed_messages = self.handle_reconstructed_plain_messages(plain_messages);
reconstructed_messages
.append(&mut self.handle_reconstructed_repliable_messages(repliable_messages));
reconstructed_messages
.append(&mut self.handle_reconstructed_reply_messages(reply_messages));
let mut inner_guard = self.inner.lock().await;
debug!(
"Adding {:?} new messages to the buffer!",
reconstructed_messages.len()
let mut reply_msg = stream_cipher::decrypt::<ReplySurbEncryptionAlgorithm>(
reply_key.inner(),
&zero_iv,
reply_ciphertext,
);
if let Some(sender) = &inner_guard.message_sender {
trace!("Sending reconstructed messages to announced sender");
if let Err(err) = sender.unbounded_send(reconstructed_messages) {
warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})");
inner_guard.message_sender = None;
inner_guard.messages.extend(err.into_inner());
}
if let Err(err) = MessageReceiver::remove_padding(&mut reply_msg) {
warn!("Received reply had malformed padding! - {:?}", err);
None
} else {
trace!("No sender available - buffering reconstructed messages");
inner_guard.messages.extend(reconstructed_messages)
}
}
// this function doesn't really belong here...
fn get_reply_key<'a>(
&self,
raw_message: &'a mut [u8],
) -> Option<(SurbEncryptionKey, &'a mut [u8])> {
let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size();
if raw_message.len() < reply_surb_digest_size {
return None;
}
let possible_key_digest =
EncryptionKeyDigest::clone_from_slice(&raw_message[..reply_surb_digest_size]);
self.reply_key_storage
.try_pop(possible_key_digest)
.map(|reply_encryption_key| {
(
*reply_encryption_key,
&mut raw_message[reply_surb_digest_size..],
)
// TODO: perhaps having to say it doesn't have a surb an indication the type should be changed?
Some(ReconstructedMessage {
message: reply_msg,
reply_surb: None,
})
}
}
async fn handle_new_received(&mut self, msgs: Vec<Vec<u8>>) {
trace!(
debug!(
"Processing {:?} new message that might get added to the buffer!",
msgs.len()
);
@@ -337,28 +212,58 @@ impl ReceivedMessagesBuffer {
let mut completed_messages = Vec::new();
let mut inner_guard = self.inner.lock().await;
let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size();
// first check if this is a reply or a chunked message
// note: there's a possible information leakage associated with this check https://github.com/nymtech/nym/issues/296
for mut msg in msgs {
// TODO: verify with @AP if this way of doing it is safe or whether it could
// cause some attacks due to, I don't know, stupid edge case collisions?
// Update: this DOES introduce a possible leakage: https://github.com/nymtech/nym/issues/296
for msg in msgs {
let possible_key_digest =
EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]);
// check first `HasherOutputSize` bytes if they correspond to known encryption key
// if yes - this is a reply message
let completed_message =
if let Some((reply_key, reply_message)) = self.get_reply_key(&mut msg) {
inner_guard.process_received_reply(reply_message, reply_key)
} else {
inner_guard.process_received_regular_packet(msg)
};
if let Some(completed) = completed_message {
info!("received {completed}");
completed_messages.push(completed)
// TODO: this might be a bottleneck - since the keys are stored on disk we, presumably,
// are doing a disk operation every single received fragment
if let Some(reply_encryption_key) = self
.reply_key_storage
.get_and_remove_encryption_key(possible_key_digest)
.expect("storage operation failed!")
{
if let Some(completed_message) = Self::process_received_reply(
&msg[reply_surb_digest_size..],
reply_encryption_key,
) {
completed_messages.push(completed_message)
}
} else {
// otherwise - it's a 'normal' message
if let Some(completed_message) = inner_guard.process_received_fragment(msg) {
completed_messages.push(completed_message)
}
}
}
drop(inner_guard);
if !completed_messages.is_empty() {
self.handle_reconstructed_messages(completed_messages).await
if let Some(sender) = &inner_guard.message_sender {
trace!("Sending reconstructed messages to announced sender");
if let Err(err) = sender.unbounded_send(completed_messages) {
warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {:?})", err);
// make sure to drop the lock to not deadlock
// (it is required by `add_reconstructed_messages`)
inner_guard.message_sender = None;
drop(inner_guard);
self.add_reconstructed_messages(err.into_inner()).await;
}
} else {
// make sure to drop the lock to not deadlock
// (it is required by `add_reconstructed_messages`)
drop(inner_guard);
trace!("No sender available - buffering reconstructed messages");
self.add_reconstructed_messages(completed_messages).await;
}
}
}
}
@@ -388,120 +293,104 @@ impl RequestReceiver {
}
}
async fn handle_message(&mut self, message: ReceivedBufferMessage) {
match message {
ReceivedBufferMessage::ReceiverAnnounce(sender) => {
self.received_buffer.connect_sender(sender).await;
fn start(mut self) -> JoinHandle<()> {
tokio::spawn(async move {
loop {
tokio::select! {
request = self.query_receiver.next() => {
match request {
Some(ReceivedBufferMessage::ReceiverAnnounce(sender)) => {
self.received_buffer.connect_sender(sender).await;
}
Some(ReceivedBufferMessage::ReceiverDisconnect) => {
self.received_buffer.disconnect_sender().await
}
None => {
log::trace!("RequestReceiver: Stopping since channel closed");
break;
},
}
},
};
}
ReceivedBufferMessage::ReceiverDisconnect => {
self.received_buffer.disconnect_sender().await
}
}
}
async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started RequestReceiver with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("RequestReceiver: Received shutdown");
}
request = self.query_receiver.next() => {
match request {
Some(message) => self.handle_message(message).await,
None => {
log::trace!("RequestReceiver: Stopping since channel closed");
break;
},
}
},
}
}
shutdown.recv_timeout().await;
log::debug!("RequestReceiver: Exiting");
assert!(SHUTDOWN_HAS_BEEN_SIGNALLED.load(Ordering::Relaxed));
log::debug!("RequestReceiver: Exiting");
})
}
}
struct FragmentedMessageReceiver {
received_buffer: ReceivedMessagesBuffer,
mixnet_packet_receiver: MixnetMessageReceiver,
shutdown: ShutdownListener,
}
impl FragmentedMessageReceiver {
fn new(
received_buffer: ReceivedMessagesBuffer,
mixnet_packet_receiver: MixnetMessageReceiver,
shutdown: ShutdownListener,
) -> Self {
FragmentedMessageReceiver {
received_buffer,
mixnet_packet_receiver,
shutdown,
}
}
async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started FragmentedMessageReceiver with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
new_messages = self.mixnet_packet_receiver.next() => match new_messages {
Some(new_messages) => {
self.received_buffer.handle_new_received(new_messages).await;
fn start(mut self) -> JoinHandle<()> {
tokio::spawn(async move {
while !self.shutdown.is_shutdown() {
tokio::select! {
new_messages = self.mixnet_packet_receiver.next() => match new_messages {
Some(new_messages) => {
self.received_buffer.handle_new_received(new_messages).await;
}
None => {
log::trace!("FragmentedMessageReceiver: Stopping since channel closed");
break;
}
},
_ = self.shutdown.recv() => {
log::trace!("FragmentedMessageReceiver: Received shutdown");
}
None => {
log::trace!("FragmentedMessageReceiver: Stopping since channel closed");
break;
}
},
_ = shutdown.recv() => {
log::trace!("FragmentedMessageReceiver: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
log::debug!("FragmentedMessageReceiver: Exiting");
assert!(self.shutdown.is_shutdown_poll());
log::debug!("FragmentedMessageReceiver: Exiting");
})
}
}
pub(crate) struct ReceivedMessagesBufferController {
pub struct ReceivedMessagesBufferController {
fragmented_message_receiver: FragmentedMessageReceiver,
request_receiver: RequestReceiver,
}
impl ReceivedMessagesBufferController {
pub(crate) fn new(
pub fn new(
local_encryption_keypair: Arc<encryption::KeyPair>,
query_receiver: ReceivedBufferRequestReceiver,
mixnet_packet_receiver: MixnetMessageReceiver,
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
) -> Self {
let received_buffer = ReceivedMessagesBuffer::new(
local_encryption_keypair,
reply_key_storage,
reply_controller_sender,
);
let received_buffer =
ReceivedMessagesBuffer::new(local_encryption_keypair, reply_key_storage);
ReceivedMessagesBufferController {
fragmented_message_receiver: FragmentedMessageReceiver::new(
received_buffer.clone(),
mixnet_packet_receiver,
shutdown,
),
request_receiver: RequestReceiver::new(received_buffer, query_receiver),
}
}
pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) {
let mut fragmented_message_receiver = self.fragmented_message_receiver;
let mut request_receiver = self.request_receiver;
let shutdown_handle = shutdown.clone();
spawn_future(async move {
fragmented_message_receiver
.run_with_shutdown(shutdown_handle)
.await;
});
spawn_future(async move {
request_receiver.run_with_shutdown(shutdown).await;
});
pub fn start(self) {
// TODO: should we do anything with JoinHandle(s) returned by start methods?
self.fragmented_message_receiver.start();
self.request_receiver.start();
}
}
@@ -1,5 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod reply_controller;
pub mod reply_storage;
@@ -1,928 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError};
use crate::client::replies::reply_storage::CombinedReplyStorage;
use client_connections::TransmissionLane;
use futures::channel::mpsc;
use futures::StreamExt;
use log::{debug, error, info, trace, warn};
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use rand::{CryptoRng, Rng};
use std::cmp::{max, min};
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::{Arc, Weak};
use std::time::Duration;
use time::OffsetDateTime;
#[cfg(not(target_arch = "wasm32"))]
type IntervalStream = tokio_stream::wrappers::IntervalStream;
#[cfg(target_arch = "wasm32")]
type IntervalStream = gloo_timers::future::IntervalStream;
pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) {
let (tx, rx) = mpsc::unbounded();
(tx.into(), rx)
}
#[derive(Debug, Clone)]
pub(crate) struct ReplyControllerSender(mpsc::UnboundedSender<ReplyControllerMessage>);
impl From<mpsc::UnboundedSender<ReplyControllerMessage>> for ReplyControllerSender {
fn from(inner: mpsc::UnboundedSender<ReplyControllerMessage>) -> Self {
ReplyControllerSender(inner)
}
}
impl ReplyControllerSender {
pub(crate) fn send_retransmission_data(
&self,
recipient: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surb_request: bool,
) {
self.0
.unbounded_send(ReplyControllerMessage::RetransmitReply {
recipient,
timed_out_ack,
extra_surb_request,
})
.expect("ReplyControllerReceiver has died!")
}
pub(crate) fn send_reply(
&self,
recipient: AnonymousSenderTag,
message: Vec<u8>,
lane: TransmissionLane,
) {
self.0
.unbounded_send(ReplyControllerMessage::SendReply {
recipient,
message,
lane,
})
.expect("ReplyControllerReceiver has died!")
}
pub(crate) fn send_additional_surbs(
&self,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) {
self.0
.unbounded_send(ReplyControllerMessage::AdditionalSurbs {
sender_tag,
reply_surbs,
from_surb_request,
})
.expect("ReplyControllerReceiver has died!")
}
pub(crate) fn send_additional_surbs_request(&self, recipient: Recipient, amount: u32) {
self.0
.unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest {
recipient: Box::new(recipient),
amount,
})
.expect("ReplyControllerReceiver has died!")
}
}
pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver<ReplyControllerMessage>;
#[derive(Debug)]
pub(crate) enum ReplyControllerMessage {
RetransmitReply {
recipient: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surb_request: bool,
},
SendReply {
recipient: AnonymousSenderTag,
message: Vec<u8>,
lane: TransmissionLane,
},
AdditionalSurbs {
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
},
// Should this also be handled in here? it's technically a completely different side of the pipe
// let's see how it works when combined, might split it before creating PR
AdditionalSurbsRequest {
recipient: Box<Recipient>,
amount: u32,
},
}
pub struct Config {
min_surb_request_size: u32,
max_surb_request_size: u32,
maximum_allowed_reply_surb_request_size: u32,
max_surb_waiting_period: Duration,
max_reply_surb_age: Duration,
max_reply_key_age: Duration,
}
impl Config {
pub(crate) fn new(
min_surb_request_size: u32,
max_surb_request_size: u32,
maximum_allowed_reply_surb_request_size: u32,
max_surb_waiting_period: Duration,
max_reply_surb_age: Duration,
max_reply_key_age: Duration,
) -> Self {
Self {
min_surb_request_size,
max_surb_request_size,
maximum_allowed_reply_surb_request_size,
max_surb_waiting_period,
max_reply_surb_age,
max_reply_key_age,
}
}
}
// the purpose of this task:
// - buffers split messages from input message listener if there were insufficient surbs to send them
// - upon getting extra surbs, resends them
// - so I guess it will handle all 'RepliableMessage' and requests from 'ReplyMessage'
// - replies to "give additional surbs" requests
// - will reply to future heartbeats
// TODO: this should be split into ingress and egress controllers
// because currently its trying to perform two distinct jobs
pub struct ReplyController<R> {
config: Config,
// TODO: incorporate that field at some point
// and use binomial distribution to determine the expected required number
// of surbs required to send the message through
// expected_reliability: f32,
request_receiver: ReplyControllerReceiver,
pending_replies: HashMap<AnonymousSenderTag, VecDeque<Fragment>>,
/// Retransmission packets that have already timed out and are waiting for additional reply SURBs
/// so that they could be sent back to the network. Once we receive more SURBs, we should send them ASAP.
// TODO: when purging stale entries, we must take extra care to also purge all pending ACK data!!
pending_retransmissions:
HashMap<AnonymousSenderTag, BTreeMap<FragmentIdentifier, Weak<PendingAcknowledgement>>>,
message_handler: MessageHandler<R>,
full_reply_storage: CombinedReplyStorage,
}
impl<R> ReplyController<R>
where
R: CryptoRng + Rng,
{
pub(crate) fn new(
config: Config,
message_handler: MessageHandler<R>,
full_reply_storage: CombinedReplyStorage,
request_receiver: ReplyControllerReceiver,
) -> Self {
ReplyController {
config,
request_receiver,
pending_replies: HashMap::new(),
pending_retransmissions: HashMap::new(),
message_handler,
full_reply_storage,
}
}
/// Inserts the pending replies into the BACK of the queue fn insert_pending_replies<V: Into<VecDeque<Fragment>>>(
fn insert_pending_replies<V: Into<VecDeque<Fragment>>>(
&mut self,
recipient: &AnonymousSenderTag,
fragments: V,
) {
if let Some(existing) = self.pending_replies.get_mut(recipient) {
existing.append(&mut fragments.into())
} else {
self.pending_replies.insert(*recipient, fragments.into());
}
}
fn re_insert_pending_retransmission(
&mut self,
recipient: &AnonymousSenderTag,
data: Vec<Arc<PendingAcknowledgement>>,
) {
// the underlying entry MUST exist as we've just got data from there
let map_entry = self
.pending_retransmissions
.get_mut(recipient)
.expect("our pending retransmission entry is somehow gone!");
for pending in data {
// if it's 0, we don't need to do anything - we just got that ack!
if Arc::strong_count(&pending) > 1 {
let id = pending.inner_fragment_identifier();
let downgraded = Arc::downgrade(&pending);
map_entry.insert(id, downgraded);
}
}
}
fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool {
trace!("checking if we should request more surbs from {:?}", target);
let pending_queue_size = self
.pending_replies
.get(target)
.map(|pending_queue| pending_queue.len())
.unwrap_or_default();
let retransmission_queue = self
.pending_retransmissions
.get(target)
.map(|pending_queue| pending_queue.len())
.unwrap_or_default();
let total_queue = pending_queue_size + retransmission_queue;
// simple as that - there's absolutely nothing to retransmit
if total_queue == 0 {
return false;
}
let available_surbs = self
.full_reply_storage
.surbs_storage_ref()
.available_surbs(target);
let pending_surbs = self
.full_reply_storage
.surbs_storage_ref()
.pending_reception(target) as usize;
let min_surbs_threshold = self
.full_reply_storage
.surbs_storage_ref()
.min_surb_threshold();
let max_surbs_threshold = self
.full_reply_storage
.surbs_storage_ref()
.max_surb_threshold();
debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}");
(pending_surbs + available_surbs) < max_surbs_threshold
&& (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold)
}
async fn handle_send_reply(
&mut self,
recipient_tag: AnonymousSenderTag,
data: Vec<u8>,
lane: TransmissionLane,
) {
if !self
.full_reply_storage
.surbs_storage_ref()
.contains_surbs_for(&recipient_tag)
{
warn!("received reply request for {:?} but we don't have any surbs stored for that recipient!", recipient_tag);
return;
}
trace!("handling reply to {:?}", recipient_tag);
let fragments = self.message_handler.split_reply_message(data);
let required_surbs = fragments.len();
trace!("This reply requires {:?} SURBs", required_surbs);
// TODO: edge case:
// we're making a lot of requests and have to request a lot of surbs
// (but at some point we run out of surbs for surb requests)
let (surbs, _surbs_left) = self
.full_reply_storage
.surbs_storage_ref()
.get_reply_surbs(&recipient_tag, required_surbs);
if let Some(reply_surbs) = surbs {
if let Err(err) = self
.message_handler
.try_send_reply_chunks(recipient_tag, fragments, reply_surbs, lane)
.await
{
let err = err.return_unused_surbs(
self.full_reply_storage.surbs_storage_ref(),
&recipient_tag,
);
warn!("failed to send reply to {:?} - {err}", recipient_tag);
// TODO: should we buffer that data to try again?
}
} else {
// we don't have enough surbs for this reply
self.insert_pending_replies(&recipient_tag, fragments);
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
}
async fn request_additional_reply_surbs(
&mut self,
target: AnonymousSenderTag,
amount: u32,
) -> Result<(), PreparationError> {
let reply_surb = self
.full_reply_storage
.surbs_storage_ref()
.get_reply_surb_ignoring_threshold(&target)
.and_then(|(reply_surb, _)| reply_surb)
.ok_or(PreparationError::NotEnoughSurbs {
available: 0,
required: 1,
})?;
if let Err(err) = self
.message_handler
.try_request_additional_reply_surbs(target, reply_surb, amount)
.await
{
let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target);
warn!(
"failed to request additional surbs from {:?} - {err}",
target
);
return Err(err);
} else {
self.full_reply_storage
.surbs_storage_ref()
.increment_pending_reception(&target, amount);
}
Ok(())
}
async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) {
trace!("trying to clear pending retransmission queue");
let available_surbs = self
.full_reply_storage
.surbs_storage_ref()
.available_surbs(&target);
let min_surbs_threshold = self
.full_reply_storage
.surbs_storage_ref()
.min_surb_threshold();
let max_to_clear = if available_surbs > min_surbs_threshold {
available_surbs - min_surbs_threshold
} else {
trace!("we don't have enough surbs for retransmission queue clearing...");
return;
};
trace!("we can clear up to {max_to_clear} entries");
let Some(pending) = self.pending_retransmissions.get_mut(&target) else {
trace!("there are no pending retransmissions for {target}!");
return;
};
let mut to_take = Vec::new();
let mut to_remove = Vec::new();
// TODO: once rust 1.66.0 is stabilised on 15.12.22, just change it to
// `.pop_front()` to directly take ownership
for (k, data) in pending.iter() {
let upgraded = match data.upgrade() {
Some(upgraded) => upgraded,
None => {
// we got the ack while the data was waiting in the queue
to_remove.push(*k);
continue;
}
};
to_take.push(upgraded);
// we have taken as many entries as we could have
if to_take.len() >= max_to_clear {
break;
}
// TODO: use if upgraded.is_extra_surb_request() to bypass the limit
}
for ack in &to_take {
pending.remove(&ack.inner_fragment_identifier());
}
for id in to_remove {
pending.remove(&id);
}
if to_take.is_empty() {
// no need to do anything
return;
}
let (surbs_for_reply, _) = self
.full_reply_storage
.surbs_storage_ref()
.get_reply_surbs(&target, to_take.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
self.re_insert_pending_retransmission(&target, to_take);
return;
};
let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect();
if let Err(err) = self
.message_handler
.try_send_retransmission_reply_chunks(
to_send_vec,
surbs_for_reply,
TransmissionLane::Retransmission,
)
.await
{
let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target);
self.re_insert_pending_retransmission(&target, to_take);
warn!(
"failed to clear pending retransmission queue for {:?} - {err}",
target
);
}
}
fn pop_at_most_pending_replies(
&mut self,
from: &AnonymousSenderTag,
amount: usize,
) -> Option<VecDeque<Fragment>> {
// if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb
let total = self.pending_replies.get(from)?.len();
trace!("pending queue has {total} elements");
if total == 0 {
return None;
}
if total < amount {
self.pending_replies.remove(from)
} else {
Some(
self.pending_replies
.get_mut(from)?
.drain(..amount)
.collect(),
)
}
}
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
trace!("trying to clear pending queue");
let available_surbs = self
.full_reply_storage
.surbs_storage_ref()
.available_surbs(&target);
let min_surbs_threshold = self
.full_reply_storage
.surbs_storage_ref()
.min_surb_threshold();
let max_to_clear = if available_surbs > min_surbs_threshold {
available_surbs - min_surbs_threshold
} else {
trace!("we don't have enough surbs for queue clearing...");
return;
};
trace!("we can clear up to {max_to_clear} entries");
// we're guaranteed to not get more entries than we have reply surbs for
if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) {
let to_send_vec = to_send.iter().cloned().collect::<Vec<_>>();
if to_send_vec.is_empty() {
panic!(
"please let the devs know if you ever see this message (reply_controller.rs)"
);
}
let (surbs_for_reply, _) = self
.full_reply_storage
.surbs_storage_ref()
.get_reply_surbs(&target, to_send_vec.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
self.insert_pending_replies(&target, to_send);
return;
};
if let Err(err) = self
.message_handler
.try_send_reply_chunks(
target,
to_send_vec,
surbs_for_reply,
TransmissionLane::General,
)
.await
{
let err =
err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target);
self.insert_pending_replies(&target, to_send);
warn!("failed to clear pending queue for {:?} - {err}", target);
}
} else {
trace!("the pending queue is empty");
}
}
async fn handle_received_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) {
trace!("handling received surbs");
// clear the requesting flag since we should have been asking for surbs
self.full_reply_storage
.surbs_storage_ref()
.reset_surbs_last_received_at(&from);
if from_surb_request {
self.full_reply_storage
.surbs_storage_ref()
.decrement_pending_reception(&from, reply_surbs.len() as u32);
}
// store received surbs
self.full_reply_storage
.surbs_storage_ref()
.insert_surbs(&from, reply_surbs);
// use as many as we can for clearing pending retransmission queue
self.try_clear_pending_retransmission(from).await;
// use as many as we can for clearing pending 'normal' queue
self.try_clear_pending_queue(from).await;
// if we have to, request more
if self.should_request_more_surbs(&from) {
self.request_reply_surbs_for_queue_clearing(from).await;
}
}
async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) {
// 1. check whether we sent any surbs in the past to this recipient, otherwise
// they have no business in asking for more
if !self
.full_reply_storage
.tags_storage_ref()
.exists(&recipient)
{
warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!");
return;
}
// 2. check whether the requested amount is within sane range
if amount > self.config.maximum_allowed_reply_surb_request_size {
warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.maximum_allowed_reply_surb_request_size);
amount = self.config.maximum_allowed_reply_surb_request_size;
}
// 3. construct and send the surbs away
// (send them in smaller batches to make the experience a bit smoother
let mut remaining = amount;
while remaining > 0 {
let to_send = min(remaining, 100);
if let Err(err) = self
.message_handler
.try_send_additional_reply_surbs(recipient, to_send)
.await
{
warn!("failed to send additional surbs to {recipient} - {err}");
} else {
trace!("sent {to_send} reply SURBs to {recipient}");
}
remaining -= to_send;
}
}
fn buffer_pending_ack(
&mut self,
recipient: AnonymousSenderTag,
ack_ref: Arc<PendingAcknowledgement>,
weak_ack_ref: Weak<PendingAcknowledgement>,
) {
let frag_id = ack_ref.inner_fragment_identifier();
if let Some(existing) = self.pending_retransmissions.get_mut(&recipient) {
if let Entry::Vacant(e) = existing.entry(frag_id) {
e.insert(weak_ack_ref);
} else {
warn!("we're already trying to retransmit {frag_id}. We must be really behind in surbs!");
}
} else {
let mut inner = BTreeMap::new();
inner.insert(frag_id, weak_ack_ref);
self.pending_retransmissions.insert(recipient, inner);
}
}
async fn handle_reply_retransmission(
&mut self,
recipient_tag: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surbs_request: bool,
) {
// seems we got the ack in the end
let ack_ref = match timed_out_ack.upgrade() {
Some(ack) => ack,
None => {
debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue");
return;
}
};
// if this is retransmission for obtaining additional reply surbs,
// we can dip below the storage threshold
let (maybe_reply_surb, _) = if extra_surbs_request {
self.full_reply_storage
.surbs_storage_ref()
.get_reply_surb_ignoring_threshold(&recipient_tag)
} else {
self.full_reply_storage
.surbs_storage_ref()
.get_reply_surb(&recipient_tag)
}
.expect("attempted to retransmit a packet to an unknown recipient - we shouldn't have sent the original packet in the first place!");
if let Some(reply_surb) = maybe_reply_surb {
match self
.message_handler
.try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data())
.await
{
Ok(prepared) => {
// drop the ack ref so that controller would not panic on `UpdateTimer` if that task
// got to handle the action before this function terminated (which is very much
// possible if `forward_messages` takes a while)
drop(ack_ref);
self.message_handler
.update_ack_delay(prepared.fragment_identifier, prepared.total_delay);
self.message_handler
.forward_messages(vec![prepared.into()], TransmissionLane::Retransmission)
.await;
}
Err(err) => {
let err = err.return_unused_surbs(
self.full_reply_storage.surbs_storage_ref(),
&recipient_tag,
);
warn!("failed to prepare message for retransmission - {err}");
// we buffer that packet and to try another day
self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack);
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
};
} else {
self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack);
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
}
async fn handle_request(&mut self, request: ReplyControllerMessage) {
match request {
ReplyControllerMessage::RetransmitReply {
recipient,
timed_out_ack,
extra_surb_request,
} => {
self.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request)
.await
}
ReplyControllerMessage::SendReply {
recipient,
message,
lane,
} => self.handle_send_reply(recipient, message, lane).await,
ReplyControllerMessage::AdditionalSurbs {
sender_tag,
reply_surbs,
from_surb_request,
} => {
self.handle_received_surbs(sender_tag, reply_surbs, from_surb_request)
.await
}
ReplyControllerMessage::AdditionalSurbsRequest { recipient, amount } => {
self.handle_surb_request(*recipient, amount).await
}
}
}
async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) {
trace!("requesting surbs for queues clearing");
let pending_queue_size = self
.pending_replies
.get(&target)
.map(|pending_queue| pending_queue.len())
.unwrap_or_default();
let retransmission_queue = self
.pending_retransmissions
.get(&target)
.map(|pending_queue| pending_queue.len())
.unwrap_or_default();
let total_queue = (pending_queue_size + retransmission_queue) as u32;
if total_queue == 0 {
trace!("the pending queues for {:?} are already empty", target);
return;
}
let request_size = min(
self.config.max_surb_request_size,
max(total_queue, self.config.min_surb_request_size),
);
if let Err(err) = self
.request_additional_reply_surbs(target, request_size)
.await
{
warn!("failed to request additional surbs... - {err}")
}
}
async fn inspect_stale_entries(&mut self) {
let mut to_request = Vec::new();
let mut to_remove = Vec::new();
let now = OffsetDateTime::now_utc();
for (pending_reply_target, vals) in &self.pending_replies {
if vals.is_empty() {
continue;
}
let Some(last_received) = self.full_reply_storage.surbs_storage_ref().surbs_last_received_at(pending_reply_target) else {
error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", vals.len());
to_remove.push(*pending_reply_target);
continue;
};
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
// purge that malformed data
let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else {
error!("somehow our stored timestamp ({last_received}) for surbs from {pending_reply_target} is corrupted!. Going to remove all the associated entries");
to_remove.push(*pending_reply_target);
continue;
};
let diff = now - last_received_time;
if diff > self.config.max_surb_waiting_period {
warn!("We haven't received any surbs in {:?} from {pending_reply_target}. Going to explicitly ask for more", diff);
to_request.push(*pending_reply_target);
}
}
for pending_reply_target in to_request {
self.request_reply_surbs_for_queue_clearing(pending_reply_target)
.await;
self.full_reply_storage
.surbs_storage_ref()
.reset_pending_reception(&pending_reply_target)
}
for to_remove in to_remove {
self.pending_replies.remove(&to_remove);
}
}
async fn invalidate_old_data(&self) {
let now = OffsetDateTime::now_utc();
let mut to_remove_surbs = Vec::new();
let mut to_remove_keys = Vec::new();
for map_ref in self.full_reply_storage.surbs_storage_ref().as_raw_iter() {
let (sender, received) = map_ref.pair();
// TODO: handle the following edge case:
// there's a malicious client sending us exactly one reply surb just before we should have invalidated
// the data thus making us keep everything in memory
// possible solution: keep timestamp PER reply surb (but that seems like an overkill)
// but I doubt this is ever going to be a problem...
// ...
// However, if you're reading this message, it probably became a legit problem,
// so I guess add timestamp per surb then? chop-chop.
let last_received = received.surbs_last_received_at();
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
// purge that malformed data
let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else {
error!("somehow our stored timestamp ({last_received}) for surbs from {sender} is corrupted!. Going to remove all the associated entries");
to_remove_surbs.push(*sender);
continue;
};
let diff = now - last_received_time;
if diff > self.config.max_reply_surb_age {
info!("it's been {diff:?} since we last received any reply surb from {sender}. Going to remove all stored entries...");
to_remove_surbs.push(*sender);
}
}
for map_ref in self.full_reply_storage.key_storage_ref().as_raw_iter() {
let (digest, reply_key) = map_ref.pair();
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
// purge that malformed data
let Ok(sent_at) = OffsetDateTime::from_unix_timestamp(reply_key.sent_at_timestamp) else {
error!("somehow our stored timestamp ({}) for one of our reply key is corrupted!. Going to remove all the entry", reply_key.sent_at_timestamp);
to_remove_keys.push(*digest);
continue;
};
let diff = now - sent_at;
if diff > self.config.max_reply_key_age {
debug!("it's been {diff:?} since we created this reply key. it's probably never going to get used, so we're going to purge it...");
to_remove_keys.push(*digest);
}
}
for to_remove in to_remove_surbs {
self.full_reply_storage
.surbs_storage_ref()
.remove(&to_remove);
}
for to_remove in to_remove_keys {
self.full_reply_storage.key_storage().remove(to_remove)
}
}
fn create_interval_stream(polling_rate: Duration) -> IntervalStream {
#[cfg(not(target_arch = "wasm32"))]
return tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(polling_rate));
#[cfg(target_arch = "wasm32")]
return gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32);
}
pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) {
debug!("Started ReplyController with graceful shutdown support");
let polling_rate = Duration::from_secs(5);
let mut stale_inspection = Self::create_interval_stream(polling_rate);
// this is in the order of hours/days so we don't have to poll it that often
let polling_rate = Duration::from_secs(self.config.max_reply_surb_age.as_secs() / 10);
let mut invalidation_inspection = Self::create_interval_stream(polling_rate);
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
log::trace!("ReplyController: Received shutdown");
},
req = self.request_receiver.next() => match req {
Some(req) => self.handle_request(req).await,
None => {
log::trace!("ReplyController: Stopping since channel closed");
break;
}
},
_ = stale_inspection.next() => {
self.inspect_stale_entries().await
},
_ = invalidation_inspection.next() => {
self.invalidate_old_data().await
}
}
}
assert!(shutdown.is_shutdown_poll());
log::debug!("ReplyController: Exiting");
}
}
@@ -1,43 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::backend::Empty;
use crate::client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend};
use async_trait::async_trait;
// well, right now we don't have the browser storage : (
// so we keep everything in memory
pub struct Backend {
empty: Empty,
}
impl Backend {
pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> Self {
Backend {
empty: Empty {
min_surb_threshold,
max_surb_threshold,
},
}
}
}
#[async_trait]
impl ReplyStorageBackend for Backend {
type StorageError = <Empty as ReplyStorageBackend>::StorageError;
async fn flush_surb_storage(
&mut self,
storage: &CombinedReplyStorage,
) -> Result<(), Self::StorageError> {
self.empty.flush_surb_storage(storage).await
}
async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> {
self.empty.init_fresh(fresh).await
}
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
self.empty.load_surb_storage().await
}
}
@@ -1,53 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("the provided database path doesn't have a filename defined")]
DatabasePathWithoutFilename { provided_path: PathBuf },
#[error("failed to rename our databse file - {source}")]
DatabaseRenameError {
#[source]
source: io::Error,
},
#[error("failed to rename our old databse file - {source}")]
DatabaseOldFileRemoveError {
#[source]
source: io::Error,
},
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
#[error("failed to connect to the underlying connection pool: {source}")]
DatabaseConnectionError {
#[source]
source: sqlx::error::Error,
},
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
#[error("The loaded data is inconsistent - it seems that on the last shutdown the client hasn't finished the data flush. You may have to remove the entire storage manually")]
IncompleteDataFlush,
#[error("data retrieved from the underlying storage is corrupted: {details}")]
CorruptedData {
details: String,
// err: Option<Box<dyn std::error::Error>>
},
}
@@ -1,257 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError;
use crate::client::replies::reply_storage::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use log::{error, info};
use sqlx::ConnectOptions;
use std::path::Path;
#[derive(Debug, Clone)]
pub(crate) struct StorageManager {
pub(crate) connection_pool: sqlx::SqlitePool,
}
// all SQL goes here
impl StorageManager {
pub(crate) async fn init<P: AsRef<Path>>(
database_path: P,
fresh: bool,
) -> Result<Self, StorageError> {
let mut opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(database_path)
.create_if_missing(fresh);
opts.disable_statement_logging();
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
Ok(pool) => pool,
Err(err) => {
error!("Failed to connect to SQLx database: {err}");
return Err(StorageError::DatabaseConnectionError { source: err });
}
};
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
return Err(err.into());
}
info!("Database migration finished!");
Ok(StorageManager { connection_pool })
}
#[allow(dead_code)]
pub(crate) async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&self.connection_pool)
.await
.map(|r| r.is_some())
}
pub(crate) async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub(crate) async fn set_previous_flush_timestamp(
&self,
timestamp: i64,
) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
pub(crate) async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
pub(crate) async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO sender_tag(recipient, tag) VALUES (?, ?);
"#,
stored_tag.recipient,
stored_tag.tag
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn insert_reply_key(
&self,
stored_reply_key: StoredReplyKey,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?);
"#,
stored_reply_key.key_digest,
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn insert_surb_sender(
&self,
stored_surb_sender: StoredSurbSender,
) -> Result<i64, sqlx::Error> {
let id = sqlx::query!(
r#"
INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?);
"#,
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
}
pub(crate) async fn get_reply_surbs(
&self,
sender_id: i64,
) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
sqlx::query_as!(
StoredReplySurb,
"SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
sender_id
)
.fetch_all(&self.connection_pool)
.await
}
pub(crate) async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;")
.execute(&self.connection_pool)
.await?;
sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn insert_reply_surb(
&self,
stored_reply_surb: StoredReplySurb,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
"#,
stored_reply_surb.reply_surb_sender_id,
stored_reply_surb.reply_surb
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn get_reply_surb_storage_metadata(
&self,
) -> Result<ReplySurbStorageMetadata, sqlx::Error> {
sqlx::query_as!(
ReplySurbStorageMetadata,
r#"
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&self.connection_pool)
.await
}
pub(crate) async fn insert_reply_surb_storage_metadata(
&self,
metadata: ReplySurbStorageMetadata,
) -> Result<(), sqlx::Error> {
sqlx::query!(r#"
INSERT INTO reply_surb_storage_metadata(min_reply_surb_threshold, max_reply_surb_threshold)
VALUES (?, ?);
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&self.connection_pool).await?;
Ok(())
}
}
@@ -1,336 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use self::error::StorageError;
use crate::client::replies::reply_storage::backend::fs_backend::manager::StorageManager;
use crate::client::replies::reply_storage::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbs;
use crate::client::replies::reply_storage::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
};
use async_trait::async_trait;
use log::{error, info, warn};
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
mod error;
mod manager;
mod models;
#[derive(Debug)]
pub struct Backend {
temporary_old_path: Option<PathBuf>,
database_path: PathBuf,
manager: StorageManager,
}
impl Backend {
const OLD_EXTENSION: &'static str = "old";
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
let owned_path: PathBuf = database_path.as_ref().into();
if owned_path.file_name().is_none() {
return Err(StorageError::DatabasePathWithoutFilename {
provided_path: owned_path,
});
}
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager: StorageManager::init(database_path, true).await?,
};
backend.manager.create_status_table().await?;
Ok(backend)
}
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
let owned_path: PathBuf = database_path.as_ref().into();
if owned_path.file_name().is_none() {
return Err(StorageError::DatabasePathWithoutFilename {
provided_path: owned_path,
});
}
let manager = StorageManager::init(database_path, false).await?;
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
return Err(StorageError::IncompleteDataFlush);
}
// the process has gone down without full graceful shutdown,
// meaning the database doesn't contain valid data anymore
// so we have to purge it
if manager.get_client_in_use_status().await? {
error!("the client hasn't undergone through graceful shutdown the last time it's gone down - we can't trust its reply surbs or stored encryption keys. They shall get purged");
manager.delete_all_reply_surb_data().await?;
manager.delete_all_reply_keys().await?;
}
let last_flush_timestamp = manager.get_previous_flush_timestamp().await?;
let last_flush = match OffsetDateTime::from_unix_timestamp(last_flush_timestamp) {
Ok(last_flush) => last_flush,
Err(err) => {
return Err(StorageError::CorruptedData {
details: format!("failed to parse stored timestamp - {err}"),
});
}
};
// in theory clients can use our reply surbs whenever they want, even a year in the future
// (assuming no key rotation has happened)
// but the way it's currently coded, everyone will purge old data
let since_last_flush = OffsetDateTime::now_utc() - last_flush;
if since_last_flush.whole_days() > 0 {
info!("it's been over {} days and {} hours since we last used our data store. our reply surbs are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours());
manager.delete_all_reply_surb_data().await?;
}
if since_last_flush.whole_days() > 1 {
info!("it's been over {} days and {} hours since we last used our data store. our reply keys are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours());
manager.delete_all_reply_keys().await?;
}
if since_last_flush.whole_days() > 2 {
info!("it's been over {} days and {} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours());
manager.delete_all_tags().await?;
}
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
{
format!("{existing_extension}.{}", Self::OLD_EXTENSION)
} else {
Self::OLD_EXTENSION.to_string()
};
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
fs::rename(&self.database_path, &temp_old)
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
self.temporary_old_path = Some(temp_old);
Ok(())
}
fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
fs::remove_file(old_path)
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
Ok(())
}
}
async fn start_storage_flush(&self) -> Result<(), StorageError> {
Ok(self.manager.set_flush_status(true).await?)
}
async fn end_storage_flush(&self) -> Result<(), StorageError> {
self.manager
.set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp())
.await?;
Ok(self.manager.set_flush_status(false).await?)
}
async fn start_client_use(&self) -> Result<(), StorageError> {
Ok(self.manager.set_client_in_use_status(true).await?)
}
async fn stop_client_use(&self) -> Result<(), StorageError> {
Ok(self.manager.set_client_in_use_status(false).await?)
}
async fn get_stored_tags(&self) -> Result<UsedSenderTags, StorageError> {
let stored = self.manager.get_tags().await?;
// stop at the first instance of corruption. if even a single entry is malformed,
// something weird has happened and we can't trust the rest of the data
let raw = stored
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(UsedSenderTags::from_raw(raw))
}
async fn dump_sender_tags(&self, tags: &UsedSenderTags) -> Result<(), StorageError> {
for map_ref in tags.as_raw_iter() {
let (recipient, tag) = map_ref.pair();
self.manager
.insert_tag(StoredSenderTag::new(*recipient, *tag))
.await?;
}
Ok(())
}
async fn get_stored_reply_keys(&self) -> Result<SentReplyKeys, StorageError> {
let stored = self.manager.get_reply_keys().await?;
// stop at the first instance of corruption. if even a single entry is malformed,
// something weird has happened and we can't trust the rest of the data
let raw = stored
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(SentReplyKeys::from_raw(raw))
}
async fn dump_sender_reply_keys(&self, reply_keys: &SentReplyKeys) -> Result<(), StorageError> {
for map_ref in reply_keys.as_raw_iter() {
let (digest, key) = map_ref.pair();
self.manager
.insert_reply_key(StoredReplyKey::new(*digest, *key))
.await?;
}
Ok(())
}
async fn get_stored_reply_surbs(&self) -> Result<ReceivedReplySurbsMap, StorageError> {
let surb_senders = self.manager.get_surb_senders().await?;
let metadata = self.get_reply_surb_storage_metadata().await?;
let mut received_surbs = Vec::with_capacity(surb_senders.len());
for sender in surb_senders {
let sender_id = sender.id;
let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) =
sender.try_into()?;
let stored_surbs = self
.manager
.get_reply_surbs(sender_id)
.await?
.into_iter()
.map(|raw| raw.try_into())
.collect::<Result<_, _>>()?;
received_surbs.push((
sender_tag,
ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp),
))
}
Ok(ReceivedReplySurbsMap::from_raw(
metadata.min_reply_surb_threshold as usize,
metadata.max_reply_surb_threshold as usize,
received_surbs,
))
}
async fn dump_reply_surbs(
&self,
reply_surbs: &ReceivedReplySurbsMap,
) -> Result<(), StorageError> {
for map_ref in reply_surbs.as_raw_iter() {
let (tag, received_surbs) = map_ref.pair();
let sender_id = self
.manager
.insert_surb_sender(StoredSurbSender::new(
*tag,
received_surbs.surbs_last_received_at(),
))
.await?;
for reply_surb in received_surbs.surbs_ref() {
self.manager
.insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb))
.await?
}
}
Ok(())
}
async fn get_reply_surb_storage_metadata(
&self,
) -> Result<ReplySurbStorageMetadata, StorageError> {
self.manager
.get_reply_surb_storage_metadata()
.await
.map_err(Into::into)
}
async fn dump_reply_surb_storage_metadata(
&self,
reply_surbs: &ReceivedReplySurbsMap,
) -> Result<(), StorageError> {
self.manager
.insert_reply_surb_storage_metadata(ReplySurbStorageMetadata::new(
reply_surbs.min_surb_threshold(),
reply_surbs.max_surb_threshold(),
))
.await
.map_err(Into::into)
}
}
#[async_trait]
impl ReplyStorageBackend for Backend {
type StorageError = error::StorageError;
async fn start_storage_session(&self) -> Result<(), Self::StorageError> {
self.start_client_use().await
}
async fn flush_surb_storage(
&mut self,
storage: &CombinedReplyStorage,
) -> Result<(), Self::StorageError> {
// close all connections (there should be none! and rename the file to contain .old extension)
self.rotate().await?;
self.start_storage_flush().await?;
self.dump_sender_tags(storage.tags_storage_ref()).await?;
self.dump_sender_reply_keys(storage.key_storage_ref())
.await?;
let surbs_ref = storage.surbs_storage_ref();
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old()?;
self.end_storage_flush().await
}
async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> {
// for now nothing more to do apart from dumping the metadata
self.dump_reply_surb_storage_metadata(fresh.surbs_storage_ref())
.await
}
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
let reply_keys = self.get_stored_reply_keys().await?;
let tags = self.get_stored_tags().await?;
let reply_surbs = self.get_stored_reply_surbs().await?;
Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags))
}
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
self.stop_client_use().await
}
}
@@ -1,185 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError;
use crate::client::replies::reply_storage::key_storage::UsedReplyKey;
use crypto::generic_array::typenum::Unsigned;
use crypto::Digest;
use nymsphinx::addressing::clients::{Recipient, RecipientBytes};
use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
use nymsphinx::params::ReplySurbKeyDigestAlgorithm;
#[derive(Debug, Clone)]
pub(crate) struct StoredSenderTag {
pub(crate) recipient: Vec<u8>,
pub(crate) tag: Vec<u8>,
}
impl StoredSenderTag {
pub(crate) fn new(recipient: RecipientBytes, tag: AnonymousSenderTag) -> StoredSenderTag {
StoredSenderTag {
recipient: recipient.to_vec(),
tag: tag.to_bytes().to_vec(),
}
}
}
impl TryFrom<StoredSenderTag> for (RecipientBytes, AnonymousSenderTag) {
type Error = StorageError;
fn try_from(value: StoredSenderTag) -> Result<Self, Self::Error> {
let recipient_len = value.recipient.len();
let Ok(recipient_bytes) = value.recipient.try_into() else {
return Err(StorageError::CorruptedData {
details: format!(
"the retrieved recipient has length of {recipient_len} while {} was expected",
Recipient::LEN
),
});
};
let tag_len = value.tag.len();
let Ok(sender_tag_bytes) = value.tag.try_into() else {
return Err(StorageError::CorruptedData {
details: format!(
"the retrieved sender tag has length of {tag_len} while {} was expected",
SENDER_TAG_SIZE
),
});
};
Ok((
recipient_bytes,
AnonymousSenderTag::from_bytes(sender_tag_bytes),
))
}
}
#[derive(Debug, Clone)]
pub(crate) struct StoredReplyKey {
pub(crate) key_digest: Vec<u8>,
pub(crate) reply_key: Vec<u8>,
pub(crate) sent_at_timestamp: i64,
}
impl StoredReplyKey {
pub(crate) fn new(key_digest: EncryptionKeyDigest, reply_key: UsedReplyKey) -> StoredReplyKey {
StoredReplyKey {
key_digest: key_digest.to_vec(),
reply_key: (*reply_key).to_bytes(),
sent_at_timestamp: reply_key.sent_at_timestamp,
}
}
}
impl TryFrom<StoredReplyKey> for (EncryptionKeyDigest, UsedReplyKey) {
type Error = StorageError;
fn try_from(value: StoredReplyKey) -> Result<Self, Self::Error> {
let expected_reply_key_digest_size = ReplySurbKeyDigestAlgorithm::output_size();
let reply_key_digest_size = value.key_digest.len();
let Some(digest) = EncryptionKeyDigest::from_exact_iter(value.key_digest) else {
return Err(StorageError::CorruptedData {
details: format!(
"the reply surb digest has length of {reply_key_digest_size} while {expected_reply_key_digest_size} was expected",
),
});
};
let reply_key_len = value.reply_key.len();
let Ok(reply_key) = SurbEncryptionKey::try_from_bytes(&value.reply_key) else {
return Err(StorageError::CorruptedData {
details: format!(
"the reply key has length of {reply_key_len} while {} was expected",
SurbEncryptionKeySize::USIZE
),
});
};
Ok((
digest,
UsedReplyKey::new(reply_key, value.sent_at_timestamp),
))
}
}
pub(crate) struct StoredSurbSender {
pub(crate) id: i64,
pub(crate) tag: Vec<u8>,
pub(crate) last_sent_timestamp: i64,
}
impl StoredSurbSender {
pub(crate) fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self {
StoredSurbSender {
// for the purposes of STORING data,
// we ignore that field anyway
id: 0,
tag: tag.to_bytes().to_vec(),
last_sent_timestamp,
}
}
}
impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
type Error = StorageError;
fn try_from(value: StoredSurbSender) -> Result<Self, Self::Error> {
let tag_len = value.tag.len();
let Ok(sender_tag_bytes) = value.tag.try_into() else {
return Err(StorageError::CorruptedData {
details: format!(
"the retrieved sender tag has length of {tag_len} while {} was expected",
SENDER_TAG_SIZE
),
});
};
Ok((
AnonymousSenderTag::from_bytes(sender_tag_bytes),
value.last_sent_timestamp,
))
}
}
pub(crate) struct StoredReplySurb {
pub(crate) reply_surb_sender_id: i64,
pub(crate) reply_surb: Vec<u8>,
}
impl StoredReplySurb {
pub(crate) fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
StoredReplySurb {
reply_surb_sender_id,
reply_surb: reply_surb.to_bytes(),
}
}
}
impl TryFrom<StoredReplySurb> for ReplySurb {
type Error = StorageError;
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
})
}
}
#[derive(Copy, Clone)]
pub(crate) struct ReplySurbStorageMetadata {
pub(crate) min_reply_surb_threshold: u32,
pub(crate) max_reply_surb_threshold: u32,
}
impl ReplySurbStorageMetadata {
pub(crate) fn new(min_reply_surb_threshold: usize, max_reply_surb_threshold: usize) -> Self {
Self {
min_reply_surb_threshold: min_reply_surb_threshold as u32,
max_reply_surb_threshold: max_reply_surb_threshold as u32,
}
}
}
@@ -1,78 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::CombinedReplyStorage;
use async_trait::async_trait;
use std::error::Error;
use thiserror::Error;
#[cfg(target_arch = "wasm32")]
pub mod browser_backend;
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub mod fs_backend;
// #[cfg(all(test, feature = "std"))]
// third case: node with actual filesystem
#[derive(Debug, Error)]
#[error("no information provided")]
pub struct UndefinedError;
pub struct Empty {
// we need to keep 'basic' metadata here to "load" the CombinedReplyStorage
min_surb_threshold: usize,
max_surb_threshold: usize,
}
#[async_trait]
impl ReplyStorageBackend for Empty {
type StorageError = UndefinedError;
async fn flush_surb_storage(
&mut self,
_storage: &CombinedReplyStorage,
) -> Result<(), Self::StorageError> {
Ok(())
}
async fn init_fresh(
&mut self,
_fresh: &CombinedReplyStorage,
) -> Result<(), Self::StorageError> {
Ok(())
}
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
Ok(CombinedReplyStorage::new(
self.min_surb_threshold,
self.max_surb_threshold,
))
}
}
#[async_trait]
pub trait ReplyStorageBackend: Sized {
type StorageError: Error + 'static;
async fn start_storage_session(&self) -> Result<(), Self::StorageError> {
Ok(())
}
// reply keys and surbs would need additional field set when data is loaded
// so if there's some failure, we'd trash it all
async fn flush_surb_storage(
&mut self,
storage: &CombinedReplyStorage,
) -> Result<(), Self::StorageError>;
/// The purpose of this call is to save any metadata that might be present.
/// (such as surb thresholds)
async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>;
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError>;
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
Ok(())
}
}
@@ -1,60 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
#[derive(Debug, Clone)]
pub struct CombinedReplyStorage {
sent_reply_keys: SentReplyKeys,
received_reply_surbs: ReceivedReplySurbsMap,
used_tags: UsedSenderTags,
}
impl CombinedReplyStorage {
pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> CombinedReplyStorage {
CombinedReplyStorage {
sent_reply_keys: SentReplyKeys::new(),
received_reply_surbs: ReceivedReplySurbsMap::new(
min_surb_threshold,
max_surb_threshold,
),
used_tags: UsedSenderTags::new(),
}
}
pub fn load(
sent_reply_keys: SentReplyKeys,
received_reply_surbs: ReceivedReplySurbsMap,
used_tags: UsedSenderTags,
) -> Self {
CombinedReplyStorage {
sent_reply_keys,
received_reply_surbs,
used_tags,
}
}
pub fn key_storage(&self) -> SentReplyKeys {
self.sent_reply_keys.clone()
}
pub fn surbs_storage(&self) -> ReceivedReplySurbsMap {
self.received_reply_surbs.clone()
}
pub fn tags_storage(&self) -> UsedSenderTags {
self.used_tags.clone()
}
pub fn key_storage_ref(&self) -> &SentReplyKeys {
&self.sent_reply_keys
}
pub fn surbs_storage_ref(&self) -> &ReceivedReplySurbsMap {
&self.received_reply_surbs
}
pub fn tags_storage_ref(&self) -> &UsedSenderTags {
&self.used_tags
}
}
@@ -1,86 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use dashmap::iter::Iter;
use dashmap::DashMap;
use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
use nymsphinx::anonymous_replies::SurbEncryptionKey;
use std::ops::Deref;
use std::sync::Arc;
use time::OffsetDateTime;
#[derive(Debug, Clone)]
pub struct SentReplyKeys {
inner: Arc<SentReplyKeysInner>,
}
#[derive(Debug)]
struct SentReplyKeysInner {
data: DashMap<EncryptionKeyDigest, UsedReplyKey>,
}
impl SentReplyKeys {
pub(crate) fn new() -> SentReplyKeys {
SentReplyKeys {
inner: Arc::new(SentReplyKeysInner {
data: DashMap::new(),
}),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn from_raw(raw: Vec<(EncryptionKeyDigest, UsedReplyKey)>) -> SentReplyKeys {
SentReplyKeys {
inner: Arc::new(SentReplyKeysInner {
data: raw.into_iter().collect(),
}),
}
}
pub(crate) fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, UsedReplyKey> {
self.inner.data.iter()
}
pub(crate) fn insert_multiple(&self, keys: Vec<SurbEncryptionKey>) {
let now = OffsetDateTime::now_utc().unix_timestamp();
for key in keys {
self.insert(UsedReplyKey::new(key, now))
}
}
pub(crate) fn insert(&self, key: UsedReplyKey) {
self.inner.data.insert(key.compute_digest(), key);
}
pub(crate) fn try_pop(&self, digest: EncryptionKeyDigest) -> Option<UsedReplyKey> {
self.inner.data.remove(&digest).map(|(_k, v)| v)
}
pub(crate) fn remove(&self, digest: EncryptionKeyDigest) {
self.inner.data.remove(&digest);
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct UsedReplyKey {
key: SurbEncryptionKey,
// the purpose of this field is to perform invalidation at relatively very long intervals
pub(crate) sent_at_timestamp: i64,
}
impl UsedReplyKey {
pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self {
UsedReplyKey {
key,
sent_at_timestamp,
}
}
}
impl Deref for UsedReplyKey {
type Target = SurbEncryptionKey;
fn deref(&self) -> &Self::Target {
&self.key
}
}
@@ -1,64 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use crate::client::replies::reply_storage::combined::CombinedReplyStorage;
pub use crate::client::replies::reply_storage::key_storage::SentReplyKeys;
pub use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbsMap;
pub use crate::client::replies::reply_storage::tag_storage::UsedSenderTags;
pub use backend::*;
mod backend;
mod combined;
mod key_storage;
mod surb_storage;
mod tag_storage;
// only really exists to get information about shutdown and save data to the backing storage
pub struct PersistentReplyStorage<T = backend::Empty>
where
T: ReplyStorageBackend,
{
backend: T,
}
impl<T> PersistentReplyStorage<T>
where
T: ReplyStorageBackend + Send + Sync,
{
pub fn new(backend: T) -> Self {
PersistentReplyStorage { backend }
}
pub async fn load_state_from_backend(&self) -> Result<CombinedReplyStorage, T::StorageError> {
self.backend.load_surb_storage().await
}
// this will have to get enabled after merging develop
pub async fn flush_on_shutdown(
mut self,
mem_state: CombinedReplyStorage,
mut shutdown: task::ShutdownListener,
) {
use log::{debug, error, info, warn};
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
error!("failed to start the storage session - {err}");
return;
}
shutdown.recv().await;
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
warn!("you MUST NOT forcefully shutdown now or you risk data corruption!");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
info!("Data flush is complete")
}
if let Err(err) = self.backend.stop_storage_session().await {
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
}
}
}
@@ -1,278 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use dashmap::iter::Iter;
use dashmap::DashMap;
use log::trace;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use time::OffsetDateTime;
#[derive(Debug, Clone)]
pub struct ReceivedReplySurbsMap {
inner: Arc<ReceivedReplySurbsMapInner>,
}
#[derive(Debug)]
struct ReceivedReplySurbsMapInner {
data: DashMap<AnonymousSenderTag, ReceivedReplySurbs>,
// the minimum amount of surbs that have to be kept in storage for requests for more surbs
min_surb_threshold: AtomicUsize,
// the maximum amount of surbs that we want to keep in storage so that we don't over-request them
max_surb_threshold: AtomicUsize,
}
impl ReceivedReplySurbsMap {
pub(crate) fn new(
min_surb_threshold: usize,
max_surb_threshold: usize,
) -> ReceivedReplySurbsMap {
ReceivedReplySurbsMap {
inner: Arc::new(ReceivedReplySurbsMapInner {
data: DashMap::new(),
min_surb_threshold: AtomicUsize::new(min_surb_threshold),
max_surb_threshold: AtomicUsize::new(max_surb_threshold),
}),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn from_raw(
min_surb_threshold: usize,
max_surb_threshold: usize,
raw: Vec<(AnonymousSenderTag, ReceivedReplySurbs)>,
) -> ReceivedReplySurbsMap {
ReceivedReplySurbsMap {
inner: Arc::new(ReceivedReplySurbsMapInner {
data: raw.into_iter().collect(),
min_surb_threshold: AtomicUsize::new(min_surb_threshold),
max_surb_threshold: AtomicUsize::new(max_surb_threshold),
}),
}
}
pub(crate) fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> {
self.inner.data.iter()
}
pub(crate) fn remove(&self, target: &AnonymousSenderTag) {
self.inner.data.remove(target);
}
pub(crate) fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) {
if let Some(mut entry) = self.inner.data.get_mut(target) {
entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
}
}
pub(crate) fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option<i64> {
self.inner
.data
.get(target)
.map(|e| e.surbs_last_received_at())
}
pub(crate) fn pending_reception(&self, target: &AnonymousSenderTag) -> u32 {
self.inner
.data
.get(target)
.map(|e| e.pending_reception())
.unwrap_or_default()
}
pub(crate) fn increment_pending_reception(
&self,
target: &AnonymousSenderTag,
amount: u32,
) -> Option<u32> {
self.inner
.data
.get_mut(target)
.map(|mut e| e.increment_pending_reception(amount))
}
pub(crate) fn decrement_pending_reception(
&self,
target: &AnonymousSenderTag,
amount: u32,
) -> Option<u32> {
self.inner
.data
.get_mut(target)
.map(|mut e| e.decrement_pending_reception(amount))
}
pub(crate) fn reset_pending_reception(&self, target: &AnonymousSenderTag) {
if let Some(mut e) = self.inner.data.get_mut(target) {
e.reset_pending_reception()
}
}
pub(crate) fn min_surb_threshold(&self) -> usize {
self.inner.min_surb_threshold.load(Ordering::Relaxed)
}
pub(crate) fn max_surb_threshold(&self) -> usize {
self.inner.max_surb_threshold.load(Ordering::Relaxed)
}
pub(crate) fn available_surbs(&self, target: &AnonymousSenderTag) -> usize {
self.inner
.data
.get(target)
.map(|entry| entry.items_left())
.unwrap_or_default()
}
pub(crate) fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool {
self.inner.data.contains_key(target)
}
pub(crate) fn get_reply_surbs(
&self,
target: &AnonymousSenderTag,
amount: usize,
) -> (Option<Vec<ReplySurb>>, usize) {
if let Some(mut entry) = self.inner.data.get_mut(target) {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() + amount {
(None, surbs_left)
} else {
entry.get_reply_surbs(amount)
}
} else {
(None, 0)
}
}
pub(crate) fn get_reply_surb_ignoring_threshold(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurb>, usize)> {
self.inner
.data
.get_mut(target)
.map(|mut s| s.get_reply_surb())
}
pub(crate) fn get_reply_surb(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurb>, usize)> {
self.inner.data.get_mut(target).map(|mut entry| {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() {
(None, surbs_left)
} else {
entry.get_reply_surb()
}
})
}
pub(crate) fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
&self,
target: &AnonymousSenderTag,
surbs: I,
) {
if let Some(mut existing_data) = self.inner.data.get_mut(target) {
existing_data.insert_reply_surbs(surbs)
} else {
let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect());
self.inner.data.insert(*target, new_entry);
}
}
}
#[derive(Debug)]
pub(crate) struct ReceivedReplySurbs {
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
// so we could invalidate entries from the previous key rotations
data: VecDeque<ReplySurb>,
pending_reception: u32,
surbs_last_received_at_timestamp: i64,
}
impl ReceivedReplySurbs {
fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
ReceivedReplySurbs {
data: initial_surbs,
pending_reception: 0,
surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn new_retrieved(
surbs: Vec<ReplySurb>,
surbs_last_received_at_timestamp: i64,
) -> ReceivedReplySurbs {
ReceivedReplySurbs {
data: surbs.into(),
pending_reception: 0,
surbs_last_received_at_timestamp,
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
&self.data
}
pub(crate) fn surbs_last_received_at(&self) -> i64 {
self.surbs_last_received_at_timestamp
}
pub(crate) fn pending_reception(&self) -> u32 {
self.pending_reception
}
pub(crate) fn increment_pending_reception(&mut self, amount: u32) -> u32 {
self.pending_reception += amount;
self.pending_reception
}
pub(crate) fn decrement_pending_reception(&mut self, amount: u32) -> u32 {
self.pending_reception = self.pending_reception.saturating_sub(amount);
self.pending_reception
}
pub(crate) fn reset_pending_reception(&mut self) {
self.pending_reception = 0;
}
pub(crate) fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
if self.items_left() < amount {
(None, self.items_left())
} else {
let surbs = self.data.drain(..amount).collect();
(Some(surbs), self.items_left())
}
}
pub(crate) fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
(self.pop_surb(), self.items_left())
}
fn pop_surb(&mut self) -> Option<ReplySurb> {
self.data.pop_front()
}
fn items_left(&self) -> usize {
self.data.len()
}
// realistically we're always going to be getting multiple surbs at once
pub(crate) fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
trace!("storing {} surbs in the storage", v.len());
self.data.append(&mut v);
self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
trace!("we now have {} surbs!", self.data.len());
}
}
@@ -1,59 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use nymsphinx::addressing::clients::{Recipient, RecipientBytes};
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::sync::Arc;
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
use dashmap::iter::Iter;
#[derive(Debug, Clone)]
pub struct UsedSenderTags {
inner: Arc<UsedSenderTagsInner>,
}
#[derive(Debug)]
struct UsedSenderTagsInner {
data: DashMap<RecipientBytes, AnonymousSenderTag>,
}
impl UsedSenderTags {
pub(crate) fn new() -> UsedSenderTags {
UsedSenderTags {
inner: Arc::new(UsedSenderTagsInner {
data: DashMap::new(),
}),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags {
UsedSenderTags {
inner: Arc::new(UsedSenderTagsInner {
data: raw.into_iter().collect(),
}),
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub(crate) fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> {
self.inner.data.iter()
}
pub(crate) fn insert_new(&self, recipient: &Recipient, tag: AnonymousSenderTag) {
self.inner.data.insert(recipient.to_bytes(), tag);
}
pub(crate) fn try_get_existing(&self, recipient: &Recipient) -> Option<AnonymousSenderTag> {
self.inner
.data
.get(&recipient.to_bytes())
.map(|r| *r.value())
}
pub(crate) fn exists(&self, recipient: &Recipient) -> bool {
self.inner.data.contains_key(&recipient.to_bytes())
}
}
@@ -0,0 +1,94 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crypto::generic_array::typenum::Unsigned;
use log::*;
use nymsphinx::anonymous_replies::{
encryption_key::EncryptionKeyDigest, SurbEncryptionKey, SurbEncryptionKeySize,
};
use std::path::Path;
#[derive(Debug)]
pub enum ReplyKeyStorageError {
DbReadError(sled::Error),
DbWriteError(sled::Error),
DbOpenError(sled::Error),
}
/// Permanent storage for keys in all sent [`ReplySURB`]
///
/// Each sent out [`ReplySURB`] has a new key associated with it that is going to be used for
/// payload encryption. In order to -decrypt whatever reply we receive, we need to know which
/// key to use for that purpose. We do it based on received `H(t)` which has to be included
/// with each reply.
/// Moreover, there is no restriction when the [`ReplySURB`] might get used so we need to
/// have a permanent storage for all the keys that we might ever see in the future.
#[derive(Debug, Clone)]
pub struct ReplyKeyStorage {
db: sled::Db,
}
impl ReplyKeyStorage {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, ReplyKeyStorageError> {
let db = match sled::open(path) {
Err(e) => return Err(ReplyKeyStorageError::DbOpenError(e)),
Ok(db) => db,
};
Ok(ReplyKeyStorage { db })
}
fn read_encryption_key(&self, raw_key: sled::IVec) -> SurbEncryptionKey {
let key_bytes_ref = raw_key.as_ref();
// if this fails it means we have some database corruption and we
// absolutely can't continue
if key_bytes_ref.len() != SurbEncryptionKeySize::USIZE {
error!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH");
panic!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH");
}
// this can only fail if the bytes have invalid length but we already asserted it
SurbEncryptionKey::try_from_bytes(key_bytes_ref).unwrap()
}
// TOOD: perhaps we could also store some part of original message here too?
pub fn insert_encryption_key(
&mut self,
encryption_key: SurbEncryptionKey,
) -> Result<(), ReplyKeyStorageError> {
let digest = encryption_key.compute_digest();
let insertion_result = match self.db.insert(digest, encryption_key.to_bytes()) {
Err(e) => Err(ReplyKeyStorageError::DbWriteError(e)),
Ok(existing_key) => {
if existing_key.is_some() {
panic!("HASH COLLISION DETECTED")
};
Ok(())
}
};
// TODO: perhaps we could implement some batching mechanism to avoid frequent flushes?
self.db.flush().unwrap();
insertion_result
}
// Once we use key once, we do not expect to use it again
pub fn get_and_remove_encryption_key(
&self,
key_digest: EncryptionKeyDigest,
) -> Result<Option<SurbEncryptionKey>, ReplyKeyStorageError> {
let removal_result = match self.db.remove(key_digest) {
Err(e) => Err(ReplyKeyStorageError::DbReadError(e)),
Ok(existing_key) => {
Ok(existing_key.map(|existing_key| self.read_encryption_key(existing_key)))
}
};
// TODO: not sure how to feel about flushing it every single time here...
// same with insertion
self.db.flush().unwrap();
removal_result
}
}
@@ -1,8 +1,6 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
use futures::StreamExt;
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::params::DEFAULT_NUM_MIX_HOPS;
@@ -10,9 +8,12 @@ use rand::seq::SliceRandom;
use rand::thread_rng;
use std::ops::Deref;
use std::sync::Arc;
use std::time;
use std::time::Duration;
use task::ShutdownListener;
use tokio::sync::{RwLock, RwLockReadGuard};
use topology::{nym_topology_from_detailed, NymTopology, NymTopologyError};
use tokio::task::JoinHandle;
use topology::{nym_topology_from_detailed, NymTopology};
use url::Url;
// I'm extremely curious why compiler NEVER complained about lack of Debug here before
@@ -54,36 +55,27 @@ impl<'a> TopologyReadPermit<'a> {
&'a self,
ack_recipient: &Recipient,
packet_recipient: Option<&Recipient>,
) -> Result<&'a NymTopology, NymTopologyError> {
// 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through?
let topology = self
.permit
.as_ref()
.as_ref()
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
// 2. does it have any mixnode at all?
// 3. does it have any gateways at all?
// 4. does it have a mixnode on each layer?
topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?;
// 5. does it contain OUR gateway (so that we could create an ack packet)?
if !topology.gateway_exists(ack_recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: ack_recipient.gateway().to_base58_string(),
});
}
// 6. for our target recipient, does it contain THEIR gateway (so that we could create
if let Some(recipient) = packet_recipient {
if !topology.gateway_exists(recipient.gateway()) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: recipient.gateway().to_base58_string(),
});
) -> Option<&'a NymTopology> {
// Note: implicit deref with Deref for TopologyReadPermit is happening here
let topology_ref_option = self.permit.as_ref();
match topology_ref_option {
None => None,
Some(topology_ref) => {
// see if it's possible to route the packet to both gateways
if !topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS)
|| !topology_ref.gateway_exists(ack_recipient.gateway())
|| if let Some(packet_recipient) = packet_recipient {
!topology_ref.gateway_exists(packet_recipient.gateway())
} else {
false
}
{
None
} else {
Some(topology_ref)
}
}
}
Ok(topology)
}
}
@@ -121,10 +113,10 @@ impl TopologyAccessor {
// only used by the client at startup to get a slightly more reasonable error message
// (currently displays as unused because health checker is disabled due to required changes)
pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> {
pub async fn is_routable(&self) -> bool {
match &self.inner.read().await.0 {
None => Err(NymTopologyError::EmptyNetworkTopology),
Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
None => false,
Some(ref topology) => topology.can_construct_path_through(DEFAULT_NUM_MIX_HOPS),
}
}
}
@@ -137,14 +129,14 @@ impl Default for TopologyAccessor {
pub struct TopologyRefresherConfig {
validator_api_urls: Vec<Url>,
refresh_rate: Duration,
refresh_rate: time::Duration,
client_version: String,
}
impl TopologyRefresherConfig {
pub fn new(
validator_api_urls: Vec<Url>,
refresh_rate: Duration,
refresh_rate: time::Duration,
client_version: String,
) -> Self {
TopologyRefresherConfig {
@@ -156,7 +148,7 @@ impl TopologyRefresherConfig {
}
pub struct TopologyRefresher {
validator_client: validator_client::client::ApiClient,
validator_client: validator_client::ApiClient,
client_version: String,
validator_api_urls: Vec<Url>,
@@ -172,9 +164,7 @@ impl TopologyRefresher {
cfg.validator_api_urls.shuffle(&mut thread_rng());
TopologyRefresher {
validator_client: validator_client::client::ApiClient::new(
cfg.validator_api_urls[0].clone(),
),
validator_client: validator_client::ApiClient::new(cfg.validator_api_urls[0].clone()),
client_version: cfg.client_version,
validator_api_urls: cfg.validator_api_urls,
topology_accessor,
@@ -261,7 +251,7 @@ impl TopologyRefresher {
let mixnodes = match self.validator_client.get_cached_active_mixnodes().await {
Err(err) => {
error!("failed to get network mixnodes - {err}");
error!("failed to get network mixnodes - {}", err);
return None;
}
Ok(mixes) => mixes,
@@ -269,7 +259,7 @@ impl TopologyRefresher {
let gateways = match self.validator_client.get_cached_gateways().await {
Err(err) => {
error!("failed to get network gateways - {err}");
error!("failed to get network gateways - {}", err);
return None;
}
Ok(gateways) => gateways,
@@ -310,26 +300,15 @@ impl TopologyRefresher {
.await;
}
pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> {
self.topology_accessor.ensure_is_routable().await
pub async fn is_topology_routable(&self) -> bool {
self.topology_accessor.is_routable().await
}
pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
self.refresh_rate,
));
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
pub fn start(mut self, mut shutdown: ShutdownListener) -> JoinHandle<()> {
tokio::spawn(async move {
while !shutdown.is_shutdown() {
tokio::select! {
_ = interval.next() => {
_ = tokio::time::sleep(self.refresh_rate) => {
self.refresh().await;
},
_ = shutdown.recv() => {
@@ -337,7 +316,7 @@ impl TopologyRefresher {
},
}
}
shutdown.recv_timeout().await;
assert!(shutdown.is_shutdown_poll());
log::debug!("TopologyRefresher: Exiting");
})
}
+53 -239
View File
@@ -2,16 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use config::NymConfig;
use nymsphinx::params::PacketSize;
use serde::{Deserialize, Serialize};
use std::marker::PhantomData;
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
pub mod persistence;
pub const MISSING_VALUE: &str = "MISSING VALUE";
@@ -30,33 +26,11 @@ const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_00
// bandwidth bridging protocol, we can come back to a smaller timeout value
const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
// reply-surbs related:
// define when to request
// clients/client-core/src/client/replies/reply_storage/surb_storage.rs
const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10;
const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200;
// define how much to request at once
// clients/client-core/src/client/replies/reply_controller.rs
const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10;
const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100;
const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500;
const DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD: Duration = Duration::from_secs(10);
// 12 hours
const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60);
// 24 hours
const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60);
pub fn missing_string_value() -> String {
MISSING_VALUE.to_string()
}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config<T> {
client: Client<T>,
@@ -64,21 +38,17 @@ pub struct Config<T> {
#[serde(default)]
logging: Logging,
#[serde(default)]
debug: DebugConfig,
debug: Debug,
}
impl<T> Config<T> {
pub fn new<S: Into<String>>(id: S) -> Self
where
T: NymConfig,
{
Config::default().with_id(id)
impl<T: NymConfig> Config<T> {
pub fn new<S: Into<String>>(id: S) -> Self {
let mut cfg = Config::default();
cfg.with_id(id);
cfg
}
pub fn with_id<S: Into<String>>(mut self, id: S) -> Self
where
T: NymConfig,
{
pub fn with_id<S: Into<String>>(&mut self, id: S) {
let id = id.into();
// identity key setting
@@ -122,9 +92,14 @@ impl<T> Config<T> {
self.client.ack_key_file = self::Client::<T>::default_ack_key_file(&id);
}
if self.client.reply_surb_database_path.as_os_str().is_empty() {
self.client.reply_surb_database_path =
self::Client::<T>::default_reply_surb_database_path(&id);
if self
.client
.reply_encryption_key_store_path
.as_os_str()
.is_empty()
{
self.client.reply_encryption_key_store_path =
self::Client::<T>::default_reply_encryption_key_store_path(&id);
}
if self.client.database_path.as_os_str().is_empty() {
@@ -132,14 +107,13 @@ impl<T> Config<T> {
}
self.client.id = id;
self
}
pub fn with_disabled_credentials(&mut self, disabled_credentials_mode: bool) {
self.client.disabled_credentials_mode = disabled_credentials_mode;
}
pub fn with_gateway_endpoint(&mut self, gateway_endpoint: GatewayEndpointConfig) {
pub fn with_gateway_endpoint(&mut self, gateway_endpoint: GatewayEndpoint) {
self.client.gateway_endpoint = gateway_endpoint;
}
@@ -147,25 +121,14 @@ impl<T> Config<T> {
self.client.gateway_endpoint.gateway_id = id.into();
}
pub fn set_custom_validators(&mut self, validator_urls: Vec<Url>) {
self.client.validator_urls = validator_urls;
}
pub fn set_custom_validator_apis(&mut self, validator_api_urls: Vec<Url>) {
self.client.validator_api_urls = validator_api_urls;
}
pub fn set_high_default_traffic_volume(&mut self) {
self.debug.average_packet_delay = Duration::from_millis(10);
// basically don't really send cover messages
self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000);
// 250 "real" messages / s
self.debug.message_sending_average_delay = Duration::from_millis(4);
}
pub fn set_no_cover_traffic(&mut self) {
self.debug.disable_loop_cover_traffic_stream = true;
self.debug.disable_main_poisson_packet_distribution = true;
self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000); // basically don't really send cover messages
self.debug.message_sending_average_delay = Duration::from_millis(4); // 250 "real" messages / s
}
pub fn set_custom_version(&mut self, version: &str) {
@@ -204,12 +167,12 @@ impl<T> Config<T> {
self.client.gateway_shared_key_file.clone()
}
pub fn get_ack_key_file(&self) -> PathBuf {
self.client.ack_key_file.clone()
pub fn get_reply_encryption_key_store_path(&self) -> PathBuf {
self.client.reply_encryption_key_store_path.clone()
}
pub fn get_validator_endpoints(&self) -> Vec<Url> {
self.client.validator_urls.clone()
pub fn get_ack_key_file(&self) -> PathBuf {
self.client.ack_key_file.clone()
}
pub fn get_validator_api_endpoints(&self) -> Vec<Url> {
@@ -228,11 +191,7 @@ impl<T> Config<T> {
self.client.gateway_endpoint.gateway_listener.clone()
}
pub fn get_gateway_endpoint_config(&self) -> &GatewayEndpointConfig {
&self.client.gateway_endpoint
}
pub fn get_gateway_endpoint(&self) -> &GatewayEndpointConfig {
pub fn get_gateway_endpoint(&self) -> &GatewayEndpoint {
&self.client.gateway_endpoint
}
@@ -240,19 +199,7 @@ impl<T> Config<T> {
self.client.database_path.clone()
}
pub fn get_reply_surb_database_path(&self) -> PathBuf {
self.client.reply_surb_database_path.clone()
}
pub fn get_version(&self) -> &str {
&self.client.version
}
// Debug getters
pub fn get_debug_config(&self) -> &DebugConfig {
&self.debug
}
pub fn get_average_packet_delay(&self) -> Duration {
self.debug.average_packet_delay
}
@@ -289,48 +236,8 @@ impl<T> Config<T> {
self.debug.topology_resolution_timeout
}
pub fn get_disabled_loop_cover_traffic_stream(&self) -> bool {
self.debug.disable_loop_cover_traffic_stream
}
pub fn get_disabled_main_poisson_packet_distribution(&self) -> bool {
self.debug.disable_main_poisson_packet_distribution
}
pub fn get_use_extended_packet_size(&self) -> Option<ExtendedPacketSize> {
self.debug.use_extended_packet_size
}
pub fn get_minimum_reply_surb_storage_threshold(&self) -> usize {
self.debug.minimum_reply_surb_storage_threshold
}
pub fn get_maximum_reply_surb_storage_threshold(&self) -> usize {
self.debug.maximum_reply_surb_storage_threshold
}
pub fn get_minimum_reply_surb_request_size(&self) -> u32 {
self.debug.minimum_reply_surb_request_size
}
pub fn get_maximum_reply_surb_request_size(&self) -> u32 {
self.debug.maximum_reply_surb_request_size
}
pub fn get_maximum_allowed_reply_surb_request_size(&self) -> u32 {
self.debug.maximum_allowed_reply_surb_request_size
}
pub fn get_maximum_reply_surb_waiting_period(&self) -> Duration {
self.debug.maximum_reply_surb_waiting_period
}
pub fn get_maximum_reply_surb_age(&self) -> Duration {
self.debug.maximum_reply_surb_age
}
pub fn get_maximum_reply_key_age(&self) -> Duration {
self.debug.maximum_reply_key_age
pub fn get_version(&self) -> &str {
&self.client.version
}
}
@@ -345,8 +252,7 @@ impl<T: NymConfig> Default for Config<T> {
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))]
pub struct GatewayEndpointConfig {
pub struct GatewayEndpoint {
/// gateway_id specifies ID of the gateway to which the client should send messages.
/// If initially omitted, a random gateway will be chosen from the available topology.
pub gateway_id: String,
@@ -358,26 +264,10 @@ pub struct GatewayEndpointConfig {
pub gateway_listener: String,
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl GatewayEndpointConfig {
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))]
pub fn new(
gateway_id: String,
gateway_owner: String,
gateway_listener: String,
) -> GatewayEndpointConfig {
GatewayEndpointConfig {
gateway_id,
gateway_owner,
gateway_listener,
}
}
}
impl From<topology::gateway::Node> for GatewayEndpointConfig {
fn from(node: topology::gateway::Node) -> GatewayEndpointConfig {
impl From<topology::gateway::Node> for GatewayEndpoint {
fn from(node: topology::gateway::Node) -> GatewayEndpoint {
let gateway_listener = node.clients_address();
GatewayEndpointConfig {
GatewayEndpoint {
gateway_id: node.identity_key.to_base58_string(),
gateway_owner: node.owner,
gateway_listener,
@@ -385,7 +275,7 @@ impl From<topology::gateway::Node> for GatewayEndpointConfig {
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct Client<T> {
/// Version of the client for which this configuration was created.
#[serde(default = "missing_string_value")]
@@ -399,10 +289,6 @@ pub struct Client<T> {
#[serde(default)]
disabled_credentials_mode: bool,
/// Addresses to nymd validators via which the client can communicate with the chain.
#[serde(default)]
validator_urls: Vec<Url>,
/// Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls: Vec<Url>,
@@ -426,15 +312,16 @@ pub struct Client<T> {
/// acknowledgement so that nobody besides the client knows which packet it refers to.
ack_key_file: PathBuf,
/// Full path to file containing reply encryption keys of all reply-SURBs we have ever
/// sent but not received back.
reply_encryption_key_store_path: PathBuf,
/// Information regarding how the client should send data to gateway.
gateway_endpoint: GatewayEndpointConfig,
gateway_endpoint: GatewayEndpoint,
/// Path to the database containing bandwidth credentials of this client.
database_path: PathBuf,
/// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
reply_surb_database_path: PathBuf,
/// nym_home_directory specifies absolute path to the home nym Clients directory.
/// It is expected to use default value and hence .toml file should not redefine this field.
nym_root_directory: PathBuf,
@@ -450,7 +337,6 @@ impl<T: NymConfig> Default for Client<T> {
version: env!("CARGO_PKG_VERSION").to_string(),
id: "".to_string(),
disabled_credentials_mode: true,
validator_urls: vec![],
validator_api_urls: vec![],
private_identity_key_file: Default::default(),
public_identity_key_file: Default::default(),
@@ -458,9 +344,9 @@ impl<T: NymConfig> Default for Client<T> {
public_encryption_key_file: Default::default(),
gateway_shared_key_file: Default::default(),
ack_key_file: Default::default(),
reply_encryption_key_store_path: Default::default(),
gateway_endpoint: Default::default(),
database_path: Default::default(),
reply_surb_database_path: Default::default(),
nym_root_directory: T::default_root_directory(),
super_struct: Default::default(),
}
@@ -492,129 +378,78 @@ impl<T: NymConfig> Client<T> {
T::default_data_directory(Some(id)).join("ack_key.pem")
}
fn default_reply_surb_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("persistent_reply_store.sqlite")
fn default_reply_encryption_key_store_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("reply_key_store")
}
fn default_database_path(id: &str) -> PathBuf {
T::default_data_directory(Some(id)).join("db.sqlite")
}
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Serialize)]
#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Logging {}
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct DebugConfig {
pub struct Debug {
/// The parameter of Poisson distribution determining how long, on average,
/// sent packet is going to be delayed at any given mix node.
/// So for a packet going through three mix nodes, on average, it will take three times this value
/// until the packet reaches its destination.
#[serde(with = "humantime_serde")]
pub average_packet_delay: Duration,
average_packet_delay: Duration,
/// The parameter of Poisson distribution determining how long, on average,
/// sent acknowledgement is going to be delayed at any given mix node.
/// So for an ack going through three mix nodes, on average, it will take three times this value
/// until the packet reaches its destination.
#[serde(with = "humantime_serde")]
pub average_ack_delay: Duration,
average_ack_delay: Duration,
/// Value multiplied with the expected round trip time of an acknowledgement packet before
/// it is assumed it was lost and retransmission of the data packet happens.
/// In an ideal network with 0 latency, this value would have been 1.
pub ack_wait_multiplier: f64,
ack_wait_multiplier: f64,
/// Value added to the expected round trip time of an acknowledgement packet before
/// it is assumed it was lost and retransmission of the data packet happens.
/// In an ideal network with 0 latency, this value would have been 0.
#[serde(with = "humantime_serde")]
pub ack_wait_addition: Duration,
ack_wait_addition: Duration,
/// The parameter of Poisson distribution determining how long, on average,
/// it is going to take for another loop cover traffic message to be sent.
#[serde(with = "humantime_serde")]
pub loop_cover_traffic_average_delay: Duration,
loop_cover_traffic_average_delay: Duration,
/// The parameter of Poisson distribution determining how long, on average,
/// it is going to take another 'real traffic stream' message to be sent.
/// If no real packets are available and cover traffic is enabled,
/// a loop cover message is sent instead in order to preserve the rate.
#[serde(with = "humantime_serde")]
pub message_sending_average_delay: Duration,
message_sending_average_delay: Duration,
/// How long we're willing to wait for a response to a message sent to the gateway,
/// before giving up on it.
#[serde(with = "humantime_serde")]
pub gateway_response_timeout: Duration,
gateway_response_timeout: Duration,
/// The uniform delay every which clients are querying the directory server
/// to try to obtain a compatible network topology to send sphinx packets through.
#[serde(with = "humantime_serde")]
pub topology_refresh_rate: Duration,
topology_refresh_rate: Duration,
/// During topology refresh, test packets are sent through every single possible network
/// path. This timeout determines waiting period until it is decided that the packet
/// did not reach its destination.
#[serde(with = "humantime_serde")]
pub topology_resolution_timeout: Duration,
/// Controls whether the dedicated loop cover traffic stream should be enabled.
/// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay])
pub disable_loop_cover_traffic_stream: bool,
/// Controls whether the main packet stream constantly produces packets according to the predefined
/// poisson distribution.
pub disable_main_poisson_packet_distribution: bool,
/// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size.
pub use_extended_packet_size: Option<ExtendedPacketSize>,
/// Defines the minimum number of reply surbs the client wants to keep in its storage at all times.
/// It can only allow to go below that value if its to request additional reply surbs.
pub minimum_reply_surb_storage_threshold: usize,
/// Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
pub maximum_reply_surb_storage_threshold: usize,
/// Defines the minimum number of reply surbs the client would request.
pub minimum_reply_surb_request_size: u32,
/// Defines the maximum number of reply surbs the client would request.
pub maximum_reply_surb_request_size: u32,
/// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once.
pub maximum_allowed_reply_surb_request_size: u32,
/// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking
/// for more even though in theory they wouldn't need to.
#[serde(with = "humantime_serde")]
pub maximum_reply_surb_waiting_period: Duration,
/// Defines maximum amount of time given reply surb is going to be valid for.
/// This is going to be superseded by key rotation once implemented.
#[serde(with = "humantime_serde")]
pub maximum_reply_surb_age: Duration,
/// Defines maximum amount of time given reply key is going to be valid for.
/// This is going to be superseded by key rotation once implemented.
#[serde(with = "humantime_serde")]
pub maximum_reply_key_age: Duration,
topology_resolution_timeout: Duration,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ExtendedPacketSize {
Extended8,
Extended16,
Extended32,
}
impl Default for DebugConfig {
impl Default for Debug {
fn default() -> Self {
DebugConfig {
Debug {
average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY,
average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY,
ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER,
@@ -624,27 +459,6 @@ impl Default for DebugConfig {
gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT,
topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE,
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_loop_cover_traffic_stream: false,
disable_main_poisson_packet_distribution: false,
use_extended_packet_size: None,
minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD,
maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD,
minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE,
maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE,
maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE,
maximum_reply_surb_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD,
maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE,
maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE,
}
}
}
impl From<ExtendedPacketSize> for PacketSize {
fn from(size: ExtendedPacketSize) -> PacketSize {
match size {
ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8,
ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16,
ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32,
}
}
}
+1 -11
View File
@@ -1,14 +1,12 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::ReplyStorageBackend;
use crypto::asymmetric::identity::Ed25519RecoveryError;
use gateway_client::error::GatewayClientError;
use topology::NymTopologyError;
use validator_client::ValidatorClientError;
#[derive(thiserror::Error, Debug)]
pub enum ClientCoreError<B: ReplyStorageBackend> {
pub enum ClientCoreError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Gateway client error: {0}")]
@@ -26,12 +24,4 @@ pub enum ClientCoreError<B: ReplyStorageBackend> {
ListOfValidatorApisIsEmpty,
#[error("Could not load existing gateway configuration: {0}")]
CouldNotLoadExistingGatewayConfiguration(std::io::Error),
#[error("The current network topology seem to be insufficient to route any packets through")]
InsufficientNetworkTopology(#[from] NymTopologyError),
#[error("Unexpected exit")]
UnexpectedExit,
#[error("experienced a failure with our reply surb persistent storage: {source}")]
SurbStorageError { source: B::StorageError },
}
+13 -27
View File
@@ -18,24 +18,20 @@ use tap::TapFallible;
use topology::{filter::VersionFilterable, gateway};
use url::Url;
use crate::client::replies::reply_storage::ReplyStorageBackend;
use crate::{
client::key_manager::KeyManager,
config::{persistence::key_pathfinder::ClientKeyPathfinder, Config},
error::ClientCoreError,
};
pub async fn query_gateway_details<B>(
pub async fn query_gateway_details(
validator_servers: Vec<Url>,
chosen_gateway_id: Option<&str>,
) -> Result<gateway::Node, ClientCoreError<B>>
where
B: ReplyStorageBackend,
{
) -> Result<gateway::Node, ClientCoreError> {
let validator_api = validator_servers
.choose(&mut thread_rng())
.ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?;
let validator_client = validator_client::client::ApiClient::new(validator_api.clone());
let validator_client = validator_client::ApiClient::new(validator_api.clone());
log::trace!("Fetching list of gateways from: {}", validator_api);
let gateways = validator_client.get_cached_gateways().await?;
@@ -63,13 +59,12 @@ where
}
}
pub async fn register_with_gateway_and_store_keys<T, B>(
pub async fn register_with_gateway_and_store_keys<T>(
gateway_details: gateway::Node,
config: &Config<T>,
) -> Result<(), ClientCoreError<B>>
) -> Result<(), ClientCoreError>
where
T: NymConfig,
B: ReplyStorageBackend,
{
let mut rng = OsRng;
let mut key_manager = KeyManager::new(&mut rng);
@@ -84,13 +79,10 @@ where
.tap_err(|err| log::error!("Failed to generate keys: {err}"))?)
}
async fn register_with_gateway<B>(
async fn register_with_gateway(
gateway: &gateway::Node,
our_identity: Arc<identity::KeyPair>,
) -> Result<Arc<SharedKeys>, ClientCoreError<B>>
where
B: ReplyStorageBackend,
{
) -> Result<Arc<SharedKeys>, ClientCoreError> {
let timeout = Duration::from_millis(1500);
let mut gateway_client = GatewayClient::new_init(
gateway.clients_address(),
@@ -98,6 +90,7 @@ where
gateway.owner.clone(),
our_identity.clone(),
timeout,
None,
);
gateway_client
.establish_connection()
@@ -110,17 +103,13 @@ where
Ok(shared_keys)
}
pub fn show_address<T, B>(config: &Config<T>) -> Result<(), ClientCoreError<B>>
pub fn show_address<T>(config: &Config<T>) -> Result<(), ClientCoreError>
where
T: config::NymConfig,
B: ReplyStorageBackend,
{
fn load_identity_keys<B>(
fn load_identity_keys(
pathfinder: &ClientKeyPathfinder,
) -> Result<identity::KeyPair, ClientCoreError<B>>
where
B: ReplyStorageBackend,
{
) -> Result<identity::KeyPair, ClientCoreError> {
let identity_keypair: identity::KeyPair =
pemstore::load_keypair(&pemstore::KeyPairPath::new(
pathfinder.private_identity_key().to_owned(),
@@ -130,12 +119,9 @@ where
Ok(identity_keypair)
}
fn load_sphinx_keys<B>(
fn load_sphinx_keys(
pathfinder: &ClientKeyPathfinder,
) -> Result<encryption::KeyPair, ClientCoreError<B>>
where
B: ReplyStorageBackend,
{
) -> Result<encryption::KeyPair, ClientCoreError> {
let sphinx_keypair: encryption::KeyPair =
pemstore::load_keypair(&pemstore::KeyPairPath::new(
pathfinder.private_encryption_key().to_owned(),
-19
View File
@@ -1,23 +1,4 @@
use std::future::Future;
pub mod client;
pub mod config;
pub mod error;
pub mod init;
#[cfg(target_arch = "wasm32")]
pub(crate) fn spawn_future<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn spawn_future<F>(future: F)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
tokio::spawn(future);
}
+1 -1
View File
@@ -15,7 +15,7 @@ rand = "0.7.3"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
url = "2.2"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime
coconut-interface = { path = "../../common/coconut-interface" }
config = { path = "../../common/config" }
+7 -14
View File
@@ -14,9 +14,8 @@ use credential_storage::PersistentStorage;
use credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES};
use credentials::coconut::utils::obtain_aggregate_signature;
use crypto::asymmetric::{encryption, identity};
use network_defaults::{NymNetworkDetails, VOUCHER_INFO};
use network_defaults::VOUCHER_INFO;
use validator_client::nymd::tx::Hash;
use validator_client::{CoconutApiClient, Config};
use crate::client::Client;
use crate::error::{CredentialClientError, Result};
@@ -108,9 +107,10 @@ pub(crate) struct GetCredential {
/// The hash of a successful deposit transaction
#[clap(long)]
tx_hash: String,
/// The nymd URL that should be used
/// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated
/// by comma (,)
#[clap(long)]
nymd_url: String,
signer_authorities: String,
/// If we want to get the signature without attaching a blind sign request; it is expected that
/// there is already a signature stored on the signer
#[clap(long, parse(from_flag))]
@@ -124,10 +124,7 @@ impl Execute for GetCredential {
.get::<State>(&self.tx_hash)
.ok_or(CredentialClientError::NoDeposit)?;
let network_details = NymNetworkDetails::new_from_env();
let config = Config::try_from_nym_network_details(&network_details)?;
let client = validator_client::Client::new_query(config)?;
let coconut_api_clients = CoconutApiClient::all_coconut_api_clients(&client).await?;
let urls = config::parse_validators(&self.signer_authorities);
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let bandwidth_credential_attributes = if self.__no_request {
@@ -181,12 +178,8 @@ impl Execute for GetCredential {
)?);
db.set(&self.tx_hash, &state).unwrap();
let signature = obtain_aggregate_signature(
&params,
&bandwidth_credential_attributes,
&coconut_api_clients,
)
.await?;
let signature =
obtain_aggregate_signature(&params, &bandwidth_credential_attributes, &urls).await?;
shared_storage
.insert_coconut_credential(
state.amount.to_string(),
-4
View File
@@ -8,7 +8,6 @@ use credentials::error::Error as CredentialError;
use crypto::asymmetric::encryption::KeyRecoveryError;
use crypto::asymmetric::identity::Ed25519RecoveryError;
use validator_client::nymd::error::NymdError;
use validator_client::ValidatorClientError;
pub type Result<T> = std::result::Result<T, CredentialClientError>;
@@ -17,9 +16,6 @@ pub enum CredentialClientError {
#[error("Nymd error: {0}")]
Nymd(#[from] NymdError),
#[error("Validator client error: {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("Credential error: {0}")]
Credential(#[from] CredentialError),
+1 -1
View File
@@ -22,7 +22,7 @@ cfg_if::cfg_if! {
#[clap(author = "Nymtech", version, about)]
struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(short, long)]
#[clap(long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
/// Path where the sqlite credental database will be located.
+4 -8
View File
@@ -1,10 +1,10 @@
[package]
name = "nym-client"
version = "1.1.1"
version = "1.0.2"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
rust-version = "1.65"
rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -27,20 +27,17 @@ pretty_env_logger = "0.4" # for formatting log messages
rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use
serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization
sled = "0.34" # for storage of replySURB decryption keys
thiserror = "1.0.34"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } # async runtime
tokio-tungstenite = "0.14" # websocket
## internal
client-core = { path = "../client-core", features = ["fs-surb-storage"] }
client-connections = { path = "../../common/client-connections" }
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
config = { path = "../../common/config" }
completions = { path = "../../common/completions" }
credential-storage = { path = "../../common/credential-storage" }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
gateway-client = { path = "../../common/client-libs/gateway-client" }
gateway-requests = { path = "../../gateway/gateway-requests" }
network-defaults = { path = "../../common/network-defaults" }
@@ -51,7 +48,6 @@ topology = { path = "../../common/topology" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
websocket-requests = { path = "websocket-requests" }
tap = "1.0.1"
[features]
coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"]
+47 -49
View File
@@ -25,59 +25,57 @@ async fn get_self_address(ws_stream: &mut WebSocketStream<MaybeTlsStream<TcpStre
let response = send_message_and_get_response(ws_stream, self_address_request).await;
match response {
ServerResponse::SelfAddress(recipient) => *recipient,
ServerResponse::SelfAddress(recipient) => recipient,
_ => panic!("received an unexpected response!"),
}
}
async fn send_file_with_reply() {
todo!("reimplement surb usage here : )")
// let uri = "ws://localhost:1977";
// let (mut ws_stream, _) = connect_async(uri).await.unwrap();
//
// let recipient = get_self_address(&mut ws_stream).await;
// println!("our full address is: {}", recipient);
//
// let read_data = std::fs::read("examples/dummy_file").unwrap();
//
// let send_request = ClientRequest::Send {
// recipient,
// message: read_data,
// with_reply_surb: true,
// connection_id: Some(0),
// };
//
// println!("sending content of 'dummy_file' over the mix network...");
// let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await;
//
// let received = match response {
// ServerResponse::Received(received) => received,
// _ => panic!("received an unexpected response!"),
// };
//
// println!("writing the file back to the disk!");
// std::fs::write("examples/received_file_withreply", received.message).unwrap();
//
// let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec();
// let reply_request = ClientRequest::Reply {
// message: reply_message.clone(),
// reply_surb: received.reply_surb.unwrap(),
// };
//
// println!(
// "sending {:?} (using reply SURB!) over the mix network...",
// String::from_utf8(reply_message).unwrap()
// );
// let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await;
// let received = match response {
// ServerResponse::Received(received) => received,
// _ => panic!("received an unexpected response!"),
// };
//
// println!(
// "received {:#?} from the mix network!",
// String::from_utf8(received.message).unwrap()
// );
let uri = "ws://localhost:1977";
let (mut ws_stream, _) = connect_async(uri).await.unwrap();
let recipient = get_self_address(&mut ws_stream).await;
println!("our full address is: {}", recipient);
let read_data = std::fs::read("examples/dummy_file").unwrap();
let send_request = ClientRequest::Send {
recipient,
message: read_data,
with_reply_surb: true,
};
println!("sending content of 'dummy_file' over the mix network...");
let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await;
let received = match response {
ServerResponse::Received(received) => received,
_ => panic!("received an unexpected response!"),
};
println!("writing the file back to the disk!");
std::fs::write("examples/received_file_withreply", received.message).unwrap();
let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec();
let reply_request = ClientRequest::Reply {
message: reply_message.clone(),
reply_surb: received.reply_surb.unwrap(),
};
println!(
"sending {:?} (using reply SURB!) over the mix network...",
String::from_utf8(reply_message).unwrap()
);
let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await;
let received = match response {
ServerResponse::Received(received) => received,
_ => panic!("received an unexpected response!"),
};
println!(
"received {:#?} from the mix network!",
String::from_utf8(received.message).unwrap()
);
}
async fn send_file_without_reply() {
@@ -92,7 +90,7 @@ async fn send_file_without_reply() {
let send_request = ClientRequest::Send {
recipient,
message: read_data,
connection_id: Some(0),
with_reply_surb: false,
};
println!("sending content of 'dummy_file' over the mix network...");
+1 -9
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::template::config_template;
use client_core::config::Config as BaseConfig;
pub use client_core::config::MISSING_VALUE;
use client_core::config::{Config as BaseConfig, DebugConfig};
use config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT;
use config::NymConfig;
use serde::{Deserialize, Serialize};
@@ -27,10 +27,6 @@ impl SocketType {
_ => SocketType::None,
}
}
pub fn is_websocket(&self) -> bool {
matches!(self, SocketType::WebSocket)
}
}
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
@@ -104,10 +100,6 @@ impl Config {
&mut self.base
}
pub fn get_debug_settings(&self) -> &DebugConfig {
self.get_base().get_debug_config()
}
pub fn get_socket_type(&self) -> SocketType {
self.socket.socket_type
}
+4 -10
View File
@@ -23,13 +23,6 @@ id = '{{ client.id }}'
# to claim bandwidth without presenting bandwidth credentials.
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to nymd validators via which the client can communicate with the chain.
validator_urls = [
{{#each client.validator_urls }}
'{{this}}',
{{/each}}
]
# Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls = [
{{#each client.validator_api_urls }}
@@ -49,12 +42,13 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}'
# Path to file containing public encryption key.
public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# Full path to file containing reply encryption keys of all reply-SURBs we have ever
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
reply_surb_database_path = '{{ client.reply_surb_database_path }}'
##### additional client config options #####
# A gateway specific, optional, base58 stringified shared key used for
+367 -188
View File
@@ -1,102 +1,326 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::Config;
use crate::error::ClientError;
use crate::websocket;
use client_connections::TransmissionLane;
use client_core::client::base_client::{
non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput,
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::inbound_messages::InputMessage;
use client_core::client::key_manager::KeyManager;
use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver};
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::real_messages_control;
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender,
ReceivedMessagesBufferController, ReconstructedMessagesReceiver,
};
use client_core::client::reply_key_storage::ReplyKeyStorage;
use client_core::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::{
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
MixnetMessageSender,
};
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::addressing::nodes::NodeIdentity;
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::receiver::ReconstructedMessage;
use task::{wait_for_signal, ShutdownNotifier};
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
use crate::client::config::{Config, SocketType};
use crate::websocket;
pub(crate) mod config;
pub struct SocketClient {
pub struct NymClient {
/// Client configuration options, including, among other things, packet sending rates,
/// key filepaths, etc.
config: Config,
/// KeyManager object containing smart pointers to all relevant keys used by the client.
key_manager: KeyManager,
/// Channel used for transforming 'raw' messages into sphinx packets and sending them
/// through the mix network.
/// It is only available if the client started with the websocket listener disabled.
input_tx: Option<InputMessageSender>,
/// Channel used for obtaining reconstructed messages received from the mix network.
/// It is only available if the client started with the websocket listener disabled.
receive_tx: Option<ReconstructedMessagesReceiver>,
}
impl SocketClient {
impl NymClient {
pub fn new(config: Config) -> Self {
let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base());
let key_manager = KeyManager::load_keys(&pathfinder).expect("failed to load stored keys");
SocketClient {
NymClient {
config,
key_manager,
input_tx: None,
receive_tx: None,
}
}
async fn create_bandwidth_controller(config: &Config) -> BandwidthController {
pub fn as_mix_recipient(&self) -> Recipient {
Recipient::new(
*self.key_manager.identity_keypair().public_key(),
*self.key_manager.encryption_keypair().public_key(),
// TODO: below only works under assumption that gateway address == gateway id
// (which currently is true)
NodeIdentity::from_base58_string(self.config.get_base().get_gateway_id()).unwrap(),
)
}
// future constantly pumping loop cover traffic at some specified average rate
// the pumped traffic goes to the MixTrafficController
fn start_cover_traffic_stream(
&self,
topology_accessor: TopologyAccessor,
mix_tx: BatchMixMessageSender,
shutdown: ShutdownListener,
) {
info!("Starting loop cover traffic stream...");
LoopCoverTrafficStream::new(
self.key_manager.ack_key(),
self.config.get_base().get_average_ack_delay(),
self.config.get_base().get_average_packet_delay(),
self.config
.get_base()
.get_loop_cover_traffic_average_delay(),
mix_tx,
self.as_mix_recipient(),
topology_accessor,
shutdown,
)
.start();
}
fn start_real_traffic_controller(
&self,
topology_accessor: TopologyAccessor,
reply_key_storage: ReplyKeyStorage,
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
shutdown: ShutdownListener,
) {
let controller_config = real_messages_control::Config::new(
self.key_manager.ack_key(),
self.config.get_base().get_ack_wait_multiplier(),
self.config.get_base().get_ack_wait_addition(),
self.config.get_base().get_average_ack_delay(),
self.config.get_base().get_message_sending_average_delay(),
self.config.get_base().get_average_packet_delay(),
self.as_mix_recipient(),
);
info!("Starting real traffic stream...");
RealMessagesController::new(
controller_config,
ack_receiver,
input_receiver,
mix_sender,
topology_accessor,
reply_key_storage,
shutdown,
)
.start();
}
// buffer controlling all messages fetched from provider
// required so that other components would be able to use them (say the websocket)
fn start_received_messages_buffer_controller(
&self,
query_receiver: ReceivedBufferRequestReceiver,
mixnet_receiver: MixnetMessageReceiver,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
) {
info!("Starting received messages buffer controller...");
ReceivedMessagesBufferController::new(
self.key_manager.encryption_keypair(),
query_receiver,
mixnet_receiver,
reply_key_storage,
shutdown,
)
.start()
}
async fn start_gateway_client(
&mut self,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
shutdown: ShutdownListener,
) -> GatewayClient {
let gateway_id = self.config.get_base().get_gateway_id();
if gateway_id.is_empty() {
panic!("The identity of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_owner = self.config.get_base().get_gateway_owner();
if gateway_owner.is_empty() {
panic!("The owner of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_address = self.config.get_base().get_gateway_listener();
if gateway_address.is_empty() {
panic!("The address of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
.expect("provided gateway id is invalid!");
#[cfg(feature = "coconut")]
let bandwidth_controller = {
let details = network_defaults::NymNetworkDetails::new_from_env();
let client_config = validator_client::Config::try_from_nym_network_details(&details)
.expect("failed to construct validator client config");
let client = validator_client::Client::new_query(client_config)
.expect("Could not construct query client");
let coconut_api_clients =
validator_client::CoconutApiClient::all_coconut_api_clients(&client)
.await
.expect("Could not query api clients");
BandwidthController::new(
credential_storage::initialise_storage(config.get_base().get_database_path()).await,
coconut_api_clients,
)
};
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(config.get_base().get_database_path()).await,
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
)
.expect("Could not create bandwidth controller");
bandwidth_controller
let mut gateway_client = GatewayClient::new(
gateway_address,
self.key_manager.identity_keypair(),
gateway_identity,
gateway_owner,
Some(self.key_manager.gateway_shared_key()),
mixnet_message_sender,
ack_sender,
self.config.get_base().get_gateway_response_timeout(),
Some(bandwidth_controller),
Some(shutdown),
);
gateway_client
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
gateway_client
.authenticate_and_start()
.await
.expect("could not authenticate and start up the gateway connection");
gateway_client
}
// future responsible for periodically polling directory server and updating
// the current global view of topology
async fn start_topology_refresher(
&mut self,
topology_accessor: TopologyAccessor,
shutdown: ShutdownListener,
) {
let topology_refresher_config = TopologyRefresherConfig::new(
self.config.get_base().get_validator_api_endpoints(),
self.config.get_base().get_topology_refresh_rate(),
env!("CARGO_PKG_VERSION").to_string(),
);
let mut topology_refresher =
TopologyRefresher::new(topology_refresher_config, topology_accessor);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
info!("Obtaining initial network topology");
topology_refresher.refresh().await;
// TODO: a slightly more graceful termination here
if !topology_refresher.is_topology_routable().await {
panic!(
"The current network topology seem to be insufficient to route any packets through\
- check if enough nodes and a gateway are online"
);
}
info!("Starting topology refresher...");
topology_refresher.start(shutdown);
}
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) {
info!("Starting mix traffic controller...");
MixTrafficController::new(mix_rx, gateway_client, shutdown).start();
}
fn start_websocket_listener(
config: &Config,
client_input: ClientInput,
client_output: ClientOutput,
self_address: Recipient,
&self,
buffer_requester: ReceivedBufferRequestSender,
msg_input: InputMessageSender,
) {
info!("Starting websocket listener...");
let ClientInput {
shared_lane_queue_lengths,
connection_command_sender,
input_sender,
} = client_input;
let websocket_handler =
websocket::Handler::new(msg_input, buffer_requester, self.as_mix_recipient());
let received_buffer_request_sender = client_output.received_buffer_request_sender;
let websocket_handler = websocket::Handler::new(
input_sender,
connection_command_sender,
received_buffer_request_sender,
self_address,
shared_lane_queue_lengths,
);
websocket::Listener::new(config.get_listening_port()).start(websocket_handler);
websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler);
}
/// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent)
pub async fn run_socket_forever(self) -> Result<(), ClientError> {
let mut shutdown = self.start_socket().await?;
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub fn send_message(&mut self, recipient: Recipient, message: Vec<u8>, with_reply_surb: bool) {
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
self.input_tx
.as_ref()
.expect("start method was not called before!")
.unbounded_send(input_msg)
.unwrap();
}
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec<u8>) {
let input_msg = InputMessage::new_reply(reply_surb, message);
self.input_tx
.as_ref()
.expect("start method was not called before!")
.unbounded_send(input_msg)
.unwrap();
}
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
/// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple
/// messages, you might have to call this function repeatedly.
// TODO: I guess this should really return something that `impl Stream<Item=ReconstructedMessage>`
pub async fn wait_for_messages(&mut self) -> Vec<ReconstructedMessage> {
use futures::StreamExt;
self.receive_tx
.as_mut()
.expect("start method was not called before!")
.next()
.await
.expect("buffer controller seems to have somehow died!")
}
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
pub async fn run_forever(&mut self) {
let shutdown = self.start().await;
wait_for_signal().await;
println!(
@@ -109,149 +333,104 @@ impl SocketClient {
// Some of these components have shutdown signalling implemented as part of socks5 work,
// but since it's not fully implemented (yet) for all the components of the native client,
// we don't try to wait and instead just stop immediately.
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
//log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
//shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-client");
Ok(())
}
pub async fn start_socket(self) -> Result<ShutdownNotifier, ClientError> {
if !self.config.get_socket_type().is_websocket() {
return Err(ClientError::InvalidSocketMode);
}
pub async fn start(&mut self) -> ShutdownNotifier {
info!("Starting nym client");
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
let base_builder = BaseClientBuilder::new_from_base_config(
self.config.get_base(),
self.key_manager,
Some(Self::create_bandwidth_controller(&self.config).await),
non_wasm_helpers::setup_fs_reply_surb_backend(
self.config.get_base().get_reply_surb_database_path(),
self.config.get_debug_settings(),
)
.await?,
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
// used for announcing connection or disconnection of a channel for pushing re-assembled messages to
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
// channels responsible for controlling real messages
let (input_sender, input_receiver) = mpsc::unbounded::<InputMessage>();
// channels responsible for controlling ack messages
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor = TopologyAccessor::new();
let reply_key_storage =
ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path())
.expect("Failed to load reply key storage!");
// Shutdown notifier for signalling tasks to stop
let shutdown = ShutdownNotifier::default();
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
.await;
self.start_received_messages_buffer_controller(
received_buffer_request_receiver,
mixnet_messages_receiver,
reply_key_storage.clone(),
shutdown.subscribe(),
);
let self_address = base_builder.as_mix_recipient();
let mut started_client = base_builder.start_base().await?;
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
let gateway_client = self
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
Self::start_websocket_listener(&self.config, client_input, client_output, self_address);
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
ack_receiver,
input_receiver,
sphinx_message_sender.clone(),
shutdown.subscribe(),
);
self.start_cover_traffic_stream(
shared_topology_accessor,
sphinx_message_sender,
shutdown.subscribe(),
);
match self.config.get_socket_type() {
SocketType::WebSocket => {
self.start_websocket_listener(received_buffer_request_sender, input_sender)
}
SocketType::None => {
// if we did not start the socket, it means we're running (supposedly) in the native mode
// and hence we should announce 'ourselves' to the buffer
let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded();
// tell the buffer to start sending stuff to us
received_buffer_request_sender
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
reconstructed_sender,
))
.expect("the buffer request failed!");
self.receive_tx = Some(reconstructed_receiver);
self.input_tx = Some(input_sender);
}
}
info!("Client startup finished!");
info!("The address of this client is: {}", self_address);
info!("The address of this client is: {}", self.as_mix_recipient());
Ok(started_client.shutdown_notifier)
}
pub async fn start_direct(self) -> Result<DirectClient, ClientError> {
if self.config.get_socket_type().is_websocket() {
return Err(ClientError::InvalidSocketMode);
}
let base_client = BaseClientBuilder::new_from_base_config(
self.config.get_base(),
self.key_manager,
Some(Self::create_bandwidth_controller(&self.config).await),
non_wasm_helpers::setup_fs_reply_surb_backend(
self.config.get_base().get_reply_surb_database_path(),
self.config.get_debug_settings(),
)
.await?,
);
let mut started_client = base_client.start_base().await?;
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
// register our receiver
let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded();
// tell the buffer to start sending stuff to us
client_output
.received_buffer_request_sender
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
reconstructed_sender,
))
.expect("the buffer request failed!");
Ok(DirectClient {
client_input,
reconstructed_receiver,
_shutdown_notifier: started_client.shutdown_notifier,
})
}
}
pub struct DirectClient {
client_input: ClientInput,
reconstructed_receiver: ReconstructedMessagesReceiver,
// we need to keep reference to this guy otherwise things will start dropping
_shutdown_notifier: ShutdownNotifier,
}
impl DirectClient {
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub async fn send_regular_message(&mut self, recipient: Recipient, message: Vec<u8>) {
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_regular(recipient, message, lane);
self.client_input
.input_sender
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
}
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub async fn send_anonymous_message(
&mut self,
recipient: Recipient,
message: Vec<u8>,
reply_surbs: u32,
) {
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane);
self.client_input
.input_sender
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
}
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
pub async fn send_reply(&mut self, recipient_tag: AnonymousSenderTag, message: Vec<u8>) {
let lane = TransmissionLane::General;
let input_msg = InputMessage::new_reply(recipient_tag, message, lane);
self.client_input
.input_sender
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
}
/// EXPERIMENTAL DIRECT RUST API
/// It's untested and there are absolutely no guarantees about it (but seems to have worked
/// well enough in local tests)
/// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple
/// messages, you might have to call this function repeatedly.
// TODO: I guess this should really return something that `impl Stream<Item=ReconstructedMessage>`
pub async fn wait_for_messages(&mut self) -> Vec<ReconstructedMessage> {
use futures::StreamExt;
self.reconstructed_receiver
.next()
.await
.expect("buffer controller seems to have somehow died!")
shutdown
}
}
+9 -22
View File
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use client_core::client::replies::reply_storage::fs_backend;
use client_core::{config::GatewayEndpointConfig, error::ClientCoreError};
use client_core::{config::GatewayEndpoint, error::ClientCoreError};
use config::NymConfig;
use crate::{
@@ -26,13 +25,9 @@ pub(crate) struct Init {
#[clap(long)]
force_register_gateway: bool,
/// Comma separated list of rest endpoints of the nymd validators
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
nymd_validators: Option<String>,
/// Comma separated list of rest endpoints of the API validators
#[clap(long)]
api_validators: Option<String>,
validators: Option<String>,
/// Whether to not start the websocket
#[clap(long)]
@@ -47,10 +42,6 @@ pub(crate) struct Init {
#[clap(long, hidden = true)]
fastmode: bool,
/// Disable loop cover traffic and the Poisson rate limiter (for debugging only)
#[clap(long, hidden = true)]
no_cover: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[cfg(feature = "coconut")]
@@ -61,12 +52,10 @@ pub(crate) struct Init {
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
nymd_validators: init_config.nymd_validators,
api_validators: init_config.api_validators,
validators: init_config.validators,
disable_socket: init_config.disable_socket,
port: init_config.port,
fastmode: init_config.fastmode,
no_cover: init_config.no_cover,
#[cfg(feature = "coconut")]
enabled_credentials_mode: init_config.enabled_credentials_mode,
@@ -127,12 +116,10 @@ pub(crate) async fn execute(args: &Init) {
);
println!("Client configuration completed.");
client_core::init::show_address::<_, fs_backend::Backend>(config.get_base()).unwrap_or_else(
|err| {
eprintln!("Failed to show address\nError: {err}");
std::process::exit(1)
},
);
client_core::init::show_address(config.get_base()).unwrap_or_else(|err| {
eprintln!("Failed to show address\nError: {err}");
std::process::exit(1)
});
}
async fn setup_gateway(
@@ -140,7 +127,7 @@ async fn setup_gateway(
register: bool,
user_chosen_gateway_id: Option<&str>,
config: &Config,
) -> Result<GatewayEndpointConfig, ClientCoreError<fs_backend::Backend>> {
) -> Result<GatewayEndpoint, ClientCoreError> {
if register {
// Get the gateway details by querying the validator-api. Either pick one at random or use
// the chosen one if it's among the available ones.
+5 -24
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::{Config, SocketType};
use crate::error::ClientError;
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use completions::{fig_generate, ArgShell};
@@ -50,7 +49,7 @@ fn long_version_static() -> &'static str {
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(short, long)]
#[clap(long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(subcommand)]
@@ -75,43 +74,29 @@ pub(crate) enum Commands {
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
nymd_validators: Option<String>,
api_validators: Option<String>,
validators: Option<String>,
disable_socket: bool,
port: Option<u16>,
fastmode: bool,
no_cover: bool,
#[cfg(feature = "coconut")]
enabled_credentials_mode: bool,
}
pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> {
pub(crate) async fn execute(args: &Cli) {
let bin_name = "nym-native-client";
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await?,
Commands::Run(m) => run::execute(m).await,
Commands::Upgrade(m) => upgrade::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
}
Ok(())
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.nymd_validators {
config
.get_base_mut()
.set_custom_validators(config::parse_validators(&raw_validators));
} else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() {
let raw_validators = std::env::var(network_defaults::var_names::NYMD_VALIDATOR)
.expect("nymd validator not set");
config
.get_base_mut()
.set_custom_validators(config::parse_validators(&raw_validators));
}
if let Some(raw_validators) = args.api_validators {
if let Some(raw_validators) = args.validators {
config
.get_base_mut()
.set_custom_validator_apis(config::parse_validators(&raw_validators));
@@ -142,10 +127,6 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config.get_base_mut().set_high_default_traffic_volume();
}
if args.no_cover {
config.get_base_mut().set_no_cover_traffic();
}
config
}
+9 -25
View File
@@ -2,9 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::{config::Config, SocketClient},
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
error::ClientError,
};
use clap::Args;
@@ -18,13 +17,9 @@ pub(crate) struct Run {
#[clap(long)]
id: String,
/// Comma separated list of rest endpoints of the nymd validators
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
nymd_validators: Option<String>,
/// Comma separated list of rest endpoints of the API validators
#[clap(long)]
api_validators: Option<String>,
validators: Option<String>,
/// Id of the gateway we want to connect to. If overridden, it is user's responsibility to
/// ensure prior registration happened
@@ -39,15 +34,6 @@ pub(crate) struct Run {
#[clap(short, long)]
port: Option<u16>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hidden = true)]
fastmode: bool,
/// Disable loop cover traffic and the Poisson rate limiter (for debugging only)
#[clap(long, hidden = true)]
no_cover: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[cfg(feature = "coconut")]
@@ -58,12 +44,10 @@ pub(crate) struct Run {
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
nymd_validators: run_config.nymd_validators,
api_validators: run_config.api_validators,
validators: run_config.validators,
disable_socket: run_config.disable_socket,
port: run_config.port,
fastmode: run_config.fastmode,
no_cover: run_config.no_cover,
fastmode: false,
#[cfg(feature = "coconut")]
enabled_credentials_mode: run_config.enabled_credentials_mode,
}
@@ -89,14 +73,14 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> {
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
return Err(ClientError::FailedToLoadConfig(id.to_string()));
return;
}
};
@@ -105,8 +89,8 @@ pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> {
if !version_check(&config) {
error!("failed the local version check");
return Err(ClientError::FailedLocalVersionCheck);
return;
}
SocketClient::new(config).run_socket_forever().await
NymClient::new(config).run_forever().await;
}
-17
View File
@@ -1,17 +0,0 @@
use client_core::client::replies::reply_storage::fs_backend;
use client_core::error::ClientCoreError;
#[derive(thiserror::Error, Debug)]
pub enum ClientError {
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError<fs_backend::Backend>),
#[error("Failed to load config for: {0}")]
FailedToLoadConfig(String),
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("Attempted to start the client in invalid socket mode")]
InvalidSocketMode,
}
-1
View File
@@ -2,5 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client;
pub mod error;
pub mod websocket;
+24 -5
View File
@@ -2,23 +2,20 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use error::ClientError;
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
pub mod commands;
pub mod error;
pub mod websocket;
#[tokio::main]
async fn main() -> Result<(), ClientError> {
async fn main() {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
setup_env(args.config_env_file.clone());
commands::execute(&args).await
commands::execute(&args).await;
}
fn banner() -> String {
@@ -37,3 +34,25 @@ fn banner() -> String {
crate_version!()
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.filter_module("handlebars", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.init();
}
+61 -223
View File
@@ -1,9 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use client_connections::{
ConnectionCommand, ConnectionCommandSender, LaneQueueLengths, TransmissionLane,
};
use client_core::client::{
inbound_messages::{InputMessage, InputMessageSender},
received_buffer::{
@@ -14,7 +11,7 @@ use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::receiver::ReconstructedMessage;
use tokio::net::TcpStream;
use tokio_tungstenite::{
@@ -37,12 +34,10 @@ impl Default for ReceivedResponseType {
pub(crate) struct Handler {
msg_input: InputMessageSender,
client_connection_tx: ConnectionCommandSender,
buffer_requester: ReceivedBufferRequestSender,
self_full_address: Recipient,
socket: Option<WebSocketStream<TcpStream>>,
received_response_type: ReceivedResponseType,
lane_queue_lengths: LaneQueueLengths,
}
// clone is used to use handler on a new connection, which initially is `None`
@@ -50,242 +45,81 @@ impl Clone for Handler {
fn clone(&self) -> Self {
Handler {
msg_input: self.msg_input.clone(),
client_connection_tx: self.client_connection_tx.clone(),
buffer_requester: self.buffer_requester.clone(),
self_full_address: self.self_full_address,
socket: None,
received_response_type: Default::default(),
lane_queue_lengths: self.lane_queue_lengths.clone(),
}
}
}
impl Drop for Handler {
fn drop(&mut self) {
if self
.buffer_requester
self.buffer_requester
.unbounded_send(ReceivedBufferMessage::ReceiverDisconnect)
.is_err()
{
error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!")
}
.expect("the buffer request failed!")
}
}
impl Handler {
pub(crate) fn new(
msg_input: InputMessageSender,
client_connection_tx: ConnectionCommandSender,
buffer_requester: ReceivedBufferRequestSender,
self_full_address: Recipient,
lane_queue_lengths: LaneQueueLengths,
) -> Self {
Handler {
msg_input,
client_connection_tx,
buffer_requester,
self_full_address,
socket: None,
received_response_type: Default::default(),
lane_queue_lengths,
}
}
async fn handle_send(
fn handle_send(
&mut self,
recipient: Recipient,
message: Vec<u8>,
connection_id: Option<u64>,
with_reply_surb: bool,
) -> Option<ServerResponse> {
info!(
"Attempting to send {:.2} kiB message to {recipient} on connection_id {connection_id:?}",
message.len() as f64 / 1024.0
);
// We map the absence of a connection id as going into the general lane.
let lane = connection_id.map_or(TransmissionLane::General, |id| {
TransmissionLane::ConnectionId(id)
});
// the ack control is now responsible for chunking, etc.
let input_msg = InputMessage::new_regular(recipient, message, lane);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb);
self.msg_input.unbounded_send(input_msg).unwrap();
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let connection_id = match lane {
TransmissionLane::General
| TransmissionLane::ReplySurbRequest
| TransmissionLane::Retransmission
| TransmissionLane::AdditionalReplySurbs => return None,
TransmissionLane::ConnectionId(id) => id,
};
// on receiving a send, we reply back the current lane queue length for that connection id.
// Note that this does _NOT_ take into account the packets that have been received but not
// yet reach `OutQueueControl`, so it might be a tad low.
if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() {
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
return Some(ServerResponse::LaneQueueLength {
lane: connection_id,
queue_length,
});
}
log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length");
None
}
async fn handle_send_anonymous(
&mut self,
recipient: Recipient,
message: Vec<u8>,
reply_surbs: u32,
connection_id: Option<u64>,
) -> Option<ServerResponse> {
info!(
"Attempting to anonymously send {:.2} kiB message to {recipient} on connection_id {connection_id:?} while attaching {reply_surbs} replySURBs.",
message.len() as f64 / 1024.0
);
// We map the absence of a connection id as going into the general lane.
let lane = connection_id.map_or(TransmissionLane::General, |id| {
TransmissionLane::ConnectionId(id)
});
let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let connection_id = match lane {
TransmissionLane::General
| TransmissionLane::ReplySurbRequest
| TransmissionLane::Retransmission
| TransmissionLane::AdditionalReplySurbs => return None,
TransmissionLane::ConnectionId(id) => id,
};
// on receiving a send, we reply back the current lane queue length for that connection id.
// Note that this does _NOT_ take into account the packets that have been received but not
// yet reach `OutQueueControl`, so it might be a tad low.
if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() {
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
return Some(ServerResponse::LaneQueueLength {
lane: connection_id,
queue_length,
});
fn handle_reply(&mut self, reply_surb: ReplySurb, message: Vec<u8>) -> Option<ServerResponse> {
if message.len() > ReplySurb::max_msg_len(Default::default()) {
return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default()))));
}
log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length");
None
}
let input_msg = InputMessage::new_reply(reply_surb, message);
self.msg_input.unbounded_send(input_msg).unwrap();
async fn handle_reply(
&mut self,
recipient_tag: AnonymousSenderTag,
message: Vec<u8>,
connection_id: Option<u64>,
) -> Option<ServerResponse> {
info!("Attempting to send {:.2} kiB reply message to {recipient_tag} on connection_id {connection_id:?}", message.len() as f64 / 1024.0);
// We map the absence of a connection id as going into the general lane.
let lane = connection_id.map_or(TransmissionLane::General, |id| {
TransmissionLane::ConnectionId(id)
});
let input_msg = InputMessage::new_reply(recipient_tag, message, lane);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let connection_id = match lane {
TransmissionLane::General
| TransmissionLane::ReplySurbRequest
| TransmissionLane::Retransmission
| TransmissionLane::AdditionalReplySurbs => return None,
TransmissionLane::ConnectionId(id) => id,
};
// on receiving a send, we reply back the current lane queue length for that connection id.
// Note that this does _NOT_ take into account the packets that have been received but not
// yet reach `OutQueueControl`, so it might be a tad low.
if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() {
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
return Some(ServerResponse::LaneQueueLength {
lane: connection_id,
queue_length,
});
}
log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length");
None
}
fn handle_self_address(&self) -> ServerResponse {
ServerResponse::SelfAddress(Box::new(self.self_full_address))
ServerResponse::SelfAddress(self.self_full_address)
}
fn handle_closed_connection(&self, connection_id: u64) -> Option<ServerResponse> {
self.client_connection_tx
.unbounded_send(ConnectionCommand::Close(connection_id))
.unwrap();
None
}
fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option<ServerResponse> {
let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else {
log::warn!(
"Failed to get the lane queue length lock, not responding back with the current queue length"
);
return None;
};
let lane = TransmissionLane::ConnectionId(connection_id);
let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0);
Some(ServerResponse::LaneQueueLength {
lane: connection_id,
queue_length,
})
}
async fn handle_request(&mut self, request: ClientRequest) -> Option<ServerResponse> {
fn handle_request(&mut self, request: ClientRequest) -> Option<ServerResponse> {
match request {
ClientRequest::Send {
recipient,
message,
connection_id,
} => self.handle_send(recipient, message, connection_id).await,
ClientRequest::SendAnonymous {
recipient,
message,
reply_surbs,
connection_id,
} => {
self.handle_send_anonymous(recipient, message, reply_surbs, connection_id)
.await
}
with_reply_surb,
} => self.handle_send(recipient, message, with_reply_surb),
ClientRequest::Reply {
message,
sender_tag,
connection_id,
} => self.handle_reply(sender_tag, message, connection_id).await,
reply_surb,
} => self.handle_reply(reply_surb, message),
ClientRequest::SelfAddress => Some(self.handle_self_address()),
ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id),
ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id),
}
}
async fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> {
fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> {
debug!("Handling text message request");
trace!("Content: {:?}", msg);
@@ -294,13 +128,13 @@ impl Handler {
let response = match client_request {
Err(err) => Some(ServerResponse::Error(err)),
Ok(req) => self.handle_request(req).await,
Ok(req) => self.handle_request(req),
};
response.map(|resp| WsMessage::text(resp.into_text()))
}
async fn handle_binary_message(&mut self, msg: &[u8]) -> Option<WsMessage> {
fn handle_binary_message(&mut self, msg: Vec<u8>) -> Option<WsMessage> {
debug!("Handling binary message request");
self.received_response_type = ReceivedResponseType::Binary;
@@ -308,23 +142,49 @@ impl Handler {
let response = match client_request {
Err(err) => Some(ServerResponse::Error(err)),
Ok(req) => self.handle_request(req).await,
Ok(req) => self.handle_request(req),
};
response.map(|resp| WsMessage::Binary(resp.into_binary()))
}
async fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option<WsMessage> {
fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option<WsMessage> {
// apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore
// them and let's test that claim. If that's not the case, just copy code from
// old version of this file.
match raw_request {
WsMessage::Text(text_message) => self.handle_text_message(text_message).await,
WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message).await,
WsMessage::Text(text_message) => self.handle_text_message(text_message),
WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message),
_ => None,
}
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_binary(
&self,
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
.collect()
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_text(
&self,
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
.collect()
}
async fn push_websocket_received_plaintexts(
&mut self,
reconstructed_messages: Vec<ReconstructedMessage>,
@@ -333,8 +193,10 @@ impl Handler {
// if it's text or binary, but for time being we use the naive assumption that if
// client is sending Message::Text it expects text back. Same for Message::Binary
let response_messages = match self.received_response_type {
ReceivedResponseType::Binary => prepare_reconstructed_binary(reconstructed_messages),
ReceivedResponseType::Text => prepare_reconstructed_text(reconstructed_messages),
ReceivedResponseType::Binary => {
self.prepare_reconstructed_binary(reconstructed_messages)
}
ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages),
};
let mut send_stream = futures::stream::iter(response_messages);
@@ -382,10 +244,11 @@ impl Handler {
break;
}
if let Some(response) = self.handle_ws_request(socket_msg).await {
if let Some(response) = self.handle_ws_request(socket_msg) {
if let Err(err) = self.send_websocket_response(response).await {
warn!(
"Failed to send message over websocket: {err}. Assuming the connection is dead.",
"Failed to send message over websocket: {}. Assuming the connection is dead.",
err
);
break;
}
@@ -393,10 +256,9 @@ impl Handler {
}
// or a reconstructed mix message that we need to push back to the client
mix_messages = msg_receiver.next() => {
let Some(mix_messages) = mix_messages else {
error!("mix messages sender was unexpectedly closed! this shouldn't have ever happened! (unless we're shutting down - TODO: implement proper graceful shutdown handler)");
return
};
let mix_messages = mix_messages.expect(
"mix messages sender was unexpectedly closed! this shouldn't have ever happened!",
);
if let Err(e) = self.push_websocket_received_plaintexts(mix_messages).await {
warn!("failed to send sphinx packets back to the client - {:?}, assuming the connection is dead", e);
break;
@@ -429,27 +291,3 @@ impl Handler {
self.listen_for_requests(reconstructed_receiver).await;
}
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_binary(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Binary(resp.into_binary())))
.collect()
}
// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but
// let's just play along for now
fn prepare_reconstructed_text(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
.map(|resp| Ok(WsMessage::Text(resp.into_text())))
.collect()
}
+2 -1
View File
@@ -54,8 +54,9 @@ impl Listener {
Ok((mut socket, remote_addr)) => {
debug!("Received connection from {:?}", remote_addr);
if self.state.is_connected() {
warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr);
warn!("tried to duplicate!");
// if we've already got a connection, don't allow another one
debug!("but there was already a connection present!");
// while we only ever want to accept a single connection, we don't want
// to leave clients hanging (and also allow for reconnection if it somehow
// was dropped)
+2 -30
View File
@@ -24,11 +24,8 @@ impl fmt::Debug for Error {
}
impl Error {
pub fn new<S: Into<String>>(kind: ErrorKind, message: S) -> Self {
Error {
kind,
message: message.into(),
}
pub fn new(kind: ErrorKind, message: String) -> Self {
Error { kind, message }
}
}
@@ -65,31 +62,6 @@ pub enum ErrorKind {
Other = 0xFF,
}
impl TryFrom<u8> for ErrorKind {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
_ if value == (ErrorKind::EmptyRequest as u8) => Ok(ErrorKind::EmptyRequest),
_ if value == (ErrorKind::TooShortRequest as u8) => Ok(ErrorKind::TooShortRequest),
_ if value == (ErrorKind::UnknownRequest as u8) => Ok(ErrorKind::UnknownRequest),
_ if value == (ErrorKind::MalformedRequest as u8) => Ok(ErrorKind::MalformedRequest),
_ if value == (ErrorKind::EmptyResponse as u8) => Ok(ErrorKind::EmptyResponse),
_ if value == (ErrorKind::TooShortResponse as u8) => Ok(ErrorKind::TooShortResponse),
_ if value == (ErrorKind::UnknownResponse as u8) => Ok(ErrorKind::UnknownResponse),
_ if value == (ErrorKind::MalformedResponse as u8) => Ok(ErrorKind::MalformedResponse),
_ if value == (ErrorKind::Other as u8) => Ok(ErrorKind::Other),
n => Err(Error::new(
ErrorKind::MalformedResponse,
format!("invalid error code {}", n),
)),
}
}
}
impl ErrorKind {
pub(crate) fn as_str(&self) -> &'static str {
match *self {
+125 -338
View File
@@ -1,4 +1,4 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// all variable size data is always prefixed with u64 length
@@ -7,115 +7,53 @@
use crate::error::{self, ErrorKind};
use crate::text::ClientRequestText;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nymsphinx::anonymous_replies::ReplySurb;
use std::convert::{TryFrom, TryInto};
use std::mem::size_of;
#[repr(u8)]
enum ClientRequestTag {
/// Value tag representing [`Send`] variant of the [`ClientRequest`]
Send = 0x00,
/// Value tag representing [`Send`] variant of the [`ClientRequest`]
pub const SEND_REQUEST_TAG: u8 = 0x00;
/// Value tag representing [`SendAnonymous`] variant of the [`ClientRequest`]
SendAnonymous = 0x01,
/// Value tag representing [`Reply`] variant of the [`ClientRequest`]
pub const REPLY_REQUEST_TAG: u8 = 0x01;
/// Value tag representing [`Reply`] variant of the [`ClientRequest`]
Reply = 0x02,
/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`]
SelfAddress = 0x03,
/// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`]
ClosedConnection = 0x04,
/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`]
GetLaneQueueLength = 0x05,
}
impl TryFrom<u8> for ClientRequestTag {
type Error = error::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
_ if value == (Self::Send as u8) => Ok(Self::Send),
_ if value == (Self::SendAnonymous as u8) => Ok(Self::SendAnonymous),
_ if value == (Self::Reply as u8) => Ok(Self::Reply),
_ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress),
_ if value == (Self::ClosedConnection as u8) => Ok(Self::ClosedConnection),
_ if value == (Self::GetLaneQueueLength as u8) => Ok(Self::GetLaneQueueLength),
n => Err(error::Error::new(
ErrorKind::UnknownRequest,
format!("{n} does not correspond to any valid request tag"),
)),
}
}
}
/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`]
pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02;
#[allow(non_snake_case)]
#[derive(Debug)]
pub enum ClientRequest {
/// The simplest message variant where no additional information is attached.
/// You're simply sending your `data` to specified `recipient` without any tagging.
///
/// Ends up with `NymMessage::Plain` variant
Send {
recipient: Recipient,
message: Vec<u8>,
connection_id: Option<u64>,
// Perhaps we could change it to a number to indicate how many reply_SURBs we want to include?
with_reply_surb: bool,
},
/// Create a message used for a duplex anonymous communication where the recipient
/// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`.
///
/// Note that if reply_surbs is set to zero then
/// this variant requires the client having sent some reply_surbs in the past
/// (and thus the recipient also knowing our sender tag).
///
/// Ends up with `NymMessage::Repliable` variant
SendAnonymous {
recipient: Recipient,
message: Vec<u8>,
reply_surbs: u32,
connection_id: Option<u64>,
},
/// Attempt to use our internally received and stored `ReplySurb` to send the message back
/// to specified recipient whilst not knowing its full identity (or even gateway).
///
/// Ends up with `NymMessage::Reply` variant
Reply {
sender_tag: AnonymousSenderTag,
message: Vec<u8>,
connection_id: Option<u64>,
reply_surb: ReplySurb,
},
SelfAddress,
ClosedConnection(u64),
GetLaneQueueLength(u64),
}
// we could have been parsing it directly TryFrom<WsMessage>, but we want to retain
// information about whether it came from binary or text to send appropriate response back
impl ClientRequest {
// SEND_REQUEST_TAG || recipient || conn_id || data_len || data
fn serialize_send(recipient: Recipient, data: Vec<u8>, connection_id: Option<u64>) -> Vec<u8> {
// SEND_REQUEST_TAG || with_surb || recipient || data_len || data
fn serialize_send(recipient: Recipient, data: Vec<u8>, with_reply_surb: bool) -> Vec<u8> {
let data_len_bytes = (data.len() as u64).to_be_bytes();
let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes();
std::iter::once(ClientRequestTag::Send as u8)
.chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant
.chain(conn_id_bytes.into_iter())
.chain(data_len_bytes.into_iter())
std::iter::once(SEND_REQUEST_TAG)
.chain(std::iter::once(with_reply_surb as u8))
.chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant
.chain(data_len_bytes.iter().cloned())
.chain(data.into_iter())
.collect()
}
// SEND_REQUEST_TAG || recipient || conn_id || data_len || data
// SEND_REQUEST_TAG || with_reply || recipient || data_len || data
fn deserialize_send(b: &[u8]) -> Result<Self, error::Error> {
// we need to have at least 1 (tag) + Recipient::LEN + 2*sizeof<u64> bytes
if b.len() < 1 + Recipient::LEN + 2 * size_of::<u64>() {
// we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof<u64> bytes
if b.len() < 2 + Recipient::LEN + size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::TooShortRequest,
"not enough data provided to recover 'send'".to_string(),
@@ -123,10 +61,21 @@ impl ClientRequest {
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::Send as u8);
debug_assert_eq!(b[0], SEND_REQUEST_TAG);
let with_reply_surb = match b[1] {
0 => false,
1 => true,
n => {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
format!("invalid reply surb flag {}", n),
))
}
};
let mut recipient_bytes = [0u8; Recipient::LEN];
recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]);
recipient_bytes.copy_from_slice(&b[2..2 + Recipient::LEN]);
let recipient = match Recipient::try_from_bytes(recipient_bytes) {
Ok(recipient) => recipient,
Err(err) => {
@@ -137,20 +86,9 @@ impl ClientRequest {
}
};
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes
.copy_from_slice(&b[1 + Recipient::LEN..1 + Recipient::LEN + size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
let connection_id = if connection_id == 0 {
None
} else {
Some(connection_id)
};
let data_len_bytes =
&b[1 + Recipient::LEN + size_of::<u64>()..1 + Recipient::LEN + 2 * size_of::<u64>()];
let data_len_bytes = &b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::<u64>()];
let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap());
let data = &b[1 + Recipient::LEN + 2 * size_of::<u64>()..];
let data = &b[2 + Recipient::LEN + size_of::<u64>()..];
if data.len() as u64 != data_len {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
@@ -163,111 +101,32 @@ impl ClientRequest {
}
Ok(ClientRequest::Send {
with_reply_surb,
recipient,
message: data.to_vec(),
connection_id,
})
}
// SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || conn_id || data_len || data
fn serialize_send_anonymous(
recipient: Recipient,
data: Vec<u8>,
reply_surbs: u32,
connection_id: Option<u64>,
) -> Vec<u8> {
let data_len_bytes = (data.len() as u64).to_be_bytes();
let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes();
std::iter::once(ClientRequestTag::SendAnonymous as u8)
.chain(reply_surbs.to_be_bytes().into_iter())
.chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant
.chain(conn_id_bytes.into_iter())
.chain(data_len_bytes.into_iter())
.chain(data.into_iter())
.collect()
}
// SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || data_len || data
fn deserialize_send_anonymous(b: &[u8]) -> Result<Self, error::Error> {
// we need to have at least 1 (tag) + sizeof<u32> (num surbs) + Recipient::LEN + 2 *sizeof<u64> bytes
if b.len() < 1 + size_of::<u32>() + Recipient::LEN + 2 * size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::TooShortRequest,
"not enough data provided to recover 'send_anonymous'".to_string(),
));
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::SendAnonymous as u8);
let reply_surbs = u32::from_be_bytes([b[1], b[2], b[3], b[4]]);
let mut recipient_bytes = [0u8; Recipient::LEN];
recipient_bytes.copy_from_slice(&b[5..5 + Recipient::LEN]);
let recipient = match Recipient::try_from_bytes(recipient_bytes) {
Ok(recipient) => recipient,
Err(err) => {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
format!("malformed recipient: {:?}", err),
))
}
};
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes
.copy_from_slice(&b[5 + Recipient::LEN..5 + Recipient::LEN + size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
let connection_id = if connection_id == 0 {
None
} else {
Some(connection_id)
};
let data_len_bytes =
&b[5 + Recipient::LEN + size_of::<u64>()..5 + Recipient::LEN + 2 * size_of::<u64>()];
let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap());
let data = &b[5 + Recipient::LEN + 2 * size_of::<u64>()..];
if data.len() as u64 != data_len {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
format!(
"data len has inconsistent length. specified: {} got: {}",
data_len,
data.len()
),
));
}
Ok(ClientRequest::SendAnonymous {
reply_surbs,
recipient,
message: data.to_vec(),
connection_id,
})
}
// REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message
fn serialize_reply(
message: Vec<u8>,
sender_tag: AnonymousSenderTag,
connection_id: Option<u64>,
) -> Vec<u8> {
// REPLY_REQUEST_TAG || surb_len || surb || message_len || message
fn serialize_reply(message: Vec<u8>, reply_surb: ReplySurb) -> Vec<u8> {
let reply_surb_bytes = reply_surb.to_bytes();
let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes();
let message_len_bytes = (message.len() as u64).to_be_bytes();
let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes();
std::iter::once(ClientRequestTag::Reply as u8)
.chain(sender_tag.to_bytes().into_iter())
.chain(conn_id_bytes.into_iter())
.chain(message_len_bytes.into_iter())
std::iter::once(REPLY_REQUEST_TAG)
.chain(surb_len_bytes.iter().cloned())
.chain(reply_surb_bytes.into_iter())
.chain(message_len_bytes.iter().cloned())
.chain(message.into_iter())
.collect()
}
// REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message
// REPLY_REQUEST_TAG || surb_len || surb || message_len || message
fn deserialize_reply(b: &[u8]) -> Result<Self, error::Error> {
if b.len() < 1 + SENDER_TAG_SIZE + 2 * size_of::<u64>() {
// we need to have at the very least 2 * sizeof<u64> bytes (in case, for some peculiar reason
// message and reply surb were 0 len - the request would still be malformed, but would in theory
// be parse'able)
if b.len() < 1 + 2 * size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::TooShortRequest,
"not enough data provided to recover 'reply'".to_string(),
@@ -275,28 +134,42 @@ impl ClientRequest {
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::Reply as u8);
debug_assert_eq!(b[0], REPLY_REQUEST_TAG);
// the unwrap here is fine as we're definitely using exactly SENDER_TAG_SIZE bytes
let sender_tag =
AnonymousSenderTag::from_bytes(b[1..1 + SENDER_TAG_SIZE].try_into().unwrap());
let reply_surb_len =
u64::from_be_bytes(b[1..1 + size_of::<u64>()].as_ref().try_into().unwrap());
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes
.copy_from_slice(&b[1 + SENDER_TAG_SIZE..1 + SENDER_TAG_SIZE + size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
let connection_id = if connection_id == 0 {
None
} else {
Some(connection_id)
// make sure we won't go out of bounds here
if reply_surb_len > (b.len() - 1 + 2 * size_of::<u64>()) as u64 {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
format!(
"not enough data to recover reply surb with specified length {}",
reply_surb_len
),
));
}
let surb_bound = 1 + size_of::<u64>() + reply_surb_len as usize;
let reply_surb_bytes = &b[1 + size_of::<u64>()..surb_bound];
let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) {
Ok(reply_surb) => reply_surb,
Err(err) => {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
format!("malformed reply surb: {:?}", err),
))
}
};
let message_len = u64::from_be_bytes(
b[1 + SENDER_TAG_SIZE + size_of::<u64>()..1 + SENDER_TAG_SIZE + 2 * size_of::<u64>()]
b[surb_bound..surb_bound + size_of::<u64>()]
.as_ref()
.try_into()
.unwrap(),
);
let message = &b[1 + SENDER_TAG_SIZE + 2 * size_of::<u64>()..];
let message = &b[surb_bound + size_of::<u64>()..];
if message.len() as u64 != message_len {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
@@ -307,79 +180,26 @@ impl ClientRequest {
),
));
}
// TODO: should this blow HERE, i.e. during deserialization that the data you're trying
// to send via reply is too long?
Ok(ClientRequest::Reply {
reply_surb,
message: message.to_vec(),
sender_tag,
connection_id,
})
}
// SELF_ADDRESS_REQUEST_TAG
fn serialize_self_address() -> Vec<u8> {
vec![ClientRequestTag::SelfAddress as u8]
std::iter::once(SELF_ADDRESS_REQUEST_TAG).collect()
}
// SELF_ADDRESS_REQUEST_TAG
fn deserialize_self_address(b: &[u8]) -> Result<Self, error::Error> {
fn deserialize_self_address(b: &[u8]) -> Self {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::SelfAddress as u8);
debug_assert_eq!(b[0], SELF_ADDRESS_REQUEST_TAG);
Ok(ClientRequest::SelfAddress)
}
// CLOSED_CONNECTION_REQUEST_TAG
fn serialize_closed_connection(connection_id: u64) -> Vec<u8> {
let conn_id_bytes = connection_id.to_be_bytes();
std::iter::once(ClientRequestTag::ClosedConnection as u8)
.chain(conn_id_bytes.into_iter())
.collect()
}
// CLOSED_CONNECTION_REQUEST_TAG
fn deserialize_closed_connection(b: &[u8]) -> Result<Self, error::Error> {
if b.len() != 1 + size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
"The received closed connection has invalid length",
));
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::ClosedConnection as u8);
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
Ok(ClientRequest::ClosedConnection(connection_id))
}
// GET_LANE_QUEUE_LENGHT_TAG
fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec<u8> {
let conn_id_bytes = connection_id.to_be_bytes();
std::iter::once(ClientRequestTag::GetLaneQueueLength as u8)
.chain(conn_id_bytes.into_iter())
.collect()
}
// GET_LANE_QUEUE_LENGHT_TAG
fn deserialize_get_lane_queue_length(b: &[u8]) -> Result<Self, error::Error> {
if b.len() != 1 + size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::MalformedRequest,
"The received get lane queue lengths has invalid length",
));
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ClientRequestTag::GetLaneQueueLength as u8);
let mut connection_id_bytes = [0u8; size_of::<u64>()];
connection_id_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
let connection_id = u64::from_be_bytes(connection_id_bytes);
Ok(ClientRequest::GetLaneQueueLength(connection_id))
ClientRequest::SelfAddress
}
pub fn serialize(self) -> Vec<u8> {
@@ -387,27 +207,15 @@ impl ClientRequest {
ClientRequest::Send {
recipient,
message,
connection_id,
} => Self::serialize_send(recipient, message, connection_id),
ClientRequest::SendAnonymous {
recipient,
message,
reply_surbs,
connection_id,
} => Self::serialize_send_anonymous(recipient, message, reply_surbs, connection_id),
with_reply_surb,
} => Self::serialize_send(recipient, message, with_reply_surb),
ClientRequest::Reply {
message,
sender_tag,
connection_id,
} => Self::serialize_reply(message, sender_tag, connection_id),
reply_surb,
} => Self::serialize_reply(message, reply_surb),
ClientRequest::SelfAddress => Self::serialize_self_address(),
ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id),
ClientRequest::GetLaneQueueLength(id) => Self::serialize_get_lane_queue_lengths(id),
}
}
@@ -421,21 +229,31 @@ impl ClientRequest {
));
}
let request_tag = ClientRequestTag::try_from(b[0])?;
if b.len() < size_of::<u8>() {
return Err(error::Error::new(
ErrorKind::TooShortRequest,
format!(
"not enough data provided to recover request tag. Provided only {} bytes",
b.len()
),
));
}
let request_tag = b[0];
// determine what kind of request that is and try to deserialize it
match request_tag {
ClientRequestTag::Send => Self::deserialize_send(b),
ClientRequestTag::SendAnonymous => Self::deserialize_send_anonymous(b),
ClientRequestTag::Reply => Self::deserialize_reply(b),
ClientRequestTag::SelfAddress => Self::deserialize_self_address(b),
ClientRequestTag::ClosedConnection => Self::deserialize_closed_connection(b),
ClientRequestTag::GetLaneQueueLength => Self::deserialize_get_lane_queue_length(b),
SEND_REQUEST_TAG => Self::deserialize_send(b),
REPLY_REQUEST_TAG => Self::deserialize_reply(b),
SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)),
n => Err(error::Error::new(
ErrorKind::UnknownRequest,
format!("type {}", n),
)),
}
}
pub fn try_from_binary(raw_req: &[u8]) -> Result<Self, error::Error> {
Self::deserialize(raw_req)
pub fn try_from_binary(raw_req: Vec<u8>) -> Result<Self, error::Error> {
Self::deserialize(&raw_req)
}
pub fn try_from_text(raw_req: String) -> Result<Self, error::Error> {
@@ -458,52 +276,44 @@ mod tests {
let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap();
let recipient_string = recipient.to_string();
let send_request = ClientRequest::Send {
let send_request_no_surb = ClientRequest::Send {
recipient,
message: b"foomp".to_vec(),
connection_id: Some(42),
with_reply_surb: false,
};
let bytes = send_request.serialize();
let bytes = send_request_no_surb.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::Send {
recipient,
message,
connection_id,
with_reply_surb,
} => {
assert_eq!(recipient.to_string(), recipient_string);
assert_eq!(message, b"foomp".to_vec());
assert_eq!(connection_id, Some(42))
assert!(!with_reply_surb)
}
_ => unreachable!(),
}
}
#[test]
fn send_anonymous_request_serialization_works() {
let original_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap();
let send_anonymous_request = ClientRequest::SendAnonymous {
recipient: original_recipient,
let send_request_surb = ClientRequest::Send {
recipient,
message: b"foomp".to_vec(),
reply_surbs: 666,
connection_id: Some(42),
with_reply_surb: true,
};
let bytes = send_anonymous_request.serialize();
let bytes = send_request_surb.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::SendAnonymous {
ClientRequest::Send {
recipient,
message,
reply_surbs,
connection_id,
with_reply_surb,
} => {
assert_eq!(recipient, original_recipient);
assert_eq!(recipient.to_string(), recipient_string);
assert_eq!(message, b"foomp".to_vec());
assert_eq!(connection_id, Some(42));
assert_eq!(reply_surbs, 666)
assert!(with_reply_surb)
}
_ => unreachable!(),
}
@@ -511,23 +321,22 @@ mod tests {
#[test]
fn reply_request_serialization_works() {
let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X";
let reply_surb = ReplySurb::from_base58_string(reply_surb_string).unwrap();
let reply_request = ClientRequest::Reply {
sender_tag: [8u8; SENDER_TAG_SIZE].into(),
message: b"foomp".to_vec(),
connection_id: Some(42),
reply_surb,
};
let bytes = reply_request.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::Reply {
sender_tag,
reply_surb,
message,
connection_id,
} => {
assert_eq!(sender_tag, [8u8; SENDER_TAG_SIZE].into());
assert_eq!(reply_surb.to_base58_string(), reply_surb_string);
assert_eq!(message, b"foomp".to_vec());
assert_eq!(connection_id, Some(42));
}
_ => unreachable!(),
}
@@ -543,26 +352,4 @@ mod tests {
_ => unreachable!(),
}
}
#[test]
fn close_connection_request_serialization_works() {
let close_connection_request = ClientRequest::ClosedConnection(42);
let bytes = close_connection_request.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::ClosedConnection(id) => assert_eq!(id, 42),
_ => unreachable!(),
}
}
#[test]
fn get_lane_queue_length_request_serialization_works() {
let close_connection_request = ClientRequest::GetLaneQueueLength(42);
let bytes = close_connection_request.serialize();
let recovered = ClientRequest::deserialize(&bytes).unwrap();
match recovered {
ClientRequest::GetLaneQueueLength(id) => assert_eq!(id, 42),
_ => unreachable!(),
}
}
}
+138 -147
View File
@@ -1,54 +1,32 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// all variable size data is always prefixed with u64 length
// tags are u8
#![allow(unknown_lints)] // due to using `clippy::branches_sharing_code` which does not exist on `stable` just yet
use crate::error::{self, ErrorKind};
use crate::text::ServerResponseText;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::receiver::ReconstructedMessage;
use std::convert::TryInto;
use std::mem::size_of;
#[repr(u8)]
enum ServerResponseTag {
/// Value tag representing [`Error`] variant of the [`ServerResponse`]
Error = 0x00,
/// Value tag representing [`Error`] variant of the [`ServerResponse`]
pub const ERROR_RESPONSE_TAG: u8 = 0x00;
/// Value tag representing [`Received`] variant of the [`ServerResponse`]
Received = 0x01,
/// Value tag representing [`Received`] variant of the [`ServerResponse`]
pub const RECEIVED_RESPONSE_TAG: u8 = 0x01;
/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`]
SelfAddress = 0x02,
/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`]
LaneQueueLength = 0x03,
}
impl TryFrom<u8> for ServerResponseTag {
type Error = error::Error;
fn try_from(value: u8) -> Result<Self, error::Error> {
match value {
_ if value == (Self::Error as u8) => Ok(Self::Error),
_ if value == (Self::Received as u8) => Ok(Self::Received),
_ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress),
_ if value == (Self::LaneQueueLength as u8) => Ok(Self::LaneQueueLength),
n => Err(error::Error::new(
ErrorKind::UnknownResponse,
format!("{n} does not correspond to any valid response tag"),
)),
}
}
}
/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`]
pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02;
#[derive(Debug)]
pub enum ServerResponse {
Received(ReconstructedMessage),
SelfAddress(Box<Recipient>),
LaneQueueLength { lane: u64, queue_length: usize },
SelfAddress(Recipient),
Error(error::Error),
}
@@ -60,19 +38,24 @@ impl ServerResponse {
})
}
// RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option<sender_tag> || msg_len || msg
// RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg
fn serialize_received(reconstructed_message: ReconstructedMessage) -> Vec<u8> {
let message_len_bytes = (reconstructed_message.message.len() as u64).to_be_bytes();
if let Some(reply_surb) = reconstructed_message.reply_surb {
let reply_surb_bytes = reply_surb.to_bytes();
let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes();
if let Some(sender_tag) = reconstructed_message.sender_tag {
std::iter::once(ServerResponseTag::Received as u8)
// with_reply || surb_len || surb || msg_len || msg
std::iter::once(RECEIVED_RESPONSE_TAG)
.chain(std::iter::once(true as u8))
.chain(sender_tag.to_bytes().into_iter())
.chain(surb_len_bytes.iter().cloned())
.chain(reply_surb_bytes.iter().cloned())
.chain(message_len_bytes.iter().cloned())
.chain(reconstructed_message.message.into_iter())
.collect()
} else {
std::iter::once(ServerResponseTag::Received as u8)
// without_reply || msg_len || msg
std::iter::once(RECEIVED_RESPONSE_TAG)
.chain(std::iter::once(false as u8))
.chain(message_len_bytes.iter().cloned())
.chain(reconstructed_message.message.into_iter())
@@ -80,9 +63,10 @@ impl ServerResponse {
}
}
// RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option<sender_tag> || msg_len || msg
// RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg
fn deserialize_received(b: &[u8]) -> Result<Self, error::Error> {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], RECEIVED_RESPONSE_TAG);
// we must be able to read at the very least if it has a reply_surb and length of some field
if b.len() < 2 + size_of::<u64>() {
@@ -91,70 +75,101 @@ impl ServerResponse {
"not enough data provided to recover 'received'".to_string(),
));
}
debug_assert_eq!(b[0], ServerResponseTag::Received as u8);
let has_sender_tag = match b[1] {
let with_reply_surb = match b[1] {
0 => false,
1 => true,
n => {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!("invalid sender tag flag {n}"),
format!("invalid reply flag {}", n),
))
}
};
let mut i = 2;
let sender_tag = if has_sender_tag {
if b[2..].len() < SENDER_TAG_SIZE {
// this is a false positive as even though the code is the same, it refers to different things
#[allow(clippy::branches_sharing_code)]
if with_reply_surb {
let reply_surb_len =
u64::from_be_bytes(b[2..2 + size_of::<u64>()].as_ref().try_into().unwrap());
// make sure we won't go out of bounds here
if reply_surb_len > (b.len() - 2 + 2 * size_of::<u64>()) as u64 {
return Err(error::Error::new(
ErrorKind::TooShortResponse,
"not enough data provided to recover 'received'".to_string(),
ErrorKind::MalformedResponse,
"not enough bytes to read reply_surb bytes!".to_string(),
));
}
i += SENDER_TAG_SIZE;
Some(AnonymousSenderTag::from_bytes(
b[2..2 + SENDER_TAG_SIZE].try_into().unwrap(),
))
let surb_bound = 2 + size_of::<u64>() + reply_surb_len as usize;
let reply_surb_bytes = &b[2 + size_of::<u64>()..surb_bound];
let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) {
Ok(reply_surb) => reply_surb,
Err(err) => {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!("malformed reply SURB: {:?}", err),
))
}
};
let message_len = u64::from_be_bytes(
b[surb_bound..surb_bound + size_of::<u64>()]
.as_ref()
.try_into()
.unwrap(),
);
let message = &b[surb_bound + size_of::<u64>()..];
if message.len() as u64 != message_len {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!(
"message len has inconsistent length. specified: {} got: {}",
message_len,
message.len()
),
));
}
Ok(ServerResponse::Received(ReconstructedMessage {
message: message.to_vec(),
reply_surb: Some(reply_surb),
}))
} else {
None
};
let message_len =
u64::from_be_bytes(b[2..2 + size_of::<u64>()].as_ref().try_into().unwrap());
let message = &b[2 + size_of::<u64>()..];
if message.len() as u64 != message_len {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!(
"message len has inconsistent length. specified: {} got: {}",
message_len,
message.len()
),
));
}
if b[i..].len() < size_of::<u64>() {
return Err(error::Error::new(
ErrorKind::TooShortResponse,
"not enough data provided to recover 'received'".to_string(),
));
Ok(ServerResponse::Received(ReconstructedMessage {
message: message.to_vec(),
reply_surb: None,
}))
}
let message_len = u64::from_be_bytes(b[i..i + size_of::<u64>()].try_into().unwrap());
let message = &b[i + size_of::<u64>()..];
if message.len() as u64 != message_len {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!(
"message len has inconsistent length. specified: {} got: {}",
message_len,
message.len()
),
));
}
Ok(ServerResponse::Received(ReconstructedMessage {
message: message.to_vec(),
sender_tag,
}))
}
// SELF_ADDRESS_RESPONSE_TAG || self_address
fn serialize_self_address(address: Recipient) -> Vec<u8> {
std::iter::once(ServerResponseTag::SelfAddress as u8)
.chain(address.to_bytes().into_iter())
std::iter::once(SELF_ADDRESS_RESPONSE_TAG)
.chain(address.to_bytes().iter().cloned())
.collect()
}
// SELF_ADDRESS_RESPONSE_TAG || self_address
fn deserialize_self_address(b: &[u8]) -> Result<Self, error::Error> {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], SELF_ADDRESS_RESPONSE_TAG);
if b.len() != 1 + Recipient::LEN {
return Err(error::Error::new(
ErrorKind::TooShortResponse,
@@ -162,9 +177,6 @@ impl ServerResponse {
));
}
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ServerResponseTag::SelfAddress as u8);
let mut recipient_bytes = [0u8; Recipient::LEN];
recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]);
@@ -178,40 +190,15 @@ impl ServerResponse {
}
};
Ok(ServerResponse::SelfAddress(Box::new(recipient)))
}
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec<u8> {
std::iter::once(ServerResponseTag::LaneQueueLength as u8)
.chain(lane.to_be_bytes().iter().cloned())
.chain(queue_length.to_be_bytes().iter().cloned())
.collect()
}
// LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length
fn deserialize_lane_queue_length(b: &[u8]) -> Result<Self, error::Error> {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ServerResponseTag::LaneQueueLength as u8);
let mut lane_bytes = [0u8; size_of::<u64>()];
lane_bytes.copy_from_slice(&b[1..=size_of::<u64>()]);
let lane = u64::from_be_bytes(lane_bytes);
let mut queue_length_bytes = [0u8; size_of::<usize>()];
queue_length_bytes
.copy_from_slice(&b[1 + size_of::<u64>()..1 + size_of::<u64>() + size_of::<usize>()]);
let queue_length = usize::from_be_bytes(queue_length_bytes);
Ok(ServerResponse::LaneQueueLength { lane, queue_length })
Ok(ServerResponse::SelfAddress(recipient))
}
// ERROR_RESPONSE_TAG || err_code || msg_len || msg
fn serialize_error(error: error::Error) -> Vec<u8> {
let message_len_bytes = (error.message.len() as u64).to_be_bytes();
std::iter::once(ServerResponseTag::Error as u8)
std::iter::once(ERROR_RESPONSE_TAG)
.chain(std::iter::once(error.kind as u8))
.chain(message_len_bytes.into_iter())
.chain(message_len_bytes.iter().cloned())
.chain(error.message.into_bytes().into_iter())
.collect()
}
@@ -219,7 +206,7 @@ impl ServerResponse {
// ERROR_RESPONSE_TAG || err_code || msg_len || msg
fn deserialize_error(b: &[u8]) -> Result<Self, error::Error> {
// this MUST match because it was called by 'deserialize'
debug_assert_eq!(b[0], ServerResponseTag::Error as u8);
debug_assert_eq!(b[0], ERROR_RESPONSE_TAG);
if b.len() < size_of::<u8>() + size_of::<u64>() {
return Err(error::Error::new(
@@ -228,7 +215,26 @@ impl ServerResponse {
));
}
let error_kind = ErrorKind::try_from(b[1])?;
let error_kind = match b[1] {
_ if b[1] == (ErrorKind::EmptyRequest as u8) => ErrorKind::EmptyRequest,
_ if b[1] == (ErrorKind::TooShortRequest as u8) => ErrorKind::TooShortRequest,
_ if b[1] == (ErrorKind::UnknownRequest as u8) => ErrorKind::UnknownRequest,
_ if b[1] == (ErrorKind::MalformedRequest as u8) => ErrorKind::MalformedRequest,
_ if b[1] == (ErrorKind::EmptyResponse as u8) => ErrorKind::EmptyResponse,
_ if b[1] == (ErrorKind::TooShortResponse as u8) => ErrorKind::TooShortResponse,
_ if b[1] == (ErrorKind::UnknownResponse as u8) => ErrorKind::UnknownResponse,
_ if b[1] == (ErrorKind::MalformedResponse as u8) => ErrorKind::MalformedResponse,
_ if b[1] == (ErrorKind::Other as u8) => ErrorKind::Other,
n => {
return Err(error::Error::new(
ErrorKind::MalformedResponse,
format!("invalid error code {}", n),
))
}
};
let message_len =
u64::from_be_bytes(b[2..2 + size_of::<u64>()].as_ref().try_into().unwrap());
@@ -265,10 +271,7 @@ impl ServerResponse {
ServerResponse::Received(reconstructed_message) => {
Self::serialize_received(reconstructed_message)
}
ServerResponse::SelfAddress(address) => Self::serialize_self_address(*address),
ServerResponse::LaneQueueLength { lane, queue_length } => {
Self::serialize_lane_queue_length(lane, queue_length)
}
ServerResponse::SelfAddress(address) => Self::serialize_self_address(address),
ServerResponse::Error(err) => Self::serialize_error(err),
}
}
@@ -293,14 +296,17 @@ impl ServerResponse {
));
}
let response_tag = ServerResponseTag::try_from(b[0])?;
let response_tag = b[0];
// determine what kind of response that is and try to deserialize it
match response_tag {
ServerResponseTag::Received => Self::deserialize_received(b),
ServerResponseTag::SelfAddress => Self::deserialize_self_address(b),
ServerResponseTag::LaneQueueLength => Self::deserialize_lane_queue_length(b),
ServerResponseTag::Error => Self::deserialize_error(b),
RECEIVED_RESPONSE_TAG => Self::deserialize_received(b),
SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b),
ERROR_RESPONSE_TAG => Self::deserialize_error(b),
n => Err(error::Error::new(
ErrorKind::UnknownResponse,
format!("type {}", n),
)),
}
}
@@ -322,33 +328,35 @@ mod tests {
#[test]
fn received_response_serialization_works() {
let received_with_sender_tag = ServerResponse::Received(ReconstructedMessage {
let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X";
let received_with_surb = ServerResponse::Received(ReconstructedMessage {
message: b"foomp".to_vec(),
sender_tag: Some([42u8; SENDER_TAG_SIZE].into()),
reply_surb: Some(ReplySurb::from_base58_string(reply_surb_string).unwrap()),
});
let bytes = received_with_sender_tag.serialize();
let bytes = received_with_surb.serialize();
let recovered = ServerResponse::deserialize(&bytes).unwrap();
match recovered {
ServerResponse::Received(reconstructed) => {
assert_eq!(reconstructed.message, b"foomp".to_vec());
assert_eq!(
reconstructed.sender_tag,
Some([42u8; SENDER_TAG_SIZE].into())
reconstructed.reply_surb.unwrap().to_base58_string(),
reply_surb_string
)
}
_ => unreachable!(),
}
let received_without_sender_tag = ServerResponse::Received(ReconstructedMessage {
let received_without_surb = ServerResponse::Received(ReconstructedMessage {
message: b"foomp".to_vec(),
sender_tag: None,
reply_surb: None,
});
let bytes = received_without_sender_tag.serialize();
let bytes = received_without_surb.serialize();
let recovered = ServerResponse::deserialize(&bytes).unwrap();
match recovered {
ServerResponse::Received(reconstructed) => {
assert_eq!(reconstructed.message, b"foomp".to_vec());
assert!(reconstructed.sender_tag.is_none())
assert!(reconstructed.reply_surb.is_none())
}
_ => unreachable!(),
}
@@ -359,7 +367,7 @@ mod tests {
let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap();
let recipient_string = recipient.to_string();
let self_address_response = ServerResponse::SelfAddress(Box::new(recipient));
let self_address_response = ServerResponse::SelfAddress(recipient);
let bytes = self_address_response.serialize();
let recovered = ServerResponse::deserialize(&bytes).unwrap();
match recovered {
@@ -370,23 +378,6 @@ mod tests {
}
}
#[test]
fn lane_queue_length_response_serialization_works() {
let lane_queue_length_response = ServerResponse::LaneQueueLength {
lane: 13,
queue_length: 42,
};
let bytes = lane_queue_length_response.serialize();
let recovered = ServerResponse::deserialize(&bytes).unwrap();
match recovered {
ServerResponse::LaneQueueLength { lane, queue_length } => {
assert_eq!(lane, 13);
assert_eq!(queue_length, 42)
}
_ => unreachable!(),
}
}
#[test]
fn error_response_serialization_works() {
let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string());
+19 -52
View File
@@ -1,11 +1,11 @@
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ErrorKind;
use crate::requests::ClientRequest;
use crate::responses::ServerResponse;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::anonymous_replies::requests::AnonymousSenderTag;
use nymsphinx::anonymous_replies::ReplySurb;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
@@ -19,22 +19,14 @@ pub(super) enum ClientRequestText {
Send {
message: String,
recipient: String,
connection_id: Option<u64>,
},
#[serde(rename_all = "camelCase")]
SendAnonymous {
recipient: String,
message: String,
reply_surbs: u32,
connection_id: Option<u64>,
},
#[serde(rename_all = "camelCase")]
Reply {
sender_tag: String,
message: String,
connection_id: Option<u64>,
with_reply_surb: bool,
},
SelfAddress,
#[serde(rename_all = "camelCase")]
Reply {
message: String,
reply_surb: String,
},
}
impl TryFrom<String> for ClientRequestText {
@@ -53,7 +45,7 @@ impl TryInto<ClientRequest> for ClientRequestText {
ClientRequestText::Send {
message,
recipient,
connection_id,
with_reply_surb,
} => {
let message_bytes = message.into_bytes();
let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| {
@@ -63,42 +55,22 @@ impl TryInto<ClientRequest> for ClientRequestText {
Ok(ClientRequest::Send {
message: message_bytes,
recipient,
connection_id,
})
}
ClientRequestText::SendAnonymous {
recipient,
message,
reply_surbs,
connection_id,
} => {
let message_bytes = message.into_bytes();
let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| {
Self::Error::new(ErrorKind::MalformedRequest, err.to_string())
})?;
Ok(ClientRequest::SendAnonymous {
recipient,
message: message_bytes,
reply_surbs,
connection_id,
with_reply_surb,
})
}
ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress),
ClientRequestText::Reply {
sender_tag,
message,
connection_id,
reply_surb,
} => {
let message_bytes = message.into_bytes();
let sender_tag =
AnonymousSenderTag::try_from_base58_string(sender_tag).map_err(|err| {
Self::Error::new(ErrorKind::MalformedRequest, err.to_string())
})?;
let reply_surb = ReplySurb::from_base58_string(reply_surb).map_err(|err| {
Self::Error::new(ErrorKind::MalformedRequest, err.to_string())
})?;
Ok(ClientRequest::Reply {
sender_tag,
message: message_bytes,
connection_id,
reply_surb,
})
}
}
@@ -114,15 +86,11 @@ pub(super) enum ServerResponseText {
#[serde(rename_all = "camelCase")]
Received {
message: String,
sender_tag: Option<String>,
reply_surb: Option<String>,
},
SelfAddress {
address: String,
},
LaneQueueLength {
lane: u64,
queue_length: usize,
},
Error {
message: String,
},
@@ -156,15 +124,14 @@ impl From<ServerResponse> for ServerResponseText {
// TODO: ask DH what is more appropriate, lossy utf8 conversion or returning error and then
// pure binary later
message: String::from_utf8_lossy(&reconstructed.message).into_owned(),
sender_tag: reconstructed.sender_tag.map(|tag| tag.to_base58_string()),
reply_surb: reconstructed
.reply_surb
.map(|reply_surb| reply_surb.to_base58_string()),
}
}
ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress {
address: recipient.to_string(),
},
ServerResponse::LaneQueueLength { lane, queue_length } => {
ServerResponseText::LaneQueueLength { lane, queue_length }
}
ServerResponse::Error(err) => ServerResponseText::Error {
message: err.to_string(),
},
+3 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.1"
version = "1.0.2"
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"
@@ -20,20 +20,17 @@ pretty_env_logger = "0.4"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { version = "1.0", features = ["derive"] } # for config serialization/deserialization
snafu = "0.6"
thiserror = "1.0.34"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] }
url = "2.2"
# internal
client-core = { path = "../client-core", features = ["fs-surb-storage"] }
client-connections = { path = "../../common/client-connections" }
client-core = { path = "../client-core" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
config = { path = "../../common/config" }
completions = { path = "../../common/completions" }
credential-storage = { path = "../../common/credential-storage" }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
logging = { path = "../../common/logging"}
gateway-client = { path = "../../common/client-libs/gateway-client" }
gateway-requests = { path = "../../gateway/gateway-requests" }
network-defaults = { path = "../../common/network-defaults" }
@@ -46,7 +43,6 @@ task = { path = "../../common/task" }
topology = { path = "../../common/topology" }
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
version-checker = { path = "../../common/version-checker" }
tap = "1.0.1"
[features]
coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"]
+7 -63
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::config::template::config_template;
use client_core::config::Config as BaseConfig;
pub use client_core::config::MISSING_VALUE;
use client_core::config::{Config as BaseConfig, DebugConfig};
use config::defaults::DEFAULT_SOCKS5_LISTENING_PORT;
use config::NymConfig;
use nymsphinx::addressing::clients::Recipient;
@@ -12,9 +12,6 @@ use std::path::PathBuf;
mod template;
const DEFAULT_CONNECTION_START_SURBS: u32 = 20;
const DEFAULT_PER_REQUEST_SURBS: u32 = 3;
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
@@ -22,9 +19,6 @@ pub struct Config {
base: BaseConfig<Config>,
socks5: Socks5,
#[serde(default)]
socks5_debug: Socks5Debug,
}
impl NymConfig for Config {
@@ -63,7 +57,6 @@ impl Config {
Config {
base: BaseConfig::new(id),
socks5: Socks5::new(provider_mix_address),
socks5_debug: Socks5Debug::default(),
}
}
@@ -77,24 +70,7 @@ impl Config {
self
}
pub fn with_anonymous_replies(mut self, anonymous_replies: bool) -> Self {
self.socks5.send_anonymously = anonymous_replies;
self
}
// getters
pub fn get_base(&self) -> &BaseConfig<Self> {
&self.base
}
pub fn get_base_mut(&mut self) -> &mut BaseConfig<Self> {
&mut self.base
}
pub fn get_debug_settings(&self) -> &DebugConfig {
self.get_base().get_debug_config()
}
pub fn get_config_file_save_location(&self) -> PathBuf {
self.config_directory().join(Self::config_file_name())
}
@@ -104,21 +80,17 @@ impl Config {
.expect("malformed provider address")
}
pub fn get_send_anonymously(&self) -> bool {
self.socks5.send_anonymously
pub fn get_base(&self) -> &BaseConfig<Self> {
&self.base
}
pub fn get_base_mut(&mut self) -> &mut BaseConfig<Self> {
&mut self.base
}
pub fn get_listening_port(&self) -> u16 {
self.socks5.listening_port
}
pub fn get_connection_start_surbs(&self) -> u32 {
self.socks5_debug.connection_start_surbs
}
pub fn get_per_request_surbs(&self) -> u32 {
self.socks5_debug.per_request_surbs
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
@@ -129,13 +101,6 @@ pub struct Socks5 {
/// The mix address of the provider to which all requests are going to be sent.
provider_mix_address: String,
/// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider.
/// While this is going to hide its actual address information, it will make the actual communication
/// slower and consume nearly double the bandwidth as it will require sending reply SURBs.
///
/// Note that some service providers might not support this.
send_anonymously: bool,
}
impl Socks5 {
@@ -143,7 +108,6 @@ impl Socks5 {
Socks5 {
listening_port: DEFAULT_SOCKS5_LISTENING_PORT,
provider_mix_address: provider_mix_address.into(),
send_anonymously: false,
}
}
}
@@ -153,26 +117,6 @@ impl Default for Socks5 {
Socks5 {
listening_port: DEFAULT_SOCKS5_LISTENING_PORT,
provider_mix_address: "".into(),
send_anonymously: false,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Socks5Debug {
/// Number of reply SURBs attached to each `Request::Connect` message.
connection_start_surbs: u32,
/// Number of reply SURBs attached to each `Request::Send` message.
per_request_surbs: u32,
}
impl Default for Socks5Debug {
fn default() -> Self {
Socks5Debug {
connection_start_surbs: DEFAULT_CONNECTION_START_SURBS,
per_request_surbs: DEFAULT_PER_REQUEST_SURBS,
}
}
}
+4 -19
View File
@@ -23,13 +23,6 @@ id = '{{ client.id }}'
# to claim bandwidth without presenting bandwidth credentials.
disabled_credentials_mode = {{ client.disabled_credentials_mode }}
# Addresses to nymd validators via which the client can communicate with the chain.
validator_urls = [
{{#each client.validator_urls }}
'{{this}}',
{{/each}}
]
# Addresses to APIs running on validator from which the client gets the view of the network.
validator_api_urls = [
{{#each client.validator_api_urls }}
@@ -49,12 +42,13 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}'
# Path to file containing public encryption key.
public_encryption_key_file = '{{ client.public_encryption_key_file }}'
# Full path to file containing reply encryption keys of all reply-SURBs we have ever
# sent but not received back.
reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}'
# Path to the database containing bandwidth credentials
database_path = '{{ client.database_path }}'
# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags.
reply_surb_database_path = '{{ client.reply_surb_database_path }}'
##### additional client config options #####
# A gateway specific, optional, base58 stringified shared key used for
@@ -91,12 +85,6 @@ provider_mix_address = '{{ socks5.provider_mix_address }}'
# The port on which the client will be listening for incoming requests
listening_port = {{ socks5.listening_port }}
# Specifies whether this client is going to use an anonymous sender tag for communication with the service provider.
# While this is going to hide its actual address information, it will make the actual communication
# slower and consume nearly double the bandwidth as it will require sending reply SURBs.
#
# Note that some service providers might not support this.
send_anonymously = {{ socks5.send_anonymously }}
##### logging configuration options #####
@@ -109,9 +97,6 @@ send_anonymously = {{ socks5.send_anonymously }}
# The following options should not be modified unless you know EXACTLY what you are doing
# as if set incorrectly, they may impact your anonymity.
# [socks5_debug]
[debug]
average_packet_delay = '{{ debug.average_packet_delay }}'
+303 -115
View File
@@ -1,25 +1,43 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::atomic::Ordering;
use client_core::client::cover_traffic_stream::LoopCoverTrafficStream;
use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::key_manager::KeyManager;
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
};
use client_core::client::reply_key_storage::ReplyKeyStorage;
use client_core::client::topology_control::{
TopologyAccessor, TopologyRefresher, TopologyRefresherConfig,
};
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use crypto::asymmetric::identity;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::bandwidth::BandwidthController;
use gateway_client::{
AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver,
MixnetMessageSender,
};
use log::*;
use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
use crate::client::config::Config;
use crate::error::Socks5ClientError;
use crate::socks;
use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
use client_core::client::base_client::{
non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput,
};
use client_core::client::key_manager::KeyManager;
use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::bandwidth::BandwidthController;
use log::*;
use nymsphinx::addressing::clients::Recipient;
use std::error::Error;
use task::{wait_for_signal_and_error, ShutdownListener, ShutdownNotifier};
pub mod config;
@@ -53,110 +71,238 @@ impl NymClient {
}
}
async fn create_bandwidth_controller(config: &Config) -> BandwidthController {
pub fn as_mix_recipient(&self) -> Recipient {
Recipient::new(
*self.key_manager.identity_keypair().public_key(),
*self.key_manager.encryption_keypair().public_key(),
// TODO: below only works under assumption that gateway address == gateway id
// (which currently is true)
NodeIdentity::from_base58_string(self.config.get_base().get_gateway_id()).unwrap(),
)
}
// future constantly pumping loop cover traffic at some specified average rate
// the pumped traffic goes to the MixTrafficController
fn start_cover_traffic_stream(
&self,
topology_accessor: TopologyAccessor,
mix_tx: BatchMixMessageSender,
shutdown: ShutdownListener,
) {
info!("Starting loop cover traffic stream...");
LoopCoverTrafficStream::new(
self.key_manager.ack_key(),
self.config.get_base().get_average_ack_delay(),
self.config.get_base().get_average_packet_delay(),
self.config
.get_base()
.get_loop_cover_traffic_average_delay(),
mix_tx,
self.as_mix_recipient(),
topology_accessor,
shutdown,
)
.start();
}
fn start_real_traffic_controller(
&self,
topology_accessor: TopologyAccessor,
reply_key_storage: ReplyKeyStorage,
ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender,
shutdown: ShutdownListener,
) {
let controller_config = client_core::client::real_messages_control::Config::new(
self.key_manager.ack_key(),
self.config.get_base().get_ack_wait_multiplier(),
self.config.get_base().get_ack_wait_addition(),
self.config.get_base().get_average_ack_delay(),
self.config.get_base().get_message_sending_average_delay(),
self.config.get_base().get_average_packet_delay(),
self.as_mix_recipient(),
);
info!("Starting real traffic stream...");
RealMessagesController::new(
controller_config,
ack_receiver,
input_receiver,
mix_sender,
topology_accessor,
reply_key_storage,
shutdown,
)
.start();
}
// buffer controlling all messages fetched from provider
// required so that other components would be able to use them (say the websocket)
fn start_received_messages_buffer_controller(
&self,
query_receiver: ReceivedBufferRequestReceiver,
mixnet_receiver: MixnetMessageReceiver,
reply_key_storage: ReplyKeyStorage,
shutdown: ShutdownListener,
) {
info!("Starting received messages buffer controller...");
ReceivedMessagesBufferController::new(
self.key_manager.encryption_keypair(),
query_receiver,
mixnet_receiver,
reply_key_storage,
shutdown,
)
.start()
}
async fn start_gateway_client(
&mut self,
mixnet_message_sender: MixnetMessageSender,
ack_sender: AcknowledgementSender,
shutdown: ShutdownListener,
) -> GatewayClient {
let gateway_id = self.config.get_base().get_gateway_id();
if gateway_id.is_empty() {
panic!("The identity of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_owner = self.config.get_base().get_gateway_owner();
if gateway_owner.is_empty() {
panic!("The owner of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_address = self.config.get_base().get_gateway_listener();
if gateway_address.is_empty() {
panic!("The address of the gateway is unknown - did you run `nym-client` init?")
}
let gateway_identity = identity::PublicKey::from_base58_string(gateway_id)
.expect("provided gateway id is invalid!");
#[cfg(feature = "coconut")]
let bandwidth_controller = {
let details = network_defaults::NymNetworkDetails::new_from_env();
let client_config = validator_client::Config::try_from_nym_network_details(&details)
.expect("failed to construct validator client config");
let client = validator_client::Client::new_query(client_config)
.expect("Could not construct query client");
let coconut_api_clients =
validator_client::CoconutApiClient::all_coconut_api_clients(&client)
.await
.expect("Could not query api clients");
BandwidthController::new(
credential_storage::initialise_storage(config.get_base().get_database_path()).await,
coconut_api_clients,
)
};
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
self.config.get_base().get_validator_api_endpoints(),
);
#[cfg(not(feature = "coconut"))]
let bandwidth_controller = BandwidthController::new(
credential_storage::initialise_storage(config.get_base().get_database_path()).await,
credential_storage::initialise_storage(self.config.get_base().get_database_path())
.await,
)
.expect("Could not create bandwidth controller");
bandwidth_controller
let mut gateway_client = GatewayClient::new(
gateway_address,
self.key_manager.identity_keypair(),
gateway_identity,
gateway_owner,
Some(self.key_manager.gateway_shared_key()),
mixnet_message_sender,
ack_sender,
self.config.get_base().get_gateway_response_timeout(),
Some(bandwidth_controller),
Some(shutdown),
);
gateway_client
.set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode());
gateway_client
.authenticate_and_start()
.await
.expect("could not authenticate and start up the gateway connection");
gateway_client
}
// future responsible for periodically polling directory server and updating
// the current global view of topology
async fn start_topology_refresher(
&mut self,
topology_accessor: TopologyAccessor,
shutdown: ShutdownListener,
) {
let topology_refresher_config = TopologyRefresherConfig::new(
self.config.get_base().get_validator_api_endpoints(),
self.config.get_base().get_topology_refresh_rate(),
env!("CARGO_PKG_VERSION").to_string(),
);
let mut topology_refresher =
TopologyRefresher::new(topology_refresher_config, topology_accessor);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
info!("Obtaining initial network topology");
topology_refresher.refresh().await;
// TODO: a slightly more graceful termination here
if !topology_refresher.is_topology_routable().await {
panic!(
"The current network topology seem to be insufficient to route any packets through\
- check if enough nodes and a gateway are online"
);
}
info!("Starting topology refresher...");
topology_refresher.start(shutdown);
}
// controller for sending sphinx packets to mixnet (either real traffic or cover traffic)
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) {
info!("Starting mix traffic controller...");
MixTrafficController::new(mix_rx, gateway_client, shutdown).start();
}
fn start_socks5_listener(
config: &Config,
client_input: ClientInput,
client_output: ClientOutput,
self_address: Recipient,
&self,
buffer_requester: ReceivedBufferRequestSender,
msg_input: InputMessageSender,
shutdown: ShutdownListener,
) {
info!("Starting socks5 listener...");
let auth_methods = vec![AuthenticationMethods::NoAuth as u8];
let allowed_users: Vec<User> = Vec::new();
let ClientInput {
shared_lane_queue_lengths,
connection_command_sender,
input_sender,
} = client_input;
let received_buffer_request_sender = client_output.received_buffer_request_sender;
let authenticator = Authenticator::new(auth_methods, allowed_users);
let mut sphinx_socks = SphinxSocksServer::new(
config.get_listening_port(),
self.config.get_listening_port(),
authenticator,
config.get_provider_mix_address(),
self_address,
shared_lane_queue_lengths,
socks::client::Config::new(
config.get_send_anonymously(),
config.get_connection_start_surbs(),
config.get_per_request_surbs(),
),
shutdown.clone(),
);
task::spawn_with_report_error(
async move {
sphinx_socks
.serve(
input_sender,
received_buffer_request_sender,
connection_command_sender,
)
.await
},
self.config.get_provider_mix_address(),
self.as_mix_recipient(),
shutdown,
);
tokio::spawn(async move { sphinx_socks.serve(msg_input, buffer_requester).await });
}
/// blocking version of `start` method. Will run forever (or until SIGINT is sent)
pub async fn run_forever(self) -> Result<(), Box<dyn Error + Send>> {
let mut shutdown = self
.start()
.await
.map_err(|err| Box::new(err) as Box<dyn Error + Send>)?;
let res = wait_for_signal_and_error(&mut shutdown).await;
pub async fn run_forever(&mut self) {
let mut shutdown = self.start().await;
wait_for_signal().await;
log::info!("Sending shutdown");
client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed);
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-socks5-client");
res
}
// Variant of `run_forever` that listends for remote control messages
pub async fn run_and_listen(
self,
mut receiver: Socks5ControlMessageReceiver,
) -> Result<(), Box<dyn Error + Send>> {
// Start the main task
let mut shutdown = self
.start()
.await
.map_err(|err| Box::new(err) as Box<dyn Error + Send>)?;
let res = tokio::select! {
biased;
pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) {
let mut shutdown = self.start().await;
tokio::select! {
message = receiver.next() => {
log::debug!("Received message: {:?}", message);
match message {
@@ -167,56 +313,98 @@ impl NymClient {
log::info!("Channel closed, stopping");
}
}
Ok(())
}
Some(msg) = shutdown.wait_for_error() => {
log::info!("Task error: {:?}", msg);
Err(msg)
}
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
Ok(())
},
};
}
log::info!("Sending shutdown");
client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed);
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping nym-socks5-client");
res
}
pub async fn start(self) -> Result<ShutdownNotifier, Socks5ClientError> {
let base_builder = BaseClientBuilder::new_from_base_config(
self.config.get_base(),
self.key_manager,
Some(Self::create_bandwidth_controller(&self.config).await),
non_wasm_helpers::setup_fs_reply_surb_backend(
self.config.get_base().get_reply_surb_database_path(),
self.config.get_debug_settings(),
)
.await?,
pub async fn start(&mut self) -> ShutdownNotifier {
info!("Starting nym client");
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
// used for announcing connection or disconnection of a channel for pushing re-assembled messages to
let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded();
// channels responsible for controlling real messages
let (input_sender, input_receiver) = mpsc::unbounded::<InputMessage>();
// channels responsible for controlling ack messages
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor = TopologyAccessor::new();
let reply_key_storage =
ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path())
.expect("Failed to load reply key storage!");
// Shutdown notifier for signalling tasks to stop
let shutdown = ShutdownNotifier::default();
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe())
.await;
self.start_received_messages_buffer_controller(
received_buffer_request_receiver,
mixnet_messages_receiver,
reply_key_storage.clone(),
shutdown.subscribe(),
);
let self_address = base_builder.as_mix_recipient();
let mut started_client = base_builder.start_base().await?;
let client_input = started_client.client_input.register_producer();
let client_output = started_client.client_output.register_consumer();
let gateway_client = self
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
Self::start_socks5_listener(
&self.config,
client_input,
client_output,
self_address,
started_client.shutdown_notifier.subscribe(),
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
ack_receiver,
input_receiver,
sphinx_message_sender.clone(),
shutdown.subscribe(),
);
self.start_cover_traffic_stream(
shared_topology_accessor,
sphinx_message_sender,
shutdown.subscribe(),
);
self.start_socks5_listener(
received_buffer_request_sender,
input_sender,
shutdown.subscribe(),
);
info!("Client startup finished!");
info!("The address of this client is: {}", self_address);
info!("The address of this client is: {}", self.as_mix_recipient());
Ok(started_client.shutdown_notifier)
shutdown
}
}
+9 -31
View File
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::Args;
use client_core::client::replies::reply_storage::fs_backend;
use client_core::{config::GatewayEndpointConfig, error::ClientCoreError};
use client_core::{config::GatewayEndpoint, error::ClientCoreError};
use config::NymConfig;
use crate::{
@@ -21,14 +20,6 @@ pub(crate) struct Init {
#[clap(long)]
provider: String,
/// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider.
/// While this is going to hide its actual address information, it will make the actual communication
/// slower and consume nearly double the bandwidth as it will require sending reply SURBs.
///
/// Note that some service providers might not support this.
#[clap(long)]
use_anonymous_sender_tag: bool,
/// Id of the gateway we are going to connect to.
#[clap(long)]
gateway: Option<String>,
@@ -38,13 +29,9 @@ pub(crate) struct Init {
#[clap(long)]
force_register_gateway: bool,
/// Comma separated list of rest endpoints of the nymd validators
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
nymd_validators: Option<String>,
/// Comma separated list of rest endpoints of the API validators
#[clap(long)]
api_validators: Option<String>,
validators: Option<String>,
/// Port for the socket to listen on in all subsequent runs
#[clap(short, long)]
@@ -55,10 +42,6 @@ pub(crate) struct Init {
#[clap(long, hidden = true)]
fastmode: bool,
/// Disable loop cover traffic and the Poisson rate limiter (for debugging only)
#[clap(long, hidden = true)]
no_cover: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[cfg(feature = "coconut")]
@@ -69,12 +52,9 @@ pub(crate) struct Init {
impl From<Init> for OverrideConfig {
fn from(init_config: Init) -> Self {
OverrideConfig {
nymd_validators: init_config.nymd_validators,
api_validators: init_config.api_validators,
validators: init_config.validators,
port: init_config.port,
use_anonymous_sender_tag: init_config.use_anonymous_sender_tag,
fastmode: init_config.fastmode,
no_cover: init_config.no_cover,
#[cfg(feature = "coconut")]
enabled_credentials_mode: init_config.enabled_credentials_mode,
}
@@ -135,12 +115,10 @@ pub(crate) async fn execute(args: &Init) {
);
println!("Client configuration completed.");
client_core::init::show_address::<_, fs_backend::Backend>(config.get_base()).unwrap_or_else(
|err| {
eprintln!("Failed to show address\nError: {err}");
std::process::exit(1)
},
);
client_core::init::show_address(config.get_base()).unwrap_or_else(|err| {
eprintln!("Failed to show address\nError: {err}");
std::process::exit(1)
});
}
async fn setup_gateway(
@@ -148,7 +126,7 @@ async fn setup_gateway(
register: bool,
user_chosen_gateway_id: Option<&str>,
config: &Config,
) -> Result<GatewayEndpointConfig, ClientCoreError<fs_backend::Backend>> {
) -> Result<GatewayEndpoint, ClientCoreError> {
if register {
// Get the gateway details by querying the validator-api. Either pick one at random or use
// the chosen one if it's among the available ones.
+5 -30
View File
@@ -1,8 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::error::Error;
use crate::client::config::Config;
use clap::CommandFactory;
use clap::{Parser, Subcommand};
@@ -52,7 +50,7 @@ fn long_version_static() -> &'static str {
#[clap(author = "Nymtech", version, long_version = long_version_static(), about)]
pub(crate) struct Cli {
/// Path pointing to an env file that configures the client.
#[clap(short, long)]
#[clap(long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(subcommand)]
@@ -63,10 +61,8 @@ pub(crate) struct Cli {
pub(crate) enum Commands {
/// Initialise a Nym client. Do this first!
Init(init::Init),
/// Run the Nym client with provided configuration client optionally overriding set parameters
Run(run::Run),
/// Try to upgrade the client
Upgrade(upgrade::Upgrade),
@@ -79,41 +75,28 @@ pub(crate) enum Commands {
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
nymd_validators: Option<String>,
api_validators: Option<String>,
validators: Option<String>,
port: Option<u16>,
use_anonymous_sender_tag: bool,
fastmode: bool,
no_cover: bool,
#[cfg(feature = "coconut")]
enabled_credentials_mode: bool,
}
pub(crate) async fn execute(args: &Cli) -> Result<(), Box<dyn Error + Send>> {
pub(crate) async fn execute(args: &Cli) {
let bin_name = "nym-socks5-client";
match &args.command {
Commands::Init(m) => init::execute(m).await,
Commands::Run(m) => run::execute(m).await?,
Commands::Run(m) => run::execute(m).await,
Commands::Upgrade(m) => upgrade::execute(m),
Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name),
Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name),
}
Ok(())
}
pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config {
if let Some(raw_validators) = args.nymd_validators {
config
.get_base_mut()
.set_custom_validators(parse_validators(&raw_validators));
} else if let Ok(raw_validators) = std::env::var(network_defaults::var_names::NYMD_VALIDATOR) {
config
.get_base_mut()
.set_custom_validators(parse_validators(&raw_validators));
}
if let Some(raw_validators) = args.api_validators {
if let Some(raw_validators) = args.validators {
config
.get_base_mut()
.set_custom_validator_apis(parse_validators(&raw_validators));
@@ -123,10 +106,6 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
.set_custom_validator_apis(parse_validators(&raw_validators));
}
if args.use_anonymous_sender_tag {
config = config.with_anonymous_replies(true)
}
if let Some(port) = args.port {
config = config.with_port(port);
}
@@ -142,10 +121,6 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi
config.get_base_mut().set_high_default_traffic_volume();
}
if args.no_cover {
config.get_base_mut().set_no_cover_traffic();
}
config
}
+9 -35
View File
@@ -4,7 +4,6 @@
use crate::{
client::{config::Config, NymClient},
commands::{override_config, OverrideConfig},
error::Socks5ClientError,
};
use clap::Args;
@@ -22,14 +21,6 @@ pub(crate) struct Run {
#[clap(long)]
config: Option<String>,
/// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider.
/// While this is going to hide its actual address information, it will make the actual communication
/// slower and consume nearly double the bandwidth as it will require sending reply SURBs.
///
/// Note that some service providers might not support this.
#[clap(long)]
use_anonymous_sender_tag: bool,
/// Address of the socks5 provider to send messages to.
#[clap(long)]
provider: Option<String>,
@@ -39,27 +30,14 @@ pub(crate) struct Run {
#[clap(long)]
gateway: Option<String>,
/// Comma separated list of rest endpoints of the nymd validators
/// Comma separated list of rest endpoints of the validators
#[clap(long)]
nymd_validators: Option<String>,
/// Comma separated list of rest endpoints of the API validators
#[clap(long)]
api_validators: Option<String>,
validators: Option<String>,
/// Port for the socket to listen on
#[clap(short, long)]
port: Option<u16>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[clap(long, hidden = true)]
fastmode: bool,
/// Disable loop cover traffic and the Poisson rate limiter (for debugging only)
#[clap(long, hidden = true)]
no_cover: bool,
/// Set this client to work in a enabled credentials mode that would attempt to use gateway
/// with bandwidth credential requirement.
#[cfg(feature = "coconut")]
@@ -70,12 +48,10 @@ pub(crate) struct Run {
impl From<Run> for OverrideConfig {
fn from(run_config: Run) -> Self {
OverrideConfig {
nymd_validators: run_config.nymd_validators,
api_validators: run_config.api_validators,
validators: run_config.validators,
port: run_config.port,
use_anonymous_sender_tag: run_config.use_anonymous_sender_tag,
fastmode: run_config.fastmode,
no_cover: run_config.no_cover,
fastmode: false,
#[cfg(feature = "coconut")]
enabled_credentials_mode: run_config.enabled_credentials_mode,
}
@@ -104,16 +80,14 @@ fn version_check(cfg: &Config) -> bool {
}
}
pub(crate) async fn execute(args: &Run) -> Result<(), Box<dyn std::error::Error + Send>> {
pub(crate) async fn execute(args: &Run) {
let id = &args.id;
let mut config = match Config::load_from_file(Some(id)) {
Ok(cfg) => cfg,
Err(err) => {
error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err);
return Err(Box::new(Socks5ClientError::FailedToLoadConfig(
id.to_string(),
)));
return;
}
};
@@ -122,8 +96,8 @@ pub(crate) async fn execute(args: &Run) -> Result<(), Box<dyn std::error::Error
if !version_check(&config) {
error!("failed the local version check");
return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck));
return;
}
NymClient::new(config).run_forever().await
NymClient::new(config).run_forever().await;
}
-24
View File
@@ -1,24 +0,0 @@
use crate::socks::types::SocksProxyError;
use client_core::client::replies::reply_storage::fs_backend;
use client_core::error::ClientCoreError;
#[derive(thiserror::Error, Debug)]
pub enum Socks5ClientError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("client-core error: {0}")]
ClientCoreError(#[from] ClientCoreError<fs_backend::Backend>),
#[error("SOCKS proxy error")]
SocksProxyError(SocksProxyError),
#[error("Failed to load config for: {0}")]
FailedToLoadConfig(String),
#[error("Failed local version check, client and config mismatch")]
FailedLocalVersionCheck,
#[error("Fail to bind address")]
FailToBindAddress,
}
-1
View File
@@ -2,5 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client;
pub mod error;
pub mod socks;
+22 -6
View File
@@ -1,25 +1,21 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::error::Error;
use clap::{crate_version, Parser};
use logging::setup_logging;
use network_defaults::setup_env;
pub mod client;
mod commands;
pub mod error;
pub mod socks;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send>> {
async fn main() {
setup_logging();
println!("{}", banner());
let args = commands::Cli::parse();
setup_env(args.config_env_file.clone());
commands::execute(&args).await
commands::execute(&args).await;
}
fn banner() -> String {
@@ -38,3 +34,23 @@ fn banner() -> String {
crate_version!()
)
}
fn setup_logging() {
let mut log_builder = pretty_env_logger::formatted_timed_builder();
if let Ok(s) = ::std::env::var("RUST_LOG") {
log_builder.parse_filters(&s);
} else {
// default to 'Info'
log_builder.filter(None, log::LevelFilter::Info);
}
log_builder
.filter_module("hyper", log::LevelFilter::Warn)
.filter_module("tokio_reactor", log::LevelFilter::Warn)
.filter_module("reqwest", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("want", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.init();
}
+51 -196
View File
@@ -2,10 +2,10 @@
use super::authentication::{AuthenticationMethods, Authenticator, User};
use super::request::{SocksCommand, SocksRequest};
use super::types::{ResponseCodeV4, ResponseCodeV5, SocksProxyError};
use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION};
use client_connections::{LaneQueueLengths, TransmissionLane};
use client_core::client::inbound_messages::{InputMessage, InputMessageSender};
use super::types::{ResponseCode, SocksProxyError};
use super::{RESERVED, SOCKS_VERSION};
use client_core::client::inbound_messages::InputMessage;
use client_core::client::inbound_messages::InputMessageSender;
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use log::*;
@@ -126,44 +126,21 @@ impl AsyncWrite for StreamState {
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Config {
use_surbs_for_responses: bool,
connection_start_surbs: u32,
per_request_surbs: u32,
}
impl Config {
pub(crate) fn new(
use_surbs_for_responses: bool,
connection_start_surbs: u32,
per_request_surbs: u32,
) -> Self {
Self {
use_surbs_for_responses,
connection_start_surbs,
per_request_surbs,
}
}
}
/// A client connecting to the Socks proxy server, because
/// it wants to make a Nym-protected outbound request. Typically, this is
/// something like e.g. a wallet app running on your laptop connecting to
/// `SphinxSocksServer`.
/// SphinxSocksServer.
pub(crate) struct SocksClient {
config: Config,
controller_sender: ControllerSender,
stream: StreamState,
auth_nmethods: u8,
authenticator: Authenticator,
socks_version: Option<SocksVersion>,
socks_version: u8,
input_sender: InputMessageSender,
connection_id: ConnectionId,
service_provider: Recipient,
self_address: Recipient,
started_proxy: bool,
lane_queue_lengths: LaneQueueLengths,
shutdown_listener: ShutdownListener,
}
@@ -180,36 +157,28 @@ impl Drop for SocksClient {
}
impl SocksClient {
#[allow(clippy::too_many_arguments)]
/// Create a new SOCKClient
pub fn new(
config: Config,
stream: TcpStream,
authenticator: Authenticator,
input_sender: InputMessageSender,
service_provider: &Recipient,
service_provider: Recipient,
controller_sender: ControllerSender,
self_address: &Recipient,
lane_queue_lengths: LaneQueueLengths,
mut shutdown_listener: ShutdownListener,
self_address: Recipient,
shutdown_listener: ShutdownListener,
) -> Self {
// If this task fails and exits, we don't want to send shutdown signal
shutdown_listener.mark_as_success();
let connection_id = Self::generate_random();
SocksClient {
config,
controller_sender,
connection_id,
stream: StreamState::Available(stream),
auth_nmethods: 0,
socks_version: None,
socks_version: 0,
authenticator,
input_sender,
service_provider: *service_provider,
self_address: *self_address,
service_provider,
self_address,
started_proxy: false,
lane_queue_lengths,
shutdown_listener,
}
}
@@ -219,49 +188,16 @@ impl SocksClient {
rng.next_u64()
}
pub async fn send_error(&mut self, err: SocksProxyError) -> Result<(), SocksProxyError> {
let error_text = format!("{}", err);
let Some(ref version) = self.socks_version else {
log::error!("Trying to send error without knowing the version");
return Ok(());
};
match version {
SocksVersion::V4 => {
let response = ResponseCodeV4::RequestRejected;
self.send_error_v4(response).await
}
SocksVersion::V5 => {
let response = if error_text.contains("Host") {
ResponseCodeV5::HostUnreachable
} else if error_text.contains("Network") {
ResponseCodeV5::NetworkUnreachable
} else if error_text.contains("ttl") {
ResponseCodeV5::TtlExpired
} else {
ResponseCodeV5::Failure
};
self.send_error_v5(response).await
}
}
}
// Send an error back to the client
pub async fn send_error_v4(&mut self, r: ResponseCodeV4) -> Result<(), SocksProxyError> {
self.stream.write_all(&[SOCKS4_VERSION, r as u8]).await?;
pub async fn error(&mut self, r: ResponseCode) -> Result<(), SocksProxyError> {
self.stream.write_all(&[5, r as u8]).await?;
Ok(())
}
pub async fn send_error_v5(&mut self, r: ResponseCodeV5) -> Result<(), SocksProxyError> {
self.stream.write_all(&[SOCKS5_VERSION, r as u8]).await?;
Ok(())
}
/// Shutdown the `TcpStream` to the client and end the session
/// Shutdown the TcpStream to the client and end the session
pub async fn shutdown(&mut self) -> Result<(), SocksProxyError> {
info!("client is shutting down its TCP stream");
self.stream.shutdown().await?;
self.shutdown_listener.mark_as_success();
Ok(())
}
@@ -269,67 +205,33 @@ impl SocksClient {
/// is in use and that the client is authenticated, then runs the request.
pub async fn run(&mut self) -> Result<(), SocksProxyError> {
debug!("New connection from: {}", self.stream.peer_addr()?.ip());
let mut header = [0u8; 2];
// Read a byte from the stream and determine the version being requested
let mut header = [0u8];
self.stream.read_exact(&mut header).await?;
self.socks_version = match SocksVersion::try_from(header[0]) {
Ok(version) => Some(version),
Err(_err) => {
warn!("Init: Unsupported version: SOCKS{}", header[0]);
return self.shutdown().await;
}
};
self.socks_version = header[0];
self.auth_nmethods = header[1];
if self.socks_version == Some(SocksVersion::V5) {
let mut auth = [0u8];
self.stream.read_exact(&mut auth).await?;
self.auth_nmethods = auth[0];
self.authenticate_socks5().await?;
// Handle SOCKS4 requests
if header[0] != SOCKS_VERSION {
warn!("Init: Unsupported version: SOCKS{}", self.socks_version);
self.shutdown().await
}
// Valid SOCKS5
else {
// Authenticate w/ client
self.authenticate().await?;
// Handle requests
self.handle_request().await
}
self.handle_request().await
}
async fn send_anonymous_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
let req = Request::new_connect(self.connection_id, remote_address, None);
let msg = Message::Request(req);
let input_message = InputMessage::new_anonymous(
self.service_provider,
msg.into_bytes(),
self.config.connection_start_surbs,
TransmissionLane::ConnectionId(self.connection_id),
);
self.input_sender
.send(input_message)
.await
.expect("InputMessageReceiver has stopped receiving!");
}
async fn send_connect_to_mixnet_with_return_address(&mut self, remote_address: RemoteAddress) {
let req = Request::new_connect(self.connection_id, remote_address, Some(self.self_address));
let msg = Message::Request(req);
let input_message = InputMessage::new_regular(
self.service_provider,
msg.into_bytes(),
TransmissionLane::ConnectionId(self.connection_id),
);
self.input_sender
.send(input_message)
.await
.expect("InputMessageReceiver has stopped receiving!");
}
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
if self.config.use_surbs_for_responses {
self.send_anonymous_connect_to_mixnet(remote_address).await
} else {
self.send_connect_to_mixnet_with_return_address(remote_address)
.await
}
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
let msg = Message::Request(req);
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
self.input_sender.unbounded_send(input_message).unwrap();
}
async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) {
@@ -337,19 +239,12 @@ impl SocksClient {
.await;
let stream = self.stream.run_proxy();
let peer_addr = match stream.peer_addr() {
Ok(peer_addr) => peer_addr,
Err(err) => {
log::error!("Unable to extract the remote peer address: {err}");
return;
}
};
let local_stream_remote = peer_addr.to_string();
let local_stream_remote = stream
.peer_addr()
.expect("failed to extract peer address")
.to_string();
let connection_id = self.connection_id;
let input_sender = self.input_sender.clone();
let anonymous = self.config.use_surbs_for_responses;
let per_request_surbs = self.config.per_request_surbs;
let recipient = self.service_provider;
let (stream, _) = ProxyRunner::new(
@@ -359,23 +254,12 @@ impl SocksClient {
conn_receiver,
input_sender,
connection_id,
Some(self.lane_queue_lengths.clone()),
self.shutdown_listener.clone(),
)
.run(move |conn_id, read_data, socket_closed| {
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
let provider_message = Message::Request(provider_request);
let lane = TransmissionLane::ConnectionId(conn_id);
if anonymous {
InputMessage::new_anonymous(
recipient,
provider_message.into_bytes(),
per_request_surbs,
lane,
)
} else {
InputMessage::new_regular(recipient, provider_message.into_bytes(), lane)
}
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
})
.await
.into_inner();
@@ -387,17 +271,8 @@ impl SocksClient {
async fn handle_request(&mut self) -> Result<(), SocksProxyError> {
debug!("Handling CONNECT Command");
let version = self
.socks_version
.as_ref()
.expect("Must read version before parsing request");
let request = match version {
SocksVersion::V4 => SocksRequest::from_stream_socks4(&mut self.stream).await?,
SocksVersion::V5 => SocksRequest::from_stream_socks5(&mut self.stream).await?,
};
let remote_address = request.address_string();
let request = SocksRequest::from_stream(&mut self.stream).await?;
let remote_address = request.to_string();
// setup for receiving from the mixnet
let (mix_sender, mix_receiver) = mpsc::unbounded();
@@ -406,10 +281,7 @@ impl SocksClient {
// Use the Proxy to connect to the specified addr/port
SocksCommand::Connect => {
trace!("Connecting to: {:?}", remote_address.clone());
match version {
SocksVersion::V4 => self.acknowledge_socks4().await,
SocksVersion::V5 => self.acknowledge_socks5().await,
}
self.acknowledge_socks5().await;
self.started_proxy = true;
self.controller_sender
@@ -440,8 +312,8 @@ impl SocksClient {
async fn acknowledge_socks5(&mut self) {
self.stream
.write_all(&[
SOCKS5_VERSION,
ResponseCodeV5::Success as u8,
SOCKS_VERSION,
ResponseCode::Success as u8,
RESERVED,
1,
127,
@@ -455,30 +327,13 @@ impl SocksClient {
.unwrap();
}
/// Writes a Socks4 header back to the requesting client's TCP stream,
async fn acknowledge_socks4(&mut self) {
self.stream
.write_all(&[
0, //SOCKS4_VERSION,
ResponseCodeV4::Granted as u8,
0,
0,
127,
0,
0,
1,
])
.await
.unwrap();
}
/// Authenticate the incoming request. Each request is checked for its
/// authentication method. A user/password request will extract the
/// username and password from the stream, then check with the Authenticator
/// to see if the resulting user is allowed.
///
/// A lot of this could probably be put into the `SocksRequest::from_stream()`
/// constructor, and/or cleaned up with `tokio::codec`. It's mostly just
/// constructor, and/or cleaned up with tokio::codec. It's mostly just
/// read-a-byte-or-two. The bytes being extracted look like this:
///
/// +----+------+----------+------+------------+
@@ -490,7 +345,7 @@ impl SocksClient {
/// Pulling out the stream code into its own home, and moving the if/else logic
/// into the Authenticator (where it'll be more easily testable)
/// would be a good next step.
async fn authenticate_socks5(&mut self) -> Result<(), SocksProxyError> {
async fn authenticate(&mut self) -> Result<(), SocksProxyError> {
debug!("Authenticating w/ {}", self.stream.peer_addr()?.ip());
// Get valid auth methods
let methods = self.get_available_methods().await?;
@@ -499,7 +354,7 @@ impl SocksClient {
let mut response = [0u8; 2];
// Set the version in the response
response[0] = SOCKS5_VERSION;
response[0] = SOCKS_VERSION;
if methods.contains(&(AuthenticationMethods::UserPass as u8)) {
// Set the default auth method (NO AUTH)
response[1] = AuthenticationMethods::UserPass as u8;
@@ -535,11 +390,11 @@ impl SocksClient {
// Authenticate passwords
if self.authenticator.is_allowed(&user) {
debug!("Access Granted. User: {}", user.username);
let response = [1, ResponseCodeV5::Success as u8];
let response = [1, ResponseCode::Success as u8];
self.stream.write_all(&response).await?;
} else {
debug!("Access Denied. User: {}", user.username);
let response = [1, ResponseCodeV5::Failure as u8];
let response = [1, ResponseCode::Failure as u8];
self.stream.write_all(&response).await?;
// Shutdown
@@ -558,7 +413,7 @@ impl SocksClient {
response[1] = AuthenticationMethods::NoMethods as u8;
self.stream.write_all(&response).await?;
self.shutdown().await?;
Err(ResponseCodeV5::Failure.into())
Err(ResponseCode::Failure.into())
}
}
+5 -17
View File
@@ -1,5 +1,3 @@
use std::time::Duration;
use futures::channel::mpsc;
use futures::StreamExt;
use log::*;
@@ -20,16 +18,9 @@ pub(crate) struct MixnetResponseListener {
impl Drop for MixnetResponseListener {
fn drop(&mut self) {
if let Err(err) = self
.buffer_requester
self.buffer_requester
.unbounded_send(ReceivedBufferMessage::ReceiverDisconnect)
{
if self.shutdown.is_shutdown_poll() {
log::debug!("The buffer request failed: {}", err);
} else {
log::error!("The buffer request failed: {}", err);
}
}
.expect("the buffer request failed!")
}
}
@@ -54,8 +45,8 @@ impl MixnetResponseListener {
async fn on_message(&self, reconstructed_message: ReconstructedMessage) {
let raw_message = reconstructed_message.message;
if reconstructed_message.sender_tag.is_some() {
warn!("this message was sent anonymously - it couldn't have come from the service provider");
if reconstructed_message.reply_surb.is_some() {
warn!("this message had a surb - we didn't do anything with it");
}
let response = match Message::try_from_bytes(&raw_message) {
@@ -105,10 +96,7 @@ impl MixnetResponseListener {
}
}
}
#[cfg(not(target_arch = "wasm32"))]
tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv())
.await
.expect("Task stopped without shutdown called");
assert!(self.shutdown.is_shutdown_poll());
log::debug!("MixnetResponseListener: Exiting");
}
}
+2 -27
View File
@@ -1,11 +1,7 @@
#![forbid(unsafe_code)]
use std::convert::TryFrom;
use self::types::SocksProxyError;
pub mod authentication;
pub(crate) mod client;
mod client;
pub(crate) mod mixnet_responses;
mod request;
pub mod server;
@@ -13,27 +9,6 @@ pub mod types;
pub mod utils;
/// Version of socks
const SOCKS4_VERSION: u8 = 0x04;
const SOCKS5_VERSION: u8 = 0x05;
const SOCKS_VERSION: u8 = 0x05;
const RESERVED: u8 = 0x00;
#[derive(Clone, PartialEq, Eq)]
pub enum SocksVersion {
V4 = 0x04,
V5 = 0x05,
}
pub struct InvalidSocksVersion;
impl TryFrom<u8> for SocksVersion {
type Error = SocksProxyError;
fn try_from(version: u8) -> Result<Self, Self::Error> {
match version {
SOCKS4_VERSION => Ok(Self::V4),
SOCKS5_VERSION => Ok(Self::V5),
_ => Err(SocksProxyError::UnsupportedProxyVersion(version)),
}
}
}
+49 -109
View File
@@ -1,7 +1,5 @@
use crate::socks::SOCKS4_VERSION;
use super::types::{AddrType, ResponseCodeV5, SocksProxyError};
use super::{utils as socks_utils, SOCKS5_VERSION};
use super::types::{AddrType, ResponseCode, SocksProxyError};
use super::{utils as socks_utils, SOCKS_VERSION};
use log::*;
use std::fmt::{self, Display};
use tokio::io::{AsyncRead, AsyncReadExt};
@@ -17,114 +15,80 @@ pub(crate) struct SocksRequest {
}
impl SocksRequest {
/// Parse a SOCKS4 request from a `TcpStream`
/// From documents at:
/// - SOCKS4: https://www.openssh.com/txt/socks4.protocol
/// - SOCKS4a: https://www.openssh.com/txt/socks4a.protocol
pub async fn from_stream_socks4<R>(stream: &mut R) -> Result<Self, SocksProxyError>
/// Parse a SOCKS5 request from a TcpStream
pub async fn from_stream<R>(stream: &mut R) -> Result<Self, SocksProxyError>
where
R: AsyncRead + Unpin,
{
log::trace!("read from stream socks4");
let mut packet = [0u8; 3];
stream.read_exact(&mut packet).await?;
// CD (command)
let Some(command) = SocksCommand::from(packet[0] as usize) else {
log::warn!("Invalid Command");
return Err(ResponseCodeV5::CommandNotSupported.into());
};
// DSTPORT
let mut port = [0u8; 2];
port.copy_from_slice(&packet[1..]);
let port = merge_u8_into_u16(port[0], port[1]);
// DSTIP
let mut ip = [0u8; 4];
stream.read_exact(&mut ip).await?;
// USERID
let _userid = read_until_zero(stream).await;
// SOCKS4a extension
// https://www.openssh.com/txt/socks4a.protocol
// If the IP is 0.0.0.x with x nonzero, read the domain name
let (addr, addr_type) = if ip[..3] == [0, 0, 0] && ip[3] != 0 {
(read_until_zero(stream).await?, AddrType::Domain)
} else {
(ip.to_vec(), AddrType::V4)
};
// Return parsed request
Ok(SocksRequest {
version: SOCKS4_VERSION,
command,
addr_type,
addr,
port,
})
}
/// Parse a SOCKS5 request from a `TcpStream`
/// From: https://www.rfc-editor.org/rfc/rfc1928
pub async fn from_stream_socks5<R>(stream: &mut R) -> Result<Self, SocksProxyError>
where
R: AsyncRead + Unpin,
{
log::info!("read from stream socks5");
let mut packet = [0u8; 4];
// Read a byte from the stream and determine the version being requested
stream.read_exact(&mut packet).await?;
// VER
if packet[0] != SOCKS5_VERSION {
warn!("Unsupported version: SOCKS{}", packet[0]);
if packet[0] != SOCKS_VERSION {
warn!("from_stream Unsupported version: SOCKS{}", packet[0]);
return Err(SocksProxyError::UnsupportedProxyVersion(packet[0]));
}
// CMD
let Some(command) = SocksCommand::from(packet[1] as usize) else {
warn!("Invalid Command");
return Err(ResponseCodeV5::CommandNotSupported.into());
};
// Get command
let mut command: SocksCommand = SocksCommand::Connect;
match SocksCommand::from(packet[1] as usize) {
Some(com) => {
command = com;
Ok(())
}
None => {
warn!("Invalid Command");
Err(ResponseCode::CommandNotSupported)
}
}?;
// RSV
// packet[2] is reserved
// DST.address
// ATYP
let Some(addr_type) = AddrType::from(packet[3] as usize) else {
error!("No Addr");
return Err(ResponseCodeV5::AddrTypeNotSupported.into())
};
let mut addr_type: AddrType = AddrType::V6;
match AddrType::from(packet[3] as usize) {
Some(addr) => {
addr_type = addr;
Ok(())
}
None => {
error!("No Addr");
Err(ResponseCode::AddrTypeNotSupported)
}
}?;
// DST.ADDR
let addr = match addr_type {
trace!("Getting Addr");
// Get Addr from addr_type and stream
let addr: Result<Vec<u8>, SocksProxyError> = match addr_type {
AddrType::Domain => {
let mut domain_length = [0u8];
let mut domain_length = [0u8; 1];
stream.read_exact(&mut domain_length).await?;
let mut domain = vec![0u8; domain_length[0] as usize];
stream.read_exact(&mut domain).await?;
domain
Ok(domain)
}
AddrType::V4 => {
let mut addr = [0u8; 4];
stream.read_exact(&mut addr).await?;
addr.to_vec()
Ok(addr.to_vec())
}
AddrType::V6 => {
let mut addr = [0u8; 16];
stream.read_exact(&mut addr).await?;
addr.to_vec()
Ok(addr.to_vec())
}
};
// DST.PORT
let addr = addr?;
// read DST.port
let mut port = [0u8; 2];
stream.read_exact(&mut port).await?;
let port = merge_u8_into_u16(port[0], port[1]);
// Merge two u8s into u16
let port = (u16::from(port[0]) << 8) | u16::from(port[1]);
// Return parsed request
Ok(SocksRequest {
version: packet[0],
command,
@@ -133,18 +97,14 @@ impl SocksRequest {
port,
})
}
/// Print out the address and port to a String.
/// This might return domain:port, ipv6:port, or ipv4:port.
pub fn address_string(&self) -> String {
let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr);
format!("{}:{}", address, self.port)
}
}
impl Display for SocksRequest {
/// Print out the address and port to a String.
/// This might return domain:port, ipv6:port, or ipv4:port.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address_string())
let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr);
write!(f, "{}:{}", address, self.port)
}
}
@@ -167,23 +127,3 @@ impl SocksCommand {
}
}
}
fn merge_u8_into_u16(a: u8, b: u8) -> u16 {
(u16::from(a) << 8) | u16::from(b)
}
async fn read_until_zero<R>(stream: &mut R) -> Result<Vec<u8>, SocksProxyError>
where
R: AsyncRead + Unpin,
{
let mut result = Vec::new();
let mut char = [0u8];
loop {
stream.read_exact(&mut char).await?;
if char[0] == 0 {
break;
}
result.push(char[0]);
}
Ok(result)
}
+39 -34
View File
@@ -1,18 +1,16 @@
use crate::error::Socks5ClientError;
use super::authentication::Authenticator;
use super::client::SocksClient;
use super::{
authentication::Authenticator, client::SocksClient, mixnet_responses::MixnetResponseListener,
mixnet_responses::MixnetResponseListener,
types::{ResponseCode, SocksProxyError},
};
use crate::socks::client;
use client_connections::{ConnectionCommandSender, LaneQueueLengths};
use client_core::client::{
inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender,
};
use log::*;
use nymsphinx::addressing::clients::Recipient;
use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller};
use proxy_helpers::connection_controller::Controller;
use std::net::SocketAddr;
use tap::TapFallible;
use task::ShutdownListener;
use tokio::net::TcpListener;
@@ -22,8 +20,6 @@ pub struct SphinxSocksServer {
listening_address: SocketAddr,
service_provider: Recipient,
self_address: Recipient,
client_config: client::Config,
lane_queue_lengths: LaneQueueLengths,
shutdown: ShutdownListener,
}
@@ -34,8 +30,6 @@ impl SphinxSocksServer {
authenticator: Authenticator,
service_provider: Recipient,
self_address: Recipient,
lane_queue_lengths: LaneQueueLengths,
client_config: client::Config,
shutdown: ShutdownListener,
) -> Self {
// hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can
@@ -47,8 +41,6 @@ impl SphinxSocksServer {
listening_address: format!("{}:{}", ip, port).parse().unwrap(),
service_provider,
self_address,
client_config,
lane_queue_lengths,
shutdown,
}
}
@@ -59,19 +51,13 @@ impl SphinxSocksServer {
&mut self,
input_sender: InputMessageSender,
buffer_requester: ReceivedBufferRequestSender,
client_connection_tx: ConnectionCommandSender,
) -> Result<(), Socks5ClientError> {
let listener = TcpListener::bind(self.listening_address)
.await
.tap_err(|err| log::error!("Failed to bind to address: {err}"))?;
) -> Result<(), SocksProxyError> {
let listener = TcpListener::bind(self.listening_address).await.unwrap();
info!("Serving Connections...");
// controller for managing all active connections
let (mut active_streams_controller, controller_sender) = Controller::new(
client_connection_tx,
BroadcastActiveConnections::Off,
self.shutdown.clone(),
);
let (mut active_streams_controller, controller_sender) =
Controller::new(self.shutdown.clone());
tokio::spawn(async move {
active_streams_controller.run().await;
});
@@ -89,27 +75,46 @@ impl SphinxSocksServer {
loop {
tokio::select! {
Ok((stream, _remote)) = listener.accept() => {
// TODO Optimize this
let mut client = SocksClient::new(
self.client_config,
stream,
self.authenticator.clone(),
input_sender.clone(),
&self.service_provider,
self.service_provider,
controller_sender.clone(),
&self.self_address,
self.lane_queue_lengths.clone(),
self.self_address,
self.shutdown.clone(),
);
tokio::spawn(async move {
if let Err(err) = client.run().await {
error!("Error! {}", err);
if client.send_error(err).await.is_err() {
warn!("Failed to send error code");
};
if client.shutdown().await.is_err() {
warn!("Failed to shutdown TcpStream");
{
match client.run().await {
Ok(_) => {}
Err(error) => {
error!("Error! {}", error);
let error_text = format!("{}", error);
let response: ResponseCode;
if error_text.contains("Host") {
response = ResponseCode::HostUnreachable;
} else if error_text.contains("Network") {
response = ResponseCode::NetworkUnreachable;
} else if error_text.contains("ttl") {
response = ResponseCode::TtlExpired
} else {
response = ResponseCode::Failure
}
if client.error(response).await.is_err() {
warn!("Failed to send error code");
};
if client.shutdown().await.is_err() {
warn!("Failed to shutdown TcpStream");
};
}
};
// client gets dropped here
}
});
},
+3 -13
View File
@@ -1,17 +1,7 @@
use snafu::Snafu;
/// SOCKS4 Response codes
#[allow(dead_code)]
pub(crate) enum ResponseCodeV4 {
Granted = 0x5a,
RequestRejected = 0x5b,
CannotConnectToIdent = 0x5c,
DifferentUserId = 0x5d,
}
/// Possible SOCKS5 Response Codes
#[derive(Debug, Snafu)]
pub(crate) enum ResponseCodeV5 {
/// Possible SOCKS5 Response Codes
pub(crate) enum ResponseCode {
Success = 0x00,
#[snafu(display("SOCKS5 Server Failure"))]
Failure = 0x01,
@@ -58,7 +48,7 @@ where
}
/// DST.addr variant types
#[derive(Debug, PartialEq)]
#[derive(PartialEq)]
pub(crate) enum AddrType {
V4 = 0x01,
Domain = 0x03,
+7 -13
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.1.0"
version = "1.0.0"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
@@ -19,18 +19,14 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"]
[dependencies]
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "=0.2.78", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.4"
tokio = { version = "1.21.2", features = ["sync"] }
url = "2.2"
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4"
# internal
client-core = { path = "../client-core", default-features = false, features = ["wasm"] }
client-connections = { path = "../../common/client-connections" }
coconut-interface = { path = "../../common/coconut-interface", optional = true }
credentials = { path = "../../common/credentials", optional = true }
crypto = { path = "../../common/crypto" }
@@ -39,7 +35,7 @@ topology = { path = "../../common/topology" }
gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] }
validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
wasm-utils = { path = "../../common/wasm-utils" }
task = { path = "../../common/task" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
@@ -57,8 +53,6 @@ wee_alloc = { version = "0.4", optional = true }
wasm-bindgen-test = "0.3"
[package.metadata.wasm-pack.profile.release]
wasm-opt = true
wasm-opt = false
[profile.release]
lto = true
opt-level = 'z'
-5
View File
@@ -1,5 +0,0 @@
clippy:
cargo clippy --all-features --target wasm32-unknown-unknown -- -D warnings
test:
wasm-pack test --node
+1 -11
View File
@@ -4,16 +4,6 @@ This example application demonstrates how to use WebAssembly to create Sphinx pa
## 🚴 Usage
Build the WASM package for bundling:
```
wasm-pack build --scope nymproject --target no-modules
```
in the `clients/webassembly` directory (one up).
Start the webpack dev server:
```
npm install # set up dependencies
npm run start # starts a web server at http://localhost:8001
@@ -25,4 +15,4 @@ Check your dev console for output.
Install `wasm-pack`. Instruction are at the [Rust WASM tutorial](https://rustwasm.github.io/docs/book/game-of-life/hello-world.html).
`wasm-pack build --scope nymproject --target no-modules` in the `clients/webassembly` directory (one up) will rebuild the wasm package if you make changes to the Rust source. That will be automatically picked up (and reloaded, if need be) by the npm dev server.
`wasm-pack build` in the `clients/webassembly` directory (one up) will rebuild the wasm package if you make changes to the Rust source. That will be automatically picked up (and reloaded, if need be) by the npm dev server.
+2 -2
View File
@@ -1,5 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import('./index.js')
.catch(e => console.error('Error importing `index.js`:', e));
import("./index.js")
.catch(e => console.error("Error importing `index.js`:", e));

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