mirror of https://github.com/telemt/telemt.git
Compare commits
No commits in common. "655a08fa5cc3676c8ba62ad0c555301d99193b17" and "2f9fddfa6fd51580ce0571afe76c3dc71956df4f" have entirely different histories.
655a08fa5c
...
2f9fddfa6f
|
|
@ -1,39 +0,0 @@
|
||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "*" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "*" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install latest stable Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache cargo registry & build artifacts
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Build Release
|
|
||||||
run: cargo build --release --verbose
|
|
||||||
|
|
@ -26,9 +26,6 @@ jobs:
|
||||||
name: GNU ${{ matrix.target }}
|
name: GNU ${{ matrix.target }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
container:
|
|
||||||
image: rust:slim-bookworm
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|
@ -50,8 +47,8 @@ jobs:
|
||||||
|
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
sudo apt-get update
|
||||||
apt-get install -y \
|
sudo apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
clang \
|
clang \
|
||||||
lld \
|
lld \
|
||||||
|
|
@ -72,10 +69,14 @@ jobs:
|
||||||
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
||||||
export CC=aarch64-linux-gnu-gcc
|
export CC=aarch64-linux-gnu-gcc
|
||||||
export CXX=aarch64-linux-gnu-g++
|
export CXX=aarch64-linux-gnu-g++
|
||||||
|
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
||||||
|
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||||
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
||||||
else
|
else
|
||||||
export CC=clang
|
export CC=clang
|
||||||
export CXX=clang++
|
export CXX=clang++
|
||||||
|
export CC_x86_64_unknown_linux_gnu=clang
|
||||||
|
export CXX_x86_64_unknown_linux_gnu=clang++
|
||||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld"
|
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -84,19 +85,20 @@ jobs:
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} dist/telemt
|
BIN=target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||||
|
|
||||||
|
cp "$BIN" dist/${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
tar -czf ${{ matrix.asset }}.tar.gz \
|
tar -czf ${{ matrix.asset }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
--owner=0 --group=0 --numeric-owner \
|
|
||||||
telemt
|
|
||||||
|
|
||||||
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset }}
|
name: ${{ matrix.asset }}
|
||||||
path: dist/*
|
path: |
|
||||||
|
dist/${{ matrix.asset }}.tar.gz
|
||||||
|
dist/${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# MUSL
|
# MUSL
|
||||||
|
|
@ -123,7 +125,43 @@ jobs:
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y musl-tools pkg-config curl
|
apt-get install -y \
|
||||||
|
musl-tools \
|
||||||
|
pkg-config \
|
||||||
|
curl
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
with:
|
||||||
|
path: ~/.musl-aarch64
|
||||||
|
key: musl-toolchain-aarch64-v1
|
||||||
|
|
||||||
|
- name: Install aarch64 musl toolchain
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TOOLCHAIN_DIR="$HOME/.musl-aarch64"
|
||||||
|
ARCHIVE="aarch64-linux-musl-cross.tgz"
|
||||||
|
URL="https://github.com/telemt/telemt/releases/download/toolchains/$ARCHIVE"
|
||||||
|
|
||||||
|
if [ -x "$TOOLCHAIN_DIR/bin/aarch64-linux-musl-gcc" ]; then
|
||||||
|
echo "✅ MUSL toolchain already installed"
|
||||||
|
else
|
||||||
|
echo "⬇️ Downloading musl toolchain from Telemt GitHub Releases..."
|
||||||
|
|
||||||
|
curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o "$ARCHIVE" "$URL"
|
||||||
|
|
||||||
|
mkdir -p "$TOOLCHAIN_DIR"
|
||||||
|
tar -xzf "$ARCHIVE" --strip-components=1 -C "$TOOLCHAIN_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$TOOLCHAIN_DIR/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: Add rust target
|
- name: Add rust target
|
||||||
run: rustup target add ${{ matrix.target }}
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
@ -140,9 +178,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
|
||||||
export CC=aarch64-linux-musl-gcc
|
export CC=aarch64-linux-musl-gcc
|
||||||
|
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
|
||||||
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc"
|
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc"
|
||||||
else
|
else
|
||||||
export CC=musl-gcc
|
export CC=musl-gcc
|
||||||
|
export CC_x86_64_unknown_linux_musl=musl-gcc
|
||||||
export RUSTFLAGS="-C target-feature=+crt-static"
|
export RUSTFLAGS="-C target-feature=+crt-static"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -151,19 +191,69 @@ jobs:
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} dist/telemt
|
BIN=target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||||
|
|
||||||
|
cp "$BIN" dist/${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
|
||||||
cd dist
|
cd dist
|
||||||
tar -czf ${{ matrix.asset }}.tar.gz \
|
tar -czf ${{ matrix.asset }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
--owner=0 --group=0 --numeric-owner \
|
|
||||||
telemt
|
|
||||||
|
|
||||||
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset }}
|
name: ${{ matrix.asset }}
|
||||||
path: dist/*
|
path: |
|
||||||
|
dist/${{ matrix.asset }}.tar.gz
|
||||||
|
dist/${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Docker
|
||||||
|
# ==========================
|
||||||
|
docker:
|
||||||
|
name: Docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-gnu, build-musl]
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Extract binaries
|
||||||
|
run: |
|
||||||
|
mkdir dist
|
||||||
|
find artifacts -name "*.tar.gz" -exec tar -xzf {} -C dist \;
|
||||||
|
|
||||||
|
cp dist/telemt-x86_64-unknown-linux-musl dist/telemt || true
|
||||||
|
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
id: vars
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build & Push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
|
||||||
|
ghcr.io/${{ github.repository }}:latest
|
||||||
|
build-args: |
|
||||||
|
BINARY=dist/telemt
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Release
|
# Release
|
||||||
|
|
@ -181,7 +271,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
- name: Flatten
|
- name: Flatten artifacts
|
||||||
run: |
|
run: |
|
||||||
mkdir dist
|
mkdir dist
|
||||||
find artifacts -type f -exec cp {} dist/ \;
|
find artifacts -type f -exec cp {} dist/ \;
|
||||||
|
|
@ -191,61 +281,5 @@ jobs:
|
||||||
with:
|
with:
|
||||||
files: dist/*
|
files: dist/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
prerelease: ${{ contains(github.ref, '-') }}
|
draft: false
|
||||||
|
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
|
||||||
# ==========================
|
|
||||||
# Docker (FROM RELEASE)
|
|
||||||
# ==========================
|
|
||||||
docker:
|
|
||||||
name: Docker (from release)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install gh
|
|
||||||
run: apt-get update && apt-get install -y gh
|
|
||||||
|
|
||||||
- name: Extract version
|
|
||||||
id: vars
|
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Download binary
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
mkdir dist
|
|
||||||
|
|
||||||
gh release download ${{ steps.vars.outputs.VERSION }} \
|
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
--pattern "telemt-x86_64-linux-musl.tar.gz" \
|
|
||||||
--dir dist
|
|
||||||
|
|
||||||
tar -xzf dist/telemt-x86_64-linux-musl.tar.gz -C dist
|
|
||||||
chmod +x dist/telemt
|
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build & Push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: |
|
|
||||||
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
|
|
||||||
ghcr.io/${{ github.repository }}:latest
|
|
||||||
build-args: |
|
|
||||||
BINARY=dist/telemt
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Compile, Test, Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
checks: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install latest stable Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Cache cargo registry & build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Compile (no tests)
|
||||||
|
run: cargo check --workspace --all-features --lib --bins --verbose
|
||||||
|
|
||||||
|
- name: Run tests (single pass)
|
||||||
|
run: cargo test --workspace --all-features --verbose
|
||||||
|
|
||||||
|
# clippy dont fail on warnings because of active development of telemt
|
||||||
|
# and many warnings
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy -- --cap-lints warn
|
||||||
|
|
||||||
|
- name: Check for unused dependencies
|
||||||
|
run: cargo udeps || true
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Stress Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 2 * * *'
|
||||||
|
pull_request:
|
||||||
|
branches: ["*"]
|
||||||
|
paths:
|
||||||
|
- src/proxy/**
|
||||||
|
- src/transport/**
|
||||||
|
- src/stream/**
|
||||||
|
- src/protocol/**
|
||||||
|
- src/tls_front/**
|
||||||
|
- Cargo.toml
|
||||||
|
- Cargo.lock
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quota-lock-stress:
|
||||||
|
name: Quota-lock stress loop
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install latest stable Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo registry and build artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-stress-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-cargo-stress-
|
||||||
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
|
- name: Run quota-lock stress suites
|
||||||
|
env:
|
||||||
|
RUST_TEST_THREADS: 16
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
echo "[quota-lock-stress] iteration ${i}/12"
|
||||||
|
cargo test quota_lock_ --bin telemt -- --nocapture --test-threads 16
|
||||||
|
cargo test relay_quota_wake --bin telemt -- --nocapture --test-threads 16
|
||||||
|
done
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
name: Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "*" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "*" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: test-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ==========================
|
|
||||||
# Formatting
|
|
||||||
# ==========================
|
|
||||||
fmt:
|
|
||||||
name: Fmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt
|
|
||||||
|
|
||||||
- run: cargo fmt -- --check
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Tests
|
|
||||||
# ==========================
|
|
||||||
test:
|
|
||||||
name: Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write
|
|
||||||
checks: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- run: cargo test --verbose
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Clippy
|
|
||||||
# ==========================
|
|
||||||
clippy:
|
|
||||||
name: Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
checks: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: clippy
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- run: cargo clippy -- --cap-lints warn
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Udeps
|
|
||||||
# ==========================
|
|
||||||
udeps:
|
|
||||||
name: Udeps
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Install cargo-udeps
|
|
||||||
run: cargo install cargo-udeps || true
|
|
||||||
|
|
||||||
# тоже не валит билд
|
|
||||||
- run: cargo udeps || true
|
|
||||||
58
Dockerfile
58
Dockerfile
|
|
@ -1,9 +1,29 @@
|
||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
ARG BINARY
|
# ==========================
|
||||||
|
# Stage 1: Build
|
||||||
|
# ==========================
|
||||||
|
FROM rust:1.88-slim-bookworm AS builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Depcache
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
|
||||||
|
cargo build --release 2>/dev/null || true && \
|
||||||
|
rm -rf src
|
||||||
|
|
||||||
|
# Build
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release && strip target/release/telemt
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Stage: minimal
|
# Stage 2: Compress (strip + UPX)
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM debian:12-slim AS minimal
|
FROM debian:12-slim AS minimal
|
||||||
|
|
||||||
|
|
@ -13,6 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
\
|
\
|
||||||
|
# install UPX from Telemt releases
|
||||||
&& curl -fL \
|
&& curl -fL \
|
||||||
--retry 5 \
|
--retry 5 \
|
||||||
--retry-delay 3 \
|
--retry-delay 3 \
|
||||||
|
|
@ -25,15 +46,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& chmod +x /usr/local/bin/upx \
|
&& chmod +x /usr/local/bin/upx \
|
||||||
&& rm -rf /tmp/upx*
|
&& rm -rf /tmp/upx*
|
||||||
|
|
||||||
COPY ${BINARY} /telemt
|
COPY --from=builder /build/target/release/telemt /telemt
|
||||||
|
|
||||||
RUN strip /telemt || true
|
RUN strip /telemt || true
|
||||||
RUN upx --best --lzma /telemt || true
|
RUN upx --best --lzma /telemt || true
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Debug image
|
# Stage 3: Debug base
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM debian:12-slim AS debug
|
FROM debian:12-slim AS debug-base
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
|
@ -43,29 +64,48 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
busybox \
|
busybox \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Stage 4: Debug image
|
||||||
|
# ==========================
|
||||||
|
FROM debug-base AS debug
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=minimal /telemt /app/telemt
|
COPY --from=minimal /telemt /app/telemt
|
||||||
COPY config.toml /app/config.toml
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
EXPOSE 443 9090 9091
|
USER root
|
||||||
|
|
||||||
|
EXPOSE 443
|
||||||
|
EXPOSE 9090
|
||||||
|
EXPOSE 9091
|
||||||
|
|
||||||
ENTRYPOINT ["/app/telemt"]
|
ENTRYPOINT ["/app/telemt"]
|
||||||
CMD ["config.toml"]
|
CMD ["config.toml"]
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Production (REAL distroless)
|
# Stage 5: Production (distroless)
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM gcr.io/distroless/static-debian12 AS prod
|
FROM gcr.io/distroless/base-debian12 AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=minimal /telemt /app/telemt
|
COPY --from=minimal /telemt /app/telemt
|
||||||
COPY config.toml /app/config.toml
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
|
# TLS + timezone + shell
|
||||||
|
COPY --from=debug-base /etc/ssl/certs /etc/ssl/certs
|
||||||
|
COPY --from=debug-base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=debug-base /bin/busybox /bin/busybox
|
||||||
|
|
||||||
|
RUN ["/bin/busybox", "--install", "-s", "/bin"]
|
||||||
|
|
||||||
|
# distroless user
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
|
|
||||||
EXPOSE 443 9090 9091
|
EXPOSE 443
|
||||||
|
EXPOSE 9090
|
||||||
|
EXPOSE 9091
|
||||||
|
|
||||||
ENTRYPOINT ["/app/telemt"]
|
ENTRYPOINT ["/app/telemt"]
|
||||||
CMD ["config.toml"]
|
CMD ["config.toml"]
|
||||||
|
|
@ -63,7 +63,7 @@ recommended range from 5 to 2147483647 inclusive
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> It is recommended to use your own, unique values.\
|
> It is recommended to use your own, unique values.\
|
||||||
> You can use the [generator](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/13f5517ca473b47c412b9a99407066de973732bd/awg-gen.html) to select parameters.
|
> You can use the [generator](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/e8b269ff0089a27effd88f8d925179b78e5666c4/awg-gen.html) to select parameters.
|
||||||
|
|
||||||
#### Server B Configuration (Netherlands):
|
#### Server B Configuration (Netherlands):
|
||||||
|
|
||||||
|
|
@ -84,8 +84,6 @@ Jmin = 8
|
||||||
Jmax = 80
|
Jmax = 80
|
||||||
S1 = 29
|
S1 = 29
|
||||||
S2 = 15
|
S2 = 15
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
H1 = 2087563914
|
||||||
H2 = 188817757
|
H2 = 188817757
|
||||||
H3 = 101784570
|
H3 = 101784570
|
||||||
|
|
@ -123,8 +121,6 @@ Jmin = 8
|
||||||
Jmax = 80
|
Jmax = 80
|
||||||
S1 = 29
|
S1 = 29
|
||||||
S2 = 15
|
S2 = 15
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
H1 = 2087563914
|
||||||
H2 = 188817757
|
H2 = 188817757
|
||||||
H3 = 101784570
|
H3 = 101784570
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ awg genkey | tee private.key | awg pubkey > public.key
|
||||||
|
|
||||||
Параметры обфускации `S1`, `S2`, `H1`, `H2`, `H3`, `H4` должны быть строго идентичными на обоих серверах.\
|
Параметры обфускации `S1`, `S2`, `H1`, `H2`, `H3`, `H4` должны быть строго идентичными на обоих серверах.\
|
||||||
Параметры `Jc`, `Jmin` и `Jmax` могут отличатся.\
|
Параметры `Jc`, `Jmin` и `Jmax` могут отличатся.\
|
||||||
Параметры `I1-I5` ([Custom Protocol Signature](https://docs.amnezia.org/documentation/amnezia-wg/)) нужно указывать на стороне _клиента_ (Сервер **А**).
|
Параметры `I1-I5` [(Custom Protocol Signature)](https://docs.amnezia.org/documentation/amnezia-wg/) нужно указывать на стороне _клиента_ (Сервер **А**).
|
||||||
|
|
||||||
Рекомендации по выбору значений:
|
Рекомендации по выбору значений:
|
||||||
```text
|
```text
|
||||||
|
|
@ -62,7 +62,7 @@ H1/H2/H3/H4 — должны быть уникальны и отличаться
|
||||||
```
|
```
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Рекомендуется использовать собственные, уникальные значения.\
|
> Рекомендуется использовать собственные, уникальные значения.\
|
||||||
> Для выбора параметров можете воспользоваться [генератором](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/13f5517ca473b47c412b9a99407066de973732bd/awg-gen.html).
|
> Для выбора параметров можете воспользоваться [генератором](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/e8b269ff0089a27effd88f8d925179b78e5666c4/awg-gen.html).
|
||||||
|
|
||||||
#### Конфигурация Сервера B (_Нидерланды_):
|
#### Конфигурация Сервера B (_Нидерланды_):
|
||||||
|
|
||||||
|
|
@ -83,8 +83,6 @@ Jmin = 8
|
||||||
Jmax = 80
|
Jmax = 80
|
||||||
S1 = 29
|
S1 = 29
|
||||||
S2 = 15
|
S2 = 15
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
H1 = 2087563914
|
||||||
H2 = 188817757
|
H2 = 188817757
|
||||||
H3 = 101784570
|
H3 = 101784570
|
||||||
|
|
@ -123,8 +121,6 @@ Jmin = 8
|
||||||
Jmax = 80
|
Jmax = 80
|
||||||
S1 = 29
|
S1 = 29
|
||||||
S2 = 15
|
S2 = 15
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
H1 = 2087563914
|
||||||
H2 = 188817757
|
H2 = 188817757
|
||||||
H3 = 101784570
|
H3 = 101784570
|
||||||
|
|
@ -276,7 +272,7 @@ backend telemt_nodes
|
||||||
|
|
||||||
```
|
```
|
||||||
>[!WARNING]
|
>[!WARNING]
|
||||||
>**Файл должен заканчиваться пустой строкой, иначе HAProxy не запустится!**
|
>**Файл должен заканчиваться пустой строкой, иначе HAProxy не запуститься!**
|
||||||
|
|
||||||
#### Разрешаем порт 443\tcp в фаерволе (если включен)
|
#### Разрешаем порт 443\tcp в фаерволе (если включен)
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,6 @@ pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
|
||||||
500
|
500
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_proxy_protocol_trusted_cidrs() -> Vec<IpNetwork> {
|
|
||||||
vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_server_max_connections() -> u32 {
|
pub(crate) fn default_server_max_connections() -> u32 {
|
||||||
10_000
|
10_000
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,9 +228,7 @@ impl HotFields {
|
||||||
me_d2c_flush_batch_max_delay_us: cfg.general.me_d2c_flush_batch_max_delay_us,
|
me_d2c_flush_batch_max_delay_us: cfg.general.me_d2c_flush_batch_max_delay_us,
|
||||||
me_d2c_ack_flush_immediate: cfg.general.me_d2c_ack_flush_immediate,
|
me_d2c_ack_flush_immediate: cfg.general.me_d2c_ack_flush_immediate,
|
||||||
me_quota_soft_overshoot_bytes: cfg.general.me_quota_soft_overshoot_bytes,
|
me_quota_soft_overshoot_bytes: cfg.general.me_quota_soft_overshoot_bytes,
|
||||||
me_d2c_frame_buf_shrink_threshold_bytes: cfg
|
me_d2c_frame_buf_shrink_threshold_bytes: cfg.general.me_d2c_frame_buf_shrink_threshold_bytes,
|
||||||
.general
|
|
||||||
.me_d2c_frame_buf_shrink_threshold_bytes,
|
|
||||||
direct_relay_copy_buf_c2s_bytes: cfg.general.direct_relay_copy_buf_c2s_bytes,
|
direct_relay_copy_buf_c2s_bytes: cfg.general.direct_relay_copy_buf_c2s_bytes,
|
||||||
direct_relay_copy_buf_s2c_bytes: cfg.general.direct_relay_copy_buf_s2c_bytes,
|
direct_relay_copy_buf_s2c_bytes: cfg.general.direct_relay_copy_buf_s2c_bytes,
|
||||||
me_health_interval_ms_unhealthy: cfg.general.me_health_interval_ms_unhealthy,
|
me_health_interval_ms_unhealthy: cfg.general.me_health_interval_ms_unhealthy,
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,8 @@ impl ProxyConfig {
|
||||||
|
|
||||||
if !(5..=50).contains(&config.censorship.mask_classifier_prefetch_timeout_ms) {
|
if !(5..=50).contains(&config.censorship.mask_classifier_prefetch_timeout_ms) {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]".to_string(),
|
"censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]"
|
||||||
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,9 +558,7 @@ impl ProxyConfig {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(4096..=16 * 1024 * 1024)
|
if !(4096..=16 * 1024 * 1024).contains(&config.general.me_d2c_frame_buf_shrink_threshold_bytes) {
|
||||||
.contains(&config.general.me_d2c_frame_buf_shrink_threshold_bytes)
|
|
||||||
{
|
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.me_d2c_frame_buf_shrink_threshold_bytes must be within [4096, 16777216]"
|
"general.me_d2c_frame_buf_shrink_threshold_bytes must be within [4096, 16777216]"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|
@ -1263,11 +1262,6 @@ mod tests {
|
||||||
assert_eq!(cfg.general.update_every, default_update_every());
|
assert_eq!(cfg.general.update_every, default_update_every());
|
||||||
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
|
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
|
||||||
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
|
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
|
||||||
assert_eq!(
|
|
||||||
cfg.server.proxy_protocol_trusted_cidrs,
|
|
||||||
default_proxy_protocol_trusted_cidrs()
|
|
||||||
);
|
|
||||||
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
|
||||||
assert_eq!(cfg.server.api.listen, default_api_listen());
|
assert_eq!(cfg.server.api.listen, default_api_listen());
|
||||||
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1400,14 +1394,6 @@ mod tests {
|
||||||
|
|
||||||
let server = ServerConfig::default();
|
let server = ServerConfig::default();
|
||||||
assert_eq!(server.listen_addr_ipv6, Some(default_listen_addr_ipv6()));
|
assert_eq!(server.listen_addr_ipv6, Some(default_listen_addr_ipv6()));
|
||||||
assert_eq!(
|
|
||||||
server.proxy_protocol_trusted_cidrs,
|
|
||||||
default_proxy_protocol_trusted_cidrs()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
AntiCensorshipConfig::default().unknown_sni_action,
|
|
||||||
UnknownSniAction::Drop
|
|
||||||
);
|
|
||||||
assert_eq!(server.api.listen, default_api_listen());
|
assert_eq!(server.api.listen, default_api_listen());
|
||||||
assert_eq!(server.api.whitelist, default_api_whitelist());
|
assert_eq!(server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1443,75 +1429,6 @@ mod tests {
|
||||||
assert_eq!(access.users, default_access_users());
|
assert_eq!(access.users, default_access_users());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn proxy_protocol_trusted_cidrs_missing_uses_trust_all_but_explicit_empty_stays_empty() {
|
|
||||||
let cfg_missing: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
cfg_missing.server.proxy_protocol_trusted_cidrs,
|
|
||||||
default_proxy_protocol_trusted_cidrs()
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg_explicit_empty: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
proxy_protocol_trusted_cidrs = []
|
|
||||||
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
cfg_explicit_empty
|
|
||||||
.server
|
|
||||||
.proxy_protocol_trusted_cidrs
|
|
||||||
.is_empty()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_sni_action_parses_and_defaults_to_drop() {
|
|
||||||
let cfg_default: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
[censorship]
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
cfg_default.censorship.unknown_sni_action,
|
|
||||||
UnknownSniAction::Drop
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg_mask: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
[censorship]
|
|
||||||
unknown_sni_action = "mask"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
cfg_mask.censorship.unknown_sni_action,
|
|
||||||
UnknownSniAction::Mask
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dc_overrides_allow_string_and_array() {
|
fn dc_overrides_allow_string_and_array() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,8 @@ fn write_temp_config(contents: &str) -> PathBuf {
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("system time must be after unix epoch")
|
.expect("system time must be after unix epoch")
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
let path = std::env::temp_dir().join(format!(
|
let path = std::env::temp_dir()
|
||||||
"telemt-load-mask-prefetch-timeout-security-{nonce}.toml"
|
.join(format!("telemt-load-mask-prefetch-timeout-security-{nonce}.toml"));
|
||||||
));
|
|
||||||
fs::write(&path, contents).expect("temp config write must succeed");
|
fs::write(&path, contents).expect("temp config write must succeed");
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +67,8 @@ mask_classifier_prefetch_timeout_ms = 20
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cfg =
|
let cfg = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect("prefetch timeout within security bounds must be accepted");
|
.expect("prefetch timeout within security bounds must be accepted");
|
||||||
assert_eq!(cfg.censorship.mask_classifier_prefetch_timeout_ms, 20);
|
assert_eq!(cfg.censorship.mask_classifier_prefetch_timeout_ms, 20);
|
||||||
|
|
||||||
remove_temp_config(&path);
|
remove_temp_config(&path);
|
||||||
|
|
|
||||||
|
|
@ -265,8 +265,8 @@ mask_relay_max_bytes = 67108865
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("mask_relay_max_bytes above hard cap must be rejected");
|
.expect_err("mask_relay_max_bytes above hard cap must be rejected");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("censorship.mask_relay_max_bytes must be <= 67108864"),
|
msg.contains("censorship.mask_relay_max_bytes must be <= 67108864"),
|
||||||
|
|
|
||||||
|
|
@ -954,8 +954,7 @@ impl Default for GeneralConfig {
|
||||||
me_d2c_flush_batch_max_delay_us: default_me_d2c_flush_batch_max_delay_us(),
|
me_d2c_flush_batch_max_delay_us: default_me_d2c_flush_batch_max_delay_us(),
|
||||||
me_d2c_ack_flush_immediate: default_me_d2c_ack_flush_immediate(),
|
me_d2c_ack_flush_immediate: default_me_d2c_ack_flush_immediate(),
|
||||||
me_quota_soft_overshoot_bytes: default_me_quota_soft_overshoot_bytes(),
|
me_quota_soft_overshoot_bytes: default_me_quota_soft_overshoot_bytes(),
|
||||||
me_d2c_frame_buf_shrink_threshold_bytes:
|
me_d2c_frame_buf_shrink_threshold_bytes: default_me_d2c_frame_buf_shrink_threshold_bytes(),
|
||||||
default_me_d2c_frame_buf_shrink_threshold_bytes(),
|
|
||||||
direct_relay_copy_buf_c2s_bytes: default_direct_relay_copy_buf_c2s_bytes(),
|
direct_relay_copy_buf_c2s_bytes: default_direct_relay_copy_buf_c2s_bytes(),
|
||||||
direct_relay_copy_buf_s2c_bytes: default_direct_relay_copy_buf_s2c_bytes(),
|
direct_relay_copy_buf_s2c_bytes: default_direct_relay_copy_buf_s2c_bytes(),
|
||||||
me_warmup_stagger_enabled: default_true(),
|
me_warmup_stagger_enabled: default_true(),
|
||||||
|
|
@ -1240,10 +1239,9 @@ pub struct ServerConfig {
|
||||||
|
|
||||||
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
||||||
///
|
///
|
||||||
/// If this field is omitted in config, it defaults to trust-all CIDRs
|
/// When non-empty, connections from addresses outside this allowlist are
|
||||||
/// (`0.0.0.0/0` and `::/0`). If it is explicitly set to an empty list,
|
/// rejected before `src_addr` is applied.
|
||||||
/// all PROXY protocol headers are rejected.
|
#[serde(default)]
|
||||||
#[serde(default = "default_proxy_protocol_trusted_cidrs")]
|
|
||||||
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
||||||
|
|
||||||
/// Port for the Prometheus-compatible metrics endpoint.
|
/// Port for the Prometheus-compatible metrics endpoint.
|
||||||
|
|
@ -1288,7 +1286,7 @@ impl Default for ServerConfig {
|
||||||
listen_tcp: None,
|
listen_tcp: None,
|
||||||
proxy_protocol: false,
|
proxy_protocol: false,
|
||||||
proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(),
|
proxy_protocol_header_timeout_ms: default_proxy_protocol_header_timeout_ms(),
|
||||||
proxy_protocol_trusted_cidrs: default_proxy_protocol_trusted_cidrs(),
|
proxy_protocol_trusted_cidrs: Vec::new(),
|
||||||
metrics_port: None,
|
metrics_port: None,
|
||||||
metrics_listen: None,
|
metrics_listen: None,
|
||||||
metrics_whitelist: default_metrics_whitelist(),
|
metrics_whitelist: default_metrics_whitelist(),
|
||||||
|
|
@ -1359,14 +1357,6 @@ impl Default for TimeoutsConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum UnknownSniAction {
|
|
||||||
#[default]
|
|
||||||
Drop,
|
|
||||||
Mask,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AntiCensorshipConfig {
|
pub struct AntiCensorshipConfig {
|
||||||
#[serde(default = "default_tls_domain")]
|
#[serde(default = "default_tls_domain")]
|
||||||
|
|
@ -1376,10 +1366,6 @@ pub struct AntiCensorshipConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tls_domains: Vec<String>,
|
pub tls_domains: Vec<String>,
|
||||||
|
|
||||||
/// Policy for TLS ClientHello with unknown (non-configured) SNI.
|
|
||||||
#[serde(default)]
|
|
||||||
pub unknown_sni_action: UnknownSniAction,
|
|
||||||
|
|
||||||
/// Upstream scope used for TLS front metadata fetches.
|
/// Upstream scope used for TLS front metadata fetches.
|
||||||
/// Empty value keeps default upstream routing behavior.
|
/// Empty value keeps default upstream routing behavior.
|
||||||
#[serde(default = "default_tls_fetch_scope")]
|
#[serde(default = "default_tls_fetch_scope")]
|
||||||
|
|
@ -1490,7 +1476,6 @@ impl Default for AntiCensorshipConfig {
|
||||||
Self {
|
Self {
|
||||||
tls_domain: default_tls_domain(),
|
tls_domain: default_tls_domain(),
|
||||||
tls_domains: Vec::new(),
|
tls_domains: Vec::new(),
|
||||||
unknown_sni_action: UnknownSniAction::Drop,
|
|
||||||
tls_fetch_scope: default_tls_fetch_scope(),
|
tls_fetch_scope: default_tls_fetch_scope(),
|
||||||
mask: default_true(),
|
mask: default_true(),
|
||||||
mask_host: None,
|
mask_host: None,
|
||||||
|
|
|
||||||
|
|
@ -216,9 +216,6 @@ pub enum ProxyError {
|
||||||
#[error("Invalid proxy protocol header")]
|
#[error("Invalid proxy protocol header")]
|
||||||
InvalidProxyProtocol,
|
InvalidProxyProtocol,
|
||||||
|
|
||||||
#[error("Unknown TLS SNI")]
|
|
||||||
UnknownTlsSni,
|
|
||||||
|
|
||||||
#[error("Proxy error: {0}")]
|
#[error("Proxy error: {0}")]
|
||||||
Proxy(String),
|
Proxy(String),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,8 @@ use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
ProxyConfigData, fetch_proxy_config_with_raw_via_upstream, load_proxy_config_cache,
|
ProxyConfigData, fetch_proxy_config_with_raw, load_proxy_config_cache, save_proxy_config_cache,
|
||||||
save_proxy_config_cache,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn resolve_runtime_config_path(
|
pub(crate) fn resolve_runtime_config_path(
|
||||||
|
|
@ -290,10 +288,9 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
||||||
cache_path: Option<&str>,
|
cache_path: Option<&str>,
|
||||||
me2dc_fallback: bool,
|
me2dc_fallback: bool,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
upstream: Option<std::sync::Arc<UpstreamManager>>,
|
|
||||||
) -> Option<ProxyConfigData> {
|
) -> Option<ProxyConfigData> {
|
||||||
loop {
|
loop {
|
||||||
match fetch_proxy_config_with_raw_via_upstream(url, upstream.clone()).await {
|
match fetch_proxy_config_with_raw(url).await {
|
||||||
Ok((cfg, raw)) => {
|
Ok((cfg, raw)) => {
|
||||||
if !cfg.map.is_empty() {
|
if !cfg.map.is_empty() {
|
||||||
if let Some(path) = cache_path
|
if let Some(path) = cache_path
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,9 @@ pub(crate) async fn initialize_me_pool(
|
||||||
let proxy_secret_path = config.general.proxy_secret_path.as_deref();
|
let proxy_secret_path = config.general.proxy_secret_path.as_deref();
|
||||||
let pool_size = config.general.middle_proxy_pool_size.max(1);
|
let pool_size = config.general.middle_proxy_pool_size.max(1);
|
||||||
let proxy_secret = loop {
|
let proxy_secret = loop {
|
||||||
match crate::transport::middle_proxy::fetch_proxy_secret_with_upstream(
|
match crate::transport::middle_proxy::fetch_proxy_secret(
|
||||||
proxy_secret_path,
|
proxy_secret_path,
|
||||||
config.general.proxy_secret_len_max,
|
config.general.proxy_secret_len_max,
|
||||||
Some(upstream_manager.clone()),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -130,7 +129,6 @@ pub(crate) async fn initialize_me_pool(
|
||||||
config.general.proxy_config_v4_cache_path.as_deref(),
|
config.general.proxy_config_v4_cache_path.as_deref(),
|
||||||
me2dc_fallback,
|
me2dc_fallback,
|
||||||
"getProxyConfig",
|
"getProxyConfig",
|
||||||
Some(upstream_manager.clone()),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if cfg_v4.is_some() {
|
if cfg_v4.is_some() {
|
||||||
|
|
@ -162,7 +160,6 @@ pub(crate) async fn initialize_me_pool(
|
||||||
config.general.proxy_config_v6_cache_path.as_deref(),
|
config.general.proxy_config_v6_cache_path.as_deref(),
|
||||||
me2dc_fallback,
|
me2dc_fallback,
|
||||||
"getProxyConfigV6",
|
"getProxyConfigV6",
|
||||||
Some(upstream_manager.clone()),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if cfg_v6.is_some() {
|
if cfg_v6.is_some() {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ mod crypto;
|
||||||
mod error;
|
mod error;
|
||||||
mod ip_tracker;
|
mod ip_tracker;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
|
|
||||||
mod ip_tracker_encapsulation_adversarial_tests;
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"]
|
#[path = "tests/ip_tracker_hotpath_adversarial_tests.rs"]
|
||||||
mod ip_tracker_hotpath_adversarial_tests;
|
mod ip_tracker_hotpath_adversarial_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
|
||||||
|
mod ip_tracker_encapsulation_adversarial_tests;
|
||||||
|
#[cfg(test)]
|
||||||
#[path = "tests/ip_tracker_regression_tests.rs"]
|
#[path = "tests/ip_tracker_regression_tests.rs"]
|
||||||
mod ip_tracker_regression_tests;
|
mod ip_tracker_regression_tests;
|
||||||
mod maestro;
|
mod maestro;
|
||||||
|
|
|
||||||
|
|
@ -1233,7 +1233,10 @@ async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIp
|
||||||
out,
|
out,
|
||||||
"# HELP telemt_me_d2c_batch_bytes_bucket_total DC->Client batch byte size buckets"
|
"# HELP telemt_me_d2c_batch_bytes_bucket_total DC->Client batch byte size buckets"
|
||||||
);
|
);
|
||||||
let _ = writeln!(out, "# TYPE telemt_me_d2c_batch_bytes_bucket_total counter");
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_me_d2c_batch_bytes_bucket_total counter"
|
||||||
|
);
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"0_1k\"}} {}",
|
"telemt_me_d2c_batch_bytes_bucket_total{{bucket=\"0_1k\"}} {}",
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,7 @@ fn should_prefetch_mask_classifier_window(initial_data: &[u8]) -> bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
initial_data
|
initial_data.iter().all(|b| b.is_ascii_alphabetic() || *b == b' ')
|
||||||
.iter()
|
|
||||||
.all(|b| b.is_ascii_alphabetic() || *b == b' ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -220,19 +218,16 @@ async fn extend_masking_initial_window<R>(reader: &mut R, initial_data: &mut Vec
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
extend_masking_initial_window_with_timeout(
|
extend_masking_initial_window_with_timeout(reader, initial_data, MASK_CLASSIFIER_PREFETCH_TIMEOUT)
|
||||||
reader,
|
.await;
|
||||||
initial_data,
|
|
||||||
MASK_CLASSIFIER_PREFETCH_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn extend_masking_initial_window_with_timeout<R>(
|
async fn extend_masking_initial_window_with_timeout<R>(
|
||||||
reader: &mut R,
|
reader: &mut R,
|
||||||
initial_data: &mut Vec<u8>,
|
initial_data: &mut Vec<u8>,
|
||||||
prefetch_timeout: Duration,
|
prefetch_timeout: Duration,
|
||||||
) where
|
)
|
||||||
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
if !should_prefetch_mask_classifier_window(initial_data) {
|
if !should_prefetch_mask_classifier_window(initial_data) {
|
||||||
|
|
@ -317,20 +312,13 @@ fn record_handshake_failure_class(
|
||||||
record_beobachten_class(beobachten, config, peer_ip, class);
|
record_beobachten_class(beobachten, config, peer_ip, class);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn increment_bad_on_unknown_tls_sni(stats: &Stats, error: &ProxyError) {
|
|
||||||
if matches!(error, ProxyError::UnknownTlsSni) {
|
|
||||||
stats.increment_connects_bad();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_trusted_proxy_source(peer_ip: IpAddr, trusted: &[IpNetwork]) -> bool {
|
fn is_trusted_proxy_source(peer_ip: IpAddr, trusted: &[IpNetwork]) -> bool {
|
||||||
if trusted.is_empty() {
|
if trusted.is_empty() {
|
||||||
static EMPTY_PROXY_TRUST_WARNED: OnceLock<AtomicBool> = OnceLock::new();
|
static EMPTY_PROXY_TRUST_WARNED: OnceLock<AtomicBool> = OnceLock::new();
|
||||||
let warned = EMPTY_PROXY_TRUST_WARNED.get_or_init(|| AtomicBool::new(false));
|
let warned = EMPTY_PROXY_TRUST_WARNED.get_or_init(|| AtomicBool::new(false));
|
||||||
if !warned.swap(true, Ordering::Relaxed) {
|
if !warned.swap(true, Ordering::Relaxed) {
|
||||||
warn!(
|
warn!(
|
||||||
"PROXY protocol enabled but server.proxy_protocol_trusted_cidrs is empty; rejecting all PROXY headers"
|
"PROXY protocol enabled but server.proxy_protocol_trusted_cidrs is empty; rejecting all PROXY headers by default"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -515,10 +503,7 @@ where
|
||||||
beobachten.clone(),
|
beobachten.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
HandshakeResult::Error(e) => {
|
HandshakeResult::Error(e) => return Err(e),
|
||||||
increment_bad_on_unknown_tls_sni(stats.as_ref(), &e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
||||||
|
|
@ -969,10 +954,7 @@ impl RunningClientHandler {
|
||||||
self.beobachten.clone(),
|
self.beobachten.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
HandshakeResult::Error(e) => {
|
HandshakeResult::Error(e) => return Err(e),
|
||||||
increment_bad_on_unknown_tls_sni(stats.as_ref(), &e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use crate::config::{ProxyConfig, UnknownSniAction};
|
use crate::config::ProxyConfig;
|
||||||
use crate::crypto::{AesCtr, SecureRandom, sha256};
|
use crate::crypto::{AesCtr, SecureRandom, sha256};
|
||||||
use crate::error::{HandshakeResult, ProxyError};
|
use crate::error::{HandshakeResult, ProxyError};
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
|
|
@ -510,21 +510,6 @@ fn decode_user_secrets(
|
||||||
secrets
|
secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn find_matching_tls_domain<'a>(config: &'a ProxyConfig, sni: &str) -> Option<&'a str> {
|
|
||||||
if config.censorship.tls_domain.eq_ignore_ascii_case(sni) {
|
|
||||||
return Some(config.censorship.tls_domain.as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
for domain in &config.censorship.tls_domains {
|
|
||||||
if domain.eq_ignore_ascii_case(sni) {
|
|
||||||
return Some(domain.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn maybe_apply_server_hello_delay(config: &ProxyConfig) {
|
async fn maybe_apply_server_hello_delay(config: &ProxyConfig) {
|
||||||
if config.censorship.server_hello_delay_max_ms == 0 {
|
if config.censorship.server_hello_delay_max_ms == 0 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -608,25 +593,6 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
||||||
let matched_tls_domain = client_sni
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|sni| find_matching_tls_domain(config, sni));
|
|
||||||
|
|
||||||
if client_sni.is_some() && matched_tls_domain.is_none() {
|
|
||||||
auth_probe_record_failure(peer.ip(), Instant::now());
|
|
||||||
maybe_apply_server_hello_delay(config).await;
|
|
||||||
debug!(
|
|
||||||
peer = %peer,
|
|
||||||
sni = ?client_sni,
|
|
||||||
action = ?config.censorship.unknown_sni_action,
|
|
||||||
"TLS handshake rejected by unknown SNI policy"
|
|
||||||
);
|
|
||||||
return match config.censorship.unknown_sni_action {
|
|
||||||
UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni),
|
|
||||||
UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let secrets = decode_user_secrets(config, client_sni.as_deref());
|
let secrets = decode_user_secrets(config, client_sni.as_deref());
|
||||||
|
|
||||||
let validation = match tls::validate_tls_handshake_with_replay_window(
|
let validation = match tls::validate_tls_handshake_with_replay_window(
|
||||||
|
|
@ -667,9 +633,16 @@ where
|
||||||
|
|
||||||
let cached = if config.censorship.tls_emulation {
|
let cached = if config.censorship.tls_emulation {
|
||||||
if let Some(cache) = tls_cache.as_ref() {
|
if let Some(cache) = tls_cache.as_ref() {
|
||||||
let selected_domain =
|
let selected_domain = if let Some(sni) = client_sni.as_ref() {
|
||||||
matched_tls_domain.unwrap_or(config.censorship.tls_domain.as_str());
|
if cache.contains_domain(sni).await {
|
||||||
let cached_entry = cache.get(selected_domain).await;
|
sni.clone()
|
||||||
|
} else {
|
||||||
|
config.censorship.tls_domain.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.censorship.tls_domain.clone()
|
||||||
|
};
|
||||||
|
let cached_entry = cache.get(&selected_domain).await;
|
||||||
let use_full_cert_payload = cache
|
let use_full_cert_payload = cache
|
||||||
.take_full_cert_budget_for_ip(
|
.take_full_cert_budget_for_ip(
|
||||||
peer.ip(),
|
peer.ip(),
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ use rand::rngs::StdRng;
|
||||||
use rand::{Rng, RngExt, SeedableRng};
|
use rand::{Rng, RngExt, SeedableRng};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::str;
|
use std::str;
|
||||||
#[cfg(test)]
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
#[cfg(test)]
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::time::{Duration, Instant as StdInstant};
|
use std::time::{Duration, Instant as StdInstant};
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
@ -107,7 +107,15 @@ where
|
||||||
fn is_http_probe(data: &[u8]) -> bool {
|
fn is_http_probe(data: &[u8]) -> bool {
|
||||||
// RFC 7540 section 3.5: HTTP/2 client preface starts with "PRI ".
|
// RFC 7540 section 3.5: HTTP/2 client preface starts with "PRI ".
|
||||||
const HTTP_METHODS: [&[u8]; 10] = [
|
const HTTP_METHODS: [&[u8]; 10] = [
|
||||||
b"GET ", b"POST", b"HEAD", b"PUT ", b"DELETE", b"OPTIONS", b"CONNECT", b"TRACE", b"PATCH",
|
b"GET ",
|
||||||
|
b"POST",
|
||||||
|
b"HEAD",
|
||||||
|
b"PUT ",
|
||||||
|
b"DELETE",
|
||||||
|
b"OPTIONS",
|
||||||
|
b"CONNECT",
|
||||||
|
b"TRACE",
|
||||||
|
b"PATCH",
|
||||||
b"PRI ",
|
b"PRI ",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -320,10 +328,7 @@ fn parse_mask_host_ip_literal(host: &str) -> Option<IpAddr> {
|
||||||
|
|
||||||
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
||||||
match ip {
|
match ip {
|
||||||
IpAddr::V6(v6) => v6
|
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(IpAddr::V6(v6)),
|
||||||
.to_ipv4_mapped()
|
|
||||||
.map(IpAddr::V4)
|
|
||||||
.unwrap_or(IpAddr::V6(v6)),
|
|
||||||
IpAddr::V4(v4) => IpAddr::V4(v4),
|
IpAddr::V4(v4) => IpAddr::V4(v4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -659,20 +664,12 @@ pub async fn handle_bad_client<R, W>(
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
||||||
debug!(error = %e, "Failed to connect to mask unix socket");
|
debug!(error = %e, "Failed to connect to mask unix socket");
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes).await;
|
||||||
reader,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!("Timeout connecting to mask unix socket");
|
debug!("Timeout connecting to mask unix socket");
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes).await;
|
||||||
reader,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -701,8 +698,7 @@ pub async fn handle_bad_client<R, W>(
|
||||||
local = %local_addr,
|
local = %local_addr,
|
||||||
"Mask target resolves to local listener; refusing self-referential masking fallback"
|
"Mask target resolves to local listener; refusing self-referential masking fallback"
|
||||||
);
|
);
|
||||||
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes).await;
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -762,20 +758,12 @@ pub async fn handle_bad_client<R, W>(
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
||||||
debug!(error = %e, "Failed to connect to mask host");
|
debug!(error = %e, "Failed to connect to mask host");
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes).await;
|
||||||
reader,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!("Timeout connecting to mask host");
|
debug!("Timeout connecting to mask host");
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes).await;
|
||||||
reader,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,7 @@ use crate::proxy::route_mode::{
|
||||||
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
||||||
cutover_stagger_delay,
|
cutover_stagger_delay,
|
||||||
};
|
};
|
||||||
use crate::stats::{
|
use crate::stats::{MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats};
|
||||||
MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats,
|
|
||||||
};
|
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter, PooledBuffer};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter, PooledBuffer};
|
||||||
use crate::transport::middle_proxy::{MePool, MeResponse, proto_flags_for_tag};
|
use crate::transport::middle_proxy::{MePool, MeResponse, proto_flags_for_tag};
|
||||||
|
|
||||||
|
|
@ -93,8 +91,7 @@ fn relay_idle_candidate_registry() -> &'static Mutex<RelayIdleCandidateRegistry>
|
||||||
RELAY_IDLE_CANDIDATE_REGISTRY.get_or_init(|| Mutex::new(RelayIdleCandidateRegistry::default()))
|
RELAY_IDLE_CANDIDATE_REGISTRY.get_or_init(|| Mutex::new(RelayIdleCandidateRegistry::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relay_idle_candidate_registry_lock() -> std::sync::MutexGuard<'static, RelayIdleCandidateRegistry>
|
fn relay_idle_candidate_registry_lock() -> std::sync::MutexGuard<'static, RelayIdleCandidateRegistry> {
|
||||||
{
|
|
||||||
let registry = relay_idle_candidate_registry();
|
let registry = relay_idle_candidate_registry();
|
||||||
match registry.lock() {
|
match registry.lock() {
|
||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
|
|
@ -1523,7 +1520,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if !idle_policy.enabled {
|
if !idle_policy.enabled {
|
||||||
consecutive_zero_len_frames = consecutive_zero_len_frames.saturating_add(1);
|
consecutive_zero_len_frames =
|
||||||
|
consecutive_zero_len_frames.saturating_add(1);
|
||||||
if consecutive_zero_len_frames > LEGACY_MAX_CONSECUTIVE_ZERO_LEN_FRAMES {
|
if consecutive_zero_len_frames > LEGACY_MAX_CONSECUTIVE_ZERO_LEN_FRAMES {
|
||||||
stats.increment_relay_protocol_desync_close_total();
|
stats.increment_relay_protocol_desync_close_total();
|
||||||
return Err(ProxyError::Proxy(
|
return Err(ProxyError::Proxy(
|
||||||
|
|
@ -1837,14 +1835,8 @@ where
|
||||||
MeD2cWriteMode::Coalesced
|
MeD2cWriteMode::Coalesced
|
||||||
} else {
|
} else {
|
||||||
let header = [first];
|
let header = [first];
|
||||||
client_writer
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
.write_all(&header)
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
client_writer
|
|
||||||
.write_all(data)
|
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
MeD2cWriteMode::Split
|
MeD2cWriteMode::Split
|
||||||
}
|
}
|
||||||
} else if len_words < (1 << 24) {
|
} else if len_words < (1 << 24) {
|
||||||
|
|
@ -1866,14 +1858,8 @@ where
|
||||||
MeD2cWriteMode::Coalesced
|
MeD2cWriteMode::Coalesced
|
||||||
} else {
|
} else {
|
||||||
let header = [first, lw[0], lw[1], lw[2]];
|
let header = [first, lw[0], lw[1], lw[2]];
|
||||||
client_writer
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
.write_all(&header)
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
client_writer
|
|
||||||
.write_all(data)
|
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
MeD2cWriteMode::Split
|
MeD2cWriteMode::Split
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1915,14 +1901,8 @@ where
|
||||||
MeD2cWriteMode::Coalesced
|
MeD2cWriteMode::Coalesced
|
||||||
} else {
|
} else {
|
||||||
let header = len_val.to_le_bytes();
|
let header = len_val.to_le_bytes();
|
||||||
client_writer
|
client_writer.write_all(&header).await.map_err(ProxyError::Io)?;
|
||||||
.write_all(&header)
|
client_writer.write_all(data).await.map_err(ProxyError::Io)?;
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
client_writer
|
|
||||||
.write_all(data)
|
|
||||||
.await
|
|
||||||
.map_err(ProxyError::Io)?;
|
|
||||||
if padding_len > 0 {
|
if padding_len > 0 {
|
||||||
frame_buf.clear();
|
frame_buf.clear();
|
||||||
if frame_buf.capacity() < padding_len {
|
if frame_buf.capacity() < padding_len {
|
||||||
|
|
@ -1997,7 +1977,3 @@ mod middle_relay_tiny_frame_debt_concurrency_security_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/middle_relay_tiny_frame_debt_proto_chunking_security_tests.rs"]
|
#[path = "tests/middle_relay_tiny_frame_debt_proto_chunking_security_tests.rs"]
|
||||||
mod middle_relay_tiny_frame_debt_proto_chunking_security_tests;
|
mod middle_relay_tiny_frame_debt_proto_chunking_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/middle_relay_atomic_quota_invariant_tests.rs"]
|
|
||||||
mod middle_relay_atomic_quota_invariant_tests;
|
|
||||||
|
|
|
||||||
100
src/proxy/mod.rs
100
src/proxy/mod.rs
|
|
@ -4,58 +4,58 @@
|
||||||
#![cfg_attr(test, allow(warnings))]
|
#![cfg_attr(test, allow(warnings))]
|
||||||
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
not(test),
|
not(test),
|
||||||
deny(
|
deny(
|
||||||
clippy::unwrap_used,
|
clippy::unwrap_used,
|
||||||
clippy::expect_used,
|
clippy::expect_used,
|
||||||
clippy::panic,
|
clippy::panic,
|
||||||
clippy::todo,
|
clippy::todo,
|
||||||
clippy::unimplemented,
|
clippy::unimplemented,
|
||||||
clippy::correctness,
|
clippy::correctness,
|
||||||
clippy::option_if_let_else,
|
clippy::option_if_let_else,
|
||||||
clippy::or_fun_call,
|
clippy::or_fun_call,
|
||||||
clippy::branches_sharing_code,
|
clippy::branches_sharing_code,
|
||||||
clippy::single_option_map,
|
clippy::single_option_map,
|
||||||
clippy::useless_let_if_seq,
|
clippy::useless_let_if_seq,
|
||||||
clippy::redundant_locals,
|
clippy::redundant_locals,
|
||||||
clippy::cloned_ref_to_slice_refs,
|
clippy::cloned_ref_to_slice_refs,
|
||||||
unsafe_code,
|
unsafe_code,
|
||||||
clippy::await_holding_lock,
|
clippy::await_holding_lock,
|
||||||
clippy::await_holding_refcell_ref,
|
clippy::await_holding_refcell_ref,
|
||||||
clippy::debug_assert_with_mut_call,
|
clippy::debug_assert_with_mut_call,
|
||||||
clippy::macro_use_imports,
|
clippy::macro_use_imports,
|
||||||
clippy::cast_ptr_alignment,
|
clippy::cast_ptr_alignment,
|
||||||
clippy::cast_lossless,
|
clippy::cast_lossless,
|
||||||
clippy::ptr_as_ptr,
|
clippy::ptr_as_ptr,
|
||||||
clippy::large_stack_arrays,
|
clippy::large_stack_arrays,
|
||||||
clippy::same_functions_in_if_condition,
|
clippy::same_functions_in_if_condition,
|
||||||
trivial_casts,
|
trivial_casts,
|
||||||
trivial_numeric_casts,
|
trivial_numeric_casts,
|
||||||
unused_extern_crates,
|
unused_extern_crates,
|
||||||
unused_import_braces,
|
unused_import_braces,
|
||||||
rust_2018_idioms
|
rust_2018_idioms
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
not(test),
|
not(test),
|
||||||
allow(
|
allow(
|
||||||
clippy::use_self,
|
clippy::use_self,
|
||||||
clippy::redundant_closure,
|
clippy::redundant_closure,
|
||||||
clippy::too_many_arguments,
|
clippy::too_many_arguments,
|
||||||
clippy::doc_markdown,
|
clippy::doc_markdown,
|
||||||
clippy::missing_const_for_fn,
|
clippy::missing_const_for_fn,
|
||||||
clippy::unnecessary_operation,
|
clippy::unnecessary_operation,
|
||||||
clippy::redundant_pub_crate,
|
clippy::redundant_pub_crate,
|
||||||
clippy::derive_partial_eq_without_eq,
|
clippy::derive_partial_eq_without_eq,
|
||||||
clippy::type_complexity,
|
clippy::type_complexity,
|
||||||
clippy::new_ret_no_self,
|
clippy::new_ret_no_self,
|
||||||
clippy::cast_possible_truncation,
|
clippy::cast_possible_truncation,
|
||||||
clippy::cast_possible_wrap,
|
clippy::cast_possible_wrap,
|
||||||
clippy::significant_drop_tightening,
|
clippy::significant_drop_tightening,
|
||||||
clippy::significant_drop_in_scrutinee,
|
clippy::significant_drop_in_scrutinee,
|
||||||
clippy::float_cmp,
|
clippy::float_cmp,
|
||||||
clippy::nursery
|
clippy::nursery
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
|
|
||||||
pub mod adaptive_buffers;
|
pub mod adaptive_buffers;
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ use crate::stats::{Stats, UserStats};
|
||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes};
|
||||||
|
|
@ -272,10 +272,12 @@ const QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES: u64 = 64 * 1024;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn quota_adaptive_interval_bytes(remaining_before: u64) -> u64 {
|
fn quota_adaptive_interval_bytes(remaining_before: u64) -> u64 {
|
||||||
remaining_before.saturating_div(2).clamp(
|
remaining_before
|
||||||
QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES,
|
.saturating_div(2)
|
||||||
QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES,
|
.clamp(
|
||||||
)
|
QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES,
|
||||||
|
QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -667,7 +669,3 @@ mod relay_quota_extended_attack_surface_security_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/relay_watchdog_delta_security_tests.rs"]
|
#[path = "tests/relay_watchdog_delta_security_tests.rs"]
|
||||||
mod relay_watchdog_delta_security_tests;
|
mod relay_watchdog_delta_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/relay_atomic_quota_invariant_tests.rs"]
|
|
||||||
mod relay_atomic_quota_invariant_tests;
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{ProxyConfig, UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType, ProxyConfig};
|
||||||
use crate::protocol::constants::{MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
use crate::protocol::constants::{MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
@ -41,9 +41,7 @@ fn edge_handshake_timeout_with_mask_grace_saturating_add_prevents_overflow() {
|
||||||
#[test]
|
#[test]
|
||||||
fn edge_tls_clienthello_len_in_bounds_exact_boundaries() {
|
fn edge_tls_clienthello_len_in_bounds_exact_boundaries() {
|
||||||
assert!(tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE));
|
assert!(tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE));
|
||||||
assert!(!tls_clienthello_len_in_bounds(
|
assert!(!tls_clienthello_len_in_bounds(MIN_TLS_CLIENT_HELLO_SIZE - 1));
|
||||||
MIN_TLS_CLIENT_HELLO_SIZE - 1
|
|
||||||
));
|
|
||||||
assert!(tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE));
|
assert!(tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE));
|
||||||
assert!(!tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE + 1));
|
assert!(!tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE + 1));
|
||||||
}
|
}
|
||||||
|
|
@ -89,15 +87,7 @@ async fn adversarial_tls_handshake_timeout_during_masking_delay() {
|
||||||
"198.51.100.1:55000".parse().unwrap(),
|
"198.51.100.1:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -109,10 +99,7 @@ async fn adversarial_tls_handshake_timeout_during_masking_delay() {
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&[0x16, 0x03, 0x01, 0xFF, 0xFF]).await.unwrap();
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0xFF, 0xFF])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(4), handle)
|
let result = tokio::time::timeout(Duration::from_secs(4), handle)
|
||||||
.await
|
.await
|
||||||
|
|
@ -136,15 +123,7 @@ async fn blackhat_proxy_protocol_slowloris_timeout() {
|
||||||
"198.51.100.2:55000".parse().unwrap(),
|
"198.51.100.2:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -188,15 +167,7 @@ async fn negative_proxy_protocol_enabled_but_client_sends_tls_hello() {
|
||||||
"198.51.100.3:55000".parse().unwrap(),
|
"198.51.100.3:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -208,10 +179,7 @@ async fn negative_proxy_protocol_enabled_but_client_sends_tls_hello() {
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&[0x16, 0x03, 0x01, 0x02, 0x00]).await.unwrap();
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x02, 0x00])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(2), handle)
|
let result = tokio::time::timeout(Duration::from_secs(2), handle)
|
||||||
.await
|
.await
|
||||||
|
|
@ -234,15 +202,7 @@ async fn edge_client_stream_exactly_4_bytes_eof() {
|
||||||
"198.51.100.4:55000".parse().unwrap(),
|
"198.51.100.4:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -254,10 +214,7 @@ async fn edge_client_stream_exactly_4_bytes_eof() {
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&[0x16, 0x03, 0x01, 0x00]).await.unwrap();
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handle).await;
|
let _ = tokio::time::timeout(Duration::from_secs(2), handle).await;
|
||||||
|
|
@ -277,15 +234,7 @@ async fn edge_client_stream_tls_header_valid_but_body_1_byte_short_eof() {
|
||||||
"198.51.100.5:55000".parse().unwrap(),
|
"198.51.100.5:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -297,10 +246,7 @@ async fn edge_client_stream_tls_header_valid_but_body_1_byte_short_eof() {
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&[0x16, 0x03, 0x01, 0x00, 100]).await.unwrap();
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00, 100])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.write_all(&vec![0x41; 99]).await.unwrap();
|
client_side.write_all(&vec![0x41; 99]).await.unwrap();
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
|
@ -323,15 +269,7 @@ async fn integration_non_tls_modes_disabled_immediately_masks() {
|
||||||
"198.51.100.6:55000".parse().unwrap(),
|
"198.51.100.6:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -434,7 +372,11 @@ async fn stress_user_connection_reservation_concurrent_same_ip_exhaustion() {
|
||||||
let ip_tracker = ip_tracker.clone();
|
let ip_tracker = ip_tracker.clone();
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
RunningClientHandler::acquire_user_connection_reservation_static(
|
RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user, &config, stats, peer, ip_tracker,
|
user,
|
||||||
|
&config,
|
||||||
|
stats,
|
||||||
|
peer,
|
||||||
|
ip_tracker,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,6 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncWriteExt, duplex};
|
use tokio::io::{AsyncWriteExt, duplex};
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invariant_wrap_tls_application_record_exact_multiples() {
|
fn invariant_wrap_tls_application_record_exact_multiples() {
|
||||||
let chunk_size = u16::MAX as usize;
|
let chunk_size = u16::MAX as usize;
|
||||||
|
|
@ -42,15 +37,7 @@ async fn invariant_tls_clienthello_truncation_exact_boundary_triggers_masking()
|
||||||
"198.51.100.20:55000".parse().unwrap(),
|
"198.51.100.20:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -73,9 +60,7 @@ async fn invariant_tls_clienthello_truncation_exact_boundary_triggers_masking()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
let _ = tokio::time::timeout(Duration::from_secs(2), handler).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
assert_eq!(stats.get_connects_bad(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,10 +68,7 @@ async fn invariant_tls_clienthello_truncation_exact_boundary_triggers_masking()
|
||||||
async fn invariant_acquire_reservation_ip_limit_rollback() {
|
async fn invariant_acquire_reservation_ip_limit_rollback() {
|
||||||
let user = "rollback-test-user";
|
let user = "rollback-test-user";
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 10);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 10);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
@ -132,7 +114,7 @@ async fn invariant_quota_exact_boundary_inclusive() {
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
let peer = "198.51.100.23:55000".parse().unwrap();
|
let peer = "198.51.100.23:55000".parse().unwrap();
|
||||||
|
|
||||||
preload_user_quota(stats.as_ref(), user, 999);
|
stats.add_user_octets_from(user, 999);
|
||||||
let res1 = RunningClientHandler::acquire_user_connection_reservation_static(
|
let res1 = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user,
|
user,
|
||||||
&config,
|
&config,
|
||||||
|
|
@ -144,7 +126,7 @@ async fn invariant_quota_exact_boundary_inclusive() {
|
||||||
assert!(res1.is_ok());
|
assert!(res1.is_ok());
|
||||||
res1.unwrap().release().await;
|
res1.unwrap().release().await;
|
||||||
|
|
||||||
preload_user_quota(stats.as_ref(), user, 1);
|
stats.add_user_octets_from(user, 1);
|
||||||
let res2 = RunningClientHandler::acquire_user_connection_reservation_static(
|
let res2 = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user,
|
user,
|
||||||
&config,
|
&config,
|
||||||
|
|
@ -172,15 +154,7 @@ async fn invariant_direct_mode_partial_header_eof_is_error_not_bad_connect() {
|
||||||
"198.51.100.25:55000".parse().unwrap(),
|
"198.51.100.25:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,14 @@ async fn run_http2_fragment_case(split_at: usize, delay_ms: u64, peer: SocketAdd
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn http2_preface_fragmentation_matrix_is_classified_and_forwarded() {
|
async fn http2_preface_fragmentation_matrix_is_classified_and_forwarded() {
|
||||||
let cases = [(2usize, 0u64), (3, 0), (4, 0), (2, 7), (3, 7), (8, 1)];
|
let cases = [
|
||||||
|
(2usize, 0u64),
|
||||||
|
(3, 0),
|
||||||
|
(4, 0),
|
||||||
|
(2, 7),
|
||||||
|
(3, 7),
|
||||||
|
(8, 1),
|
||||||
|
];
|
||||||
|
|
||||||
for (i, (split_at, delay_ms)) in cases.into_iter().enumerate() {
|
for (i, (split_at, delay_ms)) in cases.into_iter().enumerate() {
|
||||||
let peer: SocketAddr = format!("198.51.100.{}:58{}", 140 + i, 100 + i)
|
let peer: SocketAddr = format!("198.51.100.{}:58{}", 140 + i, 100 + i)
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,7 @@ async fn configured_prefetch_budget_20ms_recovers_tail_delayed_15ms() {
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
||||||
.await
|
.await
|
||||||
.expect("tail bytes must be writable");
|
.expect("tail bytes must be writable");
|
||||||
writer
|
writer.shutdown().await.expect("writer shutdown must succeed");
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
let mut initial_data = b"C".to_vec();
|
||||||
|
|
@ -63,10 +60,7 @@ async fn configured_prefetch_budget_5ms_misses_tail_delayed_15ms() {
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
||||||
.await
|
.await
|
||||||
.expect("tail bytes must be writable");
|
.expect("tail bytes must be writable");
|
||||||
writer
|
writer.shutdown().await.expect("writer shutdown must succeed");
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
let mut initial_data = b"C".to_vec();
|
||||||
|
|
|
||||||
|
|
@ -245,10 +245,7 @@ async fn blackhat_integration_empty_initial_data_path_is_byte_exact_and_eof_clea
|
||||||
assert_eq!(head[0], 0x16);
|
assert_eq!(head[0], 0x16);
|
||||||
read_and_discard_tls_record_body(&mut client_side, head).await;
|
read_and_discard_tls_record_body(&mut client_side, head).await;
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&invalid_mtproto_record).await.unwrap();
|
||||||
.write_all(&invalid_mtproto_record)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.write_all(&trailing_record).await.unwrap();
|
client_side.write_all(&trailing_record).await.unwrap();
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ async fn run_strict_prefetch_case(prefetch_ms: u64, tail_delay_ms: u64) -> Vec<u
|
||||||
|
|
||||||
let writer_task = tokio::spawn(async move {
|
let writer_task = tokio::spawn(async move {
|
||||||
sleep(Duration::from_millis(tail_delay_ms)).await;
|
sleep(Duration::from_millis(tail_delay_ms)).await;
|
||||||
let _ = writer
|
let _ = writer.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n").await;
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await;
|
|
||||||
let _ = writer.shutdown().await;
|
let _ = writer.shutdown().await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,7 @@ async fn run_prefetch_budget_case(prefetch_budget_ms: u64, delayed_tail_ms: u64)
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
||||||
.await
|
.await
|
||||||
.expect("tail bytes must be writable");
|
.expect("tail bytes must be writable");
|
||||||
writer
|
writer.shutdown().await.expect("writer shutdown must succeed");
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
let mut initial_data = b"C".to_vec();
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,9 @@ async fn run_replay_candidate_session(
|
||||||
cfg.censorship.mask_port = 1;
|
cfg.censorship.mask_port = 1;
|
||||||
cfg.censorship.mask_timing_normalization_enabled = false;
|
cfg.censorship.mask_timing_normalization_enabled = false;
|
||||||
cfg.access.ignore_time_skew = true;
|
cfg.access.ignore_time_skew = true;
|
||||||
cfg.access.users.insert(
|
cfg.access
|
||||||
"user".to_string(),
|
.users
|
||||||
"abababababababababababababababab".to_string(),
|
.insert("user".to_string(), "abababababababababababababababab".to_string());
|
||||||
);
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
let config = Arc::new(cfg);
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
@ -100,10 +99,7 @@ async fn run_replay_candidate_session(
|
||||||
|
|
||||||
if drive_mtproto_fail {
|
if drive_mtproto_fail {
|
||||||
let mut server_hello_head = [0u8; 5];
|
let mut server_hello_head = [0u8; 5];
|
||||||
client_side
|
client_side.read_exact(&mut server_hello_head).await.unwrap();
|
||||||
.read_exact(&mut server_hello_head)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(server_hello_head[0], 0x16);
|
assert_eq!(server_hello_head[0], 0x16);
|
||||||
let body_len = u16::from_be_bytes([server_hello_head[3], server_hello_head[4]]) as usize;
|
let body_len = u16::from_be_bytes([server_hello_head[3], server_hello_head[4]]) as usize;
|
||||||
let mut body = vec![0u8; body_len];
|
let mut body = vec![0u8; body_len];
|
||||||
|
|
@ -114,10 +110,7 @@ async fn run_replay_candidate_session(
|
||||||
invalid_mtproto_record.extend_from_slice(&TLS_VERSION);
|
invalid_mtproto_record.extend_from_slice(&TLS_VERSION);
|
||||||
invalid_mtproto_record.extend_from_slice(&(HANDSHAKE_LEN as u16).to_be_bytes());
|
invalid_mtproto_record.extend_from_slice(&(HANDSHAKE_LEN as u16).to_be_bytes());
|
||||||
invalid_mtproto_record.extend_from_slice(&vec![0u8; HANDSHAKE_LEN]);
|
invalid_mtproto_record.extend_from_slice(&vec![0u8; HANDSHAKE_LEN]);
|
||||||
client_side
|
client_side.write_all(&invalid_mtproto_record).await.unwrap();
|
||||||
.write_all(&invalid_mtproto_record)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side
|
client_side
|
||||||
.write_all(b"GET /replay-fallback HTTP/1.1\r\nHost: x\r\n\r\n")
|
.write_all(b"GET /replay-fallback HTTP/1.1\r\nHost: x\r\n\r\n")
|
||||||
.await
|
.await
|
||||||
|
|
@ -161,7 +154,8 @@ async fn replay_reject_still_honors_masking_timing_budget() {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
replay_elapsed >= Duration::from_millis(40) && replay_elapsed < Duration::from_millis(250),
|
replay_elapsed >= Duration::from_millis(40)
|
||||||
|
&& replay_elapsed < Duration::from_millis(250),
|
||||||
"replay rejection path must still satisfy masking timing budget without unbounded DB/CPU delay"
|
"replay rejection path must still satisfy masking timing budget without unbounded DB/CPU delay"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn edge_mask_delay_bypassed_if_max_is_zero() {
|
async fn edge_mask_delay_bypassed_if_max_is_zero() {
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
|
|
@ -47,13 +42,17 @@ async fn boundary_user_data_quota_exact_match_rejects() {
|
||||||
config.access.user_data_quota.insert(user.to_string(), 1024);
|
config.access.user_data_quota.insert(user.to_string(), 1024);
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
preload_user_quota(stats.as_ref(), user, 1024);
|
stats.add_user_octets_from(user, 1024);
|
||||||
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
let peer = "198.51.100.10:55000".parse().unwrap();
|
let peer = "198.51.100.10:55000".parse().unwrap();
|
||||||
|
|
||||||
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user, &config, stats, peer, ip_tracker,
|
user,
|
||||||
|
&config,
|
||||||
|
stats,
|
||||||
|
peer,
|
||||||
|
ip_tracker,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
@ -75,7 +74,11 @@ async fn boundary_user_expiration_in_past_rejects() {
|
||||||
let peer = "198.51.100.11:55000".parse().unwrap();
|
let peer = "198.51.100.11:55000".parse().unwrap();
|
||||||
|
|
||||||
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user, &config, stats, peer, ip_tracker,
|
user,
|
||||||
|
&config,
|
||||||
|
stats,
|
||||||
|
peer,
|
||||||
|
ip_tracker,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
@ -95,15 +98,7 @@ async fn blackhat_proxy_protocol_massive_garbage_rejected_quickly() {
|
||||||
"198.51.100.12:55000".parse().unwrap(),
|
"198.51.100.12:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -141,15 +136,7 @@ async fn edge_tls_body_immediate_eof_triggers_masking_and_bad_connect() {
|
||||||
"198.51.100.13:55000".parse().unwrap(),
|
"198.51.100.13:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -161,15 +148,10 @@ async fn edge_tls_body_immediate_eof_triggers_masking_and_bad_connect() {
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&[0x16, 0x03, 0x01, 0x00, 100]).await.unwrap();
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00, 100])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
let _ = tokio::time::timeout(Duration::from_secs(2), handler).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
assert_eq!(stats.get_connects_bad(), 1);
|
||||||
}
|
}
|
||||||
|
|
@ -190,15 +172,7 @@ async fn security_classic_mode_disabled_masks_valid_length_payload() {
|
||||||
"198.51.100.15:55000".parse().unwrap(),
|
"198.51.100.15:55000".parse().unwrap(),
|
||||||
config,
|
config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
Arc::new(UpstreamManager::new(
|
Arc::new(UpstreamManager::new(vec![], 1, 1, 1, 1, false, stats.clone())),
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
Arc::new(SecureRandom::new()),
|
Arc::new(SecureRandom::new()),
|
||||||
|
|
@ -213,9 +187,7 @@ async fn security_classic_mode_disabled_masks_valid_length_payload() {
|
||||||
client_side.write_all(&vec![0xEF; 64]).await.unwrap();
|
client_side.write_all(&vec![0xEF; 64]).await.unwrap();
|
||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
let _ = tokio::time::timeout(Duration::from_secs(2), handler).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
assert_eq!(stats.get_connects_bad(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,10 +195,7 @@ async fn security_classic_mode_disabled_masks_valid_length_payload() {
|
||||||
async fn concurrency_ip_tracker_strict_limit_one_rapid_churn() {
|
async fn concurrency_ip_tracker_strict_limit_one_rapid_churn() {
|
||||||
let user = "rapid-churn-user";
|
let user = "rapid-churn-user";
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 10);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 10);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ use crate::protocol::tls;
|
||||||
use crate::proxy::handshake::HandshakeSuccess;
|
use crate::proxy::handshake::HandshakeSuccess;
|
||||||
use crate::stream::{CryptoReader, CryptoWriter};
|
use crate::stream::{CryptoReader, CryptoWriter};
|
||||||
use crate::transport::proxy_protocol::ProxyProtocolV1Builder;
|
use crate::transport::proxy_protocol::ProxyProtocolV1Builder;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
@ -34,10 +34,7 @@ fn handshake_timeout_with_mask_grace_includes_mask_margin() {
|
||||||
config.timeouts.client_handshake = 2;
|
config.timeouts.client_handshake = 2;
|
||||||
|
|
||||||
config.censorship.mask = false;
|
config.censorship.mask = false;
|
||||||
assert_eq!(
|
assert_eq!(handshake_timeout_with_mask_grace(&config), Duration::from_secs(2));
|
||||||
handshake_timeout_with_mask_grace(&config),
|
|
||||||
Duration::from_secs(2)
|
|
||||||
);
|
|
||||||
|
|
||||||
config.censorship.mask = true;
|
config.censorship.mask = true;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -89,10 +86,7 @@ impl tokio::io::AsyncRead for ErrorReader {
|
||||||
_cx: &mut std::task::Context<'_>,
|
_cx: &mut std::task::Context<'_>,
|
||||||
_buf: &mut tokio::io::ReadBuf<'_>,
|
_buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
) -> std::task::Poll<std::io::Result<()>> {
|
) -> std::task::Poll<std::io::Result<()>> {
|
||||||
std::task::Poll::Ready(Err(std::io::Error::new(
|
std::task::Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "fake error")))
|
||||||
std::io::ErrorKind::UnexpectedEof,
|
|
||||||
"fake error",
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,10 +124,7 @@ fn handshake_timeout_without_mask_is_exact_base() {
|
||||||
config.timeouts.client_handshake = 7;
|
config.timeouts.client_handshake = 7;
|
||||||
config.censorship.mask = false;
|
config.censorship.mask = false;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(handshake_timeout_with_mask_grace(&config), Duration::from_secs(7));
|
||||||
handshake_timeout_with_mask_grace(&config),
|
|
||||||
Duration::from_secs(7)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -142,10 +133,7 @@ fn handshake_timeout_mask_enabled_adds_750ms() {
|
||||||
config.timeouts.client_handshake = 3;
|
config.timeouts.client_handshake = 3;
|
||||||
config.censorship.mask = true;
|
config.censorship.mask = true;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(handshake_timeout_with_mask_grace(&config), Duration::from_millis(3750));
|
||||||
handshake_timeout_with_mask_grace(&config),
|
|
||||||
Duration::from_millis(3750)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -167,12 +155,10 @@ async fn read_with_progress_fragmented_io_works_over_multiple_calls() {
|
||||||
let mut b = vec![0u8; chunk_size];
|
let mut b = vec![0u8; chunk_size];
|
||||||
let n = read_with_progress(&mut cursor, &mut b).await.unwrap();
|
let n = read_with_progress(&mut cursor, &mut b).await.unwrap();
|
||||||
result.extend_from_slice(&b[..n]);
|
result.extend_from_slice(&b[..n]);
|
||||||
if n == 0 {
|
if n == 0 { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(result, vec![1, 2, 3, 4, 5]);
|
assert_eq!(result, vec![1,2,3,4,5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -188,9 +174,7 @@ async fn read_with_progress_stress_randomized_chunk_sizes() {
|
||||||
let mut b = vec![0u8; chunk];
|
let mut b = vec![0u8; chunk];
|
||||||
let read = read_with_progress(&mut cursor, &mut b).await.unwrap();
|
let read = read_with_progress(&mut cursor, &mut b).await.unwrap();
|
||||||
collected.extend_from_slice(&b[..read]);
|
collected.extend_from_slice(&b[..read]);
|
||||||
if read == 0 {
|
if read == 0 { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(collected, input);
|
assert_eq!(collected, input);
|
||||||
|
|
@ -231,12 +215,10 @@ fn wrap_tls_application_record_roundtrip_size_check() {
|
||||||
let mut consumed = 0;
|
let mut consumed = 0;
|
||||||
while idx + 5 <= wrapped.len() {
|
while idx + 5 <= wrapped.len() {
|
||||||
assert_eq!(wrapped[idx], 0x17);
|
assert_eq!(wrapped[idx], 0x17);
|
||||||
let len = u16::from_be_bytes([wrapped[idx + 3], wrapped[idx + 4]]) as usize;
|
let len = u16::from_be_bytes([wrapped[idx+3], wrapped[idx+4]]) as usize;
|
||||||
consumed += len;
|
consumed += len;
|
||||||
idx += 5 + len;
|
idx += 5 + len;
|
||||||
if idx >= wrapped.len() {
|
if idx >= wrapped.len() { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(consumed, payload_len);
|
assert_eq!(consumed, payload_len);
|
||||||
|
|
@ -260,11 +242,6 @@ where
|
||||||
CryptoWriter::new(writer, AesCtr::new(&key, iv), 8 * 1024)
|
CryptoWriter::new(writer, AesCtr::new(&key, iv), 8 * 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn user_connection_reservation_drop_enqueues_cleanup_synchronously() {
|
async fn user_connection_reservation_drop_enqueues_cleanup_synchronously() {
|
||||||
let ip_tracker = Arc::new(crate::ip_tracker::UserIpTracker::new());
|
let ip_tracker = Arc::new(crate::ip_tracker::UserIpTracker::new());
|
||||||
|
|
@ -3063,7 +3040,7 @@ async fn quota_rejection_does_not_reserve_ip_or_trigger_rollback() {
|
||||||
.insert("user".to_string(), 1024);
|
.insert("user".to_string(), 1024);
|
||||||
|
|
||||||
let stats = Stats::new();
|
let stats = Stats::new();
|
||||||
preload_user_quota(&stats, "user", 1024);
|
stats.add_user_octets_from("user", 1024);
|
||||||
|
|
||||||
let ip_tracker = UserIpTracker::new();
|
let ip_tracker = UserIpTracker::new();
|
||||||
let peer_addr: SocketAddr = "203.0.113.211:50001".parse().unwrap();
|
let peer_addr: SocketAddr = "203.0.113.211:50001".parse().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -25,26 +25,13 @@ fn wrap_tls_application_record_oversized_payload_is_chunked_without_truncation()
|
||||||
let len = u16::from_be_bytes([record[offset + 3], record[offset + 4]]) as usize;
|
let len = u16::from_be_bytes([record[offset + 3], record[offset + 4]]) as usize;
|
||||||
let body_start = offset + 5;
|
let body_start = offset + 5;
|
||||||
let body_end = body_start + len;
|
let body_end = body_start + len;
|
||||||
assert!(
|
assert!(body_end <= record.len(), "declared TLS record length must be in-bounds");
|
||||||
body_end <= record.len(),
|
|
||||||
"declared TLS record length must be in-bounds"
|
|
||||||
);
|
|
||||||
recovered.extend_from_slice(&record[body_start..body_end]);
|
recovered.extend_from_slice(&record[body_start..body_end]);
|
||||||
offset = body_end;
|
offset = body_end;
|
||||||
frames += 1;
|
frames += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(offset, record.len(), "record parser must consume exact output size");
|
||||||
offset,
|
assert_eq!(frames, 2, "oversized payload should split into exactly two records");
|
||||||
record.len(),
|
assert_eq!(recovered, payload, "chunked records must preserve full payload");
|
||||||
"record parser must consume exact output size"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
frames, 2,
|
|
||||||
"oversized payload should split into exactly two records"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
recovered, payload,
|
|
||||||
"chunked records must preserve full payload"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -773,7 +773,8 @@ fn anchored_open_nix_path_writes_expected_lines() {
|
||||||
"target/telemt-unknown-dc-anchored-open-ok-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-anchored-open-ok-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
let _ = fs::remove_file(&sanitized.resolved_path);
|
let _ = fs::remove_file(&sanitized.resolved_path);
|
||||||
|
|
||||||
let mut first = open_unknown_dc_log_append_anchored(&sanitized)
|
let mut first = open_unknown_dc_log_append_anchored(&sanitized)
|
||||||
|
|
@ -786,10 +787,7 @@ fn anchored_open_nix_path_writes_expected_lines() {
|
||||||
|
|
||||||
let content =
|
let content =
|
||||||
fs::read_to_string(&sanitized.resolved_path).expect("anchored log file must be readable");
|
fs::read_to_string(&sanitized.resolved_path).expect("anchored log file must be readable");
|
||||||
let lines: Vec<&str> = content
|
let lines: Vec<&str> = content.lines().filter(|line| !line.trim().is_empty()).collect();
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(lines.len(), 2, "expected one line per anchored append call");
|
assert_eq!(lines.len(), 2, "expected one line per anchored append call");
|
||||||
assert!(
|
assert!(
|
||||||
lines.contains(&"dc_idx=31200") && lines.contains(&"dc_idx=31201"),
|
lines.contains(&"dc_idx=31200") && lines.contains(&"dc_idx=31201"),
|
||||||
|
|
@ -813,7 +811,8 @@ fn anchored_open_parallel_appends_preserve_line_integrity() {
|
||||||
"target/telemt-unknown-dc-anchored-open-parallel-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-anchored-open-parallel-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
let _ = fs::remove_file(&sanitized.resolved_path);
|
let _ = fs::remove_file(&sanitized.resolved_path);
|
||||||
|
|
||||||
let mut workers = Vec::new();
|
let mut workers = Vec::new();
|
||||||
|
|
@ -832,15 +831,8 @@ fn anchored_open_parallel_appends_preserve_line_integrity() {
|
||||||
|
|
||||||
let content =
|
let content =
|
||||||
fs::read_to_string(&sanitized.resolved_path).expect("parallel log file must be readable");
|
fs::read_to_string(&sanitized.resolved_path).expect("parallel log file must be readable");
|
||||||
let lines: Vec<&str> = content
|
let lines: Vec<&str> = content.lines().filter(|line| !line.trim().is_empty()).collect();
|
||||||
.lines()
|
assert_eq!(lines.len(), 64, "expected one complete line per worker append");
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(
|
|
||||||
lines.len(),
|
|
||||||
64,
|
|
||||||
"expected one complete line per worker append"
|
|
||||||
);
|
|
||||||
for line in lines {
|
for line in lines {
|
||||||
assert!(
|
assert!(
|
||||||
line.starts_with("dc_idx="),
|
line.starts_with("dc_idx="),
|
||||||
|
|
@ -875,7 +867,8 @@ fn anchored_open_creates_private_0600_file_permissions() {
|
||||||
"target/telemt-unknown-dc-anchored-perms-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-anchored-perms-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
let _ = fs::remove_file(&sanitized.resolved_path);
|
let _ = fs::remove_file(&sanitized.resolved_path);
|
||||||
|
|
||||||
let mut file = open_unknown_dc_log_append_anchored(&sanitized)
|
let mut file = open_unknown_dc_log_append_anchored(&sanitized)
|
||||||
|
|
@ -912,7 +905,8 @@ fn anchored_open_rejects_existing_symlink_target() {
|
||||||
"target/telemt-unknown-dc-anchored-symlink-target-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-anchored-symlink-target-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
|
|
||||||
let outside = std::env::temp_dir().join(format!(
|
let outside = std::env::temp_dir().join(format!(
|
||||||
"telemt-unknown-dc-anchored-symlink-outside-{}.log",
|
"telemt-unknown-dc-anchored-symlink-outside-{}.log",
|
||||||
|
|
@ -949,7 +943,8 @@ fn anchored_open_high_contention_multi_write_preserves_complete_lines() {
|
||||||
"target/telemt-unknown-dc-anchored-contention-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-anchored-contention-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
let _ = fs::remove_file(&sanitized.resolved_path);
|
let _ = fs::remove_file(&sanitized.resolved_path);
|
||||||
|
|
||||||
let workers = 24usize;
|
let workers = 24usize;
|
||||||
|
|
@ -975,10 +970,7 @@ fn anchored_open_high_contention_multi_write_preserves_complete_lines() {
|
||||||
|
|
||||||
let content = fs::read_to_string(&sanitized.resolved_path)
|
let content = fs::read_to_string(&sanitized.resolved_path)
|
||||||
.expect("contention output file must be readable");
|
.expect("contention output file must be readable");
|
||||||
let lines: Vec<&str> = content
|
let lines: Vec<&str> = content.lines().filter(|line| !line.trim().is_empty()).collect();
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
lines.len(),
|
lines.len(),
|
||||||
workers * rounds,
|
workers * rounds,
|
||||||
|
|
@ -1022,7 +1014,8 @@ fn append_unknown_dc_line_returns_error_for_read_only_descriptor() {
|
||||||
"target/telemt-unknown-dc-append-ro-{}/unknown-dc.log",
|
"target/telemt-unknown-dc-append-ro-{}/unknown-dc.log",
|
||||||
std::process::id()
|
std::process::id()
|
||||||
);
|
);
|
||||||
let sanitized = sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
let sanitized =
|
||||||
|
sanitize_unknown_dc_log_path(&rel_candidate).expect("candidate must sanitize");
|
||||||
fs::write(&sanitized.resolved_path, "seed\n").expect("seed file must be writable");
|
fs::write(&sanitized.resolved_path, "seed\n").expect("seed file must be writable");
|
||||||
|
|
||||||
let mut readonly = std::fs::OpenOptions::new()
|
let mut readonly = std::fs::OpenOptions::new()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::{AesCtr, sha256, sha256_hmac};
|
use crate::crypto::{sha256, sha256_hmac, AesCtr};
|
||||||
use crate::protocol::constants::{ProtoTag, RESERVED_NONCE_BEGINNINGS, RESERVED_NONCE_FIRST_BYTES};
|
use crate::protocol::constants::{ProtoTag, RESERVED_NONCE_BEGINNINGS, RESERVED_NONCE_FIRST_BYTES};
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -175,10 +175,7 @@ async fn tls_minimum_viable_length_boundary() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::Success(_)), "Exact minimum length TLS handshake must succeed");
|
||||||
matches!(res, HandshakeResult::Success(_)),
|
|
||||||
"Exact minimum length TLS handshake must succeed"
|
|
||||||
);
|
|
||||||
|
|
||||||
let short_handshake = vec![0x42u8; min_len - 1];
|
let short_handshake = vec![0x42u8; min_len - 1];
|
||||||
let res_short = handle_tls_handshake(
|
let res_short = handle_tls_handshake(
|
||||||
|
|
@ -192,10 +189,7 @@ async fn tls_minimum_viable_length_boundary() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res_short, HandshakeResult::BadClient { .. }), "Handshake 1 byte shorter than minimum must fail closed");
|
||||||
matches!(res_short, HandshakeResult::BadClient { .. }),
|
|
||||||
"Handshake 1 byte shorter than minimum must fail closed"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -225,16 +219,9 @@ async fn mtproto_extreme_dc_index_serialization() {
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
HandshakeResult::Success((_, _, success)) => {
|
HandshakeResult::Success((_, _, success)) => {
|
||||||
assert_eq!(
|
assert_eq!(success.dc_idx, extreme_dc, "Extreme DC index {} must serialize/deserialize perfectly", extreme_dc);
|
||||||
success.dc_idx, extreme_dc,
|
|
||||||
"Extreme DC index {} must serialize/deserialize perfectly",
|
|
||||||
extreme_dc
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => panic!(
|
_ => panic!("MTProto handshake with extreme DC index {} failed", extreme_dc),
|
||||||
"MTProto handshake with extreme DC index {} failed",
|
|
||||||
extreme_dc
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -266,11 +253,7 @@ async fn alpn_strict_case_and_padding_rejection() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::BadClient { .. }), "ALPN strict enforcement must reject {:?}", bad_alpn);
|
||||||
matches!(res, HandshakeResult::BadClient { .. }),
|
|
||||||
"ALPN strict enforcement must reject {:?}",
|
|
||||||
bad_alpn
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,15 +265,8 @@ fn ipv4_mapped_ipv6_bucketing_anomaly() {
|
||||||
let norm_1 = normalize_auth_probe_ip(ipv4_mapped_1);
|
let norm_1 = normalize_auth_probe_ip(ipv4_mapped_1);
|
||||||
let norm_2 = normalize_auth_probe_ip(ipv4_mapped_2);
|
let norm_2 = normalize_auth_probe_ip(ipv4_mapped_2);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(norm_1, norm_2, "IPv4-mapped IPv6 addresses must collapse into the same /64 bucket (::0)");
|
||||||
norm_1, norm_2,
|
assert_eq!(norm_1, IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), "The bucket must be exactly ::0");
|
||||||
"IPv4-mapped IPv6 addresses must collapse into the same /64 bucket (::0)"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
norm_1,
|
|
||||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)),
|
|
||||||
"The bucket must be exactly ::0"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Category 2: Adversarial & Black Hat ---
|
// --- Category 2: Adversarial & Black Hat ---
|
||||||
|
|
@ -333,10 +309,7 @@ async fn mtproto_invalid_ciphertext_does_not_poison_replay_cache() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res_valid, HandshakeResult::Success(_)), "Invalid MTProto ciphertext must not poison the replay cache");
|
||||||
matches!(res_valid, HandshakeResult::Success(_)),
|
|
||||||
"Invalid MTProto ciphertext must not poison the replay cache"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -379,10 +352,7 @@ async fn tls_invalid_session_does_not_poison_replay_cache() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res_valid, HandshakeResult::Success(_)), "Invalid TLS payload must not poison the replay cache");
|
||||||
matches!(res_valid, HandshakeResult::Success(_)),
|
|
||||||
"Invalid TLS payload must not poison the replay cache"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -417,10 +387,7 @@ async fn server_hello_delay_timing_neutrality_on_hmac_failure() {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
assert!(matches!(res, HandshakeResult::BadClient { .. }));
|
assert!(matches!(res, HandshakeResult::BadClient { .. }));
|
||||||
assert!(
|
assert!(elapsed >= Duration::from_millis(45), "Invalid HMAC must still incur the configured ServerHello delay to prevent timing side-channels");
|
||||||
elapsed >= Duration::from_millis(45),
|
|
||||||
"Invalid HMAC must still incur the configured ServerHello delay to prevent timing side-channels"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -454,10 +421,7 @@ async fn server_hello_delay_inversion_resilience() {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
assert!(matches!(res, HandshakeResult::Success(_)));
|
assert!(matches!(res, HandshakeResult::Success(_)));
|
||||||
assert!(
|
assert!(elapsed >= Duration::from_millis(90), "Delay logic must gracefully handle min > max inversions via max.max(min)");
|
||||||
elapsed >= Duration::from_millis(90),
|
|
||||||
"Delay logic must gracefully handle min > max inversions via max.max(min)"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -472,16 +436,10 @@ async fn mixed_valid_and_invalid_user_secrets_configuration() {
|
||||||
|
|
||||||
for i in 0..9 {
|
for i in 0..9 {
|
||||||
let bad_secret = if i % 2 == 0 { "badhex!" } else { "1122" };
|
let bad_secret = if i % 2 == 0 { "badhex!" } else { "1122" };
|
||||||
config
|
config.access.users.insert(format!("bad_user_{}", i), bad_secret.to_string());
|
||||||
.access
|
|
||||||
.users
|
|
||||||
.insert(format!("bad_user_{}", i), bad_secret.to_string());
|
|
||||||
}
|
}
|
||||||
let valid_secret_hex = "99999999999999999999999999999999";
|
let valid_secret_hex = "99999999999999999999999999999999";
|
||||||
config
|
config.access.users.insert("good_user".to_string(), valid_secret_hex.to_string());
|
||||||
.access
|
|
||||||
.users
|
|
||||||
.insert("good_user".to_string(), valid_secret_hex.to_string());
|
|
||||||
config.general.modes.secure = true;
|
config.general.modes.secure = true;
|
||||||
config.general.modes.classic = true;
|
config.general.modes.classic = true;
|
||||||
config.general.modes.tls = true;
|
config.general.modes.tls = true;
|
||||||
|
|
@ -505,10 +463,7 @@ async fn mixed_valid_and_invalid_user_secrets_configuration() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::Success(_)), "Proxy must gracefully skip invalid secrets and authenticate the valid one");
|
||||||
matches!(res, HandshakeResult::Success(_)),
|
|
||||||
"Proxy must gracefully skip invalid secrets and authenticate the valid one"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -539,10 +494,7 @@ async fn tls_emulation_fallback_when_cache_missing() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::Success(_)), "TLS emulation must gracefully fall back to standard ServerHello if cache is missing");
|
||||||
matches!(res, HandshakeResult::Success(_)),
|
|
||||||
"TLS emulation must gracefully fall back to standard ServerHello if cache is missing"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -572,10 +524,7 @@ async fn classic_mode_over_tls_transport_protocol_confusion() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::Success(_)), "Intermediate tag over TLS must succeed if classic mode is enabled, locking in cross-transport behavior");
|
||||||
matches!(res, HandshakeResult::Success(_)),
|
|
||||||
"Intermediate tag over TLS must succeed if classic mode is enabled, locking in cross-transport behavior"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -594,15 +543,9 @@ fn generate_tg_nonce_never_emits_reserved_bytes() {
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(!RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]), "Nonce must never start with reserved bytes");
|
||||||
!RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]),
|
|
||||||
"Nonce must never start with reserved bytes"
|
|
||||||
);
|
|
||||||
let first_four: [u8; 4] = [nonce[0], nonce[1], nonce[2], nonce[3]];
|
let first_four: [u8; 4] = [nonce[0], nonce[1], nonce[2], nonce[3]];
|
||||||
assert!(
|
assert!(!RESERVED_NONCE_BEGINNINGS.contains(&first_four), "Nonce must never match reserved 4-byte beginnings");
|
||||||
!RESERVED_NONCE_BEGINNINGS.contains(&first_four),
|
|
||||||
"Nonce must never match reserved 4-byte beginnings"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -625,18 +568,11 @@ async fn dashmap_concurrent_saturation_stress() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
task.await
|
task.await.expect("Task panicked during concurrent DashMap stress");
|
||||||
.expect("Task panicked during concurrent DashMap stress");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(auth_probe_is_throttled_for_testing(ip_a), "IP A must be throttled after concurrent stress");
|
||||||
auth_probe_is_throttled_for_testing(ip_a),
|
assert!(auth_probe_is_throttled_for_testing(ip_b), "IP B must be throttled after concurrent stress");
|
||||||
"IP A must be throttled after concurrent stress"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
auth_probe_is_throttled_for_testing(ip_b),
|
|
||||||
"IP B must be throttled after concurrent stress"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -650,12 +586,7 @@ fn prototag_invalid_bytes_fail_closed() {
|
||||||
];
|
];
|
||||||
|
|
||||||
for tag in invalid_tags {
|
for tag in invalid_tags {
|
||||||
assert_eq!(
|
assert_eq!(ProtoTag::from_bytes(tag), None, "Invalid ProtoTag bytes {:?} must fail closed", tag);
|
||||||
ProtoTag::from_bytes(tag),
|
|
||||||
None,
|
|
||||||
"Invalid ProtoTag bytes {:?} must fail closed",
|
|
||||||
tag
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,10 +603,7 @@ fn auth_probe_eviction_hash_collision_stress() {
|
||||||
auth_probe_record_failure_with_state(state, ip, now);
|
auth_probe_record_failure_with_state(state, ip, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(state.len() <= AUTH_PROBE_TRACK_MAX_ENTRIES, "Eviction logic must successfully bound the map size under heavy insertion stress");
|
||||||
state.len() <= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
|
||||||
"Eviction logic must successfully bound the map size under heavy insertion stress"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,6 @@ fn light_fuzz_offset_always_stays_inside_state_len() {
|
||||||
let now = base + Duration::from_nanos(seed & 0x0fff);
|
let now = base + Duration::from_nanos(seed & 0x0fff);
|
||||||
let start = auth_probe_scan_start_offset(ip, now, state_len, scan_limit);
|
let start = auth_probe_scan_start_offset(ip, now, state_len, scan_limit);
|
||||||
|
|
||||||
assert!(
|
assert!(start < state_len, "scan offset must stay inside state length");
|
||||||
start < state_len,
|
|
||||||
"scan offset must stay inside state length"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::{AesCtr, sha256, sha256_hmac};
|
use crate::crypto::{sha256, sha256_hmac, AesCtr};
|
||||||
use crate::protocol::constants::{ProtoTag, RESERVED_NONCE_BEGINNINGS, RESERVED_NONCE_FIRST_BYTES};
|
use crate::protocol::constants::{ProtoTag, RESERVED_NONCE_BEGINNINGS, RESERVED_NONCE_FIRST_BYTES};
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rand::rngs::StdRng;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -223,10 +223,7 @@ fn auth_probe_backoff_extreme_fail_streak_clamps_safely() {
|
||||||
assert_eq!(updated.fail_streak, u32::MAX);
|
assert_eq!(updated.fail_streak, u32::MAX);
|
||||||
|
|
||||||
let expected_blocked_until = now + Duration::from_millis(AUTH_PROBE_BACKOFF_MAX_MS);
|
let expected_blocked_until = now + Duration::from_millis(AUTH_PROBE_BACKOFF_MAX_MS);
|
||||||
assert_eq!(
|
assert_eq!(updated.blocked_until, expected_blocked_until, "Extreme fail streak must clamp cleanly to AUTH_PROBE_BACKOFF_MAX_MS");
|
||||||
updated.blocked_until, expected_blocked_until,
|
|
||||||
"Extreme fail streak must clamp cleanly to AUTH_PROBE_BACKOFF_MAX_MS"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -253,19 +250,12 @@ fn generate_tg_nonce_cryptographic_uniqueness_and_entropy() {
|
||||||
total_set_bits += byte.count_ones() as usize;
|
total_set_bits += byte.count_ones() as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(nonces.insert(nonce), "generate_tg_nonce emitted a duplicate nonce! RNG is stuck.");
|
||||||
nonces.insert(nonce),
|
|
||||||
"generate_tg_nonce emitted a duplicate nonce! RNG is stuck."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_bits = iterations * HANDSHAKE_LEN * 8;
|
let total_bits = iterations * HANDSHAKE_LEN * 8;
|
||||||
let ratio = (total_set_bits as f64) / (total_bits as f64);
|
let ratio = (total_set_bits as f64) / (total_bits as f64);
|
||||||
assert!(
|
assert!(ratio > 0.48 && ratio < 0.52, "Nonce entropy is degraded. Set bit ratio: {}", ratio);
|
||||||
ratio > 0.48 && ratio < 0.52,
|
|
||||||
"Nonce entropy is degraded. Set bit ratio: {}",
|
|
||||||
ratio
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -277,19 +267,10 @@ async fn mtproto_multi_user_decryption_isolation() {
|
||||||
config.general.modes.secure = true;
|
config.general.modes.secure = true;
|
||||||
config.access.ignore_time_skew = true;
|
config.access.ignore_time_skew = true;
|
||||||
|
|
||||||
config.access.users.insert(
|
config.access.users.insert("user_a".to_string(), "11111111111111111111111111111111".to_string());
|
||||||
"user_a".to_string(),
|
config.access.users.insert("user_b".to_string(), "22222222222222222222222222222222".to_string());
|
||||||
"11111111111111111111111111111111".to_string(),
|
|
||||||
);
|
|
||||||
config.access.users.insert(
|
|
||||||
"user_b".to_string(),
|
|
||||||
"22222222222222222222222222222222".to_string(),
|
|
||||||
);
|
|
||||||
let good_secret_hex = "33333333333333333333333333333333";
|
let good_secret_hex = "33333333333333333333333333333333";
|
||||||
config
|
config.access.users.insert("user_c".to_string(), good_secret_hex.to_string());
|
||||||
.access
|
|
||||||
.users
|
|
||||||
.insert("user_c".to_string(), good_secret_hex.to_string());
|
|
||||||
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
let peer: SocketAddr = "192.0.2.104:12345".parse().unwrap();
|
let peer: SocketAddr = "192.0.2.104:12345".parse().unwrap();
|
||||||
|
|
@ -310,14 +291,9 @@ async fn mtproto_multi_user_decryption_isolation() {
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
HandshakeResult::Success((_, _, success)) => {
|
HandshakeResult::Success((_, _, success)) => {
|
||||||
assert_eq!(
|
assert_eq!(success.user, "user_c", "Decryption attempts on previous users must not corrupt the handshake buffer for the valid user");
|
||||||
success.user, "user_c",
|
|
||||||
"Decryption attempts on previous users must not corrupt the handshake buffer for the valid user"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
_ => panic!(
|
_ => panic!("Multi-user MTProto handshake failed. Decryption buffer might be mutating in place."),
|
||||||
"Multi-user MTProto handshake failed. Decryption buffer might be mutating in place."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,9 +325,7 @@ async fn invalid_secret_warning_lock_contention_and_bound() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let warned = INVALID_SECRET_WARNED.get().unwrap();
|
let warned = INVALID_SECRET_WARNED.get().unwrap();
|
||||||
let guard = warned
|
let guard = warned.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
guard.len(),
|
guard.len(),
|
||||||
|
|
@ -368,11 +342,7 @@ async fn mtproto_strict_concurrent_replay_race_condition() {
|
||||||
let secret_hex = "4A4A4A4A4A4A4A4A4A4A4A4A4A4A4A4A";
|
let secret_hex = "4A4A4A4A4A4A4A4A4A4A4A4A4A4A4A4A";
|
||||||
let config = Arc::new(test_config_with_secret_hex(secret_hex));
|
let config = Arc::new(test_config_with_secret_hex(secret_hex));
|
||||||
let replay_checker = Arc::new(ReplayChecker::new(4096, Duration::from_secs(60)));
|
let replay_checker = Arc::new(ReplayChecker::new(4096, Duration::from_secs(60)));
|
||||||
let valid_handshake = Arc::new(make_valid_mtproto_handshake(
|
let valid_handshake = Arc::new(make_valid_mtproto_handshake(secret_hex, ProtoTag::Secure, 1));
|
||||||
secret_hex,
|
|
||||||
ProtoTag::Secure,
|
|
||||||
1,
|
|
||||||
));
|
|
||||||
|
|
||||||
let tasks = 100;
|
let tasks = 100;
|
||||||
let barrier = Arc::new(Barrier::new(tasks));
|
let barrier = Arc::new(Barrier::new(tasks));
|
||||||
|
|
@ -385,10 +355,7 @@ async fn mtproto_strict_concurrent_replay_race_condition() {
|
||||||
let hs = valid_handshake.clone();
|
let hs = valid_handshake.clone();
|
||||||
|
|
||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
let peer = SocketAddr::new(
|
let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, (i % 250) as u8)), 10000 + i as u16);
|
||||||
IpAddr::V4(Ipv4Addr::new(10, 0, 0, (i % 250) as u8)),
|
|
||||||
10000 + i as u16,
|
|
||||||
);
|
|
||||||
b.wait().await;
|
b.wait().await;
|
||||||
handle_mtproto_handshake(
|
handle_mtproto_handshake(
|
||||||
&hs,
|
&hs,
|
||||||
|
|
@ -415,15 +382,8 @@ async fn mtproto_strict_concurrent_replay_race_condition() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(successes, 1, "Replay cache race condition allowed multiple identical MTProto handshakes to succeed");
|
||||||
successes, 1,
|
assert_eq!(failures, tasks - 1, "Replay cache failed to forcefully reject concurrent duplicates");
|
||||||
"Replay cache race condition allowed multiple identical MTProto handshakes to succeed"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
failures,
|
|
||||||
tasks - 1,
|
|
||||||
"Replay cache failed to forcefully reject concurrent duplicates"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -438,8 +398,7 @@ async fn tls_alpn_zero_length_protocol_handled_safely() {
|
||||||
let rng = SecureRandom::new();
|
let rng = SecureRandom::new();
|
||||||
let peer: SocketAddr = "192.0.2.107:12345".parse().unwrap();
|
let peer: SocketAddr = "192.0.2.107:12345".parse().unwrap();
|
||||||
|
|
||||||
let handshake =
|
let handshake = make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "example.com", &[b""]);
|
||||||
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "example.com", &[b""]);
|
|
||||||
|
|
||||||
let res = handle_tls_handshake(
|
let res = handle_tls_handshake(
|
||||||
&handshake,
|
&handshake,
|
||||||
|
|
@ -453,10 +412,7 @@ async fn tls_alpn_zero_length_protocol_handled_safely() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::BadClient { .. }), "0-length ALPN must be safely rejected without panicking");
|
||||||
matches!(res, HandshakeResult::BadClient { .. }),
|
|
||||||
"0-length ALPN must be safely rejected without panicking"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -471,8 +427,7 @@ async fn tls_sni_massive_hostname_does_not_panic() {
|
||||||
let peer: SocketAddr = "192.0.2.108:12345".parse().unwrap();
|
let peer: SocketAddr = "192.0.2.108:12345".parse().unwrap();
|
||||||
|
|
||||||
let massive_hostname = String::from_utf8(vec![b'a'; 65000]).unwrap();
|
let massive_hostname = String::from_utf8(vec![b'a'; 65000]).unwrap();
|
||||||
let handshake =
|
let handshake = make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, &massive_hostname, &[]);
|
||||||
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, &massive_hostname, &[]);
|
|
||||||
|
|
||||||
let res = handle_tls_handshake(
|
let res = handle_tls_handshake(
|
||||||
&handshake,
|
&handshake,
|
||||||
|
|
@ -486,13 +441,7 @@ async fn tls_sni_massive_hostname_does_not_panic() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::Success(_) | HandshakeResult::BadClient { .. }), "Massive SNI hostname must be processed or ignored without stack overflow or panic");
|
||||||
matches!(
|
|
||||||
res,
|
|
||||||
HandshakeResult::Success(_) | HandshakeResult::BadClient { .. }
|
|
||||||
),
|
|
||||||
"Massive SNI hostname must be processed or ignored without stack overflow or panic"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -506,8 +455,7 @@ async fn tls_progressive_truncation_fuzzing_no_panics() {
|
||||||
let rng = SecureRandom::new();
|
let rng = SecureRandom::new();
|
||||||
let peer: SocketAddr = "192.0.2.109:12345".parse().unwrap();
|
let peer: SocketAddr = "192.0.2.109:12345".parse().unwrap();
|
||||||
|
|
||||||
let valid_handshake =
|
let valid_handshake = make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "example.com", &[b"h2"]);
|
||||||
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "example.com", &[b"h2"]);
|
|
||||||
let full_len = valid_handshake.len();
|
let full_len = valid_handshake.len();
|
||||||
|
|
||||||
// Truncated corpus only: full_len is a valid baseline and should not be
|
// Truncated corpus only: full_len is a valid baseline and should not be
|
||||||
|
|
@ -525,11 +473,7 @@ async fn tls_progressive_truncation_fuzzing_no_panics() {
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::BadClient { .. }), "Truncated TLS handshake at len {} must fail safely without panicking", i);
|
||||||
matches!(res, HandshakeResult::BadClient { .. }),
|
|
||||||
"Truncated TLS handshake at len {} must fail safely without panicking",
|
|
||||||
i
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -560,10 +504,7 @@ async fn mtproto_pure_entropy_fuzzing_no_panics() {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(matches!(res, HandshakeResult::BadClient { .. }), "Pure entropy MTProto payload must fail closed and never panic");
|
||||||
matches!(res, HandshakeResult::BadClient { .. }),
|
|
||||||
"Pure entropy MTProto payload must fail closed and never panic"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -576,16 +517,10 @@ fn decode_user_secret_odd_length_hex_rejection() {
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config.access.users.clear();
|
config.access.users.clear();
|
||||||
config.access.users.insert(
|
config.access.users.insert("odd_user".to_string(), "1234567890123456789012345678901".to_string());
|
||||||
"odd_user".to_string(),
|
|
||||||
"1234567890123456789012345678901".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let decoded = decode_user_secrets(&config, None);
|
let decoded = decode_user_secrets(&config, None);
|
||||||
assert!(
|
assert!(decoded.is_empty(), "Odd-length hex string must be gracefully rejected by hex::decode without unwrapping");
|
||||||
decoded.is_empty(),
|
|
||||||
"Odd-length hex string must be gracefully rejected by hex::decode without unwrapping"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -617,10 +552,7 @@ fn saturation_grace_pre_existing_high_fail_streak_immediate_throttle() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_throttled = auth_probe_should_apply_preauth_throttle(peer_ip, now);
|
let is_throttled = auth_probe_should_apply_preauth_throttle(peer_ip, now);
|
||||||
assert!(
|
assert!(is_throttled, "A peer with a pre-existing high fail streak must be immediately throttled when saturation begins, receiving no unearned grace period");
|
||||||
is_throttled,
|
|
||||||
"A peer with a pre-existing high fail streak must be immediately throttled when saturation begins, receiving no unearned grace period"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -654,11 +586,7 @@ fn mtproto_classic_tags_rejected_when_only_secure_mode_enabled() {
|
||||||
config.general.modes.tls = false;
|
config.general.modes.tls = false;
|
||||||
|
|
||||||
assert!(!mode_enabled_for_proto(&config, ProtoTag::Abridged, false));
|
assert!(!mode_enabled_for_proto(&config, ProtoTag::Abridged, false));
|
||||||
assert!(!mode_enabled_for_proto(
|
assert!(!mode_enabled_for_proto(&config, ProtoTag::Intermediate, false));
|
||||||
&config,
|
|
||||||
ProtoTag::Intermediate,
|
|
||||||
false
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::{AesCtr, SecureRandom, sha256, sha256_hmac};
|
use crate::crypto::{sha256, sha256_hmac, AesCtr, SecureRandom};
|
||||||
use crate::protocol::constants::{ProtoTag, TLS_RECORD_HANDSHAKE, TLS_VERSION};
|
use crate::protocol::constants::{ProtoTag, TLS_RECORD_HANDSHAKE, TLS_VERSION};
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -80,7 +80,8 @@ fn make_valid_tls_client_hello_with_alpn(
|
||||||
digest[28 + i] ^= ts[i];
|
digest[28 + i] ^= ts[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
record[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN].copy_from_slice(&digest);
|
record[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&digest);
|
||||||
|
|
||||||
record
|
record
|
||||||
}
|
}
|
||||||
|
|
@ -330,11 +331,7 @@ async fn saturation_grace_exhaustion_under_concurrency_keeps_peer_throttled() {
|
||||||
|
|
||||||
let final_state = state.get(&peer_ip).expect("state must exist");
|
let final_state = state.get(&peer_ip).expect("state must exist");
|
||||||
assert!(
|
assert!(
|
||||||
final_state.fail_streak
|
final_state.fail_streak >= AUTH_PROBE_BACKOFF_START_FAILS + AUTH_PROBE_SATURATION_GRACE_FAILS
|
||||||
>= AUTH_PROBE_BACKOFF_START_FAILS + AUTH_PROBE_SATURATION_GRACE_FAILS
|
|
||||||
);
|
);
|
||||||
assert!(auth_probe_should_apply_preauth_throttle(
|
assert!(auth_probe_should_apply_preauth_throttle(peer_ip, Instant::now()));
|
||||||
peer_ip,
|
|
||||||
Instant::now()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -956,89 +956,6 @@ async fn stress_tls_sni_preferred_user_hint_scales_to_large_user_set() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tls_unknown_sni_drop_policy_returns_hard_error() {
|
|
||||||
let secret = [0x48u8; 16];
|
|
||||||
let mut config = test_config_with_secret_hex("48484848484848484848484848484848");
|
|
||||||
config.censorship.unknown_sni_action = UnknownSniAction::Drop;
|
|
||||||
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let rng = SecureRandom::new();
|
|
||||||
let peer: SocketAddr = "198.51.100.190:44326".parse().unwrap();
|
|
||||||
let handshake =
|
|
||||||
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
|
|
||||||
|
|
||||||
let result = handle_tls_handshake(
|
|
||||||
&handshake,
|
|
||||||
tokio::io::empty(),
|
|
||||||
tokio::io::sink(),
|
|
||||||
peer,
|
|
||||||
&config,
|
|
||||||
&replay_checker,
|
|
||||||
&rng,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
result,
|
|
||||||
HandshakeResult::Error(ProxyError::UnknownTlsSni)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tls_unknown_sni_mask_policy_falls_back_to_bad_client() {
|
|
||||||
let secret = [0x49u8; 16];
|
|
||||||
let mut config = test_config_with_secret_hex("49494949494949494949494949494949");
|
|
||||||
config.censorship.unknown_sni_action = UnknownSniAction::Mask;
|
|
||||||
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let rng = SecureRandom::new();
|
|
||||||
let peer: SocketAddr = "198.51.100.191:44326".parse().unwrap();
|
|
||||||
let handshake =
|
|
||||||
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
|
|
||||||
|
|
||||||
let result = handle_tls_handshake(
|
|
||||||
&handshake,
|
|
||||||
tokio::io::empty(),
|
|
||||||
tokio::io::sink(),
|
|
||||||
peer,
|
|
||||||
&config,
|
|
||||||
&replay_checker,
|
|
||||||
&rng,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(result, HandshakeResult::BadClient { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tls_missing_sni_keeps_legacy_auth_path() {
|
|
||||||
let secret = [0x4Au8; 16];
|
|
||||||
let mut config = test_config_with_secret_hex("4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a");
|
|
||||||
config.censorship.unknown_sni_action = UnknownSniAction::Drop;
|
|
||||||
|
|
||||||
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
|
||||||
let rng = SecureRandom::new();
|
|
||||||
let peer: SocketAddr = "198.51.100.192:44326".parse().unwrap();
|
|
||||||
let handshake = make_valid_tls_handshake(&secret, 0);
|
|
||||||
|
|
||||||
let result = handle_tls_handshake(
|
|
||||||
&handshake,
|
|
||||||
tokio::io::empty(),
|
|
||||||
tokio::io::sink(),
|
|
||||||
peer,
|
|
||||||
&config,
|
|
||||||
&replay_checker,
|
|
||||||
&rng,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(result, HandshakeResult::Success(_)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
||||||
let secret = [0x33u8; 16];
|
let secret = [0x33u8; 16];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::{AesCtr, SecureRandom, sha256, sha256_hmac};
|
use crate::crypto::{sha256, sha256_hmac, AesCtr, SecureRandom};
|
||||||
use crate::protocol::constants::{ProtoTag, TLS_RECORD_HANDSHAKE, TLS_VERSION};
|
use crate::protocol::constants::{ProtoTag, TLS_RECORD_HANDSHAKE, TLS_VERSION};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
@ -169,10 +169,10 @@ async fn mtproto_user_scan_timing_manual_benchmark() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
config.access.users.insert(
|
||||||
.access
|
preferred_user.to_string(),
|
||||||
.users
|
target_secret_hex.to_string(),
|
||||||
.insert(preferred_user.to_string(), target_secret_hex.to_string());
|
);
|
||||||
|
|
||||||
let replay_checker_preferred = ReplayChecker::new(65_536, Duration::from_secs(60));
|
let replay_checker_preferred = ReplayChecker::new(65_536, Duration::from_secs(60));
|
||||||
let replay_checker_full_scan = ReplayChecker::new(65_536, Duration::from_secs(60));
|
let replay_checker_full_scan = ReplayChecker::new(65_536, Duration::from_secs(60));
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,7 @@ async fn timing_classifier_light_fuzz_pairwise_bucketed_accuracy_stays_bounded_u
|
||||||
if hardened_acc + 0.05 <= baseline_acc {
|
if hardened_acc + 0.05 <= baseline_acc {
|
||||||
meaningful_improvement_seen = true;
|
meaningful_improvement_seen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -78,11 +78,7 @@ fn timing_normalization_zero_floor_safety_net_defaults_to_mask_timeout() {
|
||||||
config.censorship.mask_timing_normalization_ceiling_ms = 0;
|
config.censorship.mask_timing_normalization_ceiling_ms = 0;
|
||||||
|
|
||||||
let budget = mask_outcome_target_budget(&config);
|
let budget = mask_outcome_target_budget(&config);
|
||||||
assert_eq!(
|
assert_eq!(budget, MASK_TIMEOUT);
|
||||||
budget,
|
|
||||||
Duration::from_millis(0),
|
|
||||||
"zero floor/ceiling must produce zero extra normalization budget"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,7 @@ async fn aggressive_mode_shapes_backend_silent_non_eof_path() {
|
||||||
let legacy = capture_forwarded_len_with_mode(body_sent, false, false, false, 0).await;
|
let legacy = capture_forwarded_len_with_mode(body_sent, false, false, false, 0).await;
|
||||||
let aggressive = capture_forwarded_len_with_mode(body_sent, false, true, false, 0).await;
|
let aggressive = capture_forwarded_len_with_mode(body_sent, false, true, false, 0).await;
|
||||||
|
|
||||||
assert!(
|
assert!(legacy < floor, "legacy mode should keep timeout path unshaped");
|
||||||
legacy < floor,
|
|
||||||
"legacy mode should keep timeout path unshaped"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
aggressive >= floor,
|
aggressive >= floor,
|
||||||
"aggressive mode must shape backend-silent non-EOF paths (aggressive={aggressive}, floor={floor})"
|
"aggressive mode must shape backend-silent non-EOF paths (aggressive={aggressive}, floor={floor})"
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,7 @@ async fn run_connect_failure_case(
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(n, 0, "connect-failure path must close client-visible writer");
|
||||||
n, 0,
|
|
||||||
"connect-failure path must close client-visible writer"
|
|
||||||
);
|
|
||||||
|
|
||||||
started.elapsed()
|
started.elapsed()
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +67,13 @@ async fn connect_failure_refusal_close_behavior_matrix() {
|
||||||
let peer: SocketAddr = format!("203.0.113.210:{}", 54100 + idx as u16)
|
let peer: SocketAddr = format!("203.0.113.210:{}", 54100 + idx as u16)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let elapsed =
|
let elapsed = run_connect_failure_case(
|
||||||
run_connect_failure_case("127.0.0.1", unused_port, timing_normalization_enabled, peer)
|
"127.0.0.1",
|
||||||
.await;
|
unused_port,
|
||||||
|
timing_normalization_enabled,
|
||||||
|
peer,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
if timing_normalization_enabled {
|
if timing_normalization_enabled {
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,7 @@ async fn io_error_terminates_cleanly() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(MASK_RELAY_TIMEOUT, consume_client_data(ErrReader, usize::MAX))
|
||||||
MASK_RELAY_TIMEOUT,
|
.await
|
||||||
consume_client_data(ErrReader, usize::MAX),
|
.expect("consume_client_data did not return on I/O error");
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("consume_client_data did not return on I/O error");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,8 @@ async fn run_self_target_refusal(
|
||||||
let (mut client, server) = duplex(1024);
|
let (mut client, server) = duplex(1024);
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
handle_bad_client(
|
handle_bad_client(server, tokio::io::sink(), initial, peer, local_addr, &config, &beobachten)
|
||||||
server,
|
.await;
|
||||||
tokio::io::sink(),
|
|
||||||
initial,
|
|
||||||
peer,
|
|
||||||
local_addr,
|
|
||||||
&config,
|
|
||||||
&beobachten,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,7 @@ use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exact_four_byte_http_tokens_are_classified() {
|
fn exact_four_byte_http_tokens_are_classified() {
|
||||||
for token in [
|
for token in [b"GET ".as_ref(), b"POST".as_ref(), b"HEAD".as_ref(), b"PUT ".as_ref(), b"PRI ".as_ref()] {
|
||||||
b"GET ".as_ref(),
|
|
||||||
b"POST".as_ref(),
|
|
||||||
b"HEAD".as_ref(),
|
|
||||||
b"PUT ".as_ref(),
|
|
||||||
b"PRI ".as_ref(),
|
|
||||||
] {
|
|
||||||
assert!(
|
assert!(
|
||||||
is_http_probe(token),
|
is_http_probe(token),
|
||||||
"exact 4-byte token must be classified as HTTP probe: {:?}",
|
"exact 4-byte token must be classified as HTTP probe: {:?}",
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,7 @@ async fn tdd_non_local_port_short_circuit_does_not_enumerate_interfaces() {
|
||||||
let local_addr: SocketAddr = "0.0.0.0:443".parse().expect("valid local addr");
|
let local_addr: SocketAddr = "0.0.0.0:443".parse().expect("valid local addr");
|
||||||
let is_local = is_mask_target_local_listener_async("127.0.0.1", 8443, local_addr, None).await;
|
let is_local = is_mask_target_local_listener_async("127.0.0.1", 8443, local_addr, None).await;
|
||||||
|
|
||||||
assert!(
|
assert!(!is_local, "different port must not be treated as local listener");
|
||||||
!is_local,
|
|
||||||
"different port must not be treated as local listener"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_interface_enumerations_for_tests(),
|
local_interface_enumerations_for_tests(),
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,17 @@ impl AsyncWrite for CountingWriter {
|
||||||
Poll::Ready(Ok(buf.len()))
|
Poll::Ready(Ok(buf.len()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
fn poll_flush(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<std::io::Result<()>> {
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
fn poll_shutdown(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_cx: &mut Context<'_>,
|
||||||
|
) -> Poll<std::io::Result<()>> {
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::net::TcpListener as StdTcpListener;
|
use std::net::TcpListener as StdTcpListener;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant, timeout};
|
use tokio::time::{Duration, Instant, timeout};
|
||||||
|
|
@ -15,38 +15,74 @@ fn closed_local_port() -> u16 {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_matches_literal_ipv4_listener() {
|
async fn self_target_detection_matches_literal_ipv4_listener() {
|
||||||
let local: SocketAddr = "198.51.100.40:443".parse().unwrap();
|
let local: SocketAddr = "198.51.100.40:443".parse().unwrap();
|
||||||
assert!(is_mask_target_local_listener_async("198.51.100.40", 443, local, None,).await);
|
assert!(is_mask_target_local_listener_async(
|
||||||
|
"198.51.100.40",
|
||||||
|
443,
|
||||||
|
local,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_matches_bracketed_ipv6_listener() {
|
async fn self_target_detection_matches_bracketed_ipv6_listener() {
|
||||||
let local: SocketAddr = "[2001:db8::44]:8443".parse().unwrap();
|
let local: SocketAddr = "[2001:db8::44]:8443".parse().unwrap();
|
||||||
assert!(is_mask_target_local_listener_async("[2001:db8::44]", 8443, local, None,).await);
|
assert!(is_mask_target_local_listener_async(
|
||||||
|
"[2001:db8::44]",
|
||||||
|
8443,
|
||||||
|
local,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_keeps_same_ip_different_port_forwardable() {
|
async fn self_target_detection_keeps_same_ip_different_port_forwardable() {
|
||||||
let local: SocketAddr = "203.0.113.44:443".parse().unwrap();
|
let local: SocketAddr = "203.0.113.44:443".parse().unwrap();
|
||||||
assert!(!is_mask_target_local_listener_async("203.0.113.44", 8443, local, None,).await);
|
assert!(!is_mask_target_local_listener_async(
|
||||||
|
"203.0.113.44",
|
||||||
|
8443,
|
||||||
|
local,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_normalizes_ipv4_mapped_ipv6_literal() {
|
async fn self_target_detection_normalizes_ipv4_mapped_ipv6_literal() {
|
||||||
let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
||||||
assert!(is_mask_target_local_listener_async("::ffff:127.0.0.1", 443, local, None,).await);
|
assert!(is_mask_target_local_listener_async(
|
||||||
|
"::ffff:127.0.0.1",
|
||||||
|
443,
|
||||||
|
local,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_unspecified_bind_blocks_loopback_target() {
|
async fn self_target_detection_unspecified_bind_blocks_loopback_target() {
|
||||||
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
||||||
assert!(is_mask_target_local_listener_async("127.0.0.1", 443, local, None,).await);
|
assert!(is_mask_target_local_listener_async(
|
||||||
|
"127.0.0.1",
|
||||||
|
443,
|
||||||
|
local,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn self_target_detection_unspecified_bind_keeps_remote_target_forwardable() {
|
async fn self_target_detection_unspecified_bind_keeps_remote_target_forwardable() {
|
||||||
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
||||||
let remote: SocketAddr = "198.51.100.44:443".parse().unwrap();
|
let remote: SocketAddr = "198.51.100.44:443".parse().unwrap();
|
||||||
assert!(!is_mask_target_local_listener_async("mask.example", 443, local, Some(remote),).await);
|
assert!(!is_mask_target_local_listener_async(
|
||||||
|
"mask.example",
|
||||||
|
443,
|
||||||
|
local,
|
||||||
|
Some(remote),
|
||||||
|
)
|
||||||
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -270,10 +306,7 @@ async fn offline_mask_target_refusal_respects_timing_normalization_budget() {
|
||||||
});
|
});
|
||||||
|
|
||||||
client.shutdown().await.unwrap();
|
client.shutdown().await.unwrap();
|
||||||
timeout(Duration::from_secs(2), task)
|
timeout(Duration::from_secs(2), task).await.unwrap().unwrap();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
let elapsed = started.elapsed();
|
let elapsed = started.elapsed();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -317,10 +350,7 @@ async fn offline_mask_target_refusal_with_idle_client_is_bounded_by_consume_time
|
||||||
.await
|
.await
|
||||||
.expect("connection should still be open before consume timeout expires");
|
.expect("connection should still be open before consume timeout expires");
|
||||||
|
|
||||||
timeout(Duration::from_secs(2), task)
|
timeout(Duration::from_secs(2), task).await.unwrap().unwrap();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
let elapsed = started.elapsed();
|
let elapsed = started.elapsed();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,7 @@ async fn adversarial_delayed_interface_lookup_does_not_consume_outcome_floor_bud
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(80)).await;
|
tokio::time::sleep(Duration::from_millis(80)).await;
|
||||||
drop(held_refresh_guard);
|
drop(held_refresh_guard);
|
||||||
client
|
client.shutdown().await.expect("client shutdown must succeed");
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("client shutdown must succeed");
|
|
||||||
|
|
||||||
timeout(Duration::from_secs(2), task)
|
timeout(Duration::from_secs(2), task)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
use super::*;
|
|
||||||
use crate::crypto::AesCtr;
|
|
||||||
use bytes::Bytes;
|
|
||||||
use std::io;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use tokio::io::AsyncWrite;
|
|
||||||
|
|
||||||
struct CountedWriter {
|
|
||||||
write_calls: Arc<AtomicUsize>,
|
|
||||||
fail_writes: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CountedWriter {
|
|
||||||
fn new(write_calls: Arc<AtomicUsize>, fail_writes: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
write_calls,
|
|
||||||
fail_writes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWrite for CountedWriter {
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<io::Result<usize>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
this.write_calls.fetch_add(1, Ordering::Relaxed);
|
|
||||||
if this.fail_writes {
|
|
||||||
Poll::Ready(Err(io::Error::new(
|
|
||||||
io::ErrorKind::BrokenPipe,
|
|
||||||
"forced write failure",
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Poll::Ready(Ok(buf.len()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_crypto_writer(inner: CountedWriter) -> CryptoWriter<CountedWriter> {
|
|
||||||
let key = [0u8; 32];
|
|
||||||
let iv = 0u128;
|
|
||||||
CryptoWriter::new(inner, AesCtr::new(&key, iv), 8 * 1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn me_writer_write_fail_keeps_reserved_quota_and_tracks_fail_metrics() {
|
|
||||||
let stats = Stats::new();
|
|
||||||
let user = "middle-me-writer-no-rollback-user";
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
let write_calls = Arc::new(AtomicUsize::new(0));
|
|
||||||
let mut writer = make_crypto_writer(CountedWriter::new(write_calls.clone(), true));
|
|
||||||
let mut frame_buf = Vec::new();
|
|
||||||
let bytes_me2c = AtomicU64::new(0);
|
|
||||||
let payload = Bytes::from_static(&[0x11, 0x22, 0x33, 0x44, 0x55]);
|
|
||||||
|
|
||||||
let result = process_me_writer_response(
|
|
||||||
MeResponse::Data {
|
|
||||||
flags: 0,
|
|
||||||
data: payload.clone(),
|
|
||||||
},
|
|
||||||
&mut writer,
|
|
||||||
ProtoTag::Intermediate,
|
|
||||||
&SecureRandom::new(),
|
|
||||||
&mut frame_buf,
|
|
||||||
&stats,
|
|
||||||
user,
|
|
||||||
Some(user_stats.as_ref()),
|
|
||||||
Some(64),
|
|
||||||
0,
|
|
||||||
&bytes_me2c,
|
|
||||||
11,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
matches!(result, Err(ProxyError::Io(_))),
|
|
||||||
"write failure must propagate as I/O error"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
write_calls.load(Ordering::Relaxed) > 0,
|
|
||||||
"writer must be attempted after successful quota reservation"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_user_quota_used(user),
|
|
||||||
payload.len() as u64,
|
|
||||||
"reserved quota must not roll back on write failure"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_quota_write_fail_bytes_total(),
|
|
||||||
payload.len() as u64,
|
|
||||||
"write-fail byte metric must include failed payload size"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_quota_write_fail_events_total(),
|
|
||||||
1,
|
|
||||||
"write-fail events metric must increment once"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_user_total_octets(user),
|
|
||||||
0,
|
|
||||||
"telemetry octets_to should not advance when write fails"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
bytes_me2c.load(Ordering::Relaxed),
|
|
||||||
0,
|
|
||||||
"ME->C committed byte counter must not advance on write failure"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn me_writer_pre_write_quota_reject_happens_before_writer_poll() {
|
|
||||||
let stats = Stats::new();
|
|
||||||
let user = "middle-me-writer-precheck-user";
|
|
||||||
let limit = 8u64;
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), limit);
|
|
||||||
|
|
||||||
let write_calls = Arc::new(AtomicUsize::new(0));
|
|
||||||
let mut writer = make_crypto_writer(CountedWriter::new(write_calls.clone(), false));
|
|
||||||
let mut frame_buf = Vec::new();
|
|
||||||
let bytes_me2c = AtomicU64::new(0);
|
|
||||||
|
|
||||||
let result = process_me_writer_response(
|
|
||||||
MeResponse::Data {
|
|
||||||
flags: 0,
|
|
||||||
data: Bytes::from_static(&[0xAA, 0xBB, 0xCC]),
|
|
||||||
},
|
|
||||||
&mut writer,
|
|
||||||
ProtoTag::Intermediate,
|
|
||||||
&SecureRandom::new(),
|
|
||||||
&mut frame_buf,
|
|
||||||
&stats,
|
|
||||||
user,
|
|
||||||
Some(user_stats.as_ref()),
|
|
||||||
Some(limit),
|
|
||||||
0,
|
|
||||||
&bytes_me2c,
|
|
||||||
12,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
matches!(result, Err(ProxyError::DataQuotaExceeded { .. })),
|
|
||||||
"pre-write quota rejection must return typed quota error"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
write_calls.load(Ordering::Relaxed),
|
|
||||||
0,
|
|
||||||
"writer must not be polled when pre-write quota reservation fails"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_me_d2c_quota_reject_pre_write_total(),
|
|
||||||
1,
|
|
||||||
"pre-write quota reject metric must increment"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_user_quota_used(user),
|
|
||||||
limit,
|
|
||||||
"failed pre-write reservation must keep previous quota usage unchanged"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_quota_write_fail_bytes_total(),
|
|
||||||
0,
|
|
||||||
"write-fail bytes metric must stay unchanged on pre-write reject"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_quota_write_fail_events_total(),
|
|
||||||
0,
|
|
||||||
"write-fail events metric must stay unchanged on pre-write reject"
|
|
||||||
);
|
|
||||||
assert_eq!(bytes_me2c.load(Ordering::Relaxed), 0);
|
|
||||||
}
|
|
||||||
|
|
@ -2,8 +2,8 @@ use super::*;
|
||||||
use crate::crypto::AesCtr;
|
use crate::crypto::AesCtr;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader};
|
use crate::stream::{BufferPool, CryptoReader};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::io::duplex;
|
use tokio::io::duplex;
|
||||||
use tokio::time::{Duration as TokioDuration, Instant as TokioInstant, timeout};
|
use tokio::time::{Duration as TokioDuration, Instant as TokioInstant, timeout};
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,7 @@ fn blackhat_registry_poison_recovers_with_fail_closed_reset_and_pressure_account
|
||||||
let before = relay_pressure_event_seq();
|
let before = relay_pressure_event_seq();
|
||||||
note_relay_pressure_event();
|
note_relay_pressure_event();
|
||||||
let after = relay_pressure_event_seq();
|
let after = relay_pressure_event_seq();
|
||||||
assert!(
|
assert!(after > before, "pressure accounting must still advance after poison");
|
||||||
after > before,
|
|
||||||
"pressure accounting must still advance after poison"
|
|
||||||
);
|
|
||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing();
|
clear_relay_idle_pressure_state_for_testing();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,7 @@ async fn adversarial_lockstep_alternating_attack_under_jitter_closes() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer_task
|
writer_task.await.expect("writer jitter task must not panic");
|
||||||
.await
|
|
||||||
.expect("writer jitter task must not panic");
|
|
||||||
assert!(closed, "alternating attack must close before EOF");
|
assert!(closed, "alternating attack must close before EOF");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -249,10 +247,7 @@ async fn integration_mixed_population_attackers_close_benign_survive() {
|
||||||
plaintext.push(0x01);
|
plaintext.push(0x01);
|
||||||
plaintext.extend_from_slice(&[n, n, n, n]);
|
plaintext.extend_from_slice(&[n, n, n, n]);
|
||||||
}
|
}
|
||||||
writer
|
writer.write_all(&encrypt_for_reader(&plaintext)).await.unwrap();
|
||||||
.write_all(&encrypt_for_reader(&plaintext))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
drop(writer);
|
drop(writer);
|
||||||
|
|
||||||
let mut closed = false;
|
let mut closed = false;
|
||||||
|
|
@ -284,10 +279,7 @@ async fn integration_mixed_population_attackers_close_benign_survive() {
|
||||||
}
|
}
|
||||||
plaintext.push(0x01);
|
plaintext.push(0x01);
|
||||||
plaintext.extend_from_slice(&payload);
|
plaintext.extend_from_slice(&payload);
|
||||||
writer
|
writer.write_all(&encrypt_for_reader(&plaintext)).await.unwrap();
|
||||||
.write_all(&encrypt_for_reader(&plaintext))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let got = read_once(
|
let got = read_once(
|
||||||
&mut crypto_reader,
|
&mut crypto_reader,
|
||||||
|
|
@ -337,10 +329,7 @@ async fn light_fuzz_parallel_patterns_no_hang_or_panic() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer
|
writer.write_all(&encrypt_for_reader(&plaintext)).await.unwrap();
|
||||||
.write_all(&encrypt_for_reader(&plaintext))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
drop(writer);
|
drop(writer);
|
||||||
|
|
||||||
for _ in 0..320 {
|
for _ in 0..320 {
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,7 @@ fn make_enabled_idle_policy() -> RelayClientIdlePolicy {
|
||||||
fn append_tiny_frame(plaintext: &mut Vec<u8>, proto: ProtoTag) {
|
fn append_tiny_frame(plaintext: &mut Vec<u8>, proto: ProtoTag) {
|
||||||
match proto {
|
match proto {
|
||||||
ProtoTag::Abridged => plaintext.push(0x00),
|
ProtoTag::Abridged => plaintext.push(0x00),
|
||||||
ProtoTag::Intermediate | ProtoTag::Secure => {
|
ProtoTag::Intermediate | ProtoTag::Secure => plaintext.extend_from_slice(&0u32.to_le_bytes()),
|
||||||
plaintext.extend_from_slice(&0u32.to_le_bytes())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,11 +206,7 @@ async fn intermediate_chunked_alternating_attack_closes_before_eof() {
|
||||||
let mut plaintext = Vec::with_capacity(8 * 200);
|
let mut plaintext = Vec::with_capacity(8 * 200);
|
||||||
for n in 0..180u8 {
|
for n in 0..180u8 {
|
||||||
append_tiny_frame(&mut plaintext, ProtoTag::Intermediate);
|
append_tiny_frame(&mut plaintext, ProtoTag::Intermediate);
|
||||||
append_real_frame(
|
append_real_frame(&mut plaintext, ProtoTag::Intermediate, [n, n ^ 1, n ^ 2, n ^ 3]);
|
||||||
&mut plaintext,
|
|
||||||
ProtoTag::Intermediate,
|
|
||||||
[n, n ^ 1, n ^ 2, n ^ 3],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let encrypted = encrypt_for_reader(&plaintext);
|
let encrypted = encrypt_for_reader(&plaintext);
|
||||||
|
|
||||||
|
|
@ -246,9 +240,7 @@ async fn intermediate_chunked_alternating_attack_closes_before_eof() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer_task
|
writer_task.await.expect("intermediate writer task must not panic");
|
||||||
.await
|
|
||||||
.expect("intermediate writer task must not panic");
|
|
||||||
assert!(closed, "intermediate alternating attack must fail closed");
|
assert!(closed, "intermediate alternating attack must fail closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,9 +290,7 @@ async fn secure_chunked_alternating_attack_closes_before_eof() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writer_task
|
writer_task.await.expect("secure writer task must not panic");
|
||||||
.await
|
|
||||||
.expect("secure writer task must not panic");
|
|
||||||
assert!(closed, "secure alternating attack must fail closed");
|
assert!(closed, "secure alternating attack must fail closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use super::*;
|
||||||
use crate::crypto::AesCtr;
|
use crate::crypto::AesCtr;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader};
|
use crate::stream::{BufferPool, CryptoReader};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tokio::io::{AsyncRead, AsyncWriteExt, duplex};
|
use tokio::io::{AsyncRead, AsyncWriteExt, duplex};
|
||||||
|
|
||||||
|
|
@ -156,10 +156,7 @@ fn alternating_one_to_one_closes_with_bounded_real_frame_count() {
|
||||||
}
|
}
|
||||||
let (closed_at, _, reals) = simulate_tiny_debt_pattern(&pattern, pattern.len());
|
let (closed_at, _, reals) = simulate_tiny_debt_pattern(&pattern, pattern.len());
|
||||||
assert!(closed_at.is_some());
|
assert!(closed_at.is_some());
|
||||||
assert!(
|
assert!(reals <= 80, "expected bounded real frames before close, got {reals}");
|
||||||
reals <= 80,
|
|
||||||
"expected bounded real frames before close, got {reals}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -186,10 +183,7 @@ fn alternating_one_to_seven_eventually_closes() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (closed_at, _, _) = simulate_tiny_debt_pattern(&pattern, pattern.len());
|
let (closed_at, _, _) = simulate_tiny_debt_pattern(&pattern, pattern.len());
|
||||||
assert!(
|
assert!(closed_at.is_some(), "1:7 tiny-to-real must eventually close");
|
||||||
closed_at.is_some(),
|
|
||||||
"1:7 tiny-to-real must eventually close"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ use super::*;
|
||||||
use crate::crypto::AesCtr;
|
use crate::crypto::AesCtr;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader};
|
use crate::stream::{BufferPool, CryptoReader};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::time::Instant;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncRead, AsyncWriteExt, duplex};
|
use tokio::io::{AsyncRead, AsyncWriteExt, duplex};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
fn make_crypto_reader<T>(reader: T) -> CryptoReader<T>
|
fn make_crypto_reader<T>(reader: T) -> CryptoReader<T>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,7 @@ async fn relay_hol_blocking_prevention_regression() {
|
||||||
async fn relay_quota_mid_session_cutoff() {
|
async fn relay_quota_mid_session_cutoff() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-mid-user";
|
let user = "quota-mid-user";
|
||||||
let quota = 5000u64;
|
let quota = 5000;
|
||||||
let c2s_buf_size = 1024usize;
|
|
||||||
|
|
||||||
let (client_peer, relay_client) = duplex(8192);
|
let (client_peer, relay_client) = duplex(8192);
|
||||||
let (relay_server, server_peer) = duplex(8192);
|
let (relay_server, server_peer) = duplex(8192);
|
||||||
|
|
@ -94,7 +93,7 @@ async fn relay_quota_mid_session_cutoff() {
|
||||||
client_writer,
|
client_writer,
|
||||||
server_reader,
|
server_reader,
|
||||||
server_writer,
|
server_writer,
|
||||||
c2s_buf_size,
|
1024,
|
||||||
1024,
|
1024,
|
||||||
user,
|
user,
|
||||||
Arc::clone(&stats),
|
Arc::clone(&stats),
|
||||||
|
|
@ -121,25 +120,9 @@ async fn relay_quota_mid_session_cutoff() {
|
||||||
other => panic!("Expected DataQuotaExceeded error, got: {:?}", other),
|
other => panic!("Expected DataQuotaExceeded error, got: {:?}", other),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut overshoot_bytes = 0usize;
|
let mut small_buf = [0u8; 1];
|
||||||
let mut buf = [0u8; 256];
|
let n = sp_reader.read(&mut small_buf).await.unwrap();
|
||||||
loop {
|
assert_eq!(n, 0, "Server must see EOF after quota reached");
|
||||||
match timeout(Duration::from_millis(20), sp_reader.read(&mut buf)).await {
|
|
||||||
Ok(Ok(0)) => break,
|
|
||||||
Ok(Ok(n)) => overshoot_bytes = overshoot_bytes.saturating_add(n),
|
|
||||||
Ok(Err(e)) => panic!("server read must not fail after relay cutoff: {e}"),
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
overshoot_bytes <= c2s_buf_size,
|
|
||||||
"post-write cutoff may leak at most one C->S chunk after boundary, got {overshoot_bytes}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
stats.get_user_quota_used(user) <= quota.saturating_add(c2s_buf_size as u64),
|
|
||||||
"accounted quota must remain bounded by one in-flight chunk overshoot"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
use super::*;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::io;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
|
||||||
use tokio::time::Instant;
|
|
||||||
|
|
||||||
struct ScriptedWriter {
|
|
||||||
scripted_writes: Arc<Mutex<VecDeque<usize>>>,
|
|
||||||
write_calls: Arc<AtomicUsize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScriptedWriter {
|
|
||||||
fn new(script: &[usize], write_calls: Arc<AtomicUsize>) -> Self {
|
|
||||||
Self {
|
|
||||||
scripted_writes: Arc::new(Mutex::new(script.iter().copied().collect())),
|
|
||||||
write_calls,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncWrite for ScriptedWriter {
|
|
||||||
fn poll_write(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
buf: &[u8],
|
|
||||||
) -> Poll<io::Result<usize>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
this.write_calls.fetch_add(1, Ordering::Relaxed);
|
|
||||||
let planned = this
|
|
||||||
.scripted_writes
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.pop_front()
|
|
||||||
.unwrap_or(buf.len());
|
|
||||||
Poll::Ready(Ok(planned.min(buf.len())))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_stats_io_with_script(
|
|
||||||
user: &str,
|
|
||||||
quota_limit: u64,
|
|
||||||
precharged_quota: u64,
|
|
||||||
script: &[usize],
|
|
||||||
) -> (
|
|
||||||
StatsIo<ScriptedWriter>,
|
|
||||||
Arc<Stats>,
|
|
||||||
Arc<AtomicUsize>,
|
|
||||||
Arc<AtomicBool>,
|
|
||||||
) {
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
if precharged_quota > 0 {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), precharged_quota);
|
|
||||||
}
|
|
||||||
|
|
||||||
let write_calls = Arc::new(AtomicUsize::new(0));
|
|
||||||
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
|
||||||
let io = StatsIo::new(
|
|
||||||
ScriptedWriter::new(script, write_calls.clone()),
|
|
||||||
Arc::new(SharedCounters::new()),
|
|
||||||
stats.clone(),
|
|
||||||
user.to_string(),
|
|
||||||
Some(quota_limit),
|
|
||||||
quota_exceeded.clone(),
|
|
||||||
Instant::now(),
|
|
||||||
);
|
|
||||||
|
|
||||||
(io, stats, write_calls, quota_exceeded)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn direct_partial_write_charges_only_committed_bytes_without_double_charge() {
|
|
||||||
let user = "direct-partial-charge-user";
|
|
||||||
let (mut io, stats, write_calls, quota_exceeded) =
|
|
||||||
make_stats_io_with_script(user, 1_048_576, 0, &[8 * 1024, 8 * 1024, 48 * 1024]);
|
|
||||||
let payload = vec![0xAB; 64 * 1024];
|
|
||||||
|
|
||||||
let n1 = io
|
|
||||||
.write(&payload)
|
|
||||||
.await
|
|
||||||
.expect("first partial write must succeed");
|
|
||||||
let n2 = io
|
|
||||||
.write(&payload)
|
|
||||||
.await
|
|
||||||
.expect("second partial write must succeed");
|
|
||||||
let n3 = io.write(&payload).await.expect("tail write must succeed");
|
|
||||||
|
|
||||||
assert_eq!(n1, 8 * 1024);
|
|
||||||
assert_eq!(n2, 8 * 1024);
|
|
||||||
assert_eq!(n3, 48 * 1024);
|
|
||||||
assert_eq!(write_calls.load(Ordering::Relaxed), 3);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_user_quota_used(user),
|
|
||||||
(n1 + n2 + n3) as u64,
|
|
||||||
"quota accounting must follow committed bytes only"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
stats.get_user_total_octets(user),
|
|
||||||
(n1 + n2 + n3) as u64,
|
|
||||||
"telemetry octets should match committed bytes on successful writes"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!quota_exceeded.load(Ordering::Acquire),
|
|
||||||
"quota flag should stay false under large remaining budget"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn direct_hybrid_branch_selection_matches_contract() {
|
|
||||||
let near_limit = 256 * 1024u64;
|
|
||||||
let near_remaining = 32 * 1024u64;
|
|
||||||
let (mut near_io, _stats, _calls, _flag) = make_stats_io_with_script(
|
|
||||||
"direct-near-limit-hard-check-user",
|
|
||||||
near_limit,
|
|
||||||
near_limit - near_remaining,
|
|
||||||
&[4 * 1024],
|
|
||||||
);
|
|
||||||
let near_payload = vec![0x11; 4 * 1024];
|
|
||||||
let near_written = near_io
|
|
||||||
.write(&near_payload)
|
|
||||||
.await
|
|
||||||
.expect("near-limit write must succeed");
|
|
||||||
assert_eq!(near_written, 4 * 1024);
|
|
||||||
assert_eq!(
|
|
||||||
near_io.quota_bytes_since_check, 0,
|
|
||||||
"near-limit branch must go through immediate hard check"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (mut far_small_io, _stats, _calls, _flag) =
|
|
||||||
make_stats_io_with_script("direct-far-small-amortized-user", 1_048_576, 0, &[4 * 1024]);
|
|
||||||
let far_small_payload = vec![0x22; 4 * 1024];
|
|
||||||
let far_small_written = far_small_io
|
|
||||||
.write(&far_small_payload)
|
|
||||||
.await
|
|
||||||
.expect("small far-from-limit write must succeed");
|
|
||||||
assert_eq!(far_small_written, 4 * 1024);
|
|
||||||
assert_eq!(
|
|
||||||
far_small_io.quota_bytes_since_check,
|
|
||||||
4 * 1024,
|
|
||||||
"small far-from-limit write must go through amortized path"
|
|
||||||
);
|
|
||||||
|
|
||||||
let (mut far_large_io, _stats, _calls, _flag) = make_stats_io_with_script(
|
|
||||||
"direct-far-large-hard-check-user",
|
|
||||||
1_048_576,
|
|
||||||
0,
|
|
||||||
&[32 * 1024],
|
|
||||||
);
|
|
||||||
let far_large_payload = vec![0x33; 32 * 1024];
|
|
||||||
let far_large_written = far_large_io
|
|
||||||
.write(&far_large_payload)
|
|
||||||
.await
|
|
||||||
.expect("large write must succeed");
|
|
||||||
assert_eq!(far_large_written, 32 * 1024);
|
|
||||||
assert_eq!(
|
|
||||||
far_large_io.quota_bytes_since_check, 0,
|
|
||||||
"large write must force immediate hard check even far from limit"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn remaining_before_zero_rejects_without_calling_inner_writer() {
|
|
||||||
let user = "direct-zero-remaining-user";
|
|
||||||
let limit = 8u64;
|
|
||||||
let (mut io, stats, write_calls, quota_exceeded) =
|
|
||||||
make_stats_io_with_script(user, limit, limit, &[1]);
|
|
||||||
|
|
||||||
let err = io
|
|
||||||
.write(&[0x44])
|
|
||||||
.await
|
|
||||||
.expect_err("write must fail when remaining quota is zero");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
is_quota_io_error(&err),
|
|
||||||
"zero-remaining gate must return typed quota I/O error"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
write_calls.load(Ordering::Relaxed),
|
|
||||||
0,
|
|
||||||
"inner poll_write must not be called when remaining quota is zero"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
quota_exceeded.load(Ordering::Acquire),
|
|
||||||
"zero-remaining gate must set exceeded flag"
|
|
||||||
);
|
|
||||||
assert_eq!(stats.get_user_quota_used(user), limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn exceeded_flag_blocks_following_poll_before_inner_write() {
|
|
||||||
let user = "direct-exceeded-visibility-user";
|
|
||||||
let (mut io, stats, write_calls, quota_exceeded) =
|
|
||||||
make_stats_io_with_script(user, 1, 0, &[1, 1]);
|
|
||||||
|
|
||||||
let first = io
|
|
||||||
.write(&[0x55])
|
|
||||||
.await
|
|
||||||
.expect("first byte should consume remaining quota");
|
|
||||||
assert_eq!(first, 1);
|
|
||||||
assert!(
|
|
||||||
quota_exceeded.load(Ordering::Acquire),
|
|
||||||
"hard check should store quota_exceeded after boundary hit"
|
|
||||||
);
|
|
||||||
|
|
||||||
let second = io
|
|
||||||
.write(&[0x66])
|
|
||||||
.await
|
|
||||||
.expect_err("next write must be rejected by early exceeded gate");
|
|
||||||
assert!(
|
|
||||||
is_quota_io_error(&second),
|
|
||||||
"following write must fail with typed quota error"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
write_calls.load(Ordering::Relaxed),
|
|
||||||
1,
|
|
||||||
"second write must be cut before touching inner writer"
|
|
||||||
);
|
|
||||||
assert_eq!(stats.get_user_quota_used(user), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adaptive_interval_clamp_matches_contract() {
|
|
||||||
assert_eq!(quota_adaptive_interval_bytes(0), 4 * 1024);
|
|
||||||
assert_eq!(quota_adaptive_interval_bytes(2 * 1024), 4 * 1024);
|
|
||||||
assert_eq!(quota_adaptive_interval_bytes(32 * 1024), 16 * 1024);
|
|
||||||
assert_eq!(quota_adaptive_interval_bytes(256 * 1024), 64 * 1024);
|
|
||||||
|
|
||||||
assert!(should_immediate_quota_check(32 * 1024, 4 * 1024));
|
|
||||||
assert!(should_immediate_quota_check(1_048_576, 32 * 1024));
|
|
||||||
assert!(!should_immediate_quota_check(1_048_576, 4 * 1024));
|
|
||||||
}
|
|
||||||
|
|
@ -29,11 +29,6 @@ async fn read_available<R: AsyncRead + Unpin>(reader: &mut R, budget: Duration)
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn integration_full_duplex_exact_budget_then_hard_cutoff() {
|
async fn integration_full_duplex_exact_budget_then_hard_cutoff() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
@ -107,14 +102,14 @@ async fn integration_full_duplex_exact_budget_then_hard_cutoff() {
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { ref user }) if user == "quota-full-duplex-boundary-user"
|
Err(ProxyError::DataQuotaExceeded { ref user }) if user == "quota-full-duplex-boundary-user"
|
||||||
));
|
));
|
||||||
assert!(stats.get_user_quota_used(user) <= 10);
|
assert!(stats.get_user_total_octets(user) <= 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn negative_preloaded_quota_blocks_both_directions_immediately() {
|
async fn negative_preloaded_quota_blocks_both_directions_immediately() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-preloaded-cutoff-user";
|
let user = "quota-preloaded-cutoff-user";
|
||||||
preload_user_quota(stats.as_ref(), user, 5);
|
stats.add_user_octets_from(user, 5);
|
||||||
|
|
||||||
let (mut client_peer, relay_client) = duplex(2048);
|
let (mut client_peer, relay_client) = duplex(2048);
|
||||||
let (relay_server, mut server_peer) = duplex(2048);
|
let (relay_server, mut server_peer) = duplex(2048);
|
||||||
|
|
@ -159,7 +154,7 @@ async fn negative_preloaded_quota_blocks_both_directions_immediately() {
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
Err(ProxyError::DataQuotaExceeded { .. })
|
||||||
));
|
));
|
||||||
assert!(stats.get_user_quota_used(user) <= 5);
|
assert!(stats.get_user_total_octets(user) <= 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -217,7 +212,7 @@ async fn edge_quota_one_bidirectional_race_allows_at_most_one_forwarded_octet()
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
Err(ProxyError::DataQuotaExceeded { .. })
|
||||||
));
|
));
|
||||||
assert!(stats.get_user_quota_used(user) <= 1);
|
assert!(stats.get_user_total_octets(user) <= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -282,7 +277,7 @@ async fn adversarial_blackhat_alternating_fragmented_jitter_never_overshoots_glo
|
||||||
delivered_to_server + delivered_to_client <= quota as usize,
|
delivered_to_server + delivered_to_client <= quota as usize,
|
||||||
"combined forwarded bytes must never exceed configured quota"
|
"combined forwarded bytes must never exceed configured quota"
|
||||||
);
|
);
|
||||||
assert!(stats.get_user_quota_used(user) <= quota);
|
assert!(stats.get_user_total_octets(user) <= quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -361,7 +356,7 @@ async fn light_fuzz_randomized_schedule_preserves_quota_and_forwarded_byte_invar
|
||||||
"fuzz case {case}: forwarded bytes must not exceed quota"
|
"fuzz case {case}: forwarded bytes must not exceed quota"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(&user) <= quota,
|
stats.get_user_total_octets(&user) <= quota,
|
||||||
"fuzz case {case}: accounted bytes must not exceed quota"
|
"fuzz case {case}: accounted bytes must not exceed quota"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -456,7 +451,7 @@ async fn stress_multi_relay_same_user_mixed_direction_jitter_respects_global_quo
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(user) <= quota,
|
stats.get_user_total_octets(user) <= quota,
|
||||||
"global per-user quota must hold under concurrent mixed-direction relay stress"
|
"global per-user quota must hold under concurrent mixed-direction relay stress"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,10 @@ use crate::stream::BufferPool;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{RngExt, SeedableRng};
|
use rand::{RngExt, SeedableRng};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::time::{Duration, timeout};
|
use tokio::time::{Duration, timeout};
|
||||||
|
|
||||||
async fn read_available<R: tokio::io::AsyncRead + Unpin>(
|
async fn read_available<R: tokio::io::AsyncRead + Unpin>(reader: &mut R, budget: Duration) -> usize {
|
||||||
reader: &mut R,
|
|
||||||
budget: Duration,
|
|
||||||
) -> usize {
|
|
||||||
let start = tokio::time::Instant::now();
|
let start = tokio::time::Instant::now();
|
||||||
let mut total = 0usize;
|
let mut total = 0usize;
|
||||||
let mut buf = [0u8; 128];
|
let mut buf = [0u8; 128];
|
||||||
|
|
@ -32,11 +29,6 @@ async fn read_available<R: tokio::io::AsyncRead + Unpin>(
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn positive_quota_path_forwards_both_directions_within_limit() {
|
async fn positive_quota_path_forwards_both_directions_within_limit() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
@ -60,34 +52,25 @@ async fn positive_quota_path_forwards_both_directions_within_limit() {
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
));
|
));
|
||||||
|
|
||||||
client_peer
|
client_peer.write_all(&[0xAA, 0xBB, 0xCC, 0xDD]).await.unwrap();
|
||||||
.write_all(&[0xAA, 0xBB, 0xCC, 0xDD])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
server_peer.read_exact(&mut [0u8; 4]).await.unwrap();
|
server_peer.read_exact(&mut [0u8; 4]).await.unwrap();
|
||||||
|
|
||||||
server_peer
|
server_peer.write_all(&[0x11, 0x22, 0x33, 0x44]).await.unwrap();
|
||||||
.write_all(&[0x11, 0x22, 0x33, 0x44])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_peer.read_exact(&mut [0u8; 4]).await.unwrap();
|
client_peer.read_exact(&mut [0u8; 4]).await.unwrap();
|
||||||
|
|
||||||
drop(client_peer);
|
drop(client_peer);
|
||||||
drop(server_peer);
|
drop(server_peer);
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(2), relay)
|
let relay_result = timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(relay_result.is_ok());
|
assert!(relay_result.is_ok());
|
||||||
assert!(stats.get_user_quota_used(user) <= 16);
|
assert!(stats.get_user_total_octets(user) <= 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn negative_preloaded_quota_forbids_any_forwarding() {
|
async fn negative_preloaded_quota_forbids_any_forwarding() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-extended-negative-user";
|
let user = "quota-extended-negative-user";
|
||||||
preload_user_quota(stats.as_ref(), user, 8);
|
stats.add_user_octets_from(user, 8);
|
||||||
|
|
||||||
let (mut client_peer, relay_client) = duplex(1024);
|
let (mut client_peer, relay_client) = duplex(1024);
|
||||||
let (relay_server, mut server_peer) = duplex(1024);
|
let (relay_server, mut server_peer) = duplex(1024);
|
||||||
|
|
@ -110,24 +93,12 @@ async fn negative_preloaded_quota_forbids_any_forwarding() {
|
||||||
client_peer.write_all(&[0xAA]).await.unwrap();
|
client_peer.write_all(&[0xAA]).await.unwrap();
|
||||||
server_peer.write_all(&[0xBB]).await.unwrap();
|
server_peer.write_all(&[0xBB]).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(read_available(&mut server_peer, Duration::from_millis(120)).await, 0);
|
||||||
read_available(&mut server_peer, Duration::from_millis(120)).await,
|
assert_eq!(read_available(&mut client_peer, Duration::from_millis(120)).await, 0);
|
||||||
0
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
read_available(&mut client_peer, Duration::from_millis(120)).await,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(2), relay)
|
let relay_result = timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
||||||
.await
|
assert!(matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. })));
|
||||||
.unwrap()
|
assert!(stats.get_user_total_octets(user) <= 8);
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
relay_result,
|
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
|
||||||
));
|
|
||||||
assert!(stats.get_user_quota_used(user) <= 8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -159,25 +130,13 @@ async fn edge_quota_one_ensures_at_most_one_byte_across_directions() {
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut buf = [0u8; 1];
|
let mut buf = [0u8; 1];
|
||||||
let delivered_s2c = timeout(Duration::from_millis(120), client_peer.read(&mut buf))
|
let delivered_s2c = timeout(Duration::from_millis(120), client_peer.read(&mut buf)).await.unwrap().unwrap_or(0);
|
||||||
.await
|
let delivered_c2s = timeout(Duration::from_millis(120), server_peer.read(&mut buf)).await.unwrap().unwrap_or(0);
|
||||||
.unwrap()
|
|
||||||
.unwrap_or(0);
|
|
||||||
let delivered_c2s = timeout(Duration::from_millis(120), server_peer.read(&mut buf))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
assert!(delivered_s2c + delivered_c2s <= 1);
|
assert!(delivered_s2c + delivered_c2s <= 1);
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(2), relay)
|
let relay_result = timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
||||||
.await
|
assert!(matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. })));
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
relay_result,
|
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -227,16 +186,10 @@ async fn adversarial_blackhat_alternating_jitter_does_not_overshoot_quota() {
|
||||||
tokio::time::sleep(Duration::from_millis(((i % 3) + 1) as u64)).await;
|
tokio::time::sleep(Duration::from_millis(((i % 3) + 1) as u64)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(3), relay)
|
let relay_result = timeout(Duration::from_secs(3), relay).await.unwrap().unwrap();
|
||||||
.await
|
assert!(matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. })));
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
relay_result,
|
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
|
||||||
));
|
|
||||||
assert!(total_forwarded <= quota as usize);
|
assert!(total_forwarded <= quota as usize);
|
||||||
assert!(stats.get_user_quota_used(user) <= quota);
|
assert!(stats.get_user_total_octets(user) <= quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -281,17 +234,13 @@ async fn light_fuzz_random_quota_schedule_preserves_quota_invariants() {
|
||||||
if rng.random::<bool>() {
|
if rng.random::<bool>() {
|
||||||
let _ = client_peer.write_all(&[rng.random::<u8>()]).await;
|
let _ = client_peer.write_all(&[rng.random::<u8>()]).await;
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
if let Ok(Ok(n)) =
|
if let Ok(Ok(n)) = timeout(Duration::from_millis(4), server_peer.read(&mut one)).await {
|
||||||
timeout(Duration::from_millis(4), server_peer.read(&mut one)).await
|
|
||||||
{
|
|
||||||
total_forwarded += n;
|
total_forwarded += n;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let _ = server_peer.write_all(&[rng.random::<u8>()]).await;
|
let _ = server_peer.write_all(&[rng.random::<u8>()]).await;
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
if let Ok(Ok(n)) =
|
if let Ok(Ok(n)) = timeout(Duration::from_millis(4), client_peer.read(&mut one)).await {
|
||||||
timeout(Duration::from_millis(4), client_peer.read(&mut one)).await
|
|
||||||
{
|
|
||||||
total_forwarded += n;
|
total_forwarded += n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -300,16 +249,10 @@ async fn light_fuzz_random_quota_schedule_preserves_quota_invariants() {
|
||||||
drop(client_peer);
|
drop(client_peer);
|
||||||
drop(server_peer);
|
drop(server_peer);
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(2), relay)
|
let relay_result = timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
||||||
.await
|
assert!(relay_result.is_ok() || matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. })));
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
relay_result.is_ok()
|
|
||||||
|| matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. }))
|
|
||||||
);
|
|
||||||
assert!(total_forwarded <= quota as usize);
|
assert!(total_forwarded <= quota as usize);
|
||||||
assert!(stats.get_user_quota_used(&user) <= quota);
|
assert!(stats.get_user_total_octets(&user) <= quota);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,17 +300,13 @@ async fn stress_parallel_relays_for_one_user_obey_global_quota() {
|
||||||
if (step as usize + worker as usize) % 2 == 0 {
|
if (step as usize + worker as usize) % 2 == 0 {
|
||||||
let _ = client_peer.write_all(&[(step ^ 0x5A)]).await;
|
let _ = client_peer.write_all(&[(step ^ 0x5A)]).await;
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
if let Ok(Ok(n)) =
|
if let Ok(Ok(n)) = timeout(Duration::from_millis(6), server_peer.read(&mut one)).await {
|
||||||
timeout(Duration::from_millis(6), server_peer.read(&mut one)).await
|
|
||||||
{
|
|
||||||
total += n;
|
total += n;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let _ = server_peer.write_all(&[(step ^ 0xA5)]).await;
|
let _ = server_peer.write_all(&[(step ^ 0xA5)]).await;
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
if let Ok(Ok(n)) =
|
if let Ok(Ok(n)) = timeout(Duration::from_millis(6), client_peer.read(&mut one)).await {
|
||||||
timeout(Duration::from_millis(6), client_peer.read(&mut one)).await
|
|
||||||
{
|
|
||||||
total += n;
|
total += n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -377,14 +316,8 @@ async fn stress_parallel_relays_for_one_user_obey_global_quota() {
|
||||||
drop(client_peer);
|
drop(client_peer);
|
||||||
drop(server_peer);
|
drop(server_peer);
|
||||||
|
|
||||||
let relay_result = timeout(Duration::from_secs(2), relay)
|
let relay_result = timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
||||||
.await
|
assert!(relay_result.is_ok() || matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. })));
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
relay_result.is_ok()
|
|
||||||
|| matches!(relay_result, Err(ProxyError::DataQuotaExceeded { .. }))
|
|
||||||
);
|
|
||||||
total
|
total
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -394,6 +327,6 @@ async fn stress_parallel_relays_for_one_user_obey_global_quota() {
|
||||||
delivered += task.await.unwrap();
|
delivered += task.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(stats.get_user_quota_used(&user) <= quota);
|
assert!(stats.get_user_total_octets(&user) <= quota);
|
||||||
assert!(delivered <= quota as usize);
|
assert!(delivered <= quota as usize);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ async fn drain_available<R: AsyncRead + Unpin>(reader: &mut R, out: &mut Vec<u8>
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn model_fuzz_bidirectional_schedule_preserves_prefixes_and_quota_budget() {
|
async fn model_fuzz_bidirectional_schedule_preserves_prefixes_and_quota_budget() {
|
||||||
let mut rng = StdRng::seed_from_u64(0xC0DE_CAFE_D15C_F00D);
|
let mut rng = StdRng::seed_from_u64(0xC0DE_CAFE_D15C_F00D);
|
||||||
const MAX_INPUT_CHUNK: usize = 12;
|
|
||||||
|
|
||||||
for case in 0..64u64 {
|
for case in 0..64u64 {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
|
|
@ -93,12 +92,12 @@ async fn model_fuzz_bidirectional_schedule_preserves_prefixes_and_quota_budget()
|
||||||
assert_is_prefix(&recv_at_server, &sent_c2s, "C->S");
|
assert_is_prefix(&recv_at_server, &sent_c2s, "C->S");
|
||||||
assert_is_prefix(&recv_at_client, &sent_s2c, "S->C");
|
assert_is_prefix(&recv_at_client, &sent_s2c, "S->C");
|
||||||
assert!(
|
assert!(
|
||||||
recv_at_server.len() + recv_at_client.len() <= quota as usize + MAX_INPUT_CHUNK,
|
recv_at_server.len() + recv_at_client.len() <= quota as usize,
|
||||||
"fuzz case {case}: delivered bytes exceed bounded post-check overshoot"
|
"fuzz case {case}: delivered bytes exceed quota"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(&user) <= quota + MAX_INPUT_CHUNK as u64,
|
stats.get_user_total_octets(&user) <= quota,
|
||||||
"fuzz case {case}: accounted bytes exceed bounded post-check overshoot"
|
"fuzz case {case}: accounted bytes exceed quota"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +117,8 @@ async fn model_fuzz_bidirectional_schedule_preserves_prefixes_and_quota_budget()
|
||||||
|
|
||||||
assert_is_prefix(&recv_at_server, &sent_c2s, "C->S final");
|
assert_is_prefix(&recv_at_server, &sent_c2s, "C->S final");
|
||||||
assert_is_prefix(&recv_at_client, &sent_s2c, "S->C final");
|
assert_is_prefix(&recv_at_client, &sent_s2c, "S->C final");
|
||||||
assert!(recv_at_server.len() + recv_at_client.len() <= quota as usize + MAX_INPUT_CHUNK);
|
assert!(recv_at_server.len() + recv_at_client.len() <= quota as usize);
|
||||||
assert!(stats.get_user_quota_used(&user) <= quota + MAX_INPUT_CHUNK as u64);
|
assert!(stats.get_user_total_octets(&user) <= quota);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,7 +209,7 @@ async fn adversarial_dual_direction_cutoff_race_allows_at_most_one_forwarded_byt
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
Err(ProxyError::DataQuotaExceeded { .. })
|
||||||
));
|
));
|
||||||
assert!(stats.get_user_quota_used(user) <= 1);
|
assert!(stats.get_user_total_octets(user) <= 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
|
@ -218,12 +217,9 @@ async fn stress_shared_user_multi_relay_global_quota_never_overshoots_under_mode
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-model-stress-user";
|
let user = "quota-model-stress-user";
|
||||||
let quota = 96u64;
|
let quota = 96u64;
|
||||||
const WORKERS: usize = 6;
|
|
||||||
const MAX_WORKER_CHUNK: u64 = 10;
|
|
||||||
let max_parallel_post_write_overshoot = WORKERS as u64 * MAX_WORKER_CHUNK;
|
|
||||||
|
|
||||||
let mut workers = Vec::new();
|
let mut workers = Vec::new();
|
||||||
for worker_id in 0..WORKERS as u64 {
|
for worker_id in 0..6u64 {
|
||||||
let stats = Arc::clone(&stats);
|
let stats = Arc::clone(&stats);
|
||||||
let user = user.to_string();
|
let user = user.to_string();
|
||||||
|
|
||||||
|
|
@ -309,11 +305,11 @@ async fn stress_shared_user_multi_relay_global_quota_never_overshoots_under_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(user) <= quota + max_parallel_post_write_overshoot,
|
stats.get_user_total_octets(user) <= quota,
|
||||||
"global per-user accounted bytes must stay within bounded post-write overshoot"
|
"global per-user quota must never overshoot under concurrent multi-relay model load"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
delivered_sum as u64 <= quota + max_parallel_post_write_overshoot,
|
delivered_sum <= quota as usize,
|
||||||
"aggregate delivered bytes must stay within bounded post-write overshoot"
|
"aggregate delivered bytes across relays must remain within global quota"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,13 @@ async fn read_available<R: AsyncRead + Unpin>(reader: &mut R, budget_ms: u64) ->
|
||||||
total
|
total
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preload_user_quota(stats: &Stats, user: &str, bytes: u64) {
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn regression_client_chunk_larger_than_remaining_quota_does_not_overshoot_accounting() {
|
async fn regression_client_chunk_larger_than_remaining_quota_does_not_overshoot_accounting() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-overflow-regression-client-chunk";
|
let user = "quota-overflow-regression-client-chunk";
|
||||||
let quota = 10u64;
|
|
||||||
let preloaded = 9u64;
|
|
||||||
let attempted_chunk = [0x11, 0x22, 0x33, 0x44];
|
|
||||||
let max_post_write_overshoot = attempted_chunk.len() as u64;
|
|
||||||
|
|
||||||
// Leave only 1 byte remaining under quota.
|
// Leave only 1 byte remaining under quota.
|
||||||
preload_user_quota(stats.as_ref(), user, preloaded);
|
stats.add_user_octets_from(user, 9);
|
||||||
|
|
||||||
let (mut client_peer, relay_client) = duplex(2048);
|
let (mut client_peer, relay_client) = duplex(2048);
|
||||||
let (relay_server, mut server_peer) = duplex(2048);
|
let (relay_server, mut server_peer) = duplex(2048);
|
||||||
|
|
@ -50,12 +41,15 @@ async fn regression_client_chunk_larger_than_remaining_quota_does_not_overshoot_
|
||||||
512,
|
512,
|
||||||
user,
|
user,
|
||||||
Arc::clone(&stats),
|
Arc::clone(&stats),
|
||||||
Some(quota),
|
Some(10),
|
||||||
Arc::new(BufferPool::new()),
|
Arc::new(BufferPool::new()),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Single chunk attempts to cross remaining budget (4 > 1).
|
// Single chunk attempts to cross remaining budget (4 > 1).
|
||||||
client_peer.write_all(&attempted_chunk).await.unwrap();
|
client_peer
|
||||||
|
.write_all(&[0x11, 0x22, 0x33, 0x44])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
client_peer.shutdown().await.unwrap();
|
client_peer.shutdown().await.unwrap();
|
||||||
|
|
||||||
let forwarded = read_available(&mut server_peer, 60).await;
|
let forwarded = read_available(&mut server_peer, 60).await;
|
||||||
|
|
@ -65,17 +59,17 @@ async fn regression_client_chunk_larger_than_remaining_quota_does_not_overshoot_
|
||||||
.expect("relay must terminate after quota overflow attempt")
|
.expect("relay must terminate after quota overflow attempt")
|
||||||
.expect("relay task must not panic");
|
.expect("relay task must not panic");
|
||||||
|
|
||||||
assert!(
|
assert_eq!(
|
||||||
forwarded <= attempted_chunk.len(),
|
forwarded, 0,
|
||||||
"forwarded bytes must stay within one charged post-write chunk"
|
"overflowing C->S chunk must not be forwarded when it exceeds remaining quota"
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
Err(ProxyError::DataQuotaExceeded { .. })
|
||||||
));
|
));
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(user) <= quota + max_post_write_overshoot,
|
stats.get_user_total_octets(user) <= 10,
|
||||||
"accounted bytes must stay within bounded post-write overshoot"
|
"accounted bytes must never exceed quota after overflowing chunk"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +79,7 @@ async fn regression_client_exact_remaining_quota_forwards_once_then_hard_cuts_of
|
||||||
let user = "quota-overflow-regression-boundary";
|
let user = "quota-overflow-regression-boundary";
|
||||||
|
|
||||||
// Leave exactly 4 bytes remaining.
|
// Leave exactly 4 bytes remaining.
|
||||||
preload_user_quota(stats.as_ref(), user, 6);
|
stats.add_user_octets_from(user, 6);
|
||||||
|
|
||||||
let (mut client_peer, relay_client) = duplex(2048);
|
let (mut client_peer, relay_client) = duplex(2048);
|
||||||
let (relay_server, mut server_peer) = duplex(2048);
|
let (relay_server, mut server_peer) = duplex(2048);
|
||||||
|
|
@ -137,7 +131,7 @@ async fn regression_client_exact_remaining_quota_forwards_once_then_hard_cuts_of
|
||||||
relay_result,
|
relay_result,
|
||||||
Err(ProxyError::DataQuotaExceeded { .. })
|
Err(ProxyError::DataQuotaExceeded { .. })
|
||||||
));
|
));
|
||||||
assert!(stats.get_user_quota_used(user) <= 10);
|
assert!(stats.get_user_total_octets(user) <= 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
|
@ -145,12 +139,9 @@ async fn stress_parallel_relays_same_user_quota_overflow_never_exceeds_cap() {
|
||||||
let stats = Arc::new(Stats::new());
|
let stats = Arc::new(Stats::new());
|
||||||
let user = "quota-overflow-regression-stress";
|
let user = "quota-overflow-regression-stress";
|
||||||
let quota = 12u64;
|
let quota = 12u64;
|
||||||
const WORKERS: usize = 4;
|
|
||||||
const BURST_LEN: usize = 64;
|
|
||||||
let max_parallel_post_write_overshoot = (WORKERS * BURST_LEN) as u64;
|
|
||||||
|
|
||||||
let mut handles = Vec::new();
|
let mut handles = Vec::new();
|
||||||
for _ in 0..WORKERS {
|
for _ in 0..4usize {
|
||||||
let stats = Arc::clone(&stats);
|
let stats = Arc::clone(&stats);
|
||||||
let user = user.to_string();
|
let user = user.to_string();
|
||||||
|
|
||||||
|
|
@ -179,7 +170,7 @@ async fn stress_parallel_relays_same_user_quota_overflow_never_exceeds_cap() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Aggressive sender tries to overflow shared user quota.
|
// Aggressive sender tries to overflow shared user quota.
|
||||||
let burst = vec![0x5Au8; BURST_LEN];
|
let burst = vec![0x5Au8; 64];
|
||||||
let _ = client_peer.write_all(&burst).await;
|
let _ = client_peer.write_all(&burst).await;
|
||||||
let _ = client_peer.shutdown().await;
|
let _ = client_peer.shutdown().await;
|
||||||
|
|
||||||
|
|
@ -206,11 +197,11 @@ async fn stress_parallel_relays_same_user_quota_overflow_never_exceeds_cap() {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
forwarded_sum as u64 <= quota + max_parallel_post_write_overshoot,
|
forwarded_sum <= quota as usize,
|
||||||
"aggregate forwarded bytes must stay within bounded post-write overshoot window"
|
"aggregate forwarded bytes across relays must stay within global user quota"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
stats.get_user_quota_used(user) <= quota + max_parallel_post_write_overshoot,
|
stats.get_user_total_octets(user) <= quota,
|
||||||
"global accounted bytes must stay within bounded post-write overshoot window"
|
"global accounted bytes must stay within quota under overflow stress"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -381,9 +381,7 @@ impl Stats {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Self::touch_user_stats(user_stats);
|
Self::touch_user_stats(user_stats);
|
||||||
user_stats
|
user_stats.octets_from_client.fetch_add(bytes, Ordering::Relaxed);
|
||||||
.octets_from_client
|
|
||||||
.fetch_add(bytes, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -392,9 +390,7 @@ impl Stats {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Self::touch_user_stats(user_stats);
|
Self::touch_user_stats(user_stats);
|
||||||
user_stats
|
user_stats.octets_to_client.fetch_add(bytes, Ordering::Relaxed);
|
||||||
.octets_to_client
|
|
||||||
.fetch_add(bytes, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -816,8 +812,7 @@ impl Stats {
|
||||||
}
|
}
|
||||||
pub fn increment_me_d2c_data_frames_total(&self) {
|
pub fn increment_me_d2c_data_frames_total(&self) {
|
||||||
if self.telemetry_me_allows_normal() {
|
if self.telemetry_me_allows_normal() {
|
||||||
self.me_d2c_data_frames_total
|
self.me_d2c_data_frames_total.fetch_add(1, Ordering::Relaxed);
|
||||||
.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn increment_me_d2c_ack_frames_total(&self) {
|
pub fn increment_me_d2c_ack_frames_total(&self) {
|
||||||
|
|
@ -1713,8 +1708,7 @@ impl Stats {
|
||||||
self.me_d2c_batch_bytes_bucket_1k_4k.load(Ordering::Relaxed)
|
self.me_d2c_batch_bytes_bucket_1k_4k.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
pub fn get_me_d2c_batch_bytes_bucket_4k_16k(&self) -> u64 {
|
pub fn get_me_d2c_batch_bytes_bucket_4k_16k(&self) -> u64 {
|
||||||
self.me_d2c_batch_bytes_bucket_4k_16k
|
self.me_d2c_batch_bytes_bucket_4k_16k.load(Ordering::Relaxed)
|
||||||
.load(Ordering::Relaxed)
|
|
||||||
}
|
}
|
||||||
pub fn get_me_d2c_batch_bytes_bucket_16k_64k(&self) -> u64 {
|
pub fn get_me_d2c_batch_bytes_bucket_16k_64k(&self) -> u64 {
|
||||||
self.me_d2c_batch_bytes_bucket_16k_64k
|
self.me_d2c_batch_bytes_bucket_16k_64k
|
||||||
|
|
@ -2377,8 +2371,8 @@ impl ReplayStats {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::MeTelemetryLevel;
|
use crate::config::MeTelemetryLevel;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stats_shared_counters() {
|
fn test_stats_shared_counters() {
|
||||||
|
|
@ -2586,56 +2580,6 @@ mod tests {
|
||||||
assert_eq!(user_stats.quota_used(), limit);
|
assert_eq!(user_stats.quota_used(), limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_quota_reserve_200x_1k_reaches_100k_without_overshoot() {
|
|
||||||
let user_stats = Arc::new(UserStats::default());
|
|
||||||
let successes = Arc::new(AtomicU64::new(0));
|
|
||||||
let failures = Arc::new(AtomicU64::new(0));
|
|
||||||
let attempts = 200usize;
|
|
||||||
let reserve_bytes = 1_024u64;
|
|
||||||
let limit = 100 * 1_024u64;
|
|
||||||
let mut workers = Vec::with_capacity(attempts);
|
|
||||||
|
|
||||||
for _ in 0..attempts {
|
|
||||||
let user_stats = user_stats.clone();
|
|
||||||
let successes = successes.clone();
|
|
||||||
let failures = failures.clone();
|
|
||||||
workers.push(std::thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
match user_stats.quota_try_reserve(reserve_bytes, limit) {
|
|
||||||
Ok(_) => {
|
|
||||||
successes.fetch_add(1, Ordering::Relaxed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(QuotaReserveError::LimitExceeded) => {
|
|
||||||
failures.fetch_add(1, Ordering::Relaxed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(QuotaReserveError::Contended) => {
|
|
||||||
std::hint::spin_loop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
for worker in workers {
|
|
||||||
worker.join().expect("reservation worker must finish");
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
successes.load(Ordering::Relaxed),
|
|
||||||
100,
|
|
||||||
"exactly 100 reservations of 1 KiB must fit into a 100 KiB quota"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
failures.load(Ordering::Relaxed),
|
|
||||||
100,
|
|
||||||
"remaining workers must fail once quota is fully reserved"
|
|
||||||
);
|
|
||||||
assert_eq!(user_stats.quota_used(), limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_quota_used_is_authoritative_and_independent_from_octets_telemetry() {
|
fn test_quota_used_is_authoritative_and_independent_from_octets_telemetry() {
|
||||||
let stats = Stats::new();
|
let stats = Stats::new();
|
||||||
|
|
@ -2650,33 +2594,6 @@ mod tests {
|
||||||
assert_eq!(stats.get_user_total_octets(user), 5);
|
assert_eq!(stats.get_user_total_octets(user), 5);
|
||||||
assert_eq!(stats.get_user_quota_used(user), 7);
|
assert_eq!(stats.get_user_quota_used(user), 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cached_handle_survives_map_cleanup_until_last_drop() {
|
|
||||||
let stats = Stats::new();
|
|
||||||
let user = "quota-handle-lifetime-user";
|
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(user);
|
|
||||||
let weak = Arc::downgrade(&user_stats);
|
|
||||||
|
|
||||||
stats.user_stats.remove(user);
|
|
||||||
assert!(
|
|
||||||
stats.user_stats.get(user).is_none(),
|
|
||||||
"map cleanup should remove idle entry"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
weak.upgrade().is_some(),
|
|
||||||
"cached handle must keep user stats object alive after map removal"
|
|
||||||
);
|
|
||||||
|
|
||||||
stats.quota_charge_post_write(user_stats.as_ref(), 3);
|
|
||||||
assert_eq!(user_stats.quota_used(), 3);
|
|
||||||
|
|
||||||
drop(user_stats);
|
|
||||||
assert!(
|
|
||||||
weak.upgrade().is_none(),
|
|
||||||
"user stats object must be dropped after the last cached handle is released"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,7 @@ fn padding_rounding_equivalent_for_extensive_safe_domain() {
|
||||||
let old = old_padding_round_up_to_4(len).expect("old expression must be safe");
|
let old = old_padding_round_up_to_4(len).expect("old expression must be safe");
|
||||||
let new = new_padding_round_up_to_4(len).expect("new expression must be safe");
|
let new = new_padding_round_up_to_4(len).expect("new expression must be safe");
|
||||||
assert_eq!(old, new, "mismatch for len={len}");
|
assert_eq!(old, new, "mismatch for len={len}");
|
||||||
assert!(
|
assert!(new >= len, "rounded length must not shrink: len={len}, out={new}");
|
||||||
new >= len,
|
|
||||||
"rounded length must not shrink: len={len}, out={new}"
|
|
||||||
);
|
|
||||||
assert_eq!(new % 4, 0, "rounded length must stay 4-byte aligned");
|
assert_eq!(new % 4, 0, "rounded length must stay 4-byte aligned");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,7 @@ async fn encapsulation_repeated_queue_poison_recovery_preserves_forward_progress
|
||||||
let ip_primary = ip_from_idx(10_001);
|
let ip_primary = ip_from_idx(10_001);
|
||||||
let ip_alt = ip_from_idx(10_002);
|
let ip_alt = ip_from_idx(10_002);
|
||||||
|
|
||||||
tracker
|
tracker.check_and_add("encap-poison", ip_primary).await.unwrap();
|
||||||
.check_and_add("encap-poison", ip_primary)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for _ in 0..128 {
|
for _ in 0..128 {
|
||||||
let queue = tracker.cleanup_queue_mutex_for_tests();
|
let queue = tracker.cleanup_queue_mutex_for_tests();
|
||||||
|
|
|
||||||
|
|
@ -108,28 +108,16 @@ fn build_client_hello(sni: &str, rng: &SecureRandom) -> Vec<u8> {
|
||||||
// Session ID: empty
|
// Session ID: empty
|
||||||
body.push(0);
|
body.push(0);
|
||||||
|
|
||||||
// Cipher suites:
|
// Cipher suites (common minimal set, TLS1.3 + a few 1.2 fallbacks)
|
||||||
// - TLS1.3 set
|
let cipher_suites: [u8; 10] = [
|
||||||
// - broad TLS1.2 ECDHE set for RSA/ECDSA cert chains
|
0x13, 0x01, // TLS_AES_128_GCM_SHA256
|
||||||
// This keeps raw probing compatible with common production frontends that
|
0x13, 0x02, // TLS_AES_256_GCM_SHA384
|
||||||
// still negotiate TLS1.2.
|
0x13, 0x03, // TLS_CHACHA20_POLY1305_SHA256
|
||||||
let cipher_suites: [u16; 11] = [
|
0x00, 0x2f, // TLS_RSA_WITH_AES_128_CBC_SHA (legacy)
|
||||||
0x1301, // TLS_AES_128_GCM_SHA256
|
0x00, 0xff, // RENEGOTIATION_INFO_SCSV
|
||||||
0x1302, // TLS_AES_256_GCM_SHA384
|
|
||||||
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
|
||||||
0xc02b, // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
|
||||||
0xc02c, // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
|
||||||
0xcca9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
|
||||||
0xc02f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
|
||||||
0xc030, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
|
||||||
0xcca8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
||||||
0x009e, // TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
|
|
||||||
0x00ff, // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
|
|
||||||
];
|
];
|
||||||
body.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
|
body.extend_from_slice(&(cipher_suites.len() as u16).to_be_bytes());
|
||||||
for suite in cipher_suites {
|
body.extend_from_slice(&cipher_suites);
|
||||||
body.extend_from_slice(&suite.to_be_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compression methods: null only
|
// Compression methods: null only
|
||||||
body.push(1);
|
body.push(1);
|
||||||
|
|
@ -159,7 +147,7 @@ fn build_client_hello(sni: &str, rng: &SecureRandom) -> Vec<u8> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// signature_algorithms
|
// signature_algorithms
|
||||||
let sig_algs: [u16; 4] = [0x0804, 0x0805, 0x0403, 0x0503]; // rsa_pss_rsae_sha256/384, ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384
|
let sig_algs: [u16; 4] = [0x0804, 0x0805, 0x0403, 0x0503]; // rsa_pss_rsae_sha256/384, ecdsa_secp256r1_sha256, rsa_pkcs1_sha256
|
||||||
exts.extend_from_slice(&0x000du16.to_be_bytes());
|
exts.extend_from_slice(&0x000du16.to_be_bytes());
|
||||||
exts.extend_from_slice(&((2 + sig_algs.len() * 2) as u16).to_be_bytes());
|
exts.extend_from_slice(&((2 + sig_algs.len() * 2) as u16).to_be_bytes());
|
||||||
exts.extend_from_slice(&(sig_algs.len() as u16 * 2).to_be_bytes());
|
exts.extend_from_slice(&(sig_algs.len() as u16 * 2).to_be_bytes());
|
||||||
|
|
@ -824,8 +812,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encode_tls13_certificate_message_single_cert() {
|
fn test_encode_tls13_certificate_message_single_cert() {
|
||||||
let cert = vec![0x30, 0x03, 0x02, 0x01, 0x01];
|
let cert = vec![0x30, 0x03, 0x02, 0x01, 0x01];
|
||||||
let message =
|
let message = encode_tls13_certificate_message(std::slice::from_ref(&cert))
|
||||||
encode_tls13_certificate_message(std::slice::from_ref(&cert)).expect("message");
|
.expect("message");
|
||||||
|
|
||||||
assert_eq!(message[0], 0x0b);
|
assert_eq!(message[0], 0x0b);
|
||||||
assert_eq!(read_u24(&message[1..4]), message.len() - 4);
|
assert_eq!(read_u24(&message[1..4]), message.len() - 4);
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,17 @@ use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
|
|
||||||
use super::MePool;
|
use super::MePool;
|
||||||
use super::http_fetch::https_get;
|
|
||||||
use super::rotation::{MeReinitTrigger, enqueue_reinit_trigger};
|
use super::rotation::{MeReinitTrigger, enqueue_reinit_trigger};
|
||||||
use super::secret::download_proxy_secret_with_max_len_via_upstream;
|
use super::secret::download_proxy_secret_with_max_len;
|
||||||
use super::selftest::record_timeskew_sample;
|
use super::selftest::record_timeskew_sample;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
async fn retry_fetch(url: &str, upstream: Option<Arc<UpstreamManager>>) -> Option<ProxyConfigData> {
|
async fn retry_fetch(url: &str) -> Option<ProxyConfigData> {
|
||||||
let delays = [1u64, 5, 15];
|
let delays = [1u64, 5, 15];
|
||||||
for (i, d) in delays.iter().enumerate() {
|
for (i, d) in delays.iter().enumerate() {
|
||||||
match fetch_proxy_config_via_upstream(url, upstream.clone()).await {
|
match fetch_proxy_config(url).await {
|
||||||
Ok(cfg) => return Some(cfg),
|
Ok(cfg) => return Some(cfg),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if i == delays.len() - 1 {
|
if i == delays.len() - 1 {
|
||||||
|
|
@ -97,19 +95,14 @@ pub async fn save_proxy_config_cache(path: &str, raw_text: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_proxy_config_with_raw(url: &str) -> Result<(ProxyConfigData, String)> {
|
pub async fn fetch_proxy_config_with_raw(url: &str) -> Result<(ProxyConfigData, String)> {
|
||||||
fetch_proxy_config_with_raw_via_upstream(url, None).await
|
let resp = reqwest::get(url).await.map_err(|e| {
|
||||||
}
|
crate::error::ProxyError::Proxy(format!("fetch_proxy_config GET failed: {e}"))
|
||||||
|
})?;
|
||||||
|
let http_status = resp.status().as_u16();
|
||||||
|
|
||||||
pub async fn fetch_proxy_config_with_raw_via_upstream(
|
if let Some(date) = resp.headers().get(reqwest::header::DATE)
|
||||||
url: &str,
|
&& let Ok(date_str) = date.to_str()
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<(ProxyConfigData, String)> {
|
|
||||||
let resp = https_get(url, upstream).await?;
|
|
||||||
let http_status = resp.status;
|
|
||||||
|
|
||||||
if let Some(date_str) = resp.date_header.as_deref()
|
|
||||||
&& let Ok(server_time) = httpdate::parse_http_date(date_str)
|
&& let Ok(server_time) = httpdate::parse_http_date(date_str)
|
||||||
&& let Ok(skew) = SystemTime::now()
|
&& let Ok(skew) = SystemTime::now()
|
||||||
.duration_since(server_time)
|
.duration_since(server_time)
|
||||||
|
|
@ -130,7 +123,9 @@ pub async fn fetch_proxy_config_with_raw_via_upstream(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = String::from_utf8_lossy(&resp.body).into_owned();
|
let text = resp.text().await.map_err(|e| {
|
||||||
|
crate::error::ProxyError::Proxy(format!("fetch_proxy_config read failed: {e}"))
|
||||||
|
})?;
|
||||||
let parsed = parse_proxy_config_text(&text, http_status);
|
let parsed = parse_proxy_config_text(&text, http_status);
|
||||||
Ok((parsed, text))
|
Ok((parsed, text))
|
||||||
}
|
}
|
||||||
|
|
@ -265,16 +260,8 @@ fn parse_proxy_line(line: &str) -> Option<(i32, IpAddr, u16)> {
|
||||||
Some((dc, ip, port))
|
Some((dc, ip, port))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_proxy_config(url: &str) -> Result<ProxyConfigData> {
|
pub async fn fetch_proxy_config(url: &str) -> Result<ProxyConfigData> {
|
||||||
fetch_proxy_config_via_upstream(url, None).await
|
fetch_proxy_config_with_raw(url)
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_proxy_config_via_upstream(
|
|
||||||
url: &str,
|
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<ProxyConfigData> {
|
|
||||||
fetch_proxy_config_with_raw_via_upstream(url, upstream)
|
|
||||||
.await
|
.await
|
||||||
.map(|(parsed, _raw)| parsed)
|
.map(|(parsed, _raw)| parsed)
|
||||||
}
|
}
|
||||||
|
|
@ -313,7 +300,6 @@ async fn run_update_cycle(
|
||||||
state: &mut UpdaterState,
|
state: &mut UpdaterState,
|
||||||
reinit_tx: &mpsc::Sender<MeReinitTrigger>,
|
reinit_tx: &mpsc::Sender<MeReinitTrigger>,
|
||||||
) {
|
) {
|
||||||
let upstream = pool.upstream.clone();
|
|
||||||
pool.update_runtime_reinit_policy(
|
pool.update_runtime_reinit_policy(
|
||||||
cfg.general.hardswap,
|
cfg.general.hardswap,
|
||||||
cfg.general.me_pool_drain_ttl_secs,
|
cfg.general.me_pool_drain_ttl_secs,
|
||||||
|
|
@ -368,7 +354,7 @@ async fn run_update_cycle(
|
||||||
let mut maps_changed = false;
|
let mut maps_changed = false;
|
||||||
|
|
||||||
let mut ready_v4: Option<(ProxyConfigData, u64)> = None;
|
let mut ready_v4: Option<(ProxyConfigData, u64)> = None;
|
||||||
let cfg_v4 = retry_fetch("https://core.telegram.org/getProxyConfig", upstream.clone()).await;
|
let cfg_v4 = retry_fetch("https://core.telegram.org/getProxyConfig").await;
|
||||||
if let Some(cfg_v4) = cfg_v4
|
if let Some(cfg_v4) = cfg_v4
|
||||||
&& snapshot_passes_guards(cfg, &cfg_v4, "getProxyConfig")
|
&& snapshot_passes_guards(cfg, &cfg_v4, "getProxyConfig")
|
||||||
{
|
{
|
||||||
|
|
@ -392,11 +378,7 @@ async fn run_update_cycle(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ready_v6: Option<(ProxyConfigData, u64)> = None;
|
let mut ready_v6: Option<(ProxyConfigData, u64)> = None;
|
||||||
let cfg_v6 = retry_fetch(
|
let cfg_v6 = retry_fetch("https://core.telegram.org/getProxyConfigV6").await;
|
||||||
"https://core.telegram.org/getProxyConfigV6",
|
|
||||||
upstream.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if let Some(cfg_v6) = cfg_v6
|
if let Some(cfg_v6) = cfg_v6
|
||||||
&& snapshot_passes_guards(cfg, &cfg_v6, "getProxyConfigV6")
|
&& snapshot_passes_guards(cfg, &cfg_v6, "getProxyConfigV6")
|
||||||
{
|
{
|
||||||
|
|
@ -474,12 +456,7 @@ async fn run_update_cycle(
|
||||||
pool.reset_stun_state();
|
pool.reset_stun_state();
|
||||||
|
|
||||||
if cfg.general.proxy_secret_rotate_runtime {
|
if cfg.general.proxy_secret_rotate_runtime {
|
||||||
match download_proxy_secret_with_max_len_via_upstream(
|
match download_proxy_secret_with_max_len(cfg.general.proxy_secret_len_max).await {
|
||||||
cfg.general.proxy_secret_len_max,
|
|
||||||
upstream,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(secret) => {
|
Ok(secret) => {
|
||||||
let secret_hash = hash_secret(&secret);
|
let secret_hash = hash_secret(&secret);
|
||||||
let stable_hits = state.secret.observe(secret_hash);
|
let stable_hits = state.secret.observe(secret_hash);
|
||||||
|
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use http_body_util::{BodyExt, Empty};
|
|
||||||
use hyper::header::{CONNECTION, DATE, HOST, USER_AGENT};
|
|
||||||
use hyper::{Method, Request};
|
|
||||||
use hyper_util::rt::TokioIo;
|
|
||||||
use rustls::pki_types::ServerName;
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::time::timeout;
|
|
||||||
use tokio_rustls::TlsConnector;
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::error::{ProxyError, Result};
|
|
||||||
use crate::network::dns_overrides::resolve_socket_addr;
|
|
||||||
use crate::transport::{UpstreamManager, UpstreamStream};
|
|
||||||
|
|
||||||
const HTTP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
|
||||||
const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
|
|
||||||
|
|
||||||
pub(crate) struct HttpsGetResponse {
|
|
||||||
pub(crate) status: u16,
|
|
||||||
pub(crate) date_header: Option<String>,
|
|
||||||
pub(crate) body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_tls_client_config() -> Arc<rustls::ClientConfig> {
|
|
||||||
let mut root_store = rustls::RootCertStore::empty();
|
|
||||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
|
||||||
let config = rustls::ClientConfig::builder()
|
|
||||||
.with_root_certificates(root_store)
|
|
||||||
.with_no_client_auth();
|
|
||||||
Arc::new(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_host_port_path(url: &str) -> Result<(String, u16, String)> {
|
|
||||||
let parsed =
|
|
||||||
url::Url::parse(url).map_err(|e| ProxyError::Proxy(format!("invalid URL '{url}': {e}")))?;
|
|
||||||
if parsed.scheme() != "https" {
|
|
||||||
return Err(ProxyError::Proxy(format!(
|
|
||||||
"unsupported URL scheme '{}': only https is supported",
|
|
||||||
parsed.scheme()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let host = parsed
|
|
||||||
.host_str()
|
|
||||||
.ok_or_else(|| ProxyError::Proxy(format!("URL has no host: {url}")))?
|
|
||||||
.to_string();
|
|
||||||
let port = parsed
|
|
||||||
.port_or_known_default()
|
|
||||||
.ok_or_else(|| ProxyError::Proxy(format!("URL has no known port: {url}")))?;
|
|
||||||
|
|
||||||
let mut path = parsed.path().to_string();
|
|
||||||
if path.is_empty() {
|
|
||||||
path.push('/');
|
|
||||||
}
|
|
||||||
if let Some(query) = parsed.query() {
|
|
||||||
path.push('?');
|
|
||||||
path.push_str(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((host, port, path))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn resolve_target_addr(host: &str, port: u16) -> Result<std::net::SocketAddr> {
|
|
||||||
if let Some(addr) = resolve_socket_addr(host, port) {
|
|
||||||
return Ok(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
let addrs: Vec<std::net::SocketAddr> = tokio::net::lookup_host((host, port))
|
|
||||||
.await
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("DNS resolve failed for {host}:{port}: {e}")))?
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Some(addr) = addrs.iter().copied().find(|addr| addr.is_ipv4()) {
|
|
||||||
return Ok(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs
|
|
||||||
.first()
|
|
||||||
.copied()
|
|
||||||
.ok_or_else(|| ProxyError::Proxy(format!("DNS returned no addresses for {host}:{port}")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect_https_transport(
|
|
||||||
host: &str,
|
|
||||||
port: u16,
|
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<UpstreamStream> {
|
|
||||||
if let Some(manager) = upstream {
|
|
||||||
let target = resolve_target_addr(host, port).await?;
|
|
||||||
return timeout(HTTP_CONNECT_TIMEOUT, manager.connect(target, None, None))
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("upstream connect timeout for {host}:{port}")))?
|
|
||||||
.map_err(|e| {
|
|
||||||
ProxyError::Proxy(format!("upstream connect failed for {host}:{port}: {e}"))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(addr) = resolve_socket_addr(host, port) {
|
|
||||||
let stream = timeout(HTTP_CONNECT_TIMEOUT, TcpStream::connect(addr))
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("connect timeout for {host}:{port}")))?
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("connect failed for {host}:{port}: {e}")))?;
|
|
||||||
return Ok(UpstreamStream::Tcp(stream));
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = timeout(HTTP_CONNECT_TIMEOUT, TcpStream::connect((host, port)))
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("connect timeout for {host}:{port}")))?
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("connect failed for {host}:{port}: {e}")))?;
|
|
||||||
Ok(UpstreamStream::Tcp(stream))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn https_get(
|
|
||||||
url: &str,
|
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<HttpsGetResponse> {
|
|
||||||
let (host, port, path_and_query) = extract_host_port_path(url)?;
|
|
||||||
let stream = connect_https_transport(&host, port, upstream).await?;
|
|
||||||
|
|
||||||
let server_name = ServerName::try_from(host.clone())
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("invalid TLS server name: {host}")))?;
|
|
||||||
let connector = TlsConnector::from(build_tls_client_config());
|
|
||||||
let tls_stream = timeout(HTTP_REQUEST_TIMEOUT, connector.connect(server_name, stream))
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("TLS handshake timeout for {host}:{port}")))?
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("TLS handshake failed for {host}:{port}: {e}")))?;
|
|
||||||
|
|
||||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(tls_stream))
|
|
||||||
.await
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("HTTP handshake failed for {host}:{port}: {e}")))?;
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = connection.await {
|
|
||||||
debug!(error = %e, "HTTPS fetch connection task failed");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let host_header = if port == 443 {
|
|
||||||
host.clone()
|
|
||||||
} else {
|
|
||||||
format!("{host}:{port}")
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = Request::builder()
|
|
||||||
.method(Method::GET)
|
|
||||||
.uri(path_and_query)
|
|
||||||
.header(HOST, host_header)
|
|
||||||
.header(USER_AGENT, "telemt-middle-proxy/1")
|
|
||||||
.header(CONNECTION, "close")
|
|
||||||
.body(Empty::<bytes::Bytes>::new())
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("build HTTP request failed for {url}: {e}")))?;
|
|
||||||
|
|
||||||
let response = timeout(HTTP_REQUEST_TIMEOUT, sender.send_request(request))
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("HTTP request timeout for {url}")))?
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("HTTP request failed for {url}: {e}")))?;
|
|
||||||
|
|
||||||
let status = response.status().as_u16();
|
|
||||||
let date_header = response
|
|
||||||
.headers()
|
|
||||||
.get(DATE)
|
|
||||||
.and_then(|value| value.to_str().ok())
|
|
||||||
.map(|value| value.to_string());
|
|
||||||
|
|
||||||
let body = timeout(HTTP_REQUEST_TIMEOUT, response.into_body().collect())
|
|
||||||
.await
|
|
||||||
.map_err(|_| ProxyError::Proxy(format!("HTTP body read timeout for {url}")))?
|
|
||||||
.map_err(|e| ProxyError::Proxy(format!("HTTP body read failed for {url}: {e}")))?
|
|
||||||
.to_bytes()
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
Ok(HttpsGetResponse {
|
|
||||||
status,
|
|
||||||
date_header,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,6 @@ mod health_integration_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/health_regression_tests.rs"]
|
#[path = "tests/health_regression_tests.rs"]
|
||||||
mod health_regression_tests;
|
mod health_regression_tests;
|
||||||
mod http_fetch;
|
|
||||||
mod ping;
|
mod ping;
|
||||||
mod pool;
|
mod pool;
|
||||||
mod pool_config;
|
mod pool_config;
|
||||||
|
|
@ -45,8 +44,7 @@ use bytes::Bytes;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use config_updater::{
|
pub use config_updater::{
|
||||||
ProxyConfigData, fetch_proxy_config, fetch_proxy_config_via_upstream,
|
ProxyConfigData, fetch_proxy_config, fetch_proxy_config_with_raw, load_proxy_config_cache,
|
||||||
fetch_proxy_config_with_raw, fetch_proxy_config_with_raw_via_upstream, load_proxy_config_cache,
|
|
||||||
me_config_updater, save_proxy_config_cache,
|
me_config_updater, save_proxy_config_cache,
|
||||||
};
|
};
|
||||||
pub use health::{me_drain_timeout_enforcer, me_health_monitor, me_zombie_writer_watchdog};
|
pub use health::{me_drain_timeout_enforcer, me_health_monitor, me_zombie_writer_watchdog};
|
||||||
|
|
@ -59,8 +57,7 @@ pub use pool::MePool;
|
||||||
pub use pool_nat::{detect_public_ip, stun_probe};
|
pub use pool_nat::{detect_public_ip, stun_probe};
|
||||||
pub use registry::ConnRegistry;
|
pub use registry::ConnRegistry;
|
||||||
pub use rotation::{MeReinitTrigger, me_reinit_scheduler, me_rotation_task};
|
pub use rotation::{MeReinitTrigger, me_reinit_scheduler, me_rotation_task};
|
||||||
#[allow(unused_imports)]
|
pub use secret::fetch_proxy_secret;
|
||||||
pub use secret::{fetch_proxy_secret, fetch_proxy_secret_with_upstream};
|
|
||||||
pub(crate) use selftest::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots};
|
pub(crate) use selftest::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots};
|
||||||
pub use wire::proto_flags_for_tag;
|
pub use wire::proto_flags_for_tag;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,9 @@ impl MePool {
|
||||||
WriterContour::Draining => "draining",
|
WriterContour::Draining => "draining",
|
||||||
};
|
};
|
||||||
|
|
||||||
if !draining && let Some(dc_idx) = dc {
|
if !draining
|
||||||
|
&& let Some(dc_idx) = dc
|
||||||
|
{
|
||||||
*live_writers_by_dc_endpoint
|
*live_writers_by_dc_endpoint
|
||||||
.entry((dc_idx, endpoint))
|
.entry((dc_idx, endpoint))
|
||||||
.or_insert(0) += 1;
|
.or_insert(0) += 1;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
use httpdate;
|
use httpdate;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use super::http_fetch::https_get;
|
|
||||||
use super::selftest::record_timeskew_sample;
|
use super::selftest::record_timeskew_sample;
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
|
|
||||||
pub const PROXY_SECRET_MIN_LEN: usize = 32;
|
pub const PROXY_SECRET_MIN_LEN: usize = 32;
|
||||||
|
|
||||||
|
|
@ -36,21 +33,11 @@ pub(super) fn validate_proxy_secret_len(data_len: usize, max_len: usize) -> Resu
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch Telegram proxy-secret binary.
|
/// Fetch Telegram proxy-secret binary.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn fetch_proxy_secret(cache_path: Option<&str>, max_len: usize) -> Result<Vec<u8>> {
|
pub async fn fetch_proxy_secret(cache_path: Option<&str>, max_len: usize) -> Result<Vec<u8>> {
|
||||||
fetch_proxy_secret_with_upstream(cache_path, max_len, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch Telegram proxy-secret binary, optionally through upstream routing.
|
|
||||||
pub async fn fetch_proxy_secret_with_upstream(
|
|
||||||
cache_path: Option<&str>,
|
|
||||||
max_len: usize,
|
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let cache = cache_path.unwrap_or("proxy-secret");
|
let cache = cache_path.unwrap_or("proxy-secret");
|
||||||
|
|
||||||
// 1) Try fresh download first.
|
// 1) Try fresh download first.
|
||||||
match download_proxy_secret_with_max_len_via_upstream(max_len, upstream).await {
|
match download_proxy_secret_with_max_len(max_len).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
if let Err(e) = tokio::fs::write(cache, &data).await {
|
if let Err(e) = tokio::fs::write(cache, &data).await {
|
||||||
warn!(error = %e, "Failed to cache proxy-secret (non-fatal)");
|
warn!(error = %e, "Failed to cache proxy-secret (non-fatal)");
|
||||||
|
|
@ -89,25 +76,20 @@ pub async fn fetch_proxy_secret_with_upstream(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn download_proxy_secret_with_max_len(max_len: usize) -> Result<Vec<u8>> {
|
pub async fn download_proxy_secret_with_max_len(max_len: usize) -> Result<Vec<u8>> {
|
||||||
download_proxy_secret_with_max_len_via_upstream(max_len, None).await
|
let resp = reqwest::get("https://core.telegram.org/getProxySecret")
|
||||||
}
|
.await
|
||||||
|
.map_err(|e| ProxyError::Proxy(format!("Failed to download proxy-secret: {e}")))?;
|
||||||
|
|
||||||
pub async fn download_proxy_secret_with_max_len_via_upstream(
|
if !resp.status().is_success() {
|
||||||
max_len: usize,
|
|
||||||
upstream: Option<Arc<UpstreamManager>>,
|
|
||||||
) -> Result<Vec<u8>> {
|
|
||||||
let resp = https_get("https://core.telegram.org/getProxySecret", upstream).await?;
|
|
||||||
|
|
||||||
if !(200..=299).contains(&resp.status) {
|
|
||||||
return Err(ProxyError::Proxy(format!(
|
return Err(ProxyError::Proxy(format!(
|
||||||
"proxy-secret download HTTP {}",
|
"proxy-secret download HTTP {}",
|
||||||
resp.status
|
resp.status()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(date_str) = resp.date_header.as_deref()
|
if let Some(date) = resp.headers().get(reqwest::header::DATE)
|
||||||
|
&& let Ok(date_str) = date.to_str()
|
||||||
&& let Ok(server_time) = httpdate::parse_http_date(date_str)
|
&& let Ok(server_time) = httpdate::parse_http_date(date_str)
|
||||||
&& let Ok(skew) = SystemTime::now()
|
&& let Ok(skew) = SystemTime::now()
|
||||||
.duration_since(server_time)
|
.duration_since(server_time)
|
||||||
|
|
@ -128,7 +110,11 @@ pub async fn download_proxy_secret_with_max_len_via_upstream(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = resp.body;
|
let data = resp
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ProxyError::Proxy(format!("Read proxy-secret body: {e}")))?
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
validate_proxy_secret_len(data.len(), max_len)?;
|
validate_proxy_secret_len(data.len(), max_len)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -201,10 +201,7 @@ impl ConnectionPool {
|
||||||
pub async fn close_all(&self) {
|
pub async fn close_all(&self) {
|
||||||
let pools_snapshot: Vec<(SocketAddr, Arc<Mutex<PoolInner>>)> = {
|
let pools_snapshot: Vec<(SocketAddr, Arc<Mutex<PoolInner>>)> = {
|
||||||
let pools = self.pools.read();
|
let pools = self.pools.read();
|
||||||
pools
|
pools.iter().map(|(addr, pool)| (*addr, Arc::clone(pool))).collect()
|
||||||
.iter()
|
|
||||||
.map(|(addr, pool)| (*addr, Arc::clone(pool)))
|
|
||||||
.collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (addr, pool) in pools_snapshot {
|
for (addr, pool) in pools_snapshot {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue