mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 03:22:05 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003fdefc63 | ||
|
|
a25c411249 | ||
|
|
0c11cefc04 | ||
|
|
8181830902 | ||
|
|
0a8c95879b |
@@ -9,14 +9,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = [
|
||||
"-C",
|
||||
"opt-level=z",
|
||||
"--cfg",
|
||||
'getrandom_backend="wasm_js"',
|
||||
]
|
||||
|
||||
# region
|
||||
|
||||
# region CI
|
||||
|
||||
@@ -33,24 +33,10 @@ runs:
|
||||
sudo apt-get install -qqy build-essential mold musl-tools
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: '35.1'
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
|
||||
- name: Verify protoc version
|
||||
run: |
|
||||
version="$(protoc --version | tr -d '\r')"
|
||||
test "$version" = "libprotoc 35.1"
|
||||
shell: bash
|
||||
|
||||
- name: Setup Frontend Environment
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
||||
|
||||
- name: Install GUI dependencies (Linux)
|
||||
@@ -96,3 +82,9 @@ runs:
|
||||
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
|
||||
shell: bash
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ inputs.token }}
|
||||
@@ -3,9 +3,6 @@ author: Luna
|
||||
description: 'Setup Node.js, pnpm, and install dependencies'
|
||||
|
||||
inputs:
|
||||
token:
|
||||
description: 'GitHub token, used by setup-protoc action'
|
||||
required: false
|
||||
build-filter:
|
||||
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
|
||||
required: false
|
||||
@@ -14,22 +11,6 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.95
|
||||
target: wasm32-unknown-unknown
|
||||
cache: false
|
||||
rustflags: ''
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: wasm-pack
|
||||
|
||||
- uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
version: '35.1'
|
||||
repo-token: ${{ inputs.token }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
@@ -60,16 +41,8 @@ runs:
|
||||
pnpm -r install
|
||||
if [ -n "${{ inputs.build-filter }}" ]; then
|
||||
echo "Building with filter: ${{ inputs.build-filter }}"
|
||||
pnpm -r --workspace-concurrency=1 --filter "${{ inputs.build-filter }}" build
|
||||
pnpm -r --filter "${{ inputs.build-filter }}" build
|
||||
else
|
||||
echo "No build filter provided, building all packages"
|
||||
pnpm -r --workspace-concurrency=1 build
|
||||
fi
|
||||
|
||||
- name: Bundle config generator with web frontend
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f easytier-web/frontend/dist/index.html ] && [ -f easytier-web/config-generator/dist/index.html ]; then
|
||||
mkdir -p easytier-web/frontend/dist/config-generator
|
||||
cp -R easytier-web/config-generator/dist/. easytier-web/frontend/dist/config-generator/
|
||||
pnpm -r build
|
||||
fi
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
@@ -47,7 +47,6 @@ jobs:
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Archive artifact
|
||||
@@ -58,7 +57,7 @@ jobs:
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -117,92 +117,13 @@ jobs:
|
||||
find "./easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./easytier-gui/src-tauri/ \;
|
||||
fi
|
||||
|
||||
- name: Validate macOS signing secrets
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
missing=()
|
||||
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do
|
||||
if [[ -z "${!name}" ]]; then
|
||||
missing+=("$name")
|
||||
fi
|
||||
done
|
||||
if (( ${#missing[@]} )); then
|
||||
printf 'Missing macOS signing secret(s): %s\n' "${missing[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build GUI
|
||||
if: ${{ matrix.GUI_TARGET != '' && (!contains(matrix.GUI_TARGET, 'darwin') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
if: ${{ matrix.GUI_TARGET != '' }}
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Build GUI (signed and notarized)
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
timeout-minutes: 60
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
with:
|
||||
projectPath: ./easytier-gui
|
||||
args: --verbose --target ${{ matrix.GUI_TARGET }}
|
||||
|
||||
- name: Notarize and staple macOS DMG
|
||||
if: ${{ contains(matrix.GUI_TARGET, 'darwin') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
dmg_dir="./target/$GUI_TARGET/release/bundle/dmg"
|
||||
if [[ ! -d "$dmg_dir" ]]; then
|
||||
printf 'macOS DMG directory not found: %s\n' "$dmg_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dmgs=()
|
||||
while IFS= read -r dmg; do
|
||||
dmgs+=("$dmg")
|
||||
done < <(find "$dmg_dir" -maxdepth 1 -type f -name "*.dmg" | sort)
|
||||
if (( ${#dmgs[@]} == 0 )); then
|
||||
printf 'No macOS DMG found in %s\n' "$dmg_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for dmg in "${dmgs[@]}"; do
|
||||
printf 'Verifying signed DMG: %s\n' "$dmg"
|
||||
codesign --verify --verbose=4 "$dmg"
|
||||
codesign -dv --verbose=4 "$dmg"
|
||||
|
||||
printf 'Notarizing DMG: %s\n' "$dmg"
|
||||
xcrun notarytool submit "$dmg" \
|
||||
--apple-id "$APPLE_ID" \
|
||||
--password "$APPLE_PASSWORD" \
|
||||
--team-id "$APPLE_TEAM_ID" \
|
||||
--wait \
|
||||
--timeout 40m
|
||||
|
||||
printf 'Stapling DMG: %s\n' "$dmg"
|
||||
xcrun stapler staple "$dmg"
|
||||
xcrun stapler validate "$dmg"
|
||||
done
|
||||
|
||||
- name: Collect artifact
|
||||
run: |
|
||||
mkdir -p ./artifacts/objects/
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
cancel_others: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
+197
-156
@@ -1,205 +1,246 @@
|
||||
name: ohos
|
||||
name: EasyTier OHOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop, main, "releases/**", "ohos/**"]
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
tags:
|
||||
- "v*"
|
||||
- "!*-pre"
|
||||
- 'v*'
|
||||
- '!*-pre'
|
||||
pull_request:
|
||||
branches: [develop, main, "ohos/**"]
|
||||
branches: ["develop", "main"]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
description: Publish this non-main branch and dispatch downstream builds
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# necessary for windows
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
ohos:
|
||||
name: ohos
|
||||
cargo_fmt_check:
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
- name: Prepare build environment
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: aarch64-unknown-linux-ohos
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up HarmonyOS
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
pre_job:
|
||||
# continue-on-error: true # Uncomment once integration is finished
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
# do not skip push on branch starts with releases/
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: "same_content_newer"
|
||||
skip_after_successful_duplicate: "true"
|
||||
cancel_others: "true"
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/actions/**"]'
|
||||
|
||||
build-ohos:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
env:
|
||||
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
|
||||
if: needs.pre_job.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -qq \
|
||||
build-essential \
|
||||
wget \
|
||||
unzip \
|
||||
git \
|
||||
pkg-config curl libgl1-mesa-dev expect
|
||||
|
||||
- name: Resolve easytier version
|
||||
run: |
|
||||
set -e
|
||||
|
||||
UPSTREAM_REPO="https://github.com/EasyTier/EasyTier.git"
|
||||
|
||||
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || true
|
||||
git fetch --unshallow upstream main || git fetch upstream main
|
||||
git fetch --tags upstream --force
|
||||
|
||||
# 读取 cargo 版本
|
||||
CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path easytier/Cargo.toml \
|
||||
| jq -r '.packages[0].version')
|
||||
|
||||
# 获取 upstream/main 最新 tag
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 upstream/main 2>/dev/null || echo "")
|
||||
LAST_TAG_VERSION="${LAST_TAG#v}"
|
||||
|
||||
# 语义版本比较
|
||||
version_gt() {
|
||||
[ "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ] && [ "$1" != "$2" ]
|
||||
}
|
||||
|
||||
if [ -z "$LAST_TAG_VERSION" ]; then
|
||||
BASE_VERSION="$CARGO_VERSION"
|
||||
DIFF_COUNT=$(git rev-list --count upstream/main)
|
||||
elif version_gt "$CARGO_VERSION" "$LAST_TAG_VERSION"; then
|
||||
BASE_VERSION="$CARGO_VERSION"
|
||||
DIFF_COUNT=0
|
||||
else
|
||||
BASE_VERSION="$LAST_TAG_VERSION"
|
||||
DIFF_COUNT=$(git rev-list --count "${LAST_TAG}..upstream/main")
|
||||
fi
|
||||
|
||||
COMMIT_HASH=$(git rev-parse --short upstream/main)
|
||||
EASYTIER_VERSION="${BASE_VERSION}-${DIFF_COUNT}-${COMMIT_HASH}"
|
||||
|
||||
echo "EASYTIER_VERSION=$EASYTIER_VERSION"
|
||||
echo "EASYTIER_VERSION=$EASYTIER_VERSION" >> $GITHUB_ENV
|
||||
|
||||
cd ./easytier-contrib/easytier-ohrs/package
|
||||
jq --arg v "$EASYTIER_VERSION" '.version = $v' oh-package.json5 > oh-package.tmp.json5
|
||||
mv oh-package.tmp.json5 oh-package.json5
|
||||
|
||||
|
||||
- name: Generate CHANGELOG.md for current commit
|
||||
working-directory: ./easytier-contrib/easytier-ohrs/package
|
||||
run: |
|
||||
{
|
||||
echo "## easytier-ohrs ${EASYTIER_VERSION}"
|
||||
echo
|
||||
git log -1 --pretty=format:"- %s"
|
||||
echo
|
||||
} > CHANGELOG.md
|
||||
|
||||
- name: Setup HarmonyOS CLI tools
|
||||
uses: ErBWs/setup-ohos@v1
|
||||
|
||||
- name: Install ohrs
|
||||
uses: taiki-e/install-action@v2
|
||||
- name: Download and Extract Custom SDK
|
||||
run: |
|
||||
wget https://github.com/FrankHan052176/Easytier-OHOS-sdk/releases/download/v1/ohos-sdk.zip -O /tmp/ohos-sdk.zip
|
||||
sudo unzip -o /tmp/ohos-sdk.zip -d /tmp/custom-sdk
|
||||
sudo cp -rf /tmp/custom-sdk/linux/native/* $OHOS_NDK_HOME/native
|
||||
echo "Custom SDK files deployed to $OHOS_NDK_HOME/native"
|
||||
ls -a $OHOS_NDK_HOME/native
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
|
||||
|
||||
rustup install stable
|
||||
rustup default stable
|
||||
|
||||
rustup target add aarch64-unknown-linux-ohos
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: ohrs
|
||||
|
||||
- name: Build HAR
|
||||
id: package
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
- name: Create clang wrapper script
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get install -qqy \
|
||||
pkg-config curl libgl1-mesa-dev expect llvm clang lldb lld
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all --manifest-path \
|
||||
easytier-contrib/easytier-ohrs/Cargo.toml -- --check
|
||||
|
||||
cargo_version=$(cargo metadata --format-version 1 --no-deps \
|
||||
--manifest-path easytier/Cargo.toml | jq -r '.packages[0].version')
|
||||
last_tag=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true)
|
||||
if [ -n "$last_tag" ]; then
|
||||
base_version=$(printf '%s\n' "$cargo_version" "${last_tag#v}" \
|
||||
| sort -V | tail -n 1)
|
||||
commit_count=$(git rev-list --count "$last_tag..HEAD")
|
||||
else
|
||||
base_version=$cargo_version
|
||||
commit_count=0
|
||||
fi
|
||||
|
||||
source_branch=${GITHUB_HEAD_REF:-}
|
||||
if [ -z "$source_branch" ]; then
|
||||
if [ "$GITHUB_REF_TYPE" = branch ]; then
|
||||
source_branch=$GITHUB_REF_NAME
|
||||
else
|
||||
source_branch=${DEFAULT_BRANCH:-main}
|
||||
fi
|
||||
fi
|
||||
branch_id=$(printf '%s' "$source_branch" \
|
||||
| tr '[:upper:]' '[:lower:]' \
|
||||
| sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//' \
|
||||
| cut -c1-64)
|
||||
branch_id=${branch_id:-main}
|
||||
|
||||
package_name=easytier-ohrs
|
||||
package_version="${base_version}-${branch_id}-${commit_count}-${GITHUB_RUN_NUMBER}-${GITHUB_RUN_ATTEMPT}-g$(git rev-parse --short=8 HEAD)"
|
||||
echo "name=$package_name" >> "$GITHUB_OUTPUT"
|
||||
echo "EASYTIER_PACKAGE_NAME=$package_name" >> "$GITHUB_ENV"
|
||||
echo "EASYTIER_VERSION=$package_version" >> "$GITHUB_ENV"
|
||||
|
||||
package_dir=easytier-contrib/easytier-ohrs/package
|
||||
jq --arg name "$package_name" --arg version "$package_version" \
|
||||
'.name = $name | .version = $version' \
|
||||
"$package_dir/oh-package.json5" > "$package_dir/oh-package.tmp.json5"
|
||||
mv "$package_dir/oh-package.tmp.json5" "$package_dir/oh-package.json5"
|
||||
{
|
||||
echo "## $package_name $package_version"
|
||||
echo
|
||||
echo "- Core version: $base_version"
|
||||
echo "- Core commit: $GITHUB_SHA"
|
||||
git log -1 --pretty=format:'- %s'
|
||||
echo
|
||||
} > "$package_dir/CHANGELOG.md"
|
||||
|
||||
sudo mkdir -p "$OHOS_NDK_HOME/native/llvm"
|
||||
sudo tee "$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh" >/dev/null <<'EOF'
|
||||
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
||||
sudo tee $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh > /dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
exec "$OHOS_NDK_HOME/native/llvm/bin/clang" \
|
||||
exec $OHOS_NDK_HOME/native/llvm/bin/clang \
|
||||
-target aarch64-linux-ohos \
|
||||
--sysroot="$OHOS_NDK_HOME/native/sysroot" \
|
||||
-D__MUSL__ "$@"
|
||||
--sysroot=$OHOS_NDK_HOME/native/sysroot \
|
||||
-D__MUSL__ \
|
||||
"$@"
|
||||
EOF
|
||||
sudo chmod +x \
|
||||
"$OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
sudo chmod +x $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh
|
||||
|
||||
cd easytier-contrib/easytier-ohrs
|
||||
- name: Build latest Har
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
sudo apt-get install -y llvm clang lldb lld
|
||||
sudo apt-get install -y protobuf-compiler
|
||||
source env.sh
|
||||
ohrs doctor
|
||||
ohrs build --release --arch aarch
|
||||
ohrs artifact
|
||||
mv package.har "$package_name.har"
|
||||
mv package.har easytier-ohrs.har
|
||||
|
||||
- name: Upload HAR
|
||||
- name: Build Release Package
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
run: |
|
||||
echo "🎉 Official Release detected. Building easytier-release..."
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
echo "Release Version: $TAG_VERSION"
|
||||
cd package
|
||||
jq --arg v "$TAG_VERSION" '.name = "easytier-release" | .version = $v' oh-package.json5 > oh-package.tmp.json5 && mv oh-package.tmp.json5 oh-package.json5
|
||||
cd ..
|
||||
ohrs build --release --arch aarch
|
||||
cd dist/arm64-v8a
|
||||
mv libeasytier_ohrs.so libeasytier_release.so
|
||||
cd ../..
|
||||
ohrs artifact
|
||||
mv package.har easytier-release.har
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ steps.package.outputs.name }}
|
||||
path: easytier-contrib/easytier-ohrs/${{ steps.package.outputs.name }}.har
|
||||
name: easytier-ohos
|
||||
path: |
|
||||
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish and dispatch
|
||||
if: >-
|
||||
(github.event_name == 'push' &&
|
||||
github.ref_type == 'branch' &&
|
||||
github.ref_name == 'main' &&
|
||||
github.event.forced != true) ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref_type == 'branch' &&
|
||||
(github.ref_name == 'main' || inputs.publish))
|
||||
working-directory: easytier-contrib/easytier-ohrs
|
||||
- name: Publish To Center Ohpm
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CODEARTS_PRIVATE_OHPM: ${{ secrets.CODEARTS_PRIVATE_OHPM }}
|
||||
DOWNSTREAM_DISPATCH_TOKEN: ${{ secrets.DOWNSTREAM_DISPATCH_TOKEN }}
|
||||
OHPM_PRIVATE_KEY: ${{ secrets.OHPM_PRIVATE_KEY }}
|
||||
OHPM_KEY_PASSPHRASE: ${{ secrets.OHPM_KEY_PASSPHRASE }}
|
||||
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$GITHUB_EVENT_NAME" = push ]; then
|
||||
pull_requests=$(gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"/repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls")
|
||||
if ! jq -e \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg branch "$GITHUB_REF_NAME" \
|
||||
--arg sha "$GITHUB_SHA" \
|
||||
'any(.[];
|
||||
.merged_at != null and
|
||||
.base.repo.full_name == $repository and
|
||||
.base.ref == $branch and
|
||||
.merge_commit_sha == $sha)' \
|
||||
<<< "$pull_requests" >/dev/null; then
|
||||
echo "Direct push: HAR built without publishing."
|
||||
exit 0
|
||||
fi
|
||||
ohpm config set publish_id "$OHPM_PUBLISH_CODE"
|
||||
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm
|
||||
TMP_DIR=$(mktemp -d)
|
||||
PRIVATE_KEY_FILE="$TMP_DIR/private_key"
|
||||
printf '%s' "$OHPM_PRIVATE_KEY" > "$PRIVATE_KEY_FILE"
|
||||
chmod 600 "$PRIVATE_KEY_FILE"
|
||||
ohpm config set key_path $PRIVATE_KEY_FILE
|
||||
unzip ohpm_crypto.zip -d /home/runner/work/
|
||||
ohpm config set crypto_path /home/runner/work/ohpm_crypto
|
||||
chmod 755 /home/runner/work/ohpm_crypto/*
|
||||
PASSPHRASE="$(printf '%s' "$OHPM_KEY_PASSPHRASE" | tr -d '\r\n')"
|
||||
ohpm config set key_passphrase "$PASSPHRASE"
|
||||
ohpm publish easytier-ohrs.har
|
||||
|
||||
- name: Publish To Private Ohpm
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
|
||||
run: |
|
||||
printf '%s' "${{ secrets.CODEARTS_PRIVATE_OHPM }}" > ~/.ohpm/.ohpmrc
|
||||
ohpm config set strict_ssl false
|
||||
ohpm publish easytier-ohrs.har
|
||||
if [ -f "easytier-release.har" ]; then
|
||||
echo "🚀 Publishing Release package..."
|
||||
ohpm publish easytier-release.har
|
||||
fi
|
||||
curl --header "Content-Type: application/json" --request POST --data "{}" ${{ secrets.CODEARTS_WEBHOOKS }}
|
||||
|
||||
mkdir -p "$HOME/.ohpm"
|
||||
umask 077
|
||||
printf '%s' "$CODEARTS_PRIVATE_OHPM" > "$HOME/.ohpm/.ohpmrc"
|
||||
trap 'rm -f "$HOME/.ohpm/.ohpmrc"' EXIT
|
||||
ohpm publish "$EASYTIER_PACKAGE_NAME.har"
|
||||
|
||||
payload=$(jq -nc \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg ref "refs/heads/$GITHUB_REF_NAME" \
|
||||
--arg package "$EASYTIER_PACKAGE_NAME" \
|
||||
'{
|
||||
event_type: "core-har-published",
|
||||
client_payload: {
|
||||
core_repository: $repository,
|
||||
core_ref: $ref,
|
||||
package_name: $package
|
||||
}
|
||||
}')
|
||||
for repository in \
|
||||
FrankHan052176/EasyTier-ArkTS \
|
||||
FrankHan052176/easytier-pro-app; do
|
||||
curl --fail-with-body --silent --show-error \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $DOWNSTREAM_DISPATCH_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$GITHUB_API_URL/repos/$repository/dispatches" \
|
||||
--data "$payload"
|
||||
done
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
# All of these options are optional, so you can remove them if you are happy with the defaults
|
||||
concurrent_skipping: 'never'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", "easytier-web/**", "easytier-gui/src-tauri/**", "easytier-contrib/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
|
||||
check:
|
||||
name: Run linters & check
|
||||
@@ -98,9 +98,7 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Archive test
|
||||
run: >-
|
||||
cargo nextest archive --archive-file tests.tar.zst
|
||||
--package easytier --package easytier-core --features full
|
||||
run: cargo nextest archive --archive-file tests.tar.zst --package easytier --features full
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
@@ -138,11 +136,6 @@ jobs:
|
||||
- name: Setup system for test
|
||||
run: |
|
||||
sudo modprobe br_netfilter
|
||||
sudo modprobe tun
|
||||
if [ ! -e /dev/net/tun ]; then
|
||||
sudo mkdir -p /dev/net
|
||||
sudo mknod /dev/net/tun c 10 200
|
||||
fi
|
||||
sudo sysctl net.bridge.bridge-nf-call-iptables=0
|
||||
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
|
||||
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
|
||||
@@ -158,8 +151,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo prlimit --pid $$ --nofile=1048576:1048576
|
||||
sudo -E env "PATH=$PATH" EASYTIER_LINUX_BPF_INTEGRATION=required \
|
||||
cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
sudo -E env "PATH=$PATH" cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -34,9 +34,6 @@ easytier-panic.log
|
||||
|
||||
# web
|
||||
node_modules
|
||||
easytier-web/frontend-lib/src/generated/
|
||||
easytier-web/config-generator/dist/
|
||||
easytier-web/config-generator/src/generated/
|
||||
|
||||
.vite
|
||||
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
# EasyTier Domain Context
|
||||
|
||||
## Module layers
|
||||
|
||||
`easytier-core` layers dependencies from `foundation` upward through the
|
||||
portable networking domains. `foundation` contains infrastructure Modules
|
||||
that have no dependency on a networking domain and may be used by any higher
|
||||
layer.
|
||||
|
||||
## Operation broker
|
||||
|
||||
An operation broker owns the lifecycle of asynchronous work submitted by an
|
||||
external caller to core. It allocates opaque operation IDs, arbitrates
|
||||
completion, cancellation, and disposal, retains terminal outcomes, and
|
||||
publishes a batch-drainable completion queue.
|
||||
|
||||
The broker does not interpret operation kinds, outcomes, resources, wire
|
||||
formats, or domain errors. Each domain Module owns those semantics and composes
|
||||
the broker under the same lock as any state that must change atomically with an
|
||||
operation transition.
|
||||
|
||||
Host capability operations use a separate seam. They turn Host readiness into
|
||||
Rust task wakeups and do not share the caller-to-core broker state machine.
|
||||
|
||||
## Compact compatibility Host
|
||||
|
||||
A compact compatibility Host retains accepted values in the authoritative TOML
|
||||
model for management readback, while the shared host-aware normalization path
|
||||
omits capabilities that the compact runtime cannot execute. Omitted settings
|
||||
are silent no-ops and must not be advertised as live network capabilities.
|
||||
+1
-12
@@ -113,17 +113,6 @@ cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
|
||||
|
||||
Build artifacts: `target/[target-triple]/release/`
|
||||
|
||||
### Building the WASI core
|
||||
|
||||
```bash
|
||||
script/build-wasi-core.sh
|
||||
```
|
||||
|
||||
This builds the `easytier-core` Go-host profile for `wasm32-wasip1`, then
|
||||
optimizes it with the pinned official Binaryen release. Binaryen is downloaded
|
||||
once into `target/binaryen/` and verified by SHA-256; set `WASM_OPT` to use an
|
||||
existing matching binary.
|
||||
|
||||
### Building GUI
|
||||
|
||||
```bash
|
||||
@@ -233,4 +222,4 @@ Feel free to:
|
||||
- Join our community discussions
|
||||
- Reach out to maintainers
|
||||
|
||||
Thank you for contributing to EasyTier!
|
||||
Thank you for contributing to EasyTier!
|
||||
Generated
+261
-357
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,9 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"easytier",
|
||||
"easytier-gui/src-tauri",
|
||||
"easytier-web",
|
||||
"easytier-contrib/easytier-mini",
|
||||
"easytier-contrib/easytier-ffi",
|
||||
"easytier-contrib/easytier-uptime",
|
||||
"easytier-contrib/easytier-android-jni",
|
||||
@@ -30,8 +27,3 @@ lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
[profile.mini]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
strip = "symbols"
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
# EasyTier Core Architecture
|
||||
|
||||
## Status and scope
|
||||
|
||||
This document describes the current architecture after the portable-core
|
||||
refactor. It is the source of truth for ownership, dependency direction,
|
||||
feature boundaries, and validation. It intentionally records the resulting
|
||||
design rather than the migration history.
|
||||
|
||||
The refactor has three principal crate roles:
|
||||
|
||||
- `easytier-core` owns portable EasyTier configuration, protocol state,
|
||||
routing, peer state, connectivity orchestration, packet processing, and
|
||||
instance lifecycle.
|
||||
- `easytier` is the native composition root. It owns operating-system
|
||||
resources, native protocol engines, process integration, CLI and native
|
||||
presentation.
|
||||
- `easytier-proto` owns generated protobuf and RPC types, descriptor data, and
|
||||
the feature slices needed by core and presentation users.
|
||||
|
||||
`easytier-core` is designed to compile without direct operating-system network
|
||||
access. It supports native hosts through Rust traits and has a target-only WASI
|
||||
adapter and ABI implementation under `easytier-core/src/wasi`.
|
||||
|
||||
This architecture does not require compatibility with old internal module
|
||||
paths. Wire compatibility, configuration compatibility, management semantics,
|
||||
and externally used application behaviour remain compatibility requirements.
|
||||
|
||||
## Architectural vocabulary
|
||||
|
||||
The following terms have specific meanings in this document:
|
||||
|
||||
- **Module**: an interface and the implementation hidden behind it.
|
||||
- **Host**: the process or runtime embedding core and owning platform
|
||||
resources.
|
||||
- **Host capability**: an operation core may request but must not implement
|
||||
with direct OS calls.
|
||||
- **Adapter**: a concrete implementation of a Host capability or protocol
|
||||
extension.
|
||||
- **Composition root**: code that creates core configuration, Host Adapters,
|
||||
instances, and process-level services.
|
||||
- **Runtime configuration**: the authoritative normalized state used after an
|
||||
instance starts.
|
||||
- **Packet plane**: portable packet classification, routing, transformation,
|
||||
proxy/NAT state, and forwarding decisions.
|
||||
|
||||
New abstractions should pass a deletion test: deleting a useful deep Module
|
||||
should force non-trivial policy or lifecycle logic to reappear in multiple
|
||||
callers. A pass-through wrapper with no independent invariant is not an
|
||||
architectural boundary.
|
||||
|
||||
## Crate dependency direction
|
||||
|
||||
The principal dependency direction is:
|
||||
|
||||
```text
|
||||
easytier-proto <- easytier-core <- easytier
|
||||
```
|
||||
|
||||
Presentation crates and platform integrations consume these crates. Portable
|
||||
policy must not move outward merely because one current consumer is native.
|
||||
Conversely, core must not absorb an OS mechanism or a protocol engine whose
|
||||
dependencies cannot satisfy the core target contract.
|
||||
|
||||
### `easytier-proto`
|
||||
|
||||
The protobuf crate is split by public Cargo features:
|
||||
|
||||
- `core` provides the common wire messages, peer RPC messages, generated RPC
|
||||
runtime, and descriptor bytes needed by core.
|
||||
- `api` adds management API messages.
|
||||
- protocol-specific features add only their generated message modules.
|
||||
- `json-rpc` enables the well-known protobuf JSON types used by the management
|
||||
plane.
|
||||
- `full` is the compatibility aggregate used by complete products.
|
||||
|
||||
The core crate depends on `easytier-proto` with default features disabled and
|
||||
enables only `core`, adding API or JSON-RPC types through its own management
|
||||
features.
|
||||
|
||||
The main core/native path has no `prost-reflect` dependency. OSPF route
|
||||
reflection uses the focused wire editor in
|
||||
`peers/route/route_peer_wire.rs`. It retains the original encoded
|
||||
`RoutePeerInfo`, replaces only the fields credential filtering is allowed to
|
||||
change, and leaves all other top-level and nested fields intact. This is
|
||||
required so unknown fields survive mixed-version, multi-hop propagation.
|
||||
Generated Rust types remain responsible for normal message construction and
|
||||
validation.
|
||||
|
||||
Descriptor sets are still generated and embedded by `easytier-proto`; removing
|
||||
runtime reflection did not remove descriptor data used by configuration and
|
||||
RPC tooling. The OHOS integration has its own schema service and dependency
|
||||
policy and is outside this replacement.
|
||||
|
||||
### `easytier-core`
|
||||
|
||||
Core owns portable behaviour and exposes capability seams. Its normal
|
||||
dependencies use Tokio runtime, time, synchronization, and I/O traits without
|
||||
requiring the full Tokio feature set.
|
||||
|
||||
Core may depend on optional portable engines when their owning feature is
|
||||
enabled. It does not create real native TCP/UDP sockets, alter routes, open a
|
||||
TUN device, enter a network namespace, configure system DNS, manage a service,
|
||||
or invoke UPnP/NAT-PMP directly.
|
||||
|
||||
### `easytier`
|
||||
|
||||
The native crate owns:
|
||||
|
||||
- process startup, shutdown, signals, service management, and allocators;
|
||||
- filesystem configuration input and persistence;
|
||||
- real TCP/UDP, DNS, TUN, raw-socket, route, interface, namespace, and socket
|
||||
option operations;
|
||||
- UPnP and NAT-PMP operations;
|
||||
- Unix and FakeTCP resources;
|
||||
- WebSocket/WSS, QUIC, WireGuard, and KCP concrete engines;
|
||||
- native Magic DNS serving and system DNS integration;
|
||||
- CLI, web, GUI, FFI, and native management presentation.
|
||||
|
||||
Native code may translate values and assemble Adapters. It must not maintain a
|
||||
second peer graph, reproduce core routing or hole-punch policy, or invent an
|
||||
alternative instance lifecycle.
|
||||
|
||||
## Internal core layers
|
||||
|
||||
The physical module layout follows this downward order:
|
||||
|
||||
```text
|
||||
foundation
|
||||
<- config / packet
|
||||
<- socket
|
||||
<- host
|
||||
<- tunnel
|
||||
<- listener / connectivity
|
||||
<- peers / rpc
|
||||
<- gateway
|
||||
<- instance
|
||||
<- management
|
||||
```
|
||||
|
||||
`process_runtime` is a process- or module-scoped owner shared by instances.
|
||||
`wasi` is target integration and is compiled only for tests or the WASI target;
|
||||
it is not an additional portable domain layer.
|
||||
|
||||
### Foundation
|
||||
|
||||
`foundation/` contains task supervision, the time facade, rate limiting,
|
||||
statistics primitives, and the domain-neutral external operation broker. The
|
||||
broker owns asynchronous operation lifecycle and completion storage while the
|
||||
calling domain owns operation kinds, outcomes, resources, and errors.
|
||||
Foundation must not depend on a domain layer.
|
||||
|
||||
### Configuration and packets
|
||||
|
||||
`config/` owns:
|
||||
|
||||
- the complete `TomlConfig` model;
|
||||
- parsing, serialization, and validation;
|
||||
- OS-independent defaults;
|
||||
- peer, encryption, gateway, and API input models;
|
||||
- normalized runtime snapshots and the live runtime configuration store.
|
||||
|
||||
The Host supplies platform facts through `CoreInstanceHostConfig`. Core applies
|
||||
the policy that combines those facts with TOML input. This is especially
|
||||
important for a WASI build: the compile-time guest target cannot be used as a
|
||||
proxy for the Host operating system.
|
||||
|
||||
`packet/` owns EasyTier packet structures, compression, STUN and hole-punch
|
||||
wire codecs. It does not own socket I/O or connection policy.
|
||||
|
||||
### Socket and Host seams
|
||||
|
||||
`socket/` contains transport-neutral primitives:
|
||||
|
||||
- `SocketContext`, including IP-family policy, optional socket mark, and an
|
||||
opaque network-namespace token;
|
||||
- virtual TCP socket, listener, and factory traits;
|
||||
- virtual UDP socket and factory traits;
|
||||
- UDP session multiplexing, classification, and lifecycle;
|
||||
- in-process Ring sockets.
|
||||
|
||||
`host/` is the single home of Host capability seams:
|
||||
|
||||
- DNS and DNS record resolution;
|
||||
- connector environment observations;
|
||||
- packet ingress and egress;
|
||||
- Host socket operation bridges and handle-based TCP/UDP/listener adapters.
|
||||
|
||||
Core owns scheduling, backpressure, cancellation, UDP session state, and
|
||||
protocol state even when each actual operation crosses a Host Adapter. A Host
|
||||
Adapter owns the real resource and performs the OS operation.
|
||||
|
||||
The native `NativeHostRuntime` is process-wide and does not retain an instance
|
||||
`GlobalCtx`, namespace guard, socket mark, or connectivity state. Differences
|
||||
between instances travel in each request's `SocketContext`. A narrow
|
||||
instance-host projection may expose listener and interface facts, but it does
|
||||
not become another socket factory.
|
||||
|
||||
### Tunnel and listener
|
||||
|
||||
A socket is a raw communication endpoint. A Tunnel is an EasyTier connection
|
||||
created by adding framing, metadata, handshakes, and protocol lifecycle.
|
||||
|
||||
Core owns:
|
||||
|
||||
- raw TCP framing and upgrade;
|
||||
- UDP tunnel/session framing and classification;
|
||||
- Ring Tunnel identity and registry state;
|
||||
- encryption and secure-datagram policy that is portable;
|
||||
- client/server protocol selection interfaces;
|
||||
- listener planning, optional/required listener policy, retry, accept
|
||||
scheduling, running-listener registry, and orderly shutdown.
|
||||
|
||||
Native protocol Adapters own WebSocket/WSS, QUIC, WireGuard, and KCP engines.
|
||||
Unix and FakeTCP are socket resources that feed a core protocol upgrader; they
|
||||
are not independent owners of EasyTier peer state.
|
||||
|
||||
Each protocol registration must provide a coherent client/server Adapter.
|
||||
Unavailable configured transports must be rejected during validation or
|
||||
protocol selection in the standard runtime, rather than silently falling back
|
||||
to another transport. A compact compatibility Host may instead retain the
|
||||
desired value for management readback and omit it from normalized runtime
|
||||
state; it must not advertise or partially activate the unavailable transport.
|
||||
|
||||
### Connectivity
|
||||
|
||||
`connectivity/` owns:
|
||||
|
||||
- manual connection and endpoint discovery policy;
|
||||
- direct candidate selection;
|
||||
- retry, backoff, blacklists, and listener reuse;
|
||||
- STUN requests, responses, probing, NAT inference, and published endpoint
|
||||
state;
|
||||
- TCP and UDP hole-punch state machines;
|
||||
- UDP port-mapping policy and lease lifecycle;
|
||||
- conversion of successful sockets into protocol-upgrade requests.
|
||||
|
||||
The Host owns DNS execution, socket syscalls, interface enumeration, bind
|
||||
device/mark/namespace operations, and concrete UPnP/NAT-PMP calls. STUN-only
|
||||
hole punching remains available when the Host does not supply a port-mapping
|
||||
Adapter.
|
||||
|
||||
Some connectivity files intentionally implement peer-facing adapter traits for
|
||||
`PeerManagerCore`. These are localized integration edges between adjacent
|
||||
domains, not permission for lower socket or Host layers to depend on peers.
|
||||
|
||||
### Peers and RPC
|
||||
|
||||
`peers/` is the authoritative owner of:
|
||||
|
||||
- admission and connection sessions;
|
||||
- peer maps and connection lifecycle;
|
||||
- ACL and whitelist decisions;
|
||||
- OSPF route calculation and graph algorithms;
|
||||
- peer and credential RPC registration;
|
||||
- foreign-network admission, identity, relay, and lifecycle;
|
||||
- peer-center state and public IPv6 policy;
|
||||
- traffic metrics and peer snapshots.
|
||||
|
||||
Submodules progress from kernel types and utilities, through ACL/context,
|
||||
connection state, route state, manager services, and finally foreign-network
|
||||
and peer-center composition. Callers consume the public surface declared by
|
||||
the domain rather than reaching into a parallel native peer owner.
|
||||
|
||||
`rpc/` owns the peer-flavoured RPC transport, packet fragmentation, client and
|
||||
server lifecycle, handler registry, and standalone listener/client lifecycle.
|
||||
Generated service descriptors and message types remain in `easytier-proto`.
|
||||
|
||||
### Gateway
|
||||
|
||||
`gateway/` owns portable packet-plane features:
|
||||
|
||||
- proxy CIDR state and monitoring policy;
|
||||
- packet parsing, reassembly, NAT/proxy state, and TCP/UDP/ICMP decisions;
|
||||
- the smoltcp-backed portable dataplane selected by its feature;
|
||||
- SOCKS5 framing, authentication, association, routing, and session state;
|
||||
- wrapped-transport planning and session state used by KCP and QUIC Adapters;
|
||||
- DHCP allocation policy;
|
||||
- Magic DNS route and response policy;
|
||||
- VPN portal client/session policy;
|
||||
- UDP broadcast classification and rewrite policy.
|
||||
|
||||
TUN, raw sockets, transparent-destination lookup, concrete protocol engines,
|
||||
native DNS servers, namespace operations, and route application stay in native
|
||||
Adapters.
|
||||
|
||||
Optional gateway capabilities are selected by cohesive Modules. Disabled
|
||||
implementations retain stable lifecycle calls and report unsupported
|
||||
configuration in the standard runtime. A compact compatibility Host may
|
||||
silently normalize those settings to no-ops while preserving the desired TOML
|
||||
model; disabled implementations do not duplicate portable policy.
|
||||
|
||||
The instance-scoped `DataPlaneSession` composes the foundation operation broker
|
||||
under the same session lock as its resource and quota state. The broker owns
|
||||
generic completion, cancellation, free, drain, and take transitions. The data
|
||||
plane retains TCP/UDP resource ownership, operation metadata, route deadlines,
|
||||
and error semantics.
|
||||
|
||||
The proposed restructuring of the smoltcp data plane, SOCKS5 and port-forward
|
||||
Adapters, portable KCP engine, event-driven FFI/WASI completion model, and Go
|
||||
Host integration is tracked in
|
||||
[`data-plane-runtime-plan.md`](data-plane-runtime-plan.md). That document is a
|
||||
future implementation plan; this document remains the source of truth for the
|
||||
currently implemented architecture until the plan is completed.
|
||||
|
||||
### Instance and management
|
||||
|
||||
`CoreInstance::new(CoreInstanceConfig, CoreHostAdapters)` is the sole direct
|
||||
construction path for a normalized instance. `CoreInstance::from_toml` uses
|
||||
the same normalization and construction path. Core constructs the peer graph,
|
||||
runtime store, STUN collector, connectivity managers, listener runtime, packet
|
||||
plane, gateway runtimes, and lifecycle owners.
|
||||
|
||||
A core instance:
|
||||
|
||||
- owns all mutable portable state for one network;
|
||||
- is one-shot after `stop`;
|
||||
- exposes one complete `start` and one `stop` lifecycle interface;
|
||||
- starts Modules in a fixed serial composition order without cross-Module
|
||||
started flags or staged activation;
|
||||
- installs initial ACL, proxy CIDR, and manual-peer inputs before startup;
|
||||
- serializes lifecycle operations with one instance-level operation lock;
|
||||
- owns cooperative cancellation and component shutdown order;
|
||||
- exposes `CorePacketPlane` as the narrow packet/route projection used by Host
|
||||
dataplane Adapters;
|
||||
- treats its normalized runtime store as authoritative after construction.
|
||||
|
||||
`CoreHostAdapters` contains the required Host, DNS, packet sink, and
|
||||
`CoreProcessRuntime`, plus optional protocol and platform capabilities. The
|
||||
bundle carries capabilities, not preconstructed portable managers.
|
||||
|
||||
Each Module owns partial-start cleanup for its internal resources.
|
||||
`CoreInstance` has one outer cancellation and recovery path for the complete
|
||||
serial startup. `Running` therefore means the Host runtime and every enabled
|
||||
portable Module have started successfully; there is no separate post-Host
|
||||
activation state. Host packet tasks stop before PeerManager resources are
|
||||
cleared.
|
||||
|
||||
`InstanceManager<F>` is the canonical UUID-indexed instance collection for one
|
||||
Host composition. Its `InstanceFactory` constructs one complete record before
|
||||
the manager performs an atomic uniqueness check. The manager owns collection
|
||||
membership; it does not own startup order, persistence, daemon policy, cached
|
||||
errors, ABI handles, or RPC projections.
|
||||
|
||||
`management/` consumes the canonical manager and instances. It owns:
|
||||
|
||||
- stable UUID/name selection;
|
||||
- read-only instance and peer management RPC;
|
||||
- full process mutation and configuration transactions when enabled;
|
||||
- persistence and logger-control capability interfaces;
|
||||
- management listener/client lifecycle and JSON-RPC presentation.
|
||||
|
||||
There is one process-level management entry. Instances and the manager do not
|
||||
depend on management response projections.
|
||||
|
||||
## Process-scoped state
|
||||
|
||||
`CoreProcessRuntime` owns portable resources shared across instances in one
|
||||
process or instantiated module:
|
||||
|
||||
- the Ring Tunnel registry and namespace;
|
||||
- a reference-counted protected TCP-port registry.
|
||||
|
||||
The composition root creates and shares one runtime. Management listener ports
|
||||
are protected before bind and held by leases after the concrete port is known.
|
||||
Native and target adapters supply bound resources but do not implement a
|
||||
second protected-port registry.
|
||||
|
||||
Process-global capability objects may contain stateless or shared platform
|
||||
mechanisms. They must not contain instance-specific peer, route,
|
||||
configuration, or connectivity state.
|
||||
|
||||
## Runtime configuration authority
|
||||
|
||||
`TomlConfig` is the authoritative desired configuration used for management
|
||||
readback and patch transactions. Compact Hosts keep unsupported accepted values
|
||||
there so controllers observe the configuration they submitted.
|
||||
|
||||
The separately typed, normalized core runtime store is authoritative for live
|
||||
behavior:
|
||||
|
||||
- peer feature flags and routing policy;
|
||||
- listeners and initial peers;
|
||||
- ACL and whitelist inputs;
|
||||
- manual and VPN portal CIDRs;
|
||||
- gateway and connectivity settings;
|
||||
- runtime configuration patches.
|
||||
|
||||
Host persistence is an effect following a successful core transaction. A Host
|
||||
Adapter must not call back into an instance to obtain a hidden configuration
|
||||
snapshot while core is applying an operation.
|
||||
|
||||
Non-serializable resources such as TUN descriptors, packet sinks, execution
|
||||
domains, and native protocol engines are construction context, not TOML
|
||||
fields.
|
||||
|
||||
## Logging
|
||||
|
||||
The main native runtime uses a small logger implemented in
|
||||
`easytier/src/common/log`:
|
||||
|
||||
- `log` records and `tracing` events share console and file sinks;
|
||||
- timestamps, compact formatting, optional terminal colours, `NO_COLOR`, and
|
||||
basic `RUST_LOG` target/level filters are implemented directly;
|
||||
- file rotation uses the existing EasyTier rolling appender;
|
||||
- management RPC can reload the file level;
|
||||
- an atomic maximum-level gate rejects disabled events before target matching
|
||||
or file-filter locking;
|
||||
- concurrent file-level reload serializes the filter and atomic-level update.
|
||||
|
||||
File logging and no-file logging are separate selected backends. The default
|
||||
tracing backend records events and deliberately ignores span trees. The
|
||||
optional `tracing` feature selects the tokio-console subscriber integration;
|
||||
only that diagnostic profile pulls the main crate's `tracing-subscriber` and
|
||||
`console-subscriber` dependencies.
|
||||
|
||||
Contrib applications and platform integrations may have independent logging
|
||||
requirements and are not implicitly wired to the native process logger.
|
||||
|
||||
## Feature model
|
||||
|
||||
Features represent coherent capabilities, not arbitrary source fragments.
|
||||
Important core feature relationships are:
|
||||
|
||||
- `management-rpc` enables generated management API types and read-only
|
||||
management services.
|
||||
- `management` adds configuration writes, full management composition, rich
|
||||
errors, and JSON-RPC.
|
||||
- `proxy-packet` enables portable packet parsing/proxy machinery and the
|
||||
required smoltcp packet features.
|
||||
- `proxy-smoltcp-stack` adds the async TCP/UDP smoltcp stack.
|
||||
- `dns-resolver` is the shared Hickory resolver leaf used by endpoint
|
||||
discovery and Magic DNS without coupling either capability to the other.
|
||||
- `endpoint-discovery` adds HTTPS endpoint discovery dependencies.
|
||||
- `magic-dns` enables its DNS server, management wire messages, and portable
|
||||
packet-query integration.
|
||||
- `tcp-hole-punch` enables the TCP hole-punch runtime.
|
||||
- `dhcp-ipv4`, `public-ipv6-provider`, `vpn-portal`,
|
||||
`wrapped-transport`, and `proxy-cidr-monitor` are independent gateway or
|
||||
platform-policy leaves.
|
||||
- `extended-services` is the compatibility aggregate for those leaves.
|
||||
- encryption and compression engines remain independently selectable.
|
||||
|
||||
The native crate maps product features to the core and protocol features it
|
||||
actually consumes. A protocol feature must not accidentally enable unrelated
|
||||
gateway or management capabilities.
|
||||
|
||||
Production feature and platform selection belongs at Module or Adapter
|
||||
boundaries rather than inside shared implementations. The logger demonstrates
|
||||
the intended pattern: file and tracing variants are complete backend modules
|
||||
with one stable interface, so shared event processing contains no feature
|
||||
branches.
|
||||
|
||||
## Module boundaries
|
||||
|
||||
The dependency directions in this document define the intended module
|
||||
boundaries. Changes that require a new upward edge must first define a stable
|
||||
lower-layer interface or explicitly revise this architecture.
|
||||
|
||||
Modules are `pub(crate)` by default. Each domain's `mod.rs` declares its
|
||||
outward surface. Public visibility is used for real cross-crate Host,
|
||||
configuration, management, packet-plane, or test-support interfaces.
|
||||
|
||||
## Architectural invariants
|
||||
|
||||
1. Portable EasyTier policy has one owner in `easytier-core`.
|
||||
2. Core does not perform real OS socket, DNS, TUN, route, filesystem
|
||||
configuration, process, or service-manager operations.
|
||||
3. Host-OS policy is runtime input; a WASI compile target is not Host policy.
|
||||
4. Every real socket and DNS operation crosses a Host capability seam.
|
||||
5. Core owns socket scheduling, backpressure, protocol state, and cancellation.
|
||||
6. Dial, accept, and hole-punch paths produce sockets before protocol upgrade.
|
||||
7. Peer admission consumes upgraded transports and does not create OS
|
||||
resources.
|
||||
8. Each instance owns its mutable peer, route, connectivity, gateway, and
|
||||
runtime configuration state.
|
||||
9. One Host composition has one canonical UUID-to-instance manager.
|
||||
10. Process-level runtimes do not capture instance state.
|
||||
11. `CoreInstance::new` is the sole normalized direct construction entry.
|
||||
12. The manager owns membership, not lifecycle or presentation.
|
||||
13. Management consumes the manager; the manager does not return management
|
||||
projections.
|
||||
14. Unknown protobuf fields in reflected route information survive forwarding
|
||||
and credential filtering.
|
||||
15. Feature selection is localized at cohesive Module/Adapter boundaries.
|
||||
16. The standard runtime rejects unsupported configured capabilities. Compact
|
||||
compatibility Hosts may preserve them as runtime no-ops, but never change
|
||||
wire protocol, advertise them, or silently fall back to an unsafe mode.
|
||||
|
||||
## Validation
|
||||
|
||||
Changes to these boundaries should run, at minimum:
|
||||
|
||||
```text
|
||||
cargo fmt --all -- --check
|
||||
cargo check -p easytier-core -p easytier-proto -p easytier --features full
|
||||
cargo test -p easytier-core --lib
|
||||
```
|
||||
|
||||
Feature work should add focused checks for the changed no-default, isolated,
|
||||
default, full, and cross-target profiles. Socket, TUN, namespace, protocol
|
||||
engine, and multi-node changes require the relevant Docker integration tests.
|
||||
WASI ABI or Adapter changes require a `wasm32-wasip1` build and target-side
|
||||
tests. These compiler-resolved profiles are the authority for feature and
|
||||
target boundaries.
|
||||
|
||||
CI path filters include `easytier-core`, `easytier-proto`, native, web, GUI
|
||||
Tauri, and contrib. The archived Rust test suite contains both `easytier` and
|
||||
`easytier-core`.
|
||||
|
||||
## Known limitations and debt
|
||||
|
||||
- Some production feature and platform gates still select fields or statements
|
||||
inside shared implementations. New code should prefer complete Module or
|
||||
Adapter variants, and existing cases should move only when their owning
|
||||
Module is changed.
|
||||
- Connectivity retains localized Adapter implementations that name
|
||||
`PeerManagerCore`; further decoupling requires an interface extraction, not
|
||||
a visibility-only move.
|
||||
- Native Linux namespace guards exist in paths that can cross async suspension.
|
||||
Because `setns` is thread-local, those operations should eventually be kept
|
||||
on one non-migrating execution context.
|
||||
- QUIC session retirement after failed or exhausted accepted sessions remains
|
||||
separate native-engine correctness work; it must preserve multiple
|
||||
connections sharing one QUIC endpoint/session.
|
||||
|
||||
These limitations are not reasons to add fallback owners or parallel state.
|
||||
Fixes should preserve the ownership rules above and address the responsible
|
||||
Module directly.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
# HarmonyOS HAR delivery
|
||||
|
||||
The `ohos` workflow builds the Core HAR on pushes, pull requests, tags, and
|
||||
manual runs. Every successful run retains a short-lived HAR artifact, while
|
||||
publication to the private OHPM registry is deliberately restricted:
|
||||
|
||||
- A push to `main` publishes only when the pushed SHA is the merge commit of a
|
||||
pull request targeting `main` and the push is not forced.
|
||||
- A manual run on `main` publishes by default.
|
||||
- A manual run on another branch publishes only when its `publish` input is
|
||||
enabled.
|
||||
- Direct pushes, pull requests, tags, and ordinary non-main branch builds do
|
||||
not publish.
|
||||
|
||||
## Package identity
|
||||
|
||||
All branches publish the same private package name, `easytier-ohrs`. The
|
||||
source branch is encoded in the package version instead of the package name:
|
||||
|
||||
```text
|
||||
<core-version>-<branch-id>-<commits-since-tag>-<run-number>-<run-attempt>-g<short-sha>
|
||||
```
|
||||
|
||||
`branch-id` is a lowercase, OHPM-safe form of the source branch. Publishing a
|
||||
new version advances the registry's `latest` version. After publication, Core
|
||||
sends the `core-har-published` repository dispatch to the ArkTS and Pro
|
||||
repositories. The payload contains only `core_repository`, `core_ref`, and
|
||||
`package_name`.
|
||||
|
||||
## App install sequence
|
||||
|
||||
ArkTS and Pro use the same three OHPM commands:
|
||||
|
||||
```bash
|
||||
ohpm uninstall "$CORE_HAR_PACKAGE"
|
||||
ohpm install "$CORE_HAR_PACKAGE@latest" \
|
||||
--registry "$CORE_HAR_REGISTRY"
|
||||
ohpm install
|
||||
```
|
||||
|
||||
The App workflow then reads the installed version from:
|
||||
|
||||
```text
|
||||
oh_modules/<package_name>/oh-package.json5
|
||||
```
|
||||
|
||||
The existing `oh-package-lock.json5` and `oh_modules` directory are not
|
||||
manually deleted. Because the package name remains `easytier-ohrs`, downstream
|
||||
source imports do not need to be rewritten.
|
||||
|
||||
## Secrets
|
||||
|
||||
Core requires:
|
||||
|
||||
- `CODEARTS_PRIVATE_OHPM`: publish-capable OHPM configuration.
|
||||
- `DOWNSTREAM_DISPATCH_TOKEN`: permission to dispatch both App repositories.
|
||||
|
||||
ArkTS and Pro require:
|
||||
|
||||
- `CODEARTS_PRIVATE_OHPM_READ`: read-only private OHPM authentication.
|
||||
- `SIGNING_REPOSITORY_TOKEN`: read access to the corresponding private signing
|
||||
repository.
|
||||
|
||||
Signing and AppGallery Connect credentials remain downstream application
|
||||
concerns and are not passed through the Core dispatch payload.
|
||||
@@ -1,176 +0,0 @@
|
||||
# QUIC TCP Proxy 内存对比(2026-07-27)
|
||||
|
||||
## 结论
|
||||
|
||||
在相同的双节点 network namespace 环境中,当前分支相对 2.6.4:
|
||||
|
||||
- 空闲且未建立 TCP proxy 连接时,两端合计 USS 从 15.95 MiB
|
||||
降至 11.66 MiB,下降 26.9%;
|
||||
- 66 条空闲 TCP proxy 连接时,两端合计 USS 从 19.45 MiB
|
||||
降至 13.85 MiB,下降 28.8%;
|
||||
- 固定 1 Gbit/s 的单流 TCP proxy 传输中,两端平均 USS 从
|
||||
20.69 MiB 降至 14.75 MiB,下降 28.7%,同步峰值从
|
||||
21.50 MiB 降至 15.02 MiB;
|
||||
- 从 0 增长到 66 条空闲连接推算,每条连接在两个 core 上合计
|
||||
增加约 33.9 KiB USS;2.6.4 为 54.2 KiB,下降 37.4%;
|
||||
- 当前分支的匿名内存下降约 40% 至 44%,说明堆和连接缓冲区开销
|
||||
确实降低。
|
||||
|
||||
当前分支的 RSS 比 2.6.4 高约 5% 至 11%,但这部分差异没有出现在
|
||||
Anonymous 中,主要体现为非匿名或共享驻留页。PSS 在高连接数及
|
||||
固定吞吐场景基本持平,USS 和 Anonymous 则显著更低。因此不能
|
||||
只根据 RSS 判断发生了内存回退。
|
||||
|
||||
## 测试对象
|
||||
|
||||
| 版本 | 标识 | 二进制 |
|
||||
|---|---|---|
|
||||
| 当前分支 | commit `9e2ed33aeb37`,版本 `2.6.4-9e2ed33a` | `target/x86_64-unknown-linux-musl/release/easytier-core` |
|
||||
| 2.6.4 | 版本 `2.6.4-8428a89d` | `/data/tickets/easytier/easytier-linux-x86_64/easytier-core` |
|
||||
|
||||
当前分支使用以下命令重新构建,确保被测二进制准确对应 HEAD:
|
||||
|
||||
```console
|
||||
cargo build --release \
|
||||
--target x86_64-unknown-linux-musl \
|
||||
-p easytier \
|
||||
--features jemalloc \
|
||||
--bin easytier-core \
|
||||
--bin easytier-cli
|
||||
```
|
||||
|
||||
两个二进制均为 stripped static PIE。当前分支明确使用 musl 和
|
||||
jemalloc。
|
||||
|
||||
## 测试拓扑
|
||||
|
||||
- 两个 `easytier-core` 分别运行在独立的 network namespace;
|
||||
- namespace 通过 Linux bridge 和 veth 连接;
|
||||
- underlay 地址为 `10.251.89.10/24` 和 `10.251.89.11/24`;
|
||||
- EasyTier 虚拟地址为 `10.144.144.1/24` 和
|
||||
`10.144.144.2/24`;
|
||||
- 两个节点之间使用 UDP listener 建立 EasyTier peer 连接;
|
||||
- 源节点启用 `--enable-quic-proxy true`;
|
||||
- 两个节点均保留默认 QUIC input;
|
||||
- TCP client 从 `10.144.144.1` 访问绑定在
|
||||
`10.144.144.2` 上的 server;
|
||||
- `tcp_proxy_connect` 指标的 `protocol` 标签确认为 `QUIC`;
|
||||
- 66 条连接场景通过两端各 132 个 established TCP socket 条目
|
||||
确认当前连接数。
|
||||
|
||||
## 采样口径
|
||||
|
||||
数据读取自 `/proc/<pid>/smaps_rollup`:
|
||||
|
||||
- RSS:进程映射的全部驻留页,包含共享代码页;
|
||||
- PSS:共享页按共享进程数量分摊后的驻留内存;
|
||||
- USS:`Private_Clean + Private_Dirty`,表示进程独占内存;
|
||||
- Anonymous:匿名页,主要反映堆、栈和运行时缓冲区。
|
||||
|
||||
空闲场景每隔 2 秒采样一次,共 5 次,表格记录均值。固定吞吐场景
|
||||
持续 20 秒,每隔 2 秒采样一次,共 8 次,同时记录均值和峰值。
|
||||
所有容量单位均为 MiB。
|
||||
|
||||
## 空闲连接结果
|
||||
|
||||
以下数据均为两个 EasyTier core 的合计值:
|
||||
|
||||
| 当前连接数 | 当前 RSS | 2.6.4 RSS | 当前 PSS | 2.6.4 PSS | 当前 USS | 2.6.4 USS | USS 变化 | 当前 Anonymous | 2.6.4 Anonymous |
|
||||
|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 40.42 | 36.33 | 26.03 | 24.50 | 11.66 | 15.95 | -26.9% | 8.89 | 15.62 |
|
||||
| 1 | 41.52 | 38.46 | 26.49 | 25.98 | 11.46 | 16.82 | -31.9% | 9.08 | 16.23 |
|
||||
| 10 | 41.63 | 38.79 | 26.59 | 26.32 | 11.56 | 17.15 | -32.6% | 9.19 | 16.55 |
|
||||
| 66 | 43.68 | 41.11 | 28.76 | 28.63 | 13.85 | 19.45 | -28.8% | 11.22 | 18.79 |
|
||||
|
||||
0 条和 1 条连接之间的小幅反向波动属于分配器回收和采样时序噪声,
|
||||
不能解释为连接产生负开销。使用 0 到 66 条连接的跨度估算单位
|
||||
连接成本更稳定。
|
||||
|
||||
### 分节点 USS
|
||||
|
||||
| 当前连接数 | 当前源端 | 当前目的端 | 2.6.4 源端 | 2.6.4 目的端 |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 0 | 5.45 | 6.21 | 8.07 | 7.88 |
|
||||
| 1 | 5.43 | 6.03 | 8.68 | 8.14 |
|
||||
| 10 | 5.49 | 6.07 | 8.90 | 8.25 |
|
||||
| 66 | 6.60 | 7.25 | 10.19 | 9.25 |
|
||||
|
||||
### 单位连接增量
|
||||
|
||||
以 0 到 66 条连接的 USS 增量计算:
|
||||
|
||||
| 版本 | 两端 USS 增量 | 每连接两端合计 | 每连接单端平均 |
|
||||
|---|---:|---:|---:|
|
||||
| 当前分支 | 2.19 MiB | 33.9 KiB | 17.0 KiB |
|
||||
| 2.6.4 | 3.50 MiB | 54.2 KiB | 27.1 KiB |
|
||||
|
||||
当前分支的每连接独占内存增量下降约 37.4%。
|
||||
|
||||
## 固定 1 Gbit/s 活跃流量
|
||||
|
||||
为排除两个版本最大吞吐不同造成的缓冲区差异,使用
|
||||
`iperf3 -b 1G -P 1 -t 20` 将两个版本都限制为 1 Gbit/s。
|
||||
两次测试均实际完成 2.33 GiB 传输,接收端报告 1000 Mbit/s。
|
||||
|
||||
### 平均值
|
||||
|
||||
| 版本 | 节点 | RSS | PSS | USS | Anonymous |
|
||||
|---|---|---:|---:|---:|---:|
|
||||
| 当前分支 | 源端 | 22.89 | 15.41 | 7.94 | 6.88 |
|
||||
| 当前分支 | 目的端 | 21.77 | 14.29 | 6.82 | 5.27 |
|
||||
| 当前分支 | 两端合计 | 44.67 | 29.70 | 14.75 | 12.14 |
|
||||
| 2.6.4 | 源端 | 22.59 | 16.34 | 11.70 | 11.35 |
|
||||
| 2.6.4 | 目的端 | 19.95 | 13.67 | 8.99 | 8.77 |
|
||||
| 2.6.4 | 两端合计 | 42.53 | 30.01 | 20.69 | 20.13 |
|
||||
|
||||
### 对比
|
||||
|
||||
| 指标 | 当前分支 | 2.6.4 | 变化 |
|
||||
|---|---:|---:|---:|
|
||||
| 两端平均 RSS | 44.67 | 42.53 | +5.0% |
|
||||
| 两端平均 PSS | 29.70 | 30.01 | -1.0% |
|
||||
| 两端平均 USS | 14.75 | 20.69 | -28.7% |
|
||||
| 两端平均 Anonymous | 12.14 | 20.13 | -39.7% |
|
||||
| 两端同步峰值 USS | 15.02 | 21.50 | -30.2% |
|
||||
|
||||
## 分节点原始统计
|
||||
|
||||
下表保留各场景所有样本计算出的均值;`max_uss` 是该节点采样期间
|
||||
的最大 USS。
|
||||
|
||||
| 版本 | 场景 | 节点 | 样本数 | mean_rss | mean_pss | mean_uss | mean_anon | max_uss |
|
||||
|---|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| 当前 | 0 连接 | 源端 | 5 | 19.830 | 12.636 | 5.451 | 4.314 | 5.582 |
|
||||
| 当前 | 0 连接 | 目的端 | 5 | 20.587 | 13.393 | 6.208 | 4.579 | 6.320 |
|
||||
| 当前 | 1 连接 | 源端 | 5 | 20.463 | 12.944 | 5.432 | 4.401 | 5.465 |
|
||||
| 当前 | 1 连接 | 目的端 | 5 | 21.061 | 13.541 | 6.030 | 4.682 | 6.051 |
|
||||
| 当前 | 10 连接 | 源端 | 5 | 20.522 | 13.002 | 5.491 | 4.459 | 5.496 |
|
||||
| 当前 | 10 连接 | 目的端 | 5 | 21.105 | 13.585 | 6.073 | 4.726 | 6.086 |
|
||||
| 当前 | 66 连接 | 源端 | 5 | 21.513 | 14.050 | 6.595 | 5.498 | 6.672 |
|
||||
| 当前 | 66 连接 | 目的端 | 5 | 22.169 | 14.706 | 7.251 | 5.723 | 7.375 |
|
||||
| 当前 | 1 Gbit/s | 源端 | 8 | 22.893 | 15.410 | 7.936 | 6.877 | 8.188 |
|
||||
| 当前 | 1 Gbit/s | 目的端 | 8 | 21.773 | 14.291 | 6.816 | 5.266 | 6.832 |
|
||||
| 2.6.4 | 0 连接 | 源端 | 5 | 18.278 | 12.355 | 8.071 | 7.876 | 8.328 |
|
||||
| 2.6.4 | 0 连接 | 目的端 | 5 | 18.048 | 12.144 | 7.880 | 7.747 | 8.203 |
|
||||
| 2.6.4 | 1 连接 | 源端 | 5 | 19.535 | 13.279 | 8.676 | 8.262 | 8.727 |
|
||||
| 2.6.4 | 1 连接 | 目的端 | 5 | 18.920 | 12.705 | 8.143 | 7.971 | 8.191 |
|
||||
| 2.6.4 | 10 连接 | 源端 | 5 | 19.762 | 13.506 | 8.902 | 8.473 | 8.910 |
|
||||
| 2.6.4 | 10 连接 | 目的端 | 5 | 19.027 | 12.812 | 8.250 | 8.078 | 8.297 |
|
||||
| 2.6.4 | 66 连接 | 源端 | 5 | 21.069 | 14.805 | 10.194 | 9.702 | 10.320 |
|
||||
| 2.6.4 | 66 连接 | 目的端 | 5 | 20.045 | 13.822 | 9.252 | 9.088 | 9.293 |
|
||||
| 2.6.4 | 1 Gbit/s | 源端 | 8 | 22.588 | 16.343 | 11.697 | 11.354 | 12.258 |
|
||||
| 2.6.4 | 1 Gbit/s | 目的端 | 8 | 19.946 | 13.670 | 8.993 | 8.774 | 9.277 |
|
||||
|
||||
## 解释和限制
|
||||
|
||||
1. 以固定 1 Gbit/s 场景为例,当前分支 RSS 增加 5.0%,但
|
||||
Anonymous 下降 39.7%,PSS 下降 1.0%。这说明差异主要体现
|
||||
在非匿名或共享驻留页;本次没有保存逐 VMA 数据,因此不进一步
|
||||
将它归因到某一个具体映射。
|
||||
2. 两个相同版本进程运行在同一宿主机时会共享可执行文件代码页,
|
||||
所以 PSS 比 RSS 更适合估算该测试拓扑的宿主机总成本,USS 和
|
||||
Anonymous 更适合判断 EasyTier 私有堆及缓冲区的变化。
|
||||
3. 这是一轮受控 A/B 测试,而不是长期统计分布。数值可用于确认
|
||||
差异方向和量级;若作为发布门禁,应固定机器负载并增加多轮重复。
|
||||
4. 本文只比较 QUIC TCP proxy 内存,不使用未限速吞吐结果推断性能,
|
||||
避免吞吐差异污染内存结论。
|
||||
@@ -14,4 +14,4 @@ android_logger = "0.13"
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false, features = ["ffi-dataplane"] }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
global:
|
||||
Java_com_easytier_jni_EasyTierJNI_*;
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package com.easytier.jni
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* EasyTier data-plane API for Android.
|
||||
*
|
||||
* Dataplane APIs do not create or start an EasyTier instance by themselves.
|
||||
* Start an instance with [EasyTierJNI.runNetworkInstance] first, then pass the
|
||||
* same `instanceName` to [EasyTierDataPlane.tcpConnect],
|
||||
* [EasyTierDataPlane.tcpBind], or [EasyTierDataPlane.udpBind]. If that instance
|
||||
* is not running, the native start call fails and the coroutine wrapper throws
|
||||
* the last EasyTier FFI error.
|
||||
*
|
||||
* Typical setup:
|
||||
* ```
|
||||
* val instanceName = "android-dataplane-demo"
|
||||
* val config = """
|
||||
* instance_name = "$instanceName"
|
||||
* ipv4 = "10.144.0.1"
|
||||
* listeners = ["tcp://0.0.0.0:11010"]
|
||||
*
|
||||
* [network_identity]
|
||||
* network_name = "android-dataplane-demo"
|
||||
* network_secret = "replace-with-a-real-secret"
|
||||
*
|
||||
* [[peer]]
|
||||
* uri = "tcp://peer.example.com:11010"
|
||||
*
|
||||
* [flags]
|
||||
* no_tun = true
|
||||
* bind_device = false
|
||||
* """.trimIndent()
|
||||
*
|
||||
* EasyTierJNI.runNetworkInstance(config)
|
||||
* ```
|
||||
*
|
||||
* After the instance is running, most callers should use [EasyTierDataPlane]
|
||||
* and the socket/stream classes below. [EasyTierDataPlaneJNI] is the low-level
|
||||
* native op-handle ABI used by the coroutine wrappers.
|
||||
*
|
||||
* TCP client usage:
|
||||
* ```
|
||||
* val stream = EasyTierDataPlane.tcpConnect(instanceName, "10.144.0.2", 8080, 5_000)
|
||||
* try {
|
||||
* stream.write("ping".toByteArray(), 5_000)
|
||||
* val reply = stream.read(4096, 5_000)
|
||||
* } finally {
|
||||
* stream.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* TCP server usage:
|
||||
* ```
|
||||
* val listener = EasyTierDataPlane.tcpBind(instanceName, 8080, 5_000)
|
||||
* try {
|
||||
* val stream = listener.accept(30_000)
|
||||
* try {
|
||||
* stream.write(stream.read(4096, 5_000), 5_000)
|
||||
* } finally {
|
||||
* stream.close()
|
||||
* }
|
||||
* } finally {
|
||||
* listener.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* UDP usage:
|
||||
* ```
|
||||
* val socket = EasyTierDataPlane.udpBind(instanceName, 0, 5_000)
|
||||
* try {
|
||||
* socket.sendTo("10.144.0.2", 9000, "ping".toByteArray(), 5_000)
|
||||
* val packet = socket.recvFrom(4096, 5_000)
|
||||
* } finally {
|
||||
* socket.close()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Operation model:
|
||||
* - Each suspend function starts one native async op, waits on Dispatchers.IO,
|
||||
* then consumes the op with the matching finish call.
|
||||
* - Coroutine cancellation cancels and frees the native op.
|
||||
* - Returned stream/listener/socket handles must be closed by the caller.
|
||||
* - Input ByteArray data is copied by the native start call; output data is
|
||||
* copied into Kotlin ByteArray before the native buffer is freed.
|
||||
*/
|
||||
|
||||
/** Data-plane IPv4/port pair returned by EasyTier FFI. */
|
||||
data class DataPlaneSocketAddress(val ip: String, val port: Int)
|
||||
|
||||
/** Result of a completed TCP connect op. */
|
||||
data class DataPlaneTcpConnectResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed TCP bind op. */
|
||||
data class DataPlaneTcpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed TCP accept op. */
|
||||
data class DataPlaneTcpAcceptResult(
|
||||
val handle: Long,
|
||||
val localAddress: DataPlaneSocketAddress,
|
||||
val peerAddress: DataPlaneSocketAddress
|
||||
)
|
||||
|
||||
/** Result of a completed TCP read op. */
|
||||
data class DataPlaneTcpReadResult(val data: ByteArray)
|
||||
|
||||
/** Result of a completed UDP bind op. */
|
||||
data class DataPlaneUdpBindResult(val handle: Long, val localAddress: DataPlaneSocketAddress)
|
||||
|
||||
/** Result of a completed UDP recv_from op. */
|
||||
data class DataPlaneUdpRecvResult(
|
||||
val data: ByteArray,
|
||||
val peerAddress: DataPlaneSocketAddress
|
||||
)
|
||||
|
||||
/** TCP data-plane stream handle. Call [close] when the stream is no longer needed. */
|
||||
class DataPlaneTcpStream(
|
||||
val handle: Long,
|
||||
val localAddress: DataPlaneSocketAddress? = null,
|
||||
val peerAddress: DataPlaneSocketAddress? = null
|
||||
) {
|
||||
/** Read up to [maxLength] bytes, waiting at most [timeoutMs] in native code. */
|
||||
suspend fun read(maxLength: Int, timeoutMs: Long): ByteArray =
|
||||
EasyTierDataPlane.tcpRead(this, maxLength, timeoutMs)
|
||||
|
||||
/** Write [data], waiting at most [timeoutMs] in native code. */
|
||||
suspend fun write(data: ByteArray, timeoutMs: Long): Int =
|
||||
EasyTierDataPlane.tcpWrite(this, data, timeoutMs)
|
||||
|
||||
/** Close the native TCP stream handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpClose(handle)
|
||||
}
|
||||
|
||||
/** TCP data-plane listener handle. Call [close] when the listener is no longer needed. */
|
||||
class DataPlaneTcpListener(val handle: Long, val localAddress: DataPlaneSocketAddress) {
|
||||
/** Accept one TCP data-plane stream. */
|
||||
suspend fun accept(timeoutMs: Long): DataPlaneTcpStream =
|
||||
EasyTierDataPlane.tcpAccept(this, timeoutMs)
|
||||
|
||||
/** Close the native TCP listener handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneTcpListenerClose(handle)
|
||||
}
|
||||
|
||||
/** UDP data-plane socket handle. Call [close] when the socket is no longer needed. */
|
||||
class DataPlaneUdpSocket(val handle: Long, val localAddress: DataPlaneSocketAddress) {
|
||||
/** Send one UDP datagram to [dstIp]:[dstPort]. */
|
||||
suspend fun sendTo(
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Int = EasyTierDataPlane.udpSendTo(this, dstIp, dstPort, data, timeoutMs)
|
||||
|
||||
/** Receive one UDP datagram and its peer address. */
|
||||
suspend fun recvFrom(maxLength: Int, timeoutMs: Long): DataPlaneUdpRecvResult =
|
||||
EasyTierDataPlane.udpRecvFrom(this, maxLength, timeoutMs)
|
||||
|
||||
/** Close the native UDP socket handle. */
|
||||
fun close(): Int = EasyTierDataPlaneJNI.dataPlaneUdpClose(handle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level native data-plane JNI entry points.
|
||||
*
|
||||
* These functions mirror the Rust FFI op-handle ABI directly. They are exposed
|
||||
* for completeness, but most Android callers should use [EasyTierDataPlane]
|
||||
* instead so coroutine cancellation and op cleanup are handled consistently.
|
||||
*/
|
||||
object EasyTierDataPlaneJNI {
|
||||
init {
|
||||
System.loadLibrary("easytier_android_jni")
|
||||
}
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpStatus(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpWait(handle: Long, timeoutMs: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpCancel(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneAsyncOpFree(handle: Long): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneTcpConnectStart(
|
||||
instanceName: String,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpConnectFinish(op: Long): DataPlaneTcpConnectResult?
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneTcpBindStart(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpBindFinish(op: Long): DataPlaneTcpBindResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpAcceptStart(handle: Long, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpAcceptFinish(op: Long): DataPlaneTcpAcceptResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpReadStart(handle: Long, maxLength: Int, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpReadFinish(op: Long): DataPlaneTcpReadResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpWriteStart(handle: Long, data: ByteArray, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpWriteFinish(op: Long): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneUdpBindStart(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpBindFinish(op: Long): DataPlaneUdpBindResult?
|
||||
|
||||
@JvmStatic
|
||||
external fun dataPlaneUdpSendToStart(
|
||||
handle: Long,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpSendToFinish(op: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpRecvFromStart(handle: Long, maxLength: Int, timeoutMs: Long): Long
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpRecvFromFinish(op: Long): DataPlaneUdpRecvResult?
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpClose(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneTcpListenerClose(handle: Long): Int
|
||||
|
||||
@JvmStatic external fun dataPlaneUdpClose(handle: Long): Int
|
||||
}
|
||||
|
||||
/** Coroutine-friendly Android data-plane API. */
|
||||
object EasyTierDataPlane {
|
||||
private const val DATA_PLANE_OP_PENDING = 0
|
||||
private const val DATA_PLANE_OP_READY = 1
|
||||
private const val DATA_PLANE_OP_FAILED = -1
|
||||
private const val DATA_PLANE_OP_INVALID = -2
|
||||
private const val DATA_PLANE_WAIT_SLICE_MS = 50L
|
||||
|
||||
/** Connect to a TCP endpoint through the named EasyTier instance. */
|
||||
@JvmStatic
|
||||
suspend fun tcpConnect(
|
||||
instanceName: String,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneTcpStream {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpConnectStart(
|
||||
instanceName,
|
||||
dstIp,
|
||||
dstPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpConnectFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpStream(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Bind a TCP data-plane listener on [localPort]. Port 0 asks EasyTier to allocate one. */
|
||||
@JvmStatic
|
||||
suspend fun tcpBind(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneTcpListener {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpBindStart(
|
||||
instanceName,
|
||||
localPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpBindFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpListener(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Accept one TCP stream from [listener]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpAccept(listener: DataPlaneTcpListener, timeoutMs: Long): DataPlaneTcpStream {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpAcceptStart(listener.handle, timeoutMs)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpAcceptFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneTcpStream(result.handle, result.localAddress, result.peerAddress)
|
||||
}
|
||||
|
||||
/** Read up to [maxLength] bytes from [stream]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpRead(
|
||||
stream: DataPlaneTcpStream,
|
||||
maxLength: Int,
|
||||
timeoutMs: Long
|
||||
): ByteArray {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpReadStart(
|
||||
stream.handle,
|
||||
maxLength,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpReadFinish(it)?.data
|
||||
?: throw lastDataPlaneException()
|
||||
}
|
||||
}
|
||||
|
||||
/** Write [data] to [stream]. */
|
||||
@JvmStatic
|
||||
suspend fun tcpWrite(stream: DataPlaneTcpStream, data: ByteArray, timeoutMs: Long): Int {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneTcpWriteStart(
|
||||
stream.handle,
|
||||
data,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneTcpWriteFinish(it) }
|
||||
}
|
||||
|
||||
/** Bind a UDP data-plane socket on [localPort]. Port 0 asks EasyTier to allocate one. */
|
||||
@JvmStatic
|
||||
suspend fun udpBind(
|
||||
instanceName: String,
|
||||
localPort: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneUdpSocket {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpBindStart(
|
||||
instanceName,
|
||||
localPort,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
val result = awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpBindFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
return DataPlaneUdpSocket(result.handle, result.localAddress)
|
||||
}
|
||||
|
||||
/** Send one UDP datagram through [socket]. */
|
||||
@JvmStatic
|
||||
suspend fun udpSendTo(
|
||||
socket: DataPlaneUdpSocket,
|
||||
dstIp: String,
|
||||
dstPort: Int,
|
||||
data: ByteArray,
|
||||
timeoutMs: Long
|
||||
): Int {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpSendToStart(
|
||||
socket.handle,
|
||||
dstIp,
|
||||
dstPort,
|
||||
data,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) { EasyTierDataPlaneJNI.dataPlaneUdpSendToFinish(it) }
|
||||
}
|
||||
|
||||
/** Receive one UDP datagram through [socket]. */
|
||||
@JvmStatic
|
||||
suspend fun udpRecvFrom(
|
||||
socket: DataPlaneUdpSocket,
|
||||
maxLength: Int,
|
||||
timeoutMs: Long
|
||||
): DataPlaneUdpRecvResult {
|
||||
val op =
|
||||
requireOp(
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpRecvFromStart(
|
||||
socket.handle,
|
||||
maxLength,
|
||||
timeoutMs
|
||||
)
|
||||
)
|
||||
return awaitOp(op) {
|
||||
EasyTierDataPlaneJNI.dataPlaneUdpRecvFromFinish(it) ?: throw lastDataPlaneException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireOp(op: Long): Long {
|
||||
if (op == 0L) {
|
||||
throw lastDataPlaneException()
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
private suspend fun <T> awaitOp(op: Long, finish: (Long) -> T): T =
|
||||
withContext(Dispatchers.IO) {
|
||||
var consumed = false
|
||||
try {
|
||||
awaitReady(op)
|
||||
val result = finish(op)
|
||||
consumed = true
|
||||
result
|
||||
} catch (e: CancellationException) {
|
||||
EasyTierDataPlaneJNI.dataPlaneAsyncOpCancel(op)
|
||||
throw e
|
||||
} finally {
|
||||
if (!consumed) {
|
||||
EasyTierDataPlaneJNI.dataPlaneAsyncOpFree(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitReady(op: Long) {
|
||||
while (true) {
|
||||
currentCoroutineContext().ensureActive()
|
||||
when (EasyTierDataPlaneJNI.dataPlaneAsyncOpWait(op, DATA_PLANE_WAIT_SLICE_MS)) {
|
||||
DATA_PLANE_OP_READY, DATA_PLANE_OP_FAILED -> return
|
||||
DATA_PLANE_OP_PENDING -> Unit
|
||||
DATA_PLANE_OP_INVALID -> throw RuntimeException("Data-plane async operation is invalid")
|
||||
else -> throw RuntimeException("Unknown data-plane async operation status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun lastDataPlaneException(): RuntimeException {
|
||||
return RuntimeException(EasyTierJNI.getLastError() ?: "EasyTier data-plane call failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
use std::{
|
||||
ffi::{CStr, c_char},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_ffi::{
|
||||
data_plane_async_op_cancel, data_plane_async_op_free, data_plane_async_op_status,
|
||||
data_plane_async_op_wait, data_plane_free_bytes, data_plane_tcp_accept_finish,
|
||||
data_plane_tcp_accept_start, data_plane_tcp_bind_finish, data_plane_tcp_bind_start,
|
||||
data_plane_tcp_close, data_plane_tcp_connect_finish, data_plane_tcp_connect_start,
|
||||
data_plane_tcp_listener_close, data_plane_tcp_read_finish, data_plane_tcp_read_start,
|
||||
data_plane_tcp_write_finish, data_plane_tcp_write_start, data_plane_udp_bind_finish,
|
||||
data_plane_udp_bind_start, data_plane_udp_close, data_plane_udp_recv_from_finish,
|
||||
data_plane_udp_recv_from_start, data_plane_udp_send_to_finish, data_plane_udp_send_to_start,
|
||||
free_string,
|
||||
};
|
||||
use jni::{
|
||||
JNIEnv,
|
||||
objects::{JByteArray, JClass, JObject, JString, JValue},
|
||||
sys::{jint, jlong, jobject},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{get_last_error, throw_exception},
|
||||
strings::jstring_to_cstring,
|
||||
};
|
||||
|
||||
const SOCKET_ADDR_CLASS: &str = "com/easytier/jni/DataPlaneSocketAddress";
|
||||
const TCP_CONNECT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpConnectResult";
|
||||
const TCP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpBindResult";
|
||||
const TCP_ACCEPT_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpAcceptResult";
|
||||
const TCP_READ_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneTcpReadResult";
|
||||
const UDP_BIND_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpBindResult";
|
||||
const UDP_RECV_RESULT_CLASS: &str = "com/easytier/jni/DataPlaneUdpRecvResult";
|
||||
|
||||
fn timeout_from_jlong(timeout_ms: jlong) -> u64 {
|
||||
timeout_ms.max(0) as u64
|
||||
}
|
||||
|
||||
fn port_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option<u16> {
|
||||
match u16::try_from(value) {
|
||||
Ok(port) => Some(port),
|
||||
Err(_) => {
|
||||
throw_exception(env, &format!("Invalid {}: {}", name, value));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn len_from_jint(env: &mut JNIEnv, value: jint, name: &str) -> Option<u32> {
|
||||
match u32::try_from(value) {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => {
|
||||
throw_exception(env, &format!("Invalid {}: {}", name, value));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn throw_last(env: &mut JNIEnv) {
|
||||
let message = get_last_error().unwrap_or_else(|| "EasyTier data-plane call failed".to_string());
|
||||
throw_exception(env, &message);
|
||||
}
|
||||
|
||||
unsafe fn take_ffi_string(ptr: *const c_char) -> String {
|
||||
if ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
let value = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
free_string(ptr);
|
||||
value
|
||||
}
|
||||
|
||||
fn new_socket_addr<'local>(
|
||||
env: &mut JNIEnv<'local>,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> Option<JObject<'local>> {
|
||||
let class = match env.find_class(SOCKET_ADDR_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
env,
|
||||
&format!("Failed to find socket address class: {:?}", err),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let ip = match env.new_string(ip) {
|
||||
Ok(ip) => ip,
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create IP string: {:?}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match env.new_object(
|
||||
class,
|
||||
"(Ljava/lang/String;I)V",
|
||||
&[JValue::Object(&ip), JValue::Int(port as jint)],
|
||||
) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create socket address: {:?}", err));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_handle_addr_result(
|
||||
env: &mut JNIEnv,
|
||||
class_name: &str,
|
||||
handle: u64,
|
||||
ip: String,
|
||||
port: u16,
|
||||
) -> jobject {
|
||||
let Some(addr) = new_socket_addr(env, ip, port) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(class_name) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to find result class: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("(JL{};)V", SOCKET_ADDR_CLASS);
|
||||
match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[JValue::Long(handle as jlong), JValue::Object(&addr)],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(env, &format!("Failed to create result object: {:?}", err));
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close_tcp_stream_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn close_tcp_listener_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_tcp_listener_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn close_udp_socket_on_null(result: jobject, handle: u64) -> jobject {
|
||||
if result.is_null() {
|
||||
let _ = data_plane_udp_close(handle);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn read_owned_bytes(ptr: *const u8, len: u32) -> Vec<u8> {
|
||||
if ptr.is_null() || len == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec();
|
||||
data_plane_free_bytes(ptr, len);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_status_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_status(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_wait_jni(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jint {
|
||||
data_plane_async_op_wait(handle as u64, timeout_ms.max(0) as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_cancel_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_cancel(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn async_op_free_jni(_env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
data_plane_async_op_free(handle as u64)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_connect_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid destination IP: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_connect_start(
|
||||
inst_name.as_ptr(),
|
||||
dst_ip.as_ptr(),
|
||||
dst_port,
|
||||
timeout_ms.max(0) as u64,
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_connect_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_tcp_connect_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_tcp_stream_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
TCP_CONNECT_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_bind_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_bind_start(
|
||||
inst_name.as_ptr(),
|
||||
local_port,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_tcp_bind_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_tcp_listener_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
TCP_BIND_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_accept_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let op = unsafe { data_plane_tcp_accept_start(handle as u64, timeout_from_jlong(timeout_ms)) };
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_accept_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut local_ip: *const c_char = ptr::null();
|
||||
let mut local_port = 0u16;
|
||||
let mut peer_ip: *const c_char = ptr::null();
|
||||
let mut peer_port = 0u16;
|
||||
let handle = unsafe {
|
||||
data_plane_tcp_accept_finish(
|
||||
op as u64,
|
||||
&mut local_ip,
|
||||
&mut local_port,
|
||||
&mut peer_ip,
|
||||
&mut peer_port,
|
||||
)
|
||||
};
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let Some(local_addr) =
|
||||
new_socket_addr(&mut env, unsafe { take_ffi_string(local_ip) }, local_port)
|
||||
else {
|
||||
free_string(peer_ip);
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(peer_ip) }, peer_port)
|
||||
else {
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(TCP_ACCEPT_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find accept result class: {:?}", err),
|
||||
);
|
||||
let _ = data_plane_tcp_close(handle);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("(JL{};L{};)V", SOCKET_ADDR_CLASS, SOCKET_ADDR_CLASS);
|
||||
let result = match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[
|
||||
JValue::Long(handle as jlong),
|
||||
JValue::Object(&local_addr),
|
||||
JValue::Object(&peer_addr),
|
||||
],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create accept result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
};
|
||||
close_tcp_stream_on_null(result, handle)
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_read_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_read_start(handle as u64, max_len, timeout_from_jlong(timeout_ms))
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_read_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ptr: *const u8 = ptr::null();
|
||||
let mut len = 0u32;
|
||||
let ret = unsafe { data_plane_tcp_read_finish(op as u64, &mut ptr, &mut len) };
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let bytes = read_owned_bytes(ptr, len);
|
||||
let array = match env.byte_array_from_slice(&bytes) {
|
||||
Ok(array) => array,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let class = match env.find_class(TCP_READ_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find read result class: {:?}", err),
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
match env.new_object(class, "([B)V", &[JValue::Object(&array)]) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create read result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_write_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let data = match env.convert_byte_array(&data) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid write buffer: {:?}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let ptr = if data.is_empty() {
|
||||
ptr::null()
|
||||
} else {
|
||||
data.as_ptr()
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_tcp_write_start(
|
||||
handle as u64,
|
||||
ptr,
|
||||
data.len() as u32,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_write_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint {
|
||||
let ret = data_plane_tcp_write_finish(op as u64);
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_bind_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let inst_name = match jstring_to_cstring(&mut env, &inst_name) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid instance name: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(local_port) = port_from_jint(&mut env, local_port, "local port") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_bind_start(
|
||||
inst_name.as_ptr(),
|
||||
local_port,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_bind_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let handle = unsafe { data_plane_udp_bind_finish(op as u64, &mut ip, &mut port) };
|
||||
if handle == 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
close_udp_socket_on_null(
|
||||
new_handle_addr_result(
|
||||
&mut env,
|
||||
UDP_BIND_RESULT_CLASS,
|
||||
handle,
|
||||
unsafe { take_ffi_string(ip) },
|
||||
port,
|
||||
),
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn udp_send_to_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let dst_ip = match jstring_to_cstring(&mut env, &dst_ip) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid destination IP: {}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let Some(dst_port) = port_from_jint(&mut env, dst_port, "destination port") else {
|
||||
return 0;
|
||||
};
|
||||
let data = match env.convert_byte_array(&data) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
throw_exception(&mut env, &format!("Invalid UDP send buffer: {:?}", err));
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
let ptr = if data.is_empty() {
|
||||
ptr::null()
|
||||
} else {
|
||||
data.as_ptr()
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_send_to_start(
|
||||
handle as u64,
|
||||
dst_ip.as_ptr(),
|
||||
dst_port,
|
||||
ptr,
|
||||
data.len() as u32,
|
||||
timeout_from_jlong(timeout_ms),
|
||||
)
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_send_to_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jint {
|
||||
let ret = data_plane_udp_send_to_finish(op as u64);
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_recv_from_start_jni(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
let Some(max_len) = len_from_jint(&mut env, max_len, "max length") else {
|
||||
return 0;
|
||||
};
|
||||
let op = unsafe {
|
||||
data_plane_udp_recv_from_start(handle as u64, max_len, timeout_from_jlong(timeout_ms))
|
||||
};
|
||||
if op == 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
op as jlong
|
||||
}
|
||||
|
||||
pub(crate) fn udp_recv_from_finish_jni(mut env: JNIEnv, _class: JClass, op: jlong) -> jobject {
|
||||
let mut ptr: *const u8 = ptr::null();
|
||||
let mut len = 0u32;
|
||||
let mut ip: *const c_char = ptr::null();
|
||||
let mut port = 0u16;
|
||||
let ret = unsafe {
|
||||
data_plane_udp_recv_from_finish(op as u64, &mut ptr, &mut len, &mut ip, &mut port)
|
||||
};
|
||||
if ret < 0 {
|
||||
throw_last(&mut env);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let bytes = read_owned_bytes(ptr, len);
|
||||
let array = match env.byte_array_from_slice(&bytes) {
|
||||
Ok(array) => array,
|
||||
Err(err) => {
|
||||
free_string(ip);
|
||||
throw_exception(&mut env, &format!("Failed to create byte array: {:?}", err));
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let Some(peer_addr) = new_socket_addr(&mut env, unsafe { take_ffi_string(ip) }, port) else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
let class = match env.find_class(UDP_RECV_RESULT_CLASS) {
|
||||
Ok(class) => class,
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to find UDP recv result class: {:?}", err),
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let sig = format!("([BL{};)V", SOCKET_ADDR_CLASS);
|
||||
match env.new_object(
|
||||
class,
|
||||
sig.as_str(),
|
||||
&[JValue::Object(&array), JValue::Object(&peer_addr)],
|
||||
) {
|
||||
Ok(result) => result.into_raw(),
|
||||
Err(err) => {
|
||||
throw_exception(
|
||||
&mut env,
|
||||
&format!("Failed to create UDP recv result: {:?}", err),
|
||||
);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_tcp_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn tcp_listener_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_tcp_listener_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) fn udp_close_jni(mut env: JNIEnv, _class: JClass, handle: jlong) -> jint {
|
||||
let ret = data_plane_udp_close(handle as u64);
|
||||
if ret != 0 {
|
||||
throw_last(&mut env);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@@ -22,8 +22,13 @@
|
||||
//! Error API:
|
||||
//! - `getLastError()`: return the latest FFI/JNI error string for the calling thread.
|
||||
//!
|
||||
//! Data-plane APIs:
|
||||
//! - `EasyTierDataPlaneJNI.*`: low-level async op-handle data-plane JNI.
|
||||
//! - `EasyTierJNI.dataPlane*`: compatibility exports for older callers.
|
||||
|
||||
mod callback;
|
||||
mod config_server_api;
|
||||
mod data_plane_api;
|
||||
mod error;
|
||||
mod json_rpc_api;
|
||||
mod logger;
|
||||
@@ -31,8 +36,8 @@ mod network_api;
|
||||
mod strings;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jstring};
|
||||
use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jlong, jobject, jstring};
|
||||
|
||||
/// Attach a TUN file descriptor to an EasyTier network instance.
|
||||
///
|
||||
@@ -251,3 +256,522 @@ pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_isConfigServerClientCon
|
||||
logger::init();
|
||||
config_server_api::is_config_server_client_connected_jni(env, class)
|
||||
}
|
||||
|
||||
macro_rules! export_data_plane_jni {
|
||||
(
|
||||
$op_status:ident,
|
||||
$op_wait:ident,
|
||||
$op_cancel:ident,
|
||||
$op_free:ident,
|
||||
$tcp_connect_start:ident,
|
||||
$tcp_connect_finish:ident,
|
||||
$tcp_bind_start:ident,
|
||||
$tcp_bind_finish:ident,
|
||||
$tcp_accept_start:ident,
|
||||
$tcp_accept_finish:ident,
|
||||
$tcp_read_start:ident,
|
||||
$tcp_read_finish:ident,
|
||||
$tcp_write_start:ident,
|
||||
$tcp_write_finish:ident,
|
||||
$udp_bind_start:ident,
|
||||
$udp_bind_finish:ident,
|
||||
$udp_send_to_start:ident,
|
||||
$udp_send_to_finish:ident,
|
||||
$udp_recv_from_start:ident,
|
||||
$udp_recv_from_finish:ident,
|
||||
$tcp_close:ident,
|
||||
$tcp_listener_close:ident,
|
||||
$udp_close:ident
|
||||
) => {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $op_status(env: JNIEnv, class: JClass, handle: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_status_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $op_wait(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_wait_jni(env, class, handle, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $op_cancel(env: JNIEnv, class: JClass, handle: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_cancel_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $op_free(env: JNIEnv, class: JClass, handle: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_free_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_connect_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_connect_start_jni(
|
||||
env, class, inst_name, dst_ip, dst_port, timeout_ms,
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_connect_finish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_connect_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_bind_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_bind_start_jni(env, class, inst_name, local_port, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_bind_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_bind_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_accept_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_accept_start_jni(env, class, handle, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_accept_finish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_accept_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_read_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_read_start_jni(env, class, handle, max_len, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_read_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_read_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_write_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_write_start_jni(env, class, handle, data, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_write_finish(env: JNIEnv, class: JClass, op: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_write_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_bind_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_bind_start_jni(env, class, inst_name, local_port, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_bind_finish(env: JNIEnv, class: JClass, op: jlong) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::udp_bind_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_send_to_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_send_to_start_jni(
|
||||
env, class, handle, dst_ip, dst_port, data, timeout_ms,
|
||||
)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_send_to_finish(env: JNIEnv, class: JClass, op: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::udp_send_to_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_recv_from_start(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_recv_from_start_jni(env, class, handle, max_len, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_recv_from_finish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::udp_recv_from_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_close(env: JNIEnv, class: JClass, handle: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_close_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $tcp_listener_close(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_listener_close_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn $udp_close(env: JNIEnv, class: JClass, handle: jlong) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::udp_close_jni(env, class, handle)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export_data_plane_jni!(
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpStatus,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpWait,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpCancel,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneAsyncOpFree,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpConnectStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpConnectFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpBindStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpBindFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpAcceptStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpAcceptFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpReadStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpReadFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpWriteStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpWriteFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpBindStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpBindFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpSendToStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpSendToFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpRecvFromStart,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpRecvFromFinish,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpClose,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneTcpListenerClose,
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_dataPlaneUdpClose
|
||||
);
|
||||
|
||||
// Compatibility exports for older Kotlin/Java callers that used EasyTierJNI
|
||||
// directly for data-plane operations.
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpStatus(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_status_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpWait(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_wait_jni(env, class, handle, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpCancel(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_cancel_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneAsyncOpFree(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::async_op_free_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpConnectStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_connect_start_jni(env, class, inst_name, dst_ip, dst_port, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpConnectFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_connect_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpBindStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_bind_start_jni(env, class, inst_name, local_port, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpBindFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_bind_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpAcceptStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_accept_start_jni(env, class, handle, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpAcceptFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_accept_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpReadStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_read_start_jni(env, class, handle, max_len, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpReadFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::tcp_read_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpWriteStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::tcp_write_start_jni(env, class, handle, data, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpWriteFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_write_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpBindStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
inst_name: JString,
|
||||
local_port: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_bind_start_jni(env, class, inst_name, local_port, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpBindFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::udp_bind_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpSendToStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
dst_ip: JString,
|
||||
dst_port: jint,
|
||||
data: JByteArray,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_send_to_start_jni(env, class, handle, dst_ip, dst_port, data, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpSendToFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::udp_send_to_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpRecvFromStart(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
max_len: jint,
|
||||
timeout_ms: jlong,
|
||||
) -> jlong {
|
||||
logger::init();
|
||||
data_plane_api::udp_recv_from_start_jni(env, class, handle, max_len, timeout_ms)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpRecvFromFinish(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
op: jlong,
|
||||
) -> jobject {
|
||||
logger::init();
|
||||
data_plane_api::udp_recv_from_finish_jni(env, class, op)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpClose(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_close_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneTcpListenerClose(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::tcp_listener_close_jni(env, class, handle)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_dataPlaneUdpClose(
|
||||
env: JNIEnv,
|
||||
class: JClass,
|
||||
handle: jlong,
|
||||
) -> jint {
|
||||
logger::init();
|
||||
data_plane_api::udp_close_jni(env, class, handle)
|
||||
}
|
||||
|
||||
@@ -9,26 +9,20 @@ crate-type = ["cdylib", "rlib"]
|
||||
[features]
|
||||
default = ["c-abi", "ffi-dataplane"]
|
||||
c-abi = []
|
||||
ffi-dataplane = [
|
||||
"easytier/ffi-dataplane",
|
||||
"easytier-core/proxy-smoltcp-stack",
|
||||
]
|
||||
macos-ne = ["easytier/macos-ne"]
|
||||
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../../easytier", features = ["tracing-log"] }
|
||||
easytier-core = { path = "../../easytier-core" }
|
||||
easytier = { path = "../../easytier" }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
dashmap = "6.0"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "io-util", "time", "sync", "macros"] }
|
||||
async-trait = "0.1"
|
||||
log = "0.4"
|
||||
percent-encoding = "2.3"
|
||||
url = "2"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = "1.17.0"
|
||||
|
||||
[build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
tokio-util = "0.7"
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# Native data-plane ABI v3
|
||||
|
||||
The native data-plane ABI is a thin adapter over the instance-owned
|
||||
`DataPlaneSession`. It does not own sockets, operation state, completion
|
||||
queues, routing policy, or timeouts.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Every immediate call returns `0` on success or a negative
|
||||
`DataPlaneErrorKind` value on failure.
|
||||
- `data_plane_completion_wait` returns `1` when a completion is ready, `0` on
|
||||
timeout or session close, and a negative error value on failure.
|
||||
- `data_plane_completion_drain` returns a non-negative descriptor count or a
|
||||
negative error value.
|
||||
- Handle zero is invalid.
|
||||
- `timeout_ms == UINT64_MAX` means no deadline.
|
||||
- TCP connect/bind/accept and UDP bind timeouts start when submission is
|
||||
accepted.
|
||||
- TCP streams and UDP sockets have persistent read and write deadlines.
|
||||
`data_plane_resource_deadline_set` replaces the selected directions'
|
||||
deadlines immediately, including for active operations. An expired deadline
|
||||
remains expired until it is replaced or cleared with `UINT64_MAX`.
|
||||
- Deadline direction `1` selects reads, `2` selects writes, and `3` selects
|
||||
both.
|
||||
- Request and write bytes are copied before a submit call returns.
|
||||
- Socket-address fields use native-endian integers. Address bytes are in
|
||||
network order. ABI v3 accepts IPv4 only.
|
||||
|
||||
`DataPlaneSocketAddr` is:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint16_t family; /* 4 */
|
||||
uint16_t port;
|
||||
uint8_t address[16]; /* IPv4 uses the first four bytes */
|
||||
} DataPlaneSocketAddr;
|
||||
```
|
||||
|
||||
`DataPlaneCompletion` is:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint64_t operation_id;
|
||||
uint16_t operation_kind;
|
||||
uint16_t status; /* 0 or DataPlaneErrorKind */
|
||||
} DataPlaneCompletion;
|
||||
```
|
||||
|
||||
## Lifecycle
|
||||
|
||||
One native session may be open for an EasyTier instance at a time:
|
||||
|
||||
```text
|
||||
data_plane_session_open
|
||||
-> set resource deadlines
|
||||
-> submit operations
|
||||
-> completion_wait
|
||||
-> completion_drain
|
||||
-> typed result_take
|
||||
-> resource_close / operation_free
|
||||
data_plane_session_close
|
||||
```
|
||||
|
||||
Closing a native session cancels and discards its outstanding operations and
|
||||
resources and wakes a thread blocked in `data_plane_completion_wait`.
|
||||
|
||||
The resource and operation IDs returned by the ABI belong to that session.
|
||||
They must always be passed together with the same session handle.
|
||||
|
||||
## Completion and result ownership
|
||||
|
||||
Submission returns an operation ID immediately. Completion descriptors carry
|
||||
only the operation ID, operation kind, and terminal status. Draining a
|
||||
descriptor makes its typed result available but does not consume it.
|
||||
|
||||
`data_plane_result_size` reports the TCP-read or UDP-receive payload size.
|
||||
Typed result-take functions consume the result exactly once. If a supplied
|
||||
buffer is too small, they return `-BufferTooSmall` and leave the result
|
||||
available for a later call.
|
||||
|
||||
Call `data_plane_operation_free` when a drained result is intentionally
|
||||
abandoned. Call `data_plane_resource_close` for TCP streams, listeners, and
|
||||
UDP sockets.
|
||||
|
||||
## Operation kinds
|
||||
|
||||
| Value | Operation |
|
||||
| ---: | --- |
|
||||
| 1 | TCP connect |
|
||||
| 2 | TCP bind |
|
||||
| 3 | TCP accept |
|
||||
| 4 | TCP read |
|
||||
| 5 | TCP write |
|
||||
| 6 | UDP bind |
|
||||
| 7 | UDP receive |
|
||||
| 8 | UDP send |
|
||||
|
||||
The exported function families are:
|
||||
|
||||
- `data_plane_tcp_*_submit`
|
||||
- `data_plane_udp_*_submit`
|
||||
- `data_plane_resource_deadline_set`
|
||||
- `data_plane_completion_wait`
|
||||
- `data_plane_completion_drain`
|
||||
- `data_plane_*_result_take`
|
||||
- `data_plane_operation_cancel`
|
||||
- `data_plane_operation_free`
|
||||
- `data_plane_resource_close`
|
||||
@@ -1,8 +0,0 @@
|
||||
fn main() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
|
||||
|
||||
if target_os == "windows" && (target_arch == "x86" || target_arch == "x86_64") {
|
||||
thunk::thunk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define DATA_PLANE_OP_PENDING 0
|
||||
#define DATA_PLANE_OP_READY 1
|
||||
#define DATA_PLANE_OP_FAILED -1
|
||||
#define DATA_PLANE_OP_INVALID -2
|
||||
|
||||
extern int run_network_instance(const char *cfg_str);
|
||||
extern void get_error_msg(const char **out);
|
||||
extern void free_string(const char *s);
|
||||
|
||||
extern int data_plane_async_op_status(uint64_t op);
|
||||
extern int data_plane_async_op_wait(uint64_t op, uint64_t timeout_ms);
|
||||
extern int data_plane_async_op_cancel(uint64_t op);
|
||||
extern int data_plane_async_op_free(uint64_t op);
|
||||
extern void data_plane_free_bytes(const uint8_t *ptr, uint32_t len);
|
||||
|
||||
extern uint64_t data_plane_tcp_connect_start(
|
||||
const char *inst_name,
|
||||
const char *dst_ip,
|
||||
uint16_t dst_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_connect_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_tcp_bind_start(
|
||||
const char *inst_name,
|
||||
uint16_t local_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_bind_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_tcp_accept_start(uint64_t listener, uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_tcp_accept_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port,
|
||||
const char **out_peer_ip,
|
||||
uint16_t *out_peer_port);
|
||||
extern uint64_t data_plane_tcp_read_start(
|
||||
uint64_t stream,
|
||||
uint32_t max_len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_tcp_read_finish(
|
||||
uint64_t op,
|
||||
const uint8_t **out_buf,
|
||||
uint32_t *out_len);
|
||||
extern uint64_t data_plane_tcp_write_start(
|
||||
uint64_t stream,
|
||||
const uint8_t *buf,
|
||||
uint32_t len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_tcp_write_finish(uint64_t op);
|
||||
extern int data_plane_tcp_close(uint64_t stream);
|
||||
extern int data_plane_tcp_listener_close(uint64_t listener);
|
||||
|
||||
extern uint64_t data_plane_udp_bind_start(
|
||||
const char *inst_name,
|
||||
uint16_t local_port,
|
||||
uint64_t timeout_ms);
|
||||
extern uint64_t data_plane_udp_bind_finish(
|
||||
uint64_t op,
|
||||
const char **out_local_ip,
|
||||
uint16_t *out_local_port);
|
||||
extern uint64_t data_plane_udp_send_to_start(
|
||||
uint64_t socket,
|
||||
const char *dst_ip,
|
||||
uint16_t dst_port,
|
||||
const uint8_t *buf,
|
||||
uint32_t len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_udp_send_to_finish(uint64_t op);
|
||||
extern uint64_t data_plane_udp_recv_from_start(
|
||||
uint64_t socket,
|
||||
uint32_t max_len,
|
||||
uint64_t timeout_ms);
|
||||
extern int data_plane_udp_recv_from_finish(
|
||||
uint64_t op,
|
||||
const uint8_t **out_buf,
|
||||
uint32_t *out_len,
|
||||
const char **out_ip,
|
||||
uint16_t *out_port);
|
||||
extern int data_plane_udp_close(uint64_t socket);
|
||||
|
||||
static void print_last_error(const char *prefix) {
|
||||
const char *err = NULL;
|
||||
get_error_msg(&err);
|
||||
if (err) {
|
||||
fprintf(stderr, "%s: %s\n", prefix, err);
|
||||
free_string(err);
|
||||
} else {
|
||||
fprintf(stderr, "%s\n", prefix);
|
||||
}
|
||||
}
|
||||
|
||||
static int parse_ip_port(const char *value, char *ip, size_t ip_len, uint16_t *port) {
|
||||
const char *colon = strrchr(value, ':');
|
||||
if (!colon || colon == value || !colon[1]) {
|
||||
fprintf(stderr, "expected IPv4 target in IP:PORT form, got %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
size_t host_len = (size_t)(colon - value);
|
||||
if (host_len >= ip_len) {
|
||||
fprintf(stderr, "IP address is too long: %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
char *end = NULL;
|
||||
long parsed_port = strtol(colon + 1, &end, 10);
|
||||
if (!end || *end != '\0' || parsed_port < 0 || parsed_port > 65535) {
|
||||
fprintf(stderr, "invalid port in %s\n", value);
|
||||
return -1;
|
||||
}
|
||||
memcpy(ip, value, host_len);
|
||||
ip[host_len] = '\0';
|
||||
*port = (uint16_t)parsed_port;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wait_op(uint64_t op, uint64_t timeout_ms) {
|
||||
uint64_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
int status = data_plane_async_op_wait(op, 50);
|
||||
if (status != DATA_PLANE_OP_PENDING) {
|
||||
return status;
|
||||
}
|
||||
waited += 50;
|
||||
}
|
||||
return data_plane_async_op_status(op);
|
||||
}
|
||||
|
||||
static int wait_or_cancel(uint64_t op, uint64_t timeout_ms, const char *what) {
|
||||
int status = wait_op(op, timeout_ms);
|
||||
if (status == DATA_PLANE_OP_READY || status == DATA_PLANE_OP_FAILED) {
|
||||
return status;
|
||||
}
|
||||
if (status == DATA_PLANE_OP_PENDING) {
|
||||
fprintf(stderr, "%s did not finish within %llu ms\n", what, (unsigned long long)timeout_ms);
|
||||
data_plane_async_op_cancel(op);
|
||||
data_plane_async_op_free(op);
|
||||
return DATA_PLANE_OP_INVALID;
|
||||
}
|
||||
fprintf(stderr, "%s returned invalid op status %d\n", what, status);
|
||||
return status;
|
||||
}
|
||||
|
||||
static int async_tcp_read_once(uint64_t stream, uint64_t timeout_ms) {
|
||||
uint64_t op = data_plane_tcp_read_start(stream, 512, timeout_ms);
|
||||
if (!op) {
|
||||
print_last_error("tcp read start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, timeout_ms + 1000, "tcp read") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *buf = NULL;
|
||||
uint32_t len = 0;
|
||||
int ret = data_plane_tcp_read_finish(op, &buf, &len);
|
||||
if (ret < 0) {
|
||||
print_last_error("tcp read finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp read %d bytes: %.*s\n", ret, ret, buf ? (const char *)buf : "");
|
||||
data_plane_free_bytes(buf, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int async_tcp_write_all(uint64_t stream, const char *data, uint64_t timeout_ms) {
|
||||
uint64_t op = data_plane_tcp_write_start(
|
||||
stream,
|
||||
(const uint8_t *)data,
|
||||
(uint32_t)strlen(data),
|
||||
timeout_ms);
|
||||
if (!op) {
|
||||
print_last_error("tcp write start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, timeout_ms + 1000, "tcp write") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
int ret = data_plane_tcp_write_finish(op);
|
||||
if (ret < 0) {
|
||||
print_last_error("tcp write finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp wrote %d bytes\n", ret);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_tcp_connect_demo(const char *inst, const char *target) {
|
||||
char ip[128];
|
||||
uint16_t port = 0;
|
||||
if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t op = data_plane_tcp_connect_start(inst, ip, port, 30000);
|
||||
if (!op) {
|
||||
print_last_error("tcp connect start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "tcp connect") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t stream = data_plane_tcp_connect_finish(op, &local_ip, &local_port);
|
||||
if (!stream) {
|
||||
print_last_error("tcp connect finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp connected from %s:%u to %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
ip,
|
||||
port,
|
||||
(unsigned long long)stream);
|
||||
free_string(local_ip);
|
||||
|
||||
int ret = async_tcp_read_once(stream, 10000);
|
||||
data_plane_tcp_close(stream);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int run_tcp_listen_demo(const char *inst, const char *port_text) {
|
||||
uint16_t port = (uint16_t)strtoul(port_text, NULL, 10);
|
||||
uint64_t op = data_plane_tcp_bind_start(inst, port, 30000);
|
||||
if (!op) {
|
||||
print_last_error("tcp bind start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "tcp bind") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t listener = data_plane_tcp_bind_finish(op, &local_ip, &local_port);
|
||||
if (!listener) {
|
||||
print_last_error("tcp bind finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp listening on %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)listener);
|
||||
free_string(local_ip);
|
||||
|
||||
op = data_plane_tcp_accept_start(listener, 60000);
|
||||
if (!op) {
|
||||
print_last_error("tcp accept start failed");
|
||||
data_plane_tcp_listener_close(listener);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 61000, "tcp accept") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_tcp_listener_close(listener);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *peer_ip = NULL;
|
||||
uint16_t peer_port = 0;
|
||||
local_ip = NULL;
|
||||
local_port = 0;
|
||||
uint64_t stream = data_plane_tcp_accept_finish(
|
||||
op,
|
||||
&local_ip,
|
||||
&local_port,
|
||||
&peer_ip,
|
||||
&peer_port);
|
||||
data_plane_tcp_listener_close(listener);
|
||||
if (!stream) {
|
||||
print_last_error("tcp accept finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("tcp accepted %s:%u -> %s:%u, stream=%llu\n",
|
||||
peer_ip,
|
||||
peer_port,
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)stream);
|
||||
free_string(local_ip);
|
||||
free_string(peer_ip);
|
||||
|
||||
int ret = async_tcp_read_once(stream, 10000);
|
||||
if (ret == 0) {
|
||||
ret = async_tcp_write_all(stream, "pong", 10000);
|
||||
}
|
||||
data_plane_tcp_close(stream);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int run_udp_demo(const char *inst, const char *target) {
|
||||
char ip[128];
|
||||
uint16_t port = 0;
|
||||
if (parse_ip_port(target, ip, sizeof(ip), &port) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t op = data_plane_udp_bind_start(inst, 0, 30000);
|
||||
if (!op) {
|
||||
print_last_error("udp bind start failed");
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "udp bind") == DATA_PLANE_OP_INVALID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *local_ip = NULL;
|
||||
uint16_t local_port = 0;
|
||||
uint64_t socket = data_plane_udp_bind_finish(op, &local_ip, &local_port);
|
||||
if (!socket) {
|
||||
print_last_error("udp bind finish failed");
|
||||
return -1;
|
||||
}
|
||||
printf("udp bound on %s:%u, handle=%llu\n",
|
||||
local_ip,
|
||||
local_port,
|
||||
(unsigned long long)socket);
|
||||
free_string(local_ip);
|
||||
|
||||
const char payload[] = "ping";
|
||||
op = data_plane_udp_send_to_start(
|
||||
socket,
|
||||
ip,
|
||||
port,
|
||||
(const uint8_t *)payload,
|
||||
(uint32_t)strlen(payload),
|
||||
10000);
|
||||
if (!op) {
|
||||
print_last_error("udp send start failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 11000, "udp send") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
int sent = data_plane_udp_send_to_finish(op);
|
||||
if (sent < 0) {
|
||||
print_last_error("udp send finish failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
printf("udp sent %d bytes to %s:%u\n", sent, ip, port);
|
||||
|
||||
op = data_plane_udp_recv_from_start(socket, 512, 30000);
|
||||
if (!op) {
|
||||
print_last_error("udp recv start failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
if (wait_or_cancel(op, 31000, "udp recv") == DATA_PLANE_OP_INVALID) {
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t *buf = NULL;
|
||||
uint32_t len = 0;
|
||||
const char *peer_ip = NULL;
|
||||
uint16_t peer_port = 0;
|
||||
int ret = data_plane_udp_recv_from_finish(op, &buf, &len, &peer_ip, &peer_port);
|
||||
if (ret < 0) {
|
||||
print_last_error("udp recv finish failed");
|
||||
data_plane_udp_close(socket);
|
||||
return -1;
|
||||
}
|
||||
printf("udp received %d bytes from %s:%u: %.*s\n",
|
||||
ret,
|
||||
peer_ip,
|
||||
peer_port,
|
||||
ret,
|
||||
buf ? (const char *)buf : "");
|
||||
data_plane_free_bytes(buf, len);
|
||||
free_string(peer_ip);
|
||||
data_plane_udp_close(socket);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_usage(void) {
|
||||
printf("Set EASYTIER_FFI_CONFIG and EASYTIER_FFI_INSTANCE to run the async data-plane demo.\n");
|
||||
printf("Optional demos:\n");
|
||||
printf(" EASYTIER_FFI_TARGET=10.0.0.2:22 async TCP connect/read\n");
|
||||
printf(" EASYTIER_FFI_LISTEN_PORT=12345 async TCP bind/accept/read/write\n");
|
||||
printf(" EASYTIER_FFI_UDP_TARGET=10.0.0.2:9000 async UDP bind/send_to/recv_from\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char *config = getenv("EASYTIER_FFI_CONFIG");
|
||||
const char *instance = getenv("EASYTIER_FFI_INSTANCE");
|
||||
if (!config || !instance) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (run_network_instance(config) != 0) {
|
||||
print_last_error("run_network_instance failed");
|
||||
return 1;
|
||||
}
|
||||
printf("network instance started: %s\n", instance);
|
||||
|
||||
int failed = 0;
|
||||
const char *target = getenv("EASYTIER_FFI_TARGET");
|
||||
if (target) {
|
||||
failed |= run_tcp_connect_demo(instance, target) != 0;
|
||||
}
|
||||
|
||||
const char *listen_port = getenv("EASYTIER_FFI_LISTEN_PORT");
|
||||
if (listen_port) {
|
||||
failed |= run_tcp_listen_demo(instance, listen_port) != 0;
|
||||
}
|
||||
|
||||
const char *udp_target = getenv("EASYTIER_FFI_UDP_TARGET");
|
||||
if (udp_target) {
|
||||
failed |= run_udp_demo(instance, udp_target) != 0;
|
||||
}
|
||||
|
||||
if (!target && !listen_port && !udp_target) {
|
||||
printf("No dataplane demo env var was set; nothing else to run.\n");
|
||||
print_usage();
|
||||
}
|
||||
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
# 1. Go FFI Demo
|
||||
|
||||
This demo wraps EasyTier FFI data-plane TCP as Go `net.Conn` and `net.Listener`.
|
||||
It can connect to an SSH server through EasyTier and read its banner, or accept a
|
||||
TCP connection from another EasyTier peer and run a small ping/pong exchange.
|
||||
The async op-handle wrapper is in `easytier_async.go`; the original synchronous
|
||||
wrapper stays in `easytier.go`.
|
||||
|
||||
## 1.1. Build the FFI library
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
```sh
|
||||
cargo build -p easytier-ffi --features ffi-dataplane
|
||||
```
|
||||
|
||||
The demo loads the debug library by default:
|
||||
|
||||
```text
|
||||
target/debug/libeasytier_ffi.so
|
||||
```
|
||||
|
||||
To use another library path, export `EASYTIER_FFI_LIB=/path/to/libeasytier_ffi.so`.
|
||||
|
||||
## 1.2. Configure the EasyTier config
|
||||
|
||||
`EASYTIER_FFI_CONFIG` is a string of the EasyTier config in TOML format which is passed to the FFI library. For example:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_CONFIG='instance_name = "default"
|
||||
ipv4 = "10.0.0.1"
|
||||
|
||||
[network_identity]
|
||||
network_name = "testnet"
|
||||
network_secret = "mysecret"
|
||||
|
||||
[flags]
|
||||
no_tun = true # disable tun device to avoid permission issues.
|
||||
bind_device = false # allow loopback peers in local examples.
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://123.123.123.123:11010"
|
||||
'
|
||||
```
|
||||
|
||||
You should configure with your own real values.
|
||||
|
||||
Set the local instance name and a SSH server target to connect through EasyTier:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_INSTANCE=default
|
||||
export EASYTIER_FFI_TARGET=10.0.0.2:22
|
||||
```
|
||||
|
||||
To run the TCP listen integration test in the same `go test` process as the SSH
|
||||
test, use a separate instance name and config:
|
||||
|
||||
```sh
|
||||
export EASYTIER_FFI_LISTEN_CONFIG='instance_name = "listener"
|
||||
ipv4 = "10.0.0.3"
|
||||
|
||||
[network_identity]
|
||||
network_name = "testnet"
|
||||
network_secret = "mysecret"
|
||||
|
||||
[flags]
|
||||
no_tun = true
|
||||
bind_device = false
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://123.123.123.123:11010"
|
||||
'
|
||||
export EASYTIER_FFI_LISTEN_INSTANCE=listener
|
||||
export EASYTIER_FFI_LISTEN_PORT=12345
|
||||
```
|
||||
|
||||
## 1.3. Run the demo
|
||||
|
||||
`goffi` is built without cgo on Linux, so run the tests with `CGO_ENABLED=0`:
|
||||
|
||||
```sh
|
||||
cd easytier-contrib/easytier-ffi/examples/go
|
||||
CGO_ENABLED=0 go test -v ./...
|
||||
```
|
||||
|
||||
The synchronous tests use the environment variables above. The async Go tests
|
||||
are self-contained: they start two local EasyTier instances in the same test
|
||||
process with `no_tun = true` and `bind_device = false`, then run TCP and UDP
|
||||
ping/pong over the async data-plane API.
|
||||
|
||||
The synchronous wrapper also exposes `CallJSONRPC(service, method, domain,
|
||||
payload)` for non-lifecycle EasyTier RPCs. For example,
|
||||
`CallJSONRPC("api.logger.LoggerRpcService", "get_logger_config", "", "{}")`
|
||||
returns the logger config as protobuf JSON. Instance lifecycle management RPCs
|
||||
are intentionally filtered; use the dedicated FFI APIs for starting and
|
||||
stopping instances.
|
||||
|
||||
To run only the async tests:
|
||||
|
||||
```sh
|
||||
cd easytier-contrib/easytier-ffi/examples/go
|
||||
CGO_ENABLED=0 go test -run 'TestAsync' -v ./...
|
||||
```
|
||||
|
||||
When the SSH integration environment variables are set, expected synchronous
|
||||
test output includes an SSH banner similar to:
|
||||
|
||||
```text
|
||||
attempt 1: got banner "SSH-2.0-..."
|
||||
PASS
|
||||
```
|
||||
|
||||
For `TestTCPListenIntegration`, connect from another EasyTier peer to the local
|
||||
EasyTier IPv4 address and `EASYTIER_FFI_LISTEN_PORT`, send `ping`, and expect
|
||||
`pong` in response.
|
||||
|
||||
The async test output should include local TCP bind/connect log lines and finish
|
||||
with `PASS` without any extra environment variables.
|
||||
|
||||
## 1.4. C async example
|
||||
|
||||
The C async example is kept separate from the basic C example:
|
||||
|
||||
```sh
|
||||
cargo build -p easytier-ffi --features ffi-dataplane
|
||||
cc -Wall -Wextra -pedantic \
|
||||
../example_data_plane_async.c \
|
||||
-L ../../../../target/debug -leasytier_ffi \
|
||||
-Wl,-rpath,../../../../target/debug \
|
||||
-o /tmp/easytier_data_plane_async
|
||||
|
||||
/tmp/easytier_data_plane_async
|
||||
```
|
||||
|
||||
Without environment variables it prints usage and exits successfully. With
|
||||
`EASYTIER_FFI_CONFIG`, `EASYTIER_FFI_INSTANCE`, and one of
|
||||
`EASYTIER_FFI_TARGET`, `EASYTIER_FFI_LISTEN_PORT`, or `EASYTIER_FFI_UDP_TARGET`,
|
||||
it runs the corresponding async data-plane flow.
|
||||
@@ -0,0 +1,593 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-webgpu/goffi/ffi"
|
||||
"github.com/go-webgpu/goffi/types"
|
||||
)
|
||||
|
||||
const defaultTimeout = 30 * time.Second
|
||||
|
||||
type Native struct {
|
||||
lib unsafe.Pointer
|
||||
|
||||
runNetworkInstance symCall
|
||||
callJSONRPC symCall
|
||||
getErrorMsg symCall
|
||||
freeString symCall
|
||||
tcpConnect symCall
|
||||
tcpBind symCall
|
||||
tcpAccept symCall
|
||||
tcpRead symCall
|
||||
tcpWrite symCall
|
||||
tcpClose symCall
|
||||
tcpListenerClose symCall
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
native *Native
|
||||
handle uint64
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
closed atomic.Bool
|
||||
rd atomicDeadline
|
||||
wd atomicDeadline
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
native *Native
|
||||
handle uint64
|
||||
addr net.Addr
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
type symCall struct {
|
||||
fn unsafe.Pointer
|
||||
cif types.CallInterface
|
||||
}
|
||||
|
||||
type atomicDeadline struct{ v atomic.Int64 }
|
||||
|
||||
type timeoutError string
|
||||
|
||||
func Open(path string) (*Native, error) {
|
||||
lib, err := ffi.LoadLibrary(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := &Native{lib: lib}
|
||||
if err := n.bind(); err != nil {
|
||||
ffi.FreeLibrary(lib)
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *Native) Close() error {
|
||||
if n.lib == nil {
|
||||
return nil
|
||||
}
|
||||
ffi.FreeLibrary(n.lib)
|
||||
n.lib = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) RunNetworkInstance(config string) error {
|
||||
defer pinErrorThread()()
|
||||
cfg := cString(config)
|
||||
cfgPtr := unsafe.Pointer(&cfg[0])
|
||||
var ret int32
|
||||
err := n.runNetworkInstance.call(unsafe.Pointer(&ret), unsafe.Pointer(&cfgPtr))
|
||||
runtime.KeepAlive(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) CallJSONRPC(serviceName, methodName, domainName, payloadJSON string) (string, error) {
|
||||
defer pinErrorThread()()
|
||||
service := cString(serviceName)
|
||||
method := cString(methodName)
|
||||
payload := cString(payloadJSON)
|
||||
servicePtr := unsafe.Pointer(&service[0])
|
||||
methodPtr := unsafe.Pointer(&method[0])
|
||||
payloadPtr := unsafe.Pointer(&payload[0])
|
||||
var domain []byte
|
||||
var domainPtr unsafe.Pointer
|
||||
if domainName != "" {
|
||||
domain = cString(domainName)
|
||||
domainPtr = unsafe.Pointer(&domain[0])
|
||||
}
|
||||
var response unsafe.Pointer
|
||||
responseArg := unsafe.Pointer(&response)
|
||||
var ret int32
|
||||
err := n.callJSONRPC.call(
|
||||
unsafe.Pointer(&ret),
|
||||
unsafe.Pointer(&servicePtr),
|
||||
unsafe.Pointer(&methodPtr),
|
||||
unsafe.Pointer(&domainPtr),
|
||||
unsafe.Pointer(&payloadPtr),
|
||||
unsafe.Pointer(&responseArg),
|
||||
)
|
||||
runtime.KeepAlive(service)
|
||||
runtime.KeepAlive(method)
|
||||
runtime.KeepAlive(domain)
|
||||
runtime.KeepAlive(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ret != 0 {
|
||||
return "", n.lastError()
|
||||
}
|
||||
if response == nil {
|
||||
return "", errors.New("easytier ffi JSON RPC returned nil response")
|
||||
}
|
||||
defer func() { _ = n.freeCString(response) }()
|
||||
return readCString(response), nil
|
||||
}
|
||||
|
||||
func (n *Native) DialContext(ctx context.Context, instance, network, address string) (net.Conn, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
ip, port, err := parseIPPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := defaultTimeout
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeout = time.Until(deadline)
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, local, err := n.tcpConnectTo(instance, ip.String(), uint16(port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Conn{native: n, handle: handle, local: local, remote: &net.TCPAddr{IP: ip, Port: port}}, nil
|
||||
}
|
||||
|
||||
func (n *Native) ListenContext(ctx context.Context, instance, network, address string) (net.Listener, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, net.UnknownNetworkError(network)
|
||||
}
|
||||
port, err := parseListenPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
timeout := defaultTimeout
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
timeout = time.Until(deadline)
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handle, local, err := n.tcpBindTo(instance, uint16(port), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Listener{native: n, handle: handle, addr: local}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
if c.closed.Load() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
n, err := c.native.tcpReadFrom(c.handle, b, c.rd.timeout(defaultTimeout))
|
||||
if err != nil {
|
||||
return 0, opError("read", c.remote, err)
|
||||
}
|
||||
if n == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
if c.closed.Load() {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
n, err := c.native.tcpWriteTo(c.handle, b, c.wd.timeout(defaultTimeout))
|
||||
if err != nil {
|
||||
return 0, opError("write", c.remote, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if !c.closed.CompareAndSwap(false, true) {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return c.native.tcpCloseHandle(c.handle)
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr { return c.local }
|
||||
func (c *Conn) RemoteAddr() net.Addr { return c.remote }
|
||||
func (c *Conn) SetDeadline(t time.Time) error { c.rd.set(t); c.wd.set(t); return nil }
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error { c.rd.set(t); return nil }
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error { c.wd.set(t); return nil }
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
if l.closed.Load() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
for {
|
||||
handle, local, peer, err := l.native.tcpAcceptFrom(l.handle, defaultTimeout)
|
||||
if err == nil {
|
||||
return &Conn{native: l.native, handle: handle, local: local, remote: peer}, nil
|
||||
}
|
||||
if l.closed.Load() {
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
return nil, opError("accept", l.addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
if !l.closed.CompareAndSwap(false, true) {
|
||||
return net.ErrClosed
|
||||
}
|
||||
return l.native.tcpListenerCloseHandle(l.handle)
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr { return l.addr }
|
||||
|
||||
func (n *Native) bind() error {
|
||||
return errors.Join(
|
||||
n.bindSym(&n.runNetworkInstance, "run_network_instance", types.SInt32TypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.callJSONRPC, "call_json_rpc", types.SInt32TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.getErrorMsg, "get_error_msg", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.freeString, "free_string", types.VoidTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpConnect, "data_plane_tcp_connect", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpBind, "data_plane_tcp_bind", types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt16TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpAccept, "data_plane_tcp_accept", types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor, types.PointerTypeDescriptor),
|
||||
n.bindSym(&n.tcpRead, "data_plane_tcp_read", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpWrite, "data_plane_tcp_write", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor, types.PointerTypeDescriptor, types.UInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpClose, "data_plane_tcp_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
n.bindSym(&n.tcpListenerClose, "data_plane_tcp_listener_close", types.SInt32TypeDescriptor, types.UInt64TypeDescriptor),
|
||||
)
|
||||
}
|
||||
|
||||
func (n *Native) bindSym(dst *symCall, name string, ret *types.TypeDescriptor, args ...*types.TypeDescriptor) error {
|
||||
sym, err := ffi.GetSymbol(n.lib, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ffi.PrepareCallInterface(&dst.cif, types.DefaultCall, ret, args); err != nil {
|
||||
return err
|
||||
}
|
||||
dst.fn = sym
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *symCall) call(ret unsafe.Pointer, args ...unsafe.Pointer) error {
|
||||
// `ffi.CallFunction` and libffi `ffi_call` are safe to invoke concurrently
|
||||
// because `cif` is prepared once during binding and only read afterwards.
|
||||
return ffi.CallFunction(&s.cif, s.fn, ret, args)
|
||||
}
|
||||
|
||||
func (n *Native) tcpConnectTo(instance, ip string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
inst := cString(instance)
|
||||
dst := cString(ip)
|
||||
instPtr := unsafe.Pointer(&inst[0])
|
||||
dstPtr := unsafe.Pointer(&dst[0])
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var handle uint64
|
||||
var outIP unsafe.Pointer
|
||||
outIPArg := unsafe.Pointer(&outIP)
|
||||
var outPort uint16
|
||||
outPortArg := unsafe.Pointer(&outPort)
|
||||
err := n.tcpConnect.call(
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&instPtr),
|
||||
unsafe.Pointer(&dstPtr),
|
||||
unsafe.Pointer(&port),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outIPArg),
|
||||
unsafe.Pointer(&outPortArg),
|
||||
)
|
||||
runtime.KeepAlive(inst)
|
||||
runtime.KeepAlive(dst)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if handle == 0 {
|
||||
return 0, nil, n.lastError()
|
||||
}
|
||||
return handle, n.takeTCPAddr(outIP, outPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpBindTo(instance string, port uint16, timeout time.Duration) (uint64, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
inst := cString(instance)
|
||||
instPtr := unsafe.Pointer(&inst[0])
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var handle uint64
|
||||
var outIP unsafe.Pointer
|
||||
outIPArg := unsafe.Pointer(&outIP)
|
||||
var outPort uint16
|
||||
outPortArg := unsafe.Pointer(&outPort)
|
||||
err := n.tcpBind.call(
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&instPtr),
|
||||
unsafe.Pointer(&port),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outIPArg),
|
||||
unsafe.Pointer(&outPortArg),
|
||||
)
|
||||
runtime.KeepAlive(inst)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if handle == 0 {
|
||||
return 0, nil, n.lastError()
|
||||
}
|
||||
return handle, n.takeTCPAddr(outIP, outPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpAcceptFrom(handle uint64, timeout time.Duration) (uint64, *net.TCPAddr, *net.TCPAddr, error) {
|
||||
defer pinErrorThread()()
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
var stream uint64
|
||||
var outLocalIP unsafe.Pointer
|
||||
outLocalIPArg := unsafe.Pointer(&outLocalIP)
|
||||
var outLocalPort uint16
|
||||
outLocalPortArg := unsafe.Pointer(&outLocalPort)
|
||||
var outPeerIP unsafe.Pointer
|
||||
outPeerIPArg := unsafe.Pointer(&outPeerIP)
|
||||
var outPeerPort uint16
|
||||
outPeerPortArg := unsafe.Pointer(&outPeerPort)
|
||||
err := n.tcpAccept.call(
|
||||
unsafe.Pointer(&stream),
|
||||
unsafe.Pointer(&handle),
|
||||
unsafe.Pointer(&timeoutMS),
|
||||
unsafe.Pointer(&outLocalIPArg),
|
||||
unsafe.Pointer(&outLocalPortArg),
|
||||
unsafe.Pointer(&outPeerIPArg),
|
||||
unsafe.Pointer(&outPeerPortArg),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
if stream == 0 {
|
||||
return 0, nil, nil, n.lastError()
|
||||
}
|
||||
return stream, n.takeTCPAddr(outLocalIP, outLocalPort), n.takeTCPAddr(outPeerIP, outPeerPort), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpReadFrom(handle uint64, buf []byte, timeout time.Duration) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
bufPtr := unsafe.Pointer(&buf[0])
|
||||
length := uint32(len(buf))
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
err := n.tcpRead.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
|
||||
runtime.KeepAlive(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ret < 0 {
|
||||
return 0, n.lastError()
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpWriteTo(handle uint64, buf []byte, timeout time.Duration) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
bufPtr := unsafe.Pointer(&buf[0])
|
||||
length := uint32(len(buf))
|
||||
timeoutMS := uint64(timeout / time.Millisecond)
|
||||
err := n.tcpWrite.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle), unsafe.Pointer(&bufPtr), unsafe.Pointer(&length), unsafe.Pointer(&timeoutMS))
|
||||
runtime.KeepAlive(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ret < 0 {
|
||||
return 0, n.lastError()
|
||||
}
|
||||
return int(ret), nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpCloseHandle(handle uint64) error {
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
if err := n.tcpClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Native) tcpListenerCloseHandle(handle uint64) error {
|
||||
defer pinErrorThread()()
|
||||
var ret int32
|
||||
if err := n.tcpListenerClose.call(unsafe.Pointer(&ret), unsafe.Pointer(&handle)); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret != 0 {
|
||||
return n.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pinErrorThread ties an FFI op to the get_error_msg that reads its result: the
|
||||
// Rust side stores the last error in a thread-local, so the goroutine must not
|
||||
// migrate to another OS thread between the two calls. Use as `defer pinErrorThread()()`
|
||||
// at the start of any wrapper that reports failures through lastError.
|
||||
func pinErrorThread() func() {
|
||||
runtime.LockOSThread()
|
||||
return runtime.UnlockOSThread
|
||||
}
|
||||
|
||||
func (n *Native) lastError() error {
|
||||
var out unsafe.Pointer
|
||||
outArg := unsafe.Pointer(&out)
|
||||
if err := n.getErrorMsg.call(nil, unsafe.Pointer(&outArg)); err != nil {
|
||||
return err
|
||||
}
|
||||
if out == nil {
|
||||
return errors.New("easytier ffi call failed")
|
||||
}
|
||||
msg := readCString(out)
|
||||
_ = n.freeCString(out)
|
||||
if strings.Contains(msg, "timed out") {
|
||||
return timeoutError(msg)
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func (n *Native) freeCString(ptr unsafe.Pointer) error {
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return n.freeString.call(nil, unsafe.Pointer(&ptr))
|
||||
}
|
||||
|
||||
func (n *Native) takeTCPAddr(ipPtr unsafe.Pointer, port uint16) *net.TCPAddr {
|
||||
if ipPtr == nil {
|
||||
return nil
|
||||
}
|
||||
ip := net.ParseIP(readCString(ipPtr))
|
||||
_ = n.freeCString(ipPtr)
|
||||
return &net.TCPAddr{IP: ip, Port: int(port)}
|
||||
}
|
||||
|
||||
func (d *atomicDeadline) set(t time.Time) {
|
||||
if t.IsZero() {
|
||||
d.v.Store(0)
|
||||
return
|
||||
}
|
||||
d.v.Store(t.UnixNano())
|
||||
}
|
||||
|
||||
func (d *atomicDeadline) timeout(fallback time.Duration) time.Duration {
|
||||
ns := d.v.Load()
|
||||
if ns == 0 {
|
||||
return fallback
|
||||
}
|
||||
remaining := time.Until(time.Unix(0, ns))
|
||||
if remaining <= 0 {
|
||||
return time.Millisecond
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func (e timeoutError) Error() string { return string(e) }
|
||||
func (e timeoutError) Timeout() bool { return true }
|
||||
func (e timeoutError) Temporary() bool { return true }
|
||||
|
||||
func opError(op string, addr net.Addr, err error) error {
|
||||
return &net.OpError{Op: op, Net: "easytier", Addr: addr, Err: err}
|
||||
}
|
||||
|
||||
func parseIPPort(address string) (net.IP, int, error) {
|
||||
host, portStr, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return nil, 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ip, int(port), nil
|
||||
}
|
||||
|
||||
func parseListenPort(address string) (int, error) {
|
||||
host, portStr, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if host != "" {
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return 0, fmt.Errorf("easytier ffi requires an IP address, got %q", host)
|
||||
}
|
||||
if !ip.IsUnspecified() {
|
||||
return 0, fmt.Errorf("easytier ffi listen address must be unspecified, got %q", host)
|
||||
}
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(port), nil
|
||||
}
|
||||
|
||||
func cString(s string) []byte {
|
||||
if strings.ContainsRune(s, 0) {
|
||||
panic("easytier ffi string contains NUL")
|
||||
}
|
||||
return append([]byte(s), 0)
|
||||
}
|
||||
|
||||
func readCString(ptr unsafe.Pointer) string {
|
||||
if ptr == nil {
|
||||
return ""
|
||||
}
|
||||
var b []byte
|
||||
for p := uintptr(ptr); ; p++ {
|
||||
c := *(*byte)(unsafe.Pointer(p))
|
||||
if c == 0 {
|
||||
return string(b)
|
||||
}
|
||||
b = append(b, c)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLibraryPath() string {
|
||||
if p := os.Getenv("EASYTIER_FFI_LIB"); p != "" {
|
||||
return p
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return "../../../../target/debug/libeasytier_ffi.dylib"
|
||||
case "windows":
|
||||
return "..\\..\\..\\..\\target\\debug\\easytier_ffi.dll"
|
||||
default:
|
||||
return "../../../../target/debug/libeasytier_ffi.so"
|
||||
}
|
||||
}
|
||||
|
||||
var _ net.Conn = (*Conn)(nil)
|
||||
var _ net.Listener = (*Listener)(nil)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,360 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const asyncLocalTestTimeout = 120 * time.Second
|
||||
|
||||
func TestAsyncSymbolBinding(t *testing.T) {
|
||||
n := openAsyncForTest(t)
|
||||
|
||||
status, err := n.opWaitStatus(0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != dataPlaneOpInvalid {
|
||||
t.Fatalf("expected invalid status for op 0, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncLocalTwoNodeTCPAndUDP(t *testing.T) {
|
||||
n := openAsyncForTest(t)
|
||||
topology := startLocalAsyncTopology(t, n)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), asyncLocalTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
runAsyncTCPPingPong(t, ctx, n, topology)
|
||||
runAsyncUDPPingPong(t, ctx, n, topology)
|
||||
}
|
||||
|
||||
type localAsyncTopology struct {
|
||||
dialerInstance string
|
||||
listenerInstance string
|
||||
listenerIP string
|
||||
}
|
||||
|
||||
func openAsyncForTest(t *testing.T) *AsyncNative {
|
||||
t.Helper()
|
||||
|
||||
libraryPath := defaultLibraryPath()
|
||||
if _, err := os.Stat(libraryPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Skipf("build easytier-ffi with ffi-dataplane before running async tests: %v", err)
|
||||
}
|
||||
t.Fatalf("stat async ffi library: %v", err)
|
||||
}
|
||||
|
||||
n, err := OpenAsync(libraryPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open async ffi library: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := n.Close(); err != nil {
|
||||
t.Errorf("close async native: %v", err)
|
||||
}
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
func startLocalAsyncTopology(t *testing.T, n *AsyncNative) localAsyncTopology {
|
||||
t.Helper()
|
||||
|
||||
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
networkName := "ffi-async-" + suffix
|
||||
networkSecret := "ffi-async-secret-" + suffix
|
||||
listenerInstance := "ffi-async-listener-" + suffix
|
||||
dialerInstance := "ffi-async-dialer-" + suffix
|
||||
listenerIP := "10.251.1.2"
|
||||
dialerIP := "10.251.1.1"
|
||||
listenerPort := freeLocalTCPPort(t)
|
||||
listenerEndpoint := fmt.Sprintf("tcp://127.0.0.1:%d", listenerPort)
|
||||
t.Cleanup(func() {
|
||||
if err := n.deleteNetworkInstances([]string{dialerInstance, listenerInstance}); err != nil {
|
||||
t.Errorf("cleanup async test EasyTier instances: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
listenerConfig := localAsyncConfig(
|
||||
listenerInstance,
|
||||
listenerIP,
|
||||
networkName,
|
||||
networkSecret,
|
||||
[]string{listenerEndpoint},
|
||||
nil,
|
||||
)
|
||||
dialerConfig := localAsyncConfig(
|
||||
dialerInstance,
|
||||
dialerIP,
|
||||
networkName,
|
||||
networkSecret,
|
||||
nil,
|
||||
[]string{listenerEndpoint},
|
||||
)
|
||||
|
||||
if err := n.RunNetworkInstance(listenerConfig); err != nil {
|
||||
t.Fatalf("start listener instance: %v", err)
|
||||
}
|
||||
if err := n.RunNetworkInstance(dialerConfig); err != nil {
|
||||
t.Fatalf("start dialer instance: %v", err)
|
||||
}
|
||||
|
||||
return localAsyncTopology{
|
||||
dialerInstance: dialerInstance,
|
||||
listenerInstance: listenerInstance,
|
||||
listenerIP: listenerIP,
|
||||
}
|
||||
}
|
||||
|
||||
func localAsyncConfig(instance, ipv4, networkName, networkSecret string, listeners, peers []string) string {
|
||||
config := fmt.Sprintf(`instance_name = %s
|
||||
ipv4 = %s
|
||||
listeners = %s
|
||||
|
||||
[network_identity]
|
||||
network_name = %s
|
||||
network_secret = %s
|
||||
|
||||
[flags]
|
||||
no_tun = true
|
||||
bind_device = false
|
||||
`,
|
||||
strconv.Quote(instance),
|
||||
strconv.Quote(ipv4),
|
||||
tomlStringList(listeners),
|
||||
strconv.Quote(networkName),
|
||||
strconv.Quote(networkSecret),
|
||||
)
|
||||
for _, peer := range peers {
|
||||
config += fmt.Sprintf("\n[[peer]]\nuri = %s\n", strconv.Quote(peer))
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func tomlStringList(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
|
||||
out := "["
|
||||
for i, value := range values {
|
||||
if i > 0 {
|
||||
out += ", "
|
||||
}
|
||||
out += strconv.Quote(value)
|
||||
}
|
||||
return out + "]"
|
||||
}
|
||||
|
||||
func freeLocalTCPPort(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("allocate local tcp port: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
return listener.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func runAsyncTCPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) {
|
||||
t.Helper()
|
||||
|
||||
listener, listenerAddr := eventuallyTCPListen(t, ctx, n, topology.listenerInstance)
|
||||
|
||||
tcpCtx, cancel := context.WithCancel(ctx)
|
||||
accepted := make(chan error, 1)
|
||||
defer waitForAsyncHelper(t, accepted, "tcp accept helper")
|
||||
defer cancel()
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- fmt.Errorf("accept tcp stream: %w", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
payload := make([]byte, len("ping"))
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
accepted <- fmt.Errorf("read tcp ping: %w", err)
|
||||
return
|
||||
}
|
||||
if string(payload) != "ping" {
|
||||
accepted <- fmt.Errorf("expected tcp ping, got %q", string(payload))
|
||||
return
|
||||
}
|
||||
if _, err := conn.Write([]byte("pong")); err != nil {
|
||||
accepted <- fmt.Errorf("write tcp pong: %w", err)
|
||||
return
|
||||
}
|
||||
accepted <- nil
|
||||
}()
|
||||
|
||||
conn, err := eventuallyTCPDial(t, tcpCtx, n, topology.dialerInstance, topology.listenerIP, listenerAddr.Port)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
if _, err := conn.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("write tcp ping: %v", err)
|
||||
}
|
||||
payload := make([]byte, len("pong"))
|
||||
if _, err := io.ReadFull(conn, payload); err != nil {
|
||||
t.Fatalf("read tcp pong: %v", err)
|
||||
}
|
||||
if string(payload) != "pong" {
|
||||
t.Fatalf("expected tcp pong, got %q", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func eventuallyTCPListen(t *testing.T, ctx context.Context, n *AsyncNative, instance string) (net.Listener, *net.TCPAddr) {
|
||||
t.Helper()
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
listener, err := n.ListenContext(attemptCtx, instance, "tcp", "0.0.0.0:0")
|
||||
cancel()
|
||||
if err == nil {
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
t.Logf("async tcp bind succeeded on attempt %d at %s", attempt, addr)
|
||||
return listener, addr
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: async tcp bind failed: %v", attempt, err)
|
||||
waitForRetry(ctx, 500*time.Millisecond)
|
||||
}
|
||||
t.Fatalf("async tcp bind never succeeded: %v", lastErr)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func eventuallyTCPDial(t *testing.T, ctx context.Context, n *AsyncNative, instance, ip string, port int) (net.Conn, error) {
|
||||
t.Helper()
|
||||
|
||||
address := net.JoinHostPort(ip, strconv.Itoa(port))
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
conn, err := n.DialContext(attemptCtx, instance, "tcp", address)
|
||||
cancel()
|
||||
if err == nil {
|
||||
t.Logf("async tcp connect succeeded on attempt %d to %s", attempt, address)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: async tcp connect failed: %v", attempt, err)
|
||||
waitForRetry(ctx, 500*time.Millisecond)
|
||||
}
|
||||
return nil, fmt.Errorf("async tcp connect never succeeded: %w", lastErr)
|
||||
}
|
||||
|
||||
func runAsyncUDPPingPong(t *testing.T, ctx context.Context, n *AsyncNative, topology localAsyncTopology) {
|
||||
t.Helper()
|
||||
|
||||
dialerSocket, err := n.UDPBindContext(ctx, topology.dialerInstance, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("bind dialer udp socket: %v", err)
|
||||
}
|
||||
|
||||
listenerSocket, err := n.UDPBindContext(ctx, topology.listenerInstance, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("bind listener udp socket: %v", err)
|
||||
}
|
||||
|
||||
udpCtx, cancel := context.WithCancel(ctx)
|
||||
warmupDone := make(chan error, 1)
|
||||
received := make(chan error, 1)
|
||||
defer waitForAsyncHelper(t, received, "udp receive helper")
|
||||
defer cancel()
|
||||
defer listenerSocket.Close()
|
||||
defer dialerSocket.Close()
|
||||
|
||||
go func() {
|
||||
if _, err := listenerSocket.SendTo(udpCtx, []byte("warmup"), dialerSocket.LocalAddr()); err != nil {
|
||||
err = fmt.Errorf("send udp warmup: %w", err)
|
||||
warmupDone <- err
|
||||
received <- err
|
||||
return
|
||||
}
|
||||
warmupDone <- nil
|
||||
|
||||
payload, from, err := listenerSocket.RecvFrom(udpCtx, 512)
|
||||
if err != nil {
|
||||
received <- fmt.Errorf("recv udp ping: %w", err)
|
||||
return
|
||||
}
|
||||
if string(payload) != "ping" {
|
||||
received <- fmt.Errorf("expected udp ping, got %q", string(payload))
|
||||
return
|
||||
}
|
||||
if _, err := listenerSocket.SendTo(udpCtx, []byte("pong"), from); err != nil {
|
||||
received <- fmt.Errorf("send udp pong: %w", err)
|
||||
return
|
||||
}
|
||||
received <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-warmupDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-udpCtx.Done():
|
||||
t.Fatal(udpCtx.Err())
|
||||
}
|
||||
|
||||
target := &net.UDPAddr{IP: net.ParseIP(topology.listenerIP), Port: listenerSocket.LocalAddr().Port}
|
||||
if _, err := dialerSocket.SendTo(udpCtx, []byte("ping"), target); err != nil {
|
||||
t.Fatalf("send udp ping: %v", err)
|
||||
}
|
||||
for {
|
||||
payload, from, err := dialerSocket.RecvFrom(udpCtx, 512)
|
||||
if err != nil {
|
||||
t.Fatalf("recv udp pong: %v", err)
|
||||
}
|
||||
if string(payload) == "pong" {
|
||||
if !from.IP.Equal(target.IP) || from.Port != target.Port {
|
||||
t.Fatalf("expected udp pong from %s, got %s", target, from)
|
||||
}
|
||||
break
|
||||
}
|
||||
t.Logf("skipping udp datagram from %s: %q", from, string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func waitForAsyncHelper(t *testing.T, done <-chan error, name string) {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", name, err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Errorf("%s did not stop", name)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForRetry(ctx context.Context, delay time.Duration) {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package easytierffi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSSHIntegration(t *testing.T) {
|
||||
config := os.Getenv("EASYTIER_FFI_CONFIG")
|
||||
instance := os.Getenv("EASYTIER_FFI_INSTANCE")
|
||||
target := os.Getenv("EASYTIER_FFI_TARGET")
|
||||
if config == "" || instance == "" || target == "" {
|
||||
t.Skip("set EASYTIER_FFI_CONFIG, EASYTIER_FFI_INSTANCE and EASYTIER_FFI_TARGET to run integration test")
|
||||
}
|
||||
|
||||
n, err := Open(defaultLibraryPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer n.Close()
|
||||
|
||||
if err := n.RunNetworkInstance(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; ctx.Err() == nil; attempt++ {
|
||||
conn, err := n.DialContext(ctx, instance, "tcp", target)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: dial failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
buf := make([]byte, 128)
|
||||
nn, err := conn.Read(buf)
|
||||
_ = conn.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
t.Logf("attempt %d: read failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
continue
|
||||
}
|
||||
banner := string(buf[:nn])
|
||||
if !strings.HasPrefix(banner, "SSH-") {
|
||||
t.Fatalf("attempt %d: expected SSH banner, got %q", attempt, banner)
|
||||
}
|
||||
t.Logf("attempt %d: got banner %q", attempt, strings.TrimRight(banner, "\r\n"))
|
||||
return
|
||||
}
|
||||
t.Fatalf("never got SSH banner, last err: %v", lastErr)
|
||||
}
|
||||
|
||||
func TestTCPListenIntegration(t *testing.T) {
|
||||
config := os.Getenv("EASYTIER_FFI_LISTEN_CONFIG")
|
||||
instance := os.Getenv("EASYTIER_FFI_LISTEN_INSTANCE")
|
||||
listenPort := os.Getenv("EASYTIER_FFI_LISTEN_PORT")
|
||||
if config == "" || instance == "" || listenPort == "" {
|
||||
t.Skip("set EASYTIER_FFI_LISTEN_CONFIG, EASYTIER_FFI_LISTEN_INSTANCE and EASYTIER_FFI_LISTEN_PORT to run integration test")
|
||||
}
|
||||
port, err := strconv.ParseUint(listenPort, 10, 16)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := Open(defaultLibraryPath())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer n.Close()
|
||||
|
||||
if err := n.RunNetworkInstance(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Data-plane readiness is asynchronous: the instance must finish starting
|
||||
// before the data plane accepts binds. Retry until ready or ctx expires.
|
||||
var listener net.Listener
|
||||
for attempt := 1; ; attempt++ {
|
||||
listener, err = n.ListenContext(ctx, instance, "tcp", net.JoinHostPort("0.0.0.0", strconv.Itoa(int(port))))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("bind never succeeded, last err: %v", err)
|
||||
}
|
||||
t.Logf("attempt %d: bind failed: %v", attempt, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
t.Logf("listening on %s; connect from another EasyTier peer and send ping", listener.Addr())
|
||||
|
||||
accepted := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
accepted <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
buf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
accepted <- err
|
||||
return
|
||||
}
|
||||
if string(buf) != "ping" {
|
||||
accepted <- fmt.Errorf("expected %q, got %q", "ping", string(buf))
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("pong"))
|
||||
accepted <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-accepted:
|
||||
_ = listener.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
_ = listener.Close()
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module easytierffi-example
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/go-webgpu/goffi v0.4.1
|
||||
@@ -13,14 +13,18 @@ use easytier::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader as _, TomlConfigLoader},
|
||||
},
|
||||
web_client::{WebClient, WebClientHooks, parse_config_server_endpoint, run_web_client},
|
||||
tunnel::TunnelScheme,
|
||||
web_client::{WebClient, WebClientHooks, run_web_client},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
data_plane::remove_data_plane_sessions_by_instance_ids,
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
state::{
|
||||
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
types::ConfigServerEventCallback,
|
||||
};
|
||||
@@ -72,9 +76,37 @@ pub fn validate_config_server_client_options(
|
||||
return Err("machine_id is empty".to_string());
|
||||
}
|
||||
|
||||
parse_config_server_endpoint(config_server_url_s)
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
let config_server_url = match url::Url::parse(config_server_url_s) {
|
||||
Ok(url) => url,
|
||||
Err(_) => format!(
|
||||
"udp://config-server.easytier.cn:22020/{}",
|
||||
config_server_url_s
|
||||
)
|
||||
.parse()
|
||||
.map_err(|err| format!("failed to parse config server URL: {}", err))?,
|
||||
};
|
||||
|
||||
TunnelScheme::try_from(&config_server_url).map_err(|_| {
|
||||
format!(
|
||||
"unsupported config server scheme: {}",
|
||||
config_server_url.scheme()
|
||||
)
|
||||
})?;
|
||||
|
||||
let token = config_server_url
|
||||
.path_segments()
|
||||
.and_then(|mut segments| segments.next_back())
|
||||
.map(|segment| percent_encoding::percent_decode_str(segment).decode_utf8())
|
||||
.transpose()
|
||||
.map_err(|err| format!("failed to decode config server token: {}", err))?
|
||||
.map(|token| token.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
if token.is_empty() {
|
||||
return Err("empty token".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ManagedConfigServerClient {
|
||||
@@ -118,8 +150,7 @@ impl ManagedConfigServerClientHooks {
|
||||
}
|
||||
|
||||
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||
if let Some(existing_id) =
|
||||
resolve_instance_id_by_name(inst_name).map_err(|error| error.to_string())?
|
||||
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
|
||||
&& existing_id != inst_id
|
||||
{
|
||||
return Err(format!("instance name {} already exists", inst_name));
|
||||
@@ -128,6 +159,13 @@ impl ManagedConfigServerClientHooks {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit_instance_name(&self, inst_name: String, inst_id: Uuid) -> Result<(), String> {
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, existing_id| *existing_id != inst_id);
|
||||
self.validate_instance_name(&inst_name, inst_id)?;
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, inst_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start_stopping(&self) -> Vec<Uuid> {
|
||||
let _delivery_guard = if in_config_server_callback() {
|
||||
None
|
||||
@@ -161,15 +199,11 @@ impl ManagedConfigServerClientHooks {
|
||||
let Some(callback) = self.callback else {
|
||||
return Ok(());
|
||||
};
|
||||
let instance_name = ffi_context()
|
||||
.manager
|
||||
.instance(instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
let instance_name = INSTANCE_MANAGER
|
||||
.get_instance_name(&instance_id)
|
||||
.unwrap_or_default();
|
||||
let network_name = ffi_context()
|
||||
.manager
|
||||
.config(instance_id)
|
||||
.map(|config| config.get_network_identity().network_name)
|
||||
let network_name = INSTANCE_MANAGER
|
||||
.get_network_name(&instance_id)
|
||||
.unwrap_or_default();
|
||||
let event_json = serde_json::json!({
|
||||
"event": event,
|
||||
@@ -229,27 +263,75 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
return Err("config server client is stopping".to_string());
|
||||
}
|
||||
let Some(inst_name) = ffi_context()
|
||||
.manager
|
||||
.instance(*id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
else {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
let Some(inst_name) = INSTANCE_MANAGER.get_instance_name(id) else {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
self.instance_ids
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id);
|
||||
if let Err(error) = self.validate_instance_name(&inst_name, *id) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
return Err(error);
|
||||
{
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
if !self.stopping.load(Ordering::Acquire) {
|
||||
return Err(format!("instance {} not found after start", id));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let should_delete = {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
true
|
||||
} else {
|
||||
guard.insert(*id);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if should_delete {
|
||||
if let Err(err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(err) = self.commit_instance_name(inst_name.clone(), *id) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
if let Err(delete_err) = INSTANCE_MANAGER.delete_network_instance(vec![*id]) {
|
||||
return Err(format!(
|
||||
"{}; failed to delete duplicate instance: {}",
|
||||
err, delete_err
|
||||
));
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
if self.stopping.load(Ordering::Acquire) {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Ok(());
|
||||
}
|
||||
if INSTANCE_MANAGER.get_instance_name(id).is_none() {
|
||||
self.remove_tracked_instance_ids(&[*id])?;
|
||||
remove_instance_name_ids(&[*id]);
|
||||
return Err(format!(
|
||||
"instance {} was removed before post-run completed",
|
||||
id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
remove_data_plane_sessions_by_instance_ids(&[*id]);
|
||||
remove_data_plane_handles_by_instance_ids(&[*id]);
|
||||
|
||||
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||
self.note_callback_error(err);
|
||||
@@ -258,8 +340,15 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_data_plane_sessions_by_instance_ids(&removed_ids);
|
||||
let removed_ids = {
|
||||
let _mutation_guard = INSTANCE_MUTATION_LOCK
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_instance_name_ids(ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
removed_ids
|
||||
};
|
||||
|
||||
for id in removed_ids {
|
||||
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||
@@ -396,12 +485,12 @@ pub(crate) unsafe fn start_config_server_client(
|
||||
drop(data_plane_usage_guard);
|
||||
|
||||
let hooks = Arc::new(ManagedConfigServerClientHooks::new(callback, user_data));
|
||||
let client = match ffi_context().runtime.block_on(run_web_client(
|
||||
let client = match ASYNC_RUNTIME.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
config_server_machine_id_options(machine_id),
|
||||
hostname,
|
||||
secure_mode,
|
||||
ffi_context().manager.clone(),
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
)) {
|
||||
Ok(client) => client,
|
||||
@@ -422,7 +511,7 @@ pub(crate) fn stop_config_server_client() -> c_int {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
@@ -439,25 +528,29 @@ pub(crate) fn stop_config_server_client() -> c_int {
|
||||
return -1;
|
||||
}
|
||||
let hooks = managed.hooks.clone();
|
||||
// Keep the client discoverable until the canonical transaction drains its
|
||||
// tracking. Earlier removals must still retire IDs from these same hooks.
|
||||
let managed = guard.take().expect("config server client exists");
|
||||
drop(guard);
|
||||
|
||||
let delete_result = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances_selected_by(|| hooks.start_stopping()),
|
||||
);
|
||||
let managed = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(mut guard) => guard.take(),
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let tracked_ids = hooks.start_stopping();
|
||||
drop(managed);
|
||||
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock config server client: {err}"));
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
drop(managed);
|
||||
let delete_result = INSTANCE_MANAGER.delete_network_instance(tracked_ids.clone());
|
||||
if delete_result.is_ok() {
|
||||
remove_instance_name_ids(&tracked_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&tracked_ids);
|
||||
}
|
||||
drop(_mutation_guard);
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
|
||||
@@ -0,0 +1,928 @@
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use std::{
|
||||
future::Future,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
Arc, RwLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use dashmap::DashMap;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use easytier::launcher::{DataPlaneTcpListener, DataPlaneTcpStream, DataPlaneUdpSocket};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use tokio_util::sync::CancellationToken;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||
error::{free_string, set_error_msg},
|
||||
state::{INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP},
|
||||
};
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static NEXT_DATA_PLANE_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_HANDLES: once_cell::sync::Lazy<DashMap<u64, DataPlaneHandle>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) struct DataPlaneHandle {
|
||||
pub(crate) instance_id: uuid::Uuid,
|
||||
pub(crate) runtime: tokio::runtime::Handle,
|
||||
// Cancelled by close() to wake any in-flight op on this handle.
|
||||
pub(crate) close_token: CancellationToken,
|
||||
pub(crate) resource: DataPlaneResource,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) struct TcpHalves {
|
||||
pub(crate) read: tokio::sync::Mutex<ReadHalf<DataPlaneTcpStream>>,
|
||||
pub(crate) write: tokio::sync::Mutex<WriteHalf<DataPlaneTcpStream>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) enum DataPlaneResource {
|
||||
Tcp(Arc<TcpHalves>),
|
||||
TcpListener(Arc<tokio::sync::Mutex<DataPlaneTcpListener>>),
|
||||
Udp(Arc<DataPlaneUdpSocket>),
|
||||
}
|
||||
|
||||
// Several helper functions for FFI data plane operations to facilitate logic reuse.
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn next_handle() -> u64 {
|
||||
NEXT_DATA_PLANE_HANDLE.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn timeout_duration(timeout_ms: u64) -> Duration {
|
||||
Duration::from_millis(timeout_ms)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn cstr_to_string(ptr: *const std::ffi::c_char, name: &str) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
set_error_msg(&format!("{} is null", name));
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
unsafe { std::ffi::CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_instance_id(inst_name: &str) -> Option<uuid::Uuid> {
|
||||
INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id.value())
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn parse_socket_addr(host: &str, port: u16) -> Option<SocketAddr> {
|
||||
let ip = match host.parse::<IpAddr>() {
|
||||
Ok(ip) => ip,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to parse ip address: {}", e));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SocketAddr::new(ip, port))
|
||||
}
|
||||
|
||||
/// Encode an IP address for FFI return. Returns `*mut c_char` to match
|
||||
/// `CString::into_raw`; caller releases it via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn into_ffi_ip_cstring(ip: IpAddr) -> Option<*mut std::ffi::c_char> {
|
||||
match std::ffi::CString::new(ip.to_string()) {
|
||||
Ok(s) => Some(s.into_raw()),
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to encode ip: {}", e));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_runtime_handle(
|
||||
inst_id: &uuid::Uuid,
|
||||
deadline: std::time::Instant,
|
||||
) -> Option<tokio::runtime::Handle> {
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let Some(rt) = INSTANCE_MANAGER.data_plane_wait_runtime_handle(inst_id, remaining) else {
|
||||
set_error_msg("instance runtime is not ready");
|
||||
return None;
|
||||
};
|
||||
Some(rt)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_tcp_stream_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
stream: DataPlaneTcpStream,
|
||||
) -> u64 {
|
||||
let (rd, wr) = tokio::io::split(stream);
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Tcp(Arc::new(TcpHalves {
|
||||
read: tokio::sync::Mutex::new(rd),
|
||||
write: tokio::sync::Mutex::new(wr),
|
||||
})),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_tcp_listener_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
listener: DataPlaneTcpListener,
|
||||
) -> u64 {
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::TcpListener(Arc::new(tokio::sync::Mutex::new(listener))),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn insert_udp_socket_handle(
|
||||
instance_id: uuid::Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
socket: DataPlaneUdpSocket,
|
||||
) -> u64 {
|
||||
let handle = next_handle();
|
||||
DATA_PLANE_HANDLES.insert(
|
||||
handle,
|
||||
DataPlaneHandle {
|
||||
instance_id,
|
||||
runtime,
|
||||
close_token: CancellationToken::new(),
|
||||
resource: DataPlaneResource::Udp(Arc::new(socket)),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_stream(
|
||||
handle: u64,
|
||||
) -> Option<(Arc<TcpHalves>, tokio::runtime::Handle, CancellationToken)> {
|
||||
get_tcp_stream_with_instance(handle)
|
||||
.map(|(halves, runtime, close_token, _)| (halves, runtime, close_token))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_stream_with_instance(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<TcpHalves>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp stream handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Tcp(halves) => Some((
|
||||
halves.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::TcpListener(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp stream");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_tcp_listener(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<tokio::sync::Mutex<DataPlaneTcpListener>>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("tcp listener handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::TcpListener(listener) => Some((
|
||||
listener.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::Udp(_) => {
|
||||
set_error_msg("handle is not a tcp listener");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_udp_socket(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<DataPlaneUdpSocket>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
)> {
|
||||
get_udp_socket_with_instance(handle)
|
||||
.map(|(socket, runtime, close_token, _)| (socket, runtime, close_token))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn get_udp_socket_with_instance(
|
||||
handle: u64,
|
||||
) -> Option<(
|
||||
Arc<DataPlaneUdpSocket>,
|
||||
tokio::runtime::Handle,
|
||||
CancellationToken,
|
||||
uuid::Uuid,
|
||||
)> {
|
||||
let Some(h) = DATA_PLANE_HANDLES.get(&handle) else {
|
||||
set_error_msg("udp socket handle not found");
|
||||
return None;
|
||||
};
|
||||
match &h.resource {
|
||||
DataPlaneResource::Udp(socket) => Some((
|
||||
socket.clone(),
|
||||
h.runtime.clone(),
|
||||
h.close_token.clone(),
|
||||
h.instance_id,
|
||||
)),
|
||||
DataPlaneResource::Tcp(_) | DataPlaneResource::TcpListener(_) => {
|
||||
set_error_msg("handle is not a udp socket");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let _data_plane_usage_guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.unwrap_or_else(|err| err.into_inner());
|
||||
|
||||
DATA_PLANE_HANDLES.retain(|_, handle| {
|
||||
if ids.contains(&handle.instance_id) {
|
||||
handle.close_token.cancel();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
crate::data_plane_async::remove_ops_by_instance_ids(ids);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ffi-dataplane"))]
|
||||
pub(crate) fn remove_data_plane_handles_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_rejected() -> bool {
|
||||
if in_config_server_callback() {
|
||||
set_error_msg("cannot use data plane from config server callback");
|
||||
true
|
||||
} else if is_config_server_active_or_stopping() {
|
||||
set_error_msg("cannot use data plane while config server client is active");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn enter_data_plane_operation() -> Option<std::sync::RwLockReadGuard<'static, ()>> {
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let guard = match DATA_PLANE_USAGE_LOCK.read() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock data plane usage: {}", err));
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if data_plane_rejected() {
|
||||
return None;
|
||||
}
|
||||
Some(guard)
|
||||
}
|
||||
|
||||
/// Run an IO op on the resource's owning runtime, supporting
|
||||
/// timeout and cancellation.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
async fn run_with_cancel<T, F>(
|
||||
close_token: &CancellationToken,
|
||||
timeout_ms: u64,
|
||||
error_prefix: &str,
|
||||
op: F,
|
||||
) -> Option<Result<T, std::io::Error>>
|
||||
where
|
||||
F: Future<Output = Result<T, std::io::Error>>,
|
||||
{
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = close_token.cancelled() => {
|
||||
set_error_msg(&format!("{}: handle closed", error_prefix));
|
||||
None
|
||||
}
|
||||
res = tokio::time::timeout(timeout_duration(timeout_ms), op) => match res {
|
||||
Ok(r) => Some(r),
|
||||
Err(_) => {
|
||||
set_error_msg(&format!("{} timed out", error_prefix));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn lock_for_config_server_start()
|
||||
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||
let guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.map_err(|err| format!("failed to lock data plane usage: {}", err))?;
|
||||
if !DATA_PLANE_HANDLES.is_empty() || crate::data_plane_async::has_live_ops() {
|
||||
return Err("cannot start config server client while data plane is in use".to_string());
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
/// # Safety
|
||||
/// Open a TCP stream through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. On success, writes the local socket address chosen for this
|
||||
/// connection into `out_local_ip` (a heap-allocated C string the caller must
|
||||
/// release via `free_string`) and `out_local_port`. Both out pointers must be
|
||||
/// non-null.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_connect(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_connect(&inst_id, dst_addr, remaining));
|
||||
match result {
|
||||
Ok(stream) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_tcp_stream_handle(inst_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to connect tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a TCP listener through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_tcp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(listener) => {
|
||||
let local_addr = listener.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_tcp_listener_handle(inst_id, runtime, listener);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Accept one connection from a TCP data-plane listener. Returns a TCP stream
|
||||
/// handle, or 0 on failure. Local and peer addresses are written into out
|
||||
/// parameters; returned IP strings must be released via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_accept(
|
||||
handle: u64,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
out_peer_ip: *mut *const std::ffi::c_char,
|
||||
out_peer_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null()
|
||||
|| out_local_port.is_null()
|
||||
|| out_peer_ip.is_null()
|
||||
|| out_peer_port.is_null()
|
||||
{
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some((listener, runtime, close_token, instance_id)) = get_tcp_listener(handle) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let ret = runtime.block_on(async move {
|
||||
let mut listener = listener.lock().await;
|
||||
run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"tcp data plane accept",
|
||||
listener.accept(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
match ret {
|
||||
Some(Ok((stream, peer_addr))) => {
|
||||
let local_addr = stream.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(peer_ip) = into_ffi_ip_cstring(peer_addr.ip()) else {
|
||||
free_string(local_ip);
|
||||
return 0;
|
||||
};
|
||||
let stream_handle = insert_tcp_stream_handle(instance_id, runtime, stream);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
*out_peer_ip = peer_ip as *const std::ffi::c_char;
|
||||
*out_peer_port = peer_addr.port();
|
||||
}
|
||||
stream_handle
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to accept tcp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Read from a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_read(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, len as usize) };
|
||||
runtime.block_on(async move {
|
||||
let mut rd = halves.read.lock().await;
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to read tcp data plane",
|
||||
rd.read(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to read tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Write to a TCP data-plane stream.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_tcp_write(
|
||||
handle: u64,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((halves, runtime, close_token)) = get_tcp_stream(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
let mut wr = halves.write.lock().await;
|
||||
// Use `write_all` to honor `net.Conn::Write` semantics on the Go side
|
||||
// (must write everything or return an error); single `write()` can
|
||||
// silently short-write and corrupt streams that the caller assumes are
|
||||
// fully written.
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to write tcp data plane",
|
||||
wr.write_all(buf),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(())) => total as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to write tcp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Tcp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp stream"
|
||||
} else {
|
||||
"tcp stream handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
if let DataPlaneResource::Tcp(halves) = h.resource {
|
||||
// Best-effort half-close; if write half is in use, the in-flight call
|
||||
// observes the cancel token and releases the lock shortly after.
|
||||
h.runtime.spawn(async move {
|
||||
if let Ok(mut wr) = halves.write.try_lock() {
|
||||
let _ = wr.shutdown().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_tcp_listener_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::TcpListener(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a tcp listener"
|
||||
} else {
|
||||
"tcp listener handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Bind a UDP socket through an EasyTier instance data plane. Returns 0 on
|
||||
/// failure. The local address actually bound (which may differ from the
|
||||
/// requested port when `local_port == 0`) is written into `out_local_ip` /
|
||||
/// `out_local_port`; the caller must release `*out_local_ip` via `free_string`.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_bind(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
local_port: std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const std::ffi::c_char,
|
||||
out_local_port: *mut std::ffi::c_ushort,
|
||||
) -> u64 {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return 0,
|
||||
};
|
||||
if out_local_ip.is_null() || out_local_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return 0;
|
||||
}
|
||||
let Some(inst_name) = (unsafe { cstr_to_string(inst_name, "inst_name") }) else {
|
||||
return 0;
|
||||
};
|
||||
let Some(inst_id) = get_instance_id(&inst_name) else {
|
||||
set_error_msg("instance not found");
|
||||
return 0;
|
||||
};
|
||||
let deadline = std::time::Instant::now() + timeout_duration(timeout_ms);
|
||||
let Some(runtime) = get_runtime_handle(&inst_id, deadline) else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
|
||||
let result =
|
||||
runtime.block_on(INSTANCE_MANAGER.data_plane_udp_bind(&inst_id, local_port, remaining));
|
||||
match result {
|
||||
Ok(socket) => {
|
||||
let local_addr = socket.local_addr();
|
||||
let Some(local_ip) = into_ffi_ip_cstring(local_addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
let handle = insert_udp_socket_handle(inst_id, runtime, socket);
|
||||
unsafe {
|
||||
*out_local_ip = local_ip as *const std::ffi::c_char;
|
||||
*out_local_port = local_addr.port();
|
||||
}
|
||||
handle
|
||||
}
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to bind udp data plane: {}", e));
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Send a datagram through a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_send_to(
|
||||
handle: u64,
|
||||
dst_ip: *const std::ffi::c_char,
|
||||
dst_port: std::ffi::c_ushort,
|
||||
buf: *const std::ffi::c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() {
|
||||
set_error_msg("buf is null");
|
||||
return -1;
|
||||
}
|
||||
let Some(dst_ip) = (unsafe { cstr_to_string(dst_ip, "dst_ip") }) else {
|
||||
return -1;
|
||||
};
|
||||
let Some(dst_addr) = parse_socket_addr(&dst_ip, dst_port) else {
|
||||
return -1;
|
||||
};
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts(buf, total) };
|
||||
runtime.block_on(async move {
|
||||
match run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"failed to send udp data plane",
|
||||
socket.send_to(buf, dst_addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Some(Ok(n)) => n as std::ffi::c_int,
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to send udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Receive a datagram from a UDP data-plane socket.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) unsafe fn data_plane_udp_recv_from(
|
||||
handle: u64,
|
||||
buf: *mut std::ffi::c_uchar,
|
||||
len: u32,
|
||||
out_ip: *mut *const std::ffi::c_char,
|
||||
out_port: *mut std::ffi::c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
if buf.is_null() || out_ip.is_null() || out_port.is_null() {
|
||||
set_error_msg("output pointer is null");
|
||||
return -1;
|
||||
}
|
||||
let Some((socket, runtime, close_token)) = get_udp_socket(handle) else {
|
||||
return -1;
|
||||
};
|
||||
let total = len as usize;
|
||||
// Safety: caller-owned buffer outlives this blocking call.
|
||||
let buf = unsafe { std::slice::from_raw_parts_mut(buf, total) };
|
||||
let ret = runtime.block_on(run_with_cancel(
|
||||
&close_token,
|
||||
timeout_ms,
|
||||
"udp data plane receive",
|
||||
socket.recv_from(buf),
|
||||
));
|
||||
|
||||
match ret {
|
||||
Some(Ok((n, addr))) => {
|
||||
// The returned ip pointer must be released by the caller via
|
||||
// `free_string` (which calls `CString::from_raw`, matching
|
||||
// `CString::into_raw` here).
|
||||
let Some(ip_cstr) = into_ffi_ip_cstring(addr.ip()) else {
|
||||
return -1;
|
||||
};
|
||||
unsafe {
|
||||
*out_ip = ip_cstr as *const std::ffi::c_char;
|
||||
*out_port = addr.port() as std::ffi::c_ushort;
|
||||
}
|
||||
n as std::ffi::c_int
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
set_error_msg(&format!("failed to receive udp data plane: {}", e));
|
||||
-1
|
||||
}
|
||||
None => -1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) fn data_plane_udp_close(handle: u64) -> std::ffi::c_int {
|
||||
let _data_plane_usage_guard = match enter_data_plane_operation() {
|
||||
Some(guard) => guard,
|
||||
None => return -1,
|
||||
};
|
||||
crate::data_plane_async::cancel_ops_for_handle(handle);
|
||||
let Some((_, h)) = DATA_PLANE_HANDLES.remove_if(&handle, |_, e| {
|
||||
matches!(e.resource, DataPlaneResource::Udp(_))
|
||||
}) else {
|
||||
set_error_msg(if DATA_PLANE_HANDLES.contains_key(&handle) {
|
||||
"handle is not a udp socket"
|
||||
} else {
|
||||
"udp socket handle not found"
|
||||
});
|
||||
return -1;
|
||||
};
|
||||
h.close_token.cancel();
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ffi-dataplane"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn config_server_start_waits_for_data_plane_operation() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _write_guard = lock_for_config_server_start().unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_cleanup_waits_for_data_plane_operation() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let instance_id = Uuid::new_v4();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let cleaner = std::thread::spawn(move || {
|
||||
remove_data_plane_handles_by_instance_ids(&[instance_id]);
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
cleaner.join().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,685 +0,0 @@
|
||||
use std::{
|
||||
ffi::{c_char, c_int, c_uchar},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
ptr,
|
||||
};
|
||||
|
||||
use easytier_core::gateway::DataPlaneErrorKind;
|
||||
|
||||
use super::session::{self, NativeDataPlaneError, NativeDataPlaneResult};
|
||||
use crate::{
|
||||
error::set_error_msg,
|
||||
strings::c_str_to_string,
|
||||
types::{DataPlaneCompletion, DataPlaneSocketAddr},
|
||||
};
|
||||
|
||||
pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0;
|
||||
pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1;
|
||||
|
||||
fn failure(error: NativeDataPlaneError) -> c_int {
|
||||
set_error_msg(&error.message);
|
||||
-(error.kind as c_int)
|
||||
}
|
||||
|
||||
fn status(result: NativeDataPlaneResult<()>) -> c_int {
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(message: impl Into<String>) -> NativeDataPlaneError {
|
||||
NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::Io,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_addr(address: DataPlaneSocketAddr) -> NativeDataPlaneResult<SocketAddr> {
|
||||
let ip = match address.family {
|
||||
4 => IpAddr::V4(Ipv4Addr::new(
|
||||
address.address[0],
|
||||
address.address[1],
|
||||
address.address[2],
|
||||
address.address[3],
|
||||
)),
|
||||
6 => {
|
||||
return Err(NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
|
||||
message: "IPv6 is not supported by data-plane ABI v3".to_string(),
|
||||
});
|
||||
}
|
||||
family => {
|
||||
return Err(NativeDataPlaneError {
|
||||
kind: DataPlaneErrorKind::AddressFamilyUnsupported,
|
||||
message: format!("unsupported address family {family}"),
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(SocketAddr::new(ip, address.port))
|
||||
}
|
||||
|
||||
fn ffi_socket_addr(address: SocketAddr) -> DataPlaneSocketAddr {
|
||||
match address.ip() {
|
||||
IpAddr::V4(ip) => {
|
||||
let mut bytes = [0; 16];
|
||||
bytes[..4].copy_from_slice(&ip.octets());
|
||||
DataPlaneSocketAddr {
|
||||
family: 4,
|
||||
port: address.port(),
|
||||
address: bytes,
|
||||
}
|
||||
}
|
||||
IpAddr::V6(ip) => DataPlaneSocketAddr {
|
||||
family: 6,
|
||||
port: address.port(),
|
||||
address: ip.octets(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn copy_input(ptr: *const c_uchar, len: u32) -> NativeDataPlaneResult<Vec<u8>> {
|
||||
if len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err(invalid("input buffer is null"));
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec())
|
||||
}
|
||||
|
||||
unsafe fn output_slice<'a>(ptr: *mut c_uchar, len: u32) -> NativeDataPlaneResult<&'a mut [u8]> {
|
||||
if len == 0 {
|
||||
return Ok(&mut []);
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err(invalid("output buffer is null"));
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts_mut(ptr, len as usize) })
|
||||
}
|
||||
|
||||
fn write_operation(
|
||||
out_operation: *mut u64,
|
||||
submit: impl FnOnce() -> NativeDataPlaneResult<u64>,
|
||||
) -> c_int {
|
||||
if out_operation.is_null() {
|
||||
return failure(invalid("out_operation is null"));
|
||||
}
|
||||
match submit() {
|
||||
Ok(operation) => {
|
||||
unsafe {
|
||||
*out_operation = operation;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// If non-null, `inst_name` must point to a valid NUL-terminated string.
|
||||
/// `out_session` must be null or point to writable, properly aligned storage
|
||||
/// for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_session_open(
|
||||
inst_name: *const c_char,
|
||||
out_session: *mut u64,
|
||||
) -> c_int {
|
||||
if out_session.is_null() {
|
||||
return failure(invalid("out_session is null"));
|
||||
}
|
||||
unsafe {
|
||||
*out_session = 0;
|
||||
}
|
||||
let inst_name = match unsafe { c_str_to_string(inst_name, "inst_name") } {
|
||||
Ok(inst_name) => inst_name,
|
||||
Err(error) => return failure(invalid(error)),
|
||||
};
|
||||
match session::open(&inst_name) {
|
||||
Ok(handle) => {
|
||||
unsafe {
|
||||
*out_session = handle;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_session_close(session: u64) -> c_int {
|
||||
status(super::session::close(session))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_submit(
|
||||
session: u64,
|
||||
peer_addr: DataPlaneSocketAddr,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let peer_addr = match socket_addr(peer_addr) {
|
||||
Ok(address) => address,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_connect(session, peer_addr, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_submit(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_bind(session, local_port, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_submit(
|
||||
session: u64,
|
||||
listener: u64,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_accept(session, listener, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_submit(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
max_len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_read(session, stream, max_len)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `len` is nonzero, `data` must point to `len` readable bytes.
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write_submit(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
data: *const c_uchar,
|
||||
len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let data = match unsafe { copy_input(data, len) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_tcp_write(session, stream, data)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_submit(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_bind(session, local_port, timeout_ms)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_receive_submit(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
max_len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_receive(session, socket, max_len)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `len` is nonzero, `data` must point to `len` readable bytes.
|
||||
/// `out_operation` must be null or point to writable, properly aligned
|
||||
/// storage for one `u64`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_submit(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
peer_addr: DataPlaneSocketAddr,
|
||||
data: *const c_uchar,
|
||||
len: u32,
|
||||
out_operation: *mut u64,
|
||||
) -> c_int {
|
||||
let peer_addr = match socket_addr(peer_addr) {
|
||||
Ok(address) => address,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
let data = match unsafe { copy_input(data, len) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
write_operation(out_operation, || {
|
||||
super::session::submit_udp_send(session, socket, peer_addr, data)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_resource_deadline_set(
|
||||
session: u64,
|
||||
resource: u64,
|
||||
direction: u32,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
let read = direction & DATA_PLANE_DEADLINE_READ != 0;
|
||||
let write = direction & DATA_PLANE_DEADLINE_WRITE != 0;
|
||||
if direction == 0 || direction & !(DATA_PLANE_DEADLINE_READ | DATA_PLANE_DEADLINE_WRITE) != 0 {
|
||||
return failure(invalid(format!("invalid deadline direction {direction}")));
|
||||
}
|
||||
status(super::session::set_resource_deadline(
|
||||
session, resource, read, write, timeout_ms,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_operation_cancel(session: u64, operation: u64) -> c_int {
|
||||
status(super::session::cancel_operation(session, operation))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_operation_free(session: u64, operation: u64) -> c_int {
|
||||
status(super::session::free_operation(session, operation))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_resource_close(session: u64, resource: u64) -> c_int {
|
||||
status(super::session::close_resource(session, resource))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_completion_wait(session: u64, timeout_ms: u64) -> c_int {
|
||||
match super::session::completion_wait(session, timeout_ms) {
|
||||
Ok(true) => 1,
|
||||
Ok(false) => 0,
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `completions` must point to writable, properly
|
||||
/// aligned storage for `capacity` consecutive [`DataPlaneCompletion`] values.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_completion_drain(
|
||||
session: u64,
|
||||
completions: *mut DataPlaneCompletion,
|
||||
capacity: u32,
|
||||
) -> c_int {
|
||||
if capacity != 0 && completions.is_null() {
|
||||
return failure(invalid("completions is null"));
|
||||
}
|
||||
let drained = match super::session::drain_completions(session, capacity as usize) {
|
||||
Ok(drained) => drained,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
for (index, completion) in drained.iter().enumerate() {
|
||||
unsafe {
|
||||
ptr::write(
|
||||
completions.add(index),
|
||||
DataPlaneCompletion {
|
||||
operation_id: completion.operation_id.get(),
|
||||
operation_kind: completion.kind as u16,
|
||||
status: completion.status.code(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
drained.len() as c_int
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_size` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_result_size(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_size: *mut u32,
|
||||
) -> c_int {
|
||||
if out_size.is_null() {
|
||||
return failure(invalid("out_size is null"));
|
||||
}
|
||||
match super::session::result_size(session, operation) {
|
||||
Ok(size) => match u32::try_from(size) {
|
||||
Ok(size) => {
|
||||
unsafe {
|
||||
*out_size = size;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("data-plane result size exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_stream: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
|
||||
return failure(invalid("TCP connect result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_connect(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_stream = result.stream;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_listener: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_listener.is_null() || out_local_addr.is_null() {
|
||||
return failure(invalid("TCP bind result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_bind(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_listener = result.listener;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_stream: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_stream.is_null() || out_local_addr.is_null() || out_peer_addr.is_null() {
|
||||
return failure(invalid("TCP accept result output pointer is null"));
|
||||
}
|
||||
match super::session::take_tcp_accept(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_stream = result.stream;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
|
||||
/// Each scalar output pointer must be null or point to writable, properly
|
||||
/// aligned storage for its pointee type. Non-null output ranges must not
|
||||
/// overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
data: *mut c_uchar,
|
||||
capacity: u32,
|
||||
out_len: *mut u32,
|
||||
out_eof: *mut bool,
|
||||
) -> c_int {
|
||||
if out_len.is_null() || out_eof.is_null() {
|
||||
return failure(invalid("TCP read result output pointer is null"));
|
||||
}
|
||||
let data = match unsafe { output_slice(data, capacity) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
match super::session::take_tcp_read(session, operation, data) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_len = result.len as u32;
|
||||
*out_eof = result.eof;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_len` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_len: *mut u32,
|
||||
) -> c_int {
|
||||
if out_len.is_null() {
|
||||
return failure(invalid("out_len is null"));
|
||||
}
|
||||
match super::session::take_tcp_write(session, operation) {
|
||||
Ok(len) => match u32::try_from(len) {
|
||||
Ok(len) => {
|
||||
unsafe {
|
||||
*out_len = len;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("TCP write result exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Each output pointer must be null or point to writable, properly aligned
|
||||
/// storage for its pointee type. Non-null output locations must not overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_socket: *mut u64,
|
||||
out_local_addr: *mut DataPlaneSocketAddr,
|
||||
) -> c_int {
|
||||
if out_socket.is_null() || out_local_addr.is_null() {
|
||||
return failure(invalid("UDP bind result output pointer is null"));
|
||||
}
|
||||
match super::session::take_udp_bind(session, operation) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_socket = result.socket;
|
||||
*out_local_addr = ffi_socket_addr(result.local_addr);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// When `capacity` is nonzero, `data` must point to `capacity` writable bytes.
|
||||
/// Each scalar output pointer must be null or point to writable, properly
|
||||
/// aligned storage for its pointee type. Non-null output ranges must not
|
||||
/// overlap.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_receive_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
data: *mut c_uchar,
|
||||
capacity: u32,
|
||||
out_len: *mut u32,
|
||||
out_peer_addr: *mut DataPlaneSocketAddr,
|
||||
out_truncated: *mut bool,
|
||||
) -> c_int {
|
||||
if out_len.is_null() || out_peer_addr.is_null() || out_truncated.is_null() {
|
||||
return failure(invalid("UDP receive result output pointer is null"));
|
||||
}
|
||||
let data = match unsafe { output_slice(data, capacity) } {
|
||||
Ok(data) => data,
|
||||
Err(error) => return failure(error),
|
||||
};
|
||||
match super::session::take_udp_receive(session, operation, data) {
|
||||
Ok(result) => {
|
||||
unsafe {
|
||||
*out_len = result.len as u32;
|
||||
*out_peer_addr = ffi_socket_addr(result.peer_addr);
|
||||
*out_truncated = result.truncated;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `out_len` must be null or point to writable, properly aligned storage for
|
||||
/// one `u32`.
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_result_take(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
out_len: *mut u32,
|
||||
) -> c_int {
|
||||
if out_len.is_null() {
|
||||
return failure(invalid("out_len is null"));
|
||||
}
|
||||
match super::session::take_udp_send(session, operation) {
|
||||
Ok(len) => match u32::try_from(len) {
|
||||
Ok(len) => {
|
||||
unsafe {
|
||||
*out_len = len;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => failure(invalid("UDP send result exceeds u32")),
|
||||
},
|
||||
Err(error) => failure(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn socket_address_round_trip() {
|
||||
let address = "127.0.0.1:1234".parse::<SocketAddr>().unwrap();
|
||||
assert_eq!(socket_addr(ffi_socket_addr(address)).unwrap(), address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_is_rejected_by_v3() {
|
||||
let error = socket_addr(ffi_socket_addr(
|
||||
"[2001:db8::1]:4321".parse::<SocketAddr>().unwrap(),
|
||||
))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_address_family_is_stable() {
|
||||
let error = socket_addr(DataPlaneSocketAddr {
|
||||
family: 9,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind, DataPlaneErrorKind::AddressFamilyUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_deadline_direction_is_rejected_before_session_lookup() {
|
||||
let invalid = -(DataPlaneErrorKind::Io as c_int);
|
||||
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 0, 0), invalid);
|
||||
assert_eq!(data_plane_resource_deadline_set(u64::MAX, 1, 4, 0), invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_operation_output_does_not_submit() {
|
||||
let submitted = std::cell::Cell::new(false);
|
||||
|
||||
assert_eq!(
|
||||
write_operation(std::ptr::null_mut(), || {
|
||||
submitted.set(true);
|
||||
Ok(1)
|
||||
}),
|
||||
-(DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
assert!(!submitted.get());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//! Native C ABI adapter for the instance-scoped data-plane operation broker.
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod abi;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod session;
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub use abi::*;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub(crate) use session::{
|
||||
lock_for_config_server_start, remove_data_plane_sessions_by_instance_ids,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ffi-dataplane"))]
|
||||
pub(crate) fn remove_data_plane_sessions_by_instance_ids(_ids: &[uuid::Uuid]) {}
|
||||
@@ -1,646 +0,0 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
sync::{
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use easytier::instance::host::NativeInstanceHost;
|
||||
use easytier_core::gateway::{
|
||||
DataPlaneCompletionDescriptor, DataPlaneError, DataPlaneErrorKind, DataPlaneOperationId,
|
||||
DataPlaneOperationKind, DataPlaneOperationResult, DataPlaneResourceId, DataPlaneSession,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, is_config_server_active_or_stopping},
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
};
|
||||
|
||||
type CoreDataPlaneSession = DataPlaneSession<NativeInstanceHost>;
|
||||
|
||||
static NEXT_SESSION_HANDLE: AtomicU64 = AtomicU64::new(1);
|
||||
static SESSIONS: once_cell::sync::Lazy<Mutex<HashMap<u64, Arc<NativeDataPlaneSession>>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static DATA_PLANE_USAGE_LOCK: once_cell::sync::Lazy<RwLock<()>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(()));
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct NativeDataPlaneError {
|
||||
pub(super) kind: DataPlaneErrorKind,
|
||||
pub(super) message: String,
|
||||
}
|
||||
|
||||
impl NativeDataPlaneError {
|
||||
fn new(kind: DataPlaneErrorKind, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(message: impl Into<String>) -> Self {
|
||||
Self::new(DataPlaneErrorKind::Io, message)
|
||||
}
|
||||
|
||||
fn closed(message: impl Into<String>) -> Self {
|
||||
Self::new(DataPlaneErrorKind::HandleClosed, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DataPlaneError> for NativeDataPlaneError {
|
||||
fn from(error: DataPlaneError) -> Self {
|
||||
Self::new(error.kind(), error.message())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type NativeDataPlaneResult<T> = Result<T, NativeDataPlaneError>;
|
||||
|
||||
pub(super) struct TcpConnectResult {
|
||||
pub(super) stream: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpBindResult {
|
||||
pub(super) listener: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpAcceptResult {
|
||||
pub(super) stream: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct TcpReadResult {
|
||||
pub(super) len: usize,
|
||||
pub(super) eof: bool,
|
||||
}
|
||||
|
||||
pub(super) struct UdpBindResult {
|
||||
pub(super) socket: u64,
|
||||
pub(super) local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
pub(super) struct UdpReceiveResult {
|
||||
pub(super) len: usize,
|
||||
pub(super) peer_addr: SocketAddr,
|
||||
pub(super) truncated: bool,
|
||||
}
|
||||
|
||||
struct NativeDataPlaneSession {
|
||||
instance_id: Uuid,
|
||||
runtime: tokio::runtime::Handle,
|
||||
core: Arc<CoreDataPlaneSession>,
|
||||
submit_gate: Mutex<()>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
impl NativeDataPlaneSession {
|
||||
fn close(&self) {
|
||||
let _gate = self
|
||||
.submit_gate
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
if self.closed.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
self.core.discard_all();
|
||||
}
|
||||
|
||||
fn call<T>(
|
||||
&self,
|
||||
call: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<T, DataPlaneError>,
|
||||
) -> NativeDataPlaneResult<T> {
|
||||
let _gate = self
|
||||
.submit_gate
|
||||
.lock()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
if self.closed.load(Ordering::Acquire) {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session is closed",
|
||||
));
|
||||
}
|
||||
let _runtime = self.runtime.enter();
|
||||
call(&self.core).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
submit: impl FnOnce(&Arc<CoreDataPlaneSession>) -> Result<DataPlaneOperationId, DataPlaneError>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
self.call(submit).map(DataPlaneOperationId::get)
|
||||
}
|
||||
}
|
||||
|
||||
fn sessions()
|
||||
-> NativeDataPlaneResult<std::sync::MutexGuard<'static, HashMap<u64, Arc<NativeDataPlaneSession>>>>
|
||||
{
|
||||
SESSIONS
|
||||
.lock()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))
|
||||
}
|
||||
|
||||
fn get_session(handle: u64) -> NativeDataPlaneResult<Arc<NativeDataPlaneSession>> {
|
||||
if handle == 0 {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session handle is invalid",
|
||||
));
|
||||
}
|
||||
let session = sessions()?
|
||||
.get(&handle)
|
||||
.cloned()
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
|
||||
if session.closed.load(Ordering::Acquire) {
|
||||
return Err(NativeDataPlaneError::closed(
|
||||
"native data-plane session is closed",
|
||||
));
|
||||
}
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn next_session_handle(
|
||||
sessions: &HashMap<u64, Arc<NativeDataPlaneSession>>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
for _ in 0..sessions.len().saturating_add(2) {
|
||||
let handle = NEXT_SESSION_HANDLE.fetch_add(1, Ordering::Relaxed);
|
||||
if handle != 0 && !sessions.contains_key(&handle) {
|
||||
return Ok(handle);
|
||||
}
|
||||
}
|
||||
Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::ResourceLimit,
|
||||
"native data-plane session handle space is exhausted",
|
||||
))
|
||||
}
|
||||
|
||||
fn reject_data_plane_use() -> NativeDataPlaneResult<()> {
|
||||
if in_config_server_callback() {
|
||||
Err(NativeDataPlaneError::invalid(
|
||||
"cannot use data plane from config server callback",
|
||||
))
|
||||
} else if is_config_server_active_or_stopping() {
|
||||
Err(NativeDataPlaneError::invalid(
|
||||
"cannot use data plane while config server client is active",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn open(inst_name: &str) -> NativeDataPlaneResult<u64> {
|
||||
reject_data_plane_use()?;
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.read()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
reject_data_plane_use()?;
|
||||
|
||||
let instance_id = resolve_instance_id_by_name(inst_name)
|
||||
.map_err(NativeDataPlaneError::invalid)?
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("instance not found"))?;
|
||||
let manager = &ffi_context().manager;
|
||||
let core = manager.data_plane_session(&instance_id).ok_or_else(|| {
|
||||
NativeDataPlaneError::closed("instance data-plane session is unavailable")
|
||||
})?;
|
||||
let runtime = manager
|
||||
.data_plane_runtime_handle(&instance_id)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("instance runtime is unavailable"))?;
|
||||
|
||||
let mut sessions = sessions()?;
|
||||
if sessions
|
||||
.values()
|
||||
.any(|session| session.instance_id == instance_id)
|
||||
{
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::ResourceLimit,
|
||||
"instance already has an open native data-plane session",
|
||||
));
|
||||
}
|
||||
let handle = next_session_handle(&sessions)?;
|
||||
sessions.insert(
|
||||
handle,
|
||||
Arc::new(NativeDataPlaneSession {
|
||||
instance_id,
|
||||
runtime,
|
||||
core,
|
||||
submit_gate: Mutex::new(()),
|
||||
closed: AtomicBool::new(false),
|
||||
}),
|
||||
);
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub(super) fn close(handle: u64) -> NativeDataPlaneResult<()> {
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.read()
|
||||
.map_err(|error| NativeDataPlaneError::invalid(error.to_string()))?;
|
||||
let mut sessions = sessions()?;
|
||||
let session = sessions
|
||||
.remove(&handle)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("native data-plane session is closed"))?;
|
||||
// Keep the registry locked until the shared core namespace is empty. An
|
||||
// open for the same instance must not publish a replacement session before
|
||||
// this old wrapper finishes discarding its operations and resources.
|
||||
session.close();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn timeout(timeout_ms: u64) -> Option<Duration> {
|
||||
(timeout_ms != u64::MAX).then(|| Duration::from_millis(timeout_ms))
|
||||
}
|
||||
|
||||
fn operation_id(raw: u64) -> NativeDataPlaneResult<DataPlaneOperationId> {
|
||||
DataPlaneOperationId::from_raw(raw)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("data-plane operation handle is invalid"))
|
||||
}
|
||||
|
||||
fn resource_id(raw: u64) -> NativeDataPlaneResult<DataPlaneResourceId> {
|
||||
DataPlaneResourceId::from_raw(raw)
|
||||
.ok_or_else(|| NativeDataPlaneError::closed("data-plane resource handle is invalid"))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_connect(
|
||||
session: u64,
|
||||
peer_addr: SocketAddr,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_tcp_connect(peer_addr, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_bind(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_tcp_bind(local_port, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_accept(
|
||||
session: u64,
|
||||
listener: u64,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let listener = resource_id(listener)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_accept(listener, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_read(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
max_len: u32,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let stream = resource_id(stream)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_read(stream, max_len as usize))
|
||||
}
|
||||
|
||||
pub(super) fn submit_tcp_write(
|
||||
session: u64,
|
||||
stream: u64,
|
||||
data: Vec<u8>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let stream = resource_id(stream)?;
|
||||
get_session(session)?.submit(|core| core.submit_tcp_write(stream, data))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_bind(
|
||||
session: u64,
|
||||
local_port: u16,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
get_session(session)?.submit(|core| core.submit_udp_bind(local_port, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_receive(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
max_len: u32,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let socket = resource_id(socket)?;
|
||||
get_session(session)?.submit(|core| core.submit_udp_receive(socket, max_len as usize))
|
||||
}
|
||||
|
||||
pub(super) fn submit_udp_send(
|
||||
session: u64,
|
||||
socket: u64,
|
||||
peer_addr: SocketAddr,
|
||||
data: Vec<u8>,
|
||||
) -> NativeDataPlaneResult<u64> {
|
||||
let socket = resource_id(socket)?;
|
||||
get_session(session)?.submit(|core| core.submit_udp_send(socket, peer_addr, data))
|
||||
}
|
||||
|
||||
pub(super) fn set_resource_deadline(
|
||||
session: u64,
|
||||
resource: u64,
|
||||
read: bool,
|
||||
write: bool,
|
||||
timeout_ms: u64,
|
||||
) -> NativeDataPlaneResult<()> {
|
||||
let resource = resource_id(resource)?;
|
||||
get_session(session)?
|
||||
.call(|core| core.set_resource_deadline(resource, read, write, timeout(timeout_ms)))
|
||||
}
|
||||
|
||||
pub(super) fn cancel_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?.core.cancel_operation(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn free_operation(session: u64, operation: u64) -> NativeDataPlaneResult<()> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?.core.free_operation(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn close_resource(session: u64, resource: u64) -> NativeDataPlaneResult<()> {
|
||||
let resource = resource_id(resource)?;
|
||||
get_session(session)?.core.close_resource(resource);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn completion_wait(session: u64, timeout_ms: u64) -> NativeDataPlaneResult<bool> {
|
||||
let session = get_session(session)?;
|
||||
let ready = session.core.completion_wait(timeout(timeout_ms));
|
||||
Ok(ready && !session.closed.load(Ordering::Acquire))
|
||||
}
|
||||
|
||||
pub(super) fn drain_completions(
|
||||
session: u64,
|
||||
max_count: usize,
|
||||
) -> NativeDataPlaneResult<Vec<DataPlaneCompletionDescriptor>> {
|
||||
Ok(get_session(session)?.core.drain_completions(max_count))
|
||||
}
|
||||
|
||||
pub(super) fn result_size(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
let operation = operation_id(operation)?;
|
||||
get_session(session)?
|
||||
.core
|
||||
.result_payload_bytes(operation)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
fn take_result<T>(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
expected: DataPlaneOperationKind,
|
||||
take: impl FnOnce(&DataPlaneOperationResult) -> Option<T>,
|
||||
) -> NativeDataPlaneResult<T> {
|
||||
let operation = operation_id(operation)?;
|
||||
let session = get_session(session)?;
|
||||
let actual = session.core.operation_kind(operation)?;
|
||||
if actual != expected {
|
||||
return Err(NativeDataPlaneError::invalid(format!(
|
||||
"operation kind mismatch: expected {expected:?}, got {actual:?}"
|
||||
)));
|
||||
}
|
||||
let result = session.core.take_result_with(operation, |outcome| {
|
||||
Some(match outcome {
|
||||
Ok(result) => take(result).ok_or_else(|| {
|
||||
NativeDataPlaneError::invalid("data-plane result variant does not match operation")
|
||||
}),
|
||||
Err(kind) => Err(NativeDataPlaneError::new(
|
||||
*kind,
|
||||
format!("data-plane operation failed with {kind:?}"),
|
||||
)),
|
||||
})
|
||||
})?;
|
||||
result
|
||||
.ok_or_else(|| NativeDataPlaneError::invalid("data-plane result could not be consumed"))?
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_connect(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
) -> NativeDataPlaneResult<TcpConnectResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpConnect,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpConnected {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
} => Some(TcpConnectResult {
|
||||
stream: stream.get(),
|
||||
local_addr: *local_addr,
|
||||
peer_addr: *peer_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<TcpBindResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpBind,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpBound {
|
||||
listener,
|
||||
local_addr,
|
||||
} => Some(TcpBindResult {
|
||||
listener: listener.get(),
|
||||
local_addr: *local_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_accept(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
) -> NativeDataPlaneResult<TcpAcceptResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpAccept,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpAccepted {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
} => Some(TcpAcceptResult {
|
||||
stream: stream.get(),
|
||||
local_addr: *local_addr,
|
||||
peer_addr: *peer_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_read(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
output: &mut [u8],
|
||||
) -> NativeDataPlaneResult<TcpReadResult> {
|
||||
let required = result_size(session, operation)?;
|
||||
if output.len() < required {
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::BufferTooSmall,
|
||||
format!(
|
||||
"TCP read result requires {required} bytes, buffer has {}",
|
||||
output.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpRead,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpRead { data, eof } => {
|
||||
output[..data.len()].copy_from_slice(data);
|
||||
Some(TcpReadResult {
|
||||
len: data.len(),
|
||||
eof: *eof,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_tcp_write(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::TcpWrite,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::TcpWritten { len } => Some(*len),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_bind(session: u64, operation: u64) -> NativeDataPlaneResult<UdpBindResult> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpBind,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpBound { socket, local_addr } => Some(UdpBindResult {
|
||||
socket: socket.get(),
|
||||
local_addr: *local_addr,
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_receive(
|
||||
session: u64,
|
||||
operation: u64,
|
||||
output: &mut [u8],
|
||||
) -> NativeDataPlaneResult<UdpReceiveResult> {
|
||||
let required = result_size(session, operation)?;
|
||||
if output.len() < required {
|
||||
return Err(NativeDataPlaneError::new(
|
||||
DataPlaneErrorKind::BufferTooSmall,
|
||||
format!(
|
||||
"UDP receive result requires {required} bytes, buffer has {}",
|
||||
output.len()
|
||||
),
|
||||
));
|
||||
}
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpReceive,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpReceived {
|
||||
data,
|
||||
peer_addr,
|
||||
truncated,
|
||||
} => {
|
||||
output[..data.len()].copy_from_slice(data);
|
||||
Some(UdpReceiveResult {
|
||||
len: data.len(),
|
||||
peer_addr: *peer_addr,
|
||||
truncated: *truncated,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn take_udp_send(session: u64, operation: u64) -> NativeDataPlaneResult<usize> {
|
||||
take_result(
|
||||
session,
|
||||
operation,
|
||||
DataPlaneOperationKind::UdpSend,
|
||||
|result| match result {
|
||||
DataPlaneOperationResult::UdpSent { len } => Some(*len),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_data_plane_sessions_by_instance_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
let _usage = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.unwrap_or_else(|error| error.into_inner());
|
||||
let removed = {
|
||||
let mut sessions = SESSIONS.lock().unwrap_or_else(|error| error.into_inner());
|
||||
let handles = sessions
|
||||
.iter()
|
||||
.filter_map(|(handle, session)| ids.contains(&session.instance_id).then_some(*handle))
|
||||
.collect::<Vec<_>>();
|
||||
handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| sessions.remove(&handle))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
for session in removed {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lock_for_config_server_start()
|
||||
-> Result<std::sync::RwLockWriteGuard<'static, ()>, String> {
|
||||
let guard = DATA_PLANE_USAGE_LOCK
|
||||
.write()
|
||||
.map_err(|error| format!("failed to lock data plane usage: {error}"))?;
|
||||
if !SESSIONS
|
||||
.lock()
|
||||
.map_err(|error| format!("failed to lock data-plane sessions: {error}"))?
|
||||
.is_empty()
|
||||
{
|
||||
return Err("cannot start config server client while data plane is in use".to_string());
|
||||
}
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_server_start_waits_for_session_open_or_close() {
|
||||
let read_guard = DATA_PLANE_USAGE_LOCK.read().unwrap();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _write_guard = lock_for_config_server_start().unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
assert!(done_rx.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
drop(read_guard);
|
||||
done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
waiter.join().unwrap();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,21 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
use easytier::common::config::ConfigLoader as _;
|
||||
use easytier::common::config::{ConfigFileControl, TomlConfigLoader};
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||
|
||||
use crate::{
|
||||
config_server::{in_config_server_callback, wait_for_config_server_delivery},
|
||||
config_server::{
|
||||
in_config_server_callback, remove_config_server_tracked_instance_ids,
|
||||
wait_for_config_server_delivery,
|
||||
},
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
|
||||
lock_remote_instance_mutation,
|
||||
},
|
||||
types::KeyValuePair,
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
fn mobile_tun_sources_for_legacy_set_tun_fd(inst_id: uuid::Uuid) -> Result<(), String> {
|
||||
let config = ffi_context()
|
||||
.manager
|
||||
.config(inst_id)
|
||||
.ok_or_else(|| format!("instance config unavailable: {inst_id}"))?;
|
||||
let flags = config.get_flags();
|
||||
if flags.dev_name.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"set_tun_fd legacy API cannot attach shared mobile TUN dev_name={} without tun sources",
|
||||
flags.dev_name
|
||||
))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Set the tun fd
|
||||
pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
@@ -47,35 +25,19 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let inst_id = match resolve_instance_id_by_name(&inst_name) {
|
||||
Ok(Some(instance_id)) => instance_id,
|
||||
Ok(None) => {
|
||||
set_error_msg(&format!("instance not found: {inst_name}"));
|
||||
return -1;
|
||||
}
|
||||
Err(error) => {
|
||||
set_error_msg(&error.to_string());
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "android",
|
||||
target_os = "ios",
|
||||
all(target_os = "macos", feature = "macos-ne"),
|
||||
target_env = "ohos"
|
||||
))]
|
||||
if let Err(error) = mobile_tun_sources_for_legacy_set_tun_fd(inst_id) {
|
||||
set_error_msg(&error);
|
||||
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
|
||||
let inst_id = *INSTANCE_NAME_ID_MAP
|
||||
.get(&inst_name)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.value();
|
||||
|
||||
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to set tun fd: {}", e));
|
||||
-1
|
||||
}
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,16 +81,34 @@ pub(crate) unsafe fn run_network_instance(cfg_str: *const std::ffi::c_char) -> s
|
||||
}
|
||||
};
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
if let Err(e) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.run_owned_network_instance(cfg, ConfigFileControl::STATIC_CONFIG),
|
||||
) {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if instance_name_exists(&inst_name) {
|
||||
set_error_msg("instance already exists");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let instance_id =
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
@@ -172,24 +152,50 @@ pub(crate) unsafe fn retain_network_instance(
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let retained_names = if length == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
};
|
||||
inst_names
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.retain_owned_network_instances_by_name(retained_names),
|
||||
) {
|
||||
set_error_msg(&format!("failed to retain instances: {error}"));
|
||||
if length == 0 {
|
||||
let removed_ids = INSTANCE_MANAGER.list_network_instance_ids();
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
};
|
||||
|
||||
let removed_ids = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.filter(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_none_or(|name| !inst_names.contains(&name))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
@@ -205,6 +211,15 @@ pub(crate) unsafe fn delete_network_instance(
|
||||
}
|
||||
|
||||
wait_for_config_server_delivery();
|
||||
let _remote_mutation_guard = lock_remote_instance_mutation();
|
||||
let _mutation_guard = match INSTANCE_MUTATION_LOCK.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
if length == 0 {
|
||||
return 0;
|
||||
}
|
||||
@@ -213,15 +228,22 @@ pub(crate) unsafe fn delete_network_instance(
|
||||
return -1;
|
||||
};
|
||||
|
||||
if let Err(error) = ffi_context().runtime.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances_by_name(inst_names),
|
||||
) {
|
||||
set_error_msg(&format!("failed to delete instances: {error}"));
|
||||
let removed_ids = inst_names
|
||||
.iter()
|
||||
.filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id.value()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.delete_network_instance(removed_ids.clone()) {
|
||||
set_error_msg(&format!("failed to delete instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remove_config_server_tracked_instance_ids(&removed_ids);
|
||||
remove_data_plane_handles_by_instance_ids(&removed_ids);
|
||||
for name in inst_names {
|
||||
INSTANCE_NAME_ID_MAP.remove(&name);
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
@@ -245,7 +267,7 @@ pub(crate) unsafe fn collect_network_infos(
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match ffi_context().manager.collect_network_infos_sync() {
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
@@ -258,11 +280,7 @@ pub(crate) unsafe fn collect_network_infos(
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = ffi_context()
|
||||
.manager
|
||||
.instance(*instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
else {
|
||||
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
@@ -302,15 +320,13 @@ pub(crate) unsafe fn list_instance(infos: *mut KeyValuePair, max_length: usize)
|
||||
}
|
||||
|
||||
let infos = unsafe { std::slice::from_raw_parts_mut(infos, max_length) };
|
||||
let mut instances = ffi_context()
|
||||
.manager
|
||||
.instance_ids()
|
||||
let mut instances = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
ffi_context()
|
||||
.manager
|
||||
.instance(id)
|
||||
.map(|instance| (instance.instance_name().to_owned(), id))
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(&id)
|
||||
.map(|name| (name, id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use std::{
|
||||
ffi::{CString, c_char, c_int},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
use crate::{
|
||||
config_server::in_config_server_callback,
|
||||
error::set_error_msg,
|
||||
state::ffi_context,
|
||||
state::{ASYNC_RUNTIME, INSTANCE_MANAGER},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
};
|
||||
|
||||
@@ -68,23 +65,19 @@ pub(crate) unsafe fn call_json_rpc(
|
||||
}
|
||||
};
|
||||
|
||||
let response =
|
||||
match ffi_context()
|
||||
.runtime
|
||||
.block_on(easytier_core::management::call_management_json_rpc(
|
||||
&ffi_context().manager,
|
||||
Arc::new(easytier::rpc_service::logger::NativeLoggerControl),
|
||||
&service_name,
|
||||
&method_name,
|
||||
domain_name.as_deref(),
|
||||
payload,
|
||||
)) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("RPC Error: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response = match ASYNC_RUNTIME.block_on(easytier::rpc_service::call_json_rpc(
|
||||
&INSTANCE_MANAGER,
|
||||
&service_name,
|
||||
&method_name,
|
||||
domain_name.as_deref(),
|
||||
payload,
|
||||
)) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("RPC Error: {}", err));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
let response_json = match serde_json::to_string(&response) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
|
||||
@@ -20,12 +20,19 @@
|
||||
//! - `is_config_server_client_connected`: report whether the client is connected.
|
||||
//!
|
||||
//! Data plane APIs, enabled by the `ffi-dataplane` feature:
|
||||
//! - `data_plane_session_open` / `data_plane_session_close`: own one instance session.
|
||||
//! - `data_plane_*_submit`: submit non-blocking TCP and UDP operations.
|
||||
//! - `data_plane_completion_wait` / `data_plane_completion_drain`: await completions.
|
||||
//! - `data_plane_*_result_take`: consume typed operation results.
|
||||
//! - `data_plane_operation_cancel` / `data_plane_operation_free`: control operations.
|
||||
//! - `data_plane_resource_close`: close streams, listeners, and UDP sockets.
|
||||
//! - `data_plane_tcp_connect`: open an outbound TCP data-plane stream.
|
||||
//! - `data_plane_tcp_bind`: bind a TCP data-plane listener.
|
||||
//! - `data_plane_tcp_accept`: accept a TCP data-plane connection.
|
||||
//! - `data_plane_tcp_read`: read from a TCP data-plane stream.
|
||||
//! - `data_plane_tcp_write`: write to a TCP data-plane stream.
|
||||
//! - `data_plane_tcp_close`: close a TCP data-plane stream.
|
||||
//! - `data_plane_tcp_listener_close`: close a TCP data-plane listener.
|
||||
//! - `data_plane_udp_bind`: bind a UDP data-plane socket.
|
||||
//! - `data_plane_udp_send_to`: send one UDP data-plane datagram.
|
||||
//! - `data_plane_udp_recv_from`: receive one UDP data-plane datagram.
|
||||
//! - `data_plane_udp_close`: close a UDP data-plane socket.
|
||||
//! - `data_plane_*_start` / `data_plane_*_finish`: asynchronous data-plane operations.
|
||||
//! - `data_plane_async_op_*`: poll, wait, cancel, and free asynchronous operations.
|
||||
//!
|
||||
//! Shared FFI helper APIs:
|
||||
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
|
||||
@@ -33,6 +40,8 @@
|
||||
|
||||
mod config_server;
|
||||
mod data_plane;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod data_plane_async;
|
||||
mod error;
|
||||
mod instance_api;
|
||||
mod json_rpc;
|
||||
@@ -44,11 +53,11 @@ mod types;
|
||||
mod tests;
|
||||
|
||||
pub use config_server::{in_config_server_callback, validate_config_server_client_options};
|
||||
pub use types::{
|
||||
ConfigServerEventCallback, DataPlaneCompletion, DataPlaneSocketAddr, KeyValuePair,
|
||||
};
|
||||
pub use types::{ConfigServerEventCallback, KeyValuePair};
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use std::ffi::{c_uchar, c_ushort};
|
||||
|
||||
// ===== Network Management API =====
|
||||
|
||||
@@ -245,7 +254,7 @@ pub unsafe extern "C" fn call_json_rpc(
|
||||
/// Start the managed config-server client.
|
||||
///
|
||||
/// The client reuses EasyTier's web-client path and applies remote config
|
||||
/// changes through the shared `NativeInstanceManager`. Successful remote run
|
||||
/// changes through the shared `NetworkInstanceManager`. Successful remote run
|
||||
/// and delete operations are delivered to `callback` as JSON event strings, one
|
||||
/// callback per affected instance. The event string is valid only for the
|
||||
/// duration of the callback; callers must copy it if they need to keep it.
|
||||
@@ -310,27 +319,634 @@ pub extern "C" fn is_config_server_client_connected() -> c_int {
|
||||
|
||||
// ===== Data Plane API =====
|
||||
|
||||
/// Open an outbound TCP stream through an EasyTier instance data plane.
|
||||
///
|
||||
/// On success, writes the local address selected for the connection into
|
||||
/// `out_local_ip` and `out_local_port`. The returned IP string is allocated by
|
||||
/// this library and must be released with `free_string`.
|
||||
///
|
||||
/// The data plane is mutually exclusive with the config-server client. This
|
||||
/// function returns `0` if the config-server client is active or stopping.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name`, `dst_ip`, `out_local_ip`, and `out_local_port` must be non-null.
|
||||
/// String pointers must point to null-terminated UTF-8 strings.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP stream handle on success, or `0` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
pub use data_plane::{
|
||||
DATA_PLANE_DEADLINE_READ, DATA_PLANE_DEADLINE_WRITE, data_plane_completion_drain,
|
||||
data_plane_completion_wait, data_plane_operation_cancel, data_plane_operation_free,
|
||||
data_plane_resource_close, data_plane_resource_deadline_set, data_plane_result_size,
|
||||
data_plane_session_close, data_plane_session_open, data_plane_tcp_accept_result_take,
|
||||
data_plane_tcp_accept_submit, data_plane_tcp_bind_result_take, data_plane_tcp_bind_submit,
|
||||
data_plane_tcp_connect_result_take, data_plane_tcp_connect_submit,
|
||||
data_plane_tcp_read_result_take, data_plane_tcp_read_submit, data_plane_tcp_write_result_take,
|
||||
data_plane_tcp_write_submit, data_plane_udp_bind_result_take, data_plane_udp_bind_submit,
|
||||
data_plane_udp_receive_result_take, data_plane_udp_receive_submit,
|
||||
data_plane_udp_send_result_take, data_plane_udp_send_submit,
|
||||
};
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect(
|
||||
inst_name: *const c_char,
|
||||
dst_ip: *const c_char,
|
||||
dst_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane::data_plane_tcp_connect(
|
||||
inst_name,
|
||||
dst_ip,
|
||||
dst_port,
|
||||
timeout_ms,
|
||||
out_local_ip,
|
||||
out_local_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind a TCP listener through an EasyTier instance data plane.
|
||||
///
|
||||
/// On success, writes the bound local address into `out_local_ip` and
|
||||
/// `out_local_port`. The returned IP string is allocated by this library and
|
||||
/// must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name`, `out_local_ip`, and `out_local_port` must be non-null.
|
||||
/// `inst_name` must point to a null-terminated UTF-8 string.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP listener handle on success, or `0` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind(
|
||||
inst_name: *const c_char,
|
||||
local_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane::data_plane_tcp_bind(
|
||||
inst_name,
|
||||
local_port,
|
||||
timeout_ms,
|
||||
out_local_ip,
|
||||
out_local_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept one connection from a TCP data-plane listener.
|
||||
///
|
||||
/// On success, writes both local and peer socket addresses to the output
|
||||
/// pointers. Returned IP strings are allocated by this library and must be
|
||||
/// released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// All output pointers must be non-null and writable. `handle` must be a valid
|
||||
/// TCP listener handle returned by `data_plane_tcp_bind`.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP stream handle on success, or `0` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept(
|
||||
handle: u64,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
out_peer_ip: *mut *const c_char,
|
||||
out_peer_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane::data_plane_tcp_accept(
|
||||
handle,
|
||||
timeout_ms,
|
||||
out_local_ip,
|
||||
out_local_port,
|
||||
out_peer_ip,
|
||||
out_peer_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read bytes from a TCP data-plane stream.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid TCP stream handle returned by
|
||||
/// `data_plane_tcp_connect` or `data_plane_tcp_accept`. `buf` must be non-null
|
||||
/// and writable for `len` bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of bytes read, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read(
|
||||
handle: u64,
|
||||
buf: *mut c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
unsafe { data_plane::data_plane_tcp_read(handle, buf, len, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Write bytes to a TCP data-plane stream.
|
||||
///
|
||||
/// This function attempts to write exactly `len` bytes before returning
|
||||
/// success.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid TCP stream handle returned by
|
||||
/// `data_plane_tcp_connect` or `data_plane_tcp_accept`. `buf` must be non-null
|
||||
/// and readable for `len` bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `len` on success, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write(
|
||||
handle: u64,
|
||||
buf: *const c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
unsafe { data_plane::data_plane_tcp_write(handle, buf, len, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Close a TCP data-plane stream handle.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` if the handle is missing, is not a TCP stream
|
||||
/// handle, or data-plane calls are currently rejected.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_tcp_close(handle: u64) -> c_int {
|
||||
data_plane::data_plane_tcp_close(handle)
|
||||
}
|
||||
|
||||
/// Close a TCP data-plane listener handle.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` if the handle is missing, is not a TCP
|
||||
/// listener handle, or data-plane calls are currently rejected.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_tcp_listener_close(handle: u64) -> c_int {
|
||||
data_plane::data_plane_tcp_listener_close(handle)
|
||||
}
|
||||
|
||||
/// Bind a UDP socket through an EasyTier instance data plane.
|
||||
///
|
||||
/// On success, writes the bound local address into `out_local_ip` and
|
||||
/// `out_local_port`. The returned IP string is allocated by this library and
|
||||
/// must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name`, `out_local_ip`, and `out_local_port` must be non-null.
|
||||
/// `inst_name` must point to a null-terminated UTF-8 string.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero UDP socket handle on success, or `0` on failure. On
|
||||
/// failure, call `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind(
|
||||
inst_name: *const c_char,
|
||||
local_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane::data_plane_udp_bind(
|
||||
inst_name,
|
||||
local_port,
|
||||
timeout_ms,
|
||||
out_local_ip,
|
||||
out_local_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send one UDP datagram through a data-plane socket.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid UDP socket handle returned by
|
||||
/// `data_plane_udp_bind`. `dst_ip` must be non-null and point to a
|
||||
/// null-terminated UTF-8 string. `buf` must be non-null and readable for `len`
|
||||
/// bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of bytes sent, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_to(
|
||||
handle: u64,
|
||||
dst_ip: *const c_char,
|
||||
dst_port: c_ushort,
|
||||
buf: *const c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
unsafe { data_plane::data_plane_udp_send_to(handle, dst_ip, dst_port, buf, len, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Receive one UDP datagram from a data-plane socket.
|
||||
///
|
||||
/// On success, writes the peer address into `out_ip` and `out_port`. The
|
||||
/// returned IP string is allocated by this library and must be released with
|
||||
/// `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid UDP socket handle returned by
|
||||
/// `data_plane_udp_bind`. `buf`, `out_ip`, and `out_port` must be non-null.
|
||||
/// `buf` must be writable for `len` bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of bytes received, or `-1` on failure. On failure, call
|
||||
/// `get_error_msg` on the same thread to retrieve details.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_recv_from(
|
||||
handle: u64,
|
||||
buf: *mut c_uchar,
|
||||
len: u32,
|
||||
out_ip: *mut *const c_char,
|
||||
out_port: *mut c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> c_int {
|
||||
unsafe { data_plane::data_plane_udp_recv_from(handle, buf, len, out_ip, out_port, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Close a UDP data-plane socket handle.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns `0` on success, or `-1` if the handle is missing, is not a UDP
|
||||
/// socket handle, or data-plane calls are currently rejected.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_udp_close(handle: u64) -> c_int {
|
||||
data_plane::data_plane_udp_close(handle)
|
||||
}
|
||||
|
||||
// ===== Async Data Plane API =====
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_async_op_status(handle: u64) -> c_int {
|
||||
data_plane_async::data_plane_async_op_status(handle)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_async_op_wait(handle: u64, timeout_ms: u64) -> c_int {
|
||||
data_plane_async::data_plane_async_op_wait(handle, timeout_ms)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_async_op_cancel(handle: u64) -> c_int {
|
||||
data_plane_async::data_plane_async_op_cancel(handle)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_async_op_free(handle: u64) -> c_int {
|
||||
data_plane_async::data_plane_async_op_free(handle)
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_free_bytes(ptr: *const c_uchar, len: u32) {
|
||||
data_plane_async::data_plane_free_bytes(ptr, len)
|
||||
}
|
||||
|
||||
/// Start an asynchronous TCP data-plane connection.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name` and `dst_ip` must be non-null pointers to null-terminated UTF-8
|
||||
/// strings. The strings only need to remain valid for the duration of this
|
||||
/// call.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_start(
|
||||
inst_name: *const c_char,
|
||||
dst_ip: *const c_char,
|
||||
dst_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane_async::data_plane_tcp_connect_start(inst_name, dst_ip, dst_port, timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish an asynchronous TCP data-plane connection.
|
||||
///
|
||||
/// On success, writes the stream local address into `out_local_ip` and
|
||||
/// `out_local_port`. The returned IP string is allocated by this library and
|
||||
/// must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
|
||||
/// storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP stream handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_connect_finish(
|
||||
op_handle: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane_async::data_plane_tcp_connect_finish(op_handle, out_local_ip, out_local_port)
|
||||
}
|
||||
}
|
||||
|
||||
/// Start an asynchronous TCP data-plane bind.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
|
||||
/// The string only needs to remain valid for the duration of this call.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_start(
|
||||
inst_name: *const c_char,
|
||||
local_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_tcp_bind_start(inst_name, local_port, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Finish an asynchronous TCP data-plane bind.
|
||||
///
|
||||
/// On success, writes the listener local address into `out_local_ip` and
|
||||
/// `out_local_port`. The returned IP string is allocated by this library and
|
||||
/// must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
|
||||
/// storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP listener handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_bind_finish(
|
||||
op_handle: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_tcp_bind_finish(op_handle, out_local_ip, out_local_port) }
|
||||
}
|
||||
|
||||
/// Start an asynchronous TCP data-plane accept on a listener handle.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid TCP listener handle returned by
|
||||
/// `data_plane_tcp_bind` or `data_plane_tcp_bind_finish`.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_start(handle: u64, timeout_ms: u64) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_tcp_accept_start(handle, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Finish an asynchronous TCP data-plane accept.
|
||||
///
|
||||
/// On success, writes the accepted stream local address into `out_local_ip` and
|
||||
/// `out_local_port`, and the peer address into `out_peer_ip` and
|
||||
/// `out_peer_port`. Returned IP strings are allocated by this library and must
|
||||
/// be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_local_ip`, `out_local_port`, `out_peer_ip`, and `out_peer_port` must be
|
||||
/// non-null pointers to writable storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero TCP stream handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_accept_finish(
|
||||
op_handle: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
out_peer_ip: *mut *const c_char,
|
||||
out_peer_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane_async::data_plane_tcp_accept_finish(
|
||||
op_handle,
|
||||
out_local_ip,
|
||||
out_local_port,
|
||||
out_peer_ip,
|
||||
out_peer_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Start an asynchronous TCP data-plane read.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid TCP stream handle returned by
|
||||
/// `data_plane_tcp_connect_finish` or `data_plane_tcp_accept_finish`.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_start(
|
||||
handle: u64,
|
||||
max_len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_tcp_read_start(handle, max_len, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Finish an asynchronous TCP data-plane read.
|
||||
///
|
||||
/// On success, writes the received buffer pointer and length into `out_buf` and
|
||||
/// `out_len`. The returned buffer is allocated by this library and must be
|
||||
/// released with `data_plane_free_bytes`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_buf` and `out_len` must be non-null pointers to writable storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of bytes read, or `-1` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_read_finish(
|
||||
op_handle: u64,
|
||||
out_buf: *mut *const c_uchar,
|
||||
out_len: *mut u32,
|
||||
) -> c_int {
|
||||
unsafe { data_plane_async::data_plane_tcp_read_finish(op_handle, out_buf, out_len) }
|
||||
}
|
||||
|
||||
/// Start an asynchronous TCP data-plane write.
|
||||
///
|
||||
/// The input bytes are copied before this function returns.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid TCP stream handle returned by
|
||||
/// `data_plane_tcp_connect_finish` or `data_plane_tcp_accept_finish`. If `len`
|
||||
/// is non-zero, `buf` must be non-null and readable for `len` bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_tcp_write_start(
|
||||
handle: u64,
|
||||
buf: *const c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_tcp_write_start(handle, buf, len, timeout_ms) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_tcp_write_finish(op_handle: u64) -> c_int {
|
||||
data_plane_async::data_plane_tcp_write_finish(op_handle)
|
||||
}
|
||||
|
||||
/// Start an asynchronous UDP data-plane bind.
|
||||
///
|
||||
/// # Safety
|
||||
/// `inst_name` must be a non-null pointer to a null-terminated UTF-8 string.
|
||||
/// The string only needs to remain valid for the duration of this call.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_start(
|
||||
inst_name: *const c_char,
|
||||
local_port: c_ushort,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_udp_bind_start(inst_name, local_port, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Finish an asynchronous UDP data-plane bind.
|
||||
///
|
||||
/// On success, writes the socket local address into `out_local_ip` and
|
||||
/// `out_local_port`. The returned IP string is allocated by this library and
|
||||
/// must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_local_ip` and `out_local_port` must be non-null pointers to writable
|
||||
/// storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero UDP socket handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_bind_finish(
|
||||
op_handle: u64,
|
||||
out_local_ip: *mut *const c_char,
|
||||
out_local_port: *mut c_ushort,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_udp_bind_finish(op_handle, out_local_ip, out_local_port) }
|
||||
}
|
||||
|
||||
/// Start an asynchronous UDP data-plane send.
|
||||
///
|
||||
/// The input bytes are copied before this function returns.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid UDP socket handle returned by
|
||||
/// `data_plane_udp_bind_finish`. `dst_ip` must be a non-null pointer to a
|
||||
/// null-terminated UTF-8 string. If `len` is non-zero, `buf` must be non-null
|
||||
/// and readable for `len` bytes.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_send_to_start(
|
||||
handle: u64,
|
||||
dst_ip: *const c_char,
|
||||
dst_port: c_ushort,
|
||||
buf: *const c_uchar,
|
||||
len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe {
|
||||
data_plane_async::data_plane_udp_send_to_start(
|
||||
handle, dst_ip, dst_port, buf, len, timeout_ms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub extern "C" fn data_plane_udp_send_to_finish(op_handle: u64) -> c_int {
|
||||
data_plane_async::data_plane_udp_send_to_finish(op_handle)
|
||||
}
|
||||
|
||||
/// Start an asynchronous UDP data-plane receive.
|
||||
///
|
||||
/// # Safety
|
||||
/// `handle` must be a valid UDP socket handle returned by
|
||||
/// `data_plane_udp_bind_finish`.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns a non-zero async operation handle on success, or `0` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_recv_from_start(
|
||||
handle: u64,
|
||||
max_len: u32,
|
||||
timeout_ms: u64,
|
||||
) -> u64 {
|
||||
unsafe { data_plane_async::data_plane_udp_recv_from_start(handle, max_len, timeout_ms) }
|
||||
}
|
||||
|
||||
/// Finish an asynchronous UDP data-plane receive.
|
||||
///
|
||||
/// On success, writes the received buffer into `out_buf` and `out_len`, and
|
||||
/// the peer address into `out_ip` and `out_port`. The returned buffer is
|
||||
/// allocated by this library and must be released with `data_plane_free_bytes`;
|
||||
/// the returned IP string must be released with `free_string`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `out_buf`, `out_len`, `out_ip`, and `out_port` must be non-null pointers to
|
||||
/// writable storage.
|
||||
///
|
||||
/// # Return
|
||||
/// Returns the number of bytes received, or `-1` on failure.
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[cfg_attr(feature = "c-abi", unsafe(no_mangle))]
|
||||
pub unsafe extern "C" fn data_plane_udp_recv_from_finish(
|
||||
op_handle: u64,
|
||||
out_buf: *mut *const c_uchar,
|
||||
out_len: *mut u32,
|
||||
out_ip: *mut *const c_char,
|
||||
out_port: *mut c_ushort,
|
||||
) -> c_int {
|
||||
unsafe {
|
||||
data_plane_async::data_plane_udp_recv_from_finish(
|
||||
op_handle, out_buf, out_len, out_ip, out_port,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Shared FFI Helper API =====
|
||||
|
||||
/// Return the last FFI error message.
|
||||
///
|
||||
/// API failures are stored in a thread-local buffer, so call this on the same
|
||||
/// thread that received a negative status or another documented failure
|
||||
/// sentinel. Config-server
|
||||
/// Synchronous API failures are stored in a thread-local buffer, so call this
|
||||
/// on the same thread that received `-1` or `0` from another API. Config-server
|
||||
/// callback delivery failures may happen on a runtime thread; those are stored
|
||||
/// globally and are included here so direct FFI callers can still retrieve the
|
||||
/// last callback error. If there is no error message, this writes a null pointer
|
||||
|
||||
@@ -1,66 +1,54 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use easytier::instance::factory::{
|
||||
NativeInstanceManager, NativeProcessManagement, native_instance_manager_with_runtime,
|
||||
native_process_management,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
struct FfiOwnedInstanceHooks;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl easytier_core::management::InstanceMutationHooks for FfiOwnedInstanceHooks {
|
||||
async fn post_remove_network_instances(
|
||||
&self,
|
||||
instance_ids: &[uuid::Uuid],
|
||||
) -> Result<(), String> {
|
||||
crate::config_server::remove_config_server_tracked_instance_ids(instance_ids);
|
||||
crate::data_plane::remove_data_plane_sessions_by_instance_ids(instance_ids);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FfiContext {
|
||||
pub(crate) runtime: Runtime,
|
||||
pub(crate) manager: Arc<NativeInstanceManager>,
|
||||
pub(crate) process_management: NativeProcessManagement,
|
||||
}
|
||||
|
||||
impl FfiContext {
|
||||
fn new() -> Self {
|
||||
let runtime = Builder::new_multi_thread()
|
||||
pub(crate) static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, Uuid>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
pub(crate) static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ffi");
|
||||
let manager = Arc::new(native_instance_manager_with_runtime(
|
||||
runtime.handle().clone(),
|
||||
));
|
||||
let process_management =
|
||||
native_process_management(manager.clone(), Arc::new(FfiOwnedInstanceHooks));
|
||||
Self {
|
||||
runtime,
|
||||
manager,
|
||||
process_management,
|
||||
}
|
||||
.expect("tokio runtime for easytier-ffi")
|
||||
});
|
||||
pub(crate) static INSTANCE_MUTATION_LOCK: once_cell::sync::Lazy<Mutex<()>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(()));
|
||||
|
||||
pub(crate) fn remove_instance_name_ids(ids: &[Uuid]) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
|
||||
}
|
||||
|
||||
static FFI_CONTEXT: once_cell::sync::Lazy<FfiContext> = once_cell::sync::Lazy::new(FfiContext::new);
|
||||
|
||||
pub(crate) fn ffi_context() -> &'static FfiContext {
|
||||
&FFI_CONTEXT
|
||||
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
|
||||
INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_instance_id_by_name(inst_name: &str) -> Result<Option<uuid::Uuid>, String> {
|
||||
easytier_core::management::resolve_optional_instance_by_name(
|
||||
ffi_context().manager.as_ref(),
|
||||
inst_name,
|
||||
)
|
||||
.map(|instance| instance.map(|instance| instance.instance_id()))
|
||||
.map_err(|error| error.to_string())
|
||||
pub(crate) fn instance_name_exists(inst_name: &str) -> bool {
|
||||
find_instance_id_by_name(inst_name).is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<uuid::Uuid> {
|
||||
resolve_instance_id_by_name(inst_name).ok().flatten()
|
||||
pub(crate) fn find_instance_id_by_name(inst_name: &str) -> Option<Uuid> {
|
||||
INSTANCE_NAME_ID_MAP
|
||||
.get(inst_name)
|
||||
.map(|id| *id)
|
||||
.or_else(|| {
|
||||
INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.into_iter()
|
||||
.find(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(id)
|
||||
.is_some_and(|name| name == inst_name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ use crate::{
|
||||
config_server::{
|
||||
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||
},
|
||||
state::{ffi_context, find_instance_id_by_name},
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
*,
|
||||
};
|
||||
use easytier::{
|
||||
@@ -12,7 +15,7 @@ use easytier::{
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
ffi::{CStr, CString, c_char, c_void},
|
||||
sync::{Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -98,10 +101,10 @@ fn list_instance_returns_instance_names_and_ids() {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(instance_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(instance_name.clone(), instance_id);
|
||||
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
@@ -124,14 +127,10 @@ fn list_instance_returns_instance_names_and_ids() {
|
||||
}
|
||||
|
||||
free_key_value_pairs(&infos[..count as usize]);
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
assert!(found);
|
||||
}
|
||||
|
||||
@@ -262,9 +261,8 @@ async fn config_server_hooks_emit_run_event() {
|
||||
let inst_name = format!("test-{}", instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||
@@ -280,18 +278,17 @@ async fn config_server_hooks_emit_run_event() {
|
||||
);
|
||||
|
||||
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||
let events = events.lock().unwrap().clone();
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
let event: Value = serde_json::from_str(&events[0]).unwrap();
|
||||
assert_eq!(event["event"], "run_network_instance");
|
||||
assert_eq!(event["success"], true);
|
||||
assert_eq!(event["instance_id"], instance_id.to_string());
|
||||
assert!(event["error"].is_null());
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -309,9 +306,8 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(format!("test-{}", id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -331,7 +327,7 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
let events = events.lock().unwrap().clone();
|
||||
let events = events.lock().unwrap();
|
||||
assert_eq!(events.len(), 2);
|
||||
let event_ids = events
|
||||
.iter()
|
||||
@@ -347,27 +343,29 @@ async fn config_server_hooks_emit_delete_events_for_tracked_instances() {
|
||||
event_ids,
|
||||
HashSet::from([instance_id_1.to_string(), instance_id_2.to_string()])
|
||||
);
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id_1, instance_id_2])
|
||||
.await
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id_1, instance_id_2])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_ignore_untracked_instance_without_event() {
|
||||
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
&events as *const _ as *mut c_void,
|
||||
);
|
||||
let local_id = Uuid::new_v4();
|
||||
let inst_name = format!("local-{}", local_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), local_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[local_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
@@ -377,25 +375,15 @@ async fn config_server_hooks_reject_duplicate_instance_name() {
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let existing_id = Uuid::new_v4();
|
||||
let new_id = Uuid::new_v4();
|
||||
let existing_cfg = TomlConfigLoader::default();
|
||||
existing_cfg.set_inst_name(inst_name.clone());
|
||||
existing_cfg.set_id(existing_id);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(existing_cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(new_id);
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(existing_id));
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([existing_id])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
|
||||
INSTANCE_NAME_ID_MAP.remove(&inst_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -410,23 +398,8 @@ async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error()
|
||||
let overwritten_id = Uuid::new_v4();
|
||||
let duplicate_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(overwritten_id);
|
||||
for (id, name) in [
|
||||
(overwritten_id, old_name.clone()),
|
||||
(duplicate_id, duplicate_name.clone()),
|
||||
] {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([overwritten_id])
|
||||
.await
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_id);
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[overwritten_id])
|
||||
@@ -439,17 +412,13 @@ async fn config_server_hooks_remove_overwritten_id_before_duplicate_name_error()
|
||||
|
||||
assert!(hooks.pre_run_network_instance(&cfg).await.is_err());
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(find_instance_id_by_name(&old_name).is_none());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
|
||||
assert_eq!(
|
||||
find_instance_id_by_name(&duplicate_name),
|
||||
Some(duplicate_id)
|
||||
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
|
||||
duplicate_id
|
||||
);
|
||||
assert_eq!(events.lock().unwrap().len(), 1);
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([duplicate_id])
|
||||
.await
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -458,19 +427,11 @@ async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||
let inst_name = format!("test-{}", Uuid::new_v4());
|
||||
let instance_id = Uuid::new_v4();
|
||||
hooks.instance_ids.lock().unwrap().insert(instance_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), instance_id);
|
||||
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
cfg.set_id(instance_id);
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[instance_id])
|
||||
@@ -479,7 +440,7 @@ async fn config_server_hooks_remove_tracked_state_before_overwrite_retry() {
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(find_instance_id_by_name(&inst_name).is_none());
|
||||
assert!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -490,14 +451,11 @@ async fn config_server_hooks_reject_post_run_after_external_delete() {
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(format!("test-{}", instance_id));
|
||||
hooks.pre_run_network_instance(&cfg).await.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||
@@ -510,20 +468,15 @@ fn find_instance_id_by_name_resolves_uncommitted_manager_instance_name() {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(instance_id);
|
||||
cfg.set_inst_name(inst_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -540,10 +493,10 @@ fn delete_network_instance_removes_only_named_instances() {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(name, id);
|
||||
}
|
||||
|
||||
let delete_name = CString::new(delete_name.clone()).unwrap();
|
||||
@@ -556,10 +509,10 @@ fn delete_network_instance_removes_only_named_instances() {
|
||||
assert_eq!(find_instance_id_by_name(&keep_name), Some(keep_id));
|
||||
assert!(find_instance_id_by_name(delete_name.to_str().unwrap()).is_none());
|
||||
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(ffi_context().manager.delete_network_instances([keep_id]))
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![keep_id])
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[keep_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -579,18 +532,13 @@ fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_process_management_uses_manager_mutation_lock() {
|
||||
let manager_guard = ffi_context().manager.mutation_lock().blocking_lock_owned();
|
||||
fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||
let manager_guard = INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances(Vec::new()),
|
||||
)
|
||||
.unwrap();
|
||||
let _ffi_guard = lock_remote_instance_mutation();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
@@ -601,7 +549,7 @@ fn ffi_process_management_uses_manager_mutation_lock() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_reject_late_runs_for_core_rollback() {
|
||||
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
@@ -609,54 +557,15 @@ async fn config_server_hooks_reject_late_runs_for_core_rollback() {
|
||||
);
|
||||
hooks.start_stopping();
|
||||
|
||||
assert!(
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.tracked_instance_ids().is_empty());
|
||||
assert!(events.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_network_instance_rejects_an_ambiguous_name() {
|
||||
let duplicate_name = format!("duplicate-{}", Uuid::new_v4());
|
||||
let instance_ids = [Uuid::new_v4(), Uuid::new_v4()];
|
||||
for instance_id in instance_ids {
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_id(instance_id);
|
||||
config.set_inst_name(duplicate_name.clone());
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(config, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let duplicate_name = CString::new(duplicate_name).unwrap();
|
||||
let names = [duplicate_name.as_ptr()];
|
||||
assert_eq!(
|
||||
unsafe { delete_network_instance(names.as_ptr(), names.len()) },
|
||||
-1
|
||||
);
|
||||
assert!(take_last_error().unwrap().contains("2 instances match"));
|
||||
assert!(
|
||||
instance_ids
|
||||
.iter()
|
||||
.all(|id| ffi_context().manager.instance(*id).is_some())
|
||||
);
|
||||
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances(instance_ids.to_vec()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
let _callback_scope = ConfigServerCallbackScope::enter();
|
||||
@@ -706,12 +615,112 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
{
|
||||
let mut session = 0;
|
||||
assert_eq!(
|
||||
unsafe { data_plane_session_open(std::ptr::null(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(session, 0);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_accept(
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_write(0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_tcp_close(0), -1);
|
||||
assert_eq!(data_plane_tcp_listener_close(0), -1);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_bind(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_send_to(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_udp_recv_from(
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
},
|
||||
-1
|
||||
);
|
||||
assert_eq!(data_plane_udp_close(0), -1);
|
||||
assert_eq!(data_plane_async_op_status(0), -2);
|
||||
assert_eq!(data_plane_async_op_wait(0, 0), -2);
|
||||
assert_eq!(data_plane_async_op_cancel(0), -2);
|
||||
assert_eq!(data_plane_async_op_free(0), -2);
|
||||
data_plane_free_bytes(std::ptr::null(), 0);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_bind_start(std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_tcp_accept_start(0, 0) }, 0);
|
||||
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_write_start(0, std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_bind_start(std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_udp_send_to_start(0, std::ptr::null(), 0, std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_udp_recv_from_start(0, 0, 0) }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,27 +729,38 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
fn active_config_server_rejects_data_plane() {
|
||||
set_active_for_test(true);
|
||||
|
||||
let name = CString::new("missing").unwrap();
|
||||
let mut session = 0;
|
||||
assert_eq!(
|
||||
unsafe { data_plane_session_open(name.as_ptr(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
);
|
||||
assert_eq!(session, 0);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_read(0, std::ptr::null_mut(), 0, 0) },
|
||||
-1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { data_plane_tcp_connect_start(std::ptr::null(), std::ptr::null(), 0, 0) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { data_plane_tcp_read_start(0, 0, 0) }, 0);
|
||||
|
||||
set_active_for_test(false);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
fn data_plane_invalid_handle_errors_are_stable() {
|
||||
let closed = -(easytier_core::gateway::DataPlaneErrorKind::HandleClosed as c_int);
|
||||
assert_eq!(data_plane_completion_wait(u64::MAX, 0), closed);
|
||||
assert_eq!(data_plane_operation_cancel(u64::MAX, 1), closed);
|
||||
assert_eq!(data_plane_operation_free(u64::MAX, 1), closed);
|
||||
assert_eq!(data_plane_resource_close(u64::MAX, 1), closed);
|
||||
assert_eq!(
|
||||
data_plane_resource_deadline_set(u64::MAX, 1, DATA_PLANE_DEADLINE_READ, 0),
|
||||
closed
|
||||
);
|
||||
fn async_op_invalid_handle_helpers_are_stable() {
|
||||
assert_eq!(data_plane_async_op_status(u64::MAX), -2);
|
||||
assert_eq!(data_plane_async_op_wait(u64::MAX, 1), -2);
|
||||
assert_eq!(data_plane_async_op_cancel(u64::MAX), -2);
|
||||
assert_eq!(data_plane_async_op_free(u64::MAX), -2);
|
||||
data_plane_free_bytes(std::ptr::null(), 0);
|
||||
}
|
||||
|
||||
@@ -8,23 +8,3 @@ pub struct KeyValuePair {
|
||||
}
|
||||
|
||||
pub type ConfigServerEventCallback = Option<unsafe extern "C" fn(*const c_char, *mut c_void)>;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DataPlaneSocketAddr {
|
||||
/// `4` for IPv4. Other families are reserved for later ABI versions.
|
||||
pub family: u16,
|
||||
/// Native-endian port number.
|
||||
pub port: u16,
|
||||
/// Network-order address bytes. IPv4 uses the first four bytes.
|
||||
pub address: [u8; 16],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct DataPlaneCompletion {
|
||||
pub operation_id: u64,
|
||||
pub operation_kind: u16,
|
||||
/// `0` for success, otherwise a stable `DataPlaneErrorKind` value.
|
||||
pub status: u16,
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "easytier-mini"
|
||||
description = "Minimal native EasyTier node with TCP/UDP tunnels, TUN and UDP hole punching."
|
||||
version = "2.6.4"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license-file = "../../LICENSE"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
easytier = { path = "../../easytier", version = "2.6.4", default-features = false, features = [
|
||||
"aes-gcm",
|
||||
"dhcp-ipv4",
|
||||
"logging",
|
||||
"proxy-cidr-monitor",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"web-client",
|
||||
] }
|
||||
tokio = { version = "1", default-features = false, features = ["macros", "rt", "signal"] }
|
||||
@@ -1,113 +0,0 @@
|
||||
# easytier-mini
|
||||
|
||||
`easytier-mini` is a native EasyTier POC binary. It shares EasyTier's TOML
|
||||
configuration model, peer protocol, TCP/UDP tunnel implementations, TUN,
|
||||
dynamic IPv4 allocation, the smoltcp userspace path and STUN/UDP hole-punching
|
||||
core with the full binary. It includes AES-GCM so its default encryption
|
||||
setting interoperates with the full binary's default configuration.
|
||||
|
||||
Build it with:
|
||||
|
||||
```sh
|
||||
cargo build --release -p easytier-mini
|
||||
```
|
||||
|
||||
For the static size target used by this POC:
|
||||
|
||||
```sh
|
||||
cargo build --profile mini --target x86_64-unknown-linux-musl -p easytier-mini
|
||||
```
|
||||
|
||||
MIPS targets use the repository's existing musl-cross toolchains. The helper
|
||||
builds the standard library for size, applies immediate-abort only to the mini
|
||||
MIPS target graph, and can build either or both byte orders:
|
||||
|
||||
```sh
|
||||
./easytier-contrib/easytier-mini/build-mips.sh all
|
||||
./easytier-contrib/easytier-mini/build-mips.sh mips
|
||||
./easytier-contrib/easytier-mini/build-mips.sh mipsel
|
||||
```
|
||||
|
||||
The `mini` profile derives from `release` and applies `opt-level=z` to the
|
||||
entire compact binary dependency graph. Full EasyTier release builds retain
|
||||
their normal `opt-level=3` profile. The musl builds use a mini-only static
|
||||
linker policy to stay below 5,000,000 bytes on x86-64 and 5,500,000 bytes on
|
||||
MIPS without UPX or another executable compressor. The compact x86-64 linker
|
||||
policy retains static PIE, packs relative relocations and folds identical code.
|
||||
MIPS builds omit standard-library backtrace support and use immediate abort;
|
||||
normal workspace MIPS builds are not affected. Compact linker policies omit
|
||||
unwind tables.
|
||||
|
||||
Start it with a normal EasyTier TOML file:
|
||||
|
||||
```sh
|
||||
easytier-mini --config mini.toml
|
||||
```
|
||||
|
||||
`-c` is accepted as the short form of `--config`.
|
||||
|
||||
Start it as an EasyTier Web managed node with a complete config-server URL:
|
||||
|
||||
```sh
|
||||
easytier-mini --config-server udp://config-server.easytier.cn:22020/TOKEN
|
||||
```
|
||||
|
||||
`--machine-id`, `--hostname`, and `--secure-mode` match the full client's Web
|
||||
identity and transport options. `--config` and `--config-server` may be used
|
||||
together: the local instance remains static while Web-owned instances are
|
||||
created, updated, retained, and deleted independently.
|
||||
|
||||
The node also exposes the native EasyTier management RPC protocol on
|
||||
`127.0.0.1:15888`, so the full `easytier-cli` can inspect it:
|
||||
|
||||
```sh
|
||||
easytier-cli node info
|
||||
easytier-cli peer
|
||||
easytier-cli route
|
||||
easytier-cli connector list
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```toml
|
||||
instance_name = "mini"
|
||||
ipv4 = "10.147.0.2"
|
||||
listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
|
||||
|
||||
[network_identity]
|
||||
network_name = "mini-poc"
|
||||
network_secret = "change-me"
|
||||
|
||||
[[peer]]
|
||||
uri = "tcp://example.net:11010"
|
||||
```
|
||||
|
||||
Local TOML and Web configuration both retain the complete authoritative model.
|
||||
The compact runtime silently omits unsupported capabilities while normalizing
|
||||
that model into live runtime state. EasyTier Web therefore sees every accepted
|
||||
configuration value unchanged and its consistency checks converge. This also
|
||||
applies to hot patches: for example, a port-forward patch remains visible to
|
||||
the controller while no port-forward service starts in mini. ChaCha20 falls
|
||||
back to AES-GCM rather than plaintext.
|
||||
|
||||
The compact runtime supports `tcp://` and `udp://` listener, mapped-listener
|
||||
and peer URLs. `no_tun = true` runs through smoltcp without an OS TUN device,
|
||||
and `dhcp = true` allocates the virtual IPv4 address dynamically.
|
||||
|
||||
The mini feature set keeps STUN collection, UDP hole punching, Web heartbeats,
|
||||
Web instance lifecycle management and the config hot-patch RPC. It omits TCP
|
||||
hole punching, endpoint discovery (`http://`, `https://`, `txt://` and
|
||||
`srv://` peers), protobuf reflection, logger control and the rest of the full
|
||||
management surface. Unsupported connector URLs are accepted as no-ops. Its
|
||||
local RPC surface remains read-only for node, peer, route and connector
|
||||
queries. OSPF route messages keep their original protobuf wire data, so fields
|
||||
added by future EasyTier versions are forwarded without requiring
|
||||
`prost-reflect`.
|
||||
|
||||
For size, this POC reads one file directly and does not support configuration
|
||||
from stdin or `${VAR}` expansion. It omits the process-management event journal,
|
||||
while the console logger still reports runtime events such as peer, connection,
|
||||
listener, TUN and DHCP changes. The RPC address is currently fixed, so only one
|
||||
mini process can use the default portal on a host. The x86-64 musl POC cannot
|
||||
provide reliable stack backtraces because its release binary has no unwind
|
||||
tables.
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Cargo invokes this same file as a rustc wrapper during compact MIPS builds.
|
||||
# Applying immediate-abort here keeps the size policy scoped to easytier-mini;
|
||||
# normal MIPS builds elsewhere in the workspace retain their panic behavior.
|
||||
if [ "${EASYTIER_MINI_MIPS_RUSTC_WRAPPER:-}" = "1" ]; then
|
||||
mini_rustc=$1
|
||||
shift
|
||||
for mini_rustc_arg in "$@"; do
|
||||
case "$mini_rustc_arg" in
|
||||
mips-unknown-linux-musl|mipsel-unknown-linux-musl)
|
||||
exec "$mini_rustc" "$@" \
|
||||
-Zunstable-options \
|
||||
-Cpanic=immediate-abort
|
||||
;;
|
||||
esac
|
||||
done
|
||||
exec "$mini_rustc" "$@"
|
||||
fi
|
||||
|
||||
mini_script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
mini_repo_dir=$(CDPATH= cd -- "$mini_script_dir/../.." && pwd)
|
||||
mini_requested_target=${1:-all}
|
||||
cd "$mini_repo_dir"
|
||||
|
||||
build_mips_target() {
|
||||
mini_target=$1
|
||||
mini_toolchain=$2
|
||||
PATH="$mini_repo_dir/musl_gcc/$mini_toolchain/bin:$PATH" \
|
||||
EASYTIER_MINI_MIPS_RUSTC_WRAPPER=1 \
|
||||
RUSTC_BOOTSTRAP=1 \
|
||||
RUSTC_WRAPPER="$mini_script_dir/build-mips.sh" \
|
||||
cargo build \
|
||||
--manifest-path "$mini_repo_dir/Cargo.toml" \
|
||||
--profile mini \
|
||||
--target "$mini_target" \
|
||||
-Z build-std=std \
|
||||
-Z build-std-features=optimize_for_size \
|
||||
-p easytier-mini
|
||||
}
|
||||
|
||||
case "$mini_requested_target" in
|
||||
all)
|
||||
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
|
||||
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
|
||||
;;
|
||||
mips|mips-unknown-linux-musl)
|
||||
build_mips_target mips-unknown-linux-musl mips-unknown-linux-muslsf
|
||||
;;
|
||||
mipsel|mipsel-unknown-linux-musl)
|
||||
build_mips_target mipsel-unknown-linux-musl mipsel-unknown-linux-muslsf
|
||||
;;
|
||||
-h|--help)
|
||||
echo "usage: $0 [all|mips|mipsel]"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported MIPS target: $mini_requested_target" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -1,32 +0,0 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let target = env::var("TARGET").unwrap_or_default();
|
||||
let profile = env::var("PROFILE").unwrap_or_default();
|
||||
if !matches!(profile.as_str(), "release" | "mini")
|
||||
|| !matches!(
|
||||
target.as_str(),
|
||||
"x86_64-unknown-linux-musl" | "mips-unknown-linux-musl" | "mipsel-unknown-linux-musl"
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let script =
|
||||
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("easytier-mini-musl.ld");
|
||||
println!("cargo:rerun-if-changed={}", script.display());
|
||||
// The release-derived mini profile already aborts panics. Keep the compact
|
||||
// binary's linker policy local so full EasyTier musl builds retain their
|
||||
// normal PIE/unwind settings.
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--build-id=none");
|
||||
if target == "x86_64-unknown-linux-musl" {
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--pack-dyn-relocs=relr");
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--icf=all");
|
||||
}
|
||||
println!("cargo:rustc-link-arg-bin=easytier-mini=-Wl,--no-eh-frame-hdr");
|
||||
println!(
|
||||
"cargo:rustc-link-arg-bin=easytier-mini=-Wl,-T,{}",
|
||||
script.display()
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
SECTIONS
|
||||
{
|
||||
.eh_frame :
|
||||
{
|
||||
KEEP(*crtbegin.o(.eh_frame))
|
||||
KEEP(*crtend.o(.eh_frame))
|
||||
}
|
||||
/DISCARD/ :
|
||||
{
|
||||
*(EXCLUDE_FILE (*crtbegin.o *crtend.o) .eh_frame)
|
||||
*(.eh_frame_hdr)
|
||||
}
|
||||
}
|
||||
INSERT AFTER .data;
|
||||
@@ -1,264 +0,0 @@
|
||||
use std::{ffi::OsString, path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use easytier::common::MachineIdOptions;
|
||||
use easytier::{
|
||||
common::config::{ConfigFileControl, load_toml_config_from_path},
|
||||
instance::factory::native_compact_instance_manager_with_runtime,
|
||||
rpc_service::ReadOnlyApiRpcServer,
|
||||
web_client::{WebClientHooks, parse_config_server_endpoint, run_web_client},
|
||||
};
|
||||
|
||||
enum Command {
|
||||
Run(RunOptions),
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct RunOptions {
|
||||
config: Option<PathBuf>,
|
||||
config_server: Option<String>,
|
||||
machine_id: Option<String>,
|
||||
hostname: Option<String>,
|
||||
secure_mode: bool,
|
||||
}
|
||||
|
||||
const USAGE: &str = "usage: easytier-mini [--config <FILE>] [--config-server <URL>] \
|
||||
[--machine-id <ID>] [--hostname <NAME>] [--secure-mode]";
|
||||
|
||||
fn required_value(
|
||||
args: &mut impl Iterator<Item = OsString>,
|
||||
option: &str,
|
||||
) -> anyhow::Result<OsString> {
|
||||
args.next()
|
||||
.with_context(|| format!("{option} requires a value"))
|
||||
}
|
||||
|
||||
fn parse_args(mut args: impl Iterator<Item = OsString>) -> anyhow::Result<Command> {
|
||||
let mut options = RunOptions::default();
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "-h" || arg == "--help" {
|
||||
println!(
|
||||
"easytier-mini {}\n\nUsage: {USAGE}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
return Ok(Command::Exit);
|
||||
}
|
||||
if arg == "-V" || arg == "--version" {
|
||||
println!("easytier-mini {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(Command::Exit);
|
||||
}
|
||||
if arg == "-c" || arg == "--config" {
|
||||
if options.config.is_some() {
|
||||
anyhow::bail!("--config may only be specified once");
|
||||
}
|
||||
options.config = Some(PathBuf::from(required_value(&mut args, "--config")?));
|
||||
continue;
|
||||
}
|
||||
if arg == "-w" || arg == "--config-server" {
|
||||
if options.config_server.is_some() {
|
||||
anyhow::bail!("--config-server may only be specified once");
|
||||
}
|
||||
options.config_server = Some(
|
||||
required_value(&mut args, "--config-server")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--config-server must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--machine-id" {
|
||||
options.machine_id = Some(
|
||||
required_value(&mut args, "--machine-id")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--machine-id must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--hostname" {
|
||||
options.hostname = Some(
|
||||
required_value(&mut args, "--hostname")?
|
||||
.into_string()
|
||||
.map_err(|_| anyhow::anyhow!("--hostname must be valid UTF-8"))?,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if arg == "--secure-mode" {
|
||||
options.secure_mode = true;
|
||||
continue;
|
||||
}
|
||||
anyhow::bail!("unknown argument {arg:?}; {USAGE}");
|
||||
}
|
||||
if options.config.is_none() && options.config_server.is_none() {
|
||||
anyhow::bail!("either --config or --config-server is required; {USAGE}");
|
||||
}
|
||||
Ok(Command::Run(options))
|
||||
}
|
||||
|
||||
fn require_tcp_or_udp(scheme: &str, source: &str) -> anyhow::Result<()> {
|
||||
match scheme {
|
||||
"tcp" | "udp" => Ok(()),
|
||||
scheme => anyhow::bail!(
|
||||
"{source} uses unsupported tunnel scheme {scheme:?}; easytier-mini supports only tcp:// and udp://"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_config_server(config_server: &str) -> anyhow::Result<()> {
|
||||
let endpoint = parse_config_server_endpoint(config_server)?;
|
||||
require_tcp_or_udp(endpoint.connect_url().scheme(), "config server")
|
||||
}
|
||||
|
||||
struct MiniWebClientHooks;
|
||||
|
||||
impl WebClientHooks for MiniWebClientHooks {
|
||||
fn manages_remote_config_instances(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let Command::Run(options) = parse_args(std::env::args_os().skip(1))? else {
|
||||
return Ok(());
|
||||
};
|
||||
easytier::common::log::init_console()?;
|
||||
let local_config = options
|
||||
.config
|
||||
.as_ref()
|
||||
.map(|config_path| {
|
||||
load_toml_config_from_path(config_path)
|
||||
.with_context(|| format!("failed to load {}", config_path.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
if let Some(config_server) = options.config_server.as_deref() {
|
||||
validate_config_server(config_server)?;
|
||||
}
|
||||
|
||||
let instances = Arc::new(native_compact_instance_manager_with_runtime(
|
||||
tokio::runtime::Handle::current(),
|
||||
));
|
||||
let local_instance_id = local_config
|
||||
.map(|config| instances.run_network_instance(config, ConfigFileControl::STATIC_CONFIG))
|
||||
.transpose()?;
|
||||
let _web_client = if let Some(config_server) = options.config_server.as_deref() {
|
||||
Some(
|
||||
run_web_client(
|
||||
config_server,
|
||||
MachineIdOptions {
|
||||
explicit_machine_id: options.machine_id,
|
||||
state_dir: None,
|
||||
},
|
||||
options.hostname,
|
||||
options.secure_mode,
|
||||
instances.clone(),
|
||||
Some(Arc::new(MiniWebClientHooks)),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let _rpc_server =
|
||||
ReadOnlyApiRpcServer::new(Some("127.0.0.1:15888".to_owned()), None, instances.clone())?
|
||||
.serve()
|
||||
.await?;
|
||||
eprintln!(
|
||||
"easytier-mini started: local={local_instance_id:?}, web={}; RPC: 127.0.0.1:15888",
|
||||
options.config_server.is_some()
|
||||
);
|
||||
|
||||
let stopped_unexpectedly = tokio::select! {
|
||||
signal = tokio::signal::ctrl_c() => {
|
||||
signal.context("failed to listen for Ctrl-C")?;
|
||||
false
|
||||
},
|
||||
_ = instances.wait() => true,
|
||||
};
|
||||
|
||||
for instance in instances.instances() {
|
||||
instance.stop().await;
|
||||
}
|
||||
if stopped_unexpectedly {
|
||||
anyhow::bail!("EasyTier instance stopped unexpectedly");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use easytier::common::config::{ConfigLoader as _, TomlConfigLoader};
|
||||
|
||||
#[test]
|
||||
fn parses_minimal_config_argument() {
|
||||
let Command::Run(options) =
|
||||
parse_args([OsString::from("--config"), OsString::from("mini.toml")].into_iter())
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("expected run command");
|
||||
};
|
||||
|
||||
assert_eq!(options.config, Some(PathBuf::from("mini.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_web_client_arguments_without_a_local_config() {
|
||||
let Command::Run(options) = parse_args(
|
||||
[
|
||||
OsString::from("--config-server"),
|
||||
OsString::from("token"),
|
||||
OsString::from("--machine-id"),
|
||||
OsString::from("machine"),
|
||||
OsString::from("--hostname"),
|
||||
OsString::from("mini"),
|
||||
OsString::from("--secure-mode"),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap() else {
|
||||
panic!("expected run command");
|
||||
};
|
||||
|
||||
assert_eq!(options.config_server.as_deref(), Some("token"));
|
||||
assert_eq!(options.machine_id.as_deref(), Some("machine"));
|
||||
assert_eq!(options.hostname.as_deref(), Some("mini"));
|
||||
assert!(options.secure_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_arguments() {
|
||||
let result = parse_args([OsString::from("extra")].into_iter());
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_tcp_udp_config_server() {
|
||||
assert!(validate_config_server("udp://127.0.0.1:22020/token").is_ok());
|
||||
assert!(validate_config_server("quic://127.0.0.1:22020/token").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn compact_factory_accepts_unsupported_config_without_changing_it() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
dhcp = true
|
||||
listeners = ["quic://127.0.0.1:11010"]
|
||||
proxy_network = [{ cidr = "10.20.0.0/16" }]
|
||||
|
||||
[flags]
|
||||
encryption_algorithm = "chacha20"
|
||||
data_compress_algo = "Zstd"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
config.get_id();
|
||||
let before = config.dump();
|
||||
let manager =
|
||||
native_compact_instance_manager_with_runtime(tokio::runtime::Handle::current());
|
||||
|
||||
let instance = manager.create(config, ()).unwrap();
|
||||
|
||||
assert_eq!(instance.toml_config().unwrap().dump(), before);
|
||||
}
|
||||
}
|
||||
+336
-218
@@ -150,16 +150,6 @@ version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "ariadne"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f5e3dca4e09a6f340a61a0e9c7b61e030c69fc27bf29d73218f7e5e3b7638f"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.11",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -198,6 +188,28 @@ dependencies = [
|
||||
"ringbuf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
@@ -934,6 +946,17 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.9"
|
||||
@@ -981,6 +1004,17 @@ dependencies = [
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "delegate"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.3"
|
||||
@@ -990,6 +1024,17 @@ dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -1130,13 +1175,17 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-recursion",
|
||||
"async-ringbuf",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"atomic_refcell",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bon",
|
||||
"boringtun-easytier",
|
||||
"bytecodec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
@@ -1147,10 +1196,11 @@ dependencies = [
|
||||
"clap_complete_nushell",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"dbus",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more",
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"encoding",
|
||||
"flume",
|
||||
"forwarded-header-value",
|
||||
@@ -1163,127 +1213,95 @@ dependencies = [
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hmac",
|
||||
"http",
|
||||
"http_req",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna",
|
||||
"igd-next",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"log",
|
||||
"machine-uid",
|
||||
"moka",
|
||||
"multimap",
|
||||
"natpmp",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route 0.21.0",
|
||||
"netlink-packet-utils",
|
||||
"netlink-sys",
|
||||
"network-interface",
|
||||
"nix 0.29.0",
|
||||
"once_cell",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"pin-project-lite",
|
||||
"pnet_datalink",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"proc-macro2",
|
||||
"prost 0.14.3",
|
||||
"quanta",
|
||||
"prost-build",
|
||||
"prost-reflect 0.16.4",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt-types",
|
||||
"quinn",
|
||||
"quinn-proto",
|
||||
"quinn-plaintext",
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"resolv-conf",
|
||||
"ring",
|
||||
"ringbuf",
|
||||
"rust-i18n",
|
||||
"rustls",
|
||||
"seahash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"socket2 0.5.10",
|
||||
"strum",
|
||||
"stun_codec",
|
||||
"sys-locale",
|
||||
"tabled",
|
||||
"terminal_size",
|
||||
"thiserror 1.0.69",
|
||||
"thunk-rs",
|
||||
"time",
|
||||
"timedmap",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tokio-websockets",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tun-easytier",
|
||||
"unicode-width 0.1.11",
|
||||
"url",
|
||||
"uuid",
|
||||
"version-compare",
|
||||
"which 7.0.3",
|
||||
"wildmatch",
|
||||
"winapi",
|
||||
"windivert",
|
||||
"windows 0.62.2",
|
||||
"windows-service",
|
||||
"winreg 0.52.0",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-core"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"ariadne",
|
||||
"async-ringbuf",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bytecodec",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"derive_builder",
|
||||
"easytier-proto",
|
||||
"futures",
|
||||
"getrandom 0.2.16",
|
||||
"getrandom 0.3.3",
|
||||
"guarden",
|
||||
"hmac",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"idna",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"pin-project-lite",
|
||||
"prefix-trie",
|
||||
"prost 0.14.3",
|
||||
"prost-types 0.14.3",
|
||||
"quanta",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"strum",
|
||||
"stun_codec",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"webpki-roots 0.26.11",
|
||||
"wildmatch",
|
||||
"x25519-dalek",
|
||||
"zerocopy 0.7.35",
|
||||
"zip",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@@ -1291,22 +1309,17 @@ dependencies = [
|
||||
name = "easytier-ohrs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"easytier",
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"flate2",
|
||||
"futures",
|
||||
"gethostname 1.1.0",
|
||||
"ipnet",
|
||||
"napi-build-ohos",
|
||||
"napi-derive-ohos",
|
||||
"napi-ohos",
|
||||
"once_cell",
|
||||
"prost-reflect",
|
||||
"prost-reflect 0.14.7",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1318,39 +1331,6 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-proto"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"hmac",
|
||||
"indoc",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"proc-macro2",
|
||||
"prost 0.14.3",
|
||||
"prost-build",
|
||||
"prost-types 0.14.3",
|
||||
"prost-wkt-types",
|
||||
"quote",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"x25519-dalek",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@@ -1459,6 +1439,12 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -1872,22 +1858,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guarden"
|
||||
version = "0.2.0"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8408903291a7d0cc74169d5de4dd1919a9a402a2f67fcd7df3303ed045fae73"
|
||||
checksum = "31c7272e004bec8ea7fe50b2ec5451858695bb2743e897c353753fcb3415f4ef"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures",
|
||||
"guarden-macros",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden-macros"
|
||||
version = "0.2.0"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0ef28f1077c259f9e7e238e234a78ce18cedbf0251fd2135f5fc23c40e79fe"
|
||||
checksum = "2d291d94f41471fe84384a426b3e2c9d22f960a351a5bf26aaa7cd75fbc02c88"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
@@ -1943,9 +1928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -2127,6 +2112,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http_req"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/EasyTier/http_req.git#b10aa9fc0db3067cc3d2174683a87250b80a1ea9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"rand 0.8.5",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"unicase",
|
||||
"webpki",
|
||||
"webpki-roots 0.26.11",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -2419,12 +2420,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
version = "2.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"hashbrown 0.16.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -2636,6 +2637,16 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
@@ -2860,6 +2871,9 @@ name = "multimap"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build-ohos"
|
||||
@@ -2964,7 +2978,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"libc",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route",
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-sys",
|
||||
"once_cell",
|
||||
"system-configuration",
|
||||
@@ -2996,6 +3010,21 @@ dependencies = [
|
||||
"netlink-packet-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-route"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
"byteorder",
|
||||
"libc",
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-utils"
|
||||
version = "0.5.2"
|
||||
@@ -3021,9 +3050,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "network-interface"
|
||||
version = "2.0.5"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6"
|
||||
checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3126,6 +3155,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -3344,6 +3382,20 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d"
|
||||
dependencies = [
|
||||
"ipnetwork",
|
||||
"pnet_base",
|
||||
"pnet_datalink",
|
||||
"pnet_packet",
|
||||
"pnet_sys",
|
||||
"pnet_transport",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_base"
|
||||
version = "0.35.0"
|
||||
@@ -3351,6 +3403,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
|
||||
dependencies = [
|
||||
"no-std-net",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3363,9 +3416,43 @@ dependencies = [
|
||||
"libc",
|
||||
"pnet_base",
|
||||
"pnet_sys",
|
||||
"serde",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_macros"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_macros_support"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab"
|
||||
dependencies = [
|
||||
"pnet_base",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_packet"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"pnet_base",
|
||||
"pnet_macros",
|
||||
"pnet_macros_support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_sys"
|
||||
version = "0.35.0"
|
||||
@@ -3376,6 +3463,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pnet_transport"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pnet_base",
|
||||
"pnet_packet",
|
||||
"pnet_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@@ -3465,15 +3564,6 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.8+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
@@ -3602,10 +3692,31 @@ checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"prost 0.13.5",
|
||||
"prost-reflect-derive",
|
||||
"prost-reflect-derive 0.14.0",
|
||||
"prost-types 0.13.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9"
|
||||
dependencies = [
|
||||
"prost 0.14.3",
|
||||
"prost-reflect-derive 0.16.0",
|
||||
"prost-types 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-build"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8214ae2c30bbac390db0134d08300e770ef89b6d4e5abf855e8d300eded87e28"
|
||||
dependencies = [
|
||||
"prost-build",
|
||||
"prost-reflect 0.16.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-derive"
|
||||
version = "0.14.0"
|
||||
@@ -3617,6 +3728,17 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect-derive"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b6d90e29fa6c0d13c2c19ba5e4b3fb0efbf5975d27bcf4e260b7b15455bcabe"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
@@ -3681,21 +3803,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quanta"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"raw-cpuid",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.3"
|
||||
@@ -3725,6 +3832,18 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-plaintext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3e617feaeb6493018fa35fc47ae8b630ac8903d8159e9e747018841b99bad3d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"quinn-proto",
|
||||
"seahash",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
@@ -3869,15 +3988,6 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.12.1"
|
||||
@@ -4147,6 +4257,15 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@@ -4373,7 +4492,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which",
|
||||
"which 4.4.2",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
@@ -4771,7 +4890,9 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -4794,6 +4915,12 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "timedmap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "825f6c8a18bc36d56a62f66af7296385b628c9c5543a8663d4c217fc920bfefd"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@@ -4895,7 +5022,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.13.2"
|
||||
source = "git+https://github.com/EasyTier/tokio-websockets#dc9771c7c215882349c3cb328877550a3593df21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -4921,8 +5049,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4934,15 +5062,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
@@ -4952,30 +5071,9 @@ dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.8+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.1.0+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.3",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5086,6 +5184,7 @@ dependencies = [
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"time",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
@@ -5312,6 +5411,12 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -5496,6 +5601,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.5"
|
||||
@@ -5535,6 +5650,18 @@ dependencies = [
|
||||
"rustix 0.38.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix 1.1.2",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.0"
|
||||
@@ -6135,15 +6262,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
@@ -6164,6 +6282,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wintun"
|
||||
version = "0.5.1"
|
||||
@@ -6304,12 +6428,6 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
|
||||
@@ -7,18 +7,9 @@ edition = "2024"
|
||||
crate-type=["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
bytes = "1.5"
|
||||
easytier-core = { path = "../../easytier-core", default-features = false }
|
||||
easytier-proto = { path = "../../easytier-proto", default-features = false, features = [
|
||||
"api",
|
||||
"core",
|
||||
"json-rpc",
|
||||
] }
|
||||
flate2 = "1.1"
|
||||
futures = "0.3"
|
||||
gethostname = "1.1"
|
||||
easytier = { path = "../../easytier" }
|
||||
napi-derive-ohos = "1.1"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::config::types::stored_config::{ExportTomlResult, StoredConfigRecord};
|
||||
use easytier::common::config::NetworkConfigExt;
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use easytier::common::config::NetworkConfigExt;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use serde_json::{Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -36,8 +36,8 @@ pub(crate) fn stop_kernel(
|
||||
return false;
|
||||
};
|
||||
|
||||
let ret = ASYNC_RUNTIME
|
||||
.block_on(INSTANCE_MANAGER.delete_network_instances([instance_id]))
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!("[Rust] stop_kernel failed {}: {}", config_id, err);
|
||||
@@ -46,7 +46,7 @@ pub(crate) fn stop_kernel(
|
||||
if ret {
|
||||
clear_runtime_config_snapshot(&config_id);
|
||||
}
|
||||
let has_active_instances = !INSTANCE_MANAGER.instance_ids().is_empty();
|
||||
let has_active_instances = !INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let has_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| !guard.is_empty())
|
||||
@@ -102,7 +102,7 @@ pub(crate) fn set_tun_fd(
|
||||
};
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.attach_tun_fd(instance_id, fd)
|
||||
.set_tun_fd(&instance_id, fd)
|
||||
.map(|_| {
|
||||
mark_tun_attached(&config_id);
|
||||
ohrs_log_info!(
|
||||
@@ -118,7 +118,11 @@ pub(crate) fn set_tun_fd(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn collect_runtime_state() -> RuntimeAggregateState {
|
||||
pub(crate) fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
let infos = match ASYNC_RUNTIME.block_on(INSTANCE_MANAGER.collect_network_infos()) {
|
||||
Ok(infos) => infos,
|
||||
Err(err) => {
|
||||
|
||||
@@ -3,4 +3,6 @@ mod routing;
|
||||
mod socket_server;
|
||||
|
||||
pub(crate) use routing::aggregate_requested_tun_routes;
|
||||
pub use socket_server::{start_local_socket_server, stop_local_socket_server};
|
||||
pub use socket_server::{
|
||||
set_snapshot_broadcast_enabled, start_local_socket_server, stop_local_socket_server,
|
||||
};
|
||||
|
||||
@@ -32,13 +32,6 @@ pub(crate) fn send_local_socket_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn shrink_clients_if_sparse(clients: &mut Vec<UnixStream>) {
|
||||
let sparse_limit = clients.len().saturating_mul(2).max(4);
|
||||
if clients.capacity() > sparse_limit {
|
||||
clients.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn broadcast_local_socket_message(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
message_type: &str,
|
||||
@@ -52,7 +45,6 @@ pub(crate) fn broadcast_local_socket_message(
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
shrink_clients_if_sparse(&mut active_clients);
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
@@ -87,7 +79,6 @@ pub(crate) fn broadcast_local_socket_json_payload_message(
|
||||
active_clients.push(client);
|
||||
}
|
||||
}
|
||||
shrink_clients_if_sparse(&mut active_clients);
|
||||
*clients = active_clients;
|
||||
delivered
|
||||
}
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
use super::protocol::{
|
||||
TunRequestPayload, broadcast_local_socket_json_payload_message, broadcast_local_socket_message,
|
||||
};
|
||||
use crate::collect_runtime_state_inner;
|
||||
use crate::INSTANCE_MANAGER;
|
||||
use crate::config::repository::kernel_socket_path;
|
||||
use crate::get_runtime_snapshot_inner;
|
||||
use crate::kernel_bridge::routing::aggregate_tun_routes;
|
||||
use crate::runtime::state::runtime_state::{
|
||||
PeerConnInfo as RuntimePeerConnInfo, RuntimeAggregateState, peer_conn_to_view,
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER};
|
||||
use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
|
||||
use easytier::instance::factory::subscribe_native_instance_event;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
struct LocalSocketState {
|
||||
stop_flag: std::sync::Arc<AtomicBool>,
|
||||
@@ -29,151 +23,57 @@ struct LocalSocketState {
|
||||
}
|
||||
|
||||
static LOCAL_SOCKET_STATE: Lazy<Mutex<Option<LocalSocketState>>> = Lazy::new(|| Mutex::new(None));
|
||||
static SNAPSHOT_BROADCAST_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
const SOCKET_TICK_INTERVAL: Duration = Duration::from_millis(250);
|
||||
const TRAFFIC_STATS_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const INSTANCE_POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const TUN_FAST_CHECK_WINDOW: Duration = Duration::from_secs(8);
|
||||
const EVENT_RECEIVER_SYNC_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TrafficStatsPayload {
|
||||
sampled_at_ms: i64,
|
||||
instances: Vec<InstanceTrafficStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstanceTrafficStats {
|
||||
config_id: String,
|
||||
instance_id: String,
|
||||
rx_bytes: i64,
|
||||
tx_bytes: i64,
|
||||
peers: Vec<PeerTrafficStats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PeerTrafficStats {
|
||||
peer_id: i64,
|
||||
rx_bytes: i64,
|
||||
tx_bytes: i64,
|
||||
total_bytes: i64,
|
||||
latency_us: i64,
|
||||
loss_rate: f64,
|
||||
}
|
||||
|
||||
struct PendingPeerEvent {
|
||||
event: &'static str,
|
||||
instance_id: String,
|
||||
peer_id: i64,
|
||||
conn: Option<RuntimePeerConnInfo>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DrainedKernelEvents {
|
||||
tun_refresh: bool,
|
||||
topology_lost: bool,
|
||||
peer_events: Vec<PendingPeerEvent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RuntimePeerEventPayload {
|
||||
event: &'static str,
|
||||
config_id: String,
|
||||
instance_id: String,
|
||||
peer_id: i64,
|
||||
conn: Option<RuntimePeerConnInfo>,
|
||||
}
|
||||
|
||||
fn shrink_hash_map_if_sparse<K: Eq + Hash, V>(map: &mut HashMap<K, V>) {
|
||||
let sparse_limit = map.len().saturating_mul(2).max(8);
|
||||
if map.capacity() > sparse_limit {
|
||||
map.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink_hash_set_if_sparse<T: Eq + Hash>(set: &mut HashSet<T>) {
|
||||
let sparse_limit = set.len().saturating_mul(2).max(8);
|
||||
if set.capacity() > sparse_limit {
|
||||
set.shrink_to_fit();
|
||||
}
|
||||
pub fn set_snapshot_broadcast_enabled(enabled: bool) {
|
||||
SNAPSHOT_BROADCAST_ENABLED.store(enabled, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn sync_tun_event_receivers(receivers: &mut HashMap<String, EventBusSubscriber>) {
|
||||
let mut active_instance_ids = HashSet::new();
|
||||
for instance in INSTANCE_MANAGER.instances() {
|
||||
let instance_id = instance.instance_id().to_string();
|
||||
for instance in INSTANCE_MANAGER.iter() {
|
||||
let instance_id = instance.key().to_string();
|
||||
active_instance_ids.insert(instance_id.clone());
|
||||
if !receivers.contains_key(&instance_id)
|
||||
&& let Some(receiver) = subscribe_native_instance_event(&instance)
|
||||
&& let Some(receiver) = instance.value().subscribe_event()
|
||||
{
|
||||
receivers.insert(instance_id, receiver);
|
||||
}
|
||||
}
|
||||
receivers.retain(|instance_id, _| active_instance_ids.contains(instance_id));
|
||||
shrink_hash_map_if_sparse(receivers);
|
||||
}
|
||||
|
||||
fn event_needs_tun_refresh(event: &GlobalCtxEvent) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
GlobalCtxEvent::DhcpIpv4Changed(_, _)
|
||||
| GlobalCtxEvent::ProxyCidrsUpdated(_, _)
|
||||
| GlobalCtxEvent::DhcpIpv4Conflicted(_)
|
||||
| GlobalCtxEvent::PublicIpv6Changed(_, _)
|
||||
| GlobalCtxEvent::PublicIpv6RoutesUpdated(_, _)
|
||||
| GlobalCtxEvent::ProxyCidrsUpdated(_, _)
|
||||
| GlobalCtxEvent::ConfigPatched(_)
|
||||
| GlobalCtxEvent::PeerAdded(_)
|
||||
| GlobalCtxEvent::PeerRemoved(_)
|
||||
| GlobalCtxEvent::PeerConnAdded(_)
|
||||
| GlobalCtxEvent::PeerConnRemoved(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn drain_kernel_events(receivers: &mut HashMap<String, EventBusSubscriber>) -> DrainedKernelEvents {
|
||||
let mut drained = DrainedKernelEvents::default();
|
||||
fn drain_tun_refresh_events(receivers: &mut HashMap<String, EventBusSubscriber>) -> bool {
|
||||
let mut refresh_needed = false;
|
||||
let mut closed_receivers = Vec::new();
|
||||
for (instance_id, receiver) in receivers.iter_mut() {
|
||||
loop {
|
||||
match receiver.try_recv() {
|
||||
Ok(event) => {
|
||||
drained.tun_refresh = event_needs_tun_refresh(&event) || drained.tun_refresh;
|
||||
match event {
|
||||
GlobalCtxEvent::PeerAdded(peer_id) => {
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_added",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id: peer_id as i64,
|
||||
conn: None,
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerRemoved(peer_id) => {
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_removed",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id: peer_id as i64,
|
||||
conn: None,
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerConnAdded(conn_info) => {
|
||||
let peer_id = conn_info.peer_id as i64;
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_conn_added",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id,
|
||||
conn: Some(peer_conn_to_view(conn_info)),
|
||||
});
|
||||
}
|
||||
GlobalCtxEvent::PeerConnRemoved(conn_info) => {
|
||||
let peer_id = conn_info.peer_id as i64;
|
||||
drained.peer_events.push(PendingPeerEvent {
|
||||
event: "peer_conn_removed",
|
||||
instance_id: instance_id.clone(),
|
||||
peer_id,
|
||||
conn: Some(peer_conn_to_view(conn_info)),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
refresh_needed = event_needs_tun_refresh(&event) || refresh_needed;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) => {
|
||||
drained.topology_lost = true;
|
||||
refresh_needed = true;
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::TryRecvError::Closed) => {
|
||||
@@ -186,117 +86,7 @@ fn drain_kernel_events(receivers: &mut HashMap<String, EventBusSubscriber>) -> D
|
||||
for instance_id in closed_receivers {
|
||||
receivers.remove(&instance_id);
|
||||
}
|
||||
drained
|
||||
}
|
||||
|
||||
fn broadcast_runtime_peer_events(
|
||||
clients: &mut Vec<UnixStream>,
|
||||
peer_events: Vec<PendingPeerEvent>,
|
||||
) {
|
||||
for event in peer_events {
|
||||
let payload = RuntimePeerEventPayload {
|
||||
event: event.event,
|
||||
config_id: event.instance_id.clone(),
|
||||
instance_id: event.instance_id,
|
||||
peer_id: event.peer_id,
|
||||
conn: event.conn,
|
||||
};
|
||||
match serde_json::to_string(&payload) {
|
||||
Ok(json) => {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
clients,
|
||||
"runtime_peer_event",
|
||||
&json,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize runtime peer event failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tun_candidate_ids(snapshot: &RuntimeAggregateState) -> HashSet<String> {
|
||||
snapshot
|
||||
.instances
|
||||
.iter()
|
||||
.filter(|instance| instance.running && instance.tun_required)
|
||||
.map(|instance| instance.instance_id.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_traffic_stats(sampled_at_ms: i64) -> TrafficStatsPayload {
|
||||
let running_instances = INSTANCE_MANAGER
|
||||
.instances()
|
||||
.into_iter()
|
||||
.filter(|instance| instance.is_ready())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let instances = ASYNC_RUNTIME.block_on(async {
|
||||
let mut instances = Vec::new();
|
||||
for instance in running_instances {
|
||||
let instance_id = instance.instance_id().to_string();
|
||||
let peers = instance.peer_snapshots().await;
|
||||
|
||||
let mut instance_rx_bytes = 0i64;
|
||||
let mut instance_tx_bytes = 0i64;
|
||||
let mut peer_stats = Vec::with_capacity(peers.len());
|
||||
|
||||
for peer in peers {
|
||||
let mut peer_rx_bytes = 0i64;
|
||||
let mut peer_tx_bytes = 0i64;
|
||||
let mut latency_us = i64::MAX;
|
||||
let mut loss_rate = 0f64;
|
||||
|
||||
for conn in peer.conns {
|
||||
if let Some(stats) = conn.stats {
|
||||
let rx_bytes = stats.rx_bytes as i64;
|
||||
let tx_bytes = stats.tx_bytes as i64;
|
||||
peer_rx_bytes += rx_bytes;
|
||||
peer_tx_bytes += tx_bytes;
|
||||
latency_us = latency_us.min(stats.latency_us as i64);
|
||||
}
|
||||
loss_rate = loss_rate.max(conn.loss_rate as f64);
|
||||
}
|
||||
|
||||
instance_rx_bytes += peer_rx_bytes;
|
||||
instance_tx_bytes += peer_tx_bytes;
|
||||
peer_stats.push(PeerTrafficStats {
|
||||
peer_id: peer.peer_id as i64,
|
||||
rx_bytes: peer_rx_bytes,
|
||||
tx_bytes: peer_tx_bytes,
|
||||
total_bytes: peer_rx_bytes + peer_tx_bytes,
|
||||
latency_us: if latency_us == i64::MAX {
|
||||
-1
|
||||
} else {
|
||||
latency_us
|
||||
},
|
||||
loss_rate,
|
||||
});
|
||||
}
|
||||
|
||||
instances.push(InstanceTrafficStats {
|
||||
config_id: instance_id.clone(),
|
||||
instance_id,
|
||||
rx_bytes: instance_rx_bytes,
|
||||
tx_bytes: instance_tx_bytes,
|
||||
peers: peer_stats,
|
||||
});
|
||||
}
|
||||
instances
|
||||
});
|
||||
|
||||
TrafficStatsPayload {
|
||||
sampled_at_ms,
|
||||
instances,
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_time_millis() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
|
||||
.unwrap_or_default()
|
||||
refresh_needed
|
||||
}
|
||||
|
||||
pub fn start_local_socket_server() -> bool {
|
||||
@@ -341,25 +131,21 @@ pub fn start_local_socket_server() -> bool {
|
||||
let stop_flag = std::sync::Arc::new(AtomicBool::new(false));
|
||||
let worker_stop_flag = stop_flag.clone();
|
||||
let worker = thread::spawn(move || {
|
||||
let mut last_topology_json = String::new();
|
||||
let mut last_snapshot_json = String::new();
|
||||
let mut delivered_tun_requests = HashSet::new();
|
||||
let mut last_tun_route_signatures = HashMap::<String, String>::new();
|
||||
let mut tun_fast_until = Instant::now() + TUN_FAST_CHECK_WINDOW;
|
||||
let mut tun_bootstrap_done = false;
|
||||
let mut last_event_receiver_sync_at: Option<Instant> = None;
|
||||
let mut last_traffic_stats_at: Option<Instant> = None;
|
||||
let mut last_instance_poll_at: Option<Instant> = None;
|
||||
let mut tun_event_receivers = HashMap::<String, EventBusSubscriber>::new();
|
||||
let mut clients = Vec::<UnixStream>::new();
|
||||
|
||||
while !worker_stop_flag.load(Ordering::Relaxed) {
|
||||
let mut full_topology_dirty = false;
|
||||
let mut accepted_client = false;
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _addr)) => {
|
||||
accepted_client = true;
|
||||
full_topology_dirty = true;
|
||||
clients.push(stream);
|
||||
tun_fast_until = Instant::now() + TUN_FAST_CHECK_WINDOW;
|
||||
tun_bootstrap_done = false;
|
||||
@@ -372,21 +158,15 @@ pub fn start_local_socket_server() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot_enabled = SNAPSHOT_BROADCAST_ENABLED.load(Ordering::Relaxed);
|
||||
if clients.is_empty() {
|
||||
if !last_topology_json.is_empty() {
|
||||
last_topology_json.clear();
|
||||
last_topology_json.shrink_to_fit();
|
||||
if !last_snapshot_json.is_empty() {
|
||||
last_snapshot_json.clear();
|
||||
}
|
||||
delivered_tun_requests.clear();
|
||||
shrink_hash_set_if_sparse(&mut delivered_tun_requests);
|
||||
last_tun_route_signatures.clear();
|
||||
shrink_hash_map_if_sparse(&mut last_tun_route_signatures);
|
||||
tun_event_receivers.clear();
|
||||
shrink_hash_map_if_sparse(&mut tun_event_receivers);
|
||||
clients.shrink_to_fit();
|
||||
last_event_receiver_sync_at = None;
|
||||
last_traffic_stats_at = None;
|
||||
last_instance_poll_at = None;
|
||||
tun_bootstrap_done = false;
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
continue;
|
||||
@@ -401,143 +181,115 @@ pub fn start_local_socket_server() -> bool {
|
||||
sync_tun_event_receivers(&mut tun_event_receivers);
|
||||
last_event_receiver_sync_at = Some(now);
|
||||
}
|
||||
let drained_events = drain_kernel_events(&mut tun_event_receivers);
|
||||
let tun_refresh = drained_events.tun_refresh;
|
||||
let topology_lost = drained_events.topology_lost;
|
||||
let peer_events = drained_events.peer_events;
|
||||
if topology_lost {
|
||||
full_topology_dirty = true;
|
||||
}
|
||||
if tun_refresh {
|
||||
if drain_tun_refresh_events(&mut tun_event_receivers) {
|
||||
tun_bootstrap_done = false;
|
||||
tun_fast_until = now + TUN_FAST_CHECK_WINDOW;
|
||||
}
|
||||
if !peer_events.is_empty() {
|
||||
broadcast_runtime_peer_events(&mut clients, peer_events);
|
||||
}
|
||||
let should_collect_traffic_stats = last_traffic_stats_at
|
||||
.map(|last| now.duration_since(last) >= TRAFFIC_STATS_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
if should_collect_traffic_stats {
|
||||
last_traffic_stats_at = Some(now);
|
||||
match serde_json::to_string(&collect_traffic_stats(unix_time_millis())) {
|
||||
Ok(json) => {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
"traffic_stats",
|
||||
&json,
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize traffic stats failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
let should_poll_instance = last_instance_poll_at
|
||||
.map(|last| now.duration_since(last) >= INSTANCE_POLL_INTERVAL)
|
||||
.unwrap_or(true);
|
||||
let should_collect_topology = accepted_client
|
||||
|| full_topology_dirty
|
||||
|| tun_refresh
|
||||
|| should_poll_instance
|
||||
let should_collect_snapshot = snapshot_enabled
|
||||
|| accepted_client
|
||||
|| (!tun_bootstrap_done && now < tun_fast_until);
|
||||
if !should_collect_topology {
|
||||
if !should_collect_snapshot {
|
||||
if !last_snapshot_json.is_empty() {
|
||||
last_snapshot_json.clear();
|
||||
}
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
|
||||
let snapshot = collect_runtime_state_inner();
|
||||
last_instance_poll_at = Some(now);
|
||||
match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => {
|
||||
if accepted_client || full_topology_dirty || json != last_topology_json {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
"runtime_topology",
|
||||
&json,
|
||||
);
|
||||
last_topology_json = json;
|
||||
let snapshot = get_runtime_snapshot_inner();
|
||||
if snapshot_enabled {
|
||||
let snapshot_json = match serde_json::to_string(&snapshot) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize runtime snapshot failed: {}", err);
|
||||
thread::sleep(SOCKET_TICK_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if accepted_client || snapshot_json != last_snapshot_json {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
"runtime_snapshot",
|
||||
&snapshot_json,
|
||||
);
|
||||
last_snapshot_json = snapshot_json;
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize runtime topology failed: {}", err);
|
||||
}
|
||||
} else if !last_snapshot_json.is_empty() {
|
||||
last_snapshot_json.clear();
|
||||
}
|
||||
|
||||
let active_tun_candidate_ids = tun_candidate_ids(&snapshot);
|
||||
delivered_tun_requests
|
||||
.retain(|instance_id| active_tun_candidate_ids.contains(instance_id));
|
||||
last_tun_route_signatures
|
||||
.retain(|instance_id, _| active_tun_candidate_ids.contains(instance_id));
|
||||
shrink_hash_set_if_sparse(&mut delivered_tun_requests);
|
||||
shrink_hash_map_if_sparse(&mut last_tun_route_signatures);
|
||||
let mut saw_running_instance = false;
|
||||
let mut saw_tun_candidate = false;
|
||||
for instance in snapshot.instances.iter() {
|
||||
if instance.running {
|
||||
saw_running_instance = true;
|
||||
}
|
||||
if !(instance.running && instance.tun_required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
saw_tun_candidate = true;
|
||||
let virtual_ipv4 = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4.clone());
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
if clients.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if virtual_ipv4.is_none() || virtual_ipv4_cidr.is_none() {
|
||||
continue;
|
||||
}
|
||||
let aggregated_routes = aggregate_tun_routes(instance);
|
||||
let route_signature = serde_json::to_string(&(
|
||||
&virtual_ipv4,
|
||||
&virtual_ipv4_cidr,
|
||||
&aggregated_routes,
|
||||
instance.magic_dns_enabled,
|
||||
instance.need_exit_node,
|
||||
))
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let should_send = !delivered_tun_requests.contains(&instance.instance_id)
|
||||
|| last_tun_route_signatures
|
||||
.get(&instance.instance_id)
|
||||
.map(|value| value != &route_signature)
|
||||
.unwrap_or(true);
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let payload = TunRequestPayload {
|
||||
config_id: instance.config_id.clone(),
|
||||
instance_id: instance.instance_id.clone(),
|
||||
display_name: instance.display_name.clone(),
|
||||
virtual_ipv4,
|
||||
virtual_ipv4_cidr,
|
||||
aggregated_routes,
|
||||
magic_dns_enabled: instance.magic_dns_enabled,
|
||||
need_exit_node: instance.need_exit_node,
|
||||
};
|
||||
let payload_json = match serde_json::to_string(&payload) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize tun request failed: {}", err);
|
||||
if instance.running && instance.tun_required {
|
||||
saw_tun_candidate = true;
|
||||
let virtual_ipv4 = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4.clone());
|
||||
let virtual_ipv4_cidr = instance
|
||||
.my_node_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.virtual_ipv4_cidr.clone());
|
||||
if clients.is_empty() {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if broadcast_local_socket_message(&mut clients, "tun_request", &payload_json) {
|
||||
delivered_tun_requests.insert(instance.instance_id.clone());
|
||||
last_tun_route_signatures.insert(instance.instance_id.clone(), route_signature);
|
||||
if virtual_ipv4.is_none() || virtual_ipv4_cidr.is_none() {
|
||||
continue;
|
||||
}
|
||||
let aggregated_routes = aggregate_tun_routes(instance);
|
||||
let route_signature = serde_json::to_string(&(
|
||||
&virtual_ipv4,
|
||||
&virtual_ipv4_cidr,
|
||||
&aggregated_routes,
|
||||
instance.magic_dns_enabled,
|
||||
instance.need_exit_node,
|
||||
))
|
||||
.unwrap_or_else(|_| "[]".to_string());
|
||||
let should_send = accepted_client
|
||||
|| !delivered_tun_requests.contains(&instance.instance_id)
|
||||
|| last_tun_route_signatures
|
||||
.get(&instance.instance_id)
|
||||
.map(|value| value != &route_signature)
|
||||
.unwrap_or(true);
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let payload = TunRequestPayload {
|
||||
config_id: instance.config_id.clone(),
|
||||
instance_id: instance.instance_id.clone(),
|
||||
display_name: instance.display_name.clone(),
|
||||
virtual_ipv4,
|
||||
virtual_ipv4_cidr,
|
||||
aggregated_routes,
|
||||
magic_dns_enabled: instance.magic_dns_enabled,
|
||||
need_exit_node: instance.need_exit_node,
|
||||
};
|
||||
let payload_json = match serde_json::to_string(&payload) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] serialize tun request failed: {}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if broadcast_local_socket_message(&mut clients, "tun_request", &payload_json) {
|
||||
delivered_tun_requests.insert(instance.instance_id.clone());
|
||||
last_tun_route_signatures
|
||||
.insert(instance.instance_id.clone(), route_signature);
|
||||
}
|
||||
} else {
|
||||
delivered_tun_requests.remove(&instance.instance_id);
|
||||
last_tun_route_signatures.remove(&instance.instance_id);
|
||||
}
|
||||
}
|
||||
if !delivered_tun_requests.is_empty()
|
||||
|| (saw_running_instance && !saw_tun_candidate)
|
||||
|| now >= tun_fast_until
|
||||
if !snapshot_enabled
|
||||
&& (!delivered_tun_requests.is_empty()
|
||||
|| (saw_running_instance && !saw_tun_candidate)
|
||||
|| now >= tun_fast_until)
|
||||
{
|
||||
tun_bootstrap_done = true;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ macro_rules! ohrs_log_debug {
|
||||
mod config;
|
||||
mod exports;
|
||||
mod kernel_bridge;
|
||||
mod nearby_management;
|
||||
mod platform;
|
||||
mod runtime;
|
||||
|
||||
@@ -54,50 +53,41 @@ use config::services::share_link_service::{
|
||||
};
|
||||
use config::storage::config_meta::get_config_display_name;
|
||||
use config::types::stored_config::{KeyValuePair, SharedConfigLinkPayload, SnapshotImportResult};
|
||||
use easytier::common::config::NetworkConfigExt;
|
||||
use easytier::common::constants::EASYTIER_VERSION;
|
||||
use easytier::common::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader, TomlConfigLoader},
|
||||
config::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
||||
};
|
||||
use easytier::instance::factory::{NativeInstanceManager, native_instance_manager_with_runtime};
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use easytier::proto::api::manage::NetworkingMethod;
|
||||
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||
use kernel_bridge::{
|
||||
start_local_socket_server as start_local_socket_server_inner,
|
||||
set_snapshot_broadcast_enabled, start_local_socket_server as start_local_socket_server_inner,
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use napi_derive_ohos::napi;
|
||||
use napi_ohos::bindgen_prelude::Uint8Array;
|
||||
use runtime::state::runtime_state::{RuntimeAggregateState, RuntimeInstanceState};
|
||||
use runtime::state::runtime_state::RuntimeAggregateState;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::format;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
static ASYNC_RUNTIME: once_cell::sync::Lazy<Runtime> = once_cell::sync::Lazy::new(|| {
|
||||
Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for easytier-ohrs")
|
||||
});
|
||||
pub(crate) static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NativeInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
Arc::new(native_instance_manager_with_runtime(
|
||||
ASYNC_RUNTIME.handle().clone(),
|
||||
))
|
||||
});
|
||||
static WEB_CLIENTS: once_cell::sync::Lazy<Mutex<HashMap<String, ManagedWebClient>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
const PRO_CONFIG_SERVER_CLIENT_ID: &str = "__easytier_pro_config_server_client__";
|
||||
|
||||
#[derive(Default)]
|
||||
struct TrackedWebClientHooks {
|
||||
instance_ids: Mutex<HashSet<Uuid>>,
|
||||
network_names_by_instance_id: Mutex<HashMap<Uuid, String>>,
|
||||
events: Mutex<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
struct ManagedWebClient {
|
||||
@@ -105,13 +95,6 @@ struct ManagedWebClient {
|
||||
hooks: Arc<TrackedWebClientHooks>,
|
||||
}
|
||||
|
||||
fn network_name_for_instance(id: &Uuid) -> Option<String> {
|
||||
INSTANCE_MANAGER
|
||||
.config(*id)
|
||||
.map(|config| config.get_network_identity().network_name)
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl WebClientHooks for TrackedWebClientHooks {
|
||||
async fn post_run_network_instance(&self, id: &Uuid) -> Result<(), String> {
|
||||
@@ -119,43 +102,13 @@ impl WebClientHooks for TrackedWebClientHooks {
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id);
|
||||
let network_name = network_name_for_instance(id);
|
||||
if let Some(network_name) = &network_name {
|
||||
self.network_names_by_instance_id
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.insert(*id, network_name.clone());
|
||||
}
|
||||
self.events
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?
|
||||
.push(serde_json::json!({
|
||||
"event": "run_network_instance",
|
||||
"success": true,
|
||||
"instance_id": id.to_string(),
|
||||
"instance_name": id.to_string(),
|
||||
"network_name": network_name,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
let mut guard = self.instance_ids.lock().map_err(|err| err.to_string())?;
|
||||
let mut events = self.events.lock().map_err(|err| err.to_string())?;
|
||||
let mut network_names_by_instance_id = self
|
||||
.network_names_by_instance_id
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
for id in ids {
|
||||
guard.remove(id);
|
||||
let network_name = network_names_by_instance_id.remove(id);
|
||||
events.push(serde_json::json!({
|
||||
"event": "delete_network_instance",
|
||||
"success": true,
|
||||
"instance_id": id.to_string(),
|
||||
"instance_name": id.to_string(),
|
||||
"network_name": network_name,
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -198,8 +151,8 @@ fn stop_web_client(config_id: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
let ret = ASYNC_RUNTIME
|
||||
.block_on(INSTANCE_MANAGER.delete_network_instances(tracked_ids))
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(tracked_ids)
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!(
|
||||
@@ -218,7 +171,7 @@ fn ensure_local_socket_server_started() -> bool {
|
||||
}
|
||||
|
||||
fn maybe_stop_local_socket_server() {
|
||||
let no_local_instances = INSTANCE_MANAGER.instance_ids().is_empty();
|
||||
let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let no_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| guard.is_empty())
|
||||
@@ -229,7 +182,12 @@ fn maybe_stop_local_socket_server() {
|
||||
}
|
||||
|
||||
fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
if INSTANCE_MANAGER.instance_ids().iter().next().is_some() {
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.iter()
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
@@ -285,389 +243,6 @@ fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_config_server_client(
|
||||
url: &str,
|
||||
hostname: Option<String>,
|
||||
machine_id: Option<String>,
|
||||
secure_mode: bool,
|
||||
) -> bool {
|
||||
let trimmed_url = url.trim();
|
||||
if trimmed_url.is_empty() {
|
||||
ohrs_log_error!("[Rust] config server url missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
let _ = stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID);
|
||||
let hooks = Arc::new(TrackedWebClientHooks::default());
|
||||
|
||||
if !ensure_local_socket_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let machine_id_opts = MachineIdOptions {
|
||||
explicit_machine_id: machine_id.filter(|value| !value.trim().is_empty()),
|
||||
state_dir: None,
|
||||
};
|
||||
let client = ASYNC_RUNTIME.block_on(run_web_client(
|
||||
trimmed_url,
|
||||
machine_id_opts,
|
||||
hostname.filter(|value| !value.trim().is_empty()),
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
Some(hooks.clone()),
|
||||
));
|
||||
|
||||
let client = match client {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] start pro config server client failed {}", err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match WEB_CLIENTS.lock() {
|
||||
Ok(mut guard) => {
|
||||
guard.insert(
|
||||
PRO_CONFIG_SERVER_CLIENT_ID.to_string(),
|
||||
ManagedWebClient {
|
||||
_client: client,
|
||||
hooks,
|
||||
},
|
||||
);
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
ohrs_log_error!("[Rust] store pro config server client failed {}", err);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pro_config_server_client_connected() -> bool {
|
||||
WEB_CLIENTS
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| {
|
||||
guard
|
||||
.get(PRO_CONFIG_SERVER_CLIENT_ID)
|
||||
.map(|managed| managed._client.is_connected())
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn drain_config_server_events_inner() -> Vec<serde_json::Value> {
|
||||
let Ok(guard) = WEB_CLIENTS.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(managed) = guard.get(PRO_CONFIG_SERVER_CLIENT_ID) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(mut events) = managed.hooks.events.lock() else {
|
||||
return Vec::new();
|
||||
};
|
||||
events.drain(..).collect()
|
||||
}
|
||||
|
||||
fn stop_runtime_inner() -> bool {
|
||||
let mut ok = stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID);
|
||||
let ids = INSTANCE_MANAGER.instance_ids();
|
||||
if !ids.is_empty() {
|
||||
ok = ASYNC_RUNTIME
|
||||
.block_on(INSTANCE_MANAGER.delete_network_instances(ids))
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!("[Rust] stop runtime instances failed {}", err);
|
||||
false
|
||||
})
|
||||
&& ok;
|
||||
}
|
||||
maybe_stop_local_socket_server();
|
||||
let _ = nearby_management::stop_runtime_management_server();
|
||||
ok
|
||||
}
|
||||
|
||||
fn is_pro_internal_instance(instance: &RuntimeInstanceState) -> bool {
|
||||
instance.instance_id == PRO_CONFIG_SERVER_CLIENT_ID
|
||||
|| instance.config_id == PRO_CONFIG_SERVER_CLIENT_ID
|
||||
|| instance.display_name == PRO_CONFIG_SERVER_CLIENT_ID
|
||||
}
|
||||
|
||||
fn runtime_instance_label(instance: &RuntimeInstanceState) -> String {
|
||||
let display_name = instance.display_name.trim();
|
||||
if !display_name.is_empty() && display_name != PRO_CONFIG_SERVER_CLIENT_ID {
|
||||
return display_name.to_string();
|
||||
}
|
||||
let instance_id = instance.instance_id.trim();
|
||||
if !instance_id.is_empty() {
|
||||
return instance_id.to_string();
|
||||
}
|
||||
instance.config_id.clone()
|
||||
}
|
||||
|
||||
fn runtime_instance_matches(instance: &RuntimeInstanceState, selector: &str) -> bool {
|
||||
let target = selector.trim();
|
||||
if target.is_empty() {
|
||||
return false;
|
||||
}
|
||||
instance.instance_id == target
|
||||
|| instance.config_id == target
|
||||
|| instance.display_name == target
|
||||
|| runtime_instance_label(instance) == target
|
||||
}
|
||||
|
||||
fn read_json_string_path<'a>(value: &'a serde_json::Value, path: &[&str]) -> Option<&'a str> {
|
||||
let mut cursor = value;
|
||||
for key in path {
|
||||
cursor = cursor.get(*key)?;
|
||||
}
|
||||
cursor.as_str().filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn selected_instance_from_payload(payload_json: &str) -> Option<String> {
|
||||
let value = serde_json::from_str::<serde_json::Value>(payload_json).ok()?;
|
||||
for path in [
|
||||
&["instance", "instance_selector", "name"][..],
|
||||
&["instance", "instanceSelector", "name"][..],
|
||||
&["instance", "instance_selector", "id"][..],
|
||||
&["instance", "instanceSelector", "id"][..],
|
||||
&["instance", "name"][..],
|
||||
&["instance", "id"][..],
|
||||
&["instance_name"][..],
|
||||
&["instanceName"][..],
|
||||
&["instance_id"][..],
|
||||
&["instanceId"][..],
|
||||
&["id"][..],
|
||||
] {
|
||||
if let Some(value) = read_json_string_path(&value, path) {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_runtime_instance<'a>(
|
||||
state: &'a RuntimeAggregateState,
|
||||
selector: Option<&str>,
|
||||
) -> Option<&'a RuntimeInstanceState> {
|
||||
if let Some(selector) = selector
|
||||
&& let Some(instance) = state.instances.iter().find(|instance| {
|
||||
!is_pro_internal_instance(instance) && runtime_instance_matches(instance, selector)
|
||||
})
|
||||
{
|
||||
return Some(instance);
|
||||
}
|
||||
state
|
||||
.instances
|
||||
.iter()
|
||||
.find(|instance| !is_pro_internal_instance(instance) && instance.running)
|
||||
}
|
||||
|
||||
fn list_instances_json_inner(state: &RuntimeAggregateState) -> String {
|
||||
let mut instances = serde_json::Map::new();
|
||||
for instance in state
|
||||
.instances
|
||||
.iter()
|
||||
.filter(|instance| !is_pro_internal_instance(instance) && instance.running)
|
||||
{
|
||||
let label = runtime_instance_label(instance);
|
||||
if !label.trim().is_empty() {
|
||||
instances.insert(
|
||||
label,
|
||||
serde_json::Value::String(instance.instance_id.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(instances).to_string()
|
||||
}
|
||||
|
||||
fn list_pro_instances_json_inner(
|
||||
state: &RuntimeAggregateState,
|
||||
network_names_by_instance_id: &HashMap<String, String>,
|
||||
) -> String {
|
||||
let mut instances = serde_json::Map::new();
|
||||
for instance in state.instances.iter().filter(|instance| instance.running) {
|
||||
let Some(network_name) = network_names_by_instance_id.get(&instance.instance_id) else {
|
||||
continue;
|
||||
};
|
||||
let label = if network_name.trim().is_empty() {
|
||||
instance.instance_id.clone()
|
||||
} else {
|
||||
network_name.clone()
|
||||
};
|
||||
instances.insert(
|
||||
label,
|
||||
serde_json::Value::String(instance.instance_id.clone()),
|
||||
);
|
||||
}
|
||||
serde_json::Value::Object(instances).to_string()
|
||||
}
|
||||
|
||||
fn find_pro_runtime_instance<'a>(
|
||||
state: &'a RuntimeAggregateState,
|
||||
network_names_by_instance_id: &HashMap<String, String>,
|
||||
selector: Option<&str>,
|
||||
) -> Option<(&'a RuntimeInstanceState, String)> {
|
||||
let mut tracked_instances = state.instances.iter().filter_map(|instance| {
|
||||
let network_name = network_names_by_instance_id.get(&instance.instance_id)?;
|
||||
Some((instance, network_name))
|
||||
});
|
||||
if let Some(selector) = selector {
|
||||
return tracked_instances
|
||||
.filter(|(instance, _)| instance.running)
|
||||
.find(|(instance, network_name)| {
|
||||
selector == network_name.as_str() || runtime_instance_matches(instance, selector)
|
||||
})
|
||||
.map(|(instance, network_name)| (instance, network_name.clone()));
|
||||
}
|
||||
tracked_instances
|
||||
.find(|(instance, _)| instance.running)
|
||||
.map(|(instance, network_name)| (instance, network_name.clone()))
|
||||
}
|
||||
|
||||
fn call_pro_json_rpc_inner(
|
||||
state: &RuntimeAggregateState,
|
||||
network_names_by_instance_id: &HashMap<String, String>,
|
||||
service_name: &str,
|
||||
method_name: &str,
|
||||
payload_json: &str,
|
||||
) -> String {
|
||||
let selector = selected_instance_from_payload(payload_json);
|
||||
let Some((instance, network_name)) =
|
||||
find_pro_runtime_instance(state, network_names_by_instance_id, selector.as_deref())
|
||||
else {
|
||||
return "{}".to_string();
|
||||
};
|
||||
|
||||
let method = method_name.trim();
|
||||
let service = service_name.trim();
|
||||
let response = match (service, method) {
|
||||
(_, "show_node_info") => serde_json::json!({
|
||||
"node_info": instance.my_node_info,
|
||||
}),
|
||||
(_, "list_route") => serde_json::json!({
|
||||
"routes": instance.routes,
|
||||
}),
|
||||
(_, "list_peer") => serde_json::json!({
|
||||
"my_info": instance.my_node_info,
|
||||
"peer_infos": instance.peers,
|
||||
}),
|
||||
(_, "get_stats") => {
|
||||
let mut rx_bytes = 0_i64;
|
||||
let mut tx_bytes = 0_i64;
|
||||
for peer in &instance.peers {
|
||||
for conn in &peer.conns {
|
||||
if let Some(stats) = &conn.stats {
|
||||
rx_bytes = rx_bytes.saturating_add(stats.rx_bytes);
|
||||
tx_bytes = tx_bytes.saturating_add(stats.tx_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::json!({
|
||||
"metrics": [
|
||||
{
|
||||
"name": "traffic_bytes_self_rx",
|
||||
"labels": { "network_name": network_name },
|
||||
"value": rx_bytes,
|
||||
},
|
||||
{
|
||||
"name": "traffic_bytes_self_tx",
|
||||
"labels": { "network_name": network_name },
|
||||
"value": tx_bytes,
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
_ => serde_json::json!({}),
|
||||
};
|
||||
response.to_string()
|
||||
}
|
||||
|
||||
fn pro_runtime_registry_snapshot() -> HashMap<String, String> {
|
||||
WEB_CLIENTS
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|clients| {
|
||||
clients
|
||||
.get(PRO_CONFIG_SERVER_CLIENT_ID)
|
||||
.and_then(|managed| managed.hooks.network_names_by_instance_id.lock().ok())
|
||||
.map(|registry| {
|
||||
registry
|
||||
.iter()
|
||||
.map(|(instance_id, network_name)| {
|
||||
(instance_id.to_string(), network_name.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn call_json_rpc_inner(service_name: &str, method_name: &str, payload_json: &str) -> String {
|
||||
let state = collect_runtime_state_inner();
|
||||
let selector = selected_instance_from_payload(payload_json);
|
||||
let Some(instance) = find_runtime_instance(&state, selector.as_deref()) else {
|
||||
return "{}".to_string();
|
||||
};
|
||||
|
||||
let method = method_name.trim();
|
||||
let service = service_name.trim();
|
||||
let response = match (service, method) {
|
||||
(_, "show_node_info") => serde_json::json!({
|
||||
"node_info": instance.my_node_info,
|
||||
}),
|
||||
(_, "list_route") => serde_json::json!({
|
||||
"routes": instance.routes,
|
||||
}),
|
||||
(_, "list_peer") => serde_json::json!({
|
||||
"my_info": instance.my_node_info,
|
||||
"peer_infos": instance.peers,
|
||||
}),
|
||||
(_, "get_stats") => {
|
||||
let mut rx_bytes = 0_i64;
|
||||
let mut tx_bytes = 0_i64;
|
||||
for peer in &instance.peers {
|
||||
for conn in &peer.conns {
|
||||
if let Some(stats) = &conn.stats {
|
||||
rx_bytes = rx_bytes.saturating_add(stats.rx_bytes);
|
||||
tx_bytes = tx_bytes.saturating_add(stats.tx_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
let network_name = runtime_instance_label(instance);
|
||||
serde_json::json!({
|
||||
"metrics": [
|
||||
{
|
||||
"name": "traffic_bytes_self_rx",
|
||||
"labels": { "network_name": network_name },
|
||||
"value": rx_bytes,
|
||||
},
|
||||
{
|
||||
"name": "traffic_bytes_self_tx",
|
||||
"labels": { "network_name": network_name },
|
||||
"value": tx_bytes,
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
_ => serde_json::json!({}),
|
||||
};
|
||||
response.to_string()
|
||||
}
|
||||
|
||||
fn resolve_instance_id_from_state(
|
||||
state: &RuntimeAggregateState,
|
||||
instance_name: &str,
|
||||
) -> Option<String> {
|
||||
let instance = state.instances.iter().find(|instance| {
|
||||
!is_pro_internal_instance(instance) && runtime_instance_matches(instance, instance_name)
|
||||
})?;
|
||||
Some(instance.instance_id.clone())
|
||||
}
|
||||
|
||||
fn resolve_instance_id_inner(instance_name: &str) -> Option<String> {
|
||||
resolve_instance_id_from_state(&collect_runtime_state_inner(), instance_name)
|
||||
}
|
||||
|
||||
pub(crate) fn build_default_network_config_json() -> Result<String, String> {
|
||||
let config = NetworkConfig::new_from_config(TomlConfigLoader::default())
|
||||
.map_err(|e| format!("default_network_config failed {}", e))?;
|
||||
@@ -718,7 +293,7 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
}
|
||||
};
|
||||
|
||||
if !INSTANCE_MANAGER.instance_ids().is_empty() {
|
||||
if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() {
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
@@ -728,17 +303,15 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
}
|
||||
|
||||
let inst_id = cfg.get_id();
|
||||
if INSTANCE_MANAGER.instance_ids().contains(&inst_id) {
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
ohrs_log_error!("[Rust] instance {} already exists", inst_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
let config_control = nearby_management::runtime_management_config_control(inst_id);
|
||||
if !nearby_management::ensure_runtime_management_server_started() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, config_control) {
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
Ok(_) => {
|
||||
cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config);
|
||||
true
|
||||
@@ -870,78 +443,6 @@ pub fn stop_network_instance(config_ids: Vec<String>) -> bool {
|
||||
exports::runtime_api::stop_network_instance(config_ids, stop_kernel)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn start_config_server_client(
|
||||
url: String,
|
||||
hostname: Option<String>,
|
||||
machine_id: Option<String>,
|
||||
secure_mode: Option<bool>,
|
||||
) -> bool {
|
||||
run_config_server_client(&url, hostname, machine_id, secure_mode.unwrap_or(false))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_config_server_client() -> bool {
|
||||
stop_web_client(PRO_CONFIG_SERVER_CLIENT_ID)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn is_config_server_client_connected() -> bool {
|
||||
pro_config_server_client_connected()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_runtime() -> bool {
|
||||
stop_runtime_inner()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn drain_config_server_events() -> String {
|
||||
serde_json::to_string(&drain_config_server_events_inner()).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_runtime_state_json() -> String {
|
||||
serde_json::to_string(&collect_runtime_state_inner()).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_instances_json() -> String {
|
||||
list_instances_json_inner(&collect_runtime_state_inner())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn list_pro_instances_json() -> String {
|
||||
let registry = pro_runtime_registry_snapshot();
|
||||
list_pro_instances_json_inner(&collect_runtime_state_inner(), ®istry)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn call_json_rpc(service_name: String, method_name: String, payload_json: String) -> String {
|
||||
call_json_rpc_inner(&service_name, &method_name, &payload_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn call_pro_json_rpc(
|
||||
service_name: String,
|
||||
method_name: String,
|
||||
payload_json: String,
|
||||
) -> String {
|
||||
let registry = pro_runtime_registry_snapshot();
|
||||
call_pro_json_rpc_inner(
|
||||
&collect_runtime_state_inner(),
|
||||
®istry,
|
||||
&service_name,
|
||||
&method_name,
|
||||
&payload_json,
|
||||
)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn resolve_instance_id(instance_name: String) -> Option<String> {
|
||||
resolve_instance_id_inner(&instance_name)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn easytier_version() -> String {
|
||||
EASYTIER_VERSION.to_string()
|
||||
@@ -967,92 +468,6 @@ pub fn run_network_instance(cfg_json: String) -> bool {
|
||||
run_network_instance_from_json(&cfg_json)
|
||||
}
|
||||
|
||||
/// Starts the management server in the VPN Extension process even when no
|
||||
/// network instance is active, allowing a nearby controller to deploy a
|
||||
/// one-shot config through the canonical Core RPC surface.
|
||||
#[napi]
|
||||
pub fn start_nearby_management_host() -> bool {
|
||||
nearby_management::ensure_runtime_management_server_started()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn stop_nearby_management_host() -> bool {
|
||||
nearby_management::stop_runtime_management_server()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn drain_nearby_host_commands() -> Vec<nearby_management::NearbyHostCommand> {
|
||||
nearby_management::drain_nearby_host_commands()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn complete_nearby_host_command(
|
||||
request_id: String,
|
||||
success: bool,
|
||||
error: Option<String>,
|
||||
) -> bool {
|
||||
nearby_management::complete_nearby_host_command(request_id, success, error)
|
||||
}
|
||||
|
||||
/// Returns 1 for canonical Core RPC packets, 2 for the OHOS-private settings
|
||||
/// envelope, and 0 for malformed or unsupported data.
|
||||
#[napi]
|
||||
pub fn nearby_management_packet_kind(packet: Uint8Array) -> i32 {
|
||||
nearby_management::nearby_management_packet_kind(packet)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn encode_nearby_ohos_packet(envelope_json: String) -> Option<Uint8Array> {
|
||||
nearby_management::encode_nearby_ohos_packet(envelope_json)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn decode_nearby_ohos_packet(packet: Uint8Array) -> Option<String> {
|
||||
nearby_management::decode_nearby_ohos_packet(packet)
|
||||
}
|
||||
|
||||
/// Opens one Core RPC endpoint for a HarmonyOS collaboration session.
|
||||
///
|
||||
/// The Harmony layer transports the returned native packets verbatim with
|
||||
/// `abilityConnectionManager.sendData`; all RPC framing stays inside Core.
|
||||
#[napi]
|
||||
pub fn open_nearby_management_session(session_key: String, host: bool) -> bool {
|
||||
nearby_management::open_nearby_management_session(session_key, host)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn close_nearby_management_session(session_key: String) -> bool {
|
||||
nearby_management::close_nearby_management_session(session_key)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn push_nearby_management_packet(session_key: String, packet: Uint8Array) -> bool {
|
||||
nearby_management::push_nearby_management_packet(session_key, packet)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn drain_nearby_management_packets(session_key: String) -> Vec<Uint8Array> {
|
||||
nearby_management::drain_nearby_management_packets(session_key)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn call_nearby_management_json_rpc(
|
||||
session_key: String,
|
||||
service_name: String,
|
||||
method_name: String,
|
||||
domain_name: Option<String>,
|
||||
payload_json: String,
|
||||
) -> String {
|
||||
nearby_management::call_nearby_management_json_rpc(
|
||||
session_key,
|
||||
service_name,
|
||||
method_name,
|
||||
domain_name,
|
||||
payload_json,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn collect_network_infos() -> Vec<KeyValuePair> {
|
||||
exports::runtime_api::collect_network_infos()
|
||||
@@ -1100,96 +515,20 @@ mod tests {
|
||||
.any(|field| field.name == "enabled")
|
||||
);
|
||||
}
|
||||
|
||||
fn pro_test_state() -> RuntimeAggregateState {
|
||||
RuntimeAggregateState {
|
||||
instances: vec![
|
||||
RuntimeInstanceState {
|
||||
config_id: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
|
||||
instance_id: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
|
||||
display_name: "0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
|
||||
running: true,
|
||||
tun_required: false,
|
||||
tun_attached: false,
|
||||
magic_dns_enabled: false,
|
||||
need_exit_node: false,
|
||||
error_message: None,
|
||||
my_node_info: None,
|
||||
events: vec![],
|
||||
routes: vec![],
|
||||
peers: vec![],
|
||||
},
|
||||
RuntimeInstanceState {
|
||||
config_id: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
|
||||
instance_id: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
|
||||
display_name: "ec7b6a3c-aeae-4c0e-844e-f7ec2dbdc2ce".to_string(),
|
||||
running: true,
|
||||
tun_required: false,
|
||||
tun_attached: false,
|
||||
magic_dns_enabled: false,
|
||||
need_exit_node: false,
|
||||
error_message: None,
|
||||
my_node_info: None,
|
||||
events: vec![],
|
||||
routes: vec![],
|
||||
peers: vec![],
|
||||
},
|
||||
],
|
||||
tun: runtime::state::runtime_state::TunAggregateState {
|
||||
active: false,
|
||||
attached_instance_ids: vec![],
|
||||
aggregated_routes: vec![],
|
||||
dns_servers: vec![],
|
||||
need_rebuild: false,
|
||||
},
|
||||
running_instance_count: 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn pro_test_registry() -> HashMap<String, String> {
|
||||
HashMap::from([(
|
||||
"0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string(),
|
||||
"office-network".to_string(),
|
||||
)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_instance_list_uses_registry_network_name_and_excludes_untracked_instances() {
|
||||
assert_eq!(
|
||||
list_pro_instances_json_inner(&pro_test_state(), &pro_test_registry()),
|
||||
r#"{"office-network":"0c4b33ba-4ed5-42d8-9095-21b786c66e94"}"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_json_rpc_selects_instance_by_network_name_and_labels_traffic() {
|
||||
let response = call_pro_json_rpc_inner(
|
||||
&pro_test_state(),
|
||||
&pro_test_registry(),
|
||||
"api.instance.StatsRpcService",
|
||||
"get_stats",
|
||||
r#"{"instance":{"instance_selector":{"name":"office-network"}}}"#,
|
||||
);
|
||||
let response: serde_json::Value = serde_json::from_str(&response).unwrap();
|
||||
assert_eq!(
|
||||
response["metrics"][0]["labels"]["network_name"],
|
||||
"office-network",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_instance_id_does_not_fall_back_for_unknown_selector() {
|
||||
let state = pro_test_state();
|
||||
assert_eq!(
|
||||
resolve_instance_id_from_state(&state, "0c4b33ba-4ed5-42d8-9095-21b786c66e94"),
|
||||
Some("0c4b33ba-4ed5-42d8-9095-21b786c66e94".to_string()),
|
||||
);
|
||||
assert_eq!(resolve_instance_id_from_state(&state, "stale-name"), None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_runtime_state_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::collect_runtime_state()
|
||||
#[napi]
|
||||
pub fn get_runtime_snapshot() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn set_kernel_snapshot_enabled(enabled: bool) {
|
||||
set_snapshot_broadcast_enabled(enabled);
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_inner() -> RuntimeAggregateState {
|
||||
exports::runtime_api::get_runtime_snapshot_inner()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -324,7 +324,7 @@ fn route_to_view(route: api::instance::Route) -> RouteView {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||
fn peer_conn_to_view(conn: api::instance::PeerConnInfo) -> PeerConnInfo {
|
||||
let stats = conn.stats.map(|stats| PeerConnStats {
|
||||
rx_bytes: stats.rx_bytes as i64,
|
||||
tx_bytes: stats.tx_bytes as i64,
|
||||
|
||||
@@ -10,8 +10,9 @@ use easytier::{
|
||||
common::config::{
|
||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
},
|
||||
instance::factory::{NativeInstanceManager, native_instance_manager},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
};
|
||||
use guarden::defer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
@@ -27,32 +28,6 @@ pub struct HealthCheckOneNode {
|
||||
node_id: String,
|
||||
}
|
||||
|
||||
struct InstanceCleanupGuard {
|
||||
manager: Arc<NativeInstanceManager>,
|
||||
instance_id: Option<uuid::Uuid>,
|
||||
runtime: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
impl InstanceCleanupGuard {
|
||||
async fn cleanup(mut self) {
|
||||
let instance_id = self.instance_id.unwrap();
|
||||
let _ = self.manager.delete_network_instances([instance_id]).await;
|
||||
self.instance_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InstanceCleanupGuard {
|
||||
fn drop(&mut self) {
|
||||
let Some(instance_id) = self.instance_id.take() else {
|
||||
return;
|
||||
};
|
||||
let manager = self.manager.clone();
|
||||
self.runtime.spawn(async move {
|
||||
let _ = manager.delete_network_instances([instance_id]).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const HEALTH_CHECK_RING_GRANULARITY_SEC: usize = 60 * 15; // 15分钟
|
||||
const HEALTH_CHECK_RING_MAX_DURATION_SEC: usize = 60 * 60 * 24; // 最多一天
|
||||
|
||||
@@ -263,7 +238,7 @@ impl HealthyMemRecord {
|
||||
|
||||
pub struct HealthChecker {
|
||||
db: Db,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
inst_id_map: DashMap<i32, uuid::Uuid>,
|
||||
node_tasks: DashMap<i32, AbortOnDropHandle<()>>,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
@@ -272,7 +247,7 @@ pub struct HealthChecker {
|
||||
|
||||
impl HealthChecker {
|
||||
pub fn new(db: Db) -> Self {
|
||||
let instance_mgr = Arc::new(native_instance_manager());
|
||||
let instance_mgr = Arc::new(NetworkInstanceManager::new());
|
||||
Self {
|
||||
db,
|
||||
instance_mgr,
|
||||
@@ -412,38 +387,33 @@ impl HealthChecker {
|
||||
max_time: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let cfg = self.get_node_cfg_with_model(node_info, None).await?;
|
||||
defer!({
|
||||
let _ = self
|
||||
.instance_mgr
|
||||
.delete_network_instance(vec![cfg.get_id()]);
|
||||
});
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
|
||||
.run_network_instance(cfg.clone(), false, ConfigFileControl::STATIC_CONFIG)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
let cleanup = InstanceCleanupGuard {
|
||||
manager: self.instance_mgr.clone(),
|
||||
instance_id: Some(cfg.get_id()),
|
||||
runtime: tokio::runtime::Handle::current(),
|
||||
};
|
||||
|
||||
let result = async {
|
||||
let now = Instant::now();
|
||||
let mut err = None;
|
||||
while now.elapsed() < max_time {
|
||||
match Self::test_node_healthy(cfg.get_id(), self.instance_mgr.clone()).await {
|
||||
Ok(_) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"test node healthy failed, node_info: {:?}, err: {}",
|
||||
node_info, e
|
||||
);
|
||||
err = Some(e);
|
||||
}
|
||||
let now = Instant::now();
|
||||
let mut err = None;
|
||||
while now.elapsed() < max_time {
|
||||
match Self::test_node_healthy(cfg.get_id(), self.instance_mgr.clone()).await {
|
||||
Ok(_) => {
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"test node healthy failed, node_info: {:?}, err: {}",
|
||||
node_info, e
|
||||
);
|
||||
err = Some(e);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
Err(anyhow::anyhow!("test node healthy failed, err: {:?}", err))
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
.await;
|
||||
cleanup.cleanup().await;
|
||||
result
|
||||
Err(anyhow::anyhow!("test node healthy failed, err: {:?}", err))
|
||||
}
|
||||
|
||||
async fn get_node_cfg(
|
||||
@@ -467,7 +437,7 @@ impl HealthChecker {
|
||||
);
|
||||
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
|
||||
.run_network_instance(cfg.clone(), true, ConfigFileControl::STATIC_CONFIG)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
self.inst_id_map.insert(node_id, cfg.get_id());
|
||||
|
||||
@@ -511,10 +481,7 @@ impl HealthChecker {
|
||||
pub async fn remove_node(&self, node_id: i32) -> anyhow::Result<()> {
|
||||
self.node_tasks.remove(&node_id);
|
||||
if let Some(inst_id) = self.inst_id_map.remove(&node_id) {
|
||||
let _ = self
|
||||
.instance_mgr
|
||||
.delete_network_instances([inst_id.1])
|
||||
.await;
|
||||
let _ = self.instance_mgr.delete_network_instance(vec![inst_id.1]);
|
||||
}
|
||||
self.node_cfg.remove(&node_id);
|
||||
// 保留内存记录,不删除,以便后续查询历史数据
|
||||
@@ -528,10 +495,10 @@ impl HealthChecker {
|
||||
#[instrument(err, ret, skip(instance_mgr))]
|
||||
async fn test_node_healthy(
|
||||
inst_id: uuid::Uuid,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
// return version, response time on healthy, conn_count
|
||||
) -> anyhow::Result<(String, u64, u32)> {
|
||||
let Some(instance) = instance_mgr.network_info(inst_id).await else {
|
||||
let Some(instance) = instance_mgr.get_network_info(&inst_id).await else {
|
||||
anyhow::bail!("healthy check node is not started");
|
||||
};
|
||||
|
||||
@@ -599,7 +566,7 @@ impl HealthChecker {
|
||||
async fn node_health_check_task(
|
||||
node_id: i32,
|
||||
inst_id: uuid::Uuid,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
db: Db,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
) {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
[package]
|
||||
name = "easytier-core"
|
||||
description = "EasyTier OS-free control-plane core primitives."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.6.4"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors = ["kkrainbow"]
|
||||
keywords = ["vpn", "p2p", "network", "easytier"]
|
||||
categories = ["network-programming"]
|
||||
license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
ariadne = { version = "0.5", optional = true }
|
||||
arc-swap = "1.7"
|
||||
async-ringbuf = "0.3.1"
|
||||
async-trait = "0.1.74"
|
||||
auto_impl = "1.1.0"
|
||||
base64 = "0.22"
|
||||
bitflags = "2.5"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.5.0"
|
||||
chrono = { version = "0.4.37", features = ["clock"] }
|
||||
cidr = { version = "0.3.1", features = ["serde"] }
|
||||
crossbeam = "0.8.4"
|
||||
dashmap = "6.0"
|
||||
derive_builder = "0.20.2"
|
||||
easytier-proto = { path = "../easytier-proto", default-features = false, features = ["core"] }
|
||||
futures = "0.3"
|
||||
guarden = "0.2"
|
||||
hmac = "0.12.1"
|
||||
http-body-util = { version = "0.1", optional = true }
|
||||
hyper = { version = "1", default-features = false, features = ["client", "http1"], optional = true }
|
||||
hyper-util = { version = "0.1", default-features = false, features = ["tokio"], optional = true }
|
||||
idna = "1.0"
|
||||
atomic-shim = "0.2.0"
|
||||
ordered_hash_map = "0.5.0"
|
||||
parking_lot = "0.12.1"
|
||||
percent-encoding = "2.3.1"
|
||||
petgraph = "0.8.1"
|
||||
pin-project-lite = "0.2.13"
|
||||
prefix-trie = { version = "0.7.0", features = ["cidr"] }
|
||||
prost = "0.14.3"
|
||||
prost-types = "0.14.3"
|
||||
rand = "0.8.5"
|
||||
quanta = "0.12"
|
||||
ring = { version = "0.17", optional = true }
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"], optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10.8"
|
||||
smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp.git", rev = "0a926767a68bc88d5512afefa7529c5ecdade4ea", optional = true, default-features = false }
|
||||
stun_codec = "0.3.4"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
toml = "0.8.12"
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"rt",
|
||||
"time",
|
||||
"sync",
|
||||
"macros",
|
||||
"io-util",
|
||||
] }
|
||||
tokio-util = { version = "0.7", features = ["io", "rt"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
||||
url = { version = "2.5", features = ["serde"] }
|
||||
wildmatch = "2.3.4"
|
||||
uuid = { version = "1.5.0", features = ["v4", "fast-rng", "serde"] }
|
||||
webpki-roots = { version = "0.26", optional = true }
|
||||
x25519-dalek = { version = "2.0", features = ["static_secrets"] }
|
||||
zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
|
||||
zstd = { version = "0.13", optional = true }
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
chacha20poly1305 = { version = "0.10.1", optional = true }
|
||||
openssl = { version = "0.10", optional = true, features = ["vendored"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
getrandom-02 = { package = "getrandom", version = "0.2.15", features = ["js"] }
|
||||
getrandom-03 = { package = "getrandom", version = "0.3.2", features = ["wasm_js"] }
|
||||
snow = { version = "0.10.0", default-features = false, features = ["default-resolver", "default-resolver-crypto"] }
|
||||
uuid = { version = "1.5.0", features = ["js"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dependencies]
|
||||
snow = "0.10.0"
|
||||
|
||||
[features]
|
||||
default = ["aes-gcm", "endpoint-discovery", "extended-services", "management", "tcp-hole-punch"]
|
||||
aes-gcm = ["dep:aes-gcm"]
|
||||
browser-config = ["config-write", "easytier-proto/api", "easytier-proto/json-rpc"]
|
||||
chacha20 = ["dep:chacha20poly1305"]
|
||||
openssl-crypto = ["dep:openssl"]
|
||||
ring-crypto = ["dep:ring"]
|
||||
wasi-crypto-offload = ["ring-crypto"]
|
||||
config-write = []
|
||||
endpoint-discovery = [
|
||||
"dep:http-body-util",
|
||||
"dep:hyper",
|
||||
"dep:hyper-util",
|
||||
"dep:rustls",
|
||||
"dep:tokio-rustls",
|
||||
"dep:webpki-roots",
|
||||
]
|
||||
dhcp-ipv4 = []
|
||||
public-ipv6-provider = []
|
||||
vpn-portal = []
|
||||
wrapped-transport = []
|
||||
extended-services = [
|
||||
"dhcp-ipv4",
|
||||
"public-ipv6-provider",
|
||||
"vpn-portal",
|
||||
"wrapped-transport",
|
||||
"proxy-cidr-monitor",
|
||||
]
|
||||
web-client = ["management-rpc", "config-write"]
|
||||
management = ["web-client", "extended-services", "rich-config-errors", "easytier-proto/json-rpc"]
|
||||
management-rpc = ["easytier-proto/api"]
|
||||
proxy-cidr-monitor = []
|
||||
rich-config-errors = ["dep:ariadne"]
|
||||
tcp-hole-punch = []
|
||||
proxy-packet = [
|
||||
"wrapped-transport",
|
||||
"dep:smoltcp",
|
||||
"smoltcp/std",
|
||||
"smoltcp/proto-ipv4",
|
||||
"smoltcp/proto-ipv4-fragmentation",
|
||||
"smoltcp/fragmentation-buffer-size-65536",
|
||||
"smoltcp/assembler-max-segment-count-16",
|
||||
"smoltcp/reassembly-buffer-size-65536",
|
||||
"smoltcp/reassembly-buffer-count-16",
|
||||
]
|
||||
proxy-smoltcp-stack = [
|
||||
"proxy-packet",
|
||||
"smoltcp/medium-ip",
|
||||
"smoltcp/socket-tcp",
|
||||
"smoltcp/socket-udp",
|
||||
"smoltcp/proto-ipv6",
|
||||
"smoltcp/async",
|
||||
]
|
||||
test-utils = []
|
||||
tracing-log = ["tracing/log"]
|
||||
zstd = ["dep:zstd"]
|
||||
|
||||
[target.'cfg(not(target_os = "wasi"))'.dev-dependencies]
|
||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }
|
||||
@@ -1,166 +0,0 @@
|
||||
//! Portable conversion between the shared TOML model and management schema.
|
||||
|
||||
use easytier_proto::api::manage::{
|
||||
self, NetworkConfig, NetworkingMethod, PortForwardConfig as ApiPortForwardConfig,
|
||||
};
|
||||
|
||||
use super::toml::{ConfigLoader as _, TomlConfig};
|
||||
|
||||
pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
|
||||
let default_config = TomlConfig::default();
|
||||
let mut result = NetworkConfig {
|
||||
instance_id: Some(config.get_id().to_string()),
|
||||
dhcp: Some(config.get_dhcp()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if config.get_hostname() != default_config.get_hostname() {
|
||||
result.hostname = Some(config.get_hostname());
|
||||
}
|
||||
|
||||
let network_identity = config.get_network_identity();
|
||||
result.network_name = Some(network_identity.network_name);
|
||||
result.network_secret = network_identity.network_secret;
|
||||
|
||||
if let Some(ipv4) = config.get_ipv4() {
|
||||
result.virtual_ipv4 = Some(ipv4.address().to_string());
|
||||
result.network_length = Some(ipv4.network_length() as i32);
|
||||
}
|
||||
|
||||
if config.get_ipv6_public_addr_provider() != default_config.get_ipv6_public_addr_provider() {
|
||||
result.ipv6_public_addr_provider = Some(config.get_ipv6_public_addr_provider());
|
||||
}
|
||||
if config.get_ipv6_public_addr_auto() != default_config.get_ipv6_public_addr_auto() {
|
||||
result.ipv6_public_addr_auto = Some(config.get_ipv6_public_addr_auto());
|
||||
}
|
||||
result.ipv6_public_addr_prefix = config
|
||||
.get_ipv6_public_addr_prefix()
|
||||
.map(|prefix| prefix.to_string());
|
||||
|
||||
let peers = config.get_peers();
|
||||
result.networking_method = Some(NetworkingMethod::Manual as i32);
|
||||
if !peers.is_empty() {
|
||||
result.peer_urls = peers.iter().map(|peer| peer.uri.to_string()).collect();
|
||||
result.peers = peers
|
||||
.iter()
|
||||
.map(|peer| manage::NetworkPeerConfig {
|
||||
uri: peer.uri.to_string(),
|
||||
peer_public_key: peer.peer_public_key.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
result.listener_urls = config
|
||||
.get_listeners()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
result.proxy_cidrs = config
|
||||
.get_proxy_cidrs()
|
||||
.iter()
|
||||
.map(|proxy| match proxy.mapped_cidr {
|
||||
Some(mapped) => format!("{}->{}", proxy.cidr, mapped),
|
||||
None => proxy.cidr.to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let port_forwards = config.get_port_forwards();
|
||||
if !port_forwards.is_empty() {
|
||||
result.port_forwards = port_forwards
|
||||
.iter()
|
||||
.map(|forward| ApiPortForwardConfig {
|
||||
proto: forward.proto.clone(),
|
||||
bind_ip: forward.bind_addr.ip().to_string(),
|
||||
bind_port: forward.bind_addr.port() as u32,
|
||||
dst_ip: forward.dst_addr.ip().to_string(),
|
||||
dst_port: forward.dst_addr.port() as u32,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(vpn_config) = config.get_vpn_portal_config() {
|
||||
result.enable_vpn_portal = Some(true);
|
||||
result.vpn_portal_client_network_addr =
|
||||
Some(vpn_config.client_cidr.first_address().to_string());
|
||||
result.vpn_portal_client_network_len = Some(vpn_config.client_cidr.network_length() as i32);
|
||||
result.vpn_portal_listen_port = Some(vpn_config.wireguard_listen.port() as i32);
|
||||
}
|
||||
|
||||
if let Some(routes) = config.get_routes()
|
||||
&& !routes.is_empty()
|
||||
{
|
||||
result.enable_manual_routes = Some(true);
|
||||
result.routes = routes.iter().map(ToString::to_string).collect();
|
||||
}
|
||||
let exit_nodes = config.get_exit_nodes();
|
||||
if !exit_nodes.is_empty() {
|
||||
result.exit_nodes = exit_nodes.iter().map(ToString::to_string).collect();
|
||||
}
|
||||
if let Some(socks5_portal) = config.get_socks5_portal() {
|
||||
result.enable_socks5 = Some(true);
|
||||
result.socks5_port = socks5_portal.port().map(|port| port as i32);
|
||||
}
|
||||
let mapped_listeners = config.get_mapped_listeners();
|
||||
if !mapped_listeners.is_empty() {
|
||||
result.mapped_listeners = mapped_listeners.iter().map(ToString::to_string).collect();
|
||||
}
|
||||
|
||||
result.secure_mode = config.get_secure_mode();
|
||||
result.credential_file = config
|
||||
.get_credential_file()
|
||||
.map(|path| path.to_string_lossy().into_owned());
|
||||
|
||||
let flags = config.get_flags();
|
||||
let default_flags = default_config.get_flags();
|
||||
result.latency_first = Some(flags.latency_first);
|
||||
result.dev_name = Some(flags.dev_name.clone());
|
||||
result.use_smoltcp = Some(flags.use_smoltcp);
|
||||
result.disable_ipv6 = Some(!flags.enable_ipv6);
|
||||
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
|
||||
result.disable_kcp_input = Some(flags.disable_kcp_input);
|
||||
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
|
||||
result.disable_quic_input = Some(flags.disable_quic_input);
|
||||
result.disable_p2p = Some(flags.disable_p2p);
|
||||
result.p2p_only = Some(flags.p2p_only);
|
||||
result.lazy_p2p = Some(flags.lazy_p2p);
|
||||
result.bind_device = Some(flags.bind_device);
|
||||
result.socket_mark = flags.socket_mark;
|
||||
result.no_tun = Some(flags.no_tun);
|
||||
result.enable_exit_node = Some(flags.enable_exit_node);
|
||||
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
|
||||
result.need_p2p = Some(flags.need_p2p);
|
||||
result.multi_thread = Some(flags.multi_thread);
|
||||
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
|
||||
result.disable_encryption = Some(!flags.enable_encryption);
|
||||
result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching);
|
||||
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||
result.disable_upnp = Some(flags.disable_upnp);
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
result.mtu = Some(flags.mtu as i32);
|
||||
result.data_compress_algo = (flags.data_compress_algo != default_flags.data_compress_algo)
|
||||
.then_some(flags.data_compress_algo);
|
||||
result.encryption_algorithm = (flags.encryption_algorithm
|
||||
!= default_flags.encryption_algorithm)
|
||||
.then_some(flags.encryption_algorithm);
|
||||
result.instance_recv_bps_limit =
|
||||
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
||||
result.enable_private_mode = Some(flags.private_mode);
|
||||
result.acl = config.get_acl();
|
||||
|
||||
if flags.relay_network_whitelist == "*" {
|
||||
result.enable_relay_network_whitelist = Some(false);
|
||||
} else {
|
||||
result.enable_relay_network_whitelist = Some(true);
|
||||
result.relay_network_whitelist = flags
|
||||
.relay_network_whitelist
|
||||
.split_whitespace()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -1,652 +0,0 @@
|
||||
//! Conversion between the management NetworkConfig schema and shared TOML.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::Context;
|
||||
use easytier_proto::api::manage;
|
||||
|
||||
use crate::config::{
|
||||
MappedListenerPolicy, normalize_secure_mode_config,
|
||||
toml::{
|
||||
ConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader,
|
||||
VpnPortalConfig, gen_default_flags,
|
||||
},
|
||||
};
|
||||
|
||||
fn parse_mapped_listener_urls(mapped_listeners: &[String]) -> Result<Vec<url::Url>, anyhow::Error> {
|
||||
MappedListenerPolicy::new(["tcp", "udp", "wg", "quic", "ws", "wss", "faketcp"])
|
||||
.parse_urls(mapped_listeners)
|
||||
}
|
||||
|
||||
pub fn add_proxy_network_to_config(
|
||||
proxy_network: &str,
|
||||
cfg: &TomlConfigLoader,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let parts: Vec<&str> = proxy_network.split("->").collect();
|
||||
let real_cidr = parts[0]
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse proxy network: {}", parts[0]))?;
|
||||
|
||||
if parts.len() > 2 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"invalid proxy network format: {}, support format: <real_cidr> or <real_cidr>-><mapped_cidr>, example:
|
||||
10.0.0.0/24 or 10.0.0.0/24->192.168.0.0/24",
|
||||
proxy_network
|
||||
));
|
||||
}
|
||||
|
||||
let mapped_cidr = if parts.len() == 2 {
|
||||
Some(
|
||||
parts[1]
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse mapped network: {}", parts[1]))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
cfg.add_proxy_cidr(real_cidr, mapped_cidr)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub type NetworkingMethod = easytier_proto::api::manage::NetworkingMethod;
|
||||
pub type NetworkConfig = easytier_proto::api::manage::NetworkConfig;
|
||||
|
||||
pub trait NetworkConfigExt {
|
||||
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error>;
|
||||
fn new_from_config(config: impl ConfigLoader) -> Result<NetworkConfig, anyhow::Error>;
|
||||
}
|
||||
|
||||
fn parse_peer(peer: &manage::NetworkPeerConfig) -> Result<Option<PeerConfig>, anyhow::Error> {
|
||||
let uri = peer.uri.trim();
|
||||
if uri.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(PeerConfig {
|
||||
uri: uri
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse peer uri: {}", uri))?,
|
||||
peer_public_key: peer.peer_public_key.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_peers(peers: &[manage::NetworkPeerConfig]) -> Result<Vec<PeerConfig>, anyhow::Error> {
|
||||
let mut ret = Vec::new();
|
||||
for peer in peers {
|
||||
if let Some(peer) = parse_peer(peer)? {
|
||||
ret.push(peer);
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn parse_peer_urls(peer_urls: &[String]) -> Result<Vec<PeerConfig>, anyhow::Error> {
|
||||
let mut peers = vec![];
|
||||
for peer_url in peer_urls.iter() {
|
||||
let peer_url = peer_url.trim();
|
||||
if peer_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
peers.push(PeerConfig {
|
||||
uri: peer_url
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
|
||||
peer_public_key: None,
|
||||
});
|
||||
}
|
||||
Ok(peers)
|
||||
}
|
||||
|
||||
impl NetworkConfigExt for NetworkConfig {
|
||||
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(
|
||||
self.instance_id
|
||||
.clone()
|
||||
.unwrap_or(uuid::Uuid::new_v4().to_string())
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse instance id: {:?}", self.instance_id))?,
|
||||
);
|
||||
cfg.set_hostname(self.hostname.clone());
|
||||
cfg.set_dhcp(self.dhcp.unwrap_or_default());
|
||||
cfg.set_inst_name(self.network_name.clone().unwrap_or_default());
|
||||
|
||||
// The web UI does not expose credential inputs directly, but imported/saved
|
||||
// NetworkConfig objects still need to preserve credential-mode instances via
|
||||
// secure_mode.local_private_key + empty network_secret.
|
||||
let credential_secret = if self.network_secret.is_some() {
|
||||
None
|
||||
} else {
|
||||
self.secure_mode
|
||||
.as_ref()
|
||||
.and_then(|mode| mode.local_private_key.clone())
|
||||
.filter(|s| !s.is_empty())
|
||||
};
|
||||
|
||||
if credential_secret.is_some() {
|
||||
cfg.set_network_identity(NetworkIdentity::new_credential(
|
||||
self.network_name.clone().unwrap_or_default(),
|
||||
));
|
||||
} else {
|
||||
cfg.set_network_identity(NetworkIdentity::new(
|
||||
self.network_name.clone().unwrap_or_default(),
|
||||
self.network_secret.clone().unwrap_or_default(),
|
||||
));
|
||||
}
|
||||
|
||||
if !cfg.get_dhcp() {
|
||||
let virtual_ipv4 = self.virtual_ipv4.clone().unwrap_or_default();
|
||||
if !virtual_ipv4.is_empty() {
|
||||
let ip = format!("{}/{}", virtual_ipv4, self.network_length.unwrap_or(24))
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse ipv4 inet address: {}, {:?}",
|
||||
virtual_ipv4, self.network_length
|
||||
)
|
||||
})?;
|
||||
cfg.set_ipv4(Some(ip));
|
||||
}
|
||||
}
|
||||
|
||||
match NetworkingMethod::try_from(self.networking_method.unwrap_or_default())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
NetworkingMethod::PublicServer => {
|
||||
let peers = parse_peers(&self.peers)?;
|
||||
if peers.is_empty() {
|
||||
let public_server_url = self.public_server_url.clone().unwrap_or_default();
|
||||
cfg.set_peers(vec![PeerConfig {
|
||||
uri: public_server_url.parse().with_context(|| {
|
||||
format!("failed to parse public server uri: {}", public_server_url)
|
||||
})?,
|
||||
peer_public_key: None,
|
||||
}]);
|
||||
} else {
|
||||
cfg.set_peers(peers);
|
||||
}
|
||||
}
|
||||
NetworkingMethod::Manual => {
|
||||
let mut peers = parse_peers(&self.peers)?;
|
||||
if peers.is_empty() {
|
||||
peers = parse_peer_urls(&self.peer_urls)?;
|
||||
}
|
||||
if !peers.is_empty() {
|
||||
cfg.set_peers(peers);
|
||||
}
|
||||
}
|
||||
NetworkingMethod::Standalone => {}
|
||||
}
|
||||
|
||||
let mut listener_urls = vec![];
|
||||
for listener_url in self.listener_urls.iter() {
|
||||
if listener_url.is_empty() {
|
||||
continue;
|
||||
}
|
||||
listener_urls.push(
|
||||
listener_url
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
|
||||
);
|
||||
}
|
||||
cfg.set_listeners(listener_urls);
|
||||
|
||||
for n in self.proxy_cidrs.iter() {
|
||||
add_proxy_network_to_config(n, &cfg)?;
|
||||
}
|
||||
|
||||
if !self.port_forwards.is_empty() {
|
||||
cfg.set_port_forwards(
|
||||
self.port_forwards
|
||||
.iter()
|
||||
.filter(|pf| !pf.bind_ip.is_empty() && !pf.dst_ip.is_empty())
|
||||
.filter_map(|pf| {
|
||||
let bind_addr =
|
||||
format!("{}:{}", pf.bind_ip, pf.bind_port).parse::<SocketAddr>();
|
||||
let dst_addr =
|
||||
format!("{}:{}", pf.dst_ip, pf.dst_port).parse::<SocketAddr>();
|
||||
|
||||
match (bind_addr, dst_addr) {
|
||||
(Ok(bind_addr), Ok(dst_addr)) => Some(PortForwardConfig {
|
||||
bind_addr,
|
||||
dst_addr,
|
||||
proto: pf.proto.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.enable_vpn_portal.unwrap_or_default() {
|
||||
let cidr = format!(
|
||||
"{}/{}",
|
||||
self.vpn_portal_client_network_addr
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
self.vpn_portal_client_network_len.unwrap_or(24)
|
||||
);
|
||||
cfg.set_vpn_portal_config(VpnPortalConfig {
|
||||
client_cidr: cidr
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
|
||||
wireguard_listen: format!(
|
||||
"0.0.0.0:{}",
|
||||
self.vpn_portal_listen_port.unwrap_or_default()
|
||||
)
|
||||
.parse()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse vpn portal wireguard listen port. {:?}",
|
||||
self.vpn_portal_listen_port
|
||||
)
|
||||
})?,
|
||||
});
|
||||
}
|
||||
|
||||
if self.enable_manual_routes.unwrap_or_default() {
|
||||
let mut routes = Vec::<cidr::Ipv4Cidr>::with_capacity(self.routes.len());
|
||||
for route in self.routes.iter() {
|
||||
routes.push(
|
||||
route
|
||||
.parse()
|
||||
.with_context(|| format!("failed to parse route: {}", route))?,
|
||||
);
|
||||
}
|
||||
cfg.set_routes(Some(routes));
|
||||
}
|
||||
|
||||
if !self.exit_nodes.is_empty() {
|
||||
let mut exit_nodes = Vec::<std::net::IpAddr>::with_capacity(self.exit_nodes.len());
|
||||
for node in self.exit_nodes.iter() {
|
||||
exit_nodes.push(
|
||||
node.parse()
|
||||
.with_context(|| format!("failed to parse exit node: {}", node))?,
|
||||
);
|
||||
}
|
||||
cfg.set_exit_nodes(exit_nodes);
|
||||
}
|
||||
|
||||
if self.enable_socks5.unwrap_or_default()
|
||||
&& let Some(socks5_port) = self.socks5_port
|
||||
{
|
||||
cfg.set_socks5_portal(Some(
|
||||
format!("socks5://0.0.0.0:{}", socks5_port).parse().unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
if !self.mapped_listeners.is_empty() {
|
||||
let mapped_listeners = parse_mapped_listener_urls(&self.mapped_listeners)?;
|
||||
cfg.set_mapped_listeners(Some(mapped_listeners));
|
||||
}
|
||||
|
||||
if let Some(credential_file) = self
|
||||
.credential_file
|
||||
.as_ref()
|
||||
.filter(|path| !path.is_empty())
|
||||
{
|
||||
cfg.set_credential_file(Some(credential_file.into()));
|
||||
}
|
||||
|
||||
if let Some(credential_secret) = credential_secret {
|
||||
cfg.set_secure_mode(Some(normalize_secure_mode_config(
|
||||
easytier_proto::common::SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: Some(credential_secret),
|
||||
local_public_key: None,
|
||||
},
|
||||
)?));
|
||||
} else {
|
||||
cfg.set_secure_mode(
|
||||
self.secure_mode
|
||||
.clone()
|
||||
.map(normalize_secure_mode_config)
|
||||
.transpose()?,
|
||||
);
|
||||
}
|
||||
|
||||
let mut flags = gen_default_flags();
|
||||
if let Some(latency_first) = self.latency_first {
|
||||
flags.latency_first = latency_first;
|
||||
}
|
||||
|
||||
if let Some(dev_name) = self.dev_name.clone() {
|
||||
flags.dev_name = dev_name;
|
||||
}
|
||||
|
||||
if let Some(use_smoltcp) = self.use_smoltcp {
|
||||
flags.use_smoltcp = use_smoltcp;
|
||||
}
|
||||
|
||||
if let Some(ipv6_public_addr_provider) = self.ipv6_public_addr_provider {
|
||||
cfg.set_ipv6_public_addr_provider(ipv6_public_addr_provider);
|
||||
}
|
||||
|
||||
if let Some(ipv6_public_addr_auto) = self.ipv6_public_addr_auto {
|
||||
cfg.set_ipv6_public_addr_auto(ipv6_public_addr_auto);
|
||||
}
|
||||
|
||||
if let Some(ipv6_public_addr_prefix) = self
|
||||
.ipv6_public_addr_prefix
|
||||
.as_ref()
|
||||
.filter(|prefix| !prefix.is_empty())
|
||||
{
|
||||
cfg.set_ipv6_public_addr_prefix(Some(ipv6_public_addr_prefix.parse().with_context(
|
||||
|| format!("failed to parse ipv6 public address prefix: {ipv6_public_addr_prefix}"),
|
||||
)?));
|
||||
}
|
||||
|
||||
if let Some(disable_ipv6) = self.disable_ipv6 {
|
||||
flags.enable_ipv6 = !disable_ipv6;
|
||||
}
|
||||
|
||||
if let Some(enable_kcp_proxy) = self.enable_kcp_proxy {
|
||||
flags.enable_kcp_proxy = enable_kcp_proxy;
|
||||
}
|
||||
|
||||
if let Some(disable_kcp_input) = self.disable_kcp_input {
|
||||
flags.disable_kcp_input = disable_kcp_input;
|
||||
}
|
||||
|
||||
if let Some(enable_quic_proxy) = self.enable_quic_proxy {
|
||||
flags.enable_quic_proxy = enable_quic_proxy;
|
||||
}
|
||||
|
||||
if let Some(disable_quic_input) = self.disable_quic_input {
|
||||
flags.disable_quic_input = disable_quic_input;
|
||||
}
|
||||
|
||||
if let Some(disable_p2p) = self.disable_p2p {
|
||||
flags.disable_p2p = disable_p2p;
|
||||
}
|
||||
|
||||
if let Some(p2p_only) = self.p2p_only {
|
||||
flags.p2p_only = p2p_only;
|
||||
}
|
||||
|
||||
if let Some(lazy_p2p) = self.lazy_p2p {
|
||||
flags.lazy_p2p = lazy_p2p;
|
||||
}
|
||||
|
||||
if let Some(bind_device) = self.bind_device {
|
||||
flags.bind_device = bind_device;
|
||||
}
|
||||
|
||||
if self.socket_mark.is_some() {
|
||||
flags.socket_mark = self.socket_mark;
|
||||
}
|
||||
|
||||
if let Some(no_tun) = self.no_tun {
|
||||
flags.no_tun = no_tun;
|
||||
}
|
||||
|
||||
if let Some(enable_exit_node) = self.enable_exit_node {
|
||||
flags.enable_exit_node = enable_exit_node;
|
||||
}
|
||||
|
||||
if let Some(relay_all_peer_rpc) = self.relay_all_peer_rpc {
|
||||
flags.relay_all_peer_rpc = relay_all_peer_rpc;
|
||||
}
|
||||
|
||||
if let Some(need_p2p) = self.need_p2p {
|
||||
flags.need_p2p = need_p2p;
|
||||
}
|
||||
|
||||
if let Some(multi_thread) = self.multi_thread {
|
||||
flags.multi_thread = multi_thread;
|
||||
}
|
||||
|
||||
if let Some(proxy_forward_by_system) = self.proxy_forward_by_system {
|
||||
flags.proxy_forward_by_system = proxy_forward_by_system;
|
||||
}
|
||||
|
||||
if let Some(disable_encryption) = self.disable_encryption {
|
||||
flags.enable_encryption = !disable_encryption;
|
||||
}
|
||||
|
||||
if self.enable_relay_network_whitelist.unwrap_or_default() {
|
||||
if !self.relay_network_whitelist.is_empty() {
|
||||
flags.relay_network_whitelist = self.relay_network_whitelist.join(" ");
|
||||
} else {
|
||||
flags.relay_network_whitelist = "".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(disable_tcp_hole_punching) = self.disable_tcp_hole_punching {
|
||||
flags.disable_tcp_hole_punching = disable_tcp_hole_punching;
|
||||
}
|
||||
|
||||
if let Some(disable_udp_hole_punching) = self.disable_udp_hole_punching {
|
||||
flags.disable_udp_hole_punching = disable_udp_hole_punching;
|
||||
}
|
||||
|
||||
if let Some(disable_upnp) = self.disable_upnp {
|
||||
flags.disable_upnp = disable_upnp;
|
||||
}
|
||||
|
||||
if let Some(disable_relay_data) = self.disable_relay_data {
|
||||
flags.disable_relay_data = disable_relay_data;
|
||||
}
|
||||
|
||||
if let Some(enable_udp_broadcast_relay) = self.enable_udp_broadcast_relay {
|
||||
flags.enable_udp_broadcast_relay = enable_udp_broadcast_relay;
|
||||
}
|
||||
|
||||
if let Some(disable_sym_hole_punching) = self.disable_sym_hole_punching {
|
||||
flags.disable_sym_hole_punching = disable_sym_hole_punching;
|
||||
}
|
||||
|
||||
if let Some(enable_magic_dns) = self.enable_magic_dns {
|
||||
flags.accept_dns = enable_magic_dns;
|
||||
}
|
||||
|
||||
if let Some(mtu) = self.mtu {
|
||||
flags.mtu = mtu as u32;
|
||||
}
|
||||
|
||||
if let Some(instance_recv_bps_limit) = self.instance_recv_bps_limit {
|
||||
flags.instance_recv_bps_limit = instance_recv_bps_limit;
|
||||
}
|
||||
|
||||
if let Some(enable_private_mode) = self.enable_private_mode {
|
||||
flags.private_mode = enable_private_mode;
|
||||
}
|
||||
|
||||
if let Some(encryption_algorithm) = self.encryption_algorithm.clone() {
|
||||
flags.encryption_algorithm = encryption_algorithm;
|
||||
}
|
||||
|
||||
if let Some(acl) = self.acl.as_ref()
|
||||
&& !acl.is_empty()
|
||||
{
|
||||
cfg.set_acl(Some(acl.clone()));
|
||||
}
|
||||
|
||||
if let Some(data_compress_algo) = self.data_compress_algo {
|
||||
if data_compress_algo < 1 {
|
||||
flags.data_compress_algo = 1;
|
||||
} else {
|
||||
flags.data_compress_algo = data_compress_algo
|
||||
}
|
||||
}
|
||||
|
||||
cfg.set_flags(flags);
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
fn new_from_config(config: impl ConfigLoader) -> Result<Self, anyhow::Error> {
|
||||
let default_config = TomlConfigLoader::default();
|
||||
|
||||
let mut result = Self {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
result.instance_id = Some(config.get_id().to_string());
|
||||
if config.get_hostname() != default_config.get_hostname() {
|
||||
result.hostname = Some(config.get_hostname());
|
||||
}
|
||||
|
||||
result.dhcp = Some(config.get_dhcp());
|
||||
|
||||
let network_identity = config.get_network_identity();
|
||||
result.network_name = Some(network_identity.network_name.clone());
|
||||
result.network_secret = network_identity.network_secret;
|
||||
|
||||
if let Some(ipv4) = config.get_ipv4() {
|
||||
result.virtual_ipv4 = Some(ipv4.address().to_string());
|
||||
result.network_length = Some(ipv4.network_length() as i32);
|
||||
}
|
||||
|
||||
if config.get_ipv6_public_addr_provider() != default_config.get_ipv6_public_addr_provider()
|
||||
{
|
||||
result.ipv6_public_addr_provider = Some(config.get_ipv6_public_addr_provider());
|
||||
}
|
||||
if config.get_ipv6_public_addr_auto() != default_config.get_ipv6_public_addr_auto() {
|
||||
result.ipv6_public_addr_auto = Some(config.get_ipv6_public_addr_auto());
|
||||
}
|
||||
result.ipv6_public_addr_prefix = config
|
||||
.get_ipv6_public_addr_prefix()
|
||||
.map(|prefix| prefix.to_string());
|
||||
|
||||
let peers = config.get_peers();
|
||||
result.networking_method = Some(NetworkingMethod::Manual as i32);
|
||||
if !peers.is_empty() {
|
||||
result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect();
|
||||
result.peers = peers
|
||||
.iter()
|
||||
.map(|p| manage::NetworkPeerConfig {
|
||||
uri: p.uri.to_string(),
|
||||
peer_public_key: p.peer_public_key.clone(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
result.listener_urls = config
|
||||
.get_listeners()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
|
||||
result.proxy_cidrs = config
|
||||
.get_proxy_cidrs()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
if let Some(mapped) = c.mapped_cidr {
|
||||
format!("{}->{}", c.cidr, mapped)
|
||||
} else {
|
||||
c.cidr.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let port_forwards = config.get_port_forwards();
|
||||
if !port_forwards.is_empty() {
|
||||
result.port_forwards = port_forwards
|
||||
.iter()
|
||||
.map(|f| manage::PortForwardConfig {
|
||||
proto: f.proto.clone(),
|
||||
bind_ip: f.bind_addr.ip().to_string(),
|
||||
bind_port: f.bind_addr.port() as u32,
|
||||
dst_ip: f.dst_addr.ip().to_string(),
|
||||
dst_port: f.dst_addr.port() as u32,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(vpn_config) = config.get_vpn_portal_config() {
|
||||
result.enable_vpn_portal = Some(true);
|
||||
|
||||
let cidr = vpn_config.client_cidr;
|
||||
result.vpn_portal_client_network_addr = Some(cidr.first_address().to_string());
|
||||
result.vpn_portal_client_network_len = Some(cidr.network_length() as i32);
|
||||
|
||||
result.vpn_portal_listen_port = Some(vpn_config.wireguard_listen.port() as i32);
|
||||
}
|
||||
|
||||
if let Some(routes) = config.get_routes()
|
||||
&& !routes.is_empty()
|
||||
{
|
||||
result.enable_manual_routes = Some(true);
|
||||
result.routes = routes.iter().map(|r| r.to_string()).collect();
|
||||
}
|
||||
|
||||
let exit_nodes = config.get_exit_nodes();
|
||||
if !exit_nodes.is_empty() {
|
||||
result.exit_nodes = exit_nodes.iter().map(|n| n.to_string()).collect();
|
||||
}
|
||||
|
||||
if let Some(socks5_portal) = config.get_socks5_portal() {
|
||||
result.enable_socks5 = Some(true);
|
||||
result.socks5_port = socks5_portal.port().map(|p| p as i32);
|
||||
}
|
||||
|
||||
let mapped_listeners = config.get_mapped_listeners();
|
||||
if !mapped_listeners.is_empty() {
|
||||
result.mapped_listeners = mapped_listeners.iter().map(|l| l.to_string()).collect();
|
||||
}
|
||||
|
||||
result.secure_mode = config.get_secure_mode();
|
||||
result.credential_file = config
|
||||
.get_credential_file()
|
||||
.map(|path| path.to_string_lossy().into_owned());
|
||||
let flags = config.get_flags();
|
||||
let default_flags = default_config.get_flags();
|
||||
result.latency_first = Some(flags.latency_first);
|
||||
result.dev_name = Some(flags.dev_name.clone());
|
||||
result.use_smoltcp = Some(flags.use_smoltcp);
|
||||
result.disable_ipv6 = Some(!flags.enable_ipv6);
|
||||
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
|
||||
result.disable_kcp_input = Some(flags.disable_kcp_input);
|
||||
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
|
||||
result.disable_quic_input = Some(flags.disable_quic_input);
|
||||
result.disable_p2p = Some(flags.disable_p2p);
|
||||
result.p2p_only = Some(flags.p2p_only);
|
||||
result.lazy_p2p = Some(flags.lazy_p2p);
|
||||
result.bind_device = Some(flags.bind_device);
|
||||
result.socket_mark = flags.socket_mark;
|
||||
result.no_tun = Some(flags.no_tun);
|
||||
result.enable_exit_node = Some(flags.enable_exit_node);
|
||||
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
|
||||
result.need_p2p = Some(flags.need_p2p);
|
||||
result.multi_thread = Some(flags.multi_thread);
|
||||
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
|
||||
result.disable_encryption = Some(!flags.enable_encryption);
|
||||
result.disable_tcp_hole_punching = Some(flags.disable_tcp_hole_punching);
|
||||
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||
result.disable_upnp = Some(flags.disable_upnp);
|
||||
result.disable_relay_data = Some(flags.disable_relay_data);
|
||||
result.enable_udp_broadcast_relay = Some(flags.enable_udp_broadcast_relay);
|
||||
result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching);
|
||||
result.enable_magic_dns = Some(flags.accept_dns);
|
||||
result.mtu = Some(flags.mtu as i32);
|
||||
result.data_compress_algo = (flags.data_compress_algo != default_flags.data_compress_algo)
|
||||
.then_some(flags.data_compress_algo);
|
||||
result.encryption_algorithm = (flags.encryption_algorithm
|
||||
!= default_flags.encryption_algorithm)
|
||||
.then_some(flags.encryption_algorithm.clone());
|
||||
result.instance_recv_bps_limit =
|
||||
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
||||
result.enable_private_mode = Some(flags.private_mode);
|
||||
|
||||
result.acl = config.get_acl();
|
||||
|
||||
if flags.relay_network_whitelist == "*" {
|
||||
result.enable_relay_network_whitelist = Some(false);
|
||||
} else {
|
||||
result.enable_relay_network_whitelist = Some(true);
|
||||
if flags.relay_network_whitelist.is_empty() {
|
||||
result.relay_network_whitelist = vec![];
|
||||
} else {
|
||||
result.relay_network_whitelist = flags
|
||||
.relay_network_whitelist
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::{
|
||||
api_input::{NetworkConfig, NetworkConfigExt},
|
||||
toml::{ConfigLoader, TomlConfig},
|
||||
};
|
||||
|
||||
fn js_error(error: impl std::fmt::Debug) -> JsValue {
|
||||
JsValue::from_str(&format!("{error:?}"))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_config(config_json: &str) -> Result<String, JsValue> {
|
||||
let config: NetworkConfig = serde_json::from_str(config_json).map_err(js_error)?;
|
||||
config
|
||||
.gen_config()
|
||||
.map(|config| config.dump())
|
||||
.map_err(js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_config(toml_config: &str) -> Result<String, JsValue> {
|
||||
let config = TomlConfig::new_from_str(toml_config)
|
||||
.and_then(|config| NetworkConfig::new_from_config(&config))
|
||||
.map_err(js_error)?;
|
||||
serde_json::to_string(&config).map_err(js_error)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use strum::VariantArray;
|
||||
|
||||
/// Stable configuration vocabulary for every known encryption algorithm.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, VariantArray)]
|
||||
pub enum EncryptionAlgorithm {
|
||||
Xor,
|
||||
#[default]
|
||||
AesGcm,
|
||||
Aes256Gcm,
|
||||
ChaCha20,
|
||||
}
|
||||
|
||||
impl EncryptionAlgorithm {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Xor => "xor",
|
||||
Self::AesGcm => "aes-gcm",
|
||||
Self::Aes256Gcm => "aes-256-gcm",
|
||||
Self::ChaCha20 => "chacha20",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptionAlgorithm {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncryptionAlgorithm {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"xor" => Ok(Self::Xor),
|
||||
"aes-gcm" | "openssl-aes-gcm" => Ok(Self::AesGcm),
|
||||
"aes-256-gcm" | "openssl-aes-256-gcm" => Ok(Self::Aes256Gcm),
|
||||
"chacha20" | "chacha20-poly1305" | "openssl-chacha20" => Ok(Self::ChaCha20),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_algorithm_names_are_stable() {
|
||||
let cases = [
|
||||
("xor", EncryptionAlgorithm::Xor),
|
||||
("aes-gcm", EncryptionAlgorithm::AesGcm),
|
||||
("aes-256-gcm", EncryptionAlgorithm::Aes256Gcm),
|
||||
("chacha20", EncryptionAlgorithm::ChaCha20),
|
||||
("chacha20-poly1305", EncryptionAlgorithm::ChaCha20),
|
||||
("openssl-aes-gcm", EncryptionAlgorithm::AesGcm),
|
||||
("openssl-aes-256-gcm", EncryptionAlgorithm::Aes256Gcm),
|
||||
("openssl-chacha20", EncryptionAlgorithm::ChaCha20),
|
||||
];
|
||||
|
||||
for (name, expected) in cases {
|
||||
assert_eq!(name.parse(), Ok(expected));
|
||||
}
|
||||
assert_eq!(EncryptionAlgorithm::ChaCha20.to_string(), "chacha20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_is_the_stable_default() {
|
||||
assert_eq!(EncryptionAlgorithm::default(), EncryptionAlgorithm::AesGcm);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use easytier_proto::common::{PortForwardConfigPb, SocketType};
|
||||
|
||||
/// Runtime configuration for the core-owned SOCKS and port-forward gateway.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GatewayRuntimeConfig {
|
||||
pub socks5_bind: Option<SocketAddr>,
|
||||
pub port_forwards: Vec<PortForwardConfig>,
|
||||
}
|
||||
|
||||
/// One TCP or UDP port-forward rule.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct PortForwardConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub dst_addr: SocketAddr,
|
||||
pub proto: String,
|
||||
}
|
||||
|
||||
impl From<PortForwardConfigPb> for PortForwardConfig {
|
||||
fn from(config: PortForwardConfigPb) -> Self {
|
||||
Self {
|
||||
bind_addr: config.bind_addr.unwrap_or_default().into(),
|
||||
dst_addr: config.dst_addr.unwrap_or_default().into(),
|
||||
proto: match SocketType::try_from(config.socket_type) {
|
||||
Ok(SocketType::Tcp) => "tcp".to_string(),
|
||||
Ok(SocketType::Udp) => "udp".to_string(),
|
||||
_ => "tcp".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PortForwardConfig> for PortForwardConfigPb {
|
||||
fn from(config: PortForwardConfig) -> Self {
|
||||
Self {
|
||||
bind_addr: Some(config.bind_addr.into()),
|
||||
dst_addr: Some(config.dst_addr.into()),
|
||||
socket_type: match config.proto.to_lowercase().as_str() {
|
||||
"tcp" => SocketType::Tcp as i32,
|
||||
"udp" => SocketType::Udp as i32,
|
||||
_ => SocketType::Tcp as i32,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProxyRuntimeConfig {
|
||||
pub enable_exit_node: bool,
|
||||
pub no_tun: bool,
|
||||
pub forward_by_system: bool,
|
||||
pub force_smoltcp: bool,
|
||||
pub icmp_failure_is_fatal: bool,
|
||||
pub udp_response_ipv4_mtu: usize,
|
||||
}
|
||||
|
||||
impl ProxyRuntimeConfig {
|
||||
pub fn should_start(self, has_proxy_networks: bool) -> bool {
|
||||
if !has_proxy_networks && !self.enable_exit_node && !self.no_tun {
|
||||
return false;
|
||||
}
|
||||
|
||||
!self.forward_by_system || self.no_tun
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProxyRuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_exit_node: false,
|
||||
no_tun: false,
|
||||
forward_by_system: false,
|
||||
force_smoltcp: false,
|
||||
icmp_failure_is_fatal: false,
|
||||
udp_response_ipv4_mtu: 1280,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ProxyRuntimeConfig;
|
||||
|
||||
#[test]
|
||||
fn proxy_startup_policy_preserves_runtime_modes() {
|
||||
assert!(!ProxyRuntimeConfig::default().should_start(false));
|
||||
assert!(ProxyRuntimeConfig::default().should_start(true));
|
||||
assert!(
|
||||
ProxyRuntimeConfig {
|
||||
enable_exit_node: true,
|
||||
..Default::default()
|
||||
}
|
||||
.should_start(false)
|
||||
);
|
||||
assert!(
|
||||
ProxyRuntimeConfig {
|
||||
no_tun: true,
|
||||
..Default::default()
|
||||
}
|
||||
.should_start(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_startup_policy_preserves_system_forwarding_rules() {
|
||||
assert!(
|
||||
!ProxyRuntimeConfig {
|
||||
forward_by_system: true,
|
||||
..Default::default()
|
||||
}
|
||||
.should_start(true)
|
||||
);
|
||||
assert!(
|
||||
!ProxyRuntimeConfig {
|
||||
enable_exit_node: true,
|
||||
forward_by_system: true,
|
||||
..Default::default()
|
||||
}
|
||||
.should_start(false)
|
||||
);
|
||||
assert!(
|
||||
ProxyRuntimeConfig {
|
||||
no_tun: true,
|
||||
forward_by_system: true,
|
||||
..Default::default()
|
||||
}
|
||||
.should_start(false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_runtime_defaults_preserve_udp_mtu() {
|
||||
assert_eq!(ProxyRuntimeConfig::default().udp_response_ipv4_mtu, 1280);
|
||||
}
|
||||
}
|
||||
@@ -1,810 +0,0 @@
|
||||
//! Static configuration schema plus the live runtime configuration store.
|
||||
|
||||
#[cfg(feature = "web-client")]
|
||||
pub mod api;
|
||||
#[cfg(any(feature = "web-client", feature = "browser-config"))]
|
||||
pub mod api_input;
|
||||
#[cfg(all(
|
||||
feature = "browser-config",
|
||||
target_arch = "wasm32",
|
||||
target_os = "unknown"
|
||||
))]
|
||||
mod browser;
|
||||
mod encryption;
|
||||
pub mod gateway;
|
||||
pub mod peers;
|
||||
pub mod runtime;
|
||||
pub mod toml;
|
||||
|
||||
pub use encryption::EncryptionAlgorithm;
|
||||
|
||||
pub(crate) const DEFAULT_UDP_STUN_SERVERS: &[&str] = &[
|
||||
"txt:stun.easytier.cn",
|
||||
"stun.miwifi.com",
|
||||
"stun.chat.bilibili.com",
|
||||
"stun.hitv.com",
|
||||
];
|
||||
pub(crate) const DEFAULT_TCP_STUN_SERVERS: &[&str] = &[
|
||||
"stun.hot-chilli.net",
|
||||
"stun.fitauto.ru",
|
||||
"fwa.lifesizecloud.com",
|
||||
"global.turn.twilio.com",
|
||||
"turn.cloudflare.com",
|
||||
"stun.voip.blackberry.com",
|
||||
"stun.radiojar.com",
|
||||
];
|
||||
pub(crate) const DEFAULT_UDP_V6_STUN_SERVERS: &[&str] = &["txt:stun-v6.easytier.cn"];
|
||||
|
||||
pub(crate) fn default_stun_servers(servers: &[&str]) -> Vec<String> {
|
||||
servers.iter().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, hash_map::DefaultHasher},
|
||||
hash::{Hash, Hasher},
|
||||
net::IpAddr,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
use easytier_proto::{common as common_pb, core_config as pb};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub type PeerId = u32;
|
||||
|
||||
pub type NetworkSecretDigest = [u8; 32];
|
||||
|
||||
/// Host capabilities used by the portable mapped-listener validation rule.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct MappedListenerPolicy {
|
||||
implicit_port_schemes: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl MappedListenerPolicy {
|
||||
pub fn new<I, S>(implicit_port_schemes: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
Self {
|
||||
implicit_port_schemes: implicit_port_schemes
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.map(|scheme: String| scheme.to_ascii_lowercase())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(&self, url: &Url) -> anyhow::Result<()> {
|
||||
if url.port().is_none() && !self.implicit_port_schemes.contains(url.scheme()) {
|
||||
anyhow::bail!("mapped listener port is missing: {}", url);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_urls(&self, mapped_listeners: &[String]) -> anyhow::Result<Vec<Url>> {
|
||||
mapped_listeners
|
||||
.iter()
|
||||
.map(|value| {
|
||||
let url: Url = value
|
||||
.parse()
|
||||
.with_context(|| format!("mapped listener is not a valid url: {}", value))?;
|
||||
self.validate(&url)?;
|
||||
Ok(url)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes and validates the portable secure-mode key configuration.
|
||||
pub fn normalize_secure_mode_config(
|
||||
mut config: common_pb::SecureModeConfig,
|
||||
) -> anyhow::Result<common_pb::SecureModeConfig> {
|
||||
if !config.enabled {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let private_key = if config.local_private_key.is_none() {
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
config.local_private_key = Some(BASE64_STANDARD.encode(private.as_bytes()));
|
||||
private
|
||||
} else {
|
||||
config.private_key()?
|
||||
};
|
||||
let generated_public_key = x25519_dalek::PublicKey::from(&private_key);
|
||||
let generated_public_key = BASE64_STANDARD.encode(generated_public_key.as_bytes());
|
||||
|
||||
match config.local_public_key.as_ref() {
|
||||
None => config.local_public_key = Some(generated_public_key),
|
||||
Some(configured_public_key) => {
|
||||
let public_key = config.public_key()?;
|
||||
let canonical_public_key = BASE64_STANDARD.encode(public_key.as_bytes());
|
||||
if configured_public_key != &canonical_public_key {
|
||||
anyhow::bail!(
|
||||
"local public key {} does not match generated public key {}",
|
||||
configured_public_key,
|
||||
canonical_public_key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkIdentity {
|
||||
pub network_name: String,
|
||||
pub network_secret: Option<String>,
|
||||
pub network_secret_digest: Option<NetworkSecretDigest>,
|
||||
}
|
||||
|
||||
impl NetworkIdentity {
|
||||
pub fn new(network_name: String, network_secret: String) -> Self {
|
||||
Self {
|
||||
network_secret_digest: Some(network_secret_digest(&network_name, &network_secret)),
|
||||
network_name,
|
||||
network_secret: Some(network_secret),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_credential(network_name: String) -> Self {
|
||||
Self {
|
||||
network_name,
|
||||
network_secret: None,
|
||||
network_secret_digest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secret_digest(&self) -> Option<NetworkSecretDigest> {
|
||||
if self.network_secret_digest.is_some() {
|
||||
self.network_secret_digest
|
||||
} else if let Some(network_secret) = &self.network_secret {
|
||||
let mut network_secret_digest = [0u8; 32];
|
||||
generate_digest_from_str(
|
||||
&self.network_name,
|
||||
network_secret,
|
||||
&mut network_secret_digest,
|
||||
);
|
||||
Some(network_secret_digest)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_secret_digest(mut self) -> Self {
|
||||
self.network_secret_digest = self.secret_digest();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
struct NetworkIdentityWithOnlyDigest {
|
||||
network_name: String,
|
||||
network_secret_digest: Option<NetworkSecretDigest>,
|
||||
}
|
||||
|
||||
fn generate_digest_from_str(str1: &str, str2: &str, digest: &mut [u8]) {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(str1.as_bytes());
|
||||
hasher.write(str2.as_bytes());
|
||||
|
||||
assert_eq!(digest.len() % 8, 0, "digest length must be multiple of 8");
|
||||
|
||||
let shard_count = digest.len() / 8;
|
||||
for i in 0..shard_count {
|
||||
digest[i * 8..(i + 1) * 8].copy_from_slice(&hasher.finish().to_be_bytes());
|
||||
hasher.write(&digest[..(i + 1) * 8]);
|
||||
}
|
||||
}
|
||||
|
||||
fn network_secret_digest(network_name: &str, network_secret: &str) -> NetworkSecretDigest {
|
||||
let mut digest = [0u8; 32];
|
||||
generate_digest_from_str(network_name, network_secret, &mut digest);
|
||||
digest
|
||||
}
|
||||
|
||||
impl From<NetworkIdentity> for NetworkIdentityWithOnlyDigest {
|
||||
fn from(identity: NetworkIdentity) -> Self {
|
||||
Self {
|
||||
network_secret_digest: identity.secret_digest(),
|
||||
network_name: identity.network_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for NetworkIdentity {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_with_digest = NetworkIdentityWithOnlyDigest::from(self.clone());
|
||||
let other_with_digest = NetworkIdentityWithOnlyDigest::from(other.clone());
|
||||
self_with_digest == other_with_digest
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for NetworkIdentity {}
|
||||
|
||||
impl Hash for NetworkIdentity {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let self_with_digest = NetworkIdentityWithOnlyDigest::from(self.clone());
|
||||
self_with_digest.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NetworkIdentity {
|
||||
fn default() -> Self {
|
||||
Self::new("default".to_string(), "".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct CoreConfig {
|
||||
pub node: NodeConfig,
|
||||
pub routes: RouteConfig,
|
||||
pub peer_policy: PeerPolicyConfig,
|
||||
pub traffic: TrafficConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct NodeConfig {
|
||||
pub peer_id: Option<PeerId>,
|
||||
pub instance_id: Option<[u8; 16]>,
|
||||
pub hostname: Option<String>,
|
||||
pub network_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct RouteConfig {
|
||||
pub ipv4: Option<IpPrefix>,
|
||||
pub ipv6: Option<IpPrefix>,
|
||||
pub advertised_routes: Vec<IpPrefix>,
|
||||
pub proxy_networks: Vec<ProxyNetworkConfig>,
|
||||
pub foreign_networks: Vec<ForeignNetworkConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct IpPrefix {
|
||||
pub address: IpAddr,
|
||||
pub prefix_len: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProxyNetworkConfig {
|
||||
pub real: IpPrefix,
|
||||
pub mapped: Option<IpPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ForeignNetworkConfig {
|
||||
pub name: String,
|
||||
pub cidrs: Vec<IpPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PeerPolicyConfig {
|
||||
pub p2p_enabled: bool,
|
||||
pub relay_peer_rpc: bool,
|
||||
pub relay_data: bool,
|
||||
pub latency_first: bool,
|
||||
pub encryption_required: bool,
|
||||
}
|
||||
|
||||
impl Default for PeerPolicyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
p2p_enabled: true,
|
||||
relay_peer_rpc: false,
|
||||
relay_data: true,
|
||||
latency_first: false,
|
||||
encryption_required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct P2pPolicyFlags {
|
||||
pub disable_udp_hole_punching: bool,
|
||||
pub disable_sym_hole_punching: bool,
|
||||
pub disable_upnp: bool,
|
||||
pub lazy_p2p: bool,
|
||||
pub disable_p2p: bool,
|
||||
pub need_p2p: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct TrafficConfig {
|
||||
pub mtu: Option<u16>,
|
||||
pub instance_recv_bps_limit: Option<u64>,
|
||||
pub foreign_relay_bps_limit: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("missing required field: {0}")]
|
||||
MissingField(&'static str),
|
||||
#[error("invalid IPv4 prefix length: {0}")]
|
||||
InvalidIpv4Prefix(u8),
|
||||
#[error("invalid IPv6 prefix length: {0}")]
|
||||
InvalidIpv6Prefix(u8),
|
||||
#[error("invalid MTU: {0}")]
|
||||
InvalidMtu(u32),
|
||||
}
|
||||
|
||||
impl IpPrefix {
|
||||
pub fn new(address: IpAddr, prefix_len: u8) -> Result<Self, ConfigError> {
|
||||
match address {
|
||||
IpAddr::V4(_) if prefix_len <= 32 => Ok(Self {
|
||||
address,
|
||||
prefix_len,
|
||||
}),
|
||||
IpAddr::V4(_) => Err(ConfigError::InvalidIpv4Prefix(prefix_len)),
|
||||
IpAddr::V6(_) if prefix_len <= 128 => Ok(Self {
|
||||
address,
|
||||
prefix_len,
|
||||
}),
|
||||
IpAddr::V6(_) => Err(ConfigError::InvalidIpv6Prefix(prefix_len)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::CoreConfig> for CoreConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::CoreConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
node: value
|
||||
.node
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
routes: value
|
||||
.routes
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
peer_policy: value.peer_policy.map(Into::into).unwrap_or_default(),
|
||||
traffic: value
|
||||
.traffic
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreConfig> for pb::CoreConfig {
|
||||
fn from(value: CoreConfig) -> Self {
|
||||
Self {
|
||||
node: Some(value.node.into()),
|
||||
routes: Some(value.routes.into()),
|
||||
peer_policy: Some(value.peer_policy.into()),
|
||||
traffic: Some(value.traffic.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::NodeConfig> for NodeConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::NodeConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
peer_id: value.peer_id,
|
||||
instance_id: value.instance_id.map(uuid_to_bytes),
|
||||
hostname: value.hostname,
|
||||
network_name: value.network_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NodeConfig> for pb::NodeConfig {
|
||||
fn from(value: NodeConfig) -> Self {
|
||||
Self {
|
||||
peer_id: value.peer_id,
|
||||
instance_id: value.instance_id.map(uuid_from_bytes),
|
||||
hostname: value.hostname,
|
||||
network_name: value.network_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::RouteConfig> for RouteConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::RouteConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
ipv4: value.ipv4.map(TryInto::try_into).transpose()?,
|
||||
ipv6: value.ipv6.map(TryInto::try_into).transpose()?,
|
||||
advertised_routes: value
|
||||
.advertised_routes
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
proxy_networks: value
|
||||
.proxy_networks
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
foreign_networks: value
|
||||
.foreign_networks
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RouteConfig> for pb::RouteConfig {
|
||||
fn from(value: RouteConfig) -> Self {
|
||||
Self {
|
||||
ipv4: value.ipv4.map(Into::into),
|
||||
ipv6: value.ipv6.map(Into::into),
|
||||
advertised_routes: value
|
||||
.advertised_routes
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
proxy_networks: value.proxy_networks.into_iter().map(Into::into).collect(),
|
||||
foreign_networks: value.foreign_networks.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::IpPrefix> for IpPrefix {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::IpPrefix) -> Result<Self, Self::Error> {
|
||||
let address = pb_ip_addr_to_std(
|
||||
value
|
||||
.address
|
||||
.ok_or(ConfigError::MissingField("IpPrefix.address"))?,
|
||||
)?;
|
||||
let prefix_len = u8::try_from(value.prefix_len)
|
||||
.map_err(|_| invalid_prefix_for_address(address, value.prefix_len))?;
|
||||
Self::new(address, prefix_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpPrefix> for pb::IpPrefix {
|
||||
fn from(value: IpPrefix) -> Self {
|
||||
Self {
|
||||
address: Some(value.address.into()),
|
||||
prefix_len: value.prefix_len.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::ProxyNetworkConfig> for ProxyNetworkConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::ProxyNetworkConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
real: value
|
||||
.real
|
||||
.ok_or(ConfigError::MissingField("ProxyNetworkConfig.real"))?
|
||||
.try_into()?,
|
||||
mapped: value.mapped.map(TryInto::try_into).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProxyNetworkConfig> for pb::ProxyNetworkConfig {
|
||||
fn from(value: ProxyNetworkConfig) -> Self {
|
||||
Self {
|
||||
real: Some(value.real.into()),
|
||||
mapped: value.mapped.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::ForeignNetworkConfig> for ForeignNetworkConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::ForeignNetworkConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
name: value.name,
|
||||
cidrs: value
|
||||
.cidrs
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ForeignNetworkConfig> for pb::ForeignNetworkConfig {
|
||||
fn from(value: ForeignNetworkConfig) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
cidrs: value.cidrs.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pb::PeerPolicyConfig> for PeerPolicyConfig {
|
||||
fn from(value: pb::PeerPolicyConfig) -> Self {
|
||||
let default = Self::default();
|
||||
Self {
|
||||
p2p_enabled: value.p2p_enabled.unwrap_or(default.p2p_enabled),
|
||||
relay_peer_rpc: value.relay_peer_rpc.unwrap_or(default.relay_peer_rpc),
|
||||
relay_data: value.relay_data.unwrap_or(default.relay_data),
|
||||
latency_first: value.latency_first.unwrap_or(default.latency_first),
|
||||
encryption_required: value
|
||||
.encryption_required
|
||||
.unwrap_or(default.encryption_required),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeerPolicyConfig> for pb::PeerPolicyConfig {
|
||||
fn from(value: PeerPolicyConfig) -> Self {
|
||||
Self {
|
||||
p2p_enabled: Some(value.p2p_enabled),
|
||||
relay_peer_rpc: Some(value.relay_peer_rpc),
|
||||
relay_data: Some(value.relay_data),
|
||||
latency_first: Some(value.latency_first),
|
||||
encryption_required: Some(value.encryption_required),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<pb::TrafficConfig> for TrafficConfig {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn try_from(value: pb::TrafficConfig) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
mtu: value
|
||||
.mtu
|
||||
.map(|mtu| u16::try_from(mtu).map_err(|_| ConfigError::InvalidMtu(mtu)))
|
||||
.transpose()?,
|
||||
instance_recv_bps_limit: value.instance_recv_bps_limit,
|
||||
foreign_relay_bps_limit: value.foreign_relay_bps_limit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TrafficConfig> for pb::TrafficConfig {
|
||||
fn from(value: TrafficConfig) -> Self {
|
||||
Self {
|
||||
mtu: value.mtu.map(Into::into),
|
||||
instance_recv_bps_limit: value.instance_recv_bps_limit,
|
||||
foreign_relay_bps_limit: value.foreign_relay_bps_limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pb_ip_addr_to_std(value: common_pb::IpAddr) -> Result<IpAddr, ConfigError> {
|
||||
match value.ip.ok_or(ConfigError::MissingField("IpAddr.ip"))? {
|
||||
common_pb::ip_addr::Ip::Ipv4(addr) => Ok(IpAddr::V4(addr.into())),
|
||||
common_pb::ip_addr::Ip::Ipv6(addr) => Ok(IpAddr::V6(addr.into())),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_prefix_for_address(address: IpAddr, prefix_len: u32) -> ConfigError {
|
||||
let prefix_len = u8::try_from(prefix_len).unwrap_or(u8::MAX);
|
||||
match address {
|
||||
IpAddr::V4(_) => ConfigError::InvalidIpv4Prefix(prefix_len),
|
||||
IpAddr::V6(_) => ConfigError::InvalidIpv6Prefix(prefix_len),
|
||||
}
|
||||
}
|
||||
|
||||
fn uuid_to_bytes(value: common_pb::Uuid) -> [u8; 16] {
|
||||
let mut bytes = [0; 16];
|
||||
bytes[0..4].copy_from_slice(&value.part1.to_be_bytes());
|
||||
bytes[4..8].copy_from_slice(&value.part2.to_be_bytes());
|
||||
bytes[8..12].copy_from_slice(&value.part3.to_be_bytes());
|
||||
bytes[12..16].copy_from_slice(&value.part4.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn uuid_from_bytes(bytes: [u8; 16]) -> common_pb::Uuid {
|
||||
common_pb::Uuid {
|
||||
part1: u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||||
part2: u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
|
||||
part3: u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
|
||||
part4: u32::from_be_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
fn digest(network_name: &str, network_secret: &str) -> NetworkSecretDigest {
|
||||
let mut digest = [0u8; 32];
|
||||
generate_digest_from_str(network_name, network_secret, &mut digest);
|
||||
digest
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_identity_matches_secret_to_digest_identity() {
|
||||
let local = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: Some("secret".to_string()),
|
||||
network_secret_digest: None,
|
||||
};
|
||||
let remote = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: None,
|
||||
network_secret_digest: Some(digest("net", "secret")),
|
||||
};
|
||||
|
||||
assert_eq!(local, remote);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_identity_rejects_different_digest() {
|
||||
let local = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: Some("secret".to_string()),
|
||||
network_secret_digest: None,
|
||||
};
|
||||
let remote = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: None,
|
||||
network_secret_digest: Some(digest("net", "other")),
|
||||
};
|
||||
|
||||
assert_ne!(local, remote);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_identity_equal_values_have_equal_hash() {
|
||||
let local = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: Some("secret".to_string()),
|
||||
network_secret_digest: None,
|
||||
};
|
||||
let remote = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: None,
|
||||
network_secret_digest: Some(digest("net", "secret")),
|
||||
};
|
||||
let mut local_hasher = DefaultHasher::new();
|
||||
let mut remote_hasher = DefaultHasher::new();
|
||||
|
||||
local.hash(&mut local_hasher);
|
||||
remote.hash(&mut remote_hasher);
|
||||
|
||||
assert_eq!(local_hasher.finish(), remote_hasher.finish());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_identity_derives_digest_from_plaintext_secret() {
|
||||
let identity = NetworkIdentity {
|
||||
network_name: "net".to_string(),
|
||||
network_secret: Some("secret".to_string()),
|
||||
network_secret_digest: None,
|
||||
};
|
||||
|
||||
assert_eq!(identity.secret_digest(), Some(digest("net", "secret")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_identity_default_matches_native_default_network() {
|
||||
assert_eq!(
|
||||
NetworkIdentity::default(),
|
||||
NetworkIdentity::new("default".to_string(), "".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapped_listener_policy_uses_explicit_host_capabilities() {
|
||||
let policy = MappedListenerPolicy::new(["tcp", "ws", "wss"]);
|
||||
let parsed = policy
|
||||
.parse_urls(&[
|
||||
"tcp://127.0.0.1".to_string(),
|
||||
"ws://example.com".to_string(),
|
||||
"wss://example.com/path".to_string(),
|
||||
"ring://peer-id:1000".to_string(),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(parsed.len(), 4);
|
||||
assert_eq!(parsed[0].scheme(), "tcp");
|
||||
assert_eq!(parsed[1].scheme(), "ws");
|
||||
assert_eq!(parsed[2].scheme(), "wss");
|
||||
assert_eq!(parsed[3].port(), Some(1000));
|
||||
|
||||
let error = policy
|
||||
.parse_urls(&["ring://peer-id".to_string()])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("mapped listener port is missing")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_normalization_generates_missing_key_pair() {
|
||||
let normalized = normalize_secure_mode_config(common_pb::SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: None,
|
||||
local_public_key: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let private_key = normalized.private_key().unwrap();
|
||||
let public_key = normalized.public_key().unwrap();
|
||||
assert_eq!(public_key, PublicKey::from(&private_key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_normalization_preserves_existing_key_configuration() {
|
||||
let private_key = StaticSecret::from([7; 32]);
|
||||
let public_key = PublicKey::from(&private_key);
|
||||
let config = common_pb::SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: Some(BASE64_STANDARD.encode(private_key.as_bytes())),
|
||||
local_public_key: Some(BASE64_STANDARD.encode(public_key.as_bytes())),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
normalize_secure_mode_config(config.clone()).unwrap(),
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_secure_mode_does_not_validate_keys() {
|
||||
let config = common_pb::SecureModeConfig {
|
||||
enabled: false,
|
||||
local_private_key: Some("not-base64".to_string()),
|
||||
local_public_key: Some("not-base64".to_string()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
normalize_secure_mode_config(config.clone()).unwrap(),
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_ip_prefix_lengths() {
|
||||
assert!(IpPrefix::new("10.0.0.1".parse().unwrap(), 24).is_ok());
|
||||
assert_eq!(
|
||||
IpPrefix::new("10.0.0.1".parse().unwrap(), 33),
|
||||
Err(ConfigError::InvalidIpv4Prefix(33))
|
||||
);
|
||||
assert!(IpPrefix::new("2001:db8::1".parse().unwrap(), 64).is_ok());
|
||||
assert_eq!(
|
||||
IpPrefix::new("2001:db8::1".parse().unwrap(), 129),
|
||||
Err(ConfigError::InvalidIpv6Prefix(129))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_core_config_from_proto_defaults() {
|
||||
let config = CoreConfig::try_from(pb::CoreConfig {
|
||||
node: Some(pb::NodeConfig {
|
||||
peer_id: Some(7),
|
||||
instance_id: None,
|
||||
hostname: Some("node-a".to_string()),
|
||||
network_name: "net".to_string(),
|
||||
}),
|
||||
routes: None,
|
||||
peer_policy: None,
|
||||
traffic: Some(pb::TrafficConfig {
|
||||
mtu: Some(1380),
|
||||
instance_recv_bps_limit: Some(100),
|
||||
foreign_relay_bps_limit: None,
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.node.peer_id, Some(7));
|
||||
assert_eq!(config.node.hostname.as_deref(), Some("node-a"));
|
||||
assert!(config.peer_policy.p2p_enabled);
|
||||
assert_eq!(config.traffic.mtu, Some(1380));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_ip_prefix_round_trip() {
|
||||
let prefix = IpPrefix::new("10.1.0.1".parse().unwrap(), 16).unwrap();
|
||||
let pb: pb::IpPrefix = prefix.clone().into();
|
||||
assert_eq!(IpPrefix::try_from(pb).unwrap(), prefix);
|
||||
}
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
//! Peer-flavored configuration data owned by the config layer.
|
||||
//!
|
||||
//! These types are pure serializable configuration snapshots. Normalization
|
||||
//! and derivation behavior that depends on peer-domain logic stays in
|
||||
//! `crate::peers`.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use cidr::{Ipv4Cidr, Ipv6Cidr};
|
||||
use easytier_proto::common::{FlagsInConfig, PeerFeatureFlag, SecureModeConfig, StunInfo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::proto::acl::{Acl, AclV1, Action, Chain, ChainType, GroupInfo, Protocol, Rule};
|
||||
|
||||
use super::{CoreConfig, NetworkIdentity};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AclRuleConfig {
|
||||
pub acl: Option<Acl>,
|
||||
pub tcp_whitelist: Vec<String>,
|
||||
pub udp_whitelist: Vec<String>,
|
||||
pub whitelist_priority: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AclWhitelistSnapshot {
|
||||
pub tcp_ports: Vec<String>,
|
||||
pub udp_ports: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<&AclRuleConfig> for AclWhitelistSnapshot {
|
||||
fn from(config: &AclRuleConfig) -> Self {
|
||||
Self {
|
||||
tcp_ports: config.tcp_whitelist.clone(),
|
||||
udp_ports: config.udp_whitelist.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AclRuleConfig {
|
||||
fn parse_port_list(port_list: &[String]) -> anyhow::Result<Vec<String>> {
|
||||
let mut ports = Vec::new();
|
||||
|
||||
for port_spec in port_list {
|
||||
if port_spec.contains('-') {
|
||||
let parts: Vec<&str> = port_spec.split('-').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(anyhow::anyhow!("Invalid port range format: {}", port_spec));
|
||||
}
|
||||
|
||||
let start: u16 = parts[0]
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid start port in range: {}", port_spec))?;
|
||||
let end: u16 = parts[1]
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid end port in range: {}", port_spec))?;
|
||||
|
||||
if start > end {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Start port must be <= end port in range: {}",
|
||||
port_spec
|
||||
));
|
||||
}
|
||||
ports.push(port_spec.clone());
|
||||
} else {
|
||||
let port: u16 = port_spec
|
||||
.parse()
|
||||
.with_context(|| format!("Invalid port number: {}", port_spec))?;
|
||||
ports.push(port.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
fn generate_acl_from_whitelists(&mut self) -> anyhow::Result<()> {
|
||||
if self.tcp_whitelist.is_empty() && self.udp_whitelist.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut inbound_chain = Chain {
|
||||
name: "inbound_whitelist".to_string(),
|
||||
chain_type: ChainType::Inbound as i32,
|
||||
description: "Auto-generated inbound whitelist from CLI".to_string(),
|
||||
enabled: true,
|
||||
rules: vec![],
|
||||
default_action: Action::Allow as i32,
|
||||
};
|
||||
|
||||
let mut rule_priority = self.whitelist_priority.unwrap_or(1000u32);
|
||||
|
||||
if !self.tcp_whitelist.is_empty() {
|
||||
let tcp_ports = Self::parse_port_list(&self.tcp_whitelist)?;
|
||||
inbound_chain.rules.push(Rule {
|
||||
name: "tcp_whitelist".to_string(),
|
||||
description: "Auto-generated TCP whitelist rule".to_string(),
|
||||
priority: rule_priority,
|
||||
enabled: true,
|
||||
protocol: Protocol::Tcp as i32,
|
||||
ports: tcp_ports,
|
||||
source_ips: vec![],
|
||||
destination_ips: vec![],
|
||||
source_ports: vec![],
|
||||
action: Action::Allow as i32,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: true,
|
||||
source_groups: vec![],
|
||||
destination_groups: vec![],
|
||||
});
|
||||
inbound_chain.rules.push(Rule {
|
||||
name: "tcp_whitelist_deny_other".to_string(),
|
||||
description: "Auto-generated TCP whitelist rule to deny other ports".to_string(),
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
protocol: Protocol::Tcp as i32,
|
||||
ports: vec!["0-65535".to_string()],
|
||||
source_ips: vec![],
|
||||
destination_ips: vec![],
|
||||
source_ports: vec![],
|
||||
action: Action::Drop as i32,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: false,
|
||||
source_groups: vec![],
|
||||
destination_groups: vec![],
|
||||
});
|
||||
rule_priority -= 1;
|
||||
}
|
||||
|
||||
if !self.udp_whitelist.is_empty() {
|
||||
let udp_ports = Self::parse_port_list(&self.udp_whitelist)?;
|
||||
inbound_chain.rules.push(Rule {
|
||||
name: "udp_whitelist".to_string(),
|
||||
description: "Auto-generated UDP whitelist rule".to_string(),
|
||||
priority: rule_priority,
|
||||
enabled: true,
|
||||
protocol: Protocol::Udp as i32,
|
||||
ports: udp_ports,
|
||||
source_ips: vec![],
|
||||
destination_ips: vec![],
|
||||
source_ports: vec![],
|
||||
action: Action::Allow as i32,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: false,
|
||||
source_groups: vec![],
|
||||
destination_groups: vec![],
|
||||
});
|
||||
inbound_chain.rules.push(Rule {
|
||||
name: "udp_whitelist_deny_other".to_string(),
|
||||
description: "Auto-generated UDP whitelist rule to deny other ports".to_string(),
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
protocol: Protocol::Udp as i32,
|
||||
ports: vec!["0-65535".to_string()],
|
||||
source_ips: vec![],
|
||||
destination_ips: vec![],
|
||||
source_ports: vec![],
|
||||
action: Action::Drop as i32,
|
||||
rate_limit: 0,
|
||||
burst_limit: 0,
|
||||
stateful: false,
|
||||
source_groups: vec![],
|
||||
destination_groups: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
if self.acl.is_none() {
|
||||
self.acl = Some(Acl::default());
|
||||
}
|
||||
|
||||
let acl = self.acl.as_mut().expect("ACL was initialized above");
|
||||
if let Some(acl_v1) = acl.acl_v1.as_mut() {
|
||||
acl_v1.chains.push(inbound_chain);
|
||||
} else {
|
||||
acl.acl_v1 = Some(AclV1 {
|
||||
chains: vec![inbound_chain],
|
||||
group: Some(GroupInfo {
|
||||
declares: vec![],
|
||||
members: vec![],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build(&self) -> anyhow::Result<Option<Acl>> {
|
||||
let mut config = self.clone();
|
||||
config.generate_acl_from_whitelists()?;
|
||||
Ok(config.acl)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PublicIpv6ProviderConfig {
|
||||
pub provider_enabled: bool,
|
||||
pub configured_prefix: Option<Ipv6Cidr>,
|
||||
pub provider_supported: bool,
|
||||
}
|
||||
|
||||
impl PublicIpv6ProviderConfig {
|
||||
pub fn should_run_reconcile(self) -> bool {
|
||||
self.provider_enabled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PeerRuntimeConfig {
|
||||
pub core: CoreConfig,
|
||||
pub network_identity: NetworkIdentity,
|
||||
pub stun_info: StunInfo,
|
||||
pub feature_flags: PeerFeatureFlag,
|
||||
pub secure_mode: Option<SecureModeConfig>,
|
||||
pub host_routing: HostRoutingPolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HostRoutingPolicy {
|
||||
/// Route otherwise-unreachable external IPv4 traffic through this node and
|
||||
/// keep self-delivered packets eligible for the host TUN/proxy path.
|
||||
pub local_exit_node_fallback: bool,
|
||||
}
|
||||
|
||||
/// One normalized peer configuration version submitted by a host.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PeerRuntimeSnapshot {
|
||||
pub runtime: PeerRuntimeConfig,
|
||||
pub easytier_version: String,
|
||||
pub avoid_relay_data_preference: bool,
|
||||
pub flags: FlagsInConfig,
|
||||
pub vpn_portal_cidr: Option<Ipv4Cidr>,
|
||||
pub pinned_peers: Vec<(url::Url, Option<String>)>,
|
||||
pub peer_group_memberships: Vec<PeerGroupIdentity>,
|
||||
pub acl_group_declarations: Vec<PeerGroupIdentity>,
|
||||
pub ospf_update_my_foreign_network_interval_sec: u64,
|
||||
pub max_direct_conns_per_peer_in_foreign_network: usize,
|
||||
pub hmac_secret_digest: bool,
|
||||
}
|
||||
|
||||
impl PeerRuntimeSnapshot {
|
||||
pub fn new(runtime: PeerRuntimeConfig, flags: FlagsInConfig) -> Self {
|
||||
let avoid_relay_data_preference = runtime.feature_flags.avoid_relay_data;
|
||||
Self {
|
||||
runtime,
|
||||
easytier_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
avoid_relay_data_preference,
|
||||
flags,
|
||||
vpn_portal_cidr: None,
|
||||
pinned_peers: Vec::new(),
|
||||
peer_group_memberships: Vec::new(),
|
||||
acl_group_declarations: Vec::new(),
|
||||
ospf_update_my_foreign_network_interval_sec: 10,
|
||||
max_direct_conns_per_peer_in_foreign_network: 3,
|
||||
hmac_secret_digest: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PeerRuntimeSnapshot {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
PeerRuntimeConfig {
|
||||
core: CoreConfig::default(),
|
||||
network_identity: NetworkIdentity::default(),
|
||||
stun_info: StunInfo::default(),
|
||||
feature_flags: PeerFeatureFlag::default(),
|
||||
secure_mode: None,
|
||||
host_routing: HostRoutingPolicy::default(),
|
||||
},
|
||||
FlagsInConfig::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PeerGroupIdentity {
|
||||
pub group_name: String,
|
||||
pub group_secret: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn whitelist_rules_are_built_in_core() {
|
||||
let acl = AclRuleConfig {
|
||||
tcp_whitelist: vec!["80".to_string(), "8000-9000".to_string()],
|
||||
udp_whitelist: vec!["53".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
.build()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let chain = &acl.acl_v1.unwrap().chains[0];
|
||||
assert_eq!(chain.name, "inbound_whitelist");
|
||||
assert_eq!(chain.rules.len(), 4);
|
||||
assert_eq!(chain.rules[0].ports, ["80", "8000-9000"]);
|
||||
assert_eq!(chain.rules[2].ports, ["53"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_whitelist_range_is_rejected() {
|
||||
let error = AclRuleConfig {
|
||||
tcp_whitelist: vec!["9000-8000".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
.build()
|
||||
.unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("Start port must be <= end port"));
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
//! Atomic runtime configuration owned by one core instance.
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use cidr::Ipv4Cidr;
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
gateway::{GatewayRuntimeConfig, ProxyRuntimeConfig},
|
||||
peers::{AclRuleConfig, PeerRuntimeSnapshot, PublicIpv6ProviderConfig},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CoreRuntimeConfig {
|
||||
pub acl: AclRuleConfig,
|
||||
pub dhcp_ipv4: bool,
|
||||
pub gateway: GatewayRuntimeConfig,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manual_routes: Option<BTreeSet<Ipv4Cidr>>,
|
||||
pub proxy: ProxyRuntimeConfig,
|
||||
#[serde(default)]
|
||||
pub public_ipv6_auto: bool,
|
||||
pub public_ipv6_provider: PublicIpv6ProviderConfig,
|
||||
}
|
||||
|
||||
impl Default for CoreRuntimeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
acl: AclRuleConfig::default(),
|
||||
dhcp_ipv4: false,
|
||||
gateway: GatewayRuntimeConfig::default(),
|
||||
manual_routes: None,
|
||||
proxy: ProxyRuntimeConfig::default(),
|
||||
public_ipv6_auto: false,
|
||||
public_ipv6_provider: PublicIpv6ProviderConfig {
|
||||
provider_enabled: false,
|
||||
configured_prefix: None,
|
||||
provider_supported: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoreInstanceRuntimeConfig {
|
||||
pub services: CoreRuntimeConfig,
|
||||
pub peer: Arc<PeerRuntimeSnapshot>,
|
||||
}
|
||||
|
||||
struct CoreRuntimeConfigStoreInner {
|
||||
snapshot: ArcSwap<CoreInstanceRuntimeConfig>,
|
||||
update: Mutex<()>,
|
||||
peer_changes: tokio::sync::watch::Sender<u64>,
|
||||
service_changes: tokio::sync::watch::Sender<u64>,
|
||||
}
|
||||
|
||||
/// Atomic configuration authority shared by one core instance and its peer
|
||||
/// context. Readers always observe a complete submitted version.
|
||||
#[derive(Clone)]
|
||||
pub struct CoreRuntimeConfigStore {
|
||||
inner: Arc<CoreRuntimeConfigStoreInner>,
|
||||
}
|
||||
|
||||
impl CoreRuntimeConfigStore {
|
||||
pub fn new(services: CoreRuntimeConfig, peer: Arc<PeerRuntimeSnapshot>) -> Self {
|
||||
let (peer_changes, _) = tokio::sync::watch::channel(0);
|
||||
let (service_changes, _) = tokio::sync::watch::channel(0);
|
||||
Self {
|
||||
inner: Arc::new(CoreRuntimeConfigStoreInner {
|
||||
snapshot: ArcSwap::from_pointee(CoreInstanceRuntimeConfig { services, peer }),
|
||||
update: Mutex::new(()),
|
||||
peer_changes,
|
||||
service_changes,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Arc<CoreInstanceRuntimeConfig> {
|
||||
self.inner.snapshot.load_full()
|
||||
}
|
||||
|
||||
pub(crate) fn with_snapshot<T>(&self, read: impl FnOnce(&CoreInstanceRuntimeConfig) -> T) -> T {
|
||||
let snapshot = self.inner.snapshot.load();
|
||||
read(&snapshot)
|
||||
}
|
||||
|
||||
pub fn replace(&self, config: CoreInstanceRuntimeConfig) {
|
||||
let _update = self.inner.update.lock();
|
||||
self.inner.snapshot.store(Arc::new(config));
|
||||
self.inner.peer_changes.send_modify(|version| *version += 1);
|
||||
self.inner
|
||||
.service_changes
|
||||
.send_modify(|version| *version += 1);
|
||||
}
|
||||
|
||||
pub(crate) fn replace_with_current(
|
||||
&self,
|
||||
mut config: CoreInstanceRuntimeConfig,
|
||||
merge: impl FnOnce(&CoreInstanceRuntimeConfig, &mut CoreInstanceRuntimeConfig),
|
||||
) -> Arc<CoreInstanceRuntimeConfig> {
|
||||
let _update = self.inner.update.lock();
|
||||
let current = self.inner.snapshot.load_full();
|
||||
merge(¤t, &mut config);
|
||||
let config = Arc::new(config);
|
||||
self.inner.snapshot.store(config.clone());
|
||||
self.inner.peer_changes.send_modify(|version| *version += 1);
|
||||
self.inner
|
||||
.service_changes
|
||||
.send_modify(|version| *version += 1);
|
||||
config
|
||||
}
|
||||
|
||||
pub fn update_services(&self, update: impl FnOnce(&mut CoreRuntimeConfig)) {
|
||||
let _update = self.inner.update.lock();
|
||||
let mut config = self.inner.snapshot.load_full().as_ref().clone();
|
||||
update(&mut config.services);
|
||||
self.inner.snapshot.store(Arc::new(config));
|
||||
self.inner
|
||||
.service_changes
|
||||
.send_modify(|version| *version += 1);
|
||||
}
|
||||
|
||||
pub fn update_peer(&self, peer: Arc<PeerRuntimeSnapshot>) {
|
||||
let _update = self.inner.update.lock();
|
||||
let mut config = self.inner.snapshot.load_full().as_ref().clone();
|
||||
config.peer = peer;
|
||||
self.inner.snapshot.store(Arc::new(config));
|
||||
self.inner.peer_changes.send_modify(|version| *version += 1);
|
||||
}
|
||||
|
||||
pub(crate) fn update_peer_with(&self, update: impl FnOnce(&mut PeerRuntimeSnapshot)) {
|
||||
let _update = self.inner.update.lock();
|
||||
let mut config = self.inner.snapshot.load_full().as_ref().clone();
|
||||
update(Arc::make_mut(&mut config.peer));
|
||||
self.inner.snapshot.store(Arc::new(config));
|
||||
self.inner.peer_changes.send_modify(|version| *version += 1);
|
||||
}
|
||||
|
||||
pub fn subscribe_peer_runtime_changes(&self) -> tokio::sync::watch::Receiver<u64> {
|
||||
self.inner.peer_changes.subscribe()
|
||||
}
|
||||
|
||||
pub fn subscribe_service_runtime_changes(&self) -> tokio::sync::watch::Receiver<u64> {
|
||||
self.inner.service_changes.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn replaces_service_and_peer_as_one_version() {
|
||||
let mut before_peer = PeerRuntimeSnapshot::default();
|
||||
before_peer.runtime.core.node.hostname = Some("before".to_owned());
|
||||
let store =
|
||||
CoreRuntimeConfigStore::new(CoreRuntimeConfig::default(), Arc::new(before_peer));
|
||||
let before = store.snapshot();
|
||||
|
||||
let after_services = CoreRuntimeConfig {
|
||||
dhcp_ipv4: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut after_peer = PeerRuntimeSnapshot::default();
|
||||
after_peer.runtime.core.node.hostname = Some("after".to_owned());
|
||||
store.replace(CoreInstanceRuntimeConfig {
|
||||
services: after_services,
|
||||
peer: Arc::new(after_peer),
|
||||
});
|
||||
|
||||
assert!(!before.services.dhcp_ipv4);
|
||||
assert_eq!(
|
||||
before.peer.runtime.core.node.hostname.as_deref(),
|
||||
Some("before")
|
||||
);
|
||||
let after = store.snapshot();
|
||||
assert!(after.services.dhcp_ipv4);
|
||||
assert_eq!(
|
||||
after.peer.runtime.core.node.hostname.as_deref(),
|
||||
Some("after")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notifies_peer_snapshot_changes() {
|
||||
let store = CoreRuntimeConfigStore::new(
|
||||
CoreRuntimeConfig::default(),
|
||||
Arc::new(PeerRuntimeSnapshot::default()),
|
||||
);
|
||||
let mut changes = store.subscribe_peer_runtime_changes();
|
||||
let mut peer = PeerRuntimeSnapshot::default();
|
||||
peer.runtime.core.node.hostname = Some("updated".to_owned());
|
||||
|
||||
store.update_peer(Arc::new(peer));
|
||||
|
||||
assert!(changes.changed().await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notifies_service_snapshot_changes() {
|
||||
let store = CoreRuntimeConfigStore::new(
|
||||
CoreRuntimeConfig::default(),
|
||||
Arc::new(PeerRuntimeSnapshot::default()),
|
||||
);
|
||||
let mut changes = store.subscribe_service_runtime_changes();
|
||||
|
||||
store.update_services(|services| services.dhcp_ipv4 = true);
|
||||
|
||||
assert!(changes.changed().await.is_ok());
|
||||
assert!(store.snapshot().services.dhcp_ipv4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn peer_update_does_not_notify_service_watchers() {
|
||||
let store = CoreRuntimeConfigStore::new(
|
||||
CoreRuntimeConfig::default(),
|
||||
Arc::new(PeerRuntimeSnapshot::default()),
|
||||
);
|
||||
let changes = store.subscribe_service_runtime_changes();
|
||||
let mut peer = PeerRuntimeSnapshot::default();
|
||||
peer.runtime.core.node.hostname = Some("updated".to_owned());
|
||||
|
||||
store.update_peer(Arc::new(peer));
|
||||
|
||||
assert!(!changes.has_changed().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_in_place_update_preserves_the_rest_of_the_atomic_snapshot() {
|
||||
let services = CoreRuntimeConfig {
|
||||
dhcp_ipv4: true,
|
||||
..Default::default()
|
||||
};
|
||||
let mut peer = PeerRuntimeSnapshot::default();
|
||||
peer.runtime.core.node.hostname = Some("preserved".to_owned());
|
||||
let store = CoreRuntimeConfigStore::new(services, Arc::new(peer));
|
||||
|
||||
store.update_peer_with(|peer| {
|
||||
peer.runtime.core.routes.ipv4 = Some(crate::config::IpPrefix {
|
||||
address: "10.20.30.7".parse().unwrap(),
|
||||
prefix_len: 24,
|
||||
});
|
||||
});
|
||||
|
||||
let snapshot = store.snapshot();
|
||||
assert!(snapshot.services.dhcp_ipv4);
|
||||
assert_eq!(
|
||||
snapshot.peer.runtime.core.node.hostname.as_deref(),
|
||||
Some("preserved")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.peer
|
||||
.runtime
|
||||
.core
|
||||
.routes
|
||||
.ipv4
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.address,
|
||||
"10.20.30.7".parse::<std::net::IpAddr>().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_manual_routes_preserves_portable_config_compatibility() {
|
||||
let encoded = serde_json::to_value(CoreRuntimeConfig::default()).unwrap();
|
||||
assert!(encoded.get("manual_routes").is_none());
|
||||
|
||||
let decoded: CoreRuntimeConfig = serde_json::from_value(encoded).unwrap();
|
||||
assert_eq!(decoded.manual_routes, None);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
use super::{Arc, Mutex, TomlConfig};
|
||||
|
||||
impl TomlConfig {
|
||||
pub(crate) fn detached_snapshot(&self) -> Self {
|
||||
let config = self.config.lock().unwrap().clone();
|
||||
Self {
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_from_snapshot(&self, snapshot: &Self) {
|
||||
let config = snapshot.config.lock().unwrap().clone();
|
||||
*self.config.lock().unwrap() = config;
|
||||
}
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
//! Composes process-wide socket capabilities with instance-scoped network facts.
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connectivity::{
|
||||
direct::DirectConnectorHost,
|
||||
manual::{ManualConnectorHost, ManualInterfaceAddrs},
|
||||
transport::ConnectedByteStream,
|
||||
},
|
||||
proto::peer_rpc::GetIpListResponse,
|
||||
socket::{
|
||||
SocketContext,
|
||||
tcp::{
|
||||
TcpConnectOptions, TcpListenOptions, VirtualTcpListenerFactory, VirtualTcpSocketFactory,
|
||||
},
|
||||
udp::{PreferredIpv6Source, UdpBindOptions, VirtualUdpSocketFactory},
|
||||
},
|
||||
};
|
||||
|
||||
const INTERFACE_ADDR_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedInterfaceAddrs {
|
||||
collected_at: Instant,
|
||||
response: GetIpListResponse,
|
||||
}
|
||||
|
||||
struct InterfaceAddrCacheEntry {
|
||||
context: SocketContext,
|
||||
value: Arc<tokio::sync::Mutex<Option<CachedInterfaceAddrs>>>,
|
||||
}
|
||||
|
||||
struct InterfaceAddrCache {
|
||||
entries: tokio::sync::Mutex<Vec<InterfaceAddrCacheEntry>>,
|
||||
ttl: Duration,
|
||||
}
|
||||
|
||||
impl InterfaceAddrCache {
|
||||
fn new(ttl: Duration) -> Self {
|
||||
Self {
|
||||
entries: tokio::sync::Mutex::new(Vec::new()),
|
||||
ttl,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_or_collect<F, Fut>(&self, context: &SocketContext, collect: F) -> GetIpListResponse
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = GetIpListResponse>,
|
||||
{
|
||||
let value = {
|
||||
let mut entries = self.entries.lock().await;
|
||||
entries.retain(|entry| {
|
||||
if Arc::strong_count(&entry.value) > 1 {
|
||||
return true;
|
||||
}
|
||||
entry.value.try_lock().map_or(true, |cached| {
|
||||
cached
|
||||
.as_ref()
|
||||
.is_some_and(|cached| cached.collected_at.elapsed() < self.ttl)
|
||||
})
|
||||
});
|
||||
if let Some(entry) = entries.iter().find(|entry| &entry.context == context) {
|
||||
entry.value.clone()
|
||||
} else {
|
||||
let value = Arc::new(tokio::sync::Mutex::new(None));
|
||||
entries.push(InterfaceAddrCacheEntry {
|
||||
context: context.clone(),
|
||||
value: value.clone(),
|
||||
});
|
||||
value
|
||||
}
|
||||
};
|
||||
|
||||
// Only collectors for the same socket context share this lock. A slow
|
||||
// namespace observation cannot block fresh hits or refreshes elsewhere.
|
||||
let mut cached = value.lock().await;
|
||||
if let Some(cached) = cached
|
||||
.as_ref()
|
||||
.filter(|cached| cached.collected_at.elapsed() < self.ttl)
|
||||
{
|
||||
return cached.response.clone();
|
||||
}
|
||||
|
||||
let response = collect().await;
|
||||
*cached = Some(CachedInterfaceAddrs {
|
||||
collected_at: Instant::now(),
|
||||
response: response.clone(),
|
||||
});
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
/// Mechanical connector operations supplied by one process-wide runtime.
|
||||
#[async_trait]
|
||||
pub trait ConnectorRuntime: VirtualTcpSocketFactory + Send + Sync + 'static {
|
||||
async fn connect_byte_stream(
|
||||
&self,
|
||||
url: &Url,
|
||||
) -> anyhow::Result<ConnectedByteStream<Self::Socket>>;
|
||||
|
||||
async fn local_addr_for_remote(
|
||||
&self,
|
||||
remote_addr: SocketAddr,
|
||||
context: SocketContext,
|
||||
) -> anyhow::Result<SocketAddr>;
|
||||
|
||||
async fn collect_ip_addrs(&self, context: &SocketContext) -> GetIpListResponse;
|
||||
|
||||
async fn preferred_ipv6_source(
|
||||
&self,
|
||||
ip: Ipv6Addr,
|
||||
context: SocketContext,
|
||||
) -> Option<PreferredIpv6Source>;
|
||||
}
|
||||
|
||||
/// Instance facts consumed by portable connector policy.
|
||||
///
|
||||
/// Socket creation, route probing and host interface I/O are deliberately
|
||||
/// absent; those belong to the process-wide [`ConnectorRuntime`].
|
||||
pub trait ConnectorEnvironment: Send + Sync + 'static {
|
||||
fn socket_context(&self) -> SocketContext;
|
||||
|
||||
fn mapped_listeners(&self) -> Vec<Url>;
|
||||
fn is_local_ip(&self, ip: &IpAddr) -> bool;
|
||||
}
|
||||
|
||||
/// Deep adapter that combines one socket runtime with one instance environment.
|
||||
pub struct ConnectorHostAdapter<S, E> {
|
||||
sockets: Arc<S>,
|
||||
environment: Arc<E>,
|
||||
interface_addrs: InterfaceAddrCache,
|
||||
}
|
||||
|
||||
impl<S, E> ConnectorHostAdapter<S, E> {
|
||||
pub fn new(sockets: Arc<S>, environment: Arc<E>) -> Self {
|
||||
Self {
|
||||
sockets,
|
||||
environment,
|
||||
interface_addrs: InterfaceAddrCache::new(INTERFACE_ADDR_CACHE_TTL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: ConnectorRuntime,
|
||||
{
|
||||
async fn cached_ip_addrs(&self, context: &SocketContext) -> GetIpListResponse {
|
||||
self.interface_addrs
|
||||
.get_or_collect(context, || self.sockets.collect_ip_addrs(context))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, E> VirtualTcpSocketFactory for ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: VirtualTcpSocketFactory,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
type Socket = S::Socket;
|
||||
|
||||
async fn connect_tcp(&self, options: TcpConnectOptions) -> anyhow::Result<Self::Socket> {
|
||||
self.sockets.connect_tcp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, E> VirtualTcpListenerFactory for ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: VirtualTcpListenerFactory,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
type Listener = S::Listener;
|
||||
|
||||
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>> {
|
||||
self.sockets.bind_tcp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, E> VirtualUdpSocketFactory for ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: VirtualUdpSocketFactory,
|
||||
E: Send + Sync + 'static,
|
||||
{
|
||||
type Socket = S::Socket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.sockets.bind_udp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, E> ManualConnectorHost for ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: ConnectorRuntime + VirtualUdpSocketFactory,
|
||||
E: ConnectorEnvironment,
|
||||
{
|
||||
async fn local_addr_for_remote(
|
||||
&self,
|
||||
remote_addr: SocketAddr,
|
||||
context: SocketContext,
|
||||
) -> anyhow::Result<SocketAddr> {
|
||||
self.sockets
|
||||
.local_addr_for_remote(remote_addr, context)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn interface_addrs(&self) -> anyhow::Result<ManualInterfaceAddrs> {
|
||||
let addrs = self
|
||||
.cached_ip_addrs(&self.environment.socket_context())
|
||||
.await;
|
||||
Ok(ManualInterfaceAddrs {
|
||||
interface_ipv4s: addrs
|
||||
.interface_ipv4s
|
||||
.into_iter()
|
||||
.map(std::net::Ipv4Addr::from)
|
||||
.collect(),
|
||||
interface_ipv6s: addrs
|
||||
.interface_ipv6s
|
||||
.into_iter()
|
||||
.map(std::net::Ipv6Addr::from)
|
||||
.collect(),
|
||||
public_ipv6: addrs.public_ipv6.map(std::net::Ipv6Addr::from),
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_byte_stream(
|
||||
&self,
|
||||
url: &Url,
|
||||
) -> anyhow::Result<ConnectedByteStream<<Self as VirtualTcpSocketFactory>::Socket>> {
|
||||
self.sockets.connect_byte_stream(url).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S, E> DirectConnectorHost for ConnectorHostAdapter<S, E>
|
||||
where
|
||||
S: ConnectorRuntime + VirtualUdpSocketFactory,
|
||||
E: ConnectorEnvironment,
|
||||
{
|
||||
async fn collect_ip_addrs(&self, context: &SocketContext) -> GetIpListResponse {
|
||||
self.cached_ip_addrs(context).await
|
||||
}
|
||||
|
||||
async fn collect_foreign_ip_addrs(&self, context: &SocketContext) -> GetIpListResponse {
|
||||
self.cached_ip_addrs(context).await
|
||||
}
|
||||
|
||||
fn mapped_listeners(&self) -> Vec<Url> {
|
||||
self.environment.mapped_listeners()
|
||||
}
|
||||
|
||||
fn is_local_ip(&self, ip: &IpAddr) -> bool {
|
||||
self.environment.is_local_ip(ip)
|
||||
}
|
||||
|
||||
async fn preferred_ipv6_source(
|
||||
&self,
|
||||
ip: Ipv6Addr,
|
||||
context: SocketContext,
|
||||
) -> Option<PreferredIpv6Source> {
|
||||
if !valid_public_ipv6_candidate(ip) {
|
||||
return None;
|
||||
}
|
||||
self.sockets.preferred_ipv6_source(ip, context).await
|
||||
}
|
||||
|
||||
async fn preferred_foreign_ipv6_source(
|
||||
&self,
|
||||
ip: Ipv6Addr,
|
||||
context: SocketContext,
|
||||
) -> Option<PreferredIpv6Source> {
|
||||
if !valid_public_ipv6_candidate(ip) {
|
||||
return None;
|
||||
}
|
||||
self.sockets.preferred_ipv6_source(ip, context).await
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_public_ipv6_candidate(ip: Ipv6Addr) -> bool {
|
||||
!(ip.is_loopback()
|
||||
|| ip.is_unspecified()
|
||||
|| ip.is_unique_local()
|
||||
|| ip.is_unicast_link_local()
|
||||
|| ip.is_multicast())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use super::*;
|
||||
use crate::socket::{IpVersion, NetNamespace};
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_cache_misses_share_one_collection() {
|
||||
let cache = Arc::new(InterfaceAddrCache::new(Duration::from_secs(60)));
|
||||
let context = SocketContext::default();
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let (started_tx, started_rx) = oneshot::channel();
|
||||
let (release_tx, release_rx) = oneshot::channel();
|
||||
|
||||
let first = tokio::spawn({
|
||||
let cache = cache.clone();
|
||||
let context = context.clone();
|
||||
let calls = calls.clone();
|
||||
async move {
|
||||
cache
|
||||
.get_or_collect(&context, || async move {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
let _ = started_tx.send(());
|
||||
let _ = release_rx.await;
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await
|
||||
}
|
||||
});
|
||||
started_rx.await.unwrap();
|
||||
|
||||
let (second_started_tx, second_started_rx) = oneshot::channel();
|
||||
let second = tokio::spawn({
|
||||
let cache = cache.clone();
|
||||
let context = context.clone();
|
||||
let calls = calls.clone();
|
||||
async move {
|
||||
let _ = second_started_tx.send(());
|
||||
cache
|
||||
.get_or_collect(&context, || async move {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await
|
||||
}
|
||||
});
|
||||
second_started_rx.await.unwrap();
|
||||
tokio::task::yield_now().await;
|
||||
let _ = release_tx.send(());
|
||||
|
||||
first.await.unwrap();
|
||||
second.await.unwrap();
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cache_keys_include_the_complete_socket_context() {
|
||||
let cache = InterfaceAddrCache::new(Duration::from_secs(60));
|
||||
let calls = AtomicUsize::new(0);
|
||||
let contexts = [
|
||||
SocketContext::default(),
|
||||
SocketContext::default().with_ip_version(IpVersion::V4),
|
||||
SocketContext::default().with_socket_mark(Some(7)),
|
||||
SocketContext::default().with_netns(Some(NetNamespace::new("foreign-a"))),
|
||||
];
|
||||
|
||||
for context in &contexts {
|
||||
cache
|
||||
.get_or_collect(context, || async {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
cache
|
||||
.get_or_collect(&contexts[0], || async {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(calls.load(Ordering::SeqCst), contexts.len());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slow_collection_does_not_block_a_different_context() {
|
||||
let cache = Arc::new(InterfaceAddrCache::new(Duration::from_secs(60)));
|
||||
let slow_context = SocketContext::default().with_socket_mark(Some(1));
|
||||
let other_context = SocketContext::default().with_socket_mark(Some(2));
|
||||
let (started_tx, started_rx) = oneshot::channel();
|
||||
let (release_tx, release_rx) = oneshot::channel();
|
||||
let slow = tokio::spawn({
|
||||
let cache = cache.clone();
|
||||
async move {
|
||||
cache
|
||||
.get_or_collect(&slow_context, || async move {
|
||||
let _ = started_tx.send(());
|
||||
let _ = release_rx.await;
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await
|
||||
}
|
||||
});
|
||||
started_rx.await.unwrap();
|
||||
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
cache.get_or_collect(&other_context, || async { GetIpListResponse::default() }),
|
||||
)
|
||||
.await
|
||||
.expect("different socket contexts must collect independently");
|
||||
|
||||
let _ = release_tx.send(());
|
||||
slow.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slow_collection_does_not_block_a_fresh_hit() {
|
||||
let cache = Arc::new(InterfaceAddrCache::new(Duration::from_secs(60)));
|
||||
let cached_context = SocketContext::default().with_socket_mark(Some(1));
|
||||
let slow_context = SocketContext::default().with_socket_mark(Some(2));
|
||||
cache
|
||||
.get_or_collect(&cached_context, || async { GetIpListResponse::default() })
|
||||
.await;
|
||||
|
||||
let (started_tx, started_rx) = oneshot::channel();
|
||||
let (release_tx, release_rx) = oneshot::channel();
|
||||
let slow = tokio::spawn({
|
||||
let cache = cache.clone();
|
||||
async move {
|
||||
cache
|
||||
.get_or_collect(&slow_context, || async move {
|
||||
let _ = started_tx.send(());
|
||||
let _ = release_rx.await;
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await
|
||||
}
|
||||
});
|
||||
started_rx.await.unwrap();
|
||||
|
||||
tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
cache.get_or_collect(&cached_context, || async {
|
||||
panic!("fresh cache hit must not recollect")
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("fresh hit must not wait for another socket context");
|
||||
|
||||
let _ = release_tx.send(());
|
||||
slow.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expired_entries_are_recollected() {
|
||||
let cache = InterfaceAddrCache::new(Duration::ZERO);
|
||||
let calls = AtomicUsize::new(0);
|
||||
let context = SocketContext::default();
|
||||
|
||||
for _ in 0..2 {
|
||||
cache
|
||||
.get_or_collect(&context, || async {
|
||||
calls.fetch_add(1, Ordering::SeqCst);
|
||||
GetIpListResponse::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
}
|
||||
@@ -1,708 +0,0 @@
|
||||
//! Host-operation Adapter for the shared connector composition.
|
||||
//!
|
||||
//! This module was previously named `host`, which collided with the
|
||||
//! top-level [`crate::host`] layer, and one concept was split across two
|
||||
//! same-named `environment.rs` files. The ownership split is:
|
||||
//!
|
||||
//! - Mechanical environment queries implement
|
||||
//! [`crate::host::environment::HostConnectorEnvironmentIo`].
|
||||
//! - [`HostConnectorRuntime`] adapts host socket/listener factories and an
|
||||
//! injected environment snapshot to the shared connector runtime and
|
||||
//! environment traits.
|
||||
//! - [`HostConnectorEnvironmentSnapshot`] is connectivity's captured view of
|
||||
//! the host environment.
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connectivity::{
|
||||
composite::{ConnectorEnvironment, ConnectorHostAdapter, ConnectorRuntime},
|
||||
transport::ConnectedByteStream,
|
||||
},
|
||||
host::environment::{HostConnectorEnvironmentIo, local_addr_for_remote},
|
||||
host::socket::{
|
||||
HostSocketRuntime, HostTcpStream,
|
||||
factory::{HostSocketBackend, HostSocketFactory},
|
||||
listener::{HostTcpListener, HostTcpListenerBackend, HostTcpListenerFactory},
|
||||
udp::HostUdpSocket,
|
||||
},
|
||||
proto::peer_rpc::GetIpListResponse,
|
||||
socket::{
|
||||
SocketContext,
|
||||
tcp::{
|
||||
TcpConnectOptions, TcpListenOptions, VirtualTcpListenerFactory, VirtualTcpSocketFactory,
|
||||
},
|
||||
udp::{PreferredIpv6Source, UdpBindOptions, VirtualUdpSocketFactory},
|
||||
},
|
||||
};
|
||||
|
||||
/// Host-observed facts consumed by core connector policy.
|
||||
///
|
||||
/// The host normalizes this snapshot before constructing an instance. Core
|
||||
/// owns all selection and connection policy applied to these facts.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct HostConnectorEnvironmentSnapshot {
|
||||
pub public_ipv4: Option<Ipv4Addr>,
|
||||
pub interface_ipv4s: Vec<Ipv4Addr>,
|
||||
pub public_ipv6: Option<Ipv6Addr>,
|
||||
pub interface_ipv6s: Vec<Ipv6Addr>,
|
||||
pub mapped_listeners: Vec<Url>,
|
||||
pub local_ips: Vec<IpAddr>,
|
||||
pub protected_tcp_ports: Vec<u16>,
|
||||
pub preferred_ipv6_sources: Vec<PreferredIpv6Source>,
|
||||
}
|
||||
|
||||
impl HostConnectorEnvironmentSnapshot {
|
||||
fn ip_list(&self) -> GetIpListResponse {
|
||||
GetIpListResponse {
|
||||
public_ipv4: self.public_ipv4.map(Into::into),
|
||||
interface_ipv4s: self
|
||||
.interface_ipv4s
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
public_ipv6: self.public_ipv6.map(Into::into),
|
||||
interface_ipv6s: self
|
||||
.interface_ipv6s
|
||||
.iter()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
listeners: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_ipv6_source(&self, ip: Ipv6Addr) -> Option<PreferredIpv6Source> {
|
||||
if ip.is_loopback()
|
||||
|| ip.is_unspecified()
|
||||
|| ip.is_unique_local()
|
||||
|| ip.is_unicast_link_local()
|
||||
|| ip.is_multicast()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
self.preferred_ipv6_sources
|
||||
.iter()
|
||||
.find(|source| source.ip == ip)
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// One host handle domain capable of creating and operating connector sockets.
|
||||
pub trait ConnectorHostSocketBackend: HostSocketBackend + HostTcpListenerBackend {}
|
||||
|
||||
impl<T> ConnectorHostSocketBackend for T where T: HostSocketBackend + HostTcpListenerBackend {}
|
||||
|
||||
/// Adapts mechanical host sockets and captured environment state to the
|
||||
/// shared connector composition.
|
||||
pub struct HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
socket_runtime: HostSocketRuntime,
|
||||
sockets: HostSocketFactory<B>,
|
||||
listeners: HostTcpListenerFactory<B>,
|
||||
environment: Arc<HostConnectorEnvironmentSnapshot>,
|
||||
environment_io: Arc<E>,
|
||||
}
|
||||
|
||||
impl<B, E> HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
pub fn new(
|
||||
runtime: HostSocketRuntime,
|
||||
backend: Arc<B>,
|
||||
environment: HostConnectorEnvironmentSnapshot,
|
||||
environment_io: Arc<E>,
|
||||
) -> Self {
|
||||
Self {
|
||||
socket_runtime: runtime.clone(),
|
||||
sockets: HostSocketFactory::new(runtime.clone(), backend.clone()),
|
||||
listeners: HostTcpListenerFactory::new(runtime, backend),
|
||||
environment: Arc::new(environment),
|
||||
environment_io,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, E> VirtualTcpSocketFactory for HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
type Socket = HostTcpStream;
|
||||
|
||||
async fn connect_tcp(&self, options: TcpConnectOptions) -> anyhow::Result<Self::Socket> {
|
||||
self.sockets.connect_tcp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, E> VirtualUdpSocketFactory for HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
type Socket = HostUdpSocket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.sockets.bind_udp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, E> VirtualTcpListenerFactory for HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
type Listener = HostTcpListener<B>;
|
||||
|
||||
async fn bind_tcp(&self, options: TcpListenOptions) -> anyhow::Result<Arc<Self::Listener>> {
|
||||
self.listeners.bind_tcp(options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, E> ConnectorRuntime for HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
async fn connect_byte_stream(
|
||||
&self,
|
||||
url: &Url,
|
||||
) -> anyhow::Result<ConnectedByteStream<Self::Socket>> {
|
||||
anyhow::bail!("host does not support external byte stream: {url}")
|
||||
}
|
||||
|
||||
async fn local_addr_for_remote(
|
||||
&self,
|
||||
remote_addr: SocketAddr,
|
||||
context: SocketContext,
|
||||
) -> anyhow::Result<SocketAddr> {
|
||||
local_addr_for_remote(
|
||||
&self.socket_runtime,
|
||||
self.environment_io.clone(),
|
||||
remote_addr,
|
||||
context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn collect_ip_addrs(&self, _context: &SocketContext) -> GetIpListResponse {
|
||||
self.environment.ip_list()
|
||||
}
|
||||
|
||||
async fn preferred_ipv6_source(
|
||||
&self,
|
||||
ip: Ipv6Addr,
|
||||
_context: SocketContext,
|
||||
) -> Option<PreferredIpv6Source> {
|
||||
self.environment.preferred_ipv6_source(ip)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, E> ConnectorEnvironment for HostConnectorRuntime<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
fn socket_context(&self) -> SocketContext {
|
||||
SocketContext::default()
|
||||
}
|
||||
|
||||
fn mapped_listeners(&self) -> Vec<Url> {
|
||||
self.environment.mapped_listeners.clone()
|
||||
}
|
||||
|
||||
fn is_local_ip(&self, ip: &IpAddr) -> bool {
|
||||
self.environment.local_ips.contains(ip)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared connector policy composed with a host-operation runtime.
|
||||
pub type ConnectorHost<B, E> =
|
||||
ConnectorHostAdapter<HostConnectorRuntime<B, E>, HostConnectorRuntime<B, E>>;
|
||||
|
||||
/// Builds the shared connector host over one host-operation backend.
|
||||
pub fn new_connector_host<B, E>(
|
||||
socket_runtime: HostSocketRuntime,
|
||||
backend: Arc<B>,
|
||||
environment: HostConnectorEnvironmentSnapshot,
|
||||
environment_io: Arc<E>,
|
||||
) -> ConnectorHost<B, E>
|
||||
where
|
||||
B: ConnectorHostSocketBackend,
|
||||
E: HostConnectorEnvironmentIo,
|
||||
{
|
||||
let runtime = Arc::new(HostConnectorRuntime::new(
|
||||
socket_runtime,
|
||||
backend,
|
||||
environment,
|
||||
environment_io,
|
||||
));
|
||||
ConnectorHostAdapter::new(runtime.clone(), runtime)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
connectivity::{
|
||||
direct::{DirectConnectorHost, DirectConnectorRpcHandler},
|
||||
hole_punch::tcp::TcpHolePunchHost,
|
||||
manual::ManualConnectorHost,
|
||||
stun::StunInfoProvider,
|
||||
},
|
||||
host::socket::{
|
||||
HostOperationId, HostSocketHandle, HostSocketIo, HostTcpIo,
|
||||
factory::{HostSocketFactoryIo, HostTcpConnectResult, HostUdpBindResult},
|
||||
listener::{HostTcpBindResult, HostTcpListenerIo},
|
||||
udp::{HostUdpDatagram, HostUdpIo},
|
||||
},
|
||||
proto::{
|
||||
common::StunInfo,
|
||||
peer_rpc::{DirectConnectorRpc as _, GetIpListRequest},
|
||||
rpc_types::controller::BaseController,
|
||||
},
|
||||
socket::udp::UdpSocketSendMeta,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct UnsupportedBackend {
|
||||
udp_send_attempts: Mutex<Vec<(Vec<u8>, SocketAddr, UdpSocketSendMeta)>>,
|
||||
reject_preferred_source: AtomicBool,
|
||||
}
|
||||
|
||||
struct FixedStunProvider;
|
||||
|
||||
#[async_trait]
|
||||
impl StunInfoProvider for FixedStunProvider {
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
StunInfo {
|
||||
public_ip: vec!["198.51.100.7".to_owned(), "2001:db8::1".to_owned()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, _local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
anyhow::bail!("unused by direct RPC projection test")
|
||||
}
|
||||
|
||||
async fn get_tcp_port_mapping(&self, _local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
anyhow::bail!("unused by direct RPC projection test")
|
||||
}
|
||||
|
||||
fn update_stun_info(&self) {}
|
||||
}
|
||||
|
||||
fn unsupported<T>() -> io::Result<T> {
|
||||
Err(io::ErrorKind::Unsupported.into())
|
||||
}
|
||||
|
||||
impl HostSocketIo for UnsupportedBackend {
|
||||
fn cancel_operation(&self, _operation: HostOperationId) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&self, _handle: HostSocketHandle) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTcpIo for UnsupportedBackend {
|
||||
fn submit_read(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
_operation: HostOperationId,
|
||||
_capacity: usize,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_read(&self, _operation: HostOperationId) -> Poll<io::Result<Vec<u8>>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
|
||||
fn submit_write(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
_operation: HostOperationId,
|
||||
_source: &[u8],
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_write(&self, _operation: HostOperationId) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
}
|
||||
|
||||
impl HostUdpIo for UnsupportedBackend {
|
||||
fn submit_recv(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
_operation: HostOperationId,
|
||||
_capacity: usize,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_recv(&self, _operation: HostOperationId) -> Poll<io::Result<HostUdpDatagram>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
|
||||
fn try_send(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
source: &[u8],
|
||||
peer_addr: SocketAddr,
|
||||
meta: UdpSocketSendMeta,
|
||||
) -> io::Result<()> {
|
||||
self.udp_send_attempts
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((source.to_vec(), peer_addr, meta));
|
||||
if self.reject_preferred_source.load(Ordering::Relaxed) && meta.src_ip.is_some() {
|
||||
return Err(io::ErrorKind::AddrNotAvailable.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn submit_send_ready(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
_operation: HostOperationId,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_send_ready(&self, _operation: HostOperationId) -> Poll<io::Result<()>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
}
|
||||
|
||||
impl HostSocketFactoryIo for UnsupportedBackend {
|
||||
fn submit_tcp_connect(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
_options: &TcpConnectOptions,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_tcp_connect(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
) -> Poll<io::Result<HostTcpConnectResult>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
|
||||
fn submit_udp_bind(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
_options: &UdpBindOptions,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_udp_bind(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
) -> Poll<io::Result<HostUdpBindResult>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTcpListenerIo for UnsupportedBackend {
|
||||
fn submit_tcp_bind(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
_options: &TcpListenOptions,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_tcp_bind(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
) -> Poll<io::Result<HostTcpBindResult>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
|
||||
fn submit_tcp_accept(
|
||||
&self,
|
||||
_handle: HostSocketHandle,
|
||||
_operation: HostOperationId,
|
||||
) -> io::Result<()> {
|
||||
unsupported()
|
||||
}
|
||||
|
||||
fn take_tcp_accept(
|
||||
&self,
|
||||
_operation: HostOperationId,
|
||||
) -> Poll<io::Result<HostTcpConnectResult>> {
|
||||
Poll::Ready(unsupported())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestEnvironmentIo {
|
||||
local_requests: Mutex<Vec<(SocketAddr, SocketContext)>>,
|
||||
ready: Mutex<Vec<HostOperationId>>,
|
||||
}
|
||||
|
||||
impl HostConnectorEnvironmentIo for TestEnvironmentIo {
|
||||
fn submit_local_addr_for_remote(
|
||||
&self,
|
||||
operation: HostOperationId,
|
||||
remote_addr: SocketAddr,
|
||||
context: &SocketContext,
|
||||
) -> io::Result<()> {
|
||||
self.local_requests
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((remote_addr, context.clone()));
|
||||
self.ready.lock().unwrap().push(operation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_local_addr_for_remote(
|
||||
&self,
|
||||
operation: HostOperationId,
|
||||
) -> Poll<io::Result<SocketAddr>> {
|
||||
let mut ready = self.ready.lock().unwrap();
|
||||
let Some(index) = ready.iter().position(|candidate| *candidate == operation) else {
|
||||
return Poll::Pending;
|
||||
};
|
||||
ready.swap_remove(index);
|
||||
Poll::Ready(Ok("192.0.2.1:40100".parse().unwrap()))
|
||||
}
|
||||
|
||||
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
|
||||
self.ready
|
||||
.lock()
|
||||
.unwrap()
|
||||
.retain(|candidate| *candidate != operation);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_environment_snapshot() -> HostConnectorEnvironmentSnapshot {
|
||||
HostConnectorEnvironmentSnapshot {
|
||||
interface_ipv4s: vec!["192.0.2.1".parse().unwrap()],
|
||||
public_ipv6: Some("2001:db8::1".parse().unwrap()),
|
||||
interface_ipv6s: vec!["2001:db8::1".parse().unwrap()],
|
||||
mapped_listeners: vec!["tcp://192.0.2.1:11010".parse().unwrap()],
|
||||
local_ips: vec!["192.0.2.1".parse().unwrap()],
|
||||
protected_tcp_ports: vec![11010],
|
||||
preferred_ipv6_sources: vec![PreferredIpv6Source {
|
||||
ip: "2001:db8::1".parse().unwrap(),
|
||||
ifindex: 7,
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_core_host<H>()
|
||||
where
|
||||
H: DirectConnectorHost + TcpHolePunchHost,
|
||||
{
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delegates_connector_environment_without_owning_policy() {
|
||||
type TestHost = ConnectorHost<UnsupportedBackend, TestEnvironmentIo>;
|
||||
assert_core_host::<TestHost>();
|
||||
|
||||
let environment_io = Arc::new(TestEnvironmentIo::default());
|
||||
let host = new_connector_host(
|
||||
HostSocketRuntime::new(),
|
||||
Arc::new(UnsupportedBackend::default()),
|
||||
test_environment_snapshot(),
|
||||
environment_io.clone(),
|
||||
);
|
||||
let remote = "203.0.113.1:11010".parse().unwrap();
|
||||
let context = SocketContext::default().with_socket_mark(Some(7));
|
||||
let local = ManualConnectorHost::local_addr_for_remote(&host, remote, context.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(local, "192.0.2.1:40100".parse().unwrap());
|
||||
assert_eq!(
|
||||
*environment_io.local_requests.lock().unwrap(),
|
||||
vec![(remote, context)]
|
||||
);
|
||||
assert_eq!(
|
||||
ManualConnectorHost::interface_addrs(&host)
|
||||
.await
|
||||
.unwrap()
|
||||
.public_ipv6,
|
||||
Some("2001:db8::1".parse().unwrap())
|
||||
);
|
||||
let byte_stream_error =
|
||||
match ManualConnectorHost::connect_byte_stream(&host, &"ring://42".parse().unwrap())
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("test environment should reject byte streams"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert_eq!(
|
||||
byte_stream_error.to_string(),
|
||||
"host does not support external byte stream: ring://42"
|
||||
);
|
||||
assert_eq!(
|
||||
DirectConnectorHost::mapped_listeners(&host),
|
||||
vec!["tcp://192.0.2.1:11010".parse::<Url>().unwrap()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn direct_rpc_projects_host_observations_without_instance_policy() {
|
||||
let host = Arc::new(new_connector_host(
|
||||
HostSocketRuntime::new(),
|
||||
Arc::new(UnsupportedBackend::default()),
|
||||
test_environment_snapshot(),
|
||||
Arc::new(TestEnvironmentIo::default()),
|
||||
));
|
||||
let handler = DirectConnectorRpcHandler::new_with_stun(
|
||||
host,
|
||||
SocketContext::default().with_socket_mark(Some(7)),
|
||||
Some(Arc::new(FixedStunProvider)),
|
||||
);
|
||||
|
||||
let response = handler
|
||||
.get_ip_list(BaseController::default(), GetIpListRequest {})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response.interface_ipv4s,
|
||||
vec![std::net::Ipv4Addr::new(192, 0, 2, 1).into()]
|
||||
);
|
||||
assert_eq!(
|
||||
response.interface_ipv6s,
|
||||
vec!["2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap().into()]
|
||||
);
|
||||
assert_eq!(
|
||||
response.public_ipv4,
|
||||
Some("198.51.100.7".parse::<std::net::Ipv4Addr>().unwrap().into())
|
||||
);
|
||||
assert_eq!(
|
||||
response.public_ipv6,
|
||||
Some("2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap().into())
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.listeners
|
||||
.into_iter()
|
||||
.map(Url::from)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["tcp://192.0.2.1:11010".parse::<Url>().unwrap()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn foreign_direct_rpc_preserves_parent_managed_ipv6_addresses() {
|
||||
let host = Arc::new(new_connector_host(
|
||||
HostSocketRuntime::new(),
|
||||
Arc::new(UnsupportedBackend::default()),
|
||||
test_environment_snapshot(),
|
||||
Arc::new(TestEnvironmentIo::default()),
|
||||
));
|
||||
let handler = DirectConnectorRpcHandler::new_for_foreign_network_with_stun(
|
||||
host,
|
||||
SocketContext::default().with_socket_mark(Some(7)),
|
||||
Some(Arc::new(FixedStunProvider)),
|
||||
);
|
||||
|
||||
let response = handler
|
||||
.get_ip_list(BaseController::default(), GetIpListRequest {})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response.interface_ipv6s,
|
||||
vec!["2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap().into()]
|
||||
);
|
||||
assert_eq!(
|
||||
response.public_ipv6,
|
||||
Some("2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap().into())
|
||||
);
|
||||
assert_eq!(
|
||||
response.public_ipv4,
|
||||
Some("198.51.100.7".parse::<std::net::Ipv4Addr>().unwrap().into())
|
||||
);
|
||||
}
|
||||
|
||||
fn snapshot() -> HostConnectorEnvironmentSnapshot {
|
||||
HostConnectorEnvironmentSnapshot {
|
||||
public_ipv4: Some("198.51.100.1".parse().unwrap()),
|
||||
interface_ipv4s: vec!["192.0.2.1".parse().unwrap()],
|
||||
public_ipv6: Some("2001:db8::1".parse().unwrap()),
|
||||
interface_ipv6s: vec!["2001:db8::2".parse().unwrap()],
|
||||
mapped_listeners: vec!["tcp://198.51.100.1:11010".parse().unwrap()],
|
||||
local_ips: vec!["192.0.2.1".parse().unwrap()],
|
||||
protected_tcp_ports: vec![11010],
|
||||
preferred_ipv6_sources: vec![
|
||||
PreferredIpv6Source {
|
||||
ip: "2001:db8::2".parse().unwrap(),
|
||||
ifindex: 7,
|
||||
},
|
||||
PreferredIpv6Source {
|
||||
ip: "fd00::1".parse().unwrap(),
|
||||
ifindex: 8,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projects_normalized_snapshot() {
|
||||
let snapshot = snapshot();
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<HostConnectorEnvironmentSnapshot>(
|
||||
&serde_json::to_vec(&snapshot).unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
snapshot
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.ip_list().interface_ipv4s,
|
||||
vec![Ipv4Addr::new(192, 0, 2, 1).into()]
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.preferred_ipv6_source("2001:db8::2".parse().unwrap()),
|
||||
Some(PreferredIpv6Source {
|
||||
ip: "2001:db8::2".parse().unwrap(),
|
||||
ifindex: 7,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.preferred_ipv6_source("fd00::1".parse().unwrap()),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
connectivity::transport::ConnectedUdpSession,
|
||||
socket::udp::{UdpSessionLayer, VirtualUdpSocketFactory},
|
||||
};
|
||||
|
||||
use super::DirectConnectorHost;
|
||||
|
||||
pub(super) async fn connect_with_socket<H>(
|
||||
host: Arc<H>,
|
||||
socket: Arc<<H as VirtualUdpSocketFactory>::Socket>,
|
||||
remote_addr: SocketAddr,
|
||||
) -> anyhow::Result<ConnectedUdpSession>
|
||||
where
|
||||
H: DirectConnectorHost,
|
||||
{
|
||||
let layer = Arc::new(UdpSessionLayer::new_with_stun_responder(socket, host));
|
||||
let session = layer.connect(remote_addr).await?;
|
||||
Ok(ConnectedUdpSession::new(session, layer))
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::proto::rpc_types::{controller::BaseController, handler::Handler};
|
||||
use crate::tunnel::Tunnel;
|
||||
|
||||
mod peer_adapters;
|
||||
pub(crate) mod policy;
|
||||
pub mod port_mapping;
|
||||
pub(crate) mod tcp;
|
||||
pub(crate) mod udp;
|
||||
|
||||
/// Registration seam for hole-punch RPC services.
|
||||
///
|
||||
/// The engines build the proto-generated server wrapper around their RPC
|
||||
/// endpoint; the implementation owns the peer RPC registry and the network
|
||||
/// domain the service is registered under. Implemented only by the sealed
|
||||
/// peer adapter in `peer_adapters.rs`.
|
||||
pub(crate) trait HolePunchRpcRegistry: Send + Sync + 'static {
|
||||
fn register_rpc_service<H>(&self, service: H)
|
||||
where
|
||||
H: Handler<Controller = BaseController>;
|
||||
|
||||
fn unregister_rpc_service<H>(&self, service: H)
|
||||
where
|
||||
H: Handler<Controller = BaseController>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait HolePunchTunnelSink: Send + Sync + 'static {
|
||||
async fn add_client_tunnel(&self, tunnel: Box<dyn Tunnel>) -> anyhow::Result<()>;
|
||||
|
||||
async fn add_server_tunnel(&self, tunnel: Box<dyn Tunnel>) -> anyhow::Result<()>;
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use quanta::Instant;
|
||||
|
||||
use crate::{
|
||||
config::{P2pPolicyFlags, PeerId},
|
||||
foundation::task::ExternalTaskSignal,
|
||||
peers::peer_manager::PeerManagerCore,
|
||||
proto::{
|
||||
common::NatType,
|
||||
peer_rpc::{
|
||||
TcpHolePunchRpc, TcpHolePunchRpcClientFactory, UdpHolePunchRpc,
|
||||
UdpHolePunchRpcClientFactory,
|
||||
},
|
||||
rpc_types::{controller::BaseController, handler::Handler},
|
||||
},
|
||||
tunnel::Tunnel,
|
||||
};
|
||||
|
||||
use super::{
|
||||
HolePunchRpcRegistry, HolePunchTunnelSink,
|
||||
tcp::{TcpHolePunchPeerSource, TcpPunchCandidate},
|
||||
udp::{UdpHolePunchPeerSource, UdpHolePunchRpcSource, UdpPunchCandidate},
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl UdpHolePunchPeerSource for PeerManagerCore {
|
||||
fn local_peer_id(&self) -> PeerId {
|
||||
PeerManagerCore::my_peer_id(self)
|
||||
}
|
||||
|
||||
fn p2p_policy_flags(&self) -> P2pPolicyFlags {
|
||||
PeerManagerCore::p2p_policy_flags(self)
|
||||
}
|
||||
|
||||
async fn candidates(&self) -> Vec<UdpPunchCandidate> {
|
||||
let now = Instant::now();
|
||||
let peer_map = self.get_peer_map();
|
||||
self.list_route_snapshots()
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|route| {
|
||||
let udp_nat_type = route
|
||||
.stun_info
|
||||
.as_ref()
|
||||
.map(|info| info.udp_nat_type)
|
||||
.unwrap_or_default();
|
||||
let Ok(udp_nat_type) = crate::proto::common::NatType::try_from(udp_nat_type) else {
|
||||
return None;
|
||||
};
|
||||
Some(UdpPunchCandidate {
|
||||
peer_id: route.peer_id,
|
||||
udp_nat_type,
|
||||
feature_flag: route.feature_flag,
|
||||
has_direct_connection: peer_map.has_peer(route.peer_id),
|
||||
has_recent_traffic: self.has_recent_traffic(route.peer_id, now),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn p2p_demand_notify(&self) -> Arc<ExternalTaskSignal> {
|
||||
PeerManagerCore::p2p_demand_notify(self)
|
||||
}
|
||||
|
||||
fn is_local_virtual_ip(&self, ip: &IpAddr) -> bool {
|
||||
PeerManagerCore::is_local_virtual_ip(self, ip)
|
||||
}
|
||||
|
||||
async fn is_easytier_managed_ipv6(&self, ip: &Ipv6Addr) -> bool {
|
||||
PeerManagerCore::is_easytier_managed_ipv6(self, ip).await
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpHolePunchRpcSource for PeerManagerCore {
|
||||
fn local_peer_id(&self) -> PeerId {
|
||||
PeerManagerCore::my_peer_id(self)
|
||||
}
|
||||
|
||||
fn rpc_stub(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Box<dyn UdpHolePunchRpc<Controller = BaseController> + Send + Sync + 'static> {
|
||||
PeerManagerCore::get_peer_rpc_mgr(self)
|
||||
.rpc_client()
|
||||
.scoped_client::<UdpHolePunchRpcClientFactory<BaseController>>(
|
||||
PeerManagerCore::my_peer_id(self),
|
||||
dst_peer_id,
|
||||
PeerManagerCore::network_name(self).to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HolePunchTunnelSink for PeerManagerCore {
|
||||
async fn add_client_tunnel(&self, tunnel: Box<dyn Tunnel>) -> anyhow::Result<()> {
|
||||
PeerManagerCore::add_client_tunnel(self, tunnel, false)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
async fn add_server_tunnel(&self, tunnel: Box<dyn Tunnel>) -> anyhow::Result<()> {
|
||||
PeerManagerCore::add_tunnel_as_server(self, tunnel, false)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TcpHolePunchPeerSource for PeerManagerCore {
|
||||
fn local_peer_id(&self) -> PeerId {
|
||||
PeerManagerCore::my_peer_id(self)
|
||||
}
|
||||
|
||||
fn p2p_policy_flags(&self) -> P2pPolicyFlags {
|
||||
PeerManagerCore::p2p_policy_flags(self)
|
||||
}
|
||||
|
||||
fn tcp_hole_punching_disabled(&self) -> bool {
|
||||
PeerManagerCore::tcp_hole_punching_disabled(self)
|
||||
}
|
||||
|
||||
fn p2p_demand_notify(&self) -> Arc<ExternalTaskSignal> {
|
||||
PeerManagerCore::p2p_demand_notify(self)
|
||||
}
|
||||
|
||||
async fn candidates(&self) -> Vec<TcpPunchCandidate> {
|
||||
let now = Instant::now();
|
||||
let peer_map = self.get_peer_map();
|
||||
self.list_route_snapshots()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|route| TcpPunchCandidate {
|
||||
peer_id: route.peer_id,
|
||||
tcp_nat_type: route
|
||||
.stun_info
|
||||
.as_ref()
|
||||
.map(|info| info.tcp_nat_type)
|
||||
.and_then(|nat_type| NatType::try_from(nat_type).ok())
|
||||
.unwrap_or(NatType::Unknown),
|
||||
feature_flag: route.feature_flag,
|
||||
has_direct_connection: peer_map.has_peer(route.peer_id),
|
||||
has_recent_traffic: self.has_recent_traffic(route.peer_id, now),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rpc_stub(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Box<dyn TcpHolePunchRpc<Controller = BaseController> + Send + Sync + 'static> {
|
||||
PeerManagerCore::get_peer_rpc_mgr(self)
|
||||
.rpc_client()
|
||||
.scoped_client::<TcpHolePunchRpcClientFactory<BaseController>>(
|
||||
PeerManagerCore::my_peer_id(self),
|
||||
dst_peer_id,
|
||||
PeerManagerCore::network_name(self).to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HolePunchRpcRegistry for PeerManagerCore {
|
||||
fn register_rpc_service<H>(&self, service: H)
|
||||
where
|
||||
H: Handler<Controller = BaseController>,
|
||||
{
|
||||
PeerManagerCore::get_peer_rpc_mgr(self)
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.register(service, PeerManagerCore::network_name(self));
|
||||
}
|
||||
|
||||
fn unregister_rpc_service<H>(&self, service: H)
|
||||
where
|
||||
H: Handler<Controller = BaseController>,
|
||||
{
|
||||
PeerManagerCore::get_peer_rpc_mgr(self)
|
||||
.rpc_server()
|
||||
.registry()
|
||||
.unregister(service, PeerManagerCore::network_name(self));
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
use crate::proto::common::PeerFeatureFlag;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BackOff {
|
||||
backoffs_ms: Vec<u64>,
|
||||
current_idx: usize,
|
||||
}
|
||||
|
||||
impl BackOff {
|
||||
pub fn new(backoffs_ms: Vec<u64>) -> Self {
|
||||
Self {
|
||||
backoffs_ms,
|
||||
current_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_backoff(&mut self) -> u64 {
|
||||
let backoff = self.backoffs_ms[self.current_idx];
|
||||
self.current_idx = (self.current_idx + 1).min(self.backoffs_ms.len() - 1);
|
||||
backoff
|
||||
}
|
||||
|
||||
pub fn rollback(&mut self) {
|
||||
self.current_idx = self.current_idx.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub async fn sleep_for_next_backoff(&mut self) {
|
||||
let backoff = self.next_backoff();
|
||||
if backoff > 0 {
|
||||
crate::foundation::time::sleep(crate::foundation::time::Duration::from_millis(backoff))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_try_p2p_with_peer(
|
||||
feature_flag: Option<&PeerFeatureFlag>,
|
||||
allow_public_server: bool,
|
||||
local_disable_p2p: bool,
|
||||
local_need_p2p: bool,
|
||||
) -> bool {
|
||||
feature_flag
|
||||
.map(|flag| {
|
||||
(allow_public_server || !flag.is_public_server)
|
||||
&& (!local_disable_p2p || flag.need_p2p)
|
||||
&& (!flag.disable_p2p || local_need_p2p)
|
||||
})
|
||||
.unwrap_or(!local_disable_p2p)
|
||||
}
|
||||
|
||||
pub fn should_background_p2p_with_peer(
|
||||
feature_flag: Option<&PeerFeatureFlag>,
|
||||
allow_public_server: bool,
|
||||
lazy_p2p: bool,
|
||||
local_disable_p2p: bool,
|
||||
local_need_p2p: bool,
|
||||
) -> bool {
|
||||
should_try_p2p_with_peer(
|
||||
feature_flag,
|
||||
allow_public_server,
|
||||
local_disable_p2p,
|
||||
local_need_p2p,
|
||||
) && (!lazy_p2p || feature_flag.map(|flag| flag.need_p2p).unwrap_or(false))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backoff_saturates_and_can_rollback() {
|
||||
let mut backoff = BackOff::new(vec![10, 20]);
|
||||
|
||||
assert_eq!(backoff.next_backoff(), 10);
|
||||
assert_eq!(backoff.next_backoff(), 20);
|
||||
assert_eq!(backoff.next_backoff(), 20);
|
||||
backoff.rollback();
|
||||
assert_eq!(backoff.next_backoff(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy_background_p2p_requires_need_p2p() {
|
||||
let no_need_p2p = PeerFeatureFlag {
|
||||
need_p2p: false,
|
||||
..Default::default()
|
||||
};
|
||||
let need_p2p = PeerFeatureFlag {
|
||||
need_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&no_need_p2p),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_background_p2p_with_peer(
|
||||
Some(&no_need_p2p),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&need_p2p),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p2p_policy_respects_public_server_setting() {
|
||||
let public_server = PeerFeatureFlag {
|
||||
is_public_server: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
true,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_background_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_background_p2p_with_peer(
|
||||
Some(&public_server),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_p2p_only_allows_need_p2p_exceptions() {
|
||||
let normal_peer = PeerFeatureFlag::default();
|
||||
let need_peer = PeerFeatureFlag {
|
||||
need_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
let disable_peer = PeerFeatureFlag {
|
||||
disable_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
let disable_need_peer = PeerFeatureFlag {
|
||||
disable_p2p: true,
|
||||
need_p2p: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&normal_peer),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(None, false, false, false));
|
||||
assert!(!should_try_p2p_with_peer(None, false, true, false));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&normal_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&need_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&disable_peer),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&disable_peer),
|
||||
false,
|
||||
false,
|
||||
true
|
||||
));
|
||||
assert!(should_try_p2p_with_peer(
|
||||
Some(&disable_need_peer),
|
||||
false,
|
||||
true,
|
||||
true
|
||||
));
|
||||
assert!(!should_try_p2p_with_peer(
|
||||
Some(&disable_need_peer),
|
||||
false,
|
||||
true,
|
||||
false
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
use std::{
|
||||
fmt,
|
||||
future::Future,
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::events::{CoreEvent, CoreEventSink};
|
||||
|
||||
const UPNP_RENEW_INTERVAL: Duration = Duration::from_secs(240);
|
||||
|
||||
pub(crate) trait UdpPortMappingLease: Send + Sync + fmt::Debug {
|
||||
fn public_addr_resolved(&self, _mapped_addr: SocketAddr) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UdpPortMappingBackend {
|
||||
Igd,
|
||||
NatPmp,
|
||||
}
|
||||
|
||||
impl UdpPortMappingBackend {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Igd => "igd",
|
||||
Self::NatPmp => "nat-pmp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UdpPortMappingAttemptPhase {
|
||||
Discovery,
|
||||
Establishment,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UdpPortMappingAttemptError {
|
||||
phase: UdpPortMappingAttemptPhase,
|
||||
source: anyhow::Error,
|
||||
}
|
||||
|
||||
impl UdpPortMappingAttemptError {
|
||||
pub fn discovery(source: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
phase: UdpPortMappingAttemptPhase::Discovery,
|
||||
source: source.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn establishment(source: impl Into<anyhow::Error>) -> Self {
|
||||
Self {
|
||||
phase: UdpPortMappingAttemptPhase::Establishment,
|
||||
source: source.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn phase(&self) -> UdpPortMappingAttemptPhase {
|
||||
self.phase
|
||||
}
|
||||
|
||||
pub(crate) fn source(&self) -> &anyhow::Error {
|
||||
&self.source
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UdpPortMappingAttemptError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.source.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UdpPortMappingAttemptError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(self.source.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ActiveUdpPortMapping: Send + Sync + fmt::Debug {
|
||||
fn backend(&self) -> UdpPortMappingBackend;
|
||||
|
||||
fn local_addr(&self) -> SocketAddr;
|
||||
|
||||
fn gateway_external_port(&self) -> u16;
|
||||
|
||||
async fn renew(&self) -> anyhow::Result<()>;
|
||||
|
||||
async fn remove(&self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
pub type UdpPortMappingLifecycle = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpPortMappingPlatform: Send + Sync + 'static {
|
||||
async fn establish_udp_port_mapping(
|
||||
&self,
|
||||
backend: UdpPortMappingBackend,
|
||||
local_listener: &url::Url,
|
||||
) -> Result<Box<dyn ActiveUdpPortMapping>, UdpPortMappingAttemptError>;
|
||||
|
||||
fn spawn_udp_port_mapping_lifecycle(
|
||||
&self,
|
||||
_local_listener: url::Url,
|
||||
lifecycle: UdpPortMappingLifecycle,
|
||||
) {
|
||||
tokio::spawn(lifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
struct ManagedUdpPortMappingLease {
|
||||
events: Arc<dyn CoreEventSink>,
|
||||
local_listener: url::Url,
|
||||
backend: UdpPortMappingBackend,
|
||||
gateway_external_port: u16,
|
||||
stop_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ManagedUdpPortMappingLease {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("UdpPortMappingLease")
|
||||
.field("backend", &self.backend.name())
|
||||
.field("gateway_external_port", &self.gateway_external_port)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ManagedUdpPortMappingLease {
|
||||
fn drop(&mut self) {
|
||||
if let Some(stop_tx) = self.stop_tx.take() {
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpPortMappingLease for ManagedUdpPortMappingLease {
|
||||
fn public_addr_resolved(&self, mapped_addr: SocketAddr) {
|
||||
self.events.emit(CoreEvent::UdpPortMappingEstablished {
|
||||
local_listener: self.local_listener.clone(),
|
||||
mapped_listener: udp_url(mapped_addr),
|
||||
backend: self.backend.name().to_owned(),
|
||||
});
|
||||
tracing::info!(
|
||||
local_listener = %self.local_listener,
|
||||
backend = self.backend.name(),
|
||||
gateway_external_port = self.gateway_external_port,
|
||||
stun_mapped_addr = %mapped_addr,
|
||||
"udp public addr resolved after port mapping"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start_udp_port_mapping(
|
||||
platform: Arc<dyn UdpPortMappingPlatform>,
|
||||
events: Arc<dyn CoreEventSink>,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<Option<Box<dyn UdpPortMappingLease>>> {
|
||||
if !should_map_udp_listener(local_listener) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mapping = discover_udp_port_mapping(platform.as_ref(), local_listener).await?;
|
||||
let backend = mapping.backend();
|
||||
let gateway_external_port = mapping.gateway_external_port();
|
||||
tracing::info!(
|
||||
%local_listener,
|
||||
backend = backend.name(),
|
||||
local_addr = %mapping.local_addr(),
|
||||
gateway_external_port,
|
||||
"udp port mapping established"
|
||||
);
|
||||
|
||||
let (stop_tx, stop_rx) = oneshot::channel();
|
||||
platform.spawn_udp_port_mapping_lifecycle(
|
||||
local_listener.clone(),
|
||||
Box::pin(run_udp_port_mapping_lifecycle(
|
||||
local_listener.clone(),
|
||||
mapping,
|
||||
stop_rx,
|
||||
)),
|
||||
);
|
||||
|
||||
Ok(Some(Box::new(ManagedUdpPortMappingLease {
|
||||
events,
|
||||
local_listener: local_listener.clone(),
|
||||
backend,
|
||||
gateway_external_port,
|
||||
stop_tx: Some(stop_tx),
|
||||
})))
|
||||
}
|
||||
|
||||
async fn discover_udp_port_mapping(
|
||||
platform: &dyn UdpPortMappingPlatform,
|
||||
local_listener: &url::Url,
|
||||
) -> anyhow::Result<Box<dyn ActiveUdpPortMapping>> {
|
||||
let igd_error = match platform
|
||||
.establish_udp_port_mapping(UdpPortMappingBackend::Igd, local_listener)
|
||||
.await
|
||||
{
|
||||
Ok(mapping) => return Ok(mapping),
|
||||
Err(error) => error,
|
||||
};
|
||||
match igd_error.phase() {
|
||||
UdpPortMappingAttemptPhase::Discovery => tracing::debug!(
|
||||
igd_err = ?igd_error.source(),
|
||||
%local_listener,
|
||||
"igd gateway discovery failed, retry with nat-pmp"
|
||||
),
|
||||
UdpPortMappingAttemptPhase::Establishment => tracing::debug!(
|
||||
igd_err = ?igd_error.source(),
|
||||
%local_listener,
|
||||
"igd udp port mapping failed, retry with nat-pmp"
|
||||
),
|
||||
}
|
||||
|
||||
match platform
|
||||
.establish_udp_port_mapping(UdpPortMappingBackend::NatPmp, local_listener)
|
||||
.await
|
||||
{
|
||||
Ok(mapping) => Ok(mapping),
|
||||
Err(nat_pmp_error) => Err(combined_mapping_error(
|
||||
local_listener,
|
||||
igd_error,
|
||||
nat_pmp_error,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn combined_mapping_error(
|
||||
local_listener: &url::Url,
|
||||
igd_error: UdpPortMappingAttemptError,
|
||||
nat_pmp_error: UdpPortMappingAttemptError,
|
||||
) -> anyhow::Error {
|
||||
let igd_label = match igd_error.phase() {
|
||||
UdpPortMappingAttemptPhase::Discovery => "igd discovery error",
|
||||
UdpPortMappingAttemptPhase::Establishment => "igd error",
|
||||
};
|
||||
let nat_pmp_label = match nat_pmp_error.phase() {
|
||||
UdpPortMappingAttemptPhase::Discovery => "nat-pmp discovery error",
|
||||
UdpPortMappingAttemptPhase::Establishment => "nat-pmp error",
|
||||
};
|
||||
anyhow::anyhow!(
|
||||
"udp port mapping failed for {local_listener}: {igd_label}: {}; {nat_pmp_label}: {}",
|
||||
igd_error.source(),
|
||||
nat_pmp_error.source(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn run_udp_port_mapping_lifecycle(
|
||||
local_listener: url::Url,
|
||||
mapping: Box<dyn ActiveUdpPortMapping>,
|
||||
mut stop_rx: oneshot::Receiver<()>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = crate::foundation::time::sleep(UPNP_RENEW_INTERVAL) => {
|
||||
if let Err(error) = mapping.renew().await {
|
||||
tracing::warn!(
|
||||
err = ?error,
|
||||
%local_listener,
|
||||
backend = mapping.backend().name(),
|
||||
gateway_external_port = mapping.gateway_external_port(),
|
||||
"failed to renew udp port mapping"
|
||||
);
|
||||
}
|
||||
}
|
||||
_ = &mut stop_rx => break,
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = mapping.remove().await {
|
||||
tracing::debug!(
|
||||
err = ?error,
|
||||
%local_listener,
|
||||
backend = mapping.backend().name(),
|
||||
gateway_external_port = mapping.gateway_external_port(),
|
||||
"failed to remove udp port mapping"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_map_udp_listener(local_listener: &url::Url) -> bool {
|
||||
if local_listener.scheme() != "udp" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(host) = listener_ipv4_host(local_listener) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if host.is_loopback() || host.is_broadcast() {
|
||||
return false;
|
||||
}
|
||||
|
||||
host.is_unspecified() || host.is_private() || host.is_link_local()
|
||||
}
|
||||
|
||||
fn listener_ipv4_host(local_listener: &url::Url) -> Option<Ipv4Addr> {
|
||||
local_listener.host_str()?.parse().ok()
|
||||
}
|
||||
|
||||
fn udp_url(addr: SocketAddr) -> url::Url {
|
||||
let mut url = url::Url::parse("udp://0.0.0.0").expect("static UDP URL should be valid");
|
||||
url.set_ip_host(addr.ip())
|
||||
.expect("socket IP should be a valid URL host");
|
||||
url.set_port(Some(addr.port()))
|
||||
.expect("UDP URL should accept a port");
|
||||
url
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockMapping {
|
||||
backend: UdpPortMappingBackend,
|
||||
removals: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ActiveUdpPortMapping for MockMapping {
|
||||
fn backend(&self) -> UdpPortMappingBackend {
|
||||
self.backend
|
||||
}
|
||||
|
||||
fn local_addr(&self) -> SocketAddr {
|
||||
"192.168.1.5:11010".parse().unwrap()
|
||||
}
|
||||
|
||||
fn gateway_external_port(&self) -> u16 {
|
||||
41010
|
||||
}
|
||||
|
||||
async fn renew(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove(&self) -> anyhow::Result<()> {
|
||||
self.removals.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockPlatform {
|
||||
attempts: Mutex<Vec<UdpPortMappingBackend>>,
|
||||
igd_phase: Option<UdpPortMappingAttemptPhase>,
|
||||
removals: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UdpPortMappingPlatform for MockPlatform {
|
||||
async fn establish_udp_port_mapping(
|
||||
&self,
|
||||
backend: UdpPortMappingBackend,
|
||||
_local_listener: &url::Url,
|
||||
) -> Result<Box<dyn ActiveUdpPortMapping>, UdpPortMappingAttemptError> {
|
||||
self.attempts.lock().unwrap().push(backend);
|
||||
if backend == UdpPortMappingBackend::Igd
|
||||
&& let Some(phase) = self.igd_phase
|
||||
{
|
||||
return Err(match phase {
|
||||
UdpPortMappingAttemptPhase::Discovery => {
|
||||
UdpPortMappingAttemptError::discovery(anyhow::anyhow!("no igd"))
|
||||
}
|
||||
UdpPortMappingAttemptPhase::Establishment => {
|
||||
UdpPortMappingAttemptError::establishment(anyhow::anyhow!("igd denied"))
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(Box::new(MockMapping {
|
||||
backend,
|
||||
removals: self.removals.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_requires_private_or_unspecified_ipv4_listener() {
|
||||
assert!(should_map_udp_listener(
|
||||
&"udp://0.0.0.0:11010".parse().unwrap()
|
||||
));
|
||||
assert!(should_map_udp_listener(
|
||||
&"udp://192.168.1.10:11010".parse().unwrap()
|
||||
));
|
||||
assert!(!should_map_udp_listener(
|
||||
&"udp://127.0.0.1:11010".parse().unwrap()
|
||||
));
|
||||
assert!(!should_map_udp_listener(
|
||||
&"udp://8.8.8.8:11010".parse().unwrap()
|
||||
));
|
||||
assert!(!should_map_udp_listener(
|
||||
&"tcp://0.0.0.0:11010".parse().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn falls_back_from_igd_to_nat_pmp_and_removes_on_drop() {
|
||||
let removals = Arc::new(AtomicUsize::new(0));
|
||||
let platform = Arc::new(MockPlatform {
|
||||
attempts: Mutex::new(Vec::new()),
|
||||
igd_phase: Some(UdpPortMappingAttemptPhase::Discovery),
|
||||
removals: removals.clone(),
|
||||
});
|
||||
|
||||
let lease = start_udp_port_mapping(
|
||||
platform.clone(),
|
||||
Arc::new(()),
|
||||
&"udp://0.0.0.0:11010".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
*platform.attempts.lock().unwrap(),
|
||||
vec![UdpPortMappingBackend::Igd, UdpPortMappingBackend::NatPmp]
|
||||
);
|
||||
|
||||
drop(lease);
|
||||
tokio::time::timeout(Duration::from_secs(1), async {
|
||||
while removals.load(Ordering::SeqCst) == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(removals.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,802 +0,0 @@
|
||||
//! Core-owned UDP hole-punch socket/session runtime.
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
connectivity::{
|
||||
direct::DirectConnectorHost,
|
||||
hole_punch::port_mapping::{UdpPortMappingPlatform, start_udp_port_mapping},
|
||||
hole_punch::{HolePunchRpcRegistry, HolePunchTunnelSink},
|
||||
protocol::ClientProtocolUpgrader,
|
||||
stun::{StunInfoProvider, StunSocketMapper},
|
||||
},
|
||||
proto::peer_rpc::UdpHolePunchRpcServer,
|
||||
socket::{
|
||||
IpVersion, ListenerConnectionCounter, SocketContext,
|
||||
tcp::VirtualTcpSocketFactory,
|
||||
udp::{
|
||||
UdpBindOptions, UdpSessionLayer, UdpSessionSocket, UdpSessionStunResponder,
|
||||
VirtualUdpSocket, VirtualUdpSocketFactory,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
ProtocolUdpHolePunchTransportSink, UdpHolePunchConnector, UdpHolePunchPeerSource,
|
||||
UdpHolePunchRuntime, UdpPunchAcceptor, UdpPunchListener, UdpPunchSocket, UdpResolvedPublicAddr,
|
||||
UdpSymPunchLock,
|
||||
rpc::{PeerRpcUdpHolePunchSignaling, UdpHolePunchRpcEndpoint, UdpHolePunchRpcSource},
|
||||
};
|
||||
|
||||
async fn resolve_public_addr_with_policy<S>(
|
||||
stun: &dyn StunSocketMapper<S>,
|
||||
platform: Option<Arc<dyn UdpPortMappingPlatform>>,
|
||||
events: Arc<dyn crate::events::CoreEventSink>,
|
||||
socket: Arc<S>,
|
||||
local_listener: &url::Url,
|
||||
disable_upnp: bool,
|
||||
) -> anyhow::Result<UdpResolvedPublicAddr>
|
||||
where
|
||||
S: VirtualUdpSocket + 'static,
|
||||
{
|
||||
let port_mapping_lease = if disable_upnp {
|
||||
None
|
||||
} else if let Some(platform) = platform {
|
||||
match start_udp_port_mapping(platform, events, local_listener).await {
|
||||
Ok(lease) => lease,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
?error,
|
||||
%local_listener,
|
||||
"failed to establish udp port mapping, fallback to stun-only public addr resolution"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mapped_addr = stun
|
||||
.get_udp_port_mapping_with_socket(socket)
|
||||
.await
|
||||
.with_context(|| format!("resolve udp public addr for {local_listener}"))?;
|
||||
if let Some(lease) = &port_mapping_lease {
|
||||
lease.public_addr_resolved(mapped_addr);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
%local_listener,
|
||||
stun_mapped_addr = %mapped_addr,
|
||||
"udp public addr resolved without port mapping"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(UdpResolvedPublicAddr {
|
||||
mapped_addr,
|
||||
port_mapping_lease,
|
||||
})
|
||||
}
|
||||
|
||||
fn managed_local_addr_error(
|
||||
local_addr: SocketAddr,
|
||||
is_local_virtual_ipv4: bool,
|
||||
is_easytier_managed_ipv6: bool,
|
||||
) -> Option<&'static str> {
|
||||
match local_addr.ip() {
|
||||
IpAddr::V4(_) if is_local_virtual_ipv4 => Some("local address is virtual ipv4"),
|
||||
IpAddr::V6(_) if is_easytier_managed_ipv6 => Some("local address is easytier-managed ipv6"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
type HostUdpSocket<H> = <H as VirtualUdpSocketFactory>::Socket;
|
||||
type HostTcpSocket<H> = <H as VirtualTcpSocketFactory>::Socket;
|
||||
type CoreUdpSessionLayer<H> = UdpSessionLayer<HostUdpSocket<H>, H>;
|
||||
type CoreUdpHolePunchTransportSink<H, P> = ProtocolUdpHolePunchTransportSink<HostTcpSocket<H>, P>;
|
||||
type CoreUdpHolePunchConnector<H, P> = UdpHolePunchConnector<
|
||||
P,
|
||||
PeerRpcUdpHolePunchSignaling<P>,
|
||||
CoreUdpHolePunchTransportSink<H, P>,
|
||||
CoreUdpHolePunchRuntime<H, P>,
|
||||
>;
|
||||
type CoreUdpHolePunchEndpoint<H, P> =
|
||||
UdpHolePunchRpcEndpoint<CoreUdpHolePunchRuntime<H, P>, CoreUdpHolePunchTransportSink<H, P>>;
|
||||
|
||||
pub(crate) struct CoreUdpHolePunchService<H, P>
|
||||
where
|
||||
H: DirectConnectorHost,
|
||||
P: UdpHolePunchPeerSource
|
||||
+ HolePunchTunnelSink
|
||||
+ UdpHolePunchRpcSource
|
||||
+ HolePunchRpcRegistry
|
||||
+ 'static,
|
||||
{
|
||||
server: Arc<CoreUdpHolePunchEndpoint<H, P>>,
|
||||
client: CoreUdpHolePunchConnector<H, P>,
|
||||
peer_source: Arc<P>,
|
||||
}
|
||||
|
||||
impl<H, P> CoreUdpHolePunchService<H, P>
|
||||
where
|
||||
H: DirectConnectorHost + Send + Sync + 'static,
|
||||
HostUdpSocket<H>: VirtualUdpSocket + 'static,
|
||||
P: UdpHolePunchPeerSource
|
||||
+ HolePunchTunnelSink
|
||||
+ UdpHolePunchRpcSource
|
||||
+ HolePunchRpcRegistry
|
||||
+ 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
peer_source: Arc<P>,
|
||||
host: Arc<H>,
|
||||
stun: Arc<dyn StunSocketMapper<HostUdpSocket<H>>>,
|
||||
platform: Option<Arc<dyn UdpPortMappingPlatform>>,
|
||||
events: Arc<dyn crate::events::CoreEventSink>,
|
||||
socket_context: SocketContext,
|
||||
protocol: Arc<dyn ClientProtocolUpgrader<HostTcpSocket<H>>>,
|
||||
) -> Self {
|
||||
let stun_mapper = stun.clone();
|
||||
let stun_info: Arc<dyn StunInfoProvider> = stun;
|
||||
let transport_sink = Arc::new(ProtocolUdpHolePunchTransportSink::new(
|
||||
protocol,
|
||||
peer_source.clone(),
|
||||
));
|
||||
let runtime = Arc::new(CoreUdpHolePunchRuntime::new(
|
||||
host,
|
||||
peer_source.clone(),
|
||||
stun_mapper,
|
||||
platform,
|
||||
events,
|
||||
socket_context,
|
||||
));
|
||||
let sym_punch_lock = UdpSymPunchLock::default();
|
||||
let client = UdpHolePunchConnector::new(
|
||||
peer_source.clone(),
|
||||
Arc::new(PeerRpcUdpHolePunchSignaling::new(peer_source.clone())),
|
||||
transport_sink.clone(),
|
||||
runtime.clone(),
|
||||
stun_info.clone(),
|
||||
sym_punch_lock.clone(),
|
||||
Some(peer_source.p2p_demand_notify()),
|
||||
);
|
||||
|
||||
Self {
|
||||
server: UdpHolePunchRpcEndpoint::new(
|
||||
stun_info,
|
||||
transport_sink,
|
||||
sym_punch_lock,
|
||||
runtime,
|
||||
),
|
||||
client,
|
||||
peer_source,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn start(&self) -> anyhow::Result<()> {
|
||||
if self
|
||||
.peer_source
|
||||
.p2p_policy_flags()
|
||||
.disable_udp_hole_punching
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.server.start().await;
|
||||
self.peer_source
|
||||
.register_rpc_service(UdpHolePunchRpcServer::new(Arc::downgrade(&self.server)));
|
||||
self.client.run_as_client();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn stop(&self) {
|
||||
self.client.stop().await;
|
||||
self.server.begin_stop();
|
||||
self.peer_source
|
||||
.unregister_rpc_service(UdpHolePunchRpcServer::new(Arc::downgrade(&self.server)));
|
||||
self.server.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
struct CoreUdpPunchAcceptor<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
HostUdpSocket<H>: VirtualUdpSocket,
|
||||
{
|
||||
layer: Arc<CoreUdpSessionLayer<H>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<H> UdpPunchAcceptor for CoreUdpPunchAcceptor<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory + UdpSessionStunResponder<HostUdpSocket<H>> + Send + Sync + 'static,
|
||||
HostUdpSocket<H>: VirtualUdpSocket + 'static,
|
||||
{
|
||||
async fn accept(&mut self) -> anyhow::Result<UdpPunchSocket> {
|
||||
let session = self.layer.accept().await?;
|
||||
let remote_addr = session.peer_addr()?;
|
||||
Ok(UdpPunchSocket::new(
|
||||
session,
|
||||
remote_addr,
|
||||
self.layer.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct CoreUdpPunchConnCounter<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
HostUdpSocket<H>: VirtualUdpSocket,
|
||||
{
|
||||
layer: Weak<CoreUdpSessionLayer<H>>,
|
||||
}
|
||||
|
||||
impl<H> std::fmt::Debug for CoreUdpPunchConnCounter<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
HostUdpSocket<H>: VirtualUdpSocket,
|
||||
{
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter
|
||||
.debug_struct("CoreUdpPunchConnCounter")
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> ListenerConnectionCounter for CoreUdpPunchConnCounter<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory + UdpSessionStunResponder<HostUdpSocket<H>> + Send + Sync + 'static,
|
||||
HostUdpSocket<H>: VirtualUdpSocket + 'static,
|
||||
{
|
||||
fn get(&self) -> Option<u32> {
|
||||
Some(
|
||||
self.layer
|
||||
.upgrade()
|
||||
.map(|layer| layer.active_session_count() as u32)
|
||||
.unwrap_or(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CoreUdpHolePunchRuntime<H, P>
|
||||
where
|
||||
H: DirectConnectorHost,
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
{
|
||||
host: Arc<H>,
|
||||
peer_source: Arc<P>,
|
||||
stun: Arc<dyn StunSocketMapper<HostUdpSocket<H>>>,
|
||||
platform: Option<Arc<dyn UdpPortMappingPlatform>>,
|
||||
events: Arc<dyn crate::events::CoreEventSink>,
|
||||
socket_context: SocketContext,
|
||||
}
|
||||
|
||||
impl<H, P> CoreUdpHolePunchRuntime<H, P>
|
||||
where
|
||||
H: DirectConnectorHost + Send + Sync + 'static,
|
||||
HostUdpSocket<H>: VirtualUdpSocket + 'static,
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
{
|
||||
pub fn new(
|
||||
host: Arc<H>,
|
||||
peer_source: Arc<P>,
|
||||
stun: Arc<dyn StunSocketMapper<HostUdpSocket<H>>>,
|
||||
platform: Option<Arc<dyn UdpPortMappingPlatform>>,
|
||||
events: Arc<dyn crate::events::CoreEventSink>,
|
||||
socket_context: SocketContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
host,
|
||||
peer_source,
|
||||
stun,
|
||||
platform,
|
||||
events,
|
||||
socket_context,
|
||||
}
|
||||
}
|
||||
|
||||
fn session_layer(&self, socket: Arc<HostUdpSocket<H>>) -> Arc<CoreUdpSessionLayer<H>> {
|
||||
Arc::new(UdpSessionLayer::new_with_stun_responder(
|
||||
socket,
|
||||
self.host.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn create_listener_with_mapping(
|
||||
&self,
|
||||
resolve_public_addr: bool,
|
||||
port: Option<u16>,
|
||||
) -> anyhow::Result<UdpPunchListener<HostUdpSocket<H>>> {
|
||||
let bind = match port {
|
||||
Some(port) => UdpBindOptions::hole_punch_candidate().with_local_addr(Some(
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)),
|
||||
)),
|
||||
None => UdpBindOptions::hole_punch_control(),
|
||||
}
|
||||
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V4));
|
||||
let socket = self.host.bind_udp(bind).await?;
|
||||
let local_port = socket.local_addr()?.port();
|
||||
let resolved = if resolve_public_addr {
|
||||
self.resolve_public_addr(socket.clone()).await?
|
||||
} else {
|
||||
UdpResolvedPublicAddr {
|
||||
mapped_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, local_port)),
|
||||
port_mapping_lease: None,
|
||||
}
|
||||
};
|
||||
|
||||
let layer = self.session_layer(socket.clone());
|
||||
let conn_counter = Arc::new(CoreUdpPunchConnCounter {
|
||||
layer: Arc::downgrade(&layer),
|
||||
});
|
||||
let acceptor = Box::new(CoreUdpPunchAcceptor { layer });
|
||||
|
||||
Ok(UdpPunchListener {
|
||||
socket,
|
||||
mapped_addr: resolved.mapped_addr,
|
||||
conn_counter,
|
||||
acceptor,
|
||||
port_mapping_lease: resolved.port_mapping_lease,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_public_addr(
|
||||
&self,
|
||||
socket: Arc<HostUdpSocket<H>>,
|
||||
) -> anyhow::Result<UdpResolvedPublicAddr> {
|
||||
let local_port = socket.local_addr()?.port();
|
||||
let local_listener: url::Url = format!("udp://0.0.0.0:{local_port}").parse()?;
|
||||
resolve_public_addr_with_policy(
|
||||
self.stun.as_ref(),
|
||||
self.platform.clone(),
|
||||
self.events.clone(),
|
||||
socket,
|
||||
&local_listener,
|
||||
self.peer_source.p2p_policy_flags().disable_upnp,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn validate_socket_route(
|
||||
&self,
|
||||
context: SocketContext,
|
||||
remote_addr: SocketAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
let local_addr = self
|
||||
.host
|
||||
.local_addr_for_remote(remote_addr, context)
|
||||
.await?;
|
||||
let is_local_virtual_ipv4 = match local_addr.ip() {
|
||||
IpAddr::V4(ip) => self.peer_source.is_local_virtual_ip(&IpAddr::V4(ip)),
|
||||
IpAddr::V6(_) => false,
|
||||
};
|
||||
let is_easytier_managed_ipv6 = match local_addr.ip() {
|
||||
IpAddr::V4(_) => false,
|
||||
IpAddr::V6(ip) => self.peer_source.is_easytier_managed_ipv6(&ip).await,
|
||||
};
|
||||
if let Some(error) =
|
||||
managed_local_addr_error(local_addr, is_local_virtual_ipv4, is_easytier_managed_ipv6)
|
||||
{
|
||||
anyhow::bail!(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<H, P> UdpHolePunchRuntime for CoreUdpHolePunchRuntime<H, P>
|
||||
where
|
||||
H: DirectConnectorHost + Send + Sync + 'static,
|
||||
HostUdpSocket<H>: VirtualUdpSocket + 'static,
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
{
|
||||
type Socket = HostUdpSocket<H>;
|
||||
|
||||
fn socket_context(&self) -> SocketContext {
|
||||
self.socket_context.clone()
|
||||
}
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.host.bind_udp(options).await
|
||||
}
|
||||
|
||||
async fn bind_direct_connect_udp(&self) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.host
|
||||
.bind_udp(
|
||||
UdpBindOptions::hole_punch_candidate()
|
||||
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V4)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn resolve_udp_public_addr(
|
||||
&self,
|
||||
socket: Arc<Self::Socket>,
|
||||
) -> anyhow::Result<UdpResolvedPublicAddr> {
|
||||
self.resolve_public_addr(socket).await
|
||||
}
|
||||
|
||||
async fn create_listener(
|
||||
&self,
|
||||
_prefer_port_mapping: bool,
|
||||
) -> anyhow::Result<UdpPunchListener<Self::Socket>> {
|
||||
self.create_listener_with_mapping(true, None).await
|
||||
}
|
||||
|
||||
async fn create_port_bound_listener(
|
||||
&self,
|
||||
port: u16,
|
||||
) -> anyhow::Result<UdpPunchListener<Self::Socket>> {
|
||||
self.create_listener_with_mapping(false, Some(port)).await
|
||||
}
|
||||
|
||||
async fn connect_with_socket(
|
||||
&self,
|
||||
socket: Arc<Self::Socket>,
|
||||
remote: SocketAddr,
|
||||
) -> anyhow::Result<UdpPunchSocket> {
|
||||
self.validate_socket_route(socket.socket_context(), remote)
|
||||
.await?;
|
||||
let layer = self.session_layer(socket);
|
||||
let session = layer.connect(remote).await?;
|
||||
if session.peer_addr()? != remote {
|
||||
tracing::debug!(
|
||||
recv_addr = ?session.peer_addr()?,
|
||||
?remote,
|
||||
"udp connect addr not match"
|
||||
);
|
||||
}
|
||||
Ok(UdpPunchSocket::new(session, remote, layer))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv6Addr, SocketAddrV6},
|
||||
sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{connectivity::stun::StunInfoProvider, proto::common::StunInfo};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], _addr: SocketAddr) -> io::Result<usize> {
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
struct MockStun {
|
||||
mapped_addr: SocketAddr,
|
||||
fail: bool,
|
||||
calls: AtomicUsize,
|
||||
}
|
||||
|
||||
impl MockStun {
|
||||
fn succeeds_with(mapped_addr: SocketAddr) -> Self {
|
||||
Self {
|
||||
mapped_addr,
|
||||
fail: false,
|
||||
calls: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn failing() -> Self {
|
||||
Self {
|
||||
mapped_addr: "0.0.0.0:0".parse().unwrap(),
|
||||
fail: true,
|
||||
calls: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StunInfoProvider for MockStun {
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
StunInfo::default()
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, _local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
Ok(self.mapped_addr)
|
||||
}
|
||||
|
||||
async fn get_tcp_port_mapping(&self, _local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
Ok(self.mapped_addr)
|
||||
}
|
||||
|
||||
fn update_stun_info(&self) {}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StunSocketMapper<MockSocket> for MockStun {
|
||||
async fn get_udp_port_mapping_with_socket(
|
||||
&self,
|
||||
_socket: Arc<MockSocket>,
|
||||
) -> anyhow::Result<SocketAddr> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
if self.fail {
|
||||
anyhow::bail!("mock STUN failure");
|
||||
}
|
||||
Ok(self.mapped_addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LeaseState {
|
||||
drops: AtomicUsize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockEvents {
|
||||
established: Mutex<Vec<crate::events::CoreEvent>>,
|
||||
}
|
||||
|
||||
impl crate::events::CoreEventSink for MockEvents {
|
||||
fn emit(&self, event: crate::events::CoreEvent) {
|
||||
self.established.lock().unwrap().push(event);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockMapping {
|
||||
state: Arc<LeaseState>,
|
||||
backend: crate::connectivity::hole_punch::port_mapping::UdpPortMappingBackend,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl crate::connectivity::hole_punch::port_mapping::ActiveUdpPortMapping for MockMapping {
|
||||
fn backend(&self) -> crate::connectivity::hole_punch::port_mapping::UdpPortMappingBackend {
|
||||
self.backend
|
||||
}
|
||||
|
||||
fn local_addr(&self) -> SocketAddr {
|
||||
"192.168.1.5:30123".parse().unwrap()
|
||||
}
|
||||
|
||||
fn gateway_external_port(&self) -> u16 {
|
||||
40123
|
||||
}
|
||||
|
||||
async fn renew(&self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove(&self) -> anyhow::Result<()> {
|
||||
self.state.drops.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockPlatform {
|
||||
calls: AtomicUsize,
|
||||
fail: bool,
|
||||
lease_state: Arc<LeaseState>,
|
||||
}
|
||||
|
||||
impl MockPlatform {
|
||||
fn failing() -> Self {
|
||||
Self {
|
||||
calls: AtomicUsize::new(0),
|
||||
fail: true,
|
||||
lease_state: Arc::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_lease(lease_state: Arc<LeaseState>) -> Self {
|
||||
Self {
|
||||
calls: AtomicUsize::new(0),
|
||||
fail: false,
|
||||
lease_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UdpPortMappingPlatform for MockPlatform {
|
||||
async fn establish_udp_port_mapping(
|
||||
&self,
|
||||
backend: crate::connectivity::hole_punch::port_mapping::UdpPortMappingBackend,
|
||||
_local_listener: &url::Url,
|
||||
) -> Result<
|
||||
Box<dyn crate::connectivity::hole_punch::port_mapping::ActiveUdpPortMapping>,
|
||||
crate::connectivity::hole_punch::port_mapping::UdpPortMappingAttemptError,
|
||||
> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
if self.fail {
|
||||
return Err(
|
||||
crate::connectivity::hole_punch::port_mapping::UdpPortMappingAttemptError::establishment(
|
||||
anyhow::anyhow!("mock port-mapping failure"),
|
||||
),
|
||||
);
|
||||
}
|
||||
Ok(Box::new(MockMapping {
|
||||
state: self.lease_state.clone(),
|
||||
backend,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn socket() -> Arc<MockSocket> {
|
||||
Arc::new(MockSocket {
|
||||
local_addr: "0.0.0.0:30123".parse().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn listener_url() -> url::Url {
|
||||
"udp://0.0.0.0:30123".parse().unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn port_mapping_failure_falls_back_to_stun() {
|
||||
let mapped_addr = "198.51.100.8:40123".parse().unwrap();
|
||||
let stun = MockStun::succeeds_with(mapped_addr);
|
||||
let platform = Arc::new(MockPlatform::failing());
|
||||
|
||||
let resolved = resolve_public_addr_with_policy(
|
||||
&stun,
|
||||
Some(platform.clone()),
|
||||
Arc::new(()),
|
||||
socket(),
|
||||
&listener_url(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resolved.mapped_addr, mapped_addr);
|
||||
assert!(resolved.port_mapping_lease.is_none());
|
||||
assert_eq!(platform.calls.load(Ordering::SeqCst), 2);
|
||||
assert_eq!(stun.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stun_failure_releases_mapping_without_notification() {
|
||||
let lease_state = Arc::new(LeaseState::default());
|
||||
let platform = Arc::new(MockPlatform::with_lease(lease_state.clone()));
|
||||
let events = Arc::new(MockEvents::default());
|
||||
|
||||
let result = resolve_public_addr_with_policy(
|
||||
&MockStun::failing(),
|
||||
Some(platform),
|
||||
events.clone(),
|
||||
socket(),
|
||||
&listener_url(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), async {
|
||||
while lease_state.drops.load(Ordering::SeqCst) == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(lease_state.drops.load(Ordering::SeqCst), 1);
|
||||
assert!(events.established.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disable_upnp_is_applied_per_resolution() {
|
||||
let mapped_addr = "198.51.100.9:40124".parse().unwrap();
|
||||
let stun = MockStun::succeeds_with(mapped_addr);
|
||||
let lease_state = Arc::new(LeaseState::default());
|
||||
let platform = Arc::new(MockPlatform::with_lease(lease_state));
|
||||
|
||||
let disabled = resolve_public_addr_with_policy(
|
||||
&stun,
|
||||
Some(platform.clone()),
|
||||
Arc::new(()),
|
||||
socket(),
|
||||
&listener_url(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(disabled.port_mapping_lease.is_none());
|
||||
assert_eq!(platform.calls.load(Ordering::SeqCst), 0);
|
||||
|
||||
let enabled = resolve_public_addr_with_policy(
|
||||
&stun,
|
||||
Some(platform.clone()),
|
||||
Arc::new(()),
|
||||
socket(),
|
||||
&listener_url(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(enabled.port_mapping_lease.is_some());
|
||||
assert_eq!(platform.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn successful_mapping_is_notified_and_held_with_result() {
|
||||
let mapped_addr = "198.51.100.10:40125".parse().unwrap();
|
||||
let lease_state = Arc::new(LeaseState::default());
|
||||
let platform = Arc::new(MockPlatform::with_lease(lease_state.clone()));
|
||||
let events = Arc::new(MockEvents::default());
|
||||
|
||||
let resolved = resolve_public_addr_with_policy(
|
||||
&MockStun::succeeds_with(mapped_addr),
|
||||
Some(platform),
|
||||
events.clone(),
|
||||
socket(),
|
||||
&listener_url(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
let established = events.established.lock().unwrap();
|
||||
assert!(matches!(
|
||||
established.as_slice(),
|
||||
[crate::events::CoreEvent::UdpPortMappingEstablished {
|
||||
local_listener,
|
||||
mapped_listener,
|
||||
backend,
|
||||
}] if local_listener == &listener_url()
|
||||
&& mapped_listener.as_str() == "udp://198.51.100.10:40125"
|
||||
&& backend == "igd"
|
||||
));
|
||||
}
|
||||
assert_eq!(lease_state.drops.load(Ordering::SeqCst), 0);
|
||||
drop(resolved);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), async {
|
||||
while lease_state.drops.load(Ordering::SeqCst) == 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(lease_state.drops.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_local_addresses_are_rejected() {
|
||||
let virtual_ipv4 = "10.144.0.2:1234".parse().unwrap();
|
||||
assert_eq!(
|
||||
managed_local_addr_error(virtual_ipv4, true, false),
|
||||
Some("local address is virtual ipv4")
|
||||
);
|
||||
assert_eq!(managed_local_addr_error(virtual_ipv4, false, false), None);
|
||||
|
||||
let managed_ipv6 = SocketAddr::V6(SocketAddrV6::new(
|
||||
"fd00::1".parse::<Ipv6Addr>().unwrap(),
|
||||
1234,
|
||||
0,
|
||||
0,
|
||||
));
|
||||
assert_eq!(
|
||||
managed_local_addr_error(managed_ipv6, false, true),
|
||||
Some("local address is easytier-managed ipv6")
|
||||
);
|
||||
assert_eq!(managed_local_addr_error(managed_ipv6, false, false), None);
|
||||
}
|
||||
}
|
||||
@@ -1,995 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use guarden::defer;
|
||||
use quanta::Instant;
|
||||
use rand::Rng;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use crate::{
|
||||
config::PeerId,
|
||||
connectivity::stun::StunInfoProvider,
|
||||
packet::{HOLE_PUNCH_PACKET_BODY_LEN, new_hole_punch_packet},
|
||||
socket::udp::{UdpBindOptions, VirtualUdpSocketFactory},
|
||||
};
|
||||
|
||||
use super::{
|
||||
SelectPunchListener, SendPunchPacketBothEasySym, SendPunchPacketCone, SendPunchPacketEasySym,
|
||||
SendPunchPacketHardSym, UdpHolePunchRuntime, UdpHolePunchSignalError, UdpHolePunchSignaling,
|
||||
UdpNatType, UdpPunchSocket, UdpSocketArray,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UdpHolePunchClientError {
|
||||
#[error("signaling: {0}")]
|
||||
Signaling(#[from] UdpHolePunchSignalError),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type UdpHolePunchClientResult<T> = Result<T, UdpHolePunchClientError>;
|
||||
|
||||
const UDP_ARRAY_SIZE_FOR_HARD_SYM: usize = 84;
|
||||
const UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM: usize = 25;
|
||||
const DST_PORT_OFFSET: u16 = 20;
|
||||
const REMOTE_WAIT_TIME_MS: u64 = 5000;
|
||||
|
||||
pub fn apply_peer_easy_sym_port_offset(base_port: u16, peer_is_incremental: bool) -> u16 {
|
||||
let port = if peer_is_incremental {
|
||||
(base_port as u32).saturating_add(DST_PORT_OFFSET as u32)
|
||||
} else {
|
||||
(base_port as u32).saturating_sub(DST_PORT_OFFSET as u32)
|
||||
};
|
||||
port as u16
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(runtime, signaling), fields(dst_peer_id), err)]
|
||||
pub async fn punch_cone_to_cone<R, S>(
|
||||
runtime: Arc<R>,
|
||||
signaling: Arc<S>,
|
||||
dst_peer_id: PeerId,
|
||||
) -> UdpHolePunchClientResult<Option<UdpPunchSocket>>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
tracing::info!(?dst_peer_id, "start hole punching");
|
||||
let tid = rand::random();
|
||||
|
||||
let udp_array = UdpSocketArray::new_with_context(1, runtime.clone(), runtime.socket_context());
|
||||
|
||||
let resp = signaling
|
||||
.select_punch_listener(
|
||||
dst_peer_id,
|
||||
SelectPunchListener {
|
||||
force_new: false,
|
||||
prefer_port_mapping: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let remote_mapped_addr = resp.listener_mapped_addr;
|
||||
|
||||
let local_socket = UdpHolePunchRuntime::bind_udp(
|
||||
runtime.as_ref(),
|
||||
UdpBindOptions::hole_punch_control().with_context(
|
||||
runtime
|
||||
.socket_context()
|
||||
.with_ip_version(crate::socket::IpVersion::V4),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let resolved = runtime
|
||||
.resolve_udp_public_addr(local_socket.clone())
|
||||
.await?;
|
||||
let local_mapped_addr = resolved.mapped_addr;
|
||||
let _local_port_mapping_lease = resolved.port_mapping_lease;
|
||||
|
||||
tracing::debug!(
|
||||
?local_mapped_addr,
|
||||
?remote_mapped_addr,
|
||||
"hole punch got remote listener"
|
||||
);
|
||||
|
||||
udp_array.add_new_socket(local_socket).await?;
|
||||
udp_array.add_intreast_tid(tid);
|
||||
let punch_packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
|
||||
|
||||
send_from_local(&udp_array, &punch_packet, remote_mapped_addr).await?;
|
||||
|
||||
let signaling_for_task = signaling.clone();
|
||||
let punch_task = AbortOnDropHandle::new(tokio::spawn(async move {
|
||||
if let Err(e) = signaling_for_task
|
||||
.send_punch_packet_cone(
|
||||
dst_peer_id,
|
||||
SendPunchPacketCone {
|
||||
listener_mapped_addr: remote_mapped_addr,
|
||||
dest_addr: local_mapped_addr,
|
||||
transaction_id: tid,
|
||||
packet_count_per_batch: 2,
|
||||
packet_batch_count: 5,
|
||||
packet_interval_ms: 400,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(?e, "failed to call remote send punch packet");
|
||||
}
|
||||
}));
|
||||
|
||||
let mut finish_time: Option<Instant> = None;
|
||||
while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 {
|
||||
crate::foundation::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
if finish_time.is_none() && punch_task.is_finished() {
|
||||
finish_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::debug!("no punched socket found, send some more hole punch packets");
|
||||
send_from_local(&udp_array, &punch_packet, remote_mapped_addr).await?;
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::debug!(?socket, ?tid, "punched socket found, try connect with it");
|
||||
|
||||
for _ in 0..2 {
|
||||
match runtime
|
||||
.connect_with_socket(socket.socket.clone(), remote_mapped_addr)
|
||||
.await
|
||||
{
|
||||
Ok(socket) => {
|
||||
tracing::info!(?socket, "hole punched");
|
||||
return Ok(Some(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn send_from_local<R>(
|
||||
udp_array: &UdpSocketArray<R>,
|
||||
punch_packet: &[u8],
|
||||
remote_mapped_addr: SocketAddr,
|
||||
) -> UdpHolePunchClientResult<()>
|
||||
where
|
||||
R: VirtualUdpSocketFactory,
|
||||
{
|
||||
udp_array
|
||||
.send_with_all(punch_packet, remote_mapped_addr)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct UdpSymToConePunchClient<R, S>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
runtime: Arc<R>,
|
||||
signaling: Arc<S>,
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
udp_array: RwLock<Option<Arc<UdpSocketArray<R>>>>,
|
||||
try_direct_connect: AtomicBool,
|
||||
punch_predictably: AtomicBool,
|
||||
}
|
||||
|
||||
impl<R, S> UdpSymToConePunchClient<R, S>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
pub fn new(runtime: Arc<R>, signaling: Arc<S>, stun: Arc<dyn StunInfoProvider>) -> Self {
|
||||
Self {
|
||||
runtime,
|
||||
signaling,
|
||||
stun,
|
||||
udp_array: RwLock::new(None),
|
||||
try_direct_connect: AtomicBool::new(true),
|
||||
punch_predictably: AtomicBool::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn clear_udp_array(&self) {
|
||||
let mut wlocked = self.udp_array.write().await;
|
||||
wlocked.take();
|
||||
}
|
||||
|
||||
async fn prepare_udp_array(&self) -> anyhow::Result<Arc<UdpSocketArray<R>>> {
|
||||
let rlocked = self.udp_array.read().await;
|
||||
if let Some(udp_array) = rlocked.clone() {
|
||||
return Ok(udp_array);
|
||||
}
|
||||
|
||||
drop(rlocked);
|
||||
let mut wlocked = self.udp_array.write().await;
|
||||
if let Some(udp_array) = wlocked.clone() {
|
||||
return Ok(udp_array);
|
||||
}
|
||||
|
||||
let udp_array = Arc::new(UdpSocketArray::new_with_context(
|
||||
UDP_ARRAY_SIZE_FOR_HARD_SYM,
|
||||
self.runtime.clone(),
|
||||
self.runtime.socket_context(),
|
||||
));
|
||||
udp_array.start().await?;
|
||||
wlocked.replace(udp_array.clone());
|
||||
Ok(udp_array)
|
||||
}
|
||||
|
||||
async fn get_base_port_for_easy_sym(&self, my_nat_info: UdpNatType) -> Option<u16> {
|
||||
if my_nat_info.is_easy_sym() {
|
||||
match self.stun.get_udp_port_mapping(0).await {
|
||||
Ok(addr) => Some(addr.port()),
|
||||
ret => {
|
||||
tracing::warn!(?ret, "failed to get udp port mapping for easy sym");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn remote_send_hole_punch_packet_predictable(
|
||||
signaling: Arc<S>,
|
||||
dst_peer_id: PeerId,
|
||||
base_port_for_easy_sym: Option<u16>,
|
||||
my_nat_info: UdpNatType,
|
||||
remote_mapped_addr: SocketAddr,
|
||||
public_ips: Vec<Ipv4Addr>,
|
||||
tid: u32,
|
||||
) {
|
||||
let Some(inc) = my_nat_info.get_inc_of_easy_sym() else {
|
||||
return;
|
||||
};
|
||||
let req = SendPunchPacketEasySym {
|
||||
listener_mapped_addr: remote_mapped_addr,
|
||||
public_ips,
|
||||
transaction_id: tid,
|
||||
base_port_num: base_port_for_easy_sym.unwrap() as u32,
|
||||
max_port_num: 50,
|
||||
is_incremental: inc,
|
||||
};
|
||||
tracing::debug!(?req, "send punch packet for easy sym start");
|
||||
let ret = signaling.send_punch_packet_easy_sym(dst_peer_id, req).await;
|
||||
tracing::debug!(?ret, "send punch packet for easy sym return");
|
||||
}
|
||||
|
||||
async fn remote_send_hole_punch_packet_random(
|
||||
signaling: Arc<S>,
|
||||
dst_peer_id: PeerId,
|
||||
remote_mapped_addr: SocketAddr,
|
||||
public_ips: Vec<Ipv4Addr>,
|
||||
tid: u32,
|
||||
round: u32,
|
||||
port_index: u32,
|
||||
) -> Option<u32> {
|
||||
let req = SendPunchPacketHardSym {
|
||||
listener_mapped_addr: remote_mapped_addr,
|
||||
public_ips,
|
||||
transaction_id: tid,
|
||||
round,
|
||||
port_index,
|
||||
};
|
||||
tracing::debug!(?req, "send punch packet for hard sym start");
|
||||
match signaling.send_punch_packet_hard_sym(dst_peer_id, req).await {
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to send punch packet for hard sym");
|
||||
None
|
||||
}
|
||||
Ok(resp) => Some(resp.next_port_index),
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_hole_punch_result<T>(
|
||||
&self,
|
||||
udp_array: &Arc<UdpSocketArray<R>>,
|
||||
packet: &[u8],
|
||||
tid: u32,
|
||||
remote_mapped_addr: SocketAddr,
|
||||
punch_task: &AbortOnDropHandle<T>,
|
||||
) -> anyhow::Result<Option<UdpPunchSocket>> {
|
||||
let mut ret_socket = None;
|
||||
let mut finish_time: Option<Instant> = None;
|
||||
while finish_time.is_none() || finish_time.as_ref().unwrap().elapsed().as_millis() < 1000 {
|
||||
udp_array.send_with_all(packet, remote_mapped_addr).await?;
|
||||
|
||||
crate::foundation::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
|
||||
if finish_time.is_none() && punch_task.is_finished() {
|
||||
finish_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::debug!("no punched socket found, wait for more time");
|
||||
continue;
|
||||
};
|
||||
|
||||
match self
|
||||
.runtime
|
||||
.connect_with_socket(socket.socket.clone(), remote_mapped_addr)
|
||||
.await
|
||||
{
|
||||
Ok(socket) => {
|
||||
ret_socket.replace(socket);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
udp_array.add_new_socket(socket.socket).await?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret_socket)
|
||||
}
|
||||
|
||||
#[tracing::instrument(err(level = tracing::Level::ERROR), skip(self))]
|
||||
pub async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
round: u32,
|
||||
last_port_idx: &mut usize,
|
||||
my_nat_info: UdpNatType,
|
||||
) -> UdpHolePunchClientResult<Option<UdpPunchSocket>> {
|
||||
let udp_array = self.prepare_udp_array().await?;
|
||||
|
||||
let resp = self
|
||||
.signaling
|
||||
.select_punch_listener(
|
||||
dst_peer_id,
|
||||
SelectPunchListener {
|
||||
force_new: false,
|
||||
prefer_port_mapping: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let remote_mapped_addr = resp.listener_mapped_addr;
|
||||
|
||||
if self.try_direct_connect.load(Ordering::Relaxed) {
|
||||
let socket = self.runtime.bind_direct_connect_udp().await?;
|
||||
if let Ok(socket) = self
|
||||
.runtime
|
||||
.connect_with_socket(socket, remote_mapped_addr)
|
||||
.await
|
||||
{
|
||||
return Ok(Some(socket));
|
||||
}
|
||||
}
|
||||
|
||||
let stun_info = self.stun.get_stun_info();
|
||||
let public_ips: Vec<Ipv4Addr> = stun_info
|
||||
.public_ip
|
||||
.iter()
|
||||
.filter_map(|x| x.parse().ok())
|
||||
.collect();
|
||||
if public_ips.is_empty() {
|
||||
return Err(anyhow::anyhow!("failed to get public ips").into());
|
||||
}
|
||||
|
||||
let tid = rand::thread_rng().r#gen();
|
||||
let packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes();
|
||||
udp_array.add_intreast_tid(tid);
|
||||
defer! { udp_array.remove_intreast_tid(tid); }
|
||||
|
||||
let port_index = *last_port_idx as u32;
|
||||
let base_port_for_easy_sym = self.get_base_port_for_easy_sym(my_nat_info).await;
|
||||
udp_array.send_with_all(&packet, remote_mapped_addr).await?;
|
||||
|
||||
if self.punch_predictably.load(Ordering::Relaxed) && base_port_for_easy_sym.is_some() {
|
||||
let signaling = self.signaling.clone();
|
||||
let punch_task = AbortOnDropHandle::new(tokio::spawn(
|
||||
Self::remote_send_hole_punch_packet_predictable(
|
||||
signaling,
|
||||
dst_peer_id,
|
||||
base_port_for_easy_sym,
|
||||
my_nat_info,
|
||||
remote_mapped_addr,
|
||||
public_ips.clone(),
|
||||
tid,
|
||||
),
|
||||
));
|
||||
let ret_socket = self
|
||||
.check_hole_punch_result(&udp_array, &packet, tid, remote_mapped_addr, &punch_task)
|
||||
.await?;
|
||||
|
||||
let task_ret = punch_task.await;
|
||||
tracing::debug!(?ret_socket, ?task_ret, "predictable punch task got result");
|
||||
if let Some(socket) = ret_socket {
|
||||
return Ok(Some(socket));
|
||||
}
|
||||
}
|
||||
|
||||
let signaling = self.signaling.clone();
|
||||
let punch_task =
|
||||
AbortOnDropHandle::new(tokio::spawn(Self::remote_send_hole_punch_packet_random(
|
||||
signaling,
|
||||
dst_peer_id,
|
||||
remote_mapped_addr,
|
||||
public_ips.clone(),
|
||||
tid,
|
||||
round,
|
||||
port_index,
|
||||
)));
|
||||
let ret_socket = self
|
||||
.check_hole_punch_result(&udp_array, &packet, tid, remote_mapped_addr, &punch_task)
|
||||
.await?;
|
||||
|
||||
let punch_task_result = punch_task.await;
|
||||
tracing::debug!(?punch_task_result, ?ret_socket, "punch task got result");
|
||||
|
||||
if let Ok(Some(next_port_idx)) = punch_task_result {
|
||||
*last_port_idx = next_port_idx as usize;
|
||||
} else {
|
||||
*last_port_idx = rand::random();
|
||||
}
|
||||
|
||||
Ok(ret_socket)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpBothEasySymPunchClient<R, S>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
runtime: Arc<R>,
|
||||
signaling: Arc<S>,
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
}
|
||||
|
||||
impl<R, S> UdpBothEasySymPunchClient<R, S>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
pub fn new(runtime: Arc<R>, signaling: Arc<S>, stun: Arc<dyn StunInfoProvider>) -> Self {
|
||||
Self {
|
||||
runtime,
|
||||
signaling,
|
||||
stun,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(ret, skip(self))]
|
||||
pub async fn do_hole_punching(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
my_nat_info: UdpNatType,
|
||||
peer_nat_info: UdpNatType,
|
||||
is_busy: &mut bool,
|
||||
) -> UdpHolePunchClientResult<Option<UdpPunchSocket>> {
|
||||
*is_busy = false;
|
||||
|
||||
let udp_array = UdpSocketArray::new_with_context(
|
||||
UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM,
|
||||
self.runtime.clone(),
|
||||
self.runtime.socket_context(),
|
||||
);
|
||||
udp_array.start().await?;
|
||||
|
||||
let cur_mapped_addr = self.stun.get_udp_port_mapping(0).await?;
|
||||
let my_public_ip = match cur_mapped_addr.ip() {
|
||||
IpAddr::V4(v4) => v4,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("ipv6 is not supported").into());
|
||||
}
|
||||
};
|
||||
let me_is_incremental = my_nat_info
|
||||
.get_inc_of_easy_sym()
|
||||
.ok_or(anyhow::anyhow!("me_is_incremental is required"))?;
|
||||
let peer_is_incremental = peer_nat_info
|
||||
.get_inc_of_easy_sym()
|
||||
.ok_or(anyhow::anyhow!("peer_is_incremental is required"))?;
|
||||
|
||||
let tid = rand::random();
|
||||
udp_array.add_intreast_tid(tid);
|
||||
|
||||
let remote_ret = self
|
||||
.signaling
|
||||
.send_punch_packet_both_easy_sym(
|
||||
dst_peer_id,
|
||||
SendPunchPacketBothEasySym {
|
||||
transaction_id: tid,
|
||||
public_ip: my_public_ip,
|
||||
dst_port_num: if me_is_incremental {
|
||||
cur_mapped_addr.port().saturating_add(DST_PORT_OFFSET)
|
||||
} else {
|
||||
cur_mapped_addr.port().saturating_sub(DST_PORT_OFFSET)
|
||||
} as u32,
|
||||
udp_socket_count: UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM as u32,
|
||||
wait_time_ms: REMOTE_WAIT_TIME_MS as u32,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if remote_ret.is_busy {
|
||||
*is_busy = true;
|
||||
return Err(anyhow::anyhow!("remote is busy").into());
|
||||
}
|
||||
|
||||
let mut remote_mapped_addr = remote_ret
|
||||
.base_mapped_addr
|
||||
.ok_or(anyhow::anyhow!("remote_mapped_addr is required"))?;
|
||||
|
||||
let now = Instant::now();
|
||||
remote_mapped_addr.set_port(apply_peer_easy_sym_port_offset(
|
||||
remote_mapped_addr.port(),
|
||||
peer_is_incremental,
|
||||
));
|
||||
tracing::debug!(
|
||||
?remote_mapped_addr,
|
||||
?remote_ret,
|
||||
"start send hole punch packet for both easy sym"
|
||||
);
|
||||
|
||||
while now.elapsed().as_millis() < (REMOTE_WAIT_TIME_MS + 1000).into() {
|
||||
udp_array
|
||||
.send_with_all(
|
||||
&new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN).into_bytes(),
|
||||
remote_mapped_addr,
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::foundation::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let Some(socket) = udp_array.try_fetch_punched_socket(tid) else {
|
||||
tracing::trace!(
|
||||
?remote_mapped_addr,
|
||||
?tid,
|
||||
"no punched socket found, send some more hole punch packets"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
?socket,
|
||||
?remote_mapped_addr,
|
||||
?tid,
|
||||
"got punched socket in both easy sym"
|
||||
);
|
||||
|
||||
for _ in 0..2 {
|
||||
match self
|
||||
.runtime
|
||||
.connect_with_socket(socket.socket.clone(), remote_mapped_addr)
|
||||
.await
|
||||
{
|
||||
Ok(socket) => {
|
||||
return Ok(Some(socket));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(?e, "failed to connect with socket");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
udp_array.add_new_socket(socket.socket).await?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
proto::common::{NatType, StunInfo},
|
||||
socket::udp::VirtualUdpSocket,
|
||||
};
|
||||
|
||||
impl<R, S> UdpSymToConePunchClient<R, S>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
{
|
||||
fn set_try_direct_connect(&self, enabled: bool) {
|
||||
self.try_direct_connect.store(enabled, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
struct MockSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, _data: &[u8], _addr: SocketAddr) -> io::Result<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
struct MockRuntime {
|
||||
bind_count: AtomicUsize,
|
||||
resolve_count: AtomicUsize,
|
||||
bind_options: tokio::sync::Mutex<Vec<UdpBindOptions>>,
|
||||
}
|
||||
|
||||
impl MockRuntime {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
bind_count: AtomicUsize::new(0),
|
||||
resolve_count: AtomicUsize::new(0),
|
||||
bind_options: tokio::sync::Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UdpHolePunchRuntime for MockRuntime {
|
||||
type Socket = MockSocket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.bind_options.lock().await.push(options);
|
||||
let bind_idx = self.bind_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(Arc::new(MockSocket {
|
||||
local_addr: SocketAddr::from(([127, 0, 0, 1], 10000 + bind_idx as u16)),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn resolve_udp_public_addr(
|
||||
&self,
|
||||
_socket: Arc<Self::Socket>,
|
||||
) -> anyhow::Result<super::super::UdpResolvedPublicAddr> {
|
||||
self.resolve_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(super::super::UdpResolvedPublicAddr {
|
||||
mapped_addr: SocketAddr::from(([203, 0, 113, 1], 10000)),
|
||||
port_mapping_lease: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_listener(
|
||||
&self,
|
||||
_prefer_port_mapping: bool,
|
||||
) -> anyhow::Result<super::super::UdpPunchListener<Self::Socket>> {
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
|
||||
async fn create_port_bound_listener(
|
||||
&self,
|
||||
_port: u16,
|
||||
) -> anyhow::Result<super::super::UdpPunchListener<Self::Socket>> {
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
|
||||
async fn connect_with_socket(
|
||||
&self,
|
||||
_socket: Arc<Self::Socket>,
|
||||
_remote: SocketAddr,
|
||||
) -> anyhow::Result<UdpPunchSocket> {
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockStunInfoProvider {
|
||||
port_mapping_count: AtomicUsize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl StunInfoProvider for MockStunInfoProvider {
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
StunInfo {
|
||||
public_ip: vec!["127.0.0.1".to_string()],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, _port: u16) -> anyhow::Result<SocketAddr> {
|
||||
self.port_mapping_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(SocketAddr::from(([203, 0, 113, 1], 10000)))
|
||||
}
|
||||
|
||||
async fn get_tcp_port_mapping(&self, _port: u16) -> anyhow::Result<SocketAddr> {
|
||||
unreachable!("TCP mapping is not used by UDP hole-punch tests")
|
||||
}
|
||||
|
||||
fn update_stun_info(&self) {}
|
||||
}
|
||||
|
||||
struct RejectingSignaling;
|
||||
|
||||
#[async_trait]
|
||||
impl UdpHolePunchSignaling for RejectingSignaling {
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: SelectPunchListener,
|
||||
) -> Result<super::super::SelectPunchListenerResponse, UdpHolePunchSignalError> {
|
||||
Err(UdpHolePunchSignalError::InvalidServiceKey)
|
||||
}
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: super::super::SendPunchPacketHardSym,
|
||||
) -> Result<super::super::SendPunchPacketHardSymResponse, UdpHolePunchSignalError> {
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: super::super::SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: super::super::SendPunchPacketBothEasySym,
|
||||
) -> Result<super::super::SendPunchPacketBothEasySymResponse, UdpHolePunchSignalError>
|
||||
{
|
||||
unimplemented!("not used by cone client tests")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cone_punch_does_not_bind_or_resolve_before_listener_rpc_succeeds() {
|
||||
let runtime = Arc::new(MockRuntime::new());
|
||||
let signaling = Arc::new(RejectingSignaling);
|
||||
|
||||
let err = punch_cone_to_cone(runtime.clone(), signaling, 2)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
UdpHolePunchClientError::Signaling(UdpHolePunchSignalError::InvalidServiceKey)
|
||||
));
|
||||
assert_eq!(runtime.bind_count.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(runtime.resolve_count.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_direct_connect_bind_uses_direct_connect_purpose() {
|
||||
let runtime = MockRuntime::new();
|
||||
|
||||
let socket = runtime.bind_direct_connect_udp().await.unwrap();
|
||||
|
||||
assert_eq!(socket.local_addr().unwrap().port(), 10000);
|
||||
let bind_options = runtime.bind_options.lock().await;
|
||||
assert_eq!(
|
||||
bind_options.as_slice(),
|
||||
&[UdpBindOptions::direct_connect().with_ip_version(crate::socket::IpVersion::V4)]
|
||||
);
|
||||
}
|
||||
|
||||
struct RecordingSignaling {
|
||||
easy_requests: tokio::sync::Mutex<Vec<SendPunchPacketEasySym>>,
|
||||
hard_requests: tokio::sync::Mutex<Vec<SendPunchPacketHardSym>>,
|
||||
both_requests: tokio::sync::Mutex<Vec<SendPunchPacketBothEasySym>>,
|
||||
both_response: super::super::SendPunchPacketBothEasySymResponse,
|
||||
next_port_index: u32,
|
||||
}
|
||||
|
||||
impl RecordingSignaling {
|
||||
fn new(next_port_index: u32) -> Self {
|
||||
Self {
|
||||
easy_requests: tokio::sync::Mutex::new(Vec::new()),
|
||||
hard_requests: tokio::sync::Mutex::new(Vec::new()),
|
||||
both_requests: tokio::sync::Mutex::new(Vec::new()),
|
||||
both_response: super::super::SendPunchPacketBothEasySymResponse {
|
||||
is_busy: false,
|
||||
base_mapped_addr: Some(SocketAddr::from(([127, 0, 0, 1], 40144))),
|
||||
},
|
||||
next_port_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_both_response(
|
||||
mut self,
|
||||
both_response: super::super::SendPunchPacketBothEasySymResponse,
|
||||
) -> Self {
|
||||
self.both_response = both_response;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UdpHolePunchSignaling for RecordingSignaling {
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: SelectPunchListener,
|
||||
) -> Result<super::super::SelectPunchListenerResponse, UdpHolePunchSignalError> {
|
||||
Ok(super::super::SelectPunchListenerResponse {
|
||||
listener_mapped_addr: SocketAddr::from(([127, 0, 0, 1], 30000)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
_request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
request: SendPunchPacketHardSym,
|
||||
) -> Result<super::super::SendPunchPacketHardSymResponse, UdpHolePunchSignalError> {
|
||||
self.hard_requests.lock().await.push(request);
|
||||
Ok(super::super::SendPunchPacketHardSymResponse {
|
||||
next_port_index: self.next_port_index,
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
request: SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
self.easy_requests.lock().await.push(request);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
_dst_peer_id: PeerId,
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> Result<super::super::SendPunchPacketBothEasySymResponse, UdpHolePunchSignalError>
|
||||
{
|
||||
self.both_requests.lock().await.push(request);
|
||||
Ok(self.both_response.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sym_to_cone_easy_sym_uses_port_mapping_in_predictable_request() {
|
||||
let runtime = Arc::new(MockRuntime::new());
|
||||
let signaling = Arc::new(RecordingSignaling::new(42));
|
||||
let stun = Arc::new(MockStunInfoProvider::default());
|
||||
let client = UdpSymToConePunchClient::new(runtime.clone(), signaling.clone(), stun.clone());
|
||||
client.set_try_direct_connect(false);
|
||||
|
||||
let mut last_port_idx = 7;
|
||||
let ret = client
|
||||
.do_hole_punching(2, 3, &mut last_port_idx, NatType::SymmetricEasyInc.into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(ret.is_none());
|
||||
assert_eq!(stun.port_mapping_count.load(Ordering::Relaxed), 1);
|
||||
|
||||
let easy_requests = signaling.easy_requests.lock().await;
|
||||
assert_eq!(easy_requests.len(), 1);
|
||||
let req = &easy_requests[0];
|
||||
assert_eq!(
|
||||
req.listener_mapped_addr,
|
||||
SocketAddr::from(([127, 0, 0, 1], 30000))
|
||||
);
|
||||
assert_eq!(req.public_ips, vec![Ipv4Addr::new(127, 0, 0, 1)]);
|
||||
assert_eq!(req.base_port_num, 10000);
|
||||
assert_eq!(req.max_port_num, 50);
|
||||
assert!(req.is_incremental);
|
||||
|
||||
let hard_requests = signaling.hard_requests.lock().await;
|
||||
assert_eq!(hard_requests.len(), 1);
|
||||
assert_eq!(last_port_idx, 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sym_to_cone_hard_sym_sends_random_request_and_updates_port_index() {
|
||||
let runtime = Arc::new(MockRuntime::new());
|
||||
let signaling = Arc::new(RecordingSignaling::new(321));
|
||||
let stun = Arc::new(MockStunInfoProvider::default());
|
||||
let client = UdpSymToConePunchClient::new(runtime.clone(), signaling.clone(), stun.clone());
|
||||
client.set_try_direct_connect(false);
|
||||
|
||||
let mut last_port_idx = 123;
|
||||
let ret = client
|
||||
.do_hole_punching(2, 4, &mut last_port_idx, NatType::Symmetric.into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(ret.is_none());
|
||||
assert_eq!(stun.port_mapping_count.load(Ordering::Relaxed), 0);
|
||||
assert!(signaling.easy_requests.lock().await.is_empty());
|
||||
|
||||
let hard_requests = signaling.hard_requests.lock().await;
|
||||
assert_eq!(hard_requests.len(), 1);
|
||||
let req = &hard_requests[0];
|
||||
assert_eq!(
|
||||
req.listener_mapped_addr,
|
||||
SocketAddr::from(([127, 0, 0, 1], 30000))
|
||||
);
|
||||
assert_eq!(req.public_ips, vec![Ipv4Addr::new(127, 0, 0, 1)]);
|
||||
assert_eq!(req.round, 4);
|
||||
assert_eq!(req.port_index, 123);
|
||||
assert_eq!(last_port_idx, 321);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_easy_sym_port_offset_preserves_old_proto_cast_semantics() {
|
||||
assert_eq!(apply_peer_easy_sym_port_offset(65530, true), 14);
|
||||
assert_eq!(apply_peer_easy_sym_port_offset(10, false), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn both_easy_sym_sends_remote_request_and_reports_busy() {
|
||||
let runtime = Arc::new(MockRuntime::new());
|
||||
let stun = Arc::new(MockStunInfoProvider::default());
|
||||
let signaling = Arc::new(RecordingSignaling::new(0).with_both_response(
|
||||
super::super::SendPunchPacketBothEasySymResponse {
|
||||
is_busy: true,
|
||||
base_mapped_addr: None,
|
||||
},
|
||||
));
|
||||
let client =
|
||||
UdpBothEasySymPunchClient::new(runtime.clone(), signaling.clone(), stun.clone());
|
||||
|
||||
let mut is_busy = false;
|
||||
let err = client
|
||||
.do_hole_punching(
|
||||
2,
|
||||
NatType::SymmetricEasyInc.into(),
|
||||
NatType::SymmetricEasyDec.into(),
|
||||
&mut is_busy,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(is_busy);
|
||||
assert!(err.to_string().contains("remote is busy"));
|
||||
assert_eq!(stun.port_mapping_count.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(
|
||||
runtime.bind_count.load(Ordering::Relaxed),
|
||||
UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM
|
||||
);
|
||||
|
||||
let both_requests = signaling.both_requests.lock().await;
|
||||
assert_eq!(both_requests.len(), 1);
|
||||
let req = &both_requests[0];
|
||||
assert_eq!(req.public_ip, Ipv4Addr::new(203, 0, 113, 1));
|
||||
assert_eq!(req.dst_port_num, 10020);
|
||||
assert_eq!(
|
||||
req.udp_socket_count,
|
||||
UDP_ARRAY_SIZE_FOR_BOTH_EASY_SYM as u32
|
||||
);
|
||||
assert_eq!(req.wait_time_ms, REMOTE_WAIT_TIME_MS as u32);
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
use crate::{config::PeerId, proto::common::NatType};
|
||||
|
||||
pub const BLACKLIST_TIMEOUT_SEC: u64 = 3600;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UdpPunchClientMethod {
|
||||
None,
|
||||
ConeToCone,
|
||||
SymToCone,
|
||||
EasySymToEasySym,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UdpNatType {
|
||||
Unknown,
|
||||
Open(NatType),
|
||||
Cone(NatType),
|
||||
EasySymmetric(NatType, bool),
|
||||
HardSymmetric(NatType),
|
||||
}
|
||||
|
||||
impl From<NatType> for UdpNatType {
|
||||
fn from(nat_type: NatType) -> Self {
|
||||
match nat_type {
|
||||
NatType::Unknown => UdpNatType::Unknown,
|
||||
NatType::OpenInternet => UdpNatType::Open(nat_type),
|
||||
NatType::NoPat | NatType::FullCone | NatType::Restricted | NatType::PortRestricted => {
|
||||
UdpNatType::Cone(nat_type)
|
||||
}
|
||||
NatType::Symmetric | NatType::SymUdpFirewall => UdpNatType::HardSymmetric(nat_type),
|
||||
NatType::SymmetricEasyInc => UdpNatType::EasySymmetric(nat_type, true),
|
||||
NatType::SymmetricEasyDec => UdpNatType::EasySymmetric(nat_type, false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UdpNatType> for NatType {
|
||||
fn from(val: UdpNatType) -> Self {
|
||||
match val {
|
||||
UdpNatType::Unknown => NatType::Unknown,
|
||||
UdpNatType::Open(nat_type) => nat_type,
|
||||
UdpNatType::Cone(nat_type) => nat_type,
|
||||
UdpNatType::EasySymmetric(nat_type, _) => nat_type,
|
||||
UdpNatType::HardSymmetric(nat_type) => nat_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpNatType {
|
||||
pub fn is_open(&self) -> bool {
|
||||
matches!(self, UdpNatType::Open(_))
|
||||
}
|
||||
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, UdpNatType::Unknown)
|
||||
}
|
||||
|
||||
pub fn is_sym(&self) -> bool {
|
||||
self.is_hard_sym() || self.is_easy_sym()
|
||||
}
|
||||
|
||||
pub fn is_hard_sym(&self) -> bool {
|
||||
matches!(self, UdpNatType::HardSymmetric(_))
|
||||
}
|
||||
|
||||
pub fn is_easy_sym(&self) -> bool {
|
||||
matches!(self, UdpNatType::EasySymmetric(_, _))
|
||||
}
|
||||
|
||||
pub fn is_cone(&self) -> bool {
|
||||
matches!(self, UdpNatType::Cone(_))
|
||||
}
|
||||
|
||||
pub fn get_inc_of_easy_sym(&self) -> Option<bool> {
|
||||
match self {
|
||||
UdpNatType::EasySymmetric(_, inc) => Some(*inc),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_punch_hole_method(
|
||||
&self,
|
||||
other: Self,
|
||||
disable_sym_hole_punching: bool,
|
||||
) -> UdpPunchClientMethod {
|
||||
if disable_sym_hole_punching && self.is_sym() {
|
||||
if other.is_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
}
|
||||
|
||||
if other.is_unknown() {
|
||||
if self.is_sym() {
|
||||
return UdpPunchClientMethod::SymToCone;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_unknown() {
|
||||
if other.is_sym() {
|
||||
return UdpPunchClientMethod::None;
|
||||
} else {
|
||||
return UdpPunchClientMethod::ConeToCone;
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_open() || other.is_open() {
|
||||
return UdpPunchClientMethod::None;
|
||||
}
|
||||
|
||||
if self.is_cone() {
|
||||
if other.is_sym() {
|
||||
UdpPunchClientMethod::None
|
||||
} else {
|
||||
UdpPunchClientMethod::ConeToCone
|
||||
}
|
||||
} else if self.is_easy_sym() {
|
||||
if other.is_hard_sym() {
|
||||
UdpPunchClientMethod::None
|
||||
} else if other.is_easy_sym() {
|
||||
UdpPunchClientMethod::EasySymToEasySym
|
||||
} else {
|
||||
UdpPunchClientMethod::SymToCone
|
||||
}
|
||||
} else if self.is_hard_sym() {
|
||||
if other.is_sym() {
|
||||
UdpPunchClientMethod::None
|
||||
} else {
|
||||
UdpPunchClientMethod::SymToCone
|
||||
}
|
||||
} else {
|
||||
unreachable!("invalid nat type");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_punch_hole_as_client(
|
||||
&self,
|
||||
other: Self,
|
||||
my_peer_id: PeerId,
|
||||
dst_peer_id: PeerId,
|
||||
disable_sym_hole_punching: bool,
|
||||
) -> bool {
|
||||
match self.get_punch_hole_method(other, disable_sym_hole_punching) {
|
||||
UdpPunchClientMethod::None => false,
|
||||
UdpPunchClientMethod::ConeToCone | UdpPunchClientMethod::SymToCone => true,
|
||||
UdpPunchClientMethod::EasySymToEasySym => my_peer_id < dst_peer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn nat(nat_type: NatType) -> UdpNatType {
|
||||
nat_type.into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_type_classification_matches_proto_values() {
|
||||
assert_eq!(nat(NatType::Unknown), UdpNatType::Unknown);
|
||||
assert_eq!(
|
||||
nat(NatType::OpenInternet),
|
||||
UdpNatType::Open(NatType::OpenInternet)
|
||||
);
|
||||
assert_eq!(nat(NatType::FullCone), UdpNatType::Cone(NatType::FullCone));
|
||||
assert_eq!(
|
||||
nat(NatType::Symmetric),
|
||||
UdpNatType::HardSymmetric(NatType::Symmetric)
|
||||
);
|
||||
assert_eq!(
|
||||
nat(NatType::SymmetricEasyInc),
|
||||
UdpNatType::EasySymmetric(NatType::SymmetricEasyInc, true)
|
||||
);
|
||||
assert_eq!(
|
||||
nat(NatType::SymmetricEasyDec),
|
||||
UdpNatType::EasySymmetric(NatType::SymmetricEasyDec, false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn punch_method_preserves_current_nat_matrix() {
|
||||
let cone = nat(NatType::FullCone);
|
||||
let hard_sym = nat(NatType::Symmetric);
|
||||
let easy_sym = nat(NatType::SymmetricEasyInc);
|
||||
let open = nat(NatType::OpenInternet);
|
||||
let unknown = nat(NatType::Unknown);
|
||||
|
||||
assert_eq!(
|
||||
cone.get_punch_hole_method(cone, false),
|
||||
UdpPunchClientMethod::ConeToCone
|
||||
);
|
||||
assert_eq!(
|
||||
hard_sym.get_punch_hole_method(cone, false),
|
||||
UdpPunchClientMethod::SymToCone
|
||||
);
|
||||
assert_eq!(
|
||||
cone.get_punch_hole_method(hard_sym, false),
|
||||
UdpPunchClientMethod::None
|
||||
);
|
||||
assert_eq!(
|
||||
easy_sym.get_punch_hole_method(easy_sym, false),
|
||||
UdpPunchClientMethod::EasySymToEasySym
|
||||
);
|
||||
assert_eq!(
|
||||
easy_sym.get_punch_hole_method(hard_sym, false),
|
||||
UdpPunchClientMethod::None
|
||||
);
|
||||
assert_eq!(
|
||||
hard_sym.get_punch_hole_method(unknown, false),
|
||||
UdpPunchClientMethod::SymToCone
|
||||
);
|
||||
assert_eq!(
|
||||
unknown.get_punch_hole_method(hard_sym, false),
|
||||
UdpPunchClientMethod::None
|
||||
);
|
||||
assert_eq!(
|
||||
open.get_punch_hole_method(cone, false),
|
||||
UdpPunchClientMethod::None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_symmetric_punching_keeps_existing_fallback() {
|
||||
let cone = nat(NatType::FullCone);
|
||||
let hard_sym = nat(NatType::Symmetric);
|
||||
let easy_sym = nat(NatType::SymmetricEasyInc);
|
||||
|
||||
assert_eq!(
|
||||
hard_sym.get_punch_hole_method(cone, true),
|
||||
UdpPunchClientMethod::ConeToCone
|
||||
);
|
||||
assert_eq!(
|
||||
hard_sym.get_punch_hole_method(easy_sym, true),
|
||||
UdpPunchClientMethod::None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easy_sym_to_easy_sym_uses_lower_peer_id_as_client() {
|
||||
let easy_sym = nat(NatType::SymmetricEasyInc);
|
||||
|
||||
assert!(easy_sym.can_punch_hole_as_client(easy_sym, 1, 2, false));
|
||||
assert!(!easy_sym.can_punch_hole_as_client(easy_sym, 2, 1, false));
|
||||
}
|
||||
}
|
||||
@@ -1,529 +0,0 @@
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use anyhow::Error;
|
||||
use dashmap::DashMap;
|
||||
use quanta::Instant;
|
||||
use tokio::{
|
||||
sync::{Mutex, OwnedMutexGuard, TryLockError},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::PeerId,
|
||||
connectivity::stun::StunInfoProvider,
|
||||
foundation::task::{ExternalTaskSignal, PeerTaskLauncher, PeerTaskManager},
|
||||
proto::common::NatType,
|
||||
};
|
||||
|
||||
use crate::connectivity::hole_punch::policy::BackOff;
|
||||
|
||||
use super::{
|
||||
BLACKLIST_TIMEOUT_SEC, UdpBothEasySymPunchClient, UdpHolePunchClientError,
|
||||
UdpHolePunchPeerSource, UdpHolePunchRuntime, UdpHolePunchSignaling, UdpHolePunchTransportSink,
|
||||
UdpNatType, UdpPunchClientMethod, UdpPunchSocket, UdpPunchTaskInfo, UdpSymToConePunchClient,
|
||||
collect_udp_punch_tasks, punch_cone_to_cone, should_blacklist_signal_error,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct UdpSymPunchLock {
|
||||
inner: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl UdpSymPunchLock {
|
||||
pub(crate) async fn lock(&self) -> OwnedMutexGuard<()> {
|
||||
self.inner.clone().lock_owned().await
|
||||
}
|
||||
|
||||
pub(crate) fn try_lock(&self) -> Result<OwnedMutexGuard<()>, TryLockError> {
|
||||
self.inner.clone().try_lock_owned()
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpHolePunchBlacklist {
|
||||
items: DashMap<PeerId, Instant>,
|
||||
}
|
||||
|
||||
impl UdpHolePunchBlacklist {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
items: DashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn contains(&self, peer_id: PeerId) -> bool {
|
||||
let Some(insert_time) = self.items.get(&peer_id) else {
|
||||
return false;
|
||||
};
|
||||
let expired = insert_time.elapsed().as_secs() >= BLACKLIST_TIMEOUT_SEC;
|
||||
drop(insert_time);
|
||||
|
||||
if expired {
|
||||
self.items.remove(&peer_id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&self, peer_id: PeerId) {
|
||||
self.items.insert(peer_id, Instant::now());
|
||||
}
|
||||
|
||||
fn cleanup(&self) {
|
||||
self.items
|
||||
.retain(|_, insert_time| insert_time.elapsed().as_secs() < BLACKLIST_TIMEOUT_SEC);
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpHolePunchConnectorParts<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
peer_source: Arc<P>,
|
||||
signaling: Arc<S>,
|
||||
transport_sink: Arc<T>,
|
||||
runtime: Arc<R>,
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
sym_punch_lock: UdpSymPunchLock,
|
||||
try_cone_before_sym: AtomicBool,
|
||||
}
|
||||
|
||||
pub struct UdpHolePunchConnectorData<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
peer_source: Arc<P>,
|
||||
signaling: Arc<S>,
|
||||
transport_sink: Arc<T>,
|
||||
runtime: Arc<R>,
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
sym_punch_lock: UdpSymPunchLock,
|
||||
blacklist: UdpHolePunchBlacklist,
|
||||
try_cone_before_sym: Arc<AtomicBool>,
|
||||
pub sym_to_cone_client: UdpSymToConePunchClient<R, S>,
|
||||
pub both_easy_sym_client: UdpBothEasySymPunchClient<R, S>,
|
||||
}
|
||||
|
||||
impl<P, S, T, R> UdpHolePunchConnectorData<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
fn new(parts: Arc<UdpHolePunchConnectorParts<P, S, T, R>>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
peer_source: parts.peer_source.clone(),
|
||||
signaling: parts.signaling.clone(),
|
||||
transport_sink: parts.transport_sink.clone(),
|
||||
runtime: parts.runtime.clone(),
|
||||
stun: parts.stun.clone(),
|
||||
sym_punch_lock: parts.sym_punch_lock.clone(),
|
||||
blacklist: UdpHolePunchBlacklist::new(),
|
||||
try_cone_before_sym: Arc::new(AtomicBool::new(
|
||||
parts.try_cone_before_sym.load(Ordering::Relaxed),
|
||||
)),
|
||||
sym_to_cone_client: UdpSymToConePunchClient::new(
|
||||
parts.runtime.clone(),
|
||||
parts.signaling.clone(),
|
||||
parts.stun.clone(),
|
||||
),
|
||||
both_easy_sym_client: UdpBothEasySymPunchClient::new(
|
||||
parts.runtime.clone(),
|
||||
parts.signaling.clone(),
|
||||
parts.stun.clone(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn should_skip_blacklisted(&self, peer_id: PeerId) -> bool {
|
||||
if self.blacklist.contains(peer_id) {
|
||||
tracing::debug!(
|
||||
dst_peer_id = peer_id,
|
||||
"peer is blacklisted, skipping hole punching"
|
||||
);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn map_client_result(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
ret: Result<Option<UdpPunchSocket>, UdpHolePunchClientError>,
|
||||
) -> Result<Option<UdpPunchSocket>, Error> {
|
||||
match ret {
|
||||
Ok(ret) => Ok(ret),
|
||||
Err(UdpHolePunchClientError::Signaling(err)) => {
|
||||
if should_blacklist_signal_error(&err) {
|
||||
self.blacklist.insert(dst_peer_id);
|
||||
}
|
||||
Err(err.into())
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn handle_punch_result(
|
||||
&self,
|
||||
ret: Result<Option<UdpPunchSocket>, Error>,
|
||||
backoff: Option<&mut BackOff>,
|
||||
round: Option<&mut u32>,
|
||||
) -> bool {
|
||||
let op = |rollback: bool| {
|
||||
if rollback {
|
||||
if let Some(backoff) = backoff {
|
||||
backoff.rollback();
|
||||
}
|
||||
if let Some(round) = round {
|
||||
*round = round.saturating_sub(1);
|
||||
}
|
||||
} else if let Some(round) = round {
|
||||
*round += 1;
|
||||
}
|
||||
};
|
||||
|
||||
match ret {
|
||||
Ok(Some(socket)) => {
|
||||
let (connected, requested_url) = socket.into_connected();
|
||||
if let Err(err) = self
|
||||
.transport_sink
|
||||
.add_client_transport(connected, requested_url)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(?err, "upgrade or add UDP hole-punch transport failed");
|
||||
op(true);
|
||||
false
|
||||
} else {
|
||||
tracing::info!(
|
||||
"hole punching transport admitted; awaiting liveness measurement"
|
||||
);
|
||||
true
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::info!("hole punching failed, no punched socket");
|
||||
op(false);
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::info!(?err, "hole punching failed");
|
||||
op(true);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn cone_to_cone(self: Arc<Self>, task_info: UdpPunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff = BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000]);
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
if self.should_skip_blacklisted(task_info.dst_peer_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
let ret = punch_cone_to_cone(
|
||||
self.runtime.clone(),
|
||||
self.signaling.clone(),
|
||||
task_info.dst_peer_id,
|
||||
)
|
||||
.await;
|
||||
let ret = self.map_client_result(task_info.dst_peer_id, ret);
|
||||
|
||||
if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), None)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn sym_to_cone(self: Arc<Self>, task_info: UdpPunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff =
|
||||
BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]);
|
||||
let mut round = 0;
|
||||
let mut port_idx = rand::random();
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
if self.should_skip_blacklisted(task_info.dst_peer_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
if self.try_cone_before_sym.load(Ordering::Relaxed) {
|
||||
let ret = punch_cone_to_cone(
|
||||
self.runtime.clone(),
|
||||
self.signaling.clone(),
|
||||
task_info.dst_peer_id,
|
||||
)
|
||||
.await;
|
||||
let ret = self.map_client_result(task_info.dst_peer_id, ret);
|
||||
if self.handle_punch_result(ret, None, None).await {
|
||||
break;
|
||||
}
|
||||
if self.should_skip_blacklisted(task_info.dst_peer_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ret = {
|
||||
let _lock = self.sym_punch_lock.lock().await;
|
||||
self.sym_to_cone_client
|
||||
.do_hole_punching(
|
||||
task_info.dst_peer_id,
|
||||
round,
|
||||
&mut port_idx,
|
||||
task_info.my_nat_type,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let ret = self.map_client_result(task_info.dst_peer_id, ret);
|
||||
|
||||
if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), Some(&mut round))
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn both_easy_sym(self: Arc<Self>, task_info: UdpPunchTaskInfo) -> Result<(), Error> {
|
||||
let mut backoff =
|
||||
BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]);
|
||||
|
||||
loop {
|
||||
backoff.sleep_for_next_backoff().await;
|
||||
|
||||
if self.should_skip_blacklisted(task_info.dst_peer_id) {
|
||||
break;
|
||||
}
|
||||
|
||||
if self.try_cone_before_sym.load(Ordering::Relaxed) {
|
||||
let ret = punch_cone_to_cone(
|
||||
self.runtime.clone(),
|
||||
self.signaling.clone(),
|
||||
task_info.dst_peer_id,
|
||||
)
|
||||
.await;
|
||||
let ret = self.map_client_result(task_info.dst_peer_id, ret);
|
||||
if self.handle_punch_result(ret, None, None).await {
|
||||
break;
|
||||
}
|
||||
if self.should_skip_blacklisted(task_info.dst_peer_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut is_busy = false;
|
||||
let ret = {
|
||||
let _lock = self.sym_punch_lock.lock().await;
|
||||
self.both_easy_sym_client
|
||||
.do_hole_punching(
|
||||
task_info.dst_peer_id,
|
||||
task_info.my_nat_type,
|
||||
task_info.dst_nat_type,
|
||||
&mut is_busy,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let ret = self.map_client_result(task_info.dst_peer_id, ret);
|
||||
|
||||
if is_busy {
|
||||
backoff.rollback();
|
||||
} else if self
|
||||
.handle_punch_result(ret, Some(&mut backoff), None)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct UdpHolePunchPeerTaskLauncher<P, S, T, R>(Arc<UdpHolePunchConnectorData<P, S, T, R>>)
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime;
|
||||
|
||||
impl<P, S, T, R> Clone for UdpHolePunchPeerTaskLauncher<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<P, S, T, R> PeerTaskLauncher for UdpHolePunchPeerTaskLauncher<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
type CollectPeerItem = UdpPunchTaskInfo;
|
||||
type TaskRet = ();
|
||||
|
||||
async fn collect_peers_need_task(&self) -> Vec<Self::CollectPeerItem> {
|
||||
let data = &self.0;
|
||||
let my_nat_type = data.stun.get_stun_info().udp_nat_type;
|
||||
let my_nat_type: UdpNatType = NatType::try_from(my_nat_type)
|
||||
.unwrap_or(NatType::Unknown)
|
||||
.into();
|
||||
if !my_nat_type.is_sym() {
|
||||
data.sym_to_cone_client.clear_udp_array().await;
|
||||
}
|
||||
|
||||
if my_nat_type.is_open() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
data.blacklist.cleanup();
|
||||
|
||||
let my_peer_id = data.peer_source.local_peer_id();
|
||||
let policy = data.peer_source.p2p_policy_flags();
|
||||
let candidates = data.peer_source.candidates().await;
|
||||
let peers_to_connect =
|
||||
collect_udp_punch_tasks(my_peer_id, my_nat_type, policy, candidates, |peer_id| {
|
||||
data.blacklist.contains(peer_id)
|
||||
});
|
||||
for task in &peers_to_connect {
|
||||
tracing::info!(
|
||||
peer_id = task.dst_peer_id,
|
||||
peer_nat_type = ?task.dst_nat_type,
|
||||
?my_nat_type,
|
||||
"found peer to do hole punching"
|
||||
);
|
||||
}
|
||||
|
||||
peers_to_connect
|
||||
}
|
||||
|
||||
async fn launch_task(
|
||||
&self,
|
||||
item: Self::CollectPeerItem,
|
||||
) -> JoinHandle<Result<Self::TaskRet, Error>> {
|
||||
let data = self.0.clone();
|
||||
let disable_sym_hole_punching = data
|
||||
.peer_source
|
||||
.p2p_policy_flags()
|
||||
.disable_sym_hole_punching;
|
||||
let punch_method = item
|
||||
.my_nat_type
|
||||
.get_punch_hole_method(item.dst_nat_type, disable_sym_hole_punching);
|
||||
match punch_method {
|
||||
UdpPunchClientMethod::ConeToCone => tokio::spawn(data.cone_to_cone(item)),
|
||||
UdpPunchClientMethod::SymToCone => tokio::spawn(data.sym_to_cone(item)),
|
||||
UdpPunchClientMethod::EasySymToEasySym => tokio::spawn(data.both_easy_sym(item)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn all_task_done(&self) {
|
||||
self.0.sym_to_cone_client.clear_udp_array().await;
|
||||
}
|
||||
|
||||
fn loop_interval_ms(&self) -> u64 {
|
||||
5000
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpHolePunchConnector<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
client: PeerTaskManager<UdpHolePunchPeerTaskLauncher<P, S, T, R>>,
|
||||
}
|
||||
|
||||
impl<P, S, T, R> UdpHolePunchConnector<P, S, T, R>
|
||||
where
|
||||
P: UdpHolePunchPeerSource + 'static,
|
||||
S: UdpHolePunchSignaling + 'static,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
R: UdpHolePunchRuntime,
|
||||
{
|
||||
pub fn new(
|
||||
peer_source: Arc<P>,
|
||||
signaling: Arc<S>,
|
||||
transport_sink: Arc<T>,
|
||||
runtime: Arc<R>,
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
sym_punch_lock: UdpSymPunchLock,
|
||||
external_signal: Option<Arc<ExternalTaskSignal>>,
|
||||
) -> Self {
|
||||
let parts = Arc::new(UdpHolePunchConnectorParts {
|
||||
peer_source,
|
||||
signaling,
|
||||
transport_sink,
|
||||
runtime,
|
||||
stun,
|
||||
sym_punch_lock,
|
||||
try_cone_before_sym: AtomicBool::new(true),
|
||||
});
|
||||
let data = UdpHolePunchConnectorData::new(parts);
|
||||
Self {
|
||||
client: PeerTaskManager::new_with_external_signal(
|
||||
UdpHolePunchPeerTaskLauncher(data),
|
||||
external_signal,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_as_client(&self) {
|
||||
self.client.start();
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
self.client.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::UdpSymPunchLock;
|
||||
|
||||
#[test]
|
||||
fn symmetric_punch_locks_are_scoped_per_instance() {
|
||||
let first = UdpSymPunchLock::default();
|
||||
let first_clone = first.clone();
|
||||
let second = UdpSymPunchLock::default();
|
||||
|
||||
let _first_guard = first.try_lock().unwrap();
|
||||
assert!(first_clone.try_lock().is_err());
|
||||
assert!(second.try_lock().is_ok());
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
mod binding;
|
||||
mod client;
|
||||
mod common;
|
||||
mod connector;
|
||||
mod punch_listener;
|
||||
mod rpc;
|
||||
mod runtime;
|
||||
mod server;
|
||||
mod socket_array;
|
||||
mod task;
|
||||
|
||||
pub(crate) use binding::CoreUdpHolePunchService;
|
||||
pub(crate) use client::{
|
||||
UdpBothEasySymPunchClient, UdpHolePunchClientError, UdpSymToConePunchClient, punch_cone_to_cone,
|
||||
};
|
||||
pub(crate) use common::{BLACKLIST_TIMEOUT_SEC, UdpNatType, UdpPunchClientMethod};
|
||||
pub(crate) use connector::{UdpHolePunchConnector, UdpSymPunchLock};
|
||||
pub(crate) use punch_listener::{
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS, ReusableUdpPunchListener, can_reuse_port_mapping_listener,
|
||||
can_reuse_public_listener, select_reusable_port_mapping_listener_idx,
|
||||
select_reusable_public_listener_idx, should_create_public_listener,
|
||||
should_retry_public_listener_selection,
|
||||
};
|
||||
pub(crate) use rpc::UdpHolePunchRpcSource;
|
||||
pub(crate) use runtime::{
|
||||
ProtocolUdpHolePunchTransportSink, SelectPunchListener, SelectPunchListenerResponse,
|
||||
SendPunchPacketBothEasySym, SendPunchPacketBothEasySymResponse, SendPunchPacketCone,
|
||||
SendPunchPacketEasySym, SendPunchPacketHardSym, SendPunchPacketHardSymResponse,
|
||||
UdpHolePunchInbound, UdpHolePunchPeerSource, UdpHolePunchRuntime, UdpHolePunchSignalError,
|
||||
UdpHolePunchSignaling, UdpHolePunchTransportSink, UdpPunchAcceptor, UdpPunchListener,
|
||||
UdpPunchSocket, UdpResolvedPublicAddr, should_blacklist_signal_error,
|
||||
};
|
||||
pub(crate) use server::UdpHolePunchServer;
|
||||
pub(crate) use socket_array::UdpSocketArray;
|
||||
pub(crate) use task::{UdpPunchCandidate, UdpPunchTaskInfo, collect_udp_punch_tasks};
|
||||
|
||||
const fn udp_packet_len(body_len: u16) -> usize {
|
||||
crate::packet::UDP_TUNNEL_HEADER_SIZE + body_len as usize
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use quanta::Instant;
|
||||
|
||||
pub const MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS: usize = 4;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ReusableUdpPunchListener {
|
||||
pub running: bool,
|
||||
pub mapped_addr: SocketAddr,
|
||||
pub has_port_mapping_lease: bool,
|
||||
pub last_active_time: Instant,
|
||||
}
|
||||
|
||||
pub fn can_reuse_public_listener(listener: &ReusableUdpPunchListener) -> bool {
|
||||
listener.running && !listener.mapped_addr.ip().is_unspecified()
|
||||
}
|
||||
|
||||
pub fn can_reuse_port_mapping_listener(listener: &ReusableUdpPunchListener) -> bool {
|
||||
can_reuse_public_listener(listener) && listener.has_port_mapping_lease
|
||||
}
|
||||
|
||||
pub fn select_reusable_public_listener_idx(
|
||||
listeners: &[ReusableUdpPunchListener],
|
||||
) -> Option<usize> {
|
||||
listeners
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, listener)| can_reuse_public_listener(listener))
|
||||
.max_by_key(|(_, listener)| listener.last_active_time)
|
||||
.map(|(idx, _)| idx)
|
||||
}
|
||||
|
||||
pub fn select_reusable_port_mapping_listener_idx(
|
||||
listeners: &[ReusableUdpPunchListener],
|
||||
) -> Option<usize> {
|
||||
listeners
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, listener)| can_reuse_port_mapping_listener(listener))
|
||||
.max_by_key(|(_, listener)| listener.last_active_time)
|
||||
.map(|(idx, _)| idx)
|
||||
}
|
||||
|
||||
pub fn should_create_public_listener(
|
||||
current_listener_count: usize,
|
||||
has_reusable_listener: bool,
|
||||
has_port_mapping_listener: bool,
|
||||
force_new_listener: bool,
|
||||
prefer_port_mapping: bool,
|
||||
) -> bool {
|
||||
if current_listener_count >= MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS {
|
||||
return false;
|
||||
}
|
||||
|
||||
if current_listener_count == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
if force_new_listener {
|
||||
return true;
|
||||
}
|
||||
|
||||
if prefer_port_mapping && !has_port_mapping_listener {
|
||||
return true;
|
||||
}
|
||||
|
||||
!has_reusable_listener
|
||||
}
|
||||
|
||||
pub fn should_retry_public_listener_selection(
|
||||
force_new_listener: bool,
|
||||
current_listener_count: usize,
|
||||
prefer_port_mapping: bool,
|
||||
has_port_mapping_listener: bool,
|
||||
) -> bool {
|
||||
if prefer_port_mapping && has_port_mapping_listener {
|
||||
return false;
|
||||
}
|
||||
|
||||
!force_new_listener && current_listener_count < MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn listener(
|
||||
port: u16,
|
||||
running: bool,
|
||||
has_port_mapping_lease: bool,
|
||||
active_age: Duration,
|
||||
) -> ReusableUdpPunchListener {
|
||||
ReusableUdpPunchListener {
|
||||
running,
|
||||
mapped_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)),
|
||||
has_port_mapping_lease,
|
||||
last_active_time: Instant::now() - active_age,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_prefers_reuse_before_cap() {
|
||||
assert!(!should_create_public_listener(1, true, true, false, false));
|
||||
assert!(!should_create_public_listener(
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_creates_when_empty_or_no_reusable_listener() {
|
||||
assert!(should_create_public_listener(0, false, false, false, false));
|
||||
assert!(should_create_public_listener(1, false, false, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_force_new_respects_cap() {
|
||||
assert!(should_create_public_listener(1, true, true, true, false));
|
||||
assert!(!should_create_public_listener(
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_prefers_port_mapping_until_available() {
|
||||
assert!(should_create_public_listener(1, true, false, false, true));
|
||||
assert!(!should_create_public_listener(1, true, true, false, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listener_selection_retry_respects_cap() {
|
||||
assert!(should_retry_public_listener_selection(
|
||||
false, 1, false, false
|
||||
));
|
||||
assert!(!should_retry_public_listener_selection(
|
||||
false,
|
||||
MAX_PUBLIC_UDP_HOLE_PUNCH_LISTENERS,
|
||||
false,
|
||||
false
|
||||
));
|
||||
assert!(!should_retry_public_listener_selection(
|
||||
true, 1, false, false
|
||||
));
|
||||
assert!(!should_retry_public_listener_selection(
|
||||
false, 1, true, true
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_most_recent_reusable_public_listener() {
|
||||
let listeners = vec![
|
||||
listener(1000, true, false, Duration::from_secs(10)),
|
||||
listener(1001, false, false, Duration::from_secs(1)),
|
||||
listener(1002, true, false, Duration::from_secs(2)),
|
||||
];
|
||||
|
||||
assert_eq!(select_reusable_public_listener_idx(&listeners), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_most_recent_reusable_port_mapping_listener() {
|
||||
let listeners = vec![
|
||||
listener(1000, true, false, Duration::from_secs(1)),
|
||||
listener(1001, true, true, Duration::from_secs(10)),
|
||||
listener(1002, true, true, Duration::from_secs(2)),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
select_reusable_port_mapping_listener_idx(&listeners),
|
||||
Some(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unspecified_addr_is_not_reusable() {
|
||||
let mut listener = listener(1000, true, true, Duration::ZERO);
|
||||
listener.mapped_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 1000));
|
||||
|
||||
assert!(!can_reuse_public_listener(&listener));
|
||||
assert!(!can_reuse_port_mapping_listener(&listener));
|
||||
}
|
||||
}
|
||||
@@ -1,828 +0,0 @@
|
||||
use std::{fmt, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
config::PeerId,
|
||||
connectivity::hole_punch::udp::{
|
||||
SelectPunchListener, SelectPunchListenerResponse as CoreSelectPunchListenerResponse,
|
||||
SendPunchPacketBothEasySym,
|
||||
SendPunchPacketBothEasySymResponse as CoreSendPunchPacketBothEasySymResponse,
|
||||
SendPunchPacketCone, SendPunchPacketEasySym, SendPunchPacketHardSym,
|
||||
SendPunchPacketHardSymResponse as CoreSendPunchPacketHardSymResponse, UdpHolePunchInbound,
|
||||
UdpHolePunchRuntime, UdpHolePunchServer as CoreUdpHolePunchServer, UdpHolePunchSignalError,
|
||||
UdpHolePunchSignaling, UdpHolePunchTransportSink, UdpSymPunchLock,
|
||||
},
|
||||
connectivity::stun::StunInfoProvider,
|
||||
proto::{
|
||||
common::Void,
|
||||
peer_rpc::{
|
||||
SelectPunchListenerRequest, SelectPunchListenerResponse,
|
||||
SendPunchPacketBothEasySymRequest, SendPunchPacketBothEasySymResponse,
|
||||
SendPunchPacketConeRequest, SendPunchPacketEasySymRequest,
|
||||
SendPunchPacketHardSymRequest, SendPunchPacketHardSymResponse, UdpHolePunchRpc,
|
||||
},
|
||||
rpc_types::{self, controller::BaseController},
|
||||
},
|
||||
};
|
||||
|
||||
const CONE_RPC_TIMEOUT_MS: i32 = 4000;
|
||||
const SYMMETRIC_RPC_TIMEOUT_MS: i32 = 4000;
|
||||
const BOTH_EASY_SYMMETRIC_RPC_TIMEOUT_MS: i32 = 2000;
|
||||
|
||||
fn cone_controller() -> BaseController {
|
||||
BaseController {
|
||||
timeout_ms: CONE_RPC_TIMEOUT_MS,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn symmetric_controller() -> BaseController {
|
||||
BaseController {
|
||||
timeout_ms: SYMMETRIC_RPC_TIMEOUT_MS,
|
||||
trace_id: 0,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn both_easy_symmetric_controller() -> BaseController {
|
||||
BaseController {
|
||||
timeout_ms: BOTH_EASY_SYMMETRIC_RPC_TIMEOUT_MS,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn select_listener_request_to_rpc(request: SelectPunchListener) -> SelectPunchListenerRequest {
|
||||
SelectPunchListenerRequest {
|
||||
force_new: request.force_new,
|
||||
prefer_port_mapping: request.prefer_port_mapping,
|
||||
}
|
||||
}
|
||||
|
||||
fn select_listener_request_from_rpc(input: SelectPunchListenerRequest) -> SelectPunchListener {
|
||||
SelectPunchListener {
|
||||
force_new: input.force_new,
|
||||
prefer_port_mapping: input.prefer_port_mapping,
|
||||
}
|
||||
}
|
||||
|
||||
fn select_listener_response_from_rpc(
|
||||
response: SelectPunchListenerResponse,
|
||||
) -> Result<CoreSelectPunchListenerResponse, UdpHolePunchSignalError> {
|
||||
Ok(CoreSelectPunchListenerResponse {
|
||||
listener_mapped_addr: SocketAddr::from(
|
||||
response
|
||||
.listener_mapped_addr
|
||||
.ok_or_else(|| missing_field("listener_mapped_addr"))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn select_listener_response_to_rpc(
|
||||
response: CoreSelectPunchListenerResponse,
|
||||
) -> SelectPunchListenerResponse {
|
||||
SelectPunchListenerResponse {
|
||||
listener_mapped_addr: Some(response.listener_mapped_addr.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn cone_request_to_rpc(request: SendPunchPacketCone) -> SendPunchPacketConeRequest {
|
||||
SendPunchPacketConeRequest {
|
||||
listener_mapped_addr: Some(request.listener_mapped_addr.into()),
|
||||
dest_addr: Some(request.dest_addr.into()),
|
||||
transaction_id: request.transaction_id,
|
||||
packet_count_per_batch: request.packet_count_per_batch,
|
||||
packet_batch_count: request.packet_batch_count,
|
||||
packet_interval_ms: request.packet_interval_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn hard_symmetric_request_to_rpc(request: SendPunchPacketHardSym) -> SendPunchPacketHardSymRequest {
|
||||
SendPunchPacketHardSymRequest {
|
||||
listener_mapped_addr: Some(request.listener_mapped_addr.into()),
|
||||
public_ips: request.public_ips.into_iter().map(Into::into).collect(),
|
||||
transaction_id: request.transaction_id,
|
||||
port_index: request.port_index,
|
||||
round: request.round,
|
||||
}
|
||||
}
|
||||
|
||||
fn easy_symmetric_request_to_rpc(request: SendPunchPacketEasySym) -> SendPunchPacketEasySymRequest {
|
||||
SendPunchPacketEasySymRequest {
|
||||
listener_mapped_addr: Some(request.listener_mapped_addr.into()),
|
||||
public_ips: request.public_ips.into_iter().map(Into::into).collect(),
|
||||
transaction_id: request.transaction_id,
|
||||
base_port_num: request.base_port_num,
|
||||
max_port_num: request.max_port_num,
|
||||
is_incremental: request.is_incremental,
|
||||
}
|
||||
}
|
||||
|
||||
fn both_easy_symmetric_request_to_rpc(
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> SendPunchPacketBothEasySymRequest {
|
||||
SendPunchPacketBothEasySymRequest {
|
||||
transaction_id: request.transaction_id,
|
||||
public_ip: Some(request.public_ip.into()),
|
||||
dst_port_num: request.dst_port_num,
|
||||
udp_socket_count: request.udp_socket_count,
|
||||
wait_time_ms: request.wait_time_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn hard_symmetric_response_from_rpc(
|
||||
response: SendPunchPacketHardSymResponse,
|
||||
) -> CoreSendPunchPacketHardSymResponse {
|
||||
CoreSendPunchPacketHardSymResponse {
|
||||
next_port_index: response.next_port_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn hard_symmetric_response_to_rpc(
|
||||
response: CoreSendPunchPacketHardSymResponse,
|
||||
) -> SendPunchPacketHardSymResponse {
|
||||
SendPunchPacketHardSymResponse {
|
||||
next_port_index: response.next_port_index,
|
||||
}
|
||||
}
|
||||
|
||||
fn both_easy_symmetric_response_from_rpc(
|
||||
response: SendPunchPacketBothEasySymResponse,
|
||||
) -> CoreSendPunchPacketBothEasySymResponse {
|
||||
CoreSendPunchPacketBothEasySymResponse {
|
||||
is_busy: response.is_busy,
|
||||
base_mapped_addr: response.base_mapped_addr.map(SocketAddr::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn both_easy_symmetric_response_to_rpc(
|
||||
response: CoreSendPunchPacketBothEasySymResponse,
|
||||
) -> SendPunchPacketBothEasySymResponse {
|
||||
SendPunchPacketBothEasySymResponse {
|
||||
is_busy: response.is_busy,
|
||||
base_mapped_addr: response.base_mapped_addr.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Narrow source of peer-scoped UDP hole-punch RPC stubs.
|
||||
///
|
||||
/// Implemented only by the sealed peer adapter in `super::peer_adapters`.
|
||||
pub trait UdpHolePunchRpcSource: Send + Sync + 'static {
|
||||
fn local_peer_id(&self) -> PeerId;
|
||||
|
||||
fn rpc_stub(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Box<dyn UdpHolePunchRpc<Controller = BaseController> + Send + Sync + 'static>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct PeerRpcUdpHolePunchSignaling<P>
|
||||
where
|
||||
P: UdpHolePunchRpcSource,
|
||||
{
|
||||
rpc_source: Arc<P>,
|
||||
}
|
||||
|
||||
impl<P> fmt::Debug for PeerRpcUdpHolePunchSignaling<P>
|
||||
where
|
||||
P: UdpHolePunchRpcSource,
|
||||
{
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("PeerRpcUdpHolePunchSignaling")
|
||||
.field("my_peer_id", &self.rpc_source.local_peer_id())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> PeerRpcUdpHolePunchSignaling<P>
|
||||
where
|
||||
P: UdpHolePunchRpcSource,
|
||||
{
|
||||
pub(super) fn new(rpc_source: Arc<P>) -> Self {
|
||||
Self { rpc_source }
|
||||
}
|
||||
|
||||
fn rpc_stub(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
) -> Box<dyn UdpHolePunchRpc<Controller = BaseController> + Send + Sync + 'static> {
|
||||
self.rpc_source.rpc_stub(dst_peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_rpc_error(error: rpc_types::error::Error) -> UdpHolePunchSignalError {
|
||||
match error {
|
||||
rpc_types::error::Error::InvalidServiceKey(_, _) => {
|
||||
UdpHolePunchSignalError::InvalidServiceKey
|
||||
}
|
||||
rpc_types::error::Error::Timeout(_) => UdpHolePunchSignalError::Timeout,
|
||||
rpc_types::error::Error::ExecutionError(error) => {
|
||||
UdpHolePunchSignalError::RemoteRejected(error.to_string())
|
||||
}
|
||||
other => UdpHolePunchSignalError::Transport(other.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_field(field: &str) -> UdpHolePunchSignalError {
|
||||
UdpHolePunchSignalError::RemoteRejected(format!("missing {field}"))
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<P> UdpHolePunchSignaling for PeerRpcUdpHolePunchSignaling<P>
|
||||
where
|
||||
P: UdpHolePunchRpcSource,
|
||||
{
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
request: SelectPunchListener,
|
||||
) -> Result<CoreSelectPunchListenerResponse, UdpHolePunchSignalError> {
|
||||
let response = self
|
||||
.rpc_stub(dst_peer_id)
|
||||
.select_punch_listener(
|
||||
BaseController::default(),
|
||||
select_listener_request_to_rpc(request),
|
||||
)
|
||||
.await
|
||||
.map_err(map_rpc_error)?;
|
||||
|
||||
select_listener_response_from_rpc(response)
|
||||
}
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
self.rpc_stub(dst_peer_id)
|
||||
.send_punch_packet_cone(cone_controller(), cone_request_to_rpc(request))
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_rpc_error)
|
||||
}
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
request: SendPunchPacketHardSym,
|
||||
) -> Result<CoreSendPunchPacketHardSymResponse, UdpHolePunchSignalError> {
|
||||
let response = self
|
||||
.rpc_stub(dst_peer_id)
|
||||
.send_punch_packet_hard_sym(
|
||||
symmetric_controller(),
|
||||
hard_symmetric_request_to_rpc(request),
|
||||
)
|
||||
.await
|
||||
.map_err(map_rpc_error)?;
|
||||
|
||||
Ok(hard_symmetric_response_from_rpc(response))
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
request: SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
self.rpc_stub(dst_peer_id)
|
||||
.send_punch_packet_easy_sym(
|
||||
symmetric_controller(),
|
||||
easy_symmetric_request_to_rpc(request),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_rpc_error)
|
||||
}
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
dst_peer_id: PeerId,
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> Result<CoreSendPunchPacketBothEasySymResponse, UdpHolePunchSignalError> {
|
||||
let response = self
|
||||
.rpc_stub(dst_peer_id)
|
||||
.send_punch_packet_both_easy_sym(
|
||||
both_easy_symmetric_controller(),
|
||||
both_easy_symmetric_request_to_rpc(request),
|
||||
)
|
||||
.await
|
||||
.map_err(map_rpc_error)?;
|
||||
|
||||
Ok(both_easy_symmetric_response_from_rpc(response))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct UdpHolePunchRpcEndpoint<R, T>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
{
|
||||
inner: CoreUdpHolePunchServer<R, T>,
|
||||
}
|
||||
|
||||
impl<R, T> UdpHolePunchRpcEndpoint<R, T>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
{
|
||||
pub(super) fn new(
|
||||
stun: Arc<dyn StunInfoProvider>,
|
||||
transport_sink: Arc<T>,
|
||||
sym_punch_lock: UdpSymPunchLock,
|
||||
runtime: Arc<R>,
|
||||
) -> Arc<Self> {
|
||||
let inner = CoreUdpHolePunchServer::new(runtime, stun, transport_sink, sym_punch_lock);
|
||||
Arc::new(Self { inner })
|
||||
}
|
||||
|
||||
pub(super) async fn start(&self) {
|
||||
self.inner.start().await;
|
||||
}
|
||||
|
||||
pub(super) fn begin_stop(&self) {
|
||||
self.inner.begin_stop();
|
||||
}
|
||||
|
||||
pub(super) async fn stop(&self) {
|
||||
self.inner.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
fn signal_error_to_rpc_error(error: UdpHolePunchSignalError) -> rpc_types::error::Error {
|
||||
match error {
|
||||
UdpHolePunchSignalError::InvalidServiceKey => rpc_types::error::Error::InvalidServiceKey(
|
||||
"UdpHolePunchRpc".to_owned(),
|
||||
"UdpHolePunchRpc".to_owned(),
|
||||
),
|
||||
UdpHolePunchSignalError::Timeout => anyhow::anyhow!("timeout").into(),
|
||||
UdpHolePunchSignalError::RemoteRejected(message)
|
||||
| UdpHolePunchSignalError::Transport(message) => anyhow::anyhow!(message).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cone_request_from_rpc(
|
||||
input: SendPunchPacketConeRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketCone> {
|
||||
let listener_addr = input.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone request missing listener_mapped_addr"
|
||||
))?;
|
||||
let dest_addr = input.dest_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_for_cone request missing dest_addr"
|
||||
))?;
|
||||
Ok(SendPunchPacketCone {
|
||||
listener_mapped_addr: listener_addr.into(),
|
||||
dest_addr: dest_addr.into(),
|
||||
transaction_id: input.transaction_id,
|
||||
packet_count_per_batch: input.packet_count_per_batch,
|
||||
packet_batch_count: input.packet_batch_count,
|
||||
packet_interval_ms: input.packet_interval_ms,
|
||||
})
|
||||
}
|
||||
|
||||
fn hard_symmetric_request_from_rpc(
|
||||
input: SendPunchPacketHardSymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketHardSym> {
|
||||
let listener_addr = input.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"try_punch_symmetric request missing listener_addr"
|
||||
))?;
|
||||
Ok(SendPunchPacketHardSym {
|
||||
listener_mapped_addr: listener_addr.into(),
|
||||
public_ips: input.public_ips.into_iter().map(Into::into).collect(),
|
||||
transaction_id: input.transaction_id,
|
||||
port_index: input.port_index,
|
||||
round: input.round,
|
||||
})
|
||||
}
|
||||
|
||||
fn easy_symmetric_request_from_rpc(
|
||||
input: SendPunchPacketEasySymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketEasySym> {
|
||||
let listener_addr = input.listener_mapped_addr.ok_or(anyhow::anyhow!(
|
||||
"send_punch_packet_easy_sym request missing listener_addr"
|
||||
))?;
|
||||
Ok(SendPunchPacketEasySym {
|
||||
listener_mapped_addr: listener_addr.into(),
|
||||
public_ips: input.public_ips.into_iter().map(Into::into).collect(),
|
||||
transaction_id: input.transaction_id,
|
||||
base_port_num: input.base_port_num,
|
||||
max_port_num: input.max_port_num,
|
||||
is_incremental: input.is_incremental,
|
||||
})
|
||||
}
|
||||
|
||||
fn both_easy_symmetric_request_from_rpc(
|
||||
input: SendPunchPacketBothEasySymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketBothEasySym> {
|
||||
let public_ip = input
|
||||
.public_ip
|
||||
.ok_or(anyhow::anyhow!("public_ip is required"))?;
|
||||
Ok(SendPunchPacketBothEasySym {
|
||||
transaction_id: input.transaction_id,
|
||||
public_ip: public_ip.into(),
|
||||
dst_port_num: input.dst_port_num,
|
||||
udp_socket_count: input.udp_socket_count,
|
||||
wait_time_ms: input.wait_time_ms,
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R, T> UdpHolePunchInbound for UdpHolePunchRpcEndpoint<R, T>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
{
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
request: SelectPunchListener,
|
||||
) -> Result<CoreSelectPunchListenerResponse, UdpHolePunchSignalError> {
|
||||
self.inner.select_punch_listener(request).await
|
||||
}
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
self.inner.send_punch_packet_cone(request).await
|
||||
}
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
request: SendPunchPacketHardSym,
|
||||
) -> Result<CoreSendPunchPacketHardSymResponse, UdpHolePunchSignalError> {
|
||||
self.inner.send_punch_packet_hard_sym(request).await
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError> {
|
||||
self.inner.send_punch_packet_easy_sym(request).await
|
||||
}
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> Result<CoreSendPunchPacketBothEasySymResponse, UdpHolePunchSignalError> {
|
||||
self.inner.send_punch_packet_both_easy_sym(request).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R, T> UdpHolePunchRpc for UdpHolePunchRpcEndpoint<R, T>
|
||||
where
|
||||
R: UdpHolePunchRuntime,
|
||||
T: UdpHolePunchTransportSink + 'static,
|
||||
{
|
||||
type Controller = BaseController;
|
||||
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
_controller: Self::Controller,
|
||||
input: SelectPunchListenerRequest,
|
||||
) -> rpc_types::error::Result<SelectPunchListenerResponse> {
|
||||
let response = UdpHolePunchInbound::select_punch_listener(
|
||||
self,
|
||||
select_listener_request_from_rpc(input),
|
||||
)
|
||||
.await
|
||||
.map_err(signal_error_to_rpc_error)?;
|
||||
|
||||
Ok(select_listener_response_to_rpc(response))
|
||||
}
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
_controller: Self::Controller,
|
||||
input: SendPunchPacketConeRequest,
|
||||
) -> rpc_types::error::Result<Void> {
|
||||
UdpHolePunchInbound::send_punch_packet_cone(self, cone_request_from_rpc(input)?)
|
||||
.await
|
||||
.map_err(signal_error_to_rpc_error)?;
|
||||
|
||||
Ok(Void::default())
|
||||
}
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
_controller: Self::Controller,
|
||||
input: SendPunchPacketHardSymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketHardSymResponse> {
|
||||
let response = UdpHolePunchInbound::send_punch_packet_hard_sym(
|
||||
self,
|
||||
hard_symmetric_request_from_rpc(input)?,
|
||||
)
|
||||
.await
|
||||
.map_err(signal_error_to_rpc_error)?;
|
||||
|
||||
Ok(hard_symmetric_response_to_rpc(response))
|
||||
}
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
_controller: Self::Controller,
|
||||
input: SendPunchPacketEasySymRequest,
|
||||
) -> rpc_types::error::Result<Void> {
|
||||
UdpHolePunchInbound::send_punch_packet_easy_sym(
|
||||
self,
|
||||
easy_symmetric_request_from_rpc(input)?,
|
||||
)
|
||||
.await
|
||||
.map_err(signal_error_to_rpc_error)?;
|
||||
|
||||
Ok(Void::default())
|
||||
}
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
_controller: Self::Controller,
|
||||
input: SendPunchPacketBothEasySymRequest,
|
||||
) -> rpc_types::error::Result<SendPunchPacketBothEasySymResponse> {
|
||||
let response = UdpHolePunchInbound::send_punch_packet_both_easy_sym(
|
||||
self,
|
||||
both_easy_symmetric_request_from_rpc(input)?,
|
||||
)
|
||||
.await
|
||||
.map_err(signal_error_to_rpc_error)?;
|
||||
|
||||
Ok(both_easy_symmetric_response_to_rpc(response))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{future, net::Ipv4Addr, time::Duration};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn outbound_rpc_controllers_preserve_timeouts() {
|
||||
assert_eq!(cone_controller().timeout_ms, 4000);
|
||||
assert_eq!(symmetric_controller().timeout_ms, 4000);
|
||||
assert_eq!(symmetric_controller().trace_id, 0);
|
||||
assert_eq!(both_easy_symmetric_controller().timeout_ms, 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_listener_dto_preserves_fields_and_requires_response_addr() {
|
||||
let domain_request = SelectPunchListener {
|
||||
force_new: true,
|
||||
prefer_port_mapping: false,
|
||||
};
|
||||
let rpc_request = select_listener_request_to_rpc(domain_request.clone());
|
||||
assert!(rpc_request.force_new);
|
||||
assert!(!rpc_request.prefer_port_mapping);
|
||||
assert_eq!(
|
||||
select_listener_request_from_rpc(SelectPunchListenerRequest {
|
||||
force_new: true,
|
||||
prefer_port_mapping: false,
|
||||
}),
|
||||
domain_request
|
||||
);
|
||||
|
||||
let mapped_addr: SocketAddr = "198.51.100.1:31001".parse().unwrap();
|
||||
let core_response = CoreSelectPunchListenerResponse {
|
||||
listener_mapped_addr: mapped_addr,
|
||||
};
|
||||
let rpc_response = select_listener_response_to_rpc(core_response.clone());
|
||||
assert_eq!(
|
||||
SocketAddr::from(rpc_response.listener_mapped_addr.unwrap()),
|
||||
mapped_addr
|
||||
);
|
||||
assert_eq!(
|
||||
select_listener_response_from_rpc(rpc_response).unwrap(),
|
||||
core_response
|
||||
);
|
||||
|
||||
let error =
|
||||
select_listener_response_from_rpc(SelectPunchListenerResponse::default()).unwrap_err();
|
||||
assert_eq!(
|
||||
error,
|
||||
UdpHolePunchSignalError::RemoteRejected("missing listener_mapped_addr".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cone_dto_round_trip_preserves_all_fields() {
|
||||
let request = SendPunchPacketCone {
|
||||
listener_mapped_addr: "198.51.100.2:31002".parse().unwrap(),
|
||||
dest_addr: "203.0.113.2:32002".parse().unwrap(),
|
||||
transaction_id: 12,
|
||||
packet_count_per_batch: 3,
|
||||
packet_batch_count: 4,
|
||||
packet_interval_ms: 500,
|
||||
};
|
||||
|
||||
let rpc = cone_request_to_rpc(request.clone());
|
||||
assert_eq!(
|
||||
SocketAddr::from(rpc.listener_mapped_addr.unwrap()),
|
||||
request.listener_mapped_addr
|
||||
);
|
||||
assert_eq!(SocketAddr::from(rpc.dest_addr.unwrap()), request.dest_addr);
|
||||
assert_eq!(rpc.transaction_id, 12);
|
||||
assert_eq!(rpc.packet_count_per_batch, 3);
|
||||
assert_eq!(rpc.packet_batch_count, 4);
|
||||
assert_eq!(rpc.packet_interval_ms, 500);
|
||||
assert_eq!(cone_request_from_rpc(rpc).unwrap(), request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_symmetric_dto_round_trip_preserves_all_fields() {
|
||||
let request = SendPunchPacketHardSym {
|
||||
listener_mapped_addr: "198.51.100.3:31003".parse().unwrap(),
|
||||
public_ips: vec![Ipv4Addr::new(203, 0, 113, 3), Ipv4Addr::new(203, 0, 113, 4)],
|
||||
transaction_id: 13,
|
||||
port_index: 17,
|
||||
round: 19,
|
||||
};
|
||||
|
||||
let rpc = hard_symmetric_request_to_rpc(request.clone());
|
||||
assert_eq!(
|
||||
SocketAddr::from(rpc.listener_mapped_addr.unwrap()),
|
||||
request.listener_mapped_addr
|
||||
);
|
||||
assert_eq!(
|
||||
rpc.public_ips
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Ipv4Addr::from)
|
||||
.collect::<Vec<_>>(),
|
||||
request.public_ips
|
||||
);
|
||||
assert_eq!(rpc.transaction_id, 13);
|
||||
assert_eq!(rpc.port_index, 17);
|
||||
assert_eq!(rpc.round, 19);
|
||||
assert_eq!(hard_symmetric_request_from_rpc(rpc).unwrap(), request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easy_symmetric_dto_round_trip_preserves_all_fields() {
|
||||
let request = SendPunchPacketEasySym {
|
||||
listener_mapped_addr: "198.51.100.5:31005".parse().unwrap(),
|
||||
public_ips: vec![Ipv4Addr::new(203, 0, 113, 5)],
|
||||
transaction_id: 15,
|
||||
base_port_num: 33000,
|
||||
max_port_num: 51,
|
||||
is_incremental: true,
|
||||
};
|
||||
|
||||
let rpc = easy_symmetric_request_to_rpc(request.clone());
|
||||
assert_eq!(
|
||||
SocketAddr::from(rpc.listener_mapped_addr.unwrap()),
|
||||
request.listener_mapped_addr
|
||||
);
|
||||
assert_eq!(
|
||||
rpc.public_ips
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Ipv4Addr::from)
|
||||
.collect::<Vec<_>>(),
|
||||
request.public_ips
|
||||
);
|
||||
assert_eq!(rpc.transaction_id, 15);
|
||||
assert_eq!(rpc.base_port_num, 33000);
|
||||
assert_eq!(rpc.max_port_num, 51);
|
||||
assert!(rpc.is_incremental);
|
||||
assert_eq!(easy_symmetric_request_from_rpc(rpc).unwrap(), request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_easy_symmetric_dto_round_trip_preserves_all_fields() {
|
||||
let request = SendPunchPacketBothEasySym {
|
||||
udp_socket_count: 25,
|
||||
public_ip: Ipv4Addr::new(203, 0, 113, 6),
|
||||
transaction_id: 16,
|
||||
dst_port_num: 34000,
|
||||
wait_time_ms: 2500,
|
||||
};
|
||||
|
||||
let rpc = both_easy_symmetric_request_to_rpc(request.clone());
|
||||
assert_eq!(rpc.udp_socket_count, 25);
|
||||
assert_eq!(Ipv4Addr::from(rpc.public_ip.unwrap()), request.public_ip);
|
||||
assert_eq!(rpc.transaction_id, 16);
|
||||
assert_eq!(rpc.dst_port_num, 34000);
|
||||
assert_eq!(rpc.wait_time_ms, 2500);
|
||||
assert_eq!(both_easy_symmetric_request_from_rpc(rpc).unwrap(), request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_response_dtos_preserve_all_fields() {
|
||||
let hard_response = CoreSendPunchPacketHardSymResponse {
|
||||
next_port_index: 41,
|
||||
};
|
||||
let hard_rpc = hard_symmetric_response_to_rpc(hard_response.clone());
|
||||
assert_eq!(hard_rpc.next_port_index, 41);
|
||||
assert_eq!(hard_symmetric_response_from_rpc(hard_rpc), hard_response);
|
||||
|
||||
let both_response = CoreSendPunchPacketBothEasySymResponse {
|
||||
is_busy: true,
|
||||
base_mapped_addr: Some("198.51.100.8:31008".parse().unwrap()),
|
||||
};
|
||||
let both_rpc = both_easy_symmetric_response_to_rpc(both_response.clone());
|
||||
assert!(both_rpc.is_busy);
|
||||
assert_eq!(
|
||||
SocketAddr::from(both_rpc.base_mapped_addr.unwrap()),
|
||||
both_response.base_mapped_addr.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
both_easy_symmetric_response_from_rpc(both_rpc),
|
||||
both_response
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inbound_rpc_dtos_reject_missing_required_fields() {
|
||||
let cone_listener_error = cone_request_from_rpc(SendPunchPacketConeRequest::default())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
cone_listener_error,
|
||||
"Rust error: send_punch_packet_for_cone request missing listener_mapped_addr"
|
||||
);
|
||||
|
||||
let cone_dest_error = cone_request_from_rpc(SendPunchPacketConeRequest {
|
||||
listener_mapped_addr: Some("198.51.100.7:31007".parse::<SocketAddr>().unwrap().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
cone_dest_error,
|
||||
"Rust error: send_punch_packet_for_cone request missing dest_addr"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hard_symmetric_request_from_rpc(SendPunchPacketHardSymRequest::default())
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"Rust error: try_punch_symmetric request missing listener_addr"
|
||||
);
|
||||
assert_eq!(
|
||||
easy_symmetric_request_from_rpc(SendPunchPacketEasySymRequest::default())
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"Rust error: send_punch_packet_easy_sym request missing listener_addr"
|
||||
);
|
||||
assert_eq!(
|
||||
both_easy_symmetric_request_from_rpc(SendPunchPacketBothEasySymRequest::default())
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"Rust error: public_ip is required"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rpc_errors_keep_domain_classification() {
|
||||
assert_eq!(
|
||||
map_rpc_error(rpc_types::error::Error::InvalidServiceKey(
|
||||
"service".to_owned(),
|
||||
"proto".to_owned()
|
||||
)),
|
||||
UdpHolePunchSignalError::InvalidServiceKey
|
||||
);
|
||||
assert_eq!(
|
||||
map_rpc_error(rpc_types::error::Error::ExecutionError(anyhow::anyhow!(
|
||||
"rejected"
|
||||
))),
|
||||
UdpHolePunchSignalError::RemoteRejected("rejected".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
map_rpc_error(rpc_types::error::Error::TunnelError("closed".to_owned())),
|
||||
UdpHolePunchSignalError::Transport("Tunnel error: closed".to_owned())
|
||||
);
|
||||
|
||||
let elapsed = tokio::time::timeout(Duration::ZERO, future::pending::<()>())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
map_rpc_error(rpc_types::error::Error::Timeout(elapsed)),
|
||||
UdpHolePunchSignalError::Timeout
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_errors_keep_rpc_classification() {
|
||||
assert!(matches!(
|
||||
signal_error_to_rpc_error(UdpHolePunchSignalError::InvalidServiceKey),
|
||||
rpc_types::error::Error::InvalidServiceKey(_, _)
|
||||
));
|
||||
for (domain_error, expected_message) in [
|
||||
(UdpHolePunchSignalError::Timeout, "timeout"),
|
||||
(
|
||||
UdpHolePunchSignalError::RemoteRejected("rejected".to_owned()),
|
||||
"rejected",
|
||||
),
|
||||
(
|
||||
UdpHolePunchSignalError::Transport("closed".to_owned()),
|
||||
"closed",
|
||||
),
|
||||
] {
|
||||
let rpc_types::error::Error::ExecutionError(error) =
|
||||
signal_error_to_rpc_error(domain_error)
|
||||
else {
|
||||
panic!("domain error should map to execution error");
|
||||
};
|
||||
assert_eq!(error.to_string(), expected_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::super::{HolePunchTunnelSink, port_mapping::UdpPortMappingLease};
|
||||
|
||||
use crate::{
|
||||
config::P2pPolicyFlags,
|
||||
connectivity::{
|
||||
protocol::ClientProtocolUpgrader,
|
||||
transport::{ConnectedTransport, ConnectedUdpSession},
|
||||
},
|
||||
foundation::task::ExternalTaskSignal,
|
||||
socket::{
|
||||
ListenerConnectionCounter, SocketContext,
|
||||
udp::{UdpBindOptions, UdpSession, VirtualUdpSocket, VirtualUdpSocketFactory},
|
||||
},
|
||||
tunnel::Tunnel,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpPunchAcceptor: Send {
|
||||
async fn accept(&mut self) -> anyhow::Result<UdpPunchSocket>;
|
||||
}
|
||||
|
||||
pub struct UdpPunchSocket {
|
||||
session: UdpSession,
|
||||
requested_remote_addr: SocketAddr,
|
||||
lifetime_guard: Box<dyn Send + Sync>,
|
||||
}
|
||||
|
||||
impl UdpPunchSocket {
|
||||
pub fn new<G>(session: UdpSession, requested_remote_addr: SocketAddr, lifetime_guard: G) -> Self
|
||||
where
|
||||
G: Send + Sync + 'static,
|
||||
{
|
||||
Self {
|
||||
session,
|
||||
requested_remote_addr,
|
||||
lifetime_guard: Box::new(lifetime_guard),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_connected(self) -> (ConnectedUdpSession, url::Url) {
|
||||
(
|
||||
ConnectedUdpSession::new(self.session, self.lifetime_guard),
|
||||
udp_url(self.requested_remote_addr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UdpPunchSocket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UdpPunchSocket")
|
||||
.field("session", &self.session)
|
||||
.field("requested_remote_addr", &self.requested_remote_addr)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
fn udp_url(addr: SocketAddr) -> url::Url {
|
||||
let mut url = url::Url::parse("udp://0.0.0.0").expect("static UDP URL should be valid");
|
||||
url.set_ip_host(addr.ip())
|
||||
.expect("socket IP should be a valid URL host");
|
||||
url.set_port(Some(addr.port()))
|
||||
.expect("UDP URL should accept a port");
|
||||
url
|
||||
}
|
||||
|
||||
pub struct UdpPunchListener<S> {
|
||||
pub socket: Arc<S>,
|
||||
pub mapped_addr: SocketAddr,
|
||||
pub conn_counter: Arc<dyn ListenerConnectionCounter>,
|
||||
pub acceptor: Box<dyn UdpPunchAcceptor>,
|
||||
pub(crate) port_mapping_lease: Option<Box<dyn UdpPortMappingLease>>,
|
||||
}
|
||||
|
||||
pub struct UdpResolvedPublicAddr {
|
||||
pub mapped_addr: SocketAddr,
|
||||
pub(crate) port_mapping_lease: Option<Box<dyn UdpPortMappingLease>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SelectPunchListener {
|
||||
pub force_new: bool,
|
||||
pub prefer_port_mapping: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SelectPunchListenerResponse {
|
||||
pub listener_mapped_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketCone {
|
||||
pub listener_mapped_addr: SocketAddr,
|
||||
pub dest_addr: SocketAddr,
|
||||
pub transaction_id: u32,
|
||||
pub packet_count_per_batch: u32,
|
||||
pub packet_batch_count: u32,
|
||||
pub packet_interval_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketHardSym {
|
||||
pub listener_mapped_addr: SocketAddr,
|
||||
pub public_ips: Vec<Ipv4Addr>,
|
||||
pub transaction_id: u32,
|
||||
pub port_index: u32,
|
||||
pub round: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketHardSymResponse {
|
||||
pub next_port_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketEasySym {
|
||||
pub listener_mapped_addr: SocketAddr,
|
||||
pub public_ips: Vec<Ipv4Addr>,
|
||||
pub transaction_id: u32,
|
||||
pub base_port_num: u32,
|
||||
pub max_port_num: u32,
|
||||
pub is_incremental: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketBothEasySym {
|
||||
pub udp_socket_count: u32,
|
||||
pub public_ip: Ipv4Addr,
|
||||
pub transaction_id: u32,
|
||||
pub dst_port_num: u32,
|
||||
pub wait_time_ms: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SendPunchPacketBothEasySymResponse {
|
||||
pub is_busy: bool,
|
||||
pub base_mapped_addr: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum UdpHolePunchSignalError {
|
||||
#[error("invalid service key")]
|
||||
InvalidServiceKey,
|
||||
#[error("timeout")]
|
||||
Timeout,
|
||||
#[error("remote rejected: {0}")]
|
||||
RemoteRejected(String),
|
||||
#[error("transport: {0}")]
|
||||
Transport(String),
|
||||
}
|
||||
|
||||
pub fn should_blacklist_signal_error(error: &UdpHolePunchSignalError) -> bool {
|
||||
matches!(error, UdpHolePunchSignalError::InvalidServiceKey)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpHolePunchSignaling: Send + Sync {
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
dst_peer_id: crate::config::PeerId,
|
||||
request: SelectPunchListener,
|
||||
) -> Result<SelectPunchListenerResponse, UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
dst_peer_id: crate::config::PeerId,
|
||||
request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
dst_peer_id: crate::config::PeerId,
|
||||
request: SendPunchPacketHardSym,
|
||||
) -> Result<SendPunchPacketHardSymResponse, UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
dst_peer_id: crate::config::PeerId,
|
||||
request: SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
dst_peer_id: crate::config::PeerId,
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> Result<SendPunchPacketBothEasySymResponse, UdpHolePunchSignalError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpHolePunchInbound: Send + Sync {
|
||||
async fn select_punch_listener(
|
||||
&self,
|
||||
request: SelectPunchListener,
|
||||
) -> Result<SelectPunchListenerResponse, UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_cone(
|
||||
&self,
|
||||
request: SendPunchPacketCone,
|
||||
) -> Result<(), UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_hard_sym(
|
||||
&self,
|
||||
request: SendPunchPacketHardSym,
|
||||
) -> Result<SendPunchPacketHardSymResponse, UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketEasySym,
|
||||
) -> Result<(), UdpHolePunchSignalError>;
|
||||
|
||||
async fn send_punch_packet_both_easy_sym(
|
||||
&self,
|
||||
request: SendPunchPacketBothEasySym,
|
||||
) -> Result<SendPunchPacketBothEasySymResponse, UdpHolePunchSignalError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpHolePunchTransportSink: Send + Sync {
|
||||
async fn add_client_transport(
|
||||
&self,
|
||||
connected: ConnectedUdpSession,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
async fn add_server_transport(
|
||||
&self,
|
||||
connected: ConnectedUdpSession,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
pub struct ProtocolUdpHolePunchTransportSink<TcpSocket, T> {
|
||||
protocol: Arc<dyn ClientProtocolUpgrader<TcpSocket>>,
|
||||
tunnel_sink: Arc<T>,
|
||||
}
|
||||
|
||||
impl<TcpSocket: 'static, T> ProtocolUdpHolePunchTransportSink<TcpSocket, T> {
|
||||
pub fn new(protocol: Arc<dyn ClientProtocolUpgrader<TcpSocket>>, tunnel_sink: Arc<T>) -> Self {
|
||||
Self {
|
||||
protocol,
|
||||
tunnel_sink,
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade(
|
||||
&self,
|
||||
connected: ConnectedUdpSession,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
self.protocol
|
||||
.upgrade_client(ConnectedTransport::Udp(connected), requested_url)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<TcpSocket, T> UdpHolePunchTransportSink for ProtocolUdpHolePunchTransportSink<TcpSocket, T>
|
||||
where
|
||||
TcpSocket: 'static,
|
||||
T: HolePunchTunnelSink,
|
||||
{
|
||||
async fn add_client_transport(
|
||||
&self,
|
||||
connected: ConnectedUdpSession,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let tunnel = self.upgrade(connected, requested_url).await?;
|
||||
self.tunnel_sink.add_client_tunnel(tunnel).await
|
||||
}
|
||||
|
||||
async fn add_server_transport(
|
||||
&self,
|
||||
connected: ConnectedUdpSession,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<()> {
|
||||
let tunnel = self.upgrade(connected, requested_url).await?;
|
||||
self.tunnel_sink.add_server_tunnel(tunnel).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpHolePunchPeerSource: Send + Sync {
|
||||
fn local_peer_id(&self) -> crate::config::PeerId;
|
||||
fn p2p_policy_flags(&self) -> P2pPolicyFlags;
|
||||
|
||||
async fn candidates(&self) -> Vec<super::UdpPunchCandidate>;
|
||||
|
||||
fn p2p_demand_notify(&self) -> Arc<ExternalTaskSignal>;
|
||||
|
||||
fn is_local_virtual_ip(&self, ip: &IpAddr) -> bool;
|
||||
|
||||
async fn is_easytier_managed_ipv6(&self, ip: &Ipv6Addr) -> bool;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UdpHolePunchRuntime: Send + Sync + 'static {
|
||||
type Socket: VirtualUdpSocket + 'static;
|
||||
|
||||
fn socket_context(&self) -> SocketContext {
|
||||
SocketContext::default()
|
||||
}
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>>;
|
||||
|
||||
async fn bind_direct_connect_udp(&self) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
UdpHolePunchRuntime::bind_udp(
|
||||
self,
|
||||
UdpBindOptions::direct_connect().with_context(
|
||||
self.socket_context()
|
||||
.with_ip_version(crate::socket::IpVersion::V4),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn resolve_udp_public_addr(
|
||||
&self,
|
||||
socket: Arc<Self::Socket>,
|
||||
) -> anyhow::Result<UdpResolvedPublicAddr>;
|
||||
|
||||
async fn create_listener(
|
||||
&self,
|
||||
prefer_port_mapping: bool,
|
||||
) -> anyhow::Result<UdpPunchListener<Self::Socket>>;
|
||||
|
||||
async fn create_port_bound_listener(
|
||||
&self,
|
||||
port: u16,
|
||||
) -> anyhow::Result<UdpPunchListener<Self::Socket>>;
|
||||
|
||||
async fn connect_with_socket(
|
||||
&self,
|
||||
socket: Arc<Self::Socket>,
|
||||
remote: SocketAddr,
|
||||
) -> anyhow::Result<UdpPunchSocket>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> VirtualUdpSocketFactory for T
|
||||
where
|
||||
T: UdpHolePunchRuntime + Send + Sync + 'static,
|
||||
{
|
||||
type Socket = T::Socket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
UdpHolePunchRuntime::bind_udp(self, options).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::socket::udp::UdpSessionKind;
|
||||
|
||||
struct MockSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], _addr: SocketAddr) -> io::Result<usize> {
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
struct DropSignal(Arc<AtomicBool>);
|
||||
|
||||
impl Drop for DropSignal {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockProtocol {
|
||||
upgrades: AtomicUsize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ClientProtocolUpgrader<()> for MockProtocol {
|
||||
fn supports_scheme(&self, scheme: &str) -> bool {
|
||||
scheme == "udp"
|
||||
}
|
||||
|
||||
async fn upgrade_client(
|
||||
&self,
|
||||
connected: ConnectedTransport<()>,
|
||||
requested_url: url::Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
self.upgrades.fetch_add(1, Ordering::Relaxed);
|
||||
let ConnectedTransport::Udp(connected) = connected else {
|
||||
anyhow::bail!("expected UDP transport");
|
||||
};
|
||||
Ok(crate::connectivity::protocol::raw::upgrade_connected_udp(
|
||||
connected,
|
||||
requested_url,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockTunnelSink {
|
||||
clients: AtomicUsize,
|
||||
servers: AtomicUsize,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HolePunchTunnelSink for MockTunnelSink {
|
||||
async fn add_client_tunnel(&self, _tunnel: Box<dyn Tunnel>) -> anyhow::Result<()> {
|
||||
self.clients.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_server_tunnel(&self, _tunnel: Box<dyn Tunnel>) -> anyhow::Result<()> {
|
||||
self.servers.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn punched_socket(local_port: u16, remote_port: u16) -> UdpPunchSocket {
|
||||
let remote_addr = SocketAddr::from(([203, 0, 113, 1], remote_port));
|
||||
let session = UdpSession::identity_standalone(
|
||||
Arc::new(MockSocket {
|
||||
local_addr: SocketAddr::from(([127, 0, 0, 1], local_port)),
|
||||
}),
|
||||
remote_addr,
|
||||
UdpSessionKind::EasyTierMux,
|
||||
)
|
||||
.unwrap();
|
||||
UdpPunchSocket::new(session, remote_addr, ())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn protocol_sink_upgrades_before_role_specific_admission() {
|
||||
let protocol = Arc::new(MockProtocol::default());
|
||||
let tunnel_sink = Arc::new(MockTunnelSink::default());
|
||||
let sink =
|
||||
ProtocolUdpHolePunchTransportSink::<(), _>::new(protocol.clone(), tunnel_sink.clone());
|
||||
|
||||
let (client, client_url) = punched_socket(1000, 2000).into_connected();
|
||||
sink.add_client_transport(client, client_url).await.unwrap();
|
||||
let (server, server_url) = punched_socket(1001, 2001).into_connected();
|
||||
sink.add_server_transport(server, server_url).await.unwrap();
|
||||
|
||||
assert_eq!(protocol.upgrades.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(tunnel_sink.clients.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(tunnel_sink.servers.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn punched_socket_preserves_requested_and_resolved_addresses() {
|
||||
let local_addr = SocketAddr::from(([127, 0, 0, 1], 1000));
|
||||
let requested_remote_addr = SocketAddr::from(([198, 51, 100, 1], 2000));
|
||||
let resolved_remote_addr = SocketAddr::from(([203, 0, 113, 1], 3000));
|
||||
let session = UdpSession::identity_standalone(
|
||||
Arc::new(MockSocket { local_addr }),
|
||||
resolved_remote_addr,
|
||||
UdpSessionKind::EasyTierMux,
|
||||
)
|
||||
.unwrap();
|
||||
let guard_dropped = Arc::new(AtomicBool::new(false));
|
||||
let socket = UdpPunchSocket::new(
|
||||
session,
|
||||
requested_remote_addr,
|
||||
DropSignal(guard_dropped.clone()),
|
||||
);
|
||||
|
||||
let (connected, requested_url) = socket.into_connected();
|
||||
let tunnel =
|
||||
crate::connectivity::protocol::raw::upgrade_connected_udp(connected, requested_url)
|
||||
.unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
let local_url: url::Url = info.local_addr.unwrap().into();
|
||||
let remote_url: url::Url = info.remote_addr.unwrap().into();
|
||||
let resolved_url: url::Url = info.resolved_remote_addr.unwrap().into();
|
||||
|
||||
assert_eq!(local_url.host_str(), Some("127.0.0.1"));
|
||||
assert_eq!(local_url.port(), Some(local_addr.port()));
|
||||
assert_eq!(remote_url.host_str(), Some("198.51.100.1"));
|
||||
assert_eq!(remote_url.port(), Some(requested_remote_addr.port()));
|
||||
assert_eq!(resolved_url.host_str(), Some("203.0.113.1"));
|
||||
assert_eq!(resolved_url.port(), Some(resolved_remote_addr.port()));
|
||||
assert!(!guard_dropped.load(Ordering::Relaxed));
|
||||
drop(tunnel);
|
||||
assert!(guard_dropped.load(Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,376 +0,0 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use tokio::task::JoinSet;
|
||||
use tracing::{Instrument, Level, instrument};
|
||||
|
||||
use crate::{
|
||||
foundation::task::reap_joinset_background,
|
||||
packet::{HOLE_PUNCH_PACKET_BODY_LEN, hole_punch_packet_tid},
|
||||
socket::{
|
||||
IpVersion, SocketContext,
|
||||
udp::{UdpBindOptions, VirtualUdpSocket, VirtualUdpSocketFactory},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct PunchedUdpSocket<S> {
|
||||
pub socket: Arc<S>,
|
||||
pub tid: u32,
|
||||
pub remote_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl<S> Debug for PunchedUdpSocket<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("PunchedUdpSocket")
|
||||
.field("tid", &self.tid)
|
||||
.field("remote_addr", &self.remote_addr)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UdpSocketArray<R>
|
||||
where
|
||||
R: VirtualUdpSocketFactory,
|
||||
{
|
||||
sockets: Arc<DashMap<SocketAddr, Arc<R::Socket>>>,
|
||||
max_socket_count: usize,
|
||||
socket_factory: Arc<R>,
|
||||
socket_context: SocketContext,
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
|
||||
interest_tids: Arc<DashSet<u32>>,
|
||||
tid_to_socket: Arc<DashMap<u32, Vec<PunchedUdpSocket<R::Socket>>>>,
|
||||
}
|
||||
|
||||
impl<R> UdpSocketArray<R>
|
||||
where
|
||||
R: VirtualUdpSocketFactory,
|
||||
{
|
||||
pub fn new_with_context(
|
||||
max_socket_count: usize,
|
||||
socket_factory: Arc<R>,
|
||||
socket_context: SocketContext,
|
||||
) -> Self {
|
||||
let tasks = Arc::new(Mutex::new(JoinSet::new()));
|
||||
tokio::spawn(reap_joinset_background(tasks.clone(), "UdpSocketArray"));
|
||||
|
||||
Self {
|
||||
sockets: Arc::new(DashMap::new()),
|
||||
max_socket_count,
|
||||
socket_factory,
|
||||
socket_context,
|
||||
tasks,
|
||||
|
||||
interest_tids: Arc::new(DashSet::new()),
|
||||
tid_to_socket: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn started(&self) -> bool {
|
||||
!self.sockets.is_empty()
|
||||
}
|
||||
|
||||
pub async fn add_new_socket(&self, socket: Arc<R::Socket>) -> anyhow::Result<()> {
|
||||
let socket_map = self.sockets.clone();
|
||||
let local_addr = socket.local_addr()?;
|
||||
let interest_tids = self.interest_tids.clone();
|
||||
let tid_to_socket = self.tid_to_socket.clone();
|
||||
socket_map.insert(local_addr, socket.clone());
|
||||
self.tasks.lock().unwrap().spawn(
|
||||
async move {
|
||||
let _socket_map_guard = RemoveSocketOnDrop {
|
||||
sockets: socket_map,
|
||||
local_addr,
|
||||
};
|
||||
let mut buf = [0u8; super::udp_packet_len(HOLE_PUNCH_PACKET_BODY_LEN)];
|
||||
tracing::trace!(?local_addr, "udp socket added");
|
||||
loop {
|
||||
let Ok((len, addr)) = socket.recv_from(&mut buf).await else {
|
||||
break;
|
||||
};
|
||||
|
||||
tracing::debug!(?len, ?addr, "got raw packet");
|
||||
|
||||
let packet = &buf[..len];
|
||||
let Some(tid) = hole_punch_packet_tid(packet, HOLE_PUNCH_PACKET_BODY_LEN)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::debug!(?addr, ?tid, "got udp hole punch packet");
|
||||
|
||||
if interest_tids.contains(&tid) {
|
||||
tracing::info!(?addr, ?tid, "got hole punching packet with interest tid");
|
||||
tid_to_socket
|
||||
.entry(tid)
|
||||
.or_default()
|
||||
.push(PunchedUdpSocket {
|
||||
socket: socket.clone(),
|
||||
tid,
|
||||
remote_addr: addr,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
tracing::debug!(?local_addr, "udp socket recv loop end");
|
||||
}
|
||||
.instrument(tracing::info_span!("udp array socket recv loop")),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn start(&self) -> anyhow::Result<()> {
|
||||
tracing::info!("starting udp socket array");
|
||||
|
||||
while self.sockets.len() < self.max_socket_count {
|
||||
let socket = self
|
||||
.socket_factory
|
||||
.bind_udp(
|
||||
UdpBindOptions::hole_punch_candidate()
|
||||
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V4)),
|
||||
)
|
||||
.await?;
|
||||
self.add_new_socket(socket).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(err)]
|
||||
pub async fn send_with_all(&self, data: &[u8], addr: SocketAddr) -> anyhow::Result<()> {
|
||||
tracing::info!(?addr, "sending hole punching packet");
|
||||
|
||||
let sockets = self
|
||||
.sockets
|
||||
.iter()
|
||||
.map(|s| s.value().clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for socket in sockets.iter() {
|
||||
for _ in 0..3 {
|
||||
socket.send_to(data, addr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(ret(level = Level::DEBUG))]
|
||||
pub fn try_fetch_punched_socket(&self, tid: u32) -> Option<PunchedUdpSocket<R::Socket>> {
|
||||
tracing::debug!(?tid, "try fetch punched socket");
|
||||
self.tid_to_socket.get_mut(&tid)?.value_mut().pop()
|
||||
}
|
||||
|
||||
pub fn add_interest_tid(&self, tid: u32) {
|
||||
self.interest_tids.insert(tid);
|
||||
}
|
||||
|
||||
pub fn add_intreast_tid(&self, tid: u32) {
|
||||
self.add_interest_tid(tid);
|
||||
}
|
||||
|
||||
pub fn remove_interest_tid(&self, tid: u32) {
|
||||
self.interest_tids.remove(&tid);
|
||||
self.tid_to_socket.remove(&tid);
|
||||
}
|
||||
|
||||
pub fn remove_intreast_tid(&self, tid: u32) {
|
||||
self.remove_interest_tid(tid);
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Debug for UdpSocketArray<R>
|
||||
where
|
||||
R: VirtualUdpSocketFactory,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("UdpSocketArray")
|
||||
.field("sockets", &self.sockets.len())
|
||||
.field("max_socket_count", &self.max_socket_count)
|
||||
.field("started", &self.started())
|
||||
.field("interest_tids", &self.interest_tids.len())
|
||||
.field("tid_to_socket", &self.tid_to_socket.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoveSocketOnDrop<S> {
|
||||
sockets: Arc<DashMap<SocketAddr, Arc<S>>>,
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl<S> Drop for RemoveSocketOnDrop<S> {
|
||||
fn drop(&mut self) {
|
||||
self.sockets.remove(&self.local_addr);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io,
|
||||
sync::atomic::{AtomicU16, Ordering},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
|
||||
use super::*;
|
||||
use crate::{packet::new_hole_punch_packet, socket::NetNamespace};
|
||||
|
||||
impl<R> UdpSocketArray<R>
|
||||
where
|
||||
R: VirtualUdpSocketFactory,
|
||||
{
|
||||
fn new(max_socket_count: usize, socket_factory: Arc<R>) -> Self {
|
||||
Self::new_with_context(max_socket_count, socket_factory, SocketContext::default())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockSocket {
|
||||
local_addr: SocketAddr,
|
||||
incoming: TokioMutex<VecDeque<(Vec<u8>, SocketAddr)>>,
|
||||
sent: TokioMutex<Vec<(Vec<u8>, SocketAddr)>>,
|
||||
}
|
||||
|
||||
impl MockSocket {
|
||||
fn new(local_addr: SocketAddr, incoming: Vec<(Vec<u8>, SocketAddr)>) -> Self {
|
||||
Self {
|
||||
local_addr,
|
||||
incoming: TokioMutex::new(incoming.into()),
|
||||
sent: TokioMutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> io::Result<usize> {
|
||||
self.sent.lock().await.push((data.to_vec(), addr));
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
let packet = self.incoming.lock().await.pop_front();
|
||||
let Some((packet, addr)) = packet else {
|
||||
return std::future::pending().await;
|
||||
};
|
||||
buf[..packet.len()].copy_from_slice(&packet);
|
||||
Ok((packet.len(), addr))
|
||||
}
|
||||
}
|
||||
|
||||
struct MockFactory {
|
||||
next_port: AtomicU16,
|
||||
bind_options: TokioMutex<Vec<UdpBindOptions>>,
|
||||
}
|
||||
|
||||
impl MockFactory {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
next_port: AtomicU16::new(10000),
|
||||
bind_options: TokioMutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocketFactory for MockFactory {
|
||||
type Socket = MockSocket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.bind_options.lock().await.push(options);
|
||||
let port = self.next_port.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(Arc::new(MockSocket::new(
|
||||
SocketAddr::from(([127, 0, 0, 1], port)),
|
||||
Vec::new(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetches_socket_when_interested_tid_is_received() {
|
||||
let runtime = Arc::new(MockFactory::new());
|
||||
let array = UdpSocketArray::new(0, runtime);
|
||||
let tid = 7;
|
||||
let remote_addr = SocketAddr::from(([10, 0, 0, 1], 1234));
|
||||
let packet = new_hole_punch_packet(tid, HOLE_PUNCH_PACKET_BODY_LEN)
|
||||
.into_bytes()
|
||||
.to_vec();
|
||||
let socket = Arc::new(MockSocket::new(
|
||||
SocketAddr::from(([127, 0, 0, 1], 20000)),
|
||||
vec![(packet, remote_addr)],
|
||||
));
|
||||
|
||||
array.add_interest_tid(tid);
|
||||
array.add_new_socket(socket.clone()).await.unwrap();
|
||||
|
||||
for _ in 0..10 {
|
||||
if let Some(punched) = array.try_fetch_punched_socket(tid) {
|
||||
assert_eq!(punched.tid, tid);
|
||||
assert_eq!(punched.remote_addr, remote_addr);
|
||||
assert!(Arc::ptr_eq(&punched.socket, &socket));
|
||||
return;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
panic!("punched socket was not recorded");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_with_all_sends_three_packets_per_socket() {
|
||||
let runtime = Arc::new(MockFactory::new());
|
||||
let array = UdpSocketArray::new(0, runtime);
|
||||
let socket = Arc::new(MockSocket::new(
|
||||
SocketAddr::from(([127, 0, 0, 1], 20001)),
|
||||
Vec::new(),
|
||||
));
|
||||
let remote_addr = SocketAddr::from(([10, 0, 0, 2], 1235));
|
||||
|
||||
array.add_new_socket(socket.clone()).await.unwrap();
|
||||
array.send_with_all(b"abc", remote_addr).await.unwrap();
|
||||
|
||||
let sent = socket.sent.lock().await;
|
||||
assert_eq!(sent.len(), 3);
|
||||
assert!(
|
||||
sent.iter()
|
||||
.all(|(data, addr)| data == b"abc" && *addr == remote_addr)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_binds_up_to_max_socket_count() {
|
||||
let runtime = Arc::new(MockFactory::new());
|
||||
let context = SocketContext::default()
|
||||
.with_socket_mark(Some(0))
|
||||
.with_netns(Some(NetNamespace::new("instance-a")));
|
||||
let array = UdpSocketArray::new_with_context(2, runtime.clone(), context.clone());
|
||||
|
||||
array.start().await.unwrap();
|
||||
|
||||
assert!(array.started());
|
||||
assert_eq!(array.sockets.len(), 2);
|
||||
|
||||
let bind_options = runtime.bind_options.lock().await;
|
||||
assert_eq!(
|
||||
bind_options.as_slice(),
|
||||
&[
|
||||
UdpBindOptions::hole_punch_candidate()
|
||||
.with_context(context.clone().with_ip_version(IpVersion::V4)),
|
||||
UdpBindOptions::hole_punch_candidate()
|
||||
.with_context(context.with_ip_version(IpVersion::V4)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
use crate::{
|
||||
config::{P2pPolicyFlags, PeerId},
|
||||
proto::common::{NatType, PeerFeatureFlag},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::policy::{should_background_p2p_with_peer, should_try_p2p_with_peer},
|
||||
UdpNatType,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UdpPunchCandidate {
|
||||
pub peer_id: PeerId,
|
||||
pub udp_nat_type: NatType,
|
||||
pub feature_flag: Option<PeerFeatureFlag>,
|
||||
pub has_direct_connection: bool,
|
||||
pub has_recent_traffic: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct UdpPunchTaskInfo {
|
||||
pub dst_peer_id: PeerId,
|
||||
pub dst_nat_type: UdpNatType,
|
||||
pub my_nat_type: UdpNatType,
|
||||
}
|
||||
|
||||
pub fn collect_udp_punch_tasks<I, F>(
|
||||
my_peer_id: PeerId,
|
||||
my_nat_type: UdpNatType,
|
||||
policy: P2pPolicyFlags,
|
||||
candidates: I,
|
||||
is_blacklisted: F,
|
||||
) -> Vec<UdpPunchTaskInfo>
|
||||
where
|
||||
I: IntoIterator<Item = UdpPunchCandidate>,
|
||||
F: Fn(PeerId) -> bool,
|
||||
{
|
||||
if my_nat_type.is_open() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter_map(|candidate| {
|
||||
let static_allowed = should_background_p2p_with_peer(
|
||||
candidate.feature_flag.as_ref(),
|
||||
false,
|
||||
policy.lazy_p2p,
|
||||
policy.disable_p2p,
|
||||
policy.need_p2p,
|
||||
);
|
||||
let dynamic_allowed = should_try_p2p_with_peer(
|
||||
candidate.feature_flag.as_ref(),
|
||||
false,
|
||||
policy.disable_p2p,
|
||||
policy.need_p2p,
|
||||
) && candidate.has_recent_traffic;
|
||||
if !static_allowed && !dynamic_allowed {
|
||||
return None;
|
||||
}
|
||||
|
||||
let peer_id = candidate.peer_id;
|
||||
if is_blacklisted(peer_id) || candidate.has_direct_connection {
|
||||
return None;
|
||||
}
|
||||
|
||||
let peer_nat_type = candidate.udp_nat_type.into();
|
||||
if !my_nat_type.can_punch_hole_as_client(
|
||||
peer_nat_type,
|
||||
my_peer_id,
|
||||
peer_id,
|
||||
policy.disable_sym_hole_punching,
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(UdpPunchTaskInfo {
|
||||
dst_peer_id: peer_id,
|
||||
dst_nat_type: peer_nat_type,
|
||||
my_nat_type,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::proto::common::PeerFeatureFlag;
|
||||
|
||||
fn candidate(peer_id: PeerId, udp_nat_type: NatType) -> UdpPunchCandidate {
|
||||
UdpPunchCandidate {
|
||||
peer_id,
|
||||
udp_nat_type,
|
||||
feature_flag: Some(PeerFeatureFlag::default()),
|
||||
has_direct_connection: false,
|
||||
has_recent_traffic: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect(
|
||||
my_peer_id: PeerId,
|
||||
my_nat_type: NatType,
|
||||
policy: P2pPolicyFlags,
|
||||
candidates: Vec<UdpPunchCandidate>,
|
||||
) -> Vec<UdpPunchTaskInfo> {
|
||||
collect_udp_punch_tasks(my_peer_id, my_nat_type.into(), policy, candidates, |_| {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_nat_does_not_start_udp_punch_tasks() {
|
||||
let tasks = collect(
|
||||
1,
|
||||
NatType::OpenInternet,
|
||||
P2pPolicyFlags::default(),
|
||||
vec![candidate(2, NatType::PortRestricted)],
|
||||
);
|
||||
|
||||
assert!(tasks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy_p2p_allows_recent_traffic_without_need_p2p_flag() {
|
||||
let mut idle = candidate(2, NatType::PortRestricted);
|
||||
idle.feature_flag = Some(PeerFeatureFlag {
|
||||
need_p2p: false,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut active = idle.clone();
|
||||
active.peer_id = 3;
|
||||
active.has_recent_traffic = true;
|
||||
|
||||
let tasks = collect(
|
||||
1,
|
||||
NatType::PortRestricted,
|
||||
P2pPolicyFlags {
|
||||
lazy_p2p: true,
|
||||
..Default::default()
|
||||
},
|
||||
vec![idle, active],
|
||||
);
|
||||
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].dst_peer_id, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_blacklisted_and_directly_connected_candidates() {
|
||||
let mut direct = candidate(2, NatType::PortRestricted);
|
||||
direct.has_direct_connection = true;
|
||||
|
||||
let tasks = collect_udp_punch_tasks(
|
||||
1,
|
||||
NatType::PortRestricted.into(),
|
||||
P2pPolicyFlags::default(),
|
||||
vec![direct, candidate(3, NatType::PortRestricted)],
|
||||
|peer_id| peer_id == 3,
|
||||
);
|
||||
|
||||
assert!(tasks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_candidates_by_udp_nat_method() {
|
||||
let tasks = collect(
|
||||
1,
|
||||
NatType::PortRestricted,
|
||||
P2pPolicyFlags::default(),
|
||||
vec![
|
||||
candidate(2, NatType::Symmetric),
|
||||
candidate(3, NatType::PortRestricted),
|
||||
],
|
||||
);
|
||||
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].dst_peer_id, 3);
|
||||
assert_eq!(tasks[0].dst_nat_type, NatType::PortRestricted.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn easy_symmetric_pair_uses_lower_peer_id_as_initiator() {
|
||||
let tasks = collect(
|
||||
1,
|
||||
NatType::SymmetricEasyInc,
|
||||
P2pPolicyFlags::default(),
|
||||
vec![candidate(2, NatType::SymmetricEasyDec)],
|
||||
);
|
||||
assert_eq!(tasks.len(), 1);
|
||||
|
||||
let tasks = collect(
|
||||
2,
|
||||
NatType::SymmetricEasyInc,
|
||||
P2pPolicyFlags::default(),
|
||||
vec![candidate(1, NatType::SymmetricEasyDec)],
|
||||
);
|
||||
assert!(tasks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabling_symmetric_hole_punch_keeps_sym_to_cone_as_cone_method() {
|
||||
let tasks = collect(
|
||||
1,
|
||||
NatType::Symmetric,
|
||||
P2pPolicyFlags {
|
||||
disable_sym_hole_punching: true,
|
||||
..Default::default()
|
||||
},
|
||||
vec![candidate(2, NatType::PortRestricted)],
|
||||
);
|
||||
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(tasks[0].dst_peer_id, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::socket::{IpVersion, SocketContext, tcp::TcpBindOptions};
|
||||
|
||||
#[cfg(feature = "endpoint-discovery")]
|
||||
mod implementation;
|
||||
|
||||
#[cfg(feature = "endpoint-discovery")]
|
||||
pub(crate) use implementation::CoreManualEndpointResolver;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ManualEndpointDiscoveryConfig {
|
||||
pub user_agent: String,
|
||||
pub network_name: String,
|
||||
pub http_timeout: Duration,
|
||||
pub http_ip_version: IpVersion,
|
||||
pub http_tcp_bind: TcpBindOptions,
|
||||
pub dns_record_context: SocketContext,
|
||||
pub srv_protocols: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for ManualEndpointDiscoveryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user_agent: "easytier-core".to_owned(),
|
||||
network_name: String::new(),
|
||||
http_timeout: Duration::from_secs(20),
|
||||
http_ip_version: IpVersion::Both,
|
||||
http_tcp_bind: TcpBindOptions::default(),
|
||||
dns_record_context: SocketContext::default(),
|
||||
srv_protocols: vec!["tcp".to_owned(), "udp".to_owned()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "endpoint-discovery"))]
|
||||
pub(crate) struct CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: crate::socket::tcp::VirtualTcpSocketFactory,
|
||||
{
|
||||
_host: std::marker::PhantomData<fn() -> H>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "endpoint-discovery"))]
|
||||
impl<H> CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: crate::socket::tcp::VirtualTcpSocketFactory,
|
||||
{
|
||||
pub fn new(
|
||||
host: std::sync::Arc<H>,
|
||||
dns: std::sync::Arc<dyn crate::host::dns::DnsResolver>,
|
||||
dns_records: std::sync::Arc<dyn crate::host::dns::DnsRecordResolver>,
|
||||
config: ManualEndpointDiscoveryConfig,
|
||||
) -> Self {
|
||||
let _ = (host, dns, dns_records, config);
|
||||
Self {
|
||||
_host: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "endpoint-discovery"))]
|
||||
#[async_trait::async_trait]
|
||||
impl<H> super::ManualEndpointResolver for CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: crate::socket::tcp::VirtualTcpSocketFactory,
|
||||
{
|
||||
async fn resolve_endpoint(&self, url: &url::Url) -> anyhow::Result<url::Url> {
|
||||
anyhow::bail!("endpoint discovery is disabled for {url}")
|
||||
}
|
||||
}
|
||||
@@ -1,924 +0,0 @@
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt as _, Empty};
|
||||
use hyper::{Request, header};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use rand::{Rng as _, seq::SliceRandom};
|
||||
use rustls::pki_types::ServerName;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_rustls::TlsConnector;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connectivity::transport,
|
||||
host::dns::{DnsQuery, DnsRecordResolver, DnsResolver, DnsSrvRecord},
|
||||
socket::{
|
||||
IpVersion, SocketContext,
|
||||
tcp::{TcpBindOptions, TcpSocketPurpose, VirtualTcpSocketFactory},
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{ManualEndpointResolver, resolve_url_addrs};
|
||||
use super::ManualEndpointDiscoveryConfig;
|
||||
|
||||
const HTTP_DEFAULT_PORT: u16 = 80;
|
||||
const HTTPS_DEFAULT_PORT: u16 = 443;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HttpDiscoveryRequest {
|
||||
pub url: Url,
|
||||
pub user_agent: String,
|
||||
pub network_name: String,
|
||||
pub timeout: Duration,
|
||||
pub ip_version: IpVersion,
|
||||
pub tcp_bind: TcpBindOptions,
|
||||
}
|
||||
|
||||
pub(crate) struct CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: VirtualTcpSocketFactory,
|
||||
{
|
||||
host: Arc<H>,
|
||||
dns: Arc<dyn DnsResolver>,
|
||||
dns_records: Arc<dyn DnsRecordResolver>,
|
||||
config: ManualEndpointDiscoveryConfig,
|
||||
}
|
||||
|
||||
impl<H> CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: VirtualTcpSocketFactory,
|
||||
{
|
||||
pub fn new(
|
||||
host: Arc<H>,
|
||||
dns: Arc<dyn DnsResolver>,
|
||||
dns_records: Arc<dyn DnsRecordResolver>,
|
||||
config: ManualEndpointDiscoveryConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
host,
|
||||
dns,
|
||||
dns_records,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<H> ManualEndpointResolver for CoreManualEndpointResolver<H>
|
||||
where
|
||||
H: VirtualTcpSocketFactory,
|
||||
{
|
||||
async fn resolve_endpoint(&self, url: &Url) -> anyhow::Result<Url> {
|
||||
match url.scheme() {
|
||||
"http" | "https" => {
|
||||
let response = fetch_http_discovery(
|
||||
self.host.clone(),
|
||||
self.dns.as_ref(),
|
||||
HttpDiscoveryRequest {
|
||||
url: url.clone(),
|
||||
user_agent: self.config.user_agent.clone(),
|
||||
network_name: self.config.network_name.clone(),
|
||||
timeout: self.config.http_timeout,
|
||||
ip_version: self.config.http_ip_version,
|
||||
tcp_bind: self.config.http_tcp_bind.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
resolve_http_endpoint(response)
|
||||
.map(|endpoint| endpoint.url)
|
||||
.map_err(|error| anyhow::anyhow!("Invalid Url: {error}"))
|
||||
}
|
||||
"txt" => {
|
||||
let host = endpoint_host(url)?;
|
||||
resolve_txt_endpoint(
|
||||
self.dns_records.as_ref(),
|
||||
host,
|
||||
self.config.dns_record_context.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
"srv" => {
|
||||
let host = endpoint_host(url)?;
|
||||
resolve_srv_endpoint(
|
||||
self.dns_records.as_ref(),
|
||||
host,
|
||||
&self.config.srv_protocols,
|
||||
self.config.dns_record_context.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
scheme => anyhow::bail!("unsupported manual endpoint resolver scheme: {scheme}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn endpoint_host(url: &Url) -> anyhow::Result<&str> {
|
||||
url.host_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("host should not be empty in {url}"))
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_http_discovery<H>(
|
||||
host: Arc<H>,
|
||||
dns: &dyn DnsResolver,
|
||||
request: HttpDiscoveryRequest,
|
||||
) -> anyhow::Result<HttpDiscoveryResponse>
|
||||
where
|
||||
H: VirtualTcpSocketFactory,
|
||||
{
|
||||
let timeout = request.timeout;
|
||||
crate::foundation::time::timeout(timeout, fetch_http_discovery_inner(host, dns, request))
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("HTTP discovery timed out after {timeout:?}"))?
|
||||
}
|
||||
|
||||
async fn fetch_http_discovery_inner<H>(
|
||||
host: Arc<H>,
|
||||
dns: &dyn DnsResolver,
|
||||
request: HttpDiscoveryRequest,
|
||||
) -> anyhow::Result<HttpDiscoveryResponse>
|
||||
where
|
||||
H: VirtualTcpSocketFactory,
|
||||
{
|
||||
let default_port = match request.url.scheme() {
|
||||
"http" => HTTP_DEFAULT_PORT,
|
||||
"https" => HTTPS_DEFAULT_PORT,
|
||||
scheme => anyhow::bail!("unsupported HTTP discovery scheme: {scheme}"),
|
||||
};
|
||||
let addrs = resolve_url_addrs(
|
||||
&request.url,
|
||||
default_port,
|
||||
request
|
||||
.tcp_bind
|
||||
.context
|
||||
.clone()
|
||||
.with_ip_version(request.ip_version),
|
||||
dns,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut last_error = None;
|
||||
let mut socket = None;
|
||||
for addr in addrs {
|
||||
match transport::connect_tcp(
|
||||
host.clone(),
|
||||
addr,
|
||||
Vec::new(),
|
||||
request.tcp_bind.clone(),
|
||||
TcpSocketPurpose::ManualConnect,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(connected) => {
|
||||
socket = Some(connected);
|
||||
break;
|
||||
}
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
let socket = socket.ok_or_else(|| {
|
||||
last_error.unwrap_or_else(|| anyhow::anyhow!("no HTTP discovery address candidates"))
|
||||
})?;
|
||||
|
||||
if request.url.scheme() == "https" {
|
||||
let server_name = tls_server_name(&request.url)?;
|
||||
let root_store = rustls::RootCertStore {
|
||||
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
|
||||
};
|
||||
let tls_config = rustls::ClientConfig::builder()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let stream = TlsConnector::from(Arc::new(tls_config))
|
||||
.connect(server_name, socket)
|
||||
.await
|
||||
.with_context(|| format!("HTTPS handshake failed for {}", request.url))?;
|
||||
send_http_discovery_request(stream, request).await
|
||||
} else {
|
||||
send_http_discovery_request(socket, request).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tls_server_name(url: &Url) -> anyhow::Result<ServerName<'static>> {
|
||||
match url.host() {
|
||||
Some(url::Host::Domain(host)) => ServerName::try_from(host.to_owned())
|
||||
.with_context(|| format!("invalid HTTPS server name in {url}")),
|
||||
Some(url::Host::Ipv4(ip)) => Ok(ServerName::IpAddress(ip.into())),
|
||||
Some(url::Host::Ipv6(ip)) => Ok(ServerName::IpAddress(ip.into())),
|
||||
None => anyhow::bail!("HTTP discovery URL has no host: {url}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_http_discovery_request<S>(
|
||||
stream: S,
|
||||
request: HttpDiscoveryRequest,
|
||||
) -> anyhow::Result<HttpDiscoveryResponse>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let io = TokioIo::new(stream);
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(io)
|
||||
.await
|
||||
.with_context(|| format!("starting HTTP connection failed for {}", request.url))?;
|
||||
let connection_task = AbortOnDropHandle::new(tokio::spawn(connection));
|
||||
|
||||
let request_target = match request.url.query() {
|
||||
Some(query) => format!("{}?{query}", request.url.path()),
|
||||
None => request.url.path().to_owned(),
|
||||
};
|
||||
let host_header = &request.url[url::Position::BeforeHost..url::Position::AfterPort];
|
||||
let outgoing = Request::builder()
|
||||
.method("GET")
|
||||
.uri(request_target)
|
||||
.header(header::HOST, host_header)
|
||||
.header(header::USER_AGENT, request.user_agent)
|
||||
.header("X-Network-Name", request.network_name)
|
||||
.header(header::CONNECTION, "close")
|
||||
.body(Empty::<Bytes>::new())?;
|
||||
let response = sender
|
||||
.send_request(outgoing)
|
||||
.await
|
||||
.with_context(|| format!("sending HTTP request failed for {}", request.url))?;
|
||||
let status_code = response.status().as_u16();
|
||||
let location = response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned());
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.with_context(|| format!("reading HTTP response failed for {}", request.url))?
|
||||
.to_bytes();
|
||||
drop(sender);
|
||||
connection_task
|
||||
.await
|
||||
.context("HTTP connection task failed")?
|
||||
.with_context(|| format!("HTTP connection failed for {}", request.url))?;
|
||||
|
||||
Ok(HttpDiscoveryResponse {
|
||||
status_code,
|
||||
location,
|
||||
body: String::from_utf8_lossy(&body).into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum HttpEndpointSource {
|
||||
RedirectQuery,
|
||||
RedirectUrl,
|
||||
ResponseBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct HttpDiscoveryResponse {
|
||||
pub status_code: u16,
|
||||
pub location: Option<String>,
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ResolvedHttpEndpoint {
|
||||
pub url: Url,
|
||||
pub source: HttpEndpointSource,
|
||||
}
|
||||
|
||||
fn resolve_http_redirect(location: &str) -> anyhow::Result<ResolvedHttpEndpoint> {
|
||||
let url = Url::parse(location)
|
||||
.with_context(|| format!("parsing redirect URL failed. url: {location}"))?;
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
return Ok(ResolvedHttpEndpoint {
|
||||
url,
|
||||
source: HttpEndpointSource::RedirectUrl,
|
||||
});
|
||||
}
|
||||
|
||||
let candidates = url
|
||||
.query_pairs()
|
||||
.filter_map(|(_, value)| Url::parse(&value).ok())
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(url) = candidates.choose(&mut rand::thread_rng()).cloned() {
|
||||
return Ok(ResolvedHttpEndpoint {
|
||||
url,
|
||||
source: HttpEndpointSource::RedirectQuery,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(url) = location
|
||||
.strip_prefix(&format!("{}://", url.scheme()))
|
||||
.and_then(|value| Url::parse(value).ok())
|
||||
{
|
||||
return Ok(ResolvedHttpEndpoint {
|
||||
url,
|
||||
source: HttpEndpointSource::RedirectUrl,
|
||||
});
|
||||
}
|
||||
|
||||
anyhow::bail!("no valid connector URL found in redirect location {location:?}")
|
||||
}
|
||||
|
||||
fn resolve_http_body(body: &str) -> anyhow::Result<ResolvedHttpEndpoint> {
|
||||
let mut candidates = body
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
candidates.shuffle(&mut rand::thread_rng());
|
||||
for candidate in candidates {
|
||||
if let Ok(url) = Url::parse(candidate) {
|
||||
return Ok(ResolvedHttpEndpoint {
|
||||
url,
|
||||
source: HttpEndpointSource::ResponseBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
anyhow::bail!("no valid connector URL found in response body {body:?}")
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_http_endpoint(
|
||||
response: HttpDiscoveryResponse,
|
||||
) -> anyhow::Result<ResolvedHttpEndpoint> {
|
||||
match response.status_code {
|
||||
300..=399 => resolve_http_redirect(
|
||||
response
|
||||
.location
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("HTTP redirect has no Location header"))?,
|
||||
),
|
||||
200..=299 => resolve_http_body(&response.body),
|
||||
status_code => anyhow::bail!(
|
||||
"unexpected HTTP discovery status {status_code}, body: {:?}",
|
||||
response.body
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_weighted<T>(options: &[(T, u64)]) -> Option<&T> {
|
||||
let total_weight = options.iter().map(|(_, weight)| *weight).sum();
|
||||
let mut rng = rand::thread_rng();
|
||||
let selected = rng.gen_range(0..total_weight);
|
||||
let mut accumulated = 0;
|
||||
|
||||
for (item, weight) in options {
|
||||
accumulated += *weight;
|
||||
if selected < accumulated {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_txt_endpoint(
|
||||
resolver: &dyn DnsRecordResolver,
|
||||
domain_name: &str,
|
||||
context: SocketContext,
|
||||
) -> anyhow::Result<Url> {
|
||||
let txt_data = resolver
|
||||
.resolve_txt(DnsQuery::new(domain_name, context))
|
||||
.await
|
||||
.with_context(|| format!("resolve TXT record failed for {domain_name}"))?;
|
||||
let candidates = txt_data
|
||||
.split(' ')
|
||||
.filter_map(|candidate| Url::parse(candidate).ok())
|
||||
.collect::<Vec<_>>();
|
||||
candidates
|
||||
.choose(&mut rand::thread_rng())
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no valid URL found in TXT data {txt_data:?}; expected a space-separated URL list"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn srv_record_url(protocol: &str, record: DnsSrvRecord) -> anyhow::Result<(Url, u64)> {
|
||||
if record.port == 0 {
|
||||
anyhow::bail!("SRV port must be non-zero");
|
||||
}
|
||||
let url = format!("{protocol}://{}:{}", record.target, record.port);
|
||||
// Preserve the existing EasyTier selection rule, which treats SRV priority
|
||||
// as the candidate weight.
|
||||
Ok((Url::parse(&url)?, u64::from(record.priority)))
|
||||
}
|
||||
|
||||
pub(super) fn deduplicate_srv_candidates(candidates: Vec<(Url, u64)>) -> Vec<(Url, u64)> {
|
||||
candidates
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_srv_endpoint(
|
||||
resolver: &dyn DnsRecordResolver,
|
||||
domain_name: &str,
|
||||
supported_protocols: &[String],
|
||||
context: SocketContext,
|
||||
) -> anyhow::Result<Url> {
|
||||
let lookups = supported_protocols.iter().map(|protocol| {
|
||||
let protocol = protocol.clone();
|
||||
let query = DnsQuery::new(
|
||||
format!("_easytier._{protocol}.{domain_name}"),
|
||||
context.clone(),
|
||||
);
|
||||
async move { (protocol, resolver.resolve_srv(query).await) }
|
||||
});
|
||||
|
||||
let mut candidates = Vec::new();
|
||||
for (protocol, result) in futures::future::join_all(lookups).await {
|
||||
let Ok(records) = result else {
|
||||
continue;
|
||||
};
|
||||
candidates.extend(records.into_iter().filter_map(|record| {
|
||||
match srv_record_url(&protocol, record) {
|
||||
Ok(candidate) => Some(candidate),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
?error,
|
||||
srv_domain = %format!("_easytier._{protocol}.{domain_name}"),
|
||||
"ignore invalid SRV endpoint record"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
if candidates.is_empty() {
|
||||
anyhow::bail!("no SRV endpoint found for {domain_name}");
|
||||
}
|
||||
let candidates = deduplicate_srv_candidates(candidates);
|
||||
|
||||
choose_weighted(&candidates)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to choose an SRV endpoint for {domain_name}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
pin::Pin,
|
||||
sync::Mutex,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream, ReadBuf};
|
||||
|
||||
use crate::socket::tcp::{TcpConnectOptions, VirtualTcpSocket, VirtualTcpSocketFactory};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct HttpTestSocket {
|
||||
stream: DuplexStream,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl AsyncRead for HttpTestSocket {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for HttpTestSocket {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.stream).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualTcpSocket for HttpTestSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.peer_addr)
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpTestHost {
|
||||
stream: Mutex<Option<DuplexStream>>,
|
||||
connects: Mutex<Vec<TcpConnectOptions>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualTcpSocketFactory for HttpTestHost {
|
||||
type Socket = HttpTestSocket;
|
||||
|
||||
async fn connect_tcp(&self, options: TcpConnectOptions) -> anyhow::Result<Self::Socket> {
|
||||
self.connects.lock().unwrap().push(options.clone());
|
||||
let stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow::anyhow!("test socket already connected"))?;
|
||||
Ok(HttpTestSocket {
|
||||
stream,
|
||||
local_addr: "192.0.2.2:40000".parse().unwrap(),
|
||||
peer_addr: options.remote_addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpTestDns {
|
||||
queries: Mutex<Vec<DnsQuery>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DnsResolver for HttpTestDns {
|
||||
async fn resolve(&self, query: DnsQuery) -> anyhow::Result<Vec<IpAddr>> {
|
||||
self.queries.lock().unwrap().push(query);
|
||||
Ok(vec![IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))])
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_fetch_uses_host_dns_and_socket_while_core_drives_io() {
|
||||
let (client, mut server) = tokio::io::duplex(8192);
|
||||
let host = Arc::new(HttpTestHost {
|
||||
stream: Mutex::new(Some(client)),
|
||||
connects: Mutex::new(Vec::new()),
|
||||
});
|
||||
let dns = HttpTestDns {
|
||||
queries: Mutex::new(Vec::new()),
|
||||
};
|
||||
let server_task = tokio::spawn(async move {
|
||||
let mut request = Vec::new();
|
||||
loop {
|
||||
let mut chunk = [0; 1024];
|
||||
let len = server.read(&mut chunk).await.unwrap();
|
||||
assert_ne!(len, 0, "HTTP request ended before its headers");
|
||||
request.extend_from_slice(&chunk[..len]);
|
||||
if request.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let request = String::from_utf8(request).unwrap().to_ascii_lowercase();
|
||||
assert!(request.starts_with("get /lookup?kind=peer http/1.1\r\n"));
|
||||
assert!(request.contains("host: discovery.example:18080\r\n"));
|
||||
assert!(request.contains("user-agent: easytier/test\r\n"));
|
||||
assert!(request.contains("x-network-name: test-network\r\n"));
|
||||
assert!(request.contains("connection: close\r\n"));
|
||||
|
||||
server
|
||||
.write_all(
|
||||
b"HTTP/1.1 302 Found\r\nLocation: tcp://192.0.2.10:11010\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n5\r\nhello\r\n0\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
server.shutdown().await.unwrap();
|
||||
});
|
||||
|
||||
let response = fetch_http_discovery(
|
||||
host.clone(),
|
||||
&dns,
|
||||
HttpDiscoveryRequest {
|
||||
url: "http://discovery.example:18080/lookup?kind=peer"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
user_agent: "easytier/test".to_owned(),
|
||||
network_name: "test-network".to_owned(),
|
||||
timeout: Duration::from_secs(1),
|
||||
ip_version: IpVersion::V4,
|
||||
tcp_bind: TcpBindOptions::default().with_socket_mark(Some(9)),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
crate::foundation::time::timeout(Duration::from_secs(1), server_task)
|
||||
.await
|
||||
.expect("HTTP fetch test server did not finish")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status_code, 302);
|
||||
assert_eq!(response.location.as_deref(), Some("tcp://192.0.2.10:11010"));
|
||||
assert_eq!(response.body, "hello");
|
||||
assert_eq!(
|
||||
*dns.queries.lock().unwrap(),
|
||||
[DnsQuery::new(
|
||||
"discovery.example",
|
||||
SocketContext {
|
||||
ip_version: IpVersion::V4,
|
||||
socket_mark: Some(9),
|
||||
netns: None,
|
||||
}
|
||||
)]
|
||||
);
|
||||
assert_eq!(host.connects.lock().unwrap().len(), 1);
|
||||
let options = &host.connects.lock().unwrap()[0];
|
||||
assert_eq!(options.remote_addr, "192.0.2.1:18080".parse().unwrap());
|
||||
assert_eq!(options.purpose, TcpSocketPurpose::ManualConnect);
|
||||
assert_eq!(options.bind.context.socket_mark, Some(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_discovery_interprets_redirect_and_body_forms() {
|
||||
let query = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
status_code: 302,
|
||||
location: Some("https://discovery.example/?url=tcp://127.0.0.1:11010".to_owned()),
|
||||
body: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(query.url.as_str(), "tcp://127.0.0.1:11010");
|
||||
assert_eq!(query.source, HttpEndpointSource::RedirectQuery);
|
||||
|
||||
let nested = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
status_code: 302,
|
||||
location: Some("https://udp://127.0.0.1:11010".to_owned()),
|
||||
body: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(nested.url.as_str(), "udp://127.0.0.1:11010");
|
||||
assert_eq!(nested.source, HttpEndpointSource::RedirectUrl);
|
||||
|
||||
let direct = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
status_code: 307,
|
||||
location: Some("quic://127.0.0.1:11012".to_owned()),
|
||||
body: String::new(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(direct.url.as_str(), "quic://127.0.0.1:11012");
|
||||
assert_eq!(direct.source, HttpEndpointSource::RedirectUrl);
|
||||
|
||||
let body = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
status_code: 200,
|
||||
location: None,
|
||||
body: "invalid\nwg://127.0.0.1:11011\n".to_owned(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(body.url.as_str(), "wg://127.0.0.1:11011");
|
||||
assert_eq!(body.source, HttpEndpointSource::ResponseBody);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_discovery_reports_malformed_redirect_location() {
|
||||
let error = resolve_http_endpoint(HttpDiscoveryResponse {
|
||||
status_code: 302,
|
||||
location: Some("not a URL".to_owned()),
|
||||
body: String::new(),
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
let message = error.to_string();
|
||||
assert!(message.contains("parsing redirect URL failed"));
|
||||
assert!(message.contains("not a URL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_server_name_accepts_ip_literals_without_url_brackets() {
|
||||
let ipv4: Url = "https://192.0.2.1/".parse().unwrap();
|
||||
let ipv6: Url = "https://[2001:db8::1]/".parse().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tls_server_name(&ipv4).unwrap(),
|
||||
ServerName::IpAddress(Ipv4Addr::new(192, 0, 2, 1).into())
|
||||
);
|
||||
assert_eq!(
|
||||
tls_server_name(&ipv6).unwrap(),
|
||||
ServerName::IpAddress("2001:db8::1".parse::<std::net::Ipv6Addr>().unwrap().into())
|
||||
);
|
||||
}
|
||||
|
||||
struct TestResolver {
|
||||
txt: String,
|
||||
srv: Vec<DnsSrvRecord>,
|
||||
queries: Mutex<Vec<DnsQuery>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DnsRecordResolver for TestResolver {
|
||||
async fn resolve_txt(&self, query: DnsQuery) -> anyhow::Result<String> {
|
||||
self.queries.lock().unwrap().push(query);
|
||||
Ok(self.txt.clone())
|
||||
}
|
||||
|
||||
async fn resolve_srv(&self, query: DnsQuery) -> anyhow::Result<Vec<DnsSrvRecord>> {
|
||||
self.queries.lock().unwrap().push(query);
|
||||
Ok(self.srv.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_endpoint_resolver_owns_record_scheme_dispatch() {
|
||||
let host = Arc::new(HttpTestHost {
|
||||
stream: Mutex::new(None),
|
||||
connects: Mutex::new(Vec::new()),
|
||||
});
|
||||
let dns: Arc<dyn DnsResolver> = Arc::new(HttpTestDns {
|
||||
queries: Mutex::new(Vec::new()),
|
||||
});
|
||||
let records = Arc::new(TestResolver {
|
||||
txt: "tcp://192.0.2.10:11010".to_owned(),
|
||||
srv: vec![DnsSrvRecord {
|
||||
priority: 1,
|
||||
weight: 10,
|
||||
port: 11012,
|
||||
target: "peer.example.com.".to_owned(),
|
||||
}],
|
||||
queries: Mutex::new(Vec::new()),
|
||||
});
|
||||
let record_context = SocketContext {
|
||||
ip_version: IpVersion::V6,
|
||||
socket_mark: Some(17),
|
||||
netns: None,
|
||||
};
|
||||
let resolver = CoreManualEndpointResolver::new(
|
||||
host,
|
||||
dns,
|
||||
records.clone(),
|
||||
ManualEndpointDiscoveryConfig {
|
||||
user_agent: "easytier/test".to_owned(),
|
||||
network_name: "test-network".to_owned(),
|
||||
http_timeout: Duration::from_secs(1),
|
||||
http_ip_version: IpVersion::Both,
|
||||
http_tcp_bind: TcpBindOptions::default(),
|
||||
dns_record_context: record_context.clone(),
|
||||
srv_protocols: vec!["quic".to_owned()],
|
||||
},
|
||||
);
|
||||
|
||||
let txt = resolver
|
||||
.resolve_endpoint(&"txt://discovery.example".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let srv = resolver
|
||||
.resolve_endpoint(&"srv://discovery.example".parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(txt.as_str(), "tcp://192.0.2.10:11010");
|
||||
assert_eq!(srv.as_str(), "quic://peer.example.com.:11012");
|
||||
assert_eq!(
|
||||
*records.queries.lock().unwrap(),
|
||||
[
|
||||
DnsQuery::new("discovery.example", record_context.clone()),
|
||||
DnsQuery::new("_easytier._quic.discovery.example", record_context)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_endpoint_resolver_passes_http_config_and_error_classification() {
|
||||
let (client, mut server) = tokio::io::duplex(8192);
|
||||
let host = Arc::new(HttpTestHost {
|
||||
stream: Mutex::new(Some(client)),
|
||||
connects: Mutex::new(Vec::new()),
|
||||
});
|
||||
let dns = Arc::new(HttpTestDns {
|
||||
queries: Mutex::new(Vec::new()),
|
||||
});
|
||||
let records = Arc::new(TestResolver {
|
||||
txt: String::new(),
|
||||
srv: Vec::new(),
|
||||
queries: Mutex::new(Vec::new()),
|
||||
});
|
||||
let server_task = tokio::spawn(async move {
|
||||
let mut request = Vec::new();
|
||||
loop {
|
||||
let mut chunk = [0; 1024];
|
||||
let len = server.read(&mut chunk).await.unwrap();
|
||||
assert_ne!(len, 0, "HTTP request ended before its headers");
|
||||
request.extend_from_slice(&chunk[..len]);
|
||||
if request.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let request = String::from_utf8(request).unwrap().to_ascii_lowercase();
|
||||
assert!(request.contains("user-agent: easytier/facade-test\r\n"));
|
||||
assert!(request.contains("x-network-name: facade-network\r\n"));
|
||||
server
|
||||
.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Length: 9\r\nConnection: close\r\n\r\nnot a URL",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
server.shutdown().await.unwrap();
|
||||
});
|
||||
let resolver = CoreManualEndpointResolver::new(
|
||||
host.clone(),
|
||||
dns.clone(),
|
||||
records,
|
||||
ManualEndpointDiscoveryConfig {
|
||||
user_agent: "easytier/facade-test".to_owned(),
|
||||
network_name: "facade-network".to_owned(),
|
||||
http_timeout: Duration::from_secs(1),
|
||||
http_ip_version: IpVersion::V4,
|
||||
http_tcp_bind: TcpBindOptions::default().with_socket_mark(Some(23)),
|
||||
dns_record_context: SocketContext::default(),
|
||||
srv_protocols: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let error = resolver
|
||||
.resolve_endpoint(&"http://discovery.example:18081/endpoint".parse().unwrap())
|
||||
.await
|
||||
.unwrap_err();
|
||||
crate::foundation::time::timeout(Duration::from_secs(1), server_task)
|
||||
.await
|
||||
.expect("HTTP facade test server did not finish")
|
||||
.unwrap();
|
||||
|
||||
assert!(error.to_string().starts_with("Invalid Url:"));
|
||||
assert_eq!(
|
||||
*dns.queries.lock().unwrap(),
|
||||
[DnsQuery::new(
|
||||
"discovery.example",
|
||||
SocketContext {
|
||||
ip_version: IpVersion::V4,
|
||||
socket_mark: Some(23),
|
||||
netns: None,
|
||||
}
|
||||
)]
|
||||
);
|
||||
let connects = host.connects.lock().unwrap();
|
||||
assert_eq!(connects.len(), 1);
|
||||
assert_eq!(connects[0].remote_addr, "192.0.2.1:18081".parse().unwrap());
|
||||
assert_eq!(connects[0].bind.context.socket_mark, Some(23));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn txt_discovery_parses_easy_tier_url_candidates() {
|
||||
let resolver = TestResolver {
|
||||
txt: "invalid tcp://127.0.0.1:11010".to_owned(),
|
||||
srv: Vec::new(),
|
||||
queries: Mutex::new(Vec::new()),
|
||||
};
|
||||
|
||||
let endpoint =
|
||||
resolve_txt_endpoint(&resolver, "discovery.example", SocketContext::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(endpoint.as_str(), "tcp://127.0.0.1:11010");
|
||||
assert_eq!(
|
||||
*resolver.queries.lock().unwrap(),
|
||||
[DnsQuery::new("discovery.example", SocketContext::default())]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn srv_discovery_builds_protocol_specific_endpoint() {
|
||||
let resolver = TestResolver {
|
||||
txt: String::new(),
|
||||
srv: vec![DnsSrvRecord {
|
||||
priority: 1,
|
||||
weight: 10,
|
||||
port: 11012,
|
||||
target: "peer.example.com.".to_owned(),
|
||||
}],
|
||||
queries: Mutex::new(Vec::new()),
|
||||
};
|
||||
|
||||
let endpoint = resolve_srv_endpoint(
|
||||
&resolver,
|
||||
"discovery.example",
|
||||
&["quic".to_owned()],
|
||||
SocketContext::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(endpoint.as_str(), "quic://peer.example.com.:11012");
|
||||
assert_eq!(
|
||||
*resolver.queries.lock().unwrap(),
|
||||
[DnsQuery::new(
|
||||
"_easytier._quic.discovery.example",
|
||||
SocketContext::default()
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn srv_discovery_deduplicates_url_and_priority() {
|
||||
let endpoint: Url = "tcp://peer.example.com:11010".parse().unwrap();
|
||||
let candidates = deduplicate_srv_candidates(vec![
|
||||
(endpoint.clone(), 10),
|
||||
(endpoint.clone(), 10),
|
||||
(endpoint.clone(), 20),
|
||||
]);
|
||||
|
||||
assert_eq!(candidates.len(), 2);
|
||||
assert!(candidates.contains(&(endpoint.clone(), 10)));
|
||||
assert!(candidates.contains(&(endpoint, 20)));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
//! Portable connection orchestration.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use url::Url;
|
||||
|
||||
pub mod composite;
|
||||
pub mod direct;
|
||||
pub mod hole_punch;
|
||||
// Kept public: the host-driven adapter chain is WASI-only production code
|
||||
// (cfg(target_os = "wasi")), so crate-private visibility would surface
|
||||
// dead-code warnings on host builds for code that is live on WASI.
|
||||
pub mod connector_host;
|
||||
pub mod manual;
|
||||
pub mod protocol;
|
||||
pub mod stun;
|
||||
pub mod transport;
|
||||
|
||||
/// Supplies the URLs of the instance's currently running listeners.
|
||||
///
|
||||
/// The listener layer's running-listener registry implements this seam.
|
||||
/// Connectors use it to avoid dialing addresses that would hairpin back
|
||||
/// into one of their own listeners, so connectivity depends on this narrow
|
||||
/// query rather than on the listener module's concrete registry type.
|
||||
pub trait LocalListenerUrls: Debug + Send + Sync + 'static {
|
||||
fn local_listener_urls(&self) -> Vec<Url>;
|
||||
}
|
||||
|
||||
/// Empty [`LocalListenerUrls`] for connectors that track no listeners.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NoLocalListeners;
|
||||
|
||||
impl LocalListenerUrls for NoLocalListeners {
|
||||
fn local_listener_urls(&self) -> Vec<Url> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
@@ -1,811 +0,0 @@
|
||||
use std::{marker::PhantomData, num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
socket::{tcp::VirtualTcpSocket, udp::UdpSession},
|
||||
tunnel::Tunnel,
|
||||
};
|
||||
|
||||
use super::transport::{ConnectedTransport, UdpSessionMode};
|
||||
|
||||
pub mod raw;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ProtocolTransport {
|
||||
Tcp,
|
||||
FakeTcp,
|
||||
Udp(UdpSessionMode),
|
||||
}
|
||||
|
||||
pub(crate) const fn protocol_transport(scheme: &str) -> Option<ProtocolTransport> {
|
||||
match scheme.as_bytes() {
|
||||
b"tcp" | b"ws" | b"wss" => Some(ProtocolTransport::Tcp),
|
||||
b"faketcp" => Some(ProtocolTransport::FakeTcp),
|
||||
b"udp" => Some(ProtocolTransport::Udp(UdpSessionMode::EasyTierMux)),
|
||||
b"wg" => Some(ProtocolTransport::Udp(UdpSessionMode::Classified(
|
||||
crate::socket::udp::UdpSessionProtocol::WireGuard,
|
||||
))),
|
||||
b"quic" => Some(ProtocolTransport::Udp(UdpSessionMode::Classified(
|
||||
crate::socket::udp::UdpSessionProtocol::Quic,
|
||||
))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn protocol_uses_udp(scheme: &str) -> bool {
|
||||
matches!(protocol_transport(scheme), Some(ProtocolTransport::Udp(_)))
|
||||
}
|
||||
|
||||
/// Returns the listener-port offset used when expanding a single base port
|
||||
/// into EasyTier's protocol-specific listener set.
|
||||
pub const fn protocol_port_offset(scheme: &str) -> Option<u16> {
|
||||
match scheme.as_bytes() {
|
||||
b"tcp" | b"udp" => Some(0),
|
||||
b"wg" | b"ws" => Some(1),
|
||||
b"quic" | b"wss" => Some(2),
|
||||
b"faketcp" => Some(3),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default port for a concrete EasyTier IP protocol.
|
||||
pub const fn protocol_default_port(scheme: &str) -> Option<u16> {
|
||||
match scheme.as_bytes() {
|
||||
b"ws" => Some(80),
|
||||
b"wss" => Some(443),
|
||||
_ => match protocol_port_offset(scheme) {
|
||||
Some(offset) => Some(11010 + offset),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ClientProtocolUpgrader<TcpSocket>: Send + Sync + 'static {
|
||||
fn supports_scheme(&self, scheme: &str) -> bool;
|
||||
|
||||
fn connect_timeout(&self, _scheme: &str) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn upgrade_client(
|
||||
&self,
|
||||
connected: ConnectedTransport<TcpSocket>,
|
||||
requested_url: Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ServerTunnelAcceptor: Send + 'static {
|
||||
async fn accept(&mut self) -> anyhow::Result<Box<dyn Tunnel>>;
|
||||
}
|
||||
|
||||
pub enum ServerProtocolUpgrade {
|
||||
Tunnel(Box<dyn Tunnel>),
|
||||
Acceptor(Box<dyn ServerTunnelAcceptor>),
|
||||
}
|
||||
|
||||
pub struct ServerProtocolAdmission {
|
||||
active_session: OwnedSemaphorePermit,
|
||||
handshake_slots: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl ServerProtocolAdmission {
|
||||
pub fn into_parts(self) -> (OwnedSemaphorePermit, Arc<Semaphore>) {
|
||||
(self.active_session, self.handshake_slots)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerProtocolAdmissionController {
|
||||
active_sessions: Arc<Semaphore>,
|
||||
handshake_slots: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl ServerProtocolAdmissionController {
|
||||
pub fn new(max_active_sessions: usize, max_in_flight_handshakes: usize) -> Self {
|
||||
Self {
|
||||
active_sessions: Arc::new(Semaphore::new(max_active_sessions)),
|
||||
handshake_slots: Arc::new(Semaphore::new(max_in_flight_handshakes)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_admit(&self) -> Option<ServerProtocolAdmission> {
|
||||
Some(ServerProtocolAdmission {
|
||||
active_session: self.active_sessions.clone().try_acquire_owned().ok()?,
|
||||
handshake_slots: self.handshake_slots.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn quic() -> Self {
|
||||
Self::new(1024, 128)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ServerProtocolUpgrader<TcpSocket>: Send + Sync + 'static {
|
||||
fn supports_scheme(&self, scheme: &str) -> bool;
|
||||
|
||||
fn max_pending_tcp_upgrades(&self, _scheme: &str) -> Option<NonZeroUsize> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn upgrade_tcp(
|
||||
&self,
|
||||
socket: TcpSocket,
|
||||
local_url: Url,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade>;
|
||||
|
||||
async fn upgrade_udp(
|
||||
&self,
|
||||
session: UdpSession,
|
||||
local_url: Url,
|
||||
admission: Option<ServerProtocolAdmission>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade>;
|
||||
|
||||
async fn upgrade_byte_stream(
|
||||
&self,
|
||||
socket: TcpSocket,
|
||||
local_url: Url,
|
||||
remote_url: Option<Url>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct CoreClientProtocolConfig {
|
||||
pub unix: bool,
|
||||
pub faketcp: bool,
|
||||
}
|
||||
|
||||
/// Owns portable client protocol dispatch and delegates only protocol engines
|
||||
/// that are not yet available in core.
|
||||
pub struct CoreClientProtocolUpgrader<TcpSocket> {
|
||||
config: CoreClientProtocolConfig,
|
||||
external: Option<Arc<dyn ClientProtocolUpgrader<TcpSocket>>>,
|
||||
}
|
||||
|
||||
impl<TcpSocket> CoreClientProtocolUpgrader<TcpSocket> {
|
||||
pub fn new(config: CoreClientProtocolConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
external: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_external(
|
||||
config: CoreClientProtocolConfig,
|
||||
external: Arc<dyn ClientProtocolUpgrader<TcpSocket>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
external: Some(external),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<TcpSocket> ClientProtocolUpgrader<TcpSocket> for CoreClientProtocolUpgrader<TcpSocket>
|
||||
where
|
||||
TcpSocket: VirtualTcpSocket,
|
||||
{
|
||||
fn supports_scheme(&self, scheme: &str) -> bool {
|
||||
match scheme {
|
||||
"tcp" | "udp" | "ring" => true,
|
||||
"unix" => self.config.unix,
|
||||
"faketcp" => self.config.faketcp,
|
||||
_ => self
|
||||
.external
|
||||
.as_ref()
|
||||
.is_some_and(|external| external.supports_scheme(scheme)),
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_timeout(&self, scheme: &str) -> Option<Duration> {
|
||||
self.external
|
||||
.as_ref()
|
||||
.filter(|external| external.supports_scheme(scheme))
|
||||
.and_then(|external| external.connect_timeout(scheme))
|
||||
}
|
||||
|
||||
async fn upgrade_client(
|
||||
&self,
|
||||
connected: ConnectedTransport<TcpSocket>,
|
||||
requested_url: Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
match requested_url.scheme() {
|
||||
"tcp" => match connected {
|
||||
ConnectedTransport::Tcp(socket) => {
|
||||
Ok(raw::upgrade_connected_tcp(socket, requested_url)?)
|
||||
}
|
||||
ConnectedTransport::Udp(_) | ConnectedTransport::ByteStream(_) => {
|
||||
anyhow::bail!("TCP protocol requires a TCP transport")
|
||||
}
|
||||
},
|
||||
"udp" => match connected {
|
||||
ConnectedTransport::Udp(session) => {
|
||||
Ok(raw::upgrade_connected_udp(session, requested_url)?)
|
||||
}
|
||||
ConnectedTransport::Tcp(_) | ConnectedTransport::ByteStream(_) => {
|
||||
anyhow::bail!("UDP protocol requires a UDP session")
|
||||
}
|
||||
},
|
||||
"ring" => upgrade_byte_stream(connected),
|
||||
"unix" if self.config.unix => upgrade_byte_stream(connected),
|
||||
"faketcp" if self.config.faketcp => match connected {
|
||||
ConnectedTransport::Tcp(socket) => {
|
||||
Ok(raw::upgrade_connected_tcp(socket, requested_url)?)
|
||||
}
|
||||
ConnectedTransport::Udp(_) | ConnectedTransport::ByteStream(_) => {
|
||||
anyhow::bail!("FakeTCP protocol requires a TCP transport")
|
||||
}
|
||||
},
|
||||
"unix" | "faketcp" => anyhow::bail!(
|
||||
"unsupported client protocol upgrader: {}",
|
||||
requested_url.scheme()
|
||||
),
|
||||
scheme => {
|
||||
let Some(external) = &self.external else {
|
||||
anyhow::bail!("unsupported client protocol upgrader: {scheme}");
|
||||
};
|
||||
if !external.supports_scheme(scheme) {
|
||||
anyhow::bail!("unsupported client protocol upgrader: {scheme}");
|
||||
}
|
||||
external.upgrade_client(connected, requested_url).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_byte_stream<TcpSocket>(
|
||||
connected: ConnectedTransport<TcpSocket>,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>>
|
||||
where
|
||||
TcpSocket: VirtualTcpSocket,
|
||||
{
|
||||
match connected {
|
||||
ConnectedTransport::ByteStream(stream) => Ok(raw::upgrade_connected_byte_stream(stream)?),
|
||||
ConnectedTransport::Tcp(_) | ConnectedTransport::Udp(_) => {
|
||||
anyhow::bail!("external protocol requires a host-created byte stream")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct CoreServerProtocolConfig {
|
||||
pub unix: bool,
|
||||
pub faketcp: bool,
|
||||
}
|
||||
|
||||
/// Owns portable server protocol dispatch and delegates only protocol engines
|
||||
/// that are not available in core.
|
||||
pub struct CoreServerProtocolUpgrader<TcpSocket> {
|
||||
config: CoreServerProtocolConfig,
|
||||
external: Option<Arc<dyn ServerProtocolUpgrader<TcpSocket>>>,
|
||||
tcp_socket: PhantomData<fn() -> TcpSocket>,
|
||||
}
|
||||
|
||||
impl<TcpSocket: 'static> CoreServerProtocolUpgrader<TcpSocket> {
|
||||
pub fn new(config: CoreServerProtocolConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
external: None,
|
||||
tcp_socket: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_external(
|
||||
config: CoreServerProtocolConfig,
|
||||
external: Arc<dyn ServerProtocolUpgrader<TcpSocket>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
config,
|
||||
external: Some(external),
|
||||
tcp_socket: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_core_scheme(&self, scheme: &str) -> Option<bool> {
|
||||
match scheme {
|
||||
"tcp" | "udp" | "ring" => Some(true),
|
||||
"unix" => Some(self.config.unix),
|
||||
"faketcp" => Some(self.config.faketcp),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn external(&self, scheme: &str) -> anyhow::Result<&dyn ServerProtocolUpgrader<TcpSocket>> {
|
||||
let external = self
|
||||
.external
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("unsupported server protocol upgrader: {scheme}"))?;
|
||||
if !external.supports_scheme(scheme) {
|
||||
anyhow::bail!("unsupported server protocol upgrader: {scheme}");
|
||||
}
|
||||
Ok(external)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<TcpSocket> ServerProtocolUpgrader<TcpSocket> for CoreServerProtocolUpgrader<TcpSocket>
|
||||
where
|
||||
TcpSocket: VirtualTcpSocket,
|
||||
{
|
||||
fn supports_scheme(&self, scheme: &str) -> bool {
|
||||
self.supports_core_scheme(scheme).unwrap_or_else(|| {
|
||||
self.external
|
||||
.as_ref()
|
||||
.is_some_and(|external| external.supports_scheme(scheme))
|
||||
})
|
||||
}
|
||||
|
||||
fn max_pending_tcp_upgrades(&self, scheme: &str) -> Option<NonZeroUsize> {
|
||||
self.external
|
||||
.as_ref()
|
||||
.filter(|external| external.supports_scheme(scheme))
|
||||
.and_then(|external| external.max_pending_tcp_upgrades(scheme))
|
||||
}
|
||||
|
||||
async fn upgrade_tcp(
|
||||
&self,
|
||||
socket: TcpSocket,
|
||||
local_url: Url,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
match local_url.scheme() {
|
||||
"tcp" | "faketcp" => Ok(ServerProtocolUpgrade::Tunnel(
|
||||
upgrade_accepted_tcp(socket, local_url, self.config).await?,
|
||||
)),
|
||||
"udp" | "wg" | "quic" => {
|
||||
anyhow::bail!("{} protocol requires a UDP session", local_url.scheme())
|
||||
}
|
||||
"ring" | "unix" => {
|
||||
anyhow::bail!("{} protocol requires a byte stream", local_url.scheme())
|
||||
}
|
||||
scheme => self.external(scheme)?.upgrade_tcp(socket, local_url).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade_udp(
|
||||
&self,
|
||||
session: UdpSession,
|
||||
local_url: Url,
|
||||
admission: Option<ServerProtocolAdmission>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
match local_url.scheme() {
|
||||
"udp" => Ok(ServerProtocolUpgrade::Tunnel(upgrade_accepted_udp(
|
||||
session, &local_url,
|
||||
)?)),
|
||||
"tcp" | "faketcp" => {
|
||||
anyhow::bail!("{} protocol requires a TCP transport", local_url.scheme())
|
||||
}
|
||||
"ring" | "unix" => {
|
||||
anyhow::bail!("{} protocol requires a byte stream", local_url.scheme())
|
||||
}
|
||||
scheme => {
|
||||
self.external(scheme)?
|
||||
.upgrade_udp(session, local_url, admission)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade_byte_stream(
|
||||
&self,
|
||||
socket: TcpSocket,
|
||||
local_url: Url,
|
||||
remote_url: Option<Url>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
match local_url.scheme() {
|
||||
"ring" => Ok(ServerProtocolUpgrade::Tunnel(
|
||||
raw::upgrade_accepted_byte_stream(socket, local_url, remote_url)?,
|
||||
)),
|
||||
"unix" if self.config.unix => Ok(ServerProtocolUpgrade::Tunnel(
|
||||
raw::upgrade_accepted_byte_stream(socket, local_url, remote_url)?,
|
||||
)),
|
||||
"tcp" | "faketcp" => {
|
||||
anyhow::bail!("{} protocol requires a TCP transport", local_url.scheme())
|
||||
}
|
||||
"udp" | "wg" | "quic" => {
|
||||
anyhow::bail!("{} protocol requires a UDP session", local_url.scheme())
|
||||
}
|
||||
"unix" => anyhow::bail!("unsupported server protocol upgrader: unix"),
|
||||
scheme => {
|
||||
self.external(scheme)?
|
||||
.upgrade_byte_stream(socket, local_url, remote_url)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn upgrade_accepted_tcp<TcpSocket>(
|
||||
socket: TcpSocket,
|
||||
local_url: Url,
|
||||
config: CoreServerProtocolConfig,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>>
|
||||
where
|
||||
TcpSocket: VirtualTcpSocket,
|
||||
{
|
||||
match local_url.scheme() {
|
||||
"tcp" => Ok(raw::upgrade_accepted_tcp_with_local_url(socket, local_url)?),
|
||||
"faketcp" if config.faketcp => {
|
||||
Ok(raw::upgrade_accepted_tcp_with_local_url(socket, local_url)?)
|
||||
}
|
||||
scheme => anyhow::bail!("unsupported TCP listener protocol: {scheme}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_udp(
|
||||
session: UdpSession,
|
||||
local_url: &Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
match local_url.scheme() {
|
||||
"udp" => Ok(raw::upgrade_accepted_udp_with_local_url(
|
||||
session,
|
||||
local_url.clone(),
|
||||
)?),
|
||||
scheme => anyhow::bail!("unsupported UDP listener protocol: {scheme}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io,
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::socket::udp::{UdpSessionKind, VirtualUdpSocket};
|
||||
|
||||
#[test]
|
||||
fn protocol_port_metadata_is_authoritative_for_all_ip_protocols() {
|
||||
let cases = [
|
||||
("tcp", 0, 11010),
|
||||
("udp", 0, 11010),
|
||||
("wg", 1, 11011),
|
||||
("quic", 2, 11012),
|
||||
("ws", 1, 80),
|
||||
("wss", 2, 443),
|
||||
("faketcp", 3, 11013),
|
||||
];
|
||||
|
||||
for (scheme, offset, port) in cases {
|
||||
assert_eq!(protocol_port_offset(scheme), Some(offset));
|
||||
assert_eq!(protocol_default_port(scheme), Some(port));
|
||||
}
|
||||
assert_eq!(protocol_default_port("ring"), None);
|
||||
}
|
||||
|
||||
struct MockTcpSocket;
|
||||
|
||||
impl AsyncRead for MockTcpSocket {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
_buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for MockTcpSocket {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
_buf: &[u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), io::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualTcpSocket for MockTcpSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok("127.0.0.1:1000".parse().unwrap())
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok("127.0.0.1:2000".parse().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockUdpSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockUdpSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], _addr: SocketAddr) -> io::Result<usize> {
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
struct MockExternalUpgrader;
|
||||
|
||||
#[async_trait]
|
||||
impl ClientProtocolUpgrader<MockTcpSocket> for MockExternalUpgrader {
|
||||
fn supports_scheme(&self, _scheme: &str) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn upgrade_client(
|
||||
&self,
|
||||
_connected: ConnectedTransport<MockTcpSocket>,
|
||||
_requested_url: Url,
|
||||
) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
anyhow::bail!("external protocol invoked")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ServerProtocolUpgrader<MockTcpSocket> for MockExternalUpgrader {
|
||||
fn supports_scheme(&self, scheme: &str) -> bool {
|
||||
matches!(scheme, "external" | "ring" | "unix")
|
||||
}
|
||||
|
||||
async fn upgrade_tcp(
|
||||
&self,
|
||||
_socket: MockTcpSocket,
|
||||
_local_url: Url,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
anyhow::bail!("external server protocol invoked")
|
||||
}
|
||||
|
||||
async fn upgrade_udp(
|
||||
&self,
|
||||
_session: UdpSession,
|
||||
_local_url: Url,
|
||||
_admission: Option<ServerProtocolAdmission>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
anyhow::bail!("external server UDP protocol invoked")
|
||||
}
|
||||
|
||||
async fn upgrade_byte_stream(
|
||||
&self,
|
||||
_socket: MockTcpSocket,
|
||||
_local_url: Url,
|
||||
_remote_url: Option<Url>,
|
||||
) -> anyhow::Result<ServerProtocolUpgrade> {
|
||||
anyhow::bail!("external server byte-stream protocol invoked")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_core_upgrader_rejects_external_and_mismatched_protocols() {
|
||||
let upgrader =
|
||||
CoreClientProtocolUpgrader::<MockTcpSocket>::new(CoreClientProtocolConfig::default());
|
||||
|
||||
assert!(upgrader.supports_scheme("ring"));
|
||||
assert!(!upgrader.supports_scheme("unix"));
|
||||
|
||||
let unsupported = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::Tcp(MockTcpSocket),
|
||||
"ws://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert!(unsupported.is_err());
|
||||
|
||||
let mismatched = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::Tcp(MockTcpSocket),
|
||||
"udp://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await;
|
||||
assert!(mismatched.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_upgrader_owns_builtin_capabilities_and_delegates_external_protocols() {
|
||||
let upgrader = CoreClientProtocolUpgrader::with_external(
|
||||
CoreClientProtocolConfig {
|
||||
unix: false,
|
||||
faketcp: false,
|
||||
},
|
||||
Arc::new(MockExternalUpgrader),
|
||||
);
|
||||
|
||||
assert!(upgrader.supports_scheme("tcp"));
|
||||
assert!(upgrader.supports_scheme("ring"));
|
||||
assert!(upgrader.supports_scheme("ws"));
|
||||
assert!(!upgrader.supports_scheme("unix"));
|
||||
assert!(!upgrader.supports_scheme("faketcp"));
|
||||
|
||||
let external = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::Tcp(MockTcpSocket),
|
||||
"ws://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(external.to_string(), "external protocol invoked");
|
||||
|
||||
let disabled_builtin = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::Tcp(MockTcpSocket),
|
||||
"faketcp://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
disabled_builtin
|
||||
.to_string()
|
||||
.contains("unsupported client protocol upgrader")
|
||||
);
|
||||
|
||||
let mismatched_udp = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::Tcp(MockTcpSocket),
|
||||
"udp://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
mismatched_udp.to_string(),
|
||||
"UDP protocol requires a UDP session"
|
||||
);
|
||||
|
||||
let mismatched_tcp = upgrader
|
||||
.upgrade_client(
|
||||
ConnectedTransport::ByteStream(super::super::transport::ConnectedByteStream::new(
|
||||
MockTcpSocket,
|
||||
None,
|
||||
"ring://remote".parse().unwrap(),
|
||||
None,
|
||||
)),
|
||||
"tcp://127.0.0.1:2000".parse().unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
mismatched_tcp.to_string(),
|
||||
"TCP protocol requires a TCP transport"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_server_dispatches_raw_tcp_and_enforces_host_capabilities() {
|
||||
let tunnel = upgrade_accepted_tcp(
|
||||
MockTcpSocket,
|
||||
"tcp://0.0.0.0:2000".parse().unwrap(),
|
||||
CoreServerProtocolConfig::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tunnel.info().unwrap().local_addr.unwrap().url,
|
||||
"tcp://0.0.0.0:2000"
|
||||
);
|
||||
|
||||
let disabled = upgrade_accepted_tcp(
|
||||
MockTcpSocket,
|
||||
"ws://0.0.0.0:2000".parse().unwrap(),
|
||||
CoreServerProtocolConfig::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
disabled.to_string(),
|
||||
"unsupported TCP listener protocol: ws"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_server_raw_udp_preserves_explicit_listener_url() {
|
||||
let local_url: Url = "udp://listener.example:1000/path?bind_device=eth0"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let session = UdpSession::identity_standalone(
|
||||
Arc::new(MockUdpSocket {
|
||||
local_addr: "127.0.0.1:1000".parse().unwrap(),
|
||||
}),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
UdpSessionKind::EasyTierMux,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tunnel = upgrade_accepted_udp(session, &local_url).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tunnel.info().unwrap().local_addr.unwrap().url,
|
||||
local_url.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn core_server_upgrader_owns_builtin_dispatch_and_delegates_external_protocols() {
|
||||
let upgrader = CoreServerProtocolUpgrader::with_external(
|
||||
CoreServerProtocolConfig::default(),
|
||||
Arc::new(MockExternalUpgrader),
|
||||
);
|
||||
|
||||
assert!(upgrader.supports_scheme("tcp"));
|
||||
assert!(upgrader.supports_scheme("udp"));
|
||||
assert!(!upgrader.supports_scheme("ws"));
|
||||
assert!(!upgrader.supports_scheme("quic"));
|
||||
assert!(upgrader.supports_scheme("ring"));
|
||||
assert!(!upgrader.supports_scheme("unix"));
|
||||
assert!(upgrader.supports_scheme("external"));
|
||||
|
||||
let external = upgrader
|
||||
.upgrade_tcp(MockTcpSocket, "external://0.0.0.0:2000".parse().unwrap())
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(external.to_string(), "external server protocol invoked");
|
||||
|
||||
let mismatched = upgrader
|
||||
.upgrade_tcp(MockTcpSocket, "quic://0.0.0.0:2000".parse().unwrap())
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mismatched.to_string(),
|
||||
"quic protocol requires a UDP session"
|
||||
);
|
||||
|
||||
let wrong_ring_transport = upgrader
|
||||
.upgrade_tcp(MockTcpSocket, "ring://local".parse().unwrap())
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
wrong_ring_transport.to_string(),
|
||||
"ring protocol requires a byte stream"
|
||||
);
|
||||
|
||||
let disabled_unix = upgrader
|
||||
.upgrade_byte_stream(
|
||||
MockTcpSocket,
|
||||
"unix:///tmp/easytier.sock".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
disabled_unix.to_string(),
|
||||
"unsupported server protocol upgrader: unix"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_protocol_admission_is_scoped_to_its_controller() {
|
||||
let controller = ServerProtocolAdmissionController::new(1, 2);
|
||||
let admission = controller.try_admit().unwrap();
|
||||
assert!(controller.try_admit().is_none());
|
||||
|
||||
let (active_session, handshake_slots) = admission.into_parts();
|
||||
assert_eq!(handshake_slots.available_permits(), 2);
|
||||
drop(active_session);
|
||||
assert!(controller.try_admit().is_some());
|
||||
|
||||
let other = ServerProtocolAdmissionController::new(1, 1);
|
||||
assert!(other.try_admit().is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,867 +0,0 @@
|
||||
use std::{fmt, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rand::seq::SliceRandom as _;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
connectivity::{
|
||||
manual::resolve_url_addrs,
|
||||
transport::{self, ConnectedByteStream, ConnectedUdpSession, UdpSessionMode},
|
||||
},
|
||||
host::dns::DnsResolver,
|
||||
proto::common::TunnelInfo,
|
||||
socket::{
|
||||
IpVersion, ListenerConnectionCounter, SocketListener,
|
||||
tcp::{
|
||||
TcpBindOptions, TcpListenOptions, TcpSocketListener, TcpSocketPurpose,
|
||||
VirtualTcpListenerFactory, VirtualTcpSocket, VirtualTcpSocketFactory,
|
||||
},
|
||||
udp::{
|
||||
UdpBindOptions, UdpSession, UdpSessionAcceptKind, UdpSessionListenRequest,
|
||||
UdpSessionSocket, UdpSessionSocketListener, VirtualUdpSocketFactory,
|
||||
},
|
||||
},
|
||||
tunnel::{Tunnel, TunnelError, tcp::TcpTunnelUpgrader, udp::UdpTunnelUpgrader},
|
||||
};
|
||||
|
||||
use super::protocol_default_port;
|
||||
|
||||
const BYTE_STREAM_MAX_PACKET_SIZE: usize = 4096;
|
||||
const TCP_DEFAULT_PORT: u16 = protocol_default_port("tcp").expect("tcp must have a default port");
|
||||
const UDP_DEFAULT_PORT: u16 = protocol_default_port("udp").expect("udp must have a default port");
|
||||
|
||||
#[async_trait]
|
||||
#[auto_impl::auto_impl(Box, Arc)]
|
||||
pub trait TunnelDialer: Send + Sync + 'static {
|
||||
async fn connect(&self) -> anyhow::Result<Box<dyn Tunnel>>;
|
||||
|
||||
fn remote_url(&self) -> Url;
|
||||
}
|
||||
|
||||
/// Core-owned raw TCP Tunnel connector over an injected socket factory.
|
||||
pub struct TcpTunnelDialer<F>
|
||||
where
|
||||
F: VirtualTcpSocketFactory,
|
||||
{
|
||||
remote_url: Url,
|
||||
factory: Arc<F>,
|
||||
dns: Arc<dyn DnsResolver>,
|
||||
ip_version: IpVersion,
|
||||
bind: TcpBindOptions,
|
||||
}
|
||||
|
||||
impl<F> TcpTunnelDialer<F>
|
||||
where
|
||||
F: VirtualTcpSocketFactory,
|
||||
{
|
||||
pub fn new(remote_url: Url, factory: Arc<F>, dns: Arc<dyn DnsResolver>) -> Self {
|
||||
Self {
|
||||
remote_url,
|
||||
factory,
|
||||
dns,
|
||||
ip_version: IpVersion::Both,
|
||||
bind: TcpBindOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ip_version(mut self, ip_version: IpVersion) -> Self {
|
||||
self.ip_version = ip_version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bind(mut self, bind: TcpBindOptions) -> Self {
|
||||
self.bind = bind;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F> TunnelDialer for TcpTunnelDialer<F>
|
||||
where
|
||||
F: VirtualTcpSocketFactory,
|
||||
{
|
||||
async fn connect(&self) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
if self.remote_url.scheme() != "tcp" {
|
||||
anyhow::bail!("raw TCP dialer requires tcp URL: {}", self.remote_url);
|
||||
}
|
||||
let remote_addr = resolve_url_addrs(
|
||||
&self.remote_url,
|
||||
TCP_DEFAULT_PORT,
|
||||
self.bind.context.clone().with_ip_version(self.ip_version),
|
||||
self.dns.as_ref(),
|
||||
)
|
||||
.await?
|
||||
.choose(&mut rand::thread_rng())
|
||||
.copied()
|
||||
.ok_or(TunnelError::NoDnsRecordFound(self.ip_version))?;
|
||||
let socket = transport::connect_tcp(
|
||||
self.factory.clone(),
|
||||
remote_addr,
|
||||
Vec::new(),
|
||||
self.bind.clone(),
|
||||
TcpSocketPurpose::ManualConnect,
|
||||
)
|
||||
.await?;
|
||||
Ok(upgrade_connected_tcp(socket, self.remote_url.clone())?)
|
||||
}
|
||||
|
||||
fn remote_url(&self) -> Url {
|
||||
self.remote_url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Core-owned raw TCP Tunnel listener over an injected listener factory.
|
||||
pub struct TcpTunnelListener<F>
|
||||
where
|
||||
F: VirtualTcpListenerFactory,
|
||||
{
|
||||
inner: TcpSocketListener<F>,
|
||||
}
|
||||
|
||||
impl<F> TcpTunnelListener<F>
|
||||
where
|
||||
F: VirtualTcpListenerFactory,
|
||||
{
|
||||
pub fn new(local_addr: SocketAddr, factory: Arc<F>) -> Self {
|
||||
let bind = TcpBindOptions::default()
|
||||
.with_local_addr(Some(local_addr))
|
||||
.with_only_v6(true);
|
||||
Self::new_with_bind(local_addr, bind, factory)
|
||||
}
|
||||
|
||||
pub fn new_with_bind(local_addr: SocketAddr, bind: TcpBindOptions, factory: Arc<F>) -> Self {
|
||||
Self {
|
||||
inner: TcpSocketListener::new_with_options(
|
||||
socket_url("tcp", local_addr),
|
||||
TcpListenOptions::manual_connect(local_addr).with_bind(bind),
|
||||
factory,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> fmt::Debug for TcpTunnelListener<F>
|
||||
where
|
||||
F: VirtualTcpListenerFactory,
|
||||
{
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("TcpTunnelListener")
|
||||
.field("inner", &self.inner)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<F> SocketListener for TcpTunnelListener<F>
|
||||
where
|
||||
F: VirtualTcpListenerFactory,
|
||||
{
|
||||
type Accepted = Box<dyn Tunnel>;
|
||||
|
||||
async fn listen(&mut self) -> anyhow::Result<()> {
|
||||
self.inner.listen().await
|
||||
}
|
||||
|
||||
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
||||
let local_url = self.inner.local_url();
|
||||
let socket = self.inner.accept().await?;
|
||||
Ok(upgrade_accepted_tcp_with_local_url(socket, local_url)?)
|
||||
}
|
||||
|
||||
fn local_url(&self) -> Url {
|
||||
self.inner.local_url()
|
||||
}
|
||||
}
|
||||
|
||||
/// Core-owned raw UDP Tunnel connector over an injected socket factory.
|
||||
pub struct UdpTunnelDialer<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
remote_url: Url,
|
||||
host: Arc<H>,
|
||||
dns: Arc<dyn DnsResolver>,
|
||||
ip_version: IpVersion,
|
||||
bind_addrs: Vec<SocketAddr>,
|
||||
bind: UdpBindOptions,
|
||||
}
|
||||
|
||||
impl<H> UdpTunnelDialer<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
pub fn new(remote_url: Url, host: Arc<H>, dns: Arc<dyn DnsResolver>) -> Self {
|
||||
Self {
|
||||
remote_url,
|
||||
host,
|
||||
dns,
|
||||
ip_version: IpVersion::Both,
|
||||
bind_addrs: Vec::new(),
|
||||
bind: UdpBindOptions::direct_connect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ip_version(mut self, ip_version: IpVersion) -> Self {
|
||||
self.ip_version = ip_version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bind_addrs(mut self, bind_addrs: Vec<SocketAddr>) -> Self {
|
||||
self.bind_addrs = bind_addrs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bind(mut self, bind: UdpBindOptions) -> Self {
|
||||
self.bind = bind;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<H> TunnelDialer for UdpTunnelDialer<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
async fn connect(&self) -> anyhow::Result<Box<dyn Tunnel>> {
|
||||
if self.remote_url.scheme() != "udp" {
|
||||
anyhow::bail!("raw UDP dialer requires udp URL: {}", self.remote_url);
|
||||
}
|
||||
let remote_addr = resolve_url_addrs(
|
||||
&self.remote_url,
|
||||
UDP_DEFAULT_PORT,
|
||||
self.bind.context.clone().with_ip_version(self.ip_version),
|
||||
self.dns.as_ref(),
|
||||
)
|
||||
.await?
|
||||
.choose(&mut rand::thread_rng())
|
||||
.copied()
|
||||
.ok_or(TunnelError::NoDnsRecordFound(self.ip_version))?;
|
||||
let bind_addrs = udp_bind_addrs_for_remote(remote_addr, &self.bind_addrs);
|
||||
let connected = transport::connect_udp(
|
||||
self.host.clone(),
|
||||
remote_addr,
|
||||
bind_addrs,
|
||||
self.bind.clone(),
|
||||
UdpSessionMode::EasyTierMux,
|
||||
)
|
||||
.await?;
|
||||
Ok(upgrade_connected_udp(connected, self.remote_url.clone())?)
|
||||
}
|
||||
|
||||
fn remote_url(&self) -> Url {
|
||||
self.remote_url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Core-owned raw UDP Tunnel listener over an injected socket factory.
|
||||
pub struct UdpTunnelListener<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
inner: UdpSessionSocketListener<H>,
|
||||
}
|
||||
|
||||
impl<H> UdpTunnelListener<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
pub fn new(local_url: Url, local_addr: SocketAddr, host: Arc<H>) -> Self {
|
||||
Self {
|
||||
inner: UdpSessionSocketListener::new(local_url, local_addr, host),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_request(
|
||||
local_url: Url,
|
||||
request: UdpSessionListenRequest,
|
||||
host: Arc<H>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: UdpSessionSocketListener::new_with_request(
|
||||
local_url,
|
||||
request,
|
||||
UdpSessionAcceptKind::EasyTierMux,
|
||||
host,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> fmt::Debug for UdpTunnelListener<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("UdpTunnelListener")
|
||||
.field("inner", &self.inner)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<H> SocketListener for UdpTunnelListener<H>
|
||||
where
|
||||
H: VirtualUdpSocketFactory,
|
||||
{
|
||||
type Accepted = Box<dyn Tunnel>;
|
||||
|
||||
async fn listen(&mut self) -> anyhow::Result<()> {
|
||||
let local_url = self.inner.local_url();
|
||||
if local_url.scheme() != "udp" {
|
||||
anyhow::bail!("raw UDP listener requires udp URL: {local_url}");
|
||||
}
|
||||
self.inner.listen().await
|
||||
}
|
||||
|
||||
async fn accept(&mut self) -> anyhow::Result<Self::Accepted> {
|
||||
let local_url = self.inner.local_url();
|
||||
Ok(upgrade_accepted_udp_with_local_url(
|
||||
self.inner.accept().await?,
|
||||
local_url,
|
||||
)?)
|
||||
}
|
||||
|
||||
fn local_url(&self) -> Url {
|
||||
self.inner.local_url()
|
||||
}
|
||||
|
||||
fn connection_counter(&self) -> Arc<dyn ListenerConnectionCounter> {
|
||||
self.inner.connection_counter()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_connected_byte_stream<S>(
|
||||
connected: ConnectedByteStream<S>,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError>
|
||||
where
|
||||
S: VirtualTcpSocket,
|
||||
{
|
||||
let (socket, local_url, remote_url, resolved_remote_url) = connected.into_parts();
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: remote_url.scheme().to_owned(),
|
||||
local_addr: local_url.map(Into::into),
|
||||
remote_addr: Some(remote_url.clone().into()),
|
||||
resolved_remote_addr: Some(resolved_remote_url.unwrap_or(remote_url).into()),
|
||||
};
|
||||
TcpTunnelUpgrader::new(info)
|
||||
.with_max_packet_size(BYTE_STREAM_MAX_PACKET_SIZE)
|
||||
.upgrade(socket)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_connected_tcp<S>(
|
||||
socket: S,
|
||||
requested_remote_addr: Url,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError>
|
||||
where
|
||||
S: VirtualTcpSocket,
|
||||
{
|
||||
let local_addr = socket.local_addr()?;
|
||||
let resolved_remote_addr = socket.peer_addr()?;
|
||||
let scheme = requested_remote_addr.scheme().to_owned();
|
||||
let tunnel_type = tcp_tunnel_type(&socket, &scheme)?;
|
||||
let info = connected_tunnel_info(
|
||||
&scheme,
|
||||
&tunnel_type,
|
||||
local_addr,
|
||||
resolved_remote_addr,
|
||||
requested_remote_addr,
|
||||
);
|
||||
TcpTunnelUpgrader::new(info).upgrade(socket)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_connected_udp(
|
||||
connected: ConnectedUdpSession,
|
||||
requested_remote_addr: Url,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let (session, layer) = connected.into_parts();
|
||||
let info = connected_tunnel_info(
|
||||
"udp",
|
||||
"udp",
|
||||
session.local_addr()?,
|
||||
session.peer_addr()?,
|
||||
requested_remote_addr,
|
||||
);
|
||||
UdpTunnelUpgrader::with_keep_alive(info, layer).upgrade(session)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_tcp_with_local_url<S>(
|
||||
socket: S,
|
||||
local_url: Url,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError>
|
||||
where
|
||||
S: VirtualTcpSocket,
|
||||
{
|
||||
let remote_addr = socket.peer_addr()?;
|
||||
let scheme = local_url.scheme().to_owned();
|
||||
let remote_url = socket_url(&scheme, remote_addr);
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: tcp_tunnel_type(&socket, &scheme)?,
|
||||
local_addr: Some(local_url.into()),
|
||||
remote_addr: Some(remote_url.clone().into()),
|
||||
resolved_remote_addr: Some(remote_url.into()),
|
||||
};
|
||||
TcpTunnelUpgrader::new(info).upgrade(socket)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_byte_stream<S>(
|
||||
socket: S,
|
||||
local_url: Url,
|
||||
remote_url: Option<Url>,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError>
|
||||
where
|
||||
S: VirtualTcpSocket,
|
||||
{
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: local_url.scheme().to_owned(),
|
||||
local_addr: Some(local_url.into()),
|
||||
remote_addr: remote_url.clone().map(Into::into),
|
||||
resolved_remote_addr: remote_url.map(Into::into),
|
||||
};
|
||||
TcpTunnelUpgrader::new(info)
|
||||
.with_max_packet_size(BYTE_STREAM_MAX_PACKET_SIZE)
|
||||
.upgrade(socket)
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_udp_with_local_url(
|
||||
session: UdpSession,
|
||||
local_url: Url,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
if local_url.scheme() != "udp" {
|
||||
return Err(TunnelError::InvalidProtocol(format!(
|
||||
"raw UDP listener requires udp URL: {local_url}"
|
||||
)));
|
||||
}
|
||||
let remote_url = socket_url("udp", session.peer_addr()?);
|
||||
let info = TunnelInfo {
|
||||
tunnel_type: "udp".to_owned(),
|
||||
local_addr: Some(local_url.into()),
|
||||
remote_addr: Some(remote_url.clone().into()),
|
||||
resolved_remote_addr: Some(remote_url.into()),
|
||||
};
|
||||
UdpTunnelUpgrader::new(info).upgrade(session)
|
||||
}
|
||||
|
||||
fn udp_bind_addrs_for_remote(
|
||||
remote_addr: SocketAddr,
|
||||
configured: &[SocketAddr],
|
||||
) -> Vec<SocketAddr> {
|
||||
if remote_addr.is_ipv6() {
|
||||
Vec::new()
|
||||
} else {
|
||||
configured.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
fn connected_tunnel_info(
|
||||
scheme: &str,
|
||||
tunnel_type: &str,
|
||||
local_addr: SocketAddr,
|
||||
resolved_remote_addr: SocketAddr,
|
||||
requested_remote_addr: Url,
|
||||
) -> TunnelInfo {
|
||||
TunnelInfo {
|
||||
tunnel_type: tunnel_type.to_owned(),
|
||||
local_addr: Some(socket_url(scheme, local_addr).into()),
|
||||
remote_addr: Some(requested_remote_addr.into()),
|
||||
resolved_remote_addr: Some(socket_url(scheme, resolved_remote_addr).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tcp_tunnel_type(socket: &impl VirtualTcpSocket, scheme: &str) -> Result<String, TunnelError> {
|
||||
match socket.transport_label() {
|
||||
Some(label) => Ok(label.to_owned()),
|
||||
None if scheme == "faketcp" => Err(TunnelError::InternalError(
|
||||
"FakeTCP upgrader received a socket without a FakeTCP transport label".to_owned(),
|
||||
)),
|
||||
None => Ok(scheme.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_url(scheme: &str, addr: SocketAddr) -> Url {
|
||||
let mut url =
|
||||
Url::parse(&format!("{scheme}://0.0.0.0")).expect("static transport URL should be valid");
|
||||
url.set_ip_host(addr.ip())
|
||||
.expect("socket IP should be a valid URL host");
|
||||
url.set_port(Some(addr.port()))
|
||||
.expect("transport URL should accept a port");
|
||||
url
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf};
|
||||
|
||||
use crate::{
|
||||
packet::ZCPacket,
|
||||
socket::udp::{UdpSessionKind, VirtualUdpSocket},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn upgrade_accepted_tcp<S>(socket: S) -> Result<Box<dyn Tunnel>, TunnelError>
|
||||
where
|
||||
S: VirtualTcpSocket,
|
||||
{
|
||||
let local_addr = socket.local_addr()?;
|
||||
upgrade_accepted_tcp_with_local_url(socket, socket_url("tcp", local_addr))
|
||||
}
|
||||
|
||||
pub(crate) fn upgrade_accepted_udp(
|
||||
session: UdpSession,
|
||||
) -> Result<Box<dyn Tunnel>, TunnelError> {
|
||||
let local_url = socket_url("udp", session.local_addr()?);
|
||||
upgrade_accepted_udp_with_local_url(session, local_url)
|
||||
}
|
||||
|
||||
struct MockTcpSocket {
|
||||
stream: DuplexStream,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
transport_label: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl MockTcpSocket {
|
||||
fn new(local_addr: SocketAddr, peer_addr: SocketAddr) -> Self {
|
||||
let (stream, _) = tokio::io::duplex(64);
|
||||
Self::from_stream(stream, local_addr, peer_addr)
|
||||
}
|
||||
|
||||
fn from_stream(
|
||||
stream: DuplexStream,
|
||||
local_addr: SocketAddr,
|
||||
peer_addr: SocketAddr,
|
||||
) -> Self {
|
||||
Self {
|
||||
stream,
|
||||
local_addr,
|
||||
peer_addr,
|
||||
transport_label: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_transport_label(mut self, transport_label: &'static str) -> Self {
|
||||
self.transport_label = Some(transport_label);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for MockTcpSocket {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for MockTcpSocket {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
Pin::new(&mut self.stream).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.stream).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualTcpSocket for MockTcpSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.peer_addr)
|
||||
}
|
||||
|
||||
fn transport_label(&self) -> Option<&str> {
|
||||
self.transport_label
|
||||
}
|
||||
}
|
||||
|
||||
struct MockUdpSocket {
|
||||
local_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockUdpSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok(self.local_addr)
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], _addr: SocketAddr) -> io::Result<usize> {
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_upgrader_preserves_requested_and_resolved_addresses() {
|
||||
let local_addr: SocketAddr = "127.0.0.1:1000".parse().unwrap();
|
||||
let peer_addr: SocketAddr = "127.0.0.1:2000".parse().unwrap();
|
||||
let requested_url: Url = "tcp://example.com:2000".parse().unwrap();
|
||||
|
||||
let connected = upgrade_connected_tcp(
|
||||
MockTcpSocket::new(local_addr, peer_addr),
|
||||
requested_url.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let connected_info = connected.info().unwrap();
|
||||
assert_eq!(
|
||||
connected_info.remote_addr.unwrap().url,
|
||||
requested_url.as_str()
|
||||
);
|
||||
let resolved: Url = connected_info.resolved_remote_addr.unwrap().into();
|
||||
assert_eq!(resolved.host_str(), Some("127.0.0.1"));
|
||||
assert_eq!(resolved.port(), Some(2000));
|
||||
|
||||
let accepted = upgrade_accepted_tcp(MockTcpSocket::new(local_addr, peer_addr)).unwrap();
|
||||
let accepted_info = accepted.info().unwrap();
|
||||
assert_eq!(
|
||||
accepted_info.remote_addr,
|
||||
accepted_info.resolved_remote_addr
|
||||
);
|
||||
|
||||
let requested_local_url: Url = "tcp://0.0.0.0:1000".parse().unwrap();
|
||||
let accepted = upgrade_accepted_tcp_with_local_url(
|
||||
MockTcpSocket::new(local_addr, peer_addr),
|
||||
requested_local_url.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
accepted.info().unwrap().local_addr.unwrap().url,
|
||||
requested_local_url.as_str()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_tcp_upgrader_preserves_host_transport_label() {
|
||||
let local_addr: SocketAddr = "192.0.2.1:10000".parse().unwrap();
|
||||
let peer_addr: SocketAddr = "192.0.2.2:11013".parse().unwrap();
|
||||
let requested_url: Url = "faketcp://peer.example:11013".parse().unwrap();
|
||||
|
||||
let connected = upgrade_connected_tcp(
|
||||
MockTcpSocket::new(local_addr, peer_addr).with_transport_label("faketcp_test-driver"),
|
||||
requested_url.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let connected_info = connected.info().unwrap();
|
||||
assert_eq!(connected_info.tunnel_type, "faketcp_test-driver");
|
||||
assert_eq!(
|
||||
connected_info.resolved_remote_addr.unwrap().url,
|
||||
"faketcp://192.0.2.2:11013"
|
||||
);
|
||||
|
||||
let accepted = upgrade_accepted_tcp_with_local_url(
|
||||
MockTcpSocket::new(local_addr, peer_addr).with_transport_label("faketcp_test-driver"),
|
||||
"faketcp://0.0.0.0:11013".parse().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let accepted_info = accepted.info().unwrap();
|
||||
assert_eq!(accepted_info.tunnel_type, "faketcp_test-driver");
|
||||
assert_eq!(
|
||||
accepted_info.remote_addr,
|
||||
accepted_info.resolved_remote_addr
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn faketcp_upgrader_rejects_socket_without_host_transport_label() {
|
||||
let local_addr: SocketAddr = "192.0.2.1:10000".parse().unwrap();
|
||||
let peer_addr: SocketAddr = "192.0.2.2:11013".parse().unwrap();
|
||||
|
||||
let connected_error = upgrade_connected_tcp(
|
||||
MockTcpSocket::new(local_addr, peer_addr),
|
||||
"faketcp://peer.example:11013".parse().unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(connected_error, TunnelError::InternalError(_)));
|
||||
|
||||
let accepted_error = upgrade_accepted_tcp_with_local_url(
|
||||
MockTcpSocket::new(local_addr, peer_addr),
|
||||
"faketcp://0.0.0.0:11013".parse().unwrap(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(accepted_error, TunnelError::InternalError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_udp_uses_default_bind_for_ipv6_remote() {
|
||||
let configured = vec!["192.0.2.1:0".parse().unwrap()];
|
||||
|
||||
assert_eq!(
|
||||
udp_bind_addrs_for_remote("198.51.100.1:11010".parse().unwrap(), &configured),
|
||||
configured
|
||||
);
|
||||
assert!(
|
||||
udp_bind_addrs_for_remote("[2001:db8::1]:11010".parse().unwrap(), &configured)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_udp_uses_explicit_listener_url() {
|
||||
let local_addr = "127.0.0.1:1000".parse().unwrap();
|
||||
let peer_addr = "127.0.0.1:2000".parse().unwrap();
|
||||
let local_url: Url = "udp://listener.example:1000?bind_device=eth0"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let session = UdpSession::identity_standalone(
|
||||
Arc::new(MockUdpSocket { local_addr }),
|
||||
peer_addr,
|
||||
UdpSessionKind::EasyTierMux,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tunnel = upgrade_accepted_udp_with_local_url(session, local_url.clone()).unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
|
||||
assert_eq!(info.local_addr.unwrap().url, local_url.as_str());
|
||||
assert_eq!(info.remote_addr, info.resolved_remote_addr);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_udp_rejects_non_udp_listener_url() {
|
||||
let local_addr = "127.0.0.1:1000".parse().unwrap();
|
||||
let session = UdpSession::identity_standalone(
|
||||
Arc::new(MockUdpSocket { local_addr }),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
UdpSessionKind::EasyTierMux,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error =
|
||||
upgrade_accepted_udp_with_local_url(session, "quic://127.0.0.1:1000".parse().unwrap())
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, TunnelError::InvalidProtocol(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_stream_upgrader_uses_host_endpoint_metadata() {
|
||||
let local_url: Url = "ring://local".parse().unwrap();
|
||||
let remote_url: Url = "ring://remote".parse().unwrap();
|
||||
let tunnel = upgrade_connected_byte_stream(ConnectedByteStream::new(
|
||||
MockTcpSocket::new(
|
||||
"127.0.0.1:1000".parse().unwrap(),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
),
|
||||
Some(local_url.clone()),
|
||||
remote_url.clone(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
|
||||
assert_eq!(info.tunnel_type, "ring");
|
||||
assert_eq!(info.local_addr.unwrap().url, local_url.as_str());
|
||||
assert_eq!(info.remote_addr.unwrap().url, remote_url.as_str());
|
||||
assert_eq!(info.resolved_remote_addr.unwrap().url, remote_url.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepted_byte_stream_uses_explicit_endpoint_metadata() {
|
||||
let local_url: Url = "ring://local".parse().unwrap();
|
||||
let remote_url: Url = "ring://remote".parse().unwrap();
|
||||
let tunnel = upgrade_accepted_byte_stream(
|
||||
MockTcpSocket::new(
|
||||
"127.0.0.1:1000".parse().unwrap(),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
),
|
||||
local_url.clone(),
|
||||
Some(remote_url.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
|
||||
assert_eq!(info.tunnel_type, "ring");
|
||||
assert_eq!(info.local_addr.unwrap().url, local_url.as_str());
|
||||
assert_eq!(info.remote_addr.unwrap().url, remote_url.as_str());
|
||||
assert_eq!(info.resolved_remote_addr.unwrap().url, remote_url.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepted_byte_stream_allows_unnamed_remote_endpoint() {
|
||||
let tunnel = upgrade_accepted_byte_stream(
|
||||
MockTcpSocket::new(
|
||||
"127.0.0.1:1000".parse().unwrap(),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
),
|
||||
"unix:///tmp/easytier.sock".parse().unwrap(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let info = tunnel.info().unwrap();
|
||||
|
||||
assert_eq!(info.tunnel_type, "unix");
|
||||
assert!(info.remote_addr.is_none());
|
||||
assert!(info.resolved_remote_addr.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn byte_stream_upgrader_preserves_legacy_unix_packet_limit() {
|
||||
let (client_stream, server_stream) = tokio::io::duplex(8192);
|
||||
let client = upgrade_connected_byte_stream(ConnectedByteStream::new(
|
||||
MockTcpSocket::from_stream(
|
||||
client_stream,
|
||||
"127.0.0.1:1000".parse().unwrap(),
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
),
|
||||
None,
|
||||
"unix:///tmp/easytier.sock".parse().unwrap(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
let server = upgrade_accepted_byte_stream(
|
||||
MockTcpSocket::from_stream(
|
||||
server_stream,
|
||||
"127.0.0.1:2000".parse().unwrap(),
|
||||
"127.0.0.1:1000".parse().unwrap(),
|
||||
),
|
||||
"unix:///tmp/easytier.sock".parse().unwrap(),
|
||||
Some("unix://anonymous/peer".parse().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (mut client_stream, mut client_sink) = client.split();
|
||||
let (mut server_stream, mut server_sink) = server.split();
|
||||
let payload = vec![0x5a; 3000];
|
||||
client_sink
|
||||
.send(ZCPacket::new_with_payload(&payload))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let packet = server_stream.next().await.unwrap().unwrap();
|
||||
assert_eq!(packet.payload(), payload);
|
||||
|
||||
let response_payload = vec![0xa5; 3000];
|
||||
server_sink
|
||||
.send(ZCPacket::new_with_payload(&response_payload))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let packet = client_stream.next().await.unwrap().unwrap();
|
||||
assert_eq!(packet.payload(), response_payload);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user