mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-27 16:00:49 +08:00
Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d13ee16e | ||
|
|
66a13d3d8c | ||
|
|
8b4ca6d080 | ||
|
|
9a1f4ff4b5 | ||
|
|
3b8a4a6f34 | ||
|
|
4f59e78179 | ||
|
|
2f60e61dd5 | ||
|
|
318b58b26a | ||
|
|
48cd06c7a4 | ||
|
|
99da8a3c61 | ||
|
|
1e0b0407d3 | ||
|
|
7e6dc91095 | ||
|
|
d854b6a220 | ||
|
|
f8fb64ad8c | ||
|
|
0e8c323659 | ||
|
|
0956da4ae2 | ||
|
|
03cff9d9a8 | ||
|
|
0a041c2caa | ||
|
|
83cbfb6924 | ||
|
|
6010c39cd8 | ||
|
|
4cf3f4e252 | ||
|
|
3d3110c3d7 | ||
|
|
22912707b5 | ||
|
|
e85f0a9bfe | ||
|
|
9ef5011760 | ||
|
|
d71112380c | ||
|
|
491bfbecb5 | ||
|
|
8a6cc98806 | ||
|
|
728373b067 | ||
|
|
c62ff2da87 | ||
|
|
572bf859ba | ||
|
|
aef136241a | ||
|
|
78af9f67de | ||
|
|
9f34b4109b | ||
|
|
1967d22dff | ||
|
|
0cba8ab127 | ||
|
|
d002226acb | ||
|
|
1db572e020 | ||
|
|
5de5ee42c2 | ||
|
|
2c19208094 | ||
|
|
3014edffbb | ||
|
|
ece4f303ce | ||
|
|
db9d79554a | ||
|
|
620fe0be54 | ||
|
|
79a4dcf2b0 | ||
|
|
a8ee9552a0 | ||
|
|
3749bafae0 | ||
|
|
b573f478fc | ||
|
|
d67a3ab7f8 | ||
|
|
dbe5672df4 | ||
|
|
48fd64319e | ||
|
|
ccf821ab22 | ||
|
|
b074aa5779 | ||
|
|
95a1fdf42d | ||
|
|
e678ec394d | ||
|
|
628909a192 | ||
|
|
5cab585115 | ||
|
|
dcfa3eadaf | ||
|
|
1647e70243 | ||
|
|
9c6d8a614c | ||
|
|
db7a845d3b | ||
|
|
1d48d45592 | ||
|
|
d613739b06 | ||
|
|
6e1194fe4c | ||
|
|
f101ddb918 | ||
|
|
0054100414 | ||
|
|
486f3887c2 | ||
|
|
37af369b43 | ||
|
|
827d24fde4 | ||
|
|
b8ea38b5f3 | ||
|
|
4530d8663d | ||
|
|
a4073d64cb | ||
|
|
16400df182 | ||
|
|
47af17bebc | ||
|
|
814f23a27c | ||
|
|
6bbe137f09 | ||
|
|
6fdcf5c7c9 | ||
|
|
ce1712ff2e | ||
|
|
8b09824e04 | ||
|
|
9fb23476ac | ||
|
|
971c263bf8 | ||
|
|
cb3fd3e646 | ||
|
|
d414557d74 | ||
|
|
fbcd95b617 | ||
|
|
887f29096f | ||
|
|
e0bddc2faa | ||
|
|
4f770a1eea | ||
|
|
376dc97134 | ||
|
|
27c8da9a75 | ||
|
|
5963dfa01a | ||
|
|
f86cba6ce5 | ||
|
|
e1b82519b9 | ||
|
|
32d8a62fec | ||
|
|
4cd622ff07 | ||
|
|
6ec4361f43 | ||
|
|
e9bed3688f | ||
|
|
8d5366444b | ||
|
|
4e32b05124 | ||
|
|
e0055bf491 | ||
|
|
d60ddb21dc | ||
|
|
219d1ccd08 | ||
|
|
6f697e56c8 | ||
|
|
05535b9bb2 | ||
|
|
5ce2cb658c | ||
|
|
a86df52d9a | ||
|
|
275c2ca343 | ||
|
|
5d871e8b21 | ||
|
|
694afdd07f | ||
|
|
d31e70515a | ||
|
|
701b141347 | ||
|
|
eaa114f40c | ||
|
|
563c808836 | ||
|
|
79b6683817 | ||
|
|
8bd16df6f8 | ||
|
|
fdea52d59d | ||
|
|
4031abd8d6 | ||
|
|
cf2bfaf6a7 | ||
|
|
d35021ee5d | ||
|
|
57d1597264 | ||
|
|
aca847e76c | ||
|
|
8036c164b4 | ||
|
|
0482987bf1 | ||
|
|
50dd419e5a | ||
|
|
213359b6c8 | ||
|
|
63c2bb4ca2 | ||
|
|
6693020e6d | ||
|
|
05c5f45995 | ||
|
|
1ded7a8a66 | ||
|
|
809d0ca65c | ||
|
|
139064de35 | ||
|
|
54e0cbc84e | ||
|
|
7c9166a8cf | ||
|
|
a9a16f2218 | ||
|
|
c9ae9dd893 | ||
|
|
010d50b727 | ||
|
|
c88fb03243 | ||
|
|
7bae9b633c | ||
|
|
fc3f1d6aac | ||
|
|
b719be3e38 | ||
|
|
db2410cc7f | ||
|
|
d9c2854911 | ||
|
|
99c051a874 | ||
|
|
a0ad151fbc | ||
|
|
4806a5e0a9 | ||
|
|
e60152d38b | ||
|
|
f6e97a06f5 | ||
|
|
618599266e | ||
|
|
696c1b4bb2 | ||
|
|
b346af35d3 | ||
|
|
16a65289f2 | ||
|
|
bb2691fe8f | ||
|
|
7753c83e27 | ||
|
|
1d6fb2e3c5 | ||
|
|
6c1bff5d0c | ||
|
|
f1e362a820 | ||
|
|
e2c19d550c | ||
|
|
c101ef1c80 | ||
|
|
4b7be20fe0 | ||
|
|
5c98aea7e3 | ||
|
|
da61644dbc | ||
|
|
5de7ecd4c5 | ||
|
|
3fc6b055a0 | ||
|
|
4b65eebd5d | ||
|
|
921c00c472 | ||
|
|
b6bd76534f | ||
|
|
780cfb4cca | ||
|
|
8f9272d985 | ||
|
|
f679647ec9 | ||
|
|
198552014b | ||
|
|
3e3937605e | ||
|
|
4a85fbfab8 | ||
|
|
87d1110a87 | ||
|
|
b31aae284d | ||
|
|
dc6475776e | ||
|
|
3de72677e6 | ||
|
|
6a1616c32a | ||
|
|
032f47a891 | ||
|
|
2e0ca89943 | ||
|
|
1f7cfb373c | ||
|
|
da05656a89 | ||
|
|
265852b312 | ||
|
|
02bf04ed7f | ||
|
|
8915d36bcf | ||
|
|
3ea15e37a4 | ||
|
|
cb0c66af96 | ||
|
|
a3ebcded34 | ||
|
|
f7c2cd1b90 | ||
|
|
e774210ae3 | ||
|
|
935fa823f2 | ||
|
|
dd3f73ae54 | ||
|
|
0b9d94f53f | ||
|
|
e5d6279a54 | ||
|
|
57d4091497 | ||
|
|
4e8c342905 | ||
|
|
17cd74f64c | ||
|
|
9923670426 | ||
|
|
3ee3df77b8 | ||
|
|
8ec2f25e82 | ||
|
|
c27d3a6703 | ||
|
|
6723f432a3 | ||
|
|
12a3f1c947 | ||
|
|
52754c862b | ||
|
|
e51d243324 | ||
|
|
a1ebd34083 | ||
|
|
89b19ea7dd | ||
|
|
0d47d8395d | ||
|
|
d82c863f40 | ||
|
|
d8e7de74a6 | ||
|
|
74035f8e12 | ||
|
|
8d45186eba | ||
|
|
c484580b8f | ||
|
|
56bce7937c | ||
|
|
07b982d1d2 | ||
|
|
9065e01225 | ||
|
|
cc3cc15774 | ||
|
|
fcb39c73fc | ||
|
|
7c703b8b4b | ||
|
|
8eac31f69f | ||
|
|
9653e16a68 | ||
|
|
d0c0852fbb | ||
|
|
c0a0c90cbd | ||
|
|
9e3483b836 | ||
|
|
132f445c29 | ||
|
|
4952cbaf19 | ||
|
|
099f0b1ca2 | ||
|
|
eecbc0fc13 | ||
|
|
2d81a071e5 | ||
|
|
3e35181583 | ||
|
|
c3a3f41a2c | ||
|
|
2b2b471cfb | ||
|
|
abb319068b | ||
|
|
51d7d8b8be | ||
|
|
f95714d9f0 | ||
|
|
7d52b2e2ea | ||
|
|
a2a8b3802d | ||
|
|
f4283f45a4 | ||
|
|
4784cb75e4 | ||
|
|
abc6bd1677 | ||
|
|
1c5288d783 | ||
|
|
6bcb54bd22 | ||
|
|
e20136a5ab | ||
|
|
c8fd3648ad | ||
|
|
6ef2d394d9 | ||
|
|
762a3b037d | ||
|
|
e09a906f93 | ||
|
|
95bf1a852e | ||
|
|
200f947b5d | ||
|
|
46ae0c81e2 | ||
|
|
779aa180ad | ||
|
|
ae26e3c863 | ||
|
|
eeb41159b7 | ||
|
|
24a10aa222 | ||
|
|
c119db4908 | ||
|
|
0db287bf55 | ||
|
|
e229f35777 | ||
|
|
df647b45cd | ||
|
|
b74659dcd4 | ||
|
|
4f2fb534a4 | ||
|
|
bd17f8d0f8 | ||
|
|
cee43795f8 | ||
|
|
79d90ea703 | ||
|
|
486db7b4aa | ||
|
|
016c0d5dbb | ||
|
|
6e2c6dea1c | ||
|
|
078c4e4ea1 | ||
|
|
6daa348c63 | ||
|
|
3341d8b5cd | ||
|
|
251a1e00c4 | ||
|
|
ce622e4492 | ||
|
|
5f03971579 | ||
|
|
ba1b5224ff | ||
|
|
ccaf4d1205 | ||
|
|
6ed1cf5bef | ||
|
|
74411d354c | ||
|
|
3133db9c86 | ||
|
|
934dc48208 | ||
|
|
60b294e0ab | ||
|
|
fb975875f1 | ||
|
|
f912c977d0 | ||
|
|
24c4002ef2 | ||
|
|
a7143287d9 | ||
|
|
3824e57fc5 | ||
|
|
ba6ec56cee | ||
|
|
21bea797e4 | ||
|
|
261deb1303 | ||
|
|
394baca938 | ||
|
|
7baf6fcf44 | ||
|
|
04a2ff9724 | ||
|
|
72eb2c450d | ||
|
|
f8a031c90c | ||
|
|
8b17a0c48b | ||
|
|
e75f0500e2 | ||
|
|
176e71c79e | ||
|
|
d78c6ed047 | ||
|
|
89072ad58d | ||
|
|
e7d8c93bff |
189
.github/workflows/build.yml
vendored
Normal file
189
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish_release:
|
||||
description: Publish GitHub Release
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
release_tag:
|
||||
description: Release tag name when publishing
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: web
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Upload frontend dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
deb:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: frontend
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download frontend dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# The Docker workflow builds the same targets with the same Cross images.
|
||||
shared-key: linux-cross-release
|
||||
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --version 0.2.5 --locked
|
||||
|
||||
- name: Build linux binary
|
||||
run: bash build/build-images.sh
|
||||
|
||||
- name: Package deb
|
||||
run: bash build/package-deb.sh
|
||||
|
||||
- name: Upload deb
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: one-kvm-deb
|
||||
path: target/debian/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
windows:
|
||||
runs-on: windows-2022
|
||||
needs: frontend
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download frontend dist
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: web-dist
|
||||
path: web/dist
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up MSVC
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
|
||||
- name: Prepare vcpkg and dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:VCPKG_ROOT = "C:\vcpkg"
|
||||
$env:VCPKG_DEFAULT_TRIPLET = "x64-windows-static"
|
||||
$env:VCPKG_INSTALLED_DIR = Join-Path $pwd "vcpkg_installed"
|
||||
|
||||
if (-not (Test-Path $env:VCPKG_ROOT)) {
|
||||
git clone https://github.com/microsoft/vcpkg $env:VCPKG_ROOT
|
||||
}
|
||||
|
||||
& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install --triplet $env:VCPKG_DEFAULT_TRIPLET --x-install-root="$env:VCPKG_INSTALLED_DIR"
|
||||
|
||||
$tripletRoot = Join-Path $env:VCPKG_INSTALLED_DIR $env:VCPKG_DEFAULT_TRIPLET
|
||||
$env:TURBOJPEG_SOURCE = "explicit"
|
||||
$env:TURBOJPEG_LIB_DIR = Join-Path $tripletRoot "lib"
|
||||
$env:TURBOJPEG_INCLUDE_DIR = Join-Path $tripletRoot "include"
|
||||
|
||||
"VCPKG_ROOT=$env:VCPKG_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"VCPKG_DEFAULT_TRIPLET=$env:VCPKG_DEFAULT_TRIPLET" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"VCPKG_INSTALLED_DIR=$env:VCPKG_INSTALLED_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"TURBOJPEG_SOURCE=$env:TURBOJPEG_SOURCE" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: windows-msvc-release
|
||||
key: ${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
|
||||
|
||||
- name: Build Windows exe
|
||||
shell: pwsh
|
||||
run: .\build\windows\build.ps1 -Configuration release -Package
|
||||
|
||||
- name: Upload exe
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: one-kvm-windows-exe
|
||||
path: target/x86_64-pc-windows-msvc/release/one-kvm_*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [deb, windows]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Validate release tag
|
||||
run: |
|
||||
if [ -z "${{ inputs.release_tag }}" ]; then
|
||||
echo "release_tag is required when publish_release is true"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Download deb artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: one-kvm-deb
|
||||
path: release-artifacts/deb
|
||||
|
||||
- name: Download exe artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: one-kvm-windows-exe
|
||||
path: release-artifacts/windows
|
||||
|
||||
- name: Publish GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
release-artifacts/deb/*.deb
|
||||
release-artifacts/windows/*.exe
|
||||
91
.github/workflows/docker.yml
vendored
Normal file
91
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Docker image tag
|
||||
required: false
|
||||
default: latest
|
||||
platforms:
|
||||
description: Docker platforms
|
||||
required: false
|
||||
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
DOCKER_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||
DOCKER_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 360
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# Reuse dependency artifacts produced by the deb job and vice versa.
|
||||
shared-key: linux-cross-release
|
||||
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||
cache-all-crates: true
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y unzip xz-utils
|
||||
cargo install cross --version 0.2.5 --locked
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: web
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build linux binaries
|
||||
run: bash build/build-images.sh
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Aliyun Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.cn-hangzhou.aliyuncs.com
|
||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
|
||||
- name: Publish Docker images
|
||||
run: |
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --variant full --tag "$DOCKER_TAG" --push
|
||||
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --variant full --tag "$DOCKER_TAG" --push
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
# Rust
|
||||
/target/
|
||||
Cargo.lock
|
||||
target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -30,6 +29,8 @@ Thumbs.db
|
||||
|
||||
# Build artifacts
|
||||
/dist/
|
||||
/build-staging
|
||||
/.tmp/
|
||||
|
||||
# Frontend (built files)
|
||||
/web/node_modules/
|
||||
@@ -40,3 +41,5 @@ CLAUDE.md
|
||||
# Secrets (compile-time configuration)
|
||||
secrets.toml
|
||||
.env
|
||||
/docs/
|
||||
web/package-lock.json
|
||||
|
||||
5652
Cargo.lock
generated
Normal file
5652
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
227
Cargo.toml
227
Cargo.toml
@@ -1,135 +1,212 @@
|
||||
[package]
|
||||
name = "one-kvm"
|
||||
version = "0.1.3"
|
||||
version = "0.2.8"
|
||||
edition = "2021"
|
||||
authors = ["SilentWind"]
|
||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||
license = "GPL-2.0"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/mofeng-git/One-KVM"
|
||||
keywords = ["kvm", "ipkvm", "remote-management", "embedded"]
|
||||
categories = ["embedded", "network-programming"]
|
||||
|
||||
[features]
|
||||
default = ["desktop"]
|
||||
desktop = [
|
||||
"dep:tokio",
|
||||
"dep:tokio-util",
|
||||
"dep:axum",
|
||||
"dep:axum-extra",
|
||||
"dep:tower-http",
|
||||
"dep:sqlx",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"dep:toml_edit",
|
||||
"dep:tracing",
|
||||
"dep:tracing-subscriber",
|
||||
"dep:thiserror",
|
||||
"dep:anyhow",
|
||||
"dep:argon2",
|
||||
"dep:rand",
|
||||
"dep:totp-rs",
|
||||
"dep:uuid",
|
||||
"dep:base64",
|
||||
"dep:nix",
|
||||
"dep:reqwest",
|
||||
"dep:urlencoding",
|
||||
"dep:rust-embed",
|
||||
"dep:mime_guess",
|
||||
"dep:openssl",
|
||||
"dep:rcgen",
|
||||
"dep:axum-server",
|
||||
"dep:clap",
|
||||
"dep:time",
|
||||
"dep:tempfile",
|
||||
"dep:bytes",
|
||||
"dep:bytemuck",
|
||||
"dep:xxhash-rust",
|
||||
"dep:async-stream",
|
||||
"dep:futures",
|
||||
"dep:tokio-tungstenite",
|
||||
"dep:parking_lot",
|
||||
"dep:arc-swap",
|
||||
"dep:webrtc",
|
||||
"dep:rtp",
|
||||
"dep:rtsp-types",
|
||||
"dep:sdp-types",
|
||||
"dep:serialport",
|
||||
"dep:async-trait",
|
||||
"dep:libc",
|
||||
"dep:ventoy-img",
|
||||
"dep:protobuf",
|
||||
"dep:sodiumoxide",
|
||||
"dep:des",
|
||||
"dep:sha2",
|
||||
"dep:typeshare",
|
||||
"dep:hwcodec",
|
||||
"dep:libyuv",
|
||||
"dep:turbojpeg",
|
||||
"dep:audiopus",
|
||||
"dep:v4l2r",
|
||||
"dep:alsa",
|
||||
"dep:gpio-cdev",
|
||||
"dep:one-kvm-bluetooth-hid",
|
||||
"dep:cpal",
|
||||
"dep:windows-sys",
|
||||
]
|
||||
[dependencies]
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["rt"] }
|
||||
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"], optional = true }
|
||||
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
||||
|
||||
# Web framework
|
||||
axum = { version = "0.8", features = ["ws", "multipart", "tokio"] }
|
||||
axum-extra = { version = "0.12", features = ["typed-header", "cookie"] }
|
||||
tower-http = { version = "0.6", features = ["fs", "cors", "trace", "compression-gzip"] }
|
||||
axum = { version = "0.8", features = ["ws", "multipart", "tokio"], optional = true }
|
||||
axum-extra = { version = "0.12", features = ["cookie"], optional = true }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
|
||||
|
||||
# Database - Use bundled SQLite for static linking
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite-bundled"], optional = true }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
toml_edit = { version = "0.25", optional = true }
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
||||
|
||||
# Error handling
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
thiserror = { version = "2", optional = true }
|
||||
anyhow = { version = "1", optional = true }
|
||||
|
||||
# Authentication
|
||||
argon2 = "0.5"
|
||||
rand = "0.9"
|
||||
argon2 = { version = "0.5", optional = true }
|
||||
rand = { version = "0.10", optional = true }
|
||||
totp-rs = { version = "5.7", features = ["gen_secret", "otpauth", "zeroize"], optional = true }
|
||||
|
||||
# Utilities
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
base64 = "0.22"
|
||||
nix = { version = "0.30", features = ["fs", "net", "hostname", "poll"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
tempfile = { version = "3", optional = true }
|
||||
|
||||
# HTTP client (for URL downloads)
|
||||
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
||||
reqwest = { version = "0.13", features = ["stream", "rustls"], default-features = false }
|
||||
urlencoding = "2"
|
||||
reqwest = { version = "0.13", features = ["stream", "native-tls-vendored", "json"], default-features = false, optional = true }
|
||||
urlencoding = { version = "2", optional = true }
|
||||
|
||||
# Static file embedding
|
||||
rust-embed = { version = "8", features = ["compression"] }
|
||||
mime_guess = "2"
|
||||
rust-embed = { version = "8", features = ["compression", "debug-embed"], optional = true }
|
||||
mime_guess = { version = "2", optional = true }
|
||||
|
||||
# TLS/HTTPS
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
rcgen = "0.14"
|
||||
axum-server = { version = "0.8", features = ["tls-rustls"] }
|
||||
openssl = { version = "0.10", features = ["vendored"], optional = true }
|
||||
rcgen = { version = "0.14", optional = true }
|
||||
axum-server = { version = "0.8", features = ["tls-openssl"], optional = true }
|
||||
|
||||
# CLI argument parsing
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
|
||||
# Time
|
||||
time = "0.3"
|
||||
|
||||
# Video capture (V4L2)
|
||||
v4l = "0.14"
|
||||
|
||||
# JPEG encoding (libjpeg-turbo, SIMD accelerated)
|
||||
turbojpeg = "1.3"
|
||||
# Time (cookie max_age + RFC3339 timestamps)
|
||||
time = { version = "0.3", features = ["serde", "formatting", "parsing"], optional = true }
|
||||
|
||||
# Bytes handling
|
||||
bytes = "1"
|
||||
bytemuck = { version = "1.24", features = ["derive"] }
|
||||
bytes = { version = "1", optional = true }
|
||||
bytemuck = { version = "1.24", features = ["derive"], optional = true }
|
||||
|
||||
# Frame deduplication (hash-based comparison)
|
||||
xxhash-rust = { version = "0.8", features = ["xxh64"] }
|
||||
xxhash-rust = { version = "0.8", features = ["xxh64"], optional = true }
|
||||
|
||||
# Async channels
|
||||
async-stream = "0.3"
|
||||
futures = "0.3"
|
||||
async-stream = { version = "0.3", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
|
||||
# WebSocket client (for ttyd proxy)
|
||||
tokio-tungstenite = "0.28"
|
||||
tokio-tungstenite = { version = "0.29", optional = true }
|
||||
|
||||
# High-performance synchronization
|
||||
parking_lot = "0.12"
|
||||
arc-swap = "1.8"
|
||||
parking_lot = { version = "0.12", optional = true }
|
||||
arc-swap = { version = "1.8", optional = true }
|
||||
|
||||
# WebRTC
|
||||
webrtc = "0.14"
|
||||
rtp = "0.14"
|
||||
|
||||
# Audio (ALSA capture + Opus encoding)
|
||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||
alsa = "0.11"
|
||||
audiopus = "0.2"
|
||||
webrtc = { version = "0.17", features = ["vendored-openssl"], optional = true }
|
||||
rtp = { version = "0.17", optional = true }
|
||||
rtsp-types = { version = "0.1", optional = true }
|
||||
sdp-types = { version = "0.1", optional = true }
|
||||
|
||||
# HID (serial port for CH9329)
|
||||
serialport = "4"
|
||||
async-trait = "0.1"
|
||||
libc = "0.2"
|
||||
serialport = { version = "4", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
|
||||
# Ventoy bootable image support
|
||||
ventoy-img = { path = "libs/ventoy-img-rs" }
|
||||
|
||||
# ATX (GPIO control)
|
||||
gpio-cdev = "0.6"
|
||||
|
||||
# H264 hardware/software encoding (hwcodec from rustdesk)
|
||||
hwcodec = { path = "libs/hwcodec" }
|
||||
ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
||||
|
||||
# RustDesk protocol support
|
||||
protobuf = { version = "3.7", features = ["with-bytes"] }
|
||||
sodiumoxide = "0.2"
|
||||
sha2 = "0.10"
|
||||
|
||||
# High-performance pixel format conversion (libyuv)
|
||||
libyuv = { path = "res/vcpkg/libyuv" }
|
||||
|
||||
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
||||
sodiumoxide = { version = "0.2", optional = true }
|
||||
des = { version = "0.9", optional = true }
|
||||
sha2 = { version = "0.11", optional = true }
|
||||
# TypeScript type generation
|
||||
typeshare = "1.0"
|
||||
typeshare = { version = "1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
tempfile = "3"
|
||||
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
||||
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
||||
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
||||
turbojpeg = { version = "1.3", optional = true }
|
||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||
audiopus = { version = "0.2", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
one-kvm-bluetooth-hid = { path = "libs/bluetooth-hid", optional = true }
|
||||
|
||||
# Utilities
|
||||
nix = { version = "0.31", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
||||
|
||||
# Video capture (V4L2)
|
||||
v4l2r = { path = "libs/v4l2r", optional = true }
|
||||
|
||||
# Audio (ALSA capture)
|
||||
alsa = { version = "0.11", optional = true }
|
||||
|
||||
# ATX (GPIO control)
|
||||
gpio-cdev = { version = "0.6", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
cpal = { version = "0.18", default-features = false, optional = true }
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Threading",
|
||||
], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
protobuf-codegen = "3.7"
|
||||
toml = "0.9"
|
||||
cc = "1"
|
||||
|
||||
[patch.crates-io]
|
||||
dtls = { path = "libs/dtls" }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Cross-compilation configuration for One-KVM
|
||||
# Uses pre-built custom Debian 11 based images for consistent build/runtime environment
|
||||
# Uses pre-built custom Debian 12 based images for consistent build/runtime environment
|
||||
# See: https://github.com/cross-rs/cross
|
||||
|
||||
[build]
|
||||
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
251
README.en.md
Normal file
251
README.en.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<div align="center">
|
||||
<h1>One-KVM</h1>
|
||||
<p><strong>An open, lightweight IP-KVM stack in Rust — remote management down to BIOS level</strong></p>
|
||||
|
||||
<p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
|
||||
|
||||
[](https://github.com/mofeng-git/One-KVM/releases)
|
||||
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**One-KVM (Rust)** is a lightweight IP-KVM solution written in Rust. It lets you manage servers and workstations over the network, including at BIOS level.
|
||||
|
||||
Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
||||
|
||||
- **Open**: not tied to one hardware recipe; runs across many setups.
|
||||
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
||||
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
||||
|
||||
For more information, see the [One-KVM Rust documentation](https://docs.one-kvm.cn/).
|
||||
|
||||
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
**Core**
|
||||
|
||||
| Area | Capabilities |
|
||||
|------|----------------|
|
||||
| Video capture | HDMI USB / MIPI CSI / RK3588 HDMI IN; MJPEG and WebRTC (H.264 / H.265 / VP8 / VP9) |
|
||||
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
||||
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
||||
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
||||
| ATX power | GPIO or USB relay; power control |
|
||||
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
||||
|
||||
## Installation
|
||||
|
||||
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
||||
|
||||
### Debian / Ubuntu (.deb)
|
||||
|
||||
Download a `one-kvm_*.deb` matching your CPU architecture from Releases, then from the directory containing the package:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install ./one-kvm_0.x.x_<arch>.deb
|
||||
```
|
||||
|
||||
Replace the version and architecture in the filename with your actual file name.
|
||||
|
||||
### Docker
|
||||
|
||||
Images:
|
||||
|
||||
- **one-kvm** — main app + ttyd
|
||||
- **one-kvm-full** — same plus optional extras (e.g. gostc, easytier-core)
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run --name one-kvm -itd \
|
||||
--privileged=true --restart unless-stopped \
|
||||
-v /dev:/dev -v /sys:/sys \
|
||||
--net=host \
|
||||
silentwind0/one-kvm-full
|
||||
```
|
||||
|
||||
If pulls are slow, use the Aliyun mirror, e.g. `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full` (and `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm` for the slim image).
|
||||
|
||||
### fnOS NAS (Feiniu / 飞牛)
|
||||
|
||||
One-KVM is listed in the fnOS **app store**; search and install on your NAS.
|
||||
|
||||
### Web UI and first run
|
||||
|
||||
Open `http://<device-ip>:8080` in a browser (**8420** after fnOS install). The first visit runs initial setup.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
If something breaks:
|
||||
|
||||
1. Open [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) or report in the project QQ group.
|
||||
2. Include **useful** error messages and steps to reproduce.
|
||||
3. Mention software version, hardware, and OS details.
|
||||
|
||||
## Sponsorship
|
||||
|
||||
One-KVM builds on many great open-source projects; a lot of time goes into testing and maintenance. If you find it useful, you can support development on **[Afdian (为爱发电)](https://afdian.com/a/silentwind)**.
|
||||
|
||||
### Thanks
|
||||
|
||||
<details>
|
||||
<summary><strong>Supporter list</strong></summary>
|
||||
|
||||
- 浩龙的电子嵌入式之路
|
||||
|
||||
- Tsuki
|
||||
|
||||
- H_xiaoming
|
||||
|
||||
- 0蓝蓝0
|
||||
|
||||
- fairybl
|
||||
|
||||
- Will
|
||||
|
||||
- 自.知
|
||||
|
||||
- 观棋不语٩ ི۶
|
||||
|
||||
- 爱发电用户_a57a4
|
||||
|
||||
- 爱发电用户_2c769
|
||||
|
||||
- 霜序
|
||||
|
||||
- 远方(闲鱼用户名:小远技术店铺)
|
||||
|
||||
- 爱发电用户_399fc
|
||||
|
||||
- 斐斐の
|
||||
|
||||
- 爱发电用户_09451
|
||||
|
||||
- 超高校级的錆鱼
|
||||
|
||||
- 爱发电用户_08cff
|
||||
|
||||
- guoke
|
||||
|
||||
- mgt
|
||||
|
||||
- 姜沢掵
|
||||
|
||||
- ui_beam
|
||||
|
||||
- 爱发电用户_c0dd7
|
||||
|
||||
- 爱发电用户_dnjK
|
||||
|
||||
- 忍者胖猪
|
||||
|
||||
- 永遠の願い
|
||||
|
||||
- 爱发电用户_GBrF
|
||||
|
||||
- 爱发电用户_fd65c
|
||||
|
||||
- 爱发电用户_vhNa
|
||||
|
||||
- 爱发电用户_Xu6S
|
||||
|
||||
- moss
|
||||
|
||||
- woshididi
|
||||
|
||||
- 爱发电用户_a0fd1
|
||||
|
||||
- 爱发电用户_f6bH
|
||||
|
||||
- 码农
|
||||
|
||||
- 爱发电用户_6639f
|
||||
|
||||
- jeron
|
||||
|
||||
- 爱发电用户_CN7y
|
||||
|
||||
- 爱发电用户_Up6w
|
||||
|
||||
- 爱发电用户_e3202
|
||||
|
||||
- 一语念白
|
||||
|
||||
- 云边
|
||||
|
||||
- 爱发电用户_5a711
|
||||
|
||||
- 爱发电用户_9a706
|
||||
|
||||
- T0m9ir1SUKI
|
||||
|
||||
- 爱发电用户_56d52
|
||||
|
||||
- 爱发电用户_3N6F
|
||||
|
||||
- DUSK
|
||||
|
||||
- 飘零
|
||||
|
||||
- .
|
||||
|
||||
- 饭太稀
|
||||
|
||||
- 葱
|
||||
|
||||
- MaxZ
|
||||
|
||||
- 爱发电用户_c5f33
|
||||
|
||||
- 爱发电用户_09386
|
||||
|
||||
- 爱发电用户_JT6c
|
||||
|
||||
- 爱发电用户_d3d9c
|
||||
|
||||
- 爱发电用户_97b41
|
||||
|
||||
- 偶然
|
||||
|
||||
- 爱发电用户_dba45
|
||||
|
||||
- 爱发电用户_d4f8b
|
||||
|
||||
- 故人。
|
||||
|
||||
- ......
|
||||
|
||||
</details>
|
||||
|
||||
### Sponsors
|
||||
|
||||
**File hosting**
|
||||
|
||||
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
||||
|
||||
**Cloud**
|
||||
|
||||
- **[林枫云](https://www.dkdun.cn)** — project server sponsorship
|
||||
|
||||

|
||||
|
||||
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
||||
|
||||
- **[Beta Network](https://my.beita.cc/?ref=github_onekvm)** — project server sponsorship
|
||||
|
||||

|
||||
|
||||
Remote computers, consumer GPU servers, and dedicated physical machines with fully automated online delivery.
|
||||
113
README.md
113
README.md
@@ -2,77 +2,92 @@
|
||||
<h1>One-KVM</h1>
|
||||
<p><strong>Rust 编写的开放轻量 IP-KVM 解决方案,实现 BIOS 级远程管理</strong></p>
|
||||
|
||||
<p><a href="README.md">简体中文</a></p>
|
||||
<p><a href="README.md">简体中文</a> · <a href="README.en.md">English</a></p>
|
||||
|
||||
[](https://github.com/mofeng-git/One-KVM/releases)
|
||||
[](https://github.com/mofeng-git/One-KVM/stargazers)
|
||||
[](https://github.com/mofeng-git/One-KVM/network/members)
|
||||
[](https://github.com/mofeng-git/One-KVM/issues)
|
||||
|
||||
<p>
|
||||
<a href="docs/README.md">📖 技术文档</a> •
|
||||
<a href="#快速开始">⚡ 快速开始</a> •
|
||||
<a href="#功能介绍">📊 功能介绍</a> •
|
||||
<a href="#迁移说明">🔁 迁移说明</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📖 项目概述
|
||||
## 项目概述
|
||||
|
||||
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
||||
|
||||
项目目标:
|
||||
项目目标:提供一个开放、轻量、易用的 IPKVM 解决方案。
|
||||
|
||||
- **开放**:不绑定特定硬件配置,尽量适配常见 Linux 设备
|
||||
- **轻量**:单二进制分发,部署过程更简单
|
||||
- **易用**:网页界面完成设备与参数配置,尽量减少手动改配置文件
|
||||
- **开放**:不绑定特定硬件配置,可在各类硬件环境中稳定运行。
|
||||
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
||||
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
||||
|
||||
> **注意:** One-KVM Rust 目前仍处于开发早期阶段,功能与细节会快速迭代,欢迎体验与反馈。
|
||||
更多内容可访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||
|
||||
## 🔁 迁移说明
|
||||
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
||||
|
||||
开发重心正在从 **One-KVM Python** 逐步转向 **One-KVM Rust**。
|
||||
<div align="center">
|
||||
|
||||
- 如果你在使用 **One-KVM Python(基于 PiKVM)**,请查看 [One-KVM Python 文档](https://docs.one-kvm.cn/python/)
|
||||
- One-KVM Rust 相较于 One-KVM Python:**尚未适配 CSI HDMI 采集卡**、**不支持 VNC 访问**,仍处于开发早期阶段
|
||||

|
||||
|
||||
## 📊 功能介绍
|
||||
</div>
|
||||
|
||||
### 核心功能
|
||||
## 功能介绍
|
||||
|
||||
| 功能 | 说明 |
|
||||
**核心功能**
|
||||
|
||||
| 功能 | 能力说明 |
|
||||
|------|------|
|
||||
| 视频采集 | HDMI USB 采集卡支持,提供 MJPEG / WebRTC(H.264/H.265/VP8/VP9) |
|
||||
| 视频采集 | HDMI USB /MIPI CSI/RK3588 HDMI IN 采集支持,提供 MJPEG / WebRTC(H.264/H.265/VP8/VP9) 视频流|
|
||||
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
||||
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
||||
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
||||
| ATX 电源控制 | GPIO 控制电源/重启按钮 |
|
||||
| ATX 电源控制 | GPIO/USB 继电器,支持电源控制 |
|
||||
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
||||
|
||||
### 硬件编码
|
||||
## 安装使用
|
||||
|
||||
支持自动检测和选择硬件加速:
|
||||
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||
|
||||
- **VAAPI**:Intel/AMD GPU
|
||||
- **RKMPP**:Rockchip SoC
|
||||
- **V4L2 M2M**:通用硬件编码器(尚未实现)
|
||||
- **软件编码**:CPU 编码
|
||||
### 使用 deb 安装(Debian / Ubuntu)
|
||||
|
||||
### 扩展能力
|
||||
从 Releases 下载与本机架构匹配的 `one-kvm_*.deb`,在包所在目录执行:
|
||||
|
||||
- Web UI 配置,多语言支持(中文/英文)
|
||||
- 内置 Web 终端(ttyd)内网穿透支持(gostc)、P2P 组网支持(EasyTier)、RustDesk 协议集成(用于跨平台远程访问能力扩展)
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install ./one-kvm_0.x.x_<arch>.deb
|
||||
```
|
||||
|
||||
## ⚡ 安装使用
|
||||
将文件名中的版本号与架构替换为实际下载的包名。
|
||||
|
||||
可以访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/) 获取详细信息。
|
||||
### 使用 Docker
|
||||
|
||||
镜像分为 **one-kvm**(One-KVM 主程序 + ttyd)与 **one-kvm-full**(另含 gostc、easytier-core 等可选扩展),按需选用。
|
||||
|
||||
```bash
|
||||
docker run --name one-kvm -itd \
|
||||
--privileged=true --restart unless-stopped \
|
||||
-v /dev:/dev -v /sys:/sys \
|
||||
--net=host \
|
||||
silentwind0/one-kvm-full
|
||||
```
|
||||
|
||||
拉取较慢时,可将镜像名替换为阿里云加速,例如 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm-full`(`one-kvm` 镜像同理,将 `silentwind0/one-kvm` 换为 `registry.cn-hangzhou.aliyuncs.com/silentwind/one-kvm`)。
|
||||
|
||||
### 飞牛 NAS
|
||||
|
||||
One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可。
|
||||
|
||||
### 访问 Web 与首次配置
|
||||
|
||||
浏览器访问 `http://<设备 IP>:8080`(飞牛 NAS 安装后为 8420 端口)。首次访问将引导完成初始配置。
|
||||
|
||||
## 报告问题
|
||||
|
||||
如果您发现了问题,请:
|
||||
1. 使用 [GitHub Issues](https://github.com/mofeng-git/One-KVM/issues) 报告,或加入 QQ 群聊反馈。
|
||||
2. 提供详细的错误信息和复现步骤
|
||||
3. 包含您的硬件配置和系统信息
|
||||
2. 提供有帮助的错误信息和复现步骤
|
||||
3. 包含您使用的软件版本、硬件配置和系统信息
|
||||
|
||||
## 赞助支持
|
||||
|
||||
@@ -95,8 +110,6 @@
|
||||
|
||||
- Will
|
||||
|
||||
- 浩龙的电子嵌入式之路
|
||||
|
||||
- 自.知
|
||||
|
||||
- 观棋不语٩ ི۶
|
||||
@@ -195,6 +208,18 @@
|
||||
|
||||
- 爱发电用户_JT6c
|
||||
|
||||
- 爱发电用户_d3d9c
|
||||
|
||||
- 爱发电用户_97b41
|
||||
|
||||
- 偶然
|
||||
|
||||
- 爱发电用户_dba45
|
||||
|
||||
- 爱发电用户_d4f8b
|
||||
|
||||
- 故人。
|
||||
|
||||
- ......
|
||||
|
||||
</details>
|
||||
@@ -208,8 +233,14 @@
|
||||
|
||||
**云服务商**
|
||||
|
||||
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目宁波大带宽服务器
|
||||
- **[林枫云](https://www.dkdun.cn)** - 赞助了本项目服务器
|
||||
|
||||

|
||||
<img height="128" alt="林枫云" src="https://docs.one-kvm.cn/img/36076FEFF0898A80EBD5756D28F4076C.png" />
|
||||
|
||||
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
|
||||
林枫云主营国内外地域的精品线路业务服务器、高主频游戏服务器和大带宽服务器。
|
||||
|
||||
- **[贝塔网络](https://my.beita.cc/?ref=github_onekvm)** - 赞助了本项目服务器
|
||||
|
||||
<img height="128" alt="BTBT" src="https://github.com/user-attachments/assets/c442d5f5-d72f-4a07-b9f4-400a6a0c3f1e" />
|
||||
|
||||
远程电脑、消费级GPU服务器、独服物理机,全自动在线交付。
|
||||
|
||||
50
build.rs
50
build.rs
@@ -2,21 +2,6 @@ use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
// Set BUILD_DATE environment variable for compile-time access
|
||||
// Use system time to avoid adding chrono as a build dependency
|
||||
let now = std::time::SystemTime::now();
|
||||
let duration = now.duration_since(std::time::UNIX_EPOCH).unwrap();
|
||||
let secs = duration.as_secs();
|
||||
|
||||
// Convert Unix timestamp to date (simplified calculation)
|
||||
// Days since epoch
|
||||
let days = secs / 86400;
|
||||
// Calculate year, month, day from days since 1970-01-01
|
||||
let (year, month, day) = days_to_ymd(days as i64);
|
||||
let build_date = format!("{:04}-{:02}-{:02}", year, month, day);
|
||||
|
||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
||||
|
||||
// Compile protobuf files for RustDesk protocol
|
||||
compile_protos();
|
||||
|
||||
@@ -64,25 +49,6 @@ fn generate_secrets() {
|
||||
pub mod ice {
|
||||
/// Google public STUN server URL (hardcoded)
|
||||
pub const STUN_SERVER: &str = "stun:stun.l.google.com:19302";
|
||||
|
||||
/// TURN server URLs - not provided, users must configure their own
|
||||
pub const TURN_URLS: &str = "";
|
||||
|
||||
/// TURN authentication username
|
||||
pub const TURN_USERNAME: &str = "";
|
||||
|
||||
/// TURN authentication password
|
||||
pub const TURN_PASSWORD: &str = "";
|
||||
|
||||
/// Always returns true since we have STUN
|
||||
pub const fn is_configured() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Always returns false since TURN is not provided
|
||||
pub const fn has_turn() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// RustDesk public server configuration - NOT PROVIDED
|
||||
@@ -105,19 +71,3 @@ pub mod rustdesk {
|
||||
|
||||
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
||||
}
|
||||
|
||||
/// Convert days since Unix epoch to year-month-day
|
||||
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
|
||||
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
|
||||
let z = days + 719468;
|
||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||
let doe = (z - era * 146097) as u32;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe as i64 + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let year = if m <= 2 { y + 1 } else { y };
|
||||
(year as i32, m, d)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
# One-KVM Runtime Image
|
||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||
# Used after cross-compiling with `cross build`
|
||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
||||
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||
|
||||
ARG TARGETPLATFORM=linux/amd64
|
||||
|
||||
FROM debian:11-slim
|
||||
FROM debian:12-slim
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG APT_MIRROR=
|
||||
|
||||
# Install runtime dependencies in a single layer
|
||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||
RUN apt-get update && \
|
||||
# Set APT_MIRROR=mirrors.tuna.tsinghua.edu.cn to speed up builds in China
|
||||
RUN if [ -n "$APT_MIRROR" ]; then \
|
||||
sed -i "s@deb.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources; \
|
||||
fi \
|
||||
&& sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Core runtime (all platforms) - no codec libs needed
|
||||
ca-certificates \
|
||||
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||
bluez \
|
||||
libudev1 \
|
||||
libasound2 \
|
||||
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||
network-manager \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
# v4l2 is handled by kernel, minimal userspace needed
|
||||
libv4l-0 \
|
||||
&& \
|
||||
# Platform-specific hardware acceleration
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1; \
|
||||
libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1 \
|
||||
i965-va-driver-shaders intel-media-va-driver-non-free vainfo; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libdrm2 libva2; \
|
||||
|
||||
@@ -1,30 +1,43 @@
|
||||
# One-KVM Runtime Image (full)
|
||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||
# Used after cross-compiling with `cross build`
|
||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
||||
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||
|
||||
ARG TARGETPLATFORM=linux/amd64
|
||||
|
||||
FROM debian:11-slim
|
||||
FROM debian:12-slim
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG APT_MIRROR=
|
||||
|
||||
# Install runtime dependencies in a single layer
|
||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||
RUN apt-get update && \
|
||||
# Set APT_MIRROR=mirrors.tuna.tsinghua.edu.cn to speed up builds in China
|
||||
RUN if [ -n "$APT_MIRROR" ]; then \
|
||||
sed -i "s@deb.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources; \
|
||||
fi \
|
||||
&& sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Core runtime (all platforms) - no codec libs needed
|
||||
ca-certificates \
|
||||
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||
bluez \
|
||||
libudev1 \
|
||||
libasound2 \
|
||||
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||
network-manager \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
# v4l2 is handled by kernel, minimal userspace needed
|
||||
libv4l-0 \
|
||||
&& \
|
||||
# Platform-specific hardware acceleration
|
||||
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1; \
|
||||
libva2 libva-drm2 libva-x11-2 libx11-6 libxcb1 libxau6 libxdmcp6 libmfx1 \
|
||||
i965-va-driver-shaders intel-media-va-driver-non-free vainfo; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libdrm2 libva2; \
|
||||
|
||||
@@ -19,6 +19,32 @@ ARCH_MAP=(
|
||||
build_arch() {
|
||||
local rust_target="$1"
|
||||
|
||||
case "${CHINAMIRRO:-}" in
|
||||
1|true|TRUE|yes|YES|on|ON)
|
||||
# Default Rust toolchain mirror is USTC (rsproxy.cn is unreachable via the
|
||||
# Docker bridge on some networks because its CDN blocks the SNAT source IP).
|
||||
local rust_dist="${RUSTUP_DIST_SERVER_MIRROR:-https://mirrors.ustc.edu.cn/rust-static}"
|
||||
local rust_root="${RUSTUP_UPDATE_ROOT_MIRROR:-https://mirrors.ustc.edu.cn/rust-static/rustup}"
|
||||
local crates_sparse="${CARGO_SPARSE_MIRROR:-sparse+https://mirrors.ustc.edu.cn/crates.io-index/}"
|
||||
local crates_git="${CARGO_GIT_MIRROR:-https://mirrors.ustc.edu.cn/crates.io-index}"
|
||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--progress=plain --build-arg CHINAMIRRO=1 --build-arg GH_PROXY=${GH_PROXY:-https://ghfast.top/} --build-arg DEBIAN_IMAGE=${DEBIAN_IMAGE:-dockerproxy.net/library/debian:12}"
|
||||
cross_build_opts="$cross_build_opts --build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg ALL_PROXY= --build-arg NO_PROXY="
|
||||
cross_build_opts="$cross_build_opts --build-arg http_proxy= --build-arg https_proxy= --build-arg all_proxy= --build-arg no_proxy="
|
||||
echo "=== China mirror acceleration: enabled ==="
|
||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||
env \
|
||||
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=cn-mirror \
|
||||
CARGO_SOURCE_CN-MIRROR_REGISTRY="$crates_sparse" \
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||
RUSTUP_DIST_SERVER="$rust_dist" \
|
||||
RUSTUP_UPDATE_ROOT="$rust_root" \
|
||||
cross build --release --target "$rust_target"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||
cross build --release --target "$rust_target"
|
||||
}
|
||||
@@ -49,6 +75,7 @@ case "${1:-all}" in
|
||||
echo "Examples:"
|
||||
echo " $0 # Build all"
|
||||
echo " $0 x86_64 # Build x86_64 only"
|
||||
echo " CHINAMIRRO=1 $0 arm64 # Build with China mirrors"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
@@ -65,16 +92,4 @@ for target in "${ARCH_MAP[@]}"; do
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Static libraries:"
|
||||
for target in "${ARCH_MAP[@]}"; do
|
||||
case "$target" in
|
||||
x86_64-unknown-linux-gnu) gnu_target="x86_64-linux-gnu" ;;
|
||||
aarch64-unknown-linux-gnu) gnu_target="aarch64-linux-gnu" ;;
|
||||
armv7-unknown-linux-gnueabihf) gnu_target="armv7-linux-gnueabihf" ;;
|
||||
esac
|
||||
if [ -d "$PROJECT_DIR/target/one-kvm-libs/$gnu_target/lib" ]; then
|
||||
echo " $gnu_target: OK"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
||||
|
||||
@@ -1,17 +1,55 @@
|
||||
# Cross-compilation image for ARM64 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
# Cross-compilation image for ARM64 based on Debian 12
|
||||
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:12
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Set Rustup mirrors (Aliyun)
|
||||
ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
||||
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||
ARG RKRGA_BRANCH=jellyfin-rga
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i -E \
|
||||
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||
/etc/apt/sources.list.d/debian.sources; \
|
||||
fi
|
||||
|
||||
# Install Rust toolchain
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'cn-mirror-sparse'" \
|
||||
'[source.cn-mirror]' \
|
||||
"registry = '${CARGO_INDEX_CN}'" \
|
||||
'[source.cn-mirror-sparse]' \
|
||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||
'[registries.cn-mirror]' \
|
||||
"index = '${CARGO_INDEX_CN}'" \
|
||||
'[net]' \
|
||||
'git-fetch-with-cli = true' \
|
||||
> /root/.cargo/config.toml; \
|
||||
fi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
@@ -22,6 +60,7 @@ RUN dpkg --add-architecture arm64
|
||||
# Install cross-compiler and native build tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
patch \
|
||||
pkg-config \
|
||||
cmake \
|
||||
nasm \
|
||||
@@ -31,7 +70,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
meson \
|
||||
ninja-build \
|
||||
wget \
|
||||
xz-utils \
|
||||
file \
|
||||
rsync \
|
||||
gcc-aarch64-linux-gnu \
|
||||
g++-aarch64-linux-gnu \
|
||||
libc6-dev-arm64-cross \
|
||||
@@ -43,20 +84,29 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libssl3 \
|
||||
libssl3:arm64 \
|
||||
libasound2-dev:arm64 \
|
||||
libv4l-dev:arm64 \
|
||||
libudev-dev:arm64 \
|
||||
zlib1g-dev:arm64 \
|
||||
linux-libc-dev:arm64 \
|
||||
# Note: libjpeg-turbo, libyuv, libvpx, libx264, libx265, libopus are built from source below for static linking
|
||||
libdrm-dev:arm64 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
||||
@@ -66,8 +116,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
@@ -75,14 +130,24 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
|
||||
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
|
||||
-DJPEG_FOUND=TRUE \
|
||||
-DJPEG_INCLUDE_DIR=/usr/aarch64-linux-gnu/include \
|
||||
-DJPEG_LIBRARY=/usr/aarch64-linux-gnu/lib/libjpeg.a \
|
||||
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/aarch64-linux-gnu/include" \
|
||||
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/aarch64-linux-gnu/include" \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libyuv
|
||||
|
||||
# Build static libvpx from source (cross-compile for ARM64)
|
||||
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export CXX=aarch64-linux-gnu-g++ \
|
||||
@@ -105,7 +170,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export AR=aarch64-linux-gnu-ar \
|
||||
@@ -121,12 +188,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||
@@ -160,12 +233,15 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source (cross-compile for ARM64)
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& export CC=aarch64-linux-gnu-gcc \
|
||||
&& export AR=aarch64-linux-gnu-ar \
|
||||
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--prefix=/usr/aarch64-linux-gnu \
|
||||
--host=aarch64-linux-gnu \
|
||||
@@ -173,13 +249,24 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Reject raw formats smaller than the requested frame before any V4L2 upload.
|
||||
COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch
|
||||
|
||||
# Download and build FFmpeg with RKMPP support
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& patch -p1 < /tmp/ffmpeg-v4l2-size.patch \
|
||||
&& cd .. \
|
||||
# Build RKMPP
|
||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||
&& cmake .. \
|
||||
@@ -194,6 +281,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
||||
&& rm -f /usr/aarch64-linux-gnu/lib/librockchip_mpp.so* \
|
||||
&& cd ../.. \
|
||||
# Build RKRGA - create cross file for meson
|
||||
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
||||
@@ -221,6 +309,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
||||
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
||||
&& rm -f /usr/aarch64-linux-gnu/lib/librga.so* \
|
||||
&& cd .. \
|
||||
# Create pkg-config wrapper for cross-compilation
|
||||
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
||||
@@ -309,7 +398,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& rm -rf /tmp/ffmpeg-build /tmp/aarch64-cross.txt /tmp/aarch64-pkg-config
|
||||
|
||||
# Add Rust target
|
||||
RUN rustup target add aarch64-unknown-linux-gnu
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
# Configure environment for cross-compilation
|
||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
||||
@@ -323,4 +416,4 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
||||
LIBYUV_STATIC=1 \
|
||||
OPUS_STATIC=1 \
|
||||
PKG_CONFIG_ALL_STATIC=1 \
|
||||
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
||||
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||
|
||||
@@ -1,17 +1,55 @@
|
||||
# Cross-compilation image for ARMv7 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
# Cross-compilation image for ARMv7 based on Debian 12
|
||||
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:12
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Set Rustup mirrors (Aliyun)
|
||||
ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
||||
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||
ARG RKRGA_BRANCH=jellyfin-rga
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i -E \
|
||||
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||
/etc/apt/sources.list.d/debian.sources; \
|
||||
fi
|
||||
|
||||
# Install Rust toolchain
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'cn-mirror-sparse'" \
|
||||
'[source.cn-mirror]' \
|
||||
"registry = '${CARGO_INDEX_CN}'" \
|
||||
'[source.cn-mirror-sparse]' \
|
||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||
'[registries.cn-mirror]' \
|
||||
"index = '${CARGO_INDEX_CN}'" \
|
||||
'[net]' \
|
||||
'git-fetch-with-cli = true' \
|
||||
> /root/.cargo/config.toml; \
|
||||
fi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
@@ -22,6 +60,7 @@ RUN dpkg --add-architecture armhf
|
||||
# Install cross-compiler and native build tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
patch \
|
||||
pkg-config \
|
||||
cmake \
|
||||
nasm \
|
||||
@@ -31,7 +70,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
meson \
|
||||
ninja-build \
|
||||
wget \
|
||||
xz-utils \
|
||||
file \
|
||||
rsync \
|
||||
gcc-arm-linux-gnueabihf \
|
||||
g++-arm-linux-gnueabihf \
|
||||
libc6-dev-armhf-cross \
|
||||
@@ -43,19 +84,28 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libssl3 \
|
||||
libssl3:armhf \
|
||||
libasound2-dev:armhf \
|
||||
libv4l-dev:armhf \
|
||||
libudev-dev:armhf \
|
||||
linux-libc-dev:armhf \
|
||||
zlib1g-dev:armhf \
|
||||
libdrm-dev:armhf \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
||||
@@ -65,8 +115,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
@@ -74,13 +129,23 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
|
||||
-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++ \
|
||||
-DJPEG_FOUND=TRUE \
|
||||
-DJPEG_INCLUDE_DIR=/usr/arm-linux-gnueabihf/include \
|
||||
-DJPEG_LIBRARY=/usr/arm-linux-gnueabihf/lib/libjpeg.a \
|
||||
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/arm-linux-gnueabihf/include" \
|
||||
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/arm-linux-gnueabihf/include" \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libyuv
|
||||
|
||||
# Build static libvpx from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export CXX=arm-linux-gnueabihf-g++ \
|
||||
&& export LD=arm-linux-gnueabihf-ld \
|
||||
@@ -97,7 +162,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export AR=arm-linux-gnueabihf-ar \
|
||||
@@ -114,12 +181,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||
@@ -149,12 +222,15 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source (cross-compile for ARMv7)
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& export CC=arm-linux-gnueabihf-gcc \
|
||||
&& export AR=arm-linux-gnueabihf-ar \
|
||||
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--prefix=/usr/arm-linux-gnueabihf \
|
||||
--host=arm-linux-gnueabihf \
|
||||
@@ -162,13 +238,24 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Reject raw formats smaller than the requested frame before any V4L2 upload.
|
||||
COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch
|
||||
|
||||
# Download and build FFmpeg with RKMPP support
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& patch -p1 < /tmp/ffmpeg-v4l2-size.patch \
|
||||
&& cd .. \
|
||||
# Build RKMPP
|
||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||
&& cmake .. \
|
||||
@@ -183,6 +270,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
||||
&& rm -f /usr/arm-linux-gnueabihf/lib/librockchip_mpp.so* \
|
||||
&& cd ../.. \
|
||||
# Build RKRGA - create cross file for meson
|
||||
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
||||
@@ -210,6 +298,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
||||
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
||||
&& rm -f /usr/arm-linux-gnueabihf/lib/librga.so* \
|
||||
&& cd .. \
|
||||
# Create pkg-config wrapper for cross-compilation
|
||||
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
||||
@@ -298,7 +387,11 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& rm -rf /tmp/ffmpeg-build /tmp/armhf-cross.txt /tmp/armhf-pkg-config
|
||||
|
||||
# Add Rust target
|
||||
RUN rustup target add armv7-unknown-linux-gnueabihf
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||
|
||||
# Configure environment for cross-compilation
|
||||
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
||||
@@ -312,7 +405,7 @@ ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
||||
LIBYUV_STATIC=1 \
|
||||
OPUS_STATIC=1 \
|
||||
PKG_CONFIG_ALL_STATIC=1 \
|
||||
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc"
|
||||
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
# Cross-compilation image for x86_64 based on Debian 11
|
||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
||||
# Cross-compilation image for x86_64 based on Debian 12
|
||||
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||
|
||||
FROM debian:11
|
||||
ARG DEBIAN_IMAGE=debian:12
|
||||
FROM ${DEBIAN_IMAGE}
|
||||
|
||||
# Set Rustup mirrors (Aliyun)
|
||||
ENV RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup \
|
||||
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
|
||||
ARG CHINAMIRRO=0
|
||||
ARG GH_PROXY=https://gh-proxy.com/
|
||||
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||
ARG LIBVPX_VERSION=1.16.0
|
||||
ARG X265_VERSION=3.4
|
||||
ARG OPUS_VERSION=1.5.2
|
||||
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||
|
||||
# Optionally use China mirrors for builds in China.
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
sed -i -E \
|
||||
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||
/etc/apt/sources.list.d/debian.sources; \
|
||||
fi
|
||||
|
||||
# Install Rust toolchain
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
ca-certificates \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
mkdir -p /root/.cargo; \
|
||||
printf '%s\n' \
|
||||
'[source.crates-io]' \
|
||||
"replace-with = 'cn-mirror-sparse'" \
|
||||
'[source.cn-mirror]' \
|
||||
"registry = '${CARGO_INDEX_CN}'" \
|
||||
'[source.cn-mirror-sparse]' \
|
||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||
'[registries.cn-mirror]' \
|
||||
"index = '${CARGO_INDEX_CN}'" \
|
||||
'[net]' \
|
||||
'git-fetch-with-cli = true' \
|
||||
> /root/.cargo/config.toml; \
|
||||
fi \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
@@ -29,6 +65,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libclang-dev \
|
||||
llvm \
|
||||
wget \
|
||||
xz-utils \
|
||||
rsync \
|
||||
# Autotools for libopus (requires autoreconf)
|
||||
autoconf \
|
||||
automake \
|
||||
@@ -37,6 +75,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libasound2-dev \
|
||||
libv4l-dev \
|
||||
libudev-dev \
|
||||
linux-libc-dev \
|
||||
zlib1g-dev \
|
||||
# Note: libjpeg-turbo, libx264, libx265, libopus are built from source below for static linking
|
||||
libva-dev \
|
||||
@@ -50,36 +89,61 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build static libjpeg-turbo from source (needed by libyuv)
|
||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libjpeg-turbo \
|
||||
&& cd /tmp/libjpeg-turbo \
|
||||
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DENABLE_SHARED=OFF -DENABLE_STATIC=ON \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libjpeg-turbo
|
||||
|
||||
# Build static libyuv from source (uses libjpeg-turbo headers)
|
||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
||||
RUN git init /tmp/libyuv \
|
||||
&& cd /tmp/libyuv \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& mkdir build && cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DJPEG_FOUND=TRUE \
|
||||
-DJPEG_INCLUDE_DIR=/usr/local/include \
|
||||
-DJPEG_LIBRARY=/usr/local/lib/libjpeg.a \
|
||||
-DCMAKE_C_FLAGS="-DHAVE_JPEG -I/usr/local/include" \
|
||||
-DCMAKE_CXX_FLAGS="-DHAVE_JPEG -I/usr/local/include" \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libyuv
|
||||
|
||||
# Build static libvpx from source
|
||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/libvpx \
|
||||
&& cd /tmp/libvpx \
|
||||
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& ./configure \
|
||||
--enable-static --disable-shared --enable-pic \
|
||||
--disable-examples --disable-tools --disable-docs \
|
||||
--disable-unit-tests \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libvpx
|
||||
|
||||
# Build static libx264 from source
|
||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||
&& cd /tmp/x264 \
|
||||
&& ./configure --enable-static --disable-cli \
|
||||
&& make -j$(nproc) \
|
||||
@@ -87,12 +151,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
||||
&& rm -rf /tmp/x264
|
||||
|
||||
# Build static libx265 from source
|
||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init /tmp/x265 \
|
||||
&& cd /tmp/x265 \
|
||||
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& cd source \
|
||||
&& mkdir -p build \
|
||||
&& cd build \
|
||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||
-DENABLE_SHARED=OFF \
|
||||
-DENABLE_CLI=OFF \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
@@ -117,21 +187,28 @@ Cflags: -I\${includedir}
|
||||
EOF
|
||||
|
||||
# Build static libopus from source
|
||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
||||
RUN github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||
&& cd /tmp/opus \
|
||||
&& ./autogen.sh \
|
||||
&& ./configure \
|
||||
--enable-static --disable-shared \
|
||||
--disable-doc \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/opus
|
||||
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||
|
||||
# Download and build FFmpeg with minimal configuration for encoding only
|
||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
||||
&& tar -xzf ffmpeg.tar.gz \
|
||||
&& cd ffmpeg/ffmpeg-rockchip \
|
||||
&& github_prefix="" \
|
||||
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||
&& git init ffmpeg-rockchip \
|
||||
&& cd ffmpeg-rockchip \
|
||||
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||
&& git checkout --detach FETCH_HEAD \
|
||||
&& ./configure \
|
||||
--enable-gpl \
|
||||
--enable-version3 \
|
||||
@@ -203,9 +280,13 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||
&& rm -rf /tmp/ffmpeg-build
|
||||
|
||||
# Add Rust target
|
||||
RUN rustup target add x86_64-unknown-linux-gnu
|
||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||
fi \
|
||||
&& rustup target add x86_64-unknown-linux-gnu
|
||||
|
||||
# Configure environment for static linking
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1\
|
||||
FFMPEG_STATIC=1 \
|
||||
LIBYUV_STATIC=1
|
||||
LIBYUV_STATIC=1
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
--- a/libavcodec/v4l2_m2m_enc.c
|
||||
+++ b/libavcodec/v4l2_m2m_enc.c
|
||||
@@ -338,6 +338,7 @@
|
||||
V4L2m2mPriv *priv = avctx->priv_data;
|
||||
enum AVPixelFormat pix_fmt_output;
|
||||
uint32_t v4l2_fmt_output;
|
||||
+ unsigned int output_width, output_height;
|
||||
int ret;
|
||||
|
||||
ret = ff_v4l2_m2m_create_context(priv, &s);
|
||||
@@ -370,6 +371,25 @@
|
||||
v4l2_fmt_output = output->format.fmt.pix_mp.pixelformat;
|
||||
else
|
||||
v4l2_fmt_output = output->format.fmt.pix.pixelformat;
|
||||
+
|
||||
+ /* Drivers may clamp the requested raw format to their supported size.
|
||||
+ * The software-frame upload still uses avctx/frame dimensions, so a
|
||||
+ * smaller negotiated format would overrun the mapped output buffers.
|
||||
+ * Allow upward alignment, but reject frames that do not fit.
|
||||
+ */
|
||||
+ if (V4L2_TYPE_IS_MULTIPLANAR(output->type)) {
|
||||
+ output_width = output->format.fmt.pix_mp.width;
|
||||
+ output_height = output->format.fmt.pix_mp.height;
|
||||
+ } else {
|
||||
+ output_width = output->format.fmt.pix.width;
|
||||
+ output_height = output->format.fmt.pix.height;
|
||||
+ }
|
||||
+ if (output_width < avctx->width || output_height < avctx->height) {
|
||||
+ av_log(avctx, AV_LOG_ERROR,
|
||||
+ "Encoder cannot fit requested %dx%d frame in negotiated %ux%u format\n",
|
||||
+ avctx->width, avctx->height, output_width, output_height);
|
||||
+ return AVERROR(EINVAL);
|
||||
+ }
|
||||
|
||||
pix_fmt_output = ff_v4l2_format_v4l2_to_avfmt(v4l2_fmt_output, AV_CODEC_ID_RAWVIDEO);
|
||||
if (pix_fmt_output != avctx->pix_fmt) {
|
||||
@@ -6,6 +6,7 @@ Maintainer: SilentWind <admin@mofeng.run>
|
||||
Package: one-kvm
|
||||
Architecture: {arch}
|
||||
Depends: ${{auto}}, ca-certificates{distsuffix}
|
||||
Recommends: bluez
|
||||
Description: A open and lightweight IP-KVM solution written in Rust
|
||||
Enables BIOS-level remote management of servers and workstations.
|
||||
.
|
||||
@@ -17,6 +18,7 @@ Description: A open and lightweight IP-KVM solution written in Rust
|
||||
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
||||
* WebRTC and MJPEG streaming with low latency
|
||||
* USB HID emulation via OTG gadget
|
||||
* Classic Bluetooth keyboard and mouse via BlueZ
|
||||
* Mass storage device for ISO/IMG mounting
|
||||
* ATX power control via GPIO or USB relay
|
||||
Homepage: https://github.com/mofeng-git/One-KVM
|
||||
|
||||
@@ -1,16 +1,130 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: one-kvm
|
||||
Source: https://github.com/mofeng-git/one-KVM
|
||||
Upstream-Contact: https://github.com/mofeng-git/One-KVM/issues
|
||||
Source: https://github.com/mofeng-git/One-KVM
|
||||
|
||||
Files: *
|
||||
Copyright: 2025 One-KVM contributors
|
||||
License: GPL-2.0
|
||||
License: AGPL-3.0-only
|
||||
|
||||
Files: protos/*
|
||||
Copyright: the RustDesk contributors
|
||||
2025 One-KVM contributors
|
||||
License: AGPL-3.0-only
|
||||
Comment:
|
||||
Protocol definition files derived from RustDesk
|
||||
(https://github.com/rustdesk/rustdesk). Modified for One-KVM.
|
||||
See protos/NOTICE for the full attribution.
|
||||
|
||||
Files: src/rustdesk/*
|
||||
Copyright: the RustDesk contributors
|
||||
2025 One-KVM contributors
|
||||
License: AGPL-3.0-only
|
||||
Comment:
|
||||
Code derived from the RustDesk project, modified for One-KVM.
|
||||
See src/rustdesk/NOTICE for the full attribution.
|
||||
|
||||
Files: libs/hwcodec/*
|
||||
Copyright: the RustDesk contributors
|
||||
2025 One-KVM contributors
|
||||
License: AGPL-3.0-only
|
||||
Comment:
|
||||
Derived from the RustDesk hwcodec component. See libs/hwcodec/NOTICE.
|
||||
|
||||
Files: libs/v4l2r/*
|
||||
Copyright: 2023-2024 Andrei Lytkin, Daniel Almeida, Collabora Ltd. and other v4l2r contributors
|
||||
License: Expat
|
||||
|
||||
Files: libs/ventoy-img-rs/resources/*
|
||||
Copyright: Ventoy contributors
|
||||
License: GPL-2.0
|
||||
License: GPL-3.0+
|
||||
Comment:
|
||||
Pre-built Ventoy boot files (boot.img, core.img.xz, ventoy.disk.img.xz).
|
||||
Ventoy upstream: https://github.com/ventoy/Ventoy, licensed GPL-3.0-or-later.
|
||||
|
||||
License: GPL-2.0
|
||||
Files: res/vcpkg/libyuv/*
|
||||
Copyright: The LibYuv Project Authors
|
||||
License: BSD-3-Clause
|
||||
|
||||
Files: third-party/ffmpeg
|
||||
Copyright: FFmpeg developers and contributors
|
||||
License: GPL-2.0+
|
||||
Comment:
|
||||
FFmpeg is built with --enable-gpl --enable-version3 and linked against
|
||||
x264/x265; the resulting build is GPL-compatible with the AGPL-3.0 main
|
||||
program. Upstream: https://ffmpeg.org/
|
||||
The Linux builds use nyanmisaka/ffmpeg-rockchip at the commit pinned by
|
||||
FFMPEG_ROCKCHIP_REV in build/cross/Dockerfile.*.
|
||||
|
||||
Files: third-party/x264
|
||||
Copyright: x264 LLC and x264 contributors
|
||||
License: GPL-2.0+
|
||||
Comment: Upstream: https://www.videolan.org/developers/x264.html
|
||||
|
||||
Files: third-party/x265
|
||||
Copyright: MulticoreWare, Inc. and x265 contributors
|
||||
License: GPL-2.0+
|
||||
Comment: Upstream: https://www.x265.org/
|
||||
|
||||
Files: third-party/libvpx
|
||||
Copyright: The WebM Project authors
|
||||
License: BSD-3-Clause
|
||||
Comment: Upstream: https://www.webmproject.org/code/
|
||||
|
||||
Files: third-party/libopus
|
||||
Copyright: Opus contributors
|
||||
License: BSD-3-Clause
|
||||
Comment: Upstream: https://opus-codec.org/
|
||||
|
||||
Files: third-party/libjpeg-turbo
|
||||
Copyright: D. R. Commander and other libjpeg-turbo contributors
|
||||
License: BSD-3-Clause
|
||||
Comment: Upstream: https://libjpeg-turbo.org/
|
||||
|
||||
Files: third-party/libsodium
|
||||
Copyright: 2013-2025 Frank Denis
|
||||
License: Expat
|
||||
Comment: Upstream: https://libsodium.gitbook.io/
|
||||
|
||||
Files: third-party/sqlite
|
||||
Copyright: 1994-2010 Hipp, Wyrick & Company, Inc.
|
||||
License: SQLite-public-domain
|
||||
Comment: Upstream: https://www.sqlite.org/
|
||||
|
||||
License: AGPL-3.0-only
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the full text of the GNU Affero General Public License
|
||||
version 3 can be found in the file `/usr/share/common-licenses/AGPL-3'.
|
||||
|
||||
License: GPL-3.0+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the full text of the GNU General Public License
|
||||
version 3 can be found in the file `/usr/share/common-licenses/GPL-3'.
|
||||
|
||||
License: GPL-2.0+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
@@ -22,8 +136,61 @@ License: GPL-2.0
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the full text of the GNU General Public License
|
||||
version 2 can be found in the file `/usr/share/common-licenses/GPL-2'.
|
||||
|
||||
On Debian systems, the full text of the GNU General Public License
|
||||
version 2 can be found in the file `/usr/share/common-licenses/GPL-2'.
|
||||
License: Expat
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
License: BSD-3-Clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
.
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: SQLite-public-domain
|
||||
The author disclaims copyright to this source code. In place of a legal
|
||||
notice, here is a blessing:
|
||||
.
|
||||
May you do good and not evil.
|
||||
May you find forgiveness for yourself and understand yourself.
|
||||
May you share freely, never taking more than you give.
|
||||
|
||||
@@ -4,6 +4,68 @@
|
||||
|
||||
set -e
|
||||
|
||||
detect_intel_libva_driver() {
|
||||
if [ -n "${LIBVA_DRIVER_NAME:-}" ]; then
|
||||
echo "[INFO] Using preconfigured LIBVA_DRIVER_NAME=$LIBVA_DRIVER_NAME"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$(uname -m)" != "x86_64" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local devices=()
|
||||
if [ -n "${LIBVA_DEVICE:-}" ]; then
|
||||
devices=("$LIBVA_DEVICE")
|
||||
else
|
||||
shopt -s nullglob
|
||||
devices=(/dev/dri/renderD*)
|
||||
shopt -u nullglob
|
||||
fi
|
||||
|
||||
if [ ${#devices[@]} -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local device=""
|
||||
local node=""
|
||||
local vendor=""
|
||||
local driver=""
|
||||
|
||||
for device in "${devices[@]}"; do
|
||||
if [ ! -e "$device" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
node="$(basename "$device")"
|
||||
vendor=""
|
||||
if [ -r "/sys/class/drm/$node/device/vendor" ]; then
|
||||
vendor="$(cat "/sys/class/drm/$node/device/vendor")"
|
||||
fi
|
||||
|
||||
if [ -n "$vendor" ] && [ "$vendor" != "0x8086" ]; then
|
||||
echo "[INFO] Skipping VA-API probe for $device (vendor=$vendor)"
|
||||
continue
|
||||
fi
|
||||
|
||||
for driver in iHD i965; do
|
||||
if LIBVA_DRIVER_NAME="$driver" vainfo --display drm --device "$device" >/dev/null 2>&1; then
|
||||
export LIBVA_DRIVER_NAME="$driver"
|
||||
if [ -n "$vendor" ]; then
|
||||
echo "[INFO] Detected Intel VA-API driver '$driver' on $device (vendor=$vendor)"
|
||||
else
|
||||
echo "[INFO] Detected Intel VA-API driver '$driver' on $device"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "[WARN] Unable to auto-detect an Intel VA-API driver; leaving LIBVA_DRIVER_NAME unset"
|
||||
}
|
||||
|
||||
detect_intel_libva_driver
|
||||
|
||||
# Start one-kvm with default options.
|
||||
# Additional options can be passed via environment variables.
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
[Unit]
|
||||
Description=One-KVM IP-KVM Service
|
||||
Documentation=https://github.com/mofeng-git/One-KVM
|
||||
After=network.target
|
||||
After=network-online.target bluetooth.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
# Example for older Intel GPUs:
|
||||
# Environment=LIBVA_DRIVER_NAME=i965
|
||||
ExecStart=/usr/bin/one-kvm
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Build deb packages from pre-compiled binaries
|
||||
# Binaries are compiled once on Debian 11 (GLIBC 2.31) via build-images.sh
|
||||
# Binaries are compiled once on Debian 12 (GLIBC 2.36) via build-images.sh
|
||||
# This script packages them directly on the host using dpkg-deb
|
||||
# Usage: ./build/build-deb.sh [arch]
|
||||
# Example: ./build/build-deb.sh aarch64
|
||||
@@ -125,8 +125,8 @@ EOF
|
||||
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
||||
|
||||
# Create control file
|
||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14)"
|
||||
BASE_DEPS="libc6 (>= 2.36), libgcc-s1, libstdc++6, libasound2t64 (>= 1.1) | libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx-gen1.2 (>= 22.0) | libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
||||
DEPS="$BASE_DEPS"
|
||||
if [ "$DEB_ARCH" = "amd64" ]; then
|
||||
DEPS="$DEPS, $AMD64_DEPS"
|
||||
@@ -139,10 +139,11 @@ Section: admin
|
||||
Priority: optional
|
||||
Architecture: $DEB_ARCH
|
||||
Depends: $DEPS
|
||||
Recommends: bluez
|
||||
Maintainer: SilentWind <admin@mofeng.run>
|
||||
Description: A open and lightweight IP-KVM solution
|
||||
Enables BIOS-level remote management of servers and workstations.
|
||||
Built on Debian 11, compatible with Debian 11+, Ubuntu 20.04+.
|
||||
Built on Debian 12, compatible with Debian 12+, Ubuntu 24.04+.
|
||||
EOF
|
||||
|
||||
# Build deb directly on host
|
||||
|
||||
@@ -204,9 +204,21 @@ get_tool_urls() {
|
||||
}
|
||||
|
||||
# Download tools for a platform
|
||||
# Set CURL_PROXY=http://host:port to route GitHub downloads through an HTTP proxy
|
||||
# Set GHPROXY_PREFIX=https://ghfast.top/ to prefix GitHub URLs instead
|
||||
download_tools() {
|
||||
local platform="$1"
|
||||
local staging="$2"
|
||||
local curl_proxy=()
|
||||
if [ -n "${CURL_PROXY:-}" ]; then
|
||||
curl_proxy=(-x "$CURL_PROXY")
|
||||
fi
|
||||
dl() {
|
||||
# dl <url> <output>
|
||||
local url="$1"
|
||||
[ -n "${GHPROXY_PREFIX:-}" ] && url="${GHPROXY_PREFIX%/}/$url"
|
||||
curl -fsSL "${curl_proxy[@]}" "$url" -o "$2"
|
||||
}
|
||||
|
||||
get_tool_urls "$platform"
|
||||
|
||||
@@ -214,14 +226,14 @@ download_tools() {
|
||||
|
||||
# ttyd
|
||||
if [ ! -f "$staging/ttyd" ]; then
|
||||
curl -fsSL "$TTYD_URL" -o "$staging/ttyd"
|
||||
dl "$TTYD_URL" "$staging/ttyd"
|
||||
chmod +x "$staging/ttyd"
|
||||
fi
|
||||
|
||||
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
||||
# gostc
|
||||
if [ ! -f "$staging/gostc" ]; then
|
||||
curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
|
||||
dl "$GOSTC_URL" /tmp/gostc.tar.gz
|
||||
tar -xzf /tmp/gostc.tar.gz -C "$staging"
|
||||
chmod +x "$staging/gostc"
|
||||
rm /tmp/gostc.tar.gz
|
||||
@@ -229,7 +241,7 @@ download_tools() {
|
||||
|
||||
# easytier
|
||||
if [ ! -f "$staging/easytier-core" ]; then
|
||||
curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip
|
||||
dl "$EASYTIER_URL" /tmp/easytier.zip
|
||||
unzip -o /tmp/easytier.zip -d /tmp/easytier
|
||||
cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
|
||||
chmod +x "$staging/easytier-core"
|
||||
@@ -311,9 +323,16 @@ build_for_platform() {
|
||||
|
||||
local build_cmd="docker buildx build --platform $platform"
|
||||
build_cmd="$build_cmd --build-arg TARGETPLATFORM=$platform"
|
||||
if [ -n "${APT_MIRROR:-}" ]; then
|
||||
build_cmd="$build_cmd --build-arg APT_MIRROR=$APT_MIRROR"
|
||||
fi
|
||||
|
||||
if [ "$PUSH" = true ]; then
|
||||
build_cmd="$build_cmd --push"
|
||||
# Aliyun ACR rejects the OCI artifact manifests used by BuildKit attestations.
|
||||
if [[ "$REGISTRY" == registry.cn-*.aliyuncs.com/* ]]; then
|
||||
build_cmd="$build_cmd --provenance=false --sbom=false"
|
||||
fi
|
||||
elif [ "$LOAD" = true ]; then
|
||||
build_cmd="$build_cmd --load"
|
||||
fi
|
||||
|
||||
87
build/windows/build.ps1
Normal file
87
build/windows/build.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
param(
|
||||
[string]$Configuration = "debug",
|
||||
[string]$Target = "x86_64-pc-windows-msvc",
|
||||
[string]$Triplet = "x64-windows-static",
|
||||
[string]$VcpkgRoot = $env:VCPKG_ROOT,
|
||||
[string]$VcpkgInstalledRoot = $env:VCPKG_INSTALLED_DIR,
|
||||
[switch]$NoDefaultFeatures,
|
||||
[string[]]$Features = @(),
|
||||
[switch]$Package,
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$CargoArgs = @()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..\..")
|
||||
Set-Location $repoRoot
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($VcpkgRoot)) {
|
||||
$VcpkgRoot = Join-Path (Split-Path $repoRoot -Parent) "vcpkg"
|
||||
}
|
||||
|
||||
$VcpkgRoot = [System.IO.Path]::GetFullPath($VcpkgRoot)
|
||||
if ([string]::IsNullOrWhiteSpace($VcpkgInstalledRoot)) {
|
||||
$VcpkgInstalledRoot = Join-Path $VcpkgRoot "installed"
|
||||
}
|
||||
|
||||
$VcpkgInstalledRoot = [System.IO.Path]::GetFullPath($VcpkgInstalledRoot)
|
||||
$vcpkgTripletRoot = Join-Path $VcpkgInstalledRoot $Triplet
|
||||
$turbojpegLibDir = Join-Path $vcpkgTripletRoot "lib"
|
||||
$turbojpegIncludeDir = Join-Path $vcpkgTripletRoot "include"
|
||||
|
||||
if (-not (Test-Path $VcpkgRoot)) {
|
||||
throw "VCPKG_ROOT does not exist: $VcpkgRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $turbojpegLibDir) -or -not (Test-Path $turbojpegIncludeDir)) {
|
||||
throw "vcpkg triplet is not installed at $vcpkgTripletRoot. Run build/windows/bootstrap-vcpkg.ps1 first."
|
||||
}
|
||||
|
||||
$env:VCPKG_ROOT = $VcpkgRoot
|
||||
$env:VCPKG_DEFAULT_TRIPLET = $Triplet
|
||||
$env:VCPKG_INSTALLED_DIR = $VcpkgInstalledRoot
|
||||
$env:TURBOJPEG_SOURCE = "explicit"
|
||||
$env:TURBOJPEG_LIB_DIR = $turbojpegLibDir
|
||||
$env:TURBOJPEG_INCLUDE_DIR = $turbojpegIncludeDir
|
||||
|
||||
$cargoCommand = @("build", "--target", $Target)
|
||||
|
||||
if ($Configuration -eq "release") {
|
||||
$cargoCommand += "--release"
|
||||
} elseif ($Configuration -ne "debug") {
|
||||
throw "Unsupported configuration '$Configuration'. Use 'debug' or 'release'."
|
||||
}
|
||||
|
||||
if ($NoDefaultFeatures) {
|
||||
$cargoCommand += "--no-default-features"
|
||||
}
|
||||
|
||||
if ($Features.Count -gt 0) {
|
||||
$cargoCommand += "--features"
|
||||
$cargoCommand += ($Features -join ",")
|
||||
}
|
||||
|
||||
$cargoCommand += $CargoArgs
|
||||
|
||||
cargo @cargoCommand
|
||||
|
||||
if ($Package) {
|
||||
$metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
|
||||
$packageInfo = $metadata.packages | Where-Object { $_.name -eq "one-kvm" } | Select-Object -First 1
|
||||
|
||||
if ($null -eq $packageInfo -or [string]::IsNullOrWhiteSpace($packageInfo.version)) {
|
||||
throw "Failed to resolve version from Cargo metadata"
|
||||
}
|
||||
|
||||
$sourcePath = Join-Path $repoRoot "target/$Target/release/one-kvm.exe"
|
||||
$targetName = "one-kvm_{0}_amd64.exe" -f $packageInfo.version
|
||||
$targetPath = Join-Path $repoRoot "target/$Target/release/$targetName"
|
||||
|
||||
if (-not (Test-Path $sourcePath)) {
|
||||
throw "Windows binary not found: $sourcePath"
|
||||
}
|
||||
|
||||
Copy-Item $sourcePath $targetPath
|
||||
Write-Host $targetPath
|
||||
}
|
||||
130
docs/README.md
130
docs/README.md
@@ -1,130 +0,0 @@
|
||||
# One-KVM 技术文档
|
||||
|
||||
本目录包含 One-KVM 项目的完整技术文档。
|
||||
|
||||
## 文档结构
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # 本文件 - 文档索引
|
||||
├── system-architecture.md # 系统架构文档
|
||||
├── tech-stack.md # 技术栈文档
|
||||
└── modules/ # 模块文档
|
||||
├── video.md # 视频模块
|
||||
├── hid.md # HID 模块
|
||||
├── otg.md # OTG 模块
|
||||
├── msd.md # MSD 模块
|
||||
├── atx.md # ATX 模块
|
||||
├── audio.md # 音频模块
|
||||
├── webrtc.md # WebRTC 模块
|
||||
├── rustdesk.md # RustDesk 模块
|
||||
├── auth.md # 认证模块
|
||||
├── config.md # 配置模块
|
||||
├── events.md # 事件模块
|
||||
└── web.md # Web 模块
|
||||
```
|
||||
|
||||
## 快速导航
|
||||
|
||||
### 核心文档
|
||||
|
||||
| 文档 | 描述 |
|
||||
|------|------|
|
||||
| [系统架构](./system-architecture.md) | 整体架构设计、数据流、模块依赖 |
|
||||
| [技术栈](./tech-stack.md) | 使用的技术、库和开发规范 |
|
||||
|
||||
### 功能模块
|
||||
|
||||
| 模块 | 描述 | 关键文件 |
|
||||
|------|------|---------|
|
||||
| [Video](./modules/video.md) | 视频采集和编码 | `src/video/` |
|
||||
| [HID](./modules/hid.md) | 键盘鼠标控制 | `src/hid/` |
|
||||
| [OTG](./modules/otg.md) | USB Gadget 管理 | `src/otg/` |
|
||||
| [MSD](./modules/msd.md) | 虚拟存储设备 | `src/msd/` |
|
||||
| [ATX](./modules/atx.md) | 电源控制 | `src/atx/` |
|
||||
| [Audio](./modules/audio.md) | 音频采集编码 | `src/audio/` |
|
||||
| [WebRTC](./modules/webrtc.md) | WebRTC 流媒体 | `src/webrtc/` |
|
||||
| [RustDesk](./modules/rustdesk.md) | RustDesk 协议集成 | `src/rustdesk/` |
|
||||
|
||||
### 基础设施
|
||||
|
||||
| 模块 | 描述 | 关键文件 |
|
||||
|------|------|---------|
|
||||
| [Auth](./modules/auth.md) | 认证和会话 | `src/auth/` |
|
||||
| [Config](./modules/config.md) | 配置管理 | `src/config/` |
|
||||
| [Events](./modules/events.md) | 事件系统 | `src/events/` |
|
||||
| [Web](./modules/web.md) | HTTP API | `src/web/` |
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ One-KVM System │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Web Frontend (Vue3) │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Axum Web Server │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ AppState │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ Video │ │ HID │ │ MSD │ │ ATX │ │ │
|
||||
│ │ │ Module │ │ Module │ │ Module │ │ Module │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ Audio │ │ WebRTC │ │RustDesk│ │ Events │ │ │
|
||||
│ │ │ Module │ │ Module │ │ Module │ │ Bus │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Hardware Layer │ │
|
||||
│ │ V4L2 │ USB OTG │ GPIO │ ALSA │ Network │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 关键特性
|
||||
|
||||
- **单一二进制**: Web UI + 后端一体化部署
|
||||
- **双流模式**: WebRTC (H264/H265/VP8/VP9) + MJPEG
|
||||
- **USB OTG**: 虚拟键鼠、虚拟存储
|
||||
- **硬件加速**: VAAPI/RKMPP/V4L2 M2M
|
||||
- **RustDesk**: 跨平台远程访问
|
||||
- **无配置文件**: SQLite 配置存储
|
||||
|
||||
## 目标平台
|
||||
|
||||
| 平台 | 架构 | 用途 |
|
||||
|------|------|------|
|
||||
| aarch64-unknown-linux-gnu | ARM64 | 主要目标 |
|
||||
| armv7-unknown-linux-gnueabihf | ARMv7 | 备选 |
|
||||
| x86_64-unknown-linux-gnu | x86-64 | 开发/测试 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 构建前端
|
||||
cd web && npm install && npm run build && cd ..
|
||||
|
||||
# 构建后端
|
||||
cargo build --release
|
||||
|
||||
# 运行
|
||||
./target/release/one-kvm --enable-https
|
||||
```
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [项目仓库](https://github.com/mofeng-git/One-KVM)
|
||||
- [开发计划](./DEVELOPMENT_PLAN.md)
|
||||
- [项目目标](./PROJECT_GOALS.md)
|
||||
24
docs/macos-drag-compatibility.md
Normal file
24
docs/macos-drag-compatibility.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# macOS 拖拽兼容模式(实验性)
|
||||
|
||||
适用于 OTG 和 CH9329 后端的绝对鼠标输入。遇到目标 Mac 上拖拽只能移动一小段便停止时,可在设置页的 HID 配置中开启「macOS 拖拽兼容模式」。默认关闭。
|
||||
|
||||
配置字段为 `hid.mouse_macos_drag`,旧 PR 的 `ch9329_macos_drag` 字段仍可作为读取别名。OTG 必须同时启用相对鼠标和绝对鼠标接口,否则配置保存会被拒绝。CH9329 同时启用 Linux 兼容开关时,此模式优先。
|
||||
|
||||
## 报告行为
|
||||
|
||||
- 未按键时仍使用绝对坐标定位。
|
||||
- 从绝对输入开始的拖拽:按下使用绝对报告,移动和滚轮使用相对报告,释放时先更新相对按钮状态,再发送绝对按钮释放。
|
||||
- 按键报告的通道保持到所有按钮释放,不随拖拽期间的输入模式切换而改变。
|
||||
- 从相对输入开始的点击、拖拽继续使用相对按钮报告;此选项不修复原生相对模式的点击兼容问题。
|
||||
- 位移从原始 15 位输入坐标换算,累计小数余量,大位移按单包范围分包并保留总量及方向。
|
||||
- 运行时使用当前视频采集尺寸,并在 HID 重载后恢复尺寸;视频尚未提供尺寸时使用配置尺寸。
|
||||
|
||||
## 验证范围与限制
|
||||
|
||||
本次 OTG 扩展及位移换算修改没有 macOS 实机验证。原 PR 作者对旧 CH9329 实现的实机反馈不能视为本版本已经验证。
|
||||
|
||||
本地自动化覆盖位移累计/分包、1080p/4K 换算、往返移动、滚轮、输入模式切换、复位、CH9329 命令队列、OTG 临时文件模拟端点及配置校验。临时文件测试仅验证写出的字节,不验证 USB 枚举或目标系统的事件解释。
|
||||
|
||||
相对位移受 macOS 鼠标速度、加速、显示缩放及多屏布局影响,无法保证与绝对定位逐像素一致。松手报告携带最后的客户端绝对坐标,仍可能出现落点偏移或光标跳位;大位移分包也可能增加低波特率串口延迟。
|
||||
|
||||
后续实机验证应分别覆盖两个后端:窗口/文件/文本拖拽,快速及慢速移动,滚轮和多按钮组合,1080p/4K 与缩放显示,输入模式切换、断开重连、精确落点。异常时关闭此选项恢复默认行为。
|
||||
@@ -1,484 +0,0 @@
|
||||
# ATX 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
ATX 模块提供电源控制功能,通过 GPIO 或 USB 继电器控制目标计算机的电源和重置按钮。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- 电源按钮控制
|
||||
- 重置按钮控制
|
||||
- 电源 LED 状态监视
|
||||
- Wake-on-LAN 支持
|
||||
- 多后端支持 (GPIO/USB 继电器)
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/atx/
|
||||
├── mod.rs # 模块导出
|
||||
├── controller.rs # AtxController (11KB)
|
||||
├── executor.rs # 动作执行器 (10KB)
|
||||
├── types.rs # 类型定义 (7KB)
|
||||
├── led.rs # LED 监视 (5KB)
|
||||
└── wol.rs # Wake-on-LAN (5KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ATX Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Web API
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ AtxController │
|
||||
│ (controller.rs) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ Power │ │ Reset │ │ LED │
|
||||
│Executor│ │Executor│ │Monitor │
|
||||
└───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ GPIO │ │ GPIO │ │ GPIO │
|
||||
│ or USB │ │ or USB │ │ Input │
|
||||
│ Relay │ │ Relay │ │ │
|
||||
└───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │
|
||||
└───────────┼─────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Target PC │
|
||||
│ (ATX Header) │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 AtxController (controller.rs)
|
||||
|
||||
```rust
|
||||
pub struct AtxController {
|
||||
/// 电源按钮配置
|
||||
power: Arc<AtxButton>,
|
||||
|
||||
/// 重置按钮配置
|
||||
reset: Arc<AtxButton>,
|
||||
|
||||
/// LED 监视器
|
||||
led_monitor: Arc<RwLock<Option<LedMonitor>>>,
|
||||
|
||||
/// WoL 控制器
|
||||
wol: Arc<RwLock<Option<WolController>>>,
|
||||
|
||||
/// 当前状态
|
||||
state: Arc<RwLock<AtxState>>,
|
||||
|
||||
/// 事件总线
|
||||
events: Arc<EventBus>,
|
||||
}
|
||||
|
||||
impl AtxController {
|
||||
/// 创建控制器
|
||||
pub fn new(config: &AtxConfig, events: Arc<EventBus>) -> Result<Self>;
|
||||
|
||||
/// 短按电源按钮 (开机/正常关机)
|
||||
pub async fn power_short_press(&self) -> Result<()>;
|
||||
|
||||
/// 长按电源按钮 (强制关机)
|
||||
pub async fn power_long_press(&self) -> Result<()>;
|
||||
|
||||
/// 按重置按钮
|
||||
pub async fn reset_press(&self) -> Result<()>;
|
||||
|
||||
/// 获取电源状态
|
||||
pub fn power_state(&self) -> PowerState;
|
||||
|
||||
/// 发送 WoL 魔术包
|
||||
pub async fn wake_on_lan(&self, mac: &str) -> Result<()>;
|
||||
|
||||
/// 获取状态
|
||||
pub fn state(&self) -> AtxState;
|
||||
|
||||
/// 重新加载配置
|
||||
pub async fn reload(&self, config: &AtxConfig) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct AtxState {
|
||||
/// 是否可用
|
||||
pub available: bool,
|
||||
|
||||
/// 电源是否开启
|
||||
pub power_on: bool,
|
||||
|
||||
/// 最后操作时间
|
||||
pub last_action: Option<DateTime<Utc>>,
|
||||
|
||||
/// 错误信息
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub enum PowerState {
|
||||
On,
|
||||
Off,
|
||||
Unknown,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 AtxButton (executor.rs)
|
||||
|
||||
```rust
|
||||
pub struct AtxButton {
|
||||
/// 按钮名称
|
||||
name: String,
|
||||
|
||||
/// 驱动类型
|
||||
driver: AtxDriverType,
|
||||
|
||||
/// GPIO 句柄
|
||||
gpio: Option<LineHandle>,
|
||||
|
||||
/// USB 继电器句柄
|
||||
relay: Option<UsbRelay>,
|
||||
|
||||
/// 配置
|
||||
config: AtxKeyConfig,
|
||||
}
|
||||
|
||||
impl AtxButton {
|
||||
/// 创建按钮
|
||||
pub fn new(name: &str, config: &AtxKeyConfig) -> Result<Self>;
|
||||
|
||||
/// 短按 (100ms)
|
||||
pub async fn short_press(&self) -> Result<()>;
|
||||
|
||||
/// 长按 (3000ms)
|
||||
pub async fn long_press(&self) -> Result<()>;
|
||||
|
||||
/// 自定义按压时间
|
||||
pub async fn press(&self, duration: Duration) -> Result<()>;
|
||||
|
||||
/// 设置输出状态
|
||||
fn set_output(&self, high: bool) -> Result<()>;
|
||||
}
|
||||
|
||||
pub enum AtxDriverType {
|
||||
/// GPIO 直连
|
||||
Gpio,
|
||||
|
||||
/// USB 继电器
|
||||
UsbRelay,
|
||||
|
||||
/// 禁用
|
||||
None,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 LedMonitor (led.rs)
|
||||
|
||||
```rust
|
||||
pub struct LedMonitor {
|
||||
/// GPIO 引脚
|
||||
pin: u32,
|
||||
|
||||
/// GPIO 句柄
|
||||
line: LineHandle,
|
||||
|
||||
/// 当前状态
|
||||
state: Arc<AtomicBool>,
|
||||
|
||||
/// 监视任务
|
||||
monitor_task: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl LedMonitor {
|
||||
/// 创建监视器
|
||||
pub fn new(config: &AtxLedConfig) -> Result<Self>;
|
||||
|
||||
/// 启动监视
|
||||
pub fn start(&mut self, events: Arc<EventBus>) -> Result<()>;
|
||||
|
||||
/// 停止监视
|
||||
pub fn stop(&mut self);
|
||||
|
||||
/// 获取当前状态
|
||||
pub fn state(&self) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 WolController (wol.rs)
|
||||
|
||||
```rust
|
||||
pub struct WolController {
|
||||
/// 网络接口
|
||||
interface: String,
|
||||
|
||||
/// 广播地址
|
||||
broadcast_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl WolController {
|
||||
/// 创建控制器
|
||||
pub fn new(interface: Option<&str>) -> Result<Self>;
|
||||
|
||||
/// 发送 WoL 魔术包
|
||||
pub async fn wake(&self, mac: &str) -> Result<()>;
|
||||
|
||||
/// 构建魔术包
|
||||
fn build_magic_packet(mac: &[u8; 6]) -> [u8; 102];
|
||||
|
||||
/// 解析 MAC 地址
|
||||
fn parse_mac(mac: &str) -> Result<[u8; 6]>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct AtxConfig {
|
||||
/// 是否启用
|
||||
pub enabled: bool,
|
||||
|
||||
/// 电源按钮配置
|
||||
pub power: AtxKeyConfig,
|
||||
|
||||
/// 重置按钮配置
|
||||
pub reset: AtxKeyConfig,
|
||||
|
||||
/// LED 监视配置
|
||||
pub led: AtxLedConfig,
|
||||
|
||||
/// WoL 配置
|
||||
pub wol: WolConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct AtxKeyConfig {
|
||||
/// 驱动类型
|
||||
pub driver: AtxDriverType,
|
||||
|
||||
/// GPIO 芯片 (如 /dev/gpiochip0)
|
||||
pub gpio_chip: Option<String>,
|
||||
|
||||
/// GPIO 引脚号
|
||||
pub gpio_pin: Option<u32>,
|
||||
|
||||
/// USB 继电器设备
|
||||
pub relay_device: Option<String>,
|
||||
|
||||
/// 继电器通道
|
||||
pub relay_channel: Option<u8>,
|
||||
|
||||
/// 激活电平
|
||||
pub active_level: ActiveLevel,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct AtxLedConfig {
|
||||
/// 是否启用
|
||||
pub enabled: bool,
|
||||
|
||||
/// GPIO 芯片
|
||||
pub gpio_chip: Option<String>,
|
||||
|
||||
/// GPIO 引脚号
|
||||
pub gpio_pin: Option<u32>,
|
||||
|
||||
/// 激活电平
|
||||
pub active_level: ActiveLevel,
|
||||
}
|
||||
|
||||
pub enum ActiveLevel {
|
||||
High,
|
||||
Low,
|
||||
}
|
||||
|
||||
impl Default for AtxConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
power: AtxKeyConfig::default(),
|
||||
reset: AtxKeyConfig::default(),
|
||||
led: AtxLedConfig::default(),
|
||||
wol: WolConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 端点
|
||||
|
||||
| 端点 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/api/atx/status` | GET | 获取 ATX 状态 |
|
||||
| `/api/atx/power/short` | POST | 短按电源 |
|
||||
| `/api/atx/power/long` | POST | 长按电源 |
|
||||
| `/api/atx/reset` | POST | 按重置 |
|
||||
| `/api/atx/wol` | POST | 发送 WoL |
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
// GET /api/atx/status
|
||||
{
|
||||
"available": true,
|
||||
"power_on": true,
|
||||
"last_action": "2024-01-15T10:30:00Z",
|
||||
"error": null
|
||||
}
|
||||
|
||||
// POST /api/atx/wol
|
||||
// Request: { "mac": "00:11:22:33:44:55" }
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 硬件连接
|
||||
|
||||
### 6.1 GPIO 直连
|
||||
|
||||
```
|
||||
One-KVM Device Target PC
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ GPIO Pin │───────────────│ Power SW │
|
||||
│ (Output) │ │ │
|
||||
└─────────────┘ └─────────────┘
|
||||
|
||||
接线说明:
|
||||
- GPIO 引脚连接到 ATX 电源按钮
|
||||
- 使用光耦或继电器隔离 (推荐)
|
||||
- 注意电平匹配
|
||||
```
|
||||
|
||||
### 6.2 USB 继电器
|
||||
|
||||
```
|
||||
One-KVM Device USB Relay Target PC
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ USB │───────────────│ Relay │──────────│ Power SW │
|
||||
│ │ │ │ │ │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
|
||||
优点:
|
||||
- 完全隔离
|
||||
- 无需担心电平问题
|
||||
- 更安全
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 事件
|
||||
|
||||
```rust
|
||||
pub enum SystemEvent {
|
||||
AtxStateChanged {
|
||||
power_on: bool,
|
||||
last_action: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
AtxActionPerformed {
|
||||
action: String, // "power_short" | "power_long" | "reset" | "wol"
|
||||
success: bool,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AtxError {
|
||||
#[error("ATX not available")]
|
||||
NotAvailable,
|
||||
|
||||
#[error("GPIO error: {0}")]
|
||||
GpioError(String),
|
||||
|
||||
#[error("Relay error: {0}")]
|
||||
RelayError(String),
|
||||
|
||||
#[error("WoL error: {0}")]
|
||||
WolError(String),
|
||||
|
||||
#[error("Invalid MAC address: {0}")]
|
||||
InvalidMac(String),
|
||||
|
||||
#[error("Operation in progress")]
|
||||
Busy,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 使用示例
|
||||
|
||||
```rust
|
||||
let atx = AtxController::new(&config, events)?;
|
||||
|
||||
// 开机
|
||||
atx.power_short_press().await?;
|
||||
|
||||
// 检查状态
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
if atx.power_state() == PowerState::On {
|
||||
println!("PC is now on");
|
||||
}
|
||||
|
||||
// 强制关机
|
||||
atx.power_long_press().await?;
|
||||
|
||||
// 重置
|
||||
atx.reset_press().await?;
|
||||
|
||||
// Wake-on-LAN
|
||||
atx.wake_on_lan("00:11:22:33:44:55").await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### Q: GPIO 无法控制?
|
||||
|
||||
1. 检查引脚配置
|
||||
2. 检查权限 (`/dev/gpiochip*`)
|
||||
3. 检查接线
|
||||
|
||||
### Q: LED 状态不正确?
|
||||
|
||||
1. 检查 active_level 配置
|
||||
2. 检查 GPIO 输入模式
|
||||
|
||||
### Q: WoL 不工作?
|
||||
|
||||
1. 检查目标 PC BIOS 设置
|
||||
2. 检查网卡支持
|
||||
3. 检查网络广播
|
||||
@@ -1,463 +0,0 @@
|
||||
# Audio 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
Audio 模块负责音频采集和编码,支持 ALSA 采集和 Opus 编码。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- ALSA 音频采集
|
||||
- Opus 编码
|
||||
- 多质量配置
|
||||
- WebSocket/WebRTC 传输
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/audio/
|
||||
├── mod.rs # 模块导出
|
||||
├── controller.rs # AudioController (15KB)
|
||||
├── capture.rs # ALSA 采集 (12KB)
|
||||
├── encoder.rs # Opus 编码 (8KB)
|
||||
├── shared_pipeline.rs # 共享管道 (15KB)
|
||||
├── monitor.rs # 健康监视 (11KB)
|
||||
└── device.rs # 设备枚举 (8KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Audio Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
ALSA Device (hw:0,0)
|
||||
│
|
||||
│ PCM 48kHz/16bit/Stereo
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ AudioCapturer │
|
||||
│ (capture.rs) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SharedAudioPipeline │
|
||||
│ ┌─────────────────────────────────┐ │
|
||||
│ │ Opus Encoder │ │
|
||||
│ │ 48kHz → 24-96 kbps │ │
|
||||
│ └─────────────────────────────────┘ │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ WebSocket │ │ WebRTC │
|
||||
│ Stream │ │ Audio Track │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 AudioController (controller.rs)
|
||||
|
||||
```rust
|
||||
pub struct AudioController {
|
||||
/// 采集器
|
||||
capturer: Arc<RwLock<Option<AudioCapturer>>>,
|
||||
|
||||
/// 共享管道
|
||||
pipeline: Arc<SharedAudioPipeline>,
|
||||
|
||||
/// 配置
|
||||
config: Arc<RwLock<AudioConfig>>,
|
||||
|
||||
/// 状态
|
||||
state: Arc<RwLock<AudioState>>,
|
||||
|
||||
/// 事件总线
|
||||
events: Arc<EventBus>,
|
||||
}
|
||||
|
||||
impl AudioController {
|
||||
/// 创建控制器
|
||||
pub fn new(config: &AudioConfig, events: Arc<EventBus>) -> Result<Self>;
|
||||
|
||||
/// 启动音频
|
||||
pub async fn start(&self) -> Result<()>;
|
||||
|
||||
/// 停止音频
|
||||
pub async fn stop(&self) -> Result<()>;
|
||||
|
||||
/// 订阅音频帧
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame>;
|
||||
|
||||
/// 获取状态
|
||||
pub fn status(&self) -> AudioStatus;
|
||||
|
||||
/// 设置质量
|
||||
pub fn set_quality(&self, quality: AudioQuality) -> Result<()>;
|
||||
|
||||
/// 列出设备
|
||||
pub fn list_devices(&self) -> Vec<AudioDeviceInfo>;
|
||||
|
||||
/// 重新加载配置
|
||||
pub async fn reload(&self, config: &AudioConfig) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct AudioStatus {
|
||||
pub enabled: bool,
|
||||
pub streaming: bool,
|
||||
pub device: Option<String>,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
pub bitrate: u32,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 AudioCapturer (capture.rs)
|
||||
|
||||
```rust
|
||||
pub struct AudioCapturer {
|
||||
/// PCM 句柄
|
||||
pcm: PCM,
|
||||
|
||||
/// 设备名
|
||||
device: String,
|
||||
|
||||
/// 采样率
|
||||
sample_rate: u32,
|
||||
|
||||
/// 通道数
|
||||
channels: u16,
|
||||
|
||||
/// 帧大小
|
||||
frame_size: usize,
|
||||
|
||||
/// 运行状态
|
||||
running: AtomicBool,
|
||||
}
|
||||
|
||||
impl AudioCapturer {
|
||||
/// 打开设备
|
||||
pub fn open(device: &str, config: &CaptureConfig) -> Result<Self>;
|
||||
|
||||
/// 读取音频帧
|
||||
pub fn read_frame(&self) -> Result<Vec<i16>>;
|
||||
|
||||
/// 启动采集
|
||||
pub fn start(&self) -> Result<()>;
|
||||
|
||||
/// 停止采集
|
||||
pub fn stop(&self);
|
||||
|
||||
/// 是否运行中
|
||||
pub fn is_running(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct CaptureConfig {
|
||||
pub sample_rate: u32, // 48000
|
||||
pub channels: u16, // 2
|
||||
pub frame_size: usize, // 960 (20ms)
|
||||
pub buffer_size: usize, // 4800
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 OpusEncoder (encoder.rs)
|
||||
|
||||
```rust
|
||||
pub struct OpusEncoder {
|
||||
/// Opus 编码器
|
||||
encoder: audiopus::Encoder,
|
||||
|
||||
/// 采样率
|
||||
sample_rate: u32,
|
||||
|
||||
/// 通道数
|
||||
channels: u16,
|
||||
|
||||
/// 帧大小
|
||||
frame_size: usize,
|
||||
|
||||
/// 码率
|
||||
bitrate: u32,
|
||||
}
|
||||
|
||||
impl OpusEncoder {
|
||||
/// 创建编码器
|
||||
pub fn new(quality: AudioQuality) -> Result<Self>;
|
||||
|
||||
/// 编码 PCM 数据
|
||||
pub fn encode(&mut self, pcm: &[i16]) -> Result<Vec<u8>>;
|
||||
|
||||
/// 设置码率
|
||||
pub fn set_bitrate(&mut self, bitrate: u32) -> Result<()>;
|
||||
|
||||
/// 获取码率
|
||||
pub fn bitrate(&self) -> u32;
|
||||
|
||||
/// 重置编码器
|
||||
pub fn reset(&mut self) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 SharedAudioPipeline (shared_pipeline.rs)
|
||||
|
||||
```rust
|
||||
pub struct SharedAudioPipeline {
|
||||
/// 采集器
|
||||
capturer: Arc<RwLock<Option<AudioCapturer>>>,
|
||||
|
||||
/// 编码器
|
||||
encoder: Arc<Mutex<OpusEncoder>>,
|
||||
|
||||
/// 广播通道
|
||||
tx: broadcast::Sender<AudioFrame>,
|
||||
|
||||
/// 采集任务
|
||||
capture_task: Arc<RwLock<Option<JoinHandle<()>>>>,
|
||||
|
||||
/// 配置
|
||||
config: Arc<RwLock<AudioConfig>>,
|
||||
}
|
||||
|
||||
impl SharedAudioPipeline {
|
||||
/// 创建管道
|
||||
pub fn new(config: &AudioConfig) -> Result<Self>;
|
||||
|
||||
/// 启动管道
|
||||
pub async fn start(&self) -> Result<()>;
|
||||
|
||||
/// 停止管道
|
||||
pub async fn stop(&self) -> Result<()>;
|
||||
|
||||
/// 订阅音频帧
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame>;
|
||||
|
||||
/// 获取统计
|
||||
pub fn stats(&self) -> PipelineStats;
|
||||
}
|
||||
|
||||
pub struct AudioFrame {
|
||||
/// Opus 数据
|
||||
pub data: Bytes,
|
||||
|
||||
/// 时间戳
|
||||
pub timestamp: u64,
|
||||
|
||||
/// 帧序号
|
||||
pub sequence: u64,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 音频质量
|
||||
|
||||
```rust
|
||||
pub enum AudioQuality {
|
||||
/// 24 kbps - 最低带宽
|
||||
VeryLow,
|
||||
|
||||
/// 48 kbps - 低带宽
|
||||
Low,
|
||||
|
||||
/// 64 kbps - 中等
|
||||
Medium,
|
||||
|
||||
/// 96 kbps - 高质量
|
||||
High,
|
||||
}
|
||||
|
||||
impl AudioQuality {
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
match self {
|
||||
Self::VeryLow => 24000,
|
||||
Self::Low => 48000,
|
||||
Self::Medium => 64000,
|
||||
Self::High => 96000,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct AudioConfig {
|
||||
/// 是否启用
|
||||
pub enabled: bool,
|
||||
|
||||
/// 设备名
|
||||
pub device: Option<String>,
|
||||
|
||||
/// 音频质量
|
||||
pub quality: AudioQuality,
|
||||
|
||||
/// 自动启动
|
||||
pub auto_start: bool,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
device: None, // 使用默认设备
|
||||
quality: AudioQuality::Medium,
|
||||
auto_start: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API 端点
|
||||
|
||||
| 端点 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/api/audio/status` | GET | 获取音频状态 |
|
||||
| `/api/audio/start` | POST | 启动音频 |
|
||||
| `/api/audio/stop` | POST | 停止音频 |
|
||||
| `/api/audio/devices` | GET | 列出设备 |
|
||||
| `/api/audio/quality` | GET | 获取质量 |
|
||||
| `/api/audio/quality` | POST | 设置质量 |
|
||||
| `/api/ws/audio` | WS | 音频流 |
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
// GET /api/audio/status
|
||||
{
|
||||
"enabled": true,
|
||||
"streaming": true,
|
||||
"device": "hw:0,0",
|
||||
"sample_rate": 48000,
|
||||
"channels": 2,
|
||||
"bitrate": 64000,
|
||||
"error": null
|
||||
}
|
||||
|
||||
// GET /api/audio/devices
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"name": "hw:0,0",
|
||||
"description": "USB Audio Device",
|
||||
"is_default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. WebSocket 音频流
|
||||
|
||||
```javascript
|
||||
// 连接 WebSocket
|
||||
const ws = new WebSocket('/api/ws/audio');
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
// 初始化 Opus 解码器
|
||||
const decoder = new OpusDecoder();
|
||||
|
||||
// 接收音频帧
|
||||
ws.onmessage = (event) => {
|
||||
const frame = new Uint8Array(event.data);
|
||||
const pcm = decoder.decode(frame);
|
||||
audioContext.play(pcm);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 事件
|
||||
|
||||
```rust
|
||||
pub enum SystemEvent {
|
||||
AudioStateChanged {
|
||||
enabled: bool,
|
||||
streaming: bool,
|
||||
device: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AudioError {
|
||||
#[error("Device not found: {0}")]
|
||||
DeviceNotFound(String),
|
||||
|
||||
#[error("Device busy: {0}")]
|
||||
DeviceBusy(String),
|
||||
|
||||
#[error("ALSA error: {0}")]
|
||||
AlsaError(String),
|
||||
|
||||
#[error("Encoder error: {0}")]
|
||||
EncoderError(String),
|
||||
|
||||
#[error("Not streaming")]
|
||||
NotStreaming,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 使用示例
|
||||
|
||||
```rust
|
||||
let controller = AudioController::new(&config, events)?;
|
||||
|
||||
// 启动音频
|
||||
controller.start().await?;
|
||||
|
||||
// 订阅音频帧
|
||||
let mut rx = controller.subscribe();
|
||||
while let Ok(frame) = rx.recv().await {
|
||||
// 处理 Opus 数据
|
||||
send_to_client(frame.data);
|
||||
}
|
||||
|
||||
// 停止
|
||||
controller.stop().await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见问题
|
||||
|
||||
### Q: 找不到音频设备?
|
||||
|
||||
1. 检查 ALSA 配置
|
||||
2. 运行 `arecord -l`
|
||||
3. 检查权限
|
||||
|
||||
### Q: 音频延迟高?
|
||||
|
||||
1. 减小帧大小
|
||||
2. 降低质量
|
||||
3. 检查网络
|
||||
|
||||
### Q: 音频断断续续?
|
||||
|
||||
1. 增大缓冲区
|
||||
2. 检查 CPU 负载
|
||||
3. 使用更低质量
|
||||
@@ -1,340 +0,0 @@
|
||||
# Auth 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
Auth 模块提供用户认证和会话管理功能。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- 用户管理
|
||||
- 密码哈希 (Argon2)
|
||||
- 会话管理
|
||||
- 认证中间件
|
||||
- 权限控制
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/auth/
|
||||
├── mod.rs # 模块导出
|
||||
├── user.rs # 用户管理 (5KB)
|
||||
├── session.rs # 会话管理 (4KB)
|
||||
├── password.rs # 密码哈希 (1KB)
|
||||
└── middleware.rs # 中间件 (4KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心组件
|
||||
|
||||
### 2.1 UserStore (user.rs)
|
||||
|
||||
```rust
|
||||
pub struct UserStore {
|
||||
db: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
/// 创建存储
|
||||
pub async fn new(db: Pool<Sqlite>) -> Result<Self>;
|
||||
|
||||
/// 创建用户
|
||||
pub async fn create_user(&self, user: &CreateUser) -> Result<User>;
|
||||
|
||||
/// 获取用户
|
||||
pub async fn get_user(&self, id: &str) -> Result<Option<User>>;
|
||||
|
||||
/// 按用户名获取
|
||||
pub async fn get_by_username(&self, username: &str) -> Result<Option<User>>;
|
||||
|
||||
/// 更新用户
|
||||
pub async fn update_user(&self, id: &str, update: &UpdateUser) -> Result<()>;
|
||||
|
||||
/// 删除用户
|
||||
pub async fn delete_user(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// 列出用户
|
||||
pub async fn list_users(&self) -> Result<Vec<User>>;
|
||||
|
||||
/// 验证密码
|
||||
pub async fn verify_password(&self, username: &str, password: &str) -> Result<Option<User>>;
|
||||
|
||||
/// 更新密码
|
||||
pub async fn update_password(&self, id: &str, new_password: &str) -> Result<()>;
|
||||
|
||||
/// 检查是否需要初始化
|
||||
pub async fn needs_setup(&self) -> Result<bool>;
|
||||
}
|
||||
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub role: UserRole,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub enum UserRole {
|
||||
Admin,
|
||||
User,
|
||||
}
|
||||
|
||||
pub struct CreateUser {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub role: UserRole,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 SessionStore (session.rs)
|
||||
|
||||
```rust
|
||||
pub struct SessionStore {
|
||||
/// 会话映射
|
||||
sessions: RwLock<HashMap<String, Session>>,
|
||||
|
||||
/// 会话超时
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl SessionStore {
|
||||
/// 创建存储
|
||||
pub fn new(timeout: Duration) -> Self;
|
||||
|
||||
/// 创建会话
|
||||
pub fn create_session(&self, user: &User) -> String;
|
||||
|
||||
/// 获取会话
|
||||
pub fn get_session(&self, token: &str) -> Option<Session>;
|
||||
|
||||
/// 删除会话
|
||||
pub fn delete_session(&self, token: &str);
|
||||
|
||||
/// 清理过期会话
|
||||
pub fn cleanup_expired(&self);
|
||||
|
||||
/// 刷新会话
|
||||
pub fn refresh_session(&self, token: &str) -> bool;
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub token: String,
|
||||
pub user_id: String,
|
||||
pub username: String,
|
||||
pub role: UserRole,
|
||||
pub created_at: Instant,
|
||||
pub last_active: Instant,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 密码哈希 (password.rs)
|
||||
|
||||
```rust
|
||||
/// 哈希密码
|
||||
pub fn hash_password(password: &str) -> Result<String> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hash = argon2
|
||||
.hash_password(password.as_bytes(), &salt)?
|
||||
.to_string();
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
/// 验证密码
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool> {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 认证中间件 (middleware.rs)
|
||||
|
||||
```rust
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
cookies: Cookies,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// 获取 session token
|
||||
let token = cookies
|
||||
.get("session_id")
|
||||
.map(|c| c.value().to_string());
|
||||
|
||||
// 验证会话
|
||||
let session = token
|
||||
.and_then(|t| state.sessions.get_session(&t));
|
||||
|
||||
if let Some(session) = session {
|
||||
// 将用户信息注入请求
|
||||
let mut request = request;
|
||||
request.extensions_mut().insert(session);
|
||||
next.run(request).await
|
||||
} else {
|
||||
StatusCode::UNAUTHORIZED.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn admin_middleware(
|
||||
session: Extension<Session>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
if session.role == UserRole::Admin {
|
||||
next.run(request).await
|
||||
} else {
|
||||
StatusCode::FORBIDDEN.into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端点
|
||||
|
||||
| 端点 | 方法 | 权限 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `/api/auth/login` | POST | Public | 登录 |
|
||||
| `/api/auth/logout` | POST | User | 登出 |
|
||||
| `/api/auth/check` | GET | User | 检查认证 |
|
||||
| `/api/auth/password` | POST | User | 修改密码 |
|
||||
| `/api/users` | GET | Admin | 列出用户 |
|
||||
| `/api/users` | POST | Admin | 创建用户 |
|
||||
| `/api/users/:id` | DELETE | Admin | 删除用户 |
|
||||
| `/api/setup/init` | POST | Public | 初始化设置 |
|
||||
|
||||
### 请求/响应格式
|
||||
|
||||
```json
|
||||
// POST /api/auth/login
|
||||
// Request:
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
// Response:
|
||||
{
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"username": "admin",
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/auth/check
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"username": "admin",
|
||||
"role": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct AuthConfig {
|
||||
/// 会话超时 (秒)
|
||||
pub session_timeout_secs: u64,
|
||||
|
||||
/// 是否启用认证
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for AuthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
session_timeout_secs: 86400, // 24 小时
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全特性
|
||||
|
||||
### 5.1 密码存储
|
||||
|
||||
- Argon2id 哈希
|
||||
- 随机盐值
|
||||
- 不可逆
|
||||
|
||||
### 5.2 会话安全
|
||||
|
||||
- 随机 token (UUID v4)
|
||||
- HTTPOnly Cookie
|
||||
- 会话超时
|
||||
- 自动清理
|
||||
|
||||
### 5.3 权限控制
|
||||
|
||||
- 两级权限: Admin / User
|
||||
- 中间件检查
|
||||
- 敏感操作需 Admin
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用示例
|
||||
|
||||
```rust
|
||||
// 创建用户
|
||||
let user = users.create_user(&CreateUser {
|
||||
username: "admin".to_string(),
|
||||
password: "password123".to_string(),
|
||||
role: UserRole::Admin,
|
||||
}).await?;
|
||||
|
||||
// 验证密码
|
||||
if let Some(user) = users.verify_password("admin", "password123").await? {
|
||||
// 创建会话
|
||||
let token = sessions.create_session(&user);
|
||||
|
||||
// 设置 Cookie
|
||||
cookies.add(Cookie::build("session_id", token)
|
||||
.http_only(true)
|
||||
.path("/")
|
||||
.finish());
|
||||
}
|
||||
|
||||
// 获取会话
|
||||
if let Some(session) = sessions.get_session(&token) {
|
||||
println!("User: {}", session.username);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AuthError {
|
||||
#[error("Invalid credentials")]
|
||||
InvalidCredentials,
|
||||
|
||||
#[error("User not found")]
|
||||
UserNotFound,
|
||||
|
||||
#[error("User already exists")]
|
||||
UserExists,
|
||||
|
||||
#[error("Session expired")]
|
||||
SessionExpired,
|
||||
|
||||
#[error("Permission denied")]
|
||||
PermissionDenied,
|
||||
|
||||
#[error("Setup required")]
|
||||
SetupRequired,
|
||||
}
|
||||
```
|
||||
@@ -1,755 +0,0 @@
|
||||
# Config 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
Config 模块提供配置管理功能,所有配置存储在 SQLite 数据库中,使用 ArcSwap 实现无锁读取,提供高性能配置访问。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- SQLite 配置存储(持久化)
|
||||
- 无锁配置读取(ArcSwap)
|
||||
- 类型安全的配置结构
|
||||
- 配置变更通知(broadcast channel)
|
||||
- TypeScript 类型生成(typeshare)
|
||||
- RESTful API(按功能域分离)
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/config/
|
||||
├── mod.rs # 模块导出
|
||||
├── schema.rs # 配置结构定义(包含所有子配置)
|
||||
└── store.rs # SQLite 存储与无锁缓存
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心组件
|
||||
|
||||
### 2.1 ConfigStore (store.rs)
|
||||
|
||||
配置存储使用 **ArcSwap** 实现无锁读取,提供接近零成本的配置访问性能:
|
||||
|
||||
```rust
|
||||
pub struct ConfigStore {
|
||||
pool: Pool<Sqlite>,
|
||||
/// 无锁缓存,使用 ArcSwap 实现零成本读取
|
||||
cache: Arc<ArcSwap<AppConfig>>,
|
||||
/// 配置变更通知通道
|
||||
change_tx: broadcast::Sender<ConfigChange>,
|
||||
}
|
||||
|
||||
impl ConfigStore {
|
||||
/// 创建存储
|
||||
pub async fn new(db_path: &Path) -> Result<Self>;
|
||||
|
||||
/// 获取当前配置(无锁,零拷贝)
|
||||
///
|
||||
/// 返回 Arc<AppConfig>,高效共享无需克隆
|
||||
/// 这是一个无锁操作,开销极小
|
||||
pub fn get(&self) -> Arc<AppConfig>;
|
||||
|
||||
/// 设置完整配置
|
||||
pub async fn set(&self, config: AppConfig) -> Result<()>;
|
||||
|
||||
/// 使用闭包更新配置
|
||||
///
|
||||
/// 读-修改-写模式。并发更新时,最后的写入获胜。
|
||||
/// 对于不频繁的用户触发配置更改来说是可接受的。
|
||||
pub async fn update<F>(&self, f: F) -> Result<()>
|
||||
where
|
||||
F: FnOnce(&mut AppConfig);
|
||||
|
||||
/// 订阅配置变更事件
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<ConfigChange>;
|
||||
|
||||
/// 检查系统是否已初始化(无锁)
|
||||
pub fn is_initialized(&self) -> bool;
|
||||
|
||||
/// 获取数据库连接池(用于会话管理)
|
||||
pub fn pool(&self) -> &Pool<Sqlite>;
|
||||
}
|
||||
```
|
||||
|
||||
**性能特点**:
|
||||
- `get()` 是无锁读取操作,返回 `Arc<AppConfig>`,无需克隆
|
||||
- 配置读取频率远高于写入,ArcSwap 优化了读取路径
|
||||
- 写入操作先持久化到数据库,再原子性更新内存缓存
|
||||
- 使用 broadcast channel 通知配置变更,支持多订阅者
|
||||
|
||||
**数据库连接池配置**:
|
||||
```rust
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(2) // SQLite 单写模式,2 个连接足够
|
||||
.acquire_timeout(Duration::from_secs(5))
|
||||
.idle_timeout(Duration::from_secs(300))
|
||||
```
|
||||
|
||||
### 2.2 AppConfig (schema.rs)
|
||||
|
||||
主应用配置结构,包含所有子系统的配置:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct AppConfig {
|
||||
/// 初始设置是否完成
|
||||
pub initialized: bool,
|
||||
|
||||
/// 认证配置
|
||||
pub auth: AuthConfig,
|
||||
|
||||
/// 视频采集配置
|
||||
pub video: VideoConfig,
|
||||
|
||||
/// HID(键盘/鼠标)配置
|
||||
pub hid: HidConfig,
|
||||
|
||||
/// MSD(大容量存储)配置
|
||||
pub msd: MsdConfig,
|
||||
|
||||
/// ATX 电源控制配置
|
||||
pub atx: AtxConfig,
|
||||
|
||||
/// 音频配置
|
||||
pub audio: AudioConfig,
|
||||
|
||||
/// 流媒体配置
|
||||
pub stream: StreamConfig,
|
||||
|
||||
/// Web 服务器配置
|
||||
pub web: WebConfig,
|
||||
|
||||
/// 扩展配置(ttyd, gostc, easytier)
|
||||
pub extensions: ExtensionsConfig,
|
||||
|
||||
/// RustDesk 远程访问配置
|
||||
pub rustdesk: RustDeskConfig,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 主要子配置结构
|
||||
|
||||
#### AuthConfig - 认证配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct AuthConfig {
|
||||
/// 会话超时时间(秒)
|
||||
pub session_timeout_secs: u32, // 默认 86400(24小时)
|
||||
/// 启用双因素认证
|
||||
pub totp_enabled: bool,
|
||||
/// TOTP 密钥(加密存储)
|
||||
pub totp_secret: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
#### VideoConfig - 视频采集配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct VideoConfig {
|
||||
/// 视频设备路径(如 /dev/video0)
|
||||
pub device: Option<String>,
|
||||
/// 像素格式(如 "MJPEG", "YUYV", "NV12")
|
||||
pub format: Option<String>,
|
||||
/// 分辨率宽度
|
||||
pub width: u32, // 默认 1920
|
||||
/// 分辨率高度
|
||||
pub height: u32, // 默认 1080
|
||||
/// 帧率
|
||||
pub fps: u32, // 默认 30
|
||||
/// JPEG 质量(1-100)
|
||||
pub quality: u32, // 默认 80
|
||||
}
|
||||
```
|
||||
|
||||
#### HidConfig - HID 配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct HidConfig {
|
||||
/// HID 后端类型
|
||||
pub backend: HidBackend, // Otg | Ch9329 | None
|
||||
/// OTG 键盘设备路径
|
||||
pub otg_keyboard: String, // 默认 "/dev/hidg0"
|
||||
/// OTG 鼠标设备路径
|
||||
pub otg_mouse: String, // 默认 "/dev/hidg1"
|
||||
/// OTG UDC(USB 设备控制器)名称
|
||||
pub otg_udc: Option<String>,
|
||||
/// OTG USB 设备描述符配置
|
||||
pub otg_descriptor: OtgDescriptorConfig,
|
||||
/// CH9329 串口路径
|
||||
pub ch9329_port: String, // 默认 "/dev/ttyUSB0"
|
||||
/// CH9329 波特率
|
||||
pub ch9329_baudrate: u32, // 默认 9600
|
||||
/// 鼠标模式:绝对定位或相对定位
|
||||
pub mouse_absolute: bool, // 默认 true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[typeshare]
|
||||
pub struct OtgDescriptorConfig {
|
||||
pub vendor_id: u16, // 默认 0x1d6b(Linux Foundation)
|
||||
pub product_id: u16, // 默认 0x0104
|
||||
pub manufacturer: String, // 默认 "One-KVM"
|
||||
pub product: String, // 默认 "One-KVM USB Device"
|
||||
pub serial_number: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
#### StreamConfig - 流媒体配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct StreamConfig {
|
||||
/// 流模式(WebRTC | Mjpeg)
|
||||
pub mode: StreamMode,
|
||||
/// 编码器类型
|
||||
pub encoder: EncoderType, // Auto | Software | Vaapi | Nvenc | Qsv | Amf | Rkmpp | V4l2m2m
|
||||
/// 码率预设(Speed | Balanced | Quality)
|
||||
pub bitrate_preset: BitratePreset,
|
||||
/// 自定义 STUN 服务器
|
||||
pub stun_server: Option<String>, // 默认 "stun:stun.l.google.com:19302"
|
||||
/// 自定义 TURN 服务器
|
||||
pub turn_server: Option<String>,
|
||||
/// TURN 用户名
|
||||
pub turn_username: Option<String>,
|
||||
/// TURN 密码(加密存储,不通过 API 暴露)
|
||||
pub turn_password: Option<String>,
|
||||
/// 无客户端时自动暂停
|
||||
#[typeshare(skip)]
|
||||
pub auto_pause_enabled: bool,
|
||||
/// 自动暂停延迟(秒)
|
||||
#[typeshare(skip)]
|
||||
pub auto_pause_delay_secs: u64,
|
||||
/// 客户端超时清理(秒)
|
||||
#[typeshare(skip)]
|
||||
pub client_timeout_secs: u64,
|
||||
}
|
||||
```
|
||||
|
||||
#### MsdConfig - 大容量存储配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct MsdConfig {
|
||||
/// 启用 MSD 功能
|
||||
pub enabled: bool, // 默认 true
|
||||
/// ISO/IMG 镜像存储路径
|
||||
pub images_path: String, // 默认 "./data/msd/images"
|
||||
/// Ventoy 启动驱动器文件路径
|
||||
pub drive_path: String, // 默认 "./data/msd/ventoy.img"
|
||||
/// 虚拟驱动器大小(MB,最小 1024)
|
||||
pub virtual_drive_size_mb: u32, // 默认 16384(16GB)
|
||||
}
|
||||
```
|
||||
|
||||
#### AtxConfig - ATX 电源控制配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct AtxConfig {
|
||||
/// 启用 ATX 功能
|
||||
pub enabled: bool,
|
||||
/// 电源按钮配置(短按和长按共用)
|
||||
pub power: AtxKeyConfig,
|
||||
/// 重置按钮配置
|
||||
pub reset: AtxKeyConfig,
|
||||
/// LED 检测配置(可选)
|
||||
pub led: AtxLedConfig,
|
||||
/// WOL 数据包使用的网络接口(空字符串 = 自动)
|
||||
pub wol_interface: String,
|
||||
}
|
||||
```
|
||||
|
||||
#### AudioConfig - 音频配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct AudioConfig {
|
||||
/// 启用音频采集
|
||||
pub enabled: bool, // 默认 false
|
||||
/// ALSA 设备名称
|
||||
pub device: String, // 默认 "default"
|
||||
/// 音频质量预设:"voice" | "balanced" | "high"
|
||||
pub quality: String, // 默认 "balanced"
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:采样率固定为 48000Hz,声道固定为 2(立体声),这是 Opus 编码和 WebRTC 的最佳配置。
|
||||
|
||||
#### WebConfig - Web 服务器配置
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[typeshare]
|
||||
pub struct WebConfig {
|
||||
/// HTTP 端口
|
||||
pub http_port: u16, // 默认 8080
|
||||
/// HTTPS 端口
|
||||
pub https_port: u16, // 默认 8443
|
||||
/// 绑定地址
|
||||
pub bind_address: String, // 默认 "0.0.0.0"
|
||||
/// 启用 HTTPS
|
||||
pub https_enabled: bool, // 默认 false
|
||||
/// 自定义 SSL 证书路径
|
||||
pub ssl_cert_path: Option<String>,
|
||||
/// 自定义 SSL 密钥路径
|
||||
pub ssl_key_path: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. TypeScript 类型生成
|
||||
|
||||
使用 `#[typeshare]` 属性自动生成 TypeScript 类型:
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct VideoConfig {
|
||||
pub device: Option<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
```
|
||||
|
||||
生成的 TypeScript:
|
||||
|
||||
```typescript
|
||||
export interface VideoConfig {
|
||||
device?: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
```
|
||||
|
||||
生成命令:
|
||||
|
||||
```bash
|
||||
./scripts/generate-types.sh
|
||||
# 或
|
||||
typeshare src --lang=typescript --output-file=web/src/types/generated.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 端点
|
||||
|
||||
所有配置端点均需要 **Admin** 权限,采用 RESTful 设计,按功能域分离。
|
||||
|
||||
### 4.1 全局配置
|
||||
|
||||
| 端点 | 方法 | 权限 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `/api/config` | GET | Admin | 获取完整配置(敏感信息已过滤) |
|
||||
| `/api/config` | POST | Admin | 更新完整配置(已废弃,请使用按域 PATCH) |
|
||||
|
||||
### 4.2 分域配置端点
|
||||
|
||||
| 端点 | 方法 | 权限 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `/api/config/video` | GET | Admin | 获取视频配置 |
|
||||
| `/api/config/video` | PATCH | Admin | 更新视频配置(部分更新) |
|
||||
| `/api/config/stream` | GET | Admin | 获取流配置 |
|
||||
| `/api/config/stream` | PATCH | Admin | 更新流配置(部分更新) |
|
||||
| `/api/config/hid` | GET | Admin | 获取 HID 配置 |
|
||||
| `/api/config/hid` | PATCH | Admin | 更新 HID 配置(部分更新) |
|
||||
| `/api/config/msd` | GET | Admin | 获取 MSD 配置 |
|
||||
| `/api/config/msd` | PATCH | Admin | 更新 MSD 配置(部分更新) |
|
||||
| `/api/config/atx` | GET | Admin | 获取 ATX 配置 |
|
||||
| `/api/config/atx` | PATCH | Admin | 更新 ATX 配置(部分更新) |
|
||||
| `/api/config/audio` | GET | Admin | 获取音频配置 |
|
||||
| `/api/config/audio` | PATCH | Admin | 更新音频配置(部分更新) |
|
||||
| `/api/config/web` | GET | Admin | 获取 Web 服务器配置 |
|
||||
| `/api/config/web` | PATCH | Admin | 更新 Web 服务器配置(部分更新) |
|
||||
|
||||
### 4.3 RustDesk 配置端点
|
||||
|
||||
| 端点 | 方法 | 权限 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `/api/config/rustdesk` | GET | Admin | 获取 RustDesk 配置 |
|
||||
| `/api/config/rustdesk` | PATCH | Admin | 更新 RustDesk 配置 |
|
||||
| `/api/config/rustdesk/status` | GET | Admin | 获取 RustDesk 服务状态 |
|
||||
| `/api/config/rustdesk/password` | GET | Admin | 获取设备密码 |
|
||||
| `/api/config/rustdesk/regenerate-id` | POST | Admin | 重新生成设备 ID |
|
||||
| `/api/config/rustdesk/regenerate-password` | POST | Admin | 重新生成设备密码 |
|
||||
|
||||
### 4.4 请求/响应示例
|
||||
|
||||
#### 获取视频配置
|
||||
|
||||
```bash
|
||||
GET /api/config/video
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"device": "/dev/video0",
|
||||
"format": "MJPEG",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30,
|
||||
"quality": 80
|
||||
}
|
||||
```
|
||||
|
||||
#### 部分更新视频配置
|
||||
|
||||
```bash
|
||||
PATCH /api/config/video
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 60
|
||||
}
|
||||
```
|
||||
|
||||
响应:更新后的完整 VideoConfig
|
||||
```json
|
||||
{
|
||||
"device": "/dev/video0",
|
||||
"format": "MJPEG",
|
||||
"width": 1280,
|
||||
"height": 720,
|
||||
"fps": 60,
|
||||
"quality": 80
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 所有 PATCH 请求都支持部分更新,只需要提供要修改的字段
|
||||
- 未提供的字段保持原有值不变
|
||||
- 更新后返回完整的配置对象
|
||||
- 配置变更会自动触发相关组件重载
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置变更通知
|
||||
|
||||
ConfigStore 提供 broadcast channel 用于配置变更通知:
|
||||
|
||||
```rust
|
||||
/// 配置变更事件
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfigChange {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
// 订阅配置变更
|
||||
let mut rx = config_store.subscribe();
|
||||
|
||||
// 监听变更事件
|
||||
while let Ok(change) = rx.recv().await {
|
||||
println!("配置 {} 已更新", change.key);
|
||||
// 重载相关组件
|
||||
}
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. 调用 `config_store.set()` 或 `config_store.update()`
|
||||
2. 配置写入数据库(持久化)
|
||||
3. 原子性更新内存缓存(ArcSwap)
|
||||
4. 发送 `ConfigChange` 事件到 broadcast channel
|
||||
5. 各组件的订阅者接收事件并执行重载逻辑
|
||||
|
||||
**组件重载示例**:
|
||||
```rust
|
||||
// VideoStreamManager 监听配置变更
|
||||
let mut config_rx = config_store.subscribe();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(change) = config_rx.recv().await {
|
||||
if change.key == "app_config" {
|
||||
video_manager.reload().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库结构
|
||||
|
||||
ConfigStore 使用 SQLite 存储配置和其他系统数据:
|
||||
|
||||
### 6.1 配置表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
配置以 JSON 格式存储:
|
||||
```sql
|
||||
-- 应用配置
|
||||
key: 'app_config'
|
||||
value: '{"initialized": true, "video": {...}, "hid": {...}, ...}'
|
||||
```
|
||||
|
||||
### 6.2 用户表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
### 6.3 会话表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
data TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### 6.4 API 令牌表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
permissions TEXT NOT NULL,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_used TEXT
|
||||
);
|
||||
```
|
||||
|
||||
**存储特点**:
|
||||
- 所有配置存储在单个 JSON 文本中(`app_config` key)
|
||||
- 每次配置更新都更新整个 JSON,简化事务处理
|
||||
- 使用 `ON CONFLICT` 实现 upsert 操作
|
||||
- 连接池大小为 2(1 读 + 1 写),适合嵌入式环境
|
||||
|
||||
---
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
### 7.1 基本用法
|
||||
|
||||
```rust
|
||||
use crate::config::ConfigStore;
|
||||
use std::path::Path;
|
||||
|
||||
// 创建配置存储
|
||||
let config_store = ConfigStore::new(Path::new("./data/config.db")).await?;
|
||||
|
||||
// 获取配置(无锁,零拷贝)
|
||||
let config = config_store.get();
|
||||
println!("视频设备: {:?}", config.video.device);
|
||||
println!("是否已初始化: {}", config.initialized);
|
||||
|
||||
// 检查是否已初始化
|
||||
if !config_store.is_initialized() {
|
||||
println!("系统尚未初始化,请完成初始设置");
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 更新配置
|
||||
|
||||
```rust
|
||||
// 方式 1: 使用闭包更新(推荐)
|
||||
config_store.update(|config| {
|
||||
config.video.width = 1280;
|
||||
config.video.height = 720;
|
||||
config.video.fps = 60;
|
||||
}).await?;
|
||||
|
||||
// 方式 2: 整体替换
|
||||
let mut new_config = (*config_store.get()).clone();
|
||||
new_config.stream.mode = StreamMode::WebRTC;
|
||||
new_config.stream.encoder = EncoderType::Rkmpp;
|
||||
config_store.set(new_config).await?;
|
||||
```
|
||||
|
||||
### 7.3 订阅配置变更
|
||||
|
||||
```rust
|
||||
// 在组件中监听配置变更
|
||||
let config_store = state.config.clone();
|
||||
let mut rx = config_store.subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(change) = rx.recv().await {
|
||||
tracing::info!("配置 {} 已变更", change.key);
|
||||
|
||||
// 重新加载配置
|
||||
let config = config_store.get();
|
||||
|
||||
// 执行重载逻辑
|
||||
if let Err(e) = reload_component(&config).await {
|
||||
tracing::error!("重载组件失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 7.4 在 Handler 中使用
|
||||
|
||||
```rust
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::VideoConfig;
|
||||
use crate::state::AppState;
|
||||
|
||||
// 获取视频配置
|
||||
pub async fn get_video_config(
|
||||
State(state): State<Arc<AppState>>
|
||||
) -> Json<VideoConfig> {
|
||||
let config = state.config.get();
|
||||
Json(config.video.clone())
|
||||
}
|
||||
|
||||
// 更新视频配置
|
||||
pub async fn update_video_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(update): Json<VideoConfig>,
|
||||
) -> Result<Json<VideoConfig>> {
|
||||
// 更新配置
|
||||
state.config.update(|config| {
|
||||
config.video = update;
|
||||
}).await?;
|
||||
|
||||
// 返回更新后的配置
|
||||
let config = state.config.get();
|
||||
Ok(Json(config.video.clone()))
|
||||
}
|
||||
```
|
||||
|
||||
### 7.5 访问数据库连接池
|
||||
|
||||
```rust
|
||||
// ConfigStore 还提供数据库连接池访问
|
||||
// 用于用户管理、会话管理等功能
|
||||
|
||||
let pool = config_store.pool();
|
||||
|
||||
// 查询用户
|
||||
let user: Option<User> = sqlx::query_as(
|
||||
"SELECT * FROM users WHERE username = ?"
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 默认配置
|
||||
|
||||
系统首次运行时会自动创建默认配置:
|
||||
|
||||
```rust
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
initialized: false, // 需要通过初始设置向导完成
|
||||
auth: AuthConfig {
|
||||
session_timeout_secs: 86400, // 24小时
|
||||
totp_enabled: false,
|
||||
totp_secret: None,
|
||||
},
|
||||
video: VideoConfig {
|
||||
device: None, // 自动检测
|
||||
format: None, // 自动检测或使用 MJPEG
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 30,
|
||||
quality: 80,
|
||||
},
|
||||
stream: StreamConfig {
|
||||
mode: StreamMode::Mjpeg,
|
||||
encoder: EncoderType::Auto,
|
||||
bitrate_preset: BitratePreset::Balanced,
|
||||
stun_server: Some("stun:stun.l.google.com:19302".to_string()),
|
||||
turn_server: None,
|
||||
turn_username: None,
|
||||
turn_password: None,
|
||||
auto_pause_enabled: false,
|
||||
auto_pause_delay_secs: 10,
|
||||
client_timeout_secs: 30,
|
||||
},
|
||||
hid: HidConfig {
|
||||
backend: HidBackend::None, // 需要用户手动启用
|
||||
otg_keyboard: "/dev/hidg0".to_string(),
|
||||
otg_mouse: "/dev/hidg1".to_string(),
|
||||
otg_udc: None, // 自动检测
|
||||
otg_descriptor: OtgDescriptorConfig::default(),
|
||||
ch9329_port: "/dev/ttyUSB0".to_string(),
|
||||
ch9329_baudrate: 9600,
|
||||
mouse_absolute: true,
|
||||
},
|
||||
msd: MsdConfig {
|
||||
enabled: true,
|
||||
images_path: "./data/msd/images".to_string(),
|
||||
drive_path: "./data/msd/ventoy.img".to_string(),
|
||||
virtual_drive_size_mb: 16384, // 16GB
|
||||
},
|
||||
atx: AtxConfig {
|
||||
enabled: false, // 需要用户配置硬件绑定
|
||||
power: AtxKeyConfig::default(),
|
||||
reset: AtxKeyConfig::default(),
|
||||
led: AtxLedConfig::default(),
|
||||
wol_interface: String::new(), // 自动检测
|
||||
},
|
||||
audio: AudioConfig {
|
||||
enabled: false,
|
||||
device: "default".to_string(),
|
||||
quality: "balanced".to_string(),
|
||||
},
|
||||
web: WebConfig {
|
||||
http_port: 8080,
|
||||
https_port: 8443,
|
||||
bind_address: "0.0.0.0".to_string(),
|
||||
https_enabled: false,
|
||||
ssl_cert_path: None,
|
||||
ssl_key_path: None,
|
||||
},
|
||||
extensions: ExtensionsConfig::default(),
|
||||
rustdesk: RustDeskConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**配置初始化流程**:
|
||||
1. 用户首次访问 Web UI,系统检测到 `initialized = false`
|
||||
2. 重定向到初始设置向导(`/setup`)
|
||||
3. 用户设置管理员账户、选择视频设备等
|
||||
4. 完成设置后,`initialized` 设为 `true`
|
||||
5. 后续可通过设置页面(`/settings`)修改各项配置
|
||||
@@ -1,353 +0,0 @@
|
||||
# Events 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
Events 模块提供事件总线功能,用于模块间通信和状态广播。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- 事件发布/订阅
|
||||
- 多订阅者广播
|
||||
- WebSocket 事件推送
|
||||
- 状态变更通知
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/events/
|
||||
└── mod.rs # EventBus 实现
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Event System │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ Video │ │ HID │ │ Audio │
|
||||
│ Module │ │ Module │ │ Module │
|
||||
└───────┬────────┘ └───────┬────────┘ └───────┬────────┘
|
||||
│ │ │
|
||||
│ publish() │ publish() │ publish()
|
||||
└──────────────────┼──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ EventBus │
|
||||
│ (broadcast channel) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
│ subscribe() │ subscribe() │
|
||||
▼ ▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||
│ WebSocket │ │ DeviceInfo │ │ Internal │
|
||||
│ Handler │ │ Broadcaster │ │ Tasks │
|
||||
└────────────────┘ └────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 EventBus
|
||||
|
||||
```rust
|
||||
pub struct EventBus {
|
||||
/// 广播发送器
|
||||
tx: broadcast::Sender<SystemEvent>,
|
||||
}
|
||||
|
||||
impl EventBus {
|
||||
/// 创建事件总线
|
||||
pub fn new() -> Self {
|
||||
let (tx, _) = broadcast::channel(1024);
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
/// 发布事件
|
||||
pub fn publish(&self, event: SystemEvent) {
|
||||
let _ = self.tx.send(event);
|
||||
}
|
||||
|
||||
/// 订阅事件
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<SystemEvent> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 SystemEvent
|
||||
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub enum SystemEvent {
|
||||
// 视频事件
|
||||
StreamStateChanged {
|
||||
state: String,
|
||||
device: Option<String>,
|
||||
resolution: Option<Resolution>,
|
||||
fps: Option<f32>,
|
||||
},
|
||||
|
||||
VideoDeviceChanged {
|
||||
added: Vec<String>,
|
||||
removed: Vec<String>,
|
||||
},
|
||||
|
||||
// HID 事件
|
||||
HidStateChanged {
|
||||
backend: String,
|
||||
initialized: bool,
|
||||
keyboard_connected: bool,
|
||||
mouse_connected: bool,
|
||||
mouse_mode: String,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
// MSD 事件
|
||||
MsdStateChanged {
|
||||
mode: String,
|
||||
connected: bool,
|
||||
image: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
MsdDownloadProgress {
|
||||
download_id: String,
|
||||
downloaded: u64,
|
||||
total: u64,
|
||||
speed: u64,
|
||||
},
|
||||
|
||||
// ATX 事件
|
||||
AtxStateChanged {
|
||||
power_on: bool,
|
||||
last_action: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
// 音频事件
|
||||
AudioStateChanged {
|
||||
enabled: bool,
|
||||
streaming: bool,
|
||||
device: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
// 配置事件
|
||||
ConfigChanged {
|
||||
section: String,
|
||||
},
|
||||
|
||||
// 设备信息汇总
|
||||
DeviceInfo {
|
||||
video: VideoInfo,
|
||||
hid: HidInfo,
|
||||
msd: MsdInfo,
|
||||
atx: AtxInfo,
|
||||
audio: AudioInfo,
|
||||
},
|
||||
|
||||
// 系统错误
|
||||
SystemError {
|
||||
module: String,
|
||||
severity: String,
|
||||
message: String,
|
||||
},
|
||||
|
||||
// RustDesk 事件
|
||||
RustDeskStatusChanged {
|
||||
status: String,
|
||||
device_id: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
RustDeskConnectionOpened {
|
||||
connection_id: String,
|
||||
peer_id: String,
|
||||
},
|
||||
|
||||
RustDeskConnectionClosed {
|
||||
connection_id: String,
|
||||
peer_id: String,
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 设备信息广播器
|
||||
|
||||
在 `main.rs` 中启动的后台任务:
|
||||
|
||||
```rust
|
||||
pub fn spawn_device_info_broadcaster(
|
||||
state: Arc<AppState>,
|
||||
events: Arc<EventBus>,
|
||||
) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut rx = events.subscribe();
|
||||
let mut debounce = tokio::time::interval(Duration::from_millis(100));
|
||||
let mut pending = false;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 收到事件
|
||||
result = rx.recv() => {
|
||||
if result.is_ok() {
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖定时器
|
||||
_ = debounce.tick() => {
|
||||
if pending {
|
||||
pending = false;
|
||||
// 收集设备信息
|
||||
let device_info = state.get_device_info().await;
|
||||
// 广播
|
||||
events.publish(SystemEvent::DeviceInfo(device_info));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. WebSocket 事件推送
|
||||
|
||||
```rust
|
||||
pub async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| handle_ws(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
let mut rx = state.events.subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 发送事件给客户端
|
||||
result = rx.recv() => {
|
||||
if let Ok(event) = result {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
if socket.send(Message::Text(json)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 接收客户端消息
|
||||
msg = socket.recv() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用示例
|
||||
|
||||
### 6.1 发布事件
|
||||
|
||||
```rust
|
||||
// 视频模块发布状态变更
|
||||
events.publish(SystemEvent::StreamStateChanged {
|
||||
state: "streaming".to_string(),
|
||||
device: Some("/dev/video0".to_string()),
|
||||
resolution: Some(Resolution { width: 1920, height: 1080 }),
|
||||
fps: Some(30.0),
|
||||
});
|
||||
|
||||
// HID 模块发布状态变更
|
||||
events.publish(SystemEvent::HidStateChanged {
|
||||
backend: "otg".to_string(),
|
||||
initialized: true,
|
||||
keyboard_connected: true,
|
||||
mouse_connected: true,
|
||||
mouse_mode: "absolute".to_string(),
|
||||
error: None,
|
||||
});
|
||||
```
|
||||
|
||||
### 6.2 订阅事件
|
||||
|
||||
```rust
|
||||
let mut rx = events.subscribe();
|
||||
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(SystemEvent::StreamStateChanged { state, .. }) => {
|
||||
println!("Stream state: {}", state);
|
||||
}
|
||||
Ok(SystemEvent::HidStateChanged { backend, .. }) => {
|
||||
println!("HID backend: {}", backend);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端事件处理
|
||||
|
||||
```typescript
|
||||
// 连接 WebSocket
|
||||
const ws = new WebSocket('/api/ws');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'StreamStateChanged':
|
||||
updateStreamStatus(data);
|
||||
break;
|
||||
case 'HidStateChanged':
|
||||
updateHidStatus(data);
|
||||
break;
|
||||
case 'MsdStateChanged':
|
||||
updateMsdStatus(data);
|
||||
break;
|
||||
case 'DeviceInfo':
|
||||
updateAllDevices(data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 最佳实践
|
||||
|
||||
### 8.1 事件粒度
|
||||
|
||||
- 使用细粒度事件便于精确更新
|
||||
- DeviceInfo 用于初始化和定期同步
|
||||
|
||||
### 8.2 防抖
|
||||
|
||||
- 使用 100ms 防抖避免事件风暴
|
||||
- 合并多个快速变更
|
||||
|
||||
### 8.3 错误处理
|
||||
|
||||
- 发布失败静默忽略 (fire-and-forget)
|
||||
- 订阅者断开自动清理
|
||||
1153
docs/modules/hid.md
1153
docs/modules/hid.md
File diff suppressed because it is too large
Load Diff
@@ -1,617 +0,0 @@
|
||||
# MSD 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
MSD (Mass Storage Device) 模块提供虚拟存储设备功能,允许将 ISO/IMG 镜像作为 USB 存储设备挂载到目标计算机。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- ISO/IMG 镜像挂载
|
||||
- 镜像下载管理
|
||||
- Ventoy 多 ISO 启动盘
|
||||
- 热插拔支持
|
||||
- 下载进度追踪
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/msd/
|
||||
├── mod.rs # 模块导出
|
||||
├── controller.rs # MsdController (20KB)
|
||||
├── image.rs # 镜像管理 (21KB)
|
||||
├── ventoy_drive.rs # Ventoy 驱动 (24KB)
|
||||
├── monitor.rs # 健康监视 (9KB)
|
||||
└── types.rs # 类型定义 (6KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ MSD Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Web API
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ MsdController │
|
||||
│ (controller.rs) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Image │ │ Ventoy │ │ OTG │
|
||||
│ Manager │ │ Drive │ │ Service │
|
||||
│ (image.rs) │ │(ventoy.rs)│ │ │
|
||||
└──────┬──────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ /data/ │ │ exFAT │ │ MSD │
|
||||
│ images/ │ │ Drive │ │ Function │
|
||||
└─────────────┘ └───────────┘ └───────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Target PC │
|
||||
│ (USB Drive) │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### 2.2 MSD 模式
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ MSD Modes │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Image Mode │
|
||||
│ ┌───────────┐ │
|
||||
│ │ ISO/IMG │ ──► MSD LUN ──► Target PC sees single drive │
|
||||
│ │ File │ │
|
||||
│ └───────────┘ │
|
||||
│ 特点: │
|
||||
│ - 单个镜像文件 │
|
||||
│ - 直接挂载 │
|
||||
│ - 适合系统安装 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Ventoy Mode │
|
||||
│ ┌───────────┐ │
|
||||
│ │ ISO 1 │ │
|
||||
│ ├───────────┤ ┌───────────┐ │
|
||||
│ │ ISO 2 │ ──► │ Ventoy │ ──► Target PC sees bootable drive │
|
||||
│ ├───────────┤ │ Drive │ with ISO selection menu │
|
||||
│ │ ISO 3 │ └───────────┘ │
|
||||
│ └───────────┘ │
|
||||
│ 特点: │
|
||||
│ - 多个 ISO 文件 │
|
||||
│ - exFAT 文件系统 │
|
||||
│ - 启动菜单选择 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 MsdController (controller.rs)
|
||||
|
||||
MSD 控制器主类。
|
||||
|
||||
```rust
|
||||
pub struct MsdController {
|
||||
/// 当前状态
|
||||
state: Arc<RwLock<MsdState>>,
|
||||
|
||||
/// 镜像管理器
|
||||
image_manager: Arc<ImageManager>,
|
||||
|
||||
/// Ventoy 驱动器
|
||||
ventoy_drive: Arc<RwLock<Option<VentoyDrive>>>,
|
||||
|
||||
/// OTG 服务
|
||||
otg_service: Arc<OtgService>,
|
||||
|
||||
/// MSD 函数句柄
|
||||
msd_function: Arc<RwLock<Option<MsdFunction>>>,
|
||||
|
||||
/// 事件总线
|
||||
events: Arc<EventBus>,
|
||||
|
||||
/// 数据目录
|
||||
data_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl MsdController {
|
||||
/// 创建控制器
|
||||
pub async fn new(
|
||||
otg_service: Arc<OtgService>,
|
||||
data_dir: PathBuf,
|
||||
events: Arc<EventBus>,
|
||||
) -> Result<Arc<Self>>;
|
||||
|
||||
/// 获取状态
|
||||
pub fn state(&self) -> MsdState;
|
||||
|
||||
/// 连接 MSD
|
||||
pub async fn connect(&self) -> Result<()>;
|
||||
|
||||
/// 断开 MSD
|
||||
pub async fn disconnect(&self) -> Result<()>;
|
||||
|
||||
/// 切换到镜像模式
|
||||
pub async fn set_image(&self, image_id: &str) -> Result<()>;
|
||||
|
||||
/// 切换到 Ventoy 模式
|
||||
pub async fn set_ventoy(&self) -> Result<()>;
|
||||
|
||||
/// 清除当前挂载
|
||||
pub async fn clear(&self) -> Result<()>;
|
||||
|
||||
/// 列出镜像
|
||||
pub fn list_images(&self) -> Vec<ImageInfo>;
|
||||
|
||||
/// 上传镜像
|
||||
pub async fn upload_image(&self, name: &str, data: Bytes) -> Result<ImageInfo>;
|
||||
|
||||
/// 从 URL 下载镜像
|
||||
pub async fn download_image(&self, url: &str) -> Result<String>;
|
||||
|
||||
/// 删除镜像
|
||||
pub async fn delete_image(&self, image_id: &str) -> Result<()>;
|
||||
|
||||
/// 获取下载进度
|
||||
pub fn get_download_progress(&self, download_id: &str) -> Option<DownloadProgress>;
|
||||
}
|
||||
|
||||
pub struct MsdState {
|
||||
/// 是否可用
|
||||
pub available: bool,
|
||||
|
||||
/// 当前模式
|
||||
pub mode: MsdMode,
|
||||
|
||||
/// 是否已连接
|
||||
pub connected: bool,
|
||||
|
||||
/// 当前镜像信息
|
||||
pub current_image: Option<ImageInfo>,
|
||||
|
||||
/// 驱动器信息
|
||||
pub drive_info: Option<DriveInfo>,
|
||||
|
||||
/// 错误信息
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub enum MsdMode {
|
||||
/// 未激活
|
||||
None,
|
||||
|
||||
/// 单镜像模式
|
||||
Image,
|
||||
|
||||
/// Ventoy 模式
|
||||
Drive,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 ImageManager (image.rs)
|
||||
|
||||
镜像文件管理器。
|
||||
|
||||
```rust
|
||||
pub struct ImageManager {
|
||||
/// 镜像目录
|
||||
images_dir: PathBuf,
|
||||
|
||||
/// 镜像列表缓存
|
||||
images: RwLock<HashMap<String, ImageInfo>>,
|
||||
|
||||
/// 下载任务
|
||||
downloads: RwLock<HashMap<String, DownloadTask>>,
|
||||
|
||||
/// HTTP 客户端
|
||||
http_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl ImageManager {
|
||||
/// 创建管理器
|
||||
pub fn new(images_dir: PathBuf) -> Result<Self>;
|
||||
|
||||
/// 扫描镜像目录
|
||||
pub fn scan_images(&self) -> Result<Vec<ImageInfo>>;
|
||||
|
||||
/// 获取镜像信息
|
||||
pub fn get_image(&self, id: &str) -> Option<ImageInfo>;
|
||||
|
||||
/// 添加镜像
|
||||
pub async fn add_image(&self, name: &str, data: Bytes) -> Result<ImageInfo>;
|
||||
|
||||
/// 删除镜像
|
||||
pub fn delete_image(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// 开始下载
|
||||
pub async fn start_download(&self, url: &str) -> Result<String>;
|
||||
|
||||
/// 取消下载
|
||||
pub fn cancel_download(&self, download_id: &str) -> Result<()>;
|
||||
|
||||
/// 获取下载进度
|
||||
pub fn get_download_progress(&self, download_id: &str) -> Option<DownloadProgress>;
|
||||
|
||||
/// 验证镜像文件
|
||||
fn validate_image(path: &Path) -> Result<ImageFormat>;
|
||||
}
|
||||
|
||||
pub struct ImageInfo {
|
||||
/// 唯一 ID
|
||||
pub id: String,
|
||||
|
||||
/// 文件名
|
||||
pub name: String,
|
||||
|
||||
/// 文件大小
|
||||
pub size: u64,
|
||||
|
||||
/// 格式
|
||||
pub format: ImageFormat,
|
||||
|
||||
/// 创建时间
|
||||
pub created_at: DateTime<Utc>,
|
||||
|
||||
/// 下载状态
|
||||
pub download_status: Option<DownloadStatus>,
|
||||
}
|
||||
|
||||
pub enum ImageFormat {
|
||||
/// ISO 光盘镜像
|
||||
Iso,
|
||||
|
||||
/// 原始磁盘镜像
|
||||
Img,
|
||||
|
||||
/// 未知格式
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub struct DownloadProgress {
|
||||
/// 已下载字节
|
||||
pub downloaded: u64,
|
||||
|
||||
/// 总字节数
|
||||
pub total: u64,
|
||||
|
||||
/// 下载速度 (bytes/sec)
|
||||
pub speed: u64,
|
||||
|
||||
/// 预计剩余时间
|
||||
pub eta_secs: u64,
|
||||
|
||||
/// 状态
|
||||
pub status: DownloadStatus,
|
||||
}
|
||||
|
||||
pub enum DownloadStatus {
|
||||
Pending,
|
||||
Downloading,
|
||||
Completed,
|
||||
Failed(String),
|
||||
Cancelled,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 VentoyDrive (ventoy_drive.rs)
|
||||
|
||||
Ventoy 可启动驱动器管理。
|
||||
|
||||
```rust
|
||||
pub struct VentoyDrive {
|
||||
/// 驱动器路径
|
||||
drive_path: PathBuf,
|
||||
|
||||
/// 镜像路径
|
||||
images: Vec<PathBuf>,
|
||||
|
||||
/// 容量
|
||||
capacity: u64,
|
||||
|
||||
/// 已用空间
|
||||
used: u64,
|
||||
}
|
||||
|
||||
impl VentoyDrive {
|
||||
/// 创建 Ventoy 驱动器
|
||||
pub fn create(drive_path: PathBuf, capacity: u64) -> Result<Self>;
|
||||
|
||||
/// 添加 ISO
|
||||
pub fn add_iso(&mut self, iso_path: &Path) -> Result<()>;
|
||||
|
||||
/// 移除 ISO
|
||||
pub fn remove_iso(&mut self, name: &str) -> Result<()>;
|
||||
|
||||
/// 列出 ISO
|
||||
pub fn list_isos(&self) -> Vec<String>;
|
||||
|
||||
/// 获取驱动器信息
|
||||
pub fn info(&self) -> DriveInfo;
|
||||
|
||||
/// 获取驱动器路径
|
||||
pub fn path(&self) -> &Path;
|
||||
}
|
||||
|
||||
pub struct DriveInfo {
|
||||
/// 容量
|
||||
pub capacity: u64,
|
||||
|
||||
/// 已用空间
|
||||
pub used: u64,
|
||||
|
||||
/// 可用空间
|
||||
pub available: u64,
|
||||
|
||||
/// ISO 列表
|
||||
pub isos: Vec<String>,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 类型定义
|
||||
|
||||
### 4.1 MSD 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct MsdConfig {
|
||||
/// 是否启用 MSD
|
||||
pub enabled: bool,
|
||||
|
||||
/// 镜像目录
|
||||
pub images_dir: Option<String>,
|
||||
|
||||
/// 默认模式
|
||||
pub default_mode: MsdMode,
|
||||
|
||||
/// Ventoy 容量 (MB)
|
||||
pub ventoy_capacity_mb: u32,
|
||||
}
|
||||
|
||||
impl Default for MsdConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
images_dir: None,
|
||||
default_mode: MsdMode::None,
|
||||
ventoy_capacity_mb: 4096, // 4GB
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API 端点
|
||||
|
||||
| 端点 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/api/msd/status` | GET | 获取 MSD 状态 |
|
||||
| `/api/msd/connect` | POST | 连接 MSD |
|
||||
| `/api/msd/disconnect` | POST | 断开 MSD |
|
||||
| `/api/msd/images` | GET | 列出镜像 |
|
||||
| `/api/msd/images` | POST | 上传镜像 |
|
||||
| `/api/msd/images/:id` | DELETE | 删除镜像 |
|
||||
| `/api/msd/images/download` | POST | 从 URL 下载 |
|
||||
| `/api/msd/images/download/:id` | GET | 获取下载进度 |
|
||||
| `/api/msd/images/download/:id` | DELETE | 取消下载 |
|
||||
| `/api/msd/set-image` | POST | 设置当前镜像 |
|
||||
| `/api/msd/set-ventoy` | POST | 设置 Ventoy 模式 |
|
||||
| `/api/msd/clear` | POST | 清除挂载 |
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
// GET /api/msd/status
|
||||
{
|
||||
"available": true,
|
||||
"mode": "image",
|
||||
"connected": true,
|
||||
"current_image": {
|
||||
"id": "abc123",
|
||||
"name": "ubuntu-22.04.iso",
|
||||
"size": 4700000000,
|
||||
"format": "iso"
|
||||
},
|
||||
"drive_info": null,
|
||||
"error": null
|
||||
}
|
||||
|
||||
// GET /api/msd/images
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"id": "abc123",
|
||||
"name": "ubuntu-22.04.iso",
|
||||
"size": 4700000000,
|
||||
"format": "iso",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// POST /api/msd/images/download
|
||||
// Request: { "url": "https://example.com/image.iso" }
|
||||
// Response: { "download_id": "xyz789" }
|
||||
|
||||
// GET /api/msd/images/download/xyz789
|
||||
{
|
||||
"downloaded": 1234567890,
|
||||
"total": 4700000000,
|
||||
"speed": 12345678,
|
||||
"eta_secs": 280,
|
||||
"status": "downloading"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 事件
|
||||
|
||||
```rust
|
||||
pub enum SystemEvent {
|
||||
MsdStateChanged {
|
||||
mode: MsdMode,
|
||||
connected: bool,
|
||||
image: Option<String>,
|
||||
error: Option<String>,
|
||||
},
|
||||
|
||||
MsdDownloadProgress {
|
||||
download_id: String,
|
||||
progress: DownloadProgress,
|
||||
},
|
||||
|
||||
MsdDownloadComplete {
|
||||
download_id: String,
|
||||
image_id: String,
|
||||
success: bool,
|
||||
error: Option<String>,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MsdError {
|
||||
#[error("MSD not available")]
|
||||
NotAvailable,
|
||||
|
||||
#[error("Already connected")]
|
||||
AlreadyConnected,
|
||||
|
||||
#[error("Not connected")]
|
||||
NotConnected,
|
||||
|
||||
#[error("Image not found: {0}")]
|
||||
ImageNotFound(String),
|
||||
|
||||
#[error("Invalid image format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
#[error("Download failed: {0}")]
|
||||
DownloadFailed(String),
|
||||
|
||||
#[error("Storage full")]
|
||||
StorageFull,
|
||||
|
||||
#[error("OTG error: {0}")]
|
||||
OtgError(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 使用示例
|
||||
|
||||
### 8.1 挂载 ISO 镜像
|
||||
|
||||
```rust
|
||||
let msd = MsdController::new(otg_service, data_dir, events).await?;
|
||||
|
||||
// 列出镜像
|
||||
let images = msd.list_images();
|
||||
println!("Available images: {:?}", images);
|
||||
|
||||
// 设置镜像
|
||||
msd.set_image("abc123").await?;
|
||||
|
||||
// 连接到目标 PC
|
||||
msd.connect().await?;
|
||||
|
||||
// 目标 PC 现在可以看到 USB 驱动器...
|
||||
|
||||
// 断开连接
|
||||
msd.disconnect().await?;
|
||||
```
|
||||
|
||||
### 8.2 从 URL 下载
|
||||
|
||||
```rust
|
||||
// 开始下载
|
||||
let download_id = msd.download_image("https://example.com/ubuntu.iso").await?;
|
||||
|
||||
// 监控进度
|
||||
loop {
|
||||
if let Some(progress) = msd.get_download_progress(&download_id) {
|
||||
println!("Progress: {}%", progress.downloaded * 100 / progress.total);
|
||||
|
||||
if matches!(progress.status, DownloadStatus::Completed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 使用 Ventoy 模式
|
||||
|
||||
```rust
|
||||
// 切换到 Ventoy 模式
|
||||
msd.set_ventoy().await?;
|
||||
|
||||
// 获取驱动器信息
|
||||
let state = msd.state();
|
||||
if let Some(drive_info) = state.drive_info {
|
||||
println!("Capacity: {} MB", drive_info.capacity / 1024 / 1024);
|
||||
println!("ISOs: {:?}", drive_info.isos);
|
||||
}
|
||||
|
||||
// 连接
|
||||
msd.connect().await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### Q: 镜像无法挂载?
|
||||
|
||||
1. 检查镜像文件完整性
|
||||
2. 确认文件格式正确
|
||||
3. 检查存储空间
|
||||
|
||||
### Q: 目标 PC 不识别?
|
||||
|
||||
1. 检查 USB 连接
|
||||
2. 尝试重新连接
|
||||
3. 查看目标 PC 的设备管理器
|
||||
|
||||
### Q: 下载速度慢?
|
||||
|
||||
1. 检查网络连接
|
||||
2. 使用更近的镜像源
|
||||
3. 检查磁盘 I/O
|
||||
|
||||
### Q: Ventoy 启动失败?
|
||||
|
||||
1. 检查目标 PC BIOS 设置
|
||||
2. 尝试不同的启动模式
|
||||
3. 确认 ISO 文件支持 Ventoy
|
||||
@@ -1,667 +0,0 @@
|
||||
# OTG 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
OTG (On-The-Go) 模块负责管理 Linux USB Gadget,为 HID 和 MSD 功能提供统一的 USB 设备管理。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- USB Gadget 生命周期管理
|
||||
- HID 函数配置 (键盘、鼠标)
|
||||
- MSD 函数配置 (虚拟存储)
|
||||
- ConfigFS 操作
|
||||
- UDC 绑定/解绑
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/otg/
|
||||
├── mod.rs # 模块导出
|
||||
├── service.rs # OtgService (17KB)
|
||||
├── manager.rs # OtgGadgetManager (12KB)
|
||||
├── hid.rs # HID Function (7KB)
|
||||
├── msd.rs # MSD Function (14KB)
|
||||
├── configfs.rs # ConfigFS 操作 (4KB)
|
||||
├── endpoint.rs # 端点分配 (2KB)
|
||||
└── report_desc.rs # HID 报告描述符 (6KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 设计目标
|
||||
|
||||
解决 HID 和 MSD 共享同一个 USB Gadget 的所有权问题:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OTG Ownership Model │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ OtgService │ ◄── 唯一所有者
|
||||
│ (service.rs) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
enable_hid() enable_msd() 状态查询
|
||||
│ │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│OtgGadgetManager │
|
||||
│ (manager.rs) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────┐ ┌───────┐ ┌───────┐
|
||||
│ HID │ │ MSD │ │ UDC │
|
||||
│ Func │ │ Func │ │ Bind │
|
||||
└───────┘ └───────┘ └───────┘
|
||||
```
|
||||
|
||||
### 2.2 ConfigFS 结构
|
||||
|
||||
```
|
||||
/sys/kernel/config/usb_gadget/one-kvm/
|
||||
├── idVendor # 0x05ac (Apple)
|
||||
├── idProduct # 0x0001
|
||||
├── bcdDevice # 0x0100
|
||||
├── bcdUSB # 0x0200
|
||||
├── bMaxPacketSize0 # 64
|
||||
│
|
||||
├── strings/
|
||||
│ └── 0x409/ # English
|
||||
│ ├── manufacturer # "One-KVM"
|
||||
│ ├── product # "KVM Device"
|
||||
│ └── serialnumber # UUID
|
||||
│
|
||||
├── configs/
|
||||
│ └── c.1/
|
||||
│ ├── MaxPower # 500
|
||||
│ ├── strings/
|
||||
│ │ └── 0x409/
|
||||
│ │ └── configuration # "Config 1"
|
||||
│ └── (function symlinks)
|
||||
│
|
||||
├── functions/
|
||||
│ ├── hid.usb0/ # 键盘
|
||||
│ │ ├── protocol # 1 (keyboard)
|
||||
│ │ ├── subclass # 1 (boot)
|
||||
│ │ ├── report_length # 8
|
||||
│ │ └── report_desc # (binary)
|
||||
│ │
|
||||
│ ├── hid.usb1/ # 相对鼠标
|
||||
│ │ ├── protocol # 2 (mouse)
|
||||
│ │ ├── subclass # 1 (boot)
|
||||
│ │ ├── report_length # 4
|
||||
│ │ └── report_desc # (binary)
|
||||
│ │
|
||||
│ ├── hid.usb2/ # 绝对鼠标
|
||||
│ │ ├── protocol # 2 (mouse)
|
||||
│ │ ├── subclass # 0 (none)
|
||||
│ │ ├── report_length # 6
|
||||
│ │ └── report_desc # (binary)
|
||||
│ │
|
||||
│ └── mass_storage.usb0/ # 虚拟存储
|
||||
│ ├── stall # 1
|
||||
│ └── lun.0/
|
||||
│ ├── cdrom # 1 (ISO mode)
|
||||
│ ├── ro # 1 (read-only)
|
||||
│ ├── removable # 1
|
||||
│ ├── nofua # 1
|
||||
│ └── file # /path/to/image.iso
|
||||
│
|
||||
└── UDC # UDC 设备名
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 OtgService (service.rs)
|
||||
|
||||
OTG 服务主类,提供统一的 USB Gadget 管理接口。
|
||||
|
||||
```rust
|
||||
pub struct OtgService {
|
||||
/// Gadget 管理器
|
||||
manager: Arc<Mutex<OtgGadgetManager>>,
|
||||
|
||||
/// 当前状态
|
||||
state: Arc<RwLock<OtgServiceState>>,
|
||||
|
||||
/// HID 函数句柄
|
||||
hid_function: Arc<RwLock<Option<HidFunction>>>,
|
||||
|
||||
/// MSD 函数句柄
|
||||
msd_function: Arc<RwLock<Option<MsdFunction>>>,
|
||||
|
||||
/// 请求计数器 (lock-free)
|
||||
pending_requests: AtomicU8,
|
||||
}
|
||||
|
||||
impl OtgService {
|
||||
/// 创建服务
|
||||
pub fn new() -> Result<Self>;
|
||||
|
||||
/// 启用 HID 功能
|
||||
pub async fn enable_hid(&self) -> Result<HidDevicePaths>;
|
||||
|
||||
/// 禁用 HID 功能
|
||||
pub async fn disable_hid(&self) -> Result<()>;
|
||||
|
||||
/// 启用 MSD 功能
|
||||
pub async fn enable_msd(&self) -> Result<MsdFunction>;
|
||||
|
||||
/// 禁用 MSD 功能
|
||||
pub async fn disable_msd(&self) -> Result<()>;
|
||||
|
||||
/// 获取状态
|
||||
pub fn state(&self) -> OtgServiceState;
|
||||
|
||||
/// 检查 HID 是否启用
|
||||
pub fn is_hid_enabled(&self) -> bool;
|
||||
|
||||
/// 检查 MSD 是否启用
|
||||
pub fn is_msd_enabled(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct OtgServiceState {
|
||||
/// Gadget 是否激活
|
||||
pub gadget_active: bool,
|
||||
|
||||
/// HID 是否启用
|
||||
pub hid_enabled: bool,
|
||||
|
||||
/// MSD 是否启用
|
||||
pub msd_enabled: bool,
|
||||
|
||||
/// HID 设备路径
|
||||
pub hid_paths: Option<HidDevicePaths>,
|
||||
|
||||
/// 错误信息
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub struct HidDevicePaths {
|
||||
pub keyboard: PathBuf, // /dev/hidg0
|
||||
pub mouse_relative: PathBuf, // /dev/hidg1
|
||||
pub mouse_absolute: PathBuf, // /dev/hidg2
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 OtgGadgetManager (manager.rs)
|
||||
|
||||
Gadget 生命周期管理器。
|
||||
|
||||
```rust
|
||||
pub struct OtgGadgetManager {
|
||||
/// Gadget 路径
|
||||
gadget_path: PathBuf,
|
||||
|
||||
/// UDC 设备名
|
||||
udc_name: Option<String>,
|
||||
|
||||
/// 是否已创建
|
||||
created: bool,
|
||||
|
||||
/// 是否已绑定
|
||||
bound: bool,
|
||||
|
||||
/// 端点分配器
|
||||
endpoint_allocator: EndpointAllocator,
|
||||
}
|
||||
|
||||
impl OtgGadgetManager {
|
||||
/// 创建管理器
|
||||
pub fn new() -> Result<Self>;
|
||||
|
||||
/// 创建 Gadget
|
||||
pub fn create_gadget(&mut self, config: &GadgetConfig) -> Result<()>;
|
||||
|
||||
/// 销毁 Gadget
|
||||
pub fn destroy_gadget(&mut self) -> Result<()>;
|
||||
|
||||
/// 绑定 UDC
|
||||
pub fn bind_udc(&mut self) -> Result<()>;
|
||||
|
||||
/// 解绑 UDC
|
||||
pub fn unbind_udc(&mut self) -> Result<()>;
|
||||
|
||||
/// 添加函数
|
||||
pub fn add_function(&mut self, func: &dyn GadgetFunction) -> Result<()>;
|
||||
|
||||
/// 移除函数
|
||||
pub fn remove_function(&mut self, func: &dyn GadgetFunction) -> Result<()>;
|
||||
|
||||
/// 链接函数到配置
|
||||
pub fn link_function(&self, func: &dyn GadgetFunction) -> Result<()>;
|
||||
|
||||
/// 取消链接函数
|
||||
pub fn unlink_function(&self, func: &dyn GadgetFunction) -> Result<()>;
|
||||
|
||||
/// 检测可用 UDC
|
||||
fn detect_udc() -> Result<String>;
|
||||
}
|
||||
|
||||
pub struct GadgetConfig {
|
||||
pub name: String, // "one-kvm"
|
||||
pub vendor_id: u16, // 0x05ac
|
||||
pub product_id: u16, // 0x0001
|
||||
pub manufacturer: String, // "One-KVM"
|
||||
pub product: String, // "KVM Device"
|
||||
pub serial: String, // UUID
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 HID Function (hid.rs)
|
||||
|
||||
```rust
|
||||
pub struct HidFunction {
|
||||
/// 键盘函数
|
||||
keyboard: HidFunctionConfig,
|
||||
|
||||
/// 相对鼠标函数
|
||||
mouse_relative: HidFunctionConfig,
|
||||
|
||||
/// 绝对鼠标函数
|
||||
mouse_absolute: HidFunctionConfig,
|
||||
}
|
||||
|
||||
pub struct HidFunctionConfig {
|
||||
/// 函数名
|
||||
pub name: String, // "hid.usb0"
|
||||
|
||||
/// 协议
|
||||
pub protocol: u8, // 1=keyboard, 2=mouse
|
||||
|
||||
/// 子类
|
||||
pub subclass: u8, // 1=boot, 0=none
|
||||
|
||||
/// 报告长度
|
||||
pub report_length: u8,
|
||||
|
||||
/// 报告描述符
|
||||
pub report_desc: Vec<u8>,
|
||||
}
|
||||
|
||||
impl HidFunction {
|
||||
/// 创建 HID 函数
|
||||
pub fn new() -> Self;
|
||||
|
||||
/// 获取键盘报告描述符
|
||||
pub fn keyboard_report_desc() -> Vec<u8>;
|
||||
|
||||
/// 获取相对鼠标报告描述符
|
||||
pub fn mouse_relative_report_desc() -> Vec<u8>;
|
||||
|
||||
/// 获取绝对鼠标报告描述符
|
||||
pub fn mouse_absolute_report_desc() -> Vec<u8>;
|
||||
}
|
||||
|
||||
impl GadgetFunction for HidFunction {
|
||||
fn name(&self) -> &str;
|
||||
fn function_type(&self) -> &str; // "hid"
|
||||
fn configure(&self, path: &Path) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 MSD Function (msd.rs)
|
||||
|
||||
```rust
|
||||
pub struct MsdFunction {
|
||||
/// 函数名
|
||||
name: String,
|
||||
|
||||
/// LUN 配置
|
||||
luns: Vec<MsdLun>,
|
||||
}
|
||||
|
||||
pub struct MsdLun {
|
||||
/// LUN 编号
|
||||
pub lun_id: u8,
|
||||
|
||||
/// 镜像文件路径
|
||||
pub file: Option<PathBuf>,
|
||||
|
||||
/// 是否 CD-ROM 模式
|
||||
pub cdrom: bool,
|
||||
|
||||
/// 是否只读
|
||||
pub readonly: bool,
|
||||
|
||||
/// 是否可移除
|
||||
pub removable: bool,
|
||||
}
|
||||
|
||||
impl MsdFunction {
|
||||
/// 创建 MSD 函数
|
||||
pub fn new() -> Self;
|
||||
|
||||
/// 设置镜像文件
|
||||
pub fn set_image(&mut self, path: &Path, cdrom: bool) -> Result<()>;
|
||||
|
||||
/// 清除镜像
|
||||
pub fn clear_image(&mut self) -> Result<()>;
|
||||
|
||||
/// 弹出介质
|
||||
pub fn eject(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl GadgetFunction for MsdFunction {
|
||||
fn name(&self) -> &str;
|
||||
fn function_type(&self) -> &str; // "mass_storage"
|
||||
fn configure(&self, path: &Path) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 ConfigFS 操作 (configfs.rs)
|
||||
|
||||
```rust
|
||||
pub struct ConfigFs;
|
||||
|
||||
impl ConfigFs {
|
||||
/// ConfigFS 根路径
|
||||
const ROOT: &'static str = "/sys/kernel/config/usb_gadget";
|
||||
|
||||
/// 创建目录
|
||||
pub fn mkdir(path: &Path) -> Result<()>;
|
||||
|
||||
/// 删除目录
|
||||
pub fn rmdir(path: &Path) -> Result<()>;
|
||||
|
||||
/// 写入文件
|
||||
pub fn write_file(path: &Path, content: &str) -> Result<()>;
|
||||
|
||||
/// 写入二进制文件
|
||||
pub fn write_binary(path: &Path, data: &[u8]) -> Result<()>;
|
||||
|
||||
/// 读取文件
|
||||
pub fn read_file(path: &Path) -> Result<String>;
|
||||
|
||||
/// 创建符号链接
|
||||
pub fn symlink(target: &Path, link: &Path) -> Result<()>;
|
||||
|
||||
/// 删除符号链接
|
||||
pub fn unlink(path: &Path) -> Result<()>;
|
||||
|
||||
/// 列出目录
|
||||
pub fn list_dir(path: &Path) -> Result<Vec<String>>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 端点分配 (endpoint.rs)
|
||||
|
||||
```rust
|
||||
pub struct EndpointAllocator {
|
||||
/// 已使用的端点
|
||||
used_endpoints: HashSet<u8>,
|
||||
|
||||
/// 最大端点数
|
||||
max_endpoints: u8,
|
||||
}
|
||||
|
||||
impl EndpointAllocator {
|
||||
/// 创建分配器
|
||||
pub fn new(max_endpoints: u8) -> Self;
|
||||
|
||||
/// 分配端点
|
||||
pub fn allocate(&mut self, count: u8) -> Result<Vec<u8>>;
|
||||
|
||||
/// 释放端点
|
||||
pub fn release(&mut self, endpoints: &[u8]);
|
||||
|
||||
/// 检查可用端点数
|
||||
pub fn available(&self) -> u8;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.7 报告描述符 (report_desc.rs)
|
||||
|
||||
```rust
|
||||
pub struct ReportDescriptor;
|
||||
|
||||
impl ReportDescriptor {
|
||||
/// 标准键盘报告描述符
|
||||
pub fn keyboard() -> Vec<u8> {
|
||||
vec![
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xA1, 0x01, // Collection (Application)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0xE0, // Usage Minimum (224)
|
||||
0x29, 0xE7, // Usage Maximum (231)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x81, 0x01, // Input (Constant)
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x65, // Logical Maximum (101)
|
||||
0x05, 0x07, // Usage Page (Key Codes)
|
||||
0x19, 0x00, // Usage Minimum (0)
|
||||
0x29, 0x65, // Usage Maximum (101)
|
||||
0x81, 0x00, // Input (Data, Array)
|
||||
0xC0, // End Collection
|
||||
]
|
||||
}
|
||||
|
||||
/// 相对鼠标报告描述符
|
||||
pub fn mouse_relative() -> Vec<u8>;
|
||||
|
||||
/// 绝对鼠标报告描述符
|
||||
pub fn mouse_absolute() -> Vec<u8>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 生命周期管理
|
||||
|
||||
### 4.1 初始化流程
|
||||
|
||||
```
|
||||
OtgService::new()
|
||||
│
|
||||
├── 检测 UDC 设备
|
||||
│ └── 读取 /sys/class/udc/
|
||||
│
|
||||
├── 创建 OtgGadgetManager
|
||||
│
|
||||
└── 初始化状态
|
||||
|
||||
enable_hid()
|
||||
│
|
||||
├── 检查 Gadget 是否存在
|
||||
│ └── 如不存在,创建 Gadget
|
||||
│
|
||||
├── 创建 HID 函数
|
||||
│ ├── hid.usb0 (键盘)
|
||||
│ ├── hid.usb1 (相对鼠标)
|
||||
│ └── hid.usb2 (绝对鼠标)
|
||||
│
|
||||
├── 配置函数
|
||||
│ └── 写入报告描述符
|
||||
│
|
||||
├── 链接函数到配置
|
||||
│
|
||||
├── 绑定 UDC (如未绑定)
|
||||
│
|
||||
└── 等待设备节点出现
|
||||
└── /dev/hidg0, hidg1, hidg2
|
||||
```
|
||||
|
||||
### 4.2 清理流程
|
||||
|
||||
```
|
||||
disable_hid()
|
||||
│
|
||||
├── 检查是否有其他函数使用
|
||||
│
|
||||
├── 如果只有 HID,解绑 UDC
|
||||
│
|
||||
├── 取消链接 HID 函数
|
||||
│
|
||||
└── 删除 HID 函数目录
|
||||
|
||||
disable_msd()
|
||||
│
|
||||
├── 同上...
|
||||
│
|
||||
└── 如果没有任何函数,销毁 Gadget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置
|
||||
|
||||
### 5.1 OTG 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct OtgConfig {
|
||||
/// 是否启用 OTG
|
||||
pub enabled: bool,
|
||||
|
||||
/// 厂商 ID
|
||||
pub vendor_id: u16,
|
||||
|
||||
/// 产品 ID
|
||||
pub product_id: u16,
|
||||
|
||||
/// 厂商名称
|
||||
pub manufacturer: String,
|
||||
|
||||
/// 产品名称
|
||||
pub product: String,
|
||||
}
|
||||
|
||||
impl Default for OtgConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
vendor_id: 0x05ac, // Apple
|
||||
product_id: 0x0001,
|
||||
manufacturer: "One-KVM".to_string(),
|
||||
product: "KVM Device".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum OtgError {
|
||||
#[error("No UDC device found")]
|
||||
NoUdcDevice,
|
||||
|
||||
#[error("Gadget already exists")]
|
||||
GadgetExists,
|
||||
|
||||
#[error("Gadget not found")]
|
||||
GadgetNotFound,
|
||||
|
||||
#[error("Function already exists: {0}")]
|
||||
FunctionExists(String),
|
||||
|
||||
#[error("UDC busy")]
|
||||
UdcBusy,
|
||||
|
||||
#[error("ConfigFS error: {0}")]
|
||||
ConfigFsError(String),
|
||||
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
|
||||
#[error("Device node not found: {0}")]
|
||||
DeviceNodeNotFound(String),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
### 7.1 启用 HID
|
||||
|
||||
```rust
|
||||
let otg = OtgService::new()?;
|
||||
|
||||
// 启用 HID
|
||||
let paths = otg.enable_hid().await?;
|
||||
println!("Keyboard: {:?}", paths.keyboard);
|
||||
println!("Mouse relative: {:?}", paths.mouse_relative);
|
||||
println!("Mouse absolute: {:?}", paths.mouse_absolute);
|
||||
|
||||
// 使用设备...
|
||||
|
||||
// 禁用 HID
|
||||
otg.disable_hid().await?;
|
||||
```
|
||||
|
||||
### 7.2 启用 MSD
|
||||
|
||||
```rust
|
||||
let otg = OtgService::new()?;
|
||||
|
||||
// 启用 MSD
|
||||
let mut msd = otg.enable_msd().await?;
|
||||
|
||||
// 挂载 ISO
|
||||
msd.set_image(Path::new("/data/ubuntu.iso"), true)?;
|
||||
|
||||
// 弹出
|
||||
msd.eject()?;
|
||||
|
||||
// 禁用 MSD
|
||||
otg.disable_msd().await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### Q: 找不到 UDC 设备?
|
||||
|
||||
1. 检查内核是否支持 USB Gadget
|
||||
2. 加载必要的内核模块:
|
||||
```bash
|
||||
modprobe libcomposite
|
||||
modprobe usb_f_hid
|
||||
modprobe usb_f_mass_storage
|
||||
```
|
||||
3. 检查 `/sys/class/udc/` 目录
|
||||
|
||||
### Q: 权限错误?
|
||||
|
||||
1. 以 root 运行
|
||||
2. 或配置 udev 规则
|
||||
|
||||
### Q: 设备节点不出现?
|
||||
|
||||
1. 检查 UDC 是否正确绑定
|
||||
2. 查看 `dmesg` 日志
|
||||
3. 检查 ConfigFS 配置
|
||||
|
||||
### Q: 目标 PC 不识别?
|
||||
|
||||
1. 检查 USB 线缆
|
||||
2. 检查报告描述符
|
||||
3. 使用 `lsusb` 确认设备
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,428 +0,0 @@
|
||||
# Web 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
Web 模块提供 HTTP API 和静态文件服务。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- REST API
|
||||
- WebSocket
|
||||
- 静态文件服务
|
||||
- 认证中间件
|
||||
- CORS 支持
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/web/
|
||||
├── mod.rs # 模块导出
|
||||
├── routes.rs # 路由定义 (9KB)
|
||||
├── ws.rs # WebSocket (8KB)
|
||||
├── audio_ws.rs # 音频 WebSocket (8KB)
|
||||
├── static_files.rs # 静态文件 (6KB)
|
||||
└── handlers/ # API 处理器
|
||||
├── mod.rs
|
||||
└── config/
|
||||
├── mod.rs
|
||||
├── apply.rs
|
||||
├── types.rs
|
||||
└── rustdesk.rs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 路由结构
|
||||
|
||||
### 2.1 公共路由 (无认证)
|
||||
|
||||
| 路由 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/health` | GET | 健康检查 |
|
||||
| `/auth/login` | POST | 登录 |
|
||||
| `/setup` | GET | 获取设置状态 |
|
||||
| `/setup/init` | POST | 初始化设置 |
|
||||
|
||||
### 2.2 用户路由 (需认证)
|
||||
|
||||
| 路由 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/info` | GET | 系统信息 |
|
||||
| `/devices` | GET | 设备列表 |
|
||||
| `/stream/*` | * | 流控制 |
|
||||
| `/webrtc/*` | * | WebRTC 信令 |
|
||||
| `/hid/*` | * | HID 控制 |
|
||||
| `/audio/*` | * | 音频控制 |
|
||||
| `/ws` | WS | 事件 WebSocket |
|
||||
| `/ws/audio` | WS | 音频 WebSocket |
|
||||
|
||||
### 2.3 管理员路由 (需 Admin)
|
||||
|
||||
| 路由 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/config/*` | * | 配置管理 |
|
||||
| `/msd/*` | * | MSD 操作 |
|
||||
| `/atx/*` | * | ATX 控制 |
|
||||
| `/extensions/*` | * | 扩展管理 |
|
||||
| `/rustdesk/*` | * | RustDesk |
|
||||
| `/users/*` | * | 用户管理 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 路由定义
|
||||
|
||||
```rust
|
||||
pub fn create_router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
// 公共路由
|
||||
.route("/health", get(handlers::health))
|
||||
.route("/auth/login", post(handlers::login))
|
||||
.route("/setup", get(handlers::setup_status))
|
||||
.route("/setup/init", post(handlers::setup_init))
|
||||
|
||||
// 用户路由
|
||||
.nest("/api", user_routes())
|
||||
|
||||
// 管理员路由
|
||||
.nest("/api/admin", admin_routes())
|
||||
|
||||
// 静态文件
|
||||
.fallback(static_files::serve)
|
||||
|
||||
// 中间件
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
|
||||
// 状态
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
fn user_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/info", get(handlers::system_info))
|
||||
.route("/devices", get(handlers::list_devices))
|
||||
|
||||
// 流控制
|
||||
.route("/stream/status", get(handlers::stream_status))
|
||||
.route("/stream/start", post(handlers::stream_start))
|
||||
.route("/stream/stop", post(handlers::stream_stop))
|
||||
.route("/stream/mjpeg", get(handlers::mjpeg_stream))
|
||||
|
||||
// WebRTC
|
||||
.route("/webrtc/session", post(handlers::webrtc_create_session))
|
||||
.route("/webrtc/offer", post(handlers::webrtc_offer))
|
||||
.route("/webrtc/ice", post(handlers::webrtc_ice))
|
||||
.route("/webrtc/close", post(handlers::webrtc_close))
|
||||
|
||||
// HID
|
||||
.route("/hid/status", get(handlers::hid_status))
|
||||
.route("/hid/reset", post(handlers::hid_reset))
|
||||
|
||||
// WebSocket
|
||||
.route("/ws", get(handlers::ws_handler))
|
||||
.route("/ws/audio", get(handlers::audio_ws_handler))
|
||||
|
||||
// 认证中间件
|
||||
.layer(middleware::from_fn(auth_middleware))
|
||||
}
|
||||
|
||||
fn admin_routes() -> Router {
|
||||
Router::new()
|
||||
// 配置
|
||||
.route("/config", get(handlers::config::get_config))
|
||||
.route("/config", patch(handlers::config::update_config))
|
||||
|
||||
// MSD
|
||||
.route("/msd/status", get(handlers::msd_status))
|
||||
.route("/msd/connect", post(handlers::msd_connect))
|
||||
|
||||
// ATX
|
||||
.route("/atx/status", get(handlers::atx_status))
|
||||
.route("/atx/power/short", post(handlers::atx_power_short))
|
||||
|
||||
// 认证中间件
|
||||
.layer(middleware::from_fn(auth_middleware))
|
||||
.layer(middleware::from_fn(admin_middleware))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 静态文件服务
|
||||
|
||||
```rust
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "web/dist"]
|
||||
#[include = "*.html"]
|
||||
#[include = "*.js"]
|
||||
#[include = "*.css"]
|
||||
#[include = "assets/*"]
|
||||
struct Assets;
|
||||
|
||||
pub async fn serve(uri: Uri) -> impl IntoResponse {
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
|
||||
// 尝试获取文件
|
||||
if let Some(content) = Assets::get(path) {
|
||||
let mime = mime_guess::from_path(path)
|
||||
.first_or_octet_stream();
|
||||
|
||||
return (
|
||||
[(header::CONTENT_TYPE, mime.as_ref())],
|
||||
content.data.into_owned(),
|
||||
).into_response();
|
||||
}
|
||||
|
||||
// SPA 回退到 index.html
|
||||
if let Some(content) = Assets::get("index.html") {
|
||||
return (
|
||||
[(header::CONTENT_TYPE, "text/html")],
|
||||
content.data.into_owned(),
|
||||
).into_response();
|
||||
}
|
||||
|
||||
StatusCode::NOT_FOUND.into_response()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. WebSocket 处理
|
||||
|
||||
### 5.1 事件 WebSocket (ws.rs)
|
||||
|
||||
```rust
|
||||
pub async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| handle_ws(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
// 发送初始设备信息
|
||||
let device_info = state.get_device_info().await;
|
||||
let json = serde_json::to_string(&device_info).unwrap();
|
||||
let _ = socket.send(Message::Text(json)).await;
|
||||
|
||||
// 订阅事件
|
||||
let mut rx = state.events.subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 发送事件
|
||||
result = rx.recv() => {
|
||||
if let Ok(event) = result {
|
||||
let json = serde_json::to_string(&event).unwrap();
|
||||
if socket.send(Message::Text(json)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 接收消息 (心跳/关闭)
|
||||
msg = socket.recv() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
let _ = socket.send(Message::Pong(data)).await;
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 音频 WebSocket (audio_ws.rs)
|
||||
|
||||
```rust
|
||||
pub async fn audio_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|socket| handle_audio_ws(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_audio_ws(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
// 订阅音频帧
|
||||
let mut rx = state.audio.subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 发送音频帧
|
||||
result = rx.recv() => {
|
||||
if let Ok(frame) = result {
|
||||
if socket.send(Message::Binary(frame.data.to_vec())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理关闭
|
||||
msg = socket.recv() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. MJPEG 流
|
||||
|
||||
```rust
|
||||
pub async fn mjpeg_stream(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> impl IntoResponse {
|
||||
let boundary = "frame";
|
||||
|
||||
// 订阅视频帧
|
||||
let rx = state.stream_manager.subscribe_mjpeg();
|
||||
|
||||
// 创建流
|
||||
let stream = async_stream::stream! {
|
||||
let mut rx = rx;
|
||||
while let Ok(frame) = rx.recv().await {
|
||||
let header = format!(
|
||||
"--{}\r\nContent-Type: image/jpeg\r\nContent-Length: {}\r\n\r\n",
|
||||
boundary,
|
||||
frame.data.len()
|
||||
);
|
||||
yield Ok::<_, std::io::Error>(Bytes::from(header));
|
||||
yield Ok(frame.data.clone());
|
||||
yield Ok(Bytes::from("\r\n"));
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
[(
|
||||
header::CONTENT_TYPE,
|
||||
format!("multipart/x-mixed-replace; boundary={}", boundary),
|
||||
)],
|
||||
Body::from_stream(stream),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
```rust
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match self {
|
||||
AppError::AuthError => (StatusCode::UNAUTHORIZED, "Authentication failed"),
|
||||
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
|
||||
AppError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden"),
|
||||
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.as_str()),
|
||||
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
|
||||
AppError::Internal(err) => {
|
||||
tracing::error!("Internal error: {:?}", err);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
|
||||
}
|
||||
// ...
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 请求提取器
|
||||
|
||||
```rust
|
||||
// 从 Cookie 获取会话
|
||||
pub struct AuthUser(pub Session);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let cookies = Cookies::from_request_parts(parts, state).await?;
|
||||
let token = cookies
|
||||
.get("session_id")
|
||||
.map(|c| c.value().to_string())
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
let state = parts.extensions.get::<Arc<AppState>>().unwrap();
|
||||
let session = state.sessions
|
||||
.get_session(&token)
|
||||
.ok_or(AppError::Unauthorized)?;
|
||||
|
||||
Ok(AuthUser(session))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 中间件
|
||||
|
||||
### 9.1 认证中间件
|
||||
|
||||
```rust
|
||||
pub async fn auth_middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
cookies: Cookies,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let token = cookies
|
||||
.get("session_id")
|
||||
.map(|c| c.value().to_string());
|
||||
|
||||
if let Some(session) = token.and_then(|t| state.sessions.get_session(&t)) {
|
||||
request.extensions_mut().insert(session);
|
||||
next.run(request).await
|
||||
} else {
|
||||
StatusCode::UNAUTHORIZED.into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 Admin 中间件
|
||||
|
||||
```rust
|
||||
pub async fn admin_middleware(
|
||||
Extension(session): Extension<Session>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
if session.role == UserRole::Admin {
|
||||
next.run(request).await
|
||||
} else {
|
||||
StatusCode::FORBIDDEN.into_response()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. HTTPS 支持
|
||||
|
||||
```rust
|
||||
// 使用 axum-server 提供 TLS
|
||||
let tls_config = RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
||||
|
||||
axum_server::bind_rustls(addr, tls_config)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
// 或自动生成自签名证书
|
||||
let (cert, key) = generate_self_signed_cert()?;
|
||||
let tls_config = RustlsConfig::from_pem(cert, key).await?;
|
||||
```
|
||||
@@ -1,789 +0,0 @@
|
||||
# WebRTC 模块文档
|
||||
|
||||
## 1. 模块概述
|
||||
|
||||
WebRTC 模块提供低延迟的实时音视频流传输,支持多种视频编码格式和 DataChannel HID 控制。
|
||||
|
||||
### 1.1 主要功能
|
||||
|
||||
- WebRTC 会话管理
|
||||
- 多编码器支持 (H264/H265/VP8/VP9)
|
||||
- 音频轨道 (Opus)
|
||||
- DataChannel HID
|
||||
- ICE/STUN/TURN 支持
|
||||
|
||||
### 1.2 文件结构
|
||||
|
||||
```
|
||||
src/webrtc/
|
||||
├── mod.rs # 模块导出
|
||||
├── webrtc_streamer.rs # 统一管理器 (35KB)
|
||||
├── universal_session.rs # 会话管理 (32KB)
|
||||
├── unified_video_track.rs # 统一视频轨道 (15KB)
|
||||
├── video_track.rs # 视频轨道 (19KB)
|
||||
├── rtp.rs # RTP 打包 (24KB)
|
||||
├── h265_payloader.rs # H265 RTP (15KB)
|
||||
├── peer.rs # PeerConnection (17KB)
|
||||
├── config.rs # 配置 (3KB)
|
||||
├── signaling.rs # 信令 (5KB)
|
||||
├── session.rs # 会话基类 (8KB)
|
||||
└── track.rs # 轨道基类 (11KB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ WebRTC Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Browser
|
||||
│
|
||||
│ HTTP Signaling
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ WebRtcStreamer │
|
||||
│(webrtc_streamer)│
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Session │ │Session │ │Session │
|
||||
│ 1 │ │ 2 │ │ N │
|
||||
└───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │
|
||||
├───────────┼─────────────┤
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ SharedVideoPipeline │
|
||||
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
|
||||
│ │H264 │ │H265 │ │VP8 │ │VP9 │ │
|
||||
│ └─────┘ └─────┘ └─────┘ └─────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ VideoCapturer │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 会话生命周期
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Session Lifecycle │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
1. 创建会话
|
||||
POST /webrtc/session
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Create Session │
|
||||
│ Generate ID │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
{ session_id: "..." }
|
||||
|
||||
2. 发送 Offer
|
||||
POST /webrtc/offer
|
||||
{ session_id, codec, offer_sdp }
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Process Offer │
|
||||
│ Create Answer │
|
||||
│ Setup Tracks │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
{ answer_sdp, ice_candidates }
|
||||
|
||||
3. ICE 候选
|
||||
POST /webrtc/ice
|
||||
{ session_id, candidate }
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Add ICE │
|
||||
│ Candidate │
|
||||
└─────────────────┘
|
||||
|
||||
4. 连接建立
|
||||
┌─────────────────┐
|
||||
│ DTLS Handshake │
|
||||
│ SRTP Setup │
|
||||
│ DataChannel │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
开始传输视频/音频
|
||||
|
||||
5. 关闭会话
|
||||
POST /webrtc/close
|
||||
{ session_id }
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Cleanup │
|
||||
│ Release │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心组件
|
||||
|
||||
### 3.1 WebRtcStreamer (webrtc_streamer.rs)
|
||||
|
||||
WebRTC 服务主类。
|
||||
|
||||
```rust
|
||||
pub struct WebRtcStreamer {
|
||||
/// 会话映射
|
||||
sessions: Arc<RwLock<HashMap<String, Arc<UniversalSession>>>>,
|
||||
|
||||
/// 共享视频管道
|
||||
video_pipeline: Arc<SharedVideoPipeline>,
|
||||
|
||||
/// 共享音频管道
|
||||
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||
|
||||
/// HID 控制器
|
||||
hid: Arc<HidController>,
|
||||
|
||||
/// 配置
|
||||
config: WebRtcConfig,
|
||||
|
||||
/// 事件总线
|
||||
events: Arc<EventBus>,
|
||||
}
|
||||
|
||||
impl WebRtcStreamer {
|
||||
/// 创建流服务
|
||||
pub async fn new(
|
||||
video_pipeline: Arc<SharedVideoPipeline>,
|
||||
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||
hid: Arc<HidController>,
|
||||
config: WebRtcConfig,
|
||||
events: Arc<EventBus>,
|
||||
) -> Result<Self>;
|
||||
|
||||
/// 创建会话
|
||||
pub async fn create_session(&self) -> Result<String>;
|
||||
|
||||
/// 处理 Offer
|
||||
pub async fn process_offer(
|
||||
&self,
|
||||
session_id: &str,
|
||||
offer: &str,
|
||||
codec: VideoCodec,
|
||||
) -> Result<OfferResponse>;
|
||||
|
||||
/// 添加 ICE 候选
|
||||
pub async fn add_ice_candidate(
|
||||
&self,
|
||||
session_id: &str,
|
||||
candidate: &str,
|
||||
) -> Result<()>;
|
||||
|
||||
/// 关闭会话
|
||||
pub async fn close_session(&self, session_id: &str) -> Result<()>;
|
||||
|
||||
/// 获取会话列表
|
||||
pub fn list_sessions(&self) -> Vec<SessionInfo>;
|
||||
|
||||
/// 获取统计信息
|
||||
pub fn get_stats(&self) -> WebRtcStats;
|
||||
}
|
||||
|
||||
pub struct OfferResponse {
|
||||
pub answer_sdp: String,
|
||||
pub ice_candidates: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct WebRtcStats {
|
||||
pub active_sessions: usize,
|
||||
pub total_bytes_sent: u64,
|
||||
pub avg_bitrate: u32,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 UniversalSession (universal_session.rs)
|
||||
|
||||
单个 WebRTC 会话。
|
||||
|
||||
```rust
|
||||
pub struct UniversalSession {
|
||||
/// 会话 ID
|
||||
id: String,
|
||||
|
||||
/// PeerConnection
|
||||
peer: Arc<RTCPeerConnection>,
|
||||
|
||||
/// 视频轨道
|
||||
video_track: Arc<UniversalVideoTrack>,
|
||||
|
||||
/// 音频轨道
|
||||
audio_track: Option<Arc<dyn TrackLocal>>,
|
||||
|
||||
/// HID DataChannel
|
||||
hid_channel: Arc<RwLock<Option<Arc<RTCDataChannel>>>>,
|
||||
|
||||
/// HID 处理器
|
||||
hid_handler: Arc<HidDataChannelHandler>,
|
||||
|
||||
/// 状态
|
||||
state: Arc<RwLock<SessionState>>,
|
||||
|
||||
/// 编码器类型
|
||||
codec: VideoCodec,
|
||||
}
|
||||
|
||||
impl UniversalSession {
|
||||
/// 创建会话
|
||||
pub async fn new(
|
||||
id: String,
|
||||
config: &WebRtcConfig,
|
||||
video_pipeline: Arc<SharedVideoPipeline>,
|
||||
audio_pipeline: Arc<SharedAudioPipeline>,
|
||||
hid_handler: Arc<HidDataChannelHandler>,
|
||||
codec: VideoCodec,
|
||||
) -> Result<Self>;
|
||||
|
||||
/// 处理 Offer SDP
|
||||
pub async fn handle_offer(&self, offer_sdp: &str) -> Result<String>;
|
||||
|
||||
/// 添加 ICE 候选
|
||||
pub async fn add_ice_candidate(&self, candidate: &str) -> Result<()>;
|
||||
|
||||
/// 获取 ICE 候选
|
||||
pub fn get_ice_candidates(&self) -> Vec<String>;
|
||||
|
||||
/// 关闭会话
|
||||
pub async fn close(&self) -> Result<()>;
|
||||
|
||||
/// 获取状态
|
||||
pub fn state(&self) -> SessionState;
|
||||
|
||||
/// 获取统计
|
||||
pub fn stats(&self) -> SessionStats;
|
||||
}
|
||||
|
||||
pub enum SessionState {
|
||||
New,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
Failed,
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct SessionStats {
|
||||
pub bytes_sent: u64,
|
||||
pub packets_sent: u64,
|
||||
pub bitrate: u32,
|
||||
pub frame_rate: f32,
|
||||
pub round_trip_time: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 VideoTrack (video_track.rs)
|
||||
|
||||
视频轨道封装。
|
||||
|
||||
```rust
|
||||
pub struct UniversalVideoTrack {
|
||||
/// 轨道 ID
|
||||
id: String,
|
||||
|
||||
/// 编码类型
|
||||
codec: VideoCodec,
|
||||
|
||||
/// RTP 发送器
|
||||
rtp_sender: Arc<RtpSender>,
|
||||
|
||||
/// 帧计数
|
||||
frame_count: AtomicU64,
|
||||
|
||||
/// 统计
|
||||
stats: Arc<RwLock<TrackStats>>,
|
||||
}
|
||||
|
||||
impl UniversalVideoTrack {
|
||||
/// 创建轨道
|
||||
pub fn new(id: &str, codec: VideoCodec) -> Result<Self>;
|
||||
|
||||
/// 发送编码帧
|
||||
pub async fn send_frame(&self, frame: &EncodedFrame) -> Result<()>;
|
||||
|
||||
/// 获取 RTP 参数
|
||||
pub fn rtp_params(&self) -> RtpParameters;
|
||||
|
||||
/// 获取统计
|
||||
pub fn stats(&self) -> TrackStats;
|
||||
}
|
||||
|
||||
pub struct TrackStats {
|
||||
pub frames_sent: u64,
|
||||
pub bytes_sent: u64,
|
||||
pub packets_sent: u64,
|
||||
pub packet_loss: f32,
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 RTP 打包 (rtp.rs)
|
||||
|
||||
RTP 协议实现。
|
||||
|
||||
```rust
|
||||
pub struct RtpPacketizer {
|
||||
/// SSRC
|
||||
ssrc: u32,
|
||||
|
||||
/// 序列号
|
||||
sequence: u16,
|
||||
|
||||
/// 时间戳
|
||||
timestamp: u32,
|
||||
|
||||
/// 负载类型
|
||||
payload_type: u8,
|
||||
|
||||
/// 时钟频率
|
||||
clock_rate: u32,
|
||||
}
|
||||
|
||||
impl RtpPacketizer {
|
||||
/// 创建打包器
|
||||
pub fn new(codec: VideoCodec) -> Self;
|
||||
|
||||
/// 打包 H264 帧
|
||||
pub fn packetize_h264(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||
|
||||
/// 打包 VP8 帧
|
||||
pub fn packetize_vp8(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||
|
||||
/// 打包 VP9 帧
|
||||
pub fn packetize_vp9(&mut self, frame: &[u8], keyframe: bool) -> Vec<Vec<u8>>;
|
||||
|
||||
/// 打包 Opus 帧
|
||||
pub fn packetize_opus(&mut self, frame: &[u8]) -> Vec<u8>;
|
||||
}
|
||||
|
||||
/// H264 NAL 单元分片
|
||||
pub struct H264Fragmenter;
|
||||
|
||||
impl H264Fragmenter {
|
||||
/// 分片大于 MTU 的 NAL
|
||||
pub fn fragment(nal: &[u8], mtu: usize) -> Vec<Vec<u8>>;
|
||||
|
||||
/// 创建 STAP-A 聚合
|
||||
pub fn aggregate(nals: &[&[u8]]) -> Vec<u8>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 H265 打包器 (h265_payloader.rs)
|
||||
|
||||
H265/HEVC RTP 打包。
|
||||
|
||||
```rust
|
||||
pub struct H265Payloader {
|
||||
/// MTU 大小
|
||||
mtu: usize,
|
||||
}
|
||||
|
||||
impl H265Payloader {
|
||||
/// 创建打包器
|
||||
pub fn new(mtu: usize) -> Self;
|
||||
|
||||
/// 打包 H265 帧
|
||||
pub fn packetize(&self, frame: &[u8]) -> Vec<Vec<u8>>;
|
||||
|
||||
/// 分析 NAL 单元类型
|
||||
fn get_nal_type(nal: &[u8]) -> u8;
|
||||
|
||||
/// 是否需要分片
|
||||
fn needs_fragmentation(&self, nal: &[u8]) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 信令协议
|
||||
|
||||
### 4.1 创建会话
|
||||
|
||||
```
|
||||
POST /api/webrtc/session
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
Response:
|
||||
{
|
||||
"session_id": "abc123-def456"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 发送 Offer
|
||||
|
||||
```
|
||||
POST /api/webrtc/offer
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"session_id": "abc123-def456",
|
||||
"video_codec": "h264",
|
||||
"enable_audio": true,
|
||||
"offer_sdp": "v=0\r\no=- ..."
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"answer_sdp": "v=0\r\no=- ...",
|
||||
"ice_candidates": [
|
||||
"candidate:1 1 UDP ...",
|
||||
"candidate:2 1 TCP ..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 ICE 候选
|
||||
|
||||
```
|
||||
POST /api/webrtc/ice
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"session_id": "abc123-def456",
|
||||
"candidate": "candidate:1 1 UDP ..."
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 关闭会话
|
||||
|
||||
```
|
||||
POST /api/webrtc/close
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"session_id": "abc123-def456"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
pub struct WebRtcConfig {
|
||||
/// STUN 服务器
|
||||
pub stun_servers: Vec<String>,
|
||||
|
||||
/// TURN 服务器
|
||||
pub turn_servers: Vec<TurnServer>,
|
||||
|
||||
/// 默认编码器
|
||||
pub default_codec: VideoCodec,
|
||||
|
||||
/// 码率 (kbps)
|
||||
pub bitrate_kbps: u32,
|
||||
|
||||
/// GOP 大小
|
||||
pub gop_size: u32,
|
||||
|
||||
/// 启用音频
|
||||
pub enable_audio: bool,
|
||||
|
||||
/// 启用 DataChannel HID
|
||||
pub enable_datachannel_hid: bool,
|
||||
}
|
||||
|
||||
pub struct TurnServer {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Default for WebRtcConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stun_servers: vec!["stun:stun.l.google.com:19302".to_string()],
|
||||
turn_servers: vec![],
|
||||
default_codec: VideoCodec::H264,
|
||||
bitrate_kbps: 2000,
|
||||
gop_size: 60,
|
||||
enable_audio: true,
|
||||
enable_datachannel_hid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. DataChannel HID
|
||||
|
||||
### 6.1 消息格式
|
||||
|
||||
```javascript
|
||||
// 键盘事件
|
||||
{
|
||||
"type": "keyboard",
|
||||
"keys": ["KeyA", "KeyB"],
|
||||
"modifiers": {
|
||||
"ctrl": false,
|
||||
"shift": true,
|
||||
"alt": false,
|
||||
"meta": false
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标事件
|
||||
{
|
||||
"type": "mouse",
|
||||
"x": 16384,
|
||||
"y": 16384,
|
||||
"button": "left",
|
||||
"event": "press"
|
||||
}
|
||||
|
||||
// 鼠标模式
|
||||
{
|
||||
"type": "mouse_mode",
|
||||
"mode": "absolute"
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 处理流程
|
||||
|
||||
```
|
||||
DataChannel Message
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│Parse JSON Event │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│HidDataChannel │
|
||||
│ Handler │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ HidController │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
USB/Serial
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 支持的编码器
|
||||
|
||||
| 编码器 | RTP 负载类型 | 时钟频率 | 硬件加速 |
|
||||
|--------|-------------|---------|---------|
|
||||
| H264 | 96 (动态) | 90000 | VAAPI/RKMPP/V4L2 |
|
||||
| H265 | 97 (动态) | 90000 | VAAPI |
|
||||
| VP8 | 98 (动态) | 90000 | VAAPI |
|
||||
| VP9 | 99 (动态) | 90000 | VAAPI |
|
||||
| Opus | 111 (动态) | 48000 | 无 (软件) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WebRtcError {
|
||||
#[error("Session not found: {0}")]
|
||||
SessionNotFound(String),
|
||||
|
||||
#[error("Session already exists")]
|
||||
SessionExists,
|
||||
|
||||
#[error("Invalid SDP: {0}")]
|
||||
InvalidSdp(String),
|
||||
|
||||
#[error("Codec not supported: {0}")]
|
||||
CodecNotSupported(String),
|
||||
|
||||
#[error("ICE failed")]
|
||||
IceFailed,
|
||||
|
||||
#[error("DTLS failed")]
|
||||
DtlsFailed,
|
||||
|
||||
#[error("Track error: {0}")]
|
||||
TrackError(String),
|
||||
|
||||
#[error("Connection closed")]
|
||||
ConnectionClosed,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 使用示例
|
||||
|
||||
### 9.1 创建会话
|
||||
|
||||
```rust
|
||||
let streamer = WebRtcStreamer::new(
|
||||
video_pipeline,
|
||||
audio_pipeline,
|
||||
hid,
|
||||
WebRtcConfig::default(),
|
||||
events,
|
||||
).await?;
|
||||
|
||||
// 创建会话
|
||||
let session_id = streamer.create_session().await?;
|
||||
|
||||
// 处理 Offer
|
||||
let response = streamer.process_offer(
|
||||
&session_id,
|
||||
&offer_sdp,
|
||||
VideoCodec::H264,
|
||||
).await?;
|
||||
|
||||
println!("Answer: {}", response.answer_sdp);
|
||||
```
|
||||
|
||||
### 9.2 前端连接
|
||||
|
||||
```javascript
|
||||
// 创建 PeerConnection
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
});
|
||||
|
||||
// 创建 DataChannel
|
||||
const hidChannel = pc.createDataChannel('hid');
|
||||
|
||||
// 创建 Offer
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
// 发送到服务器
|
||||
const response = await fetch('/api/webrtc/offer', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
session_id,
|
||||
video_codec: 'h264',
|
||||
offer_sdp: offer.sdp
|
||||
})
|
||||
});
|
||||
|
||||
const { answer_sdp, ice_candidates } = await response.json();
|
||||
|
||||
// 设置 Answer
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answer_sdp });
|
||||
|
||||
// 添加 ICE 候选
|
||||
for (const candidate of ice_candidates) {
|
||||
await pc.addIceCandidate({ candidate });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 管道重启机制
|
||||
|
||||
当码率或编码器配置变更时,视频管道需要重启。WebRTC 模块实现了自动重连机制:
|
||||
|
||||
### 10.1 重启流程
|
||||
|
||||
```
|
||||
用户修改码率/编码器
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ set_bitrate_preset │
|
||||
│ 1. 保存 frame_tx │ ← 关键:在停止前保存
|
||||
│ 2. 停止旧管道 │
|
||||
│ 3. 等待清理 │
|
||||
│ 4. 恢复 frame_tx │
|
||||
│ 5. 创建新管道 │
|
||||
│ 6. 重连所有会话 │
|
||||
└─────────────────────┘
|
||||
│
|
||||
▼
|
||||
所有 WebRTC 会话自动恢复
|
||||
```
|
||||
|
||||
### 10.2 关键代码
|
||||
|
||||
```rust
|
||||
pub async fn set_bitrate_preset(self: &Arc<Self>, preset: BitratePreset) -> Result<()> {
|
||||
// 保存 frame_tx (监控任务会在管道停止后清除它)
|
||||
let saved_frame_tx = self.video_frame_tx.read().await.clone();
|
||||
|
||||
// 停止管道
|
||||
pipeline.stop();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// 恢复 frame_tx 并重建管道
|
||||
if let Some(tx) = saved_frame_tx {
|
||||
*self.video_frame_tx.write().await = Some(tx.clone());
|
||||
let pipeline = self.ensure_video_pipeline(tx).await?;
|
||||
|
||||
// 重连所有会话
|
||||
for session in sessions {
|
||||
session.start_from_video_pipeline(pipeline.subscribe(), ...).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 常见问题
|
||||
|
||||
### Q: 连接超时?
|
||||
|
||||
1. 检查 STUN/TURN 配置
|
||||
2. 检查防火墙设置
|
||||
3. 尝试使用 TURN 中继
|
||||
|
||||
### Q: 视频卡顿?
|
||||
|
||||
1. 降低分辨率/码率
|
||||
2. 检查网络带宽
|
||||
3. 使用硬件编码
|
||||
|
||||
### Q: 音频不同步?
|
||||
|
||||
1. 检查时间戳同步
|
||||
2. 调整缓冲区大小
|
||||
3. 使用 NTP 同步
|
||||
|
||||
### Q: 切换码率后视频静止?
|
||||
|
||||
1. 检查管道重启逻辑是否正确保存了 `video_frame_tx`
|
||||
2. 确认会话重连成功
|
||||
3. 查看日志中是否有 "Reconnecting session" 信息
|
||||
@@ -1,477 +0,0 @@
|
||||
# hwcodec 技术架构报告
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
hwcodec 是一个基于 FFmpeg 的硬件视频编解码库,来源于 RustDesk 项目并针对 One-KVM 进行了深度定制优化。该库专注于 IP-KVM 场景,提供 Windows 和 Linux 平台的 GPU 加速视频编码能力。
|
||||
|
||||
### 1.1 项目位置
|
||||
|
||||
```
|
||||
libs/hwcodec/
|
||||
├── src/ # Rust 源代码
|
||||
└── cpp/ # C++ 源代码
|
||||
```
|
||||
|
||||
### 1.2 核心特性
|
||||
|
||||
- **多编解码格式支持**: H.264, H.265 (HEVC), VP8, VP9, MJPEG
|
||||
- **硬件加速**: NVENC, AMF, Intel QSV (Windows), VAAPI, RKMPP, V4L2 M2M (Linux)
|
||||
- **跨平台**: Windows, Linux (x86_64, ARM64, ARMv7)
|
||||
- **低延迟优化**: 专为实时流媒体场景设计
|
||||
- **Rust/C++ 混合架构**: Rust 提供安全的上层 API,C++ 实现底层编解码逻辑
|
||||
- **IP-KVM 专用**: 解码仅支持 MJPEG(采集卡输出格式),编码支持多种硬件加速
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 整体架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Rust API Layer │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ ffmpeg_ram module ││
|
||||
│ │ (encode.rs + decode.rs) ││
|
||||
│ └──────────────────────────┬──────────────────────────────┘│
|
||||
├─────────────────────────────┼───────────────────────────────┤
|
||||
│ │ │
|
||||
│ FFI Bindings (bindgen) │
|
||||
│ ▼ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ C++ Core Layer │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ ffmpeg_ram (encode/decode) ││
|
||||
│ └──────────────────────────┬──────────────────────────────┘│
|
||||
├─────────────────────────────┼───────────────────────────────┤
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ FFmpeg Libraries │ │
|
||||
│ │ libavcodec │ libavutil │ libavformat │ libswscale │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
├─────────────────────────────┼───────────────────────────────┤
|
||||
│ Hardware Acceleration Backends │
|
||||
│ ┌────────┐ ┌─────┐ ┌─────┐ ┌───────┐ ┌───────┐ ┌───────┐ │
|
||||
│ │ NVENC │ │ AMF │ │ QSV │ │ VAAPI │ │ RKMPP │ │V4L2M2M│ │
|
||||
│ └────────┘ └─────┘ └─────┘ └───────┘ └───────┘ └───────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 模块职责
|
||||
|
||||
| 模块 | 职责 | 关键文件 |
|
||||
|------|------|----------|
|
||||
| `ffmpeg_ram` | 基于 RAM 的软件/硬件编解码 | `src/ffmpeg_ram/` |
|
||||
| `common` | 公共定义和 GPU 检测 | `src/common.rs` |
|
||||
| `ffmpeg` | FFmpeg 日志和初始化 | `src/ffmpeg.rs` |
|
||||
|
||||
## 3. 模块详细分析
|
||||
|
||||
### 3.1 库入口 (lib.rs)
|
||||
|
||||
```rust
|
||||
// libs/hwcodec/src/lib.rs
|
||||
pub mod common;
|
||||
pub mod ffmpeg;
|
||||
pub mod ffmpeg_ram;
|
||||
```
|
||||
|
||||
**功能**:
|
||||
- 导出所有子模块
|
||||
- 提供 C 日志回调函数
|
||||
|
||||
### 3.2 公共模块 (common.rs)
|
||||
|
||||
**核心类型**:
|
||||
|
||||
```rust
|
||||
pub enum Driver {
|
||||
NV, // NVIDIA
|
||||
AMF, // AMD
|
||||
MFX, // Intel
|
||||
FFMPEG, // 软件编码
|
||||
}
|
||||
```
|
||||
|
||||
**GPU 检测函数**:
|
||||
|
||||
| 平台 | 检测函数 | 检测方式 |
|
||||
|------|----------|----------|
|
||||
| Linux | `linux_support_nv()` | 加载 libcuda.so + libnvidia-encode.so |
|
||||
| Linux | `linux_support_amd()` | 检查 `libamfrt64.so.1` |
|
||||
| Linux | `linux_support_intel()` | 检查 `libvpl.so`/`libmfx.so` |
|
||||
| Linux | `linux_support_rkmpp()` | 检查 `/dev/mpp_service` |
|
||||
| Linux | `linux_support_v4l2m2m()` | 检查 `/dev/video*` 设备 |
|
||||
|
||||
### 3.3 FFmpeg RAM 编码模块
|
||||
|
||||
#### 3.3.1 Rust 层 (src/ffmpeg_ram/)
|
||||
|
||||
**CodecInfo 结构体**:
|
||||
|
||||
```rust
|
||||
pub struct CodecInfo {
|
||||
pub name: String, // 编码器名称如 "h264_nvenc"
|
||||
pub mc_name: Option<String>, // MediaCodec 名称 (Android)
|
||||
pub format: DataFormat, // H264/H265/VP8/VP9/MJPEG
|
||||
pub priority: i32, // 优先级 (Best=0, Good=1, Normal=2, Soft=3, Bad=4)
|
||||
pub hwdevice: AVHWDeviceType, // 硬件设备类型
|
||||
}
|
||||
```
|
||||
|
||||
**EncodeContext 结构体**:
|
||||
|
||||
```rust
|
||||
pub struct EncodeContext {
|
||||
pub name: String, // 编码器名称
|
||||
pub width: i32, // 视频宽度
|
||||
pub height: i32, // 视频高度
|
||||
pub pixfmt: AVPixelFormat, // 像素格式 (NV12/YUV420P)
|
||||
pub align: i32, // 内存对齐
|
||||
pub fps: i32, // 帧率
|
||||
pub gop: i32, // GOP 大小
|
||||
pub rc: RateControl, // 码率控制模式
|
||||
pub quality: Quality, // 质量级别
|
||||
pub kbs: i32, // 目标码率 (kbps)
|
||||
pub q: i32, // 量化参数
|
||||
pub thread_count: i32, // 线程数
|
||||
}
|
||||
```
|
||||
|
||||
**Encoder 类**:
|
||||
|
||||
```rust
|
||||
pub struct Encoder {
|
||||
codec: *mut c_void, // C++ 编码器指针
|
||||
frames: *mut Vec<EncodeFrame>, // 编码输出帧
|
||||
pub ctx: EncodeContext,
|
||||
pub linesize: Vec<i32>, // 行大小
|
||||
pub offset: Vec<i32>, // 平面偏移
|
||||
pub length: i32, // 总数据长度
|
||||
}
|
||||
```
|
||||
|
||||
**核心方法**:
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `Encoder::new()` | 创建编码器实例 |
|
||||
| `Encoder::encode()` | 编码一帧 YUV 数据 |
|
||||
| `Encoder::set_bitrate()` | 动态调整码率 |
|
||||
| `Encoder::request_keyframe()` | 请求下一帧为关键帧 |
|
||||
| `Encoder::available_encoders()` | 检测系统可用编码器 |
|
||||
|
||||
#### 3.3.2 C++ 层 (cpp/ffmpeg_ram/)
|
||||
|
||||
**FFmpegRamEncoder 类** (ffmpeg_ram_encode.cpp):
|
||||
|
||||
```cpp
|
||||
class FFmpegRamEncoder {
|
||||
AVCodecContext *c_ = NULL; // FFmpeg 编码上下文
|
||||
AVFrame *frame_ = NULL; // 输入帧
|
||||
AVPacket *pkt_ = NULL; // 编码输出包
|
||||
AVBufferRef *hw_device_ctx_; // 硬件设备上下文
|
||||
AVFrame *hw_frame_ = NULL; // 硬件帧
|
||||
bool force_keyframe_ = false; // 强制关键帧标志
|
||||
|
||||
// 主要方法
|
||||
bool init(int *linesize, int *offset, int *length);
|
||||
int encode(const uint8_t *data, int length, const void *obj, uint64_t ms);
|
||||
int do_encode(AVFrame *frame, const void *obj, int64_t ms);
|
||||
int set_hwframe_ctx(); // 设置硬件帧上下文
|
||||
};
|
||||
```
|
||||
|
||||
**编码流程**:
|
||||
|
||||
```
|
||||
输入 YUV 数据
|
||||
│
|
||||
▼
|
||||
fill_frame() - 填充 AVFrame 数据指针
|
||||
│
|
||||
├──▶ (软件编码) 直接使用 frame_
|
||||
│
|
||||
└──▶ (硬件编码) av_hwframe_transfer_data() 传输到 GPU
|
||||
│
|
||||
▼
|
||||
使用 hw_frame_
|
||||
│
|
||||
▼
|
||||
avcodec_send_frame() - 发送帧到编码器
|
||||
│
|
||||
▼
|
||||
avcodec_receive_packet() - 获取编码数据
|
||||
│
|
||||
▼
|
||||
callback() - 回调输出
|
||||
```
|
||||
|
||||
### 3.4 FFmpeg RAM 解码模块
|
||||
|
||||
**IP-KVM 专用设计**: 解码器仅支持 MJPEG 软件解码,因为 IP-KVM 场景中视频采集卡输出的是 MJPEG 格式。
|
||||
|
||||
**Decoder 类**:
|
||||
|
||||
```rust
|
||||
pub struct Decoder {
|
||||
codec: *mut c_void,
|
||||
frames: *mut Vec<DecodeFrame>,
|
||||
pub ctx: DecodeContext,
|
||||
}
|
||||
|
||||
pub struct DecodeFrame {
|
||||
pub pixfmt: AVPixelFormat,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub data: Vec<Vec<u8>>, // Y, U, V 平面数据
|
||||
pub linesize: Vec<i32>,
|
||||
pub key: bool,
|
||||
}
|
||||
```
|
||||
|
||||
**available_decoders()**: 仅返回 MJPEG 软件解码器
|
||||
|
||||
```rust
|
||||
pub fn available_decoders() -> Vec<CodecInfo> {
|
||||
vec![CodecInfo {
|
||||
name: "mjpeg".to_owned(),
|
||||
format: MJPEG,
|
||||
hwdevice: AV_HWDEVICE_TYPE_NONE,
|
||||
priority: Priority::Best as _,
|
||||
..Default::default()
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**C++ 实现** (ffmpeg_ram_decode.cpp):
|
||||
|
||||
```cpp
|
||||
class FFmpegRamDecoder {
|
||||
AVCodecContext *c_ = NULL;
|
||||
AVFrame *frame_ = NULL; // 解码输出帧
|
||||
AVPacket *pkt_ = NULL;
|
||||
|
||||
int do_decode(const void *obj);
|
||||
};
|
||||
```
|
||||
|
||||
**解码流程**:
|
||||
|
||||
```
|
||||
输入 MJPEG 数据
|
||||
│
|
||||
▼
|
||||
avcodec_send_packet() - 发送数据到解码器
|
||||
│
|
||||
▼
|
||||
avcodec_receive_frame() - 获取解码帧 (YUV420P)
|
||||
│
|
||||
▼
|
||||
callback() - 回调输出
|
||||
```
|
||||
|
||||
## 4. 硬件加速支持
|
||||
|
||||
### 4.1 支持的硬件加速后端
|
||||
|
||||
| 后端 | 厂商 | 平台 | 编码器名称 |
|
||||
|------|------|------|-----------|
|
||||
| NVENC | NVIDIA | Windows/Linux | h264_nvenc, hevc_nvenc |
|
||||
| AMF | AMD | Windows/Linux | h264_amf, hevc_amf |
|
||||
| QSV | Intel | Windows | h264_qsv, hevc_qsv |
|
||||
| VAAPI | 通用 | Linux | h264_vaapi, hevc_vaapi, vp8_vaapi, vp9_vaapi |
|
||||
| RKMPP | Rockchip | Linux | h264_rkmpp, hevc_rkmpp |
|
||||
| V4L2 M2M | ARM SoC | Linux | h264_v4l2m2m, hevc_v4l2m2m |
|
||||
|
||||
### 4.2 硬件检测逻辑 (Linux)
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
// NVIDIA 检测 - 简化的动态库检测
|
||||
int linux_support_nv() {
|
||||
void *handle = dlopen("libcuda.so.1", RTLD_LAZY);
|
||||
if (!handle) handle = dlopen("libcuda.so", RTLD_LAZY);
|
||||
if (!handle) return -1;
|
||||
dlclose(handle);
|
||||
|
||||
handle = dlopen("libnvidia-encode.so.1", RTLD_LAZY);
|
||||
if (!handle) handle = dlopen("libnvidia-encode.so", RTLD_LAZY);
|
||||
if (!handle) return -1;
|
||||
dlclose(handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// AMD 检测 - 检查 AMF 运行时库
|
||||
int linux_support_amd() {
|
||||
void *handle = dlopen("libamfrt64.so.1", RTLD_LAZY);
|
||||
if (!handle) return -1;
|
||||
dlclose(handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Intel 检测 - 检查 VPL/MFX 库
|
||||
int linux_support_intel() {
|
||||
const char *libs[] = {"libvpl.so", "libmfx.so", ...};
|
||||
// 任一成功加载则返回 0
|
||||
}
|
||||
|
||||
// Rockchip MPP 检测 - 检查设备节点
|
||||
int linux_support_rkmpp() {
|
||||
if (access("/dev/mpp_service", F_OK) == 0) return 0;
|
||||
if (access("/dev/rga", F_OK) == 0) return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// V4L2 M2M 检测 - 检查视频设备
|
||||
int linux_support_v4l2m2m() {
|
||||
const char *devices[] = {"/dev/video10", "/dev/video11", ...};
|
||||
// 任一设备可打开则返回 0
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 编码器优先级系统
|
||||
|
||||
```rust
|
||||
pub enum Priority {
|
||||
Best = 0, // 最高优先级 (硬件加速)
|
||||
Good = 1, // 良好 (VAAPI, 部分硬件)
|
||||
Normal = 2, // 普通
|
||||
Soft = 3, // 软件编码
|
||||
Bad = 4, // 最低优先级
|
||||
}
|
||||
```
|
||||
|
||||
**优先级分配**:
|
||||
|
||||
| 编码器 | 优先级 |
|
||||
|--------|--------|
|
||||
| h264_nvenc, hevc_nvenc | Best (0) |
|
||||
| h264_amf, hevc_amf | Best (0) |
|
||||
| h264_qsv, hevc_qsv | Best (0) |
|
||||
| h264_rkmpp, hevc_rkmpp | Best (0) |
|
||||
| h264_vaapi, hevc_vaapi | Good (1) |
|
||||
| h264_v4l2m2m, hevc_v4l2m2m | Good (1) |
|
||||
| h264 (x264), hevc (x265) | Soft (3) |
|
||||
|
||||
### 4.4 低延迟优化配置
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/util.cpp
|
||||
|
||||
bool set_lantency_free(void *priv_data, const std::string &name) {
|
||||
// NVENC: 禁用延迟缓冲
|
||||
if (name.find("nvenc") != std::string::npos) {
|
||||
av_opt_set(priv_data, "delay", "0", 0);
|
||||
}
|
||||
// AMF: 设置查询超时
|
||||
if (name.find("amf") != std::string::npos) {
|
||||
av_opt_set(priv_data, "query_timeout", "1000", 0);
|
||||
}
|
||||
// QSV/VAAPI: 设置异步深度为 1
|
||||
if (name.find("qsv") != std::string::npos ||
|
||||
name.find("vaapi") != std::string::npos) {
|
||||
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||
}
|
||||
// libvpx: 实时模式
|
||||
if (name.find("libvpx") != std::string::npos) {
|
||||
av_opt_set(priv_data, "deadline", "realtime", 0);
|
||||
av_opt_set_int(priv_data, "cpu-used", 6, 0);
|
||||
av_opt_set_int(priv_data, "lag-in-frames", 0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 构建系统
|
||||
|
||||
### 5.1 Cargo.toml 配置
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "hwcodec"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
description = "Hardware video codec for IP-KVM (Windows/Linux)"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
serde_derive = "1.0"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0" # C++ 编译
|
||||
bindgen = "0.59" # FFI 绑定生成
|
||||
```
|
||||
|
||||
### 5.2 构建流程 (build.rs)
|
||||
|
||||
```
|
||||
build.rs
|
||||
│
|
||||
├── build_common()
|
||||
│ ├── 生成 common_ffi.rs (bindgen)
|
||||
│ ├── 编译平台相关 C++ 代码
|
||||
│ └── 链接系统库 (stdc++)
|
||||
│
|
||||
└── ffmpeg::build_ffmpeg()
|
||||
├── 生成 ffmpeg_ffi.rs
|
||||
├── 链接 FFmpeg 库 (VCPKG 或 pkg-config)
|
||||
└── build_ffmpeg_ram()
|
||||
└── 编译 ffmpeg_ram_encode.cpp, ffmpeg_ram_decode.cpp
|
||||
```
|
||||
|
||||
### 5.3 FFmpeg 链接方式
|
||||
|
||||
| 方式 | 平台 | 条件 |
|
||||
|------|------|------|
|
||||
| VCPKG 静态链接 | 跨平台 | 设置 `VCPKG_ROOT` 环境变量 |
|
||||
| pkg-config 动态链接 | Linux | 默认方式 |
|
||||
|
||||
## 6. 与原版 hwcodec 的区别
|
||||
|
||||
针对 One-KVM IP-KVM 场景,对原版 RustDesk hwcodec 进行了以下简化:
|
||||
|
||||
### 6.1 移除的功能
|
||||
|
||||
| 移除项 | 原因 |
|
||||
|--------|------|
|
||||
| VRAM 模块 | IP-KVM 不需要 GPU 显存直接编解码 |
|
||||
| Mux 模块 | IP-KVM 不需要录制到文件 |
|
||||
| macOS 支持 | IP-KVM 目标平台不包含 macOS |
|
||||
| Android 支持 | IP-KVM 目标平台不包含 Android |
|
||||
| 外部 SDK | 简化构建,减少依赖 |
|
||||
| 多格式解码 | IP-KVM 仅需 MJPEG 解码 |
|
||||
|
||||
### 6.2 保留的功能
|
||||
|
||||
| 保留项 | 用途 |
|
||||
|--------|------|
|
||||
| FFmpeg RAM 编码 | WebRTC 视频编码 |
|
||||
| FFmpeg RAM 解码 | MJPEG 采集卡解码 |
|
||||
| 硬件加速编码 | 低延迟高效编码 |
|
||||
| 软件编码后备 | 无硬件加速时的兜底方案 |
|
||||
|
||||
### 6.3 代码量对比
|
||||
|
||||
| 指标 | 原版 | 简化版 | 减少 |
|
||||
|------|------|--------|------|
|
||||
| 外部 SDK | ~9MB | 0 | 100% |
|
||||
| C++ 文件 | ~30 | ~10 | ~67% |
|
||||
| Rust 模块 | 6 | 3 | 50% |
|
||||
|
||||
## 7. 总结
|
||||
|
||||
hwcodec 库通过 Rust/C++ 混合架构,在保证内存安全的同时实现了高性能的视频编解码。针对 One-KVM IP-KVM 场景的优化设计特点包括:
|
||||
|
||||
1. **精简的编解码器 API**: 解码仅支持 MJPEG,编码支持多种硬件加速
|
||||
2. **自动硬件检测**: 运行时自动检测并选择最优的硬件加速后端
|
||||
3. **优先级系统**: 基于质量和性能为不同编码器分配优先级
|
||||
4. **低延迟优化**: 针对实时流媒体场景进行了专门优化
|
||||
5. **简化的构建系统**: 无需外部 SDK,仅依赖系统 FFmpeg
|
||||
6. **Windows/Linux 跨平台**: 支持 x86_64、ARM64、ARMv7 架构
|
||||
@@ -1,481 +0,0 @@
|
||||
# hwcodec 编解码器 API 详解
|
||||
|
||||
## 1. 编码器 API
|
||||
|
||||
### 1.1 编码器初始化
|
||||
|
||||
#### EncodeContext 参数
|
||||
|
||||
```rust
|
||||
pub struct EncodeContext {
|
||||
pub name: String, // 编码器名称
|
||||
pub mc_name: Option<String>, // MediaCodec 名称 (保留字段)
|
||||
pub width: i32, // 视频宽度 (必须为偶数)
|
||||
pub height: i32, // 视频高度 (必须为偶数)
|
||||
pub pixfmt: AVPixelFormat, // 像素格式
|
||||
pub align: i32, // 内存对齐 (通常为 0 或 32)
|
||||
pub fps: i32, // 帧率
|
||||
pub gop: i32, // GOP 大小 (关键帧间隔)
|
||||
pub rc: RateControl, // 码率控制模式
|
||||
pub quality: Quality, // 编码质量
|
||||
pub kbs: i32, // 目标码率 (kbps)
|
||||
pub q: i32, // 量化参数 (CQ 模式)
|
||||
pub thread_count: i32, // 编码线程数
|
||||
}
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
| 参数 | 类型 | 说明 | 推荐值 |
|
||||
|------|------|------|--------|
|
||||
| `name` | String | FFmpeg 编码器名称 | 见下表 |
|
||||
| `width` | i32 | 视频宽度 | 1920 |
|
||||
| `height` | i32 | 视频高度 | 1080 |
|
||||
| `pixfmt` | AVPixelFormat | 像素格式 | NV12 / YUV420P |
|
||||
| `align` | i32 | 内存对齐 | 0 (自动) |
|
||||
| `fps` | i32 | 帧率 | 30 |
|
||||
| `gop` | i32 | GOP 大小 | 30 (1秒) |
|
||||
| `rc` | RateControl | 码率控制 | CBR / VBR |
|
||||
| `quality` | Quality | 质量级别 | Medium |
|
||||
| `kbs` | i32 | 码率 (kbps) | 2000-8000 |
|
||||
| `thread_count` | i32 | 线程数 | 4 |
|
||||
|
||||
#### 编码器名称对照表
|
||||
|
||||
| 名称 | 格式 | 加速 | 平台 |
|
||||
|------|------|------|------|
|
||||
| `h264_nvenc` | H.264 | NVIDIA GPU | Windows/Linux |
|
||||
| `hevc_nvenc` | H.265 | NVIDIA GPU | Windows/Linux |
|
||||
| `h264_amf` | H.264 | AMD GPU | Windows/Linux |
|
||||
| `hevc_amf` | H.265 | AMD GPU | Windows/Linux |
|
||||
| `h264_qsv` | H.264 | Intel QSV | Windows |
|
||||
| `hevc_qsv` | H.265 | Intel QSV | Windows |
|
||||
| `h264_vaapi` | H.264 | VAAPI | Linux |
|
||||
| `hevc_vaapi` | H.265 | VAAPI | Linux |
|
||||
| `vp8_vaapi` | VP8 | VAAPI | Linux |
|
||||
| `vp9_vaapi` | VP9 | VAAPI | Linux |
|
||||
| `h264_rkmpp` | H.264 | Rockchip MPP | Linux |
|
||||
| `hevc_rkmpp` | H.265 | Rockchip MPP | Linux |
|
||||
| `h264_v4l2m2m` | H.264 | V4L2 M2M | Linux |
|
||||
| `hevc_v4l2m2m` | H.265 | V4L2 M2M | Linux |
|
||||
| `h264` | H.264 | 软件 (x264) | 全平台 |
|
||||
| `hevc` | H.265 | 软件 (x265) | 全平台 |
|
||||
| `libvpx` | VP8 | 软件 | 全平台 |
|
||||
| `libvpx-vp9` | VP9 | 软件 | 全平台 |
|
||||
| `mjpeg` | MJPEG | 软件 | 全平台 |
|
||||
|
||||
### 1.2 创建编码器
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||
use hwcodec::ffmpeg::{AVPixelFormat};
|
||||
use hwcodec::common::{RateControl, Quality};
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: "h264_vaapi".to_string(),
|
||||
mc_name: None,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
align: 0,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_CBR,
|
||||
quality: Quality::Quality_Medium,
|
||||
kbs: 4000,
|
||||
q: 0,
|
||||
thread_count: 4,
|
||||
};
|
||||
|
||||
let encoder = Encoder::new(ctx)?;
|
||||
println!("Linesize: {:?}", encoder.linesize);
|
||||
println!("Offset: {:?}", encoder.offset);
|
||||
println!("Buffer length: {}", encoder.length);
|
||||
```
|
||||
|
||||
### 1.3 编码帧
|
||||
|
||||
```rust
|
||||
// 准备 YUV 数据
|
||||
let yuv_data: Vec<u8> = prepare_yuv_frame();
|
||||
|
||||
// 编码
|
||||
let pts_ms: i64 = 0; // 时间戳 (毫秒)
|
||||
match encoder.encode(&yuv_data, pts_ms) {
|
||||
Ok(frames) => {
|
||||
for frame in frames.iter() {
|
||||
println!("Encoded: {} bytes, pts={}, key={}",
|
||||
frame.data.len(), frame.pts, frame.key);
|
||||
// 发送 frame.data
|
||||
}
|
||||
}
|
||||
Err(code) => {
|
||||
eprintln!("Encode error: {}", code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 动态调整码率
|
||||
|
||||
```rust
|
||||
// 动态调整到 6000 kbps
|
||||
encoder.set_bitrate(6000)?;
|
||||
```
|
||||
|
||||
### 1.5 请求关键帧
|
||||
|
||||
```rust
|
||||
// 下一帧强制编码为 IDR 帧
|
||||
encoder.request_keyframe();
|
||||
```
|
||||
|
||||
### 1.6 检测可用编码器
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||
use hwcodec::ffmpeg_ram::CodecInfo;
|
||||
|
||||
let ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
mc_name: None,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
align: 0,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
rc: RateControl::RC_DEFAULT,
|
||||
quality: Quality::Quality_Default,
|
||||
kbs: 4000,
|
||||
q: 0,
|
||||
thread_count: 4,
|
||||
};
|
||||
|
||||
let available_encoders = Encoder::available_encoders(ctx, None);
|
||||
for encoder in available_encoders {
|
||||
println!("Available: {} (format: {:?}, priority: {})",
|
||||
encoder.name, encoder.format, encoder.priority);
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 解码器 API
|
||||
|
||||
### 2.1 IP-KVM 专用设计
|
||||
|
||||
在 One-KVM IP-KVM 场景中,解码器仅支持 MJPEG 软件解码。这是因为视频采集卡输出的格式是 MJPEG,不需要其他格式的硬件解码支持。
|
||||
|
||||
### 2.2 解码器初始化
|
||||
|
||||
#### DecodeContext 参数
|
||||
|
||||
```rust
|
||||
pub struct DecodeContext {
|
||||
pub name: String, // 解码器名称 ("mjpeg")
|
||||
pub device_type: AVHWDeviceType, // 硬件设备类型 (NONE)
|
||||
pub thread_count: i32, // 解码线程数
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 创建解码器
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::decode::{Decoder, DecodeContext};
|
||||
use hwcodec::ffmpeg::AVHWDeviceType;
|
||||
|
||||
let ctx = DecodeContext {
|
||||
name: "mjpeg".to_string(),
|
||||
device_type: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||
thread_count: 4,
|
||||
};
|
||||
|
||||
let decoder = Decoder::new(ctx)?;
|
||||
```
|
||||
|
||||
### 2.4 解码帧
|
||||
|
||||
```rust
|
||||
// 输入 MJPEG 编码数据
|
||||
let mjpeg_data: Vec<u8> = receive_mjpeg_frame();
|
||||
|
||||
match decoder.decode(&mjpeg_data) {
|
||||
Ok(frames) => {
|
||||
for frame in frames.iter() {
|
||||
println!("Decoded: {}x{}, format={:?}, key={}",
|
||||
frame.width, frame.height, frame.pixfmt, frame.key);
|
||||
|
||||
// 访问 YUV 数据
|
||||
let y_plane = &frame.data[0];
|
||||
let u_plane = &frame.data[1];
|
||||
let v_plane = &frame.data[2];
|
||||
}
|
||||
}
|
||||
Err(code) => {
|
||||
eprintln!("Decode error: {}", code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 DecodeFrame 结构体
|
||||
|
||||
```rust
|
||||
pub struct DecodeFrame {
|
||||
pub pixfmt: AVPixelFormat, // 输出像素格式
|
||||
pub width: i32, // 帧宽度
|
||||
pub height: i32, // 帧高度
|
||||
pub data: Vec<Vec<u8>>, // 平面数据 [Y, U, V] 或 [Y, UV]
|
||||
pub linesize: Vec<i32>, // 每个平面的行字节数
|
||||
pub key: bool, // 是否为关键帧
|
||||
}
|
||||
```
|
||||
|
||||
#### 像素格式与平面布局
|
||||
|
||||
| 像素格式 | 平面数 | data[0] | data[1] | data[2] |
|
||||
|----------|--------|---------|---------|---------|
|
||||
| `YUV420P` | 3 | Y | U | V |
|
||||
| `YUVJ420P` | 3 | Y | U | V |
|
||||
| `YUV422P` | 3 | Y | U | V |
|
||||
| `NV12` | 2 | Y | UV (交错) | - |
|
||||
| `NV21` | 2 | Y | VU (交错) | - |
|
||||
|
||||
### 2.6 获取可用解码器
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::decode::Decoder;
|
||||
|
||||
let available_decoders = Decoder::available_decoders();
|
||||
for decoder in available_decoders {
|
||||
println!("Available: {} (format: {:?}, hwdevice: {:?})",
|
||||
decoder.name, decoder.format, decoder.hwdevice);
|
||||
}
|
||||
|
||||
// 输出:
|
||||
// Available: mjpeg (format: MJPEG, hwdevice: AV_HWDEVICE_TYPE_NONE)
|
||||
```
|
||||
|
||||
## 3. 码率控制模式
|
||||
|
||||
### 3.1 RateControl 枚举
|
||||
|
||||
```rust
|
||||
pub enum RateControl {
|
||||
RC_DEFAULT, // 使用编码器默认
|
||||
RC_CBR, // 恒定码率
|
||||
RC_VBR, // 可变码率
|
||||
RC_CQ, // 恒定质量 (需设置 q 参数)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 模式说明
|
||||
|
||||
| 模式 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| `RC_CBR` | 码率恒定,质量随场景变化 | 网络带宽受限 |
|
||||
| `RC_VBR` | 质量优先,码率波动 | 本地存储 |
|
||||
| `RC_CQ` | 恒定质量,码率波动大 | 质量敏感场景 |
|
||||
|
||||
### 3.3 各编码器支持情况
|
||||
|
||||
| 编码器 | CBR | VBR | CQ |
|
||||
|--------|-----|-----|-----|
|
||||
| nvenc | ✓ | ✓ | ✓ |
|
||||
| amf | ✓ | ✓ (低延迟) | ✗ |
|
||||
| qsv | ✓ | ✓ | ✗ |
|
||||
| vaapi | ✓ | ✓ | ✗ |
|
||||
|
||||
## 4. 质量等级
|
||||
|
||||
### 4.1 Quality 枚举
|
||||
|
||||
```rust
|
||||
pub enum Quality {
|
||||
Quality_Default, // 使用编码器默认
|
||||
Quality_High, // 高质量 (慢速)
|
||||
Quality_Medium, // 中等质量 (平衡)
|
||||
Quality_Low, // 低质量 (快速)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 编码器预设映射
|
||||
|
||||
| 质量 | nvenc | amf | qsv |
|
||||
|------|-------|-----|-----|
|
||||
| High | - | quality | veryslow |
|
||||
| Medium | p4 | balanced | medium |
|
||||
| Low | p1 | speed | veryfast |
|
||||
|
||||
## 5. 错误处理
|
||||
|
||||
### 5.1 错误码
|
||||
|
||||
| 错误码 | 常量 | 说明 |
|
||||
|--------|------|------|
|
||||
| 0 | `HWCODEC_SUCCESS` | 成功 |
|
||||
| -1 | `HWCODEC_ERR_COMMON` | 通用错误 |
|
||||
| -2 | `HWCODEC_ERR_HEVC_COULD_NOT_FIND_POC` | HEVC 解码参考帧丢失 |
|
||||
|
||||
### 5.2 常见错误处理
|
||||
|
||||
```rust
|
||||
match encoder.encode(&yuv_data, pts) {
|
||||
Ok(frames) => {
|
||||
// 处理编码帧
|
||||
}
|
||||
Err(-1) => {
|
||||
eprintln!("编码失败,可能是输入数据格式错误");
|
||||
}
|
||||
Err(code) => {
|
||||
eprintln!("未知错误: {}", code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 最佳实践
|
||||
|
||||
### 6.1 编码器选择策略
|
||||
|
||||
```rust
|
||||
fn select_best_encoder(
|
||||
width: i32,
|
||||
height: i32,
|
||||
format: DataFormat
|
||||
) -> Option<String> {
|
||||
let ctx = EncodeContext {
|
||||
width,
|
||||
height,
|
||||
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
// ... 其他参数
|
||||
};
|
||||
|
||||
let encoders = Encoder::available_encoders(ctx, None);
|
||||
|
||||
// 按优先级排序,选择最佳
|
||||
encoders.into_iter()
|
||||
.filter(|e| e.format == format)
|
||||
.min_by_key(|e| e.priority)
|
||||
.map(|e| e.name)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 帧内存布局
|
||||
|
||||
```rust
|
||||
// 获取 NV12 帧布局信息
|
||||
let (linesize, offset, length) = ffmpeg_linesize_offset_length(
|
||||
AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
1920,
|
||||
1080,
|
||||
0, // align
|
||||
)?;
|
||||
|
||||
// 分配缓冲区
|
||||
let mut buffer = vec![0u8; length as usize];
|
||||
|
||||
// 填充 Y 平面: buffer[0..offset[0]]
|
||||
// 填充 UV 平面: buffer[offset[0]..length]
|
||||
```
|
||||
|
||||
### 6.3 关键帧控制
|
||||
|
||||
```rust
|
||||
let mut frame_count = 0;
|
||||
|
||||
loop {
|
||||
// 每 30 帧强制一个关键帧
|
||||
if frame_count % 30 == 0 {
|
||||
encoder.request_keyframe();
|
||||
}
|
||||
|
||||
encoder.encode(&yuv_data, pts)?;
|
||||
frame_count += 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 线程安全
|
||||
|
||||
```rust
|
||||
// Decoder 实现了 Send + Sync
|
||||
unsafe impl Send for Decoder {}
|
||||
unsafe impl Sync for Decoder {}
|
||||
|
||||
// 可以安全地在多线程间传递
|
||||
let decoder = Arc::new(Mutex::new(Decoder::new(ctx)?));
|
||||
```
|
||||
|
||||
## 7. IP-KVM 典型使用场景
|
||||
|
||||
### 7.1 视频采集和转码流程
|
||||
|
||||
```
|
||||
USB 采集卡 (MJPEG)
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ MJPEG Decoder │ ◄── Decoder::new("mjpeg")
|
||||
│ (软件解码) │
|
||||
└────────┬────────┘
|
||||
│ YUV420P
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ H264 Encoder │ ◄── Encoder::new("h264_vaapi")
|
||||
│ (硬件加速) │
|
||||
└────────┬────────┘
|
||||
│ H264 NAL
|
||||
▼
|
||||
WebRTC 传输
|
||||
```
|
||||
|
||||
### 7.2 完整示例
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::decode::{Decoder, DecodeContext};
|
||||
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||
use hwcodec::ffmpeg::AVHWDeviceType;
|
||||
|
||||
// 创建 MJPEG 解码器
|
||||
let decode_ctx = DecodeContext {
|
||||
name: "mjpeg".to_string(),
|
||||
device_type: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||
thread_count: 4,
|
||||
};
|
||||
let mut decoder = Decoder::new(decode_ctx)?;
|
||||
|
||||
// 检测并选择最佳编码器
|
||||
let encode_ctx = EncodeContext {
|
||||
name: String::new(),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
// ...
|
||||
};
|
||||
let available = Encoder::available_encoders(encode_ctx.clone(), None);
|
||||
let best_h264 = available.iter()
|
||||
.filter(|e| e.format == DataFormat::H264)
|
||||
.min_by_key(|e| e.priority)
|
||||
.expect("No H264 encoder available");
|
||||
|
||||
// 使用最佳编码器创建实例
|
||||
let encode_ctx = EncodeContext {
|
||||
name: best_h264.name.clone(),
|
||||
..encode_ctx
|
||||
};
|
||||
let mut encoder = Encoder::new(encode_ctx)?;
|
||||
|
||||
// 处理循环
|
||||
loop {
|
||||
let mjpeg_frame = capture_frame();
|
||||
|
||||
// 解码 MJPEG -> YUV
|
||||
let decoded = decoder.decode(&mjpeg_frame)?;
|
||||
|
||||
// 编码 YUV -> H264
|
||||
for frame in decoded {
|
||||
let yuv_data = frame.data.concat();
|
||||
let encoded = encoder.encode(&yuv_data, pts)?;
|
||||
|
||||
// 发送编码数据
|
||||
for packet in encoded {
|
||||
send_to_webrtc(packet.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,561 +0,0 @@
|
||||
# hwcodec 硬件加速详解
|
||||
|
||||
## 1. 硬件加速架构
|
||||
|
||||
### 1.1 整体流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 应用层 (Rust) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐│
|
||||
│ │ Encoder::available_encoders() → 自动检测可用硬件编码器 ││
|
||||
│ └─────────────────────────────────────────────────────────┘│
|
||||
└────────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 硬件检测层 (C++) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
|
||||
│ │linux_ │ │linux_ │ │linux_ │ │linux_support_ ││
|
||||
│ │support_nv│ │support_ │ │support_ │ │rkmpp/v4l2m2m ││
|
||||
│ └────┬─────┘ │amd │ │intel │ └─────────┬────────┘│
|
||||
│ │ └────┬─────┘ └────┬─────┘ │ │
|
||||
└───────┼────────────┼────────────┼─────────────────┼─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐
|
||||
│ CUDA/ │ │ AMF │ │ VPL/MFX │ │ 设备节点检测 │
|
||||
│ NVENC │ │ Runtime │ │ Library │ │ /dev/mpp_service │
|
||||
│ 动态库 │ │ 动态库 │ │ 动态库 │ │ /dev/video* │
|
||||
└───────────┘ └───────────┘ └───────────┘ └───────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 编码器测试验证
|
||||
|
||||
每个检测到的硬件编码器都会进行实际编码测试:
|
||||
|
||||
```rust
|
||||
// libs/hwcodec/src/ffmpeg_ram/encode.rs
|
||||
|
||||
// 生成测试用 YUV 数据
|
||||
let yuv = Encoder::dummy_yuv(ctx.clone())?;
|
||||
|
||||
// 尝试创建编码器并编码测试帧
|
||||
match Encoder::new(c) {
|
||||
Ok(mut encoder) => {
|
||||
let start = std::time::Instant::now();
|
||||
match encoder.encode(&yuv, 0) {
|
||||
Ok(frames) => {
|
||||
let elapsed = start.elapsed().as_millis();
|
||||
// 验证: 必须产生 1 帧且为关键帧,且在超时时间内完成
|
||||
if frames.len() == 1 && frames[0].key == 1
|
||||
&& elapsed < TEST_TIMEOUT_MS {
|
||||
res.push(codec);
|
||||
}
|
||||
}
|
||||
Err(_) => { /* 编码失败,跳过 */ }
|
||||
}
|
||||
}
|
||||
Err(_) => { /* 创建失败,跳过 */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 2. NVIDIA NVENC/NVDEC
|
||||
|
||||
### 2.1 检测机制 (Linux)
|
||||
|
||||
使用简化的动态库检测方法,无需 CUDA SDK 依赖:
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
int linux_support_nv() {
|
||||
// 检测 CUDA 运行时库
|
||||
void *handle = dlopen("libcuda.so.1", RTLD_LAZY);
|
||||
if (!handle) {
|
||||
handle = dlopen("libcuda.so", RTLD_LAZY);
|
||||
}
|
||||
if (!handle) {
|
||||
LOG_TRACE("NVIDIA: libcuda.so not found");
|
||||
return -1;
|
||||
}
|
||||
dlclose(handle);
|
||||
|
||||
// 检测 NVENC 编码库
|
||||
handle = dlopen("libnvidia-encode.so.1", RTLD_LAZY);
|
||||
if (!handle) {
|
||||
handle = dlopen("libnvidia-encode.so", RTLD_LAZY);
|
||||
}
|
||||
if (!handle) {
|
||||
LOG_TRACE("NVIDIA: libnvidia-encode.so not found");
|
||||
return -1;
|
||||
}
|
||||
dlclose(handle);
|
||||
|
||||
LOG_TRACE("NVIDIA: driver support detected");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 编码配置
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/util.cpp
|
||||
|
||||
// NVENC 低延迟配置
|
||||
if (name.find("nvenc") != std::string::npos) {
|
||||
// 禁用编码延迟
|
||||
av_opt_set(priv_data, "delay", "0", 0);
|
||||
}
|
||||
|
||||
// GPU 选择
|
||||
if (name.find("nvenc") != std::string::npos) {
|
||||
av_opt_set_int(priv_data, "gpu", gpu_index, 0);
|
||||
}
|
||||
|
||||
// 质量预设
|
||||
switch (quality) {
|
||||
case Quality_Medium:
|
||||
av_opt_set(priv_data, "preset", "p4", 0);
|
||||
break;
|
||||
case Quality_Low:
|
||||
av_opt_set(priv_data, "preset", "p1", 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// 码率控制
|
||||
av_opt_set(priv_data, "rc", "cbr", 0); // 或 "vbr"
|
||||
```
|
||||
|
||||
### 2.3 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `RUSTDESK_HWCODEC_NVENC_GPU` | 指定使用的 GPU 索引 (-1 = 自动) |
|
||||
|
||||
### 2.4 依赖库
|
||||
|
||||
- `libcuda.so` / `libcuda.so.1` - CUDA 运行时
|
||||
- `libnvidia-encode.so` / `libnvidia-encode.so.1` - NVENC 编码器
|
||||
|
||||
## 3. AMD AMF
|
||||
|
||||
### 3.1 检测机制 (Linux)
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
int linux_support_amd() {
|
||||
#if defined(__x86_64__) || defined(__aarch64__)
|
||||
#define AMF_DLL_NAMEA "libamfrt64.so.1"
|
||||
#else
|
||||
#define AMF_DLL_NAMEA "libamfrt32.so.1"
|
||||
#endif
|
||||
|
||||
void *handle = dlopen(AMF_DLL_NAMEA, RTLD_LAZY);
|
||||
if (!handle) {
|
||||
return -1; // AMF 不可用
|
||||
}
|
||||
dlclose(handle);
|
||||
return 0; // AMF 可用
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 编码配置
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/util.cpp
|
||||
|
||||
// AMF 低延迟配置
|
||||
if (name.find("amf") != std::string::npos) {
|
||||
av_opt_set(priv_data, "query_timeout", "1000", 0);
|
||||
}
|
||||
|
||||
// 质量预设
|
||||
switch (quality) {
|
||||
case Quality_High:
|
||||
av_opt_set(priv_data, "quality", "quality", 0);
|
||||
break;
|
||||
case Quality_Medium:
|
||||
av_opt_set(priv_data, "quality", "balanced", 0);
|
||||
break;
|
||||
case Quality_Low:
|
||||
av_opt_set(priv_data, "quality", "speed", 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// 码率控制
|
||||
av_opt_set(priv_data, "rc", "cbr", 0); // 恒定码率
|
||||
av_opt_set(priv_data, "rc", "vbr_latency", 0); // 低延迟 VBR
|
||||
```
|
||||
|
||||
### 3.3 依赖库
|
||||
|
||||
- `libamfrt64.so.1` (64位) 或 `libamfrt32.so.1` (32位)
|
||||
|
||||
## 4. Intel QSV/MFX
|
||||
|
||||
### 4.1 检测机制 (Linux)
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
int linux_support_intel() {
|
||||
const char *libs[] = {
|
||||
"libvpl.so", // oneVPL (新版)
|
||||
"libmfx.so", // Media SDK
|
||||
"libmfx-gen.so.1.2", // 新驱动
|
||||
"libmfxhw64.so.1" // 旧版驱动
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(libs) / sizeof(libs[0]); i++) {
|
||||
void *handle = dlopen(libs[i], RTLD_LAZY);
|
||||
if (handle) {
|
||||
dlclose(handle);
|
||||
return 0; // 找到可用库
|
||||
}
|
||||
}
|
||||
return -1; // Intel MFX 不可用
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 编码配置
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/util.cpp
|
||||
|
||||
// QSV 低延迟配置
|
||||
if (name.find("qsv") != std::string::npos) {
|
||||
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||
}
|
||||
|
||||
// QSV 特殊码率配置
|
||||
if (name.find("qsv") != std::string::npos) {
|
||||
c->rc_max_rate = c->bit_rate;
|
||||
c->bit_rate--; // 实现 CBR 效果
|
||||
}
|
||||
|
||||
// 质量预设
|
||||
switch (quality) {
|
||||
case Quality_High:
|
||||
av_opt_set(priv_data, "preset", "veryslow", 0);
|
||||
break;
|
||||
case Quality_Medium:
|
||||
av_opt_set(priv_data, "preset", "medium", 0);
|
||||
break;
|
||||
case Quality_Low:
|
||||
av_opt_set(priv_data, "preset", "veryfast", 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// 严格标准兼容性 (用于某些特殊设置)
|
||||
c->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
|
||||
```
|
||||
|
||||
### 4.3 限制
|
||||
|
||||
- QSV 不支持 `YUV420P` 像素格式,必须使用 `NV12`
|
||||
- 在 One-KVM 简化版中仅 Windows 平台完全支持
|
||||
|
||||
## 5. VAAPI (Linux)
|
||||
|
||||
### 5.1 工作原理
|
||||
|
||||
VAAPI (Video Acceleration API) 是 Linux 上的通用硬件视频加速接口:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ FFmpeg libavcodec │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ VAAPI (libva) │
|
||||
├──────────────┬──────────────┬──────────────┬────────────────┤
|
||||
│ Intel i965 │ Intel iHD │ AMD radeonsi │ NVIDIA VDPAU │
|
||||
│ (Gen8-) │ (Gen9+) │ │ (via wrapper) │
|
||||
├──────────────┴──────────────┴──────────────┴────────────────┤
|
||||
│ Kernel DRM Driver │
|
||||
├──────────────┬──────────────┬──────────────┬────────────────┤
|
||||
│ i915 │ amdgpu │ nvidia │ ... │
|
||||
└──────────────┴──────────────┴──────────────┴────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 编码配置
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/util.cpp
|
||||
|
||||
// VAAPI 低延迟配置
|
||||
if (name.find("vaapi") != std::string::npos) {
|
||||
av_opt_set(priv_data, "async_depth", "1", 0);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 硬件上下文初始化
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp
|
||||
|
||||
// 检测 VAAPI 编码器
|
||||
if (name_.find("vaapi") != std::string::npos) {
|
||||
hw_device_type_ = AV_HWDEVICE_TYPE_VAAPI;
|
||||
hw_pixfmt_ = AV_PIX_FMT_VAAPI;
|
||||
}
|
||||
|
||||
// 创建硬件设备上下文
|
||||
ret = av_hwdevice_ctx_create(&hw_device_ctx_, hw_device_type_,
|
||||
NULL, // 使用默认设备
|
||||
NULL, 0);
|
||||
|
||||
// 设置硬件帧上下文
|
||||
set_hwframe_ctx();
|
||||
|
||||
// 分配硬件帧
|
||||
hw_frame_ = av_frame_alloc();
|
||||
av_hwframe_get_buffer(c_->hw_frames_ctx, hw_frame_, 0);
|
||||
```
|
||||
|
||||
### 5.4 编码流程
|
||||
|
||||
```
|
||||
输入 YUV (CPU 内存)
|
||||
│
|
||||
▼
|
||||
av_hwframe_transfer_data(hw_frame_, frame_, 0) // CPU → GPU
|
||||
│
|
||||
▼
|
||||
avcodec_send_frame(c_, hw_frame_) // 发送 GPU 帧
|
||||
│
|
||||
▼
|
||||
avcodec_receive_packet(c_, pkt_) // 获取编码数据
|
||||
│
|
||||
▼
|
||||
编码数据 (CPU 内存)
|
||||
```
|
||||
|
||||
### 5.5 依赖库
|
||||
|
||||
- `libva.so` - VAAPI 核心库
|
||||
- `libva-drm.so` - DRM 后端
|
||||
- `libva-x11.so` - X11 后端 (可选)
|
||||
|
||||
## 6. Rockchip MPP
|
||||
|
||||
### 6.1 检测机制
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
int linux_support_rkmpp() {
|
||||
// 检测 MPP 服务设备
|
||||
if (access("/dev/mpp_service", F_OK) == 0) {
|
||||
LOG_TRACE("RKMPP: Found /dev/mpp_service");
|
||||
return 0; // MPP 可用
|
||||
}
|
||||
// 备用: 检测 RGA 设备
|
||||
if (access("/dev/rga", F_OK) == 0) {
|
||||
LOG_TRACE("RKMPP: Found /dev/rga");
|
||||
return 0; // MPP 可能可用
|
||||
}
|
||||
LOG_TRACE("RKMPP: No Rockchip MPP device found");
|
||||
return -1; // MPP 不可用
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 支持的编码器
|
||||
|
||||
| 编码器 | 优先级 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `h264_rkmpp` | Best (0) | H.264 硬件编码 |
|
||||
| `hevc_rkmpp` | Best (0) | H.265 硬件编码 |
|
||||
|
||||
### 6.3 适用设备
|
||||
|
||||
- Rockchip RK3328 (Onecloud, Chainedbox)
|
||||
- Rockchip RK3399/RK3588 系列
|
||||
- 其他 Rockchip SoC
|
||||
|
||||
## 7. V4L2 M2M
|
||||
|
||||
### 7.1 检测机制
|
||||
|
||||
```cpp
|
||||
// libs/hwcodec/cpp/common/platform/linux/linux.cpp
|
||||
|
||||
int linux_support_v4l2m2m() {
|
||||
const char *m2m_devices[] = {
|
||||
"/dev/video10", // 常见 M2M 编码设备
|
||||
"/dev/video11", // 常见 M2M 解码设备
|
||||
"/dev/video0", // 某些 SoC 使用
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(m2m_devices) / sizeof(m2m_devices[0]); i++) {
|
||||
if (access(m2m_devices[i], F_OK) == 0) {
|
||||
int fd = open(m2m_devices[i], O_RDWR | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
LOG_TRACE("V4L2 M2M: Found device " + m2m_devices[i]);
|
||||
return 0; // V4L2 M2M 可用
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_TRACE("V4L2 M2M: No M2M device found");
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 支持的编码器
|
||||
|
||||
| 编码器 | 优先级 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `h264_v4l2m2m` | Good (1) | H.264 V4L2 编码 |
|
||||
| `hevc_v4l2m2m` | Good (1) | H.265 V4L2 编码 |
|
||||
|
||||
### 7.3 适用设备
|
||||
|
||||
- 通用 ARM SoC (Allwinner, Amlogic 等)
|
||||
- 支持 V4L2 M2M API 的设备
|
||||
|
||||
## 8. 硬件加速优先级
|
||||
|
||||
### 8.1 优先级定义
|
||||
|
||||
```rust
|
||||
pub enum Priority {
|
||||
Best = 0, // 专用硬件编码器
|
||||
Good = 1, // 通用硬件加速
|
||||
Normal = 2, // 基本硬件支持
|
||||
Soft = 3, // 软件编码
|
||||
Bad = 4, // 最低优先级
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 各编码器优先级
|
||||
|
||||
| 优先级 | 编码器 |
|
||||
|--------|--------|
|
||||
| Best (0) | nvenc, amf, qsv, rkmpp |
|
||||
| Good (1) | vaapi, v4l2m2m |
|
||||
| Soft (3) | x264, x265, libvpx |
|
||||
|
||||
### 8.3 选择策略
|
||||
|
||||
```rust
|
||||
// libs/hwcodec/src/ffmpeg_ram/mod.rs
|
||||
|
||||
pub fn prioritized(coders: Vec<CodecInfo>) -> CodecInfos {
|
||||
// 对于每种格式,选择优先级最高的编码器
|
||||
for coder in coders {
|
||||
match coder.format {
|
||||
DataFormat::H264 => {
|
||||
if h264.is_none() || h264.priority > coder.priority {
|
||||
h264 = Some(coder);
|
||||
}
|
||||
}
|
||||
// ... 其他格式类似
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 故障排除
|
||||
|
||||
### 9.1 NVIDIA
|
||||
|
||||
```bash
|
||||
# 检查 NVIDIA 驱动
|
||||
nvidia-smi
|
||||
|
||||
# 检查 NVENC 支持
|
||||
ls /dev/nvidia*
|
||||
|
||||
# 检查 CUDA 库
|
||||
ldconfig -p | grep cuda
|
||||
ldconfig -p | grep nvidia-encode
|
||||
```
|
||||
|
||||
### 9.2 AMD
|
||||
|
||||
```bash
|
||||
# 检查 AMD 驱动
|
||||
lspci | grep AMD
|
||||
|
||||
# 检查 AMF 库
|
||||
ldconfig -p | grep amf
|
||||
```
|
||||
|
||||
### 9.3 Intel
|
||||
|
||||
```bash
|
||||
# 检查 Intel 驱动
|
||||
vainfo
|
||||
|
||||
# 检查 MFX 库
|
||||
ldconfig -p | grep mfx
|
||||
ldconfig -p | grep vpl
|
||||
```
|
||||
|
||||
### 9.4 VAAPI
|
||||
|
||||
```bash
|
||||
# 安装 vainfo
|
||||
sudo apt install vainfo
|
||||
|
||||
# 检查 VAAPI 支持
|
||||
vainfo
|
||||
|
||||
# 输出示例:
|
||||
# libva info: VA-API version 1.14.0
|
||||
# libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
|
||||
# vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics
|
||||
# vainfo: Supported profile and entrypoints
|
||||
# VAProfileH264Main : VAEntrypointVLD
|
||||
# VAProfileH264Main : VAEntrypointEncSlice
|
||||
# ...
|
||||
```
|
||||
|
||||
### 9.5 Rockchip MPP
|
||||
|
||||
```bash
|
||||
# 检查 MPP 设备
|
||||
ls -la /dev/mpp_service
|
||||
ls -la /dev/rga
|
||||
|
||||
# 检查 MPP 库
|
||||
ldconfig -p | grep rockchip_mpp
|
||||
```
|
||||
|
||||
### 9.6 V4L2 M2M
|
||||
|
||||
```bash
|
||||
# 列出 V4L2 设备
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# 检查设备能力
|
||||
v4l2-ctl -d /dev/video10 --all
|
||||
```
|
||||
|
||||
## 10. 性能优化建议
|
||||
|
||||
### 10.1 编码器选择
|
||||
|
||||
1. **优先使用硬件编码**: NVENC > AMF > QSV > VAAPI > V4L2 M2M > 软件
|
||||
2. **ARM 设备**: 优先检测 RKMPP,其次 V4L2 M2M
|
||||
3. **x86 设备**: 根据 GPU 厂商自动选择
|
||||
|
||||
### 10.2 低延迟配置
|
||||
|
||||
所有硬件编码器都启用了低延迟优化:
|
||||
|
||||
| 编码器 | 配置 |
|
||||
|--------|------|
|
||||
| NVENC | `delay=0` |
|
||||
| AMF | `query_timeout=1000` |
|
||||
| QSV | `async_depth=1` |
|
||||
| VAAPI | `async_depth=1` |
|
||||
| libvpx | `deadline=realtime`, `cpu-used=6` |
|
||||
|
||||
### 10.3 码率控制
|
||||
|
||||
- **实时流**: 推荐 CBR 模式,保证稳定码率
|
||||
- **GOP 大小**: 建议 30-60 帧 (1-2秒),平衡延迟和压缩效率
|
||||
@@ -1,477 +0,0 @@
|
||||
# hwcodec 构建系统与集成指南
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
```
|
||||
libs/hwcodec/
|
||||
├── Cargo.toml # 包配置
|
||||
├── Cargo.lock # 依赖锁定
|
||||
├── build.rs # 构建脚本
|
||||
└── src/ # Rust 源码
|
||||
├── lib.rs # 库入口
|
||||
├── common.rs # 公共定义
|
||||
├── ffmpeg.rs # FFmpeg 集成
|
||||
└── ffmpeg_ram/ # RAM 编解码
|
||||
├── mod.rs
|
||||
├── encode.rs
|
||||
└── decode.rs
|
||||
└── cpp/ # C++ 源码
|
||||
├── common/ # 公共代码
|
||||
│ ├── log.cpp
|
||||
│ ├── log.h
|
||||
│ ├── util.cpp
|
||||
│ ├── util.h
|
||||
│ ├── callback.h
|
||||
│ ├── common.h
|
||||
│ └── platform/
|
||||
│ ├── linux/
|
||||
│ │ ├── linux.cpp
|
||||
│ │ └── linux.h
|
||||
│ └── win/
|
||||
│ ├── win.cpp
|
||||
│ └── win.h
|
||||
├── ffmpeg_ram/ # FFmpeg RAM 实现
|
||||
│ ├── ffmpeg_ram_encode.cpp
|
||||
│ ├── ffmpeg_ram_decode.cpp
|
||||
│ └── ffmpeg_ram_ffi.h
|
||||
└── yuv/ # YUV 处理
|
||||
└── yuv.cpp
|
||||
```
|
||||
|
||||
## 2. Cargo 配置
|
||||
|
||||
### 2.1 Cargo.toml
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "hwcodec"
|
||||
version = "0.8.0"
|
||||
edition = "2021"
|
||||
description = "Hardware video codec for IP-KVM (Windows/Linux)"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
log = "0.4" # 日志
|
||||
serde_derive = "1.0" # 序列化派生宏
|
||||
serde = "1.0" # 序列化
|
||||
serde_json = "1.0" # JSON 序列化
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0" # C++ 编译
|
||||
bindgen = "0.59" # FFI 绑定生成
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.10" # 日志输出
|
||||
```
|
||||
|
||||
### 2.2 与原版的区别
|
||||
|
||||
| 特性 | 原版 (RustDesk) | 简化版 (One-KVM) |
|
||||
|------|-----------------|------------------|
|
||||
| `vram` feature | ✓ | ✗ (已移除) |
|
||||
| 外部 SDK | 需要 | 不需要 |
|
||||
| 版本号 | 0.7.1 | 0.8.0 |
|
||||
| 目标平台 | Windows/Linux/macOS/Android | Windows/Linux |
|
||||
|
||||
### 2.3 使用方式
|
||||
|
||||
```toml
|
||||
# 在 One-KVM 项目中使用
|
||||
[dependencies]
|
||||
hwcodec = { path = "libs/hwcodec" }
|
||||
```
|
||||
|
||||
## 3. 构建脚本详解 (build.rs)
|
||||
|
||||
### 3.1 主入口
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let mut builder = Build::new();
|
||||
|
||||
// 1. 构建公共模块
|
||||
build_common(&mut builder);
|
||||
|
||||
// 2. 构建 FFmpeg 相关模块
|
||||
ffmpeg::build_ffmpeg(&mut builder);
|
||||
|
||||
// 3. 编译生成静态库
|
||||
builder.static_crt(true).compile("hwcodec");
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 公共模块构建
|
||||
|
||||
```rust
|
||||
fn build_common(builder: &mut Build) {
|
||||
let common_dir = manifest_dir.join("cpp").join("common");
|
||||
|
||||
// 生成 FFI 绑定
|
||||
bindgen::builder()
|
||||
.header(common_dir.join("common.h"))
|
||||
.header(common_dir.join("callback.h"))
|
||||
.rustified_enum("*")
|
||||
.generate()
|
||||
.write_to_file(OUT_DIR.join("common_ffi.rs"));
|
||||
|
||||
// 平台相关代码
|
||||
#[cfg(windows)]
|
||||
builder.file(common_dir.join("platform/win/win.cpp"));
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
builder.file(common_dir.join("platform/linux/linux.cpp"));
|
||||
|
||||
// 工具代码
|
||||
builder.files([
|
||||
common_dir.join("log.cpp"),
|
||||
common_dir.join("util.cpp"),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 FFmpeg 模块构建
|
||||
|
||||
```rust
|
||||
mod ffmpeg {
|
||||
pub fn build_ffmpeg(builder: &mut Build) {
|
||||
// 生成 FFmpeg FFI 绑定
|
||||
ffmpeg_ffi();
|
||||
|
||||
// 链接 FFmpeg 库
|
||||
if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") {
|
||||
link_vcpkg(builder, vcpkg_root.into());
|
||||
} else {
|
||||
link_system_ffmpeg(builder); // pkg-config
|
||||
}
|
||||
|
||||
// 链接系统库
|
||||
link_os();
|
||||
|
||||
// 构建 FFmpeg RAM 模块
|
||||
build_ffmpeg_ram(builder);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 FFmpeg 链接方式
|
||||
|
||||
#### VCPKG (跨平台静态链接)
|
||||
|
||||
```rust
|
||||
fn link_vcpkg(builder: &mut Build, path: PathBuf) -> PathBuf {
|
||||
// 目标平台识别
|
||||
let target = match (target_os, target_arch) {
|
||||
("windows", "x86_64") => "x64-windows-static",
|
||||
("linux", arch) => format!("{}-linux", arch),
|
||||
_ => panic!("unsupported platform"),
|
||||
};
|
||||
|
||||
let lib_path = path.join("installed").join(target).join("lib");
|
||||
|
||||
// 链接 FFmpeg 静态库
|
||||
println!("cargo:rustc-link-search=native={}", lib_path);
|
||||
["avcodec", "avutil", "avformat"].iter()
|
||||
.for_each(|lib| println!("cargo:rustc-link-lib=static={}", lib));
|
||||
}
|
||||
```
|
||||
|
||||
#### pkg-config (Linux 动态链接)
|
||||
|
||||
```rust
|
||||
fn link_system_ffmpeg(builder: &mut Build) {
|
||||
let libs = ["libavcodec", "libavutil", "libavformat", "libswscale"];
|
||||
|
||||
for lib in &libs {
|
||||
// 获取编译标志
|
||||
let cflags = Command::new("pkg-config")
|
||||
.args(["--cflags", lib])
|
||||
.output()?;
|
||||
|
||||
// 获取链接标志
|
||||
let libs = Command::new("pkg-config")
|
||||
.args(["--libs", lib])
|
||||
.output()?;
|
||||
|
||||
// 解析并应用
|
||||
for flag in libs.split_whitespace() {
|
||||
if flag.starts_with("-L") {
|
||||
println!("cargo:rustc-link-search=native={}", &flag[2..]);
|
||||
} else if flag.starts_with("-l") {
|
||||
println!("cargo:rustc-link-lib={}", &flag[2..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 系统库链接
|
||||
|
||||
```rust
|
||||
fn link_os() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
|
||||
let libs: Vec<&str> = match target_os.as_str() {
|
||||
"windows" => vec!["User32", "bcrypt", "ole32", "advapi32"],
|
||||
"linux" => vec!["drm", "X11", "stdc++", "z"],
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
for lib in libs {
|
||||
println!("cargo:rustc-link-lib={}", lib);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. FFI 绑定生成
|
||||
|
||||
### 4.1 bindgen 配置
|
||||
|
||||
```rust
|
||||
bindgen::builder()
|
||||
.header("path/to/header.h")
|
||||
.rustified_enum("*") // 生成 Rust 枚举
|
||||
.parse_callbacks(Box::new(Callbacks)) // 自定义回调
|
||||
.generate()
|
||||
.write_to_file(OUT_DIR.join("ffi.rs"));
|
||||
```
|
||||
|
||||
### 4.2 自定义派生
|
||||
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
struct CommonCallbacks;
|
||||
|
||||
impl bindgen::callbacks::ParseCallbacks for CommonCallbacks {
|
||||
fn add_derives(&self, name: &str) -> Vec<String> {
|
||||
// 为特定类型添加序列化支持
|
||||
match name {
|
||||
"DataFormat" | "SurfaceFormat" | "API" => {
|
||||
vec!["Serialize".to_string(), "Deserialize".to_string()]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 生成的文件
|
||||
|
||||
| 文件 | 来源 | 内容 |
|
||||
|------|------|------|
|
||||
| `common_ffi.rs` | `common.h`, `callback.h` | 枚举、常量、回调类型 |
|
||||
| `ffmpeg_ffi.rs` | `ffmpeg_ffi.h` | FFmpeg 日志级别、函数 |
|
||||
| `ffmpeg_ram_ffi.rs` | `ffmpeg_ram_ffi.h` | 编解码器函数 |
|
||||
|
||||
## 5. 平台构建指南
|
||||
|
||||
### 5.1 Linux 构建
|
||||
|
||||
```bash
|
||||
# 安装 FFmpeg 开发库
|
||||
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
|
||||
|
||||
# 安装其他依赖
|
||||
sudo apt install libdrm-dev libx11-dev pkg-config
|
||||
|
||||
# 安装 clang (bindgen 需要)
|
||||
sudo apt install clang libclang-dev
|
||||
|
||||
# 构建
|
||||
cargo build --release -p hwcodec
|
||||
```
|
||||
|
||||
### 5.2 Windows 构建 (VCPKG)
|
||||
|
||||
```powershell
|
||||
# 安装 VCPKG
|
||||
git clone https://github.com/microsoft/vcpkg
|
||||
cd vcpkg
|
||||
./bootstrap-vcpkg.bat
|
||||
|
||||
# 安装 FFmpeg
|
||||
./vcpkg install ffmpeg:x64-windows-static
|
||||
|
||||
# 设置环境变量
|
||||
$env:VCPKG_ROOT = "C:\path\to\vcpkg"
|
||||
|
||||
# 构建
|
||||
cargo build --release -p hwcodec
|
||||
```
|
||||
|
||||
### 5.3 交叉编译
|
||||
|
||||
```bash
|
||||
# 安装 cross
|
||||
cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
# ARM64 Linux
|
||||
cross build --release -p hwcodec --target aarch64-unknown-linux-gnu
|
||||
|
||||
# ARMv7 Linux
|
||||
cross build --release -p hwcodec --target armv7-unknown-linux-gnueabihf
|
||||
```
|
||||
|
||||
## 6. 集成到 One-KVM
|
||||
|
||||
### 6.1 依赖配置
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dependencies]
|
||||
hwcodec = { path = "libs/hwcodec" }
|
||||
```
|
||||
|
||||
### 6.2 使用示例
|
||||
|
||||
```rust
|
||||
use hwcodec::ffmpeg_ram::encode::{Encoder, EncodeContext};
|
||||
use hwcodec::ffmpeg_ram::decode::{Decoder, DecodeContext};
|
||||
use hwcodec::ffmpeg::{AVPixelFormat, AVHWDeviceType};
|
||||
|
||||
// 检测可用编码器
|
||||
let encoders = Encoder::available_encoders(ctx, None);
|
||||
|
||||
// 创建编码器
|
||||
let encoder = Encoder::new(EncodeContext {
|
||||
name: "h264_vaapi".to_string(),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
|
||||
fps: 30,
|
||||
gop: 30,
|
||||
kbs: 4000,
|
||||
// ...
|
||||
})?;
|
||||
|
||||
// 编码
|
||||
let frames = encoder.encode(&yuv_data, pts_ms)?;
|
||||
|
||||
// 创建 MJPEG 解码器 (IP-KVM 专用)
|
||||
let decoder = Decoder::new(DecodeContext {
|
||||
name: "mjpeg".to_string(),
|
||||
device_type: AVHWDeviceType::AV_HWDEVICE_TYPE_NONE,
|
||||
thread_count: 4,
|
||||
})?;
|
||||
|
||||
// 解码
|
||||
let frames = decoder.decode(&mjpeg_data)?;
|
||||
```
|
||||
|
||||
### 6.3 日志集成
|
||||
|
||||
```rust
|
||||
// hwcodec 使用 log crate,与 One-KVM 日志系统兼容
|
||||
use log::{debug, info, warn, error};
|
||||
|
||||
// C++ 层日志通过回调传递到 Rust
|
||||
#[no_mangle]
|
||||
pub extern "C" fn hwcodec_av_log_callback(level: i32, message: *const c_char) {
|
||||
// 转发到 Rust log 系统
|
||||
match level {
|
||||
AV_LOG_ERROR => error!("{}", message),
|
||||
AV_LOG_WARNING => warn!("{}", message),
|
||||
AV_LOG_INFO => info!("{}", message),
|
||||
AV_LOG_DEBUG => debug!("{}", message),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 故障排除
|
||||
|
||||
### 7.1 编译错误
|
||||
|
||||
**FFmpeg 未找到**:
|
||||
```
|
||||
error: pkg-config failed for libavcodec
|
||||
```
|
||||
解决: 安装 FFmpeg 开发库
|
||||
```bash
|
||||
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
|
||||
```
|
||||
|
||||
**bindgen 错误**:
|
||||
```
|
||||
error: failed to run custom build command for `hwcodec`
|
||||
```
|
||||
解决: 安装 clang
|
||||
```bash
|
||||
sudo apt install clang libclang-dev
|
||||
```
|
||||
|
||||
### 7.2 链接错误
|
||||
|
||||
**符号未定义**:
|
||||
```
|
||||
undefined reference to `av_log_set_level'
|
||||
```
|
||||
解决: 检查 FFmpeg 库链接顺序,确保 pkg-config 正确配置
|
||||
|
||||
**动态库未找到**:
|
||||
```
|
||||
error while loading shared libraries: libavcodec.so.59
|
||||
```
|
||||
解决:
|
||||
```bash
|
||||
sudo ldconfig
|
||||
# 或设置 LD_LIBRARY_PATH
|
||||
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
```
|
||||
|
||||
### 7.3 运行时错误
|
||||
|
||||
**硬件编码器不可用**:
|
||||
```
|
||||
Encoder h264_vaapi test failed
|
||||
```
|
||||
检查:
|
||||
1. 驱动是否正确安装: `vainfo`
|
||||
2. 权限是否足够: `ls -la /dev/dri/`
|
||||
3. 用户是否在 video 组: `groups`
|
||||
|
||||
**解码失败**:
|
||||
```
|
||||
avcodec_receive_frame failed, ret = -11
|
||||
```
|
||||
解决: 这通常表示需要更多输入数据 (EAGAIN),是正常行为
|
||||
|
||||
## 8. 与原版 RustDesk hwcodec 的构建差异
|
||||
|
||||
### 8.1 移除的构建步骤
|
||||
|
||||
| 步骤 | 原因 |
|
||||
|------|------|
|
||||
| `build_mux()` | 移除了 Mux 模块 |
|
||||
| `build_ffmpeg_vram()` | 移除了 VRAM 模块 |
|
||||
| `sdk::build_sdk()` | 移除了外部 SDK 依赖 |
|
||||
| macOS 框架链接 | 移除了 macOS 支持 |
|
||||
| Android NDK 链接 | 移除了 Android 支持 |
|
||||
|
||||
### 8.2 简化的构建流程
|
||||
|
||||
```
|
||||
原版构建流程:
|
||||
build.rs
|
||||
├── build_common()
|
||||
├── ffmpeg::build_ffmpeg()
|
||||
│ ├── build_ffmpeg_ram()
|
||||
│ ├── build_ffmpeg_vram() [已移除]
|
||||
│ └── build_mux() [已移除]
|
||||
└── sdk::build_sdk() [已移除]
|
||||
|
||||
简化版构建流程:
|
||||
build.rs
|
||||
├── build_common()
|
||||
└── ffmpeg::build_ffmpeg()
|
||||
└── build_ffmpeg_ram()
|
||||
```
|
||||
|
||||
### 8.3 优势
|
||||
|
||||
1. **更快的编译**: 无需编译外部 SDK 代码
|
||||
2. **更少的依赖**: 无需下载 ~9MB 的外部 SDK
|
||||
3. **更简单的维护**: 代码量减少约 67%
|
||||
4. **更小的二进制**: 不包含未使用的功能
|
||||
@@ -1,69 +0,0 @@
|
||||
# RustDesk 通信协议技术报告
|
||||
|
||||
## 概述
|
||||
|
||||
本报告详细分析 RustDesk 远程桌面软件的客户端与服务器之间的通信协议,包括 Rendezvous 服务器(hbbs)、Relay 服务器(hbbr)以及客户端之间的 P2P 连接机制。
|
||||
|
||||
## 文档结构
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [01-architecture.md](01-architecture.md) | 整体架构设计 |
|
||||
| [02-rendezvous-protocol.md](02-rendezvous-protocol.md) | Rendezvous 服务器协议 |
|
||||
| [03-relay-protocol.md](03-relay-protocol.md) | Relay 服务器协议 |
|
||||
| [04-p2p-connection.md](04-p2p-connection.md) | P2P 连接流程 |
|
||||
| [05-message-format.md](05-message-format.md) | 消息格式定义 |
|
||||
| [06-encryption.md](06-encryption.md) | 加密机制 |
|
||||
| [07-nat-traversal.md](07-nat-traversal.md) | NAT 穿透技术 |
|
||||
| [08-onekvm-comparison.md](08-onekvm-comparison.md) | **One-KVM 实现对比分析** |
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. Rendezvous Server (hbbs)
|
||||
- **功能**: ID 注册、Peer 发现、NAT 类型检测、连接协调
|
||||
- **端口**: 21116 (TCP/UDP), 21115 (NAT 测试), 21118 (WebSocket)
|
||||
- **源文件**: `rustdesk-server/src/rendezvous_server.rs`
|
||||
|
||||
### 2. Relay Server (hbbr)
|
||||
- **功能**: 当 P2P 连接失败时提供数据中转
|
||||
- **端口**: 21117 (TCP), 21119 (WebSocket)
|
||||
- **源文件**: `rustdesk-server/src/relay_server.rs`
|
||||
|
||||
### 3. 客户端 (RustDesk)
|
||||
- **功能**: 远程桌面控制、文件传输、屏幕共享
|
||||
- **核心模块**:
|
||||
- `rendezvous_mediator.rs` - 与 Rendezvous 服务器通信
|
||||
- `client.rs` - 客户端连接逻辑
|
||||
- `server/connection.rs` - 被控端连接处理
|
||||
|
||||
## 协议栈
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (Video/Audio/Keyboard/Mouse/File) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Message Layer │
|
||||
│ (Protobuf Messages) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Security Layer │
|
||||
│ (Sodium: X25519 + ChaCha20) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Transport Layer │
|
||||
│ (TCP/UDP/WebSocket/KCP) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 关键技术特点
|
||||
|
||||
1. **混合连接模式**: 优先尝试 P2P 直连,失败后自动切换到 Relay 中转
|
||||
2. **多协议支持**: TCP、UDP、WebSocket、KCP
|
||||
3. **端到端加密**: 使用 libsodium 实现的 X25519 密钥交换和 ChaCha20-Poly1305 对称加密
|
||||
4. **NAT 穿透**: 支持 UDP 打洞和 TCP 打洞技术
|
||||
5. **服务器签名**: 可选的服务器公钥签名验证,防止中间人攻击
|
||||
|
||||
## 版本信息
|
||||
|
||||
- 分析基于 RustDesk 最新版本源码
|
||||
- Protocol Buffer 版本: proto3
|
||||
- 加密库: libsodium (sodiumoxide)
|
||||
@@ -1,218 +0,0 @@
|
||||
# RustDesk 架构设计
|
||||
|
||||
## 系统架构图
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Rendezvous Server │
|
||||
│ (hbbs) │
|
||||
│ Port: 21116 │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Client A │ │ Client B │ │ Client C │
|
||||
│ (控制端) │ │ (被控端) │ │ (被控端) │
|
||||
└───────┬───────┘ └───────┬───────┘ └───────────────┘
|
||||
│ │
|
||||
│ P2P Connection │
|
||||
│◄────────────────────────►│
|
||||
│ │
|
||||
│ (如果 P2P 失败) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
└─►│ Relay Server │◄──────┘
|
||||
│ (hbbr) │
|
||||
│ Port: 21117 │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## 服务器组件详解
|
||||
|
||||
### Rendezvous Server (hbbs)
|
||||
|
||||
**监听端口:**
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21116 | TCP | 主要通信端口,处理 punch hole 请求 |
|
||||
| 21116 | UDP | Peer 注册、NAT 类型检测 |
|
||||
| 21115 | TCP | NAT 测试专用端口 |
|
||||
| 21118 | WebSocket | Web 客户端支持 |
|
||||
|
||||
**核心数据结构:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:64-83
|
||||
pub struct RendezvousServer {
|
||||
tcp_punch: Arc<Mutex<HashMap<SocketAddr, Sink>>>, // TCP punch hole 连接
|
||||
pm: PeerMap, // Peer 映射表
|
||||
tx: Sender, // 消息发送通道
|
||||
relay_servers: Arc<RelayServers>, // 可用 Relay 服务器列表
|
||||
relay_servers0: Arc<RelayServers>, // 原始 Relay 服务器列表
|
||||
rendezvous_servers: Arc<Vec<String>>, // Rendezvous 服务器列表
|
||||
inner: Arc<Inner>, // 内部配置
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
serial: i32, // 配置序列号
|
||||
version: String, // 软件版本
|
||||
software_url: String, // 软件更新 URL
|
||||
mask: Option<Ipv4Network>, // LAN 掩码
|
||||
local_ip: String, // 本地 IP
|
||||
sk: Option<sign::SecretKey>, // 服务器签名密钥
|
||||
}
|
||||
```
|
||||
|
||||
**Peer 数据结构:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/peer.rs:32-42
|
||||
pub struct Peer {
|
||||
pub socket_addr: SocketAddr, // 最后注册的地址
|
||||
pub last_reg_time: Instant, // 最后注册时间
|
||||
pub guid: Vec<u8>, // 数据库 GUID
|
||||
pub uuid: Bytes, // 设备 UUID
|
||||
pub pk: Bytes, // 公钥
|
||||
pub info: PeerInfo, // Peer 信息
|
||||
pub reg_pk: (u32, Instant), // 注册频率限制
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Server (hbbr)
|
||||
|
||||
**监听端口:**
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21117 | TCP | 主要中转端口 |
|
||||
| 21119 | WebSocket | Web 客户端支持 |
|
||||
|
||||
**核心特性:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:40-44
|
||||
static DOWNGRADE_THRESHOLD_100: AtomicUsize = AtomicUsize::new(66); // 降级阈值
|
||||
static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // 检测开始时间(ms)
|
||||
static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // 限速(bit/s)
|
||||
static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024);// 总带宽
|
||||
static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024);// 单连接带宽
|
||||
```
|
||||
|
||||
## 客户端架构
|
||||
|
||||
### 核心模块
|
||||
|
||||
```
|
||||
rustdesk/src/
|
||||
├── rendezvous_mediator.rs # Rendezvous 服务器通信
|
||||
├── client.rs # 控制端核心逻辑
|
||||
├── server/
|
||||
│ ├── mod.rs # 被控端服务
|
||||
│ ├── connection.rs # 连接处理
|
||||
│ ├── video_service.rs # 视频服务
|
||||
│ ├── audio_service.rs # 音频服务
|
||||
│ └── input_service.rs # 输入服务
|
||||
├── common.rs # 通用函数(加密、解密)
|
||||
└── platform/ # 平台特定代码
|
||||
```
|
||||
|
||||
### RendezvousMediator
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:44-50
|
||||
pub struct RendezvousMediator {
|
||||
addr: TargetAddr<'static>, // 服务器地址
|
||||
host: String, // 服务器主机名
|
||||
host_prefix: String, // 主机前缀
|
||||
keep_alive: i32, // 保活间隔
|
||||
}
|
||||
```
|
||||
|
||||
**两种连接模式:**
|
||||
|
||||
1. **UDP 模式** (默认):
|
||||
- 用于 Peer 注册和心跳
|
||||
- 更低延迟
|
||||
- 可能被某些防火墙阻止
|
||||
|
||||
2. **TCP 模式**:
|
||||
- 用于代理环境
|
||||
- WebSocket 模式
|
||||
- 更可靠
|
||||
|
||||
## 连接流程概述
|
||||
|
||||
### 被控端启动流程
|
||||
|
||||
```
|
||||
1. 生成设备 ID 和密钥对
|
||||
2. 连接 Rendezvous Server
|
||||
3. 发送 RegisterPeer 消息
|
||||
4. 如果需要,发送 RegisterPk 注册公钥
|
||||
5. 定期发送心跳保持在线状态
|
||||
6. 等待 PunchHole 或 RequestRelay 请求
|
||||
```
|
||||
|
||||
### 控制端连接流程
|
||||
|
||||
```
|
||||
1. 输入目标设备 ID
|
||||
2. 连接 Rendezvous Server
|
||||
3. 发送 PunchHoleRequest 消息
|
||||
4. 根据响应决定连接方式:
|
||||
a. 直连 (P2P): 使用 PunchHole 信息尝试打洞
|
||||
b. 局域网: 使用 LocalAddr 信息直连
|
||||
c. 中转: 通过 Relay Server 连接
|
||||
5. 建立安全加密通道
|
||||
6. 发送 LoginRequest 进行身份验证
|
||||
7. 开始远程控制会话
|
||||
```
|
||||
|
||||
## 数据流
|
||||
|
||||
### 视频流
|
||||
|
||||
```
|
||||
被控端 控制端
|
||||
│ │
|
||||
│ VideoFrame (H264/VP9/...) │
|
||||
├─────────────────────────────────►│
|
||||
│ │
|
||||
│ 加密 → 传输 → 解密 → 解码 → 显示 │
|
||||
```
|
||||
|
||||
### 输入流
|
||||
|
||||
```
|
||||
控制端 被控端
|
||||
│ │
|
||||
│ MouseEvent/KeyEvent │
|
||||
├─────────────────────────────────►│
|
||||
│ │
|
||||
│ 加密 → 传输 → 解密 → 模拟输入 │
|
||||
```
|
||||
|
||||
## 高可用设计
|
||||
|
||||
### 多服务器支持
|
||||
|
||||
- 客户端可配置多个 Rendezvous Server
|
||||
- 自动选择延迟最低的服务器
|
||||
- 连接失败时自动切换备用服务器
|
||||
|
||||
### Relay Server 选择
|
||||
|
||||
- 支持配置多个 Relay Server
|
||||
- 轮询算法分配负载
|
||||
- 定期检查 Relay Server 可用性
|
||||
|
||||
### 重连机制
|
||||
|
||||
```rust
|
||||
// 连接超时和重试参数
|
||||
const REG_INTERVAL: i64 = 12_000; // 注册间隔 12 秒
|
||||
const REG_TIMEOUT: i32 = 30_000; // 注册超时 30 秒
|
||||
const CONNECT_TIMEOUT: u64 = 18_000; // 连接超时 18 秒
|
||||
```
|
||||
@@ -1,438 +0,0 @@
|
||||
# Rendezvous 服务器协议
|
||||
|
||||
## 概述
|
||||
|
||||
Rendezvous Server(hbbs)是 RustDesk 的核心协调服务器,负责:
|
||||
- Peer ID 注册和发现
|
||||
- 公钥存储和分发
|
||||
- NAT 类型检测
|
||||
- P2P 连接协调(打洞辅助)
|
||||
- Relay Server 分配
|
||||
|
||||
## 协议消息定义
|
||||
|
||||
所有消息使用 Protocol Buffers 定义在 `protos/rendezvous.proto`:
|
||||
|
||||
```protobuf
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
KeyExchange key_exchange = 25;
|
||||
HealthCheck hc = 26;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 核心流程
|
||||
|
||||
### 1. Peer 注册流程
|
||||
|
||||
**客户端 → 服务器:RegisterPeer**
|
||||
|
||||
```protobuf
|
||||
message RegisterPeer {
|
||||
string id = 1; // Peer ID (如 "123456789")
|
||||
int32 serial = 2; // 配置序列号
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:318-333
|
||||
Some(rendezvous_message::Union::RegisterPeer(rp)) => {
|
||||
if !rp.id.is_empty() {
|
||||
log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr);
|
||||
self.update_addr(rp.id, addr, socket).await?;
|
||||
// 如果服务器配置更新,发送 ConfigUpdate
|
||||
if self.inner.serial > rp.serial {
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_configure_update(ConfigUpdate {
|
||||
serial: self.inner.serial,
|
||||
rendezvous_servers: (*self.rendezvous_servers).clone(),
|
||||
..Default::default()
|
||||
});
|
||||
socket.send(&msg_out, addr).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RegisterPeerResponse**
|
||||
|
||||
```protobuf
|
||||
message RegisterPeerResponse {
|
||||
bool request_pk = 2; // 是否需要注册公钥
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 公钥注册流程
|
||||
|
||||
当服务器检测到 Peer 的公钥为空或 IP 变化时,会请求注册公钥。
|
||||
|
||||
**客户端 → 服务器:RegisterPk**
|
||||
|
||||
```protobuf
|
||||
message RegisterPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes uuid = 2; // 设备 UUID
|
||||
bytes pk = 3; // Ed25519 公钥
|
||||
string old_id = 4; // 旧 ID(如果更换)
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:334-418
|
||||
Some(rendezvous_message::Union::RegisterPk(rk)) => {
|
||||
// 验证 UUID 和公钥
|
||||
if rk.uuid.is_empty() || rk.pk.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let id = rk.id;
|
||||
let ip = addr.ip().to_string();
|
||||
|
||||
// ID 长度检查
|
||||
if id.len() < 6 {
|
||||
return send_rk_res(socket, addr, UUID_MISMATCH).await;
|
||||
}
|
||||
|
||||
// IP 封锁检查
|
||||
if !self.check_ip_blocker(&ip, &id).await {
|
||||
return send_rk_res(socket, addr, TOO_FREQUENT).await;
|
||||
}
|
||||
|
||||
// UUID 匹配验证
|
||||
let peer = self.pm.get_or(&id).await;
|
||||
// ... UUID 验证逻辑 ...
|
||||
|
||||
// 更新数据库
|
||||
if changed {
|
||||
self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await;
|
||||
}
|
||||
|
||||
// 发送成功响应
|
||||
msg_out.set_register_pk_response(RegisterPkResponse {
|
||||
result: register_pk_response::Result::OK.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RegisterPkResponse**
|
||||
|
||||
```protobuf
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
int32 keep_alive = 2; // 心跳间隔
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Punch Hole 请求流程
|
||||
|
||||
当控制端要连接被控端时,首先发送 PunchHoleRequest。
|
||||
|
||||
**控制端 → 服务器:PunchHoleRequest**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleRequest {
|
||||
string id = 1; // 目标 Peer ID
|
||||
NatType nat_type = 2; // 请求方的 NAT 类型
|
||||
string licence_key = 3; // 许可证密钥
|
||||
ConnType conn_type = 4; // 连接类型
|
||||
string token = 5; // 认证令牌
|
||||
string version = 6; // 客户端版本
|
||||
}
|
||||
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1;
|
||||
SYMMETRIC = 2;
|
||||
}
|
||||
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
VIEW_CAMERA = 4;
|
||||
}
|
||||
```
|
||||
|
||||
**服务器处理逻辑:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:674-765
|
||||
async fn handle_punch_hole_request(...) {
|
||||
// 1. 验证许可证密钥
|
||||
if !key.is_empty() && ph.licence_key != key {
|
||||
return Ok((PunchHoleResponse { failure: LICENSE_MISMATCH }, None));
|
||||
}
|
||||
|
||||
// 2. 查找目标 Peer
|
||||
if let Some(peer) = self.pm.get(&id).await {
|
||||
let (elapsed, peer_addr) = peer.read().await;
|
||||
|
||||
// 3. 检查在线状态
|
||||
if elapsed >= REG_TIMEOUT {
|
||||
return Ok((PunchHoleResponse { failure: OFFLINE }, None));
|
||||
}
|
||||
|
||||
// 4. 判断是否同一局域网
|
||||
let same_intranet = (peer_is_lan && is_lan) ||
|
||||
(peer_addr.ip() == addr.ip());
|
||||
|
||||
if same_intranet {
|
||||
// 请求获取本地地址
|
||||
msg_out.set_fetch_local_addr(FetchLocalAddr {
|
||||
socket_addr: AddrMangle::encode(addr).into(),
|
||||
relay_server,
|
||||
});
|
||||
} else {
|
||||
// 发送 Punch Hole 请求给被控端
|
||||
msg_out.set_punch_hole(PunchHole {
|
||||
socket_addr: AddrMangle::encode(addr).into(),
|
||||
nat_type: ph.nat_type,
|
||||
relay_server,
|
||||
});
|
||||
}
|
||||
return Ok((msg_out, Some(peer_addr)));
|
||||
}
|
||||
|
||||
// Peer 不存在
|
||||
Ok((PunchHoleResponse { failure: ID_NOT_EXIST }, None))
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 被控端:PunchHole 或 FetchLocalAddr**
|
||||
|
||||
```protobuf
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1; // 控制端地址(编码)
|
||||
string relay_server = 2; // Relay 服务器地址
|
||||
NatType nat_type = 3; // 控制端 NAT 类型
|
||||
}
|
||||
|
||||
message FetchLocalAddr {
|
||||
bytes socket_addr = 1; // 控制端地址(编码)
|
||||
string relay_server = 2; // Relay 服务器地址
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 被控端响应流程
|
||||
|
||||
**被控端 → 服务器:PunchHoleSent 或 LocalAddr**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
string id = 2; // 被控端 ID
|
||||
string relay_server = 3; // Relay 服务器
|
||||
NatType nat_type = 4; // 被控端 NAT 类型
|
||||
string version = 5; // 客户端版本
|
||||
}
|
||||
|
||||
message LocalAddr {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
bytes local_addr = 2; // 被控端本地地址
|
||||
string relay_server = 3; // Relay 服务器
|
||||
string id = 4; // 被控端 ID
|
||||
string version = 5; // 客户端版本
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 控制端:PunchHoleResponse**
|
||||
|
||||
```protobuf
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1; // 被控端地址
|
||||
bytes pk = 2; // 被控端公钥(已签名)
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6; // 是否为局域网连接
|
||||
}
|
||||
string other_failure = 7;
|
||||
int32 feedback = 8;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Relay 请求流程
|
||||
|
||||
当 P2P 连接失败或 NAT 类型不支持打洞时,使用 Relay。
|
||||
|
||||
**客户端 → 服务器:RequestRelay**
|
||||
|
||||
```protobuf
|
||||
message RequestRelay {
|
||||
string id = 1; // 目标 Peer ID
|
||||
string uuid = 2; // 连接 UUID(用于配对)
|
||||
bytes socket_addr = 3; // 本端地址
|
||||
string relay_server = 4; // 指定的 Relay 服务器
|
||||
bool secure = 5; // 是否使用加密
|
||||
string licence_key = 6; // 许可证密钥
|
||||
ConnType conn_type = 7; // 连接类型
|
||||
string token = 8; // 认证令牌
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:RelayResponse**
|
||||
|
||||
```protobuf
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1; // 对端地址
|
||||
string uuid = 2; // 连接 UUID
|
||||
string relay_server = 3; // Relay 服务器地址
|
||||
oneof union {
|
||||
string id = 4; // 对端 ID
|
||||
bytes pk = 5; // 对端公钥
|
||||
}
|
||||
string refuse_reason = 6; // 拒绝原因
|
||||
string version = 7; // 版本
|
||||
int32 feedback = 9;
|
||||
}
|
||||
```
|
||||
|
||||
## NAT 类型检测
|
||||
|
||||
**客户端 → 服务器:TestNatRequest**
|
||||
|
||||
```protobuf
|
||||
message TestNatRequest {
|
||||
int32 serial = 1; // 配置序列号
|
||||
}
|
||||
```
|
||||
|
||||
**服务器 → 客户端:TestNatResponse**
|
||||
|
||||
```protobuf
|
||||
message TestNatResponse {
|
||||
int32 port = 1; // 观测到的源端口
|
||||
ConfigUpdate cu = 2; // 配置更新
|
||||
}
|
||||
```
|
||||
|
||||
NAT 检测原理:
|
||||
1. 客户端同时向主端口(21116)和 NAT 测试端口(21115)发送请求
|
||||
2. 比较两次响应中观测到的源端口
|
||||
3. 如果端口一致,则为 ASYMMETRIC NAT(适合打洞)
|
||||
4. 如果端口不一致,则为 SYMMETRIC NAT(需要 Relay)
|
||||
|
||||
## 地址编码
|
||||
|
||||
RustDesk 使用 `AddrMangle` 对 SocketAddr 进行编码:
|
||||
|
||||
```rust
|
||||
// 编码示例
|
||||
// IPv4: 4 bytes IP + 2 bytes port = 6 bytes
|
||||
// IPv6: 16 bytes IP + 2 bytes port = 18 bytes
|
||||
pub fn encode(addr: SocketAddr) -> Vec<u8>;
|
||||
pub fn decode(bytes: &[u8]) -> SocketAddr;
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 服务器签名
|
||||
|
||||
当服务器配置了私钥时,会对 Peer 的公钥进行签名:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:1160-1182
|
||||
async fn get_pk(&mut self, version: &str, id: String) -> Bytes {
|
||||
if version.is_empty() || self.inner.sk.is_none() {
|
||||
Bytes::new()
|
||||
} else {
|
||||
match self.pm.get(&id).await {
|
||||
Some(peer) => {
|
||||
let pk = peer.read().await.pk.clone();
|
||||
// 使用服务器私钥签名 IdPk
|
||||
sign::sign(
|
||||
&IdPk { id, pk, ..Default::default() }
|
||||
.write_to_bytes()
|
||||
.unwrap_or_default(),
|
||||
self.inner.sk.as_ref().unwrap(),
|
||||
).into()
|
||||
}
|
||||
_ => Bytes::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IP 封锁
|
||||
|
||||
服务器实现了 IP 封锁机制防止滥用:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:866-894
|
||||
async fn check_ip_blocker(&self, ip: &str, id: &str) -> bool {
|
||||
let mut lock = IP_BLOCKER.lock().await;
|
||||
if let Some(old) = lock.get_mut(ip) {
|
||||
// 每秒请求超过 30 次则封锁
|
||||
if counter.0 > 30 {
|
||||
return false;
|
||||
}
|
||||
// 每天超过 300 个不同 ID 则封锁
|
||||
if counter.0.len() > 300 {
|
||||
return !is_new;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
```
|
||||
|
||||
## 时序图
|
||||
|
||||
### 完整连接建立流程
|
||||
|
||||
```
|
||||
控制端 Rendezvous Server 被控端
|
||||
│ │ │
|
||||
│ PunchHoleRequest │ │
|
||||
├──────────────────────►│ │
|
||||
│ │ PunchHole │
|
||||
│ ├──────────────────────►│
|
||||
│ │ │
|
||||
│ │ PunchHoleSent │
|
||||
│ │◄──────────────────────┤
|
||||
│ PunchHoleResponse │ │
|
||||
│◄──────────────────────┤ │
|
||||
│ │ │
|
||||
│ ─────────── P2P Connection ──────────────────│
|
||||
│◄─────────────────────────────────────────────►│
|
||||
```
|
||||
@@ -1,318 +0,0 @@
|
||||
# Relay 服务器协议
|
||||
|
||||
## 概述
|
||||
|
||||
Relay Server(hbbr)是 RustDesk 的数据中转服务器,当 P2P 连接无法建立时(如双方都在 Symmetric NAT 后面),所有通信数据通过 Relay Server 转发。
|
||||
|
||||
## 服务器架构
|
||||
|
||||
### 监听端口
|
||||
|
||||
| 端口 | 协议 | 用途 |
|
||||
|------|------|------|
|
||||
| 21117 | TCP | 主要中转端口 |
|
||||
| 21119 | WebSocket | Web 客户端支持 |
|
||||
|
||||
### 核心配置
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:40-46
|
||||
static DOWNGRADE_THRESHOLD_100: AtomicUsize = AtomicUsize::new(66); // 0.66
|
||||
static DOWNGRADE_START_CHECK: AtomicUsize = AtomicUsize::new(1_800_000); // 30分钟 (ms)
|
||||
static LIMIT_SPEED: AtomicUsize = AtomicUsize::new(32 * 1024 * 1024); // 32 Mb/s
|
||||
static TOTAL_BANDWIDTH: AtomicUsize = AtomicUsize::new(1024 * 1024 * 1024);// 1024 Mb/s
|
||||
static SINGLE_BANDWIDTH: AtomicUsize = AtomicUsize::new(128 * 1024 * 1024);// 128 Mb/s
|
||||
const BLACKLIST_FILE: &str = "blacklist.txt";
|
||||
const BLOCKLIST_FILE: &str = "blocklist.txt";
|
||||
```
|
||||
|
||||
## 连接配对机制
|
||||
|
||||
### 配对原理
|
||||
|
||||
Relay Server 使用 UUID 来配对两个客户端的连接:
|
||||
|
||||
1. 第一个客户端连接并发送 `RequestRelay` 消息(包含 UUID)
|
||||
2. 服务器将该连接存储在等待队列中
|
||||
3. 第二个客户端使用相同的 UUID 连接
|
||||
4. 服务器将两个连接配对,开始转发数据
|
||||
|
||||
### 配对流程
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:425-462
|
||||
async fn make_pair_(stream: impl StreamTrait, addr: SocketAddr, key: &str, limiter: Limiter) {
|
||||
let mut stream = stream;
|
||||
if let Ok(Some(Ok(bytes))) = timeout(30_000, stream.recv()).await {
|
||||
if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
|
||||
if let Some(rendezvous_message::Union::RequestRelay(rf)) = msg_in.union {
|
||||
// 验证许可证密钥
|
||||
if !key.is_empty() && rf.licence_key != key {
|
||||
log::warn!("Relay authentication failed from {}", addr);
|
||||
return;
|
||||
}
|
||||
|
||||
if !rf.uuid.is_empty() {
|
||||
// 尝试查找配对
|
||||
let mut peer = PEERS.lock().await.remove(&rf.uuid);
|
||||
if let Some(peer) = peer.as_mut() {
|
||||
// 找到配对,开始中转
|
||||
log::info!("Relay request {} got paired", rf.uuid);
|
||||
relay(addr, &mut stream, peer, limiter).await;
|
||||
} else {
|
||||
// 没找到,存储等待配对
|
||||
log::info!("New relay request {} from {}", rf.uuid, addr);
|
||||
PEERS.lock().await.insert(rf.uuid.clone(), Box::new(stream));
|
||||
sleep(30.).await; // 等待 30 秒
|
||||
PEERS.lock().await.remove(&rf.uuid); // 超时移除
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据转发
|
||||
|
||||
### 转发逻辑
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:464-566
|
||||
async fn relay(
|
||||
addr: SocketAddr,
|
||||
stream: &mut impl StreamTrait,
|
||||
peer: &mut Box<dyn StreamTrait>,
|
||||
total_limiter: Limiter,
|
||||
) -> ResultType<()> {
|
||||
let limiter = <Limiter>::new(SINGLE_BANDWIDTH.load(Ordering::SeqCst) as f64);
|
||||
let blacklist_limiter = <Limiter>::new(LIMIT_SPEED.load(Ordering::SeqCst) as _);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// 从 peer 接收数据,发送给 stream
|
||||
res = peer.recv() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
// 带宽限制
|
||||
if blacked || downgrade {
|
||||
blacklist_limiter.consume(bytes.len() * 8).await;
|
||||
} else {
|
||||
limiter.consume(bytes.len() * 8).await;
|
||||
}
|
||||
total_limiter.consume(bytes.len() * 8).await;
|
||||
stream.send_raw(bytes.into()).await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
// 从 stream 接收数据,发送给 peer
|
||||
res = stream.recv() => {
|
||||
if let Some(Ok(bytes)) = res {
|
||||
// 带宽限制
|
||||
limiter.consume(bytes.len() * 8).await;
|
||||
total_limiter.consume(bytes.len() * 8).await;
|
||||
peer.send_raw(bytes.into()).await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = timer.tick() => {
|
||||
// 超时检测
|
||||
if last_recv_time.elapsed().as_secs() > 30 {
|
||||
bail!("Timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 降级检测
|
||||
if elapsed > DOWNGRADE_START_CHECK && total > elapsed * downgrade_threshold {
|
||||
downgrade = true;
|
||||
log::info!("Downgrade {}, exceed threshold", id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 原始模式
|
||||
|
||||
当两端都支持原始模式时,跳过 protobuf 解析以提高性能:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:440-444
|
||||
if !stream.is_ws() && !peer.is_ws() {
|
||||
peer.set_raw();
|
||||
stream.set_raw();
|
||||
log::info!("Both are raw");
|
||||
}
|
||||
```
|
||||
|
||||
## 带宽控制
|
||||
|
||||
### 多级限速
|
||||
|
||||
1. **总带宽限制**:整个服务器的总带宽
|
||||
2. **单连接限制**:每个中转连接的带宽
|
||||
3. **黑名单限速**:对黑名单 IP 的特殊限制
|
||||
|
||||
### 降级机制
|
||||
|
||||
当连接持续占用高带宽时,会触发降级:
|
||||
|
||||
```rust
|
||||
// 条件:
|
||||
// 1. 连接时间 > DOWNGRADE_START_CHECK (30分钟)
|
||||
// 2. 平均带宽 > SINGLE_BANDWIDTH * 0.66
|
||||
// 降级后使用 LIMIT_SPEED (32 Mb/s) 限速
|
||||
if elapsed > DOWNGRADE_START_CHECK.load(Ordering::SeqCst)
|
||||
&& !downgrade
|
||||
&& total > elapsed * downgrade_threshold
|
||||
{
|
||||
downgrade = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 安全控制
|
||||
|
||||
### 黑名单
|
||||
|
||||
用于限速特定 IP:
|
||||
|
||||
```
|
||||
# blacklist.txt
|
||||
192.168.1.100
|
||||
10.0.0.50
|
||||
```
|
||||
|
||||
### 封锁名单
|
||||
|
||||
用于完全拒绝特定 IP:
|
||||
|
||||
```
|
||||
# blocklist.txt
|
||||
1.2.3.4
|
||||
5.6.7.8
|
||||
```
|
||||
|
||||
### 运行时管理命令
|
||||
|
||||
通过本地 TCP 连接(仅限 localhost)发送命令:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/relay_server.rs:152-324
|
||||
match fds.next() {
|
||||
Some("h") => // 帮助
|
||||
Some("blacklist-add" | "ba") => // 添加黑名单
|
||||
Some("blacklist-remove" | "br") => // 移除黑名单
|
||||
Some("blacklist" | "b") => // 查看黑名单
|
||||
Some("blocklist-add" | "Ba") => // 添加封锁名单
|
||||
Some("blocklist-remove" | "Br") => // 移除封锁名单
|
||||
Some("blocklist" | "B") => // 查看封锁名单
|
||||
Some("downgrade-threshold" | "dt") => // 设置降级阈值
|
||||
Some("downgrade-start-check" | "t") => // 设置降级检测时间
|
||||
Some("limit-speed" | "ls") => // 设置限速
|
||||
Some("total-bandwidth" | "tb") => // 设置总带宽
|
||||
Some("single-bandwidth" | "sb") => // 设置单连接带宽
|
||||
Some("usage" | "u") => // 查看使用统计
|
||||
}
|
||||
```
|
||||
|
||||
## 协议消息
|
||||
|
||||
### RequestRelay
|
||||
|
||||
用于建立中转连接的请求消息:
|
||||
|
||||
```protobuf
|
||||
message RequestRelay {
|
||||
string id = 1; // 目标 Peer ID
|
||||
string uuid = 2; // 连接 UUID(配对用)
|
||||
bytes socket_addr = 3; // 本端地址
|
||||
string relay_server = 4; // Relay 服务器
|
||||
bool secure = 5; // 是否加密
|
||||
string licence_key = 6; // 许可证密钥
|
||||
ConnType conn_type = 7; // 连接类型
|
||||
string token = 8; // 认证令牌
|
||||
}
|
||||
```
|
||||
|
||||
## 时序图
|
||||
|
||||
### 中转连接建立
|
||||
|
||||
```
|
||||
客户端 A Relay Server 客户端 B
|
||||
│ │ │
|
||||
│ RequestRelay(uuid) │ │
|
||||
├─────────────────────────►│ │
|
||||
│ │ │
|
||||
│ │ (存储等待配对) │
|
||||
│ │ │
|
||||
│ │ RequestRelay(uuid) │
|
||||
│ │◄───────────────────────────┤
|
||||
│ │ │
|
||||
│ │ (配对成功) │
|
||||
│ │ │
|
||||
│ ◄────────── 数据转发 ─────────────────────────────────►│
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### 数据转发
|
||||
|
||||
```
|
||||
客户端 A Relay Server 客户端 B
|
||||
│ │ │
|
||||
│ ────[数据]───────► │ │
|
||||
│ │ ────[数据]───────► │
|
||||
│ │ │
|
||||
│ │ ◄───[数据]──────── │
|
||||
│ ◄───[数据]──────── │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 零拷贝
|
||||
|
||||
使用 `Bytes` 类型减少内存拷贝:
|
||||
|
||||
```rust
|
||||
async fn send_raw(&mut self, bytes: Bytes) -> ResultType<()>;
|
||||
```
|
||||
|
||||
### WebSocket 支持
|
||||
|
||||
支持 WebSocket 协议以穿越防火墙:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
impl StreamTrait for tokio_tungstenite::WebSocketStream<TcpStream> {
|
||||
async fn recv(&mut self) -> Option<Result<BytesMut, Error>> {
|
||||
if let Some(msg) = self.next().await {
|
||||
match msg {
|
||||
Ok(tungstenite::Message::Binary(bytes)) => {
|
||||
Some(Ok(bytes[..].into()))
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 监控指标
|
||||
|
||||
服务器跟踪以下指标:
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| elapsed | 连接持续时间 (ms) |
|
||||
| total | 总传输数据量 (bit) |
|
||||
| highest | 最高瞬时速率 (kb/s) |
|
||||
| speed | 当前速率 (kb/s) |
|
||||
|
||||
通过 `usage` 命令查看:
|
||||
|
||||
```
|
||||
192.168.1.100:12345: 3600s 1024.00MB 50000kb/s 45000kb/s 42000kb/s
|
||||
```
|
||||
@@ -1,424 +0,0 @@
|
||||
# P2P 连接流程
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 优先尝试建立 P2P 直连,只有在直连失败时才使用 Relay 中转。P2P 连接支持多种方式:
|
||||
- TCP 打洞
|
||||
- UDP 打洞(KCP)
|
||||
- 局域网直连
|
||||
- IPv6 直连
|
||||
|
||||
## 连接决策流程
|
||||
|
||||
```
|
||||
开始连接
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ 是否强制 Relay?│
|
||||
└──────┬───────┘
|
||||
是 │ 否
|
||||
┌─────────┴─────────┐
|
||||
▼ ▼
|
||||
使用 Relay 检查 NAT 类型
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
双方都是对称 NAT? 有一方是可穿透 NAT
|
||||
│ │
|
||||
是 │ │
|
||||
▼ ▼
|
||||
使用 Relay 尝试 P2P 连接
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
同一局域网? 不同网络
|
||||
│ │
|
||||
是 │ │
|
||||
▼ ▼
|
||||
局域网直连 尝试打洞
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
TCP 打洞成功? UDP 打洞成功?
|
||||
│ │
|
||||
是 │ 否 是 │ 否
|
||||
▼ │ ▼ │
|
||||
TCP P2P 连接 └───────────► KCP P2P 连接 │
|
||||
▼
|
||||
使用 Relay
|
||||
```
|
||||
|
||||
## 客户端连接入口
|
||||
|
||||
```rust
|
||||
// rustdesk/src/client.rs:188-230
|
||||
impl Client {
|
||||
pub async fn start(
|
||||
peer: &str,
|
||||
key: &str,
|
||||
token: &str,
|
||||
conn_type: ConnType,
|
||||
interface: impl Interface,
|
||||
) -> ResultType<...> {
|
||||
// 检查是否为 IP 直连
|
||||
if hbb_common::is_ip_str(peer) {
|
||||
return connect_tcp_local(check_port(peer, RELAY_PORT + 1), None, CONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
// 检查是否为域名:端口格式
|
||||
if hbb_common::is_domain_port_str(peer) {
|
||||
return connect_tcp_local(peer, None, CONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
// 通过 Rendezvous Server 连接
|
||||
let (rendezvous_server, servers, _) = crate::get_rendezvous_server(1_000).await;
|
||||
Self::_start_inner(peer, key, token, conn_type, interface, rendezvous_server, servers).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 被控端处理连接请求
|
||||
|
||||
### 处理 PunchHole 消息
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:554-619
|
||||
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&ph.socket_addr);
|
||||
let relay_server = self.get_relay_server(ph.relay_server);
|
||||
|
||||
// 判断是否需要 Relay
|
||||
if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC)
|
||||
|| Config::get_nat_type() == NatType::SYMMETRIC as i32
|
||||
|| relay
|
||||
{
|
||||
// 使用 Relay
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
return self.create_relay(ph.socket_addr, relay_server, uuid, server, true, true).await;
|
||||
}
|
||||
|
||||
// 尝试 UDP 打洞
|
||||
if ph.udp_port > 0 {
|
||||
peer_addr.set_port(ph.udp_port as u16);
|
||||
self.punch_udp_hole(peer_addr, server, msg_punch).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 尝试 TCP 打洞
|
||||
log::debug!("Punch tcp hole to {:?}", peer_addr);
|
||||
let socket = {
|
||||
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
// 关键步骤:尝试连接对方,让 NAT 建立映射
|
||||
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
|
||||
socket
|
||||
};
|
||||
|
||||
// 发送 PunchHoleSent 告知 Rendezvous Server
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(PunchHoleSent {
|
||||
socket_addr: ph.socket_addr,
|
||||
id: Config::get_id(),
|
||||
relay_server,
|
||||
nat_type: nat_type.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
});
|
||||
socket.send_raw(msg_out.write_to_bytes()?).await?;
|
||||
|
||||
// 接受控制端连接
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 处理 FetchLocalAddr(局域网连接)
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:481-552
|
||||
async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
let relay_server = self.get_relay_server(fla.relay_server.clone());
|
||||
|
||||
// 尝试局域网直连
|
||||
if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() {
|
||||
if let Err(err) = self.handle_intranet_(fla.clone(), server.clone(), relay_server.clone()).await {
|
||||
log::debug!("Failed to handle intranet: {:?}, will try relay", err);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 局域网直连失败,使用 Relay
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
self.create_relay(fla.socket_addr, relay_server, uuid, server, true, true).await
|
||||
}
|
||||
|
||||
async fn handle_intranet_(&self, fla: FetchLocalAddr, server: ServerPtr, relay_server: String) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&fla.socket_addr);
|
||||
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
|
||||
// 发送本地地址给 Rendezvous Server
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_local_addr(LocalAddr {
|
||||
id: Config::get_id(),
|
||||
socket_addr: AddrMangle::encode(peer_addr).into(),
|
||||
local_addr: AddrMangle::encode(local_addr).into(),
|
||||
relay_server,
|
||||
version: crate::VERSION.to_owned(),
|
||||
});
|
||||
socket.send_raw(msg_out.write_to_bytes()?).await?;
|
||||
|
||||
// 接受连接
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## UDP 打洞 (KCP)
|
||||
|
||||
### 打洞原理
|
||||
|
||||
UDP 打洞利用 NAT 的端口映射特性:
|
||||
|
||||
1. A 向 Rendezvous Server 注册,NAT 创建映射 `A_internal:port1 → A_external:port2`
|
||||
2. B 同样注册,创建映射 `B_internal:port3 → B_external:port4`
|
||||
3. A 向 B 的外部地址发送 UDP 包,A 的 NAT 创建到 B 的映射
|
||||
4. B 向 A 的外部地址发送 UDP 包,B 的 NAT 创建到 A 的映射
|
||||
5. 如果 NAT 不是 Symmetric 类型,双方的包可以到达对方
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:621-642
|
||||
async fn punch_udp_hole(
|
||||
&self,
|
||||
peer_addr: SocketAddr,
|
||||
server: ServerPtr,
|
||||
msg_punch: PunchHoleSent,
|
||||
) -> ResultType<()> {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(msg_punch);
|
||||
let (socket, addr) = new_direct_udp_for(&self.host).await?;
|
||||
let data = msg_out.write_to_bytes()?;
|
||||
|
||||
// 发送到 Rendezvous Server
|
||||
socket.send_to(&data, addr).await?;
|
||||
|
||||
// 多次尝试发送以增加成功率
|
||||
let socket_cloned = socket.clone();
|
||||
tokio::spawn(async move {
|
||||
for _ in 0..2 {
|
||||
let tm = (hbb_common::time_based_rand() % 20 + 10) as f32 / 1000.;
|
||||
hbb_common::sleep(tm).await;
|
||||
socket.send_to(&data, addr).await.ok();
|
||||
}
|
||||
});
|
||||
|
||||
// 等待对方连接
|
||||
udp_nat_listen(socket_cloned.clone(), peer_addr, peer_addr, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### KCP 协议
|
||||
|
||||
RustDesk 在 UDP 上使用 KCP 协议提供可靠传输:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:824-851
|
||||
async fn udp_nat_listen(
|
||||
socket: Arc<tokio::net::UdpSocket>,
|
||||
peer_addr: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
) -> ResultType<()> {
|
||||
socket.connect(peer_addr).await?;
|
||||
|
||||
// 执行 UDP 打洞
|
||||
let res = crate::punch_udp(socket.clone(), true).await?;
|
||||
|
||||
// 建立 KCP 流
|
||||
let stream = crate::kcp_stream::KcpStream::accept(
|
||||
socket,
|
||||
Duration::from_millis(CONNECT_TIMEOUT as _),
|
||||
res,
|
||||
).await?;
|
||||
|
||||
// 创建连接
|
||||
crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## TCP 打洞
|
||||
|
||||
### 原理
|
||||
|
||||
TCP 打洞比 UDP 更难,因为 TCP 需要三次握手。基本思路:
|
||||
|
||||
1. A 和 B 都尝试同时向对方发起连接
|
||||
2. 第一个 SYN 包会被对方的 NAT 丢弃(因为没有映射)
|
||||
3. 但这个 SYN 包会在 A 的 NAT 上创建映射
|
||||
4. 当 B 的 SYN 包到达 A 的 NAT 时,由于已有映射,会被转发给 A
|
||||
5. 连接建立
|
||||
|
||||
### 实现
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:604-617
|
||||
log::debug!("Punch tcp hole to {:?}", peer_addr);
|
||||
let mut socket = {
|
||||
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
// 关键:使用相同的本地地址尝试连接对方
|
||||
// 这会在 NAT 上创建映射,使对方的连接请求能够到达
|
||||
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
|
||||
socket
|
||||
};
|
||||
```
|
||||
|
||||
## Relay 连接
|
||||
|
||||
当 P2P 失败时,使用 Relay:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:434-479
|
||||
async fn create_relay(
|
||||
&self,
|
||||
socket_addr: Vec<u8>,
|
||||
relay_server: String,
|
||||
uuid: String,
|
||||
server: ServerPtr,
|
||||
secure: bool,
|
||||
initiate: bool,
|
||||
) -> ResultType<()> {
|
||||
let peer_addr = AddrMangle::decode(&socket_addr);
|
||||
log::info!(
|
||||
"create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}",
|
||||
peer_addr, relay_server, uuid, secure,
|
||||
);
|
||||
|
||||
// 连接 Rendezvous Server 发送 RelayResponse
|
||||
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let mut msg_out = Message::new();
|
||||
let mut rr = RelayResponse {
|
||||
socket_addr: socket_addr.into(),
|
||||
version: crate::VERSION.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
if initiate {
|
||||
rr.uuid = uuid.clone();
|
||||
rr.relay_server = relay_server.clone();
|
||||
rr.set_id(Config::get_id());
|
||||
}
|
||||
msg_out.set_relay_response(rr);
|
||||
socket.send(&msg_out).await?;
|
||||
|
||||
// 连接 Relay Server
|
||||
crate::create_relay_connection(
|
||||
server,
|
||||
relay_server,
|
||||
uuid,
|
||||
peer_addr,
|
||||
secure,
|
||||
is_ipv4(&self.addr),
|
||||
).await;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## IPv6 支持
|
||||
|
||||
RustDesk 优先尝试 IPv6 连接:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:808-822
|
||||
async fn start_ipv6(
|
||||
peer_addr_v6: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
) -> bytes::Bytes {
|
||||
crate::test_ipv6().await;
|
||||
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server).await);
|
||||
});
|
||||
return local_addr_v6;
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
```
|
||||
|
||||
## 连接状态机
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────┐ ┌────┴────┐
|
||||
│ 等待连接 │──────PunchHoleRequest──────►│正在连接 │
|
||||
└───────────┘ └────┬────┘
|
||||
│
|
||||
┌──────────────────────────────┼──────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ P2P TCP │ │ P2P UDP/KCP │ │ Relay │
|
||||
│ 连接中 │ │ 连接中 │ │ 连接中 │
|
||||
└─────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
成功 │ 失败 成功 │ 失败 成功 │ 失败
|
||||
│ │ │ │ │ │
|
||||
▼ │ ▼ │ ▼ │
|
||||
┌──────────┐│ ┌──────────┐│ ┌──────────┐│
|
||||
│已连接 ││ │已连接 ││ │已连接 ││
|
||||
│(直连) ││ │(UDP) ││ │(中转) ││
|
||||
└──────────┘│ └──────────┘│ └──────────┘│
|
||||
│ │ │
|
||||
└──────────────►尝试 Relay◄───┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 直接连接模式
|
||||
|
||||
用户可以配置允许直接 TCP 连接(不经过 Rendezvous Server):
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:727-792
|
||||
async fn direct_server(server: ServerPtr) {
|
||||
let mut listener = None;
|
||||
let mut port = get_direct_port(); // 默认 21118
|
||||
|
||||
loop {
|
||||
let disabled = !option2bool(OPTION_DIRECT_SERVER, &Config::get_option(OPTION_DIRECT_SERVER));
|
||||
|
||||
if !disabled && listener.is_none() {
|
||||
match hbb_common::tcp::listen_any(port as _).await {
|
||||
Ok(l) => {
|
||||
listener = Some(l);
|
||||
log::info!("Direct server listening on: {:?}", l.local_addr());
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to start direct server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(l) = listener.as_mut() {
|
||||
if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await {
|
||||
stream.set_nodelay(true).ok();
|
||||
log::info!("direct access from {}", addr);
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
crate::server::create_tcp_connection(server, stream, addr, false).await
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,574 +0,0 @@
|
||||
# 消息格式定义
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 使用 Protocol Buffers (protobuf) 定义所有网络消息格式。主要有两个 proto 文件:
|
||||
|
||||
- `rendezvous.proto` - Rendezvous/Relay 服务器通信消息
|
||||
- `message.proto` - 客户端之间通信消息
|
||||
|
||||
## Rendezvous 消息 (rendezvous.proto)
|
||||
|
||||
### 顶层消息
|
||||
|
||||
```protobuf
|
||||
message RendezvousMessage {
|
||||
oneof union {
|
||||
RegisterPeer register_peer = 6;
|
||||
RegisterPeerResponse register_peer_response = 7;
|
||||
PunchHoleRequest punch_hole_request = 8;
|
||||
PunchHole punch_hole = 9;
|
||||
PunchHoleSent punch_hole_sent = 10;
|
||||
PunchHoleResponse punch_hole_response = 11;
|
||||
FetchLocalAddr fetch_local_addr = 12;
|
||||
LocalAddr local_addr = 13;
|
||||
ConfigUpdate configure_update = 14;
|
||||
RegisterPk register_pk = 15;
|
||||
RegisterPkResponse register_pk_response = 16;
|
||||
SoftwareUpdate software_update = 17;
|
||||
RequestRelay request_relay = 18;
|
||||
RelayResponse relay_response = 19;
|
||||
TestNatRequest test_nat_request = 20;
|
||||
TestNatResponse test_nat_response = 21;
|
||||
PeerDiscovery peer_discovery = 22;
|
||||
OnlineRequest online_request = 23;
|
||||
OnlineResponse online_response = 24;
|
||||
KeyExchange key_exchange = 25;
|
||||
HealthCheck hc = 26;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册相关
|
||||
|
||||
```protobuf
|
||||
// Peer 注册
|
||||
message RegisterPeer {
|
||||
string id = 1; // Peer ID
|
||||
int32 serial = 2; // 配置序列号
|
||||
}
|
||||
|
||||
message RegisterPeerResponse {
|
||||
bool request_pk = 2; // 是否需要注册公钥
|
||||
}
|
||||
|
||||
// 公钥注册
|
||||
message RegisterPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes uuid = 2; // 设备 UUID
|
||||
bytes pk = 3; // Ed25519 公钥
|
||||
string old_id = 4; // 旧 ID
|
||||
}
|
||||
|
||||
message RegisterPkResponse {
|
||||
enum Result {
|
||||
OK = 0;
|
||||
UUID_MISMATCH = 2;
|
||||
ID_EXISTS = 3;
|
||||
TOO_FREQUENT = 4;
|
||||
INVALID_ID_FORMAT = 5;
|
||||
NOT_SUPPORT = 6;
|
||||
SERVER_ERROR = 7;
|
||||
}
|
||||
Result result = 1;
|
||||
int32 keep_alive = 2;
|
||||
}
|
||||
```
|
||||
|
||||
### 连接协调相关
|
||||
|
||||
```protobuf
|
||||
// 连接类型
|
||||
enum ConnType {
|
||||
DEFAULT_CONN = 0;
|
||||
FILE_TRANSFER = 1;
|
||||
PORT_FORWARD = 2;
|
||||
RDP = 3;
|
||||
VIEW_CAMERA = 4;
|
||||
}
|
||||
|
||||
// NAT 类型
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0;
|
||||
ASYMMETRIC = 1; // 可打洞
|
||||
SYMMETRIC = 2; // 需要中转
|
||||
}
|
||||
|
||||
// Punch Hole 请求
|
||||
message PunchHoleRequest {
|
||||
string id = 1; // 目标 Peer ID
|
||||
NatType nat_type = 2;
|
||||
string licence_key = 3;
|
||||
ConnType conn_type = 4;
|
||||
string token = 5;
|
||||
string version = 6;
|
||||
}
|
||||
|
||||
// Punch Hole 响应
|
||||
message PunchHoleResponse {
|
||||
bytes socket_addr = 1; // 目标地址
|
||||
bytes pk = 2; // 公钥(已签名)
|
||||
enum Failure {
|
||||
ID_NOT_EXIST = 0;
|
||||
OFFLINE = 2;
|
||||
LICENSE_MISMATCH = 3;
|
||||
LICENSE_OVERUSE = 4;
|
||||
}
|
||||
Failure failure = 3;
|
||||
string relay_server = 4;
|
||||
oneof union {
|
||||
NatType nat_type = 5;
|
||||
bool is_local = 6;
|
||||
}
|
||||
string other_failure = 7;
|
||||
int32 feedback = 8;
|
||||
}
|
||||
|
||||
// 服务器转发给被控端
|
||||
message PunchHole {
|
||||
bytes socket_addr = 1; // 控制端地址
|
||||
string relay_server = 2;
|
||||
NatType nat_type = 3;
|
||||
}
|
||||
|
||||
// 被控端发送给服务器
|
||||
message PunchHoleSent {
|
||||
bytes socket_addr = 1;
|
||||
string id = 2;
|
||||
string relay_server = 3;
|
||||
NatType nat_type = 4;
|
||||
string version = 5;
|
||||
}
|
||||
```
|
||||
|
||||
### Relay 相关
|
||||
|
||||
```protobuf
|
||||
// Relay 请求
|
||||
message RequestRelay {
|
||||
string id = 1;
|
||||
string uuid = 2; // 配对 UUID
|
||||
bytes socket_addr = 3;
|
||||
string relay_server = 4;
|
||||
bool secure = 5;
|
||||
string licence_key = 6;
|
||||
ConnType conn_type = 7;
|
||||
string token = 8;
|
||||
}
|
||||
|
||||
// Relay 响应
|
||||
message RelayResponse {
|
||||
bytes socket_addr = 1;
|
||||
string uuid = 2;
|
||||
string relay_server = 3;
|
||||
oneof union {
|
||||
string id = 4;
|
||||
bytes pk = 5;
|
||||
}
|
||||
string refuse_reason = 6;
|
||||
string version = 7;
|
||||
int32 feedback = 9;
|
||||
}
|
||||
```
|
||||
|
||||
## 会话消息 (message.proto)
|
||||
|
||||
### 顶层消息
|
||||
|
||||
```protobuf
|
||||
message Message {
|
||||
oneof union {
|
||||
SignedId signed_id = 3;
|
||||
PublicKey public_key = 4;
|
||||
TestDelay test_delay = 5;
|
||||
VideoFrame video_frame = 6;
|
||||
LoginRequest login_request = 7;
|
||||
LoginResponse login_response = 8;
|
||||
Hash hash = 9;
|
||||
MouseEvent mouse_event = 10;
|
||||
AudioFrame audio_frame = 11;
|
||||
CursorData cursor_data = 12;
|
||||
CursorPosition cursor_position = 13;
|
||||
uint64 cursor_id = 14;
|
||||
KeyEvent key_event = 15;
|
||||
Clipboard clipboard = 16;
|
||||
FileAction file_action = 17;
|
||||
FileResponse file_response = 18;
|
||||
Misc misc = 19;
|
||||
Cliprdr cliprdr = 20;
|
||||
MessageBox message_box = 21;
|
||||
SwitchSidesResponse switch_sides_response = 22;
|
||||
VoiceCallRequest voice_call_request = 23;
|
||||
VoiceCallResponse voice_call_response = 24;
|
||||
PeerInfo peer_info = 25;
|
||||
PointerDeviceEvent pointer_device_event = 26;
|
||||
Auth2FA auth_2fa = 27;
|
||||
MultiClipboards multi_clipboards = 28;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 认证相关
|
||||
|
||||
```protobuf
|
||||
// ID 和公钥
|
||||
message IdPk {
|
||||
string id = 1;
|
||||
bytes pk = 2;
|
||||
}
|
||||
|
||||
// 密钥交换
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1; // X25519 公钥
|
||||
bytes symmetric_value = 2; // 加密的对称密钥
|
||||
}
|
||||
|
||||
// 签名的 ID
|
||||
message SignedId {
|
||||
bytes id = 1; // 签名的 IdPk
|
||||
}
|
||||
|
||||
// 密码哈希挑战
|
||||
message Hash {
|
||||
string salt = 1;
|
||||
string challenge = 2;
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
message LoginRequest {
|
||||
string username = 1;
|
||||
bytes password = 2; // 加密的密码
|
||||
string my_id = 4;
|
||||
string my_name = 5;
|
||||
OptionMessage option = 6;
|
||||
oneof union {
|
||||
FileTransfer file_transfer = 7;
|
||||
PortForward port_forward = 8;
|
||||
ViewCamera view_camera = 15;
|
||||
}
|
||||
bool video_ack_required = 9;
|
||||
uint64 session_id = 10;
|
||||
string version = 11;
|
||||
OSLogin os_login = 12;
|
||||
string my_platform = 13;
|
||||
bytes hwid = 14;
|
||||
}
|
||||
|
||||
// 登录响应
|
||||
message LoginResponse {
|
||||
oneof union {
|
||||
string error = 1;
|
||||
PeerInfo peer_info = 2;
|
||||
}
|
||||
bool enable_trusted_devices = 3;
|
||||
}
|
||||
|
||||
// 2FA 认证
|
||||
message Auth2FA {
|
||||
string code = 1;
|
||||
bytes hwid = 2;
|
||||
}
|
||||
```
|
||||
|
||||
### 视频相关
|
||||
|
||||
```protobuf
|
||||
// 编码后的视频帧
|
||||
message EncodedVideoFrame {
|
||||
bytes data = 1;
|
||||
bool key = 2; // 是否关键帧
|
||||
int64 pts = 3; // 时间戳
|
||||
}
|
||||
|
||||
message EncodedVideoFrames {
|
||||
repeated EncodedVideoFrame frames = 1;
|
||||
}
|
||||
|
||||
// 视频帧
|
||||
message VideoFrame {
|
||||
oneof union {
|
||||
EncodedVideoFrames vp9s = 6;
|
||||
RGB rgb = 7;
|
||||
YUV yuv = 8;
|
||||
EncodedVideoFrames h264s = 10;
|
||||
EncodedVideoFrames h265s = 11;
|
||||
EncodedVideoFrames vp8s = 12;
|
||||
EncodedVideoFrames av1s = 13;
|
||||
}
|
||||
int32 display = 14; // 显示器索引
|
||||
}
|
||||
|
||||
// 显示信息
|
||||
message DisplayInfo {
|
||||
sint32 x = 1;
|
||||
sint32 y = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
string name = 5;
|
||||
bool online = 6;
|
||||
bool cursor_embedded = 7;
|
||||
Resolution original_resolution = 8;
|
||||
double scale = 9;
|
||||
}
|
||||
```
|
||||
|
||||
### 输入相关
|
||||
|
||||
```protobuf
|
||||
// 鼠标事件
|
||||
message MouseEvent {
|
||||
int32 mask = 1; // 按钮掩码
|
||||
sint32 x = 2;
|
||||
sint32 y = 3;
|
||||
repeated ControlKey modifiers = 4;
|
||||
}
|
||||
|
||||
// 键盘事件
|
||||
message KeyEvent {
|
||||
bool down = 1; // 按下/释放
|
||||
bool press = 2; // 单击
|
||||
oneof union {
|
||||
ControlKey control_key = 3;
|
||||
uint32 chr = 4; // 字符码
|
||||
uint32 unicode = 5; // Unicode
|
||||
string seq = 6; // 字符序列
|
||||
uint32 win2win_hotkey = 7;
|
||||
}
|
||||
repeated ControlKey modifiers = 8;
|
||||
KeyboardMode mode = 9;
|
||||
}
|
||||
|
||||
// 键盘模式
|
||||
enum KeyboardMode {
|
||||
Legacy = 0;
|
||||
Map = 1;
|
||||
Translate = 2;
|
||||
Auto = 3;
|
||||
}
|
||||
|
||||
// 控制键枚举(部分)
|
||||
enum ControlKey {
|
||||
Unknown = 0;
|
||||
Alt = 1;
|
||||
Backspace = 2;
|
||||
CapsLock = 3;
|
||||
Control = 4;
|
||||
Delete = 5;
|
||||
// ... 更多按键
|
||||
CtrlAltDel = 100;
|
||||
LockScreen = 101;
|
||||
}
|
||||
```
|
||||
|
||||
### 音频相关
|
||||
|
||||
```protobuf
|
||||
// 音频格式
|
||||
message AudioFormat {
|
||||
uint32 sample_rate = 1;
|
||||
uint32 channels = 2;
|
||||
}
|
||||
|
||||
// 音频帧
|
||||
message AudioFrame {
|
||||
bytes data = 1; // Opus 编码数据
|
||||
}
|
||||
```
|
||||
|
||||
### 剪贴板相关
|
||||
|
||||
```protobuf
|
||||
// 剪贴板格式
|
||||
enum ClipboardFormat {
|
||||
Text = 0;
|
||||
Rtf = 1;
|
||||
Html = 2;
|
||||
ImageRgba = 21;
|
||||
ImagePng = 22;
|
||||
ImageSvg = 23;
|
||||
Special = 31;
|
||||
}
|
||||
|
||||
// 剪贴板内容
|
||||
message Clipboard {
|
||||
bool compress = 1;
|
||||
bytes content = 2;
|
||||
int32 width = 3;
|
||||
int32 height = 4;
|
||||
ClipboardFormat format = 5;
|
||||
string special_name = 6;
|
||||
}
|
||||
|
||||
message MultiClipboards {
|
||||
repeated Clipboard clipboards = 1;
|
||||
}
|
||||
```
|
||||
|
||||
### 文件传输相关
|
||||
|
||||
```protobuf
|
||||
// 文件操作
|
||||
message FileAction {
|
||||
oneof union {
|
||||
ReadDir read_dir = 1;
|
||||
FileTransferSendRequest send = 2;
|
||||
FileTransferReceiveRequest receive = 3;
|
||||
FileDirCreate create = 4;
|
||||
FileRemoveDir remove_dir = 5;
|
||||
FileRemoveFile remove_file = 6;
|
||||
ReadAllFiles all_files = 7;
|
||||
FileTransferCancel cancel = 8;
|
||||
FileTransferSendConfirmRequest send_confirm = 9;
|
||||
FileRename rename = 10;
|
||||
ReadEmptyDirs read_empty_dirs = 11;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件响应
|
||||
message FileResponse {
|
||||
oneof union {
|
||||
FileDirectory dir = 1;
|
||||
FileTransferBlock block = 2;
|
||||
FileTransferError error = 3;
|
||||
FileTransferDone done = 4;
|
||||
FileTransferDigest digest = 5;
|
||||
ReadEmptyDirsResponse empty_dirs = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件传输块
|
||||
message FileTransferBlock {
|
||||
int32 id = 1;
|
||||
sint32 file_num = 2;
|
||||
bytes data = 3;
|
||||
bool compressed = 4;
|
||||
uint32 blk_id = 5;
|
||||
}
|
||||
|
||||
// 文件条目
|
||||
message FileEntry {
|
||||
FileType entry_type = 1;
|
||||
string name = 2;
|
||||
bool is_hidden = 3;
|
||||
uint64 size = 4;
|
||||
uint64 modified_time = 5;
|
||||
}
|
||||
```
|
||||
|
||||
### 杂项消息
|
||||
|
||||
```protobuf
|
||||
message Misc {
|
||||
oneof union {
|
||||
ChatMessage chat_message = 4;
|
||||
SwitchDisplay switch_display = 5;
|
||||
PermissionInfo permission_info = 6;
|
||||
OptionMessage option = 7;
|
||||
AudioFormat audio_format = 8;
|
||||
string close_reason = 9;
|
||||
bool refresh_video = 10;
|
||||
bool video_received = 12;
|
||||
BackNotification back_notification = 13;
|
||||
bool restart_remote_device = 14;
|
||||
// ... 更多选项
|
||||
}
|
||||
}
|
||||
|
||||
// Peer 信息
|
||||
message PeerInfo {
|
||||
string username = 1;
|
||||
string hostname = 2;
|
||||
string platform = 3;
|
||||
repeated DisplayInfo displays = 4;
|
||||
int32 current_display = 5;
|
||||
bool sas_enabled = 6;
|
||||
string version = 7;
|
||||
Features features = 9;
|
||||
SupportedEncoding encoding = 10;
|
||||
SupportedResolutions resolutions = 11;
|
||||
string platform_additions = 12;
|
||||
WindowsSessions windows_sessions = 13;
|
||||
}
|
||||
|
||||
// 选项消息
|
||||
message OptionMessage {
|
||||
enum BoolOption {
|
||||
NotSet = 0;
|
||||
No = 1;
|
||||
Yes = 2;
|
||||
}
|
||||
ImageQuality image_quality = 1;
|
||||
BoolOption lock_after_session_end = 2;
|
||||
BoolOption show_remote_cursor = 3;
|
||||
BoolOption privacy_mode = 4;
|
||||
BoolOption block_input = 5;
|
||||
int32 custom_image_quality = 6;
|
||||
BoolOption disable_audio = 7;
|
||||
BoolOption disable_clipboard = 8;
|
||||
BoolOption enable_file_transfer = 9;
|
||||
SupportedDecoding supported_decoding = 10;
|
||||
int32 custom_fps = 11;
|
||||
// ... 更多选项
|
||||
}
|
||||
```
|
||||
|
||||
## 消息编码
|
||||
|
||||
### 长度前缀
|
||||
|
||||
TCP 传输时使用长度前缀编码:
|
||||
|
||||
```rust
|
||||
// hbb_common/src/bytes_codec.rs
|
||||
pub struct BytesCodec {
|
||||
state: DecodeState,
|
||||
raw: bool,
|
||||
}
|
||||
|
||||
impl Decoder for BytesCodec {
|
||||
type Item = BytesMut;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<BytesMut>, Self::Error> {
|
||||
if self.raw {
|
||||
// 原始模式:直接返回数据
|
||||
if buf.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(buf.split()))
|
||||
}
|
||||
} else {
|
||||
// 标准模式:4 字节长度前缀 + 数据
|
||||
match self.state {
|
||||
DecodeState::Head => {
|
||||
if buf.len() < 4 {
|
||||
return Ok(None);
|
||||
}
|
||||
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
buf.advance(4);
|
||||
self.state = DecodeState::Data(len);
|
||||
self.decode(buf)
|
||||
}
|
||||
DecodeState::Data(len) => {
|
||||
if buf.len() < len {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = buf.split_to(len);
|
||||
self.state = DecodeState::Head;
|
||||
Ok(Some(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 加密模式
|
||||
|
||||
当启用加密时,消息结构为:
|
||||
|
||||
```
|
||||
┌─────────────┬─────────────┬─────────────────────────┐
|
||||
│ Length(4) │ Nonce(8) │ Encrypted Data(N) │
|
||||
└─────────────┴─────────────┴─────────────────────────┘
|
||||
```
|
||||
@@ -1,342 +0,0 @@
|
||||
# 加密机制
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 使用 libsodium (sodiumoxide) 库实现端到端加密,主要包含:
|
||||
|
||||
- **Ed25519**: 用于身份签名和验证
|
||||
- **X25519**: 用于密钥交换
|
||||
- **ChaCha20-Poly1305**: 用于对称加密
|
||||
|
||||
## 密钥类型
|
||||
|
||||
### 1. 身份密钥对 (Ed25519)
|
||||
|
||||
用于 Peer 身份认证和签名:
|
||||
|
||||
```rust
|
||||
// 生成密钥对
|
||||
use sodiumoxide::crypto::sign;
|
||||
let (pk, sk) = sign::gen_keypair();
|
||||
// pk: sign::PublicKey (32 bytes)
|
||||
// sk: sign::SecretKey (64 bytes)
|
||||
```
|
||||
|
||||
### 2. 服务器签名密钥
|
||||
|
||||
Rendezvous Server 可以配置签名密钥,用于签名 Peer 公钥:
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:1185-1210
|
||||
fn get_server_sk(key: &str) -> (String, Option<sign::SecretKey>) {
|
||||
let mut out_sk = None;
|
||||
let mut key = key.to_owned();
|
||||
|
||||
// 如果是 base64 编码的私钥
|
||||
if let Ok(sk) = base64::decode(&key) {
|
||||
if sk.len() == sign::SECRETKEYBYTES {
|
||||
log::info!("The key is a crypto private key");
|
||||
key = base64::encode(&sk[(sign::SECRETKEYBYTES / 2)..]); // 公钥部分
|
||||
let mut tmp = [0u8; sign::SECRETKEYBYTES];
|
||||
tmp[..].copy_from_slice(&sk);
|
||||
out_sk = Some(sign::SecretKey(tmp));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是占位符,生成新密钥对
|
||||
if key.is_empty() || key == "-" || key == "_" {
|
||||
let (pk, sk) = crate::common::gen_sk(0);
|
||||
out_sk = sk;
|
||||
if !key.is_empty() {
|
||||
key = pk;
|
||||
}
|
||||
}
|
||||
|
||||
if !key.is_empty() {
|
||||
log::info!("Key: {}", key);
|
||||
}
|
||||
(key, out_sk)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 会话密钥 (X25519 + ChaCha20)
|
||||
|
||||
用于客户端之间的加密通信:
|
||||
|
||||
```rust
|
||||
// hbb_common/src/tcp.rs:27-28
|
||||
#[derive(Clone)]
|
||||
pub struct Encrypt(pub Key, pub u64, pub u64);
|
||||
// Key: secretbox::Key (32 bytes)
|
||||
// u64: 发送计数器
|
||||
// u64: 接收计数器
|
||||
```
|
||||
|
||||
## 密钥交换流程
|
||||
|
||||
### 1. 身份验证
|
||||
|
||||
客户端首先交换签名的身份:
|
||||
|
||||
```protobuf
|
||||
message IdPk {
|
||||
string id = 1; // Peer ID
|
||||
bytes pk = 2; // Ed25519 公钥
|
||||
}
|
||||
|
||||
message SignedId {
|
||||
bytes id = 1; // 签名的 IdPk (by server or self)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. X25519 密钥交换
|
||||
|
||||
使用 X25519 ECDH 生成共享密钥:
|
||||
|
||||
```rust
|
||||
// 生成临时密钥对
|
||||
use sodiumoxide::crypto::box_;
|
||||
let (our_pk, our_sk) = box_::gen_keypair();
|
||||
|
||||
// 计算共享密钥
|
||||
let shared_secret = box_::curve25519xsalsa20poly1305::scalarmult(&our_sk, &their_pk);
|
||||
|
||||
// 派生对称密钥
|
||||
let symmetric_key = secretbox::Key::from_slice(&shared_secret[..32]).unwrap();
|
||||
```
|
||||
|
||||
### 3. 对称密钥消息
|
||||
|
||||
```protobuf
|
||||
message PublicKey {
|
||||
bytes asymmetric_value = 1; // X25519 公钥
|
||||
bytes symmetric_value = 2; // 加密的对称密钥(用于额外安全)
|
||||
}
|
||||
```
|
||||
|
||||
## 会话加密
|
||||
|
||||
### 加密实现
|
||||
|
||||
```rust
|
||||
// hbb_common/src/tcp.rs
|
||||
impl Encrypt {
|
||||
pub fn new(key: Key) -> Self {
|
||||
Self(key, 0, 0) // 初始化计数器为 0
|
||||
}
|
||||
|
||||
// 加密
|
||||
pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
|
||||
self.1 += 1; // 递增发送计数器
|
||||
let nonce = self.get_nonce(self.1);
|
||||
let encrypted = secretbox::seal(data, &nonce, &self.0);
|
||||
|
||||
// 格式: nonce (8 bytes) + encrypted data
|
||||
let mut result = Vec::with_capacity(8 + encrypted.len());
|
||||
result.extend_from_slice(&self.1.to_le_bytes());
|
||||
result.extend_from_slice(&encrypted);
|
||||
result
|
||||
}
|
||||
|
||||
// 解密
|
||||
pub fn dec(&mut self, data: &mut BytesMut) -> io::Result<()> {
|
||||
if data.len() < 8 + secretbox::MACBYTES {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "too short"));
|
||||
}
|
||||
|
||||
// 提取 nonce
|
||||
let counter = u64::from_le_bytes(data[..8].try_into().unwrap());
|
||||
|
||||
// 防重放攻击检查
|
||||
if counter <= self.2 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "replay attack"));
|
||||
}
|
||||
self.2 = counter;
|
||||
|
||||
let nonce = self.get_nonce(counter);
|
||||
let plaintext = secretbox::open(&data[8..], &nonce, &self.0)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decrypt failed"))?;
|
||||
|
||||
data.clear();
|
||||
data.extend_from_slice(&plaintext);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_nonce(&self, counter: u64) -> Nonce {
|
||||
let mut nonce = [0u8; 24];
|
||||
nonce[..8].copy_from_slice(&counter.to_le_bytes());
|
||||
Nonce(nonce)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消息格式
|
||||
|
||||
加密后的消息结构:
|
||||
|
||||
```
|
||||
┌──────────────────┬─────────────────────────────────────────┐
|
||||
│ Counter (8B) │ Encrypted Data + MAC (N+16 bytes) │
|
||||
└──────────────────┴─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 密码验证
|
||||
|
||||
### 挑战-响应机制
|
||||
|
||||
被控端生成随机盐和挑战,控制端计算哈希响应:
|
||||
|
||||
```protobuf
|
||||
message Hash {
|
||||
string salt = 1; // 随机盐
|
||||
string challenge = 2; // 随机挑战
|
||||
}
|
||||
```
|
||||
|
||||
### 密码处理
|
||||
|
||||
```rust
|
||||
// 客户端计算密码哈希
|
||||
fn get_password_hash(password: &str, salt: &str) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
hasher.update(salt.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
// 发送加密的密码(使用对称密钥加密)
|
||||
fn encrypt_password(password_hash: &[u8], symmetric_key: &Key) -> Vec<u8> {
|
||||
secretbox::seal(password_hash, &nonce, symmetric_key)
|
||||
}
|
||||
```
|
||||
|
||||
## 服务器公钥验证
|
||||
|
||||
### 签名验证
|
||||
|
||||
如果 Rendezvous Server 配置了密钥,会签名 Peer 公钥:
|
||||
|
||||
```rust
|
||||
// 服务器签名 IdPk
|
||||
let signed_id_pk = sign::sign(
|
||||
&IdPk { id, pk, ..Default::default() }
|
||||
.write_to_bytes()?,
|
||||
&server_sk,
|
||||
);
|
||||
|
||||
// 客户端验证
|
||||
fn verify_server_signature(signed_pk: &[u8], server_pk: &sign::PublicKey) -> Option<IdPk> {
|
||||
if let Ok(verified) = sign::verify(signed_pk, server_pk) {
|
||||
return IdPk::parse_from_bytes(&verified).ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
### 客户端获取服务器公钥
|
||||
|
||||
```rust
|
||||
pub async fn get_rs_pk(id: &str) -> ResultType<(String, sign::PublicKey)> {
|
||||
// 从配置或 Rendezvous Server 获取公钥
|
||||
let key = Config::get_option("key");
|
||||
if !key.is_empty() {
|
||||
if let Ok(pk) = base64::decode(&key) {
|
||||
if pk.len() == sign::PUBLICKEYBYTES {
|
||||
return Ok((key, sign::PublicKey::from_slice(&pk).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ... 从服务器获取
|
||||
}
|
||||
```
|
||||
|
||||
## TCP 连接加密
|
||||
|
||||
### 安全 TCP 握手
|
||||
|
||||
```rust
|
||||
// rustdesk/src/common.rs
|
||||
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
|
||||
// 1. 生成临时 X25519 密钥对
|
||||
let (our_pk, our_sk) = box_::gen_keypair();
|
||||
|
||||
// 2. 发送我们的公钥
|
||||
let mut msg = Message::new();
|
||||
msg.set_public_key(PublicKey {
|
||||
asymmetric_value: our_pk.0.to_vec().into(),
|
||||
..Default::default()
|
||||
});
|
||||
conn.send(&msg).await?;
|
||||
|
||||
// 3. 接收对方公钥
|
||||
let msg = conn.next_timeout(CONNECT_TIMEOUT).await?
|
||||
.ok_or_else(|| anyhow!("timeout"))?;
|
||||
let their_pk = msg.get_public_key();
|
||||
|
||||
// 4. 计算共享密钥
|
||||
let shared = box_::curve25519xsalsa20poly1305::scalarmult(
|
||||
&our_sk,
|
||||
&box_::PublicKey::from_slice(&their_pk.asymmetric_value)?,
|
||||
);
|
||||
|
||||
// 5. 设置加密
|
||||
conn.set_key(secretbox::Key::from_slice(&shared[..32]).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 安全特性
|
||||
|
||||
### 1. 前向保密
|
||||
|
||||
每个会话使用临时密钥对,即使长期密钥泄露,历史会话仍然安全。
|
||||
|
||||
### 2. 重放攻击防护
|
||||
|
||||
使用递增计数器作为 nonce 的一部分,拒绝旧的或重复的消息。
|
||||
|
||||
### 3. 中间人攻击防护
|
||||
|
||||
- 服务器签名 Peer 公钥
|
||||
- 可配置服务器公钥验证
|
||||
|
||||
### 4. 密码暴力破解防护
|
||||
|
||||
- 使用盐和多次哈希
|
||||
- 服务器端限流
|
||||
|
||||
## 加密算法参数
|
||||
|
||||
| 算法 | 密钥大小 | Nonce 大小 | MAC 大小 |
|
||||
|------|----------|------------|----------|
|
||||
| Ed25519 | 64 bytes (private), 32 bytes (public) | N/A | 64 bytes |
|
||||
| X25519 | 32 bytes | N/A | N/A |
|
||||
| ChaCha20-Poly1305 | 32 bytes | 24 bytes | 16 bytes |
|
||||
|
||||
## 密钥生命周期
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 长期密钥 (Ed25519) │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ 设备首次启动时生成 │ │
|
||||
│ │ 存储在配置文件中 │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 会话密钥 (X25519) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 每次连接时生成 │───►│ 用于密钥协商 │ │
|
||||
│ │ 临时密钥对 │ │ 派生对称密钥 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 对称密钥 (ChaCha20-Poly1305) │ │
|
||||
│ │ 用于会话中的所有消息加密 │ │
|
||||
│ │ 会话结束时销毁 │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -1,410 +0,0 @@
|
||||
# NAT 穿透技术
|
||||
|
||||
## 概述
|
||||
|
||||
RustDesk 实现了多种 NAT 穿透技术,以在不同网络环境下建立 P2P 连接:
|
||||
|
||||
- NAT 类型检测
|
||||
- UDP 打洞
|
||||
- TCP 打洞
|
||||
- Relay 中转(作为后备)
|
||||
|
||||
## NAT 类型
|
||||
|
||||
### 分类
|
||||
|
||||
```protobuf
|
||||
enum NatType {
|
||||
UNKNOWN_NAT = 0; // 未知
|
||||
ASYMMETRIC = 1; // 非对称 NAT (Cone NAT) - 可打洞
|
||||
SYMMETRIC = 2; // 对称 NAT - 通常需要 Relay
|
||||
}
|
||||
```
|
||||
|
||||
### NAT 类型说明
|
||||
|
||||
| 类型 | 描述 | 可打洞 |
|
||||
|------|------|--------|
|
||||
| Full Cone | 外部端口固定,任何外部主机可访问 | ✅ 最容易 |
|
||||
| Restricted Cone | 外部端口固定,仅允许曾发送过数据的 IP | ✅ 容易 |
|
||||
| Port Restricted Cone | 外部端口固定,仅允许曾发送过数据的 IP:Port | ✅ 可能 |
|
||||
| Symmetric | 每个目标地址使用不同外部端口 | ❌ 困难 |
|
||||
|
||||
## NAT 类型检测
|
||||
|
||||
### 检测原理
|
||||
|
||||
RustDesk 使用双端口检测法:
|
||||
|
||||
1. 客户端向 Rendezvous Server 的主端口 (21116) 发送 TestNatRequest
|
||||
2. 同时向 NAT 测试端口 (21115) 发送 TestNatRequest
|
||||
3. 比较两次响应中观测到的源端口
|
||||
|
||||
```
|
||||
客户端 Rendezvous Server
|
||||
│ │
|
||||
│ TestNatRequest ────────►│ Port 21116
|
||||
│ │
|
||||
│ TestNatRequest ────────►│ Port 21115
|
||||
│ │
|
||||
│◄──────── TestNatResponse │ (包含观测到的源端口)
|
||||
│ │
|
||||
│ │
|
||||
│ 比较两次源端口 │
|
||||
│ 相同 → ASYMMETRIC │
|
||||
│ 不同 → SYMMETRIC │
|
||||
```
|
||||
|
||||
### 实现代码
|
||||
|
||||
**客户端发送检测请求:**
|
||||
|
||||
```rust
|
||||
// rustdesk/src/lib.rs
|
||||
pub fn test_nat_type() {
|
||||
tokio::spawn(async move {
|
||||
let rendezvous_server = Config::get_rendezvous_servers().first().cloned();
|
||||
if let Some(host) = rendezvous_server {
|
||||
// 连接主端口
|
||||
let host = check_port(&host, RENDEZVOUS_PORT);
|
||||
|
||||
// 连接 NAT 测试端口
|
||||
let host2 = crate::increase_port(&host, -1);
|
||||
|
||||
// 发送测试请求
|
||||
let mut msg = RendezvousMessage::new();
|
||||
msg.set_test_nat_request(TestNatRequest {
|
||||
serial: Config::get_serial(),
|
||||
});
|
||||
|
||||
// 收集两次响应的端口
|
||||
let port1 = send_and_get_port(&host, &msg).await;
|
||||
let port2 = send_and_get_port(&host2, &msg).await;
|
||||
|
||||
// 判断 NAT 类型
|
||||
let nat_type = if port1 == port2 {
|
||||
NatType::ASYMMETRIC // 可打洞
|
||||
} else {
|
||||
NatType::SYMMETRIC // 需要 Relay
|
||||
};
|
||||
|
||||
Config::set_nat_type(nat_type as i32);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**服务器响应:**
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:1080-1087
|
||||
Some(rendezvous_message::Union::TestNatRequest(_)) => {
|
||||
let mut msg_out = RendezvousMessage::new();
|
||||
msg_out.set_test_nat_response(TestNatResponse {
|
||||
port: addr.port() as _, // 返回观测到的源端口
|
||||
..Default::default()
|
||||
});
|
||||
stream.send(&msg_out).await.ok();
|
||||
}
|
||||
```
|
||||
|
||||
## UDP 打洞
|
||||
|
||||
### 原理
|
||||
|
||||
UDP 打洞利用 NAT 的端口映射机制:
|
||||
|
||||
```
|
||||
A (内网) B (内网)
|
||||
│ │
|
||||
│ ──► NAT_A ──► Internet ──► NAT_B ──► (丢弃) │
|
||||
│ │
|
||||
│ │
|
||||
│ (NAT_A 创建了映射 A:port → A_ext:port_a) │
|
||||
│ │
|
||||
│ │
|
||||
│ (丢弃) ◄── NAT_A ◄── Internet ◄── NAT_B ◄── │
|
||||
│ │
|
||||
│ │
|
||||
│ (NAT_B 创建了映射 B:port → B_ext:port_b) │
|
||||
│ │
|
||||
│ ──► NAT_A ──► Internet ──► NAT_B ──► │
|
||||
│ (NAT_A 的映射存在,包被转发) │
|
||||
│ │
|
||||
│ ◄── NAT_A ◄── Internet ◄── NAT_B ◄── │
|
||||
│ (NAT_B 的映射存在,包被转发) │
|
||||
│ │
|
||||
│ ◄───────── 双向通信建立 ──────────► │
|
||||
```
|
||||
|
||||
### 实现
|
||||
|
||||
**被控端打洞:**
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:621-642
|
||||
async fn punch_udp_hole(
|
||||
&self,
|
||||
peer_addr: SocketAddr,
|
||||
server: ServerPtr,
|
||||
msg_punch: PunchHoleSent,
|
||||
) -> ResultType<()> {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(msg_punch);
|
||||
|
||||
// 创建 UDP socket
|
||||
let (socket, addr) = new_direct_udp_for(&self.host).await?;
|
||||
let data = msg_out.write_to_bytes()?;
|
||||
|
||||
// 发送到 Rendezvous Server(会转发给控制端)
|
||||
socket.send_to(&data, addr).await?;
|
||||
|
||||
// 多次发送以增加成功率
|
||||
let socket_cloned = socket.clone();
|
||||
tokio::spawn(async move {
|
||||
for _ in 0..2 {
|
||||
let tm = (hbb_common::time_based_rand() % 20 + 10) as f32 / 1000.;
|
||||
hbb_common::sleep(tm).await;
|
||||
socket.send_to(&data, addr).await.ok();
|
||||
}
|
||||
});
|
||||
|
||||
// 等待对方连接
|
||||
udp_nat_listen(socket_cloned, peer_addr, peer_addr, server).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**UDP 监听和 KCP 建立:**
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:824-851
|
||||
async fn udp_nat_listen(
|
||||
socket: Arc<tokio::net::UdpSocket>,
|
||||
peer_addr: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
) -> ResultType<()> {
|
||||
// 连接到对方地址
|
||||
socket.connect(peer_addr).await?;
|
||||
|
||||
// 执行 UDP 打洞
|
||||
let res = crate::punch_udp(socket.clone(), true).await?;
|
||||
|
||||
// 建立 KCP 可靠传输层
|
||||
let stream = crate::kcp_stream::KcpStream::accept(
|
||||
socket,
|
||||
Duration::from_millis(CONNECT_TIMEOUT as _),
|
||||
res,
|
||||
).await?;
|
||||
|
||||
// 创建连接
|
||||
crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### KCP 协议
|
||||
|
||||
RustDesk 在 UDP 上使用 KCP 提供可靠传输,KCP 特点:
|
||||
|
||||
- 更激进的重传策略
|
||||
- 更低的延迟
|
||||
- 可配置的可靠性级别
|
||||
|
||||
## TCP 打洞
|
||||
|
||||
### 原理
|
||||
|
||||
TCP 打洞比 UDP 困难,因为 TCP 需要三次握手。技巧是让双方同时发起连接:
|
||||
|
||||
```
|
||||
A NAT_A NAT_B B
|
||||
│ │ │ │
|
||||
│ ─── SYN ───────────────►│─────────│────► (丢弃,无映射) │
|
||||
│ │ │ │
|
||||
│ (NAT_A 创建到 B 的映射) │ │ │
|
||||
│ │ │ │
|
||||
│ (丢弃,无映射) ◄─────────│─────────│◄─── SYN ───────────── │
|
||||
│ │ │ │
|
||||
│ │ │ (NAT_B 创建到 A 的映射) │
|
||||
│ │ │ │
|
||||
│ ─── SYN ───────────────►│─────────│────► SYN ───────────► │
|
||||
│ │ │ (映射存在,转发成功) │
|
||||
│ │ │ │
|
||||
│ ◄─── SYN+ACK ──────────│─────────│◄─── SYN+ACK ───────── │
|
||||
│ │ │ │
|
||||
│ ─── ACK ───────────────►│─────────│────► ACK ───────────► │
|
||||
│ │ │ │
|
||||
│ ◄─────────── 连接建立 ─────────────────────────────────────►│
|
||||
```
|
||||
|
||||
### 实现
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:604-617
|
||||
log::debug!("Punch tcp hole to {:?}", peer_addr);
|
||||
let mut socket = {
|
||||
// 1. 先连接 Rendezvous Server 获取本地地址
|
||||
let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
|
||||
let local_addr = socket.local_addr();
|
||||
|
||||
// 2. 用相同的本地地址尝试连接对方
|
||||
// 这会在 NAT 上创建映射
|
||||
// 虽然连接会失败,但映射已建立
|
||||
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
|
||||
|
||||
socket
|
||||
};
|
||||
|
||||
// 3. 发送 PunchHoleSent 通知服务器
|
||||
// 服务器会转发给控制端
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_punch_hole_sent(msg_punch);
|
||||
socket.send_raw(msg_out.write_to_bytes()?).await?;
|
||||
|
||||
// 4. 等待控制端连接
|
||||
// 由于已有映射,控制端的连接可以成功
|
||||
crate::accept_connection(server.clone(), socket, peer_addr, true).await;
|
||||
```
|
||||
|
||||
## 局域网直连
|
||||
|
||||
### 检测同一局域网
|
||||
|
||||
```rust
|
||||
// rustdesk-server/src/rendezvous_server.rs:721-728
|
||||
let same_intranet: bool = !ws
|
||||
&& (peer_is_lan && is_lan || {
|
||||
match (peer_addr, addr) {
|
||||
(SocketAddr::V4(a), SocketAddr::V4(b)) => a.ip() == b.ip(),
|
||||
(SocketAddr::V6(a), SocketAddr::V6(b)) => a.ip() == b.ip(),
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 局域网连接流程
|
||||
|
||||
```
|
||||
控制端 Rendezvous Server 被控端
|
||||
│ │ │
|
||||
│ PunchHoleRequest ────►│ │
|
||||
│ │ │
|
||||
│ │ (检测到同一局域网) │
|
||||
│ │ │
|
||||
│ │ FetchLocalAddr ──────►│
|
||||
│ │ │
|
||||
│ │◄────── LocalAddr ────────│
|
||||
│ │ (包含被控端内网地址) │
|
||||
│ │ │
|
||||
│◄─ PunchHoleResponse ──│ │
|
||||
│ (is_local=true) │ │
|
||||
│ (socket_addr=内网地址)│ │
|
||||
│ │ │
|
||||
│ ─────────── 直接连接内网地址 ────────────────────►│
|
||||
```
|
||||
|
||||
## IPv6 支持
|
||||
|
||||
IPv6 通常不需要 NAT 穿透,但 RustDesk 仍支持 IPv6 打洞以处理有状态防火墙:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/rendezvous_mediator.rs:808-822
|
||||
async fn start_ipv6(
|
||||
peer_addr_v6: SocketAddr,
|
||||
peer_addr_v4: SocketAddr,
|
||||
server: ServerPtr,
|
||||
) -> bytes::Bytes {
|
||||
crate::test_ipv6().await;
|
||||
if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await {
|
||||
let server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
allow_err!(udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server).await);
|
||||
});
|
||||
return local_addr_v6;
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
```
|
||||
|
||||
## 连接策略决策树
|
||||
|
||||
```
|
||||
开始连接
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ NAT 类型检测 │
|
||||
└───────┬───────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
ASYMMETRIC UNKNOWN SYMMETRIC
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
┌──────────┐ ┌──────────┐ │
|
||||
│ 尝试 UDP │ │ 尝试 TCP │ │
|
||||
│ 打洞 │ │ 打洞 │ │
|
||||
└────┬─────┘ └────┬─────┘ │
|
||||
│ │ │
|
||||
成功 │ 失败 成功 │ 失败 │
|
||||
▼ │ ▼ │ │
|
||||
┌────────┐│ ┌────────┐│ │
|
||||
│UDP P2P ││ │TCP P2P ││ │
|
||||
└────────┘│ └────────┘│ │
|
||||
│ │ │
|
||||
└───────┬───────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌───────────────┐ │
|
||||
│ 使用 Relay │◄─────────┘
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 多路径尝试
|
||||
|
||||
RustDesk 同时尝试多种连接方式,选择最快成功的:
|
||||
|
||||
```rust
|
||||
// rustdesk/src/client.rs:342-364
|
||||
let mut connect_futures = Vec::new();
|
||||
|
||||
// 同时尝试 UDP 和 TCP
|
||||
if udp.0.is_some() {
|
||||
connect_futures.push(Self::_start_inner(..., udp).boxed());
|
||||
}
|
||||
connect_futures.push(Self::_start_inner(..., (None, None)).boxed());
|
||||
|
||||
// 使用 select_ok 选择第一个成功的
|
||||
match select_ok(connect_futures).await {
|
||||
Ok(conn) => Ok(conn),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
```
|
||||
|
||||
### 超时控制
|
||||
|
||||
```rust
|
||||
const CONNECT_TIMEOUT: u64 = 18_000; // 18 秒
|
||||
const REG_TIMEOUT: i32 = 30_000; // 30 秒
|
||||
|
||||
// 连接超时处理
|
||||
if let Ok(Ok((stream, addr))) = timeout(CONNECT_TIMEOUT, socket.accept()).await {
|
||||
// 连接成功
|
||||
} else {
|
||||
// 超时,尝试其他方式
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题和解决方案
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| 双 Symmetric NAT | 两端都是对称 NAT | 使用 Relay |
|
||||
| 防火墙阻止 UDP | 企业防火墙 | 使用 TCP 或 WebSocket |
|
||||
| 端口预测失败 | NAT 端口分配不规律 | 多次尝试或使用 Relay |
|
||||
| IPv6 不通 | ISP 或防火墙问题 | 回退到 IPv4 |
|
||||
@@ -1,401 +0,0 @@
|
||||
# RustDesk 协议 vs One-KVM 实现对比分析
|
||||
|
||||
本文档对比分析 RustDesk 原始协议与 One-KVM 的实现差异。
|
||||
|
||||
## 1. 概述
|
||||
|
||||
One-KVM 作为 IP-KVM 解决方案,只实现了 RustDesk 协议的**被控端(Controlled)** 功能,不实现控制端(Controller)功能。这是设计决策,因为 KVM 设备只需要接收远程控制,不需要控制其他设备。
|
||||
|
||||
### 架构差异
|
||||
|
||||
| 方面 | RustDesk 原版 | One-KVM |
|
||||
|------|---------------|---------|
|
||||
| 角色 | 双向(控制端+被控端) | 单向(仅被控端) |
|
||||
| 连接方式 | P2P + Relay | 仅 Relay (TCP) |
|
||||
| NAT 穿透 | UDP/TCP 打洞 + TURN | 不支持 |
|
||||
| 传输协议 | UDP/TCP | 仅 TCP |
|
||||
|
||||
## 2. 已实现功能
|
||||
|
||||
### 2.1 Rendezvous 协议 (hbbs 通信)
|
||||
|
||||
| 消息类型 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| RegisterPeer | ✅ 已实现 | 注册设备到服务器 |
|
||||
| RegisterPeerResponse | ✅ 已实现 | 处理注册响应 |
|
||||
| RegisterPk | ✅ 已实现 | 注册公钥 |
|
||||
| RegisterPkResponse | ✅ 已实现 | 处理公钥注册响应 |
|
||||
| PunchHoleSent | ✅ 已实现 | 响应打洞请求 |
|
||||
| FetchLocalAddr | ✅ 已实现 | 获取本地地址 |
|
||||
| LocalAddr | ✅ 已实现 | 返回本地地址 |
|
||||
| RequestRelay | ✅ 已实现 | 请求中继连接 |
|
||||
| RelayResponse | ✅ 已实现 | 处理中继响应 |
|
||||
| ConfigUpdate | ✅ 已实现 | 接收配置更新 |
|
||||
|
||||
**实现文件**: `src/rustdesk/rendezvous.rs` (~829 行)
|
||||
|
||||
```rust
|
||||
// 核心结构
|
||||
pub struct RendezvousMediator {
|
||||
config: RustDeskConfig,
|
||||
key_pair: KeyPair,
|
||||
signing_key: SigningKeyPair,
|
||||
socket: UdpSocket,
|
||||
status: Arc<RwLock<RendezvousStatus>>,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 连接协议 (客户端连接)
|
||||
|
||||
| 消息类型 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| SignedId | ✅ 已实现 | 签名身份验证 |
|
||||
| PublicKey | ✅ 已实现 | 公钥交换 |
|
||||
| Hash | ✅ 已实现 | 哈希挑战响应 |
|
||||
| LoginRequest | ✅ 已实现 | 登录认证 |
|
||||
| LoginResponse | ✅ 已实现 | 登录响应 |
|
||||
| TestDelay | ✅ 已实现 | 延迟测试 |
|
||||
| VideoFrame | ✅ 已实现 | 视频帧发送 |
|
||||
| AudioFrame | ✅ 已实现 | 音频帧发送 |
|
||||
| CursorData | ✅ 已实现 | 光标图像 |
|
||||
| CursorPosition | ✅ 已实现 | 光标位置 |
|
||||
| MouseEvent | ✅ 已实现 | 鼠标事件接收 |
|
||||
| KeyEvent | ✅ 已实现 | 键盘事件接收 |
|
||||
|
||||
**实现文件**: `src/rustdesk/connection.rs` (~1349 行)
|
||||
|
||||
```rust
|
||||
// 连接状态机
|
||||
pub enum ConnectionState {
|
||||
WaitingForSignedId,
|
||||
WaitingForPublicKey,
|
||||
WaitingForHash,
|
||||
WaitingForLogin,
|
||||
Authenticated,
|
||||
Streaming,
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 加密模块
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| Curve25519 密钥对 | ✅ 已实现 | 用于加密 |
|
||||
| Ed25519 签名密钥对 | ✅ 已实现 | 用于签名 |
|
||||
| Ed25519 → Curve25519 转换 | ✅ 已实现 | 密钥派生 |
|
||||
| XSalsa20-Poly1305 | ✅ 已实现 | 会话加密 (secretbox) |
|
||||
| 密码哈希 | ✅ 已实现 | 单重/双重 SHA256 |
|
||||
| 会话密钥协商 | ✅ 已实现 | 对称密钥派生 |
|
||||
|
||||
**实现文件**: `src/rustdesk/crypto.rs` (~468 行)
|
||||
|
||||
```rust
|
||||
// 密钥对结构
|
||||
pub struct KeyPair {
|
||||
secret_key: [u8; 32], // Curve25519 私钥
|
||||
public_key: [u8; 32], // Curve25519 公钥
|
||||
}
|
||||
|
||||
pub struct SigningKeyPair {
|
||||
secret_key: [u8; 64], // Ed25519 私钥
|
||||
public_key: [u8; 32], // Ed25519 公钥
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 视频/音频流
|
||||
|
||||
| 编码格式 | 实现状态 | 备注 |
|
||||
|----------|----------|------|
|
||||
| H.264 | ✅ 已实现 | 主要格式 |
|
||||
| H.265/HEVC | ✅ 已实现 | 高效编码 |
|
||||
| VP8 | ✅ 已实现 | WebRTC 兼容 |
|
||||
| VP9 | ✅ 已实现 | 高质量 |
|
||||
| AV1 | ✅ 已实现 | 新一代编码 |
|
||||
| Opus 音频 | ✅ 已实现 | 低延迟音频 |
|
||||
|
||||
**实现文件**: `src/rustdesk/frame_adapters.rs` (~316 行)
|
||||
|
||||
### 2.5 HID 事件
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| 鼠标移动 | ✅ 已实现 | 绝对/相对坐标 |
|
||||
| 鼠标按键 | ✅ 已实现 | 左/中/右键 |
|
||||
| 鼠标滚轮 | ✅ 已实现 | 垂直滚动 |
|
||||
| 键盘按键 | ✅ 已实现 | 按下/释放 |
|
||||
| 控制键映射 | ✅ 已实现 | ControlKey → USB HID |
|
||||
| X11 键码映射 | ✅ 已实现 | X11 → USB HID |
|
||||
|
||||
**实现文件**: `src/rustdesk/hid_adapter.rs` (~386 行)
|
||||
|
||||
### 2.6 协议帧编码
|
||||
|
||||
| 功能 | 实现状态 | 备注 |
|
||||
|------|----------|------|
|
||||
| BytesCodec | ✅ 已实现 | 变长帧编码 |
|
||||
| 1-4 字节头 | ✅ 已实现 | 根据长度自动选择 |
|
||||
| 最大 1GB 消息 | ✅ 已实现 | 与原版一致 |
|
||||
|
||||
**实现文件**: `src/rustdesk/bytes_codec.rs` (~253 行)
|
||||
|
||||
## 3. 未实现功能
|
||||
|
||||
### 3.1 NAT 穿透相关
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| UDP 打洞 | One-KVM 仅使用 TCP 中继 |
|
||||
| TCP 打洞 | 同上 |
|
||||
| STUN/TURN | 不需要 NAT 类型检测 |
|
||||
| TestNat | 同上 |
|
||||
| P2P 直连 | 设计简化,仅支持中继 |
|
||||
|
||||
### 3.2 客户端发起功能
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| PunchHole (发起) | KVM 只接收连接 |
|
||||
| RelayRequest | 同上 |
|
||||
| ConnectPeer | 同上 |
|
||||
| OnlineRequest | 不需要查询其他设备 |
|
||||
|
||||
### 3.3 文件传输
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| FileTransfer | 超出 KVM 功能范围 |
|
||||
| FileAction | 同上 |
|
||||
| FileResponse | 同上 |
|
||||
| FileTransferBlock | 同上 |
|
||||
|
||||
### 3.4 高级功能
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| 剪贴板同步 | 超出 KVM 功能范围 |
|
||||
| 多显示器切换 | One-KVM 使用单一视频源 |
|
||||
| 虚拟显示器 | 不适用 |
|
||||
| 端口转发 | 超出 KVM 功能范围 |
|
||||
| 语音通话 | 不需要 |
|
||||
| RDP 输入 | 不需要 |
|
||||
| 插件系统 | 不支持 |
|
||||
| 软件更新 | One-KVM 有自己的更新机制 |
|
||||
|
||||
### 3.5 权限协商
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| Option 消息 | One-KVM 假设完全控制权限 |
|
||||
| 权限请求 | 同上 |
|
||||
| PermissionInfo | 同上 |
|
||||
|
||||
## 4. 实现差异
|
||||
|
||||
### 4.1 连接模式
|
||||
|
||||
**RustDesk 原版:**
|
||||
```
|
||||
客户端 ──UDP打洞──> 被控端 (P2P 优先)
|
||||
└──Relay──> 被控端 (回退)
|
||||
```
|
||||
|
||||
**One-KVM:**
|
||||
```
|
||||
RustDesk客户端 ──TCP中继──> hbbr服务器 ──> One-KVM设备
|
||||
```
|
||||
|
||||
One-KVM 只支持 TCP 中继连接,不支持 P2P 直连。这简化了实现,但可能增加延迟。
|
||||
|
||||
### 4.2 会话加密
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 支持 ChaCha20-Poly1305 (流式)
|
||||
- 支持 XSalsa20-Poly1305 (secretbox)
|
||||
- 动态协商加密方式
|
||||
|
||||
**One-KVM:**
|
||||
- 仅支持 XSalsa20-Poly1305 (secretbox)
|
||||
- 使用序列号作为 nonce
|
||||
|
||||
```rust
|
||||
// One-KVM 的加密实现
|
||||
fn encrypt_message(&mut self, plaintext: &[u8]) -> Vec<u8> {
|
||||
let nonce = make_nonce(&self.send_nonce);
|
||||
self.send_nonce = self.send_nonce.wrapping_add(1);
|
||||
secretbox::seal(plaintext, &nonce, &self.session_key)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 视频流方向
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 双向视频流(可控制和被控制)
|
||||
- 远程桌面捕获
|
||||
|
||||
**One-KVM:**
|
||||
- 单向视频流(仅发送)
|
||||
- 从 V4L2 设备捕获
|
||||
- 集成到 One-KVM 的 VideoStreamManager
|
||||
|
||||
```rust
|
||||
// One-KVM 视频流集成
|
||||
pub async fn start_video_stream(&self, state: &AppState) {
|
||||
let stream_manager = &state.video_stream_manager;
|
||||
// 从 One-KVM 的视频管理器获取帧
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 HID 事件处理
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 转发到远程系统的输入子系统
|
||||
- 使用 enigo 或 uinput
|
||||
|
||||
**One-KVM:**
|
||||
- 转发到 USB OTG/HID 设备
|
||||
- 控制物理 KVM 目标机器
|
||||
|
||||
```rust
|
||||
// One-KVM HID 适配
|
||||
pub fn convert_mouse_event(event: &RustDeskMouseEvent) -> Option<OneKvmMouseEvent> {
|
||||
// 转换 RustDesk 鼠标事件到 One-KVM HID 事件
|
||||
}
|
||||
|
||||
pub fn convert_key_event(event: &RustDeskKeyEvent) -> Option<OneKvmKeyEvent> {
|
||||
// 转换 RustDesk 键盘事件到 One-KVM HID 事件
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 配置管理
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 使用 TOML/JSON 配置文件
|
||||
- 硬编码默认值
|
||||
|
||||
**One-KVM:**
|
||||
- 集成到 SQLite 配置系统
|
||||
- Web UI 管理
|
||||
- 使用 typeshare 生成 TypeScript 类型
|
||||
|
||||
```rust
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RustDeskConfig {
|
||||
pub enabled: bool,
|
||||
pub rendezvous_server: String,
|
||||
pub device_id: String,
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 设备 ID 生成
|
||||
|
||||
**RustDesk 原版:**
|
||||
- 基于 MAC 地址和硬件信息
|
||||
- 固定便携式 ID
|
||||
|
||||
**One-KVM:**
|
||||
- 随机生成 9 位数字
|
||||
- 存储在配置中
|
||||
|
||||
```rust
|
||||
pub fn generate_device_id() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let id: u32 = rng.gen_range(100_000_000..999_999_999);
|
||||
id.to_string()
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 协议兼容性
|
||||
|
||||
### 5.1 完全兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| Rendezvous 注册 | 可与官方 hbbs 服务器通信 |
|
||||
| 中继连接 | 可通过官方 hbbr 服务器中继 |
|
||||
| 加密握手 | 与 RustDesk 客户端兼容 |
|
||||
| 视频编码 | 支持所有主流编码格式 |
|
||||
| HID 事件 | 接收标准 RustDesk 输入事件 |
|
||||
|
||||
### 5.2 部分兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 密码认证 | 仅支持设备密码,不支持一次性密码 |
|
||||
| 会话加密 | 仅 XSalsa20-Poly1305 |
|
||||
|
||||
### 5.3 不兼容
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| P2P 连接 | 客户端必须通过中继连接 |
|
||||
| 文件传输 | 不支持 |
|
||||
| 剪贴板 | 不支持 |
|
||||
|
||||
## 6. 代码结构对比
|
||||
|
||||
### RustDesk 原版结构
|
||||
|
||||
```
|
||||
rustdesk/
|
||||
├── libs/hbb_common/ # 公共库
|
||||
│ ├── protos/ # Protobuf 定义
|
||||
│ └── src/
|
||||
├── src/
|
||||
│ ├── server/ # 被控端服务
|
||||
│ ├── client/ # 控制端
|
||||
│ ├── ui/ # 用户界面
|
||||
│ └── rendezvous_mediator.rs
|
||||
```
|
||||
|
||||
### One-KVM 结构
|
||||
|
||||
```
|
||||
src/rustdesk/
|
||||
├── mod.rs # 模块导出
|
||||
├── config.rs # 配置类型 (~164 行)
|
||||
├── crypto.rs # 加密模块 (~468 行)
|
||||
├── bytes_codec.rs # 帧编码 (~253 行)
|
||||
├── protocol.rs # 消息辅助 (~170 行)
|
||||
├── rendezvous.rs # Rendezvous 中介 (~829 行)
|
||||
├── connection.rs # 连接处理 (~1349 行)
|
||||
├── hid_adapter.rs # HID 转换 (~386 行)
|
||||
└── frame_adapters.rs # 视频/音频适配 (~316 行)
|
||||
```
|
||||
|
||||
**总计**: ~3935 行代码
|
||||
|
||||
## 7. 总结
|
||||
|
||||
### 实现率统计
|
||||
|
||||
| 类别 | RustDesk 功能数 | One-KVM 实现数 | 实现率 |
|
||||
|------|-----------------|----------------|--------|
|
||||
| Rendezvous 协议 | 15+ | 10 | ~67% |
|
||||
| 连接协议 | 30+ | 12 | ~40% |
|
||||
| 加密功能 | 8 | 6 | 75% |
|
||||
| 视频/音频 | 6 | 6 | 100% |
|
||||
| HID 功能 | 6 | 6 | 100% |
|
||||
|
||||
### 设计理念
|
||||
|
||||
One-KVM 的 RustDesk 实现专注于 **IP-KVM 核心功能**:
|
||||
|
||||
1. **精简**: 只实现必要的被控端功能
|
||||
2. **可靠**: 使用 TCP 中继保证连接稳定性
|
||||
3. **集成**: 与 One-KVM 现有视频/HID 系统无缝集成
|
||||
4. **安全**: 完整实现加密和认证机制
|
||||
|
||||
### 客户端兼容性
|
||||
|
||||
One-KVM 可与标准 RustDesk 客户端配合使用:
|
||||
- RustDesk 桌面客户端 (Windows/macOS/Linux)
|
||||
- RustDesk 移动客户端 (Android/iOS)
|
||||
- RustDesk Web 客户端
|
||||
|
||||
只需确保:
|
||||
1. 配置相同的 Rendezvous 服务器
|
||||
2. 使用设备 ID 和密码连接
|
||||
3. 客户端支持中继连接
|
||||
@@ -1,920 +0,0 @@
|
||||
# One-KVM 系统架构文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
One-KVM 是一个用 Rust 编写的轻量级、开源 IP-KVM 解决方案。它提供 BIOS 级别的远程服务器管理能力,支持视频流、键鼠控制、虚拟存储、电源管理和音频等功能。
|
||||
|
||||
### 1.1 核心特性
|
||||
|
||||
- **单一二进制部署**:Web UI + 后端一体化,无需额外配置文件
|
||||
- **双流模式**:支持 WebRTC(H264/H265/VP8/VP9)和 MJPEG 两种流模式
|
||||
- **USB OTG**:虚拟键鼠、虚拟存储、虚拟网卡
|
||||
- **ATX 电源控制**:GPIO/USB 继电器
|
||||
- **RustDesk 协议集成**:支持跨平台访问
|
||||
- **Vue3 SPA 前端**:支持中文/英文
|
||||
- **SQLite 配置存储**:无需配置文件
|
||||
|
||||
### 1.2 目标平台
|
||||
|
||||
| 平台 | 架构 | 用途 |
|
||||
|------|------|------|
|
||||
| aarch64-unknown-linux-gnu | ARM64 | 主要目标(Rockchip RK3328 等) |
|
||||
| armv7-unknown-linux-gnueabihf | ARMv7 | 备选平台 |
|
||||
| x86_64-unknown-linux-gnu | x86-64 | 开发/测试环境 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统架构图
|
||||
|
||||
### 2.1 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ One-KVM System │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Web Frontend (Vue3) │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||
│ │ │ Console │ │ Settings │ │ Login │ │ Setup │ │ Virtual │ │ │
|
||||
│ │ │ View │ │ View │ │ View │ │ View │ │ Keyboard │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌─────────────────┴─────────────────┐ │ │
|
||||
│ │ │ Pinia State Store │ │ │
|
||||
│ │ └─────────────────┬─────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ API Client Layer │ │ │
|
||||
│ │ │ HTTP REST │ WebSocket │ WebRTC Signaling │ MJPEG │ │ │
|
||||
│ │ └──────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ HTTP/WS/WebRTC │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Axum Web Server (routes.rs) │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Public │ │ User │ │ Admin │ │ Static │ │ │
|
||||
│ │ │ Routes │ │ Routes │ │ Routes │ │ Files │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AppState (state.rs) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Central State Hub │ │ │
|
||||
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
|
||||
│ │ │ │ConfigStore │ │SessionStore│ │ UserStore │ │ │ │
|
||||
│ │ │ │ (SQLite) │ │ (Memory) │ │ (SQLite) │ │ │ │
|
||||
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
|
||||
│ │ │ │ EventBus │ │ OtgService │ │ Extensions │ │ │ │
|
||||
│ │ │ │ (Broadcast)│ │ (USB) │ │ Manager │ │ │ │
|
||||
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┼─────────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ Video │ │ HID │ │ Audio │ │
|
||||
│ │ Module │ │ Module │ │ Module │ │
|
||||
│ ├────────────┤ ├────────────┤ ├────────────┤ │
|
||||
│ │ Capture │ │ Controller │ │ Capture │ │
|
||||
│ │ Encoder │ │ OTG Backend│ │ Encoder │ │
|
||||
│ │ Streamer │ │ CH9329 │ │ Pipeline │ │
|
||||
│ │ Pipeline │ │ Monitor │ │ (Opus) │ │
|
||||
│ │ Manager │ │ DataChan │ │ Shared │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └───────────────────────────┼──────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┼─────────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ MSD │ │ ATX │ │ RustDesk │ │
|
||||
│ │ Module │ │ Module │ │ Module │ │
|
||||
│ ├────────────┤ ├────────────┤ ├────────────┤ │
|
||||
│ │ Controller │ │ Controller │ │ Service │ │
|
||||
│ │ Image Mgr │ │ Executor │ │ Rendezvous │ │
|
||||
│ │ Ventoy │ │ LED Monitor│ │ Connection │ │
|
||||
│ │ Drive │ │ WOL │ │ Protocol │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Hardware Layer │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ V4L2 Video │ │ USB OTG │ │ GPIO │ │ ALSA │ │
|
||||
│ │ Device │ │ Gadget │ │ Sysfs │ │ Audio │ │
|
||||
│ │/dev/video* │ │ ConfigFS │ │ │ │ │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 数据流架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Data Flow Overview │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────┐
|
||||
│ Target PC │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────────────────────┼────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ HDMI Capture │ │ USB Port │ │ GPIO/Relay │
|
||||
│ Card │ │ (OTG Mode) │ │ (ATX) │
|
||||
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ /dev/video0 │ │ /dev/hidg* │ │ /sys/class/ │
|
||||
│ (V4L2) │ │ (USB Gadget) │ │ gpio/gpio* │
|
||||
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ One-KVM Application │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Video │ │ HID │ │ ATX │ │
|
||||
│ │ Pipeline │ │ Controller │ │ Controller │ │
|
||||
│ └─────┬───────┘ └─────┬───────┘ └─────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Event Bus │ │
|
||||
│ │ (tokio broadcast channel) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Web Server (Axum) │ │
|
||||
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
||||
│ │ │ MJPEG │ │ WebRTC │ │WebSocket │ │ │
|
||||
│ │ │ Stream │ │ Stream │ │ Events │ │ │
|
||||
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client Browser │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Video │ │ Input │ │ Control │ │
|
||||
│ │ Display │ │ Events │ │ Panel │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模块依赖关系
|
||||
|
||||
### 3.1 模块层次图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ main.rs ──► state.rs ──► web/routes.rs │
|
||||
│ │ │
|
||||
│ ┌───────────┼───────────┬───────────┬───────────┬───────────┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||
│ │video/│ │ hid/ │ │ msd/ │ │ atx/ │ │audio/│ │webrtc│ │
|
||||
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └──────────┼──────────┘ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌─────▼─────┐ │ │ │
|
||||
│ │ │ otg/ │ │ │ │
|
||||
│ │ │ (OtgSvc) │ │ │ │
|
||||
│ │ └───────────┘ │ │ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────────────────────────────┼──────────┘ │
|
||||
│ │ │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ config/ │ │ auth/ │ │ events/ │ │extensions│ │
|
||||
│ │(ConfigSt)│ │(Session) │ │(EventBus)│ │(ExtMgr) │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ rustdesk/ (RustDeskService) │ │
|
||||
│ │ connection.rs │ rendezvous.rs │ crypto.rs │ protocol.rs │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 依赖矩阵
|
||||
|
||||
| 模块 | 依赖的模块 |
|
||||
|------|-----------|
|
||||
| `main.rs` | state, config, auth, video, hid, msd, atx, audio, webrtc, web, rustdesk, events |
|
||||
| `state.rs` | config, auth, video, hid, msd, atx, audio, webrtc, rustdesk, events, otg |
|
||||
| `video/` | events, hwcodec (外部) |
|
||||
| `hid/` | otg, events |
|
||||
| `msd/` | otg, events |
|
||||
| `atx/` | events |
|
||||
| `audio/` | events |
|
||||
| `webrtc/` | video, audio, hid, events |
|
||||
| `web/` | state, auth, config, video, hid, msd, atx, audio, webrtc, events |
|
||||
| `rustdesk/` | video, audio, hid, events |
|
||||
| `otg/` | (无内部依赖) |
|
||||
| `config/` | (无内部依赖) |
|
||||
| `auth/` | config |
|
||||
| `events/` | (无内部依赖) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心组件详解
|
||||
|
||||
### 4.1 AppState (state.rs)
|
||||
|
||||
AppState 是整个应用的状态中枢,通过 `Arc` 包装的方式在所有 handler 之间共享。
|
||||
|
||||
```rust
|
||||
pub struct AppState {
|
||||
// 配置和存储
|
||||
config: ConfigStore, // SQLite 配置存储
|
||||
sessions: SessionStore, // 会话存储(内存)
|
||||
users: UserStore, // SQLite 用户存储
|
||||
|
||||
// 核心服务
|
||||
otg_service: Arc<OtgService>, // USB Gadget 统一管理(HID/MSD 生命周期协调者)
|
||||
stream_manager: Arc<VideoStreamManager>, // 视频流管理器(MJPEG/WebRTC)
|
||||
hid: Arc<HidController>, // HID 控制器(键鼠控制)
|
||||
msd: Arc<RwLock<Option<MsdController>>>, // MSD 控制器(可选,虚拟U盘)
|
||||
atx: Arc<RwLock<Option<AtxController>>>, // ATX 控制器(可选,电源控制)
|
||||
audio: Arc<AudioController>, // 音频控制器(ALSA + Opus)
|
||||
rustdesk: Arc<RwLock<Option<Arc<RustDeskService>>>>, // RustDesk(可选,远程访问)
|
||||
extensions: Arc<ExtensionManager>,// 扩展管理器(ttyd, gostc, easytier)
|
||||
|
||||
// 通信和生命周期
|
||||
events: Arc<EventBus>, // 事件总线(tokio broadcast channel)
|
||||
shutdown_tx: broadcast::Sender<()>, // 关闭信号
|
||||
data_dir: PathBuf, // 数据目录
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 视频流管道
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Video Pipeline Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌───────────────────┐
|
||||
│ V4L2 Device │
|
||||
│ /dev/video0 │
|
||||
└─────────┬─────────┘
|
||||
│ Raw MJPEG/YUYV/NV12
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ VideoCapturer │ ◄─── src/video/capture.rs
|
||||
│ (capture.rs) │
|
||||
└─────────┬─────────┘
|
||||
│ VideoFrame
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ SharedVideoPipeline │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Decode Stage │ │
|
||||
│ │ ┌─────────────┐ │ │
|
||||
│ │ │ MJPEG → YUV │ turbojpeg / VAAPI │ │
|
||||
│ │ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Convert Stage │ │
|
||||
│ │ ┌─────────────┐ │ │
|
||||
│ │ │YUV → Target │ libyuv (SIMD accelerated) │ │
|
||||
│ │ │ Format │ │ │
|
||||
│ │ └─────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Encode Stage │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │ H264 │ │ H265 │ │ VP8 │ │ VP9 │ │ │
|
||||
│ │ │Encoder │ │Encoder │ │Encoder │ │Encoder │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ │ (VAAPI/RKMPP/V4L2 M2M/Software) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
├──────────────────────────────┬──────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
|
||||
│ MJPEG Streamer │ │ WebRTC Streamer │ │ RustDesk Service │
|
||||
│ (HTTP Stream) │ │ (RTP Packets) │ │ (P2P Stream) │
|
||||
│ │ │ │ │ │
|
||||
│ - HTTP/1.1 │ │ - DataChannel │ │ - TCP/UDP Relay │
|
||||
│ - multipart/x- │ │ - SRTP │ │ - NaCl Encrypted │
|
||||
│ mixed-replace │ │ - ICE/STUN/TURN │ │ - Rendezvous │
|
||||
└───────────────────┘ └───────────────────┘ └───────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ Clients │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Browser │ │ Browser │ │ RustDesk Client │ │
|
||||
│ │ (MJPEG) │ │ (WebRTC) │ │ (Desktop/Mobile) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 OTG 服务架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OTG Service Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OtgService (service.rs) │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Public Interface │ │
|
||||
│ │ enable_hid() │ disable_hid() │ enable_msd() │ disable_msd() │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ OtgGadgetManager (manager.rs) │ │
|
||||
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Gadget Lifecycle │ │ │
|
||||
│ │ │ create_gadget() │ destroy_gadget() │ bind_udc() │ unbind() │ │ │
|
||||
│ │ └───────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────┼────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ HID Function │ │ MSD Function │ │ Endpoint Alloc │ │
|
||||
│ │ (hid.rs) │ │ (msd.rs) │ │ (endpoint.rs) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ ConfigFS Operations │ │
|
||||
│ │ /sys/kernel/config/usb_gadget/one-kvm/ │ │
|
||||
│ │ ├── idVendor, idProduct, strings/ │ │
|
||||
│ │ ├── configs/c.1/ │ │
|
||||
│ │ │ └── functions/ (symlinks) │ │
|
||||
│ │ └── functions/ │ │
|
||||
│ │ ├── hid.usb0, hid.usb1, hid.usb2 │ │
|
||||
│ │ └── mass_storage.usb0 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Linux Kernel │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ /dev/hidg* │ │ Mass Storage │ │
|
||||
│ │ (HID devices) │ │ Backend │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 事件系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Event System Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Event Producers │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Video │ │ HID │ │ MSD │ │ ATX │ │ Audio │ │
|
||||
│ │ Module │ │ Module │ │ Module │ │ Module │ │ Module │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │ │ │ │
|
||||
│ └────────────┴────────────┼────────────┴────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ EventBus │ │
|
||||
│ │ (tokio broadcast channel) │ │
|
||||
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ SystemEvent Enum │ │ │
|
||||
│ │ │ StreamStateChanged │ HidStateChanged │ MsdStateChanged │ │ │
|
||||
│ │ │ AtxStateChanged │ AudioStateChanged │ DeviceInfo │ Error │ │ │
|
||||
│ │ └───────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────┼─────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │WebSocket │ │ DeviceInfo│ │ Internal │ │
|
||||
│ │ Clients │ │Broadcaster│ │ Tasks │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 初始化流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Application Startup Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
main()
|
||||
│
|
||||
├──► Parse CLI Arguments (clap)
|
||||
│ - address, port, data_dir
|
||||
│ - enable_https, ssl_cert, ssl_key
|
||||
│ - verbosity (-v, -vv, -vvv)
|
||||
│
|
||||
├──► Initialize Logging (tracing)
|
||||
│
|
||||
├──► Create/Open SQLite Database
|
||||
│ └─► ConfigStore::new()
|
||||
│ └─► UserStore::new()
|
||||
│ └─► SessionStore::new()
|
||||
│
|
||||
├──► Initialize Core Services
|
||||
│ │
|
||||
│ ├──► EventBus::new()
|
||||
│ │ └─► Create tokio broadcast channel
|
||||
│ │
|
||||
│ ├──► OtgService::new()
|
||||
│ │ └─► Detect UDC device (/sys/class/udc)
|
||||
│ │ └─► Initialize OtgGadgetManager
|
||||
│ │
|
||||
│ ├──► HidController::new()
|
||||
│ │ └─► Detect backend type (OTG/CH9329/None)
|
||||
│ │ └─► Create controller with optional OtgService
|
||||
│ │
|
||||
│ ├──► HidController::init()
|
||||
│ │ └─► Request HID function from OtgService
|
||||
│ │ └─► Create HID devices (/dev/hidg0-3)
|
||||
│ │ └─► Open device files with O_NONBLOCK
|
||||
│ │ └─► Initialize HidHealthMonitor
|
||||
│ │
|
||||
│ ├──► MsdController::init() (if configured)
|
||||
│ │ └─► Request MSD function from OtgService
|
||||
│ │ └─► Create mass storage device
|
||||
│ │ └─► Initialize Ventoy drive (if available)
|
||||
│ │
|
||||
│ ├──► AtxController::init() (if configured)
|
||||
│ │ └─► Setup GPIO pins or USB relay
|
||||
│ │
|
||||
│ ├──► AudioController::init()
|
||||
│ │ └─► Open ALSA device
|
||||
│ │ └─► Initialize Opus encoder
|
||||
│ │
|
||||
│ ├──► VideoStreamManager::new()
|
||||
│ │ └─► Initialize SharedVideoPipeline
|
||||
│ │ └─► Setup encoder registry (H264/H265/VP8/VP9)
|
||||
│ │ └─► Detect hardware acceleration (VAAPI/RKMPP/V4L2 M2M)
|
||||
│ │
|
||||
│ └──► RustDeskService::new() (if configured)
|
||||
│ └─► Load/generate device ID and keys
|
||||
│ └─► Connect to rendezvous server
|
||||
│
|
||||
├──► Create AppState
|
||||
│ └─► Wrap all services in Arc<>
|
||||
│
|
||||
├──► Spawn Background Tasks
|
||||
│ ├──► spawn_device_info_broadcaster()
|
||||
│ ├──► extension_health_check_task()
|
||||
│ └──► rustdesk_reconnect_task()
|
||||
│
|
||||
├──► Create Axum Router
|
||||
│ └─► create_router(app_state)
|
||||
│
|
||||
└──► Start HTTP/HTTPS Server
|
||||
└─► axum::serve() or axum_server with TLS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 目录结构
|
||||
|
||||
```
|
||||
One-KVM-RUST/
|
||||
├── src/ # Rust 源代码
|
||||
│ ├── main.rs # 应用入口点
|
||||
│ ├── lib.rs # 库导出
|
||||
│ ├── state.rs # AppState 定义
|
||||
│ ├── error.rs # 错误类型定义
|
||||
│ │
|
||||
│ ├── video/ # 视频模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── capture.rs # V4L2 采集
|
||||
│ │ ├── streamer.rs # 视频流服务
|
||||
│ │ ├── stream_manager.rs # 流管理器
|
||||
│ │ ├── shared_video_pipeline.rs # 共享视频管道
|
||||
│ │ ├── format.rs # 像素格式
|
||||
│ │ ├── frame.rs # 视频帧
|
||||
│ │ ├── convert.rs # 格式转换
|
||||
│ │ └── encoder/ # 编码器
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── traits.rs
|
||||
│ │ ├── h264.rs
|
||||
│ │ ├── h265.rs
|
||||
│ │ ├── vp8.rs
|
||||
│ │ ├── vp9.rs
|
||||
│ │ └── jpeg.rs
|
||||
│ │
|
||||
│ ├── hid/ # HID 模块
|
||||
│ │ ├── mod.rs # HidController(主控制器)
|
||||
│ │ ├── backend.rs # HidBackend trait 和 HidBackendType
|
||||
│ │ ├── otg.rs # OTG 后端(USB Gadget HID)
|
||||
│ │ ├── ch9329.rs # CH9329 串口后端
|
||||
│ │ ├── consumer.rs # Consumer Control usage codes
|
||||
│ │ ├── keymap.rs # JS keyCode → USB HID 转换表
|
||||
│ │ ├── types.rs # 事件类型定义
|
||||
│ │ ├── monitor.rs # HidHealthMonitor(错误跟踪与恢复)
|
||||
│ │ ├── datachannel.rs # DataChannel 二进制协议解析
|
||||
│ │ └── websocket.rs # WebSocket 二进制协议适配
|
||||
│ │
|
||||
│ ├── otg/ # USB OTG 模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── service.rs # OtgService
|
||||
│ │ ├── manager.rs # GadgetManager
|
||||
│ │ ├── hid.rs # HID Function
|
||||
│ │ ├── msd.rs # MSD Function
|
||||
│ │ ├── configfs.rs # ConfigFS 操作
|
||||
│ │ ├── endpoint.rs # 端点分配
|
||||
│ │ └── report_desc.rs # HID 报告描述符
|
||||
│ │
|
||||
│ ├── msd/ # MSD 模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── controller.rs # MsdController
|
||||
│ │ ├── image.rs # 镜像管理
|
||||
│ │ ├── ventoy_drive.rs # Ventoy 驱动
|
||||
│ │ ├── monitor.rs # 健康监视
|
||||
│ │ └── types.rs # 类型定义
|
||||
│ │
|
||||
│ ├── atx/ # ATX 模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── controller.rs # AtxController
|
||||
│ │ ├── executor.rs # 动作执行器
|
||||
│ │ ├── types.rs # 类型定义
|
||||
│ │ ├── led.rs # LED 监视
|
||||
│ │ └── wol.rs # Wake-on-LAN
|
||||
│ │
|
||||
│ ├── audio/ # 音频模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── controller.rs # AudioController
|
||||
│ │ ├── capture.rs # ALSA 采集
|
||||
│ │ ├── encoder.rs # Opus 编码
|
||||
│ │ ├── shared_pipeline.rs # 共享管道
|
||||
│ │ ├── monitor.rs # 健康监视
|
||||
│ │ └── device.rs # 设备枚举
|
||||
│ │
|
||||
│ ├── webrtc/ # WebRTC 模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── webrtc_streamer.rs # WebRTC 管理器
|
||||
│ │ ├── universal_session.rs # 会话管理
|
||||
│ │ ├── video_track.rs # 视频轨道
|
||||
│ │ ├── rtp.rs # RTP 打包
|
||||
│ │ ├── h265_payloader.rs # H265 RTP
|
||||
│ │ ├── peer.rs # PeerConnection
|
||||
│ │ ├── config.rs # 配置
|
||||
│ │ ├── signaling.rs # 信令
|
||||
│ │ └── track.rs # 轨道基类
|
||||
│ │
|
||||
│ ├── auth/ # 认证模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── user.rs # 用户管理
|
||||
│ │ ├── session.rs # 会话管理
|
||||
│ │ ├── password.rs # 密码哈希
|
||||
│ │ └── middleware.rs # Axum 中间件
|
||||
│ │
|
||||
│ ├── config/ # 配置模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── schema.rs # 配置结构定义
|
||||
│ │ └── store.rs # SQLite 存储
|
||||
│ │
|
||||
│ ├── events/ # 事件模块
|
||||
│ │ └── mod.rs # EventBus
|
||||
│ │
|
||||
│ ├── rustdesk/ # RustDesk 模块
|
||||
│ │ ├── mod.rs # RustDeskService
|
||||
│ │ ├── connection.rs # 连接管理
|
||||
│ │ ├── rendezvous.rs # 渲染服务器通信
|
||||
│ │ ├── crypto.rs # NaCl 加密
|
||||
│ │ ├── config.rs # 配置
|
||||
│ │ ├── hid_adapter.rs # HID 适配
|
||||
│ │ ├── frame_adapters.rs # 帧格式转换
|
||||
│ │ ├── protocol.rs # 协议包装
|
||||
│ │ └── bytes_codec.rs # 帧编码
|
||||
│ │
|
||||
│ ├── extensions/ # 扩展模块
|
||||
│ │ └── mod.rs # ExtensionManager
|
||||
│ │
|
||||
│ ├── web/ # Web 模块
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── routes.rs # 路由定义
|
||||
│ │ ├── ws.rs # WebSocket
|
||||
│ │ ├── audio_ws.rs # 音频 WebSocket
|
||||
│ │ ├── static_files.rs # 静态文件
|
||||
│ │ └── handlers/ # API 处理器
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── config/
|
||||
│ │
|
||||
│ ├── stream/ # MJPEG 流
|
||||
│ │ └── mod.rs
|
||||
│ │
|
||||
│ └── utils/ # 工具函数
|
||||
│ └── mod.rs
|
||||
│
|
||||
├── web/ # Vue3 前端
|
||||
│ ├── src/
|
||||
│ │ ├── views/ # 页面组件
|
||||
│ │ ├── components/ # UI 组件
|
||||
│ │ ├── api/ # API 客户端
|
||||
│ │ ├── stores/ # Pinia 状态
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── i18n/ # 国际化
|
||||
│ │ └── types/ # TypeScript 类型
|
||||
│ └── package.json
|
||||
│
|
||||
├── libs/ # 外部库
|
||||
│ ├── hwcodec/ # 硬件视频编码
|
||||
│ └── ventoy-img-rs/ # Ventoy 支持
|
||||
│
|
||||
├── protos/ # Protobuf 定义
|
||||
│ ├── message.proto # RustDesk 消息
|
||||
│ └── rendezvous.proto # RustDesk 渲染
|
||||
│
|
||||
├── docs/ # 文档
|
||||
├── scripts/ # 脚本
|
||||
├── Cargo.toml # Rust 配置
|
||||
├── build.rs # 构建脚本
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全架构
|
||||
|
||||
### 7.1 认证流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Authentication Flow │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Client │ │ Axum │ │ Auth │ │ SQLite │
|
||||
│ Browser │ │ Server │ │ Module │ │ Database │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │ │
|
||||
│ POST /auth/login │ │ │
|
||||
│ {username, pass} │ │ │
|
||||
│───────────────────►│ │ │
|
||||
│ │ verify_user() │ │
|
||||
│ │───────────────────►│ │
|
||||
│ │ │ SELECT user │
|
||||
│ │ │───────────────────►│
|
||||
│ │ │◄───────────────────│
|
||||
│ │ │ │
|
||||
│ │ │ Argon2 verify │
|
||||
│ │ │ ────────────► │
|
||||
│ │ │ │
|
||||
│ │ session_token │ │
|
||||
│ │◄───────────────────│ │
|
||||
│ │ │ │
|
||||
│ Set-Cookie: │ │ │
|
||||
│ session_id=token │ │ │
|
||||
│◄───────────────────│ │ │
|
||||
│ │ │ │
|
||||
│ GET /api/... │ │ │
|
||||
│ Cookie: session │ │ │
|
||||
│───────────────────►│ │ │
|
||||
│ │ validate_session()│ │
|
||||
│ │───────────────────►│ │
|
||||
│ │ user_info │ │
|
||||
│ │◄───────────────────│ │
|
||||
│ │ │ │
|
||||
│ Response │ │ │
|
||||
│◄───────────────────│ │ │
|
||||
```
|
||||
|
||||
### 7.2 权限层级
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Permission Levels │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Public (No Auth) │
|
||||
│ ├── GET /health │
|
||||
│ ├── POST /auth/login │
|
||||
│ ├── GET /setup │
|
||||
│ └── POST /setup/init │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ User (Authenticated) │
|
||||
│ ├── GET /info (系统信息) │
|
||||
│ ├── GET /devices (设备列表) │
|
||||
│ ├── GET/POST /stream/* (流控制) │
|
||||
│ ├── POST /webrtc/* (WebRTC 信令) │
|
||||
│ ├── POST /hid/* (HID 控制) │
|
||||
│ ├── POST /audio/* (音频控制) │
|
||||
│ └── WebSocket endpoints (实时通信) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Admin (Admin Role) │
|
||||
│ ├── GET/PATCH /config/* (配置管理) │
|
||||
│ ├── POST /msd/* (MSD 操作) │
|
||||
│ ├── POST /atx/* (电源控制) │
|
||||
│ ├── POST /extensions/* (扩展管理) │
|
||||
│ ├── POST /rustdesk/* (RustDesk 配置) │
|
||||
│ └── POST /users/* (用户管理) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署架构
|
||||
|
||||
### 8.1 单机部署
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Single Binary Deployment │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ARM64 Device (e.g., Rockchip RK3328) │
|
||||
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ one-kvm (single binary, ~15MB) │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Embedded Assets (rust-embed, gzip compressed) │ │ │
|
||||
│ │ │ - index.html, app.js, app.css, assets/* │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
|
||||
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Runtime Data (data_dir) │ │ │
|
||||
│ │ │ - one-kvm.db (SQLite) │ │ │
|
||||
│ │ │ - images/ (MSD images) │ │ │
|
||||
│ │ │ - certs/ (SSL certificates) │ │ │
|
||||
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Hardware Connections: │
|
||||
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
|
||||
│ │ HDMI Input │ │ USB OTG Port │ │ GPIO Header │ │
|
||||
│ │ (/dev/video0) │ │ (USB Gadget) │ │ (ATX Control) │ │
|
||||
│ └───────────────────┘ └───────────────────┘ └───────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ USB Cable
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Target PC │
|
||||
│ - Receives USB HID events (keyboard/mouse) │
|
||||
│ - Provides HDMI video output │
|
||||
│ - Can boot from virtual USB drive │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 8.2 网络拓扑
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Network Topology │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Internet
|
||||
│
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ RustDesk │ │ Client │
|
||||
│ Server │ │ Browser │
|
||||
│ (hbbs/hbbr) │ │ │
|
||||
└───────────────┘ └───────────────┘
|
||||
│ │
|
||||
│ │
|
||||
└───────────┬───────────┘
|
||||
│
|
||||
┌────┴────┐
|
||||
│ Router │
|
||||
│ NAT │
|
||||
└────┬────┘
|
||||
│
|
||||
Local Network
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ One-KVM │───────│ Target PC │
|
||||
│ Device │ USB │ │
|
||||
│ :8080/:8443 │ HID │ │
|
||||
└───────────────┘ └───────────────┘
|
||||
|
||||
Access Methods:
|
||||
1. Local: http://one-kvm.local:8080
|
||||
2. HTTPS: https://one-kvm.local:8443
|
||||
3. RustDesk: Via RustDesk client with device ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 扩展点
|
||||
|
||||
### 9.1 添加新编码器
|
||||
|
||||
```rust
|
||||
// 1. 实现 Encoder trait
|
||||
impl Encoder for MyEncoder {
|
||||
fn encode(&mut self, frame: &VideoFrame) -> Result<Vec<u8>>;
|
||||
fn codec(&self) -> Codec;
|
||||
fn bitrate(&self) -> u32;
|
||||
// ...
|
||||
}
|
||||
|
||||
// 2. 在 registry 中注册
|
||||
encoder_registry.register("my-encoder", || Box::new(MyEncoder::new()));
|
||||
```
|
||||
|
||||
### 9.2 添加新 HID 后端
|
||||
|
||||
```rust
|
||||
// 1. 在 backend.rs 中定义新后端类型
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
pub enum HidBackendType {
|
||||
Otg,
|
||||
Ch9329 { port: String, baud_rate: u32 },
|
||||
MyBackend { /* 配置参数 */ }, // 新增
|
||||
None,
|
||||
}
|
||||
|
||||
// 2. 实现 HidBackend trait
|
||||
#[async_trait]
|
||||
impl HidBackend for MyBackend {
|
||||
fn name(&self) -> &'static str { "MyBackend" }
|
||||
async fn init(&self) -> Result<()> { /* ... */ }
|
||||
async fn send_keyboard(&self, event: KeyboardEvent) -> Result<()> { /* ... */ }
|
||||
async fn send_mouse(&self, event: MouseEvent) -> Result<()> { /* ... */ }
|
||||
async fn send_consumer(&self, event: ConsumerEvent) -> Result<()> { /* ... */ }
|
||||
async fn reset(&self) -> Result<()> { /* ... */ }
|
||||
async fn shutdown(&self) -> Result<()> { /* ... */ }
|
||||
fn supports_absolute_mouse(&self) -> bool { true }
|
||||
fn screen_resolution(&self) -> Option<(u32, u32)> { None }
|
||||
fn set_screen_resolution(&mut self, width: u32, height: u32) { /* ... */ }
|
||||
}
|
||||
|
||||
// 3. 在 HidController::init() 中添加分支
|
||||
match backend_type {
|
||||
HidBackendType::MyBackend { /* params */ } => {
|
||||
Box::new(MyBackend::new(/* params */)?)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 添加新扩展
|
||||
|
||||
```rust
|
||||
// 通过 ExtensionManager 管理外部进程
|
||||
extension_manager.register("my-extension", ExtensionConfig {
|
||||
command: "my-binary",
|
||||
args: vec!["--port", "9000"],
|
||||
health_check: HealthCheckConfig::Http { url: "http://localhost:9000/health" },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考资料
|
||||
|
||||
- [Axum Web Framework](https://github.com/tokio-rs/axum)
|
||||
- [webrtc-rs](https://github.com/webrtc-rs/webrtc)
|
||||
- [V4L2 Documentation](https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/v4l2.html)
|
||||
- [Linux USB Gadget](https://www.kernel.org/doc/html/latest/usb/gadget_configfs.html)
|
||||
- [RustDesk Protocol](https://github.com/rustdesk/rustdesk)
|
||||
1007
docs/tech-stack.md
1007
docs/tech-stack.md
File diff suppressed because it is too large
Load Diff
16
libs/bluetooth-hid/Cargo.toml
Normal file
16
libs/bluetooth-hid/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "one-kvm-bluetooth-hid"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
[dependencies]
|
||||
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||
dbus = { version = "0.9", features = ["vendored"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dbus-crossroads = "0.5"
|
||||
dbus-tokio = "0.7"
|
||||
libc = "0.2"
|
||||
tracing = "0.1"
|
||||
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Explicit hardware regression: opens a 10-second pairing window without
|
||||
//! removing existing bonds or sending input. Stop the production HID first.
|
||||
use one_kvm_bluetooth_hid::{Action, Config, Peripheral};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
let peripheral = Peripheral::start(Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM HID".into(),
|
||||
peer: None,
|
||||
})?;
|
||||
let result = async {
|
||||
for _ in 0..50 {
|
||||
if peripheral.status().initialized {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
let before = peripheral.status();
|
||||
if !before.initialized {
|
||||
return Err(format!("Initialization failed: {:?}", before.error));
|
||||
}
|
||||
if !before
|
||||
.devices
|
||||
.iter()
|
||||
.any(|d| Some(&d.address) == before.peer.as_ref() && d.paired)
|
||||
{
|
||||
return Err("This regression needs an existing bonded computer".into());
|
||||
}
|
||||
peripheral.action(Action::Pair(10)).await?;
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let open = peripheral.status();
|
||||
println!("open: {}", serde_json::to_string(&open).unwrap());
|
||||
if open.pairing_seconds == 0 {
|
||||
return Err("Existing bond prematurely closed pairing".into());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
let expired = peripheral.status();
|
||||
println!("expired: {}", serde_json::to_string(&expired).unwrap());
|
||||
if expired.pairing_seconds != 0 || expired.peer != before.peer {
|
||||
return Err("Window did not expire while preserving the bonded peer".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
let cleanup = peripheral.shutdown().await;
|
||||
result.and(cleanup)
|
||||
}
|
||||
24
libs/bluetooth-hid/examples/probe.rs
Normal file
24
libs/bluetooth-hid/examples/probe.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Native hardware smoke check. Registers for 12 seconds, sends no input,
|
||||
//! opens no pairing window, then restores the adapter configuration.
|
||||
use one_kvm_bluetooth_hid::{Config, Peripheral};
|
||||
use std::time::Duration;
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
let peripheral = Peripheral::start(Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM HID".into(),
|
||||
peer: None,
|
||||
})?;
|
||||
let mut initialized = false;
|
||||
for _ in 0..12 {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
let status = peripheral.status();
|
||||
initialized |= status.initialized;
|
||||
println!("{}", serde_json::to_string(&status).unwrap());
|
||||
}
|
||||
peripheral.shutdown().await?;
|
||||
if !initialized {
|
||||
return Err("Native BlueZ peripheral never initialized".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
236
libs/bluetooth-hid/src/agent.rs
Normal file
236
libs/bluetooth-hid/src/agent.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use crate::Shared;
|
||||
use dbus::{channel::MatchingReceiver, message::MatchRule, nonblock::Proxy, Path};
|
||||
use dbus_crossroads::Crossroads;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const PATH: &str = "/org/onekvm/bluetooth/agent";
|
||||
pub struct Agent {
|
||||
connection: Arc<dbus::nonblock::SyncConnection>,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
impl Agent {
|
||||
pub async fn register(shared: Arc<Shared>, adapter: String) -> Result<Self, String> {
|
||||
let (resource, connection) =
|
||||
dbus_tokio::connection::new_system_sync().map_err(|e| e.to_string())?;
|
||||
let task = tokio::spawn(async move {
|
||||
let _ = resource.await;
|
||||
});
|
||||
let mut cr = Crossroads::new();
|
||||
let iface = cr.register("org.bluez.Agent1", |b| {
|
||||
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
b.method("Cancel", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
let state = shared.clone();
|
||||
let name = adapter.clone();
|
||||
b.method(
|
||||
"RequestAuthorization",
|
||||
("device",),
|
||||
(),
|
||||
move |_, _: &mut (), (path,): (Path<'static>,)| {
|
||||
authorize(&state, &name, &path, true)
|
||||
},
|
||||
);
|
||||
let state = shared.clone();
|
||||
let name = adapter.clone();
|
||||
b.method(
|
||||
"RequestConfirmation",
|
||||
("device", "passkey"),
|
||||
(),
|
||||
move |_, _: &mut (), (path, _): (Path<'static>, u32)| {
|
||||
authorize(&state, &name, &path, true)
|
||||
},
|
||||
);
|
||||
b.method(
|
||||
"AuthorizeService",
|
||||
("device", "uuid"),
|
||||
(),
|
||||
move |_, _: &mut (), (path, uuid): (Path<'static>, String)| {
|
||||
if !matches!(uuid.as_str(), "00001124-0000-1000-8000-00805f9b34fb") {
|
||||
return Err(dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Not an HID service",
|
||||
)));
|
||||
}
|
||||
authorize(&shared, &adapter, &path, false)
|
||||
},
|
||||
);
|
||||
});
|
||||
cr.insert(PATH, &[iface], ());
|
||||
let profile = cr.register("org.bluez.Profile1", |b| {
|
||||
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||
b.method(
|
||||
"RequestDisconnection",
|
||||
("device",),
|
||||
(),
|
||||
|_, _: &mut (), (_device,): (Path<'static>,)| Ok(()),
|
||||
);
|
||||
b.method(
|
||||
"NewConnection",
|
||||
("device", "fd", "properties"),
|
||||
(),
|
||||
|_,
|
||||
_: &mut (),
|
||||
(_device, _fd, _props): (
|
||||
Path<'static>,
|
||||
dbus::arg::OwnedFd,
|
||||
dbus::arg::PropMap,
|
||||
)| {
|
||||
// This profile only publishes SDP. Our L2CAP sockets own both HID channels.
|
||||
Err::<(), _>(dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Unexpected profile connection",
|
||||
)))
|
||||
},
|
||||
);
|
||||
});
|
||||
cr.insert("/org/onekvm/bluetooth/profile", &[profile], ());
|
||||
connection.start_receive(
|
||||
MatchRule::new_method_call(),
|
||||
Box::new(move |msg, conn| {
|
||||
let _ = cr.handle_message(msg, conn);
|
||||
true
|
||||
}),
|
||||
);
|
||||
let agent = Self { connection, task };
|
||||
let proxy = Proxy::new(
|
||||
"org.bluez",
|
||||
"/org/bluez",
|
||||
Duration::from_secs(5),
|
||||
agent.connection.clone(),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.AgentManager1",
|
||||
"RegisterAgent",
|
||||
(Path::from(PATH), "NoInputNoOutput"),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
return Err(error.to_string());
|
||||
}
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.AgentManager1",
|
||||
"RequestDefaultAgent",
|
||||
(Path::from(PATH),),
|
||||
)
|
||||
.await;
|
||||
result.map_err(|e| e.to_string())?;
|
||||
let mut options: dbus::arg::PropMap = std::collections::HashMap::new();
|
||||
options.insert(
|
||||
"ServiceRecord".into(),
|
||||
dbus::arg::Variant(Box::new(crate::protocol::sdp())),
|
||||
);
|
||||
options.insert(
|
||||
"Role".into(),
|
||||
dbus::arg::Variant(Box::new("server".to_string())),
|
||||
);
|
||||
options.insert(
|
||||
"RequireAuthentication".into(),
|
||||
dbus::arg::Variant(Box::new(true)),
|
||||
);
|
||||
options.insert(
|
||||
"RequireAuthorization".into(),
|
||||
dbus::arg::Variant(Box::new(false)),
|
||||
);
|
||||
let result: Result<(), dbus::Error> = proxy
|
||||
.method_call(
|
||||
"org.bluez.ProfileManager1",
|
||||
"RegisterProfile",
|
||||
(
|
||||
Path::from("/org/onekvm/bluetooth/profile"),
|
||||
crate::protocol::HID_UUID,
|
||||
options,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
result.map_err(|e| format!("Register HID SDP: {e}"))?;
|
||||
Ok(agent)
|
||||
}
|
||||
}
|
||||
impl Drop for Agent {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
fn authorize(
|
||||
shared: &Shared,
|
||||
adapter: &str,
|
||||
path: &str,
|
||||
pairing: bool,
|
||||
) -> Result<(), dbus::MethodErr> {
|
||||
let rejected = || {
|
||||
dbus::MethodErr::from((
|
||||
"org.bluez.Error.Rejected",
|
||||
"Open pairing in One-KVM or select the paired computer",
|
||||
))
|
||||
};
|
||||
let prefix = format!("/org/bluez/{adapter}/dev_");
|
||||
let address = path
|
||||
.strip_prefix(&prefix)
|
||||
.ok_or_else(rejected)?
|
||||
.replace('_', ":");
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
if pairing
|
||||
&& !state
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
{
|
||||
return Err(rejected());
|
||||
}
|
||||
if state.peer.as_ref().is_some_and(|peer| peer != &address) {
|
||||
return Err(rejected());
|
||||
}
|
||||
if state.peer.is_none() {
|
||||
if !pairing || state.bonded_before_pairing.contains(&address) {
|
||||
return Err(rejected());
|
||||
}
|
||||
state.peer = Some(address);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn existing_unrelated_bond_cannot_be_adopted_by_agent_callback() {
|
||||
let shared = Shared::new(None);
|
||||
{
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state
|
||||
.bonded_before_pairing
|
||||
.insert("10:6F:D9:66:97:88".into());
|
||||
}
|
||||
assert!(authorize(
|
||||
&shared,
|
||||
"hci0",
|
||||
"/org/bluez/hci0/dev_10_6F_D9_66_97_88",
|
||||
true
|
||||
)
|
||||
.is_err());
|
||||
assert!(shared.state.lock().unwrap().peer.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn only_pair_during_window_and_pin_first_peer() {
|
||||
let state = Shared::new(None);
|
||||
let first = "/org/bluez/hci0/dev_10_6F_D9_66_97_88";
|
||||
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||
state.state.lock().unwrap().pairing_until = Some(Instant::now() + Duration::from_secs(60));
|
||||
assert!(authorize(&state, "hci0", first, true).is_ok());
|
||||
assert!(authorize(
|
||||
&state,
|
||||
"hci0",
|
||||
"/org/bluez/hci0/dev_10_6F_D9_66_97_89",
|
||||
true
|
||||
)
|
||||
.is_err());
|
||||
assert!(authorize(&state, "hci1", first, true).is_err());
|
||||
state.state.lock().unwrap().pairing_until = None;
|
||||
assert!(authorize(&state, "hci0", first, false).is_ok());
|
||||
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||
}
|
||||
}
|
||||
145
libs/bluetooth-hid/src/bonds.rs
Normal file
145
libs/bluetooth-hid/src/bonds.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
//! Only bonds explicitly owned by this HID peripheral may be removed.
|
||||
use bluer::{Adapter, Session};
|
||||
use std::{future::Future, pin::Pin};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Bond {
|
||||
pub adapter: String,
|
||||
pub peer: String,
|
||||
pub pending: bool,
|
||||
}
|
||||
pub type Operation<'a, T> = Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
|
||||
pub trait BondStore: Send + Sync {
|
||||
fn list(&self) -> Operation<'_, Vec<Bond>>;
|
||||
fn save(&self, bond: Bond) -> Operation<'_, ()>;
|
||||
fn remove(&self, bond: Bond) -> Operation<'_, ()>;
|
||||
}
|
||||
|
||||
pub async fn clean_adapter(store: &dyn BondStore, adapter: &Adapter) -> Result<(), String> {
|
||||
let address = adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string();
|
||||
clean_owned(store, &address, |peer| async move {
|
||||
let peer = peer.parse().map_err(|_| "Invalid stored HID peer")?;
|
||||
if adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.contains(&peer)
|
||||
{
|
||||
adapter
|
||||
.remove_device(peer)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn clean_owned<F, Fut>(
|
||||
store: &dyn BondStore,
|
||||
address: &str,
|
||||
mut remove: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(String) -> Fut,
|
||||
Fut: Future<Output = Result<(), String>>,
|
||||
{
|
||||
for bond in store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|b| b.pending && b.adapter == address)
|
||||
{
|
||||
remove(bond.peer.clone()).await?;
|
||||
// Keep the tombstone until BlueZ confirms removal; retry safely after interruption.
|
||||
store.remove(bond).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset(store: &dyn BondStore, explicit: Vec<Bond>) -> Result<(), String> {
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let mut owned = store.list().await?;
|
||||
owned.extend(explicit);
|
||||
for mut bond in owned {
|
||||
bond.pending = true;
|
||||
store.save(bond).await?;
|
||||
}
|
||||
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||
clean_adapter(store, &session.adapter(&name).map_err(|e| e.to_string())?).await?;
|
||||
}
|
||||
// Records for absent adapters remain pending and are cleaned before their next use.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
#[derive(Default)]
|
||||
struct Memory(Mutex<Vec<Bond>>);
|
||||
impl BondStore for Memory {
|
||||
fn list(&self) -> Operation<'_, Vec<Bond>> {
|
||||
Box::pin(async { Ok(self.0.lock().unwrap().clone()) })
|
||||
}
|
||||
fn save(&self, bond: Bond) -> Operation<'_, ()> {
|
||||
Box::pin(async move {
|
||||
let mut rows = self.0.lock().unwrap();
|
||||
rows.retain(|b| b.adapter != bond.adapter || b.peer != bond.peer);
|
||||
rows.push(bond);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
fn remove(&self, bond: Bond) -> Operation<'_, ()> {
|
||||
Box::pin(async move {
|
||||
self.0.lock().unwrap().retain(|b| b != &bond);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
fn bond(adapter: &str, peer: &str, pending: bool) -> Bond {
|
||||
Bond {
|
||||
adapter: adapter.into(),
|
||||
peer: peer.into(),
|
||||
pending,
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn cleanup_only_removes_owned_pending_bonds_on_the_selected_hardware() {
|
||||
let current = bond("adapter-A", "host-1", true);
|
||||
let absent = bond("adapter-B", "host-2", true);
|
||||
let retained = bond("adapter-A", "host-3", false);
|
||||
let store = Memory(Mutex::new(vec![current, absent.clone(), retained.clone()]));
|
||||
let removed = Mutex::new(Vec::new());
|
||||
clean_owned(&store, "adapter-A", |peer| {
|
||||
removed.lock().unwrap().push(peer);
|
||||
async { Ok(()) }
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(*removed.lock().unwrap(), vec!["host-1"]);
|
||||
assert_eq!(store.list().await.unwrap(), vec![absent, retained]);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn cleanup_failure_keeps_tombstone_and_is_reported() {
|
||||
let record = bond("adapter-A", "host-1", true);
|
||||
let store = Memory(Mutex::new(vec![record.clone()]));
|
||||
assert_eq!(
|
||||
clean_owned(&store, "adapter-A", |_| async {
|
||||
Err("BlueZ denied removal".into())
|
||||
})
|
||||
.await
|
||||
.unwrap_err(),
|
||||
"BlueZ denied removal"
|
||||
);
|
||||
assert_eq!(store.list().await.unwrap(), vec![record]);
|
||||
clean_owned(&store, "adapter-A", |_| async { Ok(()) })
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.list().await.unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
135
libs/bluetooth-hid/src/controller.rs
Normal file
135
libs/bluetooth-hid/src/controller.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! Exclusive controller ownership. No BLE switching or power cycling.
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||
process::Stdio,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
pub struct Controller {
|
||||
_lock: File,
|
||||
adapter: String,
|
||||
old_class: (u8, u8),
|
||||
old_connectable: bool,
|
||||
}
|
||||
async fn run_once(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||
let mut cmd = Command::new("btmgmt");
|
||||
cmd.arg("--index")
|
||||
.arg(adapter.trim_start_matches("hci"))
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||
// BlueZ 5.55's btmgmt exits on stdin EOF, even with a command argument.
|
||||
// Keep a pipe open until it finishes; systemd normally supplies /dev/null.
|
||||
let stdin = child.stdin.take();
|
||||
let result = tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())
|
||||
.await
|
||||
.map_err(|_| "btmgmt timeout")?
|
||||
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||
drop(stdin);
|
||||
let text = String::from_utf8_lossy(&result.stdout).into_owned();
|
||||
if !result.status.success()
|
||||
|| !result.stderr.is_empty()
|
||||
|| text.to_lowercase().contains("failed")
|
||||
{
|
||||
return Err(format!(
|
||||
"btmgmt {}: {text} {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
async fn run(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||
// BlueZ updates the EIR/class asynchronously after registering SDP.
|
||||
// Kernel management rejects a simultaneous class change with Busy.
|
||||
for attempt in 0..5 {
|
||||
match run_once(adapter, args).await {
|
||||
Err(error) if error.contains("(Busy)") && attempt < 4 => {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
}
|
||||
impl Controller {
|
||||
pub async fn acquire(adapter: &str) -> Result<Self, String> {
|
||||
let lock = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.mode(0o600)
|
||||
.custom_flags(libc::O_NOFOLLOW)
|
||||
.open(format!("/run/one-kvm-bluetooth-{adapter}.lock"))
|
||||
.map_err(|e| e.to_string())?;
|
||||
if unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
|
||||
return Err("Bluetooth adapter already owned by One-KVM".into());
|
||||
}
|
||||
let info = run(adapter, &["info"]).await?;
|
||||
let settings = info
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("current settings:"))
|
||||
.ok_or("Cannot read Bluetooth settings")?;
|
||||
if !settings.split_whitespace().any(|s| s == "br/edr") {
|
||||
return Err(
|
||||
"Classic Bluetooth disabled; enable BR/EDR with btmgmt before using HID".into(),
|
||||
);
|
||||
}
|
||||
let class = info
|
||||
.split_whitespace()
|
||||
.skip_while(|s| *s != "class")
|
||||
.nth(1)
|
||||
.ok_or("Missing Bluetooth class")?;
|
||||
let class =
|
||||
u32::from_str_radix(class.trim_start_matches("0x"), 16).map_err(|e| e.to_string())?;
|
||||
Ok(Self {
|
||||
_lock: lock,
|
||||
adapter: adapter.into(),
|
||||
old_class: (((class >> 8) & 0x1f) as u8, (class & 0xfc) as u8),
|
||||
old_connectable: settings.split_whitespace().any(|s| s == "connectable"),
|
||||
})
|
||||
}
|
||||
pub async fn configure(&self) -> Result<(), String> {
|
||||
run(&self.adapter, &["class", "5", "192"]).await?;
|
||||
run(&self.adapter, &["connectable", "on"]).await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn restore(&self) -> Result<(), String> {
|
||||
let mut errors = vec![];
|
||||
if let Err(e) = run(
|
||||
&self.adapter,
|
||||
&[
|
||||
"class",
|
||||
&self.old_class.0.to_string(),
|
||||
&self.old_class.1.to_string(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
if let Err(e) = run(
|
||||
&self.adapter,
|
||||
&[
|
||||
"connectable",
|
||||
if self.old_connectable { "on" } else { "off" },
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
964
libs/bluetooth-hid/src/lib.rs
Normal file
964
libs/bluetooth-hid/src/lib.rs
Normal file
@@ -0,0 +1,964 @@
|
||||
//! Classic Bluetooth HID peripheral: SDP + L2CAP. Linux/BlueZ only.
|
||||
mod agent;
|
||||
pub mod bonds;
|
||||
mod controller;
|
||||
mod protocol;
|
||||
|
||||
use bluer::{
|
||||
l2cap::{Security, SecurityLevel, SeqPacket, SeqPacketListener, Socket, SocketAddr},
|
||||
Adapter, Address, AddressType, Session,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub adapter: String,
|
||||
pub name: String,
|
||||
pub peer: Option<String>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if !self
|
||||
.adapter
|
||||
.strip_prefix("hci")
|
||||
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
|
||||
{
|
||||
return Err("Bluetooth adapter must be hci followed by an index".into());
|
||||
}
|
||||
if self.name.is_empty() || self.name.len() > 64 || self.name.chars().any(char::is_control) {
|
||||
return Err(
|
||||
"Bluetooth name must contain 1–64 UTF-8 bytes without control characters".into(),
|
||||
);
|
||||
}
|
||||
if let Some(peer) = &self.peer {
|
||||
peer.parse::<Address>()
|
||||
.map_err(|_| "Invalid Bluetooth peer address")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Status {
|
||||
pub initialized: bool,
|
||||
pub connected: bool,
|
||||
pub ready: bool,
|
||||
pub adapter: String,
|
||||
pub adapter_address: String,
|
||||
pub peer: Option<String>,
|
||||
pub pairing_seconds: u32,
|
||||
pub control_connected: bool,
|
||||
pub interrupt_connected: bool,
|
||||
pub leds: u8,
|
||||
pub generation: u64,
|
||||
pub error: Option<String>,
|
||||
pub devices: Vec<Device>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Device {
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub paired: bool,
|
||||
pub connected: bool,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Report {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Consumer,
|
||||
}
|
||||
impl Report {
|
||||
fn len(self) -> usize {
|
||||
match self {
|
||||
Self::Keyboard => 8,
|
||||
Self::Mouse => 4,
|
||||
Self::Consumer => 2,
|
||||
}
|
||||
}
|
||||
fn id(self) -> u8 {
|
||||
match self {
|
||||
Self::Keyboard => 1,
|
||||
Self::Mouse => 2,
|
||||
Self::Consumer => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
struct State {
|
||||
peer: Option<String>,
|
||||
pairing_until: Option<Instant>,
|
||||
bonded_before_pairing: HashSet<String>,
|
||||
}
|
||||
impl State {
|
||||
fn observe_bond(&mut self, address: &str, paired: bool) {
|
||||
// Just Works may complete without an Agent1 authorization callback.
|
||||
// Adopt only a new bond from our explicit window, respecting a pinned peer.
|
||||
if paired
|
||||
&& self.peer.is_none()
|
||||
&& self
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
&& !self.bonded_before_pairing.contains(address)
|
||||
{
|
||||
self.peer = Some(address.to_owned());
|
||||
tracing::info!(
|
||||
peer = address,
|
||||
"Bluetooth HID selected newly bonded computer"
|
||||
);
|
||||
}
|
||||
}
|
||||
fn close_pairing(&mut self, paired: bool, fallback: Option<String>) {
|
||||
self.pairing_until = None;
|
||||
if !paired {
|
||||
self.peer = fallback;
|
||||
}
|
||||
}
|
||||
fn pairing_completed(&self, selected_paired: bool) -> bool {
|
||||
selected_paired
|
||||
&& self
|
||||
.peer
|
||||
.as_ref()
|
||||
.is_some_and(|peer| !self.bonded_before_pairing.contains(peer))
|
||||
}
|
||||
}
|
||||
struct Shared {
|
||||
state: Mutex<State>,
|
||||
}
|
||||
impl Shared {
|
||||
fn new(peer: Option<String>) -> Self {
|
||||
Self {
|
||||
state: Mutex::new(State {
|
||||
peer,
|
||||
pairing_until: None,
|
||||
bonded_before_pairing: HashSet::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
fn allowed(&self, address: Address) -> bool {
|
||||
self.state.lock().unwrap().peer.as_deref() == Some(address.to_string().as_str())
|
||||
}
|
||||
}
|
||||
pub enum Action {
|
||||
Pair(u32),
|
||||
ClosePairing,
|
||||
Forget,
|
||||
Disconnect,
|
||||
Reset,
|
||||
}
|
||||
enum Command {
|
||||
Report(Report, Vec<u8>),
|
||||
Action(Action),
|
||||
Stop,
|
||||
}
|
||||
struct Request {
|
||||
command: Command,
|
||||
result: oneshot::Sender<Result<(), String>>,
|
||||
expires: Instant,
|
||||
}
|
||||
pub struct Peripheral {
|
||||
tx: mpsc::Sender<Request>,
|
||||
status: watch::Receiver<Status>,
|
||||
task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
}
|
||||
impl Peripheral {
|
||||
pub fn start(config: Config) -> Result<Self, String> {
|
||||
Self::start_with_store(config, None)
|
||||
}
|
||||
pub fn start_with_store(
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
) -> Result<Self, String> {
|
||||
config.validate()?;
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
let (state_tx, status) = watch::channel(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
..Default::default()
|
||||
});
|
||||
let task = tokio::spawn(supervise(config, bonds, rx, state_tx));
|
||||
Ok(Self {
|
||||
tx,
|
||||
status,
|
||||
task: AsyncMutex::new(Some(task)),
|
||||
})
|
||||
}
|
||||
pub fn status(&self) -> Status {
|
||||
self.status.borrow().clone()
|
||||
}
|
||||
pub fn subscribe(&self) -> watch::Receiver<Status> {
|
||||
self.status.clone()
|
||||
}
|
||||
async fn request(&self, command: Command, duration: Duration) -> Result<(), String> {
|
||||
let (result, rx) = oneshot::channel();
|
||||
let request = Request {
|
||||
command,
|
||||
result,
|
||||
expires: Instant::now() + duration,
|
||||
};
|
||||
self.tx
|
||||
.try_send(request)
|
||||
.map_err(|_| "Bluetooth input queue unavailable or full".to_string())?;
|
||||
tokio::time::timeout(duration, rx)
|
||||
.await
|
||||
.map_err(|_| "Bluetooth operation timed out".to_string())?
|
||||
.map_err(|_| "Bluetooth worker stopped".to_string())?
|
||||
}
|
||||
pub async fn send(&self, report: Report, value: Vec<u8>) -> Result<(), String> {
|
||||
if value.len() != report.len() {
|
||||
return Err("Invalid HID report size".into());
|
||||
}
|
||||
self.request(Command::Report(report, value), Duration::from_millis(500))
|
||||
.await
|
||||
}
|
||||
pub async fn action(&self, action: Action) -> Result<(), String> {
|
||||
self.request(Command::Action(action), Duration::from_secs(15))
|
||||
.await
|
||||
}
|
||||
pub async fn shutdown(&self) -> Result<(), String> {
|
||||
if let Some(task) = self.task.lock().await.take() {
|
||||
let result = self.request(Command::Stop, Duration::from_secs(45)).await;
|
||||
// Do not abort: the worker must restore the radio before a backend switch.
|
||||
task.await.map_err(|e| e.to_string())?;
|
||||
result
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn listen(address: Address, psm: u16) -> Result<SeqPacketListener, String> {
|
||||
let socket = Socket::<SeqPacket>::new_seq_packet().map_err(|e| e.to_string())?;
|
||||
socket
|
||||
.set_security(Security {
|
||||
level: SecurityLevel::Medium,
|
||||
key_size: 0,
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
socket.bind(SocketAddr::new(address,AddressType::BrEdr,psm))
|
||||
.map_err(|e|format!("Cannot bind classic HID PSM 0x{psm:02x}: {e}. Disable BlueZ input plugin; see Bluetooth HID setup."))?;
|
||||
socket.listen(2).map_err(|e| e.to_string())
|
||||
}
|
||||
struct Runtime {
|
||||
adapter: Adapter,
|
||||
_session: Session,
|
||||
shared: Arc<Shared>,
|
||||
agent: Option<agent::Agent>,
|
||||
controller: controller::Controller,
|
||||
listeners: [SeqPacketListener; 2],
|
||||
channels: [Option<Arc<SeqPacket>>; 2],
|
||||
channel_peer: Option<Address>,
|
||||
partial_since: Option<Instant>,
|
||||
hid: protocol::HidProtocol,
|
||||
generation: u64,
|
||||
old_pairable: bool,
|
||||
old_powered: bool,
|
||||
old_alias: String,
|
||||
old_discoverable: bool,
|
||||
old_discoverable_timeout: u32,
|
||||
old_pairable_timeout: u32,
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
recorded_peer: Option<String>,
|
||||
}
|
||||
impl Runtime {
|
||||
async fn open(
|
||||
config: &Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
) -> Result<Self, String> {
|
||||
let controller = controller::Controller::acquire(&config.adapter).await?;
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let adapter = session
|
||||
.adapter(&config.adapter)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let address = adapter.address().await.map_err(|e| e.to_string())?;
|
||||
let mut config = config.clone();
|
||||
if let Some(store) = &bonds {
|
||||
bonds::clean_adapter(store.as_ref(), &adapter).await?;
|
||||
if config.peer.is_none() {
|
||||
config.peer = store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|bond| bond.adapter == address.to_string() && !bond.pending)
|
||||
.map(|bond| bond.peer);
|
||||
}
|
||||
}
|
||||
// Acquire BOTH channels before changing adapter state. Conflict is a startup error.
|
||||
let listeners = [listen(address, 0x11)?, listen(address, 0x13)?];
|
||||
let old_powered = adapter.is_powered().await.map_err(|e| e.to_string())?;
|
||||
let old_pairable = adapter.is_pairable().await.map_err(|e| e.to_string())?;
|
||||
let old_alias = adapter.alias().await.map_err(|e| e.to_string())?;
|
||||
let old_discoverable = adapter.is_discoverable().await.map_err(|e| e.to_string())?;
|
||||
let old_discoverable_timeout = adapter
|
||||
.discoverable_timeout()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let old_pairable_timeout = adapter
|
||||
.pairable_timeout()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let shared = Arc::new(Shared::new(
|
||||
config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||
));
|
||||
let mut runtime = Self {
|
||||
adapter,
|
||||
_session: session,
|
||||
shared,
|
||||
agent: None,
|
||||
controller,
|
||||
listeners,
|
||||
channels: [None, None],
|
||||
channel_peer: None,
|
||||
partial_since: None,
|
||||
hid: Default::default(),
|
||||
generation: 0,
|
||||
old_pairable,
|
||||
old_powered,
|
||||
old_alias,
|
||||
old_discoverable,
|
||||
old_discoverable_timeout,
|
||||
old_pairable_timeout,
|
||||
config: config.clone(),
|
||||
bonds,
|
||||
recorded_peer: None,
|
||||
};
|
||||
if let Err(e) = runtime.setup().await {
|
||||
let restore = runtime.close().await;
|
||||
return Err(format!("{e}; cleanup: {restore:?}"));
|
||||
}
|
||||
Ok(runtime)
|
||||
}
|
||||
async fn setup(&mut self) -> Result<(), String> {
|
||||
self.adapter
|
||||
.set_powered(true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_discoverable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_alias(self.config.name.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.agent =
|
||||
Some(agent::Agent::register(self.shared.clone(), self.config.adapter.clone()).await?);
|
||||
self.controller.configure().await?;
|
||||
self.public(None).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn public(&self, seconds: Option<u32>) -> Result<(), String> {
|
||||
if let Some(seconds) = seconds {
|
||||
self.adapter
|
||||
.set_pairable_timeout(seconds)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_discoverable_timeout(seconds)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(true)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if let Err(e) = self.adapter.set_discoverable(true).await {
|
||||
let _ = self.adapter.set_pairable(false).await;
|
||||
return Err(e.to_string());
|
||||
}
|
||||
} else {
|
||||
self.adapter
|
||||
.set_discoverable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.adapter
|
||||
.set_pairable(false)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn drop_channels(&mut self) {
|
||||
for socket in self.channels.iter_mut().filter_map(Option::take) {
|
||||
let _ = socket.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
self.channel_peer = None;
|
||||
self.partial_since = None;
|
||||
self.hid = Default::default();
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
async fn packet(&self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||
let socket = self.channels[index]
|
||||
.as_ref()
|
||||
.ok_or("Classic HID channel not connected")?;
|
||||
let sent = tokio::time::timeout(Duration::from_millis(250), socket.send(data))
|
||||
.await
|
||||
.map_err(|_| "HID send timeout")?
|
||||
.map_err(|e| e.to_string())?;
|
||||
if sent != data.len() {
|
||||
return Err("Short HID packet write".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn release(&mut self) {
|
||||
for packet in self.hid.release() {
|
||||
if self.channels[1].is_some() && self.packet(1, &packet).await.is_err() {
|
||||
self.drop_channels();
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
async fn accept(
|
||||
&mut self,
|
||||
index: usize,
|
||||
socket: SeqPacket,
|
||||
peer: SocketAddr,
|
||||
) -> Result<(), String> {
|
||||
if peer.addr_type != AddressType::BrEdr {
|
||||
return Ok(());
|
||||
}
|
||||
// A host can open HID channels before the next status poll.
|
||||
let paired = self
|
||||
.adapter
|
||||
.device(peer.addr)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.observe_bond(&peer.addr.to_string(), paired);
|
||||
if !self.shared.allowed(peer.addr) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.channel_peer.is_some_and(|p| p != peer.addr) || self.channels[index].is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
// Kernel BT_SECURITY_MEDIUM negotiates encryption before accept completes.
|
||||
if socket
|
||||
.as_ref()
|
||||
.security()
|
||||
.map_err(|e| e.to_string())?
|
||||
.key_size
|
||||
< 7
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
self.channel_peer = Some(peer.addr);
|
||||
self.channels[index] = Some(Arc::new(socket));
|
||||
if self.channels.iter().all(Option::is_some) {
|
||||
self.partial_since = None;
|
||||
self.release().await;
|
||||
} else {
|
||||
self.partial_since = Some(Instant::now());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn status(&mut self) -> Result<Status, String> {
|
||||
if !self.adapter.is_powered().await.map_err(|e| e.to_string())? {
|
||||
return Err("Bluetooth adapter powered off".into());
|
||||
}
|
||||
// A daemon restart loses the SDP registration even if the adapter comes back powered.
|
||||
if !self
|
||||
.adapter
|
||||
.uuids()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.any(|id| id.to_string() == protocol::HID_UUID)
|
||||
{
|
||||
return Err("Classic HID SDP registration lost; restarting Bluetooth backend".into());
|
||||
}
|
||||
let mut status = Status {
|
||||
initialized: true,
|
||||
adapter: self.config.adapter.clone(),
|
||||
adapter_address: self
|
||||
.adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
for address in self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
let device = self.adapter.device(address).map_err(|e| e.to_string())?;
|
||||
let connected = device.is_connected().await.unwrap_or(false);
|
||||
let paired = device.is_paired().await.unwrap_or(false);
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.observe_bond(&address.to_string(), paired);
|
||||
if paired || connected {
|
||||
status.devices.push(Device {
|
||||
address: address.to_string(),
|
||||
name: device.alias().await.unwrap_or_default(),
|
||||
paired,
|
||||
connected,
|
||||
});
|
||||
}
|
||||
}
|
||||
status.devices.sort_by(|a, b| a.address.cmp(&b.address));
|
||||
let (peer, until) = {
|
||||
let state = self.shared.state.lock().unwrap();
|
||||
(state.peer.clone(), state.pairing_until)
|
||||
};
|
||||
status.peer = peer.clone();
|
||||
let selected = status
|
||||
.devices
|
||||
.iter()
|
||||
.find(|device| Some(&device.address) == peer.as_ref());
|
||||
let selected_paired = selected.is_some_and(|device| device.paired);
|
||||
if selected_paired && self.recorded_peer != peer {
|
||||
if let (Some(store), Some(peer)) = (&self.bonds, &peer) {
|
||||
store
|
||||
.save(bonds::Bond {
|
||||
adapter: status.adapter_address.clone(),
|
||||
peer: peer.clone(),
|
||||
pending: false,
|
||||
})
|
||||
.await?;
|
||||
self.recorded_peer = Some(peer.clone());
|
||||
}
|
||||
}
|
||||
status.connected = selected.is_some_and(|device| device.paired && device.connected);
|
||||
if self.channels.iter().any(Option::is_some)
|
||||
&& (!status.connected
|
||||
|| self
|
||||
.partial_since
|
||||
.is_some_and(|time| time.elapsed() > Duration::from_secs(10)))
|
||||
{
|
||||
self.drop_channels();
|
||||
}
|
||||
if let Some(until) = until {
|
||||
let completed = self
|
||||
.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.pairing_completed(selected_paired);
|
||||
if until <= Instant::now() || completed {
|
||||
{
|
||||
let mut state = self.shared.state.lock().unwrap();
|
||||
state.close_pairing(
|
||||
selected_paired,
|
||||
self.config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||
);
|
||||
status.peer = state.peer.clone();
|
||||
}
|
||||
self.public(None).await?;
|
||||
tracing::info!(reason = if completed { "bonded" } else { "expired" }, peer = ?status.peer, "Bluetooth HID pairing window closed");
|
||||
} else {
|
||||
status.pairing_seconds =
|
||||
until.saturating_duration_since(Instant::now()).as_secs() as u32 + 1;
|
||||
}
|
||||
}
|
||||
status.control_connected = self.channels[0].is_some();
|
||||
status.interrupt_connected = self.channels[1].is_some();
|
||||
status.ready = status.connected
|
||||
&& status.control_connected
|
||||
&& status.interrupt_connected
|
||||
&& !self.hid.suspended;
|
||||
status.leds = self.hid.leds;
|
||||
status.generation = self.generation;
|
||||
Ok(status)
|
||||
}
|
||||
async fn forget(&mut self) -> Result<(), String> {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
if let Some(peer) = &peer {
|
||||
let address = peer.parse().map_err(|_| "Invalid peer")?;
|
||||
if self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.contains(&address)
|
||||
{
|
||||
self.adapter
|
||||
.remove_device(address)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.bonded_before_pairing
|
||||
.remove(peer);
|
||||
tracing::info!(%peer, "Bluetooth HID forgot computer");
|
||||
}
|
||||
if let Some(store) = &self.bonds {
|
||||
let address = self
|
||||
.adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string();
|
||||
for bond in store
|
||||
.list()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|b| b.adapter == address && Some(&b.peer) == peer.as_ref())
|
||||
{
|
||||
store.remove(bond).await?;
|
||||
}
|
||||
}
|
||||
self.recorded_peer = None;
|
||||
self.config.peer = None;
|
||||
self.shared.state.lock().unwrap().peer = None;
|
||||
Ok(())
|
||||
}
|
||||
async fn incoming(&mut self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||
if index == 1 {
|
||||
self.hid.output(data);
|
||||
return Ok(());
|
||||
}
|
||||
let result = self.hid.control(data);
|
||||
if let Some(reply) = result.reply {
|
||||
self.packet(0, &reply).await?;
|
||||
}
|
||||
if result.reset {
|
||||
self.release().await;
|
||||
}
|
||||
if result.unplug {
|
||||
self.forget().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn execute(&mut self, command: Command) -> Result<(), String> {
|
||||
match command {
|
||||
Command::Report(kind, value) => {
|
||||
let peer = self.channel_peer.ok_or("Classic HID is not connected")?;
|
||||
if !self.shared.allowed(peer) || self.channels.iter().any(Option::is_none) {
|
||||
return Err("Classic HID channels are not ready".into());
|
||||
}
|
||||
let device = self.adapter.device(peer).map_err(|e| e.to_string())?;
|
||||
if !device.is_paired().await.unwrap_or(false)
|
||||
|| !device.is_connected().await.unwrap_or(false)
|
||||
{
|
||||
self.drop_channels();
|
||||
return Err("Selected Bluetooth computer disconnected".into());
|
||||
}
|
||||
let packet = self.hid.input(kind, &value)?;
|
||||
if let Err(e) = self.packet(1, &packet).await {
|
||||
self.drop_channels();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Command::Action(Action::Pair(seconds)) => {
|
||||
if !(10..=300).contains(&seconds) {
|
||||
return Err("Pairing window must be 10–300 seconds".into());
|
||||
}
|
||||
let mut bonded = HashSet::new();
|
||||
for address in self
|
||||
.adapter
|
||||
.device_addresses()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
if self
|
||||
.adapter
|
||||
.device(address)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
{
|
||||
bonded.insert(address.to_string());
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut state = self.shared.state.lock().unwrap();
|
||||
// Preserve the original baseline when extending an open window.
|
||||
if !state
|
||||
.pairing_until
|
||||
.is_some_and(|until| until > Instant::now())
|
||||
{
|
||||
state.bonded_before_pairing = bonded;
|
||||
}
|
||||
state.pairing_until =
|
||||
Some(Instant::now() + Duration::from_secs(seconds.into()));
|
||||
}
|
||||
if let Err(e) = self.public(Some(seconds)).await {
|
||||
self.shared.state.lock().unwrap().pairing_until = None;
|
||||
return Err(e);
|
||||
}
|
||||
tracing::info!(seconds, "Bluetooth HID pairing window opened");
|
||||
}
|
||||
Command::Action(Action::ClosePairing) => {
|
||||
self.public(None).await?;
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
let paired = if let Some(peer) = peer {
|
||||
self.adapter
|
||||
.device(peer.parse().map_err(|_| "Invalid HID peer")?)
|
||||
.map_err(|e| e.to_string())?
|
||||
.is_paired()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
self.shared
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.close_pairing(paired, self.config.peer.clone());
|
||||
tracing::info!(reason = "manual", "Bluetooth HID pairing window closed");
|
||||
}
|
||||
Command::Action(Action::Forget) => self.forget().await?,
|
||||
Command::Action(Action::Disconnect) => {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||
if let Some(peer) = peer {
|
||||
self.adapter
|
||||
.device(peer.parse().map_err(|_| "Invalid peer")?)
|
||||
.map_err(|e| e.to_string())?
|
||||
.disconnect()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
Command::Action(Action::Reset) => self.release().await,
|
||||
Command::Stop => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
async fn close(&mut self) -> Result<(), String> {
|
||||
self.release().await;
|
||||
self.drop_channels();
|
||||
self.agent.take();
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
let mut errors = vec![];
|
||||
if let Err(e) = self.adapter.set_discoverable(false).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.controller.restore().await {
|
||||
errors.push(e);
|
||||
}
|
||||
if let Err(e) = self.adapter.set_alias(self.old_alias.clone()).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self
|
||||
.adapter
|
||||
.set_discoverable_timeout(self.old_discoverable_timeout)
|
||||
.await
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self
|
||||
.adapter
|
||||
.set_pairable_timeout(self.old_pairable_timeout)
|
||||
.await
|
||||
{
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_pairable(self.old_pairable).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_discoverable(self.old_discoverable).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if let Err(e) = self.adapter.set_powered(self.old_powered).await {
|
||||
errors.push(e.to_string());
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn receive(socket: Option<Arc<SeqPacket>>, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
match socket {
|
||||
Some(socket) => socket.recv(buffer).await,
|
||||
None => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
async fn supervise(
|
||||
config: Config,
|
||||
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||
mut rx: mpsc::Receiver<Request>,
|
||||
tx: watch::Sender<Status>,
|
||||
) {
|
||||
let mut generation = 0;
|
||||
loop {
|
||||
let mut runtime = match Runtime::open(&config, bonds.clone()).await {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
tx.send_replace(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
error: Some(error.clone()),
|
||||
generation,
|
||||
..Default::default()
|
||||
});
|
||||
let delay = tokio::time::sleep(Duration::from_secs(5));
|
||||
tokio::pin!(delay);
|
||||
loop {
|
||||
tokio::select! {_=&mut delay=>break,request=rx.recv()=>{
|
||||
let Some(request)=request else{return;};
|
||||
if matches!(request.command,Command::Stop){let _=request.result.send(Ok(()));return;}
|
||||
let _=request.result.send(Err(error.clone()));
|
||||
}}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
runtime.generation = generation;
|
||||
let mut timer = tokio::time::interval(Duration::from_millis(500));
|
||||
let failure = loop {
|
||||
let ctl = runtime.channels[0].clone();
|
||||
let intr = runtime.channels[1].clone();
|
||||
let mut ctl_buffer = [0; 1024];
|
||||
let mut intr_buffer = [0; 1024];
|
||||
tokio::select! {
|
||||
accepted=runtime.listeners[0].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(0,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||
accepted=runtime.listeners[1].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(1,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||
read=receive(ctl,&mut ctl_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(0,&ctl_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||
read=receive(intr,&mut intr_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(1,&intr_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||
_=timer.tick()=>{match runtime.status().await{Ok(status)=>{tx.send_if_modified(|old|{if *old!=status{*old=status;true}else{false}});},Err(e)=>break e}}
|
||||
request=rx.recv()=>{
|
||||
let Some(request)=request else{let _=runtime.close().await;return;};
|
||||
if matches!(request.command,Command::Stop){let result=runtime.close().await;tx.send_replace(Status{adapter:config.adapter.clone(),..Default::default()});let _=request.result.send(result);return;}
|
||||
if request.expires<=Instant::now()||request.result.is_closed(){runtime.release().await;continue;}
|
||||
let action = matches!(&request.command, Command::Action(_));
|
||||
let result=runtime.execute(request.command).await;
|
||||
if result.is_err(){runtime.release().await;}
|
||||
if action {
|
||||
if let Ok(status) = runtime.status().await { tx.send_replace(status); }
|
||||
}
|
||||
let _=request.result.send(result);
|
||||
}
|
||||
}
|
||||
};
|
||||
let restore = runtime.close().await;
|
||||
generation = runtime.generation.wrapping_add(1);
|
||||
tx.send_replace(Status {
|
||||
adapter: config.adapter.clone(),
|
||||
error: Some(format!("{failure}; cleanup: {restore:?}")),
|
||||
generation,
|
||||
..Default::default()
|
||||
});
|
||||
drop(runtime);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn closing_pairing_preserves_completed_bonds_but_releases_unpaired_targets() {
|
||||
let shared = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.close_pairing(true, None);
|
||||
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:88"));
|
||||
assert!(state.pairing_until.is_none());
|
||||
state.close_pairing(false, None);
|
||||
assert!(state.peer.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn configuration_validation() {
|
||||
let mut c = Config {
|
||||
adapter: "hci0".into(),
|
||||
name: "One-KVM".into(),
|
||||
peer: None,
|
||||
};
|
||||
assert!(c.validate().is_ok());
|
||||
c.adapter = "hci0;reboot".into();
|
||||
assert!(c.validate().is_err());
|
||||
c.adapter = "hci1".into();
|
||||
c.peer = Some("bogus".into());
|
||||
assert!(c.validate().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn peer_isolation() {
|
||||
let state = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||
assert!(state.allowed("10:6F:D9:66:97:88".parse().unwrap()));
|
||||
assert!(!state.allowed("10:6F:D9:66:97:89".parse().unwrap()));
|
||||
}
|
||||
#[test]
|
||||
fn new_bond_without_agent_callback_completes_pairing() {
|
||||
let shared = Shared::new(None);
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
state.observe_bond(peer, false);
|
||||
assert_eq!(state.peer, None);
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||
assert!(state.pairing_completed(true));
|
||||
// A second computer cannot replace the one selected in this window.
|
||||
state.observe_bond("10:6F:D9:66:97:89", true);
|
||||
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||
}
|
||||
#[test]
|
||||
fn old_bond_does_not_finish_a_new_window() {
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
let shared = Shared::new(Some(peer.into()));
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.bonded_before_pairing.insert(peer.into());
|
||||
state.observe_bond(peer, true);
|
||||
assert!(!state.pairing_completed(true));
|
||||
state.peer = None;
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
}
|
||||
#[test]
|
||||
fn bond_observation_respects_closed_window_and_pinned_peer() {
|
||||
let peer = "10:6F:D9:66:97:88";
|
||||
let shared = Shared::new(None);
|
||||
let mut state = shared.state.lock().unwrap();
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
state.pairing_until = Some(Instant::now() - Duration::from_secs(1));
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer, None);
|
||||
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||
state.peer = Some("10:6F:D9:66:97:89".into());
|
||||
state.observe_bond(peer, true);
|
||||
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:89"));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AdapterInfo {
|
||||
pub name: String,
|
||||
pub address: String,
|
||||
pub powered: bool,
|
||||
}
|
||||
pub async fn adapters() -> Result<Vec<AdapterInfo>, String> {
|
||||
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||
let mut output = vec![];
|
||||
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||
let adapter = session.adapter(&name).map_err(|e| e.to_string())?;
|
||||
output.push(AdapterInfo {
|
||||
name,
|
||||
address: adapter
|
||||
.address()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_string(),
|
||||
powered: adapter.is_powered().await.map_err(|e| e.to_string())?,
|
||||
});
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
267
libs/bluetooth-hid/src/protocol.rs
Normal file
267
libs/bluetooth-hid/src/protocol.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! Classic HIDP framing and report descriptor.
|
||||
use crate::Report;
|
||||
pub const MAP: &[u8] = &[
|
||||
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
|
||||
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
||||
0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
||||
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
||||
0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19,
|
||||
0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75,
|
||||
0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75,
|
||||
0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x15,
|
||||
0x00, 0x26, 0xff, 0x03, 0x19, 0x00, 0x2a, 0xff, 0x03, 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, 0xc0,
|
||||
];
|
||||
|
||||
pub const HID_UUID: &str = "00001124-0000-1000-8000-00805f9b34fb";
|
||||
pub fn sdp() -> String {
|
||||
let hex: String = MAP.iter().map(|byte| format!("{byte:02x}")).collect();
|
||||
format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<record>
|
||||
<attribute id="0x0001"><sequence><uuid value="0x1124"/></sequence></attribute>
|
||||
<attribute id="0x0004"><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0011"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0005"><sequence><uuid value="0x1002"/></sequence></attribute>
|
||||
<attribute id="0x0006"><sequence><uint16 value="0x656e"/><uint16 value="0x006a"/><uint16 value="0x0100"/></sequence></attribute>
|
||||
<attribute id="0x0009"><sequence><sequence><uuid value="0x1124"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||
<attribute id="0x000d"><sequence><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0013"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></sequence></attribute>
|
||||
<attribute id="0x0100"><text value="One-KVM Keyboard and Mouse"/></attribute>
|
||||
<attribute id="0x0101"><text value="Classic Bluetooth HID"/></attribute>
|
||||
<attribute id="0x0102"><text value="One-KVM"/></attribute>
|
||||
<attribute id="0x0200"><uint16 value="0x0100"/></attribute>
|
||||
<attribute id="0x0201"><uint16 value="0x0111"/></attribute>
|
||||
<attribute id="0x0202"><uint8 value="0xc0"/></attribute>
|
||||
<attribute id="0x0203"><uint8 value="0x00"/></attribute>
|
||||
<attribute id="0x0204"><boolean value="false"/></attribute>
|
||||
<attribute id="0x0205"><boolean value="false"/></attribute>
|
||||
<attribute id="0x0206"><sequence><sequence><uint8 value="0x22"/><text encoding="hex" value="{hex}"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0207"><sequence><sequence><uint16 value="0x0409"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||
<attribute id="0x0209"><boolean value="false"/></attribute>
|
||||
<attribute id="0x020a"><boolean value="false"/></attribute>
|
||||
<attribute id="0x020b"><uint16 value="0x0100"/></attribute>
|
||||
<attribute id="0x020c"><uint16 value="0x0c80"/></attribute>
|
||||
<attribute id="0x020d"><boolean value="true"/></attribute>
|
||||
<attribute id="0x020e"><boolean value="true"/></attribute>
|
||||
</record>"#
|
||||
)
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HidProtocol {
|
||||
pub boot: bool,
|
||||
pub suspended: bool,
|
||||
pub leds: u8,
|
||||
pub keyboard: [u8; 8],
|
||||
pub buttons: u8,
|
||||
pub consumer: [u8; 2],
|
||||
}
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct ControlResult {
|
||||
pub reply: Option<Vec<u8>>,
|
||||
pub unplug: bool,
|
||||
pub reset: bool,
|
||||
}
|
||||
impl HidProtocol {
|
||||
pub fn input(&mut self, kind: Report, value: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if value.len() != kind.len() {
|
||||
return Err("Invalid HID report length".into());
|
||||
}
|
||||
if self.suspended {
|
||||
return Err("Bluetooth HID is suspended".into());
|
||||
}
|
||||
let mut packet = vec![0xa1, kind.id()];
|
||||
match kind {
|
||||
Report::Keyboard => {
|
||||
self.keyboard.copy_from_slice(value);
|
||||
packet.extend_from_slice(value);
|
||||
}
|
||||
Report::Mouse => {
|
||||
self.buttons = value[0];
|
||||
packet.extend_from_slice(&value[..if self.boot { 3 } else { 4 }]);
|
||||
if self.boot {
|
||||
packet[2] &= 7;
|
||||
}
|
||||
}
|
||||
Report::Consumer if !self.boot => {
|
||||
self.consumer.copy_from_slice(value);
|
||||
packet.extend_from_slice(value);
|
||||
}
|
||||
Report::Consumer => return Err("Consumer keys unavailable in boot protocol".into()),
|
||||
}
|
||||
Ok(packet)
|
||||
}
|
||||
pub fn release(&mut self) -> Vec<Vec<u8>> {
|
||||
self.keyboard = [0; 8];
|
||||
self.buttons = 0;
|
||||
self.consumer = [0; 2];
|
||||
let mut packets = vec![
|
||||
vec![0xa1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
if self.boot {
|
||||
vec![0xa1, 2, 0, 0, 0]
|
||||
} else {
|
||||
vec![0xa1, 2, 0, 0, 0, 0]
|
||||
},
|
||||
];
|
||||
if !self.boot {
|
||||
packets.push(vec![0xa1, 3, 0, 0]);
|
||||
}
|
||||
packets
|
||||
}
|
||||
pub fn output(&mut self, data: &[u8]) -> bool {
|
||||
if data.len() == 3 && data[0] == 0xa2 && data[1] == 1 {
|
||||
self.leds = data[2] & 0x1f;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn control(&mut self, data: &[u8]) -> ControlResult {
|
||||
let mut result = ControlResult::default();
|
||||
let handshake = |code| ControlResult {
|
||||
reply: Some(vec![code]),
|
||||
..Default::default()
|
||||
};
|
||||
let Some(&header) = data.first() else {
|
||||
return handshake(4);
|
||||
};
|
||||
match header {
|
||||
0x13 if data.len() == 1 => {
|
||||
self.suspended = true;
|
||||
result.reset = true;
|
||||
}
|
||||
0x14 if data.len() == 1 => {
|
||||
self.suspended = false;
|
||||
result.reset = true;
|
||||
}
|
||||
0x15 if data.len() == 1 => {
|
||||
result.unplug = true;
|
||||
}
|
||||
0x11 | 0x12 if data.len() == 1 => {
|
||||
self.suspended = false;
|
||||
result.reset = true;
|
||||
}
|
||||
0x60 if data.len() == 1 => {
|
||||
result.reply = Some(vec![0xa0, if self.boot { 0 } else { 1 }]);
|
||||
}
|
||||
0x70 | 0x71 if data.len() == 1 => {
|
||||
self.boot = header == 0x70;
|
||||
result.reply = Some(vec![0]);
|
||||
result.reset = true;
|
||||
}
|
||||
0x52 if data.len() == 3 && data[1] == 1 => {
|
||||
self.leds = data[2] & 0x1f;
|
||||
return handshake(0);
|
||||
}
|
||||
0x41 | 0x49 | 0x42 | 0x4a => {
|
||||
let sized = header & 8 != 0;
|
||||
if data.len() != if sized { 4 } else { 2 } {
|
||||
return handshake(4);
|
||||
}
|
||||
let mut report = vec![0xa0 | (header & 3), data[1]];
|
||||
match (header & 3, data[1]) {
|
||||
(1, 1) => report.extend(self.keyboard),
|
||||
(1, 2) => report.extend(if self.boot {
|
||||
vec![self.buttons & 7, 0, 0]
|
||||
} else {
|
||||
vec![self.buttons, 0, 0, 0]
|
||||
}),
|
||||
(1, 3) if !self.boot => report.extend(self.consumer),
|
||||
(2, 1) => report.push(self.leds),
|
||||
_ => return handshake(2),
|
||||
}
|
||||
if sized {
|
||||
report.truncate(1 + u16::from_le_bytes([data[2], data[3]]) as usize);
|
||||
}
|
||||
result.reply = Some(report);
|
||||
}
|
||||
_ => return handshake(3), // ERR_UNSUPPORTED_REQUEST
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn classic_frames_and_boot_protocol() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert_eq!(
|
||||
hid.input(Report::Keyboard, &[2, 0, 4, 0, 0, 0, 0, 0])
|
||||
.unwrap(),
|
||||
[0xa1, 1, 2, 0, 4, 0, 0, 0, 0, 0]
|
||||
);
|
||||
assert_eq!(hid.control(&[0x60]).reply.unwrap(), [0xa0, 1]);
|
||||
assert_eq!(hid.control(&[0x70]).reply.unwrap(), [0]);
|
||||
assert_eq!(
|
||||
hid.input(Report::Mouse, &[0x1f, 127, 128, 1]).unwrap(),
|
||||
[0xa1, 2, 7, 127, 128]
|
||||
);
|
||||
assert!(hid.input(Report::Consumer, &[0, 0]).is_err());
|
||||
assert!(hid.input(Report::Keyboard, &[0]).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn led_read_write_and_errors() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert!(hid.output(&[0xa2, 1, 3]));
|
||||
assert_eq!(hid.leds, 3);
|
||||
assert_eq!(hid.control(&[0x42, 1]).reply.unwrap(), [0xa2, 1, 3]);
|
||||
assert_eq!(hid.control(&[0x52, 1, 2]).reply.unwrap(), [0]);
|
||||
assert_eq!(hid.leds, 2);
|
||||
assert_eq!(hid.control(&[0x41, 99]).reply.unwrap(), [2]);
|
||||
assert_eq!(hid.control(&[0x41]).reply.unwrap(), [4]);
|
||||
assert_eq!(hid.control(&[0x90, 0]).reply.unwrap(), [3]);
|
||||
assert_eq!(
|
||||
hid.control(&[0x49, 1, 3, 0]).reply.unwrap(),
|
||||
[0xa1, 1, 0, 0]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn suspend_reset_and_virtual_unplug() {
|
||||
let mut hid = HidProtocol::default();
|
||||
assert!(hid.control(&[0x13]).reset);
|
||||
assert!(hid.input(Report::Mouse, &[0; 4]).is_err());
|
||||
assert!(hid.control(&[0x14]).reset);
|
||||
assert!(hid.input(Report::Mouse, &[0; 4]).is_ok());
|
||||
assert!(hid.control(&[0x15]).unplug);
|
||||
hid.keyboard[2] = 4;
|
||||
hid.buttons = 1;
|
||||
assert_eq!(hid.release().len(), 3);
|
||||
assert_eq!(hid.keyboard, [0; 8]);
|
||||
assert_eq!(hid.buttons, 0);
|
||||
}
|
||||
#[test]
|
||||
fn sdp_describes_both_classic_channels() {
|
||||
let record = sdp();
|
||||
assert!(record.contains("uuid value=\"0x1124\""));
|
||||
assert!(record.contains("uint16 value=\"0x0011\""));
|
||||
assert!(record.contains("uint16 value=\"0x0013\""));
|
||||
assert!(!record.contains("0x1812"));
|
||||
}
|
||||
#[test]
|
||||
fn descriptor_report_sizes() {
|
||||
let (mut pos, mut size, mut count, mut id, mut depth) = (0, 0, 0, 0, 0);
|
||||
let mut input = [0; 4];
|
||||
let mut output = [0; 4];
|
||||
while pos < MAP.len() {
|
||||
let prefix = MAP[pos];
|
||||
pos += 1;
|
||||
let length = [0, 1, 2, 4][(prefix & 3) as usize];
|
||||
let mut value = 0;
|
||||
for i in 0..length {
|
||||
value |= (MAP[pos + i] as usize) << (8 * i);
|
||||
}
|
||||
pos += length;
|
||||
match ((prefix >> 2) & 3, prefix >> 4) {
|
||||
(1, 7) => size = value,
|
||||
(1, 8) => id = value,
|
||||
(1, 9) => count = value,
|
||||
(0, 8) => input[id] += size * count,
|
||||
(0, 9) => output[id] += size * count,
|
||||
(0, 10) => depth += 1,
|
||||
(0, 12) => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(depth, 0);
|
||||
assert_eq!(input, [0, 64, 32, 16]);
|
||||
assert_eq!(output, [0, 8, 0, 0]);
|
||||
}
|
||||
}
|
||||
155
libs/dtls/Cargo.toml
Normal file
155
libs/dtls/Cargo.toml
Normal file
@@ -0,0 +1,155 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "dtls"
|
||||
version = "0.17.2"
|
||||
authors = ["Rain Liu <yliu@webrtc.rs>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A pure Rust implementation of DTLS"
|
||||
homepage = "https://webrtc.rs"
|
||||
documentation = "https://docs.rs/dtls"
|
||||
readme = "README.md"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/webrtc-rs/webrtc/tree/master/dtls"
|
||||
|
||||
[features]
|
||||
pem = ["dep:pem"]
|
||||
|
||||
[lib]
|
||||
name = "dtls"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.aes]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.aes-gcm]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.async-trait]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.bytecheck]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.byteorder]
|
||||
version = "1"
|
||||
|
||||
[dependencies.cbc]
|
||||
version = "0.1"
|
||||
features = [
|
||||
"block-padding",
|
||||
"alloc",
|
||||
]
|
||||
|
||||
[dependencies.ccm]
|
||||
version = "0.5"
|
||||
|
||||
[dependencies.chacha20poly1305]
|
||||
version = "0.10.1"
|
||||
|
||||
[dependencies.der-parser]
|
||||
version = "9.0"
|
||||
|
||||
[dependencies.hmac]
|
||||
version = "0.12"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.p256]
|
||||
version = "0.13"
|
||||
features = [
|
||||
"default",
|
||||
"ecdh",
|
||||
"ecdsa",
|
||||
]
|
||||
|
||||
[dependencies.p384]
|
||||
version = "0.13"
|
||||
|
||||
[dependencies.pem]
|
||||
version = "3"
|
||||
optional = true
|
||||
|
||||
[dependencies.portable-atomic]
|
||||
version = "1.6"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[dependencies.rand_core]
|
||||
version = "0.6"
|
||||
|
||||
[dependencies.rcgen]
|
||||
version = "0.13"
|
||||
|
||||
[dependencies.ring]
|
||||
version = "0.17.14"
|
||||
|
||||
[dependencies.rkyv]
|
||||
version = "0.8"
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.10"
|
||||
features = ["vendored"]
|
||||
|
||||
[dependencies.rustls-pki-types]
|
||||
version = "1"
|
||||
|
||||
[dependencies.sec1]
|
||||
version = "0.7"
|
||||
features = ["std"]
|
||||
|
||||
[dependencies.sha1]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "1"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.32.0"
|
||||
features = [
|
||||
"fs",
|
||||
"io-util",
|
||||
"io-std",
|
||||
"macros",
|
||||
"net",
|
||||
"parking_lot",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"sync",
|
||||
"time",
|
||||
]
|
||||
|
||||
[dependencies.util]
|
||||
version = "0.17.1"
|
||||
features = ["conn"]
|
||||
default-features = false
|
||||
package = "webrtc-util"
|
||||
|
||||
[dependencies.x25519-dalek]
|
||||
version = "2"
|
||||
features = ["static_secrets"]
|
||||
|
||||
[dependencies.x509-parser]
|
||||
version = "0.16"
|
||||
|
||||
[dev-dependencies]
|
||||
201
libs/dtls/LICENSE-APACHE
Normal file
201
libs/dtls/LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
libs/dtls/LICENSE-MIT
Normal file
21
libs/dtls/LICENSE-MIT
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 WebRTC.rs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
20
libs/dtls/README.md
Normal file
20
libs/dtls/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# DTLS certificate verification backend
|
||||
|
||||
This directory contains `dtls` 0.17.2 from the crates.io release of webrtc-rs.
|
||||
The original MIT and Apache-2.0 licenses are preserved.
|
||||
|
||||
One-KVM patches only certificate-chain verification and its configuration to
|
||||
use vendored OpenSSL instead of rustls/WebPKI. DTLS flights, record protection,
|
||||
certificate signatures, SRTP negotiation and the application fingerprint check
|
||||
remain unchanged. `rustls-pki-types` remains a certificate-data representation,
|
||||
not a TLS implementation.
|
||||
|
||||
`certificate_verifier.rs` implements trust-anchor, purpose, certificate-validity
|
||||
and DNS/IP identity checks. Server names do not fall back to the certificate
|
||||
subject, and partial-label wildcards are rejected. No trust checks are skipped
|
||||
by this backend. The upstream application-controlled `insecure_skip_verify`
|
||||
policy is preserved, including WebRTC's separate certificate fingerprint check.
|
||||
Authentication security level 2 rejects weak RSA keys and SHA-1 leaf signatures.
|
||||
|
||||
Run `cargo test --manifest-path libs/dtls/Cargo.toml --lib` after updating this
|
||||
fork, and verify the application dependency tree contains no `rustls` engine.
|
||||
50
libs/dtls/src/alert/alert_test.rs
Normal file
50
libs/dtls/src/alert/alert_test.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::io::{BufReader, BufWriter};
|
||||
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
|
||||
#[test]
|
||||
fn test_alert() -> Result<()> {
|
||||
let tests = vec![
|
||||
(
|
||||
"Valid Alert",
|
||||
vec![0x02, 0x0A],
|
||||
Alert {
|
||||
alert_level: AlertLevel::Fatal,
|
||||
alert_description: AlertDescription::UnexpectedMessage,
|
||||
},
|
||||
None,
|
||||
),
|
||||
(
|
||||
"Invalid alert length",
|
||||
vec![0x00],
|
||||
Alert {
|
||||
alert_level: AlertLevel::Invalid,
|
||||
alert_description: AlertDescription::Invalid,
|
||||
},
|
||||
Some(Error::Other("io".to_owned())),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, data, wanted, unmarshal_error) in tests {
|
||||
let mut reader = BufReader::new(data.as_slice());
|
||||
let result = Alert::unmarshal(&mut reader);
|
||||
|
||||
if let Some(err) = unmarshal_error {
|
||||
assert!(result.is_err(), "{name} expected error: {err}");
|
||||
} else if let Ok(alert) = result {
|
||||
assert_eq!(wanted, alert, "{name} expected {wanted}, but got {alert}");
|
||||
|
||||
let mut data2: Vec<u8> = vec![];
|
||||
{
|
||||
let mut writer = BufWriter::<&mut Vec<u8>>::new(data2.as_mut());
|
||||
alert.marshal(&mut writer)?;
|
||||
}
|
||||
assert_eq!(data, data2, "{name} expected {data:?}, but got {data2:?}");
|
||||
} else {
|
||||
assert!(result.is_ok(), "{name} expected Ok, but has error");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
189
libs/dtls/src/alert/mod.rs
Normal file
189
libs/dtls/src/alert/mod.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
#[cfg(test)]
|
||||
mod alert_test;
|
||||
|
||||
use std::fmt;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use super::content::*;
|
||||
use crate::error::Result;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub(crate) enum AlertLevel {
|
||||
Warning = 1,
|
||||
Fatal = 2,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl fmt::Display for AlertLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AlertLevel::Warning => write!(f, "LevelWarning"),
|
||||
AlertLevel::Fatal => write!(f, "LevelFatal"),
|
||||
_ => write!(f, "Invalid alert level"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for AlertLevel {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
1 => AlertLevel::Warning,
|
||||
2 => AlertLevel::Fatal,
|
||||
_ => AlertLevel::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub(crate) enum AlertDescription {
|
||||
CloseNotify = 0,
|
||||
UnexpectedMessage = 10,
|
||||
BadRecordMac = 20,
|
||||
DecryptionFailed = 21,
|
||||
RecordOverflow = 22,
|
||||
DecompressionFailure = 30,
|
||||
HandshakeFailure = 40,
|
||||
NoCertificate = 41,
|
||||
BadCertificate = 42,
|
||||
UnsupportedCertificate = 43,
|
||||
CertificateRevoked = 44,
|
||||
CertificateExpired = 45,
|
||||
CertificateUnknown = 46,
|
||||
IllegalParameter = 47,
|
||||
UnknownCa = 48,
|
||||
AccessDenied = 49,
|
||||
DecodeError = 50,
|
||||
DecryptError = 51,
|
||||
ExportRestriction = 60,
|
||||
ProtocolVersion = 70,
|
||||
InsufficientSecurity = 71,
|
||||
InternalError = 80,
|
||||
UserCanceled = 90,
|
||||
NoRenegotiation = 100,
|
||||
UnsupportedExtension = 110,
|
||||
UnknownPskIdentity = 115,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl fmt::Display for AlertDescription {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AlertDescription::CloseNotify => write!(f, "CloseNotify"),
|
||||
AlertDescription::UnexpectedMessage => write!(f, "UnexpectedMessage"),
|
||||
AlertDescription::BadRecordMac => write!(f, "BadRecordMac"),
|
||||
AlertDescription::DecryptionFailed => write!(f, "DecryptionFailed"),
|
||||
AlertDescription::RecordOverflow => write!(f, "RecordOverflow"),
|
||||
AlertDescription::DecompressionFailure => write!(f, "DecompressionFailure"),
|
||||
AlertDescription::HandshakeFailure => write!(f, "HandshakeFailure"),
|
||||
AlertDescription::NoCertificate => write!(f, "NoCertificate"),
|
||||
AlertDescription::BadCertificate => write!(f, "BadCertificate"),
|
||||
AlertDescription::UnsupportedCertificate => write!(f, "UnsupportedCertificate"),
|
||||
AlertDescription::CertificateRevoked => write!(f, "CertificateRevoked"),
|
||||
AlertDescription::CertificateExpired => write!(f, "CertificateExpired"),
|
||||
AlertDescription::CertificateUnknown => write!(f, "CertificateUnknown"),
|
||||
AlertDescription::IllegalParameter => write!(f, "IllegalParameter"),
|
||||
AlertDescription::UnknownCa => write!(f, "UnknownCA"),
|
||||
AlertDescription::AccessDenied => write!(f, "AccessDenied"),
|
||||
AlertDescription::DecodeError => write!(f, "DecodeError"),
|
||||
AlertDescription::DecryptError => write!(f, "DecryptError"),
|
||||
AlertDescription::ExportRestriction => write!(f, "ExportRestriction"),
|
||||
AlertDescription::ProtocolVersion => write!(f, "ProtocolVersion"),
|
||||
AlertDescription::InsufficientSecurity => write!(f, "InsufficientSecurity"),
|
||||
AlertDescription::InternalError => write!(f, "InternalError"),
|
||||
AlertDescription::UserCanceled => write!(f, "UserCanceled"),
|
||||
AlertDescription::NoRenegotiation => write!(f, "NoRenegotiation"),
|
||||
AlertDescription::UnsupportedExtension => write!(f, "UnsupportedExtension"),
|
||||
AlertDescription::UnknownPskIdentity => write!(f, "UnknownPskIdentity"),
|
||||
_ => write!(f, "Invalid alert description"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for AlertDescription {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
0 => AlertDescription::CloseNotify,
|
||||
10 => AlertDescription::UnexpectedMessage,
|
||||
20 => AlertDescription::BadRecordMac,
|
||||
21 => AlertDescription::DecryptionFailed,
|
||||
22 => AlertDescription::RecordOverflow,
|
||||
30 => AlertDescription::DecompressionFailure,
|
||||
40 => AlertDescription::HandshakeFailure,
|
||||
41 => AlertDescription::NoCertificate,
|
||||
42 => AlertDescription::BadCertificate,
|
||||
43 => AlertDescription::UnsupportedCertificate,
|
||||
44 => AlertDescription::CertificateRevoked,
|
||||
45 => AlertDescription::CertificateExpired,
|
||||
46 => AlertDescription::CertificateUnknown,
|
||||
47 => AlertDescription::IllegalParameter,
|
||||
48 => AlertDescription::UnknownCa,
|
||||
49 => AlertDescription::AccessDenied,
|
||||
50 => AlertDescription::DecodeError,
|
||||
51 => AlertDescription::DecryptError,
|
||||
60 => AlertDescription::ExportRestriction,
|
||||
70 => AlertDescription::ProtocolVersion,
|
||||
71 => AlertDescription::InsufficientSecurity,
|
||||
80 => AlertDescription::InternalError,
|
||||
90 => AlertDescription::UserCanceled,
|
||||
100 => AlertDescription::NoRenegotiation,
|
||||
110 => AlertDescription::UnsupportedExtension,
|
||||
115 => AlertDescription::UnknownPskIdentity,
|
||||
_ => AlertDescription::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One of the content types supported by the TLS record layer is the
|
||||
// alert type. Alert messages convey the severity of the message
|
||||
// (warning or fatal) and a description of the alert. Alert messages
|
||||
// with a level of fatal result in the immediate termination of the
|
||||
// connection. In this case, other connections corresponding to the
|
||||
// session may continue, but the session identifier MUST be invalidated,
|
||||
// preventing the failed session from being used to establish new
|
||||
// connections. Like other messages, alert messages are encrypted and
|
||||
// compressed, as specified by the current connection state.
|
||||
/// ## Specifications
|
||||
///
|
||||
/// * [RFC 5246 §7.2]
|
||||
///
|
||||
/// [RFC 5246 §7.2]: https://tools.ietf.org/html/rfc5246#section-7.2
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub struct Alert {
|
||||
pub(crate) alert_level: AlertLevel,
|
||||
pub(crate) alert_description: AlertDescription,
|
||||
}
|
||||
|
||||
impl fmt::Display for Alert {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Alert {}: {}", self.alert_level, self.alert_description)
|
||||
}
|
||||
}
|
||||
|
||||
impl Alert {
|
||||
pub fn content_type(&self) -> ContentType {
|
||||
ContentType::Alert
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_u8(self.alert_level as u8)?;
|
||||
writer.write_u8(self.alert_description as u8)?;
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
let alert_level = reader.read_u8()?.into();
|
||||
let alert_description = reader.read_u8()?.into();
|
||||
|
||||
Ok(Alert {
|
||||
alert_level,
|
||||
alert_description,
|
||||
})
|
||||
}
|
||||
}
|
||||
41
libs/dtls/src/application_data.rs
Normal file
41
libs/dtls/src/application_data.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use super::content::*;
|
||||
use crate::error::Result;
|
||||
|
||||
// Application data messages are carried by the record layer and are
|
||||
// fragmented, compressed, and encrypted based on the current connection
|
||||
// state. The messages are treated as transparent data to the record
|
||||
// layer.
|
||||
/// ## Specifications
|
||||
///
|
||||
/// * [RFC 5246 §10]
|
||||
///
|
||||
/// [RFC 5246 §10]: https://tools.ietf.org/html/rfc5246#section-10
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ApplicationData {
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ApplicationData {
|
||||
pub fn content_type(&self) -> ContentType {
|
||||
ContentType::ApplicationData
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_all(&self.data)?;
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
let mut data: Vec<u8> = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
|
||||
Ok(ApplicationData { data })
|
||||
}
|
||||
}
|
||||
378
libs/dtls/src/certificate_verifier.rs
Normal file
378
libs/dtls/src/certificate_verifier.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use openssl::stack::Stack;
|
||||
use openssl::x509::store::X509StoreBuilder;
|
||||
use openssl::x509::verify::{X509CheckFlags, X509VerifyParam};
|
||||
use openssl::x509::{X509PurposeId, X509StoreContext, X509};
|
||||
use rustls_pki_types::{CertificateDer, ServerName};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RootCertStore {
|
||||
certificates: Vec<X509>,
|
||||
}
|
||||
|
||||
impl RootCertStore {
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, certificate: CertificateDer<'_>) -> Result<()> {
|
||||
self.certificates
|
||||
.push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CertificateVerifier {
|
||||
roots: RootCertStore,
|
||||
}
|
||||
|
||||
impl CertificateVerifier {
|
||||
pub(crate) fn new(roots: RootCertStore) -> Self {
|
||||
Self { roots }
|
||||
}
|
||||
|
||||
pub(crate) fn verify_client_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
) -> Result<()> {
|
||||
self.verify(end_entity, intermediates, X509PurposeId::SSL_CLIENT, None)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
server_name: &str,
|
||||
) -> Result<()> {
|
||||
ServerName::try_from(server_name).map_err(|error| Error::Other(error.to_string()))?;
|
||||
self.verify(
|
||||
end_entity,
|
||||
intermediates,
|
||||
X509PurposeId::SSL_SERVER,
|
||||
Some(server_name),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify(
|
||||
&self,
|
||||
end_entity: &CertificateDer<'_>,
|
||||
intermediates: &[CertificateDer<'_>],
|
||||
purpose: X509PurposeId,
|
||||
server_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let leaf = X509::from_der(end_entity.as_ref()).map_err(openssl_error)?;
|
||||
let mut chain = Stack::new().map_err(openssl_error)?;
|
||||
for certificate in intermediates {
|
||||
chain
|
||||
.push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?)
|
||||
.map_err(openssl_error)?;
|
||||
}
|
||||
let mut parameters = X509VerifyParam::new().map_err(openssl_error)?;
|
||||
parameters.set_auth_level(2);
|
||||
parameters.set_purpose(purpose).map_err(openssl_error)?;
|
||||
parameters.set_hostflags(
|
||||
X509CheckFlags::NO_PARTIAL_WILDCARDS | X509CheckFlags::NEVER_CHECK_SUBJECT,
|
||||
);
|
||||
if let Some(server_name) = server_name {
|
||||
if let Ok(address) = server_name.parse() {
|
||||
parameters.set_ip(address).map_err(openssl_error)?;
|
||||
} else {
|
||||
parameters.set_host(server_name).map_err(openssl_error)?;
|
||||
}
|
||||
}
|
||||
let mut store = X509StoreBuilder::new().map_err(openssl_error)?;
|
||||
store.set_param(¶meters).map_err(openssl_error)?;
|
||||
for certificate in &self.roots.certificates {
|
||||
store.add_cert(certificate.clone()).map_err(openssl_error)?;
|
||||
}
|
||||
let store = store.build();
|
||||
let mut context = X509StoreContext::new().map_err(openssl_error)?;
|
||||
context
|
||||
.init(&store, &leaf, &chain, |context| {
|
||||
if context.verify_cert()? {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(context.error().to_string()))
|
||||
}
|
||||
})
|
||||
.map_err(openssl_error)?
|
||||
.map_or(Ok(()), |error| Err(Error::Other(error)))
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_error(error: openssl::error::ErrorStack) -> Error {
|
||||
Error::Other(error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rcgen::{BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair};
|
||||
|
||||
fn signed_leaf(
|
||||
parameters: CertificateParams,
|
||||
) -> (CertificateVerifier, CertificateDer<'static>) {
|
||||
let mut issuer_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||
issuer_parameters
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, "test root");
|
||||
issuer_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
let issuer_key = KeyPair::generate().unwrap();
|
||||
let issuer = issuer_parameters.self_signed(&issuer_key).unwrap();
|
||||
let leaf_key = KeyPair::generate().unwrap();
|
||||
let leaf = parameters
|
||||
.signed_by(&leaf_key, &issuer, &issuer_key)
|
||||
.unwrap();
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.add(issuer.der().clone()).unwrap();
|
||||
(CertificateVerifier::new(roots), leaf.der().clone())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_server_matches_dns_and_ip_subject_alternative_names() {
|
||||
let parameters =
|
||||
CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]).unwrap();
|
||||
let (verifier, leaf) = signed_leaf(parameters);
|
||||
verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.unwrap();
|
||||
assert!(verifier.verify_server_cert(&leaf, &[], "127.0.0.1").is_ok());
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "wrong.local")
|
||||
.is_err());
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "127.0.0.2")
|
||||
.is_err());
|
||||
assert!(verifier.verify_server_cert(&leaf, &[], "").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_untrusted_and_tampered_certificates() {
|
||||
let (verifier, leaf) =
|
||||
signed_leaf(CertificateParams::new(vec!["localhost".into()]).unwrap());
|
||||
let untrusted = CertificateVerifier::new(RootCertStore::empty());
|
||||
assert!(untrusted
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.is_err());
|
||||
let mut tampered = leaf.as_ref().to_vec();
|
||||
let last = tampered.len() - 1;
|
||||
tampered[last] ^= 1;
|
||||
assert!(verifier
|
||||
.verify_server_cert(&CertificateDer::from(tampered), &[], "localhost")
|
||||
.is_err());
|
||||
assert!(verifier
|
||||
.verify_server_cert(&CertificateDer::from(vec![0]), &[], "localhost")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_expired_certificate_and_wrong_extended_key_usage() {
|
||||
let mut expired = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||
expired.not_before = rcgen::date_time_ymd(2019, 1, 1);
|
||||
expired.not_after = rcgen::date_time_ymd(2020, 1, 1);
|
||||
let (verifier, leaf) = signed_leaf(expired);
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.is_err());
|
||||
let mut server_only = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||
server_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
let (verifier, leaf) = signed_leaf(server_only);
|
||||
verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.unwrap();
|
||||
assert!(verifier.verify_client_cert(&leaf, &[]).is_err());
|
||||
let mut client_only = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||
client_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
|
||||
let (verifier, leaf) = signed_leaf(client_only);
|
||||
assert!(verifier.verify_client_cert(&leaf, &[]).is_ok());
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_fall_back_to_common_name_or_partial_wildcard() {
|
||||
let mut parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||
parameters
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, "localhost");
|
||||
let (verifier, leaf) = signed_leaf(parameters);
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost")
|
||||
.is_err());
|
||||
let (verifier, leaf) =
|
||||
signed_leaf(CertificateParams::new(vec!["local*.example".into()]).unwrap());
|
||||
assert!(verifier
|
||||
.verify_server_cert(&leaf, &[], "localhost.example")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_weak_rsa_keys_and_sha1_leaf_signatures() {
|
||||
use openssl::asn1::Asn1Time;
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::pkey::PKey;
|
||||
use openssl::rsa::Rsa;
|
||||
use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
|
||||
use openssl::x509::X509NameBuilder;
|
||||
|
||||
let issuer_key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
|
||||
let mut issuer_name = X509NameBuilder::new().unwrap();
|
||||
issuer_name
|
||||
.append_entry_by_text("CN", "strong root")
|
||||
.unwrap();
|
||||
let issuer_name = issuer_name.build();
|
||||
let mut issuer = X509::builder().unwrap();
|
||||
issuer.set_version(2).unwrap();
|
||||
issuer.set_subject_name(&issuer_name).unwrap();
|
||||
issuer.set_issuer_name(&issuer_name).unwrap();
|
||||
issuer.set_pubkey(&issuer_key).unwrap();
|
||||
issuer
|
||||
.set_not_before(&Asn1Time::days_from_now(0).unwrap())
|
||||
.unwrap();
|
||||
issuer
|
||||
.set_not_after(&Asn1Time::days_from_now(1).unwrap())
|
||||
.unwrap();
|
||||
issuer
|
||||
.append_extension(BasicConstraints::new().critical().ca().build().unwrap())
|
||||
.unwrap();
|
||||
issuer.sign(&issuer_key, MessageDigest::sha256()).unwrap();
|
||||
let issuer = issuer.build();
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots
|
||||
.add(CertificateDer::from(issuer.to_der().unwrap()))
|
||||
.unwrap();
|
||||
let verifier = CertificateVerifier::new(roots);
|
||||
for (bits, digest, expected) in [
|
||||
(2048, MessageDigest::sha256(), true),
|
||||
(1024, MessageDigest::sha256(), false),
|
||||
(2048, MessageDigest::sha1(), false),
|
||||
] {
|
||||
let leaf_key = PKey::from_rsa(Rsa::generate(bits).unwrap()).unwrap();
|
||||
let mut leaf_name = X509NameBuilder::new().unwrap();
|
||||
leaf_name.append_entry_by_text("CN", "localhost").unwrap();
|
||||
let leaf_name = leaf_name.build();
|
||||
let mut leaf = X509::builder().unwrap();
|
||||
leaf.set_version(2).unwrap();
|
||||
leaf.set_subject_name(&leaf_name).unwrap();
|
||||
leaf.set_issuer_name(issuer.subject_name()).unwrap();
|
||||
leaf.set_pubkey(&leaf_key).unwrap();
|
||||
leaf.set_not_before(&Asn1Time::days_from_now(0).unwrap())
|
||||
.unwrap();
|
||||
leaf.set_not_after(&Asn1Time::days_from_now(1).unwrap())
|
||||
.unwrap();
|
||||
let names = SubjectAlternativeName::new()
|
||||
.dns("localhost")
|
||||
.build(&leaf.x509v3_context(Some(&issuer), None))
|
||||
.unwrap();
|
||||
leaf.append_extension(names).unwrap();
|
||||
leaf.sign(&issuer_key, digest).unwrap();
|
||||
let leaf = CertificateDer::from(leaf.build().to_der().unwrap());
|
||||
assert_eq!(
|
||||
verifier.verify_server_cert(&leaf, &[], "localhost").is_ok(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dtls_handshake_preserves_mutual_certificate_authentication() {
|
||||
use crate::config::{ClientAuthType, Config};
|
||||
use crate::conn::DTLSConn;
|
||||
use crate::crypto::Certificate;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
let client_certificate =
|
||||
Certificate::generate_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let server_certificate =
|
||||
Certificate::generate_self_signed(vec!["localhost".into()]).unwrap();
|
||||
let mut client_roots = RootCertStore::empty();
|
||||
client_roots
|
||||
.add(server_certificate.certificate[0].clone())
|
||||
.unwrap();
|
||||
let mut server_roots = RootCertStore::empty();
|
||||
server_roots
|
||||
.add(client_certificate.certificate[0].clone())
|
||||
.unwrap();
|
||||
let client_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||
let server_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||
client_socket
|
||||
.connect(server_socket.local_addr().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
server_socket
|
||||
.connect(client_socket.local_addr().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let client_config = Config {
|
||||
certificates: vec![client_certificate],
|
||||
roots_cas: client_roots,
|
||||
server_name: "localhost".into(),
|
||||
..Default::default()
|
||||
};
|
||||
let server_config = Config {
|
||||
certificates: vec![server_certificate],
|
||||
client_cas: server_roots,
|
||||
client_auth: ClientAuthType::RequireAndVerifyClientCert,
|
||||
..Default::default()
|
||||
};
|
||||
let (client, server) = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
tokio::try_join!(
|
||||
DTLSConn::new(client_socket, client_config, true, None),
|
||||
DTLSConn::new(server_socket, server_config, false, None),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
client
|
||||
.write(b"authenticated", Some(Duration::from_secs(5)))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut buffer = [0; 64];
|
||||
let count = server
|
||||
.read(&mut buffer, Some(Duration::from_secs(5)))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(&buffer[..count], b"authenticated");
|
||||
client.close().await.unwrap();
|
||||
server.close().await.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verifies_intermediate_chain_without_implicitly_trusting_it() {
|
||||
let mut root_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||
root_parameters
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, "test root");
|
||||
root_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
let root_key = KeyPair::generate().unwrap();
|
||||
let root = root_parameters.self_signed(&root_key).unwrap();
|
||||
let mut intermediate_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||
intermediate_parameters
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, "test intermediate");
|
||||
intermediate_parameters.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
|
||||
let intermediate_key = KeyPair::generate().unwrap();
|
||||
let intermediate = intermediate_parameters
|
||||
.signed_by(&intermediate_key, &root, &root_key)
|
||||
.unwrap();
|
||||
let leaf_key = KeyPair::generate().unwrap();
|
||||
let leaf = CertificateParams::new(vec!["localhost".into()])
|
||||
.unwrap()
|
||||
.signed_by(&leaf_key, &intermediate, &intermediate_key)
|
||||
.unwrap();
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.add(root.der().clone()).unwrap();
|
||||
let verifier = CertificateVerifier::new(roots);
|
||||
assert!(verifier
|
||||
.verify_server_cert(leaf.der(), &[intermediate.der().clone()], "localhost")
|
||||
.is_ok());
|
||||
assert!(verifier
|
||||
.verify_server_cert(leaf.der(), &[], "localhost")
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
37
libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs
Normal file
37
libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::io::{BufReader, BufWriter};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_change_cipher_spec_round_trip() -> Result<()> {
|
||||
let c = ChangeCipherSpec {};
|
||||
let mut raw = vec![];
|
||||
{
|
||||
let mut writer = BufWriter::<&mut Vec<u8>>::new(raw.as_mut());
|
||||
c.marshal(&mut writer)?;
|
||||
}
|
||||
|
||||
let mut reader = BufReader::new(raw.as_slice());
|
||||
let cnew = ChangeCipherSpec::unmarshal(&mut reader)?;
|
||||
assert_eq!(
|
||||
c, cnew,
|
||||
"ChangeCipherSpec round trip: got {cnew:?}, want {c:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_change_cipher_spec_invalid() -> Result<()> {
|
||||
let data = vec![0x00];
|
||||
|
||||
let mut reader = BufReader::new(data.as_slice());
|
||||
let result = ChangeCipherSpec::unmarshal(&mut reader);
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("must be error"),
|
||||
Err(err) => assert_eq!(err.to_string(), Error::ErrInvalidCipherSpec.to_string()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
46
libs/dtls/src/change_cipher_spec/mod.rs
Normal file
46
libs/dtls/src/change_cipher_spec/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[cfg(test)]
|
||||
mod change_cipher_spec_test;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use super::content::*;
|
||||
use super::error::*;
|
||||
|
||||
// The change cipher spec protocol exists to signal transitions in
|
||||
// ciphering strategies. The protocol consists of a single message,
|
||||
// which is encrypted and compressed under the current (not the pending)
|
||||
// connection state. The message consists of a single byte of value 1.
|
||||
/// ## Specifications
|
||||
///
|
||||
/// * [RFC 5246 §7.1]
|
||||
///
|
||||
/// [RFC 5246 §7.1]: https://tools.ietf.org/html/rfc5246#section-7.1
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ChangeCipherSpec;
|
||||
|
||||
impl ChangeCipherSpec {
|
||||
pub fn content_type(&self) -> ContentType {
|
||||
ContentType::ChangeCipherSpec
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_u8(0x01)?;
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
let data = reader.read_u8()?;
|
||||
if data != 0x01 {
|
||||
return Err(Error::ErrInvalidCipherSpec);
|
||||
}
|
||||
|
||||
Ok(ChangeCipherSpec {})
|
||||
}
|
||||
}
|
||||
112
libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs
Normal file
112
libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use super::*;
|
||||
use crate::client_certificate_type::ClientCertificateType;
|
||||
use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen};
|
||||
use crate::prf::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CipherSuiteAes128Ccm {
|
||||
ccm: Option<CryptoCcm>,
|
||||
client_certificate_type: ClientCertificateType,
|
||||
id: CipherSuiteId,
|
||||
psk: bool,
|
||||
crypto_ccm_tag_len: CryptoCcmTagLen,
|
||||
}
|
||||
|
||||
impl CipherSuiteAes128Ccm {
|
||||
const PRF_MAC_LEN: usize = 0;
|
||||
const PRF_KEY_LEN: usize = 16;
|
||||
const PRF_IV_LEN: usize = 4;
|
||||
|
||||
pub fn new(
|
||||
client_certificate_type: ClientCertificateType,
|
||||
id: CipherSuiteId,
|
||||
psk: bool,
|
||||
crypto_ccm_tag_len: CryptoCcmTagLen,
|
||||
) -> Self {
|
||||
CipherSuiteAes128Ccm {
|
||||
ccm: None,
|
||||
client_certificate_type,
|
||||
id,
|
||||
psk,
|
||||
crypto_ccm_tag_len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CipherSuite for CipherSuiteAes128Ccm {
|
||||
fn to_string(&self) -> String {
|
||||
format!("{}", self.id)
|
||||
}
|
||||
|
||||
fn id(&self) -> CipherSuiteId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn certificate_type(&self) -> ClientCertificateType {
|
||||
self.client_certificate_type
|
||||
}
|
||||
|
||||
fn hash_func(&self) -> CipherSuiteHash {
|
||||
CipherSuiteHash::Sha256
|
||||
}
|
||||
|
||||
fn is_psk(&self) -> bool {
|
||||
self.psk
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.ccm.is_some()
|
||||
}
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()> {
|
||||
let keys = prf_encryption_keys(
|
||||
master_secret,
|
||||
client_random,
|
||||
server_random,
|
||||
CipherSuiteAes128Ccm::PRF_MAC_LEN,
|
||||
CipherSuiteAes128Ccm::PRF_KEY_LEN,
|
||||
CipherSuiteAes128Ccm::PRF_IV_LEN,
|
||||
self.hash_func(),
|
||||
)?;
|
||||
|
||||
if is_client {
|
||||
self.ccm = Some(CryptoCcm::new(
|
||||
&self.crypto_ccm_tag_len,
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
));
|
||||
} else {
|
||||
self.ccm = Some(CryptoCcm::new(
|
||||
&self.crypto_ccm_tag_len,
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let ccm = self.ccm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||
))?;
|
||||
ccm.encrypt(pkt_rlh, raw)
|
||||
}
|
||||
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||
let ccm = self.ccm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||
))?;
|
||||
ccm.decrypt(input)
|
||||
}
|
||||
}
|
||||
107
libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs
Normal file
107
libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
use crate::crypto::crypto_gcm::*;
|
||||
use crate::prf::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CipherSuiteAes128GcmSha256 {
|
||||
gcm: Option<CryptoGcm>,
|
||||
rsa: bool,
|
||||
}
|
||||
|
||||
impl CipherSuiteAes128GcmSha256 {
|
||||
const PRF_MAC_LEN: usize = 0;
|
||||
const PRF_KEY_LEN: usize = 16;
|
||||
const PRF_IV_LEN: usize = 4;
|
||||
|
||||
pub fn new(rsa: bool) -> Self {
|
||||
CipherSuiteAes128GcmSha256 { gcm: None, rsa }
|
||||
}
|
||||
}
|
||||
|
||||
impl CipherSuite for CipherSuiteAes128GcmSha256 {
|
||||
fn to_string(&self) -> String {
|
||||
if self.rsa {
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256".to_owned()
|
||||
} else {
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> CipherSuiteId {
|
||||
if self.rsa {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256
|
||||
} else {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256
|
||||
}
|
||||
}
|
||||
|
||||
fn certificate_type(&self) -> ClientCertificateType {
|
||||
if self.rsa {
|
||||
ClientCertificateType::RsaSign
|
||||
} else {
|
||||
ClientCertificateType::EcdsaSign
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_func(&self) -> CipherSuiteHash {
|
||||
CipherSuiteHash::Sha256
|
||||
}
|
||||
|
||||
fn is_psk(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.gcm.is_some()
|
||||
}
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()> {
|
||||
let keys = prf_encryption_keys(
|
||||
master_secret,
|
||||
client_random,
|
||||
server_random,
|
||||
CipherSuiteAes128GcmSha256::PRF_MAC_LEN,
|
||||
CipherSuiteAes128GcmSha256::PRF_KEY_LEN,
|
||||
CipherSuiteAes128GcmSha256::PRF_IV_LEN,
|
||||
self.hash_func(),
|
||||
)?;
|
||||
|
||||
if is_client {
|
||||
self.gcm = Some(CryptoGcm::new(
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
));
|
||||
} else {
|
||||
self.gcm = Some(CryptoGcm::new(
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||
))?;
|
||||
cg.encrypt(pkt_rlh, raw)
|
||||
}
|
||||
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||
))?;
|
||||
cg.decrypt(input)
|
||||
}
|
||||
}
|
||||
107
libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs
Normal file
107
libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
use crate::crypto::crypto_cbc::*;
|
||||
use crate::prf::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CipherSuiteAes256CbcSha {
|
||||
cbc: Option<CryptoCbc>,
|
||||
rsa: bool,
|
||||
}
|
||||
|
||||
impl CipherSuiteAes256CbcSha {
|
||||
const PRF_MAC_LEN: usize = 20;
|
||||
const PRF_KEY_LEN: usize = 32;
|
||||
const PRF_IV_LEN: usize = 16;
|
||||
|
||||
pub fn new(rsa: bool) -> Self {
|
||||
CipherSuiteAes256CbcSha { cbc: None, rsa }
|
||||
}
|
||||
}
|
||||
|
||||
impl CipherSuite for CipherSuiteAes256CbcSha {
|
||||
fn to_string(&self) -> String {
|
||||
if self.rsa {
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA".to_owned()
|
||||
} else {
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> CipherSuiteId {
|
||||
if self.rsa {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha
|
||||
} else {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha
|
||||
}
|
||||
}
|
||||
|
||||
fn certificate_type(&self) -> ClientCertificateType {
|
||||
if self.rsa {
|
||||
ClientCertificateType::RsaSign
|
||||
} else {
|
||||
ClientCertificateType::EcdsaSign
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_func(&self) -> CipherSuiteHash {
|
||||
CipherSuiteHash::Sha256
|
||||
}
|
||||
|
||||
fn is_psk(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.cbc.is_some()
|
||||
}
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()> {
|
||||
let keys = prf_encryption_keys(
|
||||
master_secret,
|
||||
client_random,
|
||||
server_random,
|
||||
CipherSuiteAes256CbcSha::PRF_MAC_LEN,
|
||||
CipherSuiteAes256CbcSha::PRF_KEY_LEN,
|
||||
CipherSuiteAes256CbcSha::PRF_IV_LEN,
|
||||
self.hash_func(),
|
||||
)?;
|
||||
|
||||
if is_client {
|
||||
self.cbc = Some(CryptoCbc::new(
|
||||
&keys.client_write_key,
|
||||
&keys.client_mac_key,
|
||||
&keys.server_write_key,
|
||||
&keys.server_mac_key,
|
||||
)?);
|
||||
} else {
|
||||
self.cbc = Some(CryptoCbc::new(
|
||||
&keys.server_write_key,
|
||||
&keys.server_mac_key,
|
||||
&keys.client_write_key,
|
||||
&keys.client_mac_key,
|
||||
)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.cbc.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||
))?;
|
||||
cg.encrypt(pkt_rlh, raw)
|
||||
}
|
||||
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.cbc.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||
))?;
|
||||
cg.decrypt(input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
use crate::crypto::crypto_chacha20::*;
|
||||
use crate::prf::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CipherSuiteChaCha20Poly1305Sha256 {
|
||||
rsa: bool,
|
||||
cipher: Option<CryptoChaCha20>,
|
||||
}
|
||||
|
||||
impl CipherSuiteChaCha20Poly1305Sha256 {
|
||||
const PRF_MAC_LEN: usize = 0;
|
||||
const PRF_KEY_LEN: usize = 32;
|
||||
const PRF_IV_LEN: usize = 12;
|
||||
|
||||
pub fn new(rsa: bool) -> Self {
|
||||
CipherSuiteChaCha20Poly1305Sha256 { rsa, cipher: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl CipherSuite for CipherSuiteChaCha20Poly1305Sha256 {
|
||||
fn to_string(&self) -> String {
|
||||
if self.rsa {
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256".to_owned()
|
||||
} else {
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> CipherSuiteId {
|
||||
if self.rsa {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256
|
||||
} else {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256
|
||||
}
|
||||
}
|
||||
|
||||
fn certificate_type(&self) -> ClientCertificateType {
|
||||
if self.rsa {
|
||||
ClientCertificateType::RsaSign
|
||||
} else {
|
||||
ClientCertificateType::EcdsaSign
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_func(&self) -> CipherSuiteHash {
|
||||
CipherSuiteHash::Sha256
|
||||
}
|
||||
|
||||
fn is_psk(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.cipher.is_some()
|
||||
}
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()> {
|
||||
let keys = prf_encryption_keys(
|
||||
master_secret,
|
||||
client_random,
|
||||
server_random,
|
||||
CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN,
|
||||
CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN,
|
||||
CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN,
|
||||
self.hash_func(),
|
||||
)?;
|
||||
|
||||
if is_client {
|
||||
self.cipher = Some(CryptoChaCha20::new(
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
));
|
||||
} else {
|
||||
self.cipher = Some(CryptoChaCha20::new(
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.cipher.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||
))?;
|
||||
cg.encrypt(pkt_rlh, raw)
|
||||
}
|
||||
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.cipher.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||
))?;
|
||||
cg.decrypt(input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::*;
|
||||
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||
|
||||
pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm {
|
||||
CipherSuiteAes128Ccm::new(
|
||||
ClientCertificateType::EcdsaSign,
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||
false,
|
||||
CryptoCcmTagLen::CryptoCcmTagLength,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::*;
|
||||
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||
|
||||
pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm {
|
||||
CipherSuiteAes128Ccm::new(
|
||||
ClientCertificateType::EcdsaSign,
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8,
|
||||
false,
|
||||
CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::*;
|
||||
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||
|
||||
pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm {
|
||||
CipherSuiteAes128Ccm::new(
|
||||
ClientCertificateType::Unsupported,
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||
true,
|
||||
CryptoCcmTagLen::CryptoCcmTagLength,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use super::*;
|
||||
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||
|
||||
pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm {
|
||||
CipherSuiteAes128Ccm::new(
|
||||
ClientCertificateType::Unsupported,
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||
true,
|
||||
CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use super::*;
|
||||
use crate::crypto::crypto_gcm::*;
|
||||
use crate::prf::*;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||
gcm: Option<CryptoGcm>,
|
||||
}
|
||||
|
||||
impl CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||
const PRF_MAC_LEN: usize = 0;
|
||||
const PRF_KEY_LEN: usize = 16;
|
||||
const PRF_IV_LEN: usize = 4;
|
||||
}
|
||||
|
||||
impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||
fn to_string(&self) -> String {
|
||||
"TLS_PSK_WITH_AES_128_GCM_SHA256".to_owned()
|
||||
}
|
||||
|
||||
fn id(&self) -> CipherSuiteId {
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256
|
||||
}
|
||||
|
||||
fn certificate_type(&self) -> ClientCertificateType {
|
||||
ClientCertificateType::Unsupported
|
||||
}
|
||||
|
||||
fn hash_func(&self) -> CipherSuiteHash {
|
||||
CipherSuiteHash::Sha256
|
||||
}
|
||||
|
||||
fn is_psk(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.gcm.is_some()
|
||||
}
|
||||
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()> {
|
||||
let keys = prf_encryption_keys(
|
||||
master_secret,
|
||||
client_random,
|
||||
server_random,
|
||||
CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN,
|
||||
CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN,
|
||||
CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN,
|
||||
self.hash_func(),
|
||||
)?;
|
||||
|
||||
if is_client {
|
||||
self.gcm = Some(CryptoGcm::new(
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
));
|
||||
} else {
|
||||
self.gcm = Some(CryptoGcm::new(
|
||||
&keys.server_write_key,
|
||||
&keys.server_write_iv,
|
||||
&keys.client_write_key,
|
||||
&keys.client_write_iv,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||
))?;
|
||||
cg.encrypt(pkt_rlh, raw)
|
||||
}
|
||||
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||
))?;
|
||||
cg.decrypt(input)
|
||||
}
|
||||
}
|
||||
287
libs/dtls/src/cipher_suite/mod.rs
Normal file
287
libs/dtls/src/cipher_suite/mod.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
pub mod cipher_suite_aes_128_ccm;
|
||||
pub mod cipher_suite_aes_128_gcm_sha256;
|
||||
pub mod cipher_suite_aes_256_cbc_sha;
|
||||
pub mod cipher_suite_chacha20_poly1305_sha256;
|
||||
pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm;
|
||||
pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8;
|
||||
pub mod cipher_suite_tls_psk_with_aes_128_ccm;
|
||||
pub mod cipher_suite_tls_psk_with_aes_128_ccm8;
|
||||
pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256;
|
||||
|
||||
use std::fmt;
|
||||
use std::marker::{Send, Sync};
|
||||
|
||||
use cipher_suite_aes_128_gcm_sha256::*;
|
||||
use cipher_suite_aes_256_cbc_sha::*;
|
||||
use cipher_suite_chacha20_poly1305_sha256::*;
|
||||
use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm::*;
|
||||
use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8::*;
|
||||
use cipher_suite_tls_psk_with_aes_128_ccm::*;
|
||||
use cipher_suite_tls_psk_with_aes_128_ccm8::*;
|
||||
use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*;
|
||||
|
||||
use super::client_certificate_type::*;
|
||||
use super::error::*;
|
||||
use super::record_layer::record_layer_header::*;
|
||||
|
||||
// CipherSuiteID is an ID for our supported CipherSuites
|
||||
// Supported Cipher Suites
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CipherSuiteId {
|
||||
// AES-128-CCM
|
||||
Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac,
|
||||
Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae,
|
||||
|
||||
// AES-128-GCM-SHA256
|
||||
Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b,
|
||||
Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f,
|
||||
|
||||
// AES-256-CBC-SHA
|
||||
Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a,
|
||||
Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014,
|
||||
|
||||
Tls_Psk_With_Aes_128_Ccm = 0xc0a4,
|
||||
Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8,
|
||||
Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8,
|
||||
|
||||
// CHACHA20_POLY1305_SHA256
|
||||
Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 = 0xcca8,
|
||||
Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 = 0xcca9,
|
||||
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl fmt::Display for CipherSuiteId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => {
|
||||
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => {
|
||||
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => {
|
||||
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => {
|
||||
write!(f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => {
|
||||
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => {
|
||||
write!(f, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA")
|
||||
}
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => write!(f, "TLS_PSK_WITH_AES_128_CCM"),
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => write!(f, "TLS_PSK_WITH_AES_128_CCM_8"),
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => {
|
||||
write!(f, "TLS_PSK_WITH_AES_128_GCM_SHA256")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||
write!(f, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||
write!(f, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256")
|
||||
}
|
||||
|
||||
_ => write!(f, "Unsupported CipherSuiteID"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for CipherSuiteId {
|
||||
fn from(val: u16) -> Self {
|
||||
match val {
|
||||
// AES-128-CCM
|
||||
0xc0ac => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||
0xc0ae => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8,
|
||||
|
||||
// AES-128-GCM-SHA256
|
||||
0xc02b => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256,
|
||||
0xc02f => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256,
|
||||
|
||||
// AES-256-CBC-SHA
|
||||
0xc00a => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha,
|
||||
0xc014 => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha,
|
||||
|
||||
0xc0a4 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||
0xc0a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||
0x00a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256,
|
||||
|
||||
// CHACHA20_POLY1305_SHA256
|
||||
0xcca8 => CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256,
|
||||
0xcca9 => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256,
|
||||
|
||||
_ => CipherSuiteId::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CipherSuiteId {
|
||||
fn from(val: &str) -> Self {
|
||||
match val {
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM" => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8
|
||||
}
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256
|
||||
}
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256
|
||||
}
|
||||
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha
|
||||
}
|
||||
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha
|
||||
}
|
||||
"TLS_PSK_WITH_AES_128_CCM" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||
"TLS_PSK_WITH_AES_128_CCM_8" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||
"TLS_PSK_WITH_AES_128_GCM_SHA256" => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256,
|
||||
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256
|
||||
}
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256
|
||||
}
|
||||
_ => CipherSuiteId::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CipherSuiteHash {
|
||||
Sha256,
|
||||
}
|
||||
|
||||
impl CipherSuiteHash {
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
match *self {
|
||||
CipherSuiteHash::Sha256 => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CipherSuite {
|
||||
fn to_string(&self) -> String;
|
||||
fn id(&self) -> CipherSuiteId;
|
||||
fn certificate_type(&self) -> ClientCertificateType;
|
||||
fn hash_func(&self) -> CipherSuiteHash;
|
||||
fn is_psk(&self) -> bool;
|
||||
fn is_initialized(&self) -> bool;
|
||||
|
||||
// Generate the internal encryption state
|
||||
fn init(
|
||||
&mut self,
|
||||
master_secret: &[u8],
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
is_client: bool,
|
||||
) -> Result<()>;
|
||||
|
||||
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>>;
|
||||
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml
|
||||
// A cipher_suite is a specific combination of key agreement, cipher and MAC
|
||||
// function.
|
||||
pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result<Box<dyn CipherSuite + Send + Sync>> {
|
||||
match id {
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => {
|
||||
Ok(Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()))
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => Ok(Box::new(
|
||||
new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8(),
|
||||
)),
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => {
|
||||
Ok(Box::new(CipherSuiteAes128GcmSha256::new(false)))
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => {
|
||||
Ok(Box::new(CipherSuiteAes128GcmSha256::new(true)))
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => {
|
||||
Ok(Box::new(CipherSuiteAes256CbcSha::new(true)))
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => {
|
||||
Ok(Box::new(CipherSuiteAes256CbcSha::new(false)))
|
||||
}
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => {
|
||||
Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()))
|
||||
}
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => {
|
||||
Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()))
|
||||
}
|
||||
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => {
|
||||
Ok(Box::<CipherSuiteTlsPskWithAes128GcmSha256>::default())
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||
Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true)))
|
||||
}
|
||||
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||
Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)))
|
||||
}
|
||||
|
||||
_ => Err(Error::ErrInvalidCipherSuite),
|
||||
}
|
||||
}
|
||||
|
||||
// CipherSuites we support in order of preference
|
||||
pub(crate) fn default_cipher_suites() -> Vec<Box<dyn CipherSuite + Send + Sync>> {
|
||||
vec![
|
||||
Box::new(CipherSuiteAes128GcmSha256::new(false)),
|
||||
Box::new(CipherSuiteAes256CbcSha::new(false)),
|
||||
Box::new(CipherSuiteAes128GcmSha256::new(true)),
|
||||
Box::new(CipherSuiteAes256CbcSha::new(true)),
|
||||
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_cipher_suites() -> Vec<Box<dyn CipherSuite + Send + Sync>> {
|
||||
vec![
|
||||
Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()),
|
||||
Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8()),
|
||||
Box::new(CipherSuiteAes128GcmSha256::new(false)),
|
||||
Box::new(CipherSuiteAes128GcmSha256::new(true)),
|
||||
Box::new(CipherSuiteAes256CbcSha::new(false)),
|
||||
Box::new(CipherSuiteAes256CbcSha::new(true)),
|
||||
Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()),
|
||||
Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()),
|
||||
Box::<CipherSuiteTlsPskWithAes128GcmSha256>::default(),
|
||||
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)),
|
||||
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true)),
|
||||
]
|
||||
}
|
||||
|
||||
fn cipher_suites_for_ids(ids: &[CipherSuiteId]) -> Result<Vec<Box<dyn CipherSuite + Send + Sync>>> {
|
||||
let mut cipher_suites = vec![];
|
||||
for id in ids {
|
||||
cipher_suites.push(cipher_suite_for_id(*id)?);
|
||||
}
|
||||
Ok(cipher_suites)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_cipher_suites(
|
||||
user_selected_suites: &[CipherSuiteId],
|
||||
exclude_psk: bool,
|
||||
exclude_non_psk: bool,
|
||||
) -> Result<Vec<Box<dyn CipherSuite + Send + Sync>>> {
|
||||
let cipher_suites = if !user_selected_suites.is_empty() {
|
||||
cipher_suites_for_ids(user_selected_suites)?
|
||||
} else {
|
||||
default_cipher_suites()
|
||||
};
|
||||
|
||||
let filtered_cipher_suites: Vec<Box<dyn CipherSuite + Send + Sync>> = cipher_suites
|
||||
.into_iter()
|
||||
.filter(|c| !((exclude_psk && c.is_psk()) || (exclude_non_psk && !c.is_psk())))
|
||||
.collect();
|
||||
|
||||
if filtered_cipher_suites.is_empty() {
|
||||
Err(Error::ErrNoAvailableCipherSuites)
|
||||
} else {
|
||||
Ok(filtered_cipher_suites)
|
||||
}
|
||||
}
|
||||
16
libs/dtls/src/client_certificate_type.rs
Normal file
16
libs/dtls/src/client_certificate_type.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ClientCertificateType {
|
||||
RsaSign = 1,
|
||||
EcdsaSign = 64,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<u8> for ClientCertificateType {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
1 => ClientCertificateType::RsaSign,
|
||||
64 => ClientCertificateType::EcdsaSign,
|
||||
_ => ClientCertificateType::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
60
libs/dtls/src/compression_methods.rs
Normal file
60
libs/dtls/src/compression_methods.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum CompressionMethodId {
|
||||
Null = 0,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<u8> for CompressionMethodId {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
0 => CompressionMethodId::Null,
|
||||
_ => CompressionMethodId::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CompressionMethods {
|
||||
pub ids: Vec<CompressionMethodId>,
|
||||
}
|
||||
|
||||
impl CompressionMethods {
|
||||
pub fn size(&self) -> usize {
|
||||
1 + self.ids.len()
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_u8(self.ids.len() as u8)?;
|
||||
|
||||
for id in &self.ids {
|
||||
writer.write_u8(*id as u8)?;
|
||||
}
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
let compression_methods_count = reader.read_u8()? as usize;
|
||||
let mut ids = vec![];
|
||||
for _ in 0..compression_methods_count {
|
||||
let id = reader.read_u8()?.into();
|
||||
if id != CompressionMethodId::Unsupported {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CompressionMethods { ids })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_compression_methods() -> CompressionMethods {
|
||||
CompressionMethods {
|
||||
ids: vec![CompressionMethodId::Null],
|
||||
}
|
||||
}
|
||||
193
libs/dtls/src/config.rs
Normal file
193
libs/dtls/src/config.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::cipher_suite::*;
|
||||
use crate::crypto::*;
|
||||
use crate::error::*;
|
||||
use crate::extension::extension_use_srtp::SrtpProtectionProfile;
|
||||
use crate::handshaker::VerifyPeerCertificateFn;
|
||||
use crate::signature_hash_algorithm::SignatureScheme;
|
||||
|
||||
/// Config is used to configure a DTLS client or server.
|
||||
/// After a Config is passed to a DTLS function it must not be modified.
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
/// certificates contains certificate chain to present to the other side of the connection.
|
||||
/// Server MUST set this if psk is non-nil
|
||||
/// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil
|
||||
pub certificates: Vec<Certificate>,
|
||||
|
||||
/// cipher_suites is a list of supported cipher suites.
|
||||
/// If cipher_suites is nil, a default list is used
|
||||
pub cipher_suites: Vec<CipherSuiteId>,
|
||||
|
||||
/// signature_schemes contains the signature and hash schemes that the peer requests to verify.
|
||||
pub signature_schemes: Vec<SignatureScheme>,
|
||||
|
||||
/// srtp_protection_profiles are the supported protection profiles
|
||||
/// Clients will send this via use_srtp and assert that the server properly responds
|
||||
/// Servers will assert that clients send one of these profiles and will respond as needed
|
||||
pub srtp_protection_profiles: Vec<SrtpProtectionProfile>,
|
||||
|
||||
/// client_auth determines the server's policy for
|
||||
/// TLS Client Authentication. The default is NoClientCert.
|
||||
pub client_auth: ClientAuthType,
|
||||
|
||||
/// extended_master_secret determines if the "Extended Master Secret" extension
|
||||
/// should be disabled, requested, or required (default requested).
|
||||
pub extended_master_secret: ExtendedMasterSecretType,
|
||||
|
||||
/// flight_interval controls how often we send outbound handshake messages
|
||||
/// defaults to time.Second
|
||||
pub flight_interval: Duration,
|
||||
|
||||
/// psk sets the pre-shared key used by this DTLS connection
|
||||
/// If psk is non-nil only psk cipher_suites will be used
|
||||
pub psk: Option<PskCallback>,
|
||||
pub psk_identity_hint: Option<Vec<u8>>,
|
||||
|
||||
/// insecure_skip_verify controls whether a client verifies the
|
||||
/// server's certificate chain and host name.
|
||||
/// If insecure_skip_verify is true, TLS accepts any certificate
|
||||
/// presented by the server and any host name in that certificate.
|
||||
/// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
||||
/// This should be used only for testing.
|
||||
pub insecure_skip_verify: bool,
|
||||
|
||||
/// insecure_hashes allows the use of hashing algorithms that are known
|
||||
/// to be vulnerable.
|
||||
pub insecure_hashes: bool,
|
||||
|
||||
/// insecure_verification allows the use of verification algorithms that are
|
||||
/// known to be vulnerable or deprecated
|
||||
pub insecure_verification: bool,
|
||||
/// VerifyPeerCertificate, if not nil, is called after normal
|
||||
/// certificate verification by either a client or server. It
|
||||
/// receives the certificate provided by the peer and also a flag
|
||||
/// that tells if normal verification has succeeded. If it returns a
|
||||
/// non-nil error, the handshake is aborted and that error results.
|
||||
///
|
||||
/// If normal verification fails then the handshake will abort before
|
||||
/// considering this callback. If normal verification is disabled by
|
||||
/// setting insecure_skip_verify, or (for a server) when client_auth is
|
||||
/// RequestClientCert or RequireAnyClientCert, then this callback will
|
||||
/// be considered but the verifiedChains will always be nil.
|
||||
pub verify_peer_certificate: Option<VerifyPeerCertificateFn>,
|
||||
|
||||
/// roots_cas defines the set of root certificate authorities
|
||||
/// that one peer uses when verifying the other peer's certificates.
|
||||
/// If RootCAs is nil, TLS uses the host's root CA set.
|
||||
/// Used by Client to verify server's certificate
|
||||
pub roots_cas: crate::certificate_verifier::RootCertStore,
|
||||
|
||||
/// client_cas defines the set of root certificate authorities
|
||||
/// that servers use if required to verify a client certificate
|
||||
/// by the policy in client_auth.
|
||||
/// Used by Server to verify client's certificate
|
||||
pub client_cas: crate::certificate_verifier::RootCertStore,
|
||||
|
||||
/// server_name is used to verify the hostname on the returned
|
||||
/// certificates unless insecure_skip_verify is given.
|
||||
pub server_name: String,
|
||||
|
||||
/// mtu is the length at which handshake messages will be fragmented to
|
||||
/// fit within the maximum transmission unit (default is 1200 bytes)
|
||||
pub mtu: usize,
|
||||
|
||||
/// replay_protection_window is the size of the replay attack protection window.
|
||||
/// Duplication of the sequence number is checked in this window size.
|
||||
/// Packet with sequence number older than this value compared to the latest
|
||||
/// accepted packet will be discarded. (default is 64)
|
||||
pub replay_protection_window: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
certificates: vec![],
|
||||
cipher_suites: vec![],
|
||||
signature_schemes: vec![],
|
||||
srtp_protection_profiles: vec![],
|
||||
client_auth: ClientAuthType::default(),
|
||||
extended_master_secret: ExtendedMasterSecretType::default(),
|
||||
flight_interval: Duration::default(),
|
||||
psk: None,
|
||||
psk_identity_hint: None,
|
||||
insecure_skip_verify: false,
|
||||
insecure_hashes: false,
|
||||
insecure_verification: false,
|
||||
verify_peer_certificate: None,
|
||||
roots_cas: crate::certificate_verifier::RootCertStore::empty(),
|
||||
client_cas: crate::certificate_verifier::RootCertStore::empty(),
|
||||
server_name: String::default(),
|
||||
mtu: 0,
|
||||
replay_protection_window: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_MTU: usize = 1200; // bytes
|
||||
|
||||
// PSKCallback is called once we have the remote's psk_identity_hint.
|
||||
// If the remote provided none it will be nil
|
||||
pub(crate) type PskCallback =
|
||||
Arc<dyn (Fn(&[u8]) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send>>) + Send + Sync>;
|
||||
|
||||
// ClientAuthType declares the policy the server will follow for
|
||||
// TLS Client Authentication.
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ClientAuthType {
|
||||
#[default]
|
||||
NoClientCert = 0,
|
||||
RequestClientCert = 1,
|
||||
RequireAnyClientCert = 2,
|
||||
VerifyClientCertIfGiven = 3,
|
||||
RequireAndVerifyClientCert = 4,
|
||||
}
|
||||
|
||||
// ExtendedMasterSecretType declares the policy the client and server
|
||||
// will follow for the Extended Master Secret extension
|
||||
#[derive(Default, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum ExtendedMasterSecretType {
|
||||
#[default]
|
||||
Request = 0,
|
||||
Require = 1,
|
||||
Disable = 2,
|
||||
}
|
||||
|
||||
pub(crate) fn validate_config(is_client: bool, config: &Config) -> Result<()> {
|
||||
if is_client && config.psk.is_some() && config.psk_identity_hint.is_none() {
|
||||
return Err(Error::ErrPskAndIdentityMustBeSetForClient);
|
||||
}
|
||||
|
||||
if !is_client && config.psk.is_none() && config.certificates.is_empty() {
|
||||
return Err(Error::ErrServerMustHaveCertificate);
|
||||
}
|
||||
|
||||
if !config.certificates.is_empty() && config.psk.is_some() {
|
||||
return Err(Error::ErrPskAndCertificate);
|
||||
}
|
||||
|
||||
if config.psk_identity_hint.is_some() && config.psk.is_none() {
|
||||
return Err(Error::ErrIdentityNoPsk);
|
||||
}
|
||||
|
||||
for cert in &config.certificates {
|
||||
match cert.private_key.kind {
|
||||
CryptoPrivateKeyKind::Ed25519(_) => {}
|
||||
CryptoPrivateKeyKind::Ecdsa256(_) => {}
|
||||
_ => return Err(Error::ErrInvalidPrivateKey),
|
||||
}
|
||||
}
|
||||
|
||||
parse_cipher_suites(
|
||||
&config.cipher_suites,
|
||||
config.psk.is_none(),
|
||||
config.psk.is_some(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
2464
libs/dtls/src/conn/conn_test.rs
Normal file
2464
libs/dtls/src/conn/conn_test.rs
Normal file
File diff suppressed because it is too large
Load Diff
1212
libs/dtls/src/conn/mod.rs
Normal file
1212
libs/dtls/src/conn/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
85
libs/dtls/src/content.rs
Normal file
85
libs/dtls/src/content.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use super::alert::*;
|
||||
use super::application_data::*;
|
||||
use super::change_cipher_spec::*;
|
||||
use super::handshake::*;
|
||||
use crate::error::*;
|
||||
|
||||
/// ## Specifications
|
||||
///
|
||||
/// * [RFC 4346 §6.2.1]
|
||||
///
|
||||
/// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ContentType {
|
||||
ChangeCipherSpec = 20,
|
||||
Alert = 21,
|
||||
Handshake = 22,
|
||||
ApplicationData = 23,
|
||||
#[default]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl From<u8> for ContentType {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
20 => ContentType::ChangeCipherSpec,
|
||||
21 => ContentType::Alert,
|
||||
22 => ContentType::Handshake,
|
||||
23 => ContentType::ApplicationData,
|
||||
_ => ContentType::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum Content {
|
||||
ChangeCipherSpec(ChangeCipherSpec),
|
||||
Alert(Alert),
|
||||
Handshake(Handshake),
|
||||
ApplicationData(ApplicationData),
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn content_type(&self) -> ContentType {
|
||||
match self {
|
||||
Content::ChangeCipherSpec(c) => c.content_type(),
|
||||
Content::Alert(c) => c.content_type(),
|
||||
Content::Handshake(c) => c.content_type(),
|
||||
Content::ApplicationData(c) => c.content_type(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Content::ChangeCipherSpec(c) => c.size(),
|
||||
Content::Alert(c) => c.size(),
|
||||
Content::Handshake(c) => c.size(),
|
||||
Content::ApplicationData(c) => c.size(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
match self {
|
||||
Content::ChangeCipherSpec(c) => c.marshal(writer),
|
||||
Content::Alert(c) => c.marshal(writer),
|
||||
Content::Handshake(c) => c.marshal(writer),
|
||||
Content::ApplicationData(c) => c.marshal(writer),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(content_type: ContentType, reader: &mut R) -> Result<Self> {
|
||||
match content_type {
|
||||
ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec(
|
||||
ChangeCipherSpec::unmarshal(reader)?,
|
||||
)),
|
||||
ContentType::Alert => Ok(Content::Alert(Alert::unmarshal(reader)?)),
|
||||
ContentType::Handshake => Ok(Content::Handshake(Handshake::unmarshal(reader)?)),
|
||||
ContentType::ApplicationData => Ok(Content::ApplicationData(
|
||||
ApplicationData::unmarshal(reader)?,
|
||||
)),
|
||||
_ => Err(Error::ErrInvalidContentType),
|
||||
}
|
||||
}
|
||||
}
|
||||
136
libs/dtls/src/crypto/crypto_cbc.rs
Normal file
136
libs/dtls/src/crypto/crypto_cbc.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
// AES-CBC (Cipher Block Chaining)
|
||||
// First historic block cipher for AES.
|
||||
// CBC mode is insecure and must not be used. It’s been progressively deprecated and
|
||||
// removed from SSL libraries.
|
||||
// Introduced with TLS 1.0 year 2002. Superseded by GCM in TLS 1.2 year 2008.
|
||||
// Removed in TLS 1.3 year 2018.
|
||||
// RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268
|
||||
|
||||
// https://github.com/RustCrypto/block-ciphers
|
||||
|
||||
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use p256::elliptic_curve::subtle::ConstantTimeEq;
|
||||
use rand::Rng;
|
||||
use std::io::Cursor;
|
||||
use std::ops::Not;
|
||||
|
||||
use super::padding::DtlsPadding;
|
||||
use crate::content::*;
|
||||
use crate::error::*;
|
||||
use crate::prf::*;
|
||||
use crate::record_layer::record_layer_header::*;
|
||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||
|
||||
// State needed to handle encrypted input/output
|
||||
#[derive(Clone)]
|
||||
pub struct CryptoCbc {
|
||||
local_key: Vec<u8>,
|
||||
remote_key: Vec<u8>,
|
||||
write_mac: Vec<u8>,
|
||||
read_mac: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CryptoCbc {
|
||||
const BLOCK_SIZE: usize = 16;
|
||||
const MAC_SIZE: usize = 20;
|
||||
|
||||
pub fn new(
|
||||
local_key: &[u8],
|
||||
local_mac: &[u8],
|
||||
remote_key: &[u8],
|
||||
remote_mac: &[u8],
|
||||
) -> Result<Self> {
|
||||
Ok(CryptoCbc {
|
||||
local_key: local_key.to_vec(),
|
||||
write_mac: local_mac.to_vec(),
|
||||
|
||||
remote_key: remote_key.to_vec(),
|
||||
read_mac: remote_mac.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec();
|
||||
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||
|
||||
// Generate + Append MAC
|
||||
let h = pkt_rlh;
|
||||
|
||||
let mac = prf_mac(
|
||||
h.epoch,
|
||||
h.sequence_number,
|
||||
h.content_type,
|
||||
h.protocol_version,
|
||||
&payload,
|
||||
&self.write_mac,
|
||||
)?;
|
||||
payload.extend_from_slice(&mac);
|
||||
|
||||
let mut iv: Vec<u8> = vec![0; Self::BLOCK_SIZE];
|
||||
rand::rng().fill(iv.as_mut_slice());
|
||||
|
||||
let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?;
|
||||
let encrypted = write_cbc.encrypt_padded_vec_mut::<DtlsPadding>(&payload);
|
||||
|
||||
// Prepend unencrypte header with encrypted payload
|
||||
let mut r = vec![];
|
||||
r.extend_from_slice(raw);
|
||||
r.extend_from_slice(&iv);
|
||||
r.extend_from_slice(&encrypted);
|
||||
|
||||
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||
.copy_from_slice(&r_len.to_be_bytes());
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut reader = Cursor::new(r);
|
||||
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||
if h.content_type == ContentType::ChangeCipherSpec {
|
||||
// Nothing to encrypt with ChangeCipherSpec
|
||||
return Ok(r.to_vec());
|
||||
}
|
||||
|
||||
if r.len() < RECORD_LAYER_HEADER_SIZE + Self::BLOCK_SIZE {
|
||||
return Err(Error::ErrInvalidPacketLength);
|
||||
}
|
||||
|
||||
let body = &r[RECORD_LAYER_HEADER_SIZE..];
|
||||
let iv = &body[0..Self::BLOCK_SIZE];
|
||||
let body = &body[Self::BLOCK_SIZE..];
|
||||
|
||||
if body.is_empty() || !body.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||
return Err(Error::ErrInvalidPacketLength);
|
||||
}
|
||||
|
||||
let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?;
|
||||
|
||||
let decrypted = read_cbc
|
||||
.decrypt_padded_vec_mut::<DtlsPadding>(body)
|
||||
.map_err(|_| Error::ErrInvalidPacketLength)?;
|
||||
|
||||
let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..];
|
||||
let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE];
|
||||
let mac = prf_mac(
|
||||
h.epoch,
|
||||
h.sequence_number,
|
||||
h.content_type,
|
||||
h.protocol_version,
|
||||
decrypted,
|
||||
&self.read_mac,
|
||||
)?;
|
||||
|
||||
if recv_mac.ct_eq(&mac).not().into() {
|
||||
return Err(Error::ErrInvalidMac);
|
||||
}
|
||||
|
||||
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + decrypted.len());
|
||||
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||
d.extend_from_slice(decrypted);
|
||||
|
||||
Ok(d)
|
||||
}
|
||||
}
|
||||
188
libs/dtls/src/crypto/crypto_ccm.rs
Normal file
188
libs/dtls/src/crypto/crypto_ccm.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
// AES-CCM (Counter with CBC-MAC)
|
||||
// Alternative to GCM mode.
|
||||
// Available in OpenSSL as of TLS 1.3 (2018), but disabled by default.
|
||||
// Two AES computations per block, thus expected to be somewhat slower than AES-GCM.
|
||||
// RFC 6655 year 2012 https://tools.ietf.org/html/rfc6655
|
||||
// Much lower adoption, probably because it came after GCM and offer no significant benefit.
|
||||
|
||||
// https://github.com/RustCrypto/AEADs
|
||||
// https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm?
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use aes::Aes128;
|
||||
use ccm::aead::generic_array::GenericArray;
|
||||
use ccm::aead::AeadInPlace;
|
||||
use ccm::consts::{U12, U16, U8};
|
||||
use ccm::Ccm;
|
||||
use ccm::KeyInit;
|
||||
use rand::Rng;
|
||||
|
||||
use super::*;
|
||||
use crate::content::*;
|
||||
use crate::error::*;
|
||||
use crate::record_layer::record_layer_header::*;
|
||||
|
||||
const CRYPTO_CCM_8_TAG_LENGTH: usize = 8;
|
||||
const CRYPTO_CCM_TAG_LENGTH: usize = 16;
|
||||
const CRYPTO_CCM_NONCE_LENGTH: usize = 12;
|
||||
|
||||
type AesCcm8 = Ccm<Aes128, U8, U12>;
|
||||
type AesCcm = Ccm<Aes128, U16, U12>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CryptoCcmTagLen {
|
||||
CryptoCcm8TagLength,
|
||||
CryptoCcmTagLength,
|
||||
}
|
||||
|
||||
enum CryptoCcmType {
|
||||
CryptoCcm8(AesCcm8),
|
||||
CryptoCcm(AesCcm),
|
||||
}
|
||||
|
||||
// State needed to handle encrypted input/output
|
||||
pub struct CryptoCcm {
|
||||
local_ccm: CryptoCcmType,
|
||||
remote_ccm: CryptoCcmType,
|
||||
local_write_iv: Vec<u8>,
|
||||
remote_write_iv: Vec<u8>,
|
||||
// used by clone()
|
||||
local_write_key: Vec<u8>,
|
||||
remote_write_key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Clone for CryptoCcm {
|
||||
fn clone(&self) -> Self {
|
||||
match self.local_ccm {
|
||||
CryptoCcmType::CryptoCcm(_) => Self::new(
|
||||
&CryptoCcmTagLen::CryptoCcmTagLength,
|
||||
&self.local_write_key,
|
||||
&self.local_write_iv,
|
||||
&self.remote_write_key,
|
||||
&self.remote_write_iv,
|
||||
),
|
||||
CryptoCcmType::CryptoCcm8(_) => Self::new(
|
||||
&CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||
&self.local_write_key,
|
||||
&self.local_write_iv,
|
||||
&self.remote_write_key,
|
||||
&self.remote_write_iv,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoCcm {
|
||||
pub fn new(
|
||||
tag_len: &CryptoCcmTagLen,
|
||||
local_key: &[u8],
|
||||
local_write_iv: &[u8],
|
||||
remote_key: &[u8],
|
||||
remote_write_iv: &[u8],
|
||||
) -> Self {
|
||||
let key = GenericArray::from_slice(local_key);
|
||||
let local_ccm = match tag_len {
|
||||
CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)),
|
||||
CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)),
|
||||
};
|
||||
|
||||
let key = GenericArray::from_slice(remote_key);
|
||||
let remote_ccm = match tag_len {
|
||||
CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)),
|
||||
CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)),
|
||||
};
|
||||
|
||||
CryptoCcm {
|
||||
local_ccm,
|
||||
local_write_key: local_key.to_vec(),
|
||||
local_write_iv: local_write_iv.to_vec(),
|
||||
remote_ccm,
|
||||
remote_write_key: remote_key.to_vec(),
|
||||
remote_write_iv: remote_write_iv.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||
|
||||
let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH];
|
||||
nonce[..4].copy_from_slice(&self.local_write_iv[..4]);
|
||||
rand::rng().fill(&mut nonce[4..]);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(payload);
|
||||
|
||||
match &self.local_ccm {
|
||||
CryptoCcmType::CryptoCcm(ccm) => {
|
||||
ccm.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
}
|
||||
CryptoCcmType::CryptoCcm8(ccm8) => {
|
||||
ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len());
|
||||
|
||||
r.extend_from_slice(raw);
|
||||
r.extend_from_slice(&nonce[4..]);
|
||||
r.extend_from_slice(&buffer);
|
||||
|
||||
// Update recordLayer size to include explicit nonce
|
||||
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||
.copy_from_slice(&r_len.to_be_bytes());
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut reader = Cursor::new(r);
|
||||
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||
if h.content_type == ContentType::ChangeCipherSpec {
|
||||
// Nothing to encrypt with ChangeCipherSpec
|
||||
return Ok(r.to_vec());
|
||||
}
|
||||
|
||||
if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) {
|
||||
return Err(Error::ErrNotEnoughRoomForNonce);
|
||||
}
|
||||
|
||||
let mut nonce = vec![];
|
||||
nonce.extend_from_slice(&self.remote_write_iv[..4]);
|
||||
nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let out = &r[RECORD_LAYER_HEADER_SIZE + 8..];
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(out);
|
||||
|
||||
match &self.remote_ccm {
|
||||
CryptoCcmType::CryptoCcm(ccm) => {
|
||||
let additional_data =
|
||||
generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH);
|
||||
ccm.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
}
|
||||
CryptoCcmType::CryptoCcm8(ccm8) => {
|
||||
let additional_data =
|
||||
generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH);
|
||||
ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||
d.extend_from_slice(&buffer);
|
||||
|
||||
Ok(d)
|
||||
}
|
||||
}
|
||||
123
libs/dtls/src/crypto/crypto_chacha20.rs
Normal file
123
libs/dtls/src/crypto/crypto_chacha20.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use chacha20poly1305::aead::generic_array::GenericArray;
|
||||
use chacha20poly1305::aead::AeadInPlace;
|
||||
use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
|
||||
|
||||
use super::*;
|
||||
use crate::content::*;
|
||||
use crate::error::*;
|
||||
use crate::record_layer::record_layer_header::*; // what about Aes256Gcm?
|
||||
|
||||
const CRYPTO_CHACHA20_TAG_LENGTH: usize = 16;
|
||||
const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12;
|
||||
|
||||
// State needed to handle encrypted input/output
|
||||
#[derive(Clone)]
|
||||
pub struct CryptoChaCha20 {
|
||||
local_cc: ChaCha20Poly1305,
|
||||
remote_cc: ChaCha20Poly1305,
|
||||
local_key: Vec<u8>,
|
||||
remote_key: Vec<u8>,
|
||||
local_write_iv: Vec<u8>,
|
||||
remote_write_iv: Vec<u8>,
|
||||
}
|
||||
|
||||
fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) {
|
||||
let epoch: u64 = epoch.into();
|
||||
let seqnum = (seqnum & 0xFFFFFFFFFFFF) | (epoch << 48);
|
||||
for i in 0..8 {
|
||||
nonce[i + 4] ^= ((seqnum >> (8 * (7 - i))) & 0xFF) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoChaCha20 {
|
||||
pub fn new(
|
||||
local_key: &[u8],
|
||||
local_write_iv: &[u8],
|
||||
remote_key: &[u8],
|
||||
remote_write_iv: &[u8],
|
||||
) -> Self {
|
||||
let key = GenericArray::from_slice(local_key);
|
||||
let local_cc = ChaCha20Poly1305::new(key);
|
||||
|
||||
let key = GenericArray::from_slice(remote_key);
|
||||
let remote_cc = ChaCha20Poly1305::new(key);
|
||||
|
||||
CryptoChaCha20 {
|
||||
local_cc,
|
||||
local_write_iv: local_write_iv.to_vec(),
|
||||
remote_cc,
|
||||
local_key: local_key.to_vec(),
|
||||
remote_key: remote_key.to_vec(),
|
||||
remote_write_iv: remote_write_iv.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||
|
||||
let mut nonce = vec![0u8; CRYPTO_CHACHA20_NONCE_LENGTH];
|
||||
nonce[..CRYPTO_CHACHA20_NONCE_LENGTH]
|
||||
.copy_from_slice(&self.local_write_iv[..CRYPTO_CHACHA20_NONCE_LENGTH]);
|
||||
|
||||
noncegen(&mut nonce[..], pkt_rlh.epoch, pkt_rlh.sequence_number);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(payload);
|
||||
|
||||
let tag = self
|
||||
.local_cc
|
||||
.encrypt_in_place_detached(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
let mut r = Vec::with_capacity(raw.len() + buffer.len() + tag.len());
|
||||
r.extend_from_slice(raw);
|
||||
r.extend_from_slice(&buffer);
|
||||
r.extend_from_slice(&tag);
|
||||
|
||||
// Update recordLayer size to include explicit nonce
|
||||
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||
.copy_from_slice(&r_len.to_be_bytes());
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut reader = Cursor::new(r);
|
||||
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||
if h.content_type == ContentType::ChangeCipherSpec {
|
||||
// Nothing to encrypt with ChangeCipherSpec
|
||||
return Ok(r.to_vec());
|
||||
}
|
||||
|
||||
let mut nonce = vec![];
|
||||
nonce.extend_from_slice(&self.remote_write_iv[..]);
|
||||
|
||||
noncegen(&mut nonce[..], h.epoch, h.sequence_number);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let out = &r[RECORD_LAYER_HEADER_SIZE..];
|
||||
|
||||
let additional_data =
|
||||
generate_aead_additional_data(&h, out.len() - CRYPTO_CHACHA20_TAG_LENGTH);
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(out);
|
||||
|
||||
self.remote_cc
|
||||
.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||
d.extend_from_slice(&buffer);
|
||||
|
||||
Ok(d)
|
||||
}
|
||||
}
|
||||
119
libs/dtls/src/crypto/crypto_gcm.rs
Normal file
119
libs/dtls/src/crypto/crypto_gcm.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
// AES-GCM (Galois Counter Mode)
|
||||
// The most widely used block cipher worldwide.
|
||||
// Mandatory as of TLS 1.2 (2008) and used by default by most clients.
|
||||
// RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288
|
||||
|
||||
// https://github.com/RustCrypto/AEADs
|
||||
// https://docs.rs/aes-gcm/0.8.0/aes_gcm/
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use aes_gcm::aead::generic_array::GenericArray;
|
||||
use aes_gcm::aead::AeadInPlace;
|
||||
use aes_gcm::{Aes128Gcm, KeyInit};
|
||||
use rand::Rng;
|
||||
|
||||
use super::*;
|
||||
use crate::content::*;
|
||||
use crate::error::*;
|
||||
use crate::record_layer::record_layer_header::*; // what about Aes256Gcm?
|
||||
|
||||
const CRYPTO_GCM_TAG_LENGTH: usize = 16;
|
||||
const CRYPTO_GCM_NONCE_LENGTH: usize = 12;
|
||||
|
||||
// State needed to handle encrypted input/output
|
||||
#[derive(Clone)]
|
||||
pub struct CryptoGcm {
|
||||
local_gcm: Aes128Gcm,
|
||||
remote_gcm: Aes128Gcm,
|
||||
local_write_iv: Vec<u8>,
|
||||
remote_write_iv: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CryptoGcm {
|
||||
pub fn new(
|
||||
local_key: &[u8],
|
||||
local_write_iv: &[u8],
|
||||
remote_key: &[u8],
|
||||
remote_write_iv: &[u8],
|
||||
) -> Self {
|
||||
let key = GenericArray::from_slice(local_key);
|
||||
let local_gcm = Aes128Gcm::new(key);
|
||||
|
||||
let key = GenericArray::from_slice(remote_key);
|
||||
let remote_gcm = Aes128Gcm::new(key);
|
||||
|
||||
CryptoGcm {
|
||||
local_gcm,
|
||||
local_write_iv: local_write_iv.to_vec(),
|
||||
remote_gcm,
|
||||
remote_write_iv: remote_write_iv.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||
|
||||
let mut nonce = vec![0u8; CRYPTO_GCM_NONCE_LENGTH];
|
||||
nonce[..4].copy_from_slice(&self.local_write_iv[..4]);
|
||||
rand::rng().fill(&mut nonce[4..]);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(payload);
|
||||
|
||||
self.local_gcm
|
||||
.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len());
|
||||
r.extend_from_slice(raw);
|
||||
r.extend_from_slice(&nonce[4..]);
|
||||
r.extend_from_slice(&buffer);
|
||||
|
||||
// Update recordLayer size to include explicit nonce
|
||||
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||
.copy_from_slice(&r_len.to_be_bytes());
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut reader = Cursor::new(r);
|
||||
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||
if h.content_type == ContentType::ChangeCipherSpec {
|
||||
// Nothing to encrypt with ChangeCipherSpec
|
||||
return Ok(r.to_vec());
|
||||
}
|
||||
|
||||
if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) {
|
||||
return Err(Error::ErrNotEnoughRoomForNonce);
|
||||
}
|
||||
|
||||
let mut nonce = vec![];
|
||||
nonce.extend_from_slice(&self.remote_write_iv[..4]);
|
||||
nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
|
||||
let out = &r[RECORD_LAYER_HEADER_SIZE + 8..];
|
||||
|
||||
let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_GCM_TAG_LENGTH);
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
buffer.extend_from_slice(out);
|
||||
|
||||
self.remote_gcm
|
||||
.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||
d.extend_from_slice(&buffer);
|
||||
|
||||
Ok(d)
|
||||
}
|
||||
}
|
||||
221
libs/dtls/src/crypto/crypto_test.rs
Normal file
221
libs/dtls/src/crypto/crypto_test.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use x509_parser::pem::Pem;
|
||||
|
||||
use super::crypto_ccm::*;
|
||||
use super::*;
|
||||
use crate::content::ContentType;
|
||||
use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE};
|
||||
|
||||
const RAW_PRIVATE_KEY: &str = "
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP
|
||||
6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3
|
||||
bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1
|
||||
ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB
|
||||
ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL
|
||||
rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0
|
||||
N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra
|
||||
ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3
|
||||
AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0
|
||||
xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD
|
||||
8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl
|
||||
pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22
|
||||
HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S
|
||||
Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87
|
||||
mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin
|
||||
mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71
|
||||
hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5
|
||||
lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH
|
||||
BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz
|
||||
uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ
|
||||
srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq
|
||||
eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O
|
||||
UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr
|
||||
nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L
|
||||
JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/
|
||||
-----END RSA PRIVATE KEY-----
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_generate_key_signature() -> Result<()> {
|
||||
let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes());
|
||||
let pem = match Pem::read(reader) {
|
||||
Ok((pem, _)) => pem,
|
||||
Err(_) => return Err(Error::Other("Pem::read error".to_owned())),
|
||||
};
|
||||
//let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?;
|
||||
|
||||
let client_random = vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
|
||||
0x1e, 0x1f,
|
||||
];
|
||||
let server_random = vec![
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e,
|
||||
0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
|
||||
0x8e, 0x8f,
|
||||
];
|
||||
let public_key = vec![
|
||||
0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a,
|
||||
0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28,
|
||||
0x80, 0xb6, 0x15,
|
||||
];
|
||||
let expected_signature = vec![
|
||||
0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03,
|
||||
0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29,
|
||||
0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15,
|
||||
0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b,
|
||||
0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0,
|
||||
0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81,
|
||||
0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34,
|
||||
0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53,
|
||||
0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52,
|
||||
0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55,
|
||||
0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88,
|
||||
0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb,
|
||||
0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d,
|
||||
0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74,
|
||||
0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25,
|
||||
0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2,
|
||||
0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75,
|
||||
0x86,
|
||||
];
|
||||
|
||||
let signature = generate_key_signature(
|
||||
&client_random,
|
||||
&server_random,
|
||||
&public_key,
|
||||
NamedCurve::X25519,
|
||||
&CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Rsa256(
|
||||
ring::rsa::KeyPair::from_der(&pem.contents)
|
||||
.map_err(|e| Error::Other(e.to_string()))?,
|
||||
),
|
||||
serialized_der: pem.contents.clone(),
|
||||
}, //hashAlgorithmSHA256,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
signature, expected_signature,
|
||||
"Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} "
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ccm_encryption_and_decryption() -> Result<()> {
|
||||
let key = vec![
|
||||
0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, 0x63,
|
||||
0x54,
|
||||
];
|
||||
let iv = vec![0x0e, 0xb2, 0x09, 0x06];
|
||||
|
||||
let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv);
|
||||
|
||||
let rlh = RecordLayerHeader {
|
||||
content_type: ContentType::ApplicationData,
|
||||
protocol_version: ProtocolVersion {
|
||||
major: 0xfe,
|
||||
minor: 0xff,
|
||||
},
|
||||
epoch: 0,
|
||||
sequence_number: 18,
|
||||
content_len: 3,
|
||||
};
|
||||
|
||||
let raw = vec![
|
||||
0x17, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x03, 0xff, 0xaa,
|
||||
0xbb,
|
||||
];
|
||||
|
||||
let cipher_text = ccm.encrypt(&rlh, &raw)?;
|
||||
|
||||
assert_eq!(
|
||||
&cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE],
|
||||
[0, 27],
|
||||
"RecordLayer size updating failed \nexp: {:?} \nactual {:?} ",
|
||||
[0, 27],
|
||||
&cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||
);
|
||||
|
||||
let plain_text = ccm.decrypt(&cipher_text)?;
|
||||
|
||||
assert_eq!(
|
||||
raw[RECORD_LAYER_HEADER_SIZE..],
|
||||
plain_text[RECORD_LAYER_HEADER_SIZE..],
|
||||
"Decryption failed \nexp: {:?} \nactual {:?} ",
|
||||
&raw[RECORD_LAYER_HEADER_SIZE..],
|
||||
&plain_text[RECORD_LAYER_HEADER_SIZE..]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_verify() -> Result<()> {
|
||||
let plain_text: Vec<u8> = vec![
|
||||
0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03,
|
||||
0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29,
|
||||
0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15,
|
||||
0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b,
|
||||
0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0,
|
||||
0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81,
|
||||
0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34,
|
||||
0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53,
|
||||
0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52,
|
||||
0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55,
|
||||
0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88,
|
||||
0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb,
|
||||
0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d,
|
||||
0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74,
|
||||
0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25,
|
||||
0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2,
|
||||
0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75,
|
||||
0x86,
|
||||
];
|
||||
|
||||
//test ECDSA256
|
||||
let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?;
|
||||
let cert_verify_ecdsa256 =
|
||||
generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?;
|
||||
verify_certificate_verify(
|
||||
&plain_text,
|
||||
&SignatureHashAlgorithm {
|
||||
hash: HashAlgorithm::Sha256,
|
||||
signature: SignatureAlgorithm::Ecdsa,
|
||||
},
|
||||
&cert_verify_ecdsa256,
|
||||
&certificate_ecdsa256
|
||||
.certificate
|
||||
.iter()
|
||||
.map(|x| x.as_ref().to_owned())
|
||||
.collect::<Vec<Vec<u8>>>(),
|
||||
false,
|
||||
)?;
|
||||
|
||||
//test ED25519
|
||||
let certificate_ed25519 = Certificate::generate_self_signed_with_alg(
|
||||
vec!["localhost".to_owned()],
|
||||
&rcgen::PKCS_ED25519,
|
||||
)?;
|
||||
let cert_verify_ed25519 =
|
||||
generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?;
|
||||
verify_certificate_verify(
|
||||
&plain_text,
|
||||
&SignatureHashAlgorithm {
|
||||
hash: HashAlgorithm::Sha256,
|
||||
signature: SignatureAlgorithm::Ed25519,
|
||||
},
|
||||
&cert_verify_ed25519,
|
||||
&certificate_ed25519
|
||||
.certificate
|
||||
.iter()
|
||||
.map(|x| x.as_ref().to_owned())
|
||||
.collect::<Vec<Vec<u8>>>(),
|
||||
false,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
514
libs/dtls/src/crypto/mod.rs
Normal file
514
libs/dtls/src/crypto/mod.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
#[cfg(test)]
|
||||
mod crypto_test;
|
||||
|
||||
pub mod crypto_cbc;
|
||||
pub mod crypto_ccm;
|
||||
pub mod crypto_chacha20;
|
||||
pub mod crypto_gcm;
|
||||
pub mod padding;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
|
||||
use der_parser::oid;
|
||||
use der_parser::oid::Oid;
|
||||
|
||||
use crate::certificate_verifier::CertificateVerifier;
|
||||
use rustls_pki_types::CertificateDer;
|
||||
|
||||
use rcgen::{generate_simple_self_signed, CertifiedKey, KeyPair};
|
||||
use ring::rand::SystemRandom;
|
||||
use ring::signature::{EcdsaKeyPair, Ed25519KeyPair};
|
||||
|
||||
use crate::curve::named_curve::*;
|
||||
use crate::error::*;
|
||||
use crate::record_layer::record_layer_header::*;
|
||||
use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm};
|
||||
|
||||
/// A X.509 certificate(s) used to authenticate a DTLS connection.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Certificate {
|
||||
/// DER-encoded certificates.
|
||||
pub certificate: Vec<CertificateDer<'static>>,
|
||||
/// Private key.
|
||||
pub private_key: CryptoPrivateKey,
|
||||
}
|
||||
|
||||
impl Certificate {
|
||||
/// Generate a self-signed certificate.
|
||||
///
|
||||
/// See [`rcgen::generate_simple_self_signed`].
|
||||
pub fn generate_self_signed(subject_alt_names: impl Into<Vec<String>>) -> Result<Self> {
|
||||
let CertifiedKey { cert, key_pair } =
|
||||
generate_simple_self_signed(subject_alt_names).unwrap();
|
||||
Ok(Certificate {
|
||||
certificate: vec![cert.der().to_owned()],
|
||||
private_key: CryptoPrivateKey::try_from(&key_pair)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a self-signed certificate with the given algorithm.
|
||||
///
|
||||
/// See [`rcgen::Certificate::from_params`].
|
||||
pub fn generate_self_signed_with_alg(
|
||||
subject_alt_names: impl Into<Vec<String>>,
|
||||
alg: &'static rcgen::SignatureAlgorithm,
|
||||
) -> Result<Self> {
|
||||
let params = rcgen::CertificateParams::new(subject_alt_names).unwrap();
|
||||
let key_pair = rcgen::KeyPair::generate_for(alg).unwrap();
|
||||
let cert = params.self_signed(&key_pair).unwrap();
|
||||
|
||||
Ok(Certificate {
|
||||
certificate: vec![cert.der().to_owned()],
|
||||
private_key: CryptoPrivateKey::try_from(&key_pair)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a certificate from the ASCII PEM format.
|
||||
#[cfg(feature = "pem")]
|
||||
pub fn from_pem(pem_str: &str) -> Result<Self> {
|
||||
let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?;
|
||||
if pems.len() < 2 {
|
||||
return Err(Error::InvalidPEM(format!(
|
||||
"expected at least two PEM blocks, got {}",
|
||||
pems.len()
|
||||
)));
|
||||
}
|
||||
if pems[0].tag() != "PRIVATE_KEY" {
|
||||
return Err(Error::InvalidPEM(format!(
|
||||
"invalid tag (expected: 'PRIVATE_KEY', got: '{}')",
|
||||
pems[0].tag()
|
||||
)));
|
||||
}
|
||||
|
||||
let keypair = KeyPair::try_from(pems[0].contents())
|
||||
.map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?;
|
||||
|
||||
let mut rustls_certs = Vec::new();
|
||||
for p in pems.drain(1..) {
|
||||
if p.tag() != "CERTIFICATE" {
|
||||
return Err(Error::InvalidPEM(format!(
|
||||
"invalid tag (expected: 'CERTIFICATE', got: '{}')",
|
||||
p.tag()
|
||||
)));
|
||||
}
|
||||
rustls_certs.push(CertificateDer::from(p.contents().to_vec()));
|
||||
}
|
||||
|
||||
Ok(Certificate {
|
||||
certificate: rustls_certs,
|
||||
private_key: CryptoPrivateKey::try_from(&keypair)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serializes the certificate (including the private key) in PKCS#8 format in PEM.
|
||||
#[cfg(feature = "pem")]
|
||||
pub fn serialize_pem(&self) -> String {
|
||||
let mut data = vec![pem::Pem::new(
|
||||
"PRIVATE_KEY".to_string(),
|
||||
self.private_key.serialized_der.clone(),
|
||||
)];
|
||||
for rustls_cert in &self.certificate {
|
||||
data.push(pem::Pem::new(
|
||||
"CERTIFICATE".to_string(),
|
||||
rustls_cert.as_ref(),
|
||||
));
|
||||
}
|
||||
pem::encode_many(&data)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn value_key_message(
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
public_key: &[u8],
|
||||
named_curve: NamedCurve,
|
||||
) -> Vec<u8> {
|
||||
let mut server_ecdh_params = vec![0u8; 4];
|
||||
server_ecdh_params[0] = 3; // named curve
|
||||
server_ecdh_params[1..3].copy_from_slice(&(named_curve as u16).to_be_bytes());
|
||||
server_ecdh_params[3] = public_key.len() as u8;
|
||||
|
||||
let mut plaintext = vec![];
|
||||
plaintext.extend_from_slice(client_random);
|
||||
plaintext.extend_from_slice(server_random);
|
||||
plaintext.extend_from_slice(&server_ecdh_params);
|
||||
plaintext.extend_from_slice(public_key);
|
||||
|
||||
plaintext
|
||||
}
|
||||
|
||||
/// Either ED25519, ECDSA or RSA keypair.
|
||||
#[derive(Debug)]
|
||||
pub enum CryptoPrivateKeyKind {
|
||||
Ed25519(Ed25519KeyPair),
|
||||
Ecdsa256(EcdsaKeyPair),
|
||||
Rsa256(ring::rsa::KeyPair),
|
||||
}
|
||||
|
||||
/// Private key.
|
||||
#[derive(Debug)]
|
||||
pub struct CryptoPrivateKey {
|
||||
/// Keypair.
|
||||
pub kind: CryptoPrivateKeyKind,
|
||||
/// DER-encoded keypair.
|
||||
pub serialized_der: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PartialEq for CryptoPrivateKey {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.serialized_der != other.serialized_der {
|
||||
return false;
|
||||
}
|
||||
|
||||
matches!(
|
||||
(&self.kind, &other.kind),
|
||||
(
|
||||
CryptoPrivateKeyKind::Rsa256(_),
|
||||
CryptoPrivateKeyKind::Rsa256(_)
|
||||
) | (
|
||||
CryptoPrivateKeyKind::Ecdsa256(_),
|
||||
CryptoPrivateKeyKind::Ecdsa256(_)
|
||||
) | (
|
||||
CryptoPrivateKeyKind::Ed25519(_),
|
||||
CryptoPrivateKeyKind::Ed25519(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for CryptoPrivateKey {
|
||||
fn clone(&self) -> Self {
|
||||
match self.kind {
|
||||
CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Ed25519(
|
||||
Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(),
|
||||
),
|
||||
serialized_der: self.serialized_der.clone(),
|
||||
},
|
||||
CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Ecdsa256(
|
||||
EcdsaKeyPair::from_pkcs8(
|
||||
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||
&self.serialized_der,
|
||||
&SystemRandom::new(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
serialized_der: self.serialized_der.clone(),
|
||||
},
|
||||
CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Rsa256(
|
||||
ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(),
|
||||
),
|
||||
serialized_der: self.serialized_der.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&KeyPair> for CryptoPrivateKey {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(key_pair: &KeyPair) -> Result<Self> {
|
||||
Self::from_key_pair(key_pair)
|
||||
}
|
||||
}
|
||||
|
||||
impl CryptoPrivateKey {
|
||||
pub fn from_key_pair(key_pair: &KeyPair) -> Result<Self> {
|
||||
let serialized_der = key_pair.serialize_der();
|
||||
if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
|
||||
Ok(CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Ed25519(
|
||||
Ed25519KeyPair::from_pkcs8_maybe_unchecked(&serialized_der)
|
||||
.map_err(|e| Error::Other(e.to_string()))?,
|
||||
),
|
||||
serialized_der,
|
||||
})
|
||||
} else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
|
||||
Ok(CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Ecdsa256(
|
||||
EcdsaKeyPair::from_pkcs8(
|
||||
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||
&serialized_der,
|
||||
&SystemRandom::new(),
|
||||
)
|
||||
.map_err(|e| Error::Other(e.to_string()))?,
|
||||
),
|
||||
serialized_der,
|
||||
})
|
||||
} else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
|
||||
Ok(CryptoPrivateKey {
|
||||
kind: CryptoPrivateKeyKind::Rsa256(
|
||||
ring::rsa::KeyPair::from_pkcs8(&serialized_der)
|
||||
.map_err(|e| Error::Other(e.to_string()))?,
|
||||
),
|
||||
serialized_der,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Other("Unsupported key_pair".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the client provided a "signature_algorithms" extension, then all
|
||||
// certificates provided by the server MUST be signed by a
|
||||
// hash/signature algorithm pair that appears in that extension
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc5246#section-7.4.2
|
||||
pub(crate) fn generate_key_signature(
|
||||
client_random: &[u8],
|
||||
server_random: &[u8],
|
||||
public_key: &[u8],
|
||||
named_curve: NamedCurve,
|
||||
private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/
|
||||
) -> Result<Vec<u8>> {
|
||||
let msg = value_key_message(client_random, server_random, public_key, named_curve);
|
||||
let signature = match &private_key.kind {
|
||||
CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(),
|
||||
CryptoPrivateKeyKind::Ecdsa256(kp) => {
|
||||
let system_random = SystemRandom::new();
|
||||
kp.sign(&system_random, &msg)
|
||||
.map_err(|e| Error::Other(e.to_string()))?
|
||||
.as_ref()
|
||||
.to_vec()
|
||||
}
|
||||
CryptoPrivateKeyKind::Rsa256(kp) => {
|
||||
let system_random = SystemRandom::new();
|
||||
let mut signature = vec![0; kp.public().modulus_len()];
|
||||
kp.sign(
|
||||
&ring::signature::RSA_PKCS1_SHA256,
|
||||
&system_random,
|
||||
&msg,
|
||||
&mut signature,
|
||||
)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
signature
|
||||
}
|
||||
};
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
// add OID_ED25519 which is not defined in x509_parser
|
||||
pub const OID_ED25519: Oid<'static> = oid!(1.3.101 .112);
|
||||
pub const OID_ECDSA: Oid<'static> = oid!(1.2.840 .10045 .2 .1);
|
||||
|
||||
fn verify_signature(
|
||||
message: &[u8],
|
||||
hash_algorithm: &SignatureHashAlgorithm,
|
||||
remote_key_signature: &[u8],
|
||||
raw_certificates: &[Vec<u8>],
|
||||
insecure_verification: bool,
|
||||
) -> Result<()> {
|
||||
if raw_certificates.is_empty() {
|
||||
return Err(Error::ErrLengthMismatch);
|
||||
}
|
||||
|
||||
let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0])
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature {
|
||||
SignatureAlgorithm::Ed25519 => &ring::signature::ED25519,
|
||||
SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => {
|
||||
&ring::signature::ECDSA_P256_SHA256_ASN1
|
||||
}
|
||||
SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => {
|
||||
&ring::signature::ECDSA_P384_SHA384_ASN1
|
||||
}
|
||||
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => {
|
||||
&ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY
|
||||
}
|
||||
SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => {
|
||||
if remote_key_signature.len() < 256 && insecure_verification {
|
||||
&ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
|
||||
} else {
|
||||
&ring::signature::RSA_PKCS1_2048_8192_SHA256
|
||||
}
|
||||
}
|
||||
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => {
|
||||
&ring::signature::RSA_PKCS1_2048_8192_SHA384
|
||||
}
|
||||
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => {
|
||||
if remote_key_signature.len() < 256 && insecure_verification {
|
||||
&ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY
|
||||
} else {
|
||||
&ring::signature::RSA_PKCS1_2048_8192_SHA512
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::ErrKeySignatureVerifyUnimplemented),
|
||||
};
|
||||
|
||||
log::trace!("Picked an algorithm {verify_alg:?}");
|
||||
|
||||
let public_key = ring::signature::UnparsedPublicKey::new(
|
||||
verify_alg,
|
||||
certificate
|
||||
.tbs_certificate
|
||||
.subject_pki
|
||||
.subject_public_key
|
||||
.data,
|
||||
);
|
||||
|
||||
public_key
|
||||
.verify(message, remote_key_signature)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn verify_key_signature(
|
||||
message: &[u8],
|
||||
hash_algorithm: &SignatureHashAlgorithm,
|
||||
remote_key_signature: &[u8],
|
||||
raw_certificates: &[Vec<u8>],
|
||||
insecure_verification: bool,
|
||||
) -> Result<()> {
|
||||
verify_signature(
|
||||
message,
|
||||
hash_algorithm,
|
||||
remote_key_signature,
|
||||
raw_certificates,
|
||||
insecure_verification,
|
||||
)
|
||||
}
|
||||
|
||||
// If the server has sent a CertificateRequest message, the client MUST send the Certificate
|
||||
// message. The ClientKeyExchange message is now sent, and the content
|
||||
// of that message will depend on the public key algorithm selected
|
||||
// between the ClientHello and the ServerHello. If the client has sent
|
||||
// a certificate with signing ability, a digitally-signed
|
||||
// CertificateVerify message is sent to explicitly verify possession of
|
||||
// the private key in the certificate.
|
||||
// https://tools.ietf.org/html/rfc5246#section-7.3
|
||||
pub(crate) fn generate_certificate_verify(
|
||||
handshake_bodies: &[u8],
|
||||
private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/
|
||||
) -> Result<Vec<u8>> {
|
||||
let signature = match &private_key.kind {
|
||||
CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(),
|
||||
CryptoPrivateKeyKind::Ecdsa256(kp) => {
|
||||
let system_random = SystemRandom::new();
|
||||
kp.sign(&system_random, handshake_bodies)
|
||||
.map_err(|e| Error::Other(e.to_string()))?
|
||||
.as_ref()
|
||||
.to_vec()
|
||||
}
|
||||
CryptoPrivateKeyKind::Rsa256(kp) => {
|
||||
let system_random = SystemRandom::new();
|
||||
let mut signature = vec![0; kp.public().modulus_len()];
|
||||
kp.sign(
|
||||
&ring::signature::RSA_PKCS1_SHA256,
|
||||
&system_random,
|
||||
handshake_bodies,
|
||||
&mut signature,
|
||||
)
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
|
||||
signature
|
||||
}
|
||||
};
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_certificate_verify(
|
||||
handshake_bodies: &[u8],
|
||||
hash_algorithm: &SignatureHashAlgorithm,
|
||||
remote_key_signature: &[u8],
|
||||
raw_certificates: &[Vec<u8>],
|
||||
insecure_verification: bool,
|
||||
) -> Result<()> {
|
||||
verify_signature(
|
||||
handshake_bodies,
|
||||
hash_algorithm,
|
||||
remote_key_signature,
|
||||
raw_certificates,
|
||||
insecure_verification,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn load_certs(raw_certificates: &[Vec<u8>]) -> Result<Vec<CertificateDer<'static>>> {
|
||||
if raw_certificates.is_empty() {
|
||||
return Err(Error::ErrLengthMismatch);
|
||||
}
|
||||
|
||||
let mut certs = vec![];
|
||||
for raw_cert in raw_certificates {
|
||||
let cert = CertificateDer::from(raw_cert.to_vec());
|
||||
certs.push(cert);
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_client_cert(
|
||||
raw_certificates: &[Vec<u8>],
|
||||
cert_verifier: &Arc<CertificateVerifier>,
|
||||
) -> Result<Vec<CertificateDer<'static>>> {
|
||||
let chains = load_certs(raw_certificates)?;
|
||||
|
||||
let (end_entity, intermediates) = chains
|
||||
.split_first()
|
||||
.ok_or(Error::ErrClientCertificateRequired)?;
|
||||
|
||||
match cert_verifier.verify_client_cert(end_entity, intermediates) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return Err(Error::Other(err.to_string())),
|
||||
};
|
||||
|
||||
Ok(chains)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_server_cert(
|
||||
raw_certificates: &[Vec<u8>],
|
||||
cert_verifier: &Arc<CertificateVerifier>,
|
||||
server_name: &str,
|
||||
) -> Result<Vec<CertificateDer<'static>>> {
|
||||
let chains = load_certs(raw_certificates)?;
|
||||
|
||||
let (end_entity, intermediates) = chains
|
||||
.split_first()
|
||||
.ok_or(Error::ErrServerMustHaveCertificate)?;
|
||||
match cert_verifier.verify_server_cert(end_entity, intermediates, server_name) {
|
||||
Ok(_) => {}
|
||||
Err(err) => return Err(Error::Other(err.to_string())),
|
||||
};
|
||||
|
||||
Ok(chains)
|
||||
}
|
||||
|
||||
pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: usize) -> Vec<u8> {
|
||||
let mut additional_data = vec![0u8; 13];
|
||||
// SequenceNumber MUST be set first
|
||||
// we only want uint48, clobbering an extra 2 (using uint64, rust doesn't have uint48)
|
||||
additional_data[..8].copy_from_slice(&h.sequence_number.to_be_bytes());
|
||||
additional_data[..2].copy_from_slice(&h.epoch.to_be_bytes());
|
||||
additional_data[8] = h.content_type as u8;
|
||||
additional_data[9] = h.protocol_version.major;
|
||||
additional_data[10] = h.protocol_version.minor;
|
||||
additional_data[11..].copy_from_slice(&(payload_len as u16).to_be_bytes());
|
||||
|
||||
additional_data
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[cfg(feature = "pem")]
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "pem")]
|
||||
#[test]
|
||||
fn test_certificate_serialize_pem_and_from_pem() -> crate::error::Result<()> {
|
||||
let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?;
|
||||
|
||||
let pem = cert.serialize_pem();
|
||||
let loaded_cert = Certificate::from_pem(&pem)?;
|
||||
|
||||
assert_eq!(loaded_cert, cert);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
122
libs/dtls/src/crypto/padding.rs
Normal file
122
libs/dtls/src/crypto/padding.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError};
|
||||
use core::panic;
|
||||
|
||||
pub enum DtlsPadding {}
|
||||
/// Reference: RFC5246, 6.2.3.2
|
||||
impl RawPadding for DtlsPadding {
|
||||
const TYPE: PadType = PadType::Reversible;
|
||||
|
||||
fn raw_pad(block: &mut [u8], pos: usize) {
|
||||
if pos >= block.len() {
|
||||
panic!("`pos` is bigger or equal to block size");
|
||||
}
|
||||
|
||||
let padding_length = block.len() - pos - 1;
|
||||
if padding_length > 255 {
|
||||
panic!("block size is too big for DTLS");
|
||||
}
|
||||
|
||||
set(&mut block[pos..], padding_length as u8);
|
||||
}
|
||||
|
||||
fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> {
|
||||
let padding_length = data.last().copied().unwrap_or(1) as usize;
|
||||
if padding_length + 1 > data.len() {
|
||||
return Err(UnpadError);
|
||||
}
|
||||
|
||||
let padding_begin = data.len() - padding_length - 1;
|
||||
|
||||
if data[padding_begin..data.len() - 1]
|
||||
.iter()
|
||||
.any(|&byte| byte as usize != padding_length)
|
||||
{
|
||||
return Err(UnpadError);
|
||||
}
|
||||
|
||||
Ok(&data[0..padding_begin])
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets all bytes in `dst` equal to `value`
|
||||
#[inline(always)]
|
||||
fn set(dst: &mut [u8], value: u8) {
|
||||
// SAFETY: we overwrite valid memory behind `dst`
|
||||
// note: loop is not used here because it produces
|
||||
// unnecessary branch which tests for zero-length slices
|
||||
unsafe {
|
||||
core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use rand::Rng;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> {
|
||||
for original_length in 0..128 {
|
||||
for padding_length in 0..(256 - original_length) {
|
||||
let mut block = vec![0; original_length + padding_length + 1];
|
||||
rand::rng().fill(&mut block[0..original_length]);
|
||||
let original = block[0..original_length].to_vec();
|
||||
DtlsPadding::raw_pad(&mut block, original_length);
|
||||
|
||||
for byte in block[original_length..].iter() {
|
||||
assert_eq!(*byte as usize, padding_length);
|
||||
}
|
||||
assert_eq!(block[0..original_length], original);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn full_block_is_padding_error() {
|
||||
for original_length in 0..256 {
|
||||
let mut block = vec![0; original_length];
|
||||
DtlsPadding::raw_pad(&mut block, original_length);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn padding_length_bigger_than_255_is_a_pad_error() {
|
||||
let padding_length = 256;
|
||||
for original_length in 0..128 {
|
||||
let mut block = vec![0; original_length + padding_length + 1];
|
||||
DtlsPadding::raw_pad(&mut block, original_length);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_block_is_unpadding_error() {
|
||||
let r = DtlsPadding::raw_unpad(&[]);
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_too_big_for_block_is_unpadding_error() {
|
||||
let r = DtlsPadding::raw_unpad(&[1]);
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() {
|
||||
for padding_length in 0..16 {
|
||||
for invalid_byte in 0..padding_length {
|
||||
let mut block = vec![0; padding_length + 1];
|
||||
DtlsPadding::raw_pad(&mut block, 0);
|
||||
|
||||
assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..]));
|
||||
block[invalid_byte] = (padding_length - 1) as u8;
|
||||
let r = DtlsPadding::raw_unpad(&block);
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
libs/dtls/src/curve/mod.rs
Normal file
17
libs/dtls/src/curve/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
pub mod named_curve;
|
||||
|
||||
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EllipticCurveType {
|
||||
NamedCurve = 0x03,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl From<u8> for EllipticCurveType {
|
||||
fn from(val: u8) -> Self {
|
||||
match val {
|
||||
0x03 => EllipticCurveType::NamedCurve,
|
||||
_ => EllipticCurveType::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
83
libs/dtls/src/curve/named_curve.rs
Normal file
83
libs/dtls/src/curve/named_curve.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use rand_core::OsRng; // requires 'getrandom' feature
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum NamedCurve {
|
||||
Unsupported = 0x0000,
|
||||
P256 = 0x0017,
|
||||
P384 = 0x0018,
|
||||
X25519 = 0x001d,
|
||||
}
|
||||
|
||||
impl From<u16> for NamedCurve {
|
||||
fn from(val: u16) -> Self {
|
||||
match val {
|
||||
0x0017 => NamedCurve::P256,
|
||||
0x0018 => NamedCurve::P384,
|
||||
0x001d => NamedCurve::X25519,
|
||||
_ => NamedCurve::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum NamedCurvePrivateKey {
|
||||
EphemeralSecretP256(p256::ecdh::EphemeralSecret),
|
||||
EphemeralSecretP384(p384::ecdh::EphemeralSecret),
|
||||
StaticSecretX25519(x25519_dalek::StaticSecret),
|
||||
}
|
||||
|
||||
pub struct NamedCurveKeypair {
|
||||
pub(crate) curve: NamedCurve,
|
||||
pub(crate) public_key: Vec<u8>,
|
||||
pub(crate) private_key: NamedCurvePrivateKey,
|
||||
}
|
||||
|
||||
fn elliptic_curve_keypair(curve: NamedCurve) -> Result<NamedCurveKeypair> {
|
||||
let (public_key, private_key) = match curve {
|
||||
NamedCurve::P256 => {
|
||||
let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng);
|
||||
let public_key = p256::EncodedPoint::from(secret_key.public_key());
|
||||
(
|
||||
public_key.as_bytes().to_vec(),
|
||||
NamedCurvePrivateKey::EphemeralSecretP256(secret_key),
|
||||
)
|
||||
}
|
||||
NamedCurve::P384 => {
|
||||
let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng);
|
||||
let public_key = p384::EncodedPoint::from(secret_key.public_key());
|
||||
(
|
||||
public_key.as_bytes().to_vec(),
|
||||
NamedCurvePrivateKey::EphemeralSecretP384(secret_key),
|
||||
)
|
||||
}
|
||||
NamedCurve::X25519 => {
|
||||
let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng);
|
||||
let public_key = x25519_dalek::PublicKey::from(&secret_key);
|
||||
(
|
||||
public_key.as_bytes().to_vec(),
|
||||
NamedCurvePrivateKey::StaticSecretX25519(secret_key),
|
||||
)
|
||||
}
|
||||
_ => return Err(Error::ErrInvalidNamedCurve),
|
||||
};
|
||||
|
||||
Ok(NamedCurveKeypair {
|
||||
curve,
|
||||
public_key,
|
||||
private_key,
|
||||
})
|
||||
}
|
||||
|
||||
impl NamedCurve {
|
||||
pub fn generate_keypair(&self) -> Result<NamedCurveKeypair> {
|
||||
match *self {
|
||||
NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519),
|
||||
NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256),
|
||||
NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384),
|
||||
_ => Err(Error::ErrInvalidNamedCurve),
|
||||
}
|
||||
}
|
||||
}
|
||||
223
libs/dtls/src/error.rs
Normal file
223
libs/dtls/src/error.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use thiserror::Error;
|
||||
use tokio::sync::mpsc::error::SendError as MpscSendError;
|
||||
use util::KeyingMaterialExporterError;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("conn is closed")]
|
||||
ErrConnClosed,
|
||||
#[error("read/write timeout")]
|
||||
ErrDeadlineExceeded,
|
||||
#[error("buffer is too small")]
|
||||
ErrBufferTooSmall,
|
||||
#[error("context is not supported for export_keying_material")]
|
||||
ErrContextUnsupported,
|
||||
#[error("packet is too short")]
|
||||
ErrDtlspacketInvalidLength,
|
||||
#[error("handshake is in progress")]
|
||||
ErrHandshakeInProgress,
|
||||
#[error("invalid content type")]
|
||||
ErrInvalidContentType,
|
||||
#[error("invalid mac")]
|
||||
ErrInvalidMac,
|
||||
#[error("packet length and declared length do not match")]
|
||||
ErrInvalidPacketLength,
|
||||
#[error("export_keying_material can not be used with a reserved label")]
|
||||
ErrReservedExportKeyingMaterial,
|
||||
#[error("client sent certificate verify but we have no certificate to verify")]
|
||||
ErrCertificateVerifyNoCertificate,
|
||||
#[error("client+server do not support any shared cipher suites")]
|
||||
ErrCipherSuiteNoIntersection,
|
||||
#[error("server hello can not be created without a cipher suite")]
|
||||
ErrCipherSuiteUnset,
|
||||
#[error("client sent certificate but did not verify it")]
|
||||
ErrClientCertificateNotVerified,
|
||||
#[error("server required client verification, but got none")]
|
||||
ErrClientCertificateRequired,
|
||||
#[error("server responded with SRTP Profile we do not support")]
|
||||
ErrClientNoMatchingSrtpProfile,
|
||||
#[error("client required Extended Master Secret extension, but server does not support it")]
|
||||
ErrClientRequiredButNoServerEms,
|
||||
#[error("server hello can not be created without a compression method")]
|
||||
ErrCompressionMethodUnset,
|
||||
#[error("client+server cookie does not match")]
|
||||
ErrCookieMismatch,
|
||||
#[error("cookie must not be longer then 255 bytes")]
|
||||
ErrCookieTooLong,
|
||||
#[error("PSK Identity Hint provided but PSK is nil")]
|
||||
ErrIdentityNoPsk,
|
||||
#[error("no certificate provided")]
|
||||
ErrInvalidCertificate,
|
||||
#[error("cipher spec invalid")]
|
||||
ErrInvalidCipherSpec,
|
||||
#[error("invalid or unknown cipher suite")]
|
||||
ErrInvalidCipherSuite,
|
||||
#[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")]
|
||||
ErrInvalidClientKeyExchange,
|
||||
#[error("invalid or unknown compression method")]
|
||||
ErrInvalidCompressionMethod,
|
||||
#[error("ECDSA signature contained zero or negative values")]
|
||||
ErrInvalidEcdsasignature,
|
||||
#[error("invalid or unknown elliptic curve type")]
|
||||
ErrInvalidEllipticCurveType,
|
||||
#[error("invalid extension type")]
|
||||
ErrInvalidExtensionType,
|
||||
#[error("invalid hash algorithm")]
|
||||
ErrInvalidHashAlgorithm,
|
||||
#[error("invalid named curve")]
|
||||
ErrInvalidNamedCurve,
|
||||
#[error("invalid private key type")]
|
||||
ErrInvalidPrivateKey,
|
||||
#[error("named curve and private key type does not match")]
|
||||
ErrNamedCurveAndPrivateKeyMismatch,
|
||||
#[error("invalid server name format")]
|
||||
ErrInvalidSniFormat,
|
||||
#[error("invalid signature algorithm")]
|
||||
ErrInvalidSignatureAlgorithm,
|
||||
#[error("expected and actual key signature do not match")]
|
||||
ErrKeySignatureMismatch,
|
||||
#[error("Conn can not be created with a nil nextConn")]
|
||||
ErrNilNextConn,
|
||||
#[error("connection can not be created, no CipherSuites satisfy this Config")]
|
||||
ErrNoAvailableCipherSuites,
|
||||
#[error("connection can not be created, no SignatureScheme satisfy this Config")]
|
||||
ErrNoAvailableSignatureSchemes,
|
||||
#[error("no certificates configured")]
|
||||
ErrNoCertificates,
|
||||
#[error("no config provided")]
|
||||
ErrNoConfigProvided,
|
||||
#[error("client requested zero or more elliptic curves that are not supported by the server")]
|
||||
ErrNoSupportedEllipticCurves,
|
||||
#[error("unsupported protocol version")]
|
||||
ErrUnsupportedProtocolVersion,
|
||||
#[error("Certificate and PSK provided")]
|
||||
ErrPskAndCertificate,
|
||||
#[error("PSK and PSK Identity Hint must both be set for client")]
|
||||
ErrPskAndIdentityMustBeSetForClient,
|
||||
#[error("SRTP support was requested but server did not respond with use_srtp extension")]
|
||||
ErrRequestedButNoSrtpExtension,
|
||||
#[error("Certificate is mandatory for server")]
|
||||
ErrServerMustHaveCertificate,
|
||||
#[error("client requested SRTP but we have no matching profiles")]
|
||||
ErrServerNoMatchingSrtpProfile,
|
||||
#[error(
|
||||
"server requires the Extended Master Secret extension, but the client does not support it"
|
||||
)]
|
||||
ErrServerRequiredButNoClientEms,
|
||||
#[error("expected and actual verify data does not match")]
|
||||
ErrVerifyDataMismatch,
|
||||
#[error("handshake message unset, unable to marshal")]
|
||||
ErrHandshakeMessageUnset,
|
||||
#[error("invalid flight number")]
|
||||
ErrInvalidFlight,
|
||||
#[error("unable to generate key signature, unimplemented")]
|
||||
ErrKeySignatureGenerateUnimplemented,
|
||||
#[error("unable to verify key signature, unimplemented")]
|
||||
ErrKeySignatureVerifyUnimplemented,
|
||||
#[error("data length and declared length do not match")]
|
||||
ErrLengthMismatch,
|
||||
#[error("buffer not long enough to contain nonce")]
|
||||
ErrNotEnoughRoomForNonce,
|
||||
#[error("feature has not been implemented yet")]
|
||||
ErrNotImplemented,
|
||||
#[error("sequence number overflow")]
|
||||
ErrSequenceNumberOverflow,
|
||||
#[error("unable to marshal fragmented handshakes")]
|
||||
ErrUnableToMarshalFragmented,
|
||||
#[error("invalid state machine transition")]
|
||||
ErrInvalidFsmTransition,
|
||||
#[error("ApplicationData with epoch of 0")]
|
||||
ErrApplicationDataEpochZero,
|
||||
#[error("unhandled contentType")]
|
||||
ErrUnhandledContextType,
|
||||
#[error("context canceled")]
|
||||
ErrContextCanceled,
|
||||
#[error("empty fragment")]
|
||||
ErrEmptyFragment,
|
||||
#[error("Alert is Fatal or Close Notify")]
|
||||
ErrAlertFatalOrClose,
|
||||
|
||||
#[error(
|
||||
"Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}"
|
||||
)]
|
||||
ErrFragmentBufferOverflow { new_size: usize, max_size: usize },
|
||||
|
||||
#[error("{0}")]
|
||||
Io(#[source] IoError),
|
||||
#[error("{0}")]
|
||||
Util(#[from] util::Error),
|
||||
#[error("utf8: {0}")]
|
||||
Utf8(#[from] FromUtf8Error),
|
||||
#[error("{0}")]
|
||||
Sec1(#[source] sec1::Error),
|
||||
#[error("{0}")]
|
||||
Aes(#[from] aes::cipher::InvalidLength),
|
||||
#[error("{0}")]
|
||||
P256(#[source] P256Error),
|
||||
#[error("{0}")]
|
||||
RcGen(#[from] rcgen::Error),
|
||||
#[error("mpsc send: {0}")]
|
||||
MpscSend(String),
|
||||
#[error("keying material: {0}")]
|
||||
KeyingMaterial(#[from] KeyingMaterialExporterError),
|
||||
|
||||
/// Error parsing a given PEM string.
|
||||
#[error("invalid PEM: {0}")]
|
||||
InvalidPEM(String),
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("io error: {0}")]
|
||||
pub struct IoError(#[from] pub io::Error);
|
||||
|
||||
// Workaround for wanting PartialEq for io::Error.
|
||||
impl PartialEq for IoError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.kind() == other.0.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Error::Io(IoError(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sec1::Error> for Error {
|
||||
fn from(e: sec1::Error) -> Self {
|
||||
Error::Sec1(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0}")]
|
||||
pub struct P256Error(#[source] p256::elliptic_curve::Error);
|
||||
|
||||
impl PartialEq for P256Error {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl From<p256::elliptic_curve::Error> for Error {
|
||||
fn from(e: p256::elliptic_curve::Error) -> Self {
|
||||
Error::P256(P256Error(e))
|
||||
}
|
||||
}
|
||||
|
||||
// Because Tokio SendError is parameterized, we sadly lose the backtrace.
|
||||
impl<T> From<MpscSendError<T>> for Error {
|
||||
fn from(e: MpscSendError<T>) -> Self {
|
||||
Error::MpscSend(e.to_string())
|
||||
}
|
||||
}
|
||||
56
libs/dtls/src/extension/extension_server_name.rs
Normal file
56
libs/dtls/src/extension/extension_server_name.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
#[cfg(test)]
|
||||
mod extension_server_name_test;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use super::*;
|
||||
|
||||
const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ExtensionServerName {
|
||||
pub(crate) server_name: String,
|
||||
}
|
||||
|
||||
impl ExtensionServerName {
|
||||
pub fn extension_value(&self) -> ExtensionValue {
|
||||
ExtensionValue::ServerName
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
//TODO: check how to do cryptobyte?
|
||||
2 + 2 + 1 + 2 + self.server_name.len()
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
//TODO: check how to do cryptobyte?
|
||||
writer.write_u16::<BigEndian>(2 + 1 + 2 + self.server_name.len() as u16)?;
|
||||
writer.write_u16::<BigEndian>(1 + 2 + self.server_name.len() as u16)?;
|
||||
writer.write_u8(EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME)?;
|
||||
writer.write_u16::<BigEndian>(self.server_name.len() as u16)?;
|
||||
writer.write_all(self.server_name.as_bytes())?;
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
//TODO: check how to do cryptobyte?
|
||||
let _ = reader.read_u16::<BigEndian>()? as usize;
|
||||
let _ = reader.read_u16::<BigEndian>()? as usize;
|
||||
|
||||
let name_type = reader.read_u8()?;
|
||||
if name_type != EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME {
|
||||
return Err(Error::ErrInvalidSniFormat);
|
||||
}
|
||||
|
||||
let buf_len = reader.read_u16::<BigEndian>()? as usize;
|
||||
let mut buf: Vec<u8> = vec![0u8; buf_len];
|
||||
reader.read_exact(&mut buf)?;
|
||||
|
||||
let server_name = String::from_utf8(buf)?;
|
||||
|
||||
Ok(ExtensionServerName { server_name })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use std::io::{BufReader, BufWriter};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extension_server_name() -> Result<()> {
|
||||
let extension = ExtensionServerName {
|
||||
server_name: "test.domain".to_owned(),
|
||||
};
|
||||
|
||||
let mut raw = vec![];
|
||||
{
|
||||
let mut writer = BufWriter::<&mut Vec<u8>>::new(raw.as_mut());
|
||||
extension.marshal(&mut writer)?;
|
||||
}
|
||||
|
||||
let mut reader = BufReader::new(raw.as_slice());
|
||||
let new_extension = ExtensionServerName::unmarshal(&mut reader)?;
|
||||
|
||||
assert_eq!(
|
||||
new_extension, extension,
|
||||
"extensionServerName marshal: got {new_extension:?} expected {extension:?}",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#[cfg(test)]
|
||||
mod extension_supported_elliptic_curves_test;
|
||||
|
||||
use super::*;
|
||||
use crate::curve::named_curve::*;
|
||||
|
||||
const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6;
|
||||
|
||||
/// ## Specifications
|
||||
///
|
||||
/// * [RFC 8422 §5.1.1]
|
||||
///
|
||||
/// [RFC 8422 §5.1.1]: https://tools.ietf.org/html/rfc8422#section-5.1.1
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ExtensionSupportedEllipticCurves {
|
||||
pub elliptic_curves: Vec<NamedCurve>,
|
||||
}
|
||||
|
||||
impl ExtensionSupportedEllipticCurves {
|
||||
pub fn extension_value(&self) -> ExtensionValue {
|
||||
ExtensionValue::SupportedEllipticCurves
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
2 + 2 + self.elliptic_curves.len() * 2
|
||||
}
|
||||
|
||||
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
writer.write_u16::<BigEndian>(2 + 2 * self.elliptic_curves.len() as u16)?;
|
||||
writer.write_u16::<BigEndian>(2 * self.elliptic_curves.len() as u16)?;
|
||||
for v in &self.elliptic_curves {
|
||||
writer.write_u16::<BigEndian>(*v as u16)?;
|
||||
}
|
||||
|
||||
Ok(writer.flush()?)
|
||||
}
|
||||
|
||||
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||
let _ = reader.read_u16::<BigEndian>()?;
|
||||
|
||||
let group_count = reader.read_u16::<BigEndian>()? as usize / 2;
|
||||
let mut elliptic_curves = vec![];
|
||||
for _ in 0..group_count {
|
||||
let elliptic_curve = reader.read_u16::<BigEndian>()?.into();
|
||||
elliptic_curves.push(elliptic_curve);
|
||||
}
|
||||
|
||||
Ok(ExtensionSupportedEllipticCurves { elliptic_curves })
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user