mirror of
https://github.com/telemt/telemt.git
synced 2026-07-25 06:56:15 +03:00
Compare commits
1 Commits
3.3.35
..
ae908fbd70
| Author | SHA1 | Date | |
|---|---|---|---|
| ae908fbd70 |
@@ -7,16 +7,7 @@ queries:
|
|||||||
- uses: security-and-quality
|
- uses: security-and-quality
|
||||||
- uses: ./.github/codeql/queries
|
- uses: ./.github/codeql/queries
|
||||||
|
|
||||||
paths-ignore:
|
|
||||||
- "**/tests/**"
|
|
||||||
- "**/test/**"
|
|
||||||
- "**/*_test.rs"
|
|
||||||
- "**/*/tests.rs"
|
|
||||||
query-filters:
|
query-filters:
|
||||||
- exclude:
|
|
||||||
tags:
|
|
||||||
- test
|
|
||||||
|
|
||||||
- exclude:
|
- exclude:
|
||||||
id:
|
id:
|
||||||
- rust/unwrap-on-option
|
- rust/unwrap-on-option
|
||||||
|
|||||||
@@ -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
|
|
||||||
+225
-278
@@ -4,15 +4,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+-*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Release tag (example: 3.3.15)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}-${{ github.event.inputs.tag || 'auto' }}
|
group: release-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -20,318 +16,258 @@ permissions:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: "1"
|
||||||
BINARY_NAME: telemt
|
BINARY_NAME: telemt
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Prepare
|
name: Prepare metadata
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.vars.outputs.version }}
|
version: ${{ steps.meta.outputs.version }}
|
||||||
prerelease: ${{ steps.vars.outputs.prerelease }}
|
prerelease: ${{ steps.meta.outputs.prerelease }}
|
||||||
|
release_enabled: ${{ steps.meta.outputs.release_enabled }}
|
||||||
steps:
|
steps:
|
||||||
- name: Resolve version
|
- name: Derive version
|
||||||
id: vars
|
id: meta
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||||
VERSION="${{ github.event.inputs.tag }}"
|
|
||||||
else
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
|
RELEASE_ENABLED=true
|
||||||
|
else
|
||||||
|
VERSION="manual-${GITHUB_SHA::7}"
|
||||||
|
RELEASE_ENABLED=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
VERSION="${VERSION#refs/tags/}"
|
if [[ "$VERSION" == *"-alpha"* || "$VERSION" == *"-beta"* || "$VERSION" == *"-rc"* ]]; then
|
||||||
|
|
||||||
if [ -z "${VERSION}" ]; then
|
|
||||||
echo "Release version is empty" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${VERSION}" == *-* ]]; then
|
|
||||||
PRERELEASE=true
|
PRERELEASE=true
|
||||||
else
|
else
|
||||||
PRERELEASE=false
|
PRERELEASE=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
echo "prerelease=${PRERELEASE}" >> "${GITHUB_OUTPUT}"
|
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_enabled=$RELEASE_ENABLED" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# ==========================
|
checks:
|
||||||
# GNU / glibc
|
name: Checks
|
||||||
# ==========================
|
|
||||||
build-gnu:
|
|
||||||
name: GNU ${{ matrix.asset }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: prepare
|
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: rust:slim-bookworm
|
image: debian:trixie
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
|
||||||
asset: telemt-x86_64-linux-gnu
|
|
||||||
cpu: baseline
|
|
||||||
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
|
||||||
asset: telemt-x86_64-v3-linux-gnu
|
|
||||||
cpu: v3
|
|
||||||
|
|
||||||
- target: aarch64-unknown-linux-gnu
|
|
||||||
asset: telemt-aarch64-linux-gnu
|
|
||||||
cpu: generic
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Install system dependencies
|
||||||
|
shell: bash
|
||||||
- uses: dtolnay/rust-toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
targets: |
|
|
||||||
x86_64-unknown-linux-gnu
|
|
||||||
aarch64-unknown-linux-gnu
|
|
||||||
|
|
||||||
- name: Install deps
|
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y \
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
build-essential \
|
build-essential \
|
||||||
clang \
|
|
||||||
lld \
|
|
||||||
pkg-config \
|
pkg-config \
|
||||||
gcc-aarch64-linux-gnu \
|
clang \
|
||||||
g++-aarch64-linux-gnu
|
llvm \
|
||||||
|
python3 \
|
||||||
|
python3-pip
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/usr/local/cargo/registry
|
/github/home/.cargo/registry
|
||||||
/usr/local/cargo/git
|
/github/home/.cargo/git
|
||||||
target
|
target
|
||||||
key: gnu-${{ matrix.asset }}-${{ hashFiles('**/Cargo.lock') }}
|
key: checks-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
gnu-${{ matrix.asset }}-
|
checks-${{ runner.os }}-
|
||||||
gnu-
|
|
||||||
|
|
||||||
- name: Build
|
- name: Cargo fetch
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: cargo fetch --locked
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
- name: Format
|
||||||
export CC=aarch64-linux-gnu-gcc
|
|
||||||
export CXX=aarch64-linux-gnu-g++
|
|
||||||
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C lto=fat -C panic=abort"
|
|
||||||
else
|
|
||||||
export CC=clang
|
|
||||||
export CXX=clang++
|
|
||||||
|
|
||||||
if [ "${{ matrix.cpu }}" = "v3" ]; then
|
|
||||||
CPU_FLAGS="-C target-cpu=x86-64-v3"
|
|
||||||
else
|
|
||||||
CPU_FLAGS="-C target-cpu=x86-64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld -C lto=fat -C panic=abort ${CPU_FLAGS}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cargo build --release --target ${{ matrix.target }} -j "$(nproc)"
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: cargo fmt --all -- --check
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
mkdir -p dist
|
- name: Clippy
|
||||||
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
|
shell: bash
|
||||||
|
run: cargo clippy --workspace --all-targets --locked -- -D warnings
|
||||||
|
|
||||||
cd dist
|
- name: Tests
|
||||||
tar -czf "${{ matrix.asset }}.tar.gz" \
|
shell: bash
|
||||||
--owner=0 --group=0 --numeric-owner \
|
run: cargo test --workspace --all-targets --locked
|
||||||
telemt
|
|
||||||
|
|
||||||
sha256sum "${{ matrix.asset }}.tar.gz" > "${{ matrix.asset }}.tar.gz.sha256"
|
build-binaries:
|
||||||
|
name: Build ${{ matrix.asset_name }}
|
||||||
- uses: actions/upload-artifact@v4
|
needs: [prepare, checks]
|
||||||
with:
|
|
||||||
name: ${{ matrix.asset }}
|
|
||||||
path: dist/*
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# MUSL
|
|
||||||
# ==========================
|
|
||||||
build-musl:
|
|
||||||
name: MUSL ${{ matrix.asset }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: prepare
|
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: rust:slim-bookworm
|
image: debian:trixie
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-musl
|
- rust_target: x86_64-unknown-linux-gnu
|
||||||
asset: telemt-x86_64-linux-musl
|
zig_target: x86_64-unknown-linux-gnu.2.28
|
||||||
cpu: baseline
|
asset_name: telemt-x86_64-linux-gnu
|
||||||
|
- rust_target: aarch64-unknown-linux-gnu
|
||||||
- target: x86_64-unknown-linux-musl
|
zig_target: aarch64-unknown-linux-gnu.2.28
|
||||||
asset: telemt-x86_64-v3-linux-musl
|
asset_name: telemt-aarch64-linux-gnu
|
||||||
cpu: v3
|
- rust_target: x86_64-unknown-linux-musl
|
||||||
|
zig_target: x86_64-unknown-linux-musl
|
||||||
- target: aarch64-unknown-linux-musl
|
asset_name: telemt-x86_64-linux-musl
|
||||||
asset: telemt-aarch64-linux-musl
|
- rust_target: aarch64-unknown-linux-musl
|
||||||
cpu: generic
|
zig_target: aarch64-unknown-linux-musl
|
||||||
|
asset_name: telemt-aarch64-linux-musl
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install system dependencies
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
clang \
|
||||||
|
llvm \
|
||||||
|
file \
|
||||||
|
tar \
|
||||||
|
xz-utils \
|
||||||
|
python3 \
|
||||||
|
python3-pip
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install deps
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y \
|
|
||||||
musl-tools \
|
|
||||||
pkg-config \
|
|
||||||
curl
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
if: matrix.target == 'aarch64-unknown-linux-musl'
|
|
||||||
with:
|
with:
|
||||||
path: ~/.musl-aarch64
|
targets: ${{ matrix.rust_target }}
|
||||||
key: musl-toolchain-aarch64-v1
|
|
||||||
|
|
||||||
- name: Install aarch64 musl toolchain
|
- name: Cache cargo
|
||||||
if: matrix.target == 'aarch64-unknown-linux-musl'
|
uses: actions/cache@v4
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
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 cached"
|
|
||||||
else
|
|
||||||
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
|
|
||||||
run: rustup target add ${{ matrix.target }}
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/usr/local/cargo/registry
|
/github/home/.cargo/registry
|
||||||
/usr/local/cargo/git
|
/github/home/.cargo/git
|
||||||
target
|
target
|
||||||
key: musl-${{ matrix.asset }}-${{ hashFiles('**/Cargo.lock') }}
|
key: build-${{ matrix.zig_target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
musl-${{ matrix.asset }}-
|
build-${{ matrix.zig_target }}-
|
||||||
musl-
|
|
||||||
|
|
||||||
- name: Build
|
- name: Install cargo-zigbuild + Zig
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
python3 -m pip install --user --break-system-packages cargo-zigbuild
|
||||||
|
echo "/github/home/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
|
- name: Cargo fetch
|
||||||
export CC=aarch64-linux-musl-gcc
|
shell: bash
|
||||||
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
|
run: cargo fetch --locked
|
||||||
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc -C lto=fat -C panic=abort"
|
|
||||||
else
|
|
||||||
export CC=musl-gcc
|
|
||||||
export CC_x86_64_unknown_linux_musl=musl-gcc
|
|
||||||
|
|
||||||
if [ "${{ matrix.cpu }}" = "v3" ]; then
|
- name: Build release
|
||||||
CPU_FLAGS="-C target-cpu=x86-64-v3"
|
shell: bash
|
||||||
else
|
env:
|
||||||
CPU_FLAGS="-C target-cpu=x86-64"
|
CARGO_PROFILE_RELEASE_LTO: "fat"
|
||||||
fi
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
|
||||||
|
CARGO_PROFILE_RELEASE_PANIC: "abort"
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cargo zigbuild --release --locked --target "${{ matrix.zig_target }}"
|
||||||
|
|
||||||
export RUSTFLAGS="-C target-feature=+crt-static -C lto=fat -C panic=abort ${CPU_FLAGS}"
|
- name: Strip binary
|
||||||
fi
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
llvm-strip "target/${{ matrix.zig_target }}/release/${BINARY_NAME}" || true
|
||||||
|
|
||||||
cargo build --release --target ${{ matrix.target }} -j "$(nproc)"
|
- name: Inspect binary
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
file "target/${{ matrix.zig_target }}/release/${BINARY_NAME}"
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUTDIR="$RUNNER_TEMP/pkg/${{ matrix.asset_name }}"
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
|
||||||
|
install -m 0755 "target/${{ matrix.zig_target }}/release/${BINARY_NAME}" "$OUTDIR/${BINARY_NAME}"
|
||||||
|
|
||||||
|
if [[ -f LICENSE ]]; then cp LICENSE "$OUTDIR/"; fi
|
||||||
|
if [[ -f README.md ]]; then cp README.md "$OUTDIR/"; fi
|
||||||
|
|
||||||
|
cat > "$OUTDIR/BUILD-INFO.txt" <<EOF
|
||||||
|
project=${GITHUB_REPOSITORY}
|
||||||
|
version=${{ needs.prepare.outputs.version }}
|
||||||
|
git_ref=${GITHUB_REF}
|
||||||
|
git_sha=${GITHUB_SHA}
|
||||||
|
rust_target=${{ matrix.rust_target }}
|
||||||
|
zig_target=${{ matrix.zig_target }}
|
||||||
|
built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
EOF
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
|
tar -C "$RUNNER_TEMP/pkg" -czf "dist/${{ matrix.asset_name }}.tar.gz" "${{ matrix.asset_name }}"
|
||||||
|
sha256sum "dist/${{ matrix.asset_name }}.tar.gz" > "dist/${{ matrix.asset_name }}.sha256"
|
||||||
cd dist
|
|
||||||
tar -czf "${{ matrix.asset }}.tar.gz" \
|
|
||||||
--owner=0 --group=0 --numeric-owner \
|
|
||||||
telemt
|
|
||||||
|
|
||||||
sha256sum "${{ matrix.asset }}.tar.gz" > "${{ matrix.asset }}.tar.gz.sha256"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset }}
|
name: ${{ matrix.asset_name }}
|
||||||
path: dist/*
|
path: |
|
||||||
|
dist/${{ matrix.asset_name }}.tar.gz
|
||||||
|
dist/${{ matrix.asset_name }}.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
# ==========================
|
attest-binaries:
|
||||||
# Release
|
name: Attest binary archives
|
||||||
# ==========================
|
needs: build-binaries
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare, build-gnu, build-musl]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
|
attestations: write
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: dist
|
||||||
|
|
||||||
- name: Flatten artifacts
|
- name: Flatten artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mkdir -p dist
|
mkdir -p upload
|
||||||
find artifacts -type f -exec cp {} dist/ \;
|
find dist -type f \( -name '*.tar.gz' -o -name '*.sha256' \) -exec cp {} upload/ \;
|
||||||
|
ls -lah upload
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Attest release archives
|
||||||
uses: softprops/action-gh-release@v2
|
uses: actions/attest-build-provenance@v3
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.prepare.outputs.version }}
|
subject-path: 'upload/*.tar.gz'
|
||||||
target_commitish: ${{ github.sha }}
|
|
||||||
files: dist/*
|
|
||||||
generate_release_notes: true
|
|
||||||
prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}
|
|
||||||
overwrite_files: true
|
|
||||||
|
|
||||||
# ==========================
|
docker-image:
|
||||||
# Docker
|
name: Build and push GHCR image
|
||||||
# ==========================
|
needs: [prepare, checks]
|
||||||
docker:
|
|
||||||
name: Docker
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare, release]
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -339,67 +275,78 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- name: Set up Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
- name: Log in to GHCR
|
||||||
|
if: ${{ needs.prepare.outputs.release_enabled == 'true' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Probe release assets
|
- name: Docker metadata
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
VERSION: ${{ needs.prepare.outputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
for asset in \
|
|
||||||
telemt-x86_64-linux-musl.tar.gz \
|
|
||||||
telemt-x86_64-linux-musl.tar.gz.sha256 \
|
|
||||||
telemt-aarch64-linux-musl.tar.gz \
|
|
||||||
telemt-aarch64-linux-musl.tar.gz.sha256
|
|
||||||
do
|
|
||||||
curl -fsIL \
|
|
||||||
--retry 10 \
|
|
||||||
--retry-delay 3 \
|
|
||||||
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${VERSION}/${asset}" \
|
|
||||||
> /dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Compute image tags
|
|
||||||
id: meta
|
id: meta
|
||||||
shell: bash
|
uses: docker/metadata-action@v5
|
||||||
env:
|
with:
|
||||||
VERSION: ${{ needs.prepare.outputs.version }}
|
images: ghcr.io/${{ github.repository }}
|
||||||
run: |
|
tags: |
|
||||||
set -euo pipefail
|
type=raw,value=${{ needs.prepare.outputs.version }}
|
||||||
|
type=raw,value=latest,enable=${{ needs.prepare.outputs.prerelease != 'true' && needs.prepare.outputs.release_enabled == 'true' }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=telemt
|
||||||
|
org.opencontainers.image.description=telemt
|
||||||
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||||
|
org.opencontainers.image.version=${{ needs.prepare.outputs.version }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
|
||||||
IMAGE="$(echo "ghcr.io/${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
|
- name: Build and push
|
||||||
TAGS="${IMAGE}:${VERSION}"
|
id: build
|
||||||
|
|
||||||
if [[ "${VERSION}" != *-* ]]; then
|
|
||||||
TAGS="${TAGS}"$'\n'"${IMAGE}:latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "tags<<EOF"
|
|
||||||
printf '%s\n' "${TAGS}"
|
|
||||||
echo "EOF"
|
|
||||||
} >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Build & Push
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
file: ./Dockerfile
|
||||||
pull: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ needs.prepare.outputs.release_enabled == 'true' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
build-args: |
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
TELEMT_REPOSITORY=${{ github.repository }}
|
|
||||||
TELEMT_VERSION=${{ needs.prepare.outputs.version }}
|
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
provenance: mode=max
|
||||||
|
sbom: true
|
||||||
|
build-args: |
|
||||||
|
TELEMT_VERSION=${{ needs.prepare.outputs.version }}
|
||||||
|
VCS_REF=${{ github.sha }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
if: ${{ needs.prepare.outputs.release_enabled == 'true' }}
|
||||||
|
needs: [prepare, build-binaries, attest-binaries, docker-image]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: release-artifacts
|
||||||
|
|
||||||
|
- name: Flatten artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p upload
|
||||||
|
find release-artifacts -type f \( -name '*.tar.gz' -o -name '*.sha256' \) -exec cp {} upload/ \;
|
||||||
|
ls -lah upload
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: upload/*
|
||||||
|
generate_release_notes: true
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
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: Build Release
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
- name: Stress quota-lock suites (PR only)
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -1,139 +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/bin
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-nextest-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-nextest-
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Install cargo-nextest
|
|
||||||
run: cargo install --locked cargo-nextest || true
|
|
||||||
|
|
||||||
- name: Run tests with nextest
|
|
||||||
run: cargo nextest run -j "$(nproc)"
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# 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-clippy-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-clippy-
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy -j "$(nproc)" -- --cap-lints warn
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Udeps
|
|
||||||
# ==========================
|
|
||||||
udeps:
|
|
||||||
name: Udeps
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rust-src
|
|
||||||
|
|
||||||
- name: Cache cargo
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-udeps-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-cargo-udeps-
|
|
||||||
${{ runner.os }}-cargo-
|
|
||||||
|
|
||||||
- name: Install cargo-udeps
|
|
||||||
run: cargo install --locked cargo-udeps || true
|
|
||||||
|
|
||||||
- name: Run udeps
|
|
||||||
run: cargo udeps -j "$(nproc)" || true
|
|
||||||
+45
-49
@@ -1,8 +1,8 @@
|
|||||||
# Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
## Purpose
|
## 1. Purpose
|
||||||
|
|
||||||
**Telemt exists to solve technical problems.**
|
Telemt exists to solve technical problems.
|
||||||
|
|
||||||
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
||||||
|
|
||||||
@@ -18,34 +18,27 @@ Technology has consequences. Responsibility is inherent.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Principles
|
## 2. Principles
|
||||||
|
|
||||||
* **Technical over emotional**
|
* **Technical over emotional**
|
||||||
|
|
||||||
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
||||||
|
|
||||||
* **Clarity over noise**
|
* **Clarity over noise**
|
||||||
|
|
||||||
Communication is structured, concise, and relevant.
|
Communication is structured, concise, and relevant.
|
||||||
|
|
||||||
* **Openness with standards**
|
* **Openness with standards**
|
||||||
|
|
||||||
Participation is open. The work remains disciplined.
|
Participation is open. The work remains disciplined.
|
||||||
|
|
||||||
* **Independence of judgment**
|
* **Independence of judgment**
|
||||||
|
|
||||||
Claims are evaluated on technical merit, not affiliation or posture.
|
Claims are evaluated on technical merit, not affiliation or posture.
|
||||||
|
|
||||||
* **Responsibility over capability**
|
* **Responsibility over capability**
|
||||||
|
|
||||||
Capability does not justify careless use.
|
Capability does not justify careless use.
|
||||||
|
|
||||||
* **Cooperation over friction**
|
* **Cooperation over friction**
|
||||||
|
|
||||||
Progress depends on coordination, mutual support, and honest review.
|
Progress depends on coordination, mutual support, and honest review.
|
||||||
|
|
||||||
* **Good intent, rigorous method**
|
* **Good intent, rigorous method**
|
||||||
|
|
||||||
Assume good intent, but require rigor.
|
Assume good intent, but require rigor.
|
||||||
|
|
||||||
> **Aussagen gelten nach ihrer Begründung.**
|
> **Aussagen gelten nach ihrer Begründung.**
|
||||||
@@ -54,7 +47,7 @@ Technology has consequences. Responsibility is inherent.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Expected Behavior
|
## 3. Expected Behavior
|
||||||
|
|
||||||
Participants are expected to:
|
Participants are expected to:
|
||||||
|
|
||||||
@@ -76,7 +69,7 @@ New contributors are welcome. They are expected to grow into these standards. Ex
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unacceptable Behavior
|
## 4. Unacceptable Behavior
|
||||||
|
|
||||||
The following is not allowed:
|
The following is not allowed:
|
||||||
|
|
||||||
@@ -96,7 +89,7 @@ Such discussions may be closed, removed, or redirected.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Security and Misuse
|
## 5. Security and Misuse
|
||||||
|
|
||||||
Telemt is intended for responsible use.
|
Telemt is intended for responsible use.
|
||||||
|
|
||||||
@@ -116,13 +109,15 @@ Security is both technical and behavioral.
|
|||||||
|
|
||||||
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
|
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
|
||||||
|
|
||||||
- Standards are public, legible, and applied to the work itself.
|
Standards are public, legible, and applied to the work itself.
|
||||||
- Questions are welcome. Careful disagreement is welcome. Honest correction is welcome.
|
|
||||||
- Gatekeeping by obscurity, status signaling, or hostility is not.
|
Questions are welcome. Careful disagreement is welcome. Honest correction is welcome.
|
||||||
|
|
||||||
|
Gatekeeping by obscurity, status signaling, or hostility is not.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scope
|
## 7. Scope
|
||||||
|
|
||||||
This Code of Conduct applies to all official spaces:
|
This Code of Conduct applies to all official spaces:
|
||||||
|
|
||||||
@@ -132,19 +127,16 @@ This Code of Conduct applies to all official spaces:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Maintainer Stewardship
|
## 8. Maintainer Stewardship
|
||||||
|
|
||||||
Maintainers are responsible for final decisions in matters of conduct, scope, and direction.
|
Maintainers are responsible for final decisions in matters of conduct, scope, and direction.
|
||||||
|
|
||||||
This responsibility is stewardship:
|
This responsibility is stewardship: preserving continuity, protecting signal, maintaining standards, and keeping Telemt workable for others.
|
||||||
- preserving continuity,
|
|
||||||
- protecting signal,
|
|
||||||
- maintaining standards,
|
|
||||||
- keeping Telemt workable for others.
|
|
||||||
|
|
||||||
Judgment should be exercised with restraint, consistency, and institutional responsibility.
|
Judgment should be exercised with restraint, consistency, and institutional responsibility.
|
||||||
- Not every decision requires extended debate.
|
|
||||||
- Not every intervention requires public explanation.
|
Not every decision requires extended debate.
|
||||||
|
Not every intervention requires public explanation.
|
||||||
|
|
||||||
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
|
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
|
||||||
|
|
||||||
@@ -154,7 +146,7 @@ All decisions are expected to serve the durability, clarity, and integrity of Te
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Enforcement
|
## 9. Enforcement
|
||||||
|
|
||||||
Maintainers may act to preserve the integrity of Telemt, including by:
|
Maintainers may act to preserve the integrity of Telemt, including by:
|
||||||
|
|
||||||
@@ -164,40 +156,44 @@ Maintainers may act to preserve the integrity of Telemt, including by:
|
|||||||
* Restricting or banning participants
|
* Restricting or banning participants
|
||||||
|
|
||||||
Actions are taken to maintain function, continuity, and signal quality.
|
Actions are taken to maintain function, continuity, and signal quality.
|
||||||
- Where possible, correction is preferred to exclusion.
|
|
||||||
- Where necessary, exclusion is preferred to decay.
|
Where possible, correction is preferred to exclusion.
|
||||||
|
|
||||||
|
Where necessary, exclusion is preferred to decay.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Final
|
## 10. Final
|
||||||
|
|
||||||
Telemt is built on discipline, structure, and shared intent.
|
Telemt is built on discipline, structure, and shared intent.
|
||||||
- Signal over noise.
|
|
||||||
- Facts over opinion.
|
|
||||||
- Systems over rhetoric.
|
|
||||||
|
|
||||||
- Work is collective.
|
Signal over noise.
|
||||||
- Outcomes are shared.
|
Facts over opinion.
|
||||||
- Responsibility is distributed.
|
Systems over rhetoric.
|
||||||
|
|
||||||
- Precision is learned.
|
Work is collective.
|
||||||
- Rigor is expected.
|
Outcomes are shared.
|
||||||
- Help is part of the work.
|
Responsibility is distributed.
|
||||||
|
|
||||||
|
Precision is learned.
|
||||||
|
Rigor is expected.
|
||||||
|
Help is part of the work.
|
||||||
|
|
||||||
> **Ordnung ist Voraussetzung der Freiheit.**
|
> **Ordnung ist Voraussetzung der Freiheit.**
|
||||||
|
|
||||||
- If you contribute — contribute with care.
|
If you contribute — contribute with care.
|
||||||
- If you speak — speak with substance.
|
If you speak — speak with substance.
|
||||||
- If you engage — engage constructively.
|
If you engage — engage constructively.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## After All
|
## 11. After All
|
||||||
|
|
||||||
Systems outlive intentions.
|
Systems outlive intentions.
|
||||||
- What is built will be used.
|
|
||||||
- What is released will propagate.
|
What is built will be used.
|
||||||
- What is maintained will define the future state.
|
What is released will propagate.
|
||||||
|
What is maintained will define the future state.
|
||||||
|
|
||||||
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
||||||
|
|
||||||
@@ -205,8 +201,8 @@ There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
|||||||
|
|
||||||
> Every system carries responsibility.
|
> Every system carries responsibility.
|
||||||
|
|
||||||
- Stability requires discipline.
|
Stability requires discipline.
|
||||||
- Freedom requires structure.
|
Freedom requires structure.
|
||||||
- Trust requires honesty.
|
Trust requires honesty.
|
||||||
|
|
||||||
In the end: the system reflects its contributors.
|
In the end, the system reflects its contributors.
|
||||||
|
|||||||
+9
-72
@@ -1,82 +1,19 @@
|
|||||||
# Issues
|
# Issues - Rules
|
||||||
## Warnung
|
|
||||||
Before opening Issue, if it is more question than problem or bug - ask about that [in our chat](https://t.me/telemtrs)
|
|
||||||
|
|
||||||
## What it is not
|
## What it is not
|
||||||
- NOT Question and Answer
|
- NOT Question and Answer
|
||||||
- NOT Helpdesk
|
- NOT Helpdesk
|
||||||
|
|
||||||
***Each of your Issues triggers attempts to reproduce problems and analyze them, which are done manually by people***
|
# Pull Requests - Rules
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Pull Requests
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- ONLY signed and verified commits
|
- ONLY signed and verified commits
|
||||||
- ONLY from your name
|
- ONLY from your name
|
||||||
- DO NOT commit with `codex`, `claude`, or other AI tools as author/committer
|
- DO NOT commit with `codex` or `claude` as author/commiter
|
||||||
- PREFER `flow` branch for development, not `main`
|
- PREFER `flow` branch for development, not `main`
|
||||||
|
|
||||||
---
|
## AI
|
||||||
|
We are not against modern tools, like AI, where you act as a principal or architect, but we consider it important:
|
||||||
|
|
||||||
## Definition of Ready (MANDATORY)
|
- you really understand what you're doing
|
||||||
|
- you understand the relationships and dependencies of the components being modified
|
||||||
A Pull Request WILL be ignored or closed if:
|
- you understand the architecture of Telegram MTProto, MTProxy, Middle-End KDF at least generically
|
||||||
|
- you DO NOT commit for the sake of commits, but to help the community, core-developers and ordinary users
|
||||||
- it does NOT build
|
|
||||||
- it does NOT pass tests
|
|
||||||
- it does NOT follow formatting rules
|
|
||||||
- it contains unrelated or excessive changes
|
|
||||||
- the author cannot clearly explain the change
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Blessed Principles
|
|
||||||
- PR must build
|
|
||||||
- PR must pass tests
|
|
||||||
- PR must be understood by author
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AI Usage Policy
|
|
||||||
|
|
||||||
AI tools (Claude, ChatGPT, Codex, DeepSeek, etc.) are allowed as **assistants**, NOT as decision-makers.
|
|
||||||
|
|
||||||
By submitting a PR, you confirm that:
|
|
||||||
|
|
||||||
- you fully understand the code you submit
|
|
||||||
- you verified correctness manually
|
|
||||||
- you reviewed architecture and dependencies
|
|
||||||
- you take full responsibility for the change
|
|
||||||
|
|
||||||
AI-generated code is treated as **draft** and must be validated like any other external contribution.
|
|
||||||
|
|
||||||
PRs that look like unverified AI dumps WILL be closed
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Maintainer Policy
|
|
||||||
|
|
||||||
Maintainers reserve the right to:
|
|
||||||
|
|
||||||
- close PRs that do not meet basic quality requirements
|
|
||||||
- request explanations before review
|
|
||||||
- ignore low-effort contributions
|
|
||||||
|
|
||||||
Respect the reviewers time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Pull Requests that violate project standards may be closed without review.
|
|
||||||
|
|
||||||
This includes (but is not limited to):
|
|
||||||
|
|
||||||
- non-building code
|
|
||||||
- failing tests
|
|
||||||
- unverified or low-effort changes
|
|
||||||
- inability to explain the change
|
|
||||||
|
|
||||||
These actions follow the Code of Conduct and are intended to preserve signal, quality, and Telemt's integrity
|
|
||||||
|
|||||||
Generated
+10
-45
@@ -90,9 +90,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.9.0"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
|
checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -1454,9 +1454,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.11"
|
version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
|
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1486,7 +1486,7 @@ dependencies = [
|
|||||||
"cesu8",
|
"cesu8",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"combine",
|
"combine",
|
||||||
"jni-sys 0.3.1",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -1495,31 +1495,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni-sys"
|
name = "jni-sys"
|
||||||
version = "0.3.1"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
dependencies = [
|
|
||||||
"jni-sys 0.4.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
|
||||||
dependencies = [
|
|
||||||
"jni-sys-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys-macros"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
@@ -1681,9 +1659,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moka"
|
name = "moka"
|
||||||
version = "0.12.15"
|
version = "0.12.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
|
checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"crossbeam-epoch",
|
"crossbeam-epoch",
|
||||||
@@ -2793,7 +2771,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.35"
|
version = "3.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -2844,7 +2822,6 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
@@ -3171,18 +3148,6 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-appender"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"time",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.31"
|
version = "0.1.31"
|
||||||
|
|||||||
+5
-23
@@ -1,11 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.35"
|
version = "3.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
|
||||||
redteam_offline_expected_fail = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# C
|
# C
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -30,13 +27,7 @@ static_assertions = "1.1"
|
|||||||
|
|
||||||
# Network
|
# Network
|
||||||
socket2 = { version = "0.6", features = ["all"] }
|
socket2 = { version = "0.6", features = ["all"] }
|
||||||
nix = { version = "0.31", default-features = false, features = [
|
nix = { version = "0.31", default-features = false, features = ["net"] }
|
||||||
"net",
|
|
||||||
"user",
|
|
||||||
"process",
|
|
||||||
"fs",
|
|
||||||
"signal",
|
|
||||||
] }
|
|
||||||
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
@@ -50,7 +41,6 @@ bytes = "1.9"
|
|||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-appender = "0.2"
|
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
dashmap = "6.1"
|
dashmap = "6.1"
|
||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
@@ -75,14 +65,8 @@ hyper = { version = "1", features = ["server", "http1"] }
|
|||||||
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
|
hyper-util = { version = "0.1", features = ["tokio", "server-auto"] }
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
httpdate = "1.0"
|
httpdate = "1.0"
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = [
|
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] }
|
||||||
"tls12",
|
rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
|
||||||
] }
|
|
||||||
rustls = { version = "0.23", default-features = false, features = [
|
|
||||||
"std",
|
|
||||||
"tls12",
|
|
||||||
"ring",
|
|
||||||
] }
|
|
||||||
webpki-roots = "1.0"
|
webpki-roots = "1.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
@@ -96,6 +80,4 @@ name = "crypto_bench"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "thin"
|
||||||
codegen-units = 1
|
|
||||||
|
|
||||||
|
|||||||
+29
-83
@@ -1,98 +1,44 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# ==========================
|
||||||
|
# Stage 1: Build
|
||||||
|
# ==========================
|
||||||
|
FROM rust:1.88-slim-bookworm AS builder
|
||||||
|
|
||||||
ARG TELEMT_REPOSITORY=telemt/telemt
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ARG TELEMT_VERSION=latest
|
pkg-config \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
|
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
|
||||||
|
cargo build --release 2>/dev/null || true && \
|
||||||
|
rm -rf src
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release && strip target/release/telemt
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Minimal Image
|
# Stage 2: Runtime
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM debian:12-slim AS minimal
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
ARG TARGETARCH
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ARG TELEMT_REPOSITORY
|
|
||||||
ARG TELEMT_VERSION
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
binutils \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
tar; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN useradd -r -s /usr/sbin/nologin telemt
|
||||||
case "${TARGETARCH}" in \
|
|
||||||
amd64) ASSET="telemt-x86_64-linux-musl.tar.gz" ;; \
|
|
||||||
arm64) ASSET="telemt-aarch64-linux-musl.tar.gz" ;; \
|
|
||||||
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
|
|
||||||
esac; \
|
|
||||||
VERSION="${TELEMT_VERSION#refs/tags/}"; \
|
|
||||||
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then \
|
|
||||||
BASE_URL="https://github.com/${TELEMT_REPOSITORY}/releases/latest/download"; \
|
|
||||||
else \
|
|
||||||
BASE_URL="https://github.com/${TELEMT_REPOSITORY}/releases/download/${VERSION}"; \
|
|
||||||
fi; \
|
|
||||||
curl -fL \
|
|
||||||
--retry 5 \
|
|
||||||
--retry-delay 3 \
|
|
||||||
--connect-timeout 10 \
|
|
||||||
--max-time 120 \
|
|
||||||
-o "/tmp/${ASSET}" \
|
|
||||||
"${BASE_URL}/${ASSET}"; \
|
|
||||||
curl -fL \
|
|
||||||
--retry 5 \
|
|
||||||
--retry-delay 3 \
|
|
||||||
--connect-timeout 10 \
|
|
||||||
--max-time 120 \
|
|
||||||
-o "/tmp/${ASSET}.sha256" \
|
|
||||||
"${BASE_URL}/${ASSET}.sha256"; \
|
|
||||||
cd /tmp; \
|
|
||||||
sha256sum -c "${ASSET}.sha256"; \
|
|
||||||
tar -xzf "${ASSET}" -C /tmp; \
|
|
||||||
test -f /tmp/telemt; \
|
|
||||||
install -m 0755 /tmp/telemt /telemt; \
|
|
||||||
strip --strip-unneeded /telemt || true; \
|
|
||||||
rm -f "/tmp/${ASSET}" "/tmp/${ASSET}.sha256" /tmp/telemt
|
|
||||||
|
|
||||||
# ==========================
|
|
||||||
# Debug Image
|
|
||||||
# ==========================
|
|
||||||
FROM debian:12-slim AS debug
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
apt-get update; \
|
|
||||||
apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
tzdata \
|
|
||||||
curl \
|
|
||||||
iproute2 \
|
|
||||||
busybox; \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=minimal /telemt /app/telemt
|
COPY --from=builder /build/target/release/telemt /app/telemt
|
||||||
COPY config.toml /app/config.toml
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
EXPOSE 443 9090 9091
|
RUN chown -R telemt:telemt /app
|
||||||
|
USER telemt
|
||||||
ENTRYPOINT ["/app/telemt"]
|
|
||||||
CMD ["config.toml"]
|
EXPOSE 443
|
||||||
|
EXPOSE 9090
|
||||||
# ==========================
|
EXPOSE 9091
|
||||||
# Production Distroless on MUSL
|
|
||||||
# ==========================
|
|
||||||
FROM gcr.io/distroless/static-debian12 AS prod
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=minimal /telemt /app/telemt
|
|
||||||
COPY config.toml /app/config.toml
|
|
||||||
|
|
||||||
USER nonroot:nonroot
|
|
||||||
|
|
||||||
EXPOSE 443 9090 9091
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/telemt"]
|
ENTRYPOINT ["/app/telemt"]
|
||||||
CMD ["config.toml"]
|
CMD ["config.toml"]
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
|
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
|
||||||
|
|
||||||
[**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
|
||||||
|
|
||||||
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
||||||
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
||||||
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/API.md)
|
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/API.md)
|
||||||
@@ -11,6 +9,60 @@
|
|||||||
- Prometheus-format Metrics
|
- Prometheus-format Metrics
|
||||||
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes
|
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes
|
||||||
|
|
||||||
|
[**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
||||||
|
|
||||||
|
## NEWS and EMERGENCY
|
||||||
|
### ✈️ Telemt 3 is released!
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
### 🇷🇺 RU
|
||||||
|
|
||||||
|
#### О релизах
|
||||||
|
|
||||||
|
[3.3.27](https://github.com/telemt/telemt/releases/tag/3.3.27) даёт баланс стабильности и передового функционала, а так же последние исправления по безопасности и багам
|
||||||
|
|
||||||
|
Будем рады вашему фидбеку и предложениям по улучшению — особенно в части **API**, **статистики**, **UX**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Если у вас есть компетенции в:
|
||||||
|
|
||||||
|
- Асинхронных сетевых приложениях
|
||||||
|
- Анализе трафика
|
||||||
|
- Реверс-инжиниринге
|
||||||
|
- Сетевых расследованиях
|
||||||
|
|
||||||
|
Мы открыты к архитектурным предложениям, идеям и pull requests
|
||||||
|
</td>
|
||||||
|
<td width="50%" valign="top">
|
||||||
|
|
||||||
|
### 🇬🇧 EN
|
||||||
|
|
||||||
|
#### About releases
|
||||||
|
|
||||||
|
[3.3.27](https://github.com/telemt/telemt/releases/tag/3.3.27) provides a balance of stability and advanced functionality, as well as the latest security and bug fixes
|
||||||
|
|
||||||
|
We are looking forward to your feedback and improvement proposals — especially regarding **API**, **statistics**, **UX**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you have expertise in:
|
||||||
|
|
||||||
|
- Asynchronous network applications
|
||||||
|
- Traffic analysis
|
||||||
|
- Reverse engineering
|
||||||
|
- Network forensics
|
||||||
|
|
||||||
|
We welcome ideas, architectural feedback, and pull requests.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# Features
|
||||||
|
💥 The configuration structure has changed since version 1.1.0.0. change it in your environment!
|
||||||
|
|
||||||
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
⚓ Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
|
||||||
|
|
||||||
⚓ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
⚓ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Cryptobench
|
// Cryptobench
|
||||||
use criterion::{Criterion, black_box, criterion_group};
|
use criterion::{black_box, criterion_group, Criterion};
|
||||||
|
|
||||||
fn bench_aes_ctr(c: &mut Criterion) {
|
fn bench_aes_ctr(c: &mut Criterion) {
|
||||||
c.bench_function("aes_ctr_encrypt_64kb", |b| {
|
c.bench_function("aes_ctr_encrypt_64kb", |b| {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| Parameter | Type | Default | Constraints / validation | Description |
|
| Parameter | Type | Default | Constraints / validation | Description |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| data_path | `String \| null` | `null` | — | Optional runtime data directory path. |
|
| data_path | `String \| null` | `null` | — | Optional runtime data directory path. |
|
||||||
| prefer_ipv6 | `bool` | `false` | Deprecated. Use `network.prefer`. | Deprecated legacy IPv6 preference flag migrated to `network.prefer`. |
|
| prefer_ipv6 | `bool` | `false` | — | Prefer IPv6 where applicable in runtime logic. |
|
||||||
| fast_mode | `bool` | `true` | — | Enables fast-path optimizations for traffic processing. |
|
| fast_mode | `bool` | `true` | — | Enables fast-path optimizations for traffic processing. |
|
||||||
| use_middle_proxy | `bool` | `true` | none | Enables ME transport mode; if `false`, runtime falls back to direct DC routing. |
|
| use_middle_proxy | `bool` | `true` | none | Enables ME transport mode; if `false`, runtime falls back to direct DC routing. |
|
||||||
| proxy_secret_path | `String \| null` | `"proxy-secret"` | Path may be `null`. | Path to Telegram infrastructure proxy-secret file used by ME handshake logic. |
|
| proxy_secret_path | `String \| null` | `"proxy-secret"` | Path may be `null`. | Path to Telegram infrastructure proxy-secret file used by ME handshake logic. |
|
||||||
@@ -44,14 +44,11 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| me_writer_cmd_channel_capacity | `usize` | `4096` | Must be `> 0`. | Capacity of per-writer command channel. |
|
| me_writer_cmd_channel_capacity | `usize` | `4096` | Must be `> 0`. | Capacity of per-writer command channel. |
|
||||||
| me_route_channel_capacity | `usize` | `768` | Must be `> 0`. | Capacity of per-connection ME response route channel. |
|
| me_route_channel_capacity | `usize` | `768` | Must be `> 0`. | Capacity of per-connection ME response route channel. |
|
||||||
| me_c2me_channel_capacity | `usize` | `1024` | Must be `> 0`. | Capacity of per-client command queue (client reader -> ME sender). |
|
| me_c2me_channel_capacity | `usize` | `1024` | Must be `> 0`. | Capacity of per-client command queue (client reader -> ME sender). |
|
||||||
| me_c2me_send_timeout_ms | `u64` | `4000` | `0..=60000`. | Maximum wait for enqueueing client->ME commands when the per-client queue is full (`0` keeps legacy unbounded wait). |
|
|
||||||
| me_reader_route_data_wait_ms | `u64` | `2` | `0..=20`. | Bounded wait for routing ME DATA to per-connection queue (`0` = no wait). |
|
| me_reader_route_data_wait_ms | `u64` | `2` | `0..=20`. | Bounded wait for routing ME DATA to per-connection queue (`0` = no wait). |
|
||||||
| me_d2c_flush_batch_max_frames | `usize` | `32` | `1..=512`. | Max ME->client frames coalesced before flush. |
|
| me_d2c_flush_batch_max_frames | `usize` | `32` | `1..=512`. | Max ME->client frames coalesced before flush. |
|
||||||
| me_d2c_flush_batch_max_bytes | `usize` | `131072` | `4096..=2_097_152`. | Max ME->client payload bytes coalesced before flush. |
|
| me_d2c_flush_batch_max_bytes | `usize` | `131072` | `4096..=2_097_152`. | Max ME->client payload bytes coalesced before flush. |
|
||||||
| me_d2c_flush_batch_max_delay_us | `u64` | `500` | `0..=5000`. | Max microsecond wait for coalescing more ME->client frames (`0` disables timed coalescing). |
|
| me_d2c_flush_batch_max_delay_us | `u64` | `500` | `0..=5000`. | Max microsecond wait for coalescing more ME->client frames (`0` disables timed coalescing). |
|
||||||
| me_d2c_ack_flush_immediate | `bool` | `true` | — | Flushes client writer immediately after quick-ack write. |
|
| me_d2c_ack_flush_immediate | `bool` | `true` | — | Flushes client writer immediately after quick-ack write. |
|
||||||
| me_quota_soft_overshoot_bytes | `u64` | `65536` | `0..=16_777_216`. | Extra per-route quota allowance (bytes) tolerated before writer-side quota enforcement drops route data. |
|
|
||||||
| me_d2c_frame_buf_shrink_threshold_bytes | `usize` | `262144` | `4096..=16_777_216`. | Threshold for shrinking oversized ME->client frame-aggregation buffers after flush. |
|
|
||||||
| direct_relay_copy_buf_c2s_bytes | `usize` | `65536` | `4096..=1_048_576`. | Copy buffer size for client->DC direction in direct relay. |
|
| direct_relay_copy_buf_c2s_bytes | `usize` | `65536` | `4096..=1_048_576`. | Copy buffer size for client->DC direction in direct relay. |
|
||||||
| direct_relay_copy_buf_s2c_bytes | `usize` | `262144` | `8192..=2_097_152`. | Copy buffer size for DC->client direction in direct relay. |
|
| direct_relay_copy_buf_s2c_bytes | `usize` | `262144` | `8192..=2_097_152`. | Copy buffer size for DC->client direction in direct relay. |
|
||||||
| crypto_pending_buffer | `usize` | `262144` | — | Max pending ciphertext buffer per client writer (bytes). |
|
| crypto_pending_buffer | `usize` | `262144` | — | Max pending ciphertext buffer per client writer (bytes). |
|
||||||
@@ -91,7 +88,6 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| upstream_connect_retry_attempts | `u32` | `2` | Must be `> 0`. | Connect attempts for selected upstream before error/fallback. |
|
| upstream_connect_retry_attempts | `u32` | `2` | Must be `> 0`. | Connect attempts for selected upstream before error/fallback. |
|
||||||
| upstream_connect_retry_backoff_ms | `u64` | `100` | — | Delay between upstream connect attempts (ms). |
|
| upstream_connect_retry_backoff_ms | `u64` | `100` | — | Delay between upstream connect attempts (ms). |
|
||||||
| upstream_connect_budget_ms | `u64` | `3000` | Must be `> 0`. | Total wall-clock budget for one upstream connect request (ms). |
|
| upstream_connect_budget_ms | `u64` | `3000` | Must be `> 0`. | Total wall-clock budget for one upstream connect request (ms). |
|
||||||
| tg_connect | `u64` | `10` | Must be `> 0`. | Per-attempt upstream TCP connect timeout to Telegram DC (seconds). |
|
|
||||||
| upstream_unhealthy_fail_threshold | `u32` | `5` | Must be `> 0`. | Consecutive failed requests before upstream is marked unhealthy. |
|
| upstream_unhealthy_fail_threshold | `u32` | `5` | Must be `> 0`. | Consecutive failed requests before upstream is marked unhealthy. |
|
||||||
| upstream_connect_failfast_hard_errors | `bool` | `false` | — | Skips additional retries for hard non-transient connect errors. |
|
| upstream_connect_failfast_hard_errors | `bool` | `false` | — | Skips additional retries for hard non-transient connect errors. |
|
||||||
| stun_iface_mismatch_ignore | `bool` | `false` | none | Reserved compatibility flag in current runtime revision. |
|
| stun_iface_mismatch_ignore | `bool` | `false` | none | Reserved compatibility flag in current runtime revision. |
|
||||||
@@ -109,8 +105,6 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| me_warn_rate_limit_ms | `u64` | `5000` | Must be `> 0`. | Cooldown for repetitive ME warning logs (ms). |
|
| me_warn_rate_limit_ms | `u64` | `5000` | Must be `> 0`. | Cooldown for repetitive ME warning logs (ms). |
|
||||||
| me_route_no_writer_mode | `"async_recovery_failfast" \| "inline_recovery_legacy" \| "hybrid_async_persistent"` | `"hybrid_async_persistent"` | — | Route behavior when no writer is immediately available. |
|
| me_route_no_writer_mode | `"async_recovery_failfast" \| "inline_recovery_legacy" \| "hybrid_async_persistent"` | `"hybrid_async_persistent"` | — | Route behavior when no writer is immediately available. |
|
||||||
| me_route_no_writer_wait_ms | `u64` | `250` | `10..=5000`. | Max wait in async-recovery failfast mode (ms). |
|
| me_route_no_writer_wait_ms | `u64` | `250` | `10..=5000`. | Max wait in async-recovery failfast mode (ms). |
|
||||||
| me_route_hybrid_max_wait_ms | `u64` | `3000` | `50..=60000`. | Maximum cumulative wait in hybrid no-writer mode before failfast fallback (ms). |
|
|
||||||
| me_route_blocking_send_timeout_ms | `u64` | `250` | `0..=5000`. | Maximum wait for blocking route-channel send fallback (`0` keeps legacy unbounded wait). |
|
|
||||||
| me_route_inline_recovery_attempts | `u32` | `3` | Must be `> 0`. | Inline recovery attempts in legacy mode. |
|
| me_route_inline_recovery_attempts | `u32` | `3` | Must be `> 0`. | Inline recovery attempts in legacy mode. |
|
||||||
| me_route_inline_recovery_wait_ms | `u64` | `3000` | `10..=30000`. | Max inline recovery wait in legacy mode (ms). |
|
| me_route_inline_recovery_wait_ms | `u64` | `3000` | `10..=30000`. | Max inline recovery wait in legacy mode (ms). |
|
||||||
| fast_mode_min_tls_record | `usize` | `0` | — | Minimum TLS record size when fast-mode coalescing is enabled (`0` disables). |
|
| fast_mode_min_tls_record | `usize` | `0` | — | Minimum TLS record size when fast-mode coalescing is enabled (`0` disables). |
|
||||||
@@ -130,7 +124,6 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| me_secret_atomic_snapshot | `bool` | `true` | — | Keeps selector and secret bytes from the same snapshot atomically. |
|
| me_secret_atomic_snapshot | `bool` | `true` | — | Keeps selector and secret bytes from the same snapshot atomically. |
|
||||||
| proxy_secret_len_max | `usize` | `256` | Must be within `[32, 4096]`. | Upper length limit for accepted proxy-secret bytes. |
|
| proxy_secret_len_max | `usize` | `256` | Must be within `[32, 4096]`. | Upper length limit for accepted proxy-secret bytes. |
|
||||||
| me_pool_drain_ttl_secs | `u64` | `90` | none | Time window where stale writers remain fallback-eligible after map change. |
|
| me_pool_drain_ttl_secs | `u64` | `90` | none | Time window where stale writers remain fallback-eligible after map change. |
|
||||||
| me_instadrain | `bool` | `false` | — | Forces draining stale writers to be removed on the next cleanup tick, bypassing TTL/deadline waiting. |
|
|
||||||
| me_pool_drain_threshold | `u64` | `128` | — | Max draining stale writers before batch force-close (`0` disables threshold cleanup). |
|
| me_pool_drain_threshold | `u64` | `128` | — | Max draining stale writers before batch force-close (`0` disables threshold cleanup). |
|
||||||
| me_pool_drain_soft_evict_enabled | `bool` | `true` | — | Enables gradual soft-eviction of stale writers during drain/reinit instead of immediate hard close. |
|
| me_pool_drain_soft_evict_enabled | `bool` | `true` | — | Enables gradual soft-eviction of stale writers during drain/reinit instead of immediate hard close. |
|
||||||
| me_pool_drain_soft_evict_grace_secs | `u64` | `30` | `0..=3600`. | Grace period before stale writers become soft-evict candidates. |
|
| me_pool_drain_soft_evict_grace_secs | `u64` | `30` | `0..=3600`. | Grace period before stale writers become soft-evict candidates. |
|
||||||
@@ -205,14 +198,10 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
| listen_tcp | `bool \| null` | `null` (auto) | — | Explicit TCP listener enable/disable override. |
|
| listen_tcp | `bool \| null` | `null` (auto) | — | Explicit TCP listener enable/disable override. |
|
||||||
| proxy_protocol | `bool` | `false` | — | Enables HAProxy PROXY protocol parsing on incoming client connections. |
|
| proxy_protocol | `bool` | `false` | — | Enables HAProxy PROXY protocol parsing on incoming client connections. |
|
||||||
| proxy_protocol_header_timeout_ms | `u64` | `500` | Must be `> 0`. | Timeout for PROXY protocol header read/parse (ms). |
|
| proxy_protocol_header_timeout_ms | `u64` | `500` | Must be `> 0`. | Timeout for PROXY protocol header read/parse (ms). |
|
||||||
| proxy_protocol_trusted_cidrs | `IpNetwork[]` | `[]` | — | When non-empty, only connections from these proxy source CIDRs are allowed to provide PROXY protocol headers. If empty, PROXY headers are rejected by default (security hardening). |
|
|
||||||
| metrics_port | `u16 \| null` | `null` | — | Metrics endpoint port (enables metrics listener). |
|
| metrics_port | `u16 \| null` | `null` | — | Metrics endpoint port (enables metrics listener). |
|
||||||
| metrics_listen | `String \| null` | `null` | — | Full metrics bind address (`IP:PORT`), overrides `metrics_port`. |
|
| metrics_listen | `String \| null` | `null` | — | Full metrics bind address (`IP:PORT`), overrides `metrics_port`. |
|
||||||
| metrics_whitelist | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` | — | CIDR whitelist for metrics endpoint access. |
|
| metrics_whitelist | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` | — | CIDR whitelist for metrics endpoint access. |
|
||||||
| max_connections | `u32` | `10000` | — | Max concurrent client connections (`0` = unlimited). |
|
| max_connections | `u32` | `10000` | — | Max concurrent client connections (`0` = unlimited). |
|
||||||
| accept_permit_timeout_ms | `u64` | `250` | `0..=60000`. | Maximum wait for acquiring a connection-slot permit before the accepted connection is dropped (`0` keeps legacy unbounded wait). |
|
|
||||||
|
|
||||||
Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers are parsed from the first bytes of the connection and the client source address is replaced with `src_addr` from the header. For security, the peer source IP (the direct connection address) is verified against `server.proxy_protocol_trusted_cidrs`; if this list is empty, PROXY headers are rejected and the connection is considered untrusted.
|
|
||||||
|
|
||||||
## [server.api]
|
## [server.api]
|
||||||
|
|
||||||
@@ -237,7 +226,7 @@ Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers a
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| ip | `IpAddr` | — | — | Listener bind IP. |
|
| ip | `IpAddr` | — | — | Listener bind IP. |
|
||||||
| announce | `String \| null` | — | — | Public IP/domain announced in proxy links (priority over `announce_ip`). |
|
| announce | `String \| null` | — | — | Public IP/domain announced in proxy links (priority over `announce_ip`). |
|
||||||
| announce_ip | `IpAddr \| null` | — | Deprecated. Use `announce`. | Deprecated legacy announce IP (migrated to `announce` if needed). |
|
| announce_ip | `IpAddr \| null` | — | — | Deprecated legacy announce IP (migrated to `announce` if needed). |
|
||||||
| proxy_protocol | `bool \| null` | `null` | — | Per-listener override for PROXY protocol enable flag. |
|
| proxy_protocol | `bool \| null` | `null` | — | Per-listener override for PROXY protocol enable flag. |
|
||||||
| reuse_allow | `bool` | `false` | — | Enables `SO_REUSEPORT` for multi-instance bind sharing. |
|
| reuse_allow | `bool` | `false` | — | Enables `SO_REUSEPORT` for multi-instance bind sharing. |
|
||||||
|
|
||||||
@@ -246,10 +235,7 @@ Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers a
|
|||||||
| Parameter | Type | Default | Constraints / validation | Description |
|
| Parameter | Type | Default | Constraints / validation | Description |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| client_handshake | `u64` | `30` | — | Client handshake timeout. |
|
| client_handshake | `u64` | `30` | — | Client handshake timeout. |
|
||||||
| relay_idle_policy_v2_enabled | `bool` | `true` | — | Enables soft/hard middle-relay client idle policy. |
|
| tg_connect | `u64` | `10` | — | Upstream Telegram connect timeout. |
|
||||||
| relay_client_idle_soft_secs | `u64` | `120` | Must be `> 0`; must be `<= relay_client_idle_hard_secs`. | Soft idle threshold for middle-relay client uplink inactivity (seconds). |
|
|
||||||
| relay_client_idle_hard_secs | `u64` | `360` | Must be `> 0`; must be `>= relay_client_idle_soft_secs`. | Hard idle threshold for middle-relay client uplink inactivity (seconds). |
|
|
||||||
| relay_idle_grace_after_downstream_activity_secs | `u64` | `30` | Must be `<= relay_client_idle_hard_secs`. | Extra hard-idle grace after recent downstream activity (seconds). |
|
|
||||||
| client_keepalive | `u64` | `15` | — | Client keepalive timeout. |
|
| client_keepalive | `u64` | `15` | — | Client keepalive timeout. |
|
||||||
| client_ack | `u64` | `90` | — | Client ACK timeout. |
|
| client_ack | `u64` | `90` | — | Client ACK timeout. |
|
||||||
| me_one_retry | `u8` | `12` | none | Fast reconnect attempts budget for single-endpoint DC scenarios. |
|
| me_one_retry | `u8` | `12` | none | Fast reconnect attempts budget for single-endpoint DC scenarios. |
|
||||||
@@ -261,9 +247,6 @@ Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers a
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| tls_domain | `String` | `"petrovich.ru"` | — | Primary TLS domain used in fake TLS handshake profile. |
|
| tls_domain | `String` | `"petrovich.ru"` | — | Primary TLS domain used in fake TLS handshake profile. |
|
||||||
| tls_domains | `String[]` | `[]` | — | Additional TLS domains for generating multiple links. |
|
| tls_domains | `String[]` | `[]` | — | Additional TLS domains for generating multiple links. |
|
||||||
| unknown_sni_action | `"drop" \| "mask"` | `"drop"` | — | Action for TLS ClientHello with unknown/non-configured SNI. |
|
|
||||||
| tls_fetch_scope | `String` | `""` | Value is trimmed during load; empty keeps default upstream routing behavior. | Upstream scope tag used for TLS-front metadata fetches. |
|
|
||||||
| tls_fetch | `Table` | built-in defaults | See `[censorship.tls_fetch]` section below. | TLS-front metadata fetch strategy settings. |
|
|
||||||
| mask | `bool` | `true` | — | Enables masking/fronting relay mode. |
|
| mask | `bool` | `true` | — | Enables masking/fronting relay mode. |
|
||||||
| mask_host | `String \| null` | `null` | — | Upstream mask host for TLS fronting relay. |
|
| mask_host | `String \| null` | `null` | — | Upstream mask host for TLS fronting relay. |
|
||||||
| mask_port | `u16` | `443` | — | Upstream mask port for TLS fronting relay. |
|
| mask_port | `u16` | `443` | — | Upstream mask port for TLS fronting relay. |
|
||||||
@@ -278,29 +261,14 @@ Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers a
|
|||||||
| alpn_enforce | `bool` | `true` | — | Enforces ALPN echo behavior based on client preference. |
|
| alpn_enforce | `bool` | `true` | — | Enforces ALPN echo behavior based on client preference. |
|
||||||
| mask_proxy_protocol | `u8` | `0` | — | PROXY protocol mode for mask backend (`0` disabled, `1` v1, `2` v2). |
|
| mask_proxy_protocol | `u8` | `0` | — | PROXY protocol mode for mask backend (`0` disabled, `1` v1, `2` v2). |
|
||||||
| mask_shape_hardening | `bool` | `true` | — | Enables client->mask shape-channel hardening by applying controlled tail padding to bucket boundaries on mask relay shutdown. |
|
| mask_shape_hardening | `bool` | `true` | — | Enables client->mask shape-channel hardening by applying controlled tail padding to bucket boundaries on mask relay shutdown. |
|
||||||
| mask_shape_hardening_aggressive_mode | `bool` | `false` | Requires `mask_shape_hardening = true`. | Opt-in aggressive shaping profile: allows shaping on backend-silent non-EOF paths and switches above-cap blur to strictly positive random tail. |
|
|
||||||
| mask_shape_bucket_floor_bytes | `usize` | `512` | Must be `> 0`; should be `<= mask_shape_bucket_cap_bytes`. | Minimum bucket size used by shape-channel hardening. |
|
| mask_shape_bucket_floor_bytes | `usize` | `512` | Must be `> 0`; should be `<= mask_shape_bucket_cap_bytes`. | Minimum bucket size used by shape-channel hardening. |
|
||||||
| mask_shape_bucket_cap_bytes | `usize` | `4096` | Must be `>= mask_shape_bucket_floor_bytes`. | Maximum bucket size used by shape-channel hardening; traffic above cap is not padded further. |
|
| mask_shape_bucket_cap_bytes | `usize` | `4096` | Must be `>= mask_shape_bucket_floor_bytes`. | Maximum bucket size used by shape-channel hardening; traffic above cap is not padded further. |
|
||||||
| mask_shape_above_cap_blur | `bool` | `false` | Requires `mask_shape_hardening = true`; requires `mask_shape_above_cap_blur_max_bytes > 0`. | Adds bounded randomized tail bytes even when forwarded size already exceeds cap. |
|
| mask_shape_above_cap_blur | `bool` | `false` | Requires `mask_shape_hardening = true`; requires `mask_shape_above_cap_blur_max_bytes > 0`. | Adds bounded randomized tail bytes even when forwarded size already exceeds cap. |
|
||||||
| mask_shape_above_cap_blur_max_bytes | `usize` | `512` | Must be `<= 1048576`; must be `> 0` when `mask_shape_above_cap_blur = true`. | Maximum randomized extra bytes appended above cap. |
|
| mask_shape_above_cap_blur_max_bytes | `usize` | `512` | Must be `<= 1048576`; must be `> 0` when `mask_shape_above_cap_blur = true`. | Maximum randomized extra bytes appended above cap. |
|
||||||
| mask_relay_max_bytes | `usize` | `5242880` | Must be `> 0`; must be `<= 67108864`. | Maximum relayed bytes per direction on unauthenticated masking fallback path. |
|
|
||||||
| mask_classifier_prefetch_timeout_ms | `u64` | `5` | Must be within `[5, 50]`. | Timeout budget (ms) for extending fragmented initial classifier window on masking fallback. |
|
|
||||||
| mask_timing_normalization_enabled | `bool` | `false` | Requires `mask_timing_normalization_floor_ms > 0`; requires `ceiling >= floor`. | Enables timing envelope normalization on masking outcomes. |
|
| mask_timing_normalization_enabled | `bool` | `false` | Requires `mask_timing_normalization_floor_ms > 0`; requires `ceiling >= floor`. | Enables timing envelope normalization on masking outcomes. |
|
||||||
| mask_timing_normalization_floor_ms | `u64` | `0` | Must be `> 0` when timing normalization is enabled; must be `<= ceiling`. | Lower bound (ms) for masking outcome normalization target. |
|
| mask_timing_normalization_floor_ms | `u64` | `0` | Must be `> 0` when timing normalization is enabled; must be `<= ceiling`. | Lower bound (ms) for masking outcome normalization target. |
|
||||||
| mask_timing_normalization_ceiling_ms | `u64` | `0` | Must be `>= floor`; must be `<= 60000`. | Upper bound (ms) for masking outcome normalization target. |
|
| mask_timing_normalization_ceiling_ms | `u64` | `0` | Must be `>= floor`; must be `<= 60000`. | Upper bound (ms) for masking outcome normalization target. |
|
||||||
|
|
||||||
## [censorship.tls_fetch]
|
|
||||||
|
|
||||||
| Parameter | Type | Default | Constraints / validation | Description |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| profiles | `("modern_chrome_like" \| "modern_firefox_like" \| "compat_tls12" \| "legacy_minimal")[]` | `["modern_chrome_like", "modern_firefox_like", "compat_tls12", "legacy_minimal"]` | Empty list falls back to defaults; values are deduplicated preserving order. | Ordered ClientHello profile fallback chain for TLS-front metadata fetch. |
|
|
||||||
| strict_route | `bool` | `true` | — | Fails closed on upstream-route connect errors instead of falling back to direct TCP when route is configured. |
|
|
||||||
| attempt_timeout_ms | `u64` | `5000` | Must be `> 0`. | Timeout budget per one TLS-fetch profile attempt (ms). |
|
|
||||||
| total_budget_ms | `u64` | `15000` | Must be `> 0`. | Total wall-clock budget across all TLS-fetch attempts (ms). |
|
|
||||||
| grease_enabled | `bool` | `false` | — | Enables GREASE-style random values in selected ClientHello extensions for fetch traffic. |
|
|
||||||
| deterministic | `bool` | `false` | — | Enables deterministic ClientHello randomness for debugging/tests. |
|
|
||||||
| profile_cache_ttl_secs | `u64` | `600` | `0` disables cache. | TTL for winner-profile cache entries used by TLS fetch path. |
|
|
||||||
|
|
||||||
### Shape-channel hardening notes (`[censorship]`)
|
### Shape-channel hardening notes (`[censorship]`)
|
||||||
|
|
||||||
These parameters are designed to reduce one specific fingerprint source during masking: the exact number of bytes sent from proxy to `mask_host` for invalid or probing traffic.
|
These parameters are designed to reduce one specific fingerprint source during masking: the exact number of bytes sent from proxy to `mask_host` for invalid or probing traffic.
|
||||||
@@ -316,27 +284,6 @@ When `mask_shape_hardening = true`, Telemt pads the **client->mask** stream tail
|
|||||||
|
|
||||||
This means multiple nearby probe sizes collapse into the same backend-observed size class, making active classification harder.
|
This means multiple nearby probe sizes collapse into the same backend-observed size class, making active classification harder.
|
||||||
|
|
||||||
What each parameter changes in practice:
|
|
||||||
|
|
||||||
- `mask_shape_hardening`
|
|
||||||
Enables or disables this entire length-shaping stage on the fallback path.
|
|
||||||
When `false`, backend-observed length stays close to the real forwarded probe length.
|
|
||||||
When `true`, clean relay shutdown can append random padding bytes to move the total into a bucket.
|
|
||||||
|
|
||||||
- `mask_shape_bucket_floor_bytes`
|
|
||||||
Sets the first bucket boundary used for small probes.
|
|
||||||
Example: with floor `512`, a malformed probe that would otherwise forward `37` bytes can be expanded to `512` bytes on clean EOF.
|
|
||||||
Larger floor values hide very small probes better, but increase egress cost.
|
|
||||||
|
|
||||||
- `mask_shape_bucket_cap_bytes`
|
|
||||||
Sets the largest bucket Telemt will pad up to with bucket logic.
|
|
||||||
Example: with cap `4096`, a forwarded total of `1800` bytes may be padded to `2048` or `4096` depending on the bucket ladder, but a total already above `4096` will not be bucket-padded further.
|
|
||||||
Larger cap values increase the range over which size classes are collapsed, but also increase worst-case overhead.
|
|
||||||
|
|
||||||
- Clean EOF matters in conservative mode
|
|
||||||
In the default profile, shape padding is intentionally conservative: it is applied on clean relay shutdown, not on every timeout/drip path.
|
|
||||||
This avoids introducing new timeout-tail artifacts that some backends or tests interpret as a separate fingerprint.
|
|
||||||
|
|
||||||
Practical trade-offs:
|
Practical trade-offs:
|
||||||
|
|
||||||
- Better anti-fingerprinting on size/shape channel.
|
- Better anti-fingerprinting on size/shape channel.
|
||||||
@@ -349,56 +296,14 @@ Recommended starting profile:
|
|||||||
- `mask_shape_bucket_floor_bytes = 512`
|
- `mask_shape_bucket_floor_bytes = 512`
|
||||||
- `mask_shape_bucket_cap_bytes = 4096`
|
- `mask_shape_bucket_cap_bytes = 4096`
|
||||||
|
|
||||||
### Aggressive mode notes (`[censorship]`)
|
|
||||||
|
|
||||||
`mask_shape_hardening_aggressive_mode` is an opt-in profile for higher anti-classifier pressure.
|
|
||||||
|
|
||||||
- Default is `false` to preserve conservative timeout/no-tail behavior.
|
|
||||||
- Requires `mask_shape_hardening = true`.
|
|
||||||
- When enabled, backend-silent non-EOF masking paths may be shaped.
|
|
||||||
- When enabled together with above-cap blur, the random extra tail uses `[1, max]` instead of `[0, max]`.
|
|
||||||
|
|
||||||
What changes when aggressive mode is enabled:
|
|
||||||
|
|
||||||
- Backend-silent timeout paths can be shaped
|
|
||||||
In default mode, a client that keeps the socket half-open and times out will usually not receive shape padding on that path.
|
|
||||||
In aggressive mode, Telemt may still shape that backend-silent session if no backend bytes were returned.
|
|
||||||
This is specifically aimed at active probes that try to avoid EOF in order to preserve an exact backend-observed length.
|
|
||||||
|
|
||||||
- Above-cap blur always adds at least one byte
|
|
||||||
In default mode, above-cap blur may choose `0`, so some oversized probes still land on their exact base forwarded length.
|
|
||||||
In aggressive mode, that exact-base sample is removed by construction.
|
|
||||||
|
|
||||||
- Tradeoff
|
|
||||||
Aggressive mode improves resistance to active length classifiers, but it is more opinionated and less conservative.
|
|
||||||
If your deployment prioritizes strict compatibility with timeout/no-tail semantics, leave it disabled.
|
|
||||||
If your threat model includes repeated active probing by a censor, this mode is the stronger profile.
|
|
||||||
|
|
||||||
Use this mode only when your threat model prioritizes classifier resistance over strict compatibility with conservative masking semantics.
|
|
||||||
|
|
||||||
### Above-cap blur notes (`[censorship]`)
|
### Above-cap blur notes (`[censorship]`)
|
||||||
|
|
||||||
`mask_shape_above_cap_blur` adds a second-stage blur for very large probes that are already above `mask_shape_bucket_cap_bytes`.
|
`mask_shape_above_cap_blur` adds a second-stage blur for very large probes that are already above `mask_shape_bucket_cap_bytes`.
|
||||||
|
|
||||||
- A random tail in `[0, mask_shape_above_cap_blur_max_bytes]` is appended in default mode.
|
- A random tail in `[0, mask_shape_above_cap_blur_max_bytes]` is appended.
|
||||||
- In aggressive mode, the random tail becomes strictly positive: `[1, mask_shape_above_cap_blur_max_bytes]`.
|
|
||||||
- This reduces exact-size leakage above cap at bounded overhead.
|
- This reduces exact-size leakage above cap at bounded overhead.
|
||||||
- Keep `mask_shape_above_cap_blur_max_bytes` conservative to avoid unnecessary egress growth.
|
- Keep `mask_shape_above_cap_blur_max_bytes` conservative to avoid unnecessary egress growth.
|
||||||
|
|
||||||
Operational meaning:
|
|
||||||
|
|
||||||
- Without above-cap blur
|
|
||||||
A probe that forwards `5005` bytes will still look like `5005` bytes to the backend if it is already above cap.
|
|
||||||
|
|
||||||
- With above-cap blur enabled
|
|
||||||
That same probe may look like any value in a bounded window above its base length.
|
|
||||||
Example with `mask_shape_above_cap_blur_max_bytes = 64`:
|
|
||||||
backend-observed size becomes `5005..5069` in default mode, or `5006..5069` in aggressive mode.
|
|
||||||
|
|
||||||
- Choosing `mask_shape_above_cap_blur_max_bytes`
|
|
||||||
Small values reduce cost but preserve more separability between far-apart oversized classes.
|
|
||||||
Larger values blur oversized classes more aggressively, but add more egress overhead and more output variance.
|
|
||||||
|
|
||||||
### Timing normalization envelope notes (`[censorship]`)
|
### Timing normalization envelope notes (`[censorship]`)
|
||||||
|
|
||||||
`mask_timing_normalization_enabled` smooths timing differences between masking outcomes by applying a target duration envelope.
|
`mask_timing_normalization_enabled` smooths timing differences between masking outcomes by applying a target duration envelope.
|
||||||
|
|||||||
+42
-57
@@ -1,122 +1,107 @@
|
|||||||
## How to set up a "proxy sponsor" channel and statistics via the @MTProxybot
|
## How to set up "proxy sponsor" channel and statistics via @MTProxybot bot
|
||||||
|
|
||||||
1. Go to the @MTProxybot.
|
1. Go to @MTProxybot bot.
|
||||||
2. Enter the `/newproxy` command.
|
2. Enter the command `/newproxy`
|
||||||
3. Send your server's IP address and port. For example: `1.2.3.4:443`.
|
3. Send the server IP and port. For example: 1.2.3.4:443
|
||||||
4. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
4. Open the config `nano /etc/telemt.toml`.
|
||||||
5. Copy and send the user secret from the `[access.users]` section to the bot.
|
5. Copy and send the user secret from the [access.users] section to the bot.
|
||||||
6. Copy the tag provided by the bot. For example: `1234567890abcdef1234567890abcdef`.
|
6. Copy the tag received from the bot. For example 1234567890abcdef1234567890abcdef.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> The link provided by the bot will not work. Do not copy or use it!
|
> The link provided by the bot will not work. Do not copy or use it!
|
||||||
7. Uncomment the `ad_tag` parameter and enter the tag received from the bot.
|
7. Uncomment the ad_tag parameter and enter the tag received from the bot.
|
||||||
8. Uncomment or add the `use_middle_proxy = true` parameter.
|
8. Uncomment/add the parameter `use_middle_proxy = true`.
|
||||||
|
|
||||||
Configuration example:
|
Config example:
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
ad_tag = "1234567890abcdef1234567890abcdef"
|
ad_tag = "1234567890abcdef1234567890abcdef"
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
```
|
```
|
||||||
9. Save the changes (in nano: Ctrl+S -> Ctrl+X).
|
9. Save the config. Ctrl+S -> Ctrl+X.
|
||||||
10. Restart the telemt service: `systemctl restart telemt`.
|
10. Restart telemt `systemctl restart telemt`.
|
||||||
11. Send the `/myproxies` command to the bot and select the added server.
|
11. In the bot, send the command /myproxies and select the added server.
|
||||||
12. Click the "Set promotion" button.
|
12. Click the "Set promotion" button.
|
||||||
13. Send a **public link** to the channel. Private channels cannot be added!
|
13. Send a **public link** to the channel. Private channels cannot be added!
|
||||||
14. Wait for about 1 hour for the information to update on Telegram servers.
|
14. Wait approximately 1 hour for the information to update on Telegram servers.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> The sponsored channel will not be displayed to you if you are already subscribed to it.
|
> You will not see the "proxy sponsor" if you are already subscribed to the channel.
|
||||||
|
|
||||||
**You can also configure different sponsored channels for different users:**
|
**You can also set up different channels for different users.**
|
||||||
```toml
|
```toml
|
||||||
[access.user_ad_tags]
|
[access.user_ad_tags]
|
||||||
hello = "ad_tag"
|
hello = "ad_tag"
|
||||||
hello2 = "ad_tag2"
|
hello2 = "ad_tag2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why do you need a middle proxy (ME)
|
## How many people can use 1 link
|
||||||
https://github.com/telemt/telemt/discussions/167
|
|
||||||
|
|
||||||
|
By default, 1 link can be used by any number of people.
|
||||||
## How many people can use one link
|
You can limit the number of IPs using the proxy.
|
||||||
|
|
||||||
By default, an unlimited number of people can use a single link.
|
|
||||||
However, you can limit the number of unique IP addresses for each user:
|
|
||||||
```toml
|
```toml
|
||||||
[access.user_max_unique_ips]
|
[access.user_max_unique_ips]
|
||||||
hello = 1
|
hello = 1
|
||||||
```
|
```
|
||||||
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect. At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
|
This parameter limits how many unique IPs can use 1 link simultaneously. If one user disconnects, a second user can connect. Also, multiple users can sit behind the same IP.
|
||||||
|
|
||||||
## How to create multiple different links
|
## How to create multiple different links
|
||||||
|
|
||||||
1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
|
1. Generate the required number of secrets `openssl rand -hex 16`
|
||||||
2. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
2. Open the config `nano /etc/telemt.toml`
|
||||||
3. Add new users to the `[access.users]` section:
|
3. Add new users.
|
||||||
```toml
|
```toml
|
||||||
[access.users]
|
[access.users]
|
||||||
user1 = "00000000000000000000000000000001"
|
user1 = "00000000000000000000000000000001"
|
||||||
user2 = "00000000000000000000000000000002"
|
user2 = "00000000000000000000000000000002"
|
||||||
user3 = "00000000000000000000000000000003"
|
user3 = "00000000000000000000000000000003"
|
||||||
```
|
```
|
||||||
4. Save the configuration (Ctrl+S -> Ctrl+X). There is no need to restart the telemt service.
|
4. Save the config. Ctrl+S -> Ctrl+X. You don't need to restart telemt.
|
||||||
5. Get the ready-to-use links using the command:
|
5. Get the links via
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
## "Unknown TLS SNI" error
|
|
||||||
Usually, this error occurs if you have changed the `tls_domain` parameter, but users continue to connect using old links with the previous domain.
|
|
||||||
|
|
||||||
If you need to allow connections with any domains (ignoring SNI mismatches), add the following parameters:
|
|
||||||
```toml
|
|
||||||
[censorship]
|
|
||||||
unknown_sni_action = "mask"
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to view metrics
|
## How to view metrics
|
||||||
|
|
||||||
1. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
1. Open the config `nano /etc/telemt.toml`
|
||||||
2. Add the following parameters:
|
2. Add the following parameters
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
metrics_port = 9090
|
metrics_port = 9090
|
||||||
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
||||||
```
|
```
|
||||||
3. Save the changes (Ctrl+S -> Ctrl+X).
|
3. Save the config. Ctrl+S -> Ctrl+X.
|
||||||
4. After that, metrics will be available at: `SERVER_IP:9090/metrics`.
|
4. Metrics are available at SERVER_IP:9090/metrics.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> The value `"0.0.0.0/0"` in `metrics_whitelist` opens access to metrics from any IP address. It is recommended to replace it with your personal IP, for example: `"1.2.3.4/32"`.
|
> "0.0.0.0/0" in metrics_whitelist opens access from any IP. Replace with your own IP. For example "1.2.3.4"
|
||||||
|
|
||||||
## Additional parameters
|
## Additional parameters
|
||||||
|
|
||||||
### Domain in the link instead of IP
|
### Domain in link instead of IP
|
||||||
To display a domain instead of an IP address in the connection links, add the following lines to the configuration file:
|
To specify a domain in the links, add to the `[general.links]` section of the config file.
|
||||||
```toml
|
```toml
|
||||||
[general.links]
|
[general.links]
|
||||||
public_host = "proxy.example.com"
|
public_host = "proxy.example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Total server connection limit
|
### Server connection limit
|
||||||
This parameter limits the total number of active connections to the server:
|
Limits the total number of open connections to the server:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
max_connections = 10000 # 0 - unlimited, 10000 - default
|
max_connections = 10000 # 0 - unlimited, 10000 - default
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upstream Manager
|
### Upstream Manager
|
||||||
To configure outbound connections (upstreams), add the corresponding parameters to the `[[upstreams]]` section of the configuration file:
|
To specify an upstream, add to the `[[upstreams]]` section of the config.toml file:
|
||||||
|
#### Binding to IP
|
||||||
#### Binding to an outbound IP address
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "direct"
|
type = "direct"
|
||||||
weight = 1
|
weight = 1
|
||||||
enabled = true
|
enabled = true
|
||||||
interface = "192.168.1.100" # Replace with your outbound IP
|
interface = "192.168.1.100" # Change to your outgoing IP
|
||||||
```
|
```
|
||||||
|
#### SOCKS4/5 as Upstream
|
||||||
#### Using SOCKS4/5 as an Upstream
|
- Without authentication:
|
||||||
- Without authorization:
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||||
@@ -125,7 +110,7 @@ weight = 1 # Set Weight for Scenarios
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
- With authorization:
|
- With authentication:
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||||
@@ -136,8 +121,8 @@ weight = 1 # Set Weight for Scenarios
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using Shadowsocks as an Upstream
|
#### Shadowsocks as Upstream
|
||||||
For this method to work, the `use_middle_proxy = false` parameter must be set.
|
Requires `use_middle_proxy = false`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
|
|||||||
+42
-57
@@ -1,121 +1,106 @@
|
|||||||
## Как настроить канал "спонсор прокси" и статистику через бота @MTProxybot
|
## Как настроить канал "спонсор прокси" и статистику через бота @MTProxybot
|
||||||
|
|
||||||
1. Зайдите в бота @MTProxybot.
|
1. Зайти в бота @MTProxybot.
|
||||||
2. Введите команду `/newproxy`.
|
2. Ввести команду `/newproxy`
|
||||||
3. Отправьте IP-адрес и порт сервера. Например: `1.2.3.4:443`.
|
3. Отправить IP и порт сервера. Например: 1.2.3.4:443
|
||||||
4. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
4. Открыть конфиг `nano /etc/telemt.toml`.
|
||||||
5. Скопируйте и отправьте боту секрет пользователя из раздела `[access.users]`.
|
5. Скопировать и отправить боту секрет пользователя из раздела [access.users].
|
||||||
6. Скопируйте тег (tag), который выдаст бот. Например: `1234567890abcdef1234567890abcdef`.
|
6. Скопировать полученный tag у бота. Например 1234567890abcdef1234567890abcdef.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Ссылка, которую выдает бот, работать не будет. Не копируйте и не используйте её!
|
> Ссылка, которую выдает бот, не будет работать. Не копируйте и не используйте её!
|
||||||
7. Раскомментируйте параметр `ad_tag` и впишите тег, полученный от бота.
|
7. Раскомментировать параметр ad_tag и вписать tag, полученный у бота.
|
||||||
8. Раскомментируйте или добавьте параметр `use_middle_proxy = true`.
|
8. Раскомментировать/добавить параметр use_middle_proxy = true.
|
||||||
|
|
||||||
Пример конфигурации:
|
Пример конфига:
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
ad_tag = "1234567890abcdef1234567890abcdef"
|
ad_tag = "1234567890abcdef1234567890abcdef"
|
||||||
use_middle_proxy = true
|
use_middle_proxy = true
|
||||||
```
|
```
|
||||||
9. Сохраните изменения (в nano: Ctrl+S -> Ctrl+X).
|
9. Сохранить конфиг. Ctrl+S -> Ctrl+X.
|
||||||
10. Перезапустите службу telemt: `systemctl restart telemt`.
|
10. Перезапустить telemt `systemctl restart telemt`.
|
||||||
11. В боте отправьте команду `/myproxies` и выберите добавленный сервер.
|
11. В боте отправить команду /myproxies и выбрать добавленный сервер.
|
||||||
12. Нажмите кнопку «Set promotion».
|
12. Нажать кнопку "Set promotion".
|
||||||
13. Отправьте **публичную ссылку** на канал. Приватные каналы добавлять нельзя!
|
13. Отправить **публичную ссылку** на канал. Приватный канал добавить нельзя!
|
||||||
14. Подождите примерно 1 час, пока информация обновится на серверах Telegram.
|
14. Подождать примерно 1 час, пока информация обновится на серверах Telegram.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Спонсорский канал не будет у вас отображаться, если вы уже на него подписаны.
|
> У вас не будет отображаться "спонсор прокси" если вы уже подписаны на канал.
|
||||||
|
|
||||||
**Вы также можете настроить разные спонсорские каналы для разных пользователей:**
|
**Также вы можете настроить разные каналы для разных пользователей.**
|
||||||
```toml
|
```toml
|
||||||
[access.user_ad_tags]
|
[access.user_ad_tags]
|
||||||
hello = "ad_tag"
|
hello = "ad_tag"
|
||||||
hello2 = "ad_tag2"
|
hello2 = "ad_tag2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Зачем нужен middle proxy (ME)
|
## Сколько человек может пользоваться 1 ссылкой
|
||||||
https://github.com/telemt/telemt/discussions/167
|
|
||||||
|
|
||||||
|
По умолчанию 1 ссылкой может пользоваться сколько угодно человек.
|
||||||
## Сколько человек может пользоваться одной ссылкой
|
Вы можете ограничить число IP, использующих прокси.
|
||||||
|
|
||||||
По умолчанию одной ссылкой может пользоваться неограниченное число людей.
|
|
||||||
Однако вы можете ограничить количество уникальных IP-адресов для каждого пользователя:
|
|
||||||
```toml
|
```toml
|
||||||
[access.user_max_unique_ips]
|
[access.user_max_unique_ips]
|
||||||
hello = 1
|
hello = 1
|
||||||
```
|
```
|
||||||
Этот параметр задает максимальное количество уникальных IP-адресов, с которых можно одновременно использовать одну ссылку. Если первый пользователь отключится, второй сможет подключиться. При этом с одного IP-адреса могут подключаться несколько пользователей одновременно (например, устройства в одной Wi-Fi сети).
|
Этот параметр ограничивает, сколько уникальных IP может использовать 1 ссылку одновременно. Если один пользователь отключится, второй сможет подключиться. Также с одного IP может сидеть несколько пользователей.
|
||||||
|
|
||||||
## Как создать несколько разных ссылок
|
## Как сделать несколько разных ссылок
|
||||||
|
|
||||||
1. Сгенерируйте необходимое количество секретов с помощью команды: `openssl rand -hex 16`.
|
1. Сгенерируйте нужное число секретов `openssl rand -hex 16`
|
||||||
2. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
2. Открыть конфиг `nano /etc/telemt.toml`
|
||||||
3. Добавьте новых пользователей в секцию `[access.users]`:
|
3. Добавить новых пользователей.
|
||||||
```toml
|
```toml
|
||||||
[access.users]
|
[access.users]
|
||||||
user1 = "00000000000000000000000000000001"
|
user1 = "00000000000000000000000000000001"
|
||||||
user2 = "00000000000000000000000000000002"
|
user2 = "00000000000000000000000000000002"
|
||||||
user3 = "00000000000000000000000000000003"
|
user3 = "00000000000000000000000000000003"
|
||||||
```
|
```
|
||||||
4. Сохраните конфигурацию (Ctrl+S -> Ctrl+X). Перезапускать службу telemt не нужно.
|
4. Сохранить конфиг. Ctrl+S -> Ctrl+X. Перезапускать telemt не нужно.
|
||||||
5. Получите готовые ссылки с помощью команды:
|
5. Получить ссылки через
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ошибка "Unknown TLS SNI"
|
|
||||||
Обычно эта ошибка возникает, если вы изменили параметр `tls_domain`, но пользователи продолжают подключаться по старым ссылкам с прежним доменом.
|
|
||||||
|
|
||||||
Если необходимо разрешить подключение с любыми доменами (игнорируя несовпадения SNI), добавьте следующие параметры:
|
|
||||||
```toml
|
|
||||||
[censorship]
|
|
||||||
unknown_sni_action = "mask"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как посмотреть метрики
|
## Как посмотреть метрики
|
||||||
|
|
||||||
1. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
|
1. Открыть конфиг `nano /etc/telemt.toml`
|
||||||
2. Добавьте следующие параметры:
|
2. Добавить следующие параметры
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
metrics_port = 9090
|
metrics_port = 9090
|
||||||
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
||||||
```
|
```
|
||||||
3. Сохраните изменения (Ctrl+S -> Ctrl+X).
|
3. Сохранить конфиг. Ctrl+S -> Ctrl+X.
|
||||||
4. После этого метрики будут доступны по адресу: `SERVER_IP:9090/metrics`.
|
4. Метрики доступны по адресу SERVER_IP:9090/metrics.
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Значение `"0.0.0.0/0"` в `metrics_whitelist` открывает доступ к метрикам с любого IP-адреса. Рекомендуется заменить его на ваш личный IP, например: `"1.2.3.4/32"`.
|
> "0.0.0.0/0" в metrics_whitelist открывает доступ с любого IP. Замените на свой ip. Например "1.2.3.4"
|
||||||
|
|
||||||
## Дополнительные параметры
|
## Дополнительные параметры
|
||||||
|
|
||||||
### Домен в ссылке вместо IP
|
### Домен в ссылке вместо IP
|
||||||
Чтобы в ссылках для подключения отображался домен вместо IP-адреса, добавьте следующие строки в файл конфигурации:
|
Чтобы указать домен в ссылках, добавьте в секцию `[general.links]` файла config.
|
||||||
```toml
|
```toml
|
||||||
[general.links]
|
[general.links]
|
||||||
public_host = "proxy.example.com"
|
public_host = "proxy.example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Общий лимит подключений к серверу
|
### Общий лимит подключений к серверу
|
||||||
Этот параметр ограничивает общее количество активных подключений к серверу:
|
Ограничивает общее число открытых подключений к серверу:
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
max_connections = 10000 # 0 - без ограничений, 10000 - по умолчанию
|
max_connections = 10000 # 0 - unlimited, 10000 - default
|
||||||
```
|
```
|
||||||
|
|
||||||
### Upstream Manager
|
### Upstream Manager
|
||||||
Для настройки исходящих подключений (апстримов) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
|
Чтобы указать апстрим, добавьте в секцию `[[upstreams]]` файла config.toml:
|
||||||
|
#### Привязка к IP
|
||||||
#### Привязка к исходящему IP-адресу
|
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
type = "direct"
|
type = "direct"
|
||||||
weight = 1
|
weight = 1
|
||||||
enabled = true
|
enabled = true
|
||||||
interface = "192.168.1.100" # Замените на ваш исходящий IP
|
interface = "192.168.1.100" # Change to your outgoing IP
|
||||||
```
|
```
|
||||||
|
#### SOCKS4/5 как Upstream
|
||||||
#### Использование SOCKS4/5 в качестве Upstream
|
|
||||||
- Без авторизации:
|
- Без авторизации:
|
||||||
```toml
|
```toml
|
||||||
[[upstreams]]
|
[[upstreams]]
|
||||||
@@ -136,8 +121,8 @@ weight = 1 # Set Weight for Scenarios
|
|||||||
enabled = true
|
enabled = true
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Использование Shadowsocks в качестве Upstream
|
#### Shadowsocks как Upstream
|
||||||
Для работы этого метода требуется установить параметр `use_middle_proxy = false`.
|
Требует `use_middle_proxy = false`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ chmod +x /bin/telemt
|
|||||||
|
|
||||||
**0. Check port and generate secrets**
|
**0. Check port and generate secrets**
|
||||||
|
|
||||||
The port you have selected for use should not be in the list:
|
The port you have selected for use should be MISSING from the list, when:
|
||||||
```bash
|
```bash
|
||||||
netstat -lnp
|
netstat -lnp
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate 16 bytes/32 characters in HEX format with OpenSSL or another way:
|
Generate 16 bytes/32 characters HEX with OpenSSL or another way:
|
||||||
```bash
|
```bash
|
||||||
openssl rand -hex 16
|
openssl rand -hex 16
|
||||||
```
|
```
|
||||||
@@ -50,7 +50,7 @@ Save the obtained result somewhere. You will need it later!
|
|||||||
|
|
||||||
**1. Place your config to /etc/telemt/telemt.toml**
|
**1. Place your config to /etc/telemt/telemt.toml**
|
||||||
|
|
||||||
Create the config directory:
|
Create config directory:
|
||||||
```bash
|
```bash
|
||||||
mkdir /etc/telemt
|
mkdir /etc/telemt
|
||||||
```
|
```
|
||||||
@@ -59,7 +59,7 @@ Open nano
|
|||||||
```bash
|
```bash
|
||||||
nano /etc/telemt/telemt.toml
|
nano /etc/telemt/telemt.toml
|
||||||
```
|
```
|
||||||
Insert your configuration:
|
paste your config
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# === General Settings ===
|
# === General Settings ===
|
||||||
@@ -94,8 +94,7 @@ then Ctrl+S -> Ctrl+X to save
|
|||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Replace the value of the hello parameter with the value you obtained in step 0.
|
> Replace the value of the hello parameter with the value you obtained in step 0.
|
||||||
> Additionally, change the value of the tls_domain parameter to a different website.
|
> Replace the value of the tls_domain parameter with another website.
|
||||||
> Changing the tls_domain parameter will break all links that use the old domain!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -106,14 +105,14 @@ useradd -d /opt/telemt -m -r -U telemt
|
|||||||
chown -R telemt:telemt /etc/telemt
|
chown -R telemt:telemt /etc/telemt
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Create service in /etc/systemd/system/telemt.service**
|
**3. Create service on /etc/systemd/system/telemt.service**
|
||||||
|
|
||||||
Open nano
|
Open nano
|
||||||
```bash
|
```bash
|
||||||
nano /etc/systemd/system/telemt.service
|
nano /etc/systemd/system/telemt.service
|
||||||
```
|
```
|
||||||
|
|
||||||
Insert this Systemd module:
|
paste this Systemd Module
|
||||||
```bash
|
```bash
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Telemt
|
Description=Telemt
|
||||||
@@ -148,16 +147,13 @@ systemctl daemon-reload
|
|||||||
|
|
||||||
**6.** For automatic startup at system boot, enter `systemctl enable telemt`
|
**6.** For automatic startup at system boot, enter `systemctl enable telemt`
|
||||||
|
|
||||||
**7.** To get the link(s), enter:
|
**7.** To get the link(s), enter
|
||||||
```bash
|
```bash
|
||||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
> Any number of people can use one link.
|
> Any number of people can use one link.
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> Only the command from step 7 can provide a working link. Do not try to create it yourself or copy it from anywhere if you are not sure what you are doing!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Telemt via Docker Compose
|
# Telemt via Docker Compose
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ hello = "00000000000000000000000000000000"
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
|
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
|
||||||
> Так же замените значение параметра tls_domain на другой сайт.
|
> Так же замените значение параметра tls_domain на другой сайт.
|
||||||
> Изменение параметра tls_domain сделает нерабочими все ссылки, использующие старый домен!
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
|
|
||||||
|
|
||||||
## Concept
|
|
||||||
- **Server A** (__conditionally Russian Federation_):\
|
|
||||||
Entry point, receives Telegram proxy user traffic via **HAProxy** (port `443`)\
|
|
||||||
and sends it to the tunnel to Server **B**.\
|
|
||||||
Internal IP in the tunnel — `10.10.10.2`\
|
|
||||||
Port for HAProxy clients — `443\tcp`
|
|
||||||
- **Server B** (_conditionally Netherlands_):\
|
|
||||||
Exit point, runs **telemt** and accepts client connections through Server **A**.\
|
|
||||||
The server must have unrestricted access to Telegram servers.\
|
|
||||||
Internal IP in the tunnel — `10.10.10.1`\
|
|
||||||
AmneziaWG port — `8443\udp`\
|
|
||||||
Port for telemt clients — `443\tcp`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1. Setting up the AmneziaWG tunnel (A <-> B)
|
|
||||||
[AmneziaWG](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module) must be installed on all servers.\
|
|
||||||
All following commands are given for **Ubuntu 24.04**.\
|
|
||||||
For RHEL-based distributions, installation instructions are available at the link above.
|
|
||||||
|
|
||||||
### Installing AmneziaWG (Servers A and B)
|
|
||||||
The following steps must be performed on each server:
|
|
||||||
|
|
||||||
#### 1. Adding the AmneziaWG repository and installing required packages:
|
|
||||||
```bash
|
|
||||||
sudo apt install -y software-properties-common python3-launchpadlib gnupg2 linux-headers-$(uname -r) && \
|
|
||||||
sudo add-apt-repository ppa:amnezia/ppa && \
|
|
||||||
sudo apt-get install -y amneziawg
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Generating a unique key pair:
|
|
||||||
```bash
|
|
||||||
cd /etc/amnezia/amneziawg && \
|
|
||||||
awg genkey | tee private.key | awg pubkey > public.key
|
|
||||||
```
|
|
||||||
|
|
||||||
As a result, you will get two files in the `/etc/amnezia/amneziawg` folder:\
|
|
||||||
`private.key` - private, and\
|
|
||||||
`public.key` - public server keys
|
|
||||||
|
|
||||||
#### 3. Configuring network interfaces:
|
|
||||||
Obfuscation parameters `S1`, `S2`, `H1`, `H2`, `H3`, `H4` must be strictly identical on both servers.\
|
|
||||||
Parameters `Jc`, `Jmin` and `Jmax` can differ.\
|
|
||||||
Parameters `I1-I5` ([Custom Protocol Signature](https://docs.amnezia.org/documentation/amnezia-wg/)) must be specified on the client side (Server **A**).
|
|
||||||
|
|
||||||
Recommendations for choosing values:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Jc — 1 ≤ Jc ≤ 128; from 4 to 12 inclusive
|
|
||||||
Jmin — Jmax > Jmin < 1280*; recommended 8
|
|
||||||
Jmax — Jmin < Jmax ≤ 1280*; recommended 80
|
|
||||||
S1 — S1 ≤ 1132* (1280* - 148 = 1132); S1 + 56 ≠ S2;
|
|
||||||
recommended range from 15 to 150 inclusive
|
|
||||||
S2 — S2 ≤ 1188* (1280* - 92 = 1188);
|
|
||||||
recommended range from 15 to 150 inclusive
|
|
||||||
H1/H2/H3/H4 — must be unique and differ from each other;
|
|
||||||
recommended range from 5 to 2147483647 inclusive
|
|
||||||
|
|
||||||
* It is assumed that the Internet connection has an MTU of 1280.
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> 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.
|
|
||||||
|
|
||||||
#### Server B Configuration (Netherlands):
|
|
||||||
|
|
||||||
Create the interface configuration file (`awg0`)
|
|
||||||
```bash
|
|
||||||
nano /etc/amnezia/amneziawg/awg0.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
File content
|
|
||||||
```ini
|
|
||||||
[Interface]
|
|
||||||
Address = 10.10.10.1/24
|
|
||||||
ListenPort = 8443
|
|
||||||
PrivateKey = <PRIVATE_KEY_SERVER_B>
|
|
||||||
SaveConfig = true
|
|
||||||
Jc = 4
|
|
||||||
Jmin = 8
|
|
||||||
Jmax = 80
|
|
||||||
S1 = 29
|
|
||||||
S2 = 15
|
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
|
||||||
H2 = 188817757
|
|
||||||
H3 = 101784570
|
|
||||||
H4 = 432174303
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <PUBLIC_KEY_SERVER_A>
|
|
||||||
AllowedIPs = 10.10.10.2/32
|
|
||||||
```
|
|
||||||
`ListenPort` - the port on which the server will wait for connections, you can choose any free one.\
|
|
||||||
`<PRIVATE_KEY_SERVER_B>` - the content of the `private.key` file from Server **B**.\
|
|
||||||
`<PUBLIC_KEY_SERVER_A>` - the content of the `public.key` file from Server **A**.
|
|
||||||
|
|
||||||
Open the port on the firewall (if enabled):
|
|
||||||
```bash
|
|
||||||
sudo ufw allow from <PUBLIC_IP_SERVER_A> to any port 8443 proto udp
|
|
||||||
```
|
|
||||||
|
|
||||||
`<PUBLIC_IP_SERVER_A>` - the external IP address of Server **A**.
|
|
||||||
|
|
||||||
#### Server A Configuration (Russian Federation):
|
|
||||||
Create the interface configuration file (awg0)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano /etc/amnezia/amneziawg/awg0.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
File content
|
|
||||||
```ini
|
|
||||||
[Interface]
|
|
||||||
Address = 10.10.10.2/24
|
|
||||||
PrivateKey = <PRIVATE_KEY_SERVER_A>
|
|
||||||
Jc = 4
|
|
||||||
Jmin = 8
|
|
||||||
Jmax = 80
|
|
||||||
S1 = 29
|
|
||||||
S2 = 15
|
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
|
||||||
H2 = 188817757
|
|
||||||
H3 = 101784570
|
|
||||||
H4 = 432174303
|
|
||||||
I1 = <b 0xc10000000108981eba846e21f74e00>
|
|
||||||
I2 = <b 0xc20000000108981eba846e21f74e00>
|
|
||||||
I3 = <b 0xc30000000108981eba846e21f74e00>
|
|
||||||
I4 = <b 0x43981eba846e21f74e>
|
|
||||||
I5 = <b 0x43981eba846e21f74e>
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <PUBLIC_KEY_SERVER_B>
|
|
||||||
Endpoint = <PUBLIC_IP_SERVER_B>:8443
|
|
||||||
AllowedIPs = 10.10.10.1/32
|
|
||||||
PersistentKeepalive = 25
|
|
||||||
```
|
|
||||||
|
|
||||||
`<PRIVATE_KEY_SERVER_A>` - the content of the `private.key` file from Server **A**.\
|
|
||||||
`<PUBLIC_KEY_SERVER_B>` - the content of the `public.key` file from Server **B**.\
|
|
||||||
`<PUBLIC_IP_SERVER_B>` - the public IP address of Server **B**.
|
|
||||||
|
|
||||||
Enable the tunnel on both servers:
|
|
||||||
```bash
|
|
||||||
sudo systemctl enable --now awg-quick@awg0
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure Server B is accessible from Server A through the tunnel.
|
|
||||||
```bash
|
|
||||||
ping 10.10.10.1
|
|
||||||
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=35.1 ms
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=35.0 ms
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=35.1 ms
|
|
||||||
^C
|
|
||||||
```
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2. Installing telemt on Server B (conditionally Netherlands)
|
|
||||||
Installation and configuration are described [here](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) or [here](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
|
|
||||||
It is assumed that telemt expects connections on port `443\tcp`.
|
|
||||||
|
|
||||||
In the telemt config, you must enable the `Proxy` protocol and restrict connections to it only through the tunnel.
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
port = 443
|
|
||||||
listen_addr_ipv4 = "10.10.10.1"
|
|
||||||
proxy_protocol = true
|
|
||||||
```
|
|
||||||
|
|
||||||
Also, for correct link generation, specify the FQDN or IP address and port of Server `A`
|
|
||||||
```toml
|
|
||||||
[general.links]
|
|
||||||
show = "*"
|
|
||||||
public_host = "<FQDN_OR_IP_SERVER_A>"
|
|
||||||
public_port = 443
|
|
||||||
```
|
|
||||||
|
|
||||||
Open the port on the firewall (if enabled):
|
|
||||||
```bash
|
|
||||||
sudo ufw allow from 10.10.10.2 to any port 443 proto tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3. Configuring HAProxy on Server A (Russian Federation)
|
|
||||||
Since the version in the standard Ubuntu repository is relatively old, it makes sense to use the official Docker image.\
|
|
||||||
[Instructions](https://docs.docker.com/engine/install/ubuntu/) for installing Docker on Ubuntu.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> By default, regular users do not have rights to use ports < 1024.
|
|
||||||
> Attempts to run HAProxy on port 443 can lead to errors:
|
|
||||||
> ```
|
|
||||||
> [ALERT] (8) : Binding [/usr/local/etc/haproxy/haproxy.cfg:17] for frontend tcp_in_443:
|
|
||||||
> protocol tcpv4: cannot bind socket (Permission denied) for [0.0.0.0:443].
|
|
||||||
> ```
|
|
||||||
> There are two simple ways to bypass this restriction, choose one:
|
|
||||||
> 1. At the OS level, change the net.ipv4.ip_unprivileged_port_start setting to allow users to use all ports:
|
|
||||||
> ```
|
|
||||||
> echo "net.ipv4.ip_unprivileged_port_start = 0" | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
|
||||||
> ```
|
|
||||||
> or
|
|
||||||
>
|
|
||||||
> 2. Run HAProxy as root:
|
|
||||||
> Uncomment the `user: "root"` parameter in docker-compose.yaml.
|
|
||||||
|
|
||||||
#### Create a folder for HAProxy:
|
|
||||||
```bash
|
|
||||||
mkdir -p /opt/docker-compose/haproxy && cd $_
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create the docker-compose.yaml file
|
|
||||||
`nano docker-compose.yaml`
|
|
||||||
|
|
||||||
File content
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
haproxy:
|
|
||||||
image: haproxy:latest
|
|
||||||
container_name: haproxy
|
|
||||||
restart: unless-stopped
|
|
||||||
# user: "root"
|
|
||||||
network_mode: "host"
|
|
||||||
volumes:
|
|
||||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "1"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Create the haproxy.cfg config file
|
|
||||||
Accept connections on port 443\tcp and send them through the tunnel to Server `B` 10.10.10.1:443
|
|
||||||
|
|
||||||
`nano haproxy.cfg`
|
|
||||||
|
|
||||||
File content
|
|
||||||
|
|
||||||
```haproxy
|
|
||||||
global
|
|
||||||
log stdout format raw local0
|
|
||||||
maxconn 10000
|
|
||||||
|
|
||||||
defaults
|
|
||||||
log global
|
|
||||||
mode tcp
|
|
||||||
option tcplog
|
|
||||||
option clitcpka
|
|
||||||
option srvtcpka
|
|
||||||
timeout connect 5s
|
|
||||||
timeout client 2h
|
|
||||||
timeout server 2h
|
|
||||||
timeout check 5s
|
|
||||||
|
|
||||||
frontend tcp_in_443
|
|
||||||
bind *:443
|
|
||||||
maxconn 8000
|
|
||||||
option tcp-smart-accept
|
|
||||||
default_backend telemt_nodes
|
|
||||||
|
|
||||||
backend telemt_nodes
|
|
||||||
option tcp-smart-connect
|
|
||||||
server server_a 10.10.10.1:443 check inter 5s rise 2 fall 3 send-proxy-v2
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
> [!WARNING]
|
|
||||||
> **The file must end with an empty line, otherwise HAProxy will not start!**
|
|
||||||
|
|
||||||
#### Allow port 443\tcp in the firewall (if enabled)
|
|
||||||
```bash
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Start the HAProxy container
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
If everything is configured correctly, you can now try connecting Telegram clients using links from the telemt log\api.
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
|
|
||||||
|
|
||||||
## Концепция
|
|
||||||
- **Сервер A** (_РФ_):\
|
|
||||||
Точка входа, принимает трафик пользователей Telegram-прокси через **HAProxy** (порт `443`)\
|
|
||||||
и отправляет в туннель на Сервер **B**.\
|
|
||||||
Внутренний IP в туннеле — `10.10.10.2`\
|
|
||||||
Порт для клиентов HAProxy — `443\tcp`
|
|
||||||
- **Сервер B** (_условно Нидерланды_):\
|
|
||||||
Точка выхода, на нем работает **telemt** и принимает подключения клиентов через Сервер **A**.\
|
|
||||||
На сервере должен быть неограниченный доступ до серверов Telegram.\
|
|
||||||
Внутренний IP в туннеле — `10.10.10.1`\
|
|
||||||
Порт AmneziaWG — `8443\udp`\
|
|
||||||
Порт для клиентов telemt — `443\tcp`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Шаг 1. Настройка туннеля AmneziaWG (A <-> B)
|
|
||||||
|
|
||||||
На всех серверах необходимо установить [amneziawg](https://github.com/amnezia-vpn/amneziawg-linux-kernel-module).\
|
|
||||||
Далее все команды даны для **Ununtu 24.04**.\
|
|
||||||
Для RHEL-based дистрибутивов инструкция по установке есть по ссылке выше.
|
|
||||||
|
|
||||||
### Установка AmneziaWG (Сервера A и B)
|
|
||||||
На каждом из серверов необходимо выполнить следующие шаги:
|
|
||||||
|
|
||||||
#### 1. Добавление репозитория AmneziaWG и установка необходимых пакетов:
|
|
||||||
```bash
|
|
||||||
sudo apt install -y software-properties-common python3-launchpadlib gnupg2 linux-headers-$(uname -r) && \
|
|
||||||
sudo add-apt-repository ppa:amnezia/ppa && \
|
|
||||||
sudo apt-get install -y amneziawg
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Генерация уникальной пары ключей:
|
|
||||||
```bash
|
|
||||||
cd /etc/amnezia/amneziawg && \
|
|
||||||
awg genkey | tee private.key | awg pubkey > public.key
|
|
||||||
```
|
|
||||||
В результате вы получите в папке `/etc/amnezia/amneziawg` два файла:\
|
|
||||||
`private.key` - приватный и\
|
|
||||||
`public.key` - публичный ключи сервера
|
|
||||||
|
|
||||||
#### 3. Настройка сетевых интерфейсов:
|
|
||||||
|
|
||||||
Параметры обфускации `S1`, `S2`, `H1`, `H2`, `H3`, `H4` должны быть строго идентичными на обоих серверах.\
|
|
||||||
Параметры `Jc`, `Jmin` и `Jmax` могут отличатся.\
|
|
||||||
Параметры `I1-I5` ([Custom Protocol Signature](https://docs.amnezia.org/documentation/amnezia-wg/)) нужно указывать на стороне _клиента_ (Сервер **А**).
|
|
||||||
|
|
||||||
Рекомендации по выбору значений:
|
|
||||||
```text
|
|
||||||
Jc — 1 ≤ Jc ≤ 128; от 4 до 12 включительно
|
|
||||||
Jmin — Jmax > Jmin < 1280*; рекомендовано 8
|
|
||||||
Jmax — Jmin < Jmax ≤ 1280*; рекомендовано 80
|
|
||||||
S1 — S1 ≤ 1132* (1280* - 148 = 1132); S1 + 56 ≠ S2;
|
|
||||||
рекомендованный диапазон от 15 до 150 включительно
|
|
||||||
S2 — S2 ≤ 1188* (1280* - 92 = 1188);
|
|
||||||
рекомендованный диапазон от 15 до 150 включительно
|
|
||||||
H1/H2/H3/H4 — должны быть уникальны и отличаться друг от друга;
|
|
||||||
рекомендованный диапазон от 5 до 2147483647 включительно
|
|
||||||
|
|
||||||
* Предполагается, что подключение к Интернету имеет MTU 1280.
|
|
||||||
```
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Рекомендуется использовать собственные, уникальные значения.\
|
|
||||||
> Для выбора параметров можете воспользоваться [генератором](https://htmlpreview.github.io/?https://gist.githubusercontent.com/avbor/955782b5c37b06240b243aa375baeac5/raw/13f5517ca473b47c412b9a99407066de973732bd/awg-gen.html).
|
|
||||||
|
|
||||||
#### Конфигурация Сервера B (_Нидерланды_):
|
|
||||||
|
|
||||||
Создаем файл конфигурации интерфейса (`awg0`)
|
|
||||||
```bash
|
|
||||||
nano /etc/amnezia/amneziawg/awg0.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
Содержимое файла
|
|
||||||
```ini
|
|
||||||
[Interface]
|
|
||||||
Address = 10.10.10.1/24
|
|
||||||
ListenPort = 8443
|
|
||||||
PrivateKey = <PRIVATE_KEY_SERVER_B>
|
|
||||||
SaveConfig = true
|
|
||||||
Jc = 4
|
|
||||||
Jmin = 8
|
|
||||||
Jmax = 80
|
|
||||||
S1 = 29
|
|
||||||
S2 = 15
|
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
|
||||||
H2 = 188817757
|
|
||||||
H3 = 101784570
|
|
||||||
H4 = 432174303
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <PUBLIC_KEY_SERVER_A>
|
|
||||||
AllowedIPs = 10.10.10.2/32
|
|
||||||
```
|
|
||||||
|
|
||||||
`ListenPort` - порт, на котором сервер будет ждать подключения, можете выбрать любой свободный.\
|
|
||||||
`<PRIVATE_KEY_SERVER_B>` - содержимое файла `private.key` с сервера **B**.\
|
|
||||||
`<PUBLIC_KEY_SERVER_A>` - содержимое файла `public.key` с сервера **A**.
|
|
||||||
|
|
||||||
Открываем порт на фаерволе (если включен):
|
|
||||||
```bash
|
|
||||||
sudo ufw allow from <PUBLIC_IP_SERVER_A> to any port 8443 proto udp
|
|
||||||
```
|
|
||||||
|
|
||||||
`<PUBLIC_IP_SERVER_A>` - внешний IP адрес Сервера **A**.
|
|
||||||
|
|
||||||
#### Конфигурация Сервера A (_РФ_):
|
|
||||||
|
|
||||||
Создаем файл конфигурации интерфейса (`awg0`)
|
|
||||||
```bash
|
|
||||||
nano /etc/amnezia/amneziawg/awg0.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
Содержимое файла
|
|
||||||
```ini
|
|
||||||
[Interface]
|
|
||||||
Address = 10.10.10.2/24
|
|
||||||
PrivateKey = <PRIVATE_KEY_SERVER_A>
|
|
||||||
Jc = 4
|
|
||||||
Jmin = 8
|
|
||||||
Jmax = 80
|
|
||||||
S1 = 29
|
|
||||||
S2 = 15
|
|
||||||
S3 = 18
|
|
||||||
S4 = 0
|
|
||||||
H1 = 2087563914
|
|
||||||
H2 = 188817757
|
|
||||||
H3 = 101784570
|
|
||||||
H4 = 432174303
|
|
||||||
I1 = <b 0xc10000000108981eba846e21f74e00>
|
|
||||||
I2 = <b 0xc20000000108981eba846e21f74e00>
|
|
||||||
I3 = <b 0xc30000000108981eba846e21f74e00>
|
|
||||||
I4 = <b 0x43981eba846e21f74e>
|
|
||||||
I5 = <b 0x43981eba846e21f74e>
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = <PUBLIC_KEY_SERVER_B>
|
|
||||||
Endpoint = <PUBLIC_IP_SERVER_B>:8443
|
|
||||||
AllowedIPs = 10.10.10.1/32
|
|
||||||
PersistentKeepalive = 25
|
|
||||||
```
|
|
||||||
|
|
||||||
`<PRIVATE_KEY_SERVER_A>` - содержимое файла `private.key` с сервера **A**.\
|
|
||||||
`<PUBLIC_KEY_SERVER_B>` - содержимое файла `public.key` с сервера **B**.\
|
|
||||||
`<PUBLIC_IP_SERVER_B>` - публичный IP адресс сервера **B**.
|
|
||||||
|
|
||||||
#### Включаем туннель на обоих серверах:
|
|
||||||
```bash
|
|
||||||
sudo systemctl enable --now awg-quick@awg0
|
|
||||||
```
|
|
||||||
|
|
||||||
Убедитесь, что с Сервера `A` доступен Сервер `B` через туннель.
|
|
||||||
```bash
|
|
||||||
ping 10.10.10.1
|
|
||||||
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=35.1 ms
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=35.0 ms
|
|
||||||
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=35.1 ms
|
|
||||||
^C
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Шаг 2. Установка telemt на Сервере B (_условно Нидерланды_)
|
|
||||||
|
|
||||||
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) или [здесь](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
|
|
||||||
Подразумевается что telemt ожидает подключения на порту `443\tcp`.
|
|
||||||
|
|
||||||
В конфиге telemt необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
port = 443
|
|
||||||
listen_addr_ipv4 = "10.10.10.1"
|
|
||||||
proxy_protocol = true
|
|
||||||
```
|
|
||||||
|
|
||||||
А также, для правильной генерации ссылок, указать FQDN или IP адрес и порт Сервера `A`
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[general.links]
|
|
||||||
show = "*"
|
|
||||||
public_host = "<FQDN_OR_IP_SERVER_A>"
|
|
||||||
public_port = 443
|
|
||||||
```
|
|
||||||
|
|
||||||
Открываем порт на фаерволе (если включен):
|
|
||||||
```bash
|
|
||||||
sudo ufw allow from 10.10.10.2 to any port 443 proto tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Шаг 3. Настройка HAProxy на Сервере A (_РФ_)
|
|
||||||
|
|
||||||
Т.к. в стандартном репозитории Ubuntu версия относительно старая, имеет смысл воспользоваться официальным образом Docker.\
|
|
||||||
[Инструкция](https://docs.docker.com/engine/install/ubuntu/) по установке Docker на Ubuntu.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> По умолчанию у обычных пользователей нет прав на использование портов < 1024.\
|
|
||||||
> Попытки запустить HAProxy на 443 порту могут приводить к ошибкам:
|
|
||||||
> ```
|
|
||||||
> [ALERT] (8) : Binding [/usr/local/etc/haproxy/haproxy.cfg:17] for frontend tcp_in_443:
|
|
||||||
> protocol tcpv4: cannot bind socket (Permission denied) for [0.0.0.0:443].
|
|
||||||
> ```
|
|
||||||
> Есть два простых способа обойти это ограничение, выберите что-то одно:
|
|
||||||
> 1. На уровне ОС изменить настройку net.ipv4.ip_unprivileged_port_start, разрешив пользователям использовать все порты:
|
|
||||||
> ```
|
|
||||||
> echo "net.ipv4.ip_unprivileged_port_start = 0" | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
|
||||||
> ```
|
|
||||||
> или
|
|
||||||
>
|
|
||||||
> 2. Запустить HAProxy под root:\
|
|
||||||
> Раскомментируйте в docker-compose.yaml параметр `user: "root"`.
|
|
||||||
|
|
||||||
#### Создаем папку для HAProxy:
|
|
||||||
```bash
|
|
||||||
mkdir -p /opt/docker-compose/haproxy && cd $_
|
|
||||||
```
|
|
||||||
#### Создаем файл docker-compose.yaml
|
|
||||||
|
|
||||||
`nano docker-compose.yaml`
|
|
||||||
|
|
||||||
Содержимое файла
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
haproxy:
|
|
||||||
image: haproxy:latest
|
|
||||||
container_name: haproxy
|
|
||||||
restart: unless-stopped
|
|
||||||
# user: "root"
|
|
||||||
network_mode: "host"
|
|
||||||
volumes:
|
|
||||||
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "1m"
|
|
||||||
max-file: "1"
|
|
||||||
```
|
|
||||||
#### Создаем файл конфига haproxy.cfg
|
|
||||||
Принимаем подключения на порту 443\tcp и отправляем их через туннель на Сервер `B` 10.10.10.1:443
|
|
||||||
|
|
||||||
`nano haproxy.cfg`
|
|
||||||
|
|
||||||
Содержимое файла
|
|
||||||
```haproxy
|
|
||||||
global
|
|
||||||
log stdout format raw local0
|
|
||||||
maxconn 10000
|
|
||||||
|
|
||||||
defaults
|
|
||||||
log global
|
|
||||||
mode tcp
|
|
||||||
option tcplog
|
|
||||||
option clitcpka
|
|
||||||
option srvtcpka
|
|
||||||
timeout connect 5s
|
|
||||||
timeout client 2h
|
|
||||||
timeout server 2h
|
|
||||||
timeout check 5s
|
|
||||||
|
|
||||||
frontend tcp_in_443
|
|
||||||
bind *:443
|
|
||||||
maxconn 8000
|
|
||||||
option tcp-smart-accept
|
|
||||||
default_backend telemt_nodes
|
|
||||||
|
|
||||||
backend telemt_nodes
|
|
||||||
option tcp-smart-connect
|
|
||||||
server server_a 10.10.10.1:443 check inter 5s rise 2 fall 3 send-proxy-v2
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
>[!WARNING]
|
|
||||||
>**Файл должен заканчиваться пустой строкой, иначе HAProxy не запустится!**
|
|
||||||
|
|
||||||
#### Разрешаем порт 443\tcp в фаерволе (если включен)
|
|
||||||
```bash
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Запускаем контейнер HAProxy
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Если все настроено верно, то теперь можно пробовать подключить клиентов Telegram с использованием ссылок из лога\api telemt.
|
|
||||||
@@ -24,7 +24,10 @@ pub(super) fn success_response<T: Serialize>(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn error_response(request_id: u64, failure: ApiFailure) -> hyper::Response<Full<Bytes>> {
|
pub(super) fn error_response(
|
||||||
|
request_id: u64,
|
||||||
|
failure: ApiFailure,
|
||||||
|
) -> hyper::Response<Full<Bytes>> {
|
||||||
let payload = ErrorResponse {
|
let payload = ErrorResponse {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: ErrorBody {
|
error: ErrorBody {
|
||||||
|
|||||||
+31
-93
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
|
||||||
|
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -21,8 +19,8 @@ use crate::ip_tracker::UserIpTracker;
|
|||||||
use crate::proxy::route_mode::RouteRuntimeController;
|
use crate::proxy::route_mode::RouteRuntimeController;
|
||||||
use crate::startup::StartupTracker;
|
use crate::startup::StartupTracker;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
mod config_store;
|
mod config_store;
|
||||||
mod events;
|
mod events;
|
||||||
@@ -37,12 +35,11 @@ mod runtime_watch;
|
|||||||
mod runtime_zero;
|
mod runtime_zero;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
use config_store::{current_revision, load_config_from_disk, parse_if_match};
|
use config_store::{current_revision, parse_if_match};
|
||||||
use events::ApiEventStore;
|
|
||||||
use http_utils::{error_response, read_json, read_optional_json, success_response};
|
use http_utils::{error_response, read_json, read_optional_json, success_response};
|
||||||
|
use events::ApiEventStore;
|
||||||
use model::{
|
use model::{
|
||||||
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, PatchUserRequest,
|
ApiFailure, CreateUserRequest, HealthData, PatchUserRequest, RotateSecretRequest, SummaryData,
|
||||||
RotateSecretRequest, SummaryData, UserActiveIps,
|
|
||||||
};
|
};
|
||||||
use runtime_edge::{
|
use runtime_edge::{
|
||||||
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
|
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
|
||||||
@@ -58,11 +55,11 @@ use runtime_stats::{
|
|||||||
MinimalCacheEntry, build_dcs_data, build_me_writers_data, build_minimal_all_data,
|
MinimalCacheEntry, build_dcs_data, build_me_writers_data, build_minimal_all_data,
|
||||||
build_upstreams_data, build_zero_all_data,
|
build_upstreams_data, build_zero_all_data,
|
||||||
};
|
};
|
||||||
use runtime_watch::spawn_runtime_watchers;
|
|
||||||
use runtime_zero::{
|
use runtime_zero::{
|
||||||
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
build_limits_effective_data, build_runtime_gates_data, build_security_posture_data,
|
||||||
build_system_info_data,
|
build_system_info_data,
|
||||||
};
|
};
|
||||||
|
use runtime_watch::spawn_runtime_watchers;
|
||||||
use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config};
|
use users::{create_user, delete_user, patch_user, rotate_secret, users_from_config};
|
||||||
|
|
||||||
pub(super) struct ApiRuntimeState {
|
pub(super) struct ApiRuntimeState {
|
||||||
@@ -211,15 +208,15 @@ async fn handle(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
if !api_cfg.whitelist.is_empty()
|
||||||
|
&& !api_cfg
|
||||||
|
.whitelist
|
||||||
|
.iter()
|
||||||
|
.any(|net| net.contains(peer.ip()))
|
||||||
{
|
{
|
||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
request_id,
|
request_id,
|
||||||
ApiFailure::new(
|
ApiFailure::new(StatusCode::FORBIDDEN, "forbidden", "Source IP is not allowed"),
|
||||||
StatusCode::FORBIDDEN,
|
|
||||||
"forbidden",
|
|
||||||
"Source IP is not allowed",
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,8 +347,7 @@ async fn handle(
|
|||||||
}
|
}
|
||||||
("GET", "/v1/runtime/connections/summary") => {
|
("GET", "/v1/runtime/connections/summary") => {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
let data =
|
let data = build_runtime_connections_summary_data(shared.as_ref(), cfg.as_ref()).await;
|
||||||
build_runtime_connections_summary_data(shared.as_ref(), cfg.as_ref()).await;
|
|
||||||
Ok(success_response(StatusCode::OK, data, revision))
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
}
|
}
|
||||||
("GET", "/v1/runtime/events/recent") => {
|
("GET", "/v1/runtime/events/recent") => {
|
||||||
@@ -363,33 +359,15 @@ async fn handle(
|
|||||||
);
|
);
|
||||||
Ok(success_response(StatusCode::OK, data, revision))
|
Ok(success_response(StatusCode::OK, data, revision))
|
||||||
}
|
}
|
||||||
("GET", "/v1/stats/users/active-ips") => {
|
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
|
||||||
let usernames: Vec<_> = cfg.access.users.keys().cloned().collect();
|
|
||||||
let active_ips_map = shared.ip_tracker.get_active_ips_for_users(&usernames).await;
|
|
||||||
let mut data: Vec<UserActiveIps> = active_ips_map
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(_, ips)| !ips.is_empty())
|
|
||||||
.map(|(username, active_ips)| UserActiveIps {
|
|
||||||
username,
|
|
||||||
active_ips,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
data.sort_by(|a, b| a.username.cmp(&b.username));
|
|
||||||
Ok(success_response(StatusCode::OK, data, revision))
|
|
||||||
}
|
|
||||||
("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
|
("GET", "/v1/stats/users") | ("GET", "/v1/users") => {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
|
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
let users = users_from_config(
|
let users = users_from_config(
|
||||||
&disk_cfg,
|
&cfg,
|
||||||
&shared.stats,
|
&shared.stats,
|
||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
Some(runtime_cfg.as_ref()),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Ok(success_response(StatusCode::OK, users, revision))
|
Ok(success_response(StatusCode::OK, users, revision))
|
||||||
@@ -408,27 +386,17 @@ async fn handle(
|
|||||||
let expected_revision = parse_if_match(req.headers());
|
let expected_revision = parse_if_match(req.headers());
|
||||||
let body = read_json::<CreateUserRequest>(req.into_body(), body_limit).await?;
|
let body = read_json::<CreateUserRequest>(req.into_body(), body_limit).await?;
|
||||||
let result = create_user(body, expected_revision, &shared).await;
|
let result = create_user(body, expected_revision, &shared).await;
|
||||||
let (mut data, revision) = match result {
|
let (data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared
|
shared.runtime_events.record("api.user.create.failed", error.code);
|
||||||
.runtime_events
|
|
||||||
.record("api.user.create.failed", error.code);
|
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
shared
|
||||||
data.user.in_runtime = runtime_cfg.access.users.contains_key(&data.user.username);
|
.runtime_events
|
||||||
shared.runtime_events.record(
|
.record("api.user.create.ok", format!("username={}", data.user.username));
|
||||||
"api.user.create.ok",
|
Ok(success_response(StatusCode::CREATED, data, revision))
|
||||||
format!("username={}", data.user.username),
|
|
||||||
);
|
|
||||||
let status = if data.user.in_runtime {
|
|
||||||
StatusCode::CREATED
|
|
||||||
} else {
|
|
||||||
StatusCode::ACCEPTED
|
|
||||||
};
|
|
||||||
Ok(success_response(status, data, revision))
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(user) = path.strip_prefix("/v1/users/")
|
if let Some(user) = path.strip_prefix("/v1/users/")
|
||||||
@@ -437,20 +405,16 @@ async fn handle(
|
|||||||
{
|
{
|
||||||
if method == Method::GET {
|
if method == Method::GET {
|
||||||
let revision = current_revision(&shared.config_path).await?;
|
let revision = current_revision(&shared.config_path).await?;
|
||||||
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
|
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
let users = users_from_config(
|
let users = users_from_config(
|
||||||
&disk_cfg,
|
&cfg,
|
||||||
&shared.stats,
|
&shared.stats,
|
||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
Some(runtime_cfg.as_ref()),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Some(user_info) =
|
if let Some(user_info) = users.into_iter().find(|entry| entry.username == user)
|
||||||
users.into_iter().find(|entry| entry.username == user)
|
|
||||||
{
|
{
|
||||||
return Ok(success_response(StatusCode::OK, user_info, revision));
|
return Ok(success_response(StatusCode::OK, user_info, revision));
|
||||||
}
|
}
|
||||||
@@ -471,10 +435,9 @@ async fn handle(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let expected_revision = parse_if_match(req.headers());
|
let expected_revision = parse_if_match(req.headers());
|
||||||
let body =
|
let body = read_json::<PatchUserRequest>(req.into_body(), body_limit).await?;
|
||||||
read_json::<PatchUserRequest>(req.into_body(), body_limit).await?;
|
|
||||||
let result = patch_user(user, body, expected_revision, &shared).await;
|
let result = patch_user(user, body, expected_revision, &shared).await;
|
||||||
let (mut data, revision) = match result {
|
let (data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
@@ -484,17 +447,10 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
|
||||||
data.in_runtime = runtime_cfg.access.users.contains_key(&data.username);
|
|
||||||
shared
|
shared
|
||||||
.runtime_events
|
.runtime_events
|
||||||
.record("api.user.patch.ok", format!("username={}", data.username));
|
.record("api.user.patch.ok", format!("username={}", data.username));
|
||||||
let status = if data.in_runtime {
|
return Ok(success_response(StatusCode::OK, data, revision));
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::ACCEPTED
|
|
||||||
};
|
|
||||||
return Ok(success_response(status, data, revision));
|
|
||||||
}
|
}
|
||||||
if method == Method::DELETE {
|
if method == Method::DELETE {
|
||||||
if api_cfg.read_only {
|
if api_cfg.read_only {
|
||||||
@@ -519,21 +475,11 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
shared
|
shared.runtime_events.record(
|
||||||
.runtime_events
|
"api.user.delete.ok",
|
||||||
.record("api.user.delete.ok", format!("username={}", deleted_user));
|
format!("username={}", deleted_user),
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
);
|
||||||
let in_runtime = runtime_cfg.access.users.contains_key(&deleted_user);
|
return Ok(success_response(StatusCode::OK, deleted_user, revision));
|
||||||
let response = DeleteUserResponse {
|
|
||||||
username: deleted_user,
|
|
||||||
in_runtime,
|
|
||||||
};
|
|
||||||
let status = if response.in_runtime {
|
|
||||||
StatusCode::ACCEPTED
|
|
||||||
} else {
|
|
||||||
StatusCode::OK
|
|
||||||
};
|
|
||||||
return Ok(success_response(status, response, revision));
|
|
||||||
}
|
}
|
||||||
if method == Method::POST
|
if method == Method::POST
|
||||||
&& let Some(base_user) = user.strip_suffix("/rotate-secret")
|
&& let Some(base_user) = user.strip_suffix("/rotate-secret")
|
||||||
@@ -561,7 +507,7 @@ async fn handle(
|
|||||||
&shared,
|
&shared,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (mut data, revision) = match result {
|
let (data, revision) = match result {
|
||||||
Ok(ok) => ok,
|
Ok(ok) => ok,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
@@ -571,19 +517,11 @@ async fn handle(
|
|||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let runtime_cfg = config_rx.borrow().clone();
|
|
||||||
data.user.in_runtime =
|
|
||||||
runtime_cfg.access.users.contains_key(&data.user.username);
|
|
||||||
shared.runtime_events.record(
|
shared.runtime_events.record(
|
||||||
"api.user.rotate_secret.ok",
|
"api.user.rotate_secret.ok",
|
||||||
format!("username={}", base_user),
|
format!("username={}", base_user),
|
||||||
);
|
);
|
||||||
let status = if data.user.in_runtime {
|
return Ok(success_response(StatusCode::OK, data, revision));
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::ACCEPTED
|
|
||||||
};
|
|
||||||
return Ok(success_response(status, data, revision));
|
|
||||||
}
|
}
|
||||||
if method == Method::POST {
|
if method == Method::POST {
|
||||||
return Ok(error_response(
|
return Ok(error_response(
|
||||||
|
|||||||
@@ -174,24 +174,6 @@ pub(super) struct ZeroMiddleProxyData {
|
|||||||
pub(super) route_drop_queue_full_total: u64,
|
pub(super) route_drop_queue_full_total: u64,
|
||||||
pub(super) route_drop_queue_full_base_total: u64,
|
pub(super) route_drop_queue_full_base_total: u64,
|
||||||
pub(super) route_drop_queue_full_high_total: u64,
|
pub(super) route_drop_queue_full_high_total: u64,
|
||||||
pub(super) d2c_batches_total: u64,
|
|
||||||
pub(super) d2c_batch_frames_total: u64,
|
|
||||||
pub(super) d2c_batch_bytes_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_queue_drain_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_batch_frames_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_batch_bytes_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_max_delay_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_ack_immediate_total: u64,
|
|
||||||
pub(super) d2c_flush_reason_close_total: u64,
|
|
||||||
pub(super) d2c_data_frames_total: u64,
|
|
||||||
pub(super) d2c_ack_frames_total: u64,
|
|
||||||
pub(super) d2c_payload_bytes_total: u64,
|
|
||||||
pub(super) d2c_write_mode_coalesced_total: u64,
|
|
||||||
pub(super) d2c_write_mode_split_total: u64,
|
|
||||||
pub(super) d2c_quota_reject_pre_write_total: u64,
|
|
||||||
pub(super) d2c_quota_reject_post_write_total: u64,
|
|
||||||
pub(super) d2c_frame_buf_shrink_total: u64,
|
|
||||||
pub(super) d2c_frame_buf_shrink_bytes_total: u64,
|
|
||||||
pub(super) socks_kdf_strict_reject_total: u64,
|
pub(super) socks_kdf_strict_reject_total: u64,
|
||||||
pub(super) socks_kdf_compat_fallback_total: u64,
|
pub(super) socks_kdf_compat_fallback_total: u64,
|
||||||
pub(super) endpoint_quarantine_total: u64,
|
pub(super) endpoint_quarantine_total: u64,
|
||||||
@@ -428,7 +410,6 @@ pub(super) struct UserLinks {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct UserInfo {
|
pub(super) struct UserInfo {
|
||||||
pub(super) username: String,
|
pub(super) username: String,
|
||||||
pub(super) in_runtime: bool,
|
|
||||||
pub(super) user_ad_tag: Option<String>,
|
pub(super) user_ad_tag: Option<String>,
|
||||||
pub(super) max_tcp_conns: Option<usize>,
|
pub(super) max_tcp_conns: Option<usize>,
|
||||||
pub(super) expiration_rfc3339: Option<String>,
|
pub(super) expiration_rfc3339: Option<String>,
|
||||||
@@ -443,24 +424,12 @@ pub(super) struct UserInfo {
|
|||||||
pub(super) links: UserLinks,
|
pub(super) links: UserLinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(super) struct UserActiveIps {
|
|
||||||
pub(super) username: String,
|
|
||||||
pub(super) active_ips: Vec<IpAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct CreateUserResponse {
|
pub(super) struct CreateUserResponse {
|
||||||
pub(super) user: UserInfo,
|
pub(super) user: UserInfo,
|
||||||
pub(super) secret: String,
|
pub(super) secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(super) struct DeleteUserResponse {
|
|
||||||
pub(super) username: String,
|
|
||||||
pub(super) in_runtime: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct CreateUserRequest {
|
pub(super) struct CreateUserRequest {
|
||||||
pub(super) username: String,
|
pub(super) username: String,
|
||||||
|
|||||||
@@ -167,7 +167,11 @@ async fn current_me_pool_stage_progress(shared: &ApiShared) -> Option<f64> {
|
|||||||
let pool = shared.me_pool.read().await.clone()?;
|
let pool = shared.me_pool.read().await.clone()?;
|
||||||
let status = pool.api_status_snapshot().await;
|
let status = pool.api_status_snapshot().await;
|
||||||
let configured_dc_groups = status.configured_dc_groups;
|
let configured_dc_groups = status.configured_dc_groups;
|
||||||
let covered_dc_groups = status.dcs.iter().filter(|dc| dc.alive_writers > 0).count();
|
let covered_dc_groups = status
|
||||||
|
.dcs
|
||||||
|
.iter()
|
||||||
|
.filter(|dc| dc.alive_writers > 0)
|
||||||
|
.count();
|
||||||
|
|
||||||
let dc_coverage = ratio_01(covered_dc_groups, configured_dc_groups);
|
let dc_coverage = ratio_01(covered_dc_groups, configured_dc_groups);
|
||||||
let writer_coverage = ratio_01(status.alive_writers, status.required_writers);
|
let writer_coverage = ratio_01(status.alive_writers, status.required_writers);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use crate::config::ApiConfig;
|
use crate::config::ApiConfig;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::UpstreamRouteKind;
|
|
||||||
use crate::transport::upstream::IpPreference;
|
use crate::transport::upstream::IpPreference;
|
||||||
|
use crate::transport::UpstreamRouteKind;
|
||||||
|
|
||||||
use super::ApiShared;
|
use super::ApiShared;
|
||||||
use super::model::{
|
use super::model::{
|
||||||
@@ -68,25 +68,6 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer
|
|||||||
route_drop_queue_full_total: stats.get_me_route_drop_queue_full(),
|
route_drop_queue_full_total: stats.get_me_route_drop_queue_full(),
|
||||||
route_drop_queue_full_base_total: stats.get_me_route_drop_queue_full_base(),
|
route_drop_queue_full_base_total: stats.get_me_route_drop_queue_full_base(),
|
||||||
route_drop_queue_full_high_total: stats.get_me_route_drop_queue_full_high(),
|
route_drop_queue_full_high_total: stats.get_me_route_drop_queue_full_high(),
|
||||||
d2c_batches_total: stats.get_me_d2c_batches_total(),
|
|
||||||
d2c_batch_frames_total: stats.get_me_d2c_batch_frames_total(),
|
|
||||||
d2c_batch_bytes_total: stats.get_me_d2c_batch_bytes_total(),
|
|
||||||
d2c_flush_reason_queue_drain_total: stats.get_me_d2c_flush_reason_queue_drain_total(),
|
|
||||||
d2c_flush_reason_batch_frames_total: stats.get_me_d2c_flush_reason_batch_frames_total(),
|
|
||||||
d2c_flush_reason_batch_bytes_total: stats.get_me_d2c_flush_reason_batch_bytes_total(),
|
|
||||||
d2c_flush_reason_max_delay_total: stats.get_me_d2c_flush_reason_max_delay_total(),
|
|
||||||
d2c_flush_reason_ack_immediate_total: stats
|
|
||||||
.get_me_d2c_flush_reason_ack_immediate_total(),
|
|
||||||
d2c_flush_reason_close_total: stats.get_me_d2c_flush_reason_close_total(),
|
|
||||||
d2c_data_frames_total: stats.get_me_d2c_data_frames_total(),
|
|
||||||
d2c_ack_frames_total: stats.get_me_d2c_ack_frames_total(),
|
|
||||||
d2c_payload_bytes_total: stats.get_me_d2c_payload_bytes_total(),
|
|
||||||
d2c_write_mode_coalesced_total: stats.get_me_d2c_write_mode_coalesced_total(),
|
|
||||||
d2c_write_mode_split_total: stats.get_me_d2c_write_mode_split_total(),
|
|
||||||
d2c_quota_reject_pre_write_total: stats.get_me_d2c_quota_reject_pre_write_total(),
|
|
||||||
d2c_quota_reject_post_write_total: stats.get_me_d2c_quota_reject_post_write_total(),
|
|
||||||
d2c_frame_buf_shrink_total: stats.get_me_d2c_frame_buf_shrink_total(),
|
|
||||||
d2c_frame_buf_shrink_bytes_total: stats.get_me_d2c_frame_buf_shrink_bytes_total(),
|
|
||||||
socks_kdf_strict_reject_total: stats.get_me_socks_kdf_strict_reject(),
|
socks_kdf_strict_reject_total: stats.get_me_socks_kdf_strict_reject(),
|
||||||
socks_kdf_compat_fallback_total: stats.get_me_socks_kdf_compat_fallback(),
|
socks_kdf_compat_fallback_total: stats.get_me_socks_kdf_compat_fallback(),
|
||||||
endpoint_quarantine_total: stats.get_me_endpoint_quarantine_total(),
|
endpoint_quarantine_total: stats.get_me_endpoint_quarantine_total(),
|
||||||
|
|||||||
+6
-37
@@ -35,14 +35,11 @@ pub(super) struct RuntimeGatesData {
|
|||||||
pub(super) conditional_cast_enabled: bool,
|
pub(super) conditional_cast_enabled: bool,
|
||||||
pub(super) me_runtime_ready: bool,
|
pub(super) me_runtime_ready: bool,
|
||||||
pub(super) me2dc_fallback_enabled: bool,
|
pub(super) me2dc_fallback_enabled: bool,
|
||||||
pub(super) me2dc_fast_enabled: bool,
|
|
||||||
pub(super) use_middle_proxy: bool,
|
pub(super) use_middle_proxy: bool,
|
||||||
pub(super) route_mode: &'static str,
|
pub(super) route_mode: &'static str,
|
||||||
pub(super) reroute_active: bool,
|
pub(super) reroute_active: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub(super) reroute_to_direct_at_epoch_secs: Option<u64>,
|
pub(super) reroute_to_direct_at_epoch_secs: Option<u64>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub(super) reroute_reason: Option<&'static str>,
|
|
||||||
pub(super) startup_status: &'static str,
|
pub(super) startup_status: &'static str,
|
||||||
pub(super) startup_stage: String,
|
pub(super) startup_stage: String,
|
||||||
pub(super) startup_progress_pct: f64,
|
pub(super) startup_progress_pct: f64,
|
||||||
@@ -50,7 +47,6 @@ pub(super) struct RuntimeGatesData {
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct EffectiveTimeoutLimits {
|
pub(super) struct EffectiveTimeoutLimits {
|
||||||
pub(super) client_first_byte_idle_secs: u64,
|
|
||||||
pub(super) client_handshake_secs: u64,
|
pub(super) client_handshake_secs: u64,
|
||||||
pub(super) tg_connect_secs: u64,
|
pub(super) tg_connect_secs: u64,
|
||||||
pub(super) client_keepalive_secs: u64,
|
pub(super) client_keepalive_secs: u64,
|
||||||
@@ -90,7 +86,6 @@ pub(super) struct EffectiveMiddleProxyLimits {
|
|||||||
pub(super) writer_pick_mode: &'static str,
|
pub(super) writer_pick_mode: &'static str,
|
||||||
pub(super) writer_pick_sample_size: u8,
|
pub(super) writer_pick_sample_size: u8,
|
||||||
pub(super) me2dc_fallback: bool,
|
pub(super) me2dc_fallback: bool,
|
||||||
pub(super) me2dc_fast: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -100,11 +95,6 @@ pub(super) struct EffectiveUserIpPolicyLimits {
|
|||||||
pub(super) window_secs: u64,
|
pub(super) window_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(super) struct EffectiveUserTcpPolicyLimits {
|
|
||||||
pub(super) global_each: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub(super) struct EffectiveLimitsData {
|
pub(super) struct EffectiveLimitsData {
|
||||||
pub(super) update_every_secs: u64,
|
pub(super) update_every_secs: u64,
|
||||||
@@ -114,7 +104,6 @@ pub(super) struct EffectiveLimitsData {
|
|||||||
pub(super) upstream: EffectiveUpstreamLimits,
|
pub(super) upstream: EffectiveUpstreamLimits,
|
||||||
pub(super) middle_proxy: EffectiveMiddleProxyLimits,
|
pub(super) middle_proxy: EffectiveMiddleProxyLimits,
|
||||||
pub(super) user_ip_policy: EffectiveUserIpPolicyLimits,
|
pub(super) user_ip_policy: EffectiveUserIpPolicyLimits,
|
||||||
pub(super) user_tcp_policy: EffectiveUserTcpPolicyLimits,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -139,8 +128,7 @@ pub(super) fn build_system_info_data(
|
|||||||
.runtime_state
|
.runtime_state
|
||||||
.last_config_reload_epoch_secs
|
.last_config_reload_epoch_secs
|
||||||
.load(Ordering::Relaxed);
|
.load(Ordering::Relaxed);
|
||||||
let last_config_reload_epoch_secs =
|
let last_config_reload_epoch_secs = (last_reload_epoch_secs > 0).then_some(last_reload_epoch_secs);
|
||||||
(last_reload_epoch_secs > 0).then_some(last_reload_epoch_secs);
|
|
||||||
|
|
||||||
let git_commit = option_env!("TELEMT_GIT_COMMIT")
|
let git_commit = option_env!("TELEMT_GIT_COMMIT")
|
||||||
.or(option_env!("VERGEN_GIT_SHA"))
|
.or(option_env!("VERGEN_GIT_SHA"))
|
||||||
@@ -165,10 +153,7 @@ pub(super) fn build_system_info_data(
|
|||||||
uptime_seconds: shared.stats.uptime_secs(),
|
uptime_seconds: shared.stats.uptime_secs(),
|
||||||
config_path: shared.config_path.display().to_string(),
|
config_path: shared.config_path.display().to_string(),
|
||||||
config_hash: revision.to_string(),
|
config_hash: revision.to_string(),
|
||||||
config_reload_count: shared
|
config_reload_count: shared.runtime_state.config_reload_count.load(Ordering::Relaxed),
|
||||||
.runtime_state
|
|
||||||
.config_reload_count
|
|
||||||
.load(Ordering::Relaxed),
|
|
||||||
last_config_reload_epoch_secs,
|
last_config_reload_epoch_secs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,8 +165,6 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
let startup_summary = build_runtime_startup_summary(shared).await;
|
let startup_summary = build_runtime_startup_summary(shared).await;
|
||||||
let route_state = shared.route_runtime.snapshot();
|
let route_state = shared.route_runtime.snapshot();
|
||||||
let route_mode = route_state.mode.as_str();
|
let route_mode = route_state.mode.as_str();
|
||||||
let fast_fallback_enabled =
|
|
||||||
cfg.general.use_middle_proxy && cfg.general.me2dc_fallback && cfg.general.me2dc_fast;
|
|
||||||
let reroute_active = cfg.general.use_middle_proxy
|
let reroute_active = cfg.general.use_middle_proxy
|
||||||
&& cfg.general.me2dc_fallback
|
&& cfg.general.me2dc_fallback
|
||||||
&& matches!(route_state.mode, RelayRouteMode::Direct);
|
&& matches!(route_state.mode, RelayRouteMode::Direct);
|
||||||
@@ -190,15 +173,6 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let reroute_reason = if reroute_active {
|
|
||||||
if fast_fallback_enabled {
|
|
||||||
Some("fast_not_ready_fallback")
|
|
||||||
} else {
|
|
||||||
Some("strict_grace_fallback")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let me_runtime_ready = if !cfg.general.use_middle_proxy {
|
let me_runtime_ready = if !cfg.general.use_middle_proxy {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -216,12 +190,10 @@ pub(super) async fn build_runtime_gates_data(
|
|||||||
conditional_cast_enabled: cfg.general.use_middle_proxy,
|
conditional_cast_enabled: cfg.general.use_middle_proxy,
|
||||||
me_runtime_ready,
|
me_runtime_ready,
|
||||||
me2dc_fallback_enabled: cfg.general.me2dc_fallback,
|
me2dc_fallback_enabled: cfg.general.me2dc_fallback,
|
||||||
me2dc_fast_enabled: fast_fallback_enabled,
|
|
||||||
use_middle_proxy: cfg.general.use_middle_proxy,
|
use_middle_proxy: cfg.general.use_middle_proxy,
|
||||||
route_mode,
|
route_mode,
|
||||||
reroute_active,
|
reroute_active,
|
||||||
reroute_to_direct_at_epoch_secs,
|
reroute_to_direct_at_epoch_secs,
|
||||||
reroute_reason,
|
|
||||||
startup_status: startup_summary.status,
|
startup_status: startup_summary.status,
|
||||||
startup_stage: startup_summary.stage,
|
startup_stage: startup_summary.stage,
|
||||||
startup_progress_pct: startup_summary.progress_pct,
|
startup_progress_pct: startup_summary.progress_pct,
|
||||||
@@ -234,9 +206,8 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
me_reinit_every_secs: cfg.general.effective_me_reinit_every_secs(),
|
me_reinit_every_secs: cfg.general.effective_me_reinit_every_secs(),
|
||||||
me_pool_force_close_secs: cfg.general.effective_me_pool_force_close_secs(),
|
me_pool_force_close_secs: cfg.general.effective_me_pool_force_close_secs(),
|
||||||
timeouts: EffectiveTimeoutLimits {
|
timeouts: EffectiveTimeoutLimits {
|
||||||
client_first_byte_idle_secs: cfg.timeouts.client_first_byte_idle_secs,
|
|
||||||
client_handshake_secs: cfg.timeouts.client_handshake,
|
client_handshake_secs: cfg.timeouts.client_handshake,
|
||||||
tg_connect_secs: cfg.general.tg_connect,
|
tg_connect_secs: cfg.timeouts.tg_connect,
|
||||||
client_keepalive_secs: cfg.timeouts.client_keepalive,
|
client_keepalive_secs: cfg.timeouts.client_keepalive,
|
||||||
client_ack_secs: cfg.timeouts.client_ack,
|
client_ack_secs: cfg.timeouts.client_ack,
|
||||||
me_one_retry: cfg.timeouts.me_one_retry,
|
me_one_retry: cfg.timeouts.me_one_retry,
|
||||||
@@ -262,7 +233,9 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
adaptive_floor_writers_per_core_total: cfg
|
adaptive_floor_writers_per_core_total: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_writers_per_core_total,
|
.me_adaptive_floor_writers_per_core_total,
|
||||||
adaptive_floor_cpu_cores_override: cfg.general.me_adaptive_floor_cpu_cores_override,
|
adaptive_floor_cpu_cores_override: cfg
|
||||||
|
.general
|
||||||
|
.me_adaptive_floor_cpu_cores_override,
|
||||||
adaptive_floor_max_extra_writers_single_per_core: cfg
|
adaptive_floor_max_extra_writers_single_per_core: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core,
|
.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
@@ -288,16 +261,12 @@ pub(super) fn build_limits_effective_data(cfg: &ProxyConfig) -> EffectiveLimitsD
|
|||||||
writer_pick_mode: me_writer_pick_mode_label(cfg.general.me_writer_pick_mode),
|
writer_pick_mode: me_writer_pick_mode_label(cfg.general.me_writer_pick_mode),
|
||||||
writer_pick_sample_size: cfg.general.me_writer_pick_sample_size,
|
writer_pick_sample_size: cfg.general.me_writer_pick_sample_size,
|
||||||
me2dc_fallback: cfg.general.me2dc_fallback,
|
me2dc_fallback: cfg.general.me2dc_fallback,
|
||||||
me2dc_fast: cfg.general.me2dc_fast,
|
|
||||||
},
|
},
|
||||||
user_ip_policy: EffectiveUserIpPolicyLimits {
|
user_ip_policy: EffectiveUserIpPolicyLimits {
|
||||||
global_each: cfg.access.user_max_unique_ips_global_each,
|
global_each: cfg.access.user_max_unique_ips_global_each,
|
||||||
mode: user_max_unique_ips_mode_label(cfg.access.user_max_unique_ips_mode),
|
mode: user_max_unique_ips_mode_label(cfg.access.user_max_unique_ips_mode),
|
||||||
window_secs: cfg.access.user_max_unique_ips_window_secs,
|
window_secs: cfg.access.user_max_unique_ips_window_secs,
|
||||||
},
|
},
|
||||||
user_tcp_policy: EffectiveUserTcpPolicyLimits {
|
|
||||||
global_each: cfg.access.user_max_tcp_conns_global_each,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-162
@@ -46,9 +46,7 @@ pub(super) async fn create_user(
|
|||||||
None => random_user_secret(),
|
None => random_user_secret(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ad_tag) = body.user_ad_tag.as_ref()
|
if let Some(ad_tag) = body.user_ad_tag.as_ref() && !is_valid_ad_tag(ad_tag) {
|
||||||
&& !is_valid_ad_tag(ad_tag)
|
|
||||||
{
|
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"user_ad_tag must be exactly 32 hex characters",
|
"user_ad_tag must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
@@ -67,18 +65,12 @@ pub(super) async fn create_user(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.access
|
cfg.access.users.insert(body.username.clone(), secret.clone());
|
||||||
.users
|
|
||||||
.insert(body.username.clone(), secret.clone());
|
|
||||||
if let Some(ad_tag) = body.user_ad_tag {
|
if let Some(ad_tag) = body.user_ad_tag {
|
||||||
cfg.access
|
cfg.access.user_ad_tags.insert(body.username.clone(), ad_tag);
|
||||||
.user_ad_tags
|
|
||||||
.insert(body.username.clone(), ad_tag);
|
|
||||||
}
|
}
|
||||||
if let Some(limit) = body.max_tcp_conns {
|
if let Some(limit) = body.max_tcp_conns {
|
||||||
cfg.access
|
cfg.access.user_max_tcp_conns.insert(body.username.clone(), limit);
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(body.username.clone(), limit);
|
|
||||||
}
|
}
|
||||||
if let Some(expiration) = expiration {
|
if let Some(expiration) = expiration {
|
||||||
cfg.access
|
cfg.access
|
||||||
@@ -86,9 +78,7 @@ pub(super) async fn create_user(
|
|||||||
.insert(body.username.clone(), expiration);
|
.insert(body.username.clone(), expiration);
|
||||||
}
|
}
|
||||||
if let Some(quota) = body.data_quota_bytes {
|
if let Some(quota) = body.data_quota_bytes {
|
||||||
cfg.access
|
cfg.access.user_data_quota.insert(body.username.clone(), quota);
|
||||||
.user_data_quota
|
|
||||||
.insert(body.username.clone(), quota);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let updated_limit = body.max_unique_ips;
|
let updated_limit = body.max_unique_ips;
|
||||||
@@ -118,15 +108,11 @@ pub(super) async fn create_user(
|
|||||||
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
touched_sections.push(AccessSection::UserMaxUniqueIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
let revision =
|
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
|
|
||||||
if let Some(limit) = updated_limit {
|
if let Some(limit) = updated_limit {
|
||||||
shared
|
shared.ip_tracker.set_user_limit(&body.username, limit).await;
|
||||||
.ip_tracker
|
|
||||||
.set_user_limit(&body.username, limit)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
|
|
||||||
@@ -136,7 +122,6 @@ pub(super) async fn create_user(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user = users
|
let user = users
|
||||||
@@ -144,16 +129,8 @@ pub(super) async fn create_user(
|
|||||||
.find(|entry| entry.username == body.username)
|
.find(|entry| entry.username == body.username)
|
||||||
.unwrap_or(UserInfo {
|
.unwrap_or(UserInfo {
|
||||||
username: body.username.clone(),
|
username: body.username.clone(),
|
||||||
in_runtime: false,
|
|
||||||
user_ad_tag: None,
|
user_ad_tag: None,
|
||||||
max_tcp_conns: cfg
|
max_tcp_conns: None,
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.get(&body.username)
|
|
||||||
.copied()
|
|
||||||
.filter(|limit| *limit > 0)
|
|
||||||
.or((cfg.access.user_max_tcp_conns_global_each > 0)
|
|
||||||
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
|
||||||
expiration_rfc3339: None,
|
expiration_rfc3339: None,
|
||||||
data_quota_bytes: None,
|
data_quota_bytes: None,
|
||||||
max_unique_ips: updated_limit,
|
max_unique_ips: updated_limit,
|
||||||
@@ -163,7 +140,12 @@ pub(super) async fn create_user(
|
|||||||
recent_unique_ips: 0,
|
recent_unique_ips: 0,
|
||||||
recent_unique_ips_list: Vec::new(),
|
recent_unique_ips_list: Vec::new(),
|
||||||
total_octets: 0,
|
total_octets: 0,
|
||||||
links: build_user_links(&cfg, &secret, detected_ip_v4, detected_ip_v6),
|
links: build_user_links(
|
||||||
|
&cfg,
|
||||||
|
&secret,
|
||||||
|
detected_ip_v4,
|
||||||
|
detected_ip_v6,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok((CreateUserResponse { user, secret }, revision))
|
Ok((CreateUserResponse { user, secret }, revision))
|
||||||
@@ -175,16 +157,12 @@ pub(super) async fn patch_user(
|
|||||||
expected_revision: Option<String>,
|
expected_revision: Option<String>,
|
||||||
shared: &ApiShared,
|
shared: &ApiShared,
|
||||||
) -> Result<(UserInfo, String), ApiFailure> {
|
) -> Result<(UserInfo, String), ApiFailure> {
|
||||||
if let Some(secret) = body.secret.as_ref()
|
if let Some(secret) = body.secret.as_ref() && !is_valid_user_secret(secret) {
|
||||||
&& !is_valid_user_secret(secret)
|
|
||||||
{
|
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"secret must be exactly 32 hex characters",
|
"secret must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(ad_tag) = body.user_ad_tag.as_ref()
|
if let Some(ad_tag) = body.user_ad_tag.as_ref() && !is_valid_ad_tag(ad_tag) {
|
||||||
&& !is_valid_ad_tag(ad_tag)
|
|
||||||
{
|
|
||||||
return Err(ApiFailure::bad_request(
|
return Err(ApiFailure::bad_request(
|
||||||
"user_ad_tag must be exactly 32 hex characters",
|
"user_ad_tag must be exactly 32 hex characters",
|
||||||
));
|
));
|
||||||
@@ -209,14 +187,10 @@ pub(super) async fn patch_user(
|
|||||||
cfg.access.user_ad_tags.insert(user.to_string(), ad_tag);
|
cfg.access.user_ad_tags.insert(user.to_string(), ad_tag);
|
||||||
}
|
}
|
||||||
if let Some(limit) = body.max_tcp_conns {
|
if let Some(limit) = body.max_tcp_conns {
|
||||||
cfg.access
|
cfg.access.user_max_tcp_conns.insert(user.to_string(), limit);
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), limit);
|
|
||||||
}
|
}
|
||||||
if let Some(expiration) = expiration {
|
if let Some(expiration) = expiration {
|
||||||
cfg.access
|
cfg.access.user_expirations.insert(user.to_string(), expiration);
|
||||||
.user_expirations
|
|
||||||
.insert(user.to_string(), expiration);
|
|
||||||
}
|
}
|
||||||
if let Some(quota) = body.data_quota_bytes {
|
if let Some(quota) = body.data_quota_bytes {
|
||||||
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
cfg.access.user_data_quota.insert(user.to_string(), quota);
|
||||||
@@ -224,9 +198,7 @@ pub(super) async fn patch_user(
|
|||||||
|
|
||||||
let mut updated_limit = None;
|
let mut updated_limit = None;
|
||||||
if let Some(limit) = body.max_unique_ips {
|
if let Some(limit) = body.max_unique_ips {
|
||||||
cfg.access
|
cfg.access.user_max_unique_ips.insert(user.to_string(), limit);
|
||||||
.user_max_unique_ips
|
|
||||||
.insert(user.to_string(), limit);
|
|
||||||
updated_limit = Some(limit);
|
updated_limit = Some(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +217,6 @@ pub(super) async fn patch_user(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user_info = users
|
let user_info = users
|
||||||
@@ -292,8 +263,7 @@ pub(super) async fn rotate_secret(
|
|||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision =
|
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
|
|
||||||
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
let (detected_ip_v4, detected_ip_v6) = shared.detected_link_ips();
|
||||||
@@ -303,7 +273,6 @@ pub(super) async fn rotate_secret(
|
|||||||
&shared.ip_tracker,
|
&shared.ip_tracker,
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
detected_ip_v6,
|
detected_ip_v6,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let user_info = users
|
let user_info = users
|
||||||
@@ -361,8 +330,7 @@ pub(super) async fn delete_user(
|
|||||||
AccessSection::UserDataQuota,
|
AccessSection::UserDataQuota,
|
||||||
AccessSection::UserMaxUniqueIps,
|
AccessSection::UserMaxUniqueIps,
|
||||||
];
|
];
|
||||||
let revision =
|
let revision = save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
||||||
save_access_sections_to_disk(&shared.config_path, &cfg, &touched_sections).await?;
|
|
||||||
drop(_guard);
|
drop(_guard);
|
||||||
shared.ip_tracker.remove_user_limit(user).await;
|
shared.ip_tracker.remove_user_limit(user).await;
|
||||||
shared.ip_tracker.clear_user_ips(user).await;
|
shared.ip_tracker.clear_user_ips(user).await;
|
||||||
@@ -376,7 +344,6 @@ pub(super) async fn users_from_config(
|
|||||||
ip_tracker: &UserIpTracker,
|
ip_tracker: &UserIpTracker,
|
||||||
startup_detected_ip_v4: Option<IpAddr>,
|
startup_detected_ip_v4: Option<IpAddr>,
|
||||||
startup_detected_ip_v6: Option<IpAddr>,
|
startup_detected_ip_v6: Option<IpAddr>,
|
||||||
runtime_cfg: Option<&ProxyConfig>,
|
|
||||||
) -> Vec<UserInfo> {
|
) -> Vec<UserInfo> {
|
||||||
let mut names = cfg.access.users.keys().cloned().collect::<Vec<_>>();
|
let mut names = cfg.access.users.keys().cloned().collect::<Vec<_>>();
|
||||||
names.sort();
|
names.sort();
|
||||||
@@ -398,7 +365,12 @@ pub(super) async fn users_from_config(
|
|||||||
.users
|
.users
|
||||||
.get(&username)
|
.get(&username)
|
||||||
.map(|secret| {
|
.map(|secret| {
|
||||||
build_user_links(cfg, secret, startup_detected_ip_v4, startup_detected_ip_v6)
|
build_user_links(
|
||||||
|
cfg,
|
||||||
|
secret,
|
||||||
|
startup_detected_ip_v4,
|
||||||
|
startup_detected_ip_v6,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or(UserLinks {
|
.unwrap_or(UserLinks {
|
||||||
classic: Vec::new(),
|
classic: Vec::new(),
|
||||||
@@ -406,18 +378,8 @@ pub(super) async fn users_from_config(
|
|||||||
tls: Vec::new(),
|
tls: Vec::new(),
|
||||||
});
|
});
|
||||||
users.push(UserInfo {
|
users.push(UserInfo {
|
||||||
in_runtime: runtime_cfg
|
|
||||||
.map(|runtime| runtime.access.users.contains_key(&username))
|
|
||||||
.unwrap_or(false),
|
|
||||||
user_ad_tag: cfg.access.user_ad_tags.get(&username).cloned(),
|
user_ad_tag: cfg.access.user_ad_tags.get(&username).cloned(),
|
||||||
max_tcp_conns: cfg
|
max_tcp_conns: cfg.access.user_max_tcp_conns.get(&username).copied(),
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.get(&username)
|
|
||||||
.copied()
|
|
||||||
.filter(|limit| *limit > 0)
|
|
||||||
.or((cfg.access.user_max_tcp_conns_global_each > 0)
|
|
||||||
.then_some(cfg.access.user_max_tcp_conns_global_each)),
|
|
||||||
expiration_rfc3339: cfg
|
expiration_rfc3339: cfg
|
||||||
.access
|
.access
|
||||||
.user_expirations
|
.user_expirations
|
||||||
@@ -430,8 +392,10 @@ pub(super) async fn users_from_config(
|
|||||||
.get(&username)
|
.get(&username)
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|limit| *limit > 0)
|
.filter(|limit| *limit > 0)
|
||||||
.or((cfg.access.user_max_unique_ips_global_each > 0)
|
.or(
|
||||||
.then_some(cfg.access.user_max_unique_ips_global_each)),
|
(cfg.access.user_max_unique_ips_global_each > 0)
|
||||||
|
.then_some(cfg.access.user_max_unique_ips_global_each),
|
||||||
|
),
|
||||||
current_connections: stats.get_user_curr_connects(&username),
|
current_connections: stats.get_user_curr_connects(&username),
|
||||||
active_unique_ips: active_ip_list.len(),
|
active_unique_ips: active_ip_list.len(),
|
||||||
active_unique_ips_list: active_ip_list,
|
active_unique_ips_list: active_ip_list,
|
||||||
@@ -517,12 +481,12 @@ fn resolve_link_hosts(
|
|||||||
push_unique_host(&mut hosts, host);
|
push_unique_host(&mut hosts, host);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(ip) = listener.announce_ip
|
if let Some(ip) = listener.announce_ip {
|
||||||
&& !ip.is_unspecified()
|
if !ip.is_unspecified() {
|
||||||
{
|
|
||||||
push_unique_host(&mut hosts, &ip.to_string());
|
push_unique_host(&mut hosts, &ip.to_string());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if listener.ip.is_unspecified() {
|
if listener.ip.is_unspecified() {
|
||||||
let detected_ip = if listener.ip.is_ipv4() {
|
let detected_ip = if listener.ip.is_ipv4() {
|
||||||
startup_detected_ip_v4
|
startup_detected_ip_v4
|
||||||
@@ -594,94 +558,3 @@ fn resolve_tls_domains(cfg: &ProxyConfig) -> Vec<&str> {
|
|||||||
}
|
}
|
||||||
domains
|
domains
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ip_tracker::UserIpTracker;
|
|
||||||
use crate::stats::Stats;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn users_from_config_reports_effective_tcp_limit_with_global_fallback() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.access.users.insert(
|
|
||||||
"alice".to_string(),
|
|
||||||
"0123456789abcdef0123456789abcdef".to_string(),
|
|
||||||
);
|
|
||||||
cfg.access.user_max_tcp_conns_global_each = 7;
|
|
||||||
|
|
||||||
let stats = Stats::new();
|
|
||||||
let tracker = UserIpTracker::new();
|
|
||||||
|
|
||||||
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
|
||||||
let alice = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "alice")
|
|
||||||
.expect("alice must be present");
|
|
||||||
assert!(!alice.in_runtime);
|
|
||||||
assert_eq!(alice.max_tcp_conns, Some(7));
|
|
||||||
|
|
||||||
cfg.access.user_max_tcp_conns.insert("alice".to_string(), 5);
|
|
||||||
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
|
||||||
let alice = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "alice")
|
|
||||||
.expect("alice must be present");
|
|
||||||
assert!(!alice.in_runtime);
|
|
||||||
assert_eq!(alice.max_tcp_conns, Some(5));
|
|
||||||
|
|
||||||
cfg.access.user_max_tcp_conns.insert("alice".to_string(), 0);
|
|
||||||
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
|
||||||
let alice = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "alice")
|
|
||||||
.expect("alice must be present");
|
|
||||||
assert!(!alice.in_runtime);
|
|
||||||
assert_eq!(alice.max_tcp_conns, Some(7));
|
|
||||||
|
|
||||||
cfg.access.user_max_tcp_conns_global_each = 0;
|
|
||||||
let users = users_from_config(&cfg, &stats, &tracker, None, None, None).await;
|
|
||||||
let alice = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "alice")
|
|
||||||
.expect("alice must be present");
|
|
||||||
assert!(!alice.in_runtime);
|
|
||||||
assert_eq!(alice.max_tcp_conns, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn users_from_config_marks_runtime_membership_when_snapshot_is_provided() {
|
|
||||||
let mut disk_cfg = ProxyConfig::default();
|
|
||||||
disk_cfg.access.users.insert(
|
|
||||||
"alice".to_string(),
|
|
||||||
"0123456789abcdef0123456789abcdef".to_string(),
|
|
||||||
);
|
|
||||||
disk_cfg.access.users.insert(
|
|
||||||
"bob".to_string(),
|
|
||||||
"fedcba9876543210fedcba9876543210".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut runtime_cfg = ProxyConfig::default();
|
|
||||||
runtime_cfg.access.users.insert(
|
|
||||||
"alice".to_string(),
|
|
||||||
"0123456789abcdef0123456789abcdef".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let stats = Stats::new();
|
|
||||||
let tracker = UserIpTracker::new();
|
|
||||||
let users =
|
|
||||||
users_from_config(&disk_cfg, &stats, &tracker, None, None, Some(&runtime_cfg)).await;
|
|
||||||
|
|
||||||
let alice = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "alice")
|
|
||||||
.expect("alice must be present");
|
|
||||||
let bob = users
|
|
||||||
.iter()
|
|
||||||
.find(|entry| entry.username == "bob")
|
|
||||||
.expect("bob must be present");
|
|
||||||
|
|
||||||
assert!(alice.in_runtime);
|
|
||||||
assert!(!bob.in_runtime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+55
-407
@@ -1,270 +1,11 @@
|
|||||||
//! CLI commands: --init (fire-and-forget setup), daemon options, subcommands
|
//! CLI commands: --init (fire-and-forget setup)
|
||||||
//!
|
|
||||||
//! Subcommands:
|
|
||||||
//! - `start [OPTIONS] [config.toml]` - Start the daemon
|
|
||||||
//! - `stop [--pid-file PATH]` - Stop a running daemon
|
|
||||||
//! - `reload [--pid-file PATH]` - Reload configuration (SIGHUP)
|
|
||||||
//! - `status [--pid-file PATH]` - Check daemon status
|
|
||||||
//! - `run [OPTIONS] [config.toml]` - Run in foreground (default behavior)
|
|
||||||
|
|
||||||
use rand::RngExt;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use rand::RngExt;
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::daemon::{self, DEFAULT_PID_FILE, DaemonOptions};
|
|
||||||
|
|
||||||
/// CLI subcommand to execute.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Subcommand {
|
|
||||||
/// Run the proxy (default, or explicit `run` subcommand).
|
|
||||||
Run,
|
|
||||||
/// Start as daemon (`start` subcommand).
|
|
||||||
Start,
|
|
||||||
/// Stop a running daemon (`stop` subcommand).
|
|
||||||
Stop,
|
|
||||||
/// Reload configuration (`reload` subcommand).
|
|
||||||
Reload,
|
|
||||||
/// Check daemon status (`status` subcommand).
|
|
||||||
Status,
|
|
||||||
/// Fire-and-forget setup (`--init`).
|
|
||||||
Init,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parsed subcommand with its options.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ParsedCommand {
|
|
||||||
pub subcommand: Subcommand,
|
|
||||||
pub pid_file: PathBuf,
|
|
||||||
pub config_path: String,
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub daemon_opts: DaemonOptions,
|
|
||||||
pub init_opts: Option<InitOptions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ParsedCommand {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
subcommand: Subcommand::Run,
|
|
||||||
#[cfg(unix)]
|
|
||||||
pid_file: PathBuf::from(DEFAULT_PID_FILE),
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pid_file: PathBuf::from("/var/run/telemt.pid"),
|
|
||||||
config_path: "config.toml".to_string(),
|
|
||||||
#[cfg(unix)]
|
|
||||||
daemon_opts: DaemonOptions::default(),
|
|
||||||
init_opts: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse CLI arguments into a command structure.
|
|
||||||
pub fn parse_command(args: &[String]) -> ParsedCommand {
|
|
||||||
let mut cmd = ParsedCommand::default();
|
|
||||||
|
|
||||||
// Check for --init first (legacy form)
|
|
||||||
if args.iter().any(|a| a == "--init") {
|
|
||||||
cmd.subcommand = Subcommand::Init;
|
|
||||||
cmd.init_opts = parse_init_args(args);
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for subcommand as first argument
|
|
||||||
if let Some(first) = args.first() {
|
|
||||||
match first.as_str() {
|
|
||||||
"start" => {
|
|
||||||
cmd.subcommand = Subcommand::Start;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
// Force daemonize for start command
|
|
||||||
cmd.daemon_opts.daemonize = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stop" => {
|
|
||||||
cmd.subcommand = Subcommand::Stop;
|
|
||||||
}
|
|
||||||
"reload" => {
|
|
||||||
cmd.subcommand = Subcommand::Reload;
|
|
||||||
}
|
|
||||||
"status" => {
|
|
||||||
cmd.subcommand = Subcommand::Status;
|
|
||||||
}
|
|
||||||
"run" => {
|
|
||||||
cmd.subcommand = Subcommand::Run;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// No subcommand, default to Run
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse remaining options
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
// Skip subcommand names
|
|
||||||
"start" | "stop" | "reload" | "status" | "run" => {}
|
|
||||||
// PID file option (for stop/reload/status)
|
|
||||||
"--pid-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
cmd.pid_file = PathBuf::from(&args[i]);
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--pid-file=") => {
|
|
||||||
cmd.pid_file = PathBuf::from(s.trim_start_matches("--pid-file="));
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Config path (positional, non-flag argument)
|
|
||||||
s if !s.starts_with('-') => {
|
|
||||||
cmd.config_path = s.to_string();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a subcommand that doesn't require starting the server.
|
|
||||||
/// Returns `Some(exit_code)` if the command was handled, `None` if server should start.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
|
||||||
match cmd.subcommand {
|
|
||||||
Subcommand::Stop => Some(cmd_stop(&cmd.pid_file)),
|
|
||||||
Subcommand::Reload => Some(cmd_reload(&cmd.pid_file)),
|
|
||||||
Subcommand::Status => Some(cmd_status(&cmd.pid_file)),
|
|
||||||
Subcommand::Init => {
|
|
||||||
if let Some(opts) = cmd.init_opts.clone() {
|
|
||||||
match run_init(opts) {
|
|
||||||
Ok(()) => Some(0),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Init failed: {}", e);
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Run and Start need the server
|
|
||||||
Subcommand::Run | Subcommand::Start => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
|
||||||
match cmd.subcommand {
|
|
||||||
Subcommand::Stop | Subcommand::Reload | Subcommand::Status => {
|
|
||||||
eprintln!("[telemt] Subcommand not supported on this platform");
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
Subcommand::Init => {
|
|
||||||
if let Some(opts) = cmd.init_opts.clone() {
|
|
||||||
match run_init(opts) {
|
|
||||||
Ok(()) => Some(0),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Init failed: {}", e);
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Subcommand::Run | Subcommand::Start => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop command: send SIGTERM to the running daemon.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_stop(pid_file: &Path) -> i32 {
|
|
||||||
use nix::sys::signal::Signal;
|
|
||||||
|
|
||||||
println!("Stopping telemt daemon...");
|
|
||||||
|
|
||||||
match daemon::signal_pid_file(pid_file, Signal::SIGTERM) {
|
|
||||||
Ok(()) => {
|
|
||||||
println!("Stop signal sent successfully");
|
|
||||||
|
|
||||||
// Wait for process to exit (up to 10 seconds)
|
|
||||||
for _ in 0..20 {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
||||||
if let daemon::DaemonStatus::NotRunning = daemon::check_status(pid_file) {
|
|
||||||
println!("Daemon stopped");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("Daemon may still be shutting down");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to stop daemon: {}", e);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload command: send SIGHUP to trigger config reload.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_reload(pid_file: &Path) -> i32 {
|
|
||||||
use nix::sys::signal::Signal;
|
|
||||||
|
|
||||||
println!("Reloading telemt configuration...");
|
|
||||||
|
|
||||||
match daemon::signal_pid_file(pid_file, Signal::SIGHUP) {
|
|
||||||
Ok(()) => {
|
|
||||||
println!("Reload signal sent successfully");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to reload daemon: {}", e);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Status command: check if daemon is running.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_status(pid_file: &Path) -> i32 {
|
|
||||||
match daemon::check_status(pid_file) {
|
|
||||||
daemon::DaemonStatus::Running(pid) => {
|
|
||||||
println!("telemt is running (pid {})", pid);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
daemon::DaemonStatus::Stale(pid) => {
|
|
||||||
println!("telemt is not running (stale pid file, was pid {})", pid);
|
|
||||||
// Clean up stale PID file
|
|
||||||
let _ = std::fs::remove_file(pid_file);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
daemon::DaemonStatus::NotRunning => {
|
|
||||||
println!("telemt is not running");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for the init command
|
/// Options for the init command
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct InitOptions {
|
pub struct InitOptions {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
@@ -274,64 +15,6 @@ pub struct InitOptions {
|
|||||||
pub no_start: bool,
|
pub no_start: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse daemon-related options from CLI args.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
|
||||||
let mut opts = DaemonOptions::default();
|
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
"--daemon" | "-d" => {
|
|
||||||
opts.daemonize = true;
|
|
||||||
}
|
|
||||||
"--foreground" | "-f" => {
|
|
||||||
opts.foreground = true;
|
|
||||||
}
|
|
||||||
"--pid-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.pid_file = Some(PathBuf::from(&args[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--pid-file=") => {
|
|
||||||
opts.pid_file = Some(PathBuf::from(s.trim_start_matches("--pid-file=")));
|
|
||||||
}
|
|
||||||
"--run-as-user" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.user = Some(args[i].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-user=") => {
|
|
||||||
opts.user = Some(s.trim_start_matches("--run-as-user=").to_string());
|
|
||||||
}
|
|
||||||
"--run-as-group" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.group = Some(args[i].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-group=") => {
|
|
||||||
opts.group = Some(s.trim_start_matches("--run-as-group=").to_string());
|
|
||||||
}
|
|
||||||
"--working-dir" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.working_dir = Some(PathBuf::from(&args[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--working-dir=") => {
|
|
||||||
opts.working_dir = Some(PathBuf::from(s.trim_start_matches("--working-dir=")));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
opts
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for InitOptions {
|
impl Default for InitOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -401,16 +84,10 @@ pub fn parse_init_args(args: &[String]) -> Option<InitOptions> {
|
|||||||
|
|
||||||
/// Run the fire-and-forget setup.
|
/// Run the fire-and-forget setup.
|
||||||
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use crate::service::{self, InitSystem, ServiceOptions};
|
|
||||||
|
|
||||||
eprintln!("[telemt] Fire-and-forget setup");
|
eprintln!("[telemt] Fire-and-forget setup");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 1. Detect init system
|
// 1. Generate or validate secret
|
||||||
let init_system = service::detect_init_system();
|
|
||||||
eprintln!("[+] Detected init system: {}", init_system);
|
|
||||||
|
|
||||||
// 2. Generate or validate secret
|
|
||||||
let secret = match opts.secret {
|
let secret = match opts.secret {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
@@ -427,74 +104,50 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
eprintln!("[+] Port: {}", opts.port);
|
eprintln!("[+] Port: {}", opts.port);
|
||||||
eprintln!("[+] Domain: {}", opts.domain);
|
eprintln!("[+] Domain: {}", opts.domain);
|
||||||
|
|
||||||
// 3. Create config directory
|
// 2. Create config directory
|
||||||
fs::create_dir_all(&opts.config_dir)?;
|
fs::create_dir_all(&opts.config_dir)?;
|
||||||
let config_path = opts.config_dir.join("config.toml");
|
let config_path = opts.config_dir.join("config.toml");
|
||||||
|
|
||||||
// 4. Write config
|
// 3. Write config
|
||||||
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
fs::write(&config_path, &config_content)?;
|
fs::write(&config_path, &config_content)?;
|
||||||
eprintln!("[+] Config written to {}", config_path.display());
|
eprintln!("[+] Config written to {}", config_path.display());
|
||||||
|
|
||||||
// 5. Generate and write service file
|
// 4. Write systemd unit
|
||||||
let exe_path =
|
let exe_path = std::env::current_exe()
|
||||||
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
.unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
||||||
|
|
||||||
let service_opts = ServiceOptions {
|
let unit_path = Path::new("/etc/systemd/system/telemt.service");
|
||||||
exe_path: &exe_path,
|
let unit_content = generate_systemd_unit(&exe_path, &config_path);
|
||||||
config_path: &config_path,
|
|
||||||
user: None, // Let systemd/init handle user
|
|
||||||
group: None,
|
|
||||||
pid_file: "/var/run/telemt.pid",
|
|
||||||
working_dir: Some("/var/lib/telemt"),
|
|
||||||
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
|
||||||
};
|
|
||||||
|
|
||||||
let service_path = service::service_file_path(init_system);
|
match fs::write(unit_path, &unit_content) {
|
||||||
let service_content = service::generate_service_file(init_system, &service_opts);
|
|
||||||
|
|
||||||
// Ensure parent directory exists
|
|
||||||
if let Some(parent) = Path::new(service_path).parent() {
|
|
||||||
let _ = fs::create_dir_all(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
match fs::write(service_path, &service_content) {
|
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
eprintln!("[+] Service file written to {}", service_path);
|
eprintln!("[+] Systemd unit written to {}", unit_path.display());
|
||||||
|
|
||||||
// Make script executable for OpenRC/FreeBSD
|
|
||||||
#[cfg(unix)]
|
|
||||||
if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let mut perms = fs::metadata(service_path)?.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
fs::set_permissions(service_path, perms)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[!] Cannot write service file (run as root?): {}", e);
|
eprintln!("[!] Cannot write systemd unit (run as root?): {}", e);
|
||||||
eprintln!("[!] Manual service file content:");
|
eprintln!("[!] Manual unit file content:");
|
||||||
eprintln!("{}", service_content);
|
eprintln!("{}", unit_content);
|
||||||
|
|
||||||
// Still print links and installation instructions
|
// Still print links and config
|
||||||
eprintln!();
|
|
||||||
eprintln!("{}", service::installation_instructions(init_system));
|
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Install and enable service based on init system
|
// 5. Reload systemd
|
||||||
match init_system {
|
|
||||||
InitSystem::Systemd => {
|
|
||||||
run_cmd("systemctl", &["daemon-reload"]);
|
run_cmd("systemctl", &["daemon-reload"]);
|
||||||
|
|
||||||
|
// 6. Enable service
|
||||||
run_cmd("systemctl", &["enable", "telemt.service"]);
|
run_cmd("systemctl", &["enable", "telemt.service"]);
|
||||||
eprintln!("[+] Service enabled");
|
eprintln!("[+] Service enabled");
|
||||||
|
|
||||||
|
// 7. Start service (unless --no-start)
|
||||||
if !opts.no_start {
|
if !opts.no_start {
|
||||||
run_cmd("systemctl", &["start", "telemt.service"]);
|
run_cmd("systemctl", &["start", "telemt.service"]);
|
||||||
eprintln!("[+] Service started");
|
eprintln!("[+] Service started");
|
||||||
|
|
||||||
|
// Brief delay then check status
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
let status = Command::new("systemctl")
|
let status = Command::new("systemctl")
|
||||||
.args(["is-active", "telemt.service"])
|
.args(["is-active", "telemt.service"])
|
||||||
@@ -513,40 +166,10 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
eprintln!("[+] Service not started (--no-start)");
|
eprintln!("[+] Service not started (--no-start)");
|
||||||
eprintln!("[+] Start manually: systemctl start telemt.service");
|
eprintln!("[+] Start manually: systemctl start telemt.service");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
InitSystem::OpenRC => {
|
|
||||||
run_cmd("rc-update", &["add", "telemt", "default"]);
|
|
||||||
eprintln!("[+] Service enabled");
|
|
||||||
|
|
||||||
if !opts.no_start {
|
|
||||||
run_cmd("rc-service", &["telemt", "start"]);
|
|
||||||
eprintln!("[+] Service started");
|
|
||||||
} else {
|
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
|
||||||
eprintln!("[+] Start manually: rc-service telemt start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InitSystem::FreeBSDRc => {
|
|
||||||
run_cmd("sysrc", &["telemt_enable=YES"]);
|
|
||||||
eprintln!("[+] Service enabled");
|
|
||||||
|
|
||||||
if !opts.no_start {
|
|
||||||
run_cmd("service", &["telemt", "start"]);
|
|
||||||
eprintln!("[+] Service started");
|
|
||||||
} else {
|
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
|
||||||
eprintln!("[+] Start manually: service telemt start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InitSystem::Unknown => {
|
|
||||||
eprintln!("[!] Unknown init system - service file written but not installed");
|
|
||||||
eprintln!("[!] You may need to install it manually");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 7. Print links
|
// 8. Print links
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -584,7 +207,6 @@ me_pool_drain_soft_evict_cooldown_ms = 1000
|
|||||||
me_bind_stale_mode = "never"
|
me_bind_stale_mode = "never"
|
||||||
me_pool_min_fresh_ratio = 0.8
|
me_pool_min_fresh_ratio = 0.8
|
||||||
me_reinit_drain_timeout_secs = 90
|
me_reinit_drain_timeout_secs = 90
|
||||||
tg_connect = 10
|
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
ipv4 = true
|
ipv4 = true
|
||||||
@@ -610,8 +232,8 @@ ip = "0.0.0.0"
|
|||||||
ip = "::"
|
ip = "::"
|
||||||
|
|
||||||
[timeouts]
|
[timeouts]
|
||||||
client_first_byte_idle_secs = 300
|
client_handshake = 15
|
||||||
client_handshake = 60
|
tg_connect = 10
|
||||||
client_keepalive = 60
|
client_keepalive = 60
|
||||||
client_ack = 300
|
client_ack = 300
|
||||||
|
|
||||||
@@ -623,7 +245,6 @@ fake_cert_len = 2048
|
|||||||
tls_full_cert_ttl_secs = 90
|
tls_full_cert_ttl_secs = 90
|
||||||
|
|
||||||
[access]
|
[access]
|
||||||
user_max_tcp_conns_global_each = 0
|
|
||||||
replay_check_len = 65536
|
replay_check_len = 65536
|
||||||
replay_window_secs = 120
|
replay_window_secs = 120
|
||||||
ignore_time_skew = false
|
ignore_time_skew = false
|
||||||
@@ -643,6 +264,35 @@ weight = 10
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String {
|
||||||
|
format!(
|
||||||
|
r#"[Unit]
|
||||||
|
Description=Telemt MTProxy
|
||||||
|
Documentation=https://github.com/telemt/telemt
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart={exe} {config}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=65535
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/etc/telemt
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"#,
|
||||||
|
exe = exe_path.display(),
|
||||||
|
config = config_path.display(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn run_cmd(cmd: &str, args: &[&str]) {
|
fn run_cmd(cmd: &str, args: &[&str]) {
|
||||||
match Command::new(cmd).args(args).output() {
|
match Command::new(cmd).args(args).output() {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
@@ -662,10 +312,8 @@ fn print_links(username: &str, secret: &str, port: u16, domain: &str) {
|
|||||||
|
|
||||||
println!("=== Proxy Links ===");
|
println!("=== Proxy Links ===");
|
||||||
println!("[{}]", username);
|
println!("[{}]", username);
|
||||||
println!(
|
println!(" EE-TLS: tg://proxy?server=YOUR_SERVER_IP&port={}&secret=ee{}{}",
|
||||||
" EE-TLS: tg://proxy?server=YOUR_SERVER_IP&port={}&secret=ee{}{}",
|
port, secret, domain_hex);
|
||||||
port, secret, domain_hex
|
|
||||||
);
|
|
||||||
println!();
|
println!();
|
||||||
println!("Replace YOUR_SERVER_IP with your server's public IP.");
|
println!("Replace YOUR_SERVER_IP with your server's public IP.");
|
||||||
println!("The proxy will auto-detect and display the correct link on startup.");
|
println!("The proxy will auto-detect and display the correct link on startup.");
|
||||||
|
|||||||
+10
-79
@@ -1,6 +1,6 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// Helper defaults kept private to the config module.
|
// Helper defaults kept private to the config module.
|
||||||
const DEFAULT_NETWORK_IPV6: Option<bool> = Some(false);
|
const DEFAULT_NETWORK_IPV6: Option<bool> = Some(false);
|
||||||
@@ -29,8 +29,6 @@ const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_FRAMES: usize = 32;
|
|||||||
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_BYTES: usize = 128 * 1024;
|
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_BYTES: usize = 128 * 1024;
|
||||||
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_DELAY_US: u64 = 500;
|
const DEFAULT_ME_D2C_FLUSH_BATCH_MAX_DELAY_US: u64 = 500;
|
||||||
const DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE: bool = true;
|
const DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE: bool = true;
|
||||||
const DEFAULT_ME_QUOTA_SOFT_OVERSHOOT_BYTES: u64 = 64 * 1024;
|
|
||||||
const DEFAULT_ME_D2C_FRAME_BUF_SHRINK_THRESHOLD_BYTES: usize = 256 * 1024;
|
|
||||||
const DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES: usize = 64 * 1024;
|
const DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES: usize = 64 * 1024;
|
||||||
const DEFAULT_DIRECT_RELAY_COPY_BUF_S2C_BYTES: usize = 256 * 1024;
|
const DEFAULT_DIRECT_RELAY_COPY_BUF_S2C_BYTES: usize = 256 * 1024;
|
||||||
const DEFAULT_ME_WRITER_PICK_SAMPLE_SIZE: u8 = 3;
|
const DEFAULT_ME_WRITER_PICK_SAMPLE_SIZE: u8 = 3;
|
||||||
@@ -71,22 +69,6 @@ pub(crate) fn default_tls_fetch_scope() -> String {
|
|||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_fetch_attempt_timeout_ms() -> u64 {
|
|
||||||
5_000
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_tls_fetch_total_budget_ms() -> u64 {
|
|
||||||
15_000
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_tls_fetch_strict_route() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_tls_fetch_profile_cache_ttl_secs() -> u64 {
|
|
||||||
600
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_mask_port() -> u16 {
|
pub(crate) fn default_mask_port() -> u16 {
|
||||||
443
|
443
|
||||||
}
|
}
|
||||||
@@ -110,11 +92,7 @@ pub(crate) fn default_replay_window_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_handshake_timeout() -> u64 {
|
pub(crate) fn default_handshake_timeout() -> u64 {
|
||||||
60
|
30
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_client_first_byte_idle_secs() -> u64 {
|
|
||||||
300
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_relay_idle_policy_v2_enabled() -> bool {
|
pub(crate) fn default_relay_idle_policy_v2_enabled() -> bool {
|
||||||
@@ -165,7 +143,10 @@ pub(crate) fn default_weight() -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_metrics_whitelist() -> Vec<IpNetwork> {
|
pub(crate) fn default_metrics_whitelist() -> Vec<IpNetwork> {
|
||||||
vec!["127.0.0.1/32".parse().unwrap(), "::1/128".parse().unwrap()]
|
vec![
|
||||||
|
"127.0.0.1/32".parse().unwrap(),
|
||||||
|
"::1/128".parse().unwrap(),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_api_listen() -> String {
|
pub(crate) fn default_api_listen() -> String {
|
||||||
@@ -188,35 +169,19 @@ pub(crate) fn default_api_minimal_runtime_cache_ttl_ms() -> u64 {
|
|||||||
1000
|
1000
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_api_runtime_edge_enabled() -> bool {
|
pub(crate) fn default_api_runtime_edge_enabled() -> bool { false }
|
||||||
false
|
pub(crate) fn default_api_runtime_edge_cache_ttl_ms() -> u64 { 1000 }
|
||||||
}
|
pub(crate) fn default_api_runtime_edge_top_n() -> usize { 10 }
|
||||||
pub(crate) fn default_api_runtime_edge_cache_ttl_ms() -> u64 {
|
pub(crate) fn default_api_runtime_edge_events_capacity() -> usize { 256 }
|
||||||
1000
|
|
||||||
}
|
|
||||||
pub(crate) fn default_api_runtime_edge_top_n() -> usize {
|
|
||||||
10
|
|
||||||
}
|
|
||||||
pub(crate) fn default_api_runtime_edge_events_capacity() -> usize {
|
|
||||||
256
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_listen_backlog() -> u32 {
|
|
||||||
1024
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_accept_permit_timeout_ms() -> u64 {
|
pub(crate) fn default_accept_permit_timeout_ms() -> u64 {
|
||||||
DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS
|
DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS
|
||||||
}
|
}
|
||||||
@@ -281,10 +246,6 @@ pub(crate) fn default_me2dc_fallback() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me2dc_fast() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_keepalive_interval() -> u64 {
|
pub(crate) fn default_keepalive_interval() -> u64 {
|
||||||
8
|
8
|
||||||
}
|
}
|
||||||
@@ -421,14 +382,6 @@ pub(crate) fn default_me_d2c_ack_flush_immediate() -> bool {
|
|||||||
DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE
|
DEFAULT_ME_D2C_ACK_FLUSH_IMMEDIATE
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me_quota_soft_overshoot_bytes() -> u64 {
|
|
||||||
DEFAULT_ME_QUOTA_SOFT_OVERSHOOT_BYTES
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_me_d2c_frame_buf_shrink_threshold_bytes() -> usize {
|
|
||||||
DEFAULT_ME_D2C_FRAME_BUF_SHRINK_THRESHOLD_BYTES
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_direct_relay_copy_buf_c2s_bytes() -> usize {
|
pub(crate) fn default_direct_relay_copy_buf_c2s_bytes() -> usize {
|
||||||
DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES
|
DEFAULT_DIRECT_RELAY_COPY_BUF_C2S_BYTES
|
||||||
}
|
}
|
||||||
@@ -565,10 +518,6 @@ pub(crate) fn default_mask_shape_hardening() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_mask_shape_hardening_aggressive_mode() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_mask_shape_bucket_floor_bytes() -> usize {
|
pub(crate) fn default_mask_shape_bucket_floor_bytes() -> usize {
|
||||||
512
|
512
|
||||||
}
|
}
|
||||||
@@ -585,20 +534,6 @@ pub(crate) fn default_mask_shape_above_cap_blur_max_bytes() -> usize {
|
|||||||
512
|
512
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
|
||||||
5 * 1024 * 1024
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
|
||||||
32 * 1024
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
|
|
||||||
5
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_mask_timing_normalization_enabled() -> bool {
|
pub(crate) fn default_mask_timing_normalization_enabled() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -811,10 +746,6 @@ pub(crate) fn default_user_max_unique_ips_window_secs() -> u64 {
|
|||||||
DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS
|
DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_user_max_tcp_conns_global_each() -> usize {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_user_max_unique_ips_global_each() -> usize {
|
pub(crate) fn default_user_max_unique_ips_global_each() -> usize {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-149
@@ -31,10 +31,11 @@ use notify::{EventKind, RecursiveMode, Watcher, recommended_watcher};
|
|||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use super::load::{LoadedConfig, ProxyConfig};
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel, MeWriterPickMode,
|
LogLevel, MeBindStaleMode, MeFloorMode, MeSocksKdfPolicy, MeTelemetryLevel,
|
||||||
|
MeWriterPickMode,
|
||||||
};
|
};
|
||||||
|
use super::load::{LoadedConfig, ProxyConfig};
|
||||||
|
|
||||||
const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
const HOT_RELOAD_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
@@ -106,8 +107,6 @@ pub struct HotFields {
|
|||||||
pub me_d2c_flush_batch_max_bytes: usize,
|
pub me_d2c_flush_batch_max_bytes: usize,
|
||||||
pub me_d2c_flush_batch_max_delay_us: u64,
|
pub me_d2c_flush_batch_max_delay_us: u64,
|
||||||
pub me_d2c_ack_flush_immediate: bool,
|
pub me_d2c_ack_flush_immediate: bool,
|
||||||
pub me_quota_soft_overshoot_bytes: u64,
|
|
||||||
pub me_d2c_frame_buf_shrink_threshold_bytes: usize,
|
|
||||||
pub direct_relay_copy_buf_c2s_bytes: usize,
|
pub direct_relay_copy_buf_c2s_bytes: usize,
|
||||||
pub direct_relay_copy_buf_s2c_bytes: usize,
|
pub direct_relay_copy_buf_s2c_bytes: usize,
|
||||||
pub me_health_interval_ms_unhealthy: u64,
|
pub me_health_interval_ms_unhealthy: u64,
|
||||||
@@ -117,7 +116,6 @@ pub struct HotFields {
|
|||||||
pub users: std::collections::HashMap<String, String>,
|
pub users: std::collections::HashMap<String, String>,
|
||||||
pub user_ad_tags: std::collections::HashMap<String, String>,
|
pub user_ad_tags: std::collections::HashMap<String, String>,
|
||||||
pub user_max_tcp_conns: std::collections::HashMap<String, usize>,
|
pub user_max_tcp_conns: std::collections::HashMap<String, usize>,
|
||||||
pub user_max_tcp_conns_global_each: usize,
|
|
||||||
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
||||||
pub user_data_quota: std::collections::HashMap<String, u64>,
|
pub user_data_quota: std::collections::HashMap<String, u64>,
|
||||||
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
||||||
@@ -191,11 +189,15 @@ impl HotFields {
|
|||||||
me_adaptive_floor_min_writers_multi_endpoint: cfg
|
me_adaptive_floor_min_writers_multi_endpoint: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_min_writers_multi_endpoint,
|
.me_adaptive_floor_min_writers_multi_endpoint,
|
||||||
me_adaptive_floor_recover_grace_secs: cfg.general.me_adaptive_floor_recover_grace_secs,
|
me_adaptive_floor_recover_grace_secs: cfg
|
||||||
|
.general
|
||||||
|
.me_adaptive_floor_recover_grace_secs,
|
||||||
me_adaptive_floor_writers_per_core_total: cfg
|
me_adaptive_floor_writers_per_core_total: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_writers_per_core_total,
|
.me_adaptive_floor_writers_per_core_total,
|
||||||
me_adaptive_floor_cpu_cores_override: cfg.general.me_adaptive_floor_cpu_cores_override,
|
me_adaptive_floor_cpu_cores_override: cfg
|
||||||
|
.general
|
||||||
|
.me_adaptive_floor_cpu_cores_override,
|
||||||
me_adaptive_floor_max_extra_writers_single_per_core: cfg
|
me_adaptive_floor_max_extra_writers_single_per_core: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core,
|
.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
@@ -214,24 +216,14 @@ impl HotFields {
|
|||||||
me_adaptive_floor_max_warm_writers_global: cfg
|
me_adaptive_floor_max_warm_writers_global: cfg
|
||||||
.general
|
.general
|
||||||
.me_adaptive_floor_max_warm_writers_global,
|
.me_adaptive_floor_max_warm_writers_global,
|
||||||
me_route_backpressure_base_timeout_ms: cfg
|
me_route_backpressure_base_timeout_ms: cfg.general.me_route_backpressure_base_timeout_ms,
|
||||||
.general
|
me_route_backpressure_high_timeout_ms: cfg.general.me_route_backpressure_high_timeout_ms,
|
||||||
.me_route_backpressure_base_timeout_ms,
|
me_route_backpressure_high_watermark_pct: cfg.general.me_route_backpressure_high_watermark_pct,
|
||||||
me_route_backpressure_high_timeout_ms: cfg
|
|
||||||
.general
|
|
||||||
.me_route_backpressure_high_timeout_ms,
|
|
||||||
me_route_backpressure_high_watermark_pct: cfg
|
|
||||||
.general
|
|
||||||
.me_route_backpressure_high_watermark_pct,
|
|
||||||
me_reader_route_data_wait_ms: cfg.general.me_reader_route_data_wait_ms,
|
me_reader_route_data_wait_ms: cfg.general.me_reader_route_data_wait_ms,
|
||||||
me_d2c_flush_batch_max_frames: cfg.general.me_d2c_flush_batch_max_frames,
|
me_d2c_flush_batch_max_frames: cfg.general.me_d2c_flush_batch_max_frames,
|
||||||
me_d2c_flush_batch_max_bytes: cfg.general.me_d2c_flush_batch_max_bytes,
|
me_d2c_flush_batch_max_bytes: cfg.general.me_d2c_flush_batch_max_bytes,
|
||||||
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_d2c_frame_buf_shrink_threshold_bytes: cfg
|
|
||||||
.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,
|
||||||
@@ -241,7 +233,6 @@ impl HotFields {
|
|||||||
users: cfg.access.users.clone(),
|
users: cfg.access.users.clone(),
|
||||||
user_ad_tags: cfg.access.user_ad_tags.clone(),
|
user_ad_tags: cfg.access.user_ad_tags.clone(),
|
||||||
user_max_tcp_conns: cfg.access.user_max_tcp_conns.clone(),
|
user_max_tcp_conns: cfg.access.user_max_tcp_conns.clone(),
|
||||||
user_max_tcp_conns_global_each: cfg.access.user_max_tcp_conns_global_each,
|
|
||||||
user_expirations: cfg.access.user_expirations.clone(),
|
user_expirations: cfg.access.user_expirations.clone(),
|
||||||
user_data_quota: cfg.access.user_data_quota.clone(),
|
user_data_quota: cfg.access.user_data_quota.clone(),
|
||||||
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
||||||
@@ -343,9 +334,7 @@ struct ReloadState {
|
|||||||
|
|
||||||
impl ReloadState {
|
impl ReloadState {
|
||||||
fn new(applied_snapshot_hash: Option<u64>) -> Self {
|
fn new(applied_snapshot_hash: Option<u64>) -> Self {
|
||||||
Self {
|
Self { applied_snapshot_hash }
|
||||||
applied_snapshot_hash,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_applied(&self, hash: u64) -> bool {
|
fn is_applied(&self, hash: u64) -> bool {
|
||||||
@@ -492,14 +481,10 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
new.general.me_adaptive_floor_writers_per_core_total;
|
new.general.me_adaptive_floor_writers_per_core_total;
|
||||||
cfg.general.me_adaptive_floor_cpu_cores_override =
|
cfg.general.me_adaptive_floor_cpu_cores_override =
|
||||||
new.general.me_adaptive_floor_cpu_cores_override;
|
new.general.me_adaptive_floor_cpu_cores_override;
|
||||||
cfg.general
|
cfg.general.me_adaptive_floor_max_extra_writers_single_per_core =
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core = new
|
new.general.me_adaptive_floor_max_extra_writers_single_per_core;
|
||||||
.general
|
cfg.general.me_adaptive_floor_max_extra_writers_multi_per_core =
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core;
|
new.general.me_adaptive_floor_max_extra_writers_multi_per_core;
|
||||||
cfg.general
|
|
||||||
.me_adaptive_floor_max_extra_writers_multi_per_core = new
|
|
||||||
.general
|
|
||||||
.me_adaptive_floor_max_extra_writers_multi_per_core;
|
|
||||||
cfg.general.me_adaptive_floor_max_active_writers_per_core =
|
cfg.general.me_adaptive_floor_max_active_writers_per_core =
|
||||||
new.general.me_adaptive_floor_max_active_writers_per_core;
|
new.general.me_adaptive_floor_max_active_writers_per_core;
|
||||||
cfg.general.me_adaptive_floor_max_warm_writers_per_core =
|
cfg.general.me_adaptive_floor_max_warm_writers_per_core =
|
||||||
@@ -519,9 +504,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
cfg.general.me_d2c_flush_batch_max_bytes = new.general.me_d2c_flush_batch_max_bytes;
|
cfg.general.me_d2c_flush_batch_max_bytes = new.general.me_d2c_flush_batch_max_bytes;
|
||||||
cfg.general.me_d2c_flush_batch_max_delay_us = new.general.me_d2c_flush_batch_max_delay_us;
|
cfg.general.me_d2c_flush_batch_max_delay_us = new.general.me_d2c_flush_batch_max_delay_us;
|
||||||
cfg.general.me_d2c_ack_flush_immediate = new.general.me_d2c_ack_flush_immediate;
|
cfg.general.me_d2c_ack_flush_immediate = new.general.me_d2c_ack_flush_immediate;
|
||||||
cfg.general.me_quota_soft_overshoot_bytes = new.general.me_quota_soft_overshoot_bytes;
|
|
||||||
cfg.general.me_d2c_frame_buf_shrink_threshold_bytes =
|
|
||||||
new.general.me_d2c_frame_buf_shrink_threshold_bytes;
|
|
||||||
cfg.general.direct_relay_copy_buf_c2s_bytes = new.general.direct_relay_copy_buf_c2s_bytes;
|
cfg.general.direct_relay_copy_buf_c2s_bytes = new.general.direct_relay_copy_buf_c2s_bytes;
|
||||||
cfg.general.direct_relay_copy_buf_s2c_bytes = new.general.direct_relay_copy_buf_s2c_bytes;
|
cfg.general.direct_relay_copy_buf_s2c_bytes = new.general.direct_relay_copy_buf_s2c_bytes;
|
||||||
cfg.general.me_health_interval_ms_unhealthy = new.general.me_health_interval_ms_unhealthy;
|
cfg.general.me_health_interval_ms_unhealthy = new.general.me_health_interval_ms_unhealthy;
|
||||||
@@ -532,7 +514,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
|||||||
cfg.access.users = new.access.users.clone();
|
cfg.access.users = new.access.users.clone();
|
||||||
cfg.access.user_ad_tags = new.access.user_ad_tags.clone();
|
cfg.access.user_ad_tags = new.access.user_ad_tags.clone();
|
||||||
cfg.access.user_max_tcp_conns = new.access.user_max_tcp_conns.clone();
|
cfg.access.user_max_tcp_conns = new.access.user_max_tcp_conns.clone();
|
||||||
cfg.access.user_max_tcp_conns_global_each = new.access.user_max_tcp_conns_global_each;
|
|
||||||
cfg.access.user_expirations = new.access.user_expirations.clone();
|
cfg.access.user_expirations = new.access.user_expirations.clone();
|
||||||
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
||||||
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
||||||
@@ -562,7 +543,8 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.server.api.minimal_runtime_cache_ttl_ms
|
|| old.server.api.minimal_runtime_cache_ttl_ms
|
||||||
!= new.server.api.minimal_runtime_cache_ttl_ms
|
!= new.server.api.minimal_runtime_cache_ttl_ms
|
||||||
|| old.server.api.runtime_edge_enabled != new.server.api.runtime_edge_enabled
|
|| old.server.api.runtime_edge_enabled != new.server.api.runtime_edge_enabled
|
||||||
|| old.server.api.runtime_edge_cache_ttl_ms != new.server.api.runtime_edge_cache_ttl_ms
|
|| old.server.api.runtime_edge_cache_ttl_ms
|
||||||
|
!= new.server.api.runtime_edge_cache_ttl_ms
|
||||||
|| old.server.api.runtime_edge_top_n != new.server.api.runtime_edge_top_n
|
|| old.server.api.runtime_edge_top_n != new.server.api.runtime_edge_top_n
|
||||||
|| old.server.api.runtime_edge_events_capacity
|
|| old.server.api.runtime_edge_events_capacity
|
||||||
!= new.server.api.runtime_edge_events_capacity
|
!= new.server.api.runtime_edge_events_capacity
|
||||||
@@ -573,7 +555,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
}
|
}
|
||||||
if old.server.proxy_protocol != new.server.proxy_protocol
|
if old.server.proxy_protocol != new.server.proxy_protocol
|
||||||
|| !listeners_equal(&old.server.listeners, &new.server.listeners)
|
|| !listeners_equal(&old.server.listeners, &new.server.listeners)
|
||||||
|| old.server.listen_backlog != new.server.listen_backlog
|
|
||||||
|| old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
|
|| old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
|
||||||
|| old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
|
|| old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
|
||||||
|| old.server.listen_tcp != new.server.listen_tcp
|
|| old.server.listen_tcp != new.server.listen_tcp
|
||||||
@@ -602,13 +583,12 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.censorship.mask_shape_hardening != new.censorship.mask_shape_hardening
|
|| old.censorship.mask_shape_hardening != new.censorship.mask_shape_hardening
|
||||||
|| old.censorship.mask_shape_bucket_floor_bytes
|
|| old.censorship.mask_shape_bucket_floor_bytes
|
||||||
!= new.censorship.mask_shape_bucket_floor_bytes
|
!= new.censorship.mask_shape_bucket_floor_bytes
|
||||||
|| old.censorship.mask_shape_bucket_cap_bytes != new.censorship.mask_shape_bucket_cap_bytes
|
|| old.censorship.mask_shape_bucket_cap_bytes
|
||||||
|| old.censorship.mask_shape_above_cap_blur != new.censorship.mask_shape_above_cap_blur
|
!= new.censorship.mask_shape_bucket_cap_bytes
|
||||||
|
|| old.censorship.mask_shape_above_cap_blur
|
||||||
|
!= new.censorship.mask_shape_above_cap_blur
|
||||||
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|
|
||||||
|| old.censorship.mask_classifier_prefetch_timeout_ms
|
|
||||||
!= new.censorship.mask_classifier_prefetch_timeout_ms
|
|
||||||
|| old.censorship.mask_timing_normalization_enabled
|
|| old.censorship.mask_timing_normalization_enabled
|
||||||
!= new.censorship.mask_timing_normalization_enabled
|
!= new.censorship.mask_timing_normalization_enabled
|
||||||
|| old.censorship.mask_timing_normalization_floor_ms
|
|| old.censorship.mask_timing_normalization_floor_ms
|
||||||
@@ -655,9 +635,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
}
|
}
|
||||||
if old.general.me_route_no_writer_mode != new.general.me_route_no_writer_mode
|
if old.general.me_route_no_writer_mode != new.general.me_route_no_writer_mode
|
||||||
|| old.general.me_route_no_writer_wait_ms != new.general.me_route_no_writer_wait_ms
|
|| old.general.me_route_no_writer_wait_ms != new.general.me_route_no_writer_wait_ms
|
||||||
|| old.general.me_route_hybrid_max_wait_ms != new.general.me_route_hybrid_max_wait_ms
|
|
||||||
|| old.general.me_route_blocking_send_timeout_ms
|
|
||||||
!= new.general.me_route_blocking_send_timeout_ms
|
|
||||||
|| old.general.me_route_inline_recovery_attempts
|
|| old.general.me_route_inline_recovery_attempts
|
||||||
!= new.general.me_route_inline_recovery_attempts
|
!= new.general.me_route_inline_recovery_attempts
|
||||||
|| old.general.me_route_inline_recovery_wait_ms
|
|| old.general.me_route_inline_recovery_wait_ms
|
||||||
@@ -676,11 +653,9 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
warned = true;
|
warned = true;
|
||||||
warn!("config reload: general.me_init_retry_attempts changed; restart required");
|
warn!("config reload: general.me_init_retry_attempts changed; restart required");
|
||||||
}
|
}
|
||||||
if old.general.me2dc_fallback != new.general.me2dc_fallback
|
if old.general.me2dc_fallback != new.general.me2dc_fallback {
|
||||||
|| old.general.me2dc_fast != new.general.me2dc_fast
|
|
||||||
{
|
|
||||||
warned = true;
|
warned = true;
|
||||||
warn!("config reload: general.me2dc_fallback/me2dc_fast changed; restart required");
|
warn!("config reload: general.me2dc_fallback changed; restart required");
|
||||||
}
|
}
|
||||||
if old.general.proxy_config_v4_cache_path != new.general.proxy_config_v4_cache_path
|
if old.general.proxy_config_v4_cache_path != new.general.proxy_config_v4_cache_path
|
||||||
|| old.general.proxy_config_v6_cache_path != new.general.proxy_config_v6_cache_path
|
|| old.general.proxy_config_v6_cache_path != new.general.proxy_config_v6_cache_path
|
||||||
@@ -699,7 +674,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
|
||||||
|| old.general.upstream_connect_retry_backoff_ms
|
|| old.general.upstream_connect_retry_backoff_ms
|
||||||
!= new.general.upstream_connect_retry_backoff_ms
|
!= new.general.upstream_connect_retry_backoff_ms
|
||||||
|| old.general.tg_connect != new.general.tg_connect
|
|
||||||
|| old.general.upstream_unhealthy_fail_threshold
|
|| old.general.upstream_unhealthy_fail_threshold
|
||||||
!= new.general.upstream_unhealthy_fail_threshold
|
!= new.general.upstream_unhealthy_fail_threshold
|
||||||
|| old.general.upstream_connect_failfast_hard_errors
|
|| old.general.upstream_connect_failfast_hard_errors
|
||||||
@@ -896,7 +870,8 @@ fn log_changes(
|
|||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: me_bind_stale: mode={:?} ttl={}s",
|
"config reload: me_bind_stale: mode={:?} ttl={}s",
|
||||||
new_hot.me_bind_stale_mode, new_hot.me_bind_stale_ttl_secs
|
new_hot.me_bind_stale_mode,
|
||||||
|
new_hot.me_bind_stale_ttl_secs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.me_secret_atomic_snapshot != new_hot.me_secret_atomic_snapshot
|
if old_hot.me_secret_atomic_snapshot != new_hot.me_secret_atomic_snapshot
|
||||||
@@ -976,7 +951,8 @@ fn log_changes(
|
|||||||
if old_hot.me_socks_kdf_policy != new_hot.me_socks_kdf_policy {
|
if old_hot.me_socks_kdf_policy != new_hot.me_socks_kdf_policy {
|
||||||
info!(
|
info!(
|
||||||
"config reload: me_socks_kdf_policy: {:?} → {:?}",
|
"config reload: me_socks_kdf_policy: {:?} → {:?}",
|
||||||
old_hot.me_socks_kdf_policy, new_hot.me_socks_kdf_policy,
|
old_hot.me_socks_kdf_policy,
|
||||||
|
new_hot.me_socks_kdf_policy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,7 +1006,8 @@ fn log_changes(
|
|||||||
|| old_hot.me_route_backpressure_high_watermark_pct
|
|| old_hot.me_route_backpressure_high_watermark_pct
|
||||||
!= new_hot.me_route_backpressure_high_watermark_pct
|
!= new_hot.me_route_backpressure_high_watermark_pct
|
||||||
|| old_hot.me_reader_route_data_wait_ms != new_hot.me_reader_route_data_wait_ms
|
|| old_hot.me_reader_route_data_wait_ms != new_hot.me_reader_route_data_wait_ms
|
||||||
|| old_hot.me_health_interval_ms_unhealthy != new_hot.me_health_interval_ms_unhealthy
|
|| old_hot.me_health_interval_ms_unhealthy
|
||||||
|
!= new_hot.me_health_interval_ms_unhealthy
|
||||||
|| old_hot.me_health_interval_ms_healthy != new_hot.me_health_interval_ms_healthy
|
|| old_hot.me_health_interval_ms_healthy != new_hot.me_health_interval_ms_healthy
|
||||||
|| old_hot.me_admission_poll_ms != new_hot.me_admission_poll_ms
|
|| old_hot.me_admission_poll_ms != new_hot.me_admission_poll_ms
|
||||||
|| old_hot.me_warn_rate_limit_ms != new_hot.me_warn_rate_limit_ms
|
|| old_hot.me_warn_rate_limit_ms != new_hot.me_warn_rate_limit_ms
|
||||||
@@ -1052,47 +1029,34 @@ fn log_changes(
|
|||||||
|| old_hot.me_d2c_flush_batch_max_bytes != new_hot.me_d2c_flush_batch_max_bytes
|
|| old_hot.me_d2c_flush_batch_max_bytes != new_hot.me_d2c_flush_batch_max_bytes
|
||||||
|| old_hot.me_d2c_flush_batch_max_delay_us != new_hot.me_d2c_flush_batch_max_delay_us
|
|| old_hot.me_d2c_flush_batch_max_delay_us != new_hot.me_d2c_flush_batch_max_delay_us
|
||||||
|| old_hot.me_d2c_ack_flush_immediate != new_hot.me_d2c_ack_flush_immediate
|
|| old_hot.me_d2c_ack_flush_immediate != new_hot.me_d2c_ack_flush_immediate
|
||||||
|| old_hot.me_quota_soft_overshoot_bytes != new_hot.me_quota_soft_overshoot_bytes
|
|
||||||
|| old_hot.me_d2c_frame_buf_shrink_threshold_bytes
|
|
||||||
!= new_hot.me_d2c_frame_buf_shrink_threshold_bytes
|
|
||||||
|| old_hot.direct_relay_copy_buf_c2s_bytes != new_hot.direct_relay_copy_buf_c2s_bytes
|
|| old_hot.direct_relay_copy_buf_c2s_bytes != new_hot.direct_relay_copy_buf_c2s_bytes
|
||||||
|| old_hot.direct_relay_copy_buf_s2c_bytes != new_hot.direct_relay_copy_buf_s2c_bytes
|
|| old_hot.direct_relay_copy_buf_s2c_bytes != new_hot.direct_relay_copy_buf_s2c_bytes
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: relay_tuning: me_d2c_frames={} me_d2c_bytes={} me_d2c_delay_us={} me_ack_flush_immediate={} me_quota_soft_overshoot_bytes={} me_d2c_frame_buf_shrink_threshold_bytes={} direct_buf_c2s={} direct_buf_s2c={}",
|
"config reload: relay_tuning: me_d2c_frames={} me_d2c_bytes={} me_d2c_delay_us={} me_ack_flush_immediate={} direct_buf_c2s={} direct_buf_s2c={}",
|
||||||
new_hot.me_d2c_flush_batch_max_frames,
|
new_hot.me_d2c_flush_batch_max_frames,
|
||||||
new_hot.me_d2c_flush_batch_max_bytes,
|
new_hot.me_d2c_flush_batch_max_bytes,
|
||||||
new_hot.me_d2c_flush_batch_max_delay_us,
|
new_hot.me_d2c_flush_batch_max_delay_us,
|
||||||
new_hot.me_d2c_ack_flush_immediate,
|
new_hot.me_d2c_ack_flush_immediate,
|
||||||
new_hot.me_quota_soft_overshoot_bytes,
|
|
||||||
new_hot.me_d2c_frame_buf_shrink_threshold_bytes,
|
|
||||||
new_hot.direct_relay_copy_buf_c2s_bytes,
|
new_hot.direct_relay_copy_buf_c2s_bytes,
|
||||||
new_hot.direct_relay_copy_buf_s2c_bytes,
|
new_hot.direct_relay_copy_buf_s2c_bytes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if old_hot.users != new_hot.users {
|
if old_hot.users != new_hot.users {
|
||||||
let mut added: Vec<&String> = new_hot
|
let mut added: Vec<&String> = new_hot.users.keys()
|
||||||
.users
|
|
||||||
.keys()
|
|
||||||
.filter(|u| !old_hot.users.contains_key(*u))
|
.filter(|u| !old_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
added.sort();
|
added.sort();
|
||||||
|
|
||||||
let mut removed: Vec<&String> = old_hot
|
let mut removed: Vec<&String> = old_hot.users.keys()
|
||||||
.users
|
|
||||||
.keys()
|
|
||||||
.filter(|u| !new_hot.users.contains_key(*u))
|
.filter(|u| !new_hot.users.contains_key(*u))
|
||||||
.collect();
|
.collect();
|
||||||
removed.sort();
|
removed.sort();
|
||||||
|
|
||||||
let mut changed: Vec<&String> = new_hot
|
let mut changed: Vec<&String> = new_hot.users.keys()
|
||||||
.users
|
|
||||||
.keys()
|
|
||||||
.filter(|u| {
|
.filter(|u| {
|
||||||
old_hot
|
old_hot.users.get(*u)
|
||||||
.users
|
|
||||||
.get(*u)
|
|
||||||
.map(|s| s != &new_hot.users[*u])
|
.map(|s| s != &new_hot.users[*u])
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
@@ -1102,18 +1066,10 @@ fn log_changes(
|
|||||||
if !added.is_empty() {
|
if !added.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users added: [{}]",
|
"config reload: users added: [{}]",
|
||||||
added
|
added.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
.iter()
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
);
|
||||||
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
let host = resolve_link_host(new_cfg, detected_ip_v4, detected_ip_v6);
|
||||||
let port = new_cfg
|
let port = new_cfg.general.links.public_port.unwrap_or(new_cfg.server.port);
|
||||||
.general
|
|
||||||
.links
|
|
||||||
.public_port
|
|
||||||
.unwrap_or(new_cfg.server.port);
|
|
||||||
for user in &added {
|
for user in &added {
|
||||||
if let Some(secret) = new_hot.users.get(*user) {
|
if let Some(secret) = new_hot.users.get(*user) {
|
||||||
print_user_links(user, secret, &host, port, new_cfg);
|
print_user_links(user, secret, &host, port, new_cfg);
|
||||||
@@ -1123,21 +1079,13 @@ fn log_changes(
|
|||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users removed: [{}]",
|
"config reload: users removed: [{}]",
|
||||||
removed
|
removed.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
.iter()
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !changed.is_empty() {
|
if !changed.is_empty() {
|
||||||
info!(
|
info!(
|
||||||
"config reload: users secret changed: [{}]",
|
"config reload: users secret changed: [{}]",
|
||||||
changed
|
changed.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
.iter()
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1148,12 +1096,6 @@ fn log_changes(
|
|||||||
new_hot.user_max_tcp_conns.len()
|
new_hot.user_max_tcp_conns.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if old_hot.user_max_tcp_conns_global_each != new_hot.user_max_tcp_conns_global_each {
|
|
||||||
info!(
|
|
||||||
"config reload: user_max_tcp_conns policy global_each={}",
|
|
||||||
new_hot.user_max_tcp_conns_global_each
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if old_hot.user_expirations != new_hot.user_expirations {
|
if old_hot.user_expirations != new_hot.user_expirations {
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_expirations updated ({} entries)",
|
"config reload: user_expirations updated ({} entries)",
|
||||||
@@ -1174,7 +1116,8 @@ fn log_changes(
|
|||||||
}
|
}
|
||||||
if old_hot.user_max_unique_ips_global_each != new_hot.user_max_unique_ips_global_each
|
if old_hot.user_max_unique_ips_global_each != new_hot.user_max_unique_ips_global_each
|
||||||
|| old_hot.user_max_unique_ips_mode != new_hot.user_max_unique_ips_mode
|
|| old_hot.user_max_unique_ips_mode != new_hot.user_max_unique_ips_mode
|
||||||
|| old_hot.user_max_unique_ips_window_secs != new_hot.user_max_unique_ips_window_secs
|
|| old_hot.user_max_unique_ips_window_secs
|
||||||
|
!= new_hot.user_max_unique_ips_window_secs
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
"config reload: user_max_unique_ips policy global_each={} mode={:?} window={}s",
|
"config reload: user_max_unique_ips policy global_each={} mode={:?} window={}s",
|
||||||
@@ -1209,10 +1152,7 @@ fn reload_config(
|
|||||||
let next_manifest = WatchManifest::from_source_files(&source_files);
|
let next_manifest = WatchManifest::from_source_files(&source_files);
|
||||||
|
|
||||||
if let Err(e) = new_cfg.validate() {
|
if let Err(e) = new_cfg.validate() {
|
||||||
error!(
|
error!("config reload: validation failed: {}; keeping old config", e);
|
||||||
"config reload: validation failed: {}; keeping old config",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return Some(next_manifest);
|
return Some(next_manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1294,13 +1234,9 @@ pub fn spawn_config_watcher(
|
|||||||
|
|
||||||
let tx_inotify = notify_tx.clone();
|
let tx_inotify = notify_tx.clone();
|
||||||
let manifest_for_inotify = manifest_state.clone();
|
let manifest_for_inotify = manifest_state.clone();
|
||||||
let mut inotify_watcher =
|
let mut inotify_watcher = match recommended_watcher(move |res: notify::Result<notify::Event>| {
|
||||||
match recommended_watcher(move |res: notify::Result<notify::Event>| {
|
|
||||||
let Ok(event) = res else { return };
|
let Ok(event) = res else { return };
|
||||||
if !matches!(
|
if !matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
||||||
event.kind,
|
|
||||||
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let is_our_file = manifest_for_inotify
|
let is_our_file = manifest_for_inotify
|
||||||
@@ -1332,10 +1268,7 @@ pub fn spawn_config_watcher(
|
|||||||
let mut poll_watcher = match notify::poll::PollWatcher::new(
|
let mut poll_watcher = match notify::poll::PollWatcher::new(
|
||||||
move |res: notify::Result<notify::Event>| {
|
move |res: notify::Result<notify::Event>| {
|
||||||
let Ok(event) = res else { return };
|
let Ok(event) = res else { return };
|
||||||
if !matches!(
|
if !matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
||||||
event.kind,
|
|
||||||
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let is_our_file = manifest_for_poll
|
let is_our_file = manifest_for_poll
|
||||||
@@ -1383,9 +1316,7 @@ pub fn spawn_config_watcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
if notify_rx.recv().await.is_none() {
|
if notify_rx.recv().await.is_none() { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce: drain extra events that arrive within a short quiet window.
|
// Debounce: drain extra events that arrive within a short quiet window.
|
||||||
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
|
tokio::time::sleep(HOT_RELOAD_DEBOUNCE).await;
|
||||||
@@ -1487,10 +1418,7 @@ mod tests {
|
|||||||
new.server.port = old.server.port.saturating_add(1);
|
new.server.port = old.server.port.saturating_add(1);
|
||||||
|
|
||||||
let applied = overlay_hot_fields(&old, &new);
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
assert_eq!(
|
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
HotFields::from_config(&old),
|
|
||||||
HotFields::from_config(&applied)
|
|
||||||
);
|
|
||||||
assert_eq!(applied.server.port, old.server.port);
|
assert_eq!(applied.server.port, old.server.port);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1509,10 +1437,7 @@ mod tests {
|
|||||||
applied.general.me_bind_stale_mode,
|
applied.general.me_bind_stale_mode,
|
||||||
new.general.me_bind_stale_mode
|
new.general.me_bind_stale_mode
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
HotFields::from_config(&old),
|
|
||||||
HotFields::from_config(&applied)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1526,10 +1451,7 @@ mod tests {
|
|||||||
applied.general.me_keepalive_interval_secs,
|
applied.general.me_keepalive_interval_secs,
|
||||||
old.general.me_keepalive_interval_secs
|
old.general.me_keepalive_interval_secs
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(HotFields::from_config(&old), HotFields::from_config(&applied));
|
||||||
HotFields::from_config(&old),
|
|
||||||
HotFields::from_config(&applied)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1541,10 +1463,7 @@ mod tests {
|
|||||||
|
|
||||||
let applied = overlay_hot_fields(&old, &new);
|
let applied = overlay_hot_fields(&old, &new);
|
||||||
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
assert_eq!(applied.general.hardswap, new.general.hardswap);
|
||||||
assert_eq!(
|
assert_eq!(applied.general.use_middle_proxy, old.general.use_middle_proxy);
|
||||||
applied.general.use_middle_proxy,
|
|
||||||
old.general.use_middle_proxy
|
|
||||||
);
|
|
||||||
assert!(!config_equal(&applied, &new));
|
assert!(!config_equal(&applied, &new));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1556,19 +1475,14 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
||||||
.unwrap()
|
|
||||||
.rendered_hash;
|
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
|
|
||||||
write_reload_config(&path, Some(final_tag), None);
|
write_reload_config(&path, Some(final_tag), None);
|
||||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(config_tx.borrow().general.ad_tag.as_deref(), Some(final_tag));
|
||||||
config_tx.borrow().general.ad_tag.as_deref(),
|
|
||||||
Some(final_tag)
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
@@ -1581,9 +1495,7 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
||||||
.unwrap()
|
|
||||||
.rendered_hash;
|
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
@@ -1606,9 +1518,7 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(initial_tag), None);
|
write_reload_config(&path, Some(initial_tag), None);
|
||||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||||
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
let initial_hash = ProxyConfig::load_with_metadata(&path).unwrap().rendered_hash;
|
||||||
.unwrap()
|
|
||||||
.rendered_hash;
|
|
||||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||||
@@ -1622,10 +1532,7 @@ mod tests {
|
|||||||
|
|
||||||
write_reload_config(&path, Some(final_tag), None);
|
write_reload_config(&path, Some(final_tag), None);
|
||||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(config_tx.borrow().general.ad_tag.as_deref(), Some(final_tag));
|
||||||
config_tx.borrow().general.ad_tag.as_deref(),
|
|
||||||
Some(final_tag)
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-291
@@ -1,6 +1,6 @@
|
|||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
|
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -346,12 +346,6 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.general.tg_connect == 0 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"general.tg_connect must be > 0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.general.upstream_unhealthy_fail_threshold == 0 {
|
if config.general.upstream_unhealthy_fail_threshold == 0 {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.upstream_unhealthy_fail_threshold must be > 0".to_string(),
|
"general.upstream_unhealthy_fail_threshold must be > 0".to_string(),
|
||||||
@@ -405,18 +399,11 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.censorship.mask_shape_above_cap_blur && !config.censorship.mask_shape_hardening {
|
if config.censorship.mask_shape_above_cap_blur
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.censorship.mask_shape_hardening_aggressive_mode
|
|
||||||
&& !config.censorship.mask_shape_hardening
|
&& !config.censorship.mask_shape_hardening
|
||||||
{
|
{
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"censorship.mask_shape_hardening_aggressive_mode requires censorship.mask_shape_hardening = true"
|
"censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -432,25 +419,8 @@ impl ProxyConfig {
|
|||||||
|
|
||||||
if config.censorship.mask_shape_above_cap_blur_max_bytes > 1_048_576 {
|
if config.censorship.mask_shape_above_cap_blur_max_bytes > 1_048_576 {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"censorship.mask_shape_above_cap_blur_max_bytes must be <= 1048576".to_string(),
|
"censorship.mask_shape_above_cap_blur_max_bytes must be <= 1048576"
|
||||||
));
|
.to_string(),
|
||||||
}
|
|
||||||
|
|
||||||
if config.censorship.mask_relay_max_bytes == 0 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.mask_relay_max_bytes must be > 0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.censorship.mask_relay_max_bytes > 67_108_864 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.mask_relay_max_bytes must be <= 67108864".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(5..=50).contains(&config.censorship.mask_classifier_prefetch_timeout_ms) {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]".to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +444,8 @@ impl ProxyConfig {
|
|||||||
|
|
||||||
if config.censorship.mask_timing_normalization_ceiling_ms > 60_000 {
|
if config.censorship.mask_timing_normalization_ceiling_ms > 60_000 {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"censorship.mask_timing_normalization_ceiling_ms must be <= 60000".to_string(),
|
"censorship.mask_timing_normalization_ceiling_ms must be <= 60000"
|
||||||
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +461,8 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.timeouts.relay_client_idle_hard_secs < config.timeouts.relay_client_idle_soft_secs
|
if config.timeouts.relay_client_idle_hard_secs
|
||||||
|
< config.timeouts.relay_client_idle_soft_secs
|
||||||
{
|
{
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"timeouts.relay_client_idle_hard_secs must be >= timeouts.relay_client_idle_soft_secs"
|
"timeouts.relay_client_idle_hard_secs must be >= timeouts.relay_client_idle_soft_secs"
|
||||||
@@ -498,9 +470,7 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config
|
if config.timeouts.relay_idle_grace_after_downstream_activity_secs
|
||||||
.timeouts
|
|
||||||
.relay_idle_grace_after_downstream_activity_secs
|
|
||||||
> config.timeouts.relay_client_idle_hard_secs
|
> config.timeouts.relay_client_idle_hard_secs
|
||||||
{
|
{
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
@@ -557,21 +527,6 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.general.me_quota_soft_overshoot_bytes > 16 * 1024 * 1024 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"general.me_quota_soft_overshoot_bytes must be within [0, 16777216]".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(4096..=16 * 1024 * 1024)
|
|
||||||
.contains(&config.general.me_d2c_frame_buf_shrink_threshold_bytes)
|
|
||||||
{
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"general.me_d2c_frame_buf_shrink_threshold_bytes must be within [4096, 16777216]"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(4096..=1024 * 1024).contains(&config.general.direct_relay_copy_buf_c2s_bytes) {
|
if !(4096..=1024 * 1024).contains(&config.general.direct_relay_copy_buf_c2s_bytes) {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]"
|
"general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]"
|
||||||
@@ -812,8 +767,7 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
if config.general.me_route_backpressure_base_timeout_ms > 5000 {
|
if config.general.me_route_backpressure_base_timeout_ms > 5000 {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.me_route_backpressure_base_timeout_ms must be within [1, 5000]"
|
"general.me_route_backpressure_base_timeout_ms must be within [1, 5000]".to_string(),
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,8 +780,7 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
if config.general.me_route_backpressure_high_timeout_ms > 5000 {
|
if config.general.me_route_backpressure_high_timeout_ms > 5000 {
|
||||||
return Err(ProxyError::Config(
|
return Err(ProxyError::Config(
|
||||||
"general.me_route_backpressure_high_timeout_ms must be within [1, 5000]"
|
"general.me_route_backpressure_high_timeout_ms must be within [1, 5000]".to_string(),
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,28 +936,6 @@ impl ProxyConfig {
|
|||||||
// Normalize optional TLS fetch scope: whitespace-only values disable scoped routing.
|
// Normalize optional TLS fetch scope: whitespace-only values disable scoped routing.
|
||||||
config.censorship.tls_fetch_scope = config.censorship.tls_fetch_scope.trim().to_string();
|
config.censorship.tls_fetch_scope = config.censorship.tls_fetch_scope.trim().to_string();
|
||||||
|
|
||||||
if config.censorship.tls_fetch.profiles.is_empty() {
|
|
||||||
config.censorship.tls_fetch.profiles = TlsFetchConfig::default().profiles;
|
|
||||||
} else {
|
|
||||||
let mut seen = HashSet::new();
|
|
||||||
config
|
|
||||||
.censorship
|
|
||||||
.tls_fetch
|
|
||||||
.profiles
|
|
||||||
.retain(|profile| seen.insert(*profile));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.censorship.tls_fetch.attempt_timeout_ms == 0 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.tls_fetch.attempt_timeout_ms must be > 0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.censorship.tls_fetch.total_budget_ms == 0 {
|
|
||||||
return Err(ProxyError::Config(
|
|
||||||
"censorship.tls_fetch.total_budget_ms must be > 0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge primary + extra TLS domains, deduplicate (primary always first).
|
// Merge primary + extra TLS domains, deduplicate (primary always first).
|
||||||
if !config.censorship.tls_domains.is_empty() {
|
if !config.censorship.tls_domains.is_empty() {
|
||||||
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
let mut all = Vec::with_capacity(1 + config.censorship.tls_domains.len());
|
||||||
@@ -1182,10 +1113,6 @@ mod load_security_tests;
|
|||||||
#[path = "tests/load_mask_shape_security_tests.rs"]
|
#[path = "tests/load_mask_shape_security_tests.rs"]
|
||||||
mod load_mask_shape_security_tests;
|
mod load_mask_shape_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/load_mask_classifier_prefetch_timeout_security_tests.rs"]
|
|
||||||
mod load_mask_classifier_prefetch_timeout_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1223,7 +1150,6 @@ mod tests {
|
|||||||
default_me_init_retry_attempts()
|
default_me_init_retry_attempts()
|
||||||
);
|
);
|
||||||
assert_eq!(cfg.general.me2dc_fallback, default_me2dc_fallback());
|
assert_eq!(cfg.general.me2dc_fallback, default_me2dc_fallback());
|
||||||
assert_eq!(cfg.general.me2dc_fast, default_me2dc_fast());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.general.proxy_config_v4_cache_path,
|
cfg.general.proxy_config_v4_cache_path,
|
||||||
default_proxy_config_v4_cache_path()
|
default_proxy_config_v4_cache_path()
|
||||||
@@ -1292,11 +1218,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!(
|
||||||
@@ -1328,10 +1249,6 @@ mod tests {
|
|||||||
default_api_runtime_edge_events_capacity()
|
default_api_runtime_edge_events_capacity()
|
||||||
);
|
);
|
||||||
assert_eq!(cfg.access.users, default_access_users());
|
assert_eq!(cfg.access.users, default_access_users());
|
||||||
assert_eq!(
|
|
||||||
cfg.access.user_max_tcp_conns_global_each,
|
|
||||||
default_user_max_tcp_conns_global_each()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.access.user_max_unique_ips_mode,
|
cfg.access.user_max_unique_ips_mode,
|
||||||
UserMaxUniqueIpsMode::default()
|
UserMaxUniqueIpsMode::default()
|
||||||
@@ -1367,7 +1284,6 @@ mod tests {
|
|||||||
default_me_init_retry_attempts()
|
default_me_init_retry_attempts()
|
||||||
);
|
);
|
||||||
assert_eq!(general.me2dc_fallback, default_me2dc_fallback());
|
assert_eq!(general.me2dc_fallback, default_me2dc_fallback());
|
||||||
assert_eq!(general.me2dc_fast, default_me2dc_fast());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
general.proxy_config_v4_cache_path,
|
general.proxy_config_v4_cache_path,
|
||||||
default_proxy_config_v4_cache_path()
|
default_proxy_config_v4_cache_path()
|
||||||
@@ -1434,14 +1350,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!(
|
||||||
@@ -1475,79 +1383,6 @@ mod tests {
|
|||||||
|
|
||||||
let access = AccessConfig::default();
|
let access = AccessConfig::default();
|
||||||
assert_eq!(access.users, default_access_users());
|
assert_eq!(access.users, default_access_users());
|
||||||
assert_eq!(
|
|
||||||
access.user_max_tcp_conns_global_each,
|
|
||||||
default_user_max_tcp_conns_global_each()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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]
|
||||||
@@ -1921,26 +1756,6 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tg_connect_zero_is_rejected() {
|
|
||||||
let toml = r#"
|
|
||||||
[general]
|
|
||||||
tg_connect = 0
|
|
||||||
|
|
||||||
[censorship]
|
|
||||||
tls_domain = "example.com"
|
|
||||||
|
|
||||||
[access.users]
|
|
||||||
user = "00000000000000000000000000000000"
|
|
||||||
"#;
|
|
||||||
let dir = std::env::temp_dir();
|
|
||||||
let path = dir.join("telemt_tg_connect_zero_test.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
|
||||||
assert!(err.contains("general.tg_connect must be > 0"));
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rpc_proxy_req_every_out_of_range_is_rejected() {
|
fn rpc_proxy_req_every_out_of_range_is_rejected() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
@@ -2013,9 +1828,7 @@ mod tests {
|
|||||||
let path = dir.join("telemt_me_route_backpressure_base_timeout_ms_out_of_range_test.toml");
|
let path = dir.join("telemt_me_route_backpressure_base_timeout_ms_out_of_range_test.toml");
|
||||||
std::fs::write(&path, toml).unwrap();
|
std::fs::write(&path, toml).unwrap();
|
||||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
assert!(
|
assert!(err.contains("general.me_route_backpressure_base_timeout_ms must be within [1, 5000]"));
|
||||||
err.contains("general.me_route_backpressure_base_timeout_ms must be within [1, 5000]")
|
|
||||||
);
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2036,9 +1849,7 @@ mod tests {
|
|||||||
let path = dir.join("telemt_me_route_backpressure_high_timeout_ms_out_of_range_test.toml");
|
let path = dir.join("telemt_me_route_backpressure_high_timeout_ms_out_of_range_test.toml");
|
||||||
std::fs::write(&path, toml).unwrap();
|
std::fs::write(&path, toml).unwrap();
|
||||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
assert!(
|
assert!(err.contains("general.me_route_backpressure_high_timeout_ms must be within [1, 5000]"));
|
||||||
err.contains("general.me_route_backpressure_high_timeout_ms must be within [1, 5000]")
|
|
||||||
);
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2517,94 +2328,6 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_fetch_defaults_are_applied() {
|
|
||||||
let toml = r#"
|
|
||||||
[censorship]
|
|
||||||
tls_domain = "example.com"
|
|
||||||
|
|
||||||
[access.users]
|
|
||||||
user = "00000000000000000000000000000000"
|
|
||||||
"#;
|
|
||||||
let dir = std::env::temp_dir();
|
|
||||||
let path = dir.join("telemt_tls_fetch_defaults_test.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let cfg = ProxyConfig::load(&path).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
cfg.censorship.tls_fetch.profiles,
|
|
||||||
TlsFetchConfig::default().profiles
|
|
||||||
);
|
|
||||||
assert!(cfg.censorship.tls_fetch.strict_route);
|
|
||||||
assert_eq!(cfg.censorship.tls_fetch.attempt_timeout_ms, 5_000);
|
|
||||||
assert_eq!(cfg.censorship.tls_fetch.total_budget_ms, 15_000);
|
|
||||||
assert_eq!(cfg.censorship.tls_fetch.profile_cache_ttl_secs, 600);
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_fetch_profiles_are_deduplicated_preserving_order() {
|
|
||||||
let toml = r#"
|
|
||||||
[censorship]
|
|
||||||
tls_domain = "example.com"
|
|
||||||
[censorship.tls_fetch]
|
|
||||||
profiles = ["compat_tls12", "modern_chrome_like", "compat_tls12", "legacy_minimal"]
|
|
||||||
|
|
||||||
[access.users]
|
|
||||||
user = "00000000000000000000000000000000"
|
|
||||||
"#;
|
|
||||||
let dir = std::env::temp_dir();
|
|
||||||
let path = dir.join("telemt_tls_fetch_profiles_dedup_test.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let cfg = ProxyConfig::load(&path).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
cfg.censorship.tls_fetch.profiles,
|
|
||||||
vec![
|
|
||||||
TlsFetchProfile::CompatTls12,
|
|
||||||
TlsFetchProfile::ModernChromeLike,
|
|
||||||
TlsFetchProfile::LegacyMinimal
|
|
||||||
]
|
|
||||||
);
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_fetch_attempt_timeout_zero_is_rejected() {
|
|
||||||
let toml = r#"
|
|
||||||
[censorship]
|
|
||||||
tls_domain = "example.com"
|
|
||||||
[censorship.tls_fetch]
|
|
||||||
attempt_timeout_ms = 0
|
|
||||||
|
|
||||||
[access.users]
|
|
||||||
user = "00000000000000000000000000000000"
|
|
||||||
"#;
|
|
||||||
let dir = std::env::temp_dir();
|
|
||||||
let path = dir.join("telemt_tls_fetch_attempt_timeout_zero_test.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
|
||||||
assert!(err.contains("censorship.tls_fetch.attempt_timeout_ms must be > 0"));
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tls_fetch_total_budget_zero_is_rejected() {
|
|
||||||
let toml = r#"
|
|
||||||
[censorship]
|
|
||||||
tls_domain = "example.com"
|
|
||||||
[censorship.tls_fetch]
|
|
||||||
total_budget_ms = 0
|
|
||||||
|
|
||||||
[access.users]
|
|
||||||
user = "00000000000000000000000000000000"
|
|
||||||
"#;
|
|
||||||
let dir = std::env::temp_dir();
|
|
||||||
let path = dir.join("telemt_tls_fetch_total_budget_zero_test.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
|
||||||
assert!(err.contains("censorship.tls_fetch.total_budget_ms must be > 0"));
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_ad_tag_is_disabled_during_load() {
|
fn invalid_ad_tag_is_disabled_during_load() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
//! Configuration.
|
//! Configuration.
|
||||||
|
|
||||||
pub(crate) mod defaults;
|
pub(crate) mod defaults;
|
||||||
pub mod hot_reload;
|
|
||||||
mod load;
|
|
||||||
mod types;
|
mod types;
|
||||||
|
mod load;
|
||||||
|
pub mod hot_reload;
|
||||||
|
|
||||||
pub use load::ProxyConfig;
|
pub use load::ProxyConfig;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -17,28 +17,6 @@ fn remove_temp_config(path: &PathBuf) {
|
|||||||
let _ = fs::remove_file(path);
|
let _ = fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn default_timeouts_enable_apple_compatible_handshake_profile() {
|
|
||||||
let cfg = ProxyConfig::default();
|
|
||||||
assert_eq!(cfg.timeouts.client_first_byte_idle_secs, 300);
|
|
||||||
assert_eq!(cfg.timeouts.client_handshake, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_accepts_zero_first_byte_idle_timeout_as_legacy_opt_out() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[timeouts]
|
|
||||||
client_first_byte_idle_secs = 0
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg = ProxyConfig::load(&path).expect("config with zero first-byte idle timeout must load");
|
|
||||||
assert_eq!(cfg.timeouts.client_first_byte_idle_secs, 0);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_rejects_relay_hard_idle_smaller_than_soft_idle_with_clear_error() {
|
fn load_rejects_relay_hard_idle_smaller_than_soft_idle_with_clear_error() {
|
||||||
let path = write_temp_config(
|
let path = write_temp_config(
|
||||||
@@ -52,9 +30,7 @@ relay_client_idle_hard_secs = 60
|
|||||||
let err = ProxyConfig::load(&path).expect_err("config with hard<soft must fail");
|
let err = ProxyConfig::load(&path).expect_err("config with hard<soft must fail");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains(
|
msg.contains("timeouts.relay_client_idle_hard_secs must be >= timeouts.relay_client_idle_soft_secs"),
|
||||||
"timeouts.relay_client_idle_hard_secs must be >= timeouts.relay_client_idle_soft_secs"
|
|
||||||
),
|
|
||||||
"error must explain the violated hard>=soft invariant, got: {msg}"
|
"error must explain the violated hard>=soft invariant, got: {msg}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
fn write_temp_config(contents: &str) -> PathBuf {
|
|
||||||
let nonce = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.expect("system time must be after unix epoch")
|
|
||||||
.as_nanos();
|
|
||||||
let path = std::env::temp_dir().join(format!(
|
|
||||||
"telemt-load-mask-prefetch-timeout-security-{nonce}.toml"
|
|
||||||
));
|
|
||||||
fs::write(&path, contents).expect("temp config write must succeed");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_temp_config(path: &PathBuf) {
|
|
||||||
let _ = fs::remove_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_rejects_mask_classifier_prefetch_timeout_below_min_bound() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_classifier_prefetch_timeout_ms = 4
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let err = ProxyConfig::load(&path)
|
|
||||||
.expect_err("prefetch timeout below minimum security bound must be rejected");
|
|
||||||
let msg = err.to_string();
|
|
||||||
assert!(
|
|
||||||
msg.contains("censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]"),
|
|
||||||
"error must explain timeout bound invariant, got: {msg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_rejects_mask_classifier_prefetch_timeout_above_max_bound() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_classifier_prefetch_timeout_ms = 51
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let err = ProxyConfig::load(&path)
|
|
||||||
.expect_err("prefetch timeout above max security bound must be rejected");
|
|
||||||
let msg = err.to_string();
|
|
||||||
assert!(
|
|
||||||
msg.contains("censorship.mask_classifier_prefetch_timeout_ms must be within [5, 50]"),
|
|
||||||
"error must explain timeout bound invariant, got: {msg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_accepts_mask_classifier_prefetch_timeout_within_bounds() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_classifier_prefetch_timeout_ms = 20
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg =
|
|
||||||
ProxyConfig::load(&path).expect("prefetch timeout within security bounds must be accepted");
|
|
||||||
assert_eq!(cfg.censorship.mask_classifier_prefetch_timeout_ms, 20);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
@@ -91,13 +91,11 @@ mask_shape_above_cap_blur_max_bytes = 64
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("above-cap blur must require shape hardening enabled");
|
.expect_err("above-cap blur must require shape hardening enabled");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains(
|
msg.contains("censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"),
|
||||||
"censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"
|
|
||||||
),
|
|
||||||
"error must explain blur prerequisite, got: {msg}"
|
"error must explain blur prerequisite, got: {msg}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,8 +113,8 @@ mask_shape_above_cap_blur_max_bytes = 0
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("above-cap blur max bytes must be > 0 when enabled");
|
.expect_err("above-cap blur max bytes must be > 0 when enabled");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("censorship.mask_shape_above_cap_blur_max_bytes must be > 0 when censorship.mask_shape_above_cap_blur is enabled"),
|
msg.contains("censorship.mask_shape_above_cap_blur_max_bytes must be > 0 when censorship.mask_shape_above_cap_blur is enabled"),
|
||||||
@@ -137,8 +135,8 @@ mask_timing_normalization_ceiling_ms = 200
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("timing normalization floor must be > 0 when enabled");
|
.expect_err("timing normalization floor must be > 0 when enabled");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("censorship.mask_timing_normalization_floor_ms must be > 0 when censorship.mask_timing_normalization_enabled is true"),
|
msg.contains("censorship.mask_timing_normalization_floor_ms must be > 0 when censorship.mask_timing_normalization_enabled is true"),
|
||||||
@@ -159,7 +157,8 @@ mask_timing_normalization_ceiling_ms = 200
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err = ProxyConfig::load(&path).expect_err("timing normalization ceiling must be >= floor");
|
let err = ProxyConfig::load(&path)
|
||||||
|
.expect_err("timing normalization ceiling must be >= floor");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains("censorship.mask_timing_normalization_ceiling_ms must be >= censorship.mask_timing_normalization_floor_ms"),
|
msg.contains("censorship.mask_timing_normalization_ceiling_ms must be >= censorship.mask_timing_normalization_floor_ms"),
|
||||||
@@ -194,99 +193,3 @@ mask_timing_normalization_ceiling_ms = 240
|
|||||||
|
|
||||||
remove_temp_config(&path);
|
remove_temp_config(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_rejects_aggressive_shape_mode_when_shape_hardening_disabled() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_shape_hardening = false
|
|
||||||
mask_shape_hardening_aggressive_mode = true
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let err = ProxyConfig::load(&path)
|
|
||||||
.expect_err("aggressive shape hardening mode must require shape hardening enabled");
|
|
||||||
let msg = err.to_string();
|
|
||||||
assert!(
|
|
||||||
msg.contains("censorship.mask_shape_hardening_aggressive_mode requires censorship.mask_shape_hardening = true"),
|
|
||||||
"error must explain aggressive-mode prerequisite, got: {msg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_accepts_aggressive_shape_mode_when_shape_hardening_enabled() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_shape_hardening = true
|
|
||||||
mask_shape_hardening_aggressive_mode = true
|
|
||||||
mask_shape_above_cap_blur = true
|
|
||||||
mask_shape_above_cap_blur_max_bytes = 8
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg = ProxyConfig::load(&path)
|
|
||||||
.expect("aggressive shape hardening mode should be accepted when prerequisites are met");
|
|
||||||
assert!(cfg.censorship.mask_shape_hardening);
|
|
||||||
assert!(cfg.censorship.mask_shape_hardening_aggressive_mode);
|
|
||||||
assert!(cfg.censorship.mask_shape_above_cap_blur);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_rejects_zero_mask_relay_max_bytes() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_relay_max_bytes = 0
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let err = ProxyConfig::load(&path).expect_err("mask_relay_max_bytes must be > 0");
|
|
||||||
let msg = err.to_string();
|
|
||||||
assert!(
|
|
||||||
msg.contains("censorship.mask_relay_max_bytes must be > 0"),
|
|
||||||
"error must explain non-zero relay cap invariant, got: {msg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_rejects_mask_relay_max_bytes_above_upper_bound() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_relay_max_bytes = 67108865
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let err =
|
|
||||||
ProxyConfig::load(&path).expect_err("mask_relay_max_bytes above hard cap must be rejected");
|
|
||||||
let msg = err.to_string();
|
|
||||||
assert!(
|
|
||||||
msg.contains("censorship.mask_relay_max_bytes must be <= 67108864"),
|
|
||||||
"error must explain relay cap upper bound invariant, got: {msg}"
|
|
||||||
);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_accepts_valid_mask_relay_max_bytes() {
|
|
||||||
let path = write_temp_config(
|
|
||||||
r#"
|
|
||||||
[censorship]
|
|
||||||
mask_relay_max_bytes = 8388608
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg = ProxyConfig::load(&path).expect("valid mask_relay_max_bytes must be accepted");
|
|
||||||
assert_eq!(cfg.censorship.mask_relay_max_bytes, 8_388_608);
|
|
||||||
|
|
||||||
remove_temp_config(&path);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,13 +29,11 @@ server_hello_delay_max_ms = 1000
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("delay equal to handshake timeout must be rejected");
|
.expect_err("delay equal to handshake timeout must be rejected");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains(
|
msg.contains("censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"),
|
||||||
"censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"
|
|
||||||
),
|
|
||||||
"error must explain delay<timeout invariant, got: {msg}"
|
"error must explain delay<timeout invariant, got: {msg}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,13 +52,11 @@ server_hello_delay_max_ms = 1500
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let err =
|
let err = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect_err("delay larger than handshake timeout must be rejected");
|
.expect_err("delay larger than handshake timeout must be rejected");
|
||||||
let msg = err.to_string();
|
let msg = err.to_string();
|
||||||
assert!(
|
assert!(
|
||||||
msg.contains(
|
msg.contains("censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"),
|
||||||
"censorship.server_hello_delay_max_ms must be < timeouts.client_handshake * 1000"
|
|
||||||
),
|
|
||||||
"error must explain delay<timeout invariant, got: {msg}"
|
"error must explain delay<timeout invariant, got: {msg}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -79,8 +75,8 @@ server_hello_delay_max_ms = 999
|
|||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cfg =
|
let cfg = ProxyConfig::load(&path)
|
||||||
ProxyConfig::load(&path).expect("delay below handshake timeout budget must be accepted");
|
.expect("delay below handshake timeout budget must be accepted");
|
||||||
assert_eq!(cfg.timeouts.client_handshake, 1);
|
assert_eq!(cfg.timeouts.client_handshake, 1);
|
||||||
assert_eq!(cfg.censorship.server_hello_delay_max_ms, 999);
|
assert_eq!(cfg.censorship.server_hello_delay_max_ms, 999);
|
||||||
|
|
||||||
|
|||||||
+11
-160
@@ -429,11 +429,6 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me2dc_fallback")]
|
#[serde(default = "default_me2dc_fallback")]
|
||||||
pub me2dc_fallback: bool,
|
pub me2dc_fallback: bool,
|
||||||
|
|
||||||
/// Fast ME->Direct fallback mode for new sessions.
|
|
||||||
/// Active only when both `use_middle_proxy=true` and `me2dc_fallback=true`.
|
|
||||||
#[serde(default = "default_me2dc_fast")]
|
|
||||||
pub me2dc_fast: bool,
|
|
||||||
|
|
||||||
/// Enable ME keepalive padding frames.
|
/// Enable ME keepalive padding frames.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub me_keepalive_enabled: bool,
|
pub me_keepalive_enabled: bool,
|
||||||
@@ -473,7 +468,7 @@ pub struct GeneralConfig {
|
|||||||
pub me_c2me_send_timeout_ms: u64,
|
pub me_c2me_send_timeout_ms: u64,
|
||||||
|
|
||||||
/// Bounded wait in milliseconds for routing ME DATA to per-connection queue.
|
/// Bounded wait in milliseconds for routing ME DATA to per-connection queue.
|
||||||
/// `0` keeps non-blocking routing; values >0 enable bounded wait for compatibility.
|
/// `0` keeps legacy no-wait behavior.
|
||||||
#[serde(default = "default_me_reader_route_data_wait_ms")]
|
#[serde(default = "default_me_reader_route_data_wait_ms")]
|
||||||
pub me_reader_route_data_wait_ms: u64,
|
pub me_reader_route_data_wait_ms: u64,
|
||||||
|
|
||||||
@@ -494,14 +489,6 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_me_d2c_ack_flush_immediate")]
|
#[serde(default = "default_me_d2c_ack_flush_immediate")]
|
||||||
pub me_d2c_ack_flush_immediate: bool,
|
pub me_d2c_ack_flush_immediate: bool,
|
||||||
|
|
||||||
/// Additional bytes above strict per-user quota allowed in hot-path soft mode.
|
|
||||||
#[serde(default = "default_me_quota_soft_overshoot_bytes")]
|
|
||||||
pub me_quota_soft_overshoot_bytes: u64,
|
|
||||||
|
|
||||||
/// Shrink threshold for reusable ME->Client frame assembly buffer.
|
|
||||||
#[serde(default = "default_me_d2c_frame_buf_shrink_threshold_bytes")]
|
|
||||||
pub me_d2c_frame_buf_shrink_threshold_bytes: usize,
|
|
||||||
|
|
||||||
/// Copy buffer size for client->DC direction in direct relay.
|
/// Copy buffer size for client->DC direction in direct relay.
|
||||||
#[serde(default = "default_direct_relay_copy_buf_c2s_bytes")]
|
#[serde(default = "default_direct_relay_copy_buf_c2s_bytes")]
|
||||||
pub direct_relay_copy_buf_c2s_bytes: usize,
|
pub direct_relay_copy_buf_c2s_bytes: usize,
|
||||||
@@ -663,10 +650,6 @@ pub struct GeneralConfig {
|
|||||||
#[serde(default = "default_upstream_connect_budget_ms")]
|
#[serde(default = "default_upstream_connect_budget_ms")]
|
||||||
pub upstream_connect_budget_ms: u64,
|
pub upstream_connect_budget_ms: u64,
|
||||||
|
|
||||||
/// Per-attempt TCP connect timeout to Telegram DC (seconds).
|
|
||||||
#[serde(default = "default_connect_timeout")]
|
|
||||||
pub tg_connect: u64,
|
|
||||||
|
|
||||||
/// Consecutive failed requests before upstream is marked unhealthy.
|
/// Consecutive failed requests before upstream is marked unhealthy.
|
||||||
#[serde(default = "default_upstream_unhealthy_fail_threshold")]
|
#[serde(default = "default_upstream_unhealthy_fail_threshold")]
|
||||||
pub upstream_unhealthy_fail_threshold: u32,
|
pub upstream_unhealthy_fail_threshold: u32,
|
||||||
@@ -948,7 +931,6 @@ impl Default for GeneralConfig {
|
|||||||
middle_proxy_warm_standby: default_middle_proxy_warm_standby(),
|
middle_proxy_warm_standby: default_middle_proxy_warm_standby(),
|
||||||
me_init_retry_attempts: default_me_init_retry_attempts(),
|
me_init_retry_attempts: default_me_init_retry_attempts(),
|
||||||
me2dc_fallback: default_me2dc_fallback(),
|
me2dc_fallback: default_me2dc_fallback(),
|
||||||
me2dc_fast: default_me2dc_fast(),
|
|
||||||
me_keepalive_enabled: default_true(),
|
me_keepalive_enabled: default_true(),
|
||||||
me_keepalive_interval_secs: default_keepalive_interval(),
|
me_keepalive_interval_secs: default_keepalive_interval(),
|
||||||
me_keepalive_jitter_secs: default_keepalive_jitter(),
|
me_keepalive_jitter_secs: default_keepalive_jitter(),
|
||||||
@@ -963,9 +945,6 @@ impl Default for GeneralConfig {
|
|||||||
me_d2c_flush_batch_max_bytes: default_me_d2c_flush_batch_max_bytes(),
|
me_d2c_flush_batch_max_bytes: default_me_d2c_flush_batch_max_bytes(),
|
||||||
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_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(),
|
||||||
@@ -1011,7 +990,6 @@ impl Default for GeneralConfig {
|
|||||||
upstream_connect_retry_attempts: default_upstream_connect_retry_attempts(),
|
upstream_connect_retry_attempts: default_upstream_connect_retry_attempts(),
|
||||||
upstream_connect_retry_backoff_ms: default_upstream_connect_retry_backoff_ms(),
|
upstream_connect_retry_backoff_ms: default_upstream_connect_retry_backoff_ms(),
|
||||||
upstream_connect_budget_ms: default_upstream_connect_budget_ms(),
|
upstream_connect_budget_ms: default_upstream_connect_budget_ms(),
|
||||||
tg_connect: default_connect_timeout(),
|
|
||||||
upstream_unhealthy_fail_threshold: default_upstream_unhealthy_fail_threshold(),
|
upstream_unhealthy_fail_threshold: default_upstream_unhealthy_fail_threshold(),
|
||||||
upstream_connect_failfast_hard_errors: default_upstream_connect_failfast_hard_errors(),
|
upstream_connect_failfast_hard_errors: default_upstream_connect_failfast_hard_errors(),
|
||||||
stun_iface_mismatch_ignore: false,
|
stun_iface_mismatch_ignore: false,
|
||||||
@@ -1069,7 +1047,8 @@ impl Default for GeneralConfig {
|
|||||||
me_pool_drain_soft_evict_per_writer: default_me_pool_drain_soft_evict_per_writer(),
|
me_pool_drain_soft_evict_per_writer: default_me_pool_drain_soft_evict_per_writer(),
|
||||||
me_pool_drain_soft_evict_budget_per_core:
|
me_pool_drain_soft_evict_budget_per_core:
|
||||||
default_me_pool_drain_soft_evict_budget_per_core(),
|
default_me_pool_drain_soft_evict_budget_per_core(),
|
||||||
me_pool_drain_soft_evict_cooldown_ms: default_me_pool_drain_soft_evict_cooldown_ms(),
|
me_pool_drain_soft_evict_cooldown_ms:
|
||||||
|
default_me_pool_drain_soft_evict_cooldown_ms(),
|
||||||
me_bind_stale_mode: MeBindStaleMode::default(),
|
me_bind_stale_mode: MeBindStaleMode::default(),
|
||||||
me_bind_stale_ttl_secs: default_me_bind_stale_ttl_secs(),
|
me_bind_stale_ttl_secs: default_me_bind_stale_ttl_secs(),
|
||||||
me_pool_min_fresh_ratio: default_me_pool_min_fresh_ratio(),
|
me_pool_min_fresh_ratio: default_me_pool_min_fresh_ratio(),
|
||||||
@@ -1251,10 +1230,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.
|
||||||
@@ -1277,11 +1255,6 @@ pub struct ServerConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub listeners: Vec<ListenerConfig>,
|
pub listeners: Vec<ListenerConfig>,
|
||||||
|
|
||||||
/// TCP `listen(2)` backlog for client-facing sockets (also used for the metrics HTTP listener).
|
|
||||||
/// The effective queue is capped by the kernel (for example `somaxconn` on Linux).
|
|
||||||
#[serde(default = "default_listen_backlog")]
|
|
||||||
pub listen_backlog: u32,
|
|
||||||
|
|
||||||
/// Maximum number of concurrent client connections.
|
/// Maximum number of concurrent client connections.
|
||||||
/// 0 means unlimited.
|
/// 0 means unlimited.
|
||||||
#[serde(default = "default_server_max_connections")]
|
#[serde(default = "default_server_max_connections")]
|
||||||
@@ -1304,13 +1277,12 @@ 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(),
|
||||||
api: ApiConfig::default(),
|
api: ApiConfig::default(),
|
||||||
listeners: Vec::new(),
|
listeners: Vec::new(),
|
||||||
listen_backlog: default_listen_backlog(),
|
|
||||||
max_connections: default_server_max_connections(),
|
max_connections: default_server_max_connections(),
|
||||||
accept_permit_timeout_ms: default_accept_permit_timeout_ms(),
|
accept_permit_timeout_ms: default_accept_permit_timeout_ms(),
|
||||||
}
|
}
|
||||||
@@ -1319,12 +1291,6 @@ impl Default for ServerConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TimeoutsConfig {
|
pub struct TimeoutsConfig {
|
||||||
/// Maximum idle wait in seconds for the first client byte before handshake parsing starts.
|
|
||||||
/// `0` disables the separate idle phase and keeps legacy timeout behavior.
|
|
||||||
#[serde(default = "default_client_first_byte_idle_secs")]
|
|
||||||
pub client_first_byte_idle_secs: u64,
|
|
||||||
|
|
||||||
/// Maximum active handshake duration in seconds after the first client byte is received.
|
|
||||||
#[serde(default = "default_handshake_timeout")]
|
#[serde(default = "default_handshake_timeout")]
|
||||||
pub client_handshake: u64,
|
pub client_handshake: u64,
|
||||||
|
|
||||||
@@ -1346,6 +1312,9 @@ pub struct TimeoutsConfig {
|
|||||||
#[serde(default = "default_relay_idle_grace_after_downstream_activity_secs")]
|
#[serde(default = "default_relay_idle_grace_after_downstream_activity_secs")]
|
||||||
pub relay_idle_grace_after_downstream_activity_secs: u64,
|
pub relay_idle_grace_after_downstream_activity_secs: u64,
|
||||||
|
|
||||||
|
#[serde(default = "default_connect_timeout")]
|
||||||
|
pub tg_connect: u64,
|
||||||
|
|
||||||
#[serde(default = "default_keepalive")]
|
#[serde(default = "default_keepalive")]
|
||||||
pub client_keepalive: u64,
|
pub client_keepalive: u64,
|
||||||
|
|
||||||
@@ -1364,13 +1333,13 @@ pub struct TimeoutsConfig {
|
|||||||
impl Default for TimeoutsConfig {
|
impl Default for TimeoutsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
client_first_byte_idle_secs: default_client_first_byte_idle_secs(),
|
|
||||||
client_handshake: default_handshake_timeout(),
|
client_handshake: default_handshake_timeout(),
|
||||||
relay_idle_policy_v2_enabled: default_relay_idle_policy_v2_enabled(),
|
relay_idle_policy_v2_enabled: default_relay_idle_policy_v2_enabled(),
|
||||||
relay_client_idle_soft_secs: default_relay_client_idle_soft_secs(),
|
relay_client_idle_soft_secs: default_relay_client_idle_soft_secs(),
|
||||||
relay_client_idle_hard_secs: default_relay_client_idle_hard_secs(),
|
relay_client_idle_hard_secs: default_relay_client_idle_hard_secs(),
|
||||||
relay_idle_grace_after_downstream_activity_secs:
|
relay_idle_grace_after_downstream_activity_secs:
|
||||||
default_relay_idle_grace_after_downstream_activity_secs(),
|
default_relay_idle_grace_after_downstream_activity_secs(),
|
||||||
|
tg_connect: default_connect_timeout(),
|
||||||
client_keepalive: default_keepalive(),
|
client_keepalive: default_keepalive(),
|
||||||
client_ack: default_ack_timeout(),
|
client_ack: default_ack_timeout(),
|
||||||
me_one_retry: default_me_one_retry(),
|
me_one_retry: default_me_one_retry(),
|
||||||
@@ -1379,90 +1348,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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum TlsFetchProfile {
|
|
||||||
ModernChromeLike,
|
|
||||||
ModernFirefoxLike,
|
|
||||||
CompatTls12,
|
|
||||||
LegacyMinimal,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TlsFetchProfile {
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
TlsFetchProfile::ModernChromeLike => "modern_chrome_like",
|
|
||||||
TlsFetchProfile::ModernFirefoxLike => "modern_firefox_like",
|
|
||||||
TlsFetchProfile::CompatTls12 => "compat_tls12",
|
|
||||||
TlsFetchProfile::LegacyMinimal => "legacy_minimal",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_tls_fetch_profiles() -> Vec<TlsFetchProfile> {
|
|
||||||
vec![
|
|
||||||
TlsFetchProfile::ModernChromeLike,
|
|
||||||
TlsFetchProfile::ModernFirefoxLike,
|
|
||||||
TlsFetchProfile::CompatTls12,
|
|
||||||
TlsFetchProfile::LegacyMinimal,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TlsFetchConfig {
|
|
||||||
/// Ordered list of ClientHello profiles used for adaptive fallback.
|
|
||||||
#[serde(default = "default_tls_fetch_profiles")]
|
|
||||||
pub profiles: Vec<TlsFetchProfile>,
|
|
||||||
|
|
||||||
/// When true and upstream route is configured, TLS fetch fails closed on
|
|
||||||
/// upstream connect errors and does not fallback to direct TCP.
|
|
||||||
#[serde(default = "default_tls_fetch_strict_route")]
|
|
||||||
pub strict_route: bool,
|
|
||||||
|
|
||||||
/// Timeout per one profile attempt in milliseconds.
|
|
||||||
#[serde(default = "default_tls_fetch_attempt_timeout_ms")]
|
|
||||||
pub attempt_timeout_ms: u64,
|
|
||||||
|
|
||||||
/// Total wall-clock budget in milliseconds across all profile attempts.
|
|
||||||
#[serde(default = "default_tls_fetch_total_budget_ms")]
|
|
||||||
pub total_budget_ms: u64,
|
|
||||||
|
|
||||||
/// Adds GREASE-style values into selected ClientHello extensions.
|
|
||||||
#[serde(default)]
|
|
||||||
pub grease_enabled: bool,
|
|
||||||
|
|
||||||
/// Produces deterministic ClientHello randomness for debugging/tests.
|
|
||||||
#[serde(default)]
|
|
||||||
pub deterministic: bool,
|
|
||||||
|
|
||||||
/// TTL for winner-profile cache entries in seconds.
|
|
||||||
/// Set to 0 to disable profile cache.
|
|
||||||
#[serde(default = "default_tls_fetch_profile_cache_ttl_secs")]
|
|
||||||
pub profile_cache_ttl_secs: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TlsFetchConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
profiles: default_tls_fetch_profiles(),
|
|
||||||
strict_route: default_tls_fetch_strict_route(),
|
|
||||||
attempt_timeout_ms: default_tls_fetch_attempt_timeout_ms(),
|
|
||||||
total_budget_ms: default_tls_fetch_total_budget_ms(),
|
|
||||||
grease_enabled: false,
|
|
||||||
deterministic: false,
|
|
||||||
profile_cache_ttl_secs: default_tls_fetch_profile_cache_ttl_secs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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")]
|
||||||
@@ -1472,19 +1357,11 @@ 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")]
|
||||||
pub tls_fetch_scope: String,
|
pub tls_fetch_scope: String,
|
||||||
|
|
||||||
/// Fetch strategy for TLS front metadata bootstrap and periodic refresh.
|
|
||||||
#[serde(default)]
|
|
||||||
pub tls_fetch: TlsFetchConfig,
|
|
||||||
|
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub mask: bool,
|
pub mask: bool,
|
||||||
|
|
||||||
@@ -1541,12 +1418,6 @@ pub struct AntiCensorshipConfig {
|
|||||||
#[serde(default = "default_mask_shape_hardening")]
|
#[serde(default = "default_mask_shape_hardening")]
|
||||||
pub mask_shape_hardening: bool,
|
pub mask_shape_hardening: bool,
|
||||||
|
|
||||||
/// Opt-in aggressive shape hardening mode.
|
|
||||||
/// When enabled, masking may shape some backend-silent timeout paths and
|
|
||||||
/// enforces strictly positive above-cap blur when blur is enabled.
|
|
||||||
#[serde(default = "default_mask_shape_hardening_aggressive_mode")]
|
|
||||||
pub mask_shape_hardening_aggressive_mode: bool,
|
|
||||||
|
|
||||||
/// Minimum bucket size for mask shape hardening padding.
|
/// Minimum bucket size for mask shape hardening padding.
|
||||||
#[serde(default = "default_mask_shape_bucket_floor_bytes")]
|
#[serde(default = "default_mask_shape_bucket_floor_bytes")]
|
||||||
pub mask_shape_bucket_floor_bytes: usize,
|
pub mask_shape_bucket_floor_bytes: usize,
|
||||||
@@ -1564,14 +1435,6 @@ pub struct AntiCensorshipConfig {
|
|||||||
#[serde(default = "default_mask_shape_above_cap_blur_max_bytes")]
|
#[serde(default = "default_mask_shape_above_cap_blur_max_bytes")]
|
||||||
pub mask_shape_above_cap_blur_max_bytes: usize,
|
pub mask_shape_above_cap_blur_max_bytes: usize,
|
||||||
|
|
||||||
/// Maximum bytes relayed per direction on unauthenticated masking fallback paths.
|
|
||||||
#[serde(default = "default_mask_relay_max_bytes")]
|
|
||||||
pub mask_relay_max_bytes: usize,
|
|
||||||
|
|
||||||
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
|
|
||||||
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
|
|
||||||
pub mask_classifier_prefetch_timeout_ms: u64,
|
|
||||||
|
|
||||||
/// Enable outcome-time normalization envelope for masking fallback.
|
/// Enable outcome-time normalization envelope for masking fallback.
|
||||||
#[serde(default = "default_mask_timing_normalization_enabled")]
|
#[serde(default = "default_mask_timing_normalization_enabled")]
|
||||||
pub mask_timing_normalization_enabled: bool,
|
pub mask_timing_normalization_enabled: bool,
|
||||||
@@ -1590,9 +1453,7 @@ 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(),
|
||||||
tls_fetch: TlsFetchConfig::default(),
|
|
||||||
mask: default_true(),
|
mask: default_true(),
|
||||||
mask_host: None,
|
mask_host: None,
|
||||||
mask_port: default_mask_port(),
|
mask_port: default_mask_port(),
|
||||||
@@ -1607,13 +1468,10 @@ impl Default for AntiCensorshipConfig {
|
|||||||
alpn_enforce: default_alpn_enforce(),
|
alpn_enforce: default_alpn_enforce(),
|
||||||
mask_proxy_protocol: 0,
|
mask_proxy_protocol: 0,
|
||||||
mask_shape_hardening: default_mask_shape_hardening(),
|
mask_shape_hardening: default_mask_shape_hardening(),
|
||||||
mask_shape_hardening_aggressive_mode: default_mask_shape_hardening_aggressive_mode(),
|
|
||||||
mask_shape_bucket_floor_bytes: default_mask_shape_bucket_floor_bytes(),
|
mask_shape_bucket_floor_bytes: default_mask_shape_bucket_floor_bytes(),
|
||||||
mask_shape_bucket_cap_bytes: default_mask_shape_bucket_cap_bytes(),
|
mask_shape_bucket_cap_bytes: default_mask_shape_bucket_cap_bytes(),
|
||||||
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
||||||
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
||||||
mask_relay_max_bytes: default_mask_relay_max_bytes(),
|
|
||||||
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
|
|
||||||
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
||||||
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
||||||
mask_timing_normalization_ceiling_ms: default_mask_timing_normalization_ceiling_ms(),
|
mask_timing_normalization_ceiling_ms: default_mask_timing_normalization_ceiling_ms(),
|
||||||
@@ -1633,12 +1491,6 @@ pub struct AccessConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub user_max_tcp_conns: HashMap<String, usize>,
|
pub user_max_tcp_conns: HashMap<String, usize>,
|
||||||
|
|
||||||
/// Global per-user TCP connection limit applied when a user has no
|
|
||||||
/// positive individual override.
|
|
||||||
/// `0` disables the inherited limit.
|
|
||||||
#[serde(default = "default_user_max_tcp_conns_global_each")]
|
|
||||||
pub user_max_tcp_conns_global_each: usize,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub user_expirations: HashMap<String, DateTime<Utc>>,
|
pub user_expirations: HashMap<String, DateTime<Utc>>,
|
||||||
|
|
||||||
@@ -1675,7 +1527,6 @@ impl Default for AccessConfig {
|
|||||||
users: default_access_users(),
|
users: default_access_users(),
|
||||||
user_ad_tags: HashMap::new(),
|
user_ad_tags: HashMap::new(),
|
||||||
user_max_tcp_conns: HashMap::new(),
|
user_max_tcp_conns: HashMap::new(),
|
||||||
user_max_tcp_conns_global_each: default_user_max_tcp_conns_global_each(),
|
|
||||||
user_expirations: HashMap::new(),
|
user_expirations: HashMap::new(),
|
||||||
user_data_quota: HashMap::new(),
|
user_data_quota: HashMap::new(),
|
||||||
user_max_unique_ips: HashMap::new(),
|
user_max_unique_ips: HashMap::new(),
|
||||||
|
|||||||
+18
-37
@@ -13,13 +13,10 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::error::{ProxyError, Result};
|
|
||||||
use aes::Aes256;
|
use aes::Aes256;
|
||||||
use ctr::{
|
use ctr::{Ctr128BE, cipher::{KeyIvInit, StreamCipher}};
|
||||||
Ctr128BE,
|
|
||||||
cipher::{KeyIvInit, StreamCipher},
|
|
||||||
};
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
use crate::error::{ProxyError, Result};
|
||||||
|
|
||||||
type Aes256Ctr = Ctr128BE<Aes256>;
|
type Aes256Ctr = Ctr128BE<Aes256>;
|
||||||
|
|
||||||
@@ -49,16 +46,10 @@ impl AesCtr {
|
|||||||
/// Create from key and IV slices
|
/// Create from key and IV slices
|
||||||
pub fn from_key_iv(key: &[u8], iv: &[u8]) -> Result<Self> {
|
pub fn from_key_iv(key: &[u8], iv: &[u8]) -> Result<Self> {
|
||||||
if key.len() != 32 {
|
if key.len() != 32 {
|
||||||
return Err(ProxyError::InvalidKeyLength {
|
return Err(ProxyError::InvalidKeyLength { expected: 32, got: key.len() });
|
||||||
expected: 32,
|
|
||||||
got: key.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if iv.len() != 16 {
|
if iv.len() != 16 {
|
||||||
return Err(ProxyError::InvalidKeyLength {
|
return Err(ProxyError::InvalidKeyLength { expected: 16, got: iv.len() });
|
||||||
expected: 16,
|
|
||||||
got: iv.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let key: [u8; 32] = key.try_into().unwrap();
|
let key: [u8; 32] = key.try_into().unwrap();
|
||||||
@@ -117,16 +108,10 @@ impl AesCbc {
|
|||||||
/// Create from slices
|
/// Create from slices
|
||||||
pub fn from_slices(key: &[u8], iv: &[u8]) -> Result<Self> {
|
pub fn from_slices(key: &[u8], iv: &[u8]) -> Result<Self> {
|
||||||
if key.len() != 32 {
|
if key.len() != 32 {
|
||||||
return Err(ProxyError::InvalidKeyLength {
|
return Err(ProxyError::InvalidKeyLength { expected: 32, got: key.len() });
|
||||||
expected: 32,
|
|
||||||
got: key.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if iv.len() != 16 {
|
if iv.len() != 16 {
|
||||||
return Err(ProxyError::InvalidKeyLength {
|
return Err(ProxyError::InvalidKeyLength { expected: 16, got: iv.len() });
|
||||||
expected: 16,
|
|
||||||
got: iv.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -165,10 +150,9 @@ impl AesCbc {
|
|||||||
/// CBC Encryption: C[i] = AES_Encrypt(P[i] XOR C[i-1]), where C[-1] = IV
|
/// CBC Encryption: C[i] = AES_Encrypt(P[i] XOR C[i-1]), where C[-1] = IV
|
||||||
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(format!(
|
return Err(ProxyError::Crypto(
|
||||||
"CBC data must be aligned to 16 bytes, got {}",
|
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
||||||
data.len()
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -197,10 +181,9 @@ impl AesCbc {
|
|||||||
/// CBC Decryption: P[i] = AES_Decrypt(C[i]) XOR C[i-1], where C[-1] = IV
|
/// CBC Decryption: P[i] = AES_Decrypt(C[i]) XOR C[i-1], where C[-1] = IV
|
||||||
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(format!(
|
return Err(ProxyError::Crypto(
|
||||||
"CBC data must be aligned to 16 bytes, got {}",
|
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
||||||
data.len()
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -227,10 +210,9 @@ impl AesCbc {
|
|||||||
/// Encrypt data in-place
|
/// Encrypt data in-place
|
||||||
pub fn encrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
pub fn encrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(format!(
|
return Err(ProxyError::Crypto(
|
||||||
"CBC data must be aligned to 16 bytes, got {}",
|
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
||||||
data.len()
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -261,10 +243,9 @@ impl AesCbc {
|
|||||||
/// Decrypt data in-place
|
/// Decrypt data in-place
|
||||||
pub fn decrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
pub fn decrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
|
||||||
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
return Err(ProxyError::Crypto(format!(
|
return Err(ProxyError::Crypto(
|
||||||
"CBC data must be aligned to 16 bytes, got {}",
|
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
|
||||||
data.len()
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
|
|||||||
+25
-6
@@ -12,10 +12,10 @@
|
|||||||
//! usages are intentional and protocol-mandated.
|
//! usages are intentional and protocol-mandated.
|
||||||
|
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
use md5::Md5;
|
use md5::Md5;
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use sha2::Sha256;
|
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
@@ -28,7 +28,8 @@ pub fn sha256(data: &[u8]) -> [u8; 32] {
|
|||||||
|
|
||||||
/// SHA-256 HMAC
|
/// SHA-256 HMAC
|
||||||
pub fn sha256_hmac(key: &[u8], data: &[u8]) -> [u8; 32] {
|
pub fn sha256_hmac(key: &[u8], data: &[u8]) -> [u8; 32] {
|
||||||
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
|
let mut mac = HmacSha256::new_from_slice(key)
|
||||||
|
.expect("HMAC accepts any key length");
|
||||||
mac.update(data);
|
mac.update(data);
|
||||||
mac.finalize().into_bytes().into()
|
mac.finalize().into_bytes().into()
|
||||||
}
|
}
|
||||||
@@ -123,8 +124,17 @@ pub fn derive_middleproxy_keys(
|
|||||||
srv_ipv6: Option<&[u8; 16]>,
|
srv_ipv6: Option<&[u8; 16]>,
|
||||||
) -> ([u8; 32], [u8; 16]) {
|
) -> ([u8; 32], [u8; 16]) {
|
||||||
let s = build_middleproxy_prekey(
|
let s = build_middleproxy_prekey(
|
||||||
nonce_srv, nonce_clt, clt_ts, srv_ip, clt_port, purpose, clt_ip, srv_port, secret,
|
nonce_srv,
|
||||||
clt_ipv6, srv_ipv6,
|
nonce_clt,
|
||||||
|
clt_ts,
|
||||||
|
srv_ip,
|
||||||
|
clt_port,
|
||||||
|
purpose,
|
||||||
|
clt_ip,
|
||||||
|
srv_port,
|
||||||
|
secret,
|
||||||
|
clt_ipv6,
|
||||||
|
srv_ipv6,
|
||||||
);
|
);
|
||||||
|
|
||||||
let md5_1 = md5(&s[1..]);
|
let md5_1 = md5(&s[1..]);
|
||||||
@@ -154,8 +164,17 @@ mod tests {
|
|||||||
let secret = vec![0x55u8; 128];
|
let secret = vec![0x55u8; 128];
|
||||||
|
|
||||||
let prekey = build_middleproxy_prekey(
|
let prekey = build_middleproxy_prekey(
|
||||||
&nonce_srv, &nonce_clt, &clt_ts, srv_ip, &clt_port, b"CLIENT", clt_ip, &srv_port,
|
&nonce_srv,
|
||||||
&secret, None, None,
|
&nonce_clt,
|
||||||
|
&clt_ts,
|
||||||
|
srv_ip,
|
||||||
|
&clt_port,
|
||||||
|
b"CLIENT",
|
||||||
|
clt_ip,
|
||||||
|
&srv_port,
|
||||||
|
&secret,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
let digest = sha256(&prekey);
|
let digest = sha256(&prekey);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ pub mod aes;
|
|||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod random;
|
pub mod random;
|
||||||
|
|
||||||
pub use aes::{AesCbc, AesCtr};
|
pub use aes::{AesCtr, AesCbc};
|
||||||
pub use hash::{
|
pub use hash::{
|
||||||
build_middleproxy_prekey, crc32, crc32c, derive_middleproxy_keys, sha256, sha256_hmac,
|
build_middleproxy_prekey, crc32, crc32c, derive_middleproxy_keys, sha256, sha256_hmac,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
#![allow(deprecated)]
|
#![allow(deprecated)]
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::crypto::AesCtr;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::{Rng, RngExt, SeedableRng};
|
use rand::{Rng, RngExt, SeedableRng};
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
use crate::crypto::AesCtr;
|
||||||
|
|
||||||
/// Cryptographically secure PRNG with AES-CTR
|
/// Cryptographically secure PRNG with AES-CTR
|
||||||
pub struct SecureRandom {
|
pub struct SecureRandom {
|
||||||
|
|||||||
@@ -1,541 +0,0 @@
|
|||||||
//! Unix daemon support for telemt.
|
|
||||||
//!
|
|
||||||
//! Provides classic Unix daemonization (double-fork), PID file management,
|
|
||||||
//! and privilege dropping for running telemt as a background service.
|
|
||||||
|
|
||||||
use std::fs::{self, File, OpenOptions};
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use nix::fcntl::{Flock, FlockArg};
|
|
||||||
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid};
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
/// Default PID file location.
|
|
||||||
pub const DEFAULT_PID_FILE: &str = "/var/run/telemt.pid";
|
|
||||||
|
|
||||||
/// Daemon configuration options parsed from CLI.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct DaemonOptions {
|
|
||||||
/// Run as daemon (fork to background).
|
|
||||||
pub daemonize: bool,
|
|
||||||
/// Path to PID file.
|
|
||||||
pub pid_file: Option<PathBuf>,
|
|
||||||
/// User to run as after binding sockets.
|
|
||||||
pub user: Option<String>,
|
|
||||||
/// Group to run as after binding sockets.
|
|
||||||
pub group: Option<String>,
|
|
||||||
/// Working directory for the daemon.
|
|
||||||
pub working_dir: Option<PathBuf>,
|
|
||||||
/// Explicit foreground mode (for systemd Type=simple).
|
|
||||||
pub foreground: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DaemonOptions {
|
|
||||||
/// Returns the effective PID file path.
|
|
||||||
pub fn pid_file_path(&self) -> &Path {
|
|
||||||
self.pid_file
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(Path::new(DEFAULT_PID_FILE))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if we should actually daemonize.
|
|
||||||
/// Foreground flag takes precedence.
|
|
||||||
pub fn should_daemonize(&self) -> bool {
|
|
||||||
self.daemonize && !self.foreground
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error types for daemon operations.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum DaemonError {
|
|
||||||
#[error("fork failed: {0}")]
|
|
||||||
ForkFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("setsid failed: {0}")]
|
|
||||||
SetsidFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("chdir failed: {0}")]
|
|
||||||
ChdirFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("failed to open /dev/null: {0}")]
|
|
||||||
DevNullFailed(#[source] io::Error),
|
|
||||||
|
|
||||||
#[error("failed to redirect stdio: {0}")]
|
|
||||||
RedirectFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("PID file error: {0}")]
|
|
||||||
PidFile(String),
|
|
||||||
|
|
||||||
#[error("another instance is already running (pid {0})")]
|
|
||||||
AlreadyRunning(i32),
|
|
||||||
|
|
||||||
#[error("user '{0}' not found")]
|
|
||||||
UserNotFound(String),
|
|
||||||
|
|
||||||
#[error("group '{0}' not found")]
|
|
||||||
GroupNotFound(String),
|
|
||||||
|
|
||||||
#[error("failed to set uid/gid: {0}")]
|
|
||||||
PrivilegeDrop(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result of a successful daemonize() call.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DaemonizeResult {
|
|
||||||
/// We are the parent process and should exit.
|
|
||||||
Parent,
|
|
||||||
/// We are the daemon child process and should continue.
|
|
||||||
Child,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs classic Unix double-fork daemonization.
|
|
||||||
///
|
|
||||||
/// This detaches the process from the controlling terminal:
|
|
||||||
/// 1. First fork - parent exits, child continues
|
|
||||||
/// 2. setsid() - become session leader
|
|
||||||
/// 3. Second fork - ensure we can never acquire a controlling terminal
|
|
||||||
/// 4. chdir("/") - don't hold any directory open
|
|
||||||
/// 5. Redirect stdin/stdout/stderr to /dev/null
|
|
||||||
///
|
|
||||||
/// Returns `DaemonizeResult::Parent` in the original parent (which should exit),
|
|
||||||
/// or `DaemonizeResult::Child` in the final daemon child.
|
|
||||||
pub fn daemonize(working_dir: Option<&Path>) -> Result<DaemonizeResult, DaemonError> {
|
|
||||||
// First fork
|
|
||||||
match unsafe { fork() } {
|
|
||||||
Ok(ForkResult::Parent { .. }) => {
|
|
||||||
// Parent exits
|
|
||||||
return Ok(DaemonizeResult::Parent);
|
|
||||||
}
|
|
||||||
Ok(ForkResult::Child) => {
|
|
||||||
// Child continues
|
|
||||||
}
|
|
||||||
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new session, become session leader
|
|
||||||
setsid().map_err(DaemonError::SetsidFailed)?;
|
|
||||||
|
|
||||||
// Second fork to ensure we can never acquire a controlling terminal
|
|
||||||
match unsafe { fork() } {
|
|
||||||
Ok(ForkResult::Parent { .. }) => {
|
|
||||||
// Intermediate parent exits
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Ok(ForkResult::Child) => {
|
|
||||||
// Final daemon child continues
|
|
||||||
}
|
|
||||||
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change working directory
|
|
||||||
let target_dir = working_dir.unwrap_or(Path::new("/"));
|
|
||||||
chdir(target_dir).map_err(DaemonError::ChdirFailed)?;
|
|
||||||
|
|
||||||
// Redirect stdin, stdout, stderr to /dev/null
|
|
||||||
redirect_stdio_to_devnull()?;
|
|
||||||
|
|
||||||
Ok(DaemonizeResult::Child)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redirects stdin, stdout, and stderr to /dev/null.
|
|
||||||
fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
|
|
||||||
let devnull = File::options()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open("/dev/null")
|
|
||||||
.map_err(DaemonError::DevNullFailed)?;
|
|
||||||
|
|
||||||
let devnull_fd = std::os::unix::io::AsRawFd::as_raw_fd(&devnull);
|
|
||||||
|
|
||||||
// Use libc::dup2 directly for redirecting standard file descriptors
|
|
||||||
// nix 0.31's dup2 requires OwnedFd which doesn't work well with stdio fds
|
|
||||||
unsafe {
|
|
||||||
// Redirect stdin (fd 0)
|
|
||||||
if libc::dup2(devnull_fd, 0) < 0 {
|
|
||||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
|
||||||
}
|
|
||||||
// Redirect stdout (fd 1)
|
|
||||||
if libc::dup2(devnull_fd, 1) < 0 {
|
|
||||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
|
||||||
}
|
|
||||||
// Redirect stderr (fd 2)
|
|
||||||
if libc::dup2(devnull_fd, 2) < 0 {
|
|
||||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close original devnull fd if it's not one of the standard fds
|
|
||||||
if devnull_fd > 2 {
|
|
||||||
let _ = close(devnull_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PID file manager with flock-based locking.
|
|
||||||
pub struct PidFile {
|
|
||||||
path: PathBuf,
|
|
||||||
file: Option<File>,
|
|
||||||
locked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PidFile {
|
|
||||||
/// Creates a new PID file manager for the given path.
|
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
|
||||||
Self {
|
|
||||||
path: path.as_ref().to_path_buf(),
|
|
||||||
file: None,
|
|
||||||
locked: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if another instance is already running.
|
|
||||||
///
|
|
||||||
/// Returns the PID of the running instance if one exists.
|
|
||||||
pub fn check_running(&self) -> Result<Option<i32>, DaemonError> {
|
|
||||||
if !self.path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to read existing PID
|
|
||||||
let mut contents = String::new();
|
|
||||||
File::open(&self.path)
|
|
||||||
.and_then(|mut f| f.read_to_string(&mut contents))
|
|
||||||
.map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot read {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let pid: i32 = contents
|
|
||||||
.trim()
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", self.path.display())))?;
|
|
||||||
|
|
||||||
// Check if process is still running
|
|
||||||
if is_process_running(pid) {
|
|
||||||
Ok(Some(pid))
|
|
||||||
} else {
|
|
||||||
// Stale PID file
|
|
||||||
debug!(pid, path = %self.path.display(), "Removing stale PID file");
|
|
||||||
let _ = fs::remove_file(&self.path);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acquires the PID file lock and writes the current PID.
|
|
||||||
///
|
|
||||||
/// Fails if another instance is already running.
|
|
||||||
pub fn acquire(&mut self) -> Result<(), DaemonError> {
|
|
||||||
// Check for running instance first
|
|
||||||
if let Some(pid) = self.check_running()? {
|
|
||||||
return Err(DaemonError::AlreadyRunning(pid));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure parent directory exists
|
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
if !parent.exists() {
|
|
||||||
fs::create_dir_all(parent).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!(
|
|
||||||
"cannot create directory {}: {}",
|
|
||||||
parent.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open/create PID file with exclusive lock
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.mode(0o644)
|
|
||||||
.open(&self.path)
|
|
||||||
.map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot open {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Try to acquire exclusive lock (non-blocking)
|
|
||||||
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
|
||||||
// Check if another instance grabbed the lock
|
|
||||||
if let Some(pid) = self.check_running().ok().flatten() {
|
|
||||||
DaemonError::AlreadyRunning(pid)
|
|
||||||
} else {
|
|
||||||
DaemonError::PidFile(format!("cannot lock {}: {}", self.path.display(), errno))
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Write our PID
|
|
||||||
let pid = getpid();
|
|
||||||
let mut file = flock
|
|
||||||
.unlock()
|
|
||||||
.map_err(|(_, errno)| DaemonError::PidFile(format!("unlock failed: {}", errno)))?;
|
|
||||||
|
|
||||||
writeln!(file, "{}", pid).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!(
|
|
||||||
"cannot write PID to {}: {}",
|
|
||||||
self.path.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Re-acquire lock and keep it
|
|
||||||
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
|
||||||
DaemonError::PidFile(format!("cannot re-lock {}: {}", self.path.display(), errno))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.file = Some(flock.unlock().map_err(|(_, errno)| {
|
|
||||||
DaemonError::PidFile(format!("unlock for storage failed: {}", errno))
|
|
||||||
})?);
|
|
||||||
self.locked = true;
|
|
||||||
|
|
||||||
info!(pid = pid.as_raw(), path = %self.path.display(), "PID file created");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases the PID file lock and removes the file.
|
|
||||||
pub fn release(&mut self) -> Result<(), DaemonError> {
|
|
||||||
if let Some(file) = self.file.take() {
|
|
||||||
drop(file);
|
|
||||||
}
|
|
||||||
self.locked = false;
|
|
||||||
|
|
||||||
if self.path.exists() {
|
|
||||||
fs::remove_file(&self.path).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot remove {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
debug!(path = %self.path.display(), "PID file removed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path to this PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn path(&self) -> &Path {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PidFile {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.locked {
|
|
||||||
if let Err(e) = self.release() {
|
|
||||||
warn!(error = %e, "Failed to clean up PID file on drop");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if a process with the given PID is running.
|
|
||||||
fn is_process_running(pid: i32) -> bool {
|
|
||||||
// kill(pid, 0) checks if process exists without sending a signal
|
|
||||||
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drops privileges to the specified user and group.
|
|
||||||
///
|
|
||||||
/// This should be called after binding privileged ports but before
|
|
||||||
/// entering the main event loop.
|
|
||||||
pub fn drop_privileges(user: Option<&str>, group: Option<&str>) -> Result<(), DaemonError> {
|
|
||||||
// Look up group first (need to do this while still root)
|
|
||||||
let target_gid = if let Some(group_name) = group {
|
|
||||||
Some(lookup_group(group_name)?)
|
|
||||||
} else if let Some(user_name) = user {
|
|
||||||
// If no group specified but user is, use user's primary group
|
|
||||||
Some(lookup_user_primary_gid(user_name)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Look up user
|
|
||||||
let target_uid = if let Some(user_name) = user {
|
|
||||||
Some(lookup_user(user_name)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drop privileges: set GID first, then UID
|
|
||||||
// (Setting UID first would prevent us from setting GID)
|
|
||||||
if let Some(gid) = target_gid {
|
|
||||||
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
// Also set supplementary groups to just this one
|
|
||||||
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
info!(gid = gid.as_raw(), "Dropped group privileges");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(uid) = target_uid {
|
|
||||||
unistd::setuid(uid).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
info!(uid = uid.as_raw(), "Dropped user privileges");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a user by name and returns their UID.
|
|
||||||
fn lookup_user(name: &str) -> Result<Uid, DaemonError> {
|
|
||||||
// Use libc getpwnam
|
|
||||||
let c_name =
|
|
||||||
std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let pwd = libc::getpwnam(c_name.as_ptr());
|
|
||||||
if pwd.is_null() {
|
|
||||||
Err(DaemonError::UserNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Uid::from_raw((*pwd).pw_uid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a user's primary GID by username.
|
|
||||||
fn lookup_user_primary_gid(name: &str) -> Result<Gid, DaemonError> {
|
|
||||||
let c_name =
|
|
||||||
std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let pwd = libc::getpwnam(c_name.as_ptr());
|
|
||||||
if pwd.is_null() {
|
|
||||||
Err(DaemonError::UserNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Gid::from_raw((*pwd).pw_gid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a group by name and returns its GID.
|
|
||||||
fn lookup_group(name: &str) -> Result<Gid, DaemonError> {
|
|
||||||
let c_name =
|
|
||||||
std::ffi::CString::new(name).map_err(|_| DaemonError::GroupNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let grp = libc::getgrnam(c_name.as_ptr());
|
|
||||||
if grp.is_null() {
|
|
||||||
Err(DaemonError::GroupNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Gid::from_raw((*grp).gr_gid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads PID from a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn read_pid_file<P: AsRef<Path>>(path: P) -> Result<i32, DaemonError> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
let mut contents = String::new();
|
|
||||||
File::open(path)
|
|
||||||
.and_then(|mut f| f.read_to_string(&mut contents))
|
|
||||||
.map_err(|e| DaemonError::PidFile(format!("cannot read {}: {}", path.display(), e)))?;
|
|
||||||
|
|
||||||
contents
|
|
||||||
.trim()
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", path.display())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a signal to the process specified in a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn signal_pid_file<P: AsRef<Path>>(
|
|
||||||
path: P,
|
|
||||||
signal: nix::sys::signal::Signal,
|
|
||||||
) -> Result<(), DaemonError> {
|
|
||||||
let pid = read_pid_file(&path)?;
|
|
||||||
|
|
||||||
if !is_process_running(pid) {
|
|
||||||
return Err(DaemonError::PidFile(format!(
|
|
||||||
"process {} from {} is not running",
|
|
||||||
pid,
|
|
||||||
path.as_ref().display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
nix::sys::signal::kill(Pid::from_raw(pid), signal)
|
|
||||||
.map_err(|e| DaemonError::PidFile(format!("cannot signal process {}: {}", pid, e)))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the status of the daemon based on PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum DaemonStatus {
|
|
||||||
/// Daemon is running with the given PID.
|
|
||||||
Running(i32),
|
|
||||||
/// PID file exists but process is not running.
|
|
||||||
Stale(i32),
|
|
||||||
/// No PID file exists.
|
|
||||||
NotRunning,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the daemon status from a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn check_status<P: AsRef<Path>>(path: P) -> DaemonStatus {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return DaemonStatus::NotRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
match read_pid_file(path) {
|
|
||||||
Ok(pid) => {
|
|
||||||
if is_process_running(pid) {
|
|
||||||
DaemonStatus::Running(pid)
|
|
||||||
} else {
|
|
||||||
DaemonStatus::Stale(pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => DaemonStatus::NotRunning,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_daemon_options_default() {
|
|
||||||
let opts = DaemonOptions::default();
|
|
||||||
assert!(!opts.daemonize);
|
|
||||||
assert!(!opts.should_daemonize());
|
|
||||||
assert_eq!(opts.pid_file_path(), Path::new(DEFAULT_PID_FILE));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_daemon_options_foreground_overrides() {
|
|
||||||
let opts = DaemonOptions {
|
|
||||||
daemonize: true,
|
|
||||||
foreground: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert!(!opts.should_daemonize());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_status_not_running() {
|
|
||||||
let path = "/tmp/telemt_test_nonexistent.pid";
|
|
||||||
assert_eq!(check_status(path), DaemonStatus::NotRunning);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_file_basic() {
|
|
||||||
let path = "/tmp/telemt_test_pidfile.pid";
|
|
||||||
let _ = fs::remove_file(path);
|
|
||||||
|
|
||||||
let mut pf = PidFile::new(path);
|
|
||||||
assert!(pf.check_running().unwrap().is_none());
|
|
||||||
|
|
||||||
pf.acquire().unwrap();
|
|
||||||
assert!(Path::new(path).exists());
|
|
||||||
|
|
||||||
// Read it back
|
|
||||||
let pid = read_pid_file(path).unwrap();
|
|
||||||
assert_eq!(pid, std::process::id() as i32);
|
|
||||||
|
|
||||||
pf.release().unwrap();
|
|
||||||
assert!(!Path::new(path).exists());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+40
-61
@@ -12,15 +12,28 @@ use thiserror::Error;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum StreamError {
|
pub enum StreamError {
|
||||||
/// Partial read: got fewer bytes than expected
|
/// Partial read: got fewer bytes than expected
|
||||||
PartialRead { expected: usize, got: usize },
|
PartialRead {
|
||||||
|
expected: usize,
|
||||||
|
got: usize,
|
||||||
|
},
|
||||||
/// Partial write: wrote fewer bytes than expected
|
/// Partial write: wrote fewer bytes than expected
|
||||||
PartialWrite { expected: usize, written: usize },
|
PartialWrite {
|
||||||
|
expected: usize,
|
||||||
|
written: usize,
|
||||||
|
},
|
||||||
/// Stream is in poisoned state and cannot be used
|
/// Stream is in poisoned state and cannot be used
|
||||||
Poisoned { reason: String },
|
Poisoned {
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
/// Buffer overflow: attempted to buffer more than allowed
|
/// Buffer overflow: attempted to buffer more than allowed
|
||||||
BufferOverflow { limit: usize, attempted: usize },
|
BufferOverflow {
|
||||||
|
limit: usize,
|
||||||
|
attempted: usize,
|
||||||
|
},
|
||||||
/// Invalid frame format
|
/// Invalid frame format
|
||||||
InvalidFrame { details: String },
|
InvalidFrame {
|
||||||
|
details: String,
|
||||||
|
},
|
||||||
/// Unexpected end of stream
|
/// Unexpected end of stream
|
||||||
UnexpectedEof,
|
UnexpectedEof,
|
||||||
/// Underlying I/O error
|
/// Underlying I/O error
|
||||||
@@ -34,21 +47,13 @@ impl fmt::Display for StreamError {
|
|||||||
write!(f, "partial read: expected {} bytes, got {}", expected, got)
|
write!(f, "partial read: expected {} bytes, got {}", expected, got)
|
||||||
}
|
}
|
||||||
Self::PartialWrite { expected, written } => {
|
Self::PartialWrite { expected, written } => {
|
||||||
write!(
|
write!(f, "partial write: expected {} bytes, wrote {}", expected, written)
|
||||||
f,
|
|
||||||
"partial write: expected {} bytes, wrote {}",
|
|
||||||
expected, written
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Self::Poisoned { reason } => {
|
Self::Poisoned { reason } => {
|
||||||
write!(f, "stream poisoned: {}", reason)
|
write!(f, "stream poisoned: {}", reason)
|
||||||
}
|
}
|
||||||
Self::BufferOverflow { limit, attempted } => {
|
Self::BufferOverflow { limit, attempted } => {
|
||||||
write!(
|
write!(f, "buffer overflow: limit {}, attempted {}", limit, attempted)
|
||||||
f,
|
|
||||||
"buffer overflow: limit {}, attempted {}",
|
|
||||||
limit, attempted
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Self::InvalidFrame { details } => {
|
Self::InvalidFrame { details } => {
|
||||||
write!(f, "invalid frame: {}", details)
|
write!(f, "invalid frame: {}", details)
|
||||||
@@ -85,7 +90,9 @@ impl From<StreamError> for std::io::Error {
|
|||||||
StreamError::UnexpectedEof => {
|
StreamError::UnexpectedEof => {
|
||||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, err)
|
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, err)
|
||||||
}
|
}
|
||||||
StreamError::Poisoned { .. } => std::io::Error::other(err),
|
StreamError::Poisoned { .. } => {
|
||||||
|
std::io::Error::other(err)
|
||||||
|
}
|
||||||
StreamError::BufferOverflow { .. } => {
|
StreamError::BufferOverflow { .. } => {
|
||||||
std::io::Error::new(std::io::ErrorKind::OutOfMemory, err)
|
std::io::Error::new(std::io::ErrorKind::OutOfMemory, err)
|
||||||
}
|
}
|
||||||
@@ -128,10 +135,7 @@ impl Recoverable for StreamError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn can_continue(&self) -> bool {
|
fn can_continue(&self) -> bool {
|
||||||
!matches!(
|
!matches!(self, Self::Poisoned { .. } | Self::UnexpectedEof | Self::BufferOverflow { .. })
|
||||||
self,
|
|
||||||
Self::Poisoned { .. } | Self::UnexpectedEof | Self::BufferOverflow { .. }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +165,7 @@ impl Recoverable for std::io::Error {
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ProxyError {
|
pub enum ProxyError {
|
||||||
// ============= Crypto Errors =============
|
// ============= Crypto Errors =============
|
||||||
|
|
||||||
#[error("Crypto error: {0}")]
|
#[error("Crypto error: {0}")]
|
||||||
Crypto(String),
|
Crypto(String),
|
||||||
|
|
||||||
@@ -168,10 +173,12 @@ pub enum ProxyError {
|
|||||||
InvalidKeyLength { expected: usize, got: usize },
|
InvalidKeyLength { expected: usize, got: usize },
|
||||||
|
|
||||||
// ============= Stream Errors =============
|
// ============= Stream Errors =============
|
||||||
|
|
||||||
#[error("Stream error: {0}")]
|
#[error("Stream error: {0}")]
|
||||||
Stream(#[from] StreamError),
|
Stream(#[from] StreamError),
|
||||||
|
|
||||||
// ============= Protocol Errors =============
|
// ============= Protocol Errors =============
|
||||||
|
|
||||||
#[error("Invalid handshake: {0}")]
|
#[error("Invalid handshake: {0}")]
|
||||||
InvalidHandshake(String),
|
InvalidHandshake(String),
|
||||||
|
|
||||||
@@ -203,6 +210,7 @@ pub enum ProxyError {
|
|||||||
TgHandshakeTimeout,
|
TgHandshakeTimeout,
|
||||||
|
|
||||||
// ============= Network Errors =============
|
// ============= Network Errors =============
|
||||||
|
|
||||||
#[error("Connection timeout to {addr}")]
|
#[error("Connection timeout to {addr}")]
|
||||||
ConnectionTimeout { addr: String },
|
ConnectionTimeout { addr: String },
|
||||||
|
|
||||||
@@ -213,16 +221,15 @@ pub enum ProxyError {
|
|||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
// ============= Proxy Protocol Errors =============
|
// ============= Proxy Protocol Errors =============
|
||||||
|
|
||||||
#[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),
|
||||||
|
|
||||||
// ============= Config Errors =============
|
// ============= Config Errors =============
|
||||||
|
|
||||||
#[error("Config error: {0}")]
|
#[error("Config error: {0}")]
|
||||||
Config(String),
|
Config(String),
|
||||||
|
|
||||||
@@ -230,6 +237,7 @@ pub enum ProxyError {
|
|||||||
InvalidSecret { user: String, reason: String },
|
InvalidSecret { user: String, reason: String },
|
||||||
|
|
||||||
// ============= User Errors =============
|
// ============= User Errors =============
|
||||||
|
|
||||||
#[error("User {user} expired")]
|
#[error("User {user} expired")]
|
||||||
UserExpired { user: String },
|
UserExpired { user: String },
|
||||||
|
|
||||||
@@ -246,6 +254,7 @@ pub enum ProxyError {
|
|||||||
RateLimited,
|
RateLimited,
|
||||||
|
|
||||||
// ============= General Errors =============
|
// ============= General Errors =============
|
||||||
|
|
||||||
#[error("Internal error: {0}")]
|
#[error("Internal error: {0}")]
|
||||||
Internal(String),
|
Internal(String),
|
||||||
}
|
}
|
||||||
@@ -302,9 +311,7 @@ impl<T, R, W> HandshakeResult<T, R, W> {
|
|||||||
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> HandshakeResult<U, R, W> {
|
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> HandshakeResult<U, R, W> {
|
||||||
match self {
|
match self {
|
||||||
HandshakeResult::Success(v) => HandshakeResult::Success(f(v)),
|
HandshakeResult::Success(v) => HandshakeResult::Success(f(v)),
|
||||||
HandshakeResult::BadClient { reader, writer } => {
|
HandshakeResult::BadClient { reader, writer } => HandshakeResult::BadClient { reader, writer },
|
||||||
HandshakeResult::BadClient { reader, writer }
|
|
||||||
}
|
|
||||||
HandshakeResult::Error(e) => HandshakeResult::Error(e),
|
HandshakeResult::Error(e) => HandshakeResult::Error(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,35 +341,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stream_error_display() {
|
fn test_stream_error_display() {
|
||||||
let err = StreamError::PartialRead {
|
let err = StreamError::PartialRead { expected: 100, got: 50 };
|
||||||
expected: 100,
|
|
||||||
got: 50,
|
|
||||||
};
|
|
||||||
assert!(err.to_string().contains("100"));
|
assert!(err.to_string().contains("100"));
|
||||||
assert!(err.to_string().contains("50"));
|
assert!(err.to_string().contains("50"));
|
||||||
|
|
||||||
let err = StreamError::Poisoned {
|
let err = StreamError::Poisoned { reason: "test".into() };
|
||||||
reason: "test".into(),
|
|
||||||
};
|
|
||||||
assert!(err.to_string().contains("test"));
|
assert!(err.to_string().contains("test"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_stream_error_recoverable() {
|
fn test_stream_error_recoverable() {
|
||||||
assert!(
|
assert!(StreamError::PartialRead { expected: 10, got: 5 }.is_recoverable());
|
||||||
StreamError::PartialRead {
|
assert!(StreamError::PartialWrite { expected: 10, written: 5 }.is_recoverable());
|
||||||
expected: 10,
|
|
||||||
got: 5
|
|
||||||
}
|
|
||||||
.is_recoverable()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
StreamError::PartialWrite {
|
|
||||||
expected: 10,
|
|
||||||
written: 5
|
|
||||||
}
|
|
||||||
.is_recoverable()
|
|
||||||
);
|
|
||||||
assert!(!StreamError::Poisoned { reason: "x".into() }.is_recoverable());
|
assert!(!StreamError::Poisoned { reason: "x".into() }.is_recoverable());
|
||||||
assert!(!StreamError::UnexpectedEof.is_recoverable());
|
assert!(!StreamError::UnexpectedEof.is_recoverable());
|
||||||
}
|
}
|
||||||
@@ -371,13 +361,7 @@ mod tests {
|
|||||||
fn test_stream_error_can_continue() {
|
fn test_stream_error_can_continue() {
|
||||||
assert!(!StreamError::Poisoned { reason: "x".into() }.can_continue());
|
assert!(!StreamError::Poisoned { reason: "x".into() }.can_continue());
|
||||||
assert!(!StreamError::UnexpectedEof.can_continue());
|
assert!(!StreamError::UnexpectedEof.can_continue());
|
||||||
assert!(
|
assert!(StreamError::PartialRead { expected: 10, got: 5 }.can_continue());
|
||||||
StreamError::PartialRead {
|
|
||||||
expected: 10,
|
|
||||||
got: 5
|
|
||||||
}
|
|
||||||
.can_continue()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -393,10 +377,7 @@ mod tests {
|
|||||||
assert!(success.is_success());
|
assert!(success.is_success());
|
||||||
assert!(!success.is_bad_client());
|
assert!(!success.is_bad_client());
|
||||||
|
|
||||||
let bad: HandshakeResult<i32, (), ()> = HandshakeResult::BadClient {
|
let bad: HandshakeResult<i32, (), ()> = HandshakeResult::BadClient { reader: (), writer: () };
|
||||||
reader: (),
|
|
||||||
writer: (),
|
|
||||||
};
|
|
||||||
assert!(!bad.is_success());
|
assert!(!bad.is_success());
|
||||||
assert!(bad.is_bad_client());
|
assert!(bad.is_bad_client());
|
||||||
}
|
}
|
||||||
@@ -423,9 +404,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_display() {
|
fn test_error_display() {
|
||||||
let err = ProxyError::ConnectionTimeout {
|
let err = ProxyError::ConnectionTimeout { addr: "1.2.3.4:443".into() };
|
||||||
addr: "1.2.3.4:443".into(),
|
|
||||||
};
|
|
||||||
assert!(err.to_string().contains("1.2.3.4:443"));
|
assert!(err.to_string().contains("1.2.3.4:443"));
|
||||||
|
|
||||||
let err = ProxyError::InvalidProxyProtocol;
|
let err = ProxyError::InvalidProxyProtocol;
|
||||||
|
|||||||
+6
-25
@@ -5,9 +5,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use tokio::sync::{Mutex as AsyncMutex, RwLock};
|
use tokio::sync::{Mutex as AsyncMutex, RwLock};
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ pub struct UserIpTracker {
|
|||||||
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
limit_mode: Arc<RwLock<UserMaxUniqueIpsMode>>,
|
||||||
limit_window: Arc<RwLock<Duration>>,
|
limit_window: Arc<RwLock<Duration>>,
|
||||||
last_compact_epoch_secs: Arc<AtomicU64>,
|
last_compact_epoch_secs: Arc<AtomicU64>,
|
||||||
cleanup_queue: Arc<Mutex<Vec<(String, IpAddr)>>>,
|
pub(crate) cleanup_queue: Arc<Mutex<Vec<(String, IpAddr)>>>,
|
||||||
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
cleanup_drain_lock: Arc<AsyncMutex<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
pub fn enqueue_cleanup(&self, user: String, ip: IpAddr) {
|
||||||
match self.cleanup_queue.lock() {
|
match self.cleanup_queue.lock() {
|
||||||
Ok(mut queue) => queue.push((user, ip)),
|
Ok(mut queue) => queue.push((user, ip)),
|
||||||
@@ -57,19 +58,6 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn cleanup_queue_len_for_tests(&self) -> usize {
|
|
||||||
self.cleanup_queue
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn cleanup_queue_mutex_for_tests(&self) -> Arc<Mutex<Vec<(String, IpAddr)>>> {
|
|
||||||
Arc::clone(&self.cleanup_queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn drain_cleanup_queue(&self) {
|
pub(crate) async fn drain_cleanup_queue(&self) {
|
||||||
// Serialize queue draining and active-IP mutation so check-and-add cannot
|
// Serialize queue draining and active-IP mutation so check-and-add cannot
|
||||||
// observe stale active entries that are already queued for removal.
|
// observe stale active entries that are already queued for removal.
|
||||||
@@ -141,8 +129,7 @@ impl UserIpTracker {
|
|||||||
|
|
||||||
let mut active_ips = self.active_ips.write().await;
|
let mut active_ips = self.active_ips.write().await;
|
||||||
let mut recent_ips = self.recent_ips.write().await;
|
let mut recent_ips = self.recent_ips.write().await;
|
||||||
let mut users =
|
let mut users = Vec::<String>::with_capacity(active_ips.len().saturating_add(recent_ips.len()));
|
||||||
Vec::<String>::with_capacity(active_ips.len().saturating_add(recent_ips.len()));
|
|
||||||
users.extend(active_ips.keys().cloned());
|
users.extend(active_ips.keys().cloned());
|
||||||
for user in recent_ips.keys() {
|
for user in recent_ips.keys() {
|
||||||
if !active_ips.contains_key(user) {
|
if !active_ips.contains_key(user) {
|
||||||
@@ -151,14 +138,8 @@ impl UserIpTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for user in users {
|
for user in users {
|
||||||
let active_empty = active_ips
|
let active_empty = active_ips.get(&user).map(|ips| ips.is_empty()).unwrap_or(true);
|
||||||
.get(&user)
|
let recent_empty = recent_ips.get(&user).map(|ips| ips.is_empty()).unwrap_or(true);
|
||||||
.map(|ips| ips.is_empty())
|
|
||||||
.unwrap_or(true);
|
|
||||||
let recent_empty = recent_ips
|
|
||||||
.get(&user)
|
|
||||||
.map(|ips| ips.is_empty())
|
|
||||||
.unwrap_or(true);
|
|
||||||
if active_empty && recent_empty {
|
if active_empty && recent_empty {
|
||||||
active_ips.remove(&user);
|
active_ips.remove(&user);
|
||||||
recent_ips.remove(&user);
|
recent_ips.remove(&user);
|
||||||
|
|||||||
-305
@@ -1,305 +0,0 @@
|
|||||||
//! Logging configuration for telemt.
|
|
||||||
//!
|
|
||||||
//! Supports multiple log destinations:
|
|
||||||
//! - stderr (default, works with systemd journald)
|
|
||||||
//! - syslog (Unix only, for traditional init systems)
|
|
||||||
//! - file (with optional rotation)
|
|
||||||
|
|
||||||
#![allow(dead_code)] // Infrastructure module - used via CLI flags
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
use tracing_subscriber::{EnvFilter, fmt, reload};
|
|
||||||
|
|
||||||
/// Log destination configuration.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub enum LogDestination {
|
|
||||||
/// Log to stderr (default, captured by systemd journald).
|
|
||||||
#[default]
|
|
||||||
Stderr,
|
|
||||||
/// Log to syslog (Unix only).
|
|
||||||
#[cfg(unix)]
|
|
||||||
Syslog,
|
|
||||||
/// Log to a file with optional rotation.
|
|
||||||
File {
|
|
||||||
path: String,
|
|
||||||
/// Rotate daily if true.
|
|
||||||
rotate_daily: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logging options parsed from CLI/config.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct LoggingOptions {
|
|
||||||
/// Where to send logs.
|
|
||||||
pub destination: LogDestination,
|
|
||||||
/// Disable ANSI colors.
|
|
||||||
pub disable_colors: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Guard that must be held to keep file logging active.
|
|
||||||
/// When dropped, flushes and closes log files.
|
|
||||||
pub struct LoggingGuard {
|
|
||||||
_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoggingGuard {
|
|
||||||
fn new(guard: Option<tracing_appender::non_blocking::WorkerGuard>) -> Self {
|
|
||||||
Self { _guard: guard }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a no-op guard for stderr/syslog logging.
|
|
||||||
pub fn noop() -> Self {
|
|
||||||
Self { _guard: None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize the tracing subscriber with the specified options.
|
|
||||||
///
|
|
||||||
/// Returns a reload handle for dynamic log level changes and a guard
|
|
||||||
/// that must be kept alive for file logging.
|
|
||||||
pub fn init_logging(
|
|
||||||
opts: &LoggingOptions,
|
|
||||||
initial_filter: &str,
|
|
||||||
) -> (
|
|
||||||
reload::Handle<EnvFilter, impl tracing::Subscriber + Send + Sync>,
|
|
||||||
LoggingGuard,
|
|
||||||
) {
|
|
||||||
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new(initial_filter));
|
|
||||||
|
|
||||||
match &opts.destination {
|
|
||||||
LogDestination::Stderr => {
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(!opts.disable_colors)
|
|
||||||
.with_target(true);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::noop())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
LogDestination::Syslog => {
|
|
||||||
// Use a custom fmt layer that writes to syslog
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_target(true)
|
|
||||||
.with_writer(SyslogWriter::new);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::noop())
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
let (non_blocking, guard) = if *rotate_daily {
|
|
||||||
// Extract directory and filename prefix
|
|
||||||
let path = Path::new(path);
|
|
||||||
let dir = path.parent().unwrap_or(Path::new("/var/log"));
|
|
||||||
let prefix = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("telemt");
|
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(dir, prefix);
|
|
||||||
tracing_appender::non_blocking(file_appender)
|
|
||||||
} else {
|
|
||||||
let file = std::fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(path)
|
|
||||||
.expect("Failed to open log file");
|
|
||||||
tracing_appender::non_blocking(file)
|
|
||||||
};
|
|
||||||
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_target(true)
|
|
||||||
.with_writer(non_blocking);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::new(Some(guard)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Syslog writer for tracing.
|
|
||||||
#[cfg(unix)]
|
|
||||||
struct SyslogWriter {
|
|
||||||
_private: (),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl SyslogWriter {
|
|
||||||
fn new() -> Self {
|
|
||||||
// Open syslog connection on first use
|
|
||||||
static INIT: std::sync::Once = std::sync::Once::new();
|
|
||||||
INIT.call_once(|| {
|
|
||||||
unsafe {
|
|
||||||
// Open syslog with ident "telemt", LOG_PID, LOG_DAEMON facility
|
|
||||||
let ident = b"telemt\0".as_ptr() as *const libc::c_char;
|
|
||||||
libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Self { _private: () }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl std::io::Write for SyslogWriter {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
||||||
// Convert to C string, stripping newlines
|
|
||||||
let msg = String::from_utf8_lossy(buf);
|
|
||||||
let msg = msg.trim_end();
|
|
||||||
|
|
||||||
if msg.is_empty() {
|
|
||||||
return Ok(buf.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine priority based on log level in the message
|
|
||||||
let priority = if msg.contains(" ERROR ") || msg.contains(" error ") {
|
|
||||||
libc::LOG_ERR
|
|
||||||
} else if msg.contains(" WARN ") || msg.contains(" warn ") {
|
|
||||||
libc::LOG_WARNING
|
|
||||||
} else if msg.contains(" INFO ") || msg.contains(" info ") {
|
|
||||||
libc::LOG_INFO
|
|
||||||
} else if msg.contains(" DEBUG ") || msg.contains(" debug ") {
|
|
||||||
libc::LOG_DEBUG
|
|
||||||
} else {
|
|
||||||
libc::LOG_INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write to syslog
|
|
||||||
let c_msg = std::ffi::CString::new(msg.as_bytes())
|
|
||||||
.unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap());
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
libc::syslog(
|
|
||||||
priority,
|
|
||||||
b"%s\0".as_ptr() as *const libc::c_char,
|
|
||||||
c_msg.as_ptr(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> std::io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogWriter {
|
|
||||||
type Writer = SyslogWriter;
|
|
||||||
|
|
||||||
fn make_writer(&'a self) -> Self::Writer {
|
|
||||||
SyslogWriter::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse log destination from CLI arguments.
|
|
||||||
pub fn parse_log_destination(args: &[String]) -> LogDestination {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
#[cfg(unix)]
|
|
||||||
"--syslog" => {
|
|
||||||
return LogDestination::Syslog;
|
|
||||||
}
|
|
||||||
"--log-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: args[i].clone(),
|
|
||||||
rotate_daily: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--log-file=") => {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: s.trim_start_matches("--log-file=").to_string(),
|
|
||||||
rotate_daily: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
"--log-file-daily" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: args[i].clone(),
|
|
||||||
rotate_daily: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--log-file-daily=") => {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: s.trim_start_matches("--log-file-daily=").to_string(),
|
|
||||||
rotate_daily: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
LogDestination::Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_default() {
|
|
||||||
let args: Vec<String> = vec![];
|
|
||||||
assert!(matches!(
|
|
||||||
parse_log_destination(&args),
|
|
||||||
LogDestination::Stderr
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_file() {
|
|
||||||
let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()];
|
|
||||||
match parse_log_destination(&args) {
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
assert_eq!(path, "/var/log/telemt.log");
|
|
||||||
assert!(!rotate_daily);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected File destination"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_file_daily() {
|
|
||||||
let args = vec!["--log-file-daily=/var/log/telemt".to_string()];
|
|
||||||
match parse_log_destination(&args) {
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
assert_eq!(path, "/var/log/telemt");
|
|
||||||
assert!(rotate_daily);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected File destination"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_syslog() {
|
|
||||||
let args = vec!["--syslog".to_string()];
|
|
||||||
assert!(matches!(
|
|
||||||
parse_log_destination(&args),
|
|
||||||
LogDestination::Syslog
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-50
@@ -21,29 +21,10 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
if config.general.use_middle_proxy {
|
if config.general.use_middle_proxy {
|
||||||
if let Some(pool) = me_pool.as_ref() {
|
if let Some(pool) = me_pool.as_ref() {
|
||||||
let initial_ready = pool.admission_ready_conditional_cast().await;
|
let initial_ready = pool.admission_ready_conditional_cast().await;
|
||||||
let mut fallback_enabled = config.general.me2dc_fallback;
|
admission_tx.send_replace(initial_ready);
|
||||||
let mut fast_fallback_enabled = fallback_enabled && config.general.me2dc_fast;
|
let _ = route_runtime.set_mode(RelayRouteMode::Middle);
|
||||||
let (initial_gate_open, initial_route_mode, initial_fallback_reason) = if initial_ready
|
|
||||||
{
|
|
||||||
(true, RelayRouteMode::Middle, None)
|
|
||||||
} else if fast_fallback_enabled {
|
|
||||||
(
|
|
||||||
true,
|
|
||||||
RelayRouteMode::Direct,
|
|
||||||
Some("fast_not_ready_fallback"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(false, RelayRouteMode::Middle, None)
|
|
||||||
};
|
|
||||||
admission_tx.send_replace(initial_gate_open);
|
|
||||||
let _ = route_runtime.set_mode(initial_route_mode);
|
|
||||||
if initial_ready {
|
if initial_ready {
|
||||||
info!("Conditional-admission gate: open / ME pool READY");
|
info!("Conditional-admission gate: open / ME pool READY");
|
||||||
} else if let Some(reason) = initial_fallback_reason {
|
|
||||||
warn!(
|
|
||||||
fallback_reason = reason,
|
|
||||||
"Conditional-admission gate opened in ME fast fallback mode"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
warn!("Conditional-admission gate: closed / ME pool is NOT ready)");
|
||||||
}
|
}
|
||||||
@@ -53,9 +34,10 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
let route_runtime_gate = route_runtime.clone();
|
let route_runtime_gate = route_runtime.clone();
|
||||||
let mut config_rx_gate = config_rx.clone();
|
let mut config_rx_gate = config_rx.clone();
|
||||||
let mut admission_poll_ms = config.general.me_admission_poll_ms.max(1);
|
let mut admission_poll_ms = config.general.me_admission_poll_ms.max(1);
|
||||||
|
let mut fallback_enabled = config.general.me2dc_fallback;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut gate_open = initial_gate_open;
|
let mut gate_open = initial_ready;
|
||||||
let mut route_mode = initial_route_mode;
|
let mut route_mode = RelayRouteMode::Middle;
|
||||||
let mut ready_observed = initial_ready;
|
let mut ready_observed = initial_ready;
|
||||||
let mut not_ready_since = if initial_ready {
|
let mut not_ready_since = if initial_ready {
|
||||||
None
|
None
|
||||||
@@ -71,23 +53,16 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
let cfg = config_rx_gate.borrow_and_update().clone();
|
let cfg = config_rx_gate.borrow_and_update().clone();
|
||||||
admission_poll_ms = cfg.general.me_admission_poll_ms.max(1);
|
admission_poll_ms = cfg.general.me_admission_poll_ms.max(1);
|
||||||
fallback_enabled = cfg.general.me2dc_fallback;
|
fallback_enabled = cfg.general.me2dc_fallback;
|
||||||
fast_fallback_enabled = cfg.general.me2dc_fallback && cfg.general.me2dc_fast;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
_ = tokio::time::sleep(Duration::from_millis(admission_poll_ms)) => {}
|
||||||
}
|
}
|
||||||
let ready = pool_for_gate.admission_ready_conditional_cast().await;
|
let ready = pool_for_gate.admission_ready_conditional_cast().await;
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let (next_gate_open, next_route_mode, next_fallback_reason) = if ready {
|
let (next_gate_open, next_route_mode, next_fallback_active) = if ready {
|
||||||
ready_observed = true;
|
ready_observed = true;
|
||||||
not_ready_since = None;
|
not_ready_since = None;
|
||||||
(true, RelayRouteMode::Middle, None)
|
(true, RelayRouteMode::Middle, false)
|
||||||
} else if fast_fallback_enabled {
|
|
||||||
(
|
|
||||||
true,
|
|
||||||
RelayRouteMode::Direct,
|
|
||||||
Some("fast_not_ready_fallback"),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
let not_ready_started_at = *not_ready_since.get_or_insert(now);
|
let not_ready_started_at = *not_ready_since.get_or_insert(now);
|
||||||
let not_ready_for = now.saturating_duration_since(not_ready_started_at);
|
let not_ready_for = now.saturating_duration_since(not_ready_started_at);
|
||||||
@@ -97,12 +72,11 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
STARTUP_FALLBACK_AFTER
|
STARTUP_FALLBACK_AFTER
|
||||||
};
|
};
|
||||||
if fallback_enabled && not_ready_for > fallback_after {
|
if fallback_enabled && not_ready_for > fallback_after {
|
||||||
(true, RelayRouteMode::Direct, Some("strict_grace_fallback"))
|
(true, RelayRouteMode::Direct, true)
|
||||||
} else {
|
} else {
|
||||||
(false, RelayRouteMode::Middle, None)
|
(false, RelayRouteMode::Middle, false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let next_fallback_active = next_fallback_reason.is_some();
|
|
||||||
|
|
||||||
if next_route_mode != route_mode {
|
if next_route_mode != route_mode {
|
||||||
route_mode = next_route_mode;
|
route_mode = next_route_mode;
|
||||||
@@ -114,8 +88,6 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
"Middle-End routing restored for new sessions"
|
"Middle-End routing restored for new sessions"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let fallback_reason = next_fallback_reason.unwrap_or("unknown");
|
|
||||||
if fallback_reason == "strict_grace_fallback" {
|
|
||||||
let fallback_after = if ready_observed {
|
let fallback_after = if ready_observed {
|
||||||
RUNTIME_FALLBACK_AFTER
|
RUNTIME_FALLBACK_AFTER
|
||||||
} else {
|
} else {
|
||||||
@@ -125,17 +97,8 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
target_mode = route_mode.as_str(),
|
target_mode = route_mode.as_str(),
|
||||||
cutover_generation = snapshot.generation,
|
cutover_generation = snapshot.generation,
|
||||||
grace_secs = fallback_after.as_secs(),
|
grace_secs = fallback_after.as_secs(),
|
||||||
fallback_reason,
|
|
||||||
"ME pool stayed not-ready beyond grace; routing new sessions via Direct-DC"
|
"ME pool stayed not-ready beyond grace; routing new sessions via Direct-DC"
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
target_mode = route_mode.as_str(),
|
|
||||||
cutover_generation = snapshot.generation,
|
|
||||||
fallback_reason,
|
|
||||||
"ME pool not-ready; routing new sessions via Direct-DC (fast mode)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,10 +108,7 @@ pub(crate) async fn configure_admission_gate(
|
|||||||
admission_tx_gate.send_replace(gate_open);
|
admission_tx_gate.send_replace(gate_open);
|
||||||
if gate_open {
|
if gate_open {
|
||||||
if next_fallback_active {
|
if next_fallback_active {
|
||||||
warn!(
|
warn!("Conditional-admission gate opened in ME fallback mode");
|
||||||
fallback_reason = next_fallback_reason.unwrap_or("unknown"),
|
|
||||||
"Conditional-admission gate opened in ME fallback mode"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
info!("Conditional-admission gate opened / ME pool READY");
|
info!("Conditional-admission gate opened / ME pool READY");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -13,10 +11,10 @@ use crate::startup::{
|
|||||||
COMPONENT_DC_CONNECTIVITY_PING, COMPONENT_ME_CONNECTIVITY_PING, COMPONENT_RUNTIME_READY,
|
COMPONENT_DC_CONNECTIVITY_PING, COMPONENT_ME_CONNECTIVITY_PING, COMPONENT_RUNTIME_READY,
|
||||||
StartupTracker,
|
StartupTracker,
|
||||||
};
|
};
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
MePingFamily, MePingSample, MePool, format_me_route, format_sample_line, run_me_ping,
|
MePingFamily, MePingSample, MePool, format_me_route, format_sample_line, run_me_ping,
|
||||||
};
|
};
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
pub(crate) async fn run_startup_connectivity(
|
pub(crate) async fn run_startup_connectivity(
|
||||||
config: &Arc<ProxyConfig>,
|
config: &Arc<ProxyConfig>,
|
||||||
@@ -49,15 +47,11 @@ pub(crate) async fn run_startup_connectivity(
|
|||||||
|
|
||||||
let v4_ok = me_results.iter().any(|r| {
|
let v4_ok = me_results.iter().any(|r| {
|
||||||
matches!(r.family, MePingFamily::V4)
|
matches!(r.family, MePingFamily::V4)
|
||||||
&& r.samples
|
&& r.samples.iter().any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
||||||
.iter()
|
|
||||||
.any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
|
||||||
});
|
});
|
||||||
let v6_ok = me_results.iter().any(|r| {
|
let v6_ok = me_results.iter().any(|r| {
|
||||||
matches!(r.family, MePingFamily::V6)
|
matches!(r.family, MePingFamily::V6)
|
||||||
&& r.samples
|
&& r.samples.iter().any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
||||||
.iter()
|
|
||||||
.any(|s| s.error.is_none() && s.handshake_ms.is_some())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("================= Telegram ME Connectivity =================");
|
info!("================= Telegram ME Connectivity =================");
|
||||||
@@ -137,14 +131,8 @@ pub(crate) async fn run_startup_connectivity(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
for upstream_result in &ping_results {
|
for upstream_result in &ping_results {
|
||||||
let v6_works = upstream_result
|
let v6_works = upstream_result.v6_results.iter().any(|r| r.rtt_ms.is_some());
|
||||||
.v6_results
|
let v4_works = upstream_result.v4_results.iter().any(|r| r.rtt_ms.is_some());
|
||||||
.iter()
|
|
||||||
.any(|r| r.rtt_ms.is_some());
|
|
||||||
let v4_works = upstream_result
|
|
||||||
.v4_results
|
|
||||||
.iter()
|
|
||||||
.any(|r| r.rtt_ms.is_some());
|
|
||||||
|
|
||||||
if upstream_result.both_available {
|
if upstream_result.both_available {
|
||||||
if prefer_ipv6 {
|
if prefer_ipv6 {
|
||||||
|
|||||||
+35
-131
@@ -1,24 +1,16 @@
|
|||||||
#![allow(clippy::items_after_test_module)]
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::logging::LogDestination;
|
|
||||||
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(config_path_cli: &str, startup_cwd: &std::path::Path) -> PathBuf {
|
||||||
config_path_cli: &str,
|
|
||||||
startup_cwd: &std::path::Path,
|
|
||||||
) -> PathBuf {
|
|
||||||
let raw = PathBuf::from(config_path_cli);
|
let raw = PathBuf::from(config_path_cli);
|
||||||
let absolute = if raw.is_absolute() {
|
let absolute = if raw.is_absolute() {
|
||||||
raw
|
raw
|
||||||
@@ -28,16 +20,7 @@ pub(crate) fn resolve_runtime_config_path(
|
|||||||
absolute.canonicalize().unwrap_or(absolute)
|
absolute.canonicalize().unwrap_or(absolute)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed CLI arguments.
|
pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
||||||
pub(crate) struct CliArgs {
|
|
||||||
pub config_path: String,
|
|
||||||
pub data_path: Option<PathBuf>,
|
|
||||||
pub silent: bool,
|
|
||||||
pub log_level: Option<String>,
|
|
||||||
pub log_destination: LogDestination,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_cli() -> CliArgs {
|
|
||||||
let mut config_path = "config.toml".to_string();
|
let mut config_path = "config.toml".to_string();
|
||||||
let mut data_path: Option<PathBuf> = None;
|
let mut data_path: Option<PathBuf> = None;
|
||||||
let mut silent = false;
|
let mut silent = false;
|
||||||
@@ -45,9 +28,6 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
|
||||||
// Parse log destination
|
|
||||||
let log_destination = crate::logging::parse_log_destination(&args);
|
|
||||||
|
|
||||||
// Check for --init first (handled before tokio)
|
// Check for --init first (handled before tokio)
|
||||||
if let Some(init_opts) = cli::parse_init_args(&args) {
|
if let Some(init_opts) = cli::parse_init_args(&args) {
|
||||||
if let Err(e) = cli::run_init(init_opts) {
|
if let Err(e) = cli::run_init(init_opts) {
|
||||||
@@ -70,9 +50,7 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--data-path=") => {
|
s if s.starts_with("--data-path=") => {
|
||||||
data_path = Some(PathBuf::from(
|
data_path = Some(PathBuf::from(s.trim_start_matches("--data-path=").to_string()));
|
||||||
s.trim_start_matches("--data-path=").to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
"--silent" | "-s" => {
|
"--silent" | "-s" => {
|
||||||
silent = true;
|
silent = true;
|
||||||
@@ -87,35 +65,34 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
print_help();
|
eprintln!("Usage: telemt [config.toml] [OPTIONS]");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" --data-path <DIR> Set data directory (absolute path; overrides config value)");
|
||||||
|
eprintln!(" --silent, -s Suppress info logs");
|
||||||
|
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
||||||
|
eprintln!(" --help, -h Show this help");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Setup (fire-and-forget):");
|
||||||
|
eprintln!(
|
||||||
|
" --init Generate config, install systemd service, start"
|
||||||
|
);
|
||||||
|
eprintln!(" --port <PORT> Listen port (default: 443)");
|
||||||
|
eprintln!(
|
||||||
|
" --domain <DOMAIN> TLS domain for masking (default: www.google.com)"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" --secret <HEX> 32-char hex secret (auto-generated if omitted)"
|
||||||
|
);
|
||||||
|
eprintln!(" --user <NAME> Username (default: user)");
|
||||||
|
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
||||||
|
eprintln!(" --no-start Don't start the service after install");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
// Skip daemon-related flags (already parsed)
|
|
||||||
"--daemon" | "-d" | "--foreground" | "-f" => {}
|
|
||||||
s if s.starts_with("--pid-file") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1; // skip value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-user") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-group") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--working-dir") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if !s.starts_with('-') => {
|
s if !s.starts_with('-') => {
|
||||||
config_path = s.to_string();
|
config_path = s.to_string();
|
||||||
}
|
}
|
||||||
@@ -126,73 +103,7 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
CliArgs {
|
(config_path, data_path, silent, log_level)
|
||||||
config_path,
|
|
||||||
data_path,
|
|
||||||
silent,
|
|
||||||
log_level,
|
|
||||||
log_destination,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
eprintln!("Usage: telemt [COMMAND] [OPTIONS] [config.toml]");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Commands:");
|
|
||||||
eprintln!(" run Run in foreground (default if no command given)");
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!(" start Start as background daemon");
|
|
||||||
eprintln!(" stop Stop a running daemon");
|
|
||||||
eprintln!(" reload Reload configuration (send SIGHUP)");
|
|
||||||
eprintln!(" status Check if daemon is running");
|
|
||||||
}
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Options:");
|
|
||||||
eprintln!(
|
|
||||||
" --data-path <DIR> Set data directory (absolute path; overrides config value)"
|
|
||||||
);
|
|
||||||
eprintln!(" --silent, -s Suppress info logs");
|
|
||||||
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
|
||||||
eprintln!(" --help, -h Show this help");
|
|
||||||
eprintln!(" --version, -V Show version");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Logging options:");
|
|
||||||
eprintln!(" --log-file <PATH> Log to file (default: stderr)");
|
|
||||||
eprintln!(" --log-file-daily <PATH> Log to file with daily rotation");
|
|
||||||
#[cfg(unix)]
|
|
||||||
eprintln!(" --syslog Log to syslog (Unix only)");
|
|
||||||
eprintln!();
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!("Daemon options (Unix only):");
|
|
||||||
eprintln!(" --daemon, -d Fork to background (daemonize)");
|
|
||||||
eprintln!(" --foreground, -f Explicit foreground mode (for systemd)");
|
|
||||||
eprintln!(" --pid-file <PATH> PID file path (default: /var/run/telemt.pid)");
|
|
||||||
eprintln!(" --run-as-user <USER> Drop privileges to this user after binding");
|
|
||||||
eprintln!(" --run-as-group <GROUP> Drop privileges to this group after binding");
|
|
||||||
eprintln!(" --working-dir <DIR> Working directory for daemon mode");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
eprintln!("Setup (fire-and-forget):");
|
|
||||||
eprintln!(" --init Generate config, install systemd service, start");
|
|
||||||
eprintln!(" --port <PORT> Listen port (default: 443)");
|
|
||||||
eprintln!(" --domain <DOMAIN> TLS domain for masking (default: www.google.com)");
|
|
||||||
eprintln!(" --secret <HEX> 32-char hex secret (auto-generated if omitted)");
|
|
||||||
eprintln!(" --user <NAME> Username (default: user)");
|
|
||||||
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
|
||||||
eprintln!(" --no-start Don't start the service after install");
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Examples:");
|
|
||||||
eprintln!(" telemt config.toml Run in foreground");
|
|
||||||
eprintln!(" telemt start config.toml Start as daemon");
|
|
||||||
eprintln!(" telemt start --pid-file /tmp/t.pid Start with custom PID file");
|
|
||||||
eprintln!(" telemt stop Stop daemon");
|
|
||||||
eprintln!(" telemt reload Reload configuration");
|
|
||||||
eprintln!(" telemt status Check daemon status");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -235,12 +146,7 @@ mod tests {
|
|||||||
|
|
||||||
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
||||||
info!(target: "telemt::links", "--- Proxy Links ({}) ---", host);
|
info!(target: "telemt::links", "--- Proxy Links ({}) ---", host);
|
||||||
for user_name in config
|
for user_name in config.general.links.show.resolve_users(&config.access.users) {
|
||||||
.general
|
|
||||||
.links
|
|
||||||
.show
|
|
||||||
.resolve_users(&config.access.users)
|
|
||||||
{
|
|
||||||
if let Some(secret) = config.access.users.get(user_name) {
|
if let Some(secret) = config.access.users.get(user_name) {
|
||||||
info!(target: "telemt::links", "User: {}", user_name);
|
info!(target: "telemt::links", "User: {}", user_name);
|
||||||
if config.general.modes.classic {
|
if config.general.modes.classic {
|
||||||
@@ -368,10 +274,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
|
||||||
@@ -382,10 +287,7 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
return Some(cfg);
|
return Some(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn!(
|
warn!(snapshot = label, url, "Startup proxy-config is empty; trying disk cache");
|
||||||
snapshot = label,
|
|
||||||
url, "Startup proxy-config is empty; trying disk cache"
|
|
||||||
);
|
|
||||||
if let Some(path) = cache_path {
|
if let Some(path) = cache_path {
|
||||||
match load_proxy_config_cache(path).await {
|
match load_proxy_config_cache(path).await {
|
||||||
Ok(cached) if !cached.map.is_empty() => {
|
Ok(cached) if !cached.map.is_empty() => {
|
||||||
@@ -400,7 +302,8 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
warn!(
|
warn!(
|
||||||
snapshot = label,
|
snapshot = label,
|
||||||
path, "Startup proxy-config cache is empty; ignoring cache file"
|
path,
|
||||||
|
"Startup proxy-config cache is empty; ignoring cache file"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(cache_err) => {
|
Err(cache_err) => {
|
||||||
@@ -444,7 +347,8 @@ pub(crate) async fn load_startup_proxy_config_snapshot(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
warn!(
|
warn!(
|
||||||
snapshot = label,
|
snapshot = label,
|
||||||
path, "Startup proxy-config cache is empty; ignoring cache file"
|
path,
|
||||||
|
"Startup proxy-config cache is empty; ignoring cache file"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(cache_err) => {
|
Err(cache_err) => {
|
||||||
|
|||||||
+23
-37
@@ -12,15 +12,17 @@ use tracing::{debug, error, info, warn};
|
|||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::proxy::ClientHandler;
|
|
||||||
use crate::proxy::route_mode::{ROUTE_SWITCH_ERROR_MSG, RouteRuntimeController};
|
use crate::proxy::route_mode::{ROUTE_SWITCH_ERROR_MSG, RouteRuntimeController};
|
||||||
|
use crate::proxy::ClientHandler;
|
||||||
use crate::startup::{COMPONENT_LISTENERS_BIND, StartupTracker};
|
use crate::startup::{COMPONENT_LISTENERS_BIND, StartupTracker};
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use crate::tls_front::TlsFrontCache;
|
use crate::tls_front::TlsFrontCache;
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes};
|
use crate::transport::{
|
||||||
|
ListenOptions, UpstreamManager, create_listener, find_listener_processes,
|
||||||
|
};
|
||||||
|
|
||||||
use super::helpers::{is_expected_handshake_eof, print_proxy_links};
|
use super::helpers::{is_expected_handshake_eof, print_proxy_links};
|
||||||
|
|
||||||
@@ -72,7 +74,6 @@ pub(crate) async fn bind_listeners(
|
|||||||
let options = ListenOptions {
|
let options = ListenOptions {
|
||||||
reuse_port: listener_conf.reuse_allow,
|
reuse_port: listener_conf.reuse_allow,
|
||||||
ipv6_only: listener_conf.ip.is_ipv6(),
|
ipv6_only: listener_conf.ip.is_ipv6(),
|
||||||
backlog: config.server.listen_backlog,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,9 +81,8 @@ pub(crate) async fn bind_listeners(
|
|||||||
Ok(socket) => {
|
Ok(socket) => {
|
||||||
let listener = TcpListener::from_std(socket.into())?;
|
let listener = TcpListener::from_std(socket.into())?;
|
||||||
info!("Listening on {}", addr);
|
info!("Listening on {}", addr);
|
||||||
let listener_proxy_protocol = listener_conf
|
let listener_proxy_protocol =
|
||||||
.proxy_protocol
|
listener_conf.proxy_protocol.unwrap_or(config.server.proxy_protocol);
|
||||||
.unwrap_or(config.server.proxy_protocol);
|
|
||||||
|
|
||||||
let public_host = if let Some(ref announce) = listener_conf.announce {
|
let public_host = if let Some(ref announce) = listener_conf.announce {
|
||||||
announce.clone()
|
announce.clone()
|
||||||
@@ -100,14 +100,8 @@ pub(crate) async fn bind_listeners(
|
|||||||
listener_conf.ip.to_string()
|
listener_conf.ip.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
if config.general.links.public_host.is_none()
|
if config.general.links.public_host.is_none() && !config.general.links.show.is_empty() {
|
||||||
&& !config.general.links.show.is_empty()
|
let link_port = config.general.links.public_port.unwrap_or(config.server.port);
|
||||||
{
|
|
||||||
let link_port = config
|
|
||||||
.general
|
|
||||||
.links
|
|
||||||
.public_port
|
|
||||||
.unwrap_or(config.server.port);
|
|
||||||
print_proxy_links(&public_host, link_port, config);
|
print_proxy_links(&public_host, link_port, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,14 +145,12 @@ pub(crate) async fn bind_listeners(
|
|||||||
let (host, port) = if let Some(ref h) = config.general.links.public_host {
|
let (host, port) = if let Some(ref h) = config.general.links.public_host {
|
||||||
(
|
(
|
||||||
h.clone(),
|
h.clone(),
|
||||||
config
|
config.general.links.public_port.unwrap_or(config.server.port),
|
||||||
.general
|
|
||||||
.links
|
|
||||||
.public_port
|
|
||||||
.unwrap_or(config.server.port),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string());
|
let ip = detected_ip_v4
|
||||||
|
.or(detected_ip_v6)
|
||||||
|
.map(|ip| ip.to_string());
|
||||||
if ip.is_none() {
|
if ip.is_none() {
|
||||||
warn!(
|
warn!(
|
||||||
"show_link is configured but public IP could not be detected. Set public_host in config."
|
"show_link is configured but public IP could not be detected. Set public_host in config."
|
||||||
@@ -166,11 +158,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
}
|
}
|
||||||
(
|
(
|
||||||
ip.unwrap_or_else(|| "UNKNOWN".to_string()),
|
ip.unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||||
config
|
config.general.links.public_port.unwrap_or(config.server.port),
|
||||||
.general
|
|
||||||
.links
|
|
||||||
.public_port
|
|
||||||
.unwrap_or(config.server.port),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -190,19 +178,13 @@ pub(crate) async fn bind_listeners(
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let perms = std::fs::Permissions::from_mode(mode);
|
let perms = std::fs::Permissions::from_mode(mode);
|
||||||
if let Err(e) = std::fs::set_permissions(unix_path, perms) {
|
if let Err(e) = std::fs::set_permissions(unix_path, perms) {
|
||||||
error!(
|
error!("Failed to set unix socket permissions to {}: {}", perm_str, e);
|
||||||
"Failed to set unix socket permissions to {}: {}",
|
|
||||||
perm_str, e
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
info!("Listening on unix:{} (mode {})", unix_path, perm_str);
|
info!("Listening on unix:{} (mode {})", unix_path, perm_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(
|
warn!("Invalid listen_unix_sock_perm '{}': {}. Ignoring.", perm_str, e);
|
||||||
"Invalid listen_unix_sock_perm '{}': {}. Ignoring.",
|
|
||||||
perm_str, e
|
|
||||||
);
|
|
||||||
info!("Listening on unix:{}", unix_path);
|
info!("Listening on unix:{}", unix_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,8 +218,10 @@ pub(crate) async fn bind_listeners(
|
|||||||
drop(stream);
|
drop(stream);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accept_permit_timeout_ms =
|
let accept_permit_timeout_ms = config_rx_unix
|
||||||
config_rx_unix.borrow().server.accept_permit_timeout_ms;
|
.borrow()
|
||||||
|
.server
|
||||||
|
.accept_permit_timeout_ms;
|
||||||
let permit = if accept_permit_timeout_ms == 0 {
|
let permit = if accept_permit_timeout_ms == 0 {
|
||||||
match max_connections_unix.clone().acquire_owned().await {
|
match max_connections_unix.clone().acquire_owned().await {
|
||||||
Ok(permit) => permit,
|
Ok(permit) => permit,
|
||||||
@@ -377,8 +361,10 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
drop(stream);
|
drop(stream);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let accept_permit_timeout_ms =
|
let accept_permit_timeout_ms = config_rx
|
||||||
config_rx.borrow().server.accept_permit_timeout_ms;
|
.borrow()
|
||||||
|
.server
|
||||||
|
.accept_permit_timeout_ms;
|
||||||
let permit = if accept_permit_timeout_ms == 0 {
|
let permit = if accept_permit_timeout_ms == 0 {
|
||||||
match max_connections_tcp.clone().acquire_owned().await {
|
match max_connections_tcp.clone().acquire_owned().await {
|
||||||
Ok(permit) => permit,
|
Ok(permit) => permit,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -14,8 +12,8 @@ use crate::startup::{
|
|||||||
COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH, StartupMeStatus, StartupTracker,
|
COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH, StartupMeStatus, StartupTracker,
|
||||||
};
|
};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
use super::helpers::load_startup_proxy_config_snapshot;
|
use super::helpers::load_startup_proxy_config_snapshot;
|
||||||
|
|
||||||
@@ -63,10 +61,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 +127,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 +158,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() {
|
||||||
@@ -234,12 +229,8 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.me_adaptive_floor_recover_grace_secs,
|
config.general.me_adaptive_floor_recover_grace_secs,
|
||||||
config.general.me_adaptive_floor_writers_per_core_total,
|
config.general.me_adaptive_floor_writers_per_core_total,
|
||||||
config.general.me_adaptive_floor_cpu_cores_override,
|
config.general.me_adaptive_floor_cpu_cores_override,
|
||||||
config
|
config.general.me_adaptive_floor_max_extra_writers_single_per_core,
|
||||||
.general
|
config.general.me_adaptive_floor_max_extra_writers_multi_per_core,
|
||||||
.me_adaptive_floor_max_extra_writers_single_per_core,
|
|
||||||
config
|
|
||||||
.general
|
|
||||||
.me_adaptive_floor_max_extra_writers_multi_per_core,
|
|
||||||
config.general.me_adaptive_floor_max_active_writers_per_core,
|
config.general.me_adaptive_floor_max_active_writers_per_core,
|
||||||
config.general.me_adaptive_floor_max_warm_writers_per_core,
|
config.general.me_adaptive_floor_max_warm_writers_per_core,
|
||||||
config.general.me_adaptive_floor_max_active_writers_global,
|
config.general.me_adaptive_floor_max_active_writers_global,
|
||||||
@@ -277,8 +268,6 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
config.general.me_warn_rate_limit_ms,
|
config.general.me_warn_rate_limit_ms,
|
||||||
config.general.me_route_no_writer_mode,
|
config.general.me_route_no_writer_mode,
|
||||||
config.general.me_route_no_writer_wait_ms,
|
config.general.me_route_no_writer_wait_ms,
|
||||||
config.general.me_route_hybrid_max_wait_ms,
|
|
||||||
config.general.me_route_blocking_send_timeout_ms,
|
|
||||||
config.general.me_route_inline_recovery_attempts,
|
config.general.me_route_inline_recovery_attempts,
|
||||||
config.general.me_route_inline_recovery_wait_ms,
|
config.general.me_route_inline_recovery_wait_ms,
|
||||||
);
|
);
|
||||||
@@ -484,9 +473,7 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => warn!(
|
Ok(()) => warn!("me_health_monitor exited unexpectedly, restarting"),
|
||||||
"me_health_monitor exited unexpectedly, restarting"
|
|
||||||
),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = %e, "me_health_monitor panicked, restarting in 1s");
|
error!(error = %e, "me_health_monitor panicked, restarting in 1s");
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
@@ -503,9 +490,7 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => warn!(
|
Ok(()) => warn!("me_drain_timeout_enforcer exited unexpectedly, restarting"),
|
||||||
"me_drain_timeout_enforcer exited unexpectedly, restarting"
|
|
||||||
),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = %e, "me_drain_timeout_enforcer panicked, restarting in 1s");
|
error!(error = %e, "me_drain_timeout_enforcer panicked, restarting in 1s");
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
@@ -522,9 +507,7 @@ pub(crate) async fn initialize_me_pool(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
match res {
|
match res {
|
||||||
Ok(()) => warn!(
|
Ok(()) => warn!("me_zombie_writer_watchdog exited unexpectedly, restarting"),
|
||||||
"me_zombie_writer_watchdog exited unexpectedly, restarting"
|
|
||||||
),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = %e, "me_zombie_writer_watchdog panicked, restarting in 1s");
|
error!(error = %e, "me_zombie_writer_watchdog panicked, restarting in 1s");
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|||||||
+43
-153
@@ -11,9 +11,9 @@
|
|||||||
// - admission: conditional-cast gate and route mode switching.
|
// - admission: conditional-cast gate and route mode switching.
|
||||||
// - listeners: TCP/Unix listener bind and accept-loop orchestration.
|
// - listeners: TCP/Unix listener bind and accept-loop orchestration.
|
||||||
// - shutdown: graceful shutdown sequence and uptime logging.
|
// - shutdown: graceful shutdown sequence and uptime logging.
|
||||||
|
mod helpers;
|
||||||
mod admission;
|
mod admission;
|
||||||
mod connectivity;
|
mod connectivity;
|
||||||
mod helpers;
|
|
||||||
mod listeners;
|
mod listeners;
|
||||||
mod me_startup;
|
mod me_startup;
|
||||||
mod runtime_tasks;
|
mod runtime_tasks;
|
||||||
@@ -33,69 +33,22 @@ use crate::crypto::SecureRandom;
|
|||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
||||||
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
use crate::proxy::route_mode::{RelayRouteMode, RouteRuntimeController};
|
||||||
use crate::startup::{
|
|
||||||
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD, COMPONENT_ME_POOL_CONSTRUCT,
|
|
||||||
COMPONENT_ME_POOL_INIT_STAGE1, COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6,
|
|
||||||
COMPONENT_ME_SECRET_FETCH, COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus,
|
|
||||||
StartupTracker,
|
|
||||||
};
|
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::telemetry::TelemetryPolicy;
|
use crate::stats::telemetry::TelemetryPolicy;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
|
use crate::startup::{
|
||||||
|
COMPONENT_API_BOOTSTRAP, COMPONENT_CONFIG_LOAD,
|
||||||
|
COMPONENT_ME_POOL_CONSTRUCT, COMPONENT_ME_POOL_INIT_STAGE1,
|
||||||
|
COMPONENT_ME_PROXY_CONFIG_V4, COMPONENT_ME_PROXY_CONFIG_V6, COMPONENT_ME_SECRET_FETCH,
|
||||||
|
COMPONENT_NETWORK_PROBE, COMPONENT_TRACING_INIT, StartupMeStatus, StartupTracker,
|
||||||
|
};
|
||||||
use crate::stream::BufferPool;
|
use crate::stream::BufferPool;
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
use helpers::{parse_cli, resolve_runtime_config_path};
|
use helpers::{parse_cli, resolve_runtime_config_path};
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::daemon::{DaemonOptions, PidFile, drop_privileges};
|
|
||||||
|
|
||||||
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
||||||
///
|
|
||||||
/// On Unix, daemon options should be handled before calling this function
|
|
||||||
/// (daemonization must happen before tokio runtime starts).
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub async fn run_with_daemon(
|
|
||||||
daemon_opts: DaemonOptions,
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
run_inner(daemon_opts).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
|
||||||
///
|
|
||||||
/// This is the main entry point for non-daemon mode or when called as a library.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
// Parse CLI to get daemon options even in simple run() path
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let daemon_opts = crate::cli::parse_daemon_args(&args);
|
|
||||||
run_inner(daemon_opts).await
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
run_inner().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn run_inner(
|
|
||||||
daemon_opts: DaemonOptions,
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// Acquire PID file if daemonizing or if explicitly requested
|
|
||||||
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
|
||||||
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
|
||||||
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
|
||||||
if let Err(e) = pf.acquire() {
|
|
||||||
eprintln!("[telemt] {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
Some(pf)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_started_at = Instant::now();
|
let process_started_at = Instant::now();
|
||||||
let process_started_at_epoch_secs = SystemTime::now()
|
let process_started_at_epoch_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -103,17 +56,9 @@ async fn run_inner(
|
|||||||
.as_secs();
|
.as_secs();
|
||||||
let startup_tracker = Arc::new(StartupTracker::new(process_started_at_epoch_secs));
|
let startup_tracker = Arc::new(StartupTracker::new(process_started_at_epoch_secs));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(
|
.start_component(COMPONENT_CONFIG_LOAD, Some("load and validate config".to_string()))
|
||||||
COMPONENT_CONFIG_LOAD,
|
|
||||||
Some("load and validate config".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
let cli_args = parse_cli();
|
let (config_path_cli, data_path, cli_silent, cli_log_level) = parse_cli();
|
||||||
let config_path_cli = cli_args.config_path;
|
|
||||||
let data_path = cli_args.data_path;
|
|
||||||
let cli_silent = cli_args.silent;
|
|
||||||
let cli_log_level = cli_args.log_level;
|
|
||||||
let log_destination = cli_args.log_destination;
|
|
||||||
let startup_cwd = match std::env::current_dir() {
|
let startup_cwd = match std::env::current_dir() {
|
||||||
Ok(cwd) => cwd,
|
Ok(cwd) => cwd,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -132,10 +77,7 @@ async fn run_inner(
|
|||||||
} else {
|
} else {
|
||||||
let default = ProxyConfig::default();
|
let default = ProxyConfig::default();
|
||||||
std::fs::write(&config_path, toml::to_string_pretty(&default).unwrap()).unwrap();
|
std::fs::write(&config_path, toml::to_string_pretty(&default).unwrap()).unwrap();
|
||||||
eprintln!(
|
eprintln!("[telemt] Created default config at {}", config_path.display());
|
||||||
"[telemt] Created default config at {}",
|
|
||||||
config_path.display()
|
|
||||||
);
|
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,36 +94,24 @@ async fn run_inner(
|
|||||||
|
|
||||||
if let Some(ref data_path) = config.general.data_path {
|
if let Some(ref data_path) = config.general.data_path {
|
||||||
if !data_path.is_absolute() {
|
if !data_path.is_absolute() {
|
||||||
eprintln!(
|
eprintln!("[telemt] data_path must be absolute: {}", data_path.display());
|
||||||
"[telemt] data_path must be absolute: {}",
|
|
||||||
data_path.display()
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if data_path.exists() {
|
if data_path.exists() {
|
||||||
if !data_path.is_dir() {
|
if !data_path.is_dir() {
|
||||||
eprintln!(
|
eprintln!("[telemt] data_path exists but is not a directory: {}", data_path.display());
|
||||||
"[telemt] data_path exists but is not a directory: {}",
|
|
||||||
data_path.display()
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
} else if let Err(e) = std::fs::create_dir_all(data_path) {
|
} else {
|
||||||
eprintln!(
|
if let Err(e) = std::fs::create_dir_all(data_path) {
|
||||||
"[telemt] Can't create data_path {}: {}",
|
eprintln!("[telemt] Can't create data_path {}: {}", data_path.display(), e);
|
||||||
data_path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = std::env::set_current_dir(data_path) {
|
if let Err(e) = std::env::set_current_dir(data_path) {
|
||||||
eprintln!(
|
eprintln!("[telemt] Can't use data_path {}: {}", data_path.display(), e);
|
||||||
"[telemt] Can't use data_path {}: {}",
|
|
||||||
data_path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,54 +135,22 @@ async fn run_inner(
|
|||||||
|
|
||||||
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new("info"));
|
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new("info"));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(
|
.start_component(COMPONENT_TRACING_INIT, Some("initialize tracing subscriber".to_string()))
|
||||||
COMPONENT_TRACING_INIT,
|
|
||||||
Some("initialize tracing subscriber".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Initialize logging based on destination
|
// Configure color output based on config
|
||||||
let _logging_guard: Option<crate::logging::LoggingGuard>;
|
|
||||||
match log_destination {
|
|
||||||
crate::logging::LogDestination::Stderr => {
|
|
||||||
// Default: log to stderr (works with systemd journald)
|
|
||||||
let fmt_layer = if config.general.disable_colors {
|
let fmt_layer = if config.general.disable_colors {
|
||||||
fmt::Layer::default().with_ansi(false)
|
fmt::Layer::default().with_ansi(false)
|
||||||
} else {
|
} else {
|
||||||
fmt::Layer::default().with_ansi(true)
|
fmt::Layer::default().with_ansi(true)
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter_layer)
|
.with(filter_layer)
|
||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
.init();
|
.init();
|
||||||
_logging_guard = None;
|
|
||||||
}
|
|
||||||
#[cfg(unix)]
|
|
||||||
crate::logging::LogDestination::Syslog => {
|
|
||||||
// Syslog: for OpenRC/FreeBSD
|
|
||||||
let logging_opts = crate::logging::LoggingOptions {
|
|
||||||
destination: log_destination,
|
|
||||||
disable_colors: true,
|
|
||||||
};
|
|
||||||
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
|
||||||
_logging_guard = Some(guard);
|
|
||||||
}
|
|
||||||
crate::logging::LogDestination::File { .. } => {
|
|
||||||
// File logging with optional rotation
|
|
||||||
let logging_opts = crate::logging::LoggingOptions {
|
|
||||||
destination: log_destination,
|
|
||||||
disable_colors: true,
|
|
||||||
};
|
|
||||||
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
|
||||||
_logging_guard = Some(guard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(
|
.complete_component(COMPONENT_TRACING_INIT, Some("tracing initialized".to_string()))
|
||||||
COMPONENT_TRACING_INIT,
|
|
||||||
Some("tracing initialized".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
info!("Telemt MTProxy v{}", env!("CARGO_PKG_VERSION"));
|
info!("Telemt MTProxy v{}", env!("CARGO_PKG_VERSION"));
|
||||||
@@ -301,7 +199,6 @@ async fn run_inner(
|
|||||||
config.general.upstream_connect_retry_attempts,
|
config.general.upstream_connect_retry_attempts,
|
||||||
config.general.upstream_connect_retry_backoff_ms,
|
config.general.upstream_connect_retry_backoff_ms,
|
||||||
config.general.upstream_connect_budget_ms,
|
config.general.upstream_connect_budget_ms,
|
||||||
config.general.tg_connect,
|
|
||||||
config.general.upstream_unhealthy_fail_threshold,
|
config.general.upstream_unhealthy_fail_threshold,
|
||||||
config.general.upstream_connect_failfast_hard_errors,
|
config.general.upstream_connect_failfast_hard_errors,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
@@ -319,8 +216,7 @@ async fn run_inner(
|
|||||||
config.access.user_max_unique_ips_window_secs,
|
config.access.user_max_unique_ips_window_secs,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if config.access.user_max_unique_ips_global_each > 0
|
if config.access.user_max_unique_ips_global_each > 0 || !config.access.user_max_unique_ips.is_empty()
|
||||||
|| !config.access.user_max_unique_ips.is_empty()
|
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
global_each_limit = config.access.user_max_unique_ips_global_each,
|
global_each_limit = config.access.user_max_unique_ips_global_each,
|
||||||
@@ -347,10 +243,7 @@ async fn run_inner(
|
|||||||
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
let route_runtime = Arc::new(RouteRuntimeController::new(initial_route_mode));
|
||||||
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
let api_me_pool = Arc::new(RwLock::new(None::<Arc<MePool>>));
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(
|
.start_component(COMPONENT_API_BOOTSTRAP, Some("spawn API listener task".to_string()))
|
||||||
COMPONENT_API_BOOTSTRAP,
|
|
||||||
Some("spawn API listener task".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if config.server.api.enabled {
|
if config.server.api.enabled {
|
||||||
@@ -433,10 +326,7 @@ async fn run_inner(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(
|
.start_component(COMPONENT_NETWORK_PROBE, Some("probe network capabilities".to_string()))
|
||||||
COMPONENT_NETWORK_PROBE,
|
|
||||||
Some("probe network capabilities".to_string()),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
let probe = run_probe(
|
let probe = run_probe(
|
||||||
&config.network,
|
&config.network,
|
||||||
@@ -449,8 +339,11 @@ async fn run_inner(
|
|||||||
probe.detected_ipv4.map(IpAddr::V4),
|
probe.detected_ipv4.map(IpAddr::V4),
|
||||||
probe.detected_ipv6.map(IpAddr::V6),
|
probe.detected_ipv6.map(IpAddr::V6),
|
||||||
));
|
));
|
||||||
let decision =
|
let decision = decide_network_capabilities(
|
||||||
decide_network_capabilities(&config.network, &probe, config.general.middle_proxy_nat_ip);
|
&config.network,
|
||||||
|
&probe,
|
||||||
|
config.general.middle_proxy_nat_ip,
|
||||||
|
);
|
||||||
log_probe_result(&probe, &decision);
|
log_probe_result(&probe, &decision);
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(
|
.complete_component(
|
||||||
@@ -553,16 +446,24 @@ async fn run_inner(
|
|||||||
|
|
||||||
// If ME failed to initialize, force direct-only mode.
|
// If ME failed to initialize, force direct-only mode.
|
||||||
if me_pool.is_some() {
|
if me_pool.is_some() {
|
||||||
startup_tracker.set_transport_mode("middle_proxy").await;
|
startup_tracker
|
||||||
startup_tracker.set_degraded(false).await;
|
.set_transport_mode("middle_proxy")
|
||||||
|
.await;
|
||||||
|
startup_tracker
|
||||||
|
.set_degraded(false)
|
||||||
|
.await;
|
||||||
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
info!("Transport: Middle-End Proxy - all DC-over-RPC");
|
||||||
} else {
|
} else {
|
||||||
let _ = use_middle_proxy;
|
let _ = use_middle_proxy;
|
||||||
use_middle_proxy = false;
|
use_middle_proxy = false;
|
||||||
// Make runtime config reflect direct-only mode for handlers.
|
// Make runtime config reflect direct-only mode for handlers.
|
||||||
config.general.use_middle_proxy = false;
|
config.general.use_middle_proxy = false;
|
||||||
startup_tracker.set_transport_mode("direct").await;
|
startup_tracker
|
||||||
startup_tracker.set_degraded(true).await;
|
.set_transport_mode("direct")
|
||||||
|
.await;
|
||||||
|
startup_tracker
|
||||||
|
.set_degraded(true)
|
||||||
|
.await;
|
||||||
if me2dc_fallback {
|
if me2dc_fallback {
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.set_me_status(StartupMeStatus::Failed, "fallback_to_direct")
|
.set_me_status(StartupMeStatus::Failed, "fallback_to_direct")
|
||||||
@@ -662,14 +563,6 @@ async fn run_inner(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop privileges after binding sockets (which may require root for port < 1024)
|
|
||||||
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
|
|
||||||
if let Err(e) = drop_privileges(daemon_opts.user.as_deref(), daemon_opts.group.as_deref()) {
|
|
||||||
error!(error = %e, "Failed to drop privileges");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime_tasks::apply_runtime_log_filter(
|
runtime_tasks::apply_runtime_log_filter(
|
||||||
has_rust_log,
|
has_rust_log,
|
||||||
&effective_log_level,
|
&effective_log_level,
|
||||||
@@ -690,9 +583,6 @@ async fn run_inner(
|
|||||||
|
|
||||||
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
||||||
|
|
||||||
// Spawn signal handlers for SIGUSR1/SIGUSR2 (non-shutdown signals)
|
|
||||||
shutdown::spawn_signal_handlers(stats.clone(), process_started_at);
|
|
||||||
|
|
||||||
listeners::spawn_tcp_accept_loops(
|
listeners::spawn_tcp_accept_loops(
|
||||||
listeners,
|
listeners,
|
||||||
config_rx.clone(),
|
config_rx.clone(),
|
||||||
@@ -710,7 +600,7 @@ async fn run_inner(
|
|||||||
max_connections.clone(),
|
max_connections.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
shutdown::wait_for_shutdown(process_started_at, me_pool, stats).await;
|
shutdown::wait_for_shutdown(process_started_at, me_pool).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,21 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use tokio::sync::{mpsc, watch};
|
use tokio::sync::{mpsc, watch};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use tracing_subscriber::EnvFilter;
|
|
||||||
use tracing_subscriber::reload;
|
use tracing_subscriber::reload;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::config::hot_reload::spawn_config_watcher;
|
|
||||||
use crate::config::{LogLevel, ProxyConfig};
|
use crate::config::{LogLevel, ProxyConfig};
|
||||||
|
use crate::config::hot_reload::spawn_config_watcher;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::metrics;
|
use crate::metrics;
|
||||||
use crate::network::probe::NetworkProbe;
|
use crate::network::probe::NetworkProbe;
|
||||||
use crate::startup::{
|
use crate::startup::{COMPONENT_CONFIG_WATCHER_START, COMPONENT_METRICS_START, COMPONENT_RUNTIME_READY, StartupTracker};
|
||||||
COMPONENT_CONFIG_WATCHER_START, COMPONENT_METRICS_START, COMPONENT_RUNTIME_READY,
|
|
||||||
StartupTracker,
|
|
||||||
};
|
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::telemetry::TelemetryPolicy;
|
use crate::stats::telemetry::TelemetryPolicy;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use crate::transport::middle_proxy::{MePool, MeReinitTrigger};
|
use crate::transport::middle_proxy::{MePool, MeReinitTrigger};
|
||||||
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
use super::helpers::write_beobachten_snapshot;
|
use super::helpers::write_beobachten_snapshot;
|
||||||
|
|
||||||
@@ -82,8 +79,10 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
Some("spawn config hot-reload watcher".to_string()),
|
Some("spawn config hot-reload watcher".to_string()),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let (config_rx, log_level_rx): (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) =
|
let (config_rx, log_level_rx): (
|
||||||
spawn_config_watcher(
|
watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
watch::Receiver<LogLevel>,
|
||||||
|
) = spawn_config_watcher(
|
||||||
config_path.to_path_buf(),
|
config_path.to_path_buf(),
|
||||||
config.clone(),
|
config.clone(),
|
||||||
detected_ip_v4,
|
detected_ip_v4,
|
||||||
@@ -115,8 +114,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let cfg = config_rx_policy.borrow_and_update().clone();
|
let cfg = config_rx_policy.borrow_and_update().clone();
|
||||||
stats_policy
|
stats_policy.apply_telemetry_policy(TelemetryPolicy::from_config(&cfg.general.telemetry));
|
||||||
.apply_telemetry_policy(TelemetryPolicy::from_config(&cfg.general.telemetry));
|
|
||||||
if let Some(pool) = &me_pool_for_policy {
|
if let Some(pool) = &me_pool_for_policy {
|
||||||
pool.update_runtime_transport_policy(
|
pool.update_runtime_transport_policy(
|
||||||
cfg.general.me_socks_kdf_policy,
|
cfg.general.me_socks_kdf_policy,
|
||||||
@@ -132,11 +130,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let ip_tracker_policy = ip_tracker.clone();
|
let ip_tracker_policy = ip_tracker.clone();
|
||||||
let mut config_rx_ip_limits = config_rx.clone();
|
let mut config_rx_ip_limits = config_rx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut prev_limits = config_rx_ip_limits
|
let mut prev_limits = config_rx_ip_limits.borrow().access.user_max_unique_ips.clone();
|
||||||
.borrow()
|
|
||||||
.access
|
|
||||||
.user_max_unique_ips
|
|
||||||
.clone();
|
|
||||||
let mut prev_global_each = config_rx_ip_limits
|
let mut prev_global_each = config_rx_ip_limits
|
||||||
.borrow()
|
.borrow()
|
||||||
.access
|
.access
|
||||||
@@ -189,9 +183,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let sleep_secs = cfg.general.beobachten_flush_secs.max(1);
|
let sleep_secs = cfg.general.beobachten_flush_secs.max(1);
|
||||||
|
|
||||||
if cfg.general.beobachten {
|
if cfg.general.beobachten {
|
||||||
let ttl = std::time::Duration::from_secs(
|
let ttl = std::time::Duration::from_secs(cfg.general.beobachten_minutes.saturating_mul(60));
|
||||||
cfg.general.beobachten_minutes.saturating_mul(60),
|
|
||||||
);
|
|
||||||
let path = cfg.general.beobachten_file.clone();
|
let path = cfg.general.beobachten_file.clone();
|
||||||
let snapshot = beobachten_writer.snapshot_text(ttl);
|
let snapshot = beobachten_writer.snapshot_text(ttl);
|
||||||
if let Err(e) = write_beobachten_snapshot(&path, &snapshot).await {
|
if let Err(e) = write_beobachten_snapshot(&path, &snapshot).await {
|
||||||
@@ -235,10 +227,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
|||||||
let config_rx_clone_rot = config_rx.clone();
|
let config_rx_clone_rot = config_rx.clone();
|
||||||
let reinit_tx_rotation = reinit_tx.clone();
|
let reinit_tx_rotation = reinit_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
crate::transport::middle_proxy::me_rotation_task(
|
crate::transport::middle_proxy::me_rotation_task(config_rx_clone_rot, reinit_tx_rotation)
|
||||||
config_rx_clone_rot,
|
|
||||||
reinit_tx_rotation,
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -323,12 +312,10 @@ pub(crate) async fn spawn_metrics_if_configured(
|
|||||||
let config_rx_metrics = config_rx.clone();
|
let config_rx_metrics = config_rx.clone();
|
||||||
let ip_tracker_metrics = ip_tracker.clone();
|
let ip_tracker_metrics = ip_tracker.clone();
|
||||||
let whitelist = config.server.metrics_whitelist.clone();
|
let whitelist = config.server.metrics_whitelist.clone();
|
||||||
let listen_backlog = config.server.listen_backlog;
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
metrics::serve(
|
metrics::serve(
|
||||||
port,
|
port,
|
||||||
listen,
|
listen,
|
||||||
listen_backlog,
|
|
||||||
stats,
|
stats,
|
||||||
beobachten,
|
beobachten,
|
||||||
ip_tracker_metrics,
|
ip_tracker_metrics,
|
||||||
|
|||||||
+5
-169
@@ -1,98 +1,20 @@
|
|||||||
//! Shutdown and signal handling for telemt.
|
|
||||||
//!
|
|
||||||
//! Handles graceful shutdown on various signals:
|
|
||||||
//! - SIGINT (Ctrl+C) / SIGTERM: Graceful shutdown
|
|
||||||
//! - SIGQUIT: Graceful shutdown with stats dump
|
|
||||||
//! - SIGUSR1: Reserved for log rotation (logs acknowledgment)
|
|
||||||
//! - SIGUSR2: Dump runtime status to log
|
|
||||||
//!
|
|
||||||
//! SIGHUP is handled separately in config/hot_reload.rs for config reload.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
#[cfg(unix)]
|
use tracing::{error, info, warn};
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
|
||||||
use super::helpers::{format_uptime, unit_label};
|
use super::helpers::{format_uptime, unit_label};
|
||||||
|
|
||||||
/// Signal that triggered shutdown.
|
pub(crate) async fn wait_for_shutdown(process_started_at: Instant, me_pool: Option<Arc<MePool>>) {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
match signal::ctrl_c().await {
|
||||||
pub enum ShutdownSignal {
|
Ok(()) => {
|
||||||
/// SIGINT (Ctrl+C)
|
|
||||||
Interrupt,
|
|
||||||
/// SIGTERM
|
|
||||||
Terminate,
|
|
||||||
/// SIGQUIT (with stats dump)
|
|
||||||
Quit,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ShutdownSignal {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ShutdownSignal::Interrupt => write!(f, "SIGINT"),
|
|
||||||
ShutdownSignal::Terminate => write!(f, "SIGTERM"),
|
|
||||||
ShutdownSignal::Quit => write!(f, "SIGQUIT"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits for a shutdown signal and performs graceful shutdown.
|
|
||||||
pub(crate) async fn wait_for_shutdown(
|
|
||||||
process_started_at: Instant,
|
|
||||||
me_pool: Option<Arc<MePool>>,
|
|
||||||
stats: Arc<Stats>,
|
|
||||||
) {
|
|
||||||
let signal = wait_for_shutdown_signal().await;
|
|
||||||
perform_shutdown(signal, process_started_at, me_pool, &stats).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits for any shutdown signal (SIGINT, SIGTERM, SIGQUIT).
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
|
||||||
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
|
|
||||||
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
|
|
||||||
let mut sigquit = signal(SignalKind::quit()).expect("Failed to register SIGQUIT handler");
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = sigint.recv() => ShutdownSignal::Interrupt,
|
|
||||||
_ = sigterm.recv() => ShutdownSignal::Terminate,
|
|
||||||
_ = sigquit.recv() => ShutdownSignal::Quit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
|
||||||
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
|
|
||||||
ShutdownSignal::Interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs graceful shutdown sequence.
|
|
||||||
async fn perform_shutdown(
|
|
||||||
signal: ShutdownSignal,
|
|
||||||
process_started_at: Instant,
|
|
||||||
me_pool: Option<Arc<MePool>>,
|
|
||||||
stats: &Stats,
|
|
||||||
) {
|
|
||||||
let shutdown_started_at = Instant::now();
|
let shutdown_started_at = Instant::now();
|
||||||
info!(signal = %signal, "Received shutdown signal");
|
|
||||||
|
|
||||||
// Dump stats if SIGQUIT
|
|
||||||
if signal == ShutdownSignal::Quit {
|
|
||||||
dump_stats(stats, process_started_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Shutting down...");
|
info!("Shutting down...");
|
||||||
let uptime_secs = process_started_at.elapsed().as_secs();
|
let uptime_secs = process_started_at.elapsed().as_secs();
|
||||||
info!("Uptime: {}", format_uptime(uptime_secs));
|
info!("Uptime: {}", format_uptime(uptime_secs));
|
||||||
|
|
||||||
// Graceful ME pool shutdown
|
|
||||||
if let Some(pool) = &me_pool {
|
if let Some(pool) = &me_pool {
|
||||||
match tokio::time::timeout(Duration::from_secs(2), pool.shutdown_send_close_conn_all())
|
match tokio::time::timeout(Duration::from_secs(2), pool.shutdown_send_close_conn_all())
|
||||||
.await
|
.await
|
||||||
@@ -108,7 +30,6 @@ async fn perform_shutdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
||||||
info!(
|
info!(
|
||||||
"Shutdown completed successfully in {} {}.",
|
"Shutdown completed successfully in {} {}.",
|
||||||
@@ -116,91 +37,6 @@ async fn perform_shutdown(
|
|||||||
unit_label(shutdown_secs, "second", "seconds")
|
unit_label(shutdown_secs, "second", "seconds")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(e) => error!("Signal error: {}", e),
|
||||||
/// Dumps runtime statistics to the log.
|
|
||||||
fn dump_stats(stats: &Stats, process_started_at: Instant) {
|
|
||||||
let uptime_secs = process_started_at.elapsed().as_secs();
|
|
||||||
|
|
||||||
info!("=== Runtime Statistics Dump ===");
|
|
||||||
info!("Uptime: {}", format_uptime(uptime_secs));
|
|
||||||
|
|
||||||
// Connection stats
|
|
||||||
info!(
|
|
||||||
"Connections: total={}, current={} (direct={}, me={}), bad={}",
|
|
||||||
stats.get_connects_all(),
|
|
||||||
stats.get_current_connections_total(),
|
|
||||||
stats.get_current_connections_direct(),
|
|
||||||
stats.get_current_connections_me(),
|
|
||||||
stats.get_connects_bad(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ME pool stats
|
|
||||||
info!(
|
|
||||||
"ME keepalive: sent={}, pong={}, failed={}, timeout={}",
|
|
||||||
stats.get_me_keepalive_sent(),
|
|
||||||
stats.get_me_keepalive_pong(),
|
|
||||||
stats.get_me_keepalive_failed(),
|
|
||||||
stats.get_me_keepalive_timeout(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Relay stats
|
|
||||||
info!(
|
|
||||||
"Relay idle: soft_mark={}, hard_close={}, pressure_evict={}",
|
|
||||||
stats.get_relay_idle_soft_mark_total(),
|
|
||||||
stats.get_relay_idle_hard_close_total(),
|
|
||||||
stats.get_relay_pressure_evict_total(),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("=== End Statistics Dump ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawns a background task to handle operational signals (SIGUSR1, SIGUSR2).
|
|
||||||
///
|
|
||||||
/// These signals don't trigger shutdown but perform specific actions:
|
|
||||||
/// - SIGUSR1: Log rotation acknowledgment (for external log rotation tools)
|
|
||||||
/// - SIGUSR2: Dump runtime status to log
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub(crate) fn spawn_signal_handlers(stats: Arc<Stats>, process_started_at: Instant) {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut sigusr1 =
|
|
||||||
signal(SignalKind::user_defined1()).expect("Failed to register SIGUSR1 handler");
|
|
||||||
let mut sigusr2 =
|
|
||||||
signal(SignalKind::user_defined2()).expect("Failed to register SIGUSR2 handler");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = sigusr1.recv() => {
|
|
||||||
handle_sigusr1();
|
|
||||||
}
|
|
||||||
_ = sigusr2.recv() => {
|
|
||||||
handle_sigusr2(&stats, process_started_at);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// No-op on non-Unix platforms.
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub(crate) fn spawn_signal_handlers(_stats: Arc<Stats>, _process_started_at: Instant) {
|
|
||||||
// No SIGUSR1/SIGUSR2 on non-Unix
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles SIGUSR1 - log rotation signal.
|
|
||||||
///
|
|
||||||
/// This signal is typically sent by logrotate or similar tools after
|
|
||||||
/// rotating log files. Since tracing-subscriber doesn't natively support
|
|
||||||
/// reopening files, we just acknowledge the signal. If file logging is
|
|
||||||
/// added in the future, this would reopen log file handles.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn handle_sigusr1() {
|
|
||||||
info!("SIGUSR1 received - log rotation acknowledged");
|
|
||||||
// Future: If using file-based logging, reopen file handles here
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles SIGUSR2 - dump runtime status.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn handle_sigusr2(stats: &Stats, process_started_at: Instant) {
|
|
||||||
info!("SIGUSR2 received - dumping runtime status");
|
|
||||||
dump_stats(stats, process_started_at);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use tracing::warn;
|
|||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::startup::{COMPONENT_TLS_FRONT_BOOTSTRAP, StartupTracker};
|
use crate::startup::{COMPONENT_TLS_FRONT_BOOTSTRAP, StartupTracker};
|
||||||
use crate::tls_front::TlsFrontCache;
|
use crate::tls_front::TlsFrontCache;
|
||||||
use crate::tls_front::fetcher::TlsFetchStrategy;
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
pub(crate) async fn bootstrap_tls_front(
|
pub(crate) async fn bootstrap_tls_front(
|
||||||
@@ -41,17 +40,7 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let mask_unix_sock = config.censorship.mask_unix_sock.clone();
|
let mask_unix_sock = config.censorship.mask_unix_sock.clone();
|
||||||
let tls_fetch_scope = (!config.censorship.tls_fetch_scope.is_empty())
|
let tls_fetch_scope = (!config.censorship.tls_fetch_scope.is_empty())
|
||||||
.then(|| config.censorship.tls_fetch_scope.clone());
|
.then(|| config.censorship.tls_fetch_scope.clone());
|
||||||
let tls_fetch = config.censorship.tls_fetch.clone();
|
let fetch_timeout = Duration::from_secs(5);
|
||||||
let fetch_strategy = TlsFetchStrategy {
|
|
||||||
profiles: tls_fetch.profiles,
|
|
||||||
strict_route: tls_fetch.strict_route,
|
|
||||||
attempt_timeout: Duration::from_millis(tls_fetch.attempt_timeout_ms.max(1)),
|
|
||||||
total_budget: Duration::from_millis(tls_fetch.total_budget_ms.max(1)),
|
|
||||||
grease_enabled: tls_fetch.grease_enabled,
|
|
||||||
deterministic: tls_fetch.deterministic,
|
|
||||||
profile_cache_ttl: Duration::from_secs(tls_fetch.profile_cache_ttl_secs),
|
|
||||||
};
|
|
||||||
let fetch_timeout = fetch_strategy.total_budget;
|
|
||||||
|
|
||||||
let cache_initial = cache.clone();
|
let cache_initial = cache.clone();
|
||||||
let domains_initial = tls_domains.to_vec();
|
let domains_initial = tls_domains.to_vec();
|
||||||
@@ -59,7 +48,6 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let unix_sock_initial = mask_unix_sock.clone();
|
let unix_sock_initial = mask_unix_sock.clone();
|
||||||
let scope_initial = tls_fetch_scope.clone();
|
let scope_initial = tls_fetch_scope.clone();
|
||||||
let upstream_initial = upstream_manager.clone();
|
let upstream_initial = upstream_manager.clone();
|
||||||
let strategy_initial = fetch_strategy.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut join = tokio::task::JoinSet::new();
|
let mut join = tokio::task::JoinSet::new();
|
||||||
for domain in domains_initial {
|
for domain in domains_initial {
|
||||||
@@ -68,13 +56,12 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let unix_sock_domain = unix_sock_initial.clone();
|
let unix_sock_domain = unix_sock_initial.clone();
|
||||||
let scope_domain = scope_initial.clone();
|
let scope_domain = scope_initial.clone();
|
||||||
let upstream_domain = upstream_initial.clone();
|
let upstream_domain = upstream_initial.clone();
|
||||||
let strategy_domain = strategy_initial.clone();
|
|
||||||
join.spawn(async move {
|
join.spawn(async move {
|
||||||
match crate::tls_front::fetcher::fetch_real_tls_with_strategy(
|
match crate::tls_front::fetcher::fetch_real_tls(
|
||||||
&host_domain,
|
&host_domain,
|
||||||
port,
|
port,
|
||||||
&domain,
|
&domain,
|
||||||
&strategy_domain,
|
fetch_timeout,
|
||||||
Some(upstream_domain),
|
Some(upstream_domain),
|
||||||
scope_domain.as_deref(),
|
scope_domain.as_deref(),
|
||||||
proxy_protocol,
|
proxy_protocol,
|
||||||
@@ -120,7 +107,6 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let unix_sock_refresh = mask_unix_sock.clone();
|
let unix_sock_refresh = mask_unix_sock.clone();
|
||||||
let scope_refresh = tls_fetch_scope.clone();
|
let scope_refresh = tls_fetch_scope.clone();
|
||||||
let upstream_refresh = upstream_manager.clone();
|
let upstream_refresh = upstream_manager.clone();
|
||||||
let strategy_refresh = fetch_strategy.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let base_secs = rand::rng().random_range(4 * 3600..=6 * 3600);
|
let base_secs = rand::rng().random_range(4 * 3600..=6 * 3600);
|
||||||
@@ -134,13 +120,12 @@ pub(crate) async fn bootstrap_tls_front(
|
|||||||
let unix_sock_domain = unix_sock_refresh.clone();
|
let unix_sock_domain = unix_sock_refresh.clone();
|
||||||
let scope_domain = scope_refresh.clone();
|
let scope_domain = scope_refresh.clone();
|
||||||
let upstream_domain = upstream_refresh.clone();
|
let upstream_domain = upstream_refresh.clone();
|
||||||
let strategy_domain = strategy_refresh.clone();
|
|
||||||
join.spawn(async move {
|
join.spawn(async move {
|
||||||
match crate::tls_front::fetcher::fetch_real_tls_with_strategy(
|
match crate::tls_front::fetcher::fetch_real_tls(
|
||||||
&host_domain,
|
&host_domain,
|
||||||
port,
|
port,
|
||||||
&domain,
|
&domain,
|
||||||
&strategy_domain,
|
fetch_timeout,
|
||||||
Some(upstream_domain),
|
Some(upstream_domain),
|
||||||
scope_domain.as_deref(),
|
scope_domain.as_deref(),
|
||||||
proxy_protocol,
|
proxy_protocol,
|
||||||
|
|||||||
+5
-54
@@ -4,26 +4,19 @@ mod api;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
#[cfg(unix)]
|
|
||||||
mod daemon;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod ip_tracker;
|
mod ip_tracker;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
|
#[path = "tests/ip_tracker_regression_tests.rs"]
|
||||||
mod ip_tracker_encapsulation_adversarial_tests;
|
mod ip_tracker_regression_tests;
|
||||||
#[cfg(test)]
|
#[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)]
|
|
||||||
#[path = "tests/ip_tracker_regression_tests.rs"]
|
|
||||||
mod ip_tracker_regression_tests;
|
|
||||||
mod logging;
|
|
||||||
mod maestro;
|
mod maestro;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod network;
|
mod network;
|
||||||
mod protocol;
|
mod protocol;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
mod service;
|
|
||||||
mod startup;
|
mod startup;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod stream;
|
mod stream;
|
||||||
@@ -31,49 +24,7 @@ mod tls_front;
|
|||||||
mod transport;
|
mod transport;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
#[tokio::main]
|
||||||
// Install rustls crypto provider early
|
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
maestro::run().await
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let cmd = cli::parse_command(&args);
|
|
||||||
|
|
||||||
// Handle subcommands that don't need the server (stop, reload, status, init)
|
|
||||||
if let Some(exit_code) = cli::execute_subcommand(&cmd) {
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let daemon_opts = cmd.daemon_opts;
|
|
||||||
|
|
||||||
// Daemonize BEFORE runtime
|
|
||||||
if daemon_opts.should_daemonize() {
|
|
||||||
match daemon::daemonize(daemon_opts.working_dir.as_deref()) {
|
|
||||||
Ok(daemon::DaemonizeResult::Parent) => {
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Ok(daemon::DaemonizeResult::Child) => {
|
|
||||||
// continue
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Daemonization failed: {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(maestro::run_with_daemon(daemon_opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(maestro::run())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+112
-851
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,9 @@ fn parse_ip_spec(ip_spec: &str) -> Result<IpAddr> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ip = ip_spec.parse::<IpAddr>().map_err(|_| {
|
let ip = ip_spec.parse::<IpAddr>().map_err(|_| {
|
||||||
ProxyError::Config(format!("network.dns_overrides IP is invalid: '{ip_spec}'"))
|
ProxyError::Config(format!(
|
||||||
|
"network.dns_overrides IP is invalid: '{ip_spec}'"
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
if matches!(ip, IpAddr::V6(_)) {
|
if matches!(ip, IpAddr::V6(_)) {
|
||||||
return Err(ProxyError::Config(format!(
|
return Err(ProxyError::Config(format!(
|
||||||
@@ -101,9 +103,9 @@ pub fn validate_entries(entries: &[String]) -> Result<()> {
|
|||||||
/// Replace runtime DNS overrides with a new validated snapshot.
|
/// Replace runtime DNS overrides with a new validated snapshot.
|
||||||
pub fn install_entries(entries: &[String]) -> Result<()> {
|
pub fn install_entries(entries: &[String]) -> Result<()> {
|
||||||
let parsed = parse_entries(entries)?;
|
let parsed = parse_entries(entries)?;
|
||||||
let mut guard = overrides_store().write().map_err(|_| {
|
let mut guard = overrides_store()
|
||||||
ProxyError::Config("network.dns_overrides runtime lock is poisoned".to_string())
|
.write()
|
||||||
})?;
|
.map_err(|_| ProxyError::Config("network.dns_overrides runtime lock is poisoned".to_string()))?;
|
||||||
*guard = parsed;
|
*guard = parsed;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-48
@@ -1,5 +1,4 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
#![allow(clippy::items_after_test_module)]
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
||||||
@@ -11,9 +10,7 @@ use tracing::{debug, info, warn};
|
|||||||
|
|
||||||
use crate::config::{NetworkConfig, UpstreamConfig, UpstreamType};
|
use crate::config::{NetworkConfig, UpstreamConfig, UpstreamType};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::network::stun::{
|
use crate::network::stun::{stun_probe_family_with_bind, DualStunResult, IpFamily, StunProbeResult};
|
||||||
DualStunResult, IpFamily, StunProbeResult, stun_probe_family_with_bind,
|
|
||||||
};
|
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
@@ -81,7 +78,12 @@ pub async fn run_probe(
|
|||||||
warn!("STUN probe is enabled but network.stun_servers is empty");
|
warn!("STUN probe is enabled but network.stun_servers is empty");
|
||||||
DualStunResult::default()
|
DualStunResult::default()
|
||||||
} else {
|
} else {
|
||||||
probe_stun_servers_parallel(&servers, stun_nat_probe_concurrency.max(1), None, None)
|
probe_stun_servers_parallel(
|
||||||
|
&servers,
|
||||||
|
stun_nat_probe_concurrency.max(1),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
} else if nat_probe {
|
} else if nat_probe {
|
||||||
@@ -97,8 +99,7 @@ pub async fn run_probe(
|
|||||||
let UpstreamType::Direct {
|
let UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
} = &upstream.upstream_type
|
} = &upstream.upstream_type else {
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(addrs) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
if let Some(addrs) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
||||||
@@ -198,11 +199,12 @@ pub async fn run_probe(
|
|||||||
if nat_probe
|
if nat_probe
|
||||||
&& probe.reflected_ipv4.is_none()
|
&& probe.reflected_ipv4.is_none()
|
||||||
&& probe.detected_ipv4.map(is_bogon_v4).unwrap_or(false)
|
&& probe.detected_ipv4.map(is_bogon_v4).unwrap_or(false)
|
||||||
&& let Some(public_ip) = detect_public_ipv4_http(&config.http_ip_detect_urls).await
|
|
||||||
{
|
{
|
||||||
|
if let Some(public_ip) = detect_public_ipv4_http(&config.http_ip_detect_urls).await {
|
||||||
probe.reflected_ipv4 = Some(SocketAddr::new(IpAddr::V4(public_ip), 0));
|
probe.reflected_ipv4 = Some(SocketAddr::new(IpAddr::V4(public_ip), 0));
|
||||||
info!(public_ip = %public_ip, "STUN unavailable, using HTTP public IPv4 fallback");
|
info!(public_ip = %public_ip, "STUN unavailable, using HTTP public IPv4 fallback");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
probe.ipv4_nat_detected = match (probe.detected_ipv4, probe.reflected_ipv4) {
|
probe.ipv4_nat_detected = match (probe.detected_ipv4, probe.reflected_ipv4) {
|
||||||
(Some(det), Some(reflected)) => det != reflected.ip(),
|
(Some(det), Some(reflected)) => det != reflected.ip(),
|
||||||
@@ -215,20 +217,12 @@ pub async fn run_probe(
|
|||||||
|
|
||||||
probe.ipv4_usable = config.ipv4
|
probe.ipv4_usable = config.ipv4
|
||||||
&& probe.detected_ipv4.is_some()
|
&& probe.detected_ipv4.is_some()
|
||||||
&& (!probe.ipv4_is_bogon
|
&& (!probe.ipv4_is_bogon || probe.reflected_ipv4.map(|r| !is_bogon(r.ip())).unwrap_or(false));
|
||||||
|| probe
|
|
||||||
.reflected_ipv4
|
|
||||||
.map(|r| !is_bogon(r.ip()))
|
|
||||||
.unwrap_or(false));
|
|
||||||
|
|
||||||
let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some());
|
let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some());
|
||||||
probe.ipv6_usable = ipv6_enabled
|
probe.ipv6_usable = ipv6_enabled
|
||||||
&& probe.detected_ipv6.is_some()
|
&& probe.detected_ipv6.is_some()
|
||||||
&& (!probe.ipv6_is_bogon
|
&& (!probe.ipv6_is_bogon || probe.reflected_ipv6.map(|r| !is_bogon(r.ip())).unwrap_or(false));
|
||||||
|| probe
|
|
||||||
.reflected_ipv6
|
|
||||||
.map(|r| !is_bogon(r.ip()))
|
|
||||||
.unwrap_or(false));
|
|
||||||
|
|
||||||
Ok(probe)
|
Ok(probe)
|
||||||
}
|
}
|
||||||
@@ -286,6 +280,8 @@ async fn probe_stun_servers_parallel(
|
|||||||
while next_idx < servers.len() && join_set.len() < concurrency {
|
while next_idx < servers.len() && join_set.len() < concurrency {
|
||||||
let stun_addr = servers[next_idx].clone();
|
let stun_addr = servers[next_idx].clone();
|
||||||
next_idx += 1;
|
next_idx += 1;
|
||||||
|
let bind_v4 = bind_v4;
|
||||||
|
let bind_v6 = bind_v6;
|
||||||
join_set.spawn(async move {
|
join_set.spawn(async move {
|
||||||
let res = timeout(STUN_BATCH_TIMEOUT, async {
|
let res = timeout(STUN_BATCH_TIMEOUT, async {
|
||||||
let v4 = stun_probe_family_with_bind(&stun_addr, IpFamily::V4, bind_v4).await?;
|
let v4 = stun_probe_family_with_bind(&stun_addr, IpFamily::V4, bind_v4).await?;
|
||||||
@@ -304,15 +300,11 @@ async fn probe_stun_servers_parallel(
|
|||||||
match task {
|
match task {
|
||||||
Ok((stun_addr, Ok(Ok(result)))) => {
|
Ok((stun_addr, Ok(Ok(result)))) => {
|
||||||
if let Some(v4) = result.v4 {
|
if let Some(v4) = result.v4 {
|
||||||
let entry = best_v4_by_ip
|
let entry = best_v4_by_ip.entry(v4.reflected_addr.ip()).or_insert((0, v4));
|
||||||
.entry(v4.reflected_addr.ip())
|
|
||||||
.or_insert((0, v4));
|
|
||||||
entry.0 += 1;
|
entry.0 += 1;
|
||||||
}
|
}
|
||||||
if let Some(v6) = result.v6 {
|
if let Some(v6) = result.v6 {
|
||||||
let entry = best_v6_by_ip
|
let entry = best_v6_by_ip.entry(v6.reflected_addr.ip()).or_insert((0, v6));
|
||||||
.entry(v6.reflected_addr.ip())
|
|
||||||
.or_insert((0, v6));
|
|
||||||
entry.0 += 1;
|
entry.0 += 1;
|
||||||
}
|
}
|
||||||
if result.v4.is_some() || result.v6.is_some() {
|
if result.v4.is_some() || result.v6.is_some() {
|
||||||
@@ -332,11 +324,17 @@ async fn probe_stun_servers_parallel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out = DualStunResult::default();
|
let mut out = DualStunResult::default();
|
||||||
if let Some((_, best)) = best_v4_by_ip.into_values().max_by_key(|(count, _)| *count) {
|
if let Some((_, best)) = best_v4_by_ip
|
||||||
|
.into_values()
|
||||||
|
.max_by_key(|(count, _)| *count)
|
||||||
|
{
|
||||||
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
||||||
out.v4 = Some(best);
|
out.v4 = Some(best);
|
||||||
}
|
}
|
||||||
if let Some((_, best)) = best_v6_by_ip.into_values().max_by_key(|(count, _)| *count) {
|
if let Some((_, best)) = best_v6_by_ip
|
||||||
|
.into_values()
|
||||||
|
.max_by_key(|(count, _)| *count)
|
||||||
|
{
|
||||||
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
info!("STUN-Quorum reached, IP: {}", best.reflected_addr.ip());
|
||||||
out.v6 = Some(best);
|
out.v6 = Some(best);
|
||||||
}
|
}
|
||||||
@@ -349,8 +347,7 @@ pub fn decide_network_capabilities(
|
|||||||
middle_proxy_nat_ip: Option<IpAddr>,
|
middle_proxy_nat_ip: Option<IpAddr>,
|
||||||
) -> NetworkDecision {
|
) -> NetworkDecision {
|
||||||
let ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
|
let ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
|
||||||
let ipv6_dc =
|
let ipv6_dc = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
|
||||||
config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
|
|
||||||
let nat_ip_v4 = matches!(middle_proxy_nat_ip, Some(IpAddr::V4(_)));
|
let nat_ip_v4 = matches!(middle_proxy_nat_ip, Some(IpAddr::V4(_)));
|
||||||
let nat_ip_v6 = matches!(middle_proxy_nat_ip, Some(IpAddr::V6(_)));
|
let nat_ip_v6 = matches!(middle_proxy_nat_ip, Some(IpAddr::V6(_)));
|
||||||
|
|
||||||
@@ -537,26 +534,10 @@ pub fn is_bogon_v6(ip: Ipv6Addr) -> bool {
|
|||||||
|
|
||||||
pub fn log_probe_result(probe: &NetworkProbe, decision: &NetworkDecision) {
|
pub fn log_probe_result(probe: &NetworkProbe, decision: &NetworkDecision) {
|
||||||
info!(
|
info!(
|
||||||
ipv4 = probe
|
ipv4 = probe.detected_ipv4.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
||||||
.detected_ipv4
|
ipv6 = probe.detected_ipv6.as_ref().map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
||||||
.as_ref()
|
reflected_v4 = probe.reflected_ipv4.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()),
|
||||||
.map(|v| v.to_string())
|
reflected_v6 = probe.reflected_ipv6.as_ref().map(|v| v.ip().to_string()).unwrap_or_else(|| "-".into()),
|
||||||
.unwrap_or_else(|| "-".into()),
|
|
||||||
ipv6 = probe
|
|
||||||
.detected_ipv6
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
.unwrap_or_else(|| "-".into()),
|
|
||||||
reflected_v4 = probe
|
|
||||||
.reflected_ipv4
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.ip().to_string())
|
|
||||||
.unwrap_or_else(|| "-".into()),
|
|
||||||
reflected_v6 = probe
|
|
||||||
.reflected_ipv6
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.ip().to_string())
|
|
||||||
.unwrap_or_else(|| "-".into()),
|
|
||||||
ipv4_bogon = probe.ipv4_is_bogon,
|
ipv4_bogon = probe.ipv4_is_bogon,
|
||||||
ipv6_bogon = probe.ipv6_is_bogon,
|
ipv6_bogon = probe.ipv6_is_bogon,
|
||||||
ipv4_me = decision.ipv4_me,
|
ipv4_me = decision.ipv4_me,
|
||||||
|
|||||||
+12
-20
@@ -4,8 +4,8 @@
|
|||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use tokio::net::{UdpSocket, lookup_host};
|
use tokio::net::{lookup_host, UdpSocket};
|
||||||
use tokio::time::{Duration, sleep, timeout};
|
use tokio::time::{timeout, Duration, sleep};
|
||||||
|
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
@@ -41,13 +41,13 @@ pub async fn stun_probe_dual(stun_addr: &str) -> Result<DualStunResult> {
|
|||||||
stun_probe_family(stun_addr, IpFamily::V6),
|
stun_probe_family(stun_addr, IpFamily::V6),
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(DualStunResult { v4: v4?, v6: v6? })
|
Ok(DualStunResult {
|
||||||
|
v4: v4?,
|
||||||
|
v6: v6?,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stun_probe_family(
|
pub async fn stun_probe_family(stun_addr: &str, family: IpFamily) -> Result<Option<StunProbeResult>> {
|
||||||
stun_addr: &str,
|
|
||||||
family: IpFamily,
|
|
||||||
) -> Result<Option<StunProbeResult>> {
|
|
||||||
stun_probe_family_with_bind(stun_addr, family, None).await
|
stun_probe_family_with_bind(stun_addr, family, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,18 +76,13 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
if let Some(addr) = target_addr {
|
if let Some(addr) = target_addr {
|
||||||
match socket.connect(addr).await {
|
match socket.connect(addr).await {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e)
|
Err(e) if family == IpFamily::V6 && matches!(
|
||||||
if family == IpFamily::V6
|
|
||||||
&& matches!(
|
|
||||||
e.kind(),
|
e.kind(),
|
||||||
std::io::ErrorKind::NetworkUnreachable
|
std::io::ErrorKind::NetworkUnreachable
|
||||||
| std::io::ErrorKind::HostUnreachable
|
| std::io::ErrorKind::HostUnreachable
|
||||||
| std::io::ErrorKind::Unsupported
|
| std::io::ErrorKind::Unsupported
|
||||||
| std::io::ErrorKind::NetworkDown
|
| std::io::ErrorKind::NetworkDown
|
||||||
) =>
|
) => return Ok(None),
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Err(e) => return Err(ProxyError::Proxy(format!("STUN connect failed: {e}"))),
|
Err(e) => return Err(ProxyError::Proxy(format!("STUN connect failed: {e}"))),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -210,6 +205,7 @@ pub async fn stun_probe_family_with_bind(
|
|||||||
|
|
||||||
idx += (alen + 3) & !3;
|
idx += (alen + 3) & !3;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -237,11 +233,7 @@ async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result<Option<S
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| ProxyError::Proxy(format!("STUN resolve failed: {e}")))?;
|
.map_err(|e| ProxyError::Proxy(format!("STUN resolve failed: {e}")))?;
|
||||||
|
|
||||||
let target = addrs.find(|a| {
|
let target = addrs
|
||||||
matches!(
|
.find(|a| matches!((a.is_ipv4(), family), (true, IpFamily::V4) | (false, IpFamily::V6)));
|
||||||
(a.is_ipv4(), family),
|
|
||||||
(true, IpFamily::V4) | (false, IpFamily::V6)
|
|
||||||
)
|
|
||||||
});
|
|
||||||
Ok(target)
|
Ok(target)
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-74
@@ -36,86 +36,32 @@ pub static TG_DATACENTERS_V6: LazyLock<Vec<IpAddr>> = LazyLock::new(|| {
|
|||||||
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
pub static TG_MIDDLE_PROXIES_V4: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
let mut m = std::collections::HashMap::new();
|
let mut m = std::collections::HashMap::new();
|
||||||
m.insert(
|
m.insert(1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
||||||
1,
|
m.insert(-1, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)]);
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
m.insert(2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
||||||
);
|
m.insert(-2, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)]);
|
||||||
m.insert(
|
m.insert(3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
||||||
-1,
|
m.insert(-3, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)]);
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 50)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
2,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-2,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 161, 144)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
3,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-3,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 175, 100)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(4, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888)]);
|
m.insert(4, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 4, 136)), 8888)]);
|
||||||
m.insert(
|
m.insert(-4, vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)]);
|
||||||
-4,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(149, 154, 165, 109)), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
m.insert(5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
||||||
m.insert(
|
m.insert(-5, vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)]);
|
||||||
-5,
|
|
||||||
vec![(IpAddr::V4(Ipv4Addr::new(91, 108, 56, 183)), 8888)],
|
|
||||||
);
|
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
|
|
||||||
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
pub static TG_MIDDLE_PROXIES_V6: LazyLock<std::collections::HashMap<i32, Vec<(IpAddr, u16)>>> =
|
||||||
LazyLock::new(|| {
|
LazyLock::new(|| {
|
||||||
let mut m = std::collections::HashMap::new();
|
let mut m = std::collections::HashMap::new();
|
||||||
m.insert(
|
m.insert(1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
||||||
1,
|
m.insert(-1, vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)]);
|
||||||
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
m.insert(2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
||||||
);
|
m.insert(-2, vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)]);
|
||||||
m.insert(
|
m.insert(3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
||||||
-1,
|
m.insert(-3, vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)]);
|
||||||
vec![(IpAddr::V6("2001:b28:f23d:f001::d".parse().unwrap()), 8888)],
|
m.insert(4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
||||||
);
|
m.insert(-4, vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)]);
|
||||||
m.insert(
|
m.insert(5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
||||||
2,
|
m.insert(-5, vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)]);
|
||||||
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-2,
|
|
||||||
vec![(IpAddr::V6("2001:67c:04e8:f002::d".parse().unwrap()), 80)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
3,
|
|
||||||
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-3,
|
|
||||||
vec![(IpAddr::V6("2001:b28:f23d:f003::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
4,
|
|
||||||
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-4,
|
|
||||||
vec![(IpAddr::V6("2001:67c:04e8:f004::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
5,
|
|
||||||
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m.insert(
|
|
||||||
-5,
|
|
||||||
vec![(IpAddr::V6("2001:b28:f23f:f005::d".parse().unwrap()), 8888)],
|
|
||||||
);
|
|
||||||
m
|
m
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -276,7 +222,9 @@ pub const SMALL_BUFFER_SIZE: usize = 8192;
|
|||||||
// ============= Statistics =============
|
// ============= Statistics =============
|
||||||
|
|
||||||
/// Duration buckets for histogram metrics
|
/// Duration buckets for histogram metrics
|
||||||
pub static DURATION_BUCKETS: &[f64] = &[0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0];
|
pub static DURATION_BUCKETS: &[f64] = &[
|
||||||
|
0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 60.0, 300.0, 600.0, 1800.0,
|
||||||
|
];
|
||||||
|
|
||||||
// ============= Reserved Nonce Patterns =============
|
// ============= Reserved Nonce Patterns =============
|
||||||
|
|
||||||
@@ -294,7 +242,9 @@ pub static RESERVED_NONCE_BEGINNINGS: &[[u8; 4]] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
/// Reserved continuation bytes (bytes 4-7)
|
/// Reserved continuation bytes (bytes 4-7)
|
||||||
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[[0x00, 0x00, 0x00, 0x00]];
|
pub static RESERVED_NONCE_CONTINUES: &[[u8; 4]] = &[
|
||||||
|
[0x00, 0x00, 0x00, 0x00],
|
||||||
|
];
|
||||||
|
|
||||||
// ============= RPC Constants (for Middle Proxy) =============
|
// ============= RPC Constants (for Middle Proxy) =============
|
||||||
|
|
||||||
@@ -335,6 +285,7 @@ pub mod rpc_flags {
|
|||||||
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
pub const FLAG_QUICKACK: u32 = 0x80000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============= Middle-End Proxy Servers =============
|
// ============= Middle-End Proxy Servers =============
|
||||||
pub const ME_PROXY_PORT: u16 = 8888;
|
pub const ME_PROXY_PORT: u16 = 8888;
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ impl FrameMode {
|
|||||||
|
|
||||||
/// Validate message length for MTProto
|
/// Validate message length for MTProto
|
||||||
pub fn validate_message_length(len: usize) -> bool {
|
pub fn validate_message_length(len: usize) -> bool {
|
||||||
use super::constants::{MAX_MSG_LEN, MIN_MSG_LEN, PADDING_FILLER};
|
use super::constants::{MIN_MSG_LEN, MAX_MSG_LEN, PADDING_FILLER};
|
||||||
|
|
||||||
(MIN_MSG_LEN..=MAX_MSG_LEN).contains(&len) && len.is_multiple_of(PADDING_FILLER.len())
|
(MIN_MSG_LEN..=MAX_MSG_LEN).contains(&len) && len.is_multiple_of(PADDING_FILLER.len())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use super::constants::*;
|
|
||||||
use crate::crypto::{AesCtr, sha256};
|
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
use crate::crypto::{sha256, AesCtr};
|
||||||
|
use super::constants::*;
|
||||||
|
|
||||||
/// Obfuscation parameters from handshake
|
/// Obfuscation parameters from handshake
|
||||||
///
|
///
|
||||||
@@ -69,8 +69,9 @@ impl ObfuscationParams {
|
|||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let dc_idx =
|
let dc_idx = i16::from_le_bytes(
|
||||||
i16::from_le_bytes(decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap());
|
decrypted[DC_IDX_POS..DC_IDX_POS + 2].try_into().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
let mut enc_key_input = Vec::with_capacity(PREKEY_LEN + secret.len());
|
||||||
enc_key_input.extend_from_slice(enc_prekey);
|
enc_key_input.extend_from_slice(enc_prekey);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::sha256_hmac;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use crate::crypto::sha256_hmac;
|
||||||
|
|
||||||
/// Helper to create a byte vector of specific length.
|
/// Helper to create a byte vector of specific length.
|
||||||
fn make_garbage(len: usize) -> Vec<u8> {
|
fn make_garbage(len: usize) -> Vec<u8> {
|
||||||
@@ -33,7 +33,8 @@ fn make_valid_tls_handshake_with_session_id(
|
|||||||
|
|
||||||
let digest = make_digest(secret, &handshake, timestamp);
|
let digest = make_digest(secret, &handshake, timestamp);
|
||||||
|
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&digest);
|
||||||
handshake
|
handshake
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +161,7 @@ fn extract_sni_with_invalid_hostname_rejected() {
|
|||||||
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
h.extend_from_slice(&(ext.len() as u16).to_be_bytes());
|
||||||
h.extend_from_slice(&ext);
|
h.extend_from_slice(&ext);
|
||||||
|
|
||||||
assert!(
|
assert!(extract_sni_from_client_hello(&h).is_none(), "Invalid SNI hostname must be rejected");
|
||||||
extract_sni_from_client_hello(&h).is_none(),
|
|
||||||
"Invalid SNI hostname must be rejected"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -325,9 +323,7 @@ fn extract_alpn_with_malformed_list_rejected() {
|
|||||||
ext.extend_from_slice(&(alpn_payload.len() as u16).to_be_bytes());
|
ext.extend_from_slice(&(alpn_payload.len() as u16).to_be_bytes());
|
||||||
ext.extend_from_slice(&alpn_payload);
|
ext.extend_from_slice(&alpn_payload);
|
||||||
|
|
||||||
let mut h = vec![
|
let mut h = vec![0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03];
|
||||||
0x16, 0x03, 0x03, 0x00, 0x40, 0x01, 0x00, 0x00, 0x3C, 0x03, 0x03,
|
|
||||||
];
|
|
||||||
h.extend_from_slice(&[0u8; 32]);
|
h.extend_from_slice(&[0u8; 32]);
|
||||||
h.push(0);
|
h.push(0);
|
||||||
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
h.extend_from_slice(&[0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
|
||||||
@@ -335,10 +331,7 @@ fn extract_alpn_with_malformed_list_rejected() {
|
|||||||
h.extend_from_slice(&ext);
|
h.extend_from_slice(&ext);
|
||||||
|
|
||||||
let res = extract_alpn_from_client_hello(&h);
|
let res = extract_alpn_from_client_hello(&h);
|
||||||
assert!(
|
assert!(res.is_empty(), "Malformed ALPN list must return empty or fail");
|
||||||
res.is_empty(),
|
|
||||||
"Malformed ALPN list must return empty or fail"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -84,10 +84,7 @@ fn make_valid_client_hello_record(host: &str, alpn_protocols: &[&[u8]]) -> Vec<u
|
|||||||
#[test]
|
#[test]
|
||||||
fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
||||||
let valid = make_valid_client_hello_record("example.com", &[b"h2", b"http/1.1"]);
|
let valid = make_valid_client_hello_record("example.com", &[b"h2", b"http/1.1"]);
|
||||||
assert_eq!(
|
assert_eq!(extract_sni_from_client_hello(&valid).as_deref(), Some("example.com"));
|
||||||
extract_sni_from_client_hello(&valid).as_deref(),
|
|
||||||
Some("example.com")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extract_alpn_from_client_hello(&valid),
|
extract_alpn_from_client_hello(&valid),
|
||||||
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
|
||||||
@@ -124,14 +121,8 @@ fn client_hello_fuzz_corpus_never_panics_or_accepts_corruption() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(extract_sni_from_client_hello(input).is_none(), "corpus item {idx} must fail closed for SNI");
|
||||||
extract_sni_from_client_hello(input).is_none(),
|
assert!(extract_alpn_from_client_hello(input).is_empty(), "corpus item {idx} must fail closed for ALPN");
|
||||||
"corpus item {idx} must fail closed for SNI"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
extract_alpn_from_client_hello(input).is_empty(),
|
|
||||||
"corpus item {idx} must fail closed for ALPN"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,9 +163,7 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
|||||||
for _ in 0..32 {
|
for _ in 0..32 {
|
||||||
let mut mutated = base.clone();
|
let mut mutated = base.clone();
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
seed = seed
|
seed = seed.wrapping_mul(2862933555777941757).wrapping_add(3037000493);
|
||||||
.wrapping_mul(2862933555777941757)
|
|
||||||
.wrapping_add(3037000493);
|
|
||||||
let idx = TLS_DIGEST_POS + (seed as usize % TLS_DIGEST_LEN);
|
let idx = TLS_DIGEST_POS + (seed as usize % TLS_DIGEST_LEN);
|
||||||
mutated[idx] ^= ((seed >> 17) as u8).wrapping_add(1);
|
mutated[idx] ^= ((seed >> 17) as u8).wrapping_add(1);
|
||||||
}
|
}
|
||||||
@@ -182,13 +171,9 @@ fn tls_handshake_fuzz_corpus_never_panics_and_rejects_digest_mutations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (idx, handshake) in corpus.iter().enumerate() {
|
for (idx, handshake) in corpus.iter().enumerate() {
|
||||||
let result =
|
let result = catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
||||||
catch_unwind(|| validate_tls_handshake_at_time(handshake, &secrets, false, now));
|
|
||||||
assert!(result.is_ok(), "corpus item {idx} must not panic");
|
assert!(result.is_ok(), "corpus item {idx} must not panic");
|
||||||
assert!(
|
assert!(result.unwrap().is_none(), "corpus item {idx} must fail closed");
|
||||||
result.unwrap().is_none(),
|
|
||||||
"corpus item {idx} must fail closed"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extension_builder_fails_closed_on_u16_length_overflow() {
|
|
||||||
let builder = TlsExtensionBuilder {
|
|
||||||
extensions: vec![0u8; (u16::MAX as usize) + 1],
|
|
||||||
};
|
|
||||||
|
|
||||||
let built = builder.build();
|
|
||||||
assert!(
|
|
||||||
built.is_empty(),
|
|
||||||
"oversized extension blob must fail closed instead of truncating length field"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn server_hello_builder_fails_closed_on_session_id_len_overflow() {
|
|
||||||
let builder = ServerHelloBuilder {
|
|
||||||
random: [0u8; 32],
|
|
||||||
session_id: vec![0xAB; (u8::MAX as usize) + 1],
|
|
||||||
cipher_suite: cipher_suite::TLS_AES_128_GCM_SHA256,
|
|
||||||
compression: 0,
|
|
||||||
extensions: TlsExtensionBuilder::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = builder.build_message();
|
|
||||||
let record = builder.build_record();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
message.is_empty(),
|
|
||||||
"session_id length overflow must fail closed in message builder"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
record.is_empty(),
|
|
||||||
"session_id length overflow must fail closed in record builder"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::tls_front::emulator::build_emulated_server_hello;
|
use crate::tls_front::emulator::build_emulated_server_hello;
|
||||||
use crate::tls_front::types::{
|
use crate::tls_front::types::{CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource};
|
||||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource,
|
|
||||||
};
|
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
/// Build a TLS-handshake-like buffer that contains a valid HMAC digest
|
/// Build a TLS-handshake-like buffer that contains a valid HMAC digest
|
||||||
@@ -41,7 +39,8 @@ fn make_valid_tls_handshake_with_session_id(
|
|||||||
digest[28 + i] ^= ts[i];
|
digest[28 + i] ^= ts[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&digest);
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&digest);
|
||||||
handshake
|
handshake
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,10 +180,7 @@ fn second_user_in_list_found_when_first_does_not_match() {
|
|||||||
("user_b".to_string(), secret_b.to_vec()),
|
("user_b".to_string(), secret_b.to_vec()),
|
||||||
];
|
];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
assert!(
|
assert!(result.is_some(), "user_b must be found even though user_a comes first");
|
||||||
result.is_some(),
|
|
||||||
"user_b must be found even though user_a comes first"
|
|
||||||
);
|
|
||||||
assert_eq!(result.unwrap().user, "user_b");
|
assert_eq!(result.unwrap().user, "user_b");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +428,8 @@ fn censor_probe_random_digests_all_rejected() {
|
|||||||
let mut h = vec![0x42u8; min_len];
|
let mut h = vec![0x42u8; min_len];
|
||||||
h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8;
|
h[TLS_DIGEST_POS + TLS_DIGEST_LEN] = session_id_len as u8;
|
||||||
let rand_digest = rng.bytes(TLS_DIGEST_LEN);
|
let rand_digest = rng.bytes(TLS_DIGEST_LEN);
|
||||||
h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&rand_digest);
|
h[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&rand_digest);
|
||||||
assert!(
|
assert!(
|
||||||
validate_tls_handshake(&h, &secrets, true).is_none(),
|
validate_tls_handshake(&h, &secrets, true).is_none(),
|
||||||
"Random digest at attempt {attempt} must not match"
|
"Random digest at attempt {attempt} must not match"
|
||||||
@@ -556,7 +553,8 @@ fn system_time_before_unix_epoch_is_rejected_without_panic() {
|
|||||||
fn system_time_far_future_overflowing_i64_returns_none() {
|
fn system_time_far_future_overflowing_i64_returns_none() {
|
||||||
// i64::MAX + 1 seconds past epoch overflows i64 when cast naively with `as`.
|
// i64::MAX + 1 seconds past epoch overflows i64 when cast naively with `as`.
|
||||||
let overflow_secs = u64::try_from(i64::MAX).unwrap() + 1;
|
let overflow_secs = u64::try_from(i64::MAX).unwrap() + 1;
|
||||||
if let Some(far_future) = UNIX_EPOCH.checked_add(std::time::Duration::from_secs(overflow_secs))
|
if let Some(far_future) =
|
||||||
|
UNIX_EPOCH.checked_add(std::time::Duration::from_secs(overflow_secs))
|
||||||
{
|
{
|
||||||
assert!(
|
assert!(
|
||||||
system_time_to_unix_secs(far_future).is_none(),
|
system_time_to_unix_secs(far_future).is_none(),
|
||||||
@@ -622,10 +620,7 @@ fn appended_trailing_byte_causes_rejection() {
|
|||||||
let mut h = make_valid_tls_handshake(secret, 0);
|
let mut h = make_valid_tls_handshake(secret, 0);
|
||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
assert!(
|
assert!(validate_tls_handshake(&h, &secrets, true).is_some(), "baseline");
|
||||||
validate_tls_handshake(&h, &secrets, true).is_some(),
|
|
||||||
"baseline"
|
|
||||||
);
|
|
||||||
|
|
||||||
h.push(0x00);
|
h.push(0x00);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -652,7 +647,8 @@ fn zero_length_session_id_accepted() {
|
|||||||
|
|
||||||
let computed = sha256_hmac(secret, &handshake);
|
let computed = sha256_hmac(secret, &handshake);
|
||||||
// timestamp = 0 → ts XOR bytes are all zero → digest = computed unchanged.
|
// timestamp = 0 → ts XOR bytes are all zero → digest = computed unchanged.
|
||||||
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&computed);
|
handshake[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&computed);
|
||||||
|
|
||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
@@ -777,18 +773,10 @@ fn ignore_time_skew_explicitly_decouples_from_boot_time_cap() {
|
|||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0);
|
let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0);
|
||||||
let cap_nonzero = validate_tls_handshake_at_time_with_boot_cap(
|
let cap_nonzero =
|
||||||
&h,
|
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, BOOT_TIME_COMPAT_MAX_SECS);
|
||||||
&secrets,
|
|
||||||
true,
|
|
||||||
0,
|
|
||||||
BOOT_TIME_COMPAT_MAX_SECS,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
assert!(cap_zero.is_some(), "ignore_time_skew=true must accept valid HMAC");
|
||||||
cap_zero.is_some(),
|
|
||||||
"ignore_time_skew=true must accept valid HMAC"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
cap_nonzero.is_some(),
|
cap_nonzero.is_some(),
|
||||||
"ignore_time_skew path must not depend on boot-time cap"
|
"ignore_time_skew path must not depend on boot-time cap"
|
||||||
@@ -900,8 +888,8 @@ fn adversarial_skew_boundary_matrix_accepts_only_inclusive_window_when_boot_disa
|
|||||||
let ts_i64 = now - offset;
|
let ts_i64 = now - offset;
|
||||||
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix");
|
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix");
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted =
|
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
||||||
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
.is_some();
|
||||||
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset);
|
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
accepted, expected,
|
accepted, expected,
|
||||||
@@ -929,8 +917,8 @@ fn light_fuzz_skew_window_rejects_outside_range_when_boot_disabled() {
|
|||||||
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test");
|
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test");
|
||||||
|
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted =
|
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
||||||
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
.is_some();
|
||||||
assert!(
|
assert!(
|
||||||
!accepted,
|
!accepted,
|
||||||
"offset {offset} must be rejected outside strict skew window"
|
"offset {offset} must be rejected outside strict skew window"
|
||||||
@@ -952,8 +940,8 @@ fn stress_boot_disabled_validation_matches_time_diff_oracle() {
|
|||||||
let ts = s as u32;
|
let ts = s as u32;
|
||||||
let h = make_valid_tls_handshake(secret, ts);
|
let h = make_valid_tls_handshake(secret, ts);
|
||||||
|
|
||||||
let accepted =
|
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
|
||||||
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0).is_some();
|
.is_some();
|
||||||
let time_diff = now - i64::from(ts);
|
let time_diff = now - i64::from(ts);
|
||||||
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff);
|
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -972,10 +960,7 @@ fn integration_large_user_list_with_boot_disabled_finds_only_matching_user() {
|
|||||||
|
|
||||||
let mut secrets = Vec::new();
|
let mut secrets = Vec::new();
|
||||||
for i in 0..512u32 {
|
for i in 0..512u32 {
|
||||||
secrets.push((
|
secrets.push((format!("noise-{i}"), format!("noise-secret-{i}").into_bytes()));
|
||||||
format!("noise-{i}"),
|
|
||||||
format!("noise-secret-{i}").into_bytes(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
secrets.push(("target-user".to_string(), target_secret.to_vec()));
|
secrets.push(("target-user".to_string(), target_secret.to_vec()));
|
||||||
|
|
||||||
@@ -1033,10 +1018,7 @@ fn u32_max_timestamp_accepted_with_ignore_time_skew() {
|
|||||||
let secrets = vec![("u".to_string(), secret.to_vec())];
|
let secrets = vec![("u".to_string(), secret.to_vec())];
|
||||||
|
|
||||||
let result = validate_tls_handshake(&h, &secrets, true);
|
let result = validate_tls_handshake(&h, &secrets, true);
|
||||||
assert!(
|
assert!(result.is_some(), "u32::MAX timestamp must be accepted with ignore_time_skew=true");
|
||||||
result.is_some(),
|
|
||||||
"u32::MAX timestamp must be accepted with ignore_time_skew=true"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap().timestamp,
|
result.unwrap().timestamp,
|
||||||
u32::MAX,
|
u32::MAX,
|
||||||
@@ -1177,8 +1159,7 @@ fn first_matching_user_wins_over_later_duplicate_secret() {
|
|||||||
let result = validate_tls_handshake(&h, &secrets, true);
|
let result = validate_tls_handshake(&h, &secrets, true);
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.unwrap().user,
|
result.unwrap().user, "winner",
|
||||||
"winner",
|
|
||||||
"first matching user must be returned even when a later entry also matches"
|
"first matching user must be returned even when a later entry also matches"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1444,8 +1425,7 @@ fn test_build_server_hello_structure() {
|
|||||||
assert!(response.len() > ccs_start + 6);
|
assert!(response.len() > ccs_start + 6);
|
||||||
assert_eq!(response[ccs_start], TLS_RECORD_CHANGE_CIPHER);
|
assert_eq!(response[ccs_start], TLS_RECORD_CHANGE_CIPHER);
|
||||||
|
|
||||||
let ccs_len =
|
let ccs_len = 5 + u16::from_be_bytes([response[ccs_start + 3], response[ccs_start + 4]]) as usize;
|
||||||
5 + u16::from_be_bytes([response[ccs_start + 3], response[ccs_start + 4]]) as usize;
|
|
||||||
let app_start = ccs_start + ccs_len;
|
let app_start = ccs_start + ccs_len;
|
||||||
assert!(response.len() > app_start + 5);
|
assert!(response.len() > app_start + 5);
|
||||||
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
|
||||||
@@ -1749,10 +1729,7 @@ fn empty_secret_hmac_is_supported() {
|
|||||||
let handshake = make_valid_tls_handshake(secret, 0);
|
let handshake = make_valid_tls_handshake(secret, 0);
|
||||||
let secrets = vec![("empty".to_string(), secret.to_vec())];
|
let secrets = vec![("empty".to_string(), secret.to_vec())];
|
||||||
let result = validate_tls_handshake(&handshake, &secrets, true);
|
let result = validate_tls_handshake(&handshake, &secrets, true);
|
||||||
assert!(
|
assert!(result.is_some(), "Empty HMAC key must not panic and must validate when correct");
|
||||||
result.is_some(),
|
|
||||||
"Empty HMAC key must not panic and must validate when correct"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1825,10 +1802,7 @@ fn server_hello_application_data_payload_varies_across_runs() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
let payload = response[app_pos + 5..app_pos + 5 + app_len].to_vec();
|
let payload = response[app_pos + 5..app_pos + 5 + app_len].to_vec();
|
||||||
|
|
||||||
assert!(
|
assert!(payload.iter().any(|&b| b != 0), "Payload must not be all-zero deterministic filler");
|
||||||
payload.iter().any(|&b| b != 0),
|
|
||||||
"Payload must not be all-zero deterministic filler"
|
|
||||||
);
|
|
||||||
unique_payloads.insert(payload);
|
unique_payloads.insert(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1872,13 +1846,7 @@ fn large_replay_window_does_not_expand_time_skew_acceptance() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_tls_record_header_accepts_tls_version_constant() {
|
fn parse_tls_record_header_accepts_tls_version_constant() {
|
||||||
let header = [
|
let header = [TLS_RECORD_HANDSHAKE, TLS_VERSION[0], TLS_VERSION[1], 0x00, 0x2A];
|
||||||
TLS_RECORD_HANDSHAKE,
|
|
||||||
TLS_VERSION[0],
|
|
||||||
TLS_VERSION[1],
|
|
||||||
0x00,
|
|
||||||
0x2A,
|
|
||||||
];
|
|
||||||
let parsed = parse_tls_record_header(&header).expect("TLS_VERSION header should be accepted");
|
let parsed = parse_tls_record_header(&header).expect("TLS_VERSION header should be accepted");
|
||||||
assert_eq!(parsed.0, TLS_RECORD_HANDSHAKE);
|
assert_eq!(parsed.0, TLS_RECORD_HANDSHAKE);
|
||||||
assert_eq!(parsed.1, 42);
|
assert_eq!(parsed.1, 42);
|
||||||
@@ -1900,10 +1868,7 @@ fn server_hello_clamps_fake_cert_len_lower_bound() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
|
|
||||||
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(
|
assert_eq!(app_len, 64, "fake cert payload must be clamped to minimum 64 bytes");
|
||||||
app_len, 64,
|
|
||||||
"fake cert payload must be clamped to minimum 64 bytes"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1922,10 +1887,7 @@ fn server_hello_clamps_fake_cert_len_upper_bound() {
|
|||||||
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
let app_len = u16::from_be_bytes([response[app_pos + 3], response[app_pos + 4]]) as usize;
|
||||||
|
|
||||||
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
assert_eq!(response[app_pos], TLS_RECORD_APPLICATION);
|
||||||
assert_eq!(
|
assert_eq!(app_len, MAX_TLS_CIPHERTEXT_SIZE, "fake cert payload must be clamped to TLS record max bound");
|
||||||
app_len, MAX_TLS_CIPHERTEXT_SIZE,
|
|
||||||
"fake cert payload must be clamped to TLS record max bound"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1936,15 +1898,7 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
|||||||
let rng = crate::crypto::SecureRandom::new();
|
let rng = crate::crypto::SecureRandom::new();
|
||||||
|
|
||||||
let tickets: u8 = 3;
|
let tickets: u8 = 3;
|
||||||
let response = build_server_hello(
|
let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, tickets);
|
||||||
secret,
|
|
||||||
&client_digest,
|
|
||||||
&session_id,
|
|
||||||
1024,
|
|
||||||
&rng,
|
|
||||||
None,
|
|
||||||
tickets,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut pos = 0usize;
|
let mut pos = 0usize;
|
||||||
let mut app_records = 0usize;
|
let mut app_records = 0usize;
|
||||||
@@ -1952,10 +1906,7 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(
|
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
||||||
next <= response.len(),
|
|
||||||
"TLS record must stay inside response bounds"
|
|
||||||
);
|
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
}
|
}
|
||||||
@@ -1976,15 +1927,7 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
let session_id = vec![0x54; 32];
|
let session_id = vec![0x54; 32];
|
||||||
let rng = crate::crypto::SecureRandom::new();
|
let rng = crate::crypto::SecureRandom::new();
|
||||||
|
|
||||||
let response = build_server_hello(
|
let response = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, u8::MAX);
|
||||||
secret,
|
|
||||||
&client_digest,
|
|
||||||
&session_id,
|
|
||||||
1024,
|
|
||||||
&rng,
|
|
||||||
None,
|
|
||||||
u8::MAX,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut pos = 0usize;
|
let mut pos = 0usize;
|
||||||
let mut app_records = 0usize;
|
let mut app_records = 0usize;
|
||||||
@@ -1992,10 +1935,7 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(
|
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
||||||
next <= response.len(),
|
|
||||||
"TLS record must stay inside response bounds"
|
|
||||||
);
|
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
}
|
}
|
||||||
@@ -2003,7 +1943,8 @@ fn server_hello_new_session_ticket_count_is_safely_capped() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app_records, 5,
|
app_records,
|
||||||
|
5,
|
||||||
"response must cap ticket-like tail records to four plus one main application record"
|
"response must cap ticket-like tail records to four plus one main application record"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2031,14 +1972,10 @@ fn boot_time_handshake_replay_remains_blocked_after_cache_window_expires() {
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(70));
|
std::thread::sleep(std::time::Duration::from_millis(70));
|
||||||
|
|
||||||
let validation_after_expiry =
|
let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
||||||
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
|
||||||
.expect("boot-time handshake must still cryptographically validate after cache expiry");
|
.expect("boot-time handshake must still cryptographically validate after cache expiry");
|
||||||
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
||||||
assert_eq!(
|
assert_eq!(digest_half, digest_half_after_expiry, "replay key must be stable for same handshake");
|
||||||
digest_half, digest_half_after_expiry,
|
|
||||||
"replay key must be stable for same handshake"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
checker.check_and_add_tls_digest(digest_half_after_expiry),
|
checker.check_and_add_tls_digest(digest_half_after_expiry),
|
||||||
@@ -2069,8 +2006,7 @@ fn adversarial_boot_time_handshake_should_not_be_replayable_after_cache_expiry()
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(70));
|
std::thread::sleep(std::time::Duration::from_millis(70));
|
||||||
|
|
||||||
let validation_after_expiry =
|
let validation_after_expiry = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
||||||
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
|
||||||
.expect("boot-time handshake still validates cryptographically after cache expiry");
|
.expect("boot-time handshake still validates cryptographically after cache expiry");
|
||||||
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
let digest_half_after_expiry = &validation_after_expiry.digest[..TLS_DIGEST_HALF_LEN];
|
||||||
|
|
||||||
@@ -2131,14 +2067,11 @@ fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap
|
|||||||
let ts = (s as u32) % 8;
|
let ts = (s as u32) % 8;
|
||||||
|
|
||||||
let handshake = make_valid_tls_handshake(secret, ts);
|
let handshake = make_valid_tls_handshake(secret, ts);
|
||||||
let accepted =
|
let accepted = validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2)
|
||||||
validate_tls_handshake_with_replay_window(&handshake, &secrets, false, 2).is_some();
|
.is_some();
|
||||||
|
|
||||||
if ts < 2 {
|
if ts < 2 {
|
||||||
assert!(
|
assert!(accepted, "timestamp {ts} must remain boot-time compatible under 2s cap");
|
||||||
accepted,
|
|
||||||
"timestamp {ts} must remain boot-time compatible under 2s cap"
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
!accepted,
|
!accepted,
|
||||||
@@ -2174,9 +2107,7 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
|
|||||||
|
|
||||||
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
||||||
assert!(
|
assert!(
|
||||||
app_payload
|
app_payload.windows(expected.len()).any(|window| window == expected),
|
||||||
.windows(expected.len())
|
|
||||||
.any(|window| window == expected),
|
|
||||||
"first application payload must carry ALPN marker for selected protocol"
|
"first application payload must carry ALPN marker for selected protocol"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2206,10 +2137,7 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
|||||||
let rtype = response[pos];
|
let rtype = response[pos];
|
||||||
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
let rlen = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
|
||||||
let next = pos + 5 + rlen;
|
let next = pos + 5 + rlen;
|
||||||
assert!(
|
assert!(next <= response.len(), "TLS record must stay inside response bounds");
|
||||||
next <= response.len(),
|
|
||||||
"TLS record must stay inside response bounds"
|
|
||||||
);
|
|
||||||
if rtype == TLS_RECORD_APPLICATION {
|
if rtype == TLS_RECORD_APPLICATION {
|
||||||
app_records += 1;
|
app_records += 1;
|
||||||
if first_app_payload.is_none() {
|
if first_app_payload.is_none() {
|
||||||
@@ -2218,9 +2146,7 @@ fn server_hello_ignores_oversized_alpn_and_still_caps_ticket_tail() {
|
|||||||
}
|
}
|
||||||
pos = next;
|
pos = next;
|
||||||
}
|
}
|
||||||
let marker = [
|
let marker = [0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x'];
|
||||||
0x00u8, 0x10, 0x00, 0x06, 0x00, 0x04, 0x03, b'x', b'x', b'x', b'x',
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app_records, 5,
|
app_records, 5,
|
||||||
@@ -2384,13 +2310,13 @@ fn light_fuzz_tls_header_classifier_and_parser_policy_consistency() {
|
|||||||
&& header[1] == 0x03
|
&& header[1] == 0x03
|
||||||
&& (header[2] == 0x01 || header[2] == 0x03);
|
&& (header[2] == 0x01 || header[2] == 0x03);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
classified, expected_classified,
|
classified,
|
||||||
|
expected_classified,
|
||||||
"classifier policy mismatch for header {header:02x?}"
|
"classifier policy mismatch for header {header:02x?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let parsed = parse_tls_record_header(&header);
|
let parsed = parse_tls_record_header(&header);
|
||||||
let expected_parsed =
|
let expected_parsed = header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
|
||||||
header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parsed.is_some(),
|
parsed.is_some(),
|
||||||
expected_parsed,
|
expected_parsed,
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
use super::{MAX_TLS_CIPHERTEXT_SIZE, MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
use super::{
|
||||||
|
MAX_TLS_CIPHERTEXT_SIZE,
|
||||||
|
MAX_TLS_PLAINTEXT_SIZE,
|
||||||
|
MIN_TLS_CLIENT_HELLO_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tls_size_constants_match_rfc_8446() {
|
fn tls_size_constants_match_rfc_8446() {
|
||||||
|
|||||||
+45
-144
@@ -5,66 +5,11 @@
|
|||||||
//! actually carries MTProto authentication data.
|
//! actually carries MTProto authentication data.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
|
||||||
#![cfg_attr(
|
|
||||||
not(test),
|
|
||||||
deny(
|
|
||||||
clippy::unwrap_used,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::todo,
|
|
||||||
clippy::unimplemented,
|
|
||||||
clippy::correctness,
|
|
||||||
clippy::option_if_let_else,
|
|
||||||
clippy::or_fun_call,
|
|
||||||
clippy::branches_sharing_code,
|
|
||||||
clippy::single_option_map,
|
|
||||||
clippy::useless_let_if_seq,
|
|
||||||
clippy::redundant_locals,
|
|
||||||
clippy::cloned_ref_to_slice_refs,
|
|
||||||
unsafe_code,
|
|
||||||
clippy::await_holding_lock,
|
|
||||||
clippy::await_holding_refcell_ref,
|
|
||||||
clippy::debug_assert_with_mut_call,
|
|
||||||
clippy::macro_use_imports,
|
|
||||||
clippy::cast_ptr_alignment,
|
|
||||||
clippy::cast_lossless,
|
|
||||||
clippy::ptr_as_ptr,
|
|
||||||
clippy::large_stack_arrays,
|
|
||||||
clippy::same_functions_in_if_condition,
|
|
||||||
trivial_casts,
|
|
||||||
trivial_numeric_casts,
|
|
||||||
unused_extern_crates,
|
|
||||||
unused_import_braces,
|
|
||||||
rust_2018_idioms
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![cfg_attr(
|
|
||||||
not(test),
|
|
||||||
allow(
|
|
||||||
clippy::use_self,
|
|
||||||
clippy::redundant_closure,
|
|
||||||
clippy::too_many_arguments,
|
|
||||||
clippy::doc_markdown,
|
|
||||||
clippy::missing_const_for_fn,
|
|
||||||
clippy::unnecessary_operation,
|
|
||||||
clippy::redundant_pub_crate,
|
|
||||||
clippy::derive_partial_eq_without_eq,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::new_ret_no_self,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::significant_drop_tightening,
|
|
||||||
clippy::significant_drop_in_scrutinee,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::nursery
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
|
|
||||||
use super::constants::*;
|
use crate::crypto::{sha256_hmac, SecureRandom};
|
||||||
use crate::crypto::{SecureRandom, sha256_hmac};
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::error::ProxyError;
|
use crate::error::ProxyError;
|
||||||
|
use super::constants::*;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
use x25519_dalek::{X25519_BASEPOINT_BYTES, x25519};
|
||||||
@@ -124,6 +69,7 @@ pub struct TlsValidation {
|
|||||||
/// Client digest for response generation
|
/// Client digest for response generation
|
||||||
pub digest: [u8; TLS_DIGEST_LEN],
|
pub digest: [u8; TLS_DIGEST_LEN],
|
||||||
/// Timestamp extracted from digest
|
/// Timestamp extracted from digest
|
||||||
|
|
||||||
pub timestamp: u32,
|
pub timestamp: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +91,7 @@ impl TlsExtensionBuilder {
|
|||||||
/// Add Key Share extension with X25519 key
|
/// Add Key Share extension with X25519 key
|
||||||
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
fn add_key_share(&mut self, public_key: &[u8; 32]) -> &mut Self {
|
||||||
// Extension type: key_share (0x0033)
|
// Extension type: key_share (0x0033)
|
||||||
self.extensions
|
self.extensions.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
||||||
.extend_from_slice(&extension_type::KEY_SHARE.to_be_bytes());
|
|
||||||
|
|
||||||
// Key share entry: curve (2) + key_len (2) + key (32) = 36 bytes
|
// Key share entry: curve (2) + key_len (2) + key (32) = 36 bytes
|
||||||
// Extension data length
|
// Extension data length
|
||||||
@@ -154,8 +99,7 @@ impl TlsExtensionBuilder {
|
|||||||
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
self.extensions.extend_from_slice(&entry_len.to_be_bytes());
|
||||||
|
|
||||||
// Named curve: x25519
|
// Named curve: x25519
|
||||||
self.extensions
|
self.extensions.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
||||||
.extend_from_slice(&named_curve::X25519.to_be_bytes());
|
|
||||||
|
|
||||||
// Key length
|
// Key length
|
||||||
self.extensions.extend_from_slice(&(32u16).to_be_bytes());
|
self.extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||||
@@ -169,8 +113,7 @@ impl TlsExtensionBuilder {
|
|||||||
/// Add Supported Versions extension
|
/// Add Supported Versions extension
|
||||||
fn add_supported_versions(&mut self, version: u16) -> &mut Self {
|
fn add_supported_versions(&mut self, version: u16) -> &mut Self {
|
||||||
// Extension type: supported_versions (0x002b)
|
// Extension type: supported_versions (0x002b)
|
||||||
self.extensions
|
self.extensions.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
||||||
.extend_from_slice(&extension_type::SUPPORTED_VERSIONS.to_be_bytes());
|
|
||||||
|
|
||||||
// Extension data: length (2) + version (2)
|
// Extension data: length (2) + version (2)
|
||||||
self.extensions.extend_from_slice(&(2u16).to_be_bytes());
|
self.extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||||
@@ -182,13 +125,12 @@ impl TlsExtensionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build final extensions with length prefix
|
/// Build final extensions with length prefix
|
||||||
|
|
||||||
fn build(self) -> Vec<u8> {
|
fn build(self) -> Vec<u8> {
|
||||||
let Ok(len) = u16::try_from(self.extensions.len()) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
||||||
|
|
||||||
// Extensions length (2 bytes)
|
// Extensions length (2 bytes)
|
||||||
|
let len = self.extensions.len() as u16;
|
||||||
result.extend_from_slice(&len.to_be_bytes());
|
result.extend_from_slice(&len.to_be_bytes());
|
||||||
|
|
||||||
// Extensions data
|
// Extensions data
|
||||||
@@ -198,6 +140,7 @@ impl TlsExtensionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get current extensions without length prefix (for calculation)
|
/// Get current extensions without length prefix (for calculation)
|
||||||
|
|
||||||
fn as_bytes(&self) -> &[u8] {
|
fn as_bytes(&self) -> &[u8] {
|
||||||
&self.extensions
|
&self.extensions
|
||||||
}
|
}
|
||||||
@@ -243,13 +186,8 @@ impl ServerHelloBuilder {
|
|||||||
|
|
||||||
/// Build ServerHello message (without record header)
|
/// Build ServerHello message (without record header)
|
||||||
fn build_message(&self) -> Vec<u8> {
|
fn build_message(&self) -> Vec<u8> {
|
||||||
let Ok(session_id_len) = u8::try_from(self.session_id.len()) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let extensions = self.extensions.extensions.clone();
|
let extensions = self.extensions.extensions.clone();
|
||||||
let Ok(extensions_len) = u16::try_from(extensions.len()) else {
|
let extensions_len = extensions.len() as u16;
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate total length
|
// Calculate total length
|
||||||
let body_len = 2 + // version
|
let body_len = 2 + // version
|
||||||
@@ -258,9 +196,6 @@ impl ServerHelloBuilder {
|
|||||||
2 + // cipher suite
|
2 + // cipher suite
|
||||||
1 + // compression
|
1 + // compression
|
||||||
2 + extensions.len(); // extensions length + data
|
2 + extensions.len(); // extensions length + data
|
||||||
if body_len > 0x00ff_ffff {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut message = Vec::with_capacity(4 + body_len);
|
let mut message = Vec::with_capacity(4 + body_len);
|
||||||
|
|
||||||
@@ -268,10 +203,7 @@ impl ServerHelloBuilder {
|
|||||||
message.push(0x02); // ServerHello message type
|
message.push(0x02); // ServerHello message type
|
||||||
|
|
||||||
// 3-byte length
|
// 3-byte length
|
||||||
let Ok(body_len_u32) = u32::try_from(body_len) else {
|
let len_bytes = (body_len as u32).to_be_bytes();
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
let len_bytes = body_len_u32.to_be_bytes();
|
|
||||||
message.extend_from_slice(&len_bytes[1..4]);
|
message.extend_from_slice(&len_bytes[1..4]);
|
||||||
|
|
||||||
// Server version (TLS 1.2 in header, actual version in extension)
|
// Server version (TLS 1.2 in header, actual version in extension)
|
||||||
@@ -281,7 +213,7 @@ impl ServerHelloBuilder {
|
|||||||
message.extend_from_slice(&self.random);
|
message.extend_from_slice(&self.random);
|
||||||
|
|
||||||
// Session ID
|
// Session ID
|
||||||
message.push(session_id_len);
|
message.push(self.session_id.len() as u8);
|
||||||
message.extend_from_slice(&self.session_id);
|
message.extend_from_slice(&self.session_id);
|
||||||
|
|
||||||
// Cipher suite
|
// Cipher suite
|
||||||
@@ -302,19 +234,13 @@ impl ServerHelloBuilder {
|
|||||||
/// Build complete ServerHello TLS record
|
/// Build complete ServerHello TLS record
|
||||||
fn build_record(&self) -> Vec<u8> {
|
fn build_record(&self) -> Vec<u8> {
|
||||||
let message = self.build_message();
|
let message = self.build_message();
|
||||||
if message.is_empty() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let Ok(message_len) = u16::try_from(message.len()) else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut record = Vec::with_capacity(5 + message.len());
|
let mut record = Vec::with_capacity(5 + message.len());
|
||||||
|
|
||||||
// TLS record header
|
// TLS record header
|
||||||
record.push(TLS_RECORD_HANDSHAKE);
|
record.push(TLS_RECORD_HANDSHAKE);
|
||||||
record.extend_from_slice(&TLS_VERSION);
|
record.extend_from_slice(&TLS_VERSION);
|
||||||
record.extend_from_slice(&message_len.to_be_bytes());
|
record.extend_from_slice(&(message.len() as u16).to_be_bytes());
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
record.extend_from_slice(&message);
|
record.extend_from_slice(&message);
|
||||||
@@ -330,6 +256,7 @@ impl ServerHelloBuilder {
|
|||||||
/// Returns validation result if a matching user is found.
|
/// Returns validation result if a matching user is found.
|
||||||
/// The result **must** be used — ignoring it silently bypasses authentication.
|
/// The result **must** be used — ignoring it silently bypasses authentication.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
||||||
pub fn validate_tls_handshake(
|
pub fn validate_tls_handshake(
|
||||||
handshake: &[u8],
|
handshake: &[u8],
|
||||||
secrets: &[(String, Vec<u8>)],
|
secrets: &[(String, Vec<u8>)],
|
||||||
@@ -393,6 +320,7 @@ fn system_time_to_unix_secs(now: SystemTime) -> Option<i64> {
|
|||||||
i64::try_from(d.as_secs()).ok()
|
i64::try_from(d.as_secs()).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn validate_tls_handshake_at_time(
|
fn validate_tls_handshake_at_time(
|
||||||
handshake: &[u8],
|
handshake: &[u8],
|
||||||
secrets: &[(String, Vec<u8>)],
|
secrets: &[(String, Vec<u8>)],
|
||||||
@@ -535,20 +463,15 @@ pub fn build_server_hello(
|
|||||||
// Build Change Cipher Spec record
|
// Build Change Cipher Spec record
|
||||||
let change_cipher_spec = [
|
let change_cipher_spec = [
|
||||||
TLS_RECORD_CHANGE_CIPHER,
|
TLS_RECORD_CHANGE_CIPHER,
|
||||||
TLS_VERSION[0],
|
TLS_VERSION[0], TLS_VERSION[1],
|
||||||
TLS_VERSION[1],
|
0x00, 0x01, // length = 1
|
||||||
0x00,
|
|
||||||
0x01, // length = 1
|
|
||||||
0x01, // CCS byte
|
0x01, // CCS byte
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
||||||
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
||||||
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
||||||
if let Some(proto) = alpn
|
if let Some(proto) = alpn.as_ref().filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize) {
|
||||||
.as_ref()
|
|
||||||
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
|
|
||||||
{
|
|
||||||
let proto_list_len = 1usize + proto.len();
|
let proto_list_len = 1usize + proto.len();
|
||||||
let ext_data_len = 2usize + proto_list_len;
|
let ext_data_len = 2usize + proto_list_len;
|
||||||
let marker_len = 4usize + ext_data_len;
|
let marker_len = 4usize + ext_data_len;
|
||||||
@@ -592,10 +515,7 @@ pub fn build_server_hello(
|
|||||||
|
|
||||||
// Combine all records
|
// Combine all records
|
||||||
let mut response = Vec::with_capacity(
|
let mut response = Vec::with_capacity(
|
||||||
server_hello.len()
|
server_hello.len() + change_cipher_spec.len() + app_data_record.len() + tickets.iter().map(|r| r.len()).sum::<usize>()
|
||||||
+ change_cipher_spec.len()
|
|
||||||
+ app_data_record.len()
|
|
||||||
+ tickets.iter().map(|r| r.len()).sum::<usize>(),
|
|
||||||
);
|
);
|
||||||
response.extend_from_slice(&server_hello);
|
response.extend_from_slice(&server_hello);
|
||||||
response.extend_from_slice(&change_cipher_spec);
|
response.extend_from_slice(&change_cipher_spec);
|
||||||
@@ -612,7 +532,8 @@ pub fn build_server_hello(
|
|||||||
|
|
||||||
// Insert computed digest into ServerHello
|
// Insert computed digest into ServerHello
|
||||||
// Position: record header (5) + message type (1) + length (3) + version (2) = 11
|
// Position: record header (5) + message type (1) + length (3) + version (2) = 11
|
||||||
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN].copy_from_slice(&response_digest);
|
response[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]
|
||||||
|
.copy_from_slice(&response_digest);
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
@@ -690,20 +611,19 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
|
|||||||
let sn_end = std::cmp::min(sn_pos + list_len, pos + elen);
|
let sn_end = std::cmp::min(sn_pos + list_len, pos + elen);
|
||||||
while sn_pos + 3 <= sn_end {
|
while sn_pos + 3 <= sn_end {
|
||||||
let name_type = handshake[sn_pos];
|
let name_type = handshake[sn_pos];
|
||||||
let name_len =
|
let name_len = u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
||||||
u16::from_be_bytes([handshake[sn_pos + 1], handshake[sn_pos + 2]]) as usize;
|
|
||||||
sn_pos += 3;
|
sn_pos += 3;
|
||||||
if sn_pos + name_len > sn_end {
|
if sn_pos + name_len > sn_end {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if name_type == 0
|
if name_type == 0 && name_len > 0
|
||||||
&& name_len > 0
|
|
||||||
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
|
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
|
||||||
&& is_valid_sni_hostname(host)
|
|
||||||
{
|
{
|
||||||
|
if is_valid_sni_hostname(host) {
|
||||||
extracted_sni = Some(host.to_string());
|
extracted_sni = Some(host.to_string());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
sn_pos += name_len;
|
sn_pos += name_len;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -759,38 +679,26 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
pos += 4; // type + len
|
pos += 4; // type + len
|
||||||
pos += 2 + 32; // version + random
|
pos += 2 + 32; // version + random
|
||||||
if pos >= handshake.len() {
|
if pos >= handshake.len() { return Vec::new(); }
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
pos += 1 + session_id_len;
|
pos += 1 + session_id_len;
|
||||||
if pos + 2 > handshake.len() {
|
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||||
pos += 2 + cipher_len;
|
pos += 2 + cipher_len;
|
||||||
if pos >= handshake.len() {
|
if pos >= handshake.len() { return Vec::new(); }
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
pos += 1 + comp_len;
|
pos += 1 + comp_len;
|
||||||
if pos + 2 > handshake.len() {
|
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||||
pos += 2;
|
pos += 2;
|
||||||
let ext_end = pos + ext_len;
|
let ext_end = pos + ext_len;
|
||||||
if ext_end > handshake.len() {
|
if ext_end > handshake.len() { return Vec::new(); }
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
while pos + 4 <= ext_end {
|
while pos + 4 <= ext_end {
|
||||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos+1]]);
|
let etype = u16::from_be_bytes([handshake[pos], handshake[pos+1]]);
|
||||||
let elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize;
|
let elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize;
|
||||||
pos += 4;
|
pos += 4;
|
||||||
if pos + elen > ext_end {
|
if pos + elen > ext_end { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
if etype == extension_type::ALPN && elen >= 3 {
|
if etype == extension_type::ALPN && elen >= 3 {
|
||||||
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
let list_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||||
let mut lp = pos + 2;
|
let mut lp = pos + 2;
|
||||||
@@ -798,9 +706,7 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
while lp < list_end {
|
while lp < list_end {
|
||||||
let plen = handshake[lp] as usize;
|
let plen = handshake[lp] as usize;
|
||||||
lp += 1;
|
lp += 1;
|
||||||
if lp + plen > list_end {
|
if lp + plen > list_end { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
out.push(handshake[lp..lp+plen].to_vec());
|
out.push(handshake[lp..lp+plen].to_vec());
|
||||||
lp += plen;
|
lp += plen;
|
||||||
}
|
}
|
||||||
@@ -811,6 +717,7 @@ pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Check if bytes look like a TLS ClientHello
|
/// Check if bytes look like a TLS ClientHello
|
||||||
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||||
if first_bytes.len() < 3 {
|
if first_bytes.len() < 3 {
|
||||||
@@ -824,6 +731,7 @@ pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse TLS record header, returns (record_type, length)
|
/// Parse TLS record header, returns (record_type, length)
|
||||||
|
|
||||||
pub fn parse_tls_record_header(header: &[u8; 5]) -> Option<(u8, u16)> {
|
pub fn parse_tls_record_header(header: &[u8; 5]) -> Option<(u8, u16)> {
|
||||||
let record_type = header[0];
|
let record_type = header[0];
|
||||||
let version = [header[1], header[2]];
|
let version = [header[1], header[2]];
|
||||||
@@ -868,28 +776,25 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
|||||||
// Check record length
|
// Check record length
|
||||||
let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
let record_len = u16::from_be_bytes([data[3], data[4]]) as usize;
|
||||||
if data.len() < 5 + record_len {
|
if data.len() < 5 + record_len {
|
||||||
return Err(ProxyError::InvalidHandshake(format!(
|
return Err(ProxyError::InvalidHandshake(
|
||||||
"ServerHello record truncated: expected {}, got {}",
|
format!("ServerHello record truncated: expected {}, got {}",
|
||||||
5 + record_len,
|
5 + record_len, data.len())
|
||||||
data.len()
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check message type
|
// Check message type
|
||||||
if data[5] != 0x02 {
|
if data[5] != 0x02 {
|
||||||
return Err(ProxyError::InvalidHandshake(format!(
|
return Err(ProxyError::InvalidHandshake(
|
||||||
"Expected ServerHello (0x02), got 0x{:02x}",
|
format!("Expected ServerHello (0x02), got 0x{:02x}", data[5])
|
||||||
data[5]
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse message length
|
// Parse message length
|
||||||
let msg_len = u32::from_be_bytes([0, data[6], data[7], data[8]]) as usize;
|
let msg_len = u32::from_be_bytes([0, data[6], data[7], data[8]]) as usize;
|
||||||
if msg_len + 4 != record_len {
|
if msg_len + 4 != record_len {
|
||||||
return Err(ProxyError::InvalidHandshake(format!(
|
return Err(ProxyError::InvalidHandshake(
|
||||||
"Message length mismatch: {} + 4 != {}",
|
format!("Message length mismatch: {} + 4 != {}", msg_len, record_len)
|
||||||
msg_len, record_len
|
));
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -901,7 +806,7 @@ fn validate_server_hello_structure(data: &[u8]) -> Result<(), ProxyError> {
|
|||||||
/// Using `static_assertions` ensures these can never silently break across
|
/// Using `static_assertions` ensures these can never silently break across
|
||||||
/// refactors without a compile error.
|
/// refactors without a compile error.
|
||||||
mod compile_time_security_checks {
|
mod compile_time_security_checks {
|
||||||
use super::{TLS_DIGEST_HALF_LEN, TLS_DIGEST_LEN};
|
use super::{TLS_DIGEST_LEN, TLS_DIGEST_HALF_LEN};
|
||||||
use static_assertions::const_assert;
|
use static_assertions::const_assert;
|
||||||
|
|
||||||
// The digest must be exactly one SHA-256 output.
|
// The digest must be exactly one SHA-256 output.
|
||||||
@@ -929,7 +834,3 @@ mod adversarial_tests;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/tls_fuzz_security_tests.rs"]
|
#[path = "tests/tls_fuzz_security_tests.rs"]
|
||||||
mod fuzz_security_tests;
|
mod fuzz_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/tls_length_cast_hardening_security_tests.rs"]
|
|
||||||
mod length_cast_hardening_security_tests;
|
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
// Adaptive buffer policy is staged and retained for deterministic rollout.
|
|
||||||
// Keep definitions compiled for compatibility and security test scaffolding.
|
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
@@ -175,8 +170,7 @@ impl SessionAdaptiveController {
|
|||||||
return self.promote(TierTransitionReason::SoftConfirmed, 0);
|
return self.promote(TierTransitionReason::SoftConfirmed, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let demote_candidate =
|
let demote_candidate = self.throughput_ema_bps < THROUGHPUT_DOWN_BPS && !tier2_now && !hard_now;
|
||||||
self.throughput_ema_bps < THROUGHPUT_DOWN_BPS && !tier2_now && !hard_now;
|
|
||||||
if demote_candidate {
|
if demote_candidate {
|
||||||
self.quiet_ticks = self.quiet_ticks.saturating_add(1);
|
self.quiet_ticks = self.quiet_ticks.saturating_add(1);
|
||||||
if self.quiet_ticks >= QUIET_DEMOTE_TICKS {
|
if self.quiet_ticks >= QUIET_DEMOTE_TICKS {
|
||||||
@@ -259,7 +253,10 @@ pub fn record_user_tier(user: &str, tier: AdaptiveTier) {
|
|||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
profiles().insert(user.to_string(), UserAdaptiveProfile { tier, seen_at: now });
|
profiles().insert(
|
||||||
|
user.to_string(),
|
||||||
|
UserAdaptiveProfile { tier, seen_at: now },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn direct_copy_buffers_for_tier(
|
pub fn direct_copy_buffers_for_tier(
|
||||||
@@ -342,7 +339,10 @@ mod tests {
|
|||||||
sample(
|
sample(
|
||||||
300_000, // ~9.6 Mbps
|
300_000, // ~9.6 Mbps
|
||||||
320_000, // incoming > outgoing to confirm tier2
|
320_000, // incoming > outgoing to confirm tier2
|
||||||
250_000, 10, 0, 0,
|
250_000,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
tick_secs,
|
tick_secs,
|
||||||
);
|
);
|
||||||
@@ -358,7 +358,10 @@ mod tests {
|
|||||||
fn test_hard_promotion_on_pending_pressure() {
|
fn test_hard_promotion_on_pending_pressure() {
|
||||||
let mut ctrl = SessionAdaptiveController::new(AdaptiveTier::Base);
|
let mut ctrl = SessionAdaptiveController::new(AdaptiveTier::Base);
|
||||||
let transition = ctrl
|
let transition = ctrl
|
||||||
.observe(sample(10_000, 20_000, 10_000, 4, 1, 3), 0.25)
|
.observe(
|
||||||
|
sample(10_000, 20_000, 10_000, 4, 1, 3),
|
||||||
|
0.25,
|
||||||
|
)
|
||||||
.expect("expected hard promotion");
|
.expect("expected hard promotion");
|
||||||
assert_eq!(transition.reason, TierTransitionReason::HardPressure);
|
assert_eq!(transition.reason, TierTransitionReason::HardPressure);
|
||||||
assert_eq!(transition.to, AdaptiveTier::Tier1);
|
assert_eq!(transition.to, AdaptiveTier::Tier1);
|
||||||
|
|||||||
+59
-347
@@ -1,7 +1,5 @@
|
|||||||
//! Client Handler
|
//! Client Handler
|
||||||
|
|
||||||
use ipnetwork::IpNetwork;
|
|
||||||
use rand::RngExt;
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@@ -9,6 +7,8 @@ use std::sync::Arc;
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use rand::RngExt;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
@@ -75,10 +75,10 @@ use crate::protocol::tls;
|
|||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::stats::{ReplayChecker, Stats};
|
use crate::stats::{ReplayChecker, Stats};
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
||||||
use crate::tls_front::TlsFrontCache;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
use crate::transport::socket::normalize_ip;
|
|
||||||
use crate::transport::{UpstreamManager, configure_client_socket, parse_proxy_protocol};
|
use crate::transport::{UpstreamManager, configure_client_socket, parse_proxy_protocol};
|
||||||
|
use crate::transport::socket::normalize_ip;
|
||||||
|
use crate::tls_front::TlsFrontCache;
|
||||||
|
|
||||||
use crate::proxy::direct_relay::handle_via_direct;
|
use crate::proxy::direct_relay::handle_via_direct;
|
||||||
use crate::proxy::handshake::{HandshakeSuccess, handle_mtproto_handshake, handle_tls_handshake};
|
use crate::proxy::handshake::{HandshakeSuccess, handle_mtproto_handshake, handle_tls_handshake};
|
||||||
@@ -116,23 +116,11 @@ fn beobachten_ttl(config: &ProxyConfig) -> Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_tls_application_record(payload: &[u8]) -> Vec<u8> {
|
fn wrap_tls_application_record(payload: &[u8]) -> Vec<u8> {
|
||||||
let chunks = payload.len().div_ceil(u16::MAX as usize).max(1);
|
let mut record = Vec::with_capacity(5 + payload.len());
|
||||||
let mut record = Vec::with_capacity(payload.len() + 5 * chunks);
|
|
||||||
|
|
||||||
if payload.is_empty() {
|
|
||||||
record.push(TLS_RECORD_APPLICATION);
|
record.push(TLS_RECORD_APPLICATION);
|
||||||
record.extend_from_slice(&TLS_VERSION);
|
record.extend_from_slice(&TLS_VERSION);
|
||||||
record.extend_from_slice(&0u16.to_be_bytes());
|
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||||
return record;
|
record.extend_from_slice(payload);
|
||||||
}
|
|
||||||
|
|
||||||
for chunk in payload.chunks(u16::MAX as usize) {
|
|
||||||
record.push(TLS_RECORD_APPLICATION);
|
|
||||||
record.extend_from_slice(&TLS_VERSION);
|
|
||||||
record.extend_from_slice(&(chunk.len() as u16).to_be_bytes());
|
|
||||||
record.extend_from_slice(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
record
|
record
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +128,7 @@ fn tls_clienthello_len_in_bounds(tls_len: usize) -> bool {
|
|||||||
(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&tls_len)
|
(MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&tls_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_with_progress<R: AsyncRead + Unpin>(
|
async fn read_with_progress<R: AsyncRead + Unpin>(reader: &mut R, mut buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
reader: &mut R,
|
|
||||||
mut buf: &mut [u8],
|
|
||||||
) -> std::io::Result<usize> {
|
|
||||||
let mut total = 0usize;
|
let mut total = 0usize;
|
||||||
while !buf.is_empty() {
|
while !buf.is_empty() {
|
||||||
match reader.read(buf).await {
|
match reader.read(buf).await {
|
||||||
@@ -186,72 +171,6 @@ fn handshake_timeout_with_mask_grace(config: &ProxyConfig) -> Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MASK_CLASSIFIER_PREFETCH_WINDOW: usize = 16;
|
|
||||||
#[cfg(test)]
|
|
||||||
const MASK_CLASSIFIER_PREFETCH_TIMEOUT: Duration = Duration::from_millis(5);
|
|
||||||
|
|
||||||
fn mask_classifier_prefetch_timeout(config: &ProxyConfig) -> Duration {
|
|
||||||
Duration::from_millis(config.censorship.mask_classifier_prefetch_timeout_ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_prefetch_mask_classifier_window(initial_data: &[u8]) -> bool {
|
|
||||||
if initial_data.len() >= MASK_CLASSIFIER_PREFETCH_WINDOW {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if initial_data.is_empty() {
|
|
||||||
// Empty initial_data means there is no client probe prefix to refine.
|
|
||||||
// Prefetching in this case can consume fallback relay payload bytes and
|
|
||||||
// accidentally route them through shaping heuristics.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if initial_data[0] == 0x16 || initial_data.starts_with(b"SSH-") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
initial_data
|
|
||||||
.iter()
|
|
||||||
.all(|b| b.is_ascii_alphabetic() || *b == b' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
async fn extend_masking_initial_window<R>(reader: &mut R, initial_data: &mut Vec<u8>)
|
|
||||||
where
|
|
||||||
R: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
extend_masking_initial_window_with_timeout(
|
|
||||||
reader,
|
|
||||||
initial_data,
|
|
||||||
MASK_CLASSIFIER_PREFETCH_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extend_masking_initial_window_with_timeout<R>(
|
|
||||||
reader: &mut R,
|
|
||||||
initial_data: &mut Vec<u8>,
|
|
||||||
prefetch_timeout: Duration,
|
|
||||||
) where
|
|
||||||
R: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
if !should_prefetch_mask_classifier_window(initial_data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let need = MASK_CLASSIFIER_PREFETCH_WINDOW.saturating_sub(initial_data.len());
|
|
||||||
if need == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut extra = [0u8; MASK_CLASSIFIER_PREFETCH_WINDOW];
|
|
||||||
if let Ok(Ok(n)) = timeout(prefetch_timeout, reader.read(&mut extra[..need])).await
|
|
||||||
&& n > 0
|
|
||||||
{
|
|
||||||
initial_data.extend_from_slice(&extra[..n]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn masking_outcome<R, W>(
|
fn masking_outcome<R, W>(
|
||||||
reader: R,
|
reader: R,
|
||||||
writer: W,
|
writer: W,
|
||||||
@@ -266,15 +185,6 @@ where
|
|||||||
W: AsyncWrite + Unpin + Send + 'static,
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
HandshakeOutcome::NeedsMasking(Box::pin(async move {
|
HandshakeOutcome::NeedsMasking(Box::pin(async move {
|
||||||
let mut reader = reader;
|
|
||||||
let mut initial_data = initial_data;
|
|
||||||
extend_masking_initial_window_with_timeout(
|
|
||||||
&mut reader,
|
|
||||||
&mut initial_data,
|
|
||||||
mask_classifier_prefetch_timeout(&config),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
handle_bad_client(
|
handle_bad_client(
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
@@ -317,20 +227,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;
|
||||||
@@ -368,14 +271,10 @@ where
|
|||||||
let mut local_addr = synthetic_local_addr(config.server.port);
|
let mut local_addr = synthetic_local_addr(config.server.port);
|
||||||
|
|
||||||
if proxy_protocol_enabled {
|
if proxy_protocol_enabled {
|
||||||
let proxy_header_timeout =
|
let proxy_header_timeout = Duration::from_millis(
|
||||||
Duration::from_millis(config.server.proxy_protocol_header_timeout_ms.max(1));
|
config.server.proxy_protocol_header_timeout_ms.max(1),
|
||||||
match timeout(
|
);
|
||||||
proxy_header_timeout,
|
match timeout(proxy_header_timeout, parse_proxy_protocol(&mut stream, peer)).await {
|
||||||
parse_proxy_protocol(&mut stream, peer),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(info)) => {
|
Ok(Ok(info)) => {
|
||||||
if !is_trusted_proxy_source(peer.ip(), &config.server.proxy_protocol_trusted_cidrs)
|
if !is_trusted_proxy_source(peer.ip(), &config.server.proxy_protocol_trusted_cidrs)
|
||||||
{
|
{
|
||||||
@@ -416,68 +315,16 @@ where
|
|||||||
|
|
||||||
debug!(peer = %real_peer, "New connection (generic stream)");
|
debug!(peer = %real_peer, "New connection (generic stream)");
|
||||||
|
|
||||||
let first_byte = if config.timeouts.client_first_byte_idle_secs == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let idle_timeout = Duration::from_secs(config.timeouts.client_first_byte_idle_secs);
|
|
||||||
let mut first_byte = [0u8; 1];
|
|
||||||
match timeout(idle_timeout, stream.read(&mut first_byte)).await {
|
|
||||||
Ok(Ok(0)) => {
|
|
||||||
debug!(peer = %real_peer, "Connection closed before first client byte");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Ok(Ok(_)) => Some(first_byte[0]),
|
|
||||||
Ok(Err(e))
|
|
||||||
if matches!(
|
|
||||||
e.kind(),
|
|
||||||
std::io::ErrorKind::UnexpectedEof
|
|
||||||
| std::io::ErrorKind::ConnectionReset
|
|
||||||
| std::io::ErrorKind::ConnectionAborted
|
|
||||||
| std::io::ErrorKind::BrokenPipe
|
|
||||||
| std::io::ErrorKind::NotConnected
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
debug!(
|
|
||||||
peer = %real_peer,
|
|
||||||
error = %e,
|
|
||||||
"Connection closed before first client byte"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
debug!(
|
|
||||||
peer = %real_peer,
|
|
||||||
error = %e,
|
|
||||||
"Failed while waiting for first client byte"
|
|
||||||
);
|
|
||||||
return Err(ProxyError::Io(e));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
debug!(
|
|
||||||
peer = %real_peer,
|
|
||||||
idle_secs = config.timeouts.client_first_byte_idle_secs,
|
|
||||||
"Closing idle pooled connection before first client byte"
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let handshake_timeout = handshake_timeout_with_mask_grace(&config);
|
let handshake_timeout = handshake_timeout_with_mask_grace(&config);
|
||||||
let stats_for_timeout = stats.clone();
|
let stats_for_timeout = stats.clone();
|
||||||
let config_for_timeout = config.clone();
|
let config_for_timeout = config.clone();
|
||||||
let beobachten_for_timeout = beobachten.clone();
|
let beobachten_for_timeout = beobachten.clone();
|
||||||
let peer_for_timeout = real_peer.ip();
|
let peer_for_timeout = real_peer.ip();
|
||||||
|
|
||||||
// Phase 2: active handshake (with timeout after the first client byte)
|
// Phase 1: handshake (with timeout)
|
||||||
let outcome = match timeout(handshake_timeout, async {
|
let outcome = match timeout(handshake_timeout, async {
|
||||||
let mut first_bytes = [0u8; 5];
|
let mut first_bytes = [0u8; 5];
|
||||||
if let Some(first_byte) = first_byte {
|
|
||||||
first_bytes[0] = first_byte;
|
|
||||||
stream.read_exact(&mut first_bytes[1..]).await?;
|
|
||||||
} else {
|
|
||||||
stream.read_exact(&mut first_bytes).await?;
|
stream.read_exact(&mut first_bytes).await?;
|
||||||
}
|
|
||||||
|
|
||||||
let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
|
let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
|
||||||
debug!(peer = %real_peer, is_tls = is_tls, "Handshake type detected");
|
debug!(peer = %real_peer, is_tls = is_tls, "Handshake type detected");
|
||||||
@@ -567,10 +414,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");
|
||||||
@@ -788,9 +632,36 @@ impl RunningClientHandler {
|
|||||||
debug!(peer = %peer, error = %e, "Failed to configure client socket");
|
debug!(peer = %peer, error = %e, "Failed to configure client socket");
|
||||||
}
|
}
|
||||||
|
|
||||||
let outcome = match self.do_handshake().await? {
|
let handshake_timeout = handshake_timeout_with_mask_grace(&self.config);
|
||||||
Some(outcome) => outcome,
|
let stats = self.stats.clone();
|
||||||
None => return Ok(()),
|
let config_for_timeout = self.config.clone();
|
||||||
|
let beobachten_for_timeout = self.beobachten.clone();
|
||||||
|
let peer_for_timeout = peer.ip();
|
||||||
|
|
||||||
|
// Phase 1: handshake (with timeout)
|
||||||
|
let outcome = match timeout(handshake_timeout, self.do_handshake()).await {
|
||||||
|
Ok(Ok(outcome)) => outcome,
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
debug!(peer = %peer, error = %e, "Handshake failed");
|
||||||
|
record_handshake_failure_class(
|
||||||
|
&beobachten_for_timeout,
|
||||||
|
&config_for_timeout,
|
||||||
|
peer_for_timeout,
|
||||||
|
&e,
|
||||||
|
);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
stats.increment_handshake_timeouts();
|
||||||
|
debug!(peer = %peer, "Handshake timeout");
|
||||||
|
record_beobachten_class(
|
||||||
|
&beobachten_for_timeout,
|
||||||
|
&config_for_timeout,
|
||||||
|
peer_for_timeout,
|
||||||
|
"other",
|
||||||
|
);
|
||||||
|
return Err(ProxyError::TgHandshakeTimeout);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
|
// Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
|
||||||
@@ -799,12 +670,13 @@ impl RunningClientHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn do_handshake(mut self) -> Result<Option<HandshakeOutcome>> {
|
async fn do_handshake(mut self) -> Result<HandshakeOutcome> {
|
||||||
let mut local_addr = self.stream.local_addr().map_err(ProxyError::Io)?;
|
let mut local_addr = self.stream.local_addr().map_err(ProxyError::Io)?;
|
||||||
|
|
||||||
if self.proxy_protocol_enabled {
|
if self.proxy_protocol_enabled {
|
||||||
let proxy_header_timeout =
|
let proxy_header_timeout = Duration::from_millis(
|
||||||
Duration::from_millis(self.config.server.proxy_protocol_header_timeout_ms.max(1));
|
self.config.server.proxy_protocol_header_timeout_ms.max(1),
|
||||||
|
);
|
||||||
match timeout(
|
match timeout(
|
||||||
proxy_header_timeout,
|
proxy_header_timeout,
|
||||||
parse_proxy_protocol(&mut self.stream, self.peer),
|
parse_proxy_protocol(&mut self.stream, self.peer),
|
||||||
@@ -874,69 +746,8 @@ impl RunningClientHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_byte = if self.config.timeouts.client_first_byte_idle_secs == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let idle_timeout =
|
|
||||||
Duration::from_secs(self.config.timeouts.client_first_byte_idle_secs);
|
|
||||||
let mut first_byte = [0u8; 1];
|
|
||||||
match timeout(idle_timeout, self.stream.read(&mut first_byte)).await {
|
|
||||||
Ok(Ok(0)) => {
|
|
||||||
debug!(peer = %self.peer, "Connection closed before first client byte");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Ok(_)) => Some(first_byte[0]),
|
|
||||||
Ok(Err(e))
|
|
||||||
if matches!(
|
|
||||||
e.kind(),
|
|
||||||
std::io::ErrorKind::UnexpectedEof
|
|
||||||
| std::io::ErrorKind::ConnectionReset
|
|
||||||
| std::io::ErrorKind::ConnectionAborted
|
|
||||||
| std::io::ErrorKind::BrokenPipe
|
|
||||||
| std::io::ErrorKind::NotConnected
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
debug!(
|
|
||||||
peer = %self.peer,
|
|
||||||
error = %e,
|
|
||||||
"Connection closed before first client byte"
|
|
||||||
);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
debug!(
|
|
||||||
peer = %self.peer,
|
|
||||||
error = %e,
|
|
||||||
"Failed while waiting for first client byte"
|
|
||||||
);
|
|
||||||
return Err(ProxyError::Io(e));
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
debug!(
|
|
||||||
peer = %self.peer,
|
|
||||||
idle_secs = self.config.timeouts.client_first_byte_idle_secs,
|
|
||||||
"Closing idle pooled connection before first client byte"
|
|
||||||
);
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let handshake_timeout = handshake_timeout_with_mask_grace(&self.config);
|
|
||||||
let stats = self.stats.clone();
|
|
||||||
let config_for_timeout = self.config.clone();
|
|
||||||
let beobachten_for_timeout = self.beobachten.clone();
|
|
||||||
let peer_for_timeout = self.peer.ip();
|
|
||||||
let peer_for_log = self.peer;
|
|
||||||
|
|
||||||
let outcome = match timeout(handshake_timeout, async {
|
|
||||||
let mut first_bytes = [0u8; 5];
|
let mut first_bytes = [0u8; 5];
|
||||||
if let Some(first_byte) = first_byte {
|
|
||||||
first_bytes[0] = first_byte;
|
|
||||||
self.stream.read_exact(&mut first_bytes[1..]).await?;
|
|
||||||
} else {
|
|
||||||
self.stream.read_exact(&mut first_bytes).await?;
|
self.stream.read_exact(&mut first_bytes).await?;
|
||||||
}
|
|
||||||
|
|
||||||
let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
|
let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
|
||||||
let peer = self.peer;
|
let peer = self.peer;
|
||||||
@@ -948,41 +759,9 @@ impl RunningClientHandler {
|
|||||||
} else {
|
} else {
|
||||||
self.handle_direct_client(first_bytes, local_addr).await
|
self.handle_direct_client(first_bytes, local_addr).await
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(outcome)) => outcome,
|
|
||||||
Ok(Err(e)) => {
|
|
||||||
debug!(peer = %peer_for_log, error = %e, "Handshake failed");
|
|
||||||
record_handshake_failure_class(
|
|
||||||
&beobachten_for_timeout,
|
|
||||||
&config_for_timeout,
|
|
||||||
peer_for_timeout,
|
|
||||||
&e,
|
|
||||||
);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
stats.increment_handshake_timeouts();
|
|
||||||
debug!(peer = %peer_for_log, "Handshake timeout");
|
|
||||||
record_beobachten_class(
|
|
||||||
&beobachten_for_timeout,
|
|
||||||
&config_for_timeout,
|
|
||||||
peer_for_timeout,
|
|
||||||
"other",
|
|
||||||
);
|
|
||||||
return Err(ProxyError::TgHandshakeTimeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(outcome))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tls_client(
|
async fn handle_tls_client(mut self, first_bytes: [u8; 5], local_addr: SocketAddr) -> Result<HandshakeOutcome> {
|
||||||
mut self,
|
|
||||||
first_bytes: [u8; 5],
|
|
||||||
local_addr: SocketAddr,
|
|
||||||
) -> Result<HandshakeOutcome> {
|
|
||||||
let peer = self.peer;
|
let peer = self.peer;
|
||||||
|
|
||||||
let tls_len = u16::from_be_bytes([first_bytes[3], first_bytes[4]]) as usize;
|
let tls_len = u16::from_be_bytes([first_bytes[3], first_bytes[4]]) as usize;
|
||||||
@@ -1083,10 +862,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");
|
||||||
@@ -1119,8 +895,7 @@ impl RunningClientHandler {
|
|||||||
} else {
|
} else {
|
||||||
wrap_tls_application_record(&pending_plaintext)
|
wrap_tls_application_record(&pending_plaintext)
|
||||||
};
|
};
|
||||||
let reader =
|
let reader = tokio::io::AsyncReadExt::chain(std::io::Cursor::new(pending_record), reader);
|
||||||
tokio::io::AsyncReadExt::chain(std::io::Cursor::new(pending_record), reader);
|
|
||||||
stats.increment_connects_bad();
|
stats.increment_connects_bad();
|
||||||
debug!(
|
debug!(
|
||||||
peer = %peer,
|
peer = %peer,
|
||||||
@@ -1158,11 +933,7 @@ impl RunningClientHandler {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_direct_client(
|
async fn handle_direct_client(mut self, first_bytes: [u8; 5], local_addr: SocketAddr) -> Result<HandshakeOutcome> {
|
||||||
mut self,
|
|
||||||
first_bytes: [u8; 5],
|
|
||||||
local_addr: SocketAddr,
|
|
||||||
) -> Result<HandshakeOutcome> {
|
|
||||||
let peer = self.peer;
|
let peer = self.peer;
|
||||||
|
|
||||||
if !self.config.general.modes.classic && !self.config.general.modes.secure {
|
if !self.config.general.modes.classic && !self.config.general.modes.secure {
|
||||||
@@ -1264,7 +1035,8 @@ impl RunningClientHandler {
|
|||||||
{
|
{
|
||||||
let user = success.user.clone();
|
let user = success.user.clone();
|
||||||
|
|
||||||
let user_limit_reservation = match Self::acquire_user_connection_reservation_static(
|
let user_limit_reservation =
|
||||||
|
match Self::acquire_user_connection_reservation_static(
|
||||||
&user,
|
&user,
|
||||||
&config,
|
&config,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
@@ -1355,22 +1127,14 @@ impl RunningClientHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(quota) = config.access.user_data_quota.get(user)
|
if let Some(quota) = config.access.user_data_quota.get(user)
|
||||||
&& stats.get_user_quota_used(user) >= *quota
|
&& stats.get_user_total_octets(user) >= *quota
|
||||||
{
|
{
|
||||||
return Err(ProxyError::DataQuotaExceeded {
|
return Err(ProxyError::DataQuotaExceeded {
|
||||||
user: user.to_string(),
|
user: user.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = config
|
let limit = config.access.user_max_tcp_conns.get(user).map(|v| *v as u64);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.get(user)
|
|
||||||
.copied()
|
|
||||||
.filter(|limit| *limit > 0)
|
|
||||||
.or((config.access.user_max_tcp_conns_global_each > 0)
|
|
||||||
.then_some(config.access.user_max_tcp_conns_global_each))
|
|
||||||
.map(|v| v as u64);
|
|
||||||
if !stats.try_acquire_user_curr_connects(user, limit) {
|
if !stats.try_acquire_user_curr_connects(user, limit) {
|
||||||
return Err(ProxyError::ConnectionLimitExceeded {
|
return Err(ProxyError::ConnectionLimitExceeded {
|
||||||
user: user.to_string(),
|
user: user.to_string(),
|
||||||
@@ -1418,7 +1182,7 @@ impl RunningClientHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(quota) = config.access.user_data_quota.get(user)
|
if let Some(quota) = config.access.user_data_quota.get(user)
|
||||||
&& stats.get_user_quota_used(user) >= *quota
|
&& stats.get_user_total_octets(user) >= *quota
|
||||||
{
|
{
|
||||||
return Err(ProxyError::DataQuotaExceeded {
|
return Err(ProxyError::DataQuotaExceeded {
|
||||||
user: user.to_string(),
|
user: user.to_string(),
|
||||||
@@ -1429,11 +1193,7 @@ impl RunningClientHandler {
|
|||||||
.access
|
.access
|
||||||
.user_max_tcp_conns
|
.user_max_tcp_conns
|
||||||
.get(user)
|
.get(user)
|
||||||
.copied()
|
.map(|v| *v as u64);
|
||||||
.filter(|limit| *limit > 0)
|
|
||||||
.or((config.access.user_max_tcp_conns_global_each > 0)
|
|
||||||
.then_some(config.access.user_max_tcp_conns_global_each))
|
|
||||||
.map(|v| v as u64);
|
|
||||||
if !stats.try_acquire_user_curr_connects(user, limit) {
|
if !stats.try_acquire_user_curr_connects(user, limit) {
|
||||||
return Err(ProxyError::ConnectionLimitExceeded {
|
return Err(ProxyError::ConnectionLimitExceeded {
|
||||||
user: user.to_string(),
|
user: user.to_string(),
|
||||||
@@ -1531,54 +1291,6 @@ mod masking_shape_classifier_fuzz_redteam_expected_fail_tests;
|
|||||||
#[path = "tests/client_masking_probe_evasion_blackhat_tests.rs"]
|
#[path = "tests/client_masking_probe_evasion_blackhat_tests.rs"]
|
||||||
mod masking_probe_evasion_blackhat_tests;
|
mod masking_probe_evasion_blackhat_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_fragmented_classifier_security_tests.rs"]
|
|
||||||
mod masking_fragmented_classifier_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_replay_timing_security_tests.rs"]
|
|
||||||
mod masking_replay_timing_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_http2_fragmented_preface_security_tests.rs"]
|
|
||||||
mod masking_http2_fragmented_preface_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_prefetch_invariant_security_tests.rs"]
|
|
||||||
mod masking_prefetch_invariant_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_prefetch_timing_matrix_security_tests.rs"]
|
|
||||||
mod masking_prefetch_timing_matrix_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_prefetch_config_runtime_security_tests.rs"]
|
|
||||||
mod masking_prefetch_config_runtime_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_prefetch_config_pipeline_integration_security_tests.rs"]
|
|
||||||
mod masking_prefetch_config_pipeline_integration_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_masking_prefetch_strict_boundary_security_tests.rs"]
|
|
||||||
mod masking_prefetch_strict_boundary_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/client_beobachten_ttl_bounds_security_tests.rs"]
|
#[path = "tests/client_beobachten_ttl_bounds_security_tests.rs"]
|
||||||
mod beobachten_ttl_bounds_security_tests;
|
mod beobachten_ttl_bounds_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_tls_record_wrap_hardening_security_tests.rs"]
|
|
||||||
mod tls_record_wrap_hardening_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_clever_advanced_tests.rs"]
|
|
||||||
mod client_clever_advanced_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_more_advanced_tests.rs"]
|
|
||||||
mod client_more_advanced_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/client_deep_invariants_tests.rs"]
|
|
||||||
mod client_deep_invariants_tests;
|
|
||||||
|
|||||||
+33
-25
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
||||||
@@ -24,13 +24,13 @@ use crate::proxy::route_mode::{
|
|||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
#[cfg(unix)]
|
|
||||||
use nix::fcntl::{Flock, FlockArg, OFlag, openat};
|
|
||||||
#[cfg(unix)]
|
|
||||||
use nix::sys::stat::Mode;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
|
|
||||||
const UNKNOWN_DC_LOG_DISTINCT_LIMIT: usize = 1024;
|
const UNKNOWN_DC_LOG_DISTINCT_LIMIT: usize = 1024;
|
||||||
static LOGGED_UNKNOWN_DCS: OnceLock<Mutex<HashSet<i16>>> = OnceLock::new();
|
static LOGGED_UNKNOWN_DCS: OnceLock<Mutex<HashSet<i16>>> = OnceLock::new();
|
||||||
@@ -160,9 +160,7 @@ fn open_unknown_dc_log_append(path: &Path) -> std::io::Result<std::fs::File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_unknown_dc_log_append_anchored(
|
fn open_unknown_dc_log_append_anchored(path: &SanitizedUnknownDcLogPath) -> std::io::Result<std::fs::File> {
|
||||||
path: &SanitizedUnknownDcLogPath,
|
|
||||||
) -> std::io::Result<std::fs::File> {
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let parent = OpenOptions::new()
|
let parent = OpenOptions::new()
|
||||||
@@ -170,16 +168,23 @@ fn open_unknown_dc_log_append_anchored(
|
|||||||
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
|
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
|
||||||
.open(&path.allowed_parent)?;
|
.open(&path.allowed_parent)?;
|
||||||
|
|
||||||
let oflags = OFlag::O_CREAT
|
let file_name = std::ffi::CString::new(path.file_name.as_os_str().as_bytes())
|
||||||
| OFlag::O_APPEND
|
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "unknown DC log file name contains NUL byte"))?;
|
||||||
| OFlag::O_WRONLY
|
|
||||||
| OFlag::O_NOFOLLOW
|
let fd = unsafe {
|
||||||
| OFlag::O_CLOEXEC;
|
libc::openat(
|
||||||
let mode = Mode::from_bits_truncate(0o600);
|
parent.as_raw_fd(),
|
||||||
let path_component = Path::new(path.file_name.as_os_str());
|
file_name.as_ptr(),
|
||||||
let fd = openat(&parent, path_component, oflags, mode)
|
libc::O_CREAT | libc::O_APPEND | libc::O_WRONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
|
||||||
.map_err(|err| std::io::Error::from_raw_os_error(err as i32))?;
|
0o600,
|
||||||
let file = std::fs::File::from(fd);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if fd < 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -195,13 +200,16 @@ fn open_unknown_dc_log_append_anchored(
|
|||||||
fn append_unknown_dc_line(file: &mut std::fs::File, dc_idx: i16) -> std::io::Result<()> {
|
fn append_unknown_dc_line(file: &mut std::fs::File, dc_idx: i16) -> std::io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let cloned = file.try_clone()?;
|
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
|
||||||
let mut locked = Flock::lock(cloned, FlockArg::LockExclusive)
|
return Err(std::io::Error::last_os_error());
|
||||||
.map_err(|(_, err)| std::io::Error::from_raw_os_error(err as i32))?;
|
}
|
||||||
let write_result = writeln!(&mut *locked, "dc_idx={dc_idx}");
|
|
||||||
let _ = locked
|
let write_result = writeln!(file, "dc_idx={dc_idx}");
|
||||||
.unlock()
|
|
||||||
.map_err(|(_, err)| std::io::Error::from_raw_os_error(err as i32))?;
|
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) } != 0 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
write_result
|
write_result
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
|
|||||||
+101
-243
@@ -2,34 +2,32 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use std::net::SocketAddr;
|
||||||
use dashmap::mapref::entry::Entry;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::hash_map::RandomState;
|
use std::collections::hash_map::RandomState;
|
||||||
use std::hash::{BuildHasher, Hash, Hasher};
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::net::{IpAddr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv6Addr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::hash::{BuildHasher, Hash, Hasher};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use dashmap::mapref::entry::Entry;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||||
use tracing::{debug, info, trace, warn};
|
use tracing::{debug, warn, trace};
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use crate::config::{ProxyConfig, UnknownSniAction};
|
use crate::crypto::{sha256, AesCtr, SecureRandom};
|
||||||
use crate::crypto::{AesCtr, SecureRandom, sha256};
|
use rand::RngExt;
|
||||||
use crate::error::{HandshakeResult, ProxyError};
|
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
|
use crate::stream::{FakeTlsReader, FakeTlsWriter, CryptoReader, CryptoWriter};
|
||||||
|
use crate::error::{ProxyError, HandshakeResult};
|
||||||
use crate::stats::ReplayChecker;
|
use crate::stats::ReplayChecker;
|
||||||
use crate::stream::{CryptoReader, CryptoWriter, FakeTlsReader, FakeTlsWriter};
|
use crate::config::ProxyConfig;
|
||||||
use crate::tls_front::{TlsFrontCache, emulator};
|
use crate::tls_front::{TlsFrontCache, emulator};
|
||||||
use rand::RngExt;
|
|
||||||
|
|
||||||
const ACCESS_SECRET_BYTES: usize = 16;
|
const ACCESS_SECRET_BYTES: usize = 16;
|
||||||
static INVALID_SECRET_WARNED: OnceLock<Mutex<HashSet<(String, String)>>> = OnceLock::new();
|
static INVALID_SECRET_WARNED: OnceLock<Mutex<HashSet<(String, String)>>> = OnceLock::new();
|
||||||
const UNKNOWN_SNI_WARN_COOLDOWN_SECS: u64 = 5;
|
|
||||||
static UNKNOWN_SNI_WARN_NEXT_ALLOWED: OnceLock<Mutex<Option<Instant>>> = OnceLock::new();
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const WARNED_SECRET_MAX_ENTRIES: usize = 64;
|
const WARNED_SECRET_MAX_ENTRIES: usize = 64;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -69,8 +67,7 @@ struct AuthProbeSaturationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static AUTH_PROBE_STATE: OnceLock<DashMap<IpAddr, AuthProbeState>> = OnceLock::new();
|
static AUTH_PROBE_STATE: OnceLock<DashMap<IpAddr, AuthProbeState>> = OnceLock::new();
|
||||||
static AUTH_PROBE_SATURATION_STATE: OnceLock<Mutex<Option<AuthProbeSaturationState>>> =
|
static AUTH_PROBE_SATURATION_STATE: OnceLock<Mutex<Option<AuthProbeSaturationState>>> = OnceLock::new();
|
||||||
OnceLock::new();
|
|
||||||
static AUTH_PROBE_EVICTION_HASHER: OnceLock<RandomState> = OnceLock::new();
|
static AUTH_PROBE_EVICTION_HASHER: OnceLock<RandomState> = OnceLock::new();
|
||||||
|
|
||||||
fn auth_probe_state_map() -> &'static DashMap<IpAddr, AuthProbeState> {
|
fn auth_probe_state_map() -> &'static DashMap<IpAddr, AuthProbeState> {
|
||||||
@@ -81,31 +78,13 @@ fn auth_probe_saturation_state() -> &'static Mutex<Option<AuthProbeSaturationSta
|
|||||||
AUTH_PROBE_SATURATION_STATE.get_or_init(|| Mutex::new(None))
|
AUTH_PROBE_SATURATION_STATE.get_or_init(|| Mutex::new(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_probe_saturation_state_lock()
|
fn auth_probe_saturation_state_lock(
|
||||||
-> std::sync::MutexGuard<'static, Option<AuthProbeSaturationState>> {
|
) -> std::sync::MutexGuard<'static, Option<AuthProbeSaturationState>> {
|
||||||
auth_probe_saturation_state()
|
auth_probe_saturation_state()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unknown_sni_warn_state_lock() -> std::sync::MutexGuard<'static, Option<Instant>> {
|
|
||||||
UNKNOWN_SNI_WARN_NEXT_ALLOWED
|
|
||||||
.get_or_init(|| Mutex::new(None))
|
|
||||||
.lock()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_emit_unknown_sni_warn(now: Instant) -> bool {
|
|
||||||
let mut guard = unknown_sni_warn_state_lock();
|
|
||||||
if let Some(next_allowed) = *guard
|
|
||||||
&& now < next_allowed
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
*guard = Some(now + Duration::from_secs(UNKNOWN_SNI_WARN_COOLDOWN_SECS));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_auth_probe_ip(peer_ip: IpAddr) -> IpAddr {
|
fn normalize_auth_probe_ip(peer_ip: IpAddr) -> IpAddr {
|
||||||
match peer_ip {
|
match peer_ip {
|
||||||
IpAddr::V4(ip) => IpAddr::V4(ip),
|
IpAddr::V4(ip) => IpAddr::V4(ip),
|
||||||
@@ -141,19 +120,6 @@ fn auth_probe_eviction_offset(peer_ip: IpAddr, now: Instant) -> usize {
|
|||||||
hasher.finish() as usize
|
hasher.finish() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_probe_scan_start_offset(
|
|
||||||
peer_ip: IpAddr,
|
|
||||||
now: Instant,
|
|
||||||
state_len: usize,
|
|
||||||
scan_limit: usize,
|
|
||||||
) -> usize {
|
|
||||||
if state_len == 0 || scan_limit == 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auth_probe_eviction_offset(peer_ip, now) % state_len
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_probe_is_throttled(peer_ip: IpAddr, now: Instant) -> bool {
|
fn auth_probe_is_throttled(peer_ip: IpAddr, now: Instant) -> bool {
|
||||||
let peer_ip = normalize_auth_probe_ip(peer_ip);
|
let peer_ip = normalize_auth_probe_ip(peer_ip);
|
||||||
let state = auth_probe_state_map();
|
let state = auth_probe_state_map();
|
||||||
@@ -286,7 +252,9 @@ fn auth_probe_record_failure_with_state(
|
|||||||
match eviction_candidate {
|
match eviction_candidate {
|
||||||
Some((_, current_fail, current_seen))
|
Some((_, current_fail, current_seen))
|
||||||
if fail_streak > current_fail
|
if fail_streak > current_fail
|
||||||
|| (fail_streak == current_fail && last_seen >= current_seen) => {}
|
|| (fail_streak == current_fail && last_seen >= current_seen) =>
|
||||||
|
{
|
||||||
|
}
|
||||||
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,25 +270,12 @@ fn auth_probe_record_failure_with_state(
|
|||||||
let mut eviction_candidate: Option<(IpAddr, u32, Instant)> = None;
|
let mut eviction_candidate: Option<(IpAddr, u32, Instant)> = None;
|
||||||
let state_len = state.len();
|
let state_len = state.len();
|
||||||
let scan_limit = state_len.min(AUTH_PROBE_PRUNE_SCAN_LIMIT);
|
let scan_limit = state_len.min(AUTH_PROBE_PRUNE_SCAN_LIMIT);
|
||||||
|
let start_offset = if state_len == 0 {
|
||||||
if state_len <= AUTH_PROBE_PRUNE_SCAN_LIMIT {
|
0
|
||||||
for entry in state.iter() {
|
|
||||||
let key = *entry.key();
|
|
||||||
let fail_streak = entry.value().fail_streak;
|
|
||||||
let last_seen = entry.value().last_seen;
|
|
||||||
match eviction_candidate {
|
|
||||||
Some((_, current_fail, current_seen))
|
|
||||||
if fail_streak > current_fail
|
|
||||||
|| (fail_streak == current_fail && last_seen >= current_seen) => {}
|
|
||||||
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
|
||||||
}
|
|
||||||
if auth_probe_state_expired(entry.value(), now) {
|
|
||||||
stale_keys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let start_offset =
|
auth_probe_eviction_offset(peer_ip, now) % state_len
|
||||||
auth_probe_scan_start_offset(peer_ip, now, state_len, scan_limit);
|
};
|
||||||
|
|
||||||
let mut scanned = 0usize;
|
let mut scanned = 0usize;
|
||||||
for entry in state.iter().skip(start_offset) {
|
for entry in state.iter().skip(start_offset) {
|
||||||
let key = *entry.key();
|
let key = *entry.key();
|
||||||
@@ -329,7 +284,9 @@ fn auth_probe_record_failure_with_state(
|
|||||||
match eviction_candidate {
|
match eviction_candidate {
|
||||||
Some((_, current_fail, current_seen))
|
Some((_, current_fail, current_seen))
|
||||||
if fail_streak > current_fail
|
if fail_streak > current_fail
|
||||||
|| (fail_streak == current_fail && last_seen >= current_seen) => {}
|
|| (fail_streak == current_fail && last_seen >= current_seen) =>
|
||||||
|
{
|
||||||
|
}
|
||||||
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
||||||
}
|
}
|
||||||
if auth_probe_state_expired(entry.value(), now) {
|
if auth_probe_state_expired(entry.value(), now) {
|
||||||
@@ -349,8 +306,9 @@ fn auth_probe_record_failure_with_state(
|
|||||||
match eviction_candidate {
|
match eviction_candidate {
|
||||||
Some((_, current_fail, current_seen))
|
Some((_, current_fail, current_seen))
|
||||||
if fail_streak > current_fail
|
if fail_streak > current_fail
|
||||||
|| (fail_streak == current_fail
|
|| (fail_streak == current_fail && last_seen >= current_seen) =>
|
||||||
&& last_seen >= current_seen) => {}
|
{
|
||||||
|
}
|
||||||
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
_ => eviction_candidate = Some((key, fail_streak, last_seen)),
|
||||||
}
|
}
|
||||||
if auth_probe_state_expired(entry.value(), now) {
|
if auth_probe_state_expired(entry.value(), now) {
|
||||||
@@ -358,7 +316,6 @@ fn auth_probe_record_failure_with_state(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for stale_key in stale_keys {
|
for stale_key in stale_keys {
|
||||||
state.remove(&stale_key);
|
state.remove(&stale_key);
|
||||||
@@ -432,25 +389,6 @@ fn auth_probe_test_lock() -> &'static Mutex<()> {
|
|||||||
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn unknown_sni_warn_test_lock() -> &'static Mutex<()> {
|
|
||||||
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
||||||
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn clear_unknown_sni_warn_state_for_testing() {
|
|
||||||
if UNKNOWN_SNI_WARN_NEXT_ALLOWED.get().is_some() {
|
|
||||||
let mut guard = unknown_sni_warn_state_lock();
|
|
||||||
*guard = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn should_emit_unknown_sni_warn_for_testing(now: Instant) -> bool {
|
|
||||||
should_emit_unknown_sni_warn(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn clear_warned_secrets_for_testing() {
|
fn clear_warned_secrets_for_testing() {
|
||||||
if let Some(warned) = INVALID_SECRET_WARNED.get()
|
if let Some(warned) = INVALID_SECRET_WARNED.get()
|
||||||
@@ -568,21 +506,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;
|
||||||
@@ -622,6 +545,7 @@ pub struct HandshakeSuccess {
|
|||||||
/// Client address
|
/// Client address
|
||||||
pub peer: SocketAddr,
|
pub peer: SocketAddr,
|
||||||
/// Whether TLS was used
|
/// Whether TLS was used
|
||||||
|
|
||||||
pub is_tls: bool,
|
pub is_tls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,12 +590,70 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
||||||
let preferred_user_hint = client_sni
|
let secrets = decode_user_secrets(config, client_sni.as_deref());
|
||||||
.as_deref()
|
|
||||||
.filter(|sni| config.access.users.contains_key(*sni));
|
let validation = match tls::validate_tls_handshake_with_replay_window(
|
||||||
let matched_tls_domain = client_sni
|
handshake,
|
||||||
.as_deref()
|
&secrets,
|
||||||
.and_then(|sni| find_matching_tls_domain(config, sni));
|
config.access.ignore_time_skew,
|
||||||
|
config.access.replay_window_secs,
|
||||||
|
) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
auth_probe_record_failure(peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
debug!(
|
||||||
|
peer = %peer,
|
||||||
|
ignore_time_skew = config.access.ignore_time_skew,
|
||||||
|
"TLS handshake validation failed - no matching user or time skew"
|
||||||
|
);
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replay tracking is applied only after successful authentication to avoid
|
||||||
|
// letting unauthenticated probes evict valid entries from the replay cache.
|
||||||
|
let digest_half = &validation.digest[..tls::TLS_DIGEST_HALF_LEN];
|
||||||
|
if replay_checker.check_and_add_tls_digest(digest_half) {
|
||||||
|
auth_probe_record_failure(peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
warn!(peer = %peer, "TLS replay attack detected (duplicate digest)");
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
|
||||||
|
let secret = match secrets.iter().find(|(name, _)| *name == validation.user) {
|
||||||
|
Some((_, s)) => s,
|
||||||
|
None => {
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
return HandshakeResult::BadClient { reader, writer };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cached = if config.censorship.tls_emulation {
|
||||||
|
if let Some(cache) = tls_cache.as_ref() {
|
||||||
|
let selected_domain = if let Some(sni) = client_sni.as_ref() {
|
||||||
|
if cache.contains_domain(&sni).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
|
||||||
|
.take_full_cert_budget_for_ip(
|
||||||
|
peer.ip(),
|
||||||
|
Duration::from_secs(config.censorship.tls_full_cert_ttl_secs),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Some((cached_entry, use_full_cert_payload))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let alpn_list = if config.censorship.alpn_enforce {
|
let alpn_list = if config.censorship.alpn_enforce {
|
||||||
tls::extract_alpn_from_client_hello(handshake)
|
tls::extract_alpn_from_client_hello(handshake)
|
||||||
@@ -694,94 +676,6 @@ where
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if client_sni.is_some() && matched_tls_domain.is_none() && preferred_user_hint.is_none() {
|
|
||||||
auth_probe_record_failure(peer.ip(), Instant::now());
|
|
||||||
maybe_apply_server_hello_delay(config).await;
|
|
||||||
let sni = client_sni.as_deref().unwrap_or_default();
|
|
||||||
let log_now = Instant::now();
|
|
||||||
if should_emit_unknown_sni_warn(log_now) {
|
|
||||||
warn!(
|
|
||||||
peer = %peer,
|
|
||||||
sni = %sni,
|
|
||||||
unknown_sni = true,
|
|
||||||
unknown_sni_action = ?config.censorship.unknown_sni_action,
|
|
||||||
"TLS handshake rejected by unknown SNI policy"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
peer = %peer,
|
|
||||||
sni = %sni,
|
|
||||||
unknown_sni = true,
|
|
||||||
unknown_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, preferred_user_hint);
|
|
||||||
|
|
||||||
let validation = match tls::validate_tls_handshake_with_replay_window(
|
|
||||||
handshake,
|
|
||||||
&secrets,
|
|
||||||
config.access.ignore_time_skew,
|
|
||||||
config.access.replay_window_secs,
|
|
||||||
) {
|
|
||||||
Some(v) => v,
|
|
||||||
None => {
|
|
||||||
auth_probe_record_failure(peer.ip(), Instant::now());
|
|
||||||
maybe_apply_server_hello_delay(config).await;
|
|
||||||
debug!(
|
|
||||||
peer = %peer,
|
|
||||||
ignore_time_skew = config.access.ignore_time_skew,
|
|
||||||
"TLS handshake validation failed - no matching user or time skew"
|
|
||||||
);
|
|
||||||
return HandshakeResult::BadClient { reader, writer };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reject known replay digests before expensive cache/domain/ALPN policy work.
|
|
||||||
let digest_half = &validation.digest[..tls::TLS_DIGEST_HALF_LEN];
|
|
||||||
if replay_checker.check_tls_digest(digest_half) {
|
|
||||||
auth_probe_record_failure(peer.ip(), Instant::now());
|
|
||||||
maybe_apply_server_hello_delay(config).await;
|
|
||||||
warn!(peer = %peer, "TLS replay attack detected (duplicate digest)");
|
|
||||||
return HandshakeResult::BadClient { reader, writer };
|
|
||||||
}
|
|
||||||
|
|
||||||
let secret = match secrets.iter().find(|(name, _)| *name == validation.user) {
|
|
||||||
Some((_, s)) => s,
|
|
||||||
None => {
|
|
||||||
maybe_apply_server_hello_delay(config).await;
|
|
||||||
return HandshakeResult::BadClient { reader, writer };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cached = if config.censorship.tls_emulation {
|
|
||||||
if let Some(cache) = tls_cache.as_ref() {
|
|
||||||
let selected_domain =
|
|
||||||
matched_tls_domain.unwrap_or(config.censorship.tls_domain.as_str());
|
|
||||||
let cached_entry = cache.get(selected_domain).await;
|
|
||||||
let use_full_cert_payload = cache
|
|
||||||
.take_full_cert_budget_for_ip(
|
|
||||||
peer.ip(),
|
|
||||||
Duration::from_secs(config.censorship.tls_full_cert_ttl_secs),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
Some((cached_entry, use_full_cert_payload))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add replay digest only for policy-valid handshakes.
|
|
||||||
replay_checker.add_tls_digest(digest_half);
|
|
||||||
|
|
||||||
let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
|
let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
|
||||||
emulator::build_emulated_server_hello(
|
emulator::build_emulated_server_hello(
|
||||||
secret,
|
secret,
|
||||||
@@ -875,13 +769,14 @@ where
|
|||||||
let decoded_users = decode_user_secrets(config, preferred_user);
|
let decoded_users = decode_user_secrets(config, preferred_user);
|
||||||
|
|
||||||
for (user, secret) in decoded_users {
|
for (user, secret) in decoded_users {
|
||||||
|
|
||||||
let dec_prekey = &dec_prekey_iv[..PREKEY_LEN];
|
let dec_prekey = &dec_prekey_iv[..PREKEY_LEN];
|
||||||
let dec_iv_bytes = &dec_prekey_iv[PREKEY_LEN..];
|
let dec_iv_bytes = &dec_prekey_iv[PREKEY_LEN..];
|
||||||
|
|
||||||
let mut dec_key_input = Zeroizing::new(Vec::with_capacity(PREKEY_LEN + secret.len()));
|
let mut dec_key_input = Zeroizing::new(Vec::with_capacity(PREKEY_LEN + secret.len()));
|
||||||
dec_key_input.extend_from_slice(dec_prekey);
|
dec_key_input.extend_from_slice(dec_prekey);
|
||||||
dec_key_input.extend_from_slice(&secret);
|
dec_key_input.extend_from_slice(&secret);
|
||||||
let dec_key = Zeroizing::new(sha256(&dec_key_input));
|
let dec_key = sha256(&dec_key_input);
|
||||||
|
|
||||||
let mut dec_iv_arr = [0u8; IV_LEN];
|
let mut dec_iv_arr = [0u8; IV_LEN];
|
||||||
dec_iv_arr.copy_from_slice(dec_iv_bytes);
|
dec_iv_arr.copy_from_slice(dec_iv_bytes);
|
||||||
@@ -917,7 +812,7 @@ where
|
|||||||
let mut enc_key_input = Zeroizing::new(Vec::with_capacity(PREKEY_LEN + secret.len()));
|
let mut enc_key_input = Zeroizing::new(Vec::with_capacity(PREKEY_LEN + secret.len()));
|
||||||
enc_key_input.extend_from_slice(enc_prekey);
|
enc_key_input.extend_from_slice(enc_prekey);
|
||||||
enc_key_input.extend_from_slice(&secret);
|
enc_key_input.extend_from_slice(&secret);
|
||||||
let enc_key = Zeroizing::new(sha256(&enc_key_input));
|
let enc_key = sha256(&enc_key_input);
|
||||||
|
|
||||||
let mut enc_iv_arr = [0u8; IV_LEN];
|
let mut enc_iv_arr = [0u8; IV_LEN];
|
||||||
enc_iv_arr.copy_from_slice(enc_iv_bytes);
|
enc_iv_arr.copy_from_slice(enc_iv_bytes);
|
||||||
@@ -942,9 +837,9 @@ where
|
|||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
dc_idx,
|
dc_idx,
|
||||||
proto_tag,
|
proto_tag,
|
||||||
dec_key: *dec_key,
|
dec_key,
|
||||||
dec_iv,
|
dec_iv,
|
||||||
enc_key: *enc_key,
|
enc_key,
|
||||||
enc_iv,
|
enc_iv,
|
||||||
peer,
|
peer,
|
||||||
is_tls,
|
is_tls,
|
||||||
@@ -990,19 +885,13 @@ pub fn generate_tg_nonce(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]) {
|
if RESERVED_NONCE_FIRST_BYTES.contains(&nonce[0]) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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]];
|
||||||
if RESERVED_NONCE_BEGINNINGS.contains(&first_four) {
|
if RESERVED_NONCE_BEGINNINGS.contains(&first_four) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let continue_four: [u8; 4] = [nonce[4], nonce[5], nonce[6], nonce[7]];
|
let continue_four: [u8; 4] = [nonce[4], nonce[5], nonce[6], nonce[7]];
|
||||||
if RESERVED_NONCE_CONTINUES.contains(&continue_four) {
|
if RESERVED_NONCE_CONTINUES.contains(&continue_four) { continue; }
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&proto_tag.to_bytes());
|
nonce[PROTO_TAG_POS..PROTO_TAG_POS + 4].copy_from_slice(&proto_tag.to_bytes());
|
||||||
// CRITICAL: write dc_idx so upstream DC knows where to route
|
// CRITICAL: write dc_idx so upstream DC knows where to route
|
||||||
@@ -1066,6 +955,7 @@ pub fn encrypt_tg_nonce_with_ciphers(nonce: &[u8; HANDSHAKE_LEN]) -> (Vec<u8>, A
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt nonce for sending to Telegram (legacy function for compatibility)
|
/// Encrypt nonce for sending to Telegram (legacy function for compatibility)
|
||||||
|
|
||||||
pub fn encrypt_tg_nonce(nonce: &[u8; HANDSHAKE_LEN]) -> Vec<u8> {
|
pub fn encrypt_tg_nonce(nonce: &[u8; HANDSHAKE_LEN]) -> Vec<u8> {
|
||||||
let (encrypted, _, _) = encrypt_tg_nonce_with_ciphers(nonce);
|
let (encrypted, _, _) = encrypt_tg_nonce_with_ciphers(nonce);
|
||||||
encrypted
|
encrypted
|
||||||
@@ -1091,38 +981,6 @@ mod saturation_poison_security_tests;
|
|||||||
#[path = "tests/handshake_auth_probe_hardening_adversarial_tests.rs"]
|
#[path = "tests/handshake_auth_probe_hardening_adversarial_tests.rs"]
|
||||||
mod auth_probe_hardening_adversarial_tests;
|
mod auth_probe_hardening_adversarial_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_auth_probe_scan_budget_security_tests.rs"]
|
|
||||||
mod auth_probe_scan_budget_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_auth_probe_scan_offset_stress_tests.rs"]
|
|
||||||
mod auth_probe_scan_offset_stress_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_auth_probe_eviction_bias_security_tests.rs"]
|
|
||||||
mod auth_probe_eviction_bias_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_advanced_clever_tests.rs"]
|
|
||||||
mod advanced_clever_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_more_clever_tests.rs"]
|
|
||||||
mod more_clever_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_real_bug_stress_tests.rs"]
|
|
||||||
mod real_bug_stress_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_timing_manual_bench_tests.rs"]
|
|
||||||
mod timing_manual_bench_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/handshake_key_material_zeroization_security_tests.rs"]
|
|
||||||
mod handshake_key_material_zeroization_security_tests;
|
|
||||||
|
|
||||||
/// Compile-time guard: HandshakeSuccess holds cryptographic key material and
|
/// Compile-time guard: HandshakeSuccess holds cryptographic key material and
|
||||||
/// must never be Copy. A Copy impl would allow silent key duplication,
|
/// must never be Copy. A Copy impl would allow silent key duplication,
|
||||||
/// undermining the zeroize-on-drop guarantee.
|
/// undermining the zeroize-on-drop guarantee.
|
||||||
|
|||||||
+68
-544
@@ -1,28 +1,19 @@
|
|||||||
//! Masking - forward unrecognized traffic to mask host
|
//! Masking - forward unrecognized traffic to mask host
|
||||||
|
|
||||||
|
use std::str;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use rand::{Rng, RngExt};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::time::{Instant, timeout};
|
||||||
|
use tracing::debug;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::network::dns_overrides::resolve_socket_addr;
|
use crate::network::dns_overrides::resolve_socket_addr;
|
||||||
use crate::stats::beobachten::BeobachtenStore;
|
use crate::stats::beobachten::BeobachtenStore;
|
||||||
use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder};
|
use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder};
|
||||||
#[cfg(unix)]
|
|
||||||
use nix::ifaddrs::getifaddrs;
|
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::{Rng, RngExt, SeedableRng};
|
|
||||||
use std::net::{IpAddr, SocketAddr};
|
|
||||||
use std::str;
|
|
||||||
#[cfg(test)]
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::sync::{Mutex, OnceLock};
|
|
||||||
use std::time::{Duration, Instant as StdInstant};
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use tokio::net::UnixStream;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
|
||||||
use tokio::time::{Instant, timeout};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
|
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
@@ -39,55 +30,28 @@ const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
|
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
const MASK_BUFFER_SIZE: usize = 8192;
|
const MASK_BUFFER_SIZE: usize = 8192;
|
||||||
#[cfg(unix)]
|
|
||||||
#[cfg(not(test))]
|
|
||||||
const LOCAL_INTERFACE_CACHE_TTL: Duration = Duration::from_secs(300);
|
|
||||||
#[cfg(all(unix, test))]
|
|
||||||
const LOCAL_INTERFACE_CACHE_TTL: Duration = Duration::from_secs(1);
|
|
||||||
|
|
||||||
struct CopyOutcome {
|
struct CopyOutcome {
|
||||||
total: usize,
|
total: usize,
|
||||||
ended_by_eof: bool,
|
ended_by_eof: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn copy_with_idle_timeout<R, W>(
|
async fn copy_with_idle_timeout<R, W>(reader: &mut R, writer: &mut W) -> CopyOutcome
|
||||||
reader: &mut R,
|
|
||||||
writer: &mut W,
|
|
||||||
byte_cap: usize,
|
|
||||||
shutdown_on_eof: bool,
|
|
||||||
) -> CopyOutcome
|
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
let mut buf = Box::new([0u8; MASK_BUFFER_SIZE]);
|
let mut buf = [0u8; MASK_BUFFER_SIZE];
|
||||||
let mut total = 0usize;
|
let mut total = 0usize;
|
||||||
let mut ended_by_eof = false;
|
let mut ended_by_eof = false;
|
||||||
|
|
||||||
if byte_cap == 0 {
|
|
||||||
return CopyOutcome {
|
|
||||||
total,
|
|
||||||
ended_by_eof,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let remaining_budget = byte_cap.saturating_sub(total);
|
let read_res = timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf)).await;
|
||||||
if remaining_budget == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
|
||||||
let read_res = timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await;
|
|
||||||
let n = match read_res {
|
let n = match read_res {
|
||||||
Ok(Ok(n)) => n,
|
Ok(Ok(n)) => n,
|
||||||
Ok(Err(_)) | Err(_) => break,
|
Ok(Err(_)) | Err(_) => break,
|
||||||
};
|
};
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
ended_by_eof = true;
|
ended_by_eof = true;
|
||||||
if shutdown_on_eof {
|
|
||||||
let _ = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.shutdown()).await;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
total = total.saturating_add(n);
|
total = total.saturating_add(n);
|
||||||
@@ -104,31 +68,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_http_probe(data: &[u8]) -> bool {
|
|
||||||
// RFC 7540 section 3.5: HTTP/2 client preface starts with "PRI ".
|
|
||||||
const HTTP_METHODS: [&[u8]; 10] = [
|
|
||||||
b"GET ", b"POST", b"HEAD", b"PUT ", b"DELETE", b"OPTIONS", b"CONNECT", b"TRACE", b"PATCH",
|
|
||||||
b"PRI ",
|
|
||||||
];
|
|
||||||
|
|
||||||
if data.is_empty() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let window = &data[..data.len().min(16)];
|
|
||||||
for method in HTTP_METHODS {
|
|
||||||
if data.len() >= method.len() && window.starts_with(method) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (2..=3).contains(&window.len()) && method.starts_with(window) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_mask_shape_bucket(total: usize, floor: usize, cap: usize) -> usize {
|
fn next_mask_shape_bucket(total: usize, floor: usize, cap: usize) -> usize {
|
||||||
if total == 0 || floor == 0 || cap < floor {
|
if total == 0 || floor == 0 || cap < floor {
|
||||||
return total;
|
return total;
|
||||||
@@ -159,8 +98,8 @@ async fn maybe_write_shape_padding<W>(
|
|||||||
cap: usize,
|
cap: usize,
|
||||||
above_cap_blur: bool,
|
above_cap_blur: bool,
|
||||||
above_cap_blur_max_bytes: usize,
|
above_cap_blur_max_bytes: usize,
|
||||||
aggressive_mode: bool,
|
)
|
||||||
) where
|
where
|
||||||
W: AsyncWrite + Unpin,
|
W: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
if !enabled {
|
if !enabled {
|
||||||
@@ -169,11 +108,7 @@ async fn maybe_write_shape_padding<W>(
|
|||||||
|
|
||||||
let target_total = if total_sent >= cap && above_cap_blur && above_cap_blur_max_bytes > 0 {
|
let target_total = if total_sent >= cap && above_cap_blur && above_cap_blur_max_bytes > 0 {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let extra = if aggressive_mode {
|
let extra = rng.random_range(0..=above_cap_blur_max_bytes);
|
||||||
rng.random_range(1..=above_cap_blur_max_bytes)
|
|
||||||
} else {
|
|
||||||
rng.random_range(0..=above_cap_blur_max_bytes)
|
|
||||||
};
|
|
||||||
total_sent.saturating_add(extra)
|
total_sent.saturating_add(extra)
|
||||||
} else {
|
} else {
|
||||||
next_mask_shape_bucket(total_sent, floor, cap)
|
next_mask_shape_bucket(total_sent, floor, cap)
|
||||||
@@ -186,11 +121,6 @@ async fn maybe_write_shape_padding<W>(
|
|||||||
let mut remaining = target_total - total_sent;
|
let mut remaining = target_total - total_sent;
|
||||||
let mut pad_chunk = [0u8; 1024];
|
let mut pad_chunk = [0u8; 1024];
|
||||||
let deadline = Instant::now() + MASK_TIMEOUT;
|
let deadline = Instant::now() + MASK_TIMEOUT;
|
||||||
// Use a Send RNG so relay futures remain spawn-safe under Tokio.
|
|
||||||
let mut rng = {
|
|
||||||
let mut seed_source = rand::rng();
|
|
||||||
StdRng::from_rng(&mut seed_source)
|
|
||||||
};
|
|
||||||
|
|
||||||
while remaining > 0 {
|
while remaining > 0 {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
@@ -199,7 +129,10 @@ async fn maybe_write_shape_padding<W>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let write_len = remaining.min(pad_chunk.len());
|
let write_len = remaining.min(pad_chunk.len());
|
||||||
|
{
|
||||||
|
let mut rng = rand::rng();
|
||||||
rng.fill_bytes(&mut pad_chunk[..write_len]);
|
rng.fill_bytes(&mut pad_chunk[..write_len]);
|
||||||
|
}
|
||||||
let write_budget = deadline.saturating_duration_since(now);
|
let write_budget = deadline.saturating_duration_since(now);
|
||||||
match timeout(write_budget, mask_write.write_all(&pad_chunk[..write_len])).await {
|
match timeout(write_budget, mask_write.write_all(&pad_chunk[..write_len])).await {
|
||||||
Ok(Ok(())) => {}
|
Ok(Ok(())) => {}
|
||||||
@@ -230,14 +163,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn consume_client_data_with_timeout_and_cap<R>(reader: R, byte_cap: usize)
|
async fn consume_client_data_with_timeout<R>(reader: R)
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, byte_cap))
|
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader)).await.is_err() {
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
debug!("Timed out while consuming client data on masking fallback path");
|
debug!("Timed out while consuming client data on masking fallback path");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,13 +183,6 @@ fn mask_outcome_target_budget(config: &ProxyConfig) -> Duration {
|
|||||||
if config.censorship.mask_timing_normalization_enabled {
|
if config.censorship.mask_timing_normalization_enabled {
|
||||||
let floor = config.censorship.mask_timing_normalization_floor_ms;
|
let floor = config.censorship.mask_timing_normalization_floor_ms;
|
||||||
let ceiling = config.censorship.mask_timing_normalization_ceiling_ms;
|
let ceiling = config.censorship.mask_timing_normalization_ceiling_ms;
|
||||||
if floor == 0 {
|
|
||||||
if ceiling == 0 {
|
|
||||||
return Duration::from_millis(0);
|
|
||||||
}
|
|
||||||
let mut rng = rand::rng();
|
|
||||||
return Duration::from_millis(rng.random_range(0..=ceiling));
|
|
||||||
}
|
|
||||||
if ceiling > floor {
|
if ceiling > floor {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
return Duration::from_millis(rng.random_range(floor..=ceiling));
|
return Duration::from_millis(rng.random_range(floor..=ceiling));
|
||||||
@@ -289,7 +212,11 @@ async fn wait_mask_outcome_budget(started: Instant, config: &ProxyConfig) {
|
|||||||
/// Detect client type based on initial data
|
/// Detect client type based on initial data
|
||||||
fn detect_client_type(data: &[u8]) -> &'static str {
|
fn detect_client_type(data: &[u8]) -> &'static str {
|
||||||
// Check for HTTP request
|
// Check for HTTP request
|
||||||
if is_http_probe(data) {
|
if data.len() > 4
|
||||||
|
&& (data.starts_with(b"GET ") || data.starts_with(b"POST") ||
|
||||||
|
data.starts_with(b"HEAD") || data.starts_with(b"PUT ") ||
|
||||||
|
data.starts_with(b"DELETE") || data.starts_with(b"OPTIONS"))
|
||||||
|
{
|
||||||
return "HTTP";
|
return "HTTP";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,247 +238,6 @@ fn detect_client_type(data: &[u8]) -> &'static str {
|
|||||||
"unknown"
|
"unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_mask_host_ip_literal(host: &str) -> Option<IpAddr> {
|
|
||||||
if host.starts_with('[') && host.ends_with(']') {
|
|
||||||
return host[1..host.len() - 1].parse::<IpAddr>().ok();
|
|
||||||
}
|
|
||||||
host.parse::<IpAddr>().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn canonical_ip(ip: IpAddr) -> IpAddr {
|
|
||||||
match ip {
|
|
||||||
IpAddr::V6(v6) => v6
|
|
||||||
.to_ipv4_mapped()
|
|
||||||
.map(IpAddr::V4)
|
|
||||||
.unwrap_or(IpAddr::V6(v6)),
|
|
||||||
IpAddr::V4(v4) => IpAddr::V4(v4),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn collect_local_interface_ips() -> Vec<IpAddr> {
|
|
||||||
#[cfg(test)]
|
|
||||||
LOCAL_INTERFACE_ENUMERATIONS.fetch_add(1, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let mut out = Vec::new();
|
|
||||||
if let Ok(addrs) = getifaddrs() {
|
|
||||||
for iface in addrs {
|
|
||||||
if let Some(address) = iface.address {
|
|
||||||
if let Some(v4) = address.as_sockaddr_in() {
|
|
||||||
out.push(canonical_ip(IpAddr::V4(v4.ip())));
|
|
||||||
} else if let Some(v6) = address.as_sockaddr_in6() {
|
|
||||||
out.push(canonical_ip(IpAddr::V6(v6.ip())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn choose_interface_snapshot(previous: &[IpAddr], refreshed: Vec<IpAddr>) -> Vec<IpAddr> {
|
|
||||||
if refreshed.is_empty() && !previous.is_empty() {
|
|
||||||
return previous.to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshed
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[derive(Default)]
|
|
||||||
struct LocalInterfaceCache {
|
|
||||||
ips: Vec<IpAddr>,
|
|
||||||
refreshed_at: Option<StdInstant>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
static LOCAL_INTERFACE_CACHE: OnceLock<Mutex<LocalInterfaceCache>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
static LOCAL_INTERFACE_REFRESH_LOCK: OnceLock<AsyncMutex<()>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[cfg(all(unix, test))]
|
|
||||||
fn local_interface_ips() -> Vec<IpAddr> {
|
|
||||||
let cache = LOCAL_INTERFACE_CACHE.get_or_init(|| Mutex::new(LocalInterfaceCache::default()));
|
|
||||||
let mut guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
|
|
||||||
|
|
||||||
let stale = guard
|
|
||||||
.refreshed_at
|
|
||||||
.is_none_or(|at| at.elapsed() >= LOCAL_INTERFACE_CACHE_TTL);
|
|
||||||
if stale {
|
|
||||||
let refreshed = collect_local_interface_ips();
|
|
||||||
guard.ips = choose_interface_snapshot(&guard.ips, refreshed);
|
|
||||||
guard.refreshed_at = Some(StdInstant::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
guard.ips.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn local_interface_ips_async() -> Vec<IpAddr> {
|
|
||||||
let cache = LOCAL_INTERFACE_CACHE.get_or_init(|| Mutex::new(LocalInterfaceCache::default()));
|
|
||||||
|
|
||||||
{
|
|
||||||
let guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
|
|
||||||
let stale = guard
|
|
||||||
.refreshed_at
|
|
||||||
.is_none_or(|at| at.elapsed() >= LOCAL_INTERFACE_CACHE_TTL);
|
|
||||||
if !stale {
|
|
||||||
return guard.ips.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let refresh_lock = LOCAL_INTERFACE_REFRESH_LOCK.get_or_init(|| AsyncMutex::new(()));
|
|
||||||
let _refresh_guard = refresh_lock.lock().await;
|
|
||||||
|
|
||||||
{
|
|
||||||
let guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
|
|
||||||
let stale = guard
|
|
||||||
.refreshed_at
|
|
||||||
.is_none_or(|at| at.elapsed() >= LOCAL_INTERFACE_CACHE_TTL);
|
|
||||||
if !stale {
|
|
||||||
return guard.ips.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let refreshed = tokio::task::spawn_blocking(collect_local_interface_ips)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
|
|
||||||
let stale = guard
|
|
||||||
.refreshed_at
|
|
||||||
.is_none_or(|at| at.elapsed() >= LOCAL_INTERFACE_CACHE_TTL);
|
|
||||||
if stale {
|
|
||||||
guard.ips = choose_interface_snapshot(&guard.ips, refreshed);
|
|
||||||
guard.refreshed_at = Some(StdInstant::now());
|
|
||||||
}
|
|
||||||
|
|
||||||
guard.ips.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(not(unix), test))]
|
|
||||||
fn local_interface_ips() -> Vec<IpAddr> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
async fn local_interface_ips_async() -> Vec<IpAddr> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
static LOCAL_INTERFACE_ENUMERATIONS: AtomicUsize = AtomicUsize::new(0);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn reset_local_interface_enumerations_for_tests() {
|
|
||||||
LOCAL_INTERFACE_ENUMERATIONS.store(0, Ordering::Relaxed);
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
if let Some(cache) = LOCAL_INTERFACE_CACHE.get() {
|
|
||||||
let mut guard = cache.lock().unwrap_or_else(|poison| poison.into_inner());
|
|
||||||
guard.ips.clear();
|
|
||||||
guard.refreshed_at = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn local_interface_enumerations_for_tests() -> usize {
|
|
||||||
LOCAL_INTERFACE_ENUMERATIONS.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_mask_target_local_listener_with_interfaces(
|
|
||||||
mask_host: &str,
|
|
||||||
mask_port: u16,
|
|
||||||
local_addr: SocketAddr,
|
|
||||||
resolved_override: Option<SocketAddr>,
|
|
||||||
interface_ips: &[IpAddr],
|
|
||||||
) -> bool {
|
|
||||||
if mask_port != local_addr.port() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let local_ip = canonical_ip(local_addr.ip());
|
|
||||||
let literal_mask_ip = parse_mask_host_ip_literal(mask_host).map(canonical_ip);
|
|
||||||
|
|
||||||
if let Some(addr) = resolved_override {
|
|
||||||
let resolved_ip = canonical_ip(addr.ip());
|
|
||||||
if resolved_ip == local_ip {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if local_ip.is_unspecified()
|
|
||||||
&& (resolved_ip.is_loopback()
|
|
||||||
|| resolved_ip.is_unspecified()
|
|
||||||
|| interface_ips.contains(&resolved_ip))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mask_ip) = literal_mask_ip {
|
|
||||||
if mask_ip == local_ip {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if local_ip.is_unspecified()
|
|
||||||
&& (mask_ip.is_loopback()
|
|
||||||
|| mask_ip.is_unspecified()
|
|
||||||
|| interface_ips.contains(&mask_ip))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn is_mask_target_local_listener(
|
|
||||||
mask_host: &str,
|
|
||||||
mask_port: u16,
|
|
||||||
local_addr: SocketAddr,
|
|
||||||
resolved_override: Option<SocketAddr>,
|
|
||||||
) -> bool {
|
|
||||||
if mask_port != local_addr.port() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let interfaces = local_interface_ips();
|
|
||||||
is_mask_target_local_listener_with_interfaces(
|
|
||||||
mask_host,
|
|
||||||
mask_port,
|
|
||||||
local_addr,
|
|
||||||
resolved_override,
|
|
||||||
&interfaces,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn is_mask_target_local_listener_async(
|
|
||||||
mask_host: &str,
|
|
||||||
mask_port: u16,
|
|
||||||
local_addr: SocketAddr,
|
|
||||||
resolved_override: Option<SocketAddr>,
|
|
||||||
) -> bool {
|
|
||||||
if mask_port != local_addr.port() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let interfaces = local_interface_ips_async().await;
|
|
||||||
is_mask_target_local_listener_with_interfaces(
|
|
||||||
mask_host,
|
|
||||||
mask_port,
|
|
||||||
local_addr,
|
|
||||||
resolved_override,
|
|
||||||
&interfaces,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn masking_beobachten_ttl(config: &ProxyConfig) -> Duration {
|
|
||||||
let minutes = config.general.beobachten_minutes;
|
|
||||||
let clamped = minutes.clamp(1, 24 * 60);
|
|
||||||
Duration::from_secs(clamped.saturating_mul(60))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_mask_proxy_header(
|
fn build_mask_proxy_header(
|
||||||
version: u8,
|
version: u8,
|
||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
@@ -566,12 +252,16 @@ fn build_mask_proxy_header(
|
|||||||
),
|
),
|
||||||
_ => {
|
_ => {
|
||||||
let header = match (peer, local_addr) {
|
let header = match (peer, local_addr) {
|
||||||
(SocketAddr::V4(src), SocketAddr::V4(dst)) => ProxyProtocolV1Builder::new()
|
(SocketAddr::V4(src), SocketAddr::V4(dst)) => {
|
||||||
|
ProxyProtocolV1Builder::new()
|
||||||
.tcp4(src.into(), dst.into())
|
.tcp4(src.into(), dst.into())
|
||||||
.build(),
|
.build()
|
||||||
(SocketAddr::V6(src), SocketAddr::V6(dst)) => ProxyProtocolV1Builder::new()
|
}
|
||||||
|
(SocketAddr::V6(src), SocketAddr::V6(dst)) => {
|
||||||
|
ProxyProtocolV1Builder::new()
|
||||||
.tcp6(src.into(), dst.into())
|
.tcp6(src.into(), dst.into())
|
||||||
.build(),
|
.build()
|
||||||
|
}
|
||||||
_ => ProxyProtocolV1Builder::new().build(),
|
_ => ProxyProtocolV1Builder::new().build(),
|
||||||
};
|
};
|
||||||
Some(header)
|
Some(header)
|
||||||
@@ -588,20 +278,20 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
config: &ProxyConfig,
|
config: &ProxyConfig,
|
||||||
beobachten: &BeobachtenStore,
|
beobachten: &BeobachtenStore,
|
||||||
) where
|
)
|
||||||
|
where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
W: AsyncWrite + Unpin + Send + 'static,
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
let client_type = detect_client_type(initial_data);
|
let client_type = detect_client_type(initial_data);
|
||||||
if config.general.beobachten {
|
if config.general.beobachten {
|
||||||
let ttl = masking_beobachten_ttl(config);
|
let ttl = Duration::from_secs(config.general.beobachten_minutes.saturating_mul(60));
|
||||||
beobachten.record(client_type, peer.ip(), ttl);
|
beobachten.record(client_type, peer.ip(), ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.censorship.mask {
|
if !config.censorship.mask {
|
||||||
// Masking disabled, just consume data
|
// Masking disabled, just consume data
|
||||||
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
consume_client_data_with_timeout(reader).await;
|
||||||
.await;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,17 +311,14 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
match connect_result {
|
match connect_result {
|
||||||
Ok(Ok(stream)) => {
|
Ok(Ok(stream)) => {
|
||||||
let (mask_read, mut mask_write) = stream.into_split();
|
let (mask_read, mut mask_write) = stream.into_split();
|
||||||
let proxy_header = build_mask_proxy_header(
|
let proxy_header =
|
||||||
config.censorship.mask_proxy_protocol,
|
build_mask_proxy_header(config.censorship.mask_proxy_protocol, peer, local_addr);
|
||||||
peer,
|
if let Some(header) = proxy_header {
|
||||||
local_addr,
|
if !write_proxy_header_with_timeout(&mut mask_write, &header).await {
|
||||||
);
|
|
||||||
if let Some(header) = proxy_header
|
|
||||||
&& !write_proxy_header_with_timeout(&mut mask_write, &header).await
|
|
||||||
{
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if timeout(
|
if timeout(
|
||||||
MASK_RELAY_TIMEOUT,
|
MASK_RELAY_TIMEOUT,
|
||||||
relay_to_mask(
|
relay_to_mask(
|
||||||
@@ -645,8 +332,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
config.censorship.mask_shape_bucket_cap_bytes,
|
config.censorship.mask_shape_bucket_cap_bytes,
|
||||||
config.censorship.mask_shape_above_cap_blur,
|
config.censorship.mask_shape_above_cap_blur,
|
||||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||||
config.censorship.mask_shape_hardening_aggressive_mode,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -659,56 +344,22 @@ 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(reader).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(reader).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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mask_host = config
|
let mask_host = config.censorship.mask_host.as_deref()
|
||||||
.censorship
|
|
||||||
.mask_host
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(&config.censorship.tls_domain);
|
.unwrap_or(&config.censorship.tls_domain);
|
||||||
let mask_port = config.censorship.mask_port;
|
let mask_port = config.censorship.mask_port;
|
||||||
|
|
||||||
// Fail closed when fallback points at our own listener endpoint.
|
|
||||||
// Self-referential masking can create recursive proxy loops under
|
|
||||||
// misconfiguration and leak distinguishable load spikes to adversaries.
|
|
||||||
let resolved_mask_addr = resolve_socket_addr(mask_host, mask_port);
|
|
||||||
if is_mask_target_local_listener_async(mask_host, mask_port, local_addr, resolved_mask_addr)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let outcome_started = Instant::now();
|
|
||||||
debug!(
|
|
||||||
client_type = client_type,
|
|
||||||
host = %mask_host,
|
|
||||||
port = mask_port,
|
|
||||||
local = %local_addr,
|
|
||||||
"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)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let outcome_started = Instant::now();
|
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
client_type = client_type,
|
client_type = client_type,
|
||||||
host = %mask_host,
|
host = %mask_host,
|
||||||
@@ -718,9 +369,10 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Apply runtime DNS override for mask target when configured.
|
// Apply runtime DNS override for mask target when configured.
|
||||||
let mask_addr = resolved_mask_addr
|
let mask_addr = resolve_socket_addr(mask_host, mask_port)
|
||||||
.map(|addr| addr.to_string())
|
.map(|addr| addr.to_string())
|
||||||
.unwrap_or_else(|| format!("{}:{}", mask_host, mask_port));
|
.unwrap_or_else(|| format!("{}:{}", mask_host, mask_port));
|
||||||
|
let outcome_started = Instant::now();
|
||||||
let connect_started = Instant::now();
|
let connect_started = Instant::now();
|
||||||
let connect_result = timeout(MASK_TIMEOUT, TcpStream::connect(&mask_addr)).await;
|
let connect_result = timeout(MASK_TIMEOUT, TcpStream::connect(&mask_addr)).await;
|
||||||
match connect_result {
|
match connect_result {
|
||||||
@@ -729,12 +381,12 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
build_mask_proxy_header(config.censorship.mask_proxy_protocol, peer, local_addr);
|
build_mask_proxy_header(config.censorship.mask_proxy_protocol, peer, local_addr);
|
||||||
|
|
||||||
let (mask_read, mut mask_write) = stream.into_split();
|
let (mask_read, mut mask_write) = stream.into_split();
|
||||||
if let Some(header) = proxy_header
|
if let Some(header) = proxy_header {
|
||||||
&& !write_proxy_header_with_timeout(&mut mask_write, &header).await
|
if !write_proxy_header_with_timeout(&mut mask_write, &header).await {
|
||||||
{
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if timeout(
|
if timeout(
|
||||||
MASK_RELAY_TIMEOUT,
|
MASK_RELAY_TIMEOUT,
|
||||||
relay_to_mask(
|
relay_to_mask(
|
||||||
@@ -748,8 +400,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
config.censorship.mask_shape_bucket_cap_bytes,
|
config.censorship.mask_shape_bucket_cap_bytes,
|
||||||
config.censorship.mask_shape_above_cap_blur,
|
config.censorship.mask_shape_above_cap_blur,
|
||||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||||
config.censorship.mask_shape_hardening_aggressive_mode,
|
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -762,20 +412,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(reader).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(reader).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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,9 +435,8 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
shape_bucket_cap_bytes: usize,
|
shape_bucket_cap_bytes: usize,
|
||||||
shape_above_cap_blur: bool,
|
shape_above_cap_blur: bool,
|
||||||
shape_above_cap_blur_max_bytes: usize,
|
shape_above_cap_blur_max_bytes: usize,
|
||||||
shape_hardening_aggressive_mode: bool,
|
)
|
||||||
mask_relay_max_bytes: usize,
|
where
|
||||||
) where
|
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
W: AsyncWrite + Unpin + Send + 'static,
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
MR: AsyncRead + Unpin + Send + 'static,
|
MR: AsyncRead + Unpin + Send + 'static,
|
||||||
@@ -809,27 +450,14 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (upstream_copy, downstream_copy) = tokio::join!(
|
let _ = tokio::join!(
|
||||||
async {
|
async {
|
||||||
copy_with_idle_timeout(
|
let copied = copy_with_idle_timeout(&mut reader, &mut mask_write).await;
|
||||||
&mut reader,
|
let total_sent = initial_data.len().saturating_add(copied.total);
|
||||||
&mut mask_write,
|
|
||||||
mask_relay_max_bytes,
|
|
||||||
!shape_hardening_enabled,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
},
|
|
||||||
async {
|
|
||||||
copy_with_idle_timeout(&mut mask_read, &mut writer, mask_relay_max_bytes, true).await
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_sent = initial_data.len().saturating_add(upstream_copy.total);
|
|
||||||
|
|
||||||
let should_shape = shape_hardening_enabled
|
let should_shape = shape_hardening_enabled
|
||||||
&& !initial_data.is_empty()
|
&& copied.ended_by_eof
|
||||||
&& (upstream_copy.ended_by_eof
|
&& !initial_data.is_empty();
|
||||||
|| (shape_hardening_aggressive_mode && downstream_copy.total == 0));
|
|
||||||
|
|
||||||
maybe_write_shape_padding(
|
maybe_write_shape_padding(
|
||||||
&mut mask_write,
|
&mut mask_write,
|
||||||
@@ -839,44 +467,24 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
shape_bucket_cap_bytes,
|
shape_bucket_cap_bytes,
|
||||||
shape_above_cap_blur,
|
shape_above_cap_blur,
|
||||||
shape_above_cap_blur_max_bytes,
|
shape_above_cap_blur_max_bytes,
|
||||||
shape_hardening_aggressive_mode,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let _ = mask_write.shutdown().await;
|
let _ = mask_write.shutdown().await;
|
||||||
|
},
|
||||||
|
async {
|
||||||
|
let _ = copy_with_idle_timeout(&mut mask_read, &mut writer).await;
|
||||||
let _ = writer.shutdown().await;
|
let _ = writer.shutdown().await;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
/// Just consume all data from client without responding.
|
|
||||||
async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R, byte_cap: usize) {
|
|
||||||
if byte_cap == 0 {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep drain path fail-closed under slow-loris stalls.
|
/// Just consume all data from client without responding
|
||||||
let mut buf = Box::new([0u8; MASK_BUFFER_SIZE]);
|
async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R) {
|
||||||
let mut total = 0usize;
|
let mut buf = vec![0u8; MASK_BUFFER_SIZE];
|
||||||
|
while let Ok(n) = reader.read(&mut buf).await {
|
||||||
loop {
|
|
||||||
let remaining_budget = byte_cap.saturating_sub(total);
|
|
||||||
if remaining_budget == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
|
||||||
let n = match timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await {
|
|
||||||
Ok(Ok(n)) => n,
|
|
||||||
Ok(Err(_)) | Err(_) => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
total = total.saturating_add(n);
|
|
||||||
if total >= byte_cap {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,10 +508,6 @@ mod masking_shape_above_cap_blur_security_tests;
|
|||||||
#[path = "tests/masking_timing_normalization_security_tests.rs"]
|
#[path = "tests/masking_timing_normalization_security_tests.rs"]
|
||||||
mod masking_timing_normalization_security_tests;
|
mod masking_timing_normalization_security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_timing_budget_coupling_security_tests.rs"]
|
|
||||||
mod masking_timing_budget_coupling_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/masking_ab_envelope_blur_integration_security_tests.rs"]
|
#[path = "tests/masking_ab_envelope_blur_integration_security_tests.rs"]
|
||||||
mod masking_ab_envelope_blur_integration_security_tests;
|
mod masking_ab_envelope_blur_integration_security_tests;
|
||||||
@@ -920,86 +524,6 @@ mod masking_shape_guard_adversarial_tests;
|
|||||||
#[path = "tests/masking_shape_classifier_resistance_adversarial_tests.rs"]
|
#[path = "tests/masking_shape_classifier_resistance_adversarial_tests.rs"]
|
||||||
mod masking_shape_classifier_resistance_adversarial_tests;
|
mod masking_shape_classifier_resistance_adversarial_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_shape_bypass_blackhat_tests.rs"]
|
|
||||||
mod masking_shape_bypass_blackhat_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_aggressive_mode_security_tests.rs"]
|
|
||||||
mod masking_aggressive_mode_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/masking_timing_sidechannel_redteam_expected_fail_tests.rs"]
|
#[path = "tests/masking_timing_sidechannel_redteam_expected_fail_tests.rs"]
|
||||||
mod masking_timing_sidechannel_redteam_expected_fail_tests;
|
mod masking_timing_sidechannel_redteam_expected_fail_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_self_target_loop_security_tests.rs"]
|
|
||||||
mod masking_self_target_loop_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_classification_completeness_security_tests.rs"]
|
|
||||||
mod masking_classification_completeness_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_relay_guardrails_security_tests.rs"]
|
|
||||||
mod masking_relay_guardrails_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_connect_failure_close_matrix_security_tests.rs"]
|
|
||||||
mod masking_connect_failure_close_matrix_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_additional_hardening_security_tests.rs"]
|
|
||||||
mod masking_additional_hardening_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_consume_idle_timeout_security_tests.rs"]
|
|
||||||
mod masking_consume_idle_timeout_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_http2_probe_classification_security_tests.rs"]
|
|
||||||
mod masking_http2_probe_classification_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_http_probe_boundary_security_tests.rs"]
|
|
||||||
mod masking_http_probe_boundary_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_rng_hoist_perf_regression_tests.rs"]
|
|
||||||
mod masking_rng_hoist_perf_regression_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_http2_preface_integration_security_tests.rs"]
|
|
||||||
mod masking_http2_preface_integration_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_consume_stress_adversarial_tests.rs"]
|
|
||||||
mod masking_consume_stress_adversarial_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_interface_cache_security_tests.rs"]
|
|
||||||
mod masking_interface_cache_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_interface_cache_defense_in_depth_security_tests.rs"]
|
|
||||||
mod masking_interface_cache_defense_in_depth_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_interface_cache_concurrency_security_tests.rs"]
|
|
||||||
mod masking_interface_cache_concurrency_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_production_cap_regression_security_tests.rs"]
|
|
||||||
mod masking_production_cap_regression_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_extended_attack_surface_security_tests.rs"]
|
|
||||||
mod masking_extended_attack_surface_security_tests;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/masking_padding_timeout_adversarial_tests.rs"]
|
|
||||||
mod masking_padding_timeout_adversarial_tests;
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "redteam_offline_expected_fail"))]
|
|
||||||
#[path = "tests/masking_offline_target_redteam_expected_fail_tests.rs"]
|
|
||||||
mod masking_offline_target_redteam_expected_fail_tests;
|
|
||||||
|
|||||||
+213
-603
File diff suppressed because it is too large
Load Diff
+1
-59
@@ -1,71 +1,13 @@
|
|||||||
//! Proxy Defs
|
//! Proxy Defs
|
||||||
|
|
||||||
// Apply strict linting to proxy production code while keeping test builds noise-tolerant.
|
|
||||||
#![cfg_attr(test, allow(warnings))]
|
|
||||||
#![cfg_attr(not(test), forbid(clippy::undocumented_unsafe_blocks))]
|
|
||||||
#![cfg_attr(
|
|
||||||
not(test),
|
|
||||||
deny(
|
|
||||||
clippy::unwrap_used,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::panic,
|
|
||||||
clippy::todo,
|
|
||||||
clippy::unimplemented,
|
|
||||||
clippy::correctness,
|
|
||||||
clippy::option_if_let_else,
|
|
||||||
clippy::or_fun_call,
|
|
||||||
clippy::branches_sharing_code,
|
|
||||||
clippy::single_option_map,
|
|
||||||
clippy::useless_let_if_seq,
|
|
||||||
clippy::redundant_locals,
|
|
||||||
clippy::cloned_ref_to_slice_refs,
|
|
||||||
unsafe_code,
|
|
||||||
clippy::await_holding_lock,
|
|
||||||
clippy::await_holding_refcell_ref,
|
|
||||||
clippy::debug_assert_with_mut_call,
|
|
||||||
clippy::macro_use_imports,
|
|
||||||
clippy::cast_ptr_alignment,
|
|
||||||
clippy::cast_lossless,
|
|
||||||
clippy::ptr_as_ptr,
|
|
||||||
clippy::large_stack_arrays,
|
|
||||||
clippy::same_functions_in_if_condition,
|
|
||||||
trivial_casts,
|
|
||||||
trivial_numeric_casts,
|
|
||||||
unused_extern_crates,
|
|
||||||
unused_import_braces,
|
|
||||||
rust_2018_idioms
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
#![cfg_attr(
|
|
||||||
not(test),
|
|
||||||
allow(
|
|
||||||
clippy::use_self,
|
|
||||||
clippy::redundant_closure,
|
|
||||||
clippy::too_many_arguments,
|
|
||||||
clippy::doc_markdown,
|
|
||||||
clippy::missing_const_for_fn,
|
|
||||||
clippy::unnecessary_operation,
|
|
||||||
clippy::redundant_pub_crate,
|
|
||||||
clippy::derive_partial_eq_without_eq,
|
|
||||||
clippy::type_complexity,
|
|
||||||
clippy::new_ret_no_self,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::significant_drop_tightening,
|
|
||||||
clippy::significant_drop_in_scrutinee,
|
|
||||||
clippy::float_cmp,
|
|
||||||
clippy::nursery
|
|
||||||
)
|
|
||||||
)]
|
|
||||||
|
|
||||||
pub mod adaptive_buffers;
|
pub mod adaptive_buffers;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod direct_relay;
|
pub mod direct_relay;
|
||||||
pub mod handshake;
|
pub mod handshake;
|
||||||
pub mod masking;
|
pub mod masking;
|
||||||
pub mod middle_relay;
|
pub mod middle_relay;
|
||||||
pub mod relay;
|
|
||||||
pub mod route_mode;
|
pub mod route_mode;
|
||||||
|
pub mod relay;
|
||||||
pub mod session_eviction;
|
pub mod session_eviction;
|
||||||
|
|
||||||
pub use client::ClientHandler;
|
pub use client::ClientHandler;
|
||||||
|
|||||||
+221
-105
@@ -51,18 +51,21 @@
|
|||||||
//! - `poll_write` on client = S→C (to client) → `octets_to`, `msgs_to`
|
//! - `poll_write` on client = S→C (to client) → `octets_to`, `msgs_to`
|
||||||
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
|
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
|
||||||
|
|
||||||
use crate::error::{ProxyError, Result};
|
|
||||||
use crate::stats::{Stats, UserStats};
|
|
||||||
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::{Arc, Mutex, OnceLock};
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
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 dashmap::DashMap;
|
||||||
|
use tokio::io::{
|
||||||
|
AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes,
|
||||||
|
};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
|
use crate::error::{ProxyError, Result};
|
||||||
|
use crate::stats::Stats;
|
||||||
|
use crate::stream::BufferPool;
|
||||||
|
|
||||||
// ============= Constants =============
|
// ============= Constants =============
|
||||||
|
|
||||||
@@ -208,10 +211,12 @@ struct StatsIo<S> {
|
|||||||
counters: Arc<SharedCounters>,
|
counters: Arc<SharedCounters>,
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
user: String,
|
user: String,
|
||||||
user_stats: Arc<UserStats>,
|
|
||||||
quota_limit: Option<u64>,
|
quota_limit: Option<u64>,
|
||||||
quota_exceeded: Arc<AtomicBool>,
|
quota_exceeded: Arc<AtomicBool>,
|
||||||
quota_bytes_since_check: u64,
|
quota_read_wake_scheduled: bool,
|
||||||
|
quota_write_wake_scheduled: bool,
|
||||||
|
quota_read_retry_active: Arc<AtomicBool>,
|
||||||
|
quota_write_retry_active: Arc<AtomicBool>,
|
||||||
epoch: Instant,
|
epoch: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,21 +232,29 @@ impl<S> StatsIo<S> {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
// Mark initial activity so the watchdog doesn't fire before data flows
|
// Mark initial activity so the watchdog doesn't fire before data flows
|
||||||
counters.touch(Instant::now(), epoch);
|
counters.touch(Instant::now(), epoch);
|
||||||
let user_stats = stats.get_or_create_user_stats_handle(&user);
|
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
counters,
|
counters,
|
||||||
stats,
|
stats,
|
||||||
user,
|
user,
|
||||||
user_stats,
|
|
||||||
quota_limit,
|
quota_limit,
|
||||||
quota_exceeded,
|
quota_exceeded,
|
||||||
quota_bytes_since_check: 0,
|
quota_read_wake_scheduled: false,
|
||||||
|
quota_write_wake_scheduled: false,
|
||||||
|
quota_read_retry_active: Arc::new(AtomicBool::new(false)),
|
||||||
|
quota_write_retry_active: Arc::new(AtomicBool::new(false)),
|
||||||
epoch,
|
epoch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S> Drop for StatsIo<S> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.quota_read_retry_active.store(false, Ordering::Relaxed);
|
||||||
|
self.quota_write_retry_active.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct QuotaIoSentinel;
|
struct QuotaIoSentinel;
|
||||||
|
|
||||||
@@ -265,22 +278,84 @@ fn is_quota_io_error(err: &io::Error) -> bool {
|
|||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUOTA_NEAR_LIMIT_BYTES: u64 = 64 * 1024;
|
#[cfg(test)]
|
||||||
const QUOTA_LARGE_CHARGE_BYTES: u64 = 16 * 1024;
|
const QUOTA_CONTENTION_RETRY_INTERVAL: Duration = Duration::from_millis(1);
|
||||||
const QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES: u64 = 4 * 1024;
|
#[cfg(not(test))]
|
||||||
const QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES: u64 = 64 * 1024;
|
const QUOTA_CONTENTION_RETRY_INTERVAL: Duration = Duration::from_millis(2);
|
||||||
|
|
||||||
#[inline]
|
fn spawn_quota_retry_waker(retry_active: Arc<AtomicBool>, waker: std::task::Waker) {
|
||||||
fn quota_adaptive_interval_bytes(remaining_before: u64) -> u64 {
|
tokio::task::spawn(async move {
|
||||||
remaining_before.saturating_div(2).clamp(
|
loop {
|
||||||
QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES,
|
if !retry_active.load(Ordering::Relaxed) {
|
||||||
QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES,
|
break;
|
||||||
)
|
}
|
||||||
|
tokio::time::sleep(QUOTA_CONTENTION_RETRY_INTERVAL).await;
|
||||||
|
if !retry_active.load(Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
waker.wake_by_ref();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
static QUOTA_USER_LOCKS: OnceLock<DashMap<String, Arc<Mutex<()>>>> = OnceLock::new();
|
||||||
fn should_immediate_quota_check(remaining_before: u64, charge_bytes: u64) -> bool {
|
static QUOTA_USER_OVERFLOW_LOCKS: OnceLock<Vec<Arc<Mutex<()>>>> = OnceLock::new();
|
||||||
remaining_before <= QUOTA_NEAR_LIMIT_BYTES || charge_bytes >= QUOTA_LARGE_CHARGE_BYTES
|
|
||||||
|
#[cfg(test)]
|
||||||
|
const QUOTA_USER_LOCKS_MAX: usize = 64;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
const QUOTA_USER_LOCKS_MAX: usize = 4_096;
|
||||||
|
#[cfg(test)]
|
||||||
|
const QUOTA_OVERFLOW_LOCK_STRIPES: usize = 16;
|
||||||
|
#[cfg(not(test))]
|
||||||
|
const QUOTA_OVERFLOW_LOCK_STRIPES: usize = 256;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn quota_user_lock_test_guard() -> &'static Mutex<()> {
|
||||||
|
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||||
|
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn quota_user_lock_test_scope() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
quota_user_lock_test_guard()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quota_overflow_user_lock(user: &str) -> Arc<Mutex<()>> {
|
||||||
|
let stripes = QUOTA_USER_OVERFLOW_LOCKS.get_or_init(|| {
|
||||||
|
(0..QUOTA_OVERFLOW_LOCK_STRIPES)
|
||||||
|
.map(|_| Arc::new(Mutex::new(())))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let hash = crc32fast::hash(user.as_bytes()) as usize;
|
||||||
|
Arc::clone(&stripes[hash % stripes.len()])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quota_user_lock(user: &str) -> Arc<Mutex<()>> {
|
||||||
|
let locks = QUOTA_USER_LOCKS.get_or_init(DashMap::new);
|
||||||
|
if let Some(existing) = locks.get(user) {
|
||||||
|
return Arc::clone(existing.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if locks.len() >= QUOTA_USER_LOCKS_MAX {
|
||||||
|
locks.retain(|_, value| Arc::strong_count(value) > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if locks.len() >= QUOTA_USER_LOCKS_MAX {
|
||||||
|
return quota_overflow_user_lock(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let created = Arc::new(Mutex::new(()));
|
||||||
|
match locks.entry(user.to_string()) {
|
||||||
|
dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
|
||||||
|
dashmap::mapref::entry::Entry::Vacant(entry) => {
|
||||||
|
entry.insert(Arc::clone(&created));
|
||||||
|
created
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
||||||
@@ -290,60 +365,78 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
|||||||
buf: &mut ReadBuf<'_>,
|
buf: &mut ReadBuf<'_>,
|
||||||
) -> Poll<io::Result<()>> {
|
) -> Poll<io::Result<()>> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
if this.quota_exceeded.load(Ordering::Acquire) {
|
if this.quota_exceeded.load(Ordering::Relaxed) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remaining_before = None;
|
let quota_lock = this
|
||||||
if let Some(limit) = this.quota_limit {
|
.quota_limit
|
||||||
let used_before = this.user_stats.quota_used();
|
.is_some()
|
||||||
let remaining = limit.saturating_sub(used_before);
|
.then(|| quota_user_lock(&this.user));
|
||||||
if remaining == 0 {
|
let _quota_guard = if let Some(lock) = quota_lock.as_ref() {
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
match lock.try_lock() {
|
||||||
|
Ok(guard) => {
|
||||||
|
this.quota_read_wake_scheduled = false;
|
||||||
|
this.quota_read_retry_active.store(false, Ordering::Relaxed);
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if !this.quota_read_wake_scheduled {
|
||||||
|
this.quota_read_wake_scheduled = true;
|
||||||
|
this.quota_read_retry_active.store(true, Ordering::Relaxed);
|
||||||
|
spawn_quota_retry_waker(
|
||||||
|
Arc::clone(&this.quota_read_retry_active),
|
||||||
|
cx.waker().clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(limit) = this.quota_limit
|
||||||
|
&& this.stats.get_user_total_octets(&this.user) >= limit
|
||||||
|
{
|
||||||
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
remaining_before = Some(remaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
let before = buf.filled().len();
|
let before = buf.filled().len();
|
||||||
|
|
||||||
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
||||||
Poll::Ready(Ok(())) => {
|
Poll::Ready(Ok(())) => {
|
||||||
let n = buf.filled().len() - before;
|
let n = buf.filled().len() - before;
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
let n_to_charge = n as u64;
|
let mut reached_quota_boundary = false;
|
||||||
|
if let Some(limit) = this.quota_limit {
|
||||||
|
let used = this.stats.get_user_total_octets(&this.user);
|
||||||
|
if used >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = limit - used;
|
||||||
|
if (n as u64) > remaining {
|
||||||
|
// Fail closed: when a single read chunk would cross quota,
|
||||||
|
// stop relay immediately without accounting beyond the cap.
|
||||||
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
reached_quota_boundary = (n as u64) == remaining;
|
||||||
|
}
|
||||||
|
|
||||||
// C→S: client sent data
|
// C→S: client sent data
|
||||||
this.counters
|
this.counters.c2s_bytes.fetch_add(n as u64, Ordering::Relaxed);
|
||||||
.c2s_bytes
|
|
||||||
.fetch_add(n_to_charge, Ordering::Relaxed);
|
|
||||||
this.counters.c2s_ops.fetch_add(1, Ordering::Relaxed);
|
this.counters.c2s_ops.fetch_add(1, Ordering::Relaxed);
|
||||||
this.counters.touch(Instant::now(), this.epoch);
|
this.counters.touch(Instant::now(), this.epoch);
|
||||||
|
|
||||||
this.stats
|
this.stats.add_user_octets_from(&this.user, n as u64);
|
||||||
.add_user_octets_from_handle(this.user_stats.as_ref(), n_to_charge);
|
this.stats.increment_user_msgs_from(&this.user);
|
||||||
this.stats
|
|
||||||
.increment_user_msgs_from_handle(this.user_stats.as_ref());
|
|
||||||
|
|
||||||
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
|
if reached_quota_boundary {
|
||||||
this.stats
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
|
|
||||||
if should_immediate_quota_check(remaining, n_to_charge) {
|
|
||||||
this.quota_bytes_since_check = 0;
|
|
||||||
if this.user_stats.quota_used() >= limit {
|
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.quota_bytes_since_check =
|
|
||||||
this.quota_bytes_since_check.saturating_add(n_to_charge);
|
|
||||||
let interval = quota_adaptive_interval_bytes(remaining);
|
|
||||||
if this.quota_bytes_since_check >= interval {
|
|
||||||
this.quota_bytes_since_check = 0;
|
|
||||||
if this.user_stats.quota_used() >= limit {
|
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(user = %this.user, bytes = n, "C->S");
|
trace!(user = %this.user, bytes = n, "C->S");
|
||||||
@@ -362,57 +455,72 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||||||
buf: &[u8],
|
buf: &[u8],
|
||||||
) -> Poll<io::Result<usize>> {
|
) -> Poll<io::Result<usize>> {
|
||||||
let this = self.get_mut();
|
let this = self.get_mut();
|
||||||
if this.quota_exceeded.load(Ordering::Acquire) {
|
if this.quota_exceeded.load(Ordering::Relaxed) {
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remaining_before = None;
|
let quota_lock = this
|
||||||
if let Some(limit) = this.quota_limit {
|
.quota_limit
|
||||||
let used_before = this.user_stats.quota_used();
|
.is_some()
|
||||||
let remaining = limit.saturating_sub(used_before);
|
.then(|| quota_user_lock(&this.user));
|
||||||
if remaining == 0 {
|
let _quota_guard = if let Some(lock) = quota_lock.as_ref() {
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
match lock.try_lock() {
|
||||||
|
Ok(guard) => {
|
||||||
|
this.quota_write_wake_scheduled = false;
|
||||||
|
this.quota_write_retry_active.store(false, Ordering::Relaxed);
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
if !this.quota_write_wake_scheduled {
|
||||||
|
this.quota_write_wake_scheduled = true;
|
||||||
|
this.quota_write_retry_active.store(true, Ordering::Relaxed);
|
||||||
|
spawn_quota_retry_waker(
|
||||||
|
Arc::clone(&this.quota_write_retry_active),
|
||||||
|
cx.waker().clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_buf = if let Some(limit) = this.quota_limit {
|
||||||
|
let used = this.stats.get_user_total_octets(&this.user);
|
||||||
|
if used >= limit {
|
||||||
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
return Poll::Ready(Err(quota_io_error()));
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
}
|
}
|
||||||
remaining_before = Some(remaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
match Pin::new(&mut this.inner).poll_write(cx, buf) {
|
let remaining = (limit - used) as usize;
|
||||||
|
if buf.len() > remaining {
|
||||||
|
// Fail closed: do not emit partial S->C payload when remaining
|
||||||
|
// quota cannot accommodate the pending write request.
|
||||||
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
|
}
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
|
||||||
|
match Pin::new(&mut this.inner).poll_write(cx, write_buf) {
|
||||||
Poll::Ready(Ok(n)) => {
|
Poll::Ready(Ok(n)) => {
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
let n_to_charge = n as u64;
|
|
||||||
|
|
||||||
// S→C: data written to client
|
// S→C: data written to client
|
||||||
this.counters
|
this.counters.s2c_bytes.fetch_add(n as u64, Ordering::Relaxed);
|
||||||
.s2c_bytes
|
|
||||||
.fetch_add(n_to_charge, Ordering::Relaxed);
|
|
||||||
this.counters.s2c_ops.fetch_add(1, Ordering::Relaxed);
|
this.counters.s2c_ops.fetch_add(1, Ordering::Relaxed);
|
||||||
this.counters.touch(Instant::now(), this.epoch);
|
this.counters.touch(Instant::now(), this.epoch);
|
||||||
|
|
||||||
this.stats
|
this.stats.add_user_octets_to(&this.user, n as u64);
|
||||||
.add_user_octets_to_handle(this.user_stats.as_ref(), n_to_charge);
|
this.stats.increment_user_msgs_to(&this.user);
|
||||||
this.stats
|
|
||||||
.increment_user_msgs_to_handle(this.user_stats.as_ref());
|
|
||||||
|
|
||||||
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
|
if let Some(limit) = this.quota_limit
|
||||||
this.stats
|
&& this.stats.get_user_total_octets(&this.user) >= limit
|
||||||
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
|
{
|
||||||
if should_immediate_quota_check(remaining, n_to_charge) {
|
this.quota_exceeded.store(true, Ordering::Relaxed);
|
||||||
this.quota_bytes_since_check = 0;
|
return Poll::Ready(Err(quota_io_error()));
|
||||||
if this.user_stats.quota_used() >= limit {
|
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.quota_bytes_since_check =
|
|
||||||
this.quota_bytes_since_check.saturating_add(n_to_charge);
|
|
||||||
let interval = quota_adaptive_interval_bytes(remaining);
|
|
||||||
if this.quota_bytes_since_check >= interval {
|
|
||||||
this.quota_bytes_since_check = 0;
|
|
||||||
if this.user_stats.quota_used() >= limit {
|
|
||||||
this.quota_exceeded.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(user = %this.user, bytes = n, "S->C");
|
trace!(user = %this.user, bytes = n, "S->C");
|
||||||
@@ -506,7 +614,7 @@ where
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let idle = wd_counters.idle_duration(now, epoch);
|
let idle = wd_counters.idle_duration(now, epoch);
|
||||||
|
|
||||||
if wd_quota_exceeded.load(Ordering::Acquire) {
|
if wd_quota_exceeded.load(Ordering::Relaxed) {
|
||||||
warn!(user = %wd_user, "User data quota reached, closing relay");
|
warn!(user = %wd_user, "User data quota reached, closing relay");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -644,10 +752,18 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_security_tests.rs"]
|
||||||
|
mod security_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/relay_adversarial_tests.rs"]
|
#[path = "tests/relay_adversarial_tests.rs"]
|
||||||
mod adversarial_tests;
|
mod adversarial_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_lock_pressure_adversarial_tests.rs"]
|
||||||
|
mod relay_quota_lock_pressure_adversarial_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "tests/relay_quota_boundary_blackhat_tests.rs"]
|
#[path = "tests/relay_quota_boundary_blackhat_tests.rs"]
|
||||||
mod relay_quota_boundary_blackhat_tests;
|
mod relay_quota_boundary_blackhat_tests;
|
||||||
@@ -660,14 +776,14 @@ mod relay_quota_model_adversarial_tests;
|
|||||||
#[path = "tests/relay_quota_overflow_regression_tests.rs"]
|
#[path = "tests/relay_quota_overflow_regression_tests.rs"]
|
||||||
mod relay_quota_overflow_regression_tests;
|
mod relay_quota_overflow_regression_tests;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[path = "tests/relay_quota_extended_attack_surface_security_tests.rs"]
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
#[path = "tests/relay_atomic_quota_invariant_tests.rs"]
|
#[path = "tests/relay_quota_waker_storm_adversarial_tests.rs"]
|
||||||
mod relay_atomic_quota_invariant_tests;
|
mod relay_quota_waker_storm_adversarial_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/relay_quota_wake_liveness_regression_tests.rs"]
|
||||||
|
mod relay_quota_wake_liveness_regression_tests;
|
||||||
@@ -119,7 +119,9 @@ pub(crate) fn affected_cutover_state(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cutover_stagger_delay(session_id: u64, generation: u64) -> Duration {
|
pub(crate) fn cutover_stagger_delay(session_id: u64, generation: u64) -> Duration {
|
||||||
let mut value = session_id ^ generation.rotate_left(17) ^ 0x9e37_79b9_7f4a_7c15;
|
let mut value = session_id
|
||||||
|
^ generation.rotate_left(17)
|
||||||
|
^ 0x9e37_79b9_7f4a_7c15;
|
||||||
value ^= value >> 30;
|
value ^= value >> 30;
|
||||||
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||||
value ^= value >> 27;
|
value ^= value >> 27;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
/// Session eviction is intentionally disabled in runtime.
|
/// Session eviction is intentionally disabled in runtime.
|
||||||
///
|
///
|
||||||
/// The initial `user+dc` single-lease model caused valid parallel client
|
/// The initial `user+dc` single-lease model caused valid parallel client
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::error::ProxyError;
|
|
||||||
use crate::ip_tracker::UserIpTracker;
|
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use crate::ip_tracker::UserIpTracker;
|
||||||
|
use crate::error::ProxyError;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Priority 3: Massive Concurrency Stress (OWASP ASVS 5.1.6)
|
// Priority 3: Massive Concurrency Stress (OWASP ASVS 5.1.6)
|
||||||
@@ -20,10 +20,7 @@ async fn client_stress_10k_connections_limit_strict() {
|
|||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), limit);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), limit);
|
|
||||||
|
|
||||||
let iterations = 1000;
|
let iterations = 1000;
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
@@ -41,10 +38,12 @@ async fn client_stress_10k_connections_limit_strict() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
match RunningClientHandler::acquire_user_connection_reservation_static(
|
match RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
&user_str, &config, stats, peer, ip_tracker,
|
&user_str,
|
||||||
)
|
&config,
|
||||||
.await
|
stats,
|
||||||
{
|
peer,
|
||||||
|
ip_tracker,
|
||||||
|
).await {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. }) => Err(()),
|
Err(ProxyError::ConnectionLimitExceeded { .. }) => Err(()),
|
||||||
Err(e) => panic!("Unexpected error: {:?}", e),
|
Err(e) => panic!("Unexpected error: {:?}", e),
|
||||||
@@ -68,27 +67,15 @@ async fn client_stress_10k_connections_limit_strict() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(successes, limit, "Should allow exactly 'limit' connections");
|
assert_eq!(successes, limit, "Should allow exactly 'limit' connections");
|
||||||
assert_eq!(
|
assert_eq!(failures, iterations - limit, "Should fail the rest with LimitExceeded");
|
||||||
failures,
|
|
||||||
iterations - limit,
|
|
||||||
"Should fail the rest with LimitExceeded"
|
|
||||||
);
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), limit as u64);
|
assert_eq!(stats.get_user_curr_connects(user), limit as u64);
|
||||||
|
|
||||||
drop(reservations);
|
drop(reservations);
|
||||||
|
|
||||||
ip_tracker.drain_cleanup_queue().await;
|
ip_tracker.drain_cleanup_queue().await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(stats.get_user_curr_connects(user), 0, "Stats must converge to 0 after all drops");
|
||||||
stats.get_user_curr_connects(user),
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0, "IP tracker must converge to 0");
|
||||||
0,
|
|
||||||
"Stats must converge to 0 after all drops"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
ip_tracker.get_active_ip_count(user).await,
|
|
||||||
0,
|
|
||||||
"IP tracker must converge to 0"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -119,11 +106,7 @@ async fn client_ip_tracker_race_condition_stress() {
|
|||||||
|
|
||||||
futures::future::join_all(tasks).await;
|
futures::future::join_all(tasks).await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0, "IP count must be zero after balanced add/remove burst");
|
||||||
ip_tracker.get_active_ip_count(user).await,
|
|
||||||
0,
|
|
||||||
"IP count must be zero after balanced add/remove burst"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -136,10 +119,7 @@ async fn client_limit_burst_peak_never_exceeds_cap() {
|
|||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), limit);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), limit);
|
|
||||||
|
|
||||||
let peak = Arc::new(AtomicU64::new(0));
|
let peak = Arc::new(AtomicU64::new(0));
|
||||||
let mut tasks = Vec::with_capacity(attempts);
|
let mut tasks = Vec::with_capacity(attempts);
|
||||||
@@ -227,10 +207,10 @@ async fn client_expiration_rejection_never_mutates_live_counters() {
|
|||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config.access.user_expirations.insert(
|
config
|
||||||
user.to_string(),
|
.access
|
||||||
chrono::Utc::now() - chrono::Duration::seconds(1),
|
.user_expirations
|
||||||
);
|
.insert(user.to_string(), chrono::Utc::now() - chrono::Duration::seconds(1));
|
||||||
|
|
||||||
let peer: SocketAddr = "198.51.100.202:31112".parse().unwrap();
|
let peer: SocketAddr = "198.51.100.202:31112".parse().unwrap();
|
||||||
let res = RunningClientHandler::acquire_user_connection_reservation_static(
|
let res = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
@@ -255,10 +235,7 @@ async fn client_ip_limit_failure_rolls_back_counter_exactly() {
|
|||||||
ip_tracker.set_user_limit(user, 1).await;
|
ip_tracker.set_user_limit(user, 1).await;
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 16);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 16);
|
|
||||||
|
|
||||||
let first_peer: SocketAddr = "198.51.100.203:31113".parse().unwrap();
|
let first_peer: SocketAddr = "198.51.100.203:31113".parse().unwrap();
|
||||||
let first = RunningClientHandler::acquire_user_connection_reservation_static(
|
let first = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
@@ -281,10 +258,7 @@ async fn client_ip_limit_failure_rolls_back_counter_exactly() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(second, Err(ProxyError::ConnectionLimitExceeded { .. })));
|
||||||
second,
|
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. })
|
|
||||||
));
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 1);
|
assert_eq!(stats.get_user_curr_connects(user), 1);
|
||||||
|
|
||||||
drop(first);
|
drop(first);
|
||||||
@@ -302,10 +276,7 @@ async fn client_parallel_limit_checks_success_path_leaves_no_residue() {
|
|||||||
ip_tracker.set_user_limit(user, 128).await;
|
ip_tracker.set_user_limit(user, 128).await;
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 128);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 128);
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for i in 0..128u16 {
|
for i in 0..128u16 {
|
||||||
@@ -339,10 +310,7 @@ async fn client_parallel_limit_checks_failure_path_leaves_no_residue() {
|
|||||||
ip_tracker.set_user_limit(user, 0).await;
|
ip_tracker.set_user_limit(user, 0).await;
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 512);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 512);
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for i in 0..64u16 {
|
for i in 0..64u16 {
|
||||||
@@ -351,10 +319,7 @@ async fn client_parallel_limit_checks_failure_path_leaves_no_residue() {
|
|||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
|
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(async move {
|
||||||
let peer = SocketAddr::new(
|
let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(172, 16, 0, (i % 250 + 1) as u8)), 33000 + i);
|
||||||
IpAddr::V4(Ipv4Addr::new(172, 16, 0, (i % 250 + 1) as u8)),
|
|
||||||
33000 + i,
|
|
||||||
);
|
|
||||||
RunningClientHandler::check_user_limits_static(user, &config, &stats, peer, &ip_tracker)
|
RunningClientHandler::check_user_limits_static(user, &config, &stats, peer, &ip_tracker)
|
||||||
.await
|
.await
|
||||||
}));
|
}));
|
||||||
@@ -395,7 +360,11 @@ async fn client_churn_mixed_success_failure_converges_to_zero_state() {
|
|||||||
34000 + (i % 32),
|
34000 + (i % 32),
|
||||||
);
|
);
|
||||||
let maybe_res = RunningClientHandler::acquire_user_connection_reservation_static(
|
let maybe_res = RunningClientHandler::acquire_user_connection_reservation_static(
|
||||||
user, &config, stats, peer, ip_tracker,
|
user,
|
||||||
|
&config,
|
||||||
|
stats,
|
||||||
|
peer,
|
||||||
|
ip_tracker,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -432,7 +401,11 @@ async fn client_same_ip_parallel_attempts_allow_at_most_one_when_limit_is_one()
|
|||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::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
|
||||||
}));
|
}));
|
||||||
@@ -451,10 +424,7 @@ async fn client_same_ip_parallel_attempts_allow_at_most_one_when_limit_is_one()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(granted, 1, "only one reservation may be granted for same IP with limit=1");
|
||||||
granted, 1,
|
|
||||||
"only one reservation may be granted for same IP with limit=1"
|
|
||||||
);
|
|
||||||
drop(reservations);
|
drop(reservations);
|
||||||
ip_tracker.drain_cleanup_queue().await;
|
ip_tracker.drain_cleanup_queue().await;
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 0);
|
assert_eq!(stats.get_user_curr_connects(user), 0);
|
||||||
@@ -469,10 +439,7 @@ async fn client_repeat_acquire_release_cycles_never_accumulate_state() {
|
|||||||
ip_tracker.set_user_limit(user, 32).await;
|
ip_tracker.set_user_limit(user, 32).await;
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 32);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 32);
|
|
||||||
|
|
||||||
for i in 0..500u16 {
|
for i in 0..500u16 {
|
||||||
let peer = SocketAddr::new(
|
let peer = SocketAddr::new(
|
||||||
@@ -517,7 +484,11 @@ async fn client_multi_user_isolation_under_parallel_limit_exhaustion() {
|
|||||||
37000 + i,
|
37000 + i,
|
||||||
);
|
);
|
||||||
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
|
||||||
}));
|
}));
|
||||||
@@ -526,11 +497,7 @@ async fn client_multi_user_isolation_under_parallel_limit_exhaustion() {
|
|||||||
let mut u1_success = 0usize;
|
let mut u1_success = 0usize;
|
||||||
let mut u2_success = 0usize;
|
let mut u2_success = 0usize;
|
||||||
let mut reservations = Vec::new();
|
let mut reservations = Vec::new();
|
||||||
for (idx, result) in futures::future::join_all(tasks)
|
for (idx, result) in futures::future::join_all(tasks).await.into_iter().enumerate() {
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
let user = if idx % 2 == 0 { "u1" } else { "u2" };
|
let user = if idx % 2 == 0 { "u1" } else { "u2" };
|
||||||
match result.unwrap() {
|
match result.unwrap() {
|
||||||
Ok(reservation) => {
|
Ok(reservation) => {
|
||||||
@@ -589,10 +556,7 @@ async fn client_limit_recovery_after_full_rejection_wave() {
|
|||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(
|
assert!(matches!(denied, Err(ProxyError::ConnectionLimitExceeded { .. })));
|
||||||
denied,
|
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. })
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(reservation);
|
drop(reservation);
|
||||||
@@ -608,10 +572,7 @@ async fn client_limit_recovery_after_full_rejection_wave() {
|
|||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(recovered.is_ok(), "capacity must recover after prior holder drops");
|
||||||
recovered.is_ok(),
|
|
||||||
"capacity must recover after prior holder drops"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -658,10 +619,7 @@ async fn client_dual_limit_cross_product_never_leaks_on_reject() {
|
|||||||
ip_tracker.clone(),
|
ip_tracker.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(
|
assert!(matches!(denied, Err(ProxyError::ConnectionLimitExceeded { .. })));
|
||||||
denied,
|
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. })
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 2);
|
assert_eq!(stats.get_user_curr_connects(user), 2);
|
||||||
@@ -679,10 +637,7 @@ async fn client_check_user_limits_concurrent_churn_no_counter_drift() {
|
|||||||
ip_tracker.set_user_limit(user, 64).await;
|
ip_tracker.set_user_limit(user, 64).await;
|
||||||
|
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config
|
config.access.user_max_tcp_conns.insert(user.to_string(), 64);
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 64);
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for i in 0..512u16 {
|
for i in 0..512u16 {
|
||||||
|
|||||||
@@ -1,473 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{ProxyConfig, UpstreamConfig, UpstreamType};
|
|
||||||
use crate::protocol::constants::{MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE};
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::task::{Context, Poll};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt, ReadBuf, duplex};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_mask_reject_delay_min_greater_than_max_does_not_panic() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.server_hello_delay_min_ms = 5000;
|
|
||||||
config.censorship.server_hello_delay_max_ms = 1000;
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
||||||
rt.block_on(async {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
maybe_apply_mask_reject_delay(&config).await;
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
|
|
||||||
assert!(elapsed >= Duration::from_millis(1000));
|
|
||||||
assert!(elapsed < Duration::from_millis(1500));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_handshake_timeout_with_mask_grace_saturating_add_prevents_overflow() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.timeouts.client_handshake = u64::MAX;
|
|
||||||
config.censorship.mask = true;
|
|
||||||
|
|
||||||
let timeout = handshake_timeout_with_mask_grace(&config);
|
|
||||||
assert_eq!(timeout.as_secs(), u64::MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
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 - 1
|
|
||||||
));
|
|
||||||
assert!(tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE));
|
|
||||||
assert!(!tls_clienthello_len_in_bounds(MAX_TLS_PLAINTEXT_SIZE + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_synthetic_local_addr_boundaries() {
|
|
||||||
assert_eq!(synthetic_local_addr(0).port(), 0);
|
|
||||||
assert_eq!(synthetic_local_addr(80).port(), 80);
|
|
||||||
assert_eq!(synthetic_local_addr(u16::MAX).port(), u16::MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_beobachten_record_handshake_failure_class_stream_error_eof() {
|
|
||||||
let beobachten = BeobachtenStore::new();
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.general.beobachten = true;
|
|
||||||
config.general.beobachten_minutes = 1;
|
|
||||||
|
|
||||||
let eof_err = ProxyError::Stream(crate::error::StreamError::UnexpectedEof);
|
|
||||||
let peer_ip: IpAddr = "198.51.100.100".parse().unwrap();
|
|
||||||
|
|
||||||
record_handshake_failure_class(&beobachten, &config, peer_ip, &eof_err);
|
|
||||||
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
assert!(snapshot.contains("[expected_64_got_0]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn adversarial_tls_handshake_timeout_during_masking_delay() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = false;
|
|
||||||
cfg.timeouts.client_handshake = 1;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.server_hello_delay_min_ms = 3000;
|
|
||||||
cfg.censorship.server_hello_delay_max_ms = 3000;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.1:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0xFF, 0xFF])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(4), handle)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ProxyError::TgHandshakeTimeout)));
|
|
||||||
assert_eq!(stats.get_handshake_timeouts(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn blackhat_proxy_protocol_slowloris_timeout() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.server.proxy_protocol_header_timeout_ms = 200;
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.2:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(b"PROXY TCP4 192.").await.unwrap();
|
|
||||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(2), handle)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ProxyError::InvalidProxyProtocol)));
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blackhat_ipv4_mapped_ipv6_proxy_source_bypass_attempt() {
|
|
||||||
let trusted = vec!["192.0.2.0/24".parse().unwrap()];
|
|
||||||
let peer_ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201));
|
|
||||||
assert!(!is_trusted_proxy_source(peer_ip, &trusted));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn negative_proxy_protocol_enabled_but_client_sends_tls_hello() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.server.proxy_protocol_header_timeout_ms = 500;
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.3:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x02, 0x00])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(2), handle)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ProxyError::InvalidProxyProtocol)));
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn edge_client_stream_exactly_4_bytes_eof() {
|
|
||||||
let config = Arc::new(ProxyConfig::default());
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.4:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handle).await;
|
|
||||||
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
assert!(snapshot.contains("[expected_64_got_0]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn edge_client_stream_tls_header_valid_but_body_1_byte_short_eof() {
|
|
||||||
let config = Arc::new(ProxyConfig::default());
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.5:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00, 100])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.write_all(&vec![0x41; 99]).await.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handle).await;
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn integration_non_tls_modes_disabled_immediately_masks() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.modes.classic = false;
|
|
||||||
cfg.general.modes.secure = false;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handle = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.6:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(b"GET / HTTP/1.1\r\n").await.unwrap();
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handle).await;
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct YieldingReader {
|
|
||||||
data: Vec<u8>,
|
|
||||||
pos: usize,
|
|
||||||
yields_left: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsyncRead for YieldingReader {
|
|
||||||
fn poll_read(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
buf: &mut ReadBuf<'_>,
|
|
||||||
) -> Poll<std::io::Result<()>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
if this.yields_left > 0 {
|
|
||||||
this.yields_left -= 1;
|
|
||||||
cx.waker().wake_by_ref();
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
if this.pos >= this.data.len() {
|
|
||||||
return Poll::Ready(Ok(()));
|
|
||||||
}
|
|
||||||
buf.put_slice(&this.data[this.pos..this.pos + 1]);
|
|
||||||
this.pos += 1;
|
|
||||||
this.yields_left = 2;
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn fuzz_read_with_progress_heavy_yielding() {
|
|
||||||
let expected_data = b"HEAVY_YIELD_TEST_DATA".to_vec();
|
|
||||||
let mut reader = YieldingReader {
|
|
||||||
data: expected_data.clone(),
|
|
||||||
pos: 0,
|
|
||||||
yields_left: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = vec![0u8; expected_data.len()];
|
|
||||||
let read_bytes = read_with_progress(&mut reader, &mut buf).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(read_bytes, expected_data.len());
|
|
||||||
assert_eq!(buf, expected_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_wrap_tls_application_record_exactly_u16_max() {
|
|
||||||
let payload = vec![0u8; 65535];
|
|
||||||
let wrapped = wrap_tls_application_record(&payload);
|
|
||||||
assert_eq!(wrapped.len(), 65540);
|
|
||||||
assert_eq!(wrapped[0], TLS_RECORD_APPLICATION);
|
|
||||||
assert_eq!(&wrapped[3..5], &65535u16.to_be_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fuzz_wrap_tls_application_record_lengths() {
|
|
||||||
let lengths = [0, 1, 65534, 65535, 65536, 131070, 131071, 131072];
|
|
||||||
for len in lengths {
|
|
||||||
let payload = vec![0u8; len];
|
|
||||||
let wrapped = wrap_tls_application_record(&payload);
|
|
||||||
let expected_chunks = len.div_ceil(65535).max(1);
|
|
||||||
assert_eq!(wrapped.len(), len + 5 * expected_chunks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn stress_user_connection_reservation_concurrent_same_ip_exhaustion() {
|
|
||||||
let user = "stress-same-ip-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.access.user_max_tcp_conns.insert(user.to_string(), 5);
|
|
||||||
|
|
||||||
let config = Arc::new(config);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
ip_tracker.set_user_limit(user, 10).await;
|
|
||||||
|
|
||||||
let peer = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 77)), 55000);
|
|
||||||
|
|
||||||
let mut tasks = tokio::task::JoinSet::new();
|
|
||||||
let mut reservations = Vec::new();
|
|
||||||
|
|
||||||
for _ in 0..10 {
|
|
||||||
let config = config.clone();
|
|
||||||
let stats = stats.clone();
|
|
||||||
let ip_tracker = ip_tracker.clone();
|
|
||||||
tasks.spawn(async move {
|
|
||||||
RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user, &config, stats, peer, ip_tracker,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut successes = 0;
|
|
||||||
let mut failures = 0;
|
|
||||||
|
|
||||||
while let Some(res) = tasks.join_next().await {
|
|
||||||
match res.unwrap() {
|
|
||||||
Ok(r) => {
|
|
||||||
successes += 1;
|
|
||||||
reservations.push(r);
|
|
||||||
}
|
|
||||||
Err(_) => failures += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(successes, 5);
|
|
||||||
assert_eq!(failures, 5);
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 5);
|
|
||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 1);
|
|
||||||
|
|
||||||
for reservation in reservations {
|
|
||||||
reservation.release().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 0);
|
|
||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::ProxyConfig;
|
|
||||||
use crate::protocol::constants::MIN_TLS_CLIENT_HELLO_SIZE;
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
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]
|
|
||||||
fn invariant_wrap_tls_application_record_exact_multiples() {
|
|
||||||
let chunk_size = u16::MAX as usize;
|
|
||||||
let payload = vec![0xAA; chunk_size * 2];
|
|
||||||
|
|
||||||
let wrapped = wrap_tls_application_record(&payload);
|
|
||||||
|
|
||||||
assert_eq!(wrapped.len(), 2 * (5 + chunk_size));
|
|
||||||
assert_eq!(wrapped[0], TLS_RECORD_APPLICATION);
|
|
||||||
assert_eq!(&wrapped[3..5], &65535u16.to_be_bytes());
|
|
||||||
|
|
||||||
let second_header_idx = 5 + chunk_size;
|
|
||||||
assert_eq!(wrapped[second_header_idx], TLS_RECORD_APPLICATION);
|
|
||||||
assert_eq!(
|
|
||||||
&wrapped[second_header_idx + 3..second_header_idx + 5],
|
|
||||||
&65535u16.to_be_bytes()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn invariant_tls_clienthello_truncation_exact_boundary_triggers_masking() {
|
|
||||||
let config = Arc::new(ProxyConfig::default());
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.20:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
let claimed_len = MIN_TLS_CLIENT_HELLO_SIZE as u16;
|
|
||||||
let mut header = vec![0x16, 0x03, 0x01];
|
|
||||||
header.extend_from_slice(&claimed_len.to_be_bytes());
|
|
||||||
|
|
||||||
client_side.write_all(&header).await.unwrap();
|
|
||||||
client_side
|
|
||||||
.write_all(&vec![0x42; MIN_TLS_CLIENT_HELLO_SIZE - 1])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn invariant_acquire_reservation_ip_limit_rollback() {
|
|
||||||
let user = "rollback-test-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config
|
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 10);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
ip_tracker.set_user_limit(user, 1).await;
|
|
||||||
|
|
||||||
let peer_a = "198.51.100.21:55000".parse().unwrap();
|
|
||||||
let _res_a = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user,
|
|
||||||
&config,
|
|
||||||
stats.clone(),
|
|
||||||
peer_a,
|
|
||||||
ip_tracker.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 1);
|
|
||||||
|
|
||||||
let peer_b = "203.0.113.22:55000".parse().unwrap();
|
|
||||||
let res_b = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user,
|
|
||||||
&config,
|
|
||||||
stats.clone(),
|
|
||||||
peer_b,
|
|
||||||
ip_tracker.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
res_b,
|
|
||||||
Err(ProxyError::ConnectionLimitExceeded { .. })
|
|
||||||
));
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn invariant_quota_exact_boundary_inclusive() {
|
|
||||||
let user = "quota-strict-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.access.user_data_quota.insert(user.to_string(), 1000);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
let peer = "198.51.100.23:55000".parse().unwrap();
|
|
||||||
|
|
||||||
preload_user_quota(stats.as_ref(), user, 999);
|
|
||||||
let res1 = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user,
|
|
||||||
&config,
|
|
||||||
stats.clone(),
|
|
||||||
peer,
|
|
||||||
ip_tracker.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(res1.is_ok());
|
|
||||||
res1.unwrap().release().await;
|
|
||||||
|
|
||||||
preload_user_quota(stats.as_ref(), user, 1);
|
|
||||||
let res2 = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user,
|
|
||||||
&config,
|
|
||||||
stats.clone(),
|
|
||||||
peer,
|
|
||||||
ip_tracker.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
assert!(matches!(res2, Err(ProxyError::DataQuotaExceeded { .. })));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn invariant_direct_mode_partial_header_eof_is_error_not_bad_connect() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = true;
|
|
||||||
cfg.general.beobachten_minutes = 1;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.25:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(&[0xEF, 0xEF, 0xEF]).await.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(2), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert_eq!(stats.get_connects_bad(), 0);
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
assert!(snapshot.contains("[expected_64_got_0]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn invariant_route_mode_snapshot_picks_up_latest_mode() {
|
|
||||||
let route_runtime = Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct));
|
|
||||||
assert!(matches!(
|
|
||||||
route_runtime.snapshot().mode,
|
|
||||||
RelayRouteMode::Direct
|
|
||||||
));
|
|
||||||
|
|
||||||
route_runtime.set_mode(RelayRouteMode::Middle);
|
|
||||||
assert!(matches!(
|
|
||||||
route_runtime.snapshot().mode,
|
|
||||||
RelayRouteMode::Middle
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,17 @@ use super::*;
|
|||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{
|
use crate::protocol::constants::{
|
||||||
HANDSHAKE_LEN, MAX_TLS_PLAINTEXT_SIZE, MIN_TLS_CLIENT_HELLO_SIZE, TLS_RECORD_APPLICATION,
|
HANDSHAKE_LEN,
|
||||||
|
MAX_TLS_PLAINTEXT_SIZE,
|
||||||
|
MIN_TLS_CLIENT_HELLO_SIZE,
|
||||||
|
TLS_RECORD_APPLICATION,
|
||||||
TLS_VERSION,
|
TLS_VERSION,
|
||||||
};
|
};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -40,7 +43,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -77,10 +79,7 @@ fn build_mask_harness(secret_hex: &str, mask_port: u16) -> CampaignHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
||||||
assert!(
|
assert!(tls_len <= u16::MAX as usize, "TLS length must fit into record header");
|
||||||
tls_len <= u16::MAX as usize,
|
|
||||||
"TLS length must fit into record header"
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_len = 5 + tls_len;
|
let total_len = 5 + tls_len;
|
||||||
let mut handshake = vec![fill; total_len];
|
let mut handshake = vec![fill; total_len];
|
||||||
@@ -172,10 +171,7 @@ async fn run_tls_success_mtproto_fail_capture(
|
|||||||
client_side.write_all(&client_hello).await.unwrap();
|
client_side.write_all(&client_hello).await.unwrap();
|
||||||
|
|
||||||
let mut tls_response_head = [0u8; 5];
|
let mut tls_response_head = [0u8; 5];
|
||||||
client_side
|
client_side.read_exact(&mut tls_response_head).await.unwrap();
|
||||||
.read_exact(&mut tls_response_head)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(tls_response_head[0], 0x16);
|
assert_eq!(tls_response_head[0], 0x16);
|
||||||
read_and_discard_tls_record_body(&mut client_side, tls_response_head).await;
|
read_and_discard_tls_record_body(&mut client_side, tls_response_head).await;
|
||||||
|
|
||||||
@@ -431,10 +427,7 @@ async fn blackhat_campaign_06_replayed_tls_hello_is_masked_without_serverhello()
|
|||||||
client_side.read_exact(&mut head).await.unwrap();
|
client_side.read_exact(&mut head).await.unwrap();
|
||||||
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(&first_tail).await.unwrap();
|
client_side.write_all(&first_tail).await.unwrap();
|
||||||
} else {
|
} else {
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
@@ -704,15 +697,13 @@ async fn blackhat_campaign_12_parallel_tls_success_mtproto_fail_sessions_keep_is
|
|||||||
|
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for i in 0..sessions {
|
for i in 0..sessions {
|
||||||
let mut harness =
|
let mut harness = build_mask_harness("abababababababababababababababab", backend_addr.port());
|
||||||
build_mask_harness("abababababababababababababababab", backend_addr.port());
|
|
||||||
let mut cfg = (*harness.config).clone();
|
let mut cfg = (*harness.config).clone();
|
||||||
cfg.censorship.mask_port = backend_addr.port();
|
cfg.censorship.mask_port = backend_addr.port();
|
||||||
harness.config = Arc::new(cfg);
|
harness.config = Arc::new(cfg);
|
||||||
tasks.push(tokio::spawn(async move {
|
tasks.push(tokio::spawn(async move {
|
||||||
let secret = [0xABu8; 16];
|
let secret = [0xABu8; 16];
|
||||||
let hello =
|
let hello = make_valid_tls_client_hello(&secret, 100 + i as u32, 600, 0x40 + (i as u8 % 10));
|
||||||
make_valid_tls_client_hello(&secret, 100 + i as u32, 600, 0x40 + (i as u8 % 10));
|
|
||||||
let bad = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
let bad = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
||||||
let tail = wrap_tls_application_data(&vec![i as u8; 8 + i]);
|
let tail = wrap_tls_application_data(&vec![i as u8; 8 + i]);
|
||||||
let (server_side, mut client_side) = duplex(131072);
|
let (server_side, mut client_side) = duplex(131072);
|
||||||
@@ -852,8 +843,8 @@ async fn blackhat_campaign_15_light_fuzz_tls_lengths_and_fragmentation() {
|
|||||||
tls_len = MAX_TLS_PLAINTEXT_SIZE + 1 + (tls_len % 1024);
|
tls_len = MAX_TLS_PLAINTEXT_SIZE + 1 + (tls_len % 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body_to_send =
|
let body_to_send = if (MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&tls_len)
|
||||||
if (MIN_TLS_CLIENT_HELLO_SIZE..=MAX_TLS_PLAINTEXT_SIZE).contains(&tls_len) {
|
{
|
||||||
(seed as usize % 29).min(tls_len.saturating_sub(1))
|
(seed as usize % 29).min(tls_len.saturating_sub(1))
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
@@ -865,9 +856,7 @@ async fn blackhat_campaign_15_light_fuzz_tls_lengths_and_fragmentation() {
|
|||||||
probe[2] = 0x01;
|
probe[2] = 0x01;
|
||||||
probe[3..5].copy_from_slice(&(tls_len as u16).to_be_bytes());
|
probe[3..5].copy_from_slice(&(tls_len as u16).to_be_bytes());
|
||||||
for b in &mut probe[5..] {
|
for b in &mut probe[5..] {
|
||||||
seed = seed
|
seed = seed.wrapping_mul(2862933555777941757).wrapping_add(3037000493);
|
||||||
.wrapping_mul(2862933555777941757)
|
|
||||||
.wrapping_add(3037000493);
|
|
||||||
*b = (seed >> 24) as u8;
|
*b = (seed >> 24) as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,8 +879,7 @@ async fn blackhat_campaign_16_mixed_probe_burst_stress_finishes_without_panics()
|
|||||||
probe[2] = 0x01;
|
probe[2] = 0x01;
|
||||||
probe[3..5].copy_from_slice(&600u16.to_be_bytes());
|
probe[3..5].copy_from_slice(&600u16.to_be_bytes());
|
||||||
probe[5..].fill((0x90 + i as u8) ^ 0x5A);
|
probe[5..].fill((0x90 + i as u8) ^ 0x5A);
|
||||||
run_invalid_tls_capture(Arc::new(ProxyConfig::default()), probe.clone(), probe)
|
run_invalid_tls_capture(Arc::new(ProxyConfig::default()), probe.clone(), probe).await;
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
let hdr = vec![0x16, 0x03, 0x01, 0xFF, i as u8];
|
let hdr = vec![0x16, 0x03, 0x01, 0xFF, i as u8];
|
||||||
run_invalid_tls_capture(Arc::new(ProxyConfig::default()), hdr.clone(), hdr).await;
|
run_invalid_tls_capture(Arc::new(ProxyConfig::default()), hdr.clone(), hdr).await;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::config::{UpstreamConfig, UpstreamType};
|
|||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -36,7 +36,6 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
@@ -56,10 +55,7 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
||||||
assert!(
|
assert!(tls_len <= u16::MAX as usize, "TLS length must fit into record header");
|
||||||
tls_len <= u16::MAX as usize,
|
|
||||||
"TLS length must fit into record header"
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_len = 5 + tls_len;
|
let total_len = 5 + tls_len;
|
||||||
let mut handshake = vec![fill; total_len];
|
let mut handshake = vec![fill; total_len];
|
||||||
@@ -154,10 +150,7 @@ async fn masking_runs_outside_handshake_timeout_budget_with_high_reject_delay()
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(result.is_ok(), "bad-client fallback must not be canceled by handshake timeout");
|
||||||
result.is_ok(),
|
|
||||||
"bad-client fallback must not be canceled by handshake timeout"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stats.get_handshake_timeouts(),
|
stats.get_handshake_timeouts(),
|
||||||
0,
|
0,
|
||||||
@@ -182,10 +175,10 @@ async fn tls_mtproto_bad_client_does_not_reinject_clienthello_into_mask_backend(
|
|||||||
config.censorship.mask_port = backend_addr.port();
|
config.censorship.mask_port = backend_addr.port();
|
||||||
config.censorship.mask_proxy_protocol = 0;
|
config.censorship.mask_proxy_protocol = 0;
|
||||||
config.access.ignore_time_skew = true;
|
config.access.ignore_time_skew = true;
|
||||||
config.access.users.insert(
|
config
|
||||||
"user".to_string(),
|
.access
|
||||||
"d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0".to_string(),
|
.users
|
||||||
);
|
.insert("user".to_string(), "d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0".to_string());
|
||||||
|
|
||||||
let harness = build_harness(config);
|
let harness = build_harness(config);
|
||||||
|
|
||||||
@@ -201,7 +194,8 @@ async fn tls_mtproto_bad_client_does_not_reinject_clienthello_into_mask_backend(
|
|||||||
let mut got = vec![0u8; expected_trailing.len()];
|
let mut got = vec![0u8; expected_trailing.len()];
|
||||||
stream.read_exact(&mut got).await.unwrap();
|
stream.read_exact(&mut got).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got, expected_trailing,
|
got,
|
||||||
|
expected_trailing,
|
||||||
"mask backend must receive only post-handshake trailing TLS records"
|
"mask backend must receive only post-handshake trailing TLS records"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -229,17 +223,11 @@ async fn tls_mtproto_bad_client_does_not_reinject_clienthello_into_mask_backend(
|
|||||||
client_side.write_all(&client_hello).await.unwrap();
|
client_side.write_all(&client_hello).await.unwrap();
|
||||||
|
|
||||||
let mut tls_response_head = [0u8; 5];
|
let mut tls_response_head = [0u8; 5];
|
||||||
client_side
|
client_side.read_exact(&mut tls_response_head).await.unwrap();
|
||||||
.read_exact(&mut tls_response_head)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(tls_response_head[0], 0x16);
|
assert_eq!(tls_response_head[0], 0x16);
|
||||||
read_and_discard_tls_record_body(&mut client_side, tls_response_head).await;
|
read_and_discard_tls_record_body(&mut client_side, tls_response_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();
|
||||||
|
|
||||||
tokio::time::timeout(Duration::from_secs(3), accept_task)
|
tokio::time::timeout(Duration::from_secs(3), accept_task)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -164,36 +163,21 @@ async fn diagnostic_timing_profiles_are_within_realistic_guardrails() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(p50 >= 650, "p50 too low for delayed reject class={}", class);
|
assert!(p50 >= 650, "p50 too low for delayed reject class={}", class);
|
||||||
assert!(
|
assert!(p95 <= 1200, "p95 too high for delayed reject class={}", class);
|
||||||
p95 <= 1200,
|
assert!(max <= 1500, "max too high for delayed reject class={}", class);
|
||||||
"p95 too high for delayed reject class={}",
|
|
||||||
class
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
max <= 1500,
|
|
||||||
"max too high for delayed reject class={}",
|
|
||||||
class
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn diagnostic_forwarded_size_profiles_by_probe_class() {
|
async fn diagnostic_forwarded_size_profiles_by_probe_class() {
|
||||||
let classes = [
|
let classes = [0usize, 1usize, 7usize, 17usize, 63usize, 511usize, 1023usize, 2047usize];
|
||||||
0usize, 1usize, 7usize, 17usize, 63usize, 511usize, 1023usize, 2047usize,
|
|
||||||
];
|
|
||||||
let mut observed = Vec::new();
|
let mut observed = Vec::new();
|
||||||
|
|
||||||
for class in classes {
|
for class in classes {
|
||||||
let len = capture_forwarded_len(class).await;
|
let len = capture_forwarded_len(class).await;
|
||||||
println!("diagnostic_shape class={} forwarded_len={}", class, len);
|
println!("diagnostic_shape class={} forwarded_len={}", class, len);
|
||||||
observed.push(len as u128);
|
observed.push(len as u128);
|
||||||
assert_eq!(
|
assert_eq!(len, 5 + class, "unexpected forwarded len for class={}", class);
|
||||||
len,
|
|
||||||
5 + class,
|
|
||||||
"unexpected forwarded len for class={}",
|
|
||||||
class
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let p50 = percentile_ms(observed.clone(), 50, 100);
|
let p50 = percentile_ms(observed.clone(), 50, 100);
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tokio::time::Duration;
|
|
||||||
|
|
||||||
fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![UpstreamConfig {
|
|
||||||
upstream_type: UpstreamType::Direct {
|
|
||||||
interface: None,
|
|
||||||
bind_addresses: None,
|
|
||||||
},
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
scopes: String::new(),
|
|
||||||
selected_scope: String::new(),
|
|
||||||
}],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn fragmented_connect_probe_is_classified_as_http_via_prefetch_window() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let accept_task = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
|
||||||
let mut got = Vec::new();
|
|
||||||
stream.read_to_end(&mut got).await.unwrap();
|
|
||||||
got
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = true;
|
|
||||||
cfg.general.beobachten_minutes = 1;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.mask_unix_sock = None;
|
|
||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
cfg.censorship.mask_port = backend_addr.port();
|
|
||||||
cfg.general.modes.classic = false;
|
|
||||||
cfg.general.modes.secure = false;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let peer: SocketAddr = "198.51.100.251:57501".parse().unwrap();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
peer,
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
new_upstream_manager(stats),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(b"CONNE").await.unwrap();
|
|
||||||
client_side
|
|
||||||
.write_all(b"CT example.org:443 HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let forwarded = tokio::time::timeout(Duration::from_secs(3), accept_task)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
forwarded.starts_with(b"CONNECT example.org:443 HTTP/1.1"),
|
|
||||||
"mask backend must receive the full fragmented CONNECT probe"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(3), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
assert!(snapshot.contains("[HTTP]"));
|
|
||||||
assert!(snapshot.contains("198.51.100.251-1"));
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ use crate::config::{UpstreamConfig, UpstreamType};
|
|||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_RECORD_APPLICATION, TLS_VERSION};
|
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_RECORD_APPLICATION, TLS_VERSION};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -71,10 +70,7 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> Harness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
||||||
assert!(
|
assert!(tls_len <= u16::MAX as usize, "TLS length must fit into record header");
|
||||||
tls_len <= u16::MAX as usize,
|
|
||||||
"TLS length must fit into record header"
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_len = 5 + tls_len;
|
let total_len = 5 + tls_len;
|
||||||
let mut handshake = vec![fill; total_len];
|
let mut handshake = vec![fill; total_len];
|
||||||
@@ -162,17 +158,11 @@ async fn run_tls_success_mtproto_fail_capture(
|
|||||||
client_side.write_all(&client_hello).await.unwrap();
|
client_side.write_all(&client_hello).await.unwrap();
|
||||||
|
|
||||||
let mut tls_response_head = [0u8; 5];
|
let mut tls_response_head = [0u8; 5];
|
||||||
client_side
|
client_side.read_exact(&mut tls_response_head).await.unwrap();
|
||||||
.read_exact(&mut tls_response_head)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(tls_response_head[0], 0x16);
|
assert_eq!(tls_response_head[0], 0x16);
|
||||||
read_tls_record_body(&mut client_side, tls_response_head).await;
|
read_tls_record_body(&mut client_side, tls_response_head).await;
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&invalid_mtproto_record).await.unwrap();
|
||||||
.write_all(&invalid_mtproto_record)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
for record in trailing_records {
|
for record in trailing_records {
|
||||||
client_side.write_all(&record).await.unwrap();
|
client_side.write_all(&record).await.unwrap();
|
||||||
}
|
}
|
||||||
@@ -340,10 +330,7 @@ async fn replayed_tls_hello_gets_no_serverhello_and_is_masked() {
|
|||||||
client_side.read_exact(&mut head).await.unwrap();
|
client_side.read_exact(&mut head).await.unwrap();
|
||||||
assert_eq!(head[0], 0x16);
|
assert_eq!(head[0], 0x16);
|
||||||
read_tls_record_body(&mut client_side, head).await;
|
read_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(&first_tail).await.unwrap();
|
client_side.write_all(&first_tail).await.unwrap();
|
||||||
} else {
|
} else {
|
||||||
let mut one = [0u8; 1];
|
let mut one = [0u8; 1];
|
||||||
@@ -415,10 +402,7 @@ async fn connects_bad_increments_once_per_invalid_mtproto() {
|
|||||||
let mut head = [0u8; 5];
|
let mut head = [0u8; 5];
|
||||||
client_side.read_exact(&mut head).await.unwrap();
|
client_side.read_exact(&mut head).await.unwrap();
|
||||||
read_tls_record_body(&mut client_side, head).await;
|
read_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(&tail).await.unwrap();
|
client_side.write_all(&tail).await.unwrap();
|
||||||
|
|
||||||
tokio::time::timeout(Duration::from_secs(3), accept_task)
|
tokio::time::timeout(Duration::from_secs(3), accept_task)
|
||||||
@@ -641,8 +625,7 @@ async fn concurrent_tls_mtproto_fail_sessions_are_isolated() {
|
|||||||
for idx in 0..sessions {
|
for idx in 0..sessions {
|
||||||
let secret_hex = "c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4";
|
let secret_hex = "c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4";
|
||||||
let harness = build_harness(secret_hex, backend_addr.port());
|
let harness = build_harness(secret_hex, backend_addr.port());
|
||||||
let hello =
|
let hello = make_valid_tls_client_hello(&[0xC4; 16], 20 + idx as u32, 600, 0x40 + idx as u8);
|
||||||
make_valid_tls_client_hello(&[0xC4; 16], 20 + idx as u32, 600, 0x40 + idx as u8);
|
|
||||||
let invalid_mtproto = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
let invalid_mtproto = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
||||||
let trailing = wrap_tls_application_data(&vec![idx as u8; 32 + idx]);
|
let trailing = wrap_tls_application_data(&vec![idx as u8; 32 + idx]);
|
||||||
let peer: SocketAddr = format!("198.51.100.217:{}", 56100 + idx as u16)
|
let peer: SocketAddr = format!("198.51.100.217:{}", 56100 + idx as u16)
|
||||||
@@ -702,67 +685,17 @@ macro_rules! tail_length_case {
|
|||||||
*b = (i as u8).wrapping_mul(17).wrapping_add(5);
|
*b = (i as u8).wrapping_mul(17).wrapping_add(5);
|
||||||
}
|
}
|
||||||
let record = wrap_tls_application_data(&payload);
|
let record = wrap_tls_application_data(&payload);
|
||||||
let got =
|
let got = run_tls_success_mtproto_fail_capture($hex, $secret, $ts, vec![record.clone()]).await;
|
||||||
run_tls_success_mtproto_fail_capture($hex, $secret, $ts, vec![record.clone()])
|
|
||||||
.await;
|
|
||||||
assert_eq!(got, record);
|
assert_eq!(got, record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
tail_length_case!(
|
tail_length_case!(tail_len_1_preserved, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", [0xD1; 16], 30, 1);
|
||||||
tail_len_1_preserved,
|
tail_length_case!(tail_len_2_preserved, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", [0xD2; 16], 31, 2);
|
||||||
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
|
tail_length_case!(tail_len_3_preserved, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", [0xD3; 16], 32, 3);
|
||||||
[0xD1; 16],
|
tail_length_case!(tail_len_7_preserved, "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", [0xD4; 16], 33, 7);
|
||||||
30,
|
tail_length_case!(tail_len_31_preserved, "d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5", [0xD5; 16], 34, 31);
|
||||||
1
|
tail_length_case!(tail_len_127_preserved, "d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6", [0xD6; 16], 35, 127);
|
||||||
);
|
tail_length_case!(tail_len_511_preserved, "d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7", [0xD7; 16], 36, 511);
|
||||||
tail_length_case!(
|
tail_length_case!(tail_len_1023_preserved, "d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8", [0xD8; 16], 37, 1023);
|
||||||
tail_len_2_preserved,
|
|
||||||
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
|
|
||||||
[0xD2; 16],
|
|
||||||
31,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_3_preserved,
|
|
||||||
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
|
|
||||||
[0xD3; 16],
|
|
||||||
32,
|
|
||||||
3
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_7_preserved,
|
|
||||||
"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
|
|
||||||
[0xD4; 16],
|
|
||||||
33,
|
|
||||||
7
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_31_preserved,
|
|
||||||
"d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5d5",
|
|
||||||
[0xD5; 16],
|
|
||||||
34,
|
|
||||||
31
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_127_preserved,
|
|
||||||
"d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6",
|
|
||||||
[0xD6; 16],
|
|
||||||
35,
|
|
||||||
127
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_511_preserved,
|
|
||||||
"d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7",
|
|
||||||
[0xD7; 16],
|
|
||||||
36,
|
|
||||||
511
|
|
||||||
);
|
|
||||||
tail_length_case!(
|
|
||||||
tail_len_1023_preserved,
|
|
||||||
"d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8d8",
|
|
||||||
[0xD8; 16],
|
|
||||||
37,
|
|
||||||
1023
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tokio::time::{Duration, sleep};
|
|
||||||
|
|
||||||
fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![UpstreamConfig {
|
|
||||||
upstream_type: UpstreamType::Direct {
|
|
||||||
interface: None,
|
|
||||||
bind_addresses: None,
|
|
||||||
},
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
scopes: String::new(),
|
|
||||||
selected_scope: String::new(),
|
|
||||||
}],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_http2_fragment_case(split_at: usize, delay_ms: u64, peer: SocketAddr) {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
|
||||||
let preface = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".to_vec();
|
|
||||||
|
|
||||||
let accept_task = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
|
||||||
let mut got = Vec::new();
|
|
||||||
stream.read_to_end(&mut got).await.unwrap();
|
|
||||||
got
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = true;
|
|
||||||
cfg.general.beobachten_minutes = 1;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.mask_unix_sock = None;
|
|
||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
cfg.censorship.mask_port = backend_addr.port();
|
|
||||||
cfg.general.modes.classic = false;
|
|
||||||
cfg.general.modes.secure = false;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
peer,
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
new_upstream_manager(stats),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
let first = split_at.min(preface.len());
|
|
||||||
client_side.write_all(&preface[..first]).await.unwrap();
|
|
||||||
if first < preface.len() {
|
|
||||||
sleep(Duration::from_millis(delay_ms)).await;
|
|
||||||
client_side.write_all(&preface[first..]).await.unwrap();
|
|
||||||
}
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let forwarded = tokio::time::timeout(Duration::from_secs(3), accept_task)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
forwarded.starts_with(&preface),
|
|
||||||
"mask backend must receive an intact HTTP/2 preface prefix"
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(3), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
assert!(snapshot.contains("[HTTP]"));
|
|
||||||
assert!(snapshot.contains(&format!("{}-1", peer.ip())));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn http2_preface_fragmentation_matrix_is_classified_and_forwarded() {
|
|
||||||
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() {
|
|
||||||
let peer: SocketAddr = format!("198.51.100.{}:58{}", 140 + i, 100 + i)
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
run_http2_fragment_case(split_at, delay_ms, peer).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn http2_preface_splitpoint_light_fuzz_classifies_http() {
|
|
||||||
for split_at in 2usize..=12 {
|
|
||||||
let delay_ms = if split_at % 3 == 0 { 7 } else { 1 };
|
|
||||||
let peer: SocketAddr = format!("198.51.101.{}:59{}", split_at, 10 + split_at)
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
run_http2_fragment_case(split_at, delay_ms, peer).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-151
@@ -1,151 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
use tokio::time::{Duration, sleep};
|
|
||||||
|
|
||||||
fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![UpstreamConfig {
|
|
||||||
upstream_type: UpstreamType::Direct {
|
|
||||||
interface: None,
|
|
||||||
bind_addresses: None,
|
|
||||||
},
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
scopes: String::new(),
|
|
||||||
selected_scope: String::new(),
|
|
||||||
}],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_pipeline_prefetch_case(
|
|
||||||
prefetch_timeout_ms: u64,
|
|
||||||
delayed_tail_ms: u64,
|
|
||||||
peer: SocketAddr,
|
|
||||||
) -> (Vec<u8>, String) {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let accept_task = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
|
||||||
let mut got = Vec::new();
|
|
||||||
stream.read_to_end(&mut got).await.unwrap();
|
|
||||||
got
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = true;
|
|
||||||
cfg.general.beobachten_minutes = 1;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.mask_unix_sock = None;
|
|
||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
cfg.censorship.mask_port = backend_addr.port();
|
|
||||||
cfg.censorship.mask_classifier_prefetch_timeout_ms = prefetch_timeout_ms;
|
|
||||||
cfg.general.modes.classic = false;
|
|
||||||
cfg.general.modes.secure = false;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
peer,
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
new_upstream_manager(stats),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(b"C").await.unwrap();
|
|
||||||
sleep(Duration::from_millis(delayed_tail_ms)).await;
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let forwarded = tokio::time::timeout(Duration::from_secs(3), accept_task)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(3), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(result.is_ok());
|
|
||||||
|
|
||||||
let snapshot = beobachten.snapshot_text(Duration::from_secs(60));
|
|
||||||
(forwarded, snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tdd_pipeline_prefetch_5ms_misses_15ms_tail_and_classifies_as_port_scanner() {
|
|
||||||
let peer: SocketAddr = "198.51.100.171:58071".parse().unwrap();
|
|
||||||
let (forwarded, snapshot) = run_pipeline_prefetch_case(5, 15, peer).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
forwarded.starts_with(b"CONNECT"),
|
|
||||||
"mask backend must still receive full payload bytes in-order"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
snapshot.contains("[HTTP]") || snapshot.contains("[port-scanner]"),
|
|
||||||
"unexpected classifier snapshot for 5ms delayed-tail case: {snapshot}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn tdd_pipeline_prefetch_20ms_recovers_15ms_tail_and_classifies_as_http() {
|
|
||||||
let peer: SocketAddr = "198.51.100.172:58072".parse().unwrap();
|
|
||||||
let (forwarded, snapshot) = run_pipeline_prefetch_case(20, 15, peer).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
forwarded.starts_with(b"CONNECT"),
|
|
||||||
"mask backend must receive full CONNECT payload"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
snapshot.contains("[HTTP]"),
|
|
||||||
"20ms budget should recover delayed fragmented prefix and classify as HTTP"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn matrix_pipeline_prefetch_budget_behavior_5_20_50ms() {
|
|
||||||
let peer5: SocketAddr = "198.51.100.173:58073".parse().unwrap();
|
|
||||||
let peer20: SocketAddr = "198.51.100.174:58074".parse().unwrap();
|
|
||||||
let peer50: SocketAddr = "198.51.100.175:58075".parse().unwrap();
|
|
||||||
|
|
||||||
let (_, snap5) = run_pipeline_prefetch_case(5, 35, peer5).await;
|
|
||||||
let (_, snap20) = run_pipeline_prefetch_case(20, 35, peer20).await;
|
|
||||||
let (_, snap50) = run_pipeline_prefetch_case(50, 35, peer50).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
snap5.contains("[HTTP]") || snap5.contains("[port-scanner]"),
|
|
||||||
"unexpected 5ms snapshot: {snap5}"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
snap20.contains("[HTTP]") || snap20.contains("[port-scanner]"),
|
|
||||||
"unexpected 20ms snapshot: {snap20}"
|
|
||||||
);
|
|
||||||
assert!(snap50.contains("[HTTP]"));
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use tokio::io::{AsyncWriteExt, duplex};
|
|
||||||
use tokio::time::{Duration, sleep};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn prefetch_timeout_budget_reads_from_config() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
assert_eq!(
|
|
||||||
mask_classifier_prefetch_timeout(&cfg),
|
|
||||||
Duration::from_millis(5),
|
|
||||||
"default prefetch timeout budget must remain 5ms"
|
|
||||||
);
|
|
||||||
|
|
||||||
cfg.censorship.mask_classifier_prefetch_timeout_ms = 20;
|
|
||||||
assert_eq!(
|
|
||||||
mask_classifier_prefetch_timeout(&cfg),
|
|
||||||
Duration::from_millis(20),
|
|
||||||
"runtime prefetch timeout budget must follow configured value"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn configured_prefetch_budget_20ms_recovers_tail_delayed_15ms() {
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
|
|
||||||
let writer_task = tokio::spawn(async move {
|
|
||||||
sleep(Duration::from_millis(15)).await;
|
|
||||||
writer
|
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await
|
|
||||||
.expect("tail bytes must be writable");
|
|
||||||
writer
|
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
|
||||||
extend_masking_initial_window_with_timeout(
|
|
||||||
&mut reader,
|
|
||||||
&mut initial_data,
|
|
||||||
Duration::from_millis(20),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
writer_task
|
|
||||||
.await
|
|
||||||
.expect("writer task must not panic in runtime timeout test");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
initial_data.starts_with(b"CONNECT"),
|
|
||||||
"20ms configured prefetch budget should recover 15ms delayed CONNECT tail"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn configured_prefetch_budget_5ms_misses_tail_delayed_15ms() {
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
|
|
||||||
let writer_task = tokio::spawn(async move {
|
|
||||||
sleep(Duration::from_millis(15)).await;
|
|
||||||
writer
|
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await
|
|
||||||
.expect("tail bytes must be writable");
|
|
||||||
writer
|
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
|
||||||
extend_masking_initial_window_with_timeout(
|
|
||||||
&mut reader,
|
|
||||||
&mut initial_data,
|
|
||||||
Duration::from_millis(5),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
writer_task
|
|
||||||
.await
|
|
||||||
.expect("writer task must not panic in runtime timeout test");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!initial_data.starts_with(b"CONNECT"),
|
|
||||||
"5ms configured prefetch budget should miss 15ms delayed CONNECT tail"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
|
||||||
use crate::crypto::sha256_hmac;
|
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
|
||||||
use crate::protocol::tls;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
struct PipelineHarness {
|
|
||||||
config: Arc<ProxyConfig>,
|
|
||||||
stats: Arc<Stats>,
|
|
||||||
upstream_manager: Arc<UpstreamManager>,
|
|
||||||
replay_checker: Arc<ReplayChecker>,
|
|
||||||
buffer_pool: Arc<BufferPool>,
|
|
||||||
rng: Arc<SecureRandom>,
|
|
||||||
route_runtime: Arc<RouteRuntimeController>,
|
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
|
||||||
beobachten: Arc<BeobachtenStore>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = false;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.mask_unix_sock = None;
|
|
||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
cfg.censorship.mask_port = mask_port;
|
|
||||||
cfg.censorship.mask_proxy_protocol = 0;
|
|
||||||
cfg.access.ignore_time_skew = true;
|
|
||||||
cfg.access
|
|
||||||
.users
|
|
||||||
.insert("user".to_string(), secret_hex.to_string());
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let upstream_manager = Arc::new(UpstreamManager::new(
|
|
||||||
vec![UpstreamConfig {
|
|
||||||
upstream_type: UpstreamType::Direct {
|
|
||||||
interface: None,
|
|
||||||
bind_addresses: None,
|
|
||||||
},
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
scopes: String::new(),
|
|
||||||
selected_scope: String::new(),
|
|
||||||
}],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
PipelineHarness {
|
|
||||||
config,
|
|
||||||
stats,
|
|
||||||
upstream_manager,
|
|
||||||
replay_checker: Arc::new(ReplayChecker::new(256, Duration::from_secs(60))),
|
|
||||||
buffer_pool: Arc::new(BufferPool::new()),
|
|
||||||
rng: Arc::new(SecureRandom::new()),
|
|
||||||
route_runtime: Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
ip_tracker: Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten: Arc::new(BeobachtenStore::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
|
||||||
let total_len = 5 + tls_len;
|
|
||||||
let mut handshake = vec![fill; total_len];
|
|
||||||
|
|
||||||
handshake[0] = 0x16;
|
|
||||||
handshake[1] = 0x03;
|
|
||||||
handshake[2] = 0x01;
|
|
||||||
handshake[3..5].copy_from_slice(&(tls_len as u16).to_be_bytes());
|
|
||||||
|
|
||||||
let session_id_len: usize = 32;
|
|
||||||
handshake[tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN] = session_id_len as u8;
|
|
||||||
|
|
||||||
handshake[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN].fill(0);
|
|
||||||
let computed = sha256_hmac(secret, &handshake);
|
|
||||||
let mut digest = computed;
|
|
||||||
let ts = timestamp.to_le_bytes();
|
|
||||||
for i in 0..4 {
|
|
||||||
digest[28 + i] ^= ts[i];
|
|
||||||
}
|
|
||||||
handshake[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN]
|
|
||||||
.copy_from_slice(&digest);
|
|
||||||
|
|
||||||
handshake
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrap_tls_application_data(payload: &[u8]) -> Vec<u8> {
|
|
||||||
let mut record = Vec::with_capacity(5 + payload.len());
|
|
||||||
record.push(0x17);
|
|
||||||
record.extend_from_slice(&TLS_VERSION);
|
|
||||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
|
||||||
record.extend_from_slice(payload);
|
|
||||||
record
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_and_discard_tls_record_body<T>(stream: &mut T, header: [u8; 5])
|
|
||||||
where
|
|
||||||
T: tokio::io::AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
let len = u16::from_be_bytes([header[3], header[4]]) as usize;
|
|
||||||
let mut body = vec![0u8; len];
|
|
||||||
stream.read_exact(&mut body).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_initial_data_prefetch_gate_is_fail_closed() {
|
|
||||||
assert!(
|
|
||||||
!should_prefetch_mask_classifier_window(&[]),
|
|
||||||
"empty initial_data must not trigger classifier prefetch"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn blackhat_empty_initial_data_prefetch_must_not_consume_fallback_payload() {
|
|
||||||
let payload = b"\x17\x03\x03\x00\x10coalesced-tail-bytes".to_vec();
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
|
|
||||||
writer.write_all(&payload).await.unwrap();
|
|
||||||
writer.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let mut initial_data = Vec::new();
|
|
||||||
extend_masking_initial_window(&mut reader, &mut initial_data).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
initial_data.is_empty(),
|
|
||||||
"empty initial_data must remain empty after prefetch stage"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut remaining = Vec::new();
|
|
||||||
reader.read_to_end(&mut remaining).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
remaining, payload,
|
|
||||||
"prefetch stage must not consume fallback payload when initial_data is empty"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn positive_fragmented_http_prefix_still_prefetches_within_window() {
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
writer
|
|
||||||
.write_all(b"NECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
writer.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let mut initial_data = b"CON".to_vec();
|
|
||||||
extend_masking_initial_window(&mut reader, &mut initial_data).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
initial_data.starts_with(b"CONNECT"),
|
|
||||||
"fragmented HTTP method prefix should still be recoverable by prefetch"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
initial_data.len() <= 16,
|
|
||||||
"prefetch window must remain bounded"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn light_fuzz_empty_initial_data_never_prefetches_any_bytes() {
|
|
||||||
let mut seed = 0xD15C_A11E_2026_0322u64;
|
|
||||||
|
|
||||||
for _ in 0..128 {
|
|
||||||
seed ^= seed << 7;
|
|
||||||
seed ^= seed >> 9;
|
|
||||||
seed ^= seed << 8;
|
|
||||||
|
|
||||||
let len = ((seed & 0x3f) as usize).saturating_add(1);
|
|
||||||
let mut payload = vec![0u8; len];
|
|
||||||
for (idx, byte) in payload.iter_mut().enumerate() {
|
|
||||||
*byte = (seed as u8).wrapping_add(idx as u8).wrapping_mul(17);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
writer.write_all(&payload).await.unwrap();
|
|
||||||
writer.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let mut initial_data = Vec::new();
|
|
||||||
extend_masking_initial_window(&mut reader, &mut initial_data).await;
|
|
||||||
assert!(initial_data.is_empty());
|
|
||||||
|
|
||||||
let mut remaining = Vec::new();
|
|
||||||
reader.read_to_end(&mut remaining).await.unwrap();
|
|
||||||
assert_eq!(remaining, payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn blackhat_integration_empty_initial_data_path_is_byte_exact_and_eof_clean() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let backend_addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let secret = [0xD3u8; 16];
|
|
||||||
let client_hello = make_valid_tls_client_hello(&secret, 411, 600, 0x2B);
|
|
||||||
let mut invalid_payload = vec![0u8; HANDSHAKE_LEN];
|
|
||||||
invalid_payload[0] = 0xFF;
|
|
||||||
let invalid_mtproto_record = wrap_tls_application_data(&invalid_payload);
|
|
||||||
let trailing_record = wrap_tls_application_data(b"empty-prefetch-invariant");
|
|
||||||
let expected = trailing_record.clone();
|
|
||||||
|
|
||||||
let accept_task = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
|
||||||
|
|
||||||
let mut got = vec![0u8; expected.len()];
|
|
||||||
stream.read_exact(&mut got).await.unwrap();
|
|
||||||
assert_eq!(got, expected);
|
|
||||||
|
|
||||||
let mut one = [0u8; 1];
|
|
||||||
let n = stream.read(&mut one).await.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
n, 0,
|
|
||||||
"fallback stream must not append synthetic bytes on empty initial_data path"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let harness = build_harness("d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", backend_addr.port());
|
|
||||||
let (server_side, mut client_side) = duplex(131072);
|
|
||||||
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.245:56145".parse().unwrap(),
|
|
||||||
harness.config,
|
|
||||||
harness.stats,
|
|
||||||
harness.upstream_manager,
|
|
||||||
harness.replay_checker,
|
|
||||||
harness.buffer_pool,
|
|
||||||
harness.rng,
|
|
||||||
None,
|
|
||||||
harness.route_runtime,
|
|
||||||
None,
|
|
||||||
harness.ip_tracker,
|
|
||||||
harness.beobachten,
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(&client_hello).await.unwrap();
|
|
||||||
let mut head = [0u8; 5];
|
|
||||||
client_side.read_exact(&mut head).await.unwrap();
|
|
||||||
assert_eq!(head[0], 0x16);
|
|
||||||
read_and_discard_tls_record_body(&mut client_side, head).await;
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&invalid_mtproto_record)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.write_all(&trailing_record).await.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
tokio::time::timeout(Duration::from_secs(3), accept_task)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(3), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use tokio::io::{AsyncWriteExt, duplex};
|
|
||||||
use tokio::time::{Duration, advance, sleep};
|
|
||||||
|
|
||||||
async fn run_strict_prefetch_case(prefetch_ms: u64, tail_delay_ms: u64) -> Vec<u8> {
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
|
|
||||||
let writer_task = tokio::spawn(async move {
|
|
||||||
sleep(Duration::from_millis(tail_delay_ms)).await;
|
|
||||||
let _ = writer
|
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await;
|
|
||||||
let _ = writer.shutdown().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
|
||||||
let mut prefetch_task = tokio::spawn(async move {
|
|
||||||
extend_masking_initial_window_with_timeout(
|
|
||||||
&mut reader,
|
|
||||||
&mut initial_data,
|
|
||||||
Duration::from_millis(prefetch_ms),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
initial_data
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
|
|
||||||
if tail_delay_ms > 0 {
|
|
||||||
advance(Duration::from_millis(tail_delay_ms)).await;
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if prefetch_ms > tail_delay_ms {
|
|
||||||
advance(Duration::from_millis(prefetch_ms - tail_delay_ms)).await;
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = prefetch_task.await.expect("prefetch task must not panic");
|
|
||||||
writer_task.await.expect("writer task must not panic");
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn strict_prefetch_5ms_misses_15ms_tail() {
|
|
||||||
let got = run_strict_prefetch_case(5, 15).await;
|
|
||||||
assert_eq!(got, b"C".to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn strict_prefetch_20ms_recovers_15ms_tail() {
|
|
||||||
let got = run_strict_prefetch_case(20, 15).await;
|
|
||||||
assert!(got.starts_with(b"CONNECT"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn strict_prefetch_50ms_recovers_35ms_tail() {
|
|
||||||
let got = run_strict_prefetch_case(50, 35).await;
|
|
||||||
assert!(got.starts_with(b"CONNECT"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn strict_prefetch_equal_budget_and_delay_recovers_tail() {
|
|
||||||
let got = run_strict_prefetch_case(20, 20).await;
|
|
||||||
assert!(got.starts_with(b"CONNECT"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
|
||||||
async fn strict_prefetch_one_ms_after_budget_misses_tail() {
|
|
||||||
let got = run_strict_prefetch_case(20, 21).await;
|
|
||||||
assert_eq!(got, b"C".to_vec());
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::time::{Duration, sleep, timeout};
|
|
||||||
|
|
||||||
async fn extend_masking_initial_window_with_budget<R>(
|
|
||||||
reader: &mut R,
|
|
||||||
initial_data: &mut Vec<u8>,
|
|
||||||
prefetch_timeout: Duration,
|
|
||||||
) where
|
|
||||||
R: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
if !should_prefetch_mask_classifier_window(initial_data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let need = 16usize.saturating_sub(initial_data.len());
|
|
||||||
if need == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut extra = [0u8; 16];
|
|
||||||
if let Ok(Ok(n)) = timeout(prefetch_timeout, reader.read(&mut extra[..need])).await
|
|
||||||
&& n > 0
|
|
||||||
{
|
|
||||||
initial_data.extend_from_slice(&extra[..n]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_prefetch_budget_case(prefetch_budget_ms: u64, delayed_tail_ms: u64) -> bool {
|
|
||||||
let (mut reader, mut writer) = duplex(1024);
|
|
||||||
|
|
||||||
let writer_task = tokio::spawn(async move {
|
|
||||||
sleep(Duration::from_millis(delayed_tail_ms)).await;
|
|
||||||
writer
|
|
||||||
.write_all(b"ONNECT example.org:443 HTTP/1.1\r\n")
|
|
||||||
.await
|
|
||||||
.expect("tail bytes must be writable");
|
|
||||||
writer
|
|
||||||
.shutdown()
|
|
||||||
.await
|
|
||||||
.expect("writer shutdown must succeed");
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut initial_data = b"C".to_vec();
|
|
||||||
extend_masking_initial_window_with_budget(
|
|
||||||
&mut reader,
|
|
||||||
&mut initial_data,
|
|
||||||
Duration::from_millis(prefetch_budget_ms),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
writer_task
|
|
||||||
.await
|
|
||||||
.expect("writer task must not panic during matrix case");
|
|
||||||
|
|
||||||
initial_data.starts_with(b"CONNECT")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn adversarial_prefetch_budget_matrix_5_20_50ms_for_fragmented_connect_tail() {
|
|
||||||
let cases = [
|
|
||||||
// (tail-delay-ms, expected CONNECT recovery for budgets [5, 20, 50])
|
|
||||||
(2u64, [true, true, true]),
|
|
||||||
(15u64, [false, true, true]),
|
|
||||||
(35u64, [false, false, true]),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (tail_delay_ms, expected) in cases {
|
|
||||||
let got_5 = run_prefetch_budget_case(5, tail_delay_ms).await;
|
|
||||||
let got_20 = run_prefetch_budget_case(20, tail_delay_ms).await;
|
|
||||||
let got_50 = run_prefetch_budget_case(50, tail_delay_ms).await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
got_5, expected[0],
|
|
||||||
"5ms prefetch budget mismatch for tail delay {}ms",
|
|
||||||
tail_delay_ms
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
got_20, expected[1],
|
|
||||||
"20ms prefetch budget mismatch for tail delay {}ms",
|
|
||||||
tail_delay_ms
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
got_50, expected[2],
|
|
||||||
"50ms prefetch budget mismatch for tail delay {}ms",
|
|
||||||
tail_delay_ms
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn control_current_runtime_prefetch_budget_is_5ms() {
|
|
||||||
assert_eq!(
|
|
||||||
MASK_CLASSIFIER_PREFETCH_TIMEOUT,
|
|
||||||
Duration::from_millis(5),
|
|
||||||
"matrix assumptions require current runtime prefetch budget to stay at 5ms"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ use rand::{Rng, SeedableRng};
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
|
||||||
const REPLY_404: &[u8] = b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
|
const REPLY_404: &[u8] = b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
|
||||||
@@ -25,7 +25,6 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -93,10 +92,7 @@ async fn run_generic_probe_and_capture_prefix(payload: Vec<u8>, expected_prefix:
|
|||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let mut observed = vec![0u8; REPLY_404.len()];
|
let mut observed = vec![0u8; REPLY_404.len()];
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(Duration::from_secs(2), client_side.read_exact(&mut observed))
|
||||||
Duration::from_secs(2),
|
|
||||||
client_side.read_exact(&mut observed),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -268,8 +264,7 @@ async fn stress_parallel_probe_mix_masks_all_sessions_without_cross_leakage() {
|
|||||||
|
|
||||||
let mut expected = std::collections::HashSet::new();
|
let mut expected = std::collections::HashSet::new();
|
||||||
for idx in 0..session_count {
|
for idx in 0..session_count {
|
||||||
let probe =
|
let probe = format!("GET /stress-{idx} HTTP/1.1\r\nHost: s{idx}.example\r\n\r\n").into_bytes();
|
||||||
format!("GET /stress-{idx} HTTP/1.1\r\nHost: s{idx}.example\r\n\r\n").into_bytes();
|
|
||||||
expected.insert(probe);
|
expected.insert(probe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,15 +274,9 @@ async fn stress_parallel_probe_mix_masks_all_sessions_without_cross_leakage() {
|
|||||||
let (mut stream, _) = listener.accept().await.unwrap();
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
let head = read_http_probe_header(&mut stream).await;
|
let head = read_http_probe_header(&mut stream).await;
|
||||||
stream.write_all(REPLY_404).await.unwrap();
|
stream.write_all(REPLY_404).await.unwrap();
|
||||||
assert!(
|
assert!(remaining.remove(&head), "backend received unexpected or duplicated probe prefix");
|
||||||
remaining.remove(&head),
|
|
||||||
"backend received unexpected or duplicated probe prefix"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
assert!(
|
assert!(remaining.is_empty(), "all session prefixes must be observed exactly once");
|
||||||
remaining.is_empty(),
|
|
||||||
"all session prefixes must be observed exactly once"
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut tasks = Vec::with_capacity(session_count);
|
let mut tasks = Vec::with_capacity(session_count);
|
||||||
@@ -302,8 +291,7 @@ async fn stress_parallel_probe_mix_masks_all_sessions_without_cross_leakage() {
|
|||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
let ip_tracker = Arc::new(UserIpTracker::new());
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
let beobachten = Arc::new(BeobachtenStore::new());
|
||||||
|
|
||||||
let probe =
|
let probe = format!("GET /stress-{idx} HTTP/1.1\r\nHost: s{idx}.example\r\n\r\n").into_bytes();
|
||||||
format!("GET /stress-{idx} HTTP/1.1\r\nHost: s{idx}.example\r\n\r\n").into_bytes();
|
|
||||||
let peer: SocketAddr = format!("203.0.113.{}:{}", 30 + idx, 56000 + idx)
|
let peer: SocketAddr = format!("203.0.113.{}:{}", 30 + idx, 56000 + idx)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -331,10 +319,7 @@ async fn stress_parallel_probe_mix_masks_all_sessions_without_cross_leakage() {
|
|||||||
client_side.shutdown().await.unwrap();
|
client_side.shutdown().await.unwrap();
|
||||||
|
|
||||||
let mut observed = vec![0u8; REPLY_404.len()];
|
let mut observed = vec![0u8; REPLY_404.len()];
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(Duration::from_secs(2), client_side.read_exact(&mut observed))
|
||||||
Duration::from_secs(2),
|
|
||||||
client_side.read_exact(&mut observed),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::config::{UpstreamConfig, UpstreamType};
|
|||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats.clone(),
|
stats.clone(),
|
||||||
@@ -68,10 +67,7 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
||||||
assert!(
|
assert!(tls_len <= u16::MAX as usize, "TLS length must fit into record header");
|
||||||
tls_len <= u16::MAX as usize,
|
|
||||||
"TLS length must fit into record header"
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_len = 5 + tls_len;
|
let total_len = 5 + tls_len;
|
||||||
let mut handshake = vec![fill; total_len];
|
let mut handshake = vec![fill; total_len];
|
||||||
@@ -152,14 +148,8 @@ async fn run_tls_success_mtproto_fail_session(
|
|||||||
let mut body = vec![0u8; body_len];
|
let mut body = vec![0u8; body_len];
|
||||||
client_side.read_exact(&mut body).await.unwrap();
|
client_side.read_exact(&mut body).await.unwrap();
|
||||||
|
|
||||||
client_side
|
client_side.write_all(&invalid_mtproto_record).await.unwrap();
|
||||||
.write_all(&invalid_mtproto_record)
|
client_side.write_all(&wrap_tls_application_data(&tail)).await.unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side
|
|
||||||
.write_all(&wrap_tls_application_data(&tail))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let forwarded = tokio::time::timeout(Duration::from_secs(3), accept_task)
|
let forwarded = tokio::time::timeout(Duration::from_secs(3), accept_task)
|
||||||
.await
|
.await
|
||||||
@@ -185,10 +175,7 @@ async fn redteam_01_backend_receives_no_data_after_mtproto_fail() {
|
|||||||
b"probe-a".to_vec(),
|
b"probe-a".to_vec(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(
|
assert!(forwarded.is_empty(), "backend unexpectedly received fallback bytes");
|
||||||
forwarded.is_empty(),
|
|
||||||
"backend unexpectedly received fallback bytes"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -201,10 +188,7 @@ async fn redteam_02_backend_must_never_receive_tls_records_after_mtproto_fail()
|
|||||||
b"probe-b".to_vec(),
|
b"probe-b".to_vec(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_ne!(
|
assert_ne!(forwarded[0], 0x17, "received TLS application record despite strict policy");
|
||||||
forwarded[0], 0x17,
|
|
||||||
"received TLS application record despite strict policy"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -216,10 +200,9 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
|
|||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
||||||
cfg.censorship.mask_port = 1;
|
cfg.censorship.mask_port = 1;
|
||||||
cfg.access.ignore_time_skew = true;
|
cfg.access.ignore_time_skew = true;
|
||||||
cfg.access.users.insert(
|
cfg.access
|
||||||
"user".to_string(),
|
.users
|
||||||
"acacacacacacacacacacacacacacacac".to_string(),
|
.insert("user".to_string(), "acacacacacacacacacacacacacacacac".to_string());
|
||||||
);
|
|
||||||
|
|
||||||
let harness = RedTeamHarness {
|
let harness = RedTeamHarness {
|
||||||
config: Arc::new(cfg),
|
config: Arc::new(cfg),
|
||||||
@@ -238,7 +221,6 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
Arc::new(Stats::new()),
|
Arc::new(Stats::new()),
|
||||||
@@ -279,10 +261,7 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(started.elapsed() < Duration::from_millis(1), "fallback path took longer than 1ms");
|
||||||
started.elapsed() < Duration::from_millis(1),
|
|
||||||
"fallback path took longer than 1ms"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! redteam_tail_must_not_forward_case {
|
macro_rules! redteam_tail_must_not_forward_case {
|
||||||
@@ -304,90 +283,18 @@ macro_rules! redteam_tail_must_not_forward_case {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
redteam_tail_must_not_forward_case!(
|
redteam_tail_must_not_forward_case!(redteam_04_tail_len_1_not_forwarded, "adadadadadadadadadadadadadadadad", [0xAD; 16], 4, 1);
|
||||||
redteam_04_tail_len_1_not_forwarded,
|
redteam_tail_must_not_forward_case!(redteam_05_tail_len_2_not_forwarded, "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae", [0xAE; 16], 5, 2);
|
||||||
"adadadadadadadadadadadadadadadad",
|
redteam_tail_must_not_forward_case!(redteam_06_tail_len_3_not_forwarded, "afafafafafafafafafafafafafafafaf", [0xAF; 16], 6, 3);
|
||||||
[0xAD; 16],
|
redteam_tail_must_not_forward_case!(redteam_07_tail_len_7_not_forwarded, "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", [0xB0; 16], 7, 7);
|
||||||
4,
|
redteam_tail_must_not_forward_case!(redteam_08_tail_len_15_not_forwarded, "b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1", [0xB1; 16], 8, 15);
|
||||||
1
|
redteam_tail_must_not_forward_case!(redteam_09_tail_len_63_not_forwarded, "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", [0xB2; 16], 9, 63);
|
||||||
);
|
redteam_tail_must_not_forward_case!(redteam_10_tail_len_127_not_forwarded, "b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3", [0xB3; 16], 10, 127);
|
||||||
redteam_tail_must_not_forward_case!(
|
redteam_tail_must_not_forward_case!(redteam_11_tail_len_255_not_forwarded, "b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4", [0xB4; 16], 11, 255);
|
||||||
redteam_05_tail_len_2_not_forwarded,
|
redteam_tail_must_not_forward_case!(redteam_12_tail_len_511_not_forwarded, "b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5", [0xB5; 16], 12, 511);
|
||||||
"aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae",
|
redteam_tail_must_not_forward_case!(redteam_13_tail_len_1023_not_forwarded, "b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6", [0xB6; 16], 13, 1023);
|
||||||
[0xAE; 16],
|
redteam_tail_must_not_forward_case!(redteam_14_tail_len_2047_not_forwarded, "b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7", [0xB7; 16], 14, 2047);
|
||||||
5,
|
redteam_tail_must_not_forward_case!(redteam_15_tail_len_4095_not_forwarded, "b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8", [0xB8; 16], 15, 4095);
|
||||||
2
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_06_tail_len_3_not_forwarded,
|
|
||||||
"afafafafafafafafafafafafafafafaf",
|
|
||||||
[0xAF; 16],
|
|
||||||
6,
|
|
||||||
3
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_07_tail_len_7_not_forwarded,
|
|
||||||
"b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0",
|
|
||||||
[0xB0; 16],
|
|
||||||
7,
|
|
||||||
7
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_08_tail_len_15_not_forwarded,
|
|
||||||
"b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1",
|
|
||||||
[0xB1; 16],
|
|
||||||
8,
|
|
||||||
15
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_09_tail_len_63_not_forwarded,
|
|
||||||
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
|
||||||
[0xB2; 16],
|
|
||||||
9,
|
|
||||||
63
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_10_tail_len_127_not_forwarded,
|
|
||||||
"b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3",
|
|
||||||
[0xB3; 16],
|
|
||||||
10,
|
|
||||||
127
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_11_tail_len_255_not_forwarded,
|
|
||||||
"b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4",
|
|
||||||
[0xB4; 16],
|
|
||||||
11,
|
|
||||||
255
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_12_tail_len_511_not_forwarded,
|
|
||||||
"b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5",
|
|
||||||
[0xB5; 16],
|
|
||||||
12,
|
|
||||||
511
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_13_tail_len_1023_not_forwarded,
|
|
||||||
"b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6",
|
|
||||||
[0xB6; 16],
|
|
||||||
13,
|
|
||||||
1023
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_14_tail_len_2047_not_forwarded,
|
|
||||||
"b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7",
|
|
||||||
[0xB7; 16],
|
|
||||||
14,
|
|
||||||
2047
|
|
||||||
);
|
|
||||||
redteam_tail_must_not_forward_case!(
|
|
||||||
redteam_15_tail_len_4095_not_forwarded,
|
|
||||||
"b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8",
|
|
||||||
[0xB8; 16],
|
|
||||||
15,
|
|
||||||
4095
|
|
||||||
);
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore = "red-team expected-fail: impossible indistinguishability envelope"]
|
#[ignore = "red-team expected-fail: impossible indistinguishability envelope"]
|
||||||
@@ -442,13 +349,14 @@ async fn redteam_16_timing_delta_between_paths_must_be_sub_1ms_under_concurrency
|
|||||||
|
|
||||||
let min = durations.iter().copied().min().unwrap();
|
let min = durations.iter().copied().min().unwrap();
|
||||||
let max = durations.iter().copied().max().unwrap();
|
let max = durations.iter().copied().max().unwrap();
|
||||||
assert!(
|
assert!(max - min <= Duration::from_millis(1), "timing spread too wide for strict anti-probing envelope");
|
||||||
max - min <= Duration::from_millis(1),
|
|
||||||
"timing spread too wide for strict anti-probing envelope"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sent: usize) -> u128 {
|
async fn measure_invalid_probe_duration_ms(
|
||||||
|
delay_ms: u64,
|
||||||
|
tls_len: u16,
|
||||||
|
body_sent: usize,
|
||||||
|
) -> u128 {
|
||||||
let mut cfg = ProxyConfig::default();
|
let mut cfg = ProxyConfig::default();
|
||||||
cfg.general.beobachten = false;
|
cfg.general.beobachten = false;
|
||||||
cfg.censorship.mask = true;
|
cfg.censorship.mask = true;
|
||||||
@@ -479,7 +387,6 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
Arc::new(Stats::new()),
|
Arc::new(Stats::new()),
|
||||||
@@ -553,7 +460,6 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
Arc::new(Stats::new()),
|
Arc::new(Stats::new()),
|
||||||
@@ -595,8 +501,7 @@ macro_rules! redteam_timing_envelope_case {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore = "red-team expected-fail: unrealistically tight reject timing envelope"]
|
#[ignore = "red-team expected-fail: unrealistically tight reject timing envelope"]
|
||||||
async fn $name() {
|
async fn $name() {
|
||||||
let elapsed_ms =
|
let elapsed_ms = measure_invalid_probe_duration_ms($delay_ms, $tls_len, $body_sent).await;
|
||||||
measure_invalid_probe_duration_ms($delay_ms, $tls_len, $body_sent).await;
|
|
||||||
assert!(
|
assert!(
|
||||||
elapsed_ms <= $max_ms,
|
elapsed_ms <= $max_ms,
|
||||||
"timing envelope violated: elapsed={}ms, max={}ms",
|
"timing envelope violated: elapsed={}ms, max={}ms",
|
||||||
@@ -614,9 +519,11 @@ macro_rules! redteam_constant_shape_case {
|
|||||||
async fn $name() {
|
async fn $name() {
|
||||||
let got = capture_forwarded_probe_len($tls_len, $body_sent).await;
|
let got = capture_forwarded_probe_len($tls_len, $body_sent).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got, $expected_len,
|
got,
|
||||||
|
$expected_len,
|
||||||
"fingerprint shape mismatch: got={} expected={} (strict constant-shape model)",
|
"fingerprint shape mismatch: got={} expected={} (strict constant-shape model)",
|
||||||
got, $expected_len
|
got,
|
||||||
|
$expected_len
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
|
||||||
use crate::crypto::sha256_hmac;
|
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_VERSION};
|
|
||||||
use crate::protocol::tls;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
||||||
use tokio::time::{Duration, Instant};
|
|
||||||
|
|
||||||
fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![UpstreamConfig {
|
|
||||||
upstream_type: UpstreamType::Direct {
|
|
||||||
interface: None,
|
|
||||||
bind_addresses: None,
|
|
||||||
},
|
|
||||||
weight: 1,
|
|
||||||
enabled: true,
|
|
||||||
scopes: String::new(),
|
|
||||||
selected_scope: String::new(),
|
|
||||||
}],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
|
||||||
let total_len = 5 + tls_len;
|
|
||||||
let mut handshake = vec![fill; total_len];
|
|
||||||
|
|
||||||
handshake[0] = 0x16;
|
|
||||||
handshake[1] = 0x03;
|
|
||||||
handshake[2] = 0x01;
|
|
||||||
handshake[3..5].copy_from_slice(&(tls_len as u16).to_be_bytes());
|
|
||||||
|
|
||||||
let session_id_len: usize = 32;
|
|
||||||
handshake[tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN] = session_id_len as u8;
|
|
||||||
|
|
||||||
handshake[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN].fill(0);
|
|
||||||
let computed = sha256_hmac(secret, &handshake);
|
|
||||||
let mut digest = computed;
|
|
||||||
let ts = timestamp.to_le_bytes();
|
|
||||||
for i in 0..4 {
|
|
||||||
digest[28 + i] ^= ts[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
handshake[tls::TLS_DIGEST_POS..tls::TLS_DIGEST_POS + tls::TLS_DIGEST_LEN]
|
|
||||||
.copy_from_slice(&digest);
|
|
||||||
handshake
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_replay_candidate_session(
|
|
||||||
replay_checker: Arc<ReplayChecker>,
|
|
||||||
hello: &[u8],
|
|
||||||
peer: SocketAddr,
|
|
||||||
drive_mtproto_fail: bool,
|
|
||||||
) -> Duration {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = false;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
cfg.censorship.mask_unix_sock = None;
|
|
||||||
cfg.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
||||||
cfg.censorship.mask_port = 1;
|
|
||||||
cfg.censorship.mask_timing_normalization_enabled = false;
|
|
||||||
cfg.access.ignore_time_skew = true;
|
|
||||||
cfg.access.users.insert(
|
|
||||||
"user".to_string(),
|
|
||||||
"abababababababababababababababab".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(65536);
|
|
||||||
let started = Instant::now();
|
|
||||||
|
|
||||||
let task = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
peer,
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
new_upstream_manager(stats),
|
|
||||||
replay_checker,
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten,
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(hello).await.unwrap();
|
|
||||||
|
|
||||||
if drive_mtproto_fail {
|
|
||||||
let mut server_hello_head = [0u8; 5];
|
|
||||||
client_side
|
|
||||||
.read_exact(&mut server_hello_head)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
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 mut body = vec![0u8; body_len];
|
|
||||||
client_side.read_exact(&mut body).await.unwrap();
|
|
||||||
|
|
||||||
let mut invalid_mtproto_record = Vec::with_capacity(5 + HANDSHAKE_LEN);
|
|
||||||
invalid_mtproto_record.push(0x17);
|
|
||||||
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(&vec![0u8; HANDSHAKE_LEN]);
|
|
||||||
client_side
|
|
||||||
.write_all(&invalid_mtproto_record)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side
|
|
||||||
.write_all(b"GET /replay-fallback HTTP/1.1\r\nHost: x\r\n\r\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(4), task)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
started.elapsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn replay_reject_still_honors_masking_timing_budget() {
|
|
||||||
let replay_checker = Arc::new(ReplayChecker::new(256, Duration::from_secs(60)));
|
|
||||||
let hello = make_valid_tls_client_hello(&[0xAB; 16], 7, 600, 0x51);
|
|
||||||
|
|
||||||
let seed_elapsed = run_replay_candidate_session(
|
|
||||||
Arc::clone(&replay_checker),
|
|
||||||
&hello,
|
|
||||||
"198.51.100.201:58001".parse().unwrap(),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
seed_elapsed >= Duration::from_millis(40) && seed_elapsed < Duration::from_millis(250),
|
|
||||||
"seed replay-candidate run must honor masking timing budget without unbounded delay"
|
|
||||||
);
|
|
||||||
|
|
||||||
let replay_elapsed = run_replay_candidate_session(
|
|
||||||
Arc::clone(&replay_checker),
|
|
||||||
&hello,
|
|
||||||
"198.51.100.202:58002".parse().unwrap(),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -173,10 +172,7 @@ async fn redteam_fuzz_01_hardened_output_length_correlation_should_be_below_0_2(
|
|||||||
let y_hard: Vec<f64> = hardened.iter().map(|v| *v as f64).collect();
|
let y_hard: Vec<f64> = hardened.iter().map(|v| *v as f64).collect();
|
||||||
|
|
||||||
let corr_hard = pearson_corr(&x, &y_hard).abs();
|
let corr_hard = pearson_corr(&x, &y_hard).abs();
|
||||||
println!(
|
println!("redteam_fuzz corr_hardened={corr_hard:.4} samples={}", sizes.len());
|
||||||
"redteam_fuzz corr_hardened={corr_hard:.4} samples={}",
|
|
||||||
sizes.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
corr_hard < 0.2,
|
corr_hard < 0.2,
|
||||||
@@ -238,7 +234,9 @@ async fn redteam_fuzz_03_hardened_signal_must_be_10x_lower_than_plain() {
|
|||||||
let corr_plain = pearson_corr(&x, &y_plain).abs();
|
let corr_plain = pearson_corr(&x, &y_plain).abs();
|
||||||
let corr_hard = pearson_corr(&x, &y_hard).abs();
|
let corr_hard = pearson_corr(&x, &y_hard).abs();
|
||||||
|
|
||||||
println!("redteam_fuzz corr_plain={corr_plain:.4} corr_hardened={corr_hard:.4}");
|
println!(
|
||||||
|
"redteam_fuzz corr_plain={corr_plain:.4} corr_hardened={corr_hard:.4}"
|
||||||
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
corr_hard <= corr_plain * 0.1,
|
corr_hard <= corr_plain * 0.1,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -165,7 +164,10 @@ async fn redteam_shape_02_padding_tail_must_be_non_deterministic() {
|
|||||||
let cap = 4096usize;
|
let cap = 4096usize;
|
||||||
let got = run_probe_capture(17, 600, true, floor, cap).await;
|
let got = run_probe_capture(17, 600, true, floor, cap).await;
|
||||||
|
|
||||||
assert!(got.len() > 22, "test requires padding tail to exist");
|
assert!(
|
||||||
|
got.len() > 22,
|
||||||
|
"test requires padding tail to exist"
|
||||||
|
);
|
||||||
|
|
||||||
let tail = &got[22..];
|
let tail = &got[22..];
|
||||||
assert!(
|
assert!(
|
||||||
@@ -192,9 +194,7 @@ async fn redteam_shape_03_exact_floor_input_should_not_be_fixed_point() {
|
|||||||
async fn redteam_shape_04_all_sub_cap_sizes_should_collapse_to_single_size() {
|
async fn redteam_shape_04_all_sub_cap_sizes_should_collapse_to_single_size() {
|
||||||
let floor = 512usize;
|
let floor = 512usize;
|
||||||
let cap = 4096usize;
|
let cap = 4096usize;
|
||||||
let classes = [
|
let classes = [17usize, 63usize, 255usize, 511usize, 1023usize, 2047usize, 3071usize];
|
||||||
17usize, 63usize, 255usize, 511usize, 1023usize, 2047usize, 3071usize,
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut observed = Vec::new();
|
let mut observed = Vec::new();
|
||||||
for body in classes {
|
for body in classes {
|
||||||
@@ -203,10 +203,7 @@ async fn redteam_shape_04_all_sub_cap_sizes_should_collapse_to_single_size() {
|
|||||||
|
|
||||||
let first = observed[0];
|
let first = observed[0];
|
||||||
for v in observed {
|
for v in observed {
|
||||||
assert_eq!(
|
assert_eq!(v, first, "strict model expects one collapsed class across all sub-cap probes");
|
||||||
v, first,
|
|
||||||
"strict model expects one collapsed class across all sub-cap probes"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{UpstreamConfig, UpstreamType};
|
use crate::config::{UpstreamConfig, UpstreamType};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::config::{UpstreamConfig, UpstreamType};
|
|||||||
use crate::crypto::sha256_hmac;
|
use crate::crypto::sha256_hmac;
|
||||||
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_RECORD_APPLICATION, TLS_VERSION};
|
use crate::protocol::constants::{HANDSHAKE_LEN, TLS_RECORD_APPLICATION, TLS_VERSION};
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
10,
|
|
||||||
1,
|
1,
|
||||||
false,
|
false,
|
||||||
stats,
|
stats,
|
||||||
@@ -71,10 +70,7 @@ fn build_harness(mask_port: u16, secret_hex: &str) -> StressHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
fn make_valid_tls_client_hello(secret: &[u8], timestamp: u32, tls_len: usize, fill: u8) -> Vec<u8> {
|
||||||
assert!(
|
assert!(tls_len <= u16::MAX as usize, "TLS length must fit into record header");
|
||||||
tls_len <= u16::MAX as usize,
|
|
||||||
"TLS length must fit into record header"
|
|
||||||
);
|
|
||||||
|
|
||||||
let total_len = 5 + tls_len;
|
let total_len = 5 + tls_len;
|
||||||
let mut handshake = vec![fill; total_len];
|
let mut handshake = vec![fill; total_len];
|
||||||
@@ -154,8 +150,12 @@ async fn run_parallel_tail_fallback_case(
|
|||||||
|
|
||||||
for idx in 0..sessions {
|
for idx in 0..sessions {
|
||||||
let harness = build_harness(backend_addr.port(), "e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0");
|
let harness = build_harness(backend_addr.port(), "e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0");
|
||||||
let hello =
|
let hello = make_valid_tls_client_hello(
|
||||||
make_valid_tls_client_hello(&[0xE0; 16], ts_base + idx as u32, 600, 0x40 + (idx as u8));
|
&[0xE0; 16],
|
||||||
|
ts_base + idx as u32,
|
||||||
|
600,
|
||||||
|
0x40 + (idx as u8),
|
||||||
|
);
|
||||||
|
|
||||||
let invalid_mtproto = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
let invalid_mtproto = wrap_tls_application_data(&vec![0u8; HANDSHAKE_LEN]);
|
||||||
let payload = vec![((idx * 37) & 0xff) as u8; payload_len + idx % 3];
|
let payload = vec![((idx * 37) & 0xff) as u8; payload_len + idx % 3];
|
||||||
@@ -194,10 +194,7 @@ async fn run_parallel_tail_fallback_case(
|
|||||||
|
|
||||||
client_side.write_all(&hello).await.unwrap();
|
client_side.write_all(&hello).await.unwrap();
|
||||||
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);
|
||||||
read_tls_record_body(&mut client_side, server_hello_head).await;
|
read_tls_record_body(&mut client_side, server_hello_head).await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,291 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
use crate::config::ProxyConfig;
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::transport::UpstreamManager;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
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]
|
|
||||||
async fn edge_mask_delay_bypassed_if_max_is_zero() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.censorship.server_hello_delay_min_ms = 10_000;
|
|
||||||
config.censorship.server_hello_delay_max_ms = 0;
|
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
maybe_apply_mask_reject_delay(&config).await;
|
|
||||||
assert!(start.elapsed() < Duration::from_millis(50));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_beobachten_ttl_clamps_exactly_to_24_hours() {
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.general.beobachten = true;
|
|
||||||
config.general.beobachten_minutes = 100_000;
|
|
||||||
|
|
||||||
let ttl = beobachten_ttl(&config);
|
|
||||||
assert_eq!(ttl.as_secs(), 24 * 60 * 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn edge_wrap_tls_application_record_empty_payload() {
|
|
||||||
let wrapped = wrap_tls_application_record(&[]);
|
|
||||||
assert_eq!(wrapped.len(), 5);
|
|
||||||
assert_eq!(wrapped[0], TLS_RECORD_APPLICATION);
|
|
||||||
assert_eq!(&wrapped[3..5], &[0, 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn boundary_user_data_quota_exact_match_rejects() {
|
|
||||||
let user = "quota-boundary-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config.access.user_data_quota.insert(user.to_string(), 1024);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
preload_user_quota(stats.as_ref(), user, 1024);
|
|
||||||
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
let peer = "198.51.100.10:55000".parse().unwrap();
|
|
||||||
|
|
||||||
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user, &config, stats, peer, ip_tracker,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ProxyError::DataQuotaExceeded { .. })));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn boundary_user_expiration_in_past_rejects() {
|
|
||||||
let user = "expired-boundary-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
let expired_time = chrono::Utc::now() - chrono::Duration::milliseconds(1);
|
|
||||||
config
|
|
||||||
.access
|
|
||||||
.user_expirations
|
|
||||||
.insert(user.to_string(), expired_time);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
let peer = "198.51.100.11:55000".parse().unwrap();
|
|
||||||
|
|
||||||
let result = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user, &config, stats, peer, ip_tracker,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ProxyError::UserExpired { .. })));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn blackhat_proxy_protocol_massive_garbage_rejected_quickly() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.server.proxy_protocol_header_timeout_ms = 300;
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.12:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
true,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(&vec![b'A'; 2000]).await.unwrap();
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(Duration::from_secs(2), handler)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(result, Err(ProxyError::InvalidProxyProtocol)));
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn edge_tls_body_immediate_eof_triggers_masking_and_bad_connect() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.beobachten = true;
|
|
||||||
cfg.general.beobachten_minutes = 1;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let beobachten = Arc::new(BeobachtenStore::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.13:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
beobachten.clone(),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side
|
|
||||||
.write_all(&[0x16, 0x03, 0x01, 0x00, 100])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn security_classic_mode_disabled_masks_valid_length_payload() {
|
|
||||||
let mut cfg = ProxyConfig::default();
|
|
||||||
cfg.general.modes.classic = false;
|
|
||||||
cfg.general.modes.secure = false;
|
|
||||||
cfg.censorship.mask = true;
|
|
||||||
|
|
||||||
let config = Arc::new(cfg);
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
|
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
|
||||||
let handler = tokio::spawn(handle_client_stream(
|
|
||||||
server_side,
|
|
||||||
"198.51.100.15:55000".parse().unwrap(),
|
|
||||||
config,
|
|
||||||
stats.clone(),
|
|
||||||
Arc::new(UpstreamManager::new(
|
|
||||||
vec![],
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
stats.clone(),
|
|
||||||
)),
|
|
||||||
Arc::new(ReplayChecker::new(128, Duration::from_secs(60))),
|
|
||||||
Arc::new(BufferPool::new()),
|
|
||||||
Arc::new(SecureRandom::new()),
|
|
||||||
None,
|
|
||||||
Arc::new(RouteRuntimeController::new(RelayRouteMode::Direct)),
|
|
||||||
None,
|
|
||||||
Arc::new(UserIpTracker::new()),
|
|
||||||
Arc::new(BeobachtenStore::new()),
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
|
|
||||||
client_side.write_all(&vec![0xEF; 64]).await.unwrap();
|
|
||||||
client_side.shutdown().await.unwrap();
|
|
||||||
|
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(2), handler)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(stats.get_connects_bad(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn concurrency_ip_tracker_strict_limit_one_rapid_churn() {
|
|
||||||
let user = "rapid-churn-user";
|
|
||||||
let mut config = ProxyConfig::default();
|
|
||||||
config
|
|
||||||
.access
|
|
||||||
.user_max_tcp_conns
|
|
||||||
.insert(user.to_string(), 10);
|
|
||||||
|
|
||||||
let stats = Arc::new(Stats::new());
|
|
||||||
let ip_tracker = Arc::new(UserIpTracker::new());
|
|
||||||
ip_tracker.set_user_limit(user, 1).await;
|
|
||||||
|
|
||||||
let peer = "198.51.100.16:55000".parse().unwrap();
|
|
||||||
|
|
||||||
for _ in 0..500 {
|
|
||||||
let reservation = RunningClientHandler::acquire_user_connection_reservation_static(
|
|
||||||
user,
|
|
||||||
&config,
|
|
||||||
stats.clone(),
|
|
||||||
peer,
|
|
||||||
ip_tracker.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
reservation.release().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(stats.get_user_curr_connects(user), 0);
|
|
||||||
assert_eq!(ip_tracker.get_active_ip_count(user).await, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn quirk_read_with_progress_zero_length_buffer_returns_zero_immediately() {
|
|
||||||
let (mut server_side, _client_side) = duplex(4096);
|
|
||||||
let mut empty_buf = &mut [][..];
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(
|
|
||||||
Duration::from_millis(50),
|
|
||||||
read_with_progress(&mut server_side, &mut empty_buf),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert_eq!(result.unwrap().unwrap(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn stress_read_with_progress_cancellation_safety() {
|
|
||||||
let (mut server_side, mut client_side) = duplex(4096);
|
|
||||||
|
|
||||||
client_side.write_all(b"12345").await.unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0u8; 10];
|
|
||||||
let result = tokio::time::timeout(
|
|
||||||
Duration::from_millis(50),
|
|
||||||
read_with_progress(&mut server_side, &mut buf),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(result.is_err());
|
|
||||||
|
|
||||||
client_side.write_all(b"67890").await.unwrap();
|
|
||||||
let mut buf2 = [0u8; 5];
|
|
||||||
server_side.read_exact(&mut buf2).await.unwrap();
|
|
||||||
assert_eq!(&buf2, b"67890");
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user