mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-20 03:22:05 +00:00
Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f2f019f16 | ||
|
|
86bf3df0fd | ||
|
|
daf30bff6e | ||
|
|
b5bc4a853d | ||
|
|
144e306562 | ||
|
|
52bb26680a | ||
|
|
0b27ac2885 | ||
|
|
23d55373a4 | ||
|
|
d375d7e455 | ||
|
|
1e40350c89 | ||
|
|
e31bde1836 | ||
|
|
86222771c5 | ||
|
|
d114cdd20f | ||
|
|
df874b85be | ||
|
|
8d475dc3fc | ||
|
|
40c857748f | ||
|
|
afbba5d928 | ||
|
|
fc3914baf5 | ||
|
|
4923a13c5d | ||
|
|
d55e63b88e | ||
|
|
7b506e25a7 | ||
|
|
7fb42c3b73 | ||
|
|
dc11298558 | ||
|
|
021f523431 | ||
|
|
346f32d3d0 | ||
|
|
f24735a86f | ||
|
|
ba86e47cc6 | ||
|
|
d742fa34aa | ||
|
|
800c840bb5 | ||
|
|
ca17e856a6 | ||
|
|
d87624b353 | ||
|
|
a7ab60e0e4 | ||
|
|
3d0d2bed9e | ||
|
|
7ef023fcdd | ||
|
|
69d476ad13 | ||
|
|
cf3fcb75b5 | ||
|
|
17195a5cc4 | ||
|
|
22bcfebe3e | ||
|
|
47a2fb19a6 | ||
|
|
62e43855cb | ||
|
|
5efbf4853a | ||
|
|
3e0bde68b8 | ||
|
|
bda06ebb61 | ||
|
|
be0859aca6 | ||
|
|
b2f1b37336 | ||
|
|
583fb45939 | ||
|
|
a343a907a9 | ||
|
|
b1ac64f40f | ||
|
|
122fb9b54e | ||
|
|
09780c1698 | ||
|
|
e533b48244 | ||
|
|
c0bd050742 |
@@ -9,6 +9,14 @@ 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
|
||||
|
||||
@@ -50,6 +50,7 @@ runs:
|
||||
if: ${{ inputs.pnpm == 'true' }}
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ inputs.token }}
|
||||
build-filter: ${{ inputs.pnpm-build-filter }}
|
||||
|
||||
- name: Install GUI dependencies (Linux)
|
||||
|
||||
@@ -3,6 +3,9 @@ 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
|
||||
@@ -11,6 +14,22 @@ 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:
|
||||
@@ -46,3 +65,11 @@ runs:
|
||||
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/
|
||||
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/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", ".github/workflows/core.yml", ".github/actions/**", "easytier-web/**"]'
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre_job
|
||||
@@ -47,6 +47,7 @@ jobs:
|
||||
- name: Setup Frontend Environment
|
||||
uses: ./.github/actions/prepare-pnpm
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
build-filter: './easytier-web/*'
|
||||
|
||||
- name: Archive artifact
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
easytier-web/frontend/dist/*
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
fail-fast: false
|
||||
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-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/actions/**", "easytier-web/frontend-lib/**"]'
|
||||
build-gui:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -117,13 +117,92 @@ 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 != '' }}
|
||||
if: ${{ matrix.GUI_TARGET != '' && (!contains(matrix.GUI_TARGET, 'darwin') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)) }}
|
||||
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-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/actions/**"]'
|
||||
build-mobile:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
+157
-198
@@ -1,246 +1,205 @@
|
||||
name: EasyTier OHOS
|
||||
name: ohos
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop", "main", "releases/**"]
|
||||
branches: [develop, main, "releases/**", "ohos/**"]
|
||||
tags:
|
||||
- 'v*'
|
||||
- '!*-pre'
|
||||
- "v*"
|
||||
- "!*-pre"
|
||||
pull_request:
|
||||
branches: ["develop", "main"]
|
||||
branches: [develop, main, "ohos/**"]
|
||||
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
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# necessary for windows
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
cargo_fmt_check:
|
||||
ohos:
|
||||
name: ohos
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare build environment
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Rust
|
||||
uses: ./.github/actions/prepare-build
|
||||
with:
|
||||
target: aarch64-unknown-linux-ohos
|
||||
gui: false
|
||||
pnpm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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
|
||||
- name: Set up HarmonyOS
|
||||
uses: ErBWs/setup-ohos@v1
|
||||
|
||||
- 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
|
||||
- name: Install ohrs
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: ohrs
|
||||
|
||||
- name: Create clang wrapper script
|
||||
- name: Build HAR
|
||||
id: package
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
sudo mkdir -p $OHOS_NDK_HOME/native/llvm
|
||||
sudo tee $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh > /dev/null <<'EOF'
|
||||
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'
|
||||
#!/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"
|
||||
|
||||
- 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
|
||||
cd easytier-contrib/easytier-ohrs
|
||||
source env.sh
|
||||
ohrs doctor
|
||||
ohrs build --release --arch aarch
|
||||
ohrs artifact
|
||||
mv package.har easytier-ohrs.har
|
||||
mv package.har "$package_name.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
|
||||
- name: Upload HAR
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: easytier-ohos
|
||||
path: |
|
||||
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
|
||||
name: ${{ steps.package.outputs.name }}
|
||||
path: easytier-contrib/easytier-ohrs/${{ steps.package.outputs.name }}.har
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish To Center Ohpm
|
||||
working-directory: ./easytier-contrib/easytier-ohrs
|
||||
- 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
|
||||
env:
|
||||
OHPM_PRIVATE_KEY: ${{ secrets.OHPM_PRIVATE_KEY }}
|
||||
OHPM_KEY_PASSPHRASE: ${{ secrets.OHPM_KEY_PASSPHRASE }}
|
||||
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CODEARTS_PRIVATE_OHPM: ${{ secrets.CODEARTS_PRIVATE_OHPM }}
|
||||
DOWNSTREAM_DISPATCH_TOKEN: ${{ secrets.DOWNSTREAM_DISPATCH_TOKEN }}
|
||||
run: |
|
||||
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
|
||||
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
|
||||
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/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-core/**", "easytier-proto/**", "easytier-web/**", "easytier-gui/src-tauri/**", "easytier-contrib/**", ".github/workflows/test.yml", ".github/actions/**"]'
|
||||
|
||||
check:
|
||||
name: Run linters & check
|
||||
@@ -98,7 +98,9 @@ jobs:
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Archive test
|
||||
run: cargo nextest archive --archive-file tests.tar.zst --package easytier --features full
|
||||
run: >-
|
||||
cargo nextest archive --archive-file tests.tar.zst
|
||||
--package easytier --package easytier-core --features full
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
@@ -136,6 +138,11 @@ 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
|
||||
@@ -151,7 +158,8 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo prlimit --pid $$ --nofile=1048576:1048576
|
||||
sudo -E env "PATH=$PATH" cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
sudo -E env "PATH=$PATH" EASYTIER_LINUX_BPF_INTEGRATION=required \
|
||||
cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -35,6 +35,8 @@ 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
@@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
+12
-1
@@ -113,6 +113,17 @@ 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
|
||||
@@ -222,4 +233,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
+135
-244
@@ -486,6 +486,16 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-write-file"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeb1e2c1d58618bea806ccca5bbe65dc4e868be16f69ff118a39049389687548"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.13"
|
||||
@@ -1943,17 +1953,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defguard_wireguard_rs"
|
||||
version = "0.4.2"
|
||||
@@ -1965,7 +1964,7 @@ dependencies = [
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-generic",
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-packet-route",
|
||||
"netlink-packet-utils",
|
||||
"netlink-packet-wireguard",
|
||||
"netlink-sys",
|
||||
@@ -2012,17 +2011,6 @@ dependencies = [
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
|
||||
[[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.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -2044,17 +2032,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[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.1"
|
||||
@@ -2316,19 +2293,15 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
name = "easytier"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"ariadne",
|
||||
"async-recursion",
|
||||
"async-ringbuf",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"atomic-write-file",
|
||||
"atomic_refcell",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.8.0",
|
||||
"bon",
|
||||
"boringtun-easytier",
|
||||
"bytecodec",
|
||||
@@ -2345,12 +2318,11 @@ dependencies = [
|
||||
"crossbeam",
|
||||
"ctor 0.8.0",
|
||||
"dashmap",
|
||||
"dbus",
|
||||
"defguard_wireguard_rs",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more 2.1.1",
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"encoding",
|
||||
"flume 0.12.0",
|
||||
"forwarded-header-value",
|
||||
@@ -2364,57 +2336,35 @@ dependencies = [
|
||||
"hickory-proto",
|
||||
"hickory-resolver",
|
||||
"hickory-server",
|
||||
"hmac",
|
||||
"http",
|
||||
"http_req",
|
||||
"humansize",
|
||||
"humantime-serde",
|
||||
"idna 1.0.3",
|
||||
"igd-next",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kcp-sys",
|
||||
"log",
|
||||
"machine-uid",
|
||||
"maplit",
|
||||
"mimalloc",
|
||||
"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",
|
||||
"openssl",
|
||||
"ordered_hash_map",
|
||||
"parking_lot",
|
||||
"paste",
|
||||
"pbjson",
|
||||
"pbjson-build",
|
||||
"percent-encoding",
|
||||
"petgraph",
|
||||
"pin-project-lite",
|
||||
"pnet",
|
||||
"prefix-trie",
|
||||
"proc-macro2",
|
||||
"pnet_datalink",
|
||||
"pnet_packet",
|
||||
"prost 0.14.3",
|
||||
"prost-build",
|
||||
"prost-reflect",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt-types",
|
||||
"quanta",
|
||||
"quinn",
|
||||
"quinn-proto",
|
||||
"quote",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"reqwest 0.12.12",
|
||||
"resolv-conf",
|
||||
"ring",
|
||||
"ringbuf",
|
||||
"rstest",
|
||||
"rust-i18n",
|
||||
"rustls",
|
||||
@@ -2423,10 +2373,8 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serial_test",
|
||||
"service-manager",
|
||||
"sha2",
|
||||
"shellexpand",
|
||||
"smoltcp",
|
||||
"snow",
|
||||
"socket2 0.5.10",
|
||||
"strum 0.27.2",
|
||||
"stun_codec",
|
||||
@@ -2439,12 +2387,9 @@ dependencies = [
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemalloc-sys",
|
||||
"tikv-jemallocator",
|
||||
"time",
|
||||
"timedmap",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-socks",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tokio-websockets",
|
||||
"toml 0.8.19",
|
||||
@@ -2454,9 +2399,6 @@ dependencies = [
|
||||
"unicode-width 0.1.11",
|
||||
"url",
|
||||
"uuid",
|
||||
"version-compare",
|
||||
"which 7.0.3",
|
||||
"wildmatch",
|
||||
"winapi",
|
||||
"windivert",
|
||||
"windows 0.62.2",
|
||||
@@ -2464,8 +2406,6 @@ dependencies = [
|
||||
"winreg 0.52.0",
|
||||
"x25519-dalek",
|
||||
"zerocopy 0.7.35",
|
||||
"zip",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2482,21 +2422,87 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-core"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"ariadne",
|
||||
"async-ringbuf",
|
||||
"async-trait",
|
||||
"atomic-shim",
|
||||
"auto_impl",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.8.0",
|
||||
"bytecodec",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"cidr",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"derive_builder",
|
||||
"easytier-proto",
|
||||
"futures",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.3.2",
|
||||
"guarden 0.2.0",
|
||||
"hmac",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"idna 1.0.3",
|
||||
"openssl",
|
||||
"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 0.27.2",
|
||||
"stun_codec",
|
||||
"thiserror 1.0.63",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"toml 0.8.19",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"webpki-roots 0.26.3",
|
||||
"wildmatch",
|
||||
"x25519-dalek",
|
||||
"zerocopy 0.7.35",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-ffi"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"easytier-core",
|
||||
"log",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thunk-rs",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2510,6 +2516,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"easytier",
|
||||
"easytier-core",
|
||||
"gethostname 1.1.0",
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -2533,6 +2540,48 @@ dependencies = [
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-mini"
|
||||
version = "2.6.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"easytier",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[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 0.12.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 1.0.63",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"x25519-dalek",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "easytier-uptime"
|
||||
version = "0.1.0"
|
||||
@@ -2588,6 +2637,7 @@ dependencies = [
|
||||
"clap",
|
||||
"dashmap",
|
||||
"easytier",
|
||||
"easytier-core",
|
||||
"image 0.24.9",
|
||||
"imageproc",
|
||||
"maxminddb",
|
||||
@@ -2836,12 +2886,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.2"
|
||||
@@ -4011,22 +4055,6 @@ 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.3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.9.4"
|
||||
@@ -4834,16 +4862,6 @@ version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
@@ -5272,9 +5290,6 @@ name = "multimap"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra"
|
||||
@@ -5360,7 +5375,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"libc",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-packet-route",
|
||||
"netlink-sys",
|
||||
"once_cell",
|
||||
"system-configuration",
|
||||
@@ -5404,21 +5419,6 @@ 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.8.0",
|
||||
"byteorder",
|
||||
"libc",
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-utils"
|
||||
version = "0.5.2"
|
||||
@@ -5707,15 +5707,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "5.0.0"
|
||||
@@ -6032,9 +6023,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.5.2+3.5.2"
|
||||
version = "300.6.1+3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4"
|
||||
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -6607,20 +6598,6 @@ dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -6628,7 +6605,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
|
||||
dependencies = [
|
||||
"no-std-net",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6641,7 +6617,6 @@ dependencies = [
|
||||
"libc",
|
||||
"pnet_base",
|
||||
"pnet_sys",
|
||||
"serde",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
@@ -6688,18 +6663,6 @@ 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 = "png"
|
||||
version = "0.17.16"
|
||||
@@ -7013,41 +6976,6 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-reflect"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"prost 0.14.3",
|
||||
"prost-reflect-derive",
|
||||
"prost-types 0.14.3",
|
||||
"serde",
|
||||
"serde-value",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[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.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.5"
|
||||
@@ -8658,7 +8586,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which 4.4.2",
|
||||
"which",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
@@ -9944,9 +9872,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
@@ -9969,12 +9895,6 @@ 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.7.6"
|
||||
@@ -10501,7 +10421,6 @@ dependencies = [
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"time",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
@@ -11233,16 +11152,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[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 = "0.26.11"
|
||||
@@ -11333,18 +11242,6 @@ dependencies = [
|
||||
"rustix 0.38.34",
|
||||
]
|
||||
|
||||
[[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.0.7",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -12128,12 +12025,6 @@ dependencies = [
|
||||
"windows-sys 0.59.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.0"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
[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",
|
||||
@@ -27,3 +30,8 @@ lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
|
||||
[profile.mini]
|
||||
inherits = "release"
|
||||
opt-level = "z"
|
||||
strip = "symbols"
|
||||
|
||||
@@ -0,0 +1,529 @@
|
||||
# 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
@@ -0,0 +1,65 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,176 @@
|
||||
# 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, features = ["ffi-dataplane"] }
|
||||
easytier-ffi = { path = "../easytier-ffi", default-features = false }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
global:
|
||||
Java_com_easytier_jni_EasyTierJNI_*;
|
||||
Java_com_easytier_jni_EasyTierDataPlaneJNI_*;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
|
||||
-451
@@ -1,451 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,673 +0,0 @@
|
||||
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,13 +22,8 @@
|
||||
//! 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;
|
||||
@@ -36,8 +31,8 @@ mod network_api;
|
||||
mod strings;
|
||||
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JByteArray, JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jlong, jobject, jstring};
|
||||
use jni::objects::{JClass, JObject, JObjectArray, JString};
|
||||
use jni::sys::{jboolean, jint, jstring};
|
||||
|
||||
/// Attach a TUN file descriptor to an EasyTier network instance.
|
||||
///
|
||||
@@ -256,522 +251,3 @@ 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,20 +9,26 @@ crate-type = ["cdylib", "rlib"]
|
||||
[features]
|
||||
default = ["c-abi", "ffi-dataplane"]
|
||||
c-abi = []
|
||||
ffi-dataplane = ["easytier/ffi-dataplane"]
|
||||
ffi-dataplane = [
|
||||
"easytier/ffi-dataplane",
|
||||
"easytier-core/proxy-smoltcp-stack",
|
||||
]
|
||||
macos-ne = ["easytier/macos-ne"]
|
||||
|
||||
[dependencies]
|
||||
easytier = { path = "../../easytier" }
|
||||
easytier = { path = "../../easytier", features = ["tracing-log"] }
|
||||
easytier-core = { path = "../../easytier-core" }
|
||||
|
||||
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"
|
||||
tokio-util = "0.7"
|
||||
|
||||
[build-dependencies]
|
||||
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = [
|
||||
"win7",
|
||||
] }
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# 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`
|
||||
@@ -0,0 +1,8 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,593 +0,0 @@
|
||||
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
@@ -1,360 +0,0 @@
|
||||
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():
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module easytierffi-example
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/go-webgpu/goffi v0.4.1
|
||||
@@ -13,18 +13,14 @@ use easytier::{
|
||||
MachineIdOptions,
|
||||
config::{ConfigLoader as _, TomlConfigLoader},
|
||||
},
|
||||
tunnel::TunnelScheme,
|
||||
web_client::{WebClient, WebClientHooks, run_web_client},
|
||||
web_client::{WebClient, WebClientHooks, parse_config_server_endpoint, run_web_client},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
data_plane::remove_data_plane_handles_by_instance_ids,
|
||||
data_plane::remove_data_plane_sessions_by_instance_ids,
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
ASYNC_RUNTIME, INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
types::ConfigServerEventCallback,
|
||||
};
|
||||
@@ -76,37 +72,9 @@ pub fn validate_config_server_client_options(
|
||||
return Err("machine_id is empty".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(())
|
||||
parse_config_server_endpoint(config_server_url_s)
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
struct ManagedConfigServerClient {
|
||||
@@ -150,7 +118,8 @@ impl ManagedConfigServerClientHooks {
|
||||
}
|
||||
|
||||
fn validate_instance_name(&self, inst_name: &str, inst_id: Uuid) -> Result<(), String> {
|
||||
if let Some(existing_id) = INSTANCE_NAME_ID_MAP.get(inst_name).map(|id| *id)
|
||||
if let Some(existing_id) =
|
||||
resolve_instance_id_by_name(inst_name).map_err(|error| error.to_string())?
|
||||
&& existing_id != inst_id
|
||||
{
|
||||
return Err(format!("instance name {} already exists", inst_name));
|
||||
@@ -159,13 +128,6 @@ 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
|
||||
@@ -199,11 +161,15 @@ impl ManagedConfigServerClientHooks {
|
||||
let Some(callback) = self.callback else {
|
||||
return Ok(());
|
||||
};
|
||||
let instance_name = INSTANCE_MANAGER
|
||||
.get_instance_name(&instance_id)
|
||||
let instance_name = ffi_context()
|
||||
.manager
|
||||
.instance(instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
.unwrap_or_default();
|
||||
let network_name = INSTANCE_MANAGER
|
||||
.get_network_name(&instance_id)
|
||||
let network_name = ffi_context()
|
||||
.manager
|
||||
.config(instance_id)
|
||||
.map(|config| config.get_network_identity().network_name)
|
||||
.unwrap_or_default();
|
||||
let event_json = serde_json::json!({
|
||||
"event": event,
|
||||
@@ -263,75 +229,27 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
.callback_delivery
|
||||
.lock()
|
||||
.map_err(|err| err.to_string())?;
|
||||
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(());
|
||||
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 _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
|
||||
));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
remove_data_plane_handles_by_instance_ids(&[*id]);
|
||||
remove_data_plane_sessions_by_instance_ids(&[*id]);
|
||||
|
||||
if let Err(err) = self.emit_event_with_delivery_locked("run_network_instance", *id) {
|
||||
self.note_callback_error(err);
|
||||
@@ -340,15 +258,8 @@ impl WebClientHooks for ManagedConfigServerClientHooks {
|
||||
}
|
||||
|
||||
async fn post_remove_network_instances(&self, ids: &[Uuid]) -> Result<(), String> {
|
||||
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
|
||||
};
|
||||
let removed_ids = self.remove_tracked_instance_ids(ids)?;
|
||||
remove_data_plane_sessions_by_instance_ids(&removed_ids);
|
||||
|
||||
for id in removed_ids {
|
||||
if let Err(err) = self.emit_event("delete_network_instance", id) {
|
||||
@@ -485,12 +396,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 ASYNC_RUNTIME.block_on(run_web_client(
|
||||
let client = match ffi_context().runtime.block_on(run_web_client(
|
||||
&config_server_url,
|
||||
config_server_machine_id_options(machine_id),
|
||||
hostname,
|
||||
secure_mode,
|
||||
INSTANCE_MANAGER.clone(),
|
||||
ffi_context().manager.clone(),
|
||||
Some(hooks.clone()),
|
||||
)) {
|
||||
Ok(client) => client,
|
||||
@@ -511,7 +422,7 @@ pub(crate) fn stop_config_server_client() -> c_int {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
let guard = match CONFIG_SERVER_CLIENT.lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(err) => {
|
||||
set_error_msg(&format!("failed to lock config server client: {}", err));
|
||||
@@ -528,29 +439,25 @@ pub(crate) fn stop_config_server_client() -> c_int {
|
||||
return -1;
|
||||
}
|
||||
let hooks = managed.hooks.clone();
|
||||
let managed = guard.take().expect("config server client exists");
|
||||
// Keep the client discoverable until the canonical transaction drains its
|
||||
// tracking. Earlier removals must still retire IDs from these same hooks.
|
||||
drop(guard);
|
||||
|
||||
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,
|
||||
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(),
|
||||
Err(err) => {
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
set_error_msg(&format!("failed to lock instance mutation: {}", err));
|
||||
set_error_msg(&format!("failed to lock config server client: {err}"));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
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);
|
||||
drop(managed);
|
||||
hooks.wait_for_callback_delivery();
|
||||
CONFIG_SERVER_CLIENT_ACTIVE.store(false, Ordering::Release);
|
||||
CONFIG_SERVER_CLIENT_STOPPING.store(false, Ordering::Release);
|
||||
|
||||
@@ -1,928 +0,0 @@
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! 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]) {}
|
||||
@@ -0,0 +1,646 @@
|
||||
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,21 +1,43 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
|
||||
use easytier::common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader};
|
||||
#[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 crate::{
|
||||
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,
|
||||
config_server::{in_config_server_callback, wait_for_config_server_delivery},
|
||||
error::set_error_msg,
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_MUTATION_LOCK, INSTANCE_NAME_ID_MAP, instance_name_exists,
|
||||
lock_remote_instance_mutation,
|
||||
},
|
||||
state::{ffi_context, resolve_instance_id_by_name},
|
||||
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 {
|
||||
@@ -25,19 +47,35 @@ pub(crate) unsafe fn set_tun_fd(inst_name: *const c_char, fd: c_int) -> c_int {
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
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);
|
||||
return -1;
|
||||
}
|
||||
|
||||
let inst_id = *INSTANCE_NAME_ID_MAP
|
||||
.get(&inst_name)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.value();
|
||||
|
||||
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
|
||||
match ffi_context().manager.attach_tun_fd(inst_id, fd) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to set tun fd: {}", e));
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,34 +119,16 @@ 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();
|
||||
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");
|
||||
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));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -152,50 +172,24 @@ pub(crate) unsafe fn retain_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));
|
||||
let retained_names = if length == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
let Some(inst_names) = (unsafe { parse_instance_names(inst_names, length) }) else {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
inst_names
|
||||
};
|
||||
|
||||
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));
|
||||
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}"));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -211,15 +205,6 @@ 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;
|
||||
}
|
||||
@@ -228,22 +213,15 @@ pub(crate) unsafe fn delete_network_instance(
|
||||
return -1;
|
||||
};
|
||||
|
||||
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));
|
||||
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}"));
|
||||
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
|
||||
}
|
||||
|
||||
@@ -267,7 +245,7 @@ pub(crate) unsafe fn collect_network_infos(
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
|
||||
let collected_infos = match ffi_context().manager.collect_network_infos_sync() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
@@ -280,7 +258,11 @@ pub(crate) unsafe fn collect_network_infos(
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
|
||||
let Some(key) = ffi_context()
|
||||
.manager
|
||||
.instance(*instance_id)
|
||||
.map(|instance| instance.instance_name().to_owned())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
@@ -320,13 +302,15 @@ 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 = INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
let mut instances = ffi_context()
|
||||
.manager
|
||||
.instance_ids()
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
INSTANCE_MANAGER
|
||||
.get_instance_name(&id)
|
||||
.map(|name| (name, id))
|
||||
ffi_context()
|
||||
.manager
|
||||
.instance(id)
|
||||
.map(|instance| (instance.instance_name().to_owned(), id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
instances.sort_by(|(left_name, left_id), (right_name, right_id)| {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::ffi::{CString, c_char, c_int};
|
||||
use std::{
|
||||
ffi::{CString, c_char, c_int},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config_server::in_config_server_callback,
|
||||
error::set_error_msg,
|
||||
state::{ASYNC_RUNTIME, INSTANCE_MANAGER},
|
||||
state::ffi_context,
|
||||
strings::{c_str_to_string, optional_c_str_to_string},
|
||||
};
|
||||
|
||||
@@ -65,19 +68,23 @@ pub(crate) unsafe fn call_json_rpc(
|
||||
}
|
||||
};
|
||||
|
||||
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 =
|
||||
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_json = match serde_json::to_string(&response) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
|
||||
@@ -20,19 +20,12 @@
|
||||
//! - `is_config_server_client_connected`: report whether the client is connected.
|
||||
//!
|
||||
//! Data plane APIs, enabled by the `ffi-dataplane` feature:
|
||||
//! - `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.
|
||||
//! - `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.
|
||||
//!
|
||||
//! Shared FFI helper APIs:
|
||||
//! - `get_error_msg`: copy the last FFI or config-server callback error message.
|
||||
@@ -40,8 +33,6 @@
|
||||
|
||||
mod config_server;
|
||||
mod data_plane;
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
mod data_plane_async;
|
||||
mod error;
|
||||
mod instance_api;
|
||||
mod json_rpc;
|
||||
@@ -53,11 +44,11 @@ mod types;
|
||||
mod tests;
|
||||
|
||||
pub use config_server::{in_config_server_callback, validate_config_server_client_options};
|
||||
pub use types::{ConfigServerEventCallback, KeyValuePair};
|
||||
pub use types::{
|
||||
ConfigServerEventCallback, DataPlaneCompletion, DataPlaneSocketAddr, KeyValuePair,
|
||||
};
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
use std::ffi::{c_uchar, c_ushort};
|
||||
|
||||
// ===== Network Management API =====
|
||||
|
||||
@@ -254,7 +245,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 `NetworkInstanceManager`. Successful remote run
|
||||
/// changes through the shared `NativeInstanceManager`. 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.
|
||||
@@ -319,634 +310,27 @@ 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")]
|
||||
#[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,
|
||||
)
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
||||
// ===== Shared FFI Helper API =====
|
||||
|
||||
/// Return the last FFI error message.
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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,54 +1,66 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::instance::factory::{
|
||||
NativeInstanceManager, NativeProcessManagement, native_instance_manager_with_runtime,
|
||||
native_process_management,
|
||||
};
|
||||
use tokio::runtime::{Builder, Runtime};
|
||||
use uuid::Uuid;
|
||||
|
||||
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()
|
||||
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()
|
||||
.enable_all()
|
||||
.build()
|
||||
.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;
|
||||
.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,
|
||||
}
|
||||
}
|
||||
|
||||
INSTANCE_NAME_ID_MAP.retain(|_, instance_id| !ids.contains(instance_id));
|
||||
}
|
||||
|
||||
pub(crate) fn lock_remote_instance_mutation() -> tokio::sync::OwnedMutexGuard<()> {
|
||||
INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned()
|
||||
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 instance_name_exists(inst_name: &str) -> bool {
|
||||
find_instance_id_by_name(inst_name).is_some()
|
||||
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 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)
|
||||
})
|
||||
})
|
||||
#[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()
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ use crate::{
|
||||
config_server::{
|
||||
ConfigServerCallbackScope, ManagedConfigServerClientHooks, set_active_for_test,
|
||||
},
|
||||
state::{
|
||||
INSTANCE_MANAGER, INSTANCE_NAME_ID_MAP, find_instance_id_by_name,
|
||||
lock_remote_instance_mutation, remove_instance_name_ids,
|
||||
},
|
||||
state::{ffi_context, find_instance_id_by_name},
|
||||
*,
|
||||
};
|
||||
use easytier::{
|
||||
@@ -15,7 +12,7 @@ use easytier::{
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::{CStr, CString, c_char, c_void},
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
sync::{Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -101,10 +98,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());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(instance_name.clone(), instance_id);
|
||||
|
||||
let mut infos = vec![
|
||||
KeyValuePair {
|
||||
@@ -127,10 +124,14 @@ fn list_instance_returns_instance_names_and_ids() {
|
||||
}
|
||||
|
||||
free_key_value_pairs(&infos[..count as usize]);
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
assert!(found);
|
||||
}
|
||||
|
||||
@@ -261,8 +262,9 @@ 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();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
hooks.post_run_network_instance(&instance_id).await.unwrap();
|
||||
@@ -278,17 +280,18 @@ async fn config_server_hooks_emit_run_event() {
|
||||
);
|
||||
|
||||
assert_eq!(hooks.tracked_instance_ids(), vec![instance_id]);
|
||||
let events = events.lock().unwrap();
|
||||
let events = events.lock().unwrap().clone();
|
||||
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());
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -306,8 +309,9 @@ 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();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -327,7 +331,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();
|
||||
let events = events.lock().unwrap().clone();
|
||||
assert_eq!(events.len(), 2);
|
||||
let event_ids = events
|
||||
.iter()
|
||||
@@ -343,29 +347,27 @@ 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()])
|
||||
);
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id_1, instance_id_2])
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id_1, instance_id_2])
|
||||
.await
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id_1, instance_id_2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_remove_untracked_name_mapping_without_event() {
|
||||
async fn config_server_hooks_ignore_untracked_instance_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());
|
||||
}
|
||||
|
||||
@@ -375,15 +377,25 @@ 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();
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name.clone(), existing_id);
|
||||
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();
|
||||
|
||||
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!(*INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap(), existing_id);
|
||||
INSTANCE_NAME_ID_MAP.remove(&inst_name);
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(existing_id));
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([existing_id])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -398,8 +410,23 @@ 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);
|
||||
INSTANCE_NAME_ID_MAP.insert(old_name.clone(), overwritten_id);
|
||||
INSTANCE_NAME_ID_MAP.insert(duplicate_name.clone(), duplicate_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();
|
||||
|
||||
hooks
|
||||
.post_remove_network_instances(&[overwritten_id])
|
||||
@@ -412,13 +439,17 @@ 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!(INSTANCE_NAME_ID_MAP.get(&old_name).is_none());
|
||||
assert!(find_instance_id_by_name(&old_name).is_none());
|
||||
assert_eq!(
|
||||
*INSTANCE_NAME_ID_MAP.get(&duplicate_name).unwrap(),
|
||||
duplicate_id
|
||||
find_instance_id_by_name(&duplicate_name),
|
||||
Some(duplicate_id)
|
||||
);
|
||||
assert_eq!(events.lock().unwrap().len(), 1);
|
||||
INSTANCE_NAME_ID_MAP.remove(&duplicate_name);
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([duplicate_id])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -427,11 +458,19 @@ 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])
|
||||
@@ -440,7 +479,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!(INSTANCE_NAME_ID_MAP.get(&inst_name).is_none());
|
||||
assert!(find_instance_id_by_name(&inst_name).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -451,11 +490,14 @@ 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();
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(hooks.post_run_network_instance(&instance_id).await.is_err());
|
||||
@@ -468,15 +510,20 @@ 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());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(find_instance_id_by_name(&inst_name), Some(instance_id));
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.manager
|
||||
.delete_network_instances([instance_id]),
|
||||
)
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[instance_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -493,10 +540,10 @@ fn delete_network_instance_removes_only_named_instances() {
|
||||
let cfg = TomlConfigLoader::default();
|
||||
cfg.set_id(id);
|
||||
cfg.set_inst_name(name.clone());
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG)
|
||||
ffi_context()
|
||||
.manager
|
||||
.run_network_instance(cfg, ConfigFileControl::STATIC_CONFIG)
|
||||
.unwrap();
|
||||
INSTANCE_NAME_ID_MAP.insert(name, id);
|
||||
}
|
||||
|
||||
let delete_name = CString::new(delete_name.clone()).unwrap();
|
||||
@@ -509,10 +556,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());
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![keep_id])
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(ffi_context().manager.delete_network_instances([keep_id]))
|
||||
.unwrap();
|
||||
remove_instance_name_ids(&[keep_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -532,13 +579,18 @@ fn retain_and_delete_network_instance_reject_invalid_name_pointers() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||
let manager_guard = INSTANCE_MANAGER
|
||||
.remote_mutation_lock()
|
||||
.blocking_lock_owned();
|
||||
fn ffi_process_management_uses_manager_mutation_lock() {
|
||||
let manager_guard = ffi_context().manager.mutation_lock().blocking_lock_owned();
|
||||
let (done_tx, done_rx) = mpsc::channel();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _ffi_guard = lock_remote_instance_mutation();
|
||||
ffi_context()
|
||||
.runtime
|
||||
.block_on(
|
||||
ffi_context()
|
||||
.process_management
|
||||
.delete_owned_network_instances(Vec::new()),
|
||||
)
|
||||
.unwrap();
|
||||
done_tx.send(()).unwrap();
|
||||
});
|
||||
|
||||
@@ -549,7 +601,7 @@ fn ffi_remote_mutation_lock_uses_manager_lock() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||
async fn config_server_hooks_reject_late_runs_for_core_rollback() {
|
||||
let events: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
let hooks = ManagedConfigServerClientHooks::new(
|
||||
Some(record_config_server_event),
|
||||
@@ -557,15 +609,54 @@ async fn config_server_hooks_suppress_late_run_events_while_stopping() {
|
||||
);
|
||||
hooks.start_stopping();
|
||||
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
hooks
|
||||
.post_run_network_instance(&Uuid::new_v4())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
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();
|
||||
@@ -615,112 +706,12 @@ fn config_server_callback_context_rejects_nested_blocking_ffi_calls() {
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
{
|
||||
let mut session = 0;
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
data_plane_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
unsafe { data_plane_session_open(std::ptr::null(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
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);
|
||||
assert_eq!(session, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,38 +720,27 @@ 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_tcp_connect(
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
0,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
},
|
||||
0
|
||||
unsafe { data_plane_session_open(name.as_ptr(), &mut session) },
|
||||
-(easytier_core::gateway::DataPlaneErrorKind::Io as c_int)
|
||||
);
|
||||
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);
|
||||
assert_eq!(session, 0);
|
||||
|
||||
set_active_for_test(false);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffi-dataplane")]
|
||||
#[test]
|
||||
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);
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,3 +8,23 @@ 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,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[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"] }
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,32 @@
|
||||
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()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
+218
-336
@@ -150,6 +150,16 @@ 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"
|
||||
@@ -188,28 +198,6 @@ 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"
|
||||
@@ -946,17 +934,6 @@ 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"
|
||||
@@ -1004,17 +981,6 @@ 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"
|
||||
@@ -1024,17 +990,6 @@ 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"
|
||||
@@ -1175,17 +1130,13 @@ 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",
|
||||
@@ -1196,11 +1147,10 @@ dependencies = [
|
||||
"clap_complete_nushell",
|
||||
"crossbeam",
|
||||
"dashmap",
|
||||
"dbus",
|
||||
"delegate",
|
||||
"derivative",
|
||||
"derive_builder",
|
||||
"derive_more",
|
||||
"easytier-core",
|
||||
"easytier-proto",
|
||||
"encoding",
|
||||
"flume",
|
||||
"forwarded-header-value",
|
||||
@@ -1213,95 +1163,127 @@ 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",
|
||||
"prefix-trie",
|
||||
"proc-macro2",
|
||||
"pnet_datalink",
|
||||
"prost 0.14.3",
|
||||
"prost-build",
|
||||
"prost-reflect 0.16.4",
|
||||
"prost-reflect-build",
|
||||
"prost-wkt-types",
|
||||
"quanta",
|
||||
"quinn",
|
||||
"quinn-plaintext",
|
||||
"quote",
|
||||
"quinn-proto",
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -1309,17 +1291,22 @@ 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 0.14.7",
|
||||
"prost-reflect",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1331,6 +1318,39 @@ 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"
|
||||
@@ -1439,12 +1459,6 @@ 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"
|
||||
@@ -1858,21 +1872,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "guarden"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31c7272e004bec8ea7fe50b2ec5451858695bb2743e897c353753fcb3415f4ef"
|
||||
checksum = "b8408903291a7d0cc74169d5de4dd1919a9a402a2f67fcd7df3303ed045fae73"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-core",
|
||||
"guarden-macros",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guarden-macros"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d291d94f41471fe84384a426b3e2c9d22f960a351a5bf26aaa7cd75fbc02c88"
|
||||
checksum = "1e0ef28f1077c259f9e7e238e234a78ce18cedbf0251fd2135f5fc23c40e79fe"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
@@ -1928,9 +1943,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -2112,22 +2127,6 @@ 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"
|
||||
@@ -2420,12 +2419,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.11.4"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -2637,16 +2636,6 @@ 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"
|
||||
@@ -2871,9 +2860,6 @@ 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"
|
||||
@@ -2978,7 +2964,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"libc",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route 0.17.1",
|
||||
"netlink-packet-route",
|
||||
"netlink-sys",
|
||||
"once_cell",
|
||||
"system-configuration",
|
||||
@@ -3010,21 +2996,6 @@ 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"
|
||||
@@ -3050,9 +3021,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "network-interface"
|
||||
version = "2.0.3"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745"
|
||||
checksum = "4ddcb8865ad3d9950f22f42ffa0ef0aecbfbf191867b3122413602b0a360b2a6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3155,15 +3126,6 @@ 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"
|
||||
@@ -3382,20 +3344,6 @@ 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"
|
||||
@@ -3403,7 +3351,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
|
||||
dependencies = [
|
||||
"no-std-net",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3416,43 +3363,9 @@ 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"
|
||||
@@ -3463,18 +3376,6 @@ 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"
|
||||
@@ -3564,6 +3465,15 @@ 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"
|
||||
@@ -3692,31 +3602,10 @@ checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"prost 0.13.5",
|
||||
"prost-reflect-derive 0.14.0",
|
||||
"prost-reflect-derive",
|
||||
"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"
|
||||
@@ -3728,17 +3617,6 @@ 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"
|
||||
@@ -3803,6 +3681,21 @@ 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"
|
||||
@@ -3832,18 +3725,6 @@ 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"
|
||||
@@ -3988,6 +3869,15 @@ 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"
|
||||
@@ -4257,15 +4147,6 @@ 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"
|
||||
@@ -4492,7 +4373,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which 4.4.2",
|
||||
"which",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
@@ -4890,9 +4771,7 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -4915,12 +4794,6 @@ 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"
|
||||
@@ -5022,8 +4895,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tokio-websockets"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb"
|
||||
source = "git+https://github.com/EasyTier/tokio-websockets#dc9771c7c215882349c3cb328877550a3593df21"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -5049,8 +4921,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_edit 0.22.27",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5062,6 +4934,15 @@ 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"
|
||||
@@ -5071,9 +4952,30 @@ dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5184,7 +5086,6 @@ dependencies = [
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"time",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
@@ -5411,12 +5312,6 @@ 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"
|
||||
@@ -5601,16 +5496,6 @@ 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"
|
||||
@@ -5650,18 +5535,6 @@ 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"
|
||||
@@ -6262,6 +6135,15 @@ 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"
|
||||
@@ -6282,12 +6164,6 @@ 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"
|
||||
@@ -6428,6 +6304,12 @@ 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,9 +7,18 @@ 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,4 +1,5 @@
|
||||
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,3 +1,4 @@
|
||||
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 = INSTANCE_MANAGER
|
||||
.delete_network_instance(vec![instance_id])
|
||||
let ret = ASYNC_RUNTIME
|
||||
.block_on(INSTANCE_MANAGER.delete_network_instances([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.list_network_instance_ids().is_empty();
|
||||
let has_active_instances = !INSTANCE_MANAGER.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
|
||||
.set_tun_fd(&instance_id, fd)
|
||||
.attach_tun_fd(instance_id, fd)
|
||||
.map(|_| {
|
||||
mark_tun_attached(&config_id);
|
||||
ohrs_log_info!(
|
||||
|
||||
@@ -9,8 +9,7 @@ use crate::runtime::state::runtime_state::{
|
||||
};
|
||||
use crate::{ASYNC_RUNTIME, INSTANCE_MANAGER};
|
||||
use easytier::common::global_ctx::{EventBusSubscriber, GlobalCtxEvent};
|
||||
use easytier::proto::api::instance::ListPeerRequest;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::instance::factory::subscribe_native_instance_event;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -21,7 +20,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
struct LocalSocketState {
|
||||
stop_flag: std::sync::Arc<AtomicBool>,
|
||||
@@ -39,6 +38,7 @@ 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>,
|
||||
}
|
||||
|
||||
@@ -103,11 +103,11 @@ fn shrink_hash_set_if_sparse<T: Eq + Hash>(set: &mut HashSet<T>) {
|
||||
|
||||
fn sync_tun_event_receivers(receivers: &mut HashMap<String, EventBusSubscriber>) {
|
||||
let mut active_instance_ids = HashSet::new();
|
||||
for instance in INSTANCE_MANAGER.iter() {
|
||||
let instance_id = instance.key().to_string();
|
||||
for instance in INSTANCE_MANAGER.instances() {
|
||||
let instance_id = instance.instance_id().to_string();
|
||||
active_instance_ids.insert(instance_id.clone());
|
||||
if !receivers.contains_key(&instance_id)
|
||||
&& let Some(receiver) = instance.value().subscribe_event()
|
||||
&& let Some(receiver) = subscribe_native_instance_event(&instance)
|
||||
{
|
||||
receivers.insert(instance_id, receiver);
|
||||
}
|
||||
@@ -225,35 +225,18 @@ fn tun_candidate_ids(snapshot: &RuntimeAggregateState) -> HashSet<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_traffic_stats() -> TrafficStatsPayload {
|
||||
let services = INSTANCE_MANAGER
|
||||
.iter()
|
||||
.filter_map(|instance| {
|
||||
instance
|
||||
.value()
|
||||
.get_api_service()
|
||||
.map(|api_service| (instance.key().to_string(), api_service))
|
||||
})
|
||||
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_id, api_service) in services {
|
||||
let peers = match api_service
|
||||
.get_peer_manage_service()
|
||||
.list_peer(BaseController::default(), ListPeerRequest::default())
|
||||
.await
|
||||
{
|
||||
Ok(response) => response.peer_infos,
|
||||
Err(err) => {
|
||||
ohrs_log_debug!(
|
||||
"[Rust] collect traffic stats list_peer failed instance={}: {}",
|
||||
instance_id,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
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;
|
||||
@@ -303,7 +286,17 @@ fn collect_traffic_stats() -> TrafficStatsPayload {
|
||||
instances
|
||||
});
|
||||
|
||||
TrafficStatsPayload { 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()
|
||||
}
|
||||
|
||||
pub fn start_local_socket_server() -> bool {
|
||||
@@ -427,7 +420,7 @@ pub fn start_local_socket_server() -> bool {
|
||||
.unwrap_or(true);
|
||||
if should_collect_traffic_stats {
|
||||
last_traffic_stats_at = Some(now);
|
||||
match serde_json::to_string(&collect_traffic_stats()) {
|
||||
match serde_json::to_string(&collect_traffic_stats(unix_time_millis())) {
|
||||
Ok(json) => {
|
||||
let _ = broadcast_local_socket_json_payload_message(
|
||||
&mut clients,
|
||||
|
||||
@@ -37,6 +37,7 @@ macro_rules! ohrs_log_debug {
|
||||
mod config;
|
||||
mod exports;
|
||||
mod kernel_bridge;
|
||||
mod nearby_management;
|
||||
mod platform;
|
||||
mod runtime;
|
||||
|
||||
@@ -53,12 +54,13 @@ 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::{ConfigFileControl, ConfigLoader, TomlConfigLoader},
|
||||
config::{ConfigLoader, TomlConfigLoader},
|
||||
};
|
||||
use easytier::instance_manager::NetworkInstanceManager;
|
||||
use easytier::instance::factory::{NativeInstanceManager, native_instance_manager_with_runtime};
|
||||
use easytier::proto::api::manage::NetworkConfig;
|
||||
use easytier::proto::api::manage::NetworkingMethod;
|
||||
use easytier::web_client::{WebClient, WebClientHooks, run_web_client};
|
||||
@@ -67,27 +69,35 @@ use kernel_bridge::{
|
||||
stop_local_socket_server as stop_local_socket_server_inner,
|
||||
};
|
||||
use napi_derive_ohos::napi;
|
||||
use runtime::state::runtime_state::RuntimeAggregateState;
|
||||
use napi_ohos::bindgen_prelude::Uint8Array;
|
||||
use runtime::state::runtime_state::{RuntimeAggregateState, RuntimeInstanceState};
|
||||
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 {
|
||||
@@ -95,6 +105,13 @@ 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> {
|
||||
@@ -102,13 +119,43 @@ 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(())
|
||||
}
|
||||
@@ -151,8 +198,8 @@ fn stop_web_client(config_id: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
let ret = INSTANCE_MANAGER
|
||||
.delete_network_instance(tracked_ids)
|
||||
let ret = ASYNC_RUNTIME
|
||||
.block_on(INSTANCE_MANAGER.delete_network_instances(tracked_ids))
|
||||
.map(|_| true)
|
||||
.unwrap_or_else(|err| {
|
||||
ohrs_log_error!(
|
||||
@@ -171,7 +218,7 @@ fn ensure_local_socket_server_started() -> bool {
|
||||
}
|
||||
|
||||
fn maybe_stop_local_socket_server() {
|
||||
let no_local_instances = INSTANCE_MANAGER.list_network_instance_ids().is_empty();
|
||||
let no_local_instances = INSTANCE_MANAGER.instance_ids().is_empty();
|
||||
let no_web_clients = WEB_CLIENTS
|
||||
.lock()
|
||||
.map(|guard| guard.is_empty())
|
||||
@@ -182,12 +229,7 @@ fn maybe_stop_local_socket_server() {
|
||||
}
|
||||
|
||||
fn run_config_server_instance(config_id: &str, config: &NetworkConfig) -> bool {
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.iter()
|
||||
.next()
|
||||
.is_some()
|
||||
{
|
||||
if INSTANCE_MANAGER.instance_ids().iter().next().is_some() {
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
@@ -243,6 +285,389 @@ 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))?;
|
||||
@@ -293,7 +718,7 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
}
|
||||
};
|
||||
|
||||
if !INSTANCE_MANAGER.list_network_instance_ids().is_empty() {
|
||||
if !INSTANCE_MANAGER.instance_ids().is_empty() {
|
||||
ohrs_log_error!("[Rust] there is a running instance!");
|
||||
return false;
|
||||
}
|
||||
@@ -303,15 +728,17 @@ pub(crate) fn run_network_instance_from_json(cfg_json: &str) -> bool {
|
||||
}
|
||||
|
||||
let inst_id = cfg.get_id();
|
||||
if INSTANCE_MANAGER
|
||||
.list_network_instance_ids()
|
||||
.contains(&inst_id)
|
||||
{
|
||||
if INSTANCE_MANAGER.instance_ids().contains(&inst_id) {
|
||||
ohrs_log_error!("[Rust] instance {} already exists", inst_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
|
||||
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) {
|
||||
Ok(_) => {
|
||||
cache_runtime_config_snapshot(inst_id.to_string(), inst_id.to_string(), config);
|
||||
true
|
||||
@@ -443,6 +870,78 @@ 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()
|
||||
@@ -468,6 +967,92 @@ 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()
|
||||
@@ -515,6 +1100,92 @@ 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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,8 @@ use easytier::{
|
||||
common::config::{
|
||||
ConfigFileControl, ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader,
|
||||
},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
instance::factory::{NativeInstanceManager, native_instance_manager},
|
||||
};
|
||||
use guarden::defer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::any;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
@@ -28,6 +27,32 @@ 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; // 最多一天
|
||||
|
||||
@@ -238,7 +263,7 @@ impl HealthyMemRecord {
|
||||
|
||||
pub struct HealthChecker {
|
||||
db: Db,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
inst_id_map: DashMap<i32, uuid::Uuid>,
|
||||
node_tasks: DashMap<i32, AbortOnDropHandle<()>>,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
@@ -247,7 +272,7 @@ pub struct HealthChecker {
|
||||
|
||||
impl HealthChecker {
|
||||
pub fn new(db: Db) -> Self {
|
||||
let instance_mgr = Arc::new(NetworkInstanceManager::new());
|
||||
let instance_mgr = Arc::new(native_instance_manager());
|
||||
Self {
|
||||
db,
|
||||
instance_mgr,
|
||||
@@ -387,33 +412,38 @@ 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(), false, ConfigFileControl::STATIC_CONFIG)
|
||||
.run_network_instance(cfg.clone(), 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 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 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);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
Err(anyhow::anyhow!("test node healthy failed, err: {:?}", err))
|
||||
}
|
||||
Err(anyhow::anyhow!("test node healthy failed, err: {:?}", err))
|
||||
.await;
|
||||
cleanup.cleanup().await;
|
||||
result
|
||||
}
|
||||
|
||||
async fn get_node_cfg(
|
||||
@@ -437,7 +467,7 @@ impl HealthChecker {
|
||||
);
|
||||
|
||||
self.instance_mgr
|
||||
.run_network_instance(cfg.clone(), true, ConfigFileControl::STATIC_CONFIG)
|
||||
.run_network_instance(cfg.clone(), ConfigFileControl::STATIC_CONFIG)
|
||||
.with_context(|| "failed to run network instance")?;
|
||||
self.inst_id_map.insert(node_id, cfg.get_id());
|
||||
|
||||
@@ -481,7 +511,10 @@ 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_instance(vec![inst_id.1]);
|
||||
let _ = self
|
||||
.instance_mgr
|
||||
.delete_network_instances([inst_id.1])
|
||||
.await;
|
||||
}
|
||||
self.node_cfg.remove(&node_id);
|
||||
// 保留内存记录,不删除,以便后续查询历史数据
|
||||
@@ -495,10 +528,10 @@ impl HealthChecker {
|
||||
#[instrument(err, ret, skip(instance_mgr))]
|
||||
async fn test_node_healthy(
|
||||
inst_id: uuid::Uuid,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
// return version, response time on healthy, conn_count
|
||||
) -> anyhow::Result<(String, u64, u32)> {
|
||||
let Some(instance) = instance_mgr.get_network_info(&inst_id).await else {
|
||||
let Some(instance) = instance_mgr.network_info(inst_id).await else {
|
||||
anyhow::bail!("healthy check node is not started");
|
||||
};
|
||||
|
||||
@@ -566,7 +599,7 @@ impl HealthChecker {
|
||||
async fn node_health_check_task(
|
||||
node_id: i32,
|
||||
inst_id: uuid::Uuid,
|
||||
instance_mgr: Arc<NetworkInstanceManager>,
|
||||
instance_mgr: Arc<NativeInstanceManager>,
|
||||
db: Db,
|
||||
node_records: Arc<DashMap<i32, HealthyMemRecord>>,
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
[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"] }
|
||||
@@ -0,0 +1,166 @@
|
||||
//! 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
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,810 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
//! 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
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
//! 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
@@ -0,0 +1,21 @@
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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<()>;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
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
@@ -0,0 +1,802 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,995 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
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
@@ -0,0 +1,376 @@
|
||||
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)),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,924 @@
|
||||
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
@@ -0,0 +1,37 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,811 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,867 @@
|
||||
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
@@ -0,0 +1,768 @@
|
||||
//! Per-instance STUN state and background detection lifecycle.
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::{
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rand::seq::IteratorRandom as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
DEFAULT_TCP_STUN_SERVERS, DEFAULT_UDP_STUN_SERVERS, DEFAULT_UDP_V6_STUN_SERVERS,
|
||||
default_stun_servers,
|
||||
},
|
||||
proto::common::{NatType, StunInfo},
|
||||
socket::{
|
||||
IpVersion, SocketContext,
|
||||
udp::{VirtualUdpSocket, VirtualUdpSocketFactory},
|
||||
},
|
||||
};
|
||||
|
||||
use super::client::{
|
||||
HostResolverIter, StunDnsRuntime, StunNatTypeDetectResult, StunSocketRuntime,
|
||||
TcpNatTypeDetector, UdpNatTypeDetector, stun_udp_bind_options, tcp_bind_request,
|
||||
udp_bind_request,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct StunServerConfig {
|
||||
pub udp_servers: Vec<String>,
|
||||
pub tcp_servers: Vec<String>,
|
||||
pub udp_v6_servers: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for StunServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
udp_servers: default_stun_servers(DEFAULT_UDP_STUN_SERVERS),
|
||||
tcp_servers: default_stun_servers(DEFAULT_TCP_STUN_SERVERS),
|
||||
udp_v6_servers: default_stun_servers(DEFAULT_UDP_V6_STUN_SERVERS),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StunInfoProvider: Send + Sync {
|
||||
fn get_stun_info(&self) -> StunInfo;
|
||||
|
||||
async fn get_udp_port_mapping(&self, local_port: u16) -> anyhow::Result<SocketAddr>;
|
||||
|
||||
async fn get_tcp_port_mapping(&self, local_port: u16) -> anyhow::Result<SocketAddr>;
|
||||
|
||||
fn update_stun_info(&self);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[auto_impl::auto_impl(&, Arc, Box)]
|
||||
pub trait StunSocketMapper<S>: StunInfoProvider + Send + Sync
|
||||
where
|
||||
S: VirtualUdpSocket,
|
||||
{
|
||||
async fn get_udp_port_mapping_with_socket(&self, socket: Arc<S>) -> anyhow::Result<SocketAddr>;
|
||||
}
|
||||
|
||||
pub struct StunInfoCollector<R, D: ?Sized>
|
||||
where
|
||||
R: StunSocketRuntime,
|
||||
D: StunDnsRuntime,
|
||||
{
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
udp_socket_context: SocketContext,
|
||||
tcp_socket_context: SocketContext,
|
||||
stun_servers: Arc<RwLock<Vec<String>>>,
|
||||
tcp_stun_servers: Arc<RwLock<Vec<String>>>,
|
||||
stun_servers_v6: Arc<RwLock<Vec<String>>>,
|
||||
udp_nat_test_result: Arc<RwLock<Option<StunNatTypeDetectResult>>>,
|
||||
tcp_nat_test_result: Arc<RwLock<Option<StunNatTypeDetectResult>>>,
|
||||
public_ipv6: Arc<RwLock<Option<Ipv6Addr>>>,
|
||||
nat_test_result_time: Arc<RwLock<i64>>,
|
||||
redetect_notify: Arc<tokio::sync::Notify>,
|
||||
tasks: Mutex<JoinSet<()>>,
|
||||
started: AtomicBool,
|
||||
}
|
||||
|
||||
impl<R, D> StunInfoCollector<R, D>
|
||||
where
|
||||
R: StunSocketRuntime,
|
||||
D: StunDnsRuntime + ?Sized,
|
||||
{
|
||||
pub fn new(
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
socket_context: SocketContext,
|
||||
udp_stun_servers: Vec<String>,
|
||||
tcp_stun_servers: Vec<String>,
|
||||
stun_servers_v6: Vec<String>,
|
||||
) -> Self {
|
||||
Self::new_with_socket_contexts(
|
||||
runtime,
|
||||
dns,
|
||||
socket_context.clone(),
|
||||
socket_context,
|
||||
udp_stun_servers,
|
||||
tcp_stun_servers,
|
||||
stun_servers_v6,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_with_socket_contexts(
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
udp_socket_context: SocketContext,
|
||||
tcp_socket_context: SocketContext,
|
||||
udp_stun_servers: Vec<String>,
|
||||
tcp_stun_servers: Vec<String>,
|
||||
stun_servers_v6: Vec<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
runtime,
|
||||
dns,
|
||||
udp_socket_context,
|
||||
tcp_socket_context,
|
||||
stun_servers: Arc::new(RwLock::new(udp_stun_servers)),
|
||||
tcp_stun_servers: Arc::new(RwLock::new(tcp_stun_servers)),
|
||||
stun_servers_v6: Arc::new(RwLock::new(stun_servers_v6)),
|
||||
udp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||
tcp_nat_test_result: Arc::new(RwLock::new(None)),
|
||||
public_ipv6: Arc::new(RwLock::new(None)),
|
||||
nat_test_result_time: Arc::new(RwLock::new(unix_timestamp())),
|
||||
redetect_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
tasks: Mutex::new(JoinSet::new()),
|
||||
started: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_default_servers(
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
socket_context: SocketContext,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
runtime,
|
||||
dns,
|
||||
socket_context,
|
||||
Self::get_default_servers(),
|
||||
Self::get_default_tcp_servers(),
|
||||
Self::get_default_servers_v6(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_with_default_servers_and_socket_contexts(
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
udp_socket_context: SocketContext,
|
||||
tcp_socket_context: SocketContext,
|
||||
) -> Self {
|
||||
Self::new_with_socket_contexts(
|
||||
runtime,
|
||||
dns,
|
||||
udp_socket_context,
|
||||
tcp_socket_context,
|
||||
Self::get_default_servers(),
|
||||
Self::get_default_tcp_servers(),
|
||||
Self::get_default_servers_v6(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_stun_servers(&self, stun_servers: Vec<String>) {
|
||||
*self.stun_servers.write().unwrap() = stun_servers;
|
||||
}
|
||||
|
||||
pub fn set_stun_servers_v6(&self, stun_servers_v6: Vec<String>) {
|
||||
*self.stun_servers_v6.write().unwrap() = stun_servers_v6;
|
||||
}
|
||||
|
||||
pub fn set_tcp_stun_servers(&self, stun_servers: Vec<String>) {
|
||||
*self.tcp_stun_servers.write().unwrap() = stun_servers;
|
||||
}
|
||||
|
||||
pub fn get_default_servers() -> Vec<String> {
|
||||
StunServerConfig::default().udp_servers
|
||||
}
|
||||
|
||||
pub fn get_default_tcp_servers() -> Vec<String> {
|
||||
StunServerConfig::default().tcp_servers
|
||||
}
|
||||
|
||||
pub fn get_default_servers_v6() -> Vec<String> {
|
||||
StunServerConfig::default().udp_v6_servers
|
||||
}
|
||||
|
||||
async fn get_public_ipv6(
|
||||
runtime: Arc<R>,
|
||||
dns: Arc<D>,
|
||||
socket_context: SocketContext,
|
||||
servers: &[String],
|
||||
) -> Option<Ipv6Addr> {
|
||||
let mut resolver = HostResolverIter::new(
|
||||
dns,
|
||||
socket_context.clone().with_ip_version(IpVersion::V6),
|
||||
servers.to_vec(),
|
||||
10,
|
||||
true,
|
||||
);
|
||||
while let Some(server) = resolver.next().await {
|
||||
let socket = runtime
|
||||
.bind_udp(stun_udp_bind_options(
|
||||
socket_context.clone(),
|
||||
IpVersion::V6,
|
||||
SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 0),
|
||||
))
|
||||
.await
|
||||
.ok()?;
|
||||
let response = udp_bind_request(socket, server).await;
|
||||
tracing::debug!(?response, "finish ipv6 udp nat type detect");
|
||||
if let Ok(Some(IpAddr::V6(ip))) =
|
||||
response.map(|response| response.mapped_socket_addr.map(|addr| addr.ip()))
|
||||
{
|
||||
return Some(ip);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn start_stun_routine(&self) {
|
||||
if self.started.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
|
||||
let runtime = self.runtime.clone();
|
||||
let dns = self.dns.clone();
|
||||
let socket_context = self.udp_socket_context.clone();
|
||||
let stun_servers = self.stun_servers.clone();
|
||||
let udp_nat_test_result = self.udp_nat_test_result.clone();
|
||||
let nat_test_time = self.nat_test_result_time.clone();
|
||||
let redetect_notify = self.redetect_notify.clone();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
let servers = sampled_servers(&stun_servers.read().unwrap());
|
||||
let detector = UdpNatTypeDetector::new(
|
||||
runtime.clone(),
|
||||
dns.clone(),
|
||||
socket_context.clone(),
|
||||
servers,
|
||||
1,
|
||||
);
|
||||
let mut result = detector.detect_nat_type(0).await;
|
||||
tracing::debug!(?result, "finish udp nat type detect");
|
||||
|
||||
let nat_type = result
|
||||
.as_ref()
|
||||
.map(StunNatTypeDetectResult::nat_type)
|
||||
.unwrap_or(NatType::Unknown);
|
||||
if nat_type == NatType::Symmetric {
|
||||
let old_result = result.as_mut().unwrap();
|
||||
tracing::debug!(?old_result, "start get extra bind result");
|
||||
for server in old_result.collect_available_stun_server() {
|
||||
let extra = detector.get_extra_bind_result(0, server).await;
|
||||
tracing::debug!(?extra, "finish udp nat type detect with another port");
|
||||
if let Ok(response) = extra {
|
||||
old_result.extra_bind_test = Some(response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sleep_sec = 10;
|
||||
if let Ok(result) = result {
|
||||
*nat_test_time.write().unwrap() = unix_timestamp();
|
||||
let completed_extra_test = result.extra_bind_test.is_some();
|
||||
*udp_nat_test_result.write().unwrap() = Some(result);
|
||||
if nat_type != NatType::Unknown
|
||||
&& (nat_type != NatType::Symmetric || completed_extra_test)
|
||||
{
|
||||
sleep_sec = 600;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = redetect_notify.notified() => {}
|
||||
_ = crate::foundation::time::sleep(Duration::from_secs(sleep_sec)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let runtime = self.runtime.clone();
|
||||
let dns = self.dns.clone();
|
||||
let socket_context = self.tcp_socket_context.clone();
|
||||
let tcp_stun_servers = self.tcp_stun_servers.clone();
|
||||
let tcp_nat_test_result = self.tcp_nat_test_result.clone();
|
||||
let nat_test_time = self.nat_test_result_time.clone();
|
||||
let redetect_notify = self.redetect_notify.clone();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
let servers = sampled_servers(&tcp_stun_servers.read().unwrap());
|
||||
let detector = TcpNatTypeDetector::new(
|
||||
runtime.clone(),
|
||||
dns.clone(),
|
||||
socket_context.clone(),
|
||||
servers,
|
||||
1,
|
||||
);
|
||||
let result = detector.detect_nat_type(0).await;
|
||||
tracing::debug!(?result, "finish tcp nat type detect");
|
||||
|
||||
let mut sleep_sec = 10;
|
||||
if let Ok(result) = result {
|
||||
*nat_test_time.write().unwrap() = unix_timestamp();
|
||||
let nat_type = result.nat_type();
|
||||
*tcp_nat_test_result.write().unwrap() = Some(result);
|
||||
if nat_type != NatType::Unknown {
|
||||
sleep_sec = 600;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = redetect_notify.notified() => {}
|
||||
_ = crate::foundation::time::sleep(Duration::from_secs(sleep_sec)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let runtime = self.runtime.clone();
|
||||
let dns = self.dns.clone();
|
||||
let socket_context = self.udp_socket_context.clone();
|
||||
let stun_servers_v6 = self.stun_servers_v6.clone();
|
||||
let public_ipv6 = self.public_ipv6.clone();
|
||||
let redetect_notify = self.redetect_notify.clone();
|
||||
self.tasks.lock().unwrap().spawn(async move {
|
||||
loop {
|
||||
let servers = stun_servers_v6.read().unwrap().clone();
|
||||
if let Some(ip) = Self::get_public_ipv6(
|
||||
runtime.clone(),
|
||||
dns.clone(),
|
||||
socket_context.clone(),
|
||||
&servers,
|
||||
)
|
||||
.await
|
||||
{
|
||||
*public_ipv6.write().unwrap() = Some(ip);
|
||||
}
|
||||
|
||||
let sleep_sec = if public_ipv6.read().unwrap().is_none() {
|
||||
60
|
||||
} else {
|
||||
360
|
||||
};
|
||||
tokio::select! {
|
||||
_ = redetect_notify.notified() => {}
|
||||
_ = crate::foundation::time::sleep(Duration::from_secs(sleep_sec)) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R, D> StunInfoProvider for StunInfoCollector<R, D>
|
||||
where
|
||||
R: StunSocketRuntime,
|
||||
D: StunDnsRuntime + ?Sized,
|
||||
{
|
||||
fn get_stun_info(&self) -> StunInfo {
|
||||
self.start_stun_routine();
|
||||
let udp_result = self.udp_nat_test_result.read().unwrap().clone();
|
||||
let tcp_result = self.tcp_nat_test_result.read().unwrap().clone();
|
||||
if udp_result.is_none() && tcp_result.is_none() {
|
||||
return StunInfo::default();
|
||||
}
|
||||
|
||||
let mut public_ip = BTreeSet::<String>::new();
|
||||
if let Some(result) = &udp_result {
|
||||
public_ip.extend(result.public_ips().into_iter().map(|ip| ip.to_string()));
|
||||
}
|
||||
if let Some(result) = &tcp_result {
|
||||
public_ip.extend(result.public_ips().into_iter().map(|ip| ip.to_string()));
|
||||
}
|
||||
if let Some(ip) = *self.public_ipv6.read().unwrap() {
|
||||
public_ip.insert(ip.to_string());
|
||||
}
|
||||
|
||||
StunInfo {
|
||||
udp_nat_type: udp_result
|
||||
.as_ref()
|
||||
.map(|result| result.nat_type() as i32)
|
||||
.unwrap_or(NatType::Unknown as i32),
|
||||
tcp_nat_type: tcp_result
|
||||
.as_ref()
|
||||
.map(|result| result.nat_type() as i32)
|
||||
.unwrap_or(NatType::Unknown as i32),
|
||||
last_update_time: *self.nat_test_result_time.read().unwrap(),
|
||||
public_ip: public_ip.into_iter().collect(),
|
||||
min_port: udp_result
|
||||
.as_ref()
|
||||
.map(|result| result.min_port() as u32)
|
||||
.or_else(|| tcp_result.as_ref().map(|result| result.min_port() as u32))
|
||||
.unwrap_or(0),
|
||||
max_port: udp_result
|
||||
.as_ref()
|
||||
.map(|result| result.max_port() as u32)
|
||||
.or_else(|| tcp_result.as_ref().map(|result| result.max_port() as u32))
|
||||
.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_udp_port_mapping(&self, local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
let socket = self
|
||||
.runtime
|
||||
.bind_udp(stun_udp_bind_options(
|
||||
self.udp_socket_context.clone(),
|
||||
IpVersion::V4,
|
||||
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), local_port),
|
||||
))
|
||||
.await?;
|
||||
StunSocketMapper::get_udp_port_mapping_with_socket(self, socket).await
|
||||
}
|
||||
|
||||
async fn get_tcp_port_mapping(&self, local_port: u16) -> anyhow::Result<SocketAddr> {
|
||||
self.start_stun_routine();
|
||||
let mut servers = self
|
||||
.tcp_nat_test_result
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.map(|result| result.collect_available_stun_server())
|
||||
.unwrap_or_default();
|
||||
if servers.is_empty() {
|
||||
let mut resolver = HostResolverIter::new(
|
||||
self.dns.clone(),
|
||||
self.tcp_socket_context
|
||||
.clone()
|
||||
.with_ip_version(IpVersion::V4),
|
||||
self.tcp_stun_servers.read().unwrap().clone(),
|
||||
2,
|
||||
false,
|
||||
);
|
||||
while let Some(addr) = resolver.next().await {
|
||||
servers.push(addr);
|
||||
if servers.len() >= 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for server in servers {
|
||||
match tcp_bind_request(
|
||||
self.runtime.clone(),
|
||||
self.tcp_socket_context.clone(),
|
||||
server,
|
||||
local_port,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if let Some(mapped_addr) = response.mapped_socket_addr {
|
||||
return Ok(mapped_addr);
|
||||
}
|
||||
}
|
||||
Err(error) => tracing::warn!(?server, ?error, "tcp stun bind request failed"),
|
||||
}
|
||||
}
|
||||
anyhow::bail!("no TCP STUN mapping found")
|
||||
}
|
||||
|
||||
fn update_stun_info(&self) {
|
||||
self.redetect_notify.notify_waiters();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<R, D> StunSocketMapper<<R as VirtualUdpSocketFactory>::Socket> for StunInfoCollector<R, D>
|
||||
where
|
||||
R: StunSocketRuntime,
|
||||
D: StunDnsRuntime + ?Sized,
|
||||
{
|
||||
async fn get_udp_port_mapping_with_socket(
|
||||
&self,
|
||||
socket: Arc<<R as VirtualUdpSocketFactory>::Socket>,
|
||||
) -> anyhow::Result<SocketAddr> {
|
||||
self.start_stun_routine();
|
||||
let mut servers = self
|
||||
.udp_nat_test_result
|
||||
.read()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.map(|result| result.collect_available_stun_server())
|
||||
.unwrap_or_default();
|
||||
if servers.is_empty() {
|
||||
let mut resolver = HostResolverIter::new(
|
||||
self.dns.clone(),
|
||||
self.udp_socket_context
|
||||
.clone()
|
||||
.with_ip_version(IpVersion::V4),
|
||||
self.stun_servers.read().unwrap().clone(),
|
||||
2,
|
||||
false,
|
||||
);
|
||||
while let Some(addr) = resolver.next().await {
|
||||
servers.push(addr);
|
||||
if servers.len() >= 2 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for server in servers {
|
||||
match udp_bind_request(socket.clone(), server).await {
|
||||
Ok(response) => {
|
||||
if let Some(mapped_addr) = response.mapped_socket_addr {
|
||||
return Ok(mapped_addr);
|
||||
}
|
||||
}
|
||||
Err(error) => tracing::warn!(?server, ?error, "stun bind request failed"),
|
||||
}
|
||||
}
|
||||
anyhow::bail!("no UDP STUN mapping found")
|
||||
}
|
||||
}
|
||||
|
||||
fn sampled_servers(servers: &[String]) -> Vec<String> {
|
||||
servers
|
||||
.iter()
|
||||
.take(2)
|
||||
.chain(servers.iter().skip(2).choose(&mut rand::thread_rng()))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unix_timestamp() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytecodec::{DecodeExt as _, EncodeExt as _};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
|
||||
use crate::host::dns::{DnsQuery, DnsRecordResolver, DnsResolver, DnsSrvRecord};
|
||||
use crate::socket::{
|
||||
NetNamespace,
|
||||
tcp::{TcpConnectOptions, VirtualTcpSocket, VirtualTcpSocketFactory},
|
||||
udp::{UdpBindOptions, UdpSocketPurpose},
|
||||
};
|
||||
|
||||
use crate::packet::stun::Attribute;
|
||||
use stun_codec::rfc5389::{attributes::XorMappedAddress, methods::BINDING};
|
||||
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct MockUdpSocket {
|
||||
local_addr: SocketAddr,
|
||||
mapped_addr: SocketAddr,
|
||||
responses: Mutex<VecDeque<(Vec<u8>, SocketAddr)>>,
|
||||
response_ready: tokio::sync::Notify,
|
||||
}
|
||||
|
||||
#[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> {
|
||||
let request = MessageDecoder::<Attribute>::new()
|
||||
.decode_from_bytes(data)
|
||||
.map_err(|error| io::Error::other(format!("{error:?}")))?
|
||||
.map_err(|error| io::Error::other(format!("{error:?}")))?;
|
||||
let mut response = Message::<Attribute>::new(
|
||||
MessageClass::SuccessResponse,
|
||||
BINDING,
|
||||
request.transaction_id(),
|
||||
);
|
||||
response.add_attribute(Attribute::XorMappedAddress(XorMappedAddress::new(
|
||||
self.mapped_addr,
|
||||
)));
|
||||
let bytes = MessageEncoder::new()
|
||||
.encode_into_bytes(response)
|
||||
.map_err(io::Error::other)?;
|
||||
self.responses.lock().unwrap().push_back((bytes, addr));
|
||||
self.response_ready.notify_one();
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
loop {
|
||||
if let Some((bytes, addr)) = self.responses.lock().unwrap().pop_front() {
|
||||
buf[..bytes.len()].copy_from_slice(&bytes);
|
||||
return Ok((bytes.len(), addr));
|
||||
}
|
||||
self.response_ready.notified().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MockTcpSocket(tokio::io::DuplexStream);
|
||||
|
||||
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.0).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.0).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.0).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
Pin::new(&mut self.0).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualTcpSocket for MockTcpSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok("127.0.0.1:40000".parse().unwrap())
|
||||
}
|
||||
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok("127.0.0.1:3478".parse().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockRuntime {
|
||||
udp_binds: Mutex<Vec<UdpBindOptions>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocketFactory for MockRuntime {
|
||||
type Socket = MockUdpSocket;
|
||||
|
||||
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
||||
self.udp_binds.lock().unwrap().push(options);
|
||||
Ok(Arc::new(MockUdpSocket {
|
||||
local_addr: "0.0.0.0:40000".parse().unwrap(),
|
||||
mapped_addr: "198.51.100.10:40123".parse().unwrap(),
|
||||
responses: Mutex::new(VecDeque::new()),
|
||||
response_ready: tokio::sync::Notify::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualTcpSocketFactory for MockRuntime {
|
||||
type Socket = MockTcpSocket;
|
||||
|
||||
async fn connect_tcp(&self, _options: TcpConnectOptions) -> anyhow::Result<Self::Socket> {
|
||||
anyhow::bail!("TCP is not used by this test")
|
||||
}
|
||||
}
|
||||
|
||||
struct MockDns;
|
||||
|
||||
#[async_trait]
|
||||
impl DnsResolver for MockDns {
|
||||
async fn resolve(&self, _query: DnsQuery) -> anyhow::Result<Vec<IpAddr>> {
|
||||
Ok(vec!["192.0.2.1".parse().unwrap()])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DnsRecordResolver for MockDns {
|
||||
async fn resolve_txt(&self, _query: DnsQuery) -> anyhow::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
async fn resolve_srv(&self, _query: DnsQuery) -> anyhow::Result<Vec<DnsSrvRecord>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sampled_servers_keep_first_two_and_at_most_one_extra() {
|
||||
let servers = ["a", "b", "c", "d"]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
let sampled = sampled_servers(&servers);
|
||||
assert_eq!(&sampled[..2], &["a", "b"]);
|
||||
assert_eq!(sampled.len(), 3);
|
||||
assert!(matches!(sampled[2].as_str(), "c" | "d"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collector_keeps_udp_and_tcp_socket_contexts_separate() {
|
||||
let udp_context = SocketContext::default()
|
||||
.with_socket_mark(Some(11))
|
||||
.with_netns(Some(NetNamespace::new("udp-instance")));
|
||||
let tcp_context = SocketContext::default()
|
||||
.with_socket_mark(Some(22))
|
||||
.with_netns(Some(NetNamespace::new("tcp-instance")));
|
||||
let collector = StunInfoCollector::new_with_socket_contexts(
|
||||
Arc::new(MockRuntime::default()),
|
||||
Arc::new(MockDns),
|
||||
udp_context.clone(),
|
||||
tcp_context.clone(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(collector.udp_socket_context, udp_context);
|
||||
assert_eq!(collector.tcp_socket_context, tcp_context);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn udp_mapping_uses_portable_runtime_and_instance_context() {
|
||||
let runtime = Arc::new(MockRuntime::default());
|
||||
let context = SocketContext::default()
|
||||
.with_socket_mark(Some(0))
|
||||
.with_netns(Some(NetNamespace::new("instance-a")));
|
||||
let collector = StunInfoCollector::new(
|
||||
runtime.clone(),
|
||||
Arc::new(MockDns),
|
||||
context,
|
||||
vec!["stun.example".to_owned()],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let mapped = collector.get_udp_port_mapping(0).await.unwrap();
|
||||
|
||||
assert_eq!(mapped, "198.51.100.10:40123".parse().unwrap());
|
||||
let binds = runtime.udp_binds.lock().unwrap();
|
||||
assert!(!binds.is_empty());
|
||||
assert_eq!(binds[0].purpose, UdpSocketPurpose::StunProbe);
|
||||
assert_eq!(binds[0].local_addr, Some("0.0.0.0:0".parse().unwrap()));
|
||||
assert_eq!(binds[0].context.ip_version, IpVersion::V4);
|
||||
assert_eq!(binds[0].context.socket_mark, Some(0));
|
||||
assert_eq!(
|
||||
binds[0].context.netns.as_ref().map(|netns| netns.token()),
|
||||
Some("instance-a")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod client;
|
||||
mod collector;
|
||||
mod responder;
|
||||
|
||||
pub use client::{
|
||||
StunDnsRuntime, StunNatTypeDetectResult, StunSocketRuntime, TcpNatTypeDetector,
|
||||
UdpNatTypeDetector,
|
||||
};
|
||||
pub use collector::{StunInfoCollector, StunInfoProvider, StunServerConfig, StunSocketMapper};
|
||||
@@ -0,0 +1,247 @@
|
||||
//! STUN binding-response support over UDP sockets.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use bytecodec::{DecodeExt as _, EncodeExt as _};
|
||||
use stun_codec::rfc5389::attributes::XorMappedAddress;
|
||||
use stun_codec::rfc5389::methods::BINDING;
|
||||
use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder};
|
||||
|
||||
use crate::packet::stun::{Attribute, ChangeRequest, tid_to_u32, u32_to_tid};
|
||||
use crate::socket::udp::{UdpBindOptions, VirtualUdpSocket, VirtualUdpSocketFactory};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum StunResponseSendSource {
|
||||
SameSocket,
|
||||
NewSocket,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StunResponse {
|
||||
pub bytes: Vec<u8>,
|
||||
pub send_source: StunResponseSendSource,
|
||||
}
|
||||
|
||||
pub fn build_stun_response(addr: SocketAddr, req_buf: &[u8]) -> anyhow::Result<StunResponse> {
|
||||
let mut decoder = MessageDecoder::<Attribute>::new();
|
||||
let req_msg = decoder
|
||||
.decode_from_bytes(req_buf)
|
||||
.map_err(|e| anyhow::anyhow!("stun decode error: {:?}", e))?
|
||||
.map_err(|e| anyhow::anyhow!("stun decode broken message error: {:?}", e))?;
|
||||
|
||||
let tid = req_msg.transaction_id();
|
||||
// we only respond easytier stun req, whose tid has 0xdeadbeef prefix
|
||||
if tid.as_bytes()[0..4] != [0xde, 0xad, 0xbe, 0xef] {
|
||||
anyhow::bail!("stun req tid not from easytier");
|
||||
}
|
||||
|
||||
let mut resp_msg = Message::<Attribute>::new(
|
||||
MessageClass::SuccessResponse,
|
||||
BINDING,
|
||||
// we discard the prefix, make sure our implementation is not compatible with other stun client
|
||||
u32_to_tid(tid_to_u32(&tid)),
|
||||
);
|
||||
resp_msg.add_attribute(Attribute::XorMappedAddress(XorMappedAddress::new(addr)));
|
||||
|
||||
let mut encoder = MessageEncoder::new();
|
||||
let bytes = encoder
|
||||
.encode_into_bytes(resp_msg.clone())
|
||||
.map_err(|e| anyhow::anyhow!("stun encode error: {:?}", e))?;
|
||||
|
||||
let change_req = req_msg
|
||||
.get_attribute::<ChangeRequest>()
|
||||
.map(|r| r.ip() || r.port())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(StunResponse {
|
||||
bytes,
|
||||
send_source: if change_req {
|
||||
StunResponseSendSource::NewSocket
|
||||
} else {
|
||||
StunResponseSendSource::SameSocket
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn respond_stun_packet<S, F>(
|
||||
socket: Arc<S>,
|
||||
factory: &F,
|
||||
addr: SocketAddr,
|
||||
req_buf: &[u8],
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
S: VirtualUdpSocket,
|
||||
F: VirtualUdpSocketFactory<Socket = S> + ?Sized,
|
||||
{
|
||||
let response = build_stun_response(addr, req_buf)?;
|
||||
match response.send_source {
|
||||
StunResponseSendSource::SameSocket => {
|
||||
socket
|
||||
.send_to(&response.bytes, addr)
|
||||
.await
|
||||
.with_context(|| "send stun response error")?;
|
||||
}
|
||||
StunResponseSendSource::NewSocket => {
|
||||
let bind_addr = if addr.is_ipv4() {
|
||||
"0.0.0.0:0".parse().unwrap()
|
||||
} else {
|
||||
"[::]:0".parse().unwrap()
|
||||
};
|
||||
let socket = factory
|
||||
.bind_udp(
|
||||
UdpBindOptions::hole_punch_control()
|
||||
.with_context(socket.socket_context())
|
||||
.with_local_addr(Some(bind_addr)),
|
||||
)
|
||||
.await?;
|
||||
socket.send_to(&response.bytes, addr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(?addr, "udp respond stun packet done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<S, F> crate::socket::udp::UdpSessionStunResponder<S> for F
|
||||
where
|
||||
S: VirtualUdpSocket,
|
||||
F: VirtualUdpSocketFactory<Socket = S>,
|
||||
{
|
||||
async fn respond_stun(
|
||||
&self,
|
||||
socket: Arc<S>,
|
||||
datagram: &[u8],
|
||||
remote_addr: SocketAddr,
|
||||
) -> std::io::Result<()> {
|
||||
respond_stun_packet(socket, self, remote_addr, datagram)
|
||||
.await
|
||||
.map_err(|error| std::io::Error::other(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{io, sync::Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use stun_codec::TransactionId;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockSocket {
|
||||
sent: Mutex<Vec<(Vec<u8>, SocketAddr)>>,
|
||||
}
|
||||
|
||||
impl MockSocket {
|
||||
fn sent(&self) -> Vec<(Vec<u8>, SocketAddr)> {
|
||||
self.sent.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VirtualUdpSocket for MockSocket {
|
||||
fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
Ok("127.0.0.1:0".parse().unwrap())
|
||||
}
|
||||
|
||||
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> io::Result<usize> {
|
||||
self.sent.lock().unwrap().push((data.to_vec(), addr));
|
||||
Ok(data.len())
|
||||
}
|
||||
|
||||
async fn recv_from(&self, _buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
std::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MockFactory {
|
||||
bind_options: Mutex<Vec<UdpBindOptions>>,
|
||||
sockets: Mutex<Vec<Arc<MockSocket>>>,
|
||||
}
|
||||
|
||||
impl MockFactory {
|
||||
fn bind_options(&self) -> Vec<UdpBindOptions> {
|
||||
self.bind_options.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn sockets(&self) -> Vec<Arc<MockSocket>> {
|
||||
self.sockets.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[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().unwrap().push(options);
|
||||
let socket = Arc::new(MockSocket::default());
|
||||
self.sockets.lock().unwrap().push(socket.clone());
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
|
||||
fn stun_request(change_ip: bool, change_port: bool) -> Vec<u8> {
|
||||
let mut request = Message::<Attribute>::new(MessageClass::Request, BINDING, u32_to_tid(7));
|
||||
if change_ip || change_port {
|
||||
request.add_attribute(Attribute::ChangeRequest(ChangeRequest::new(
|
||||
change_ip,
|
||||
change_port,
|
||||
)));
|
||||
}
|
||||
MessageEncoder::new().encode_into_bytes(request).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_stun_response_rejects_non_easytier_tid() {
|
||||
let request =
|
||||
Message::<Attribute>::new(MessageClass::Request, BINDING, TransactionId::new([0; 12]));
|
||||
let mut encoder = MessageEncoder::new();
|
||||
let request = encoder.encode_into_bytes(request).unwrap();
|
||||
|
||||
assert!(build_stun_response("127.0.0.1:1234".parse().unwrap(), &request).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_stun_response_detects_change_request() {
|
||||
let request = stun_request(true, false);
|
||||
|
||||
let response = build_stun_response("127.0.0.1:1234".parse().unwrap(), &request).unwrap();
|
||||
|
||||
assert_eq!(response.send_source, StunResponseSendSource::NewSocket);
|
||||
assert!(!response.bytes.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn respond_stun_packet_uses_ipv6_unspecified_socket_for_ipv6_change_request() {
|
||||
let listener_socket = Arc::new(MockSocket::default());
|
||||
let factory = MockFactory::default();
|
||||
let remote_addr = "[::1]:1234".parse().unwrap();
|
||||
|
||||
respond_stun_packet(
|
||||
listener_socket.clone(),
|
||||
&factory,
|
||||
remote_addr,
|
||||
&stun_request(true, false),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(listener_socket.sent().is_empty());
|
||||
assert_eq!(
|
||||
factory.bind_options(),
|
||||
vec![
|
||||
UdpBindOptions::hole_punch_control()
|
||||
.with_local_addr(Some("[::]:0".parse().unwrap()))
|
||||
]
|
||||
);
|
||||
let sockets = factory.sockets();
|
||||
assert_eq!(sockets.len(), 1);
|
||||
assert_eq!(sockets[0].sent()[0].1, remote_addr);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user