diff --git a/.cargo/config.toml b/.cargo/config.toml
index a92f80e8c..17288931a 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -5,10 +5,24 @@ rustflags = ["-C", "linker-flavor=ld.lld"]
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
+[target.aarch64-unknown-linux-ohos]
+ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
+linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
+
+[target.aarch64-unknown-linux-ohos.env]
+PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
+PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
+PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
+SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
+
[target.aarch64-unknown-linux-musl]
linker = "aarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
+[target.riscv64gc-unknown-linux-musl]
+linker = "riscv64-unknown-linux-musl-gcc"
+rustflags = ["-C", "target-feature=+crt-static"]
+
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
@@ -58,6 +72,10 @@ rustflags = ["-C", "target-feature=+crt-static"]
linker = "armv7-unknown-linux-musleabi-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
+[target.loongarch64-unknown-linux-musl]
+linker = "loongarch64-unknown-linux-musl-gcc"
+rustflags = ["-C", "target-feature=+crt-static"]
+
[target.arm-unknown-linux-musleabihf]
linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [
diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000..3550a30f2
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 819a5c7b8..c549d9bf0 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -23,31 +23,113 @@ body:
- type: textarea
id: description
attributes:
- label: 描述问题 / Describe the bug
- description: 对 bug 的明确描述。如果条件允许,请包括屏幕截图。 / A clear description of what the bug is. Include screenshots if applicable.
- placeholder: 问题描述 / Bug description
+ label: 问题简要描述 / Brief Description
+ description: 对问题的简要描述,包括期望的行为和实际发生的情况。 / A brief description of the issue, including expected vs actual behavior.
+ placeholder: |
+ 例如:节点 A 无法连接到节点 B,期望能够正常建立连接
+ Example: Node A cannot connect to Node B, expected to establish connection normally
+ validations:
+ required: true
+
+ - type: textarea
+ id: environment-info
+ attributes:
+ label: 环境信息 / Environment Information
+ description: 请提供网络拓扑、节点信息和系统环境详情。 / Please provide network topology, node information and system environment details.
+ placeholder: |
+ **EasyTier 版本(非常重要)/ EasyTier Version (Very Important):** v1.2.0
+
+ **网络拓扑 / Network Topology:**
+ - 节点 A (10.1.1.1): Windows 11 Pro 22H2, Wifi,有 IPV6 地址
+ - 节点 B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), 公网 IP
+ - 节点 C (10.1.1.3): macOS Ventura 13.4.1, 5G 流量,无 IPV6 地址
+
+ **Network Topology:**
+ - Node A (10.1.1.1): Windows 11 Pro 22H2, Wifi, has IPV6 address
+ - Node B (10.1.1.2): Ubuntu 22.04.3 LTS (Linux 5.15.0-72-generic), public IP
+ - Node C (10.1.1.3): macOS Ventura 13.4.1, 5G traffic, no IPV6 address
+ validations:
+ required: true
+
+ - type: textarea
+ id: node-configs
+ attributes:
+ label: 节点配置 / Node Configurations
+ description: 请提供每个节点的配置文件或启动参数。 / Please provide configuration files or startup parameters for each node.
+ placeholder: |
+ **节点 A 配置 / Node A Config:**
+ ```
+ easytier-core --config-file config.toml
+ ```
+
+ **节点 B 配置 / Node B Config:**
+ ```
+ easytier-core --ipv4 10.1.1.2 --peers tcp://1.2.3.4:11010
+ ```
+
+ 请贴出完整的配置文件内容或命令行参数
+ Please paste complete configuration file contents or command line arguments
+ validations:
+ required: true
+
+ - type: textarea
+ id: logs
+ attributes:
+ label: 日志信息 / Log Information
+ description: 请提供相关的日志信息,包括 GUI 的事件日志或命令行的控制台输出。 / Please provide relevant log information, including GUI event logs or command line console output.
+ placeholder: |
+ 请粘贴相关的日志信息:
+ - GUI 用户:请提供事件日志中的错误信息
+ - 命令行用户:请提供控制台输出的详细日志
+ - 一般情况下,提供默认输出的事件日志即可
+ - 如果能提供 --file-log-level debug 输出的日志,会更方便 debug
+
+ Please paste relevant log information:
+ - GUI users: Please provide error messages from event logs
+ - CLI users: Please provide detailed console output logs
+ - Default log output is usually sufficient
+ - If possible, logs with --file-log-level debug would be more helpful for debugging
+
validations:
required: true
- type: textarea
id: reproduction
attributes:
- label: 重现步骤 / Reproduction
- description: 能够重现行为的步骤或指向能够复现的存储库链接。 / A link to a reproduction repo or steps to reproduce the behaviour.
+ label: 重现步骤 / Reproduction Steps
+ description: 请提供详细的步骤来重现这个问题。 / Please provide detailed steps to reproduce this issue.
placeholder: |
- 请提供一个最小化的复现示例或复现步骤,请参考这个指南 https://stackoverflow.com/help/minimal-reproducible-example
- Please provide a minimal reproduction or steps to reproduce, see this guide https://stackoverflow.com/help/minimal-reproducible-example
- 为什么需要重现(问题)?请参阅这篇文章 https://antfu.me/posts/why-reproductions-are-required
- Why reproduction is required? see this article https://antfu.me/posts/why-reproductions-are-required
+ 1. 启动节点 A,使用配置 xxx / Start Node A with config xxx
+ 2. 启动节点 B,使用配置 yyy / Start Node B with config yyy
+ 3. 尝试从节点 A ping 节点 B / Try to ping Node B from Node A
+ 4. 观察到错误:xxx / Observe error: xxx
+
+ 请提供详细的操作步骤,以便我们能够重现问题
+ Please provide detailed steps so we can reproduce the issue
+ validations:
+ required: true
- type: textarea
id: expected-behavior
attributes:
- label: 预期结果 / Expected behavior
+ label: 预期结果 / Expected Behavior
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
+ placeholder: |
+ 例如:节点 A 应该能够成功 ping 通节点 B,延迟在 100ms 以内
+ Example: Node A should be able to ping Node B successfully with latency under 100ms
- type: textarea
- id: context
+ id: additional-context
attributes:
- label: 额外上下文 / Additional context
- description: 在这里添加关于问题的任何其他上下文。 / Add any other context about the problem here.
\ No newline at end of file
+ label: 额外信息 / Additional Context
+ description: 在这里添加关于问题的任何其他上下文信息。 / Add any other context about the problem here.
+ placeholder: |
+ 例如:
+ - 这个问题是否在特定时间出现?
+ - 是否有网络环境的特殊配置?
+ - 是否尝试过其他解决方案?
+
+ Example:
+ - Does this issue occur at specific times?
+ - Are there any special network environment configurations?
+ - Have you tried any other solutions?
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 64d60e92b..7c3661a4f 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -3,36 +3,177 @@
name: 💡 新功能请求 / Feature Request
title: '[feat] '
-description: 提出一个想法 / Suggest an idea
+description: 提出一个想法 / Suggest an idea
labels: ['type: feature request']
body:
+ - type: markdown
+ attributes:
+ value: |
+ ## 提交功能请求前请注意 / Before Submitting
+ 1. 请先搜索 [现有的功能请求](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) 确保您的想法尚未被提出。
+ 1. Please search [existing feature requests](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue+label%3A%22type%3A+feature+request%22) to ensure your idea hasn't been suggested already.
+ 2. 请确保这个功能确实适合 EasyTier 项目的目标和范围。
+ 2. Please ensure this feature fits within EasyTier's goals and scope.
+ 3. 考虑这个功能是否能让更多用户受益,而不只是解决个人需求。
+ 3. Consider whether this feature would benefit many users, not just personal needs.
+
+ - type: dropdown
+ id: feature-category
+ attributes:
+ label: 功能类别 / Feature Category
+ description: 请选择这个功能请求属于哪个类别 / Please select which category this feature request belongs to
+ options:
+ - 网络连接 / Network Connectivity
+ - 安全和加密 / Security & Encryption
+ - 性能优化 / Performance Optimization
+ - 用户界面 / User Interface
+ - 配置管理 / Configuration Management
+ - 监控和日志 / Monitoring & Logging
+ - 平台支持 / Platform Support
+ - API 和集成 / API & Integration
+ - 其他 / Other
+ validations:
+ required: true
+
+ - type: textarea
+ id: use-case
+ attributes:
+ label: 使用场景 / Use Case
+ description: 描述您希望这个功能解决的具体使用场景或问题 / Describe the specific use case or problem you want this feature to solve
+ placeholder: |
+ 例如:
+ - 作为企业用户,我需要在多个分支机构之间建立安全的网络连接
+ - 作为开发者,我希望能够通过 API 监控网络状态
+ - 作为系统管理员,我需要更详细的连接日志来排查问题
+
+ Example:
+ - As an enterprise user, I need to establish secure network connections between multiple branch offices
+ - As a developer, I want to monitor network status through APIs
+ - As a system administrator, I need more detailed connection logs for troubleshooting
+ validations:
+ required: true
+
- type: textarea
- id: problem
+ id: current-limitations
attributes:
- label: 描述问题 / Describe the problem
- description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
- placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
+ label: 当前限制 / Current Limitations
+ description: 描述当前 EasyTier 的哪些限制阻止了您实现这个使用场景 / Describe what current limitations in EasyTier prevent you from achieving this use case
+ placeholder: |
+ 例如:
+ - 目前不支持基于用户角色的访问控制
+ - 缺少对 IPv6 的完整支持
+ - 没有提供 REST API 来获取网络状态
+
+ Example:
+ - Currently lacks role-based access control
+ - Missing complete IPv6 support
+ - No REST API available for network status
validations:
required: true
- type: textarea
- id: solution
+ id: proposed-solution
attributes:
- label: "描述您想要的解决方案 / Describe the solution you'd like"
- description: 明确说明您希望做出的改变 / A clear description of what change you would like
- placeholder: '我希望... / I would like to...'
+ label: 建议的解决方案 / Proposed Solution
+ description: 详细描述您希望添加的功能以及它应该如何工作 / Describe in detail the feature you'd like to add and how it should work
+ placeholder: |
+ 请描述:
+ - 功能的具体实现方式
+ - 用户界面或 API 设计
+ - 配置选项和参数
+ - 与现有功能的集成方式
+
+ Please describe:
+ - Specific implementation approach
+ - User interface or API design
+ - Configuration options and parameters
+ - Integration with existing features
validations:
required: true
+ - type: textarea
+ id: benefits
+ attributes:
+ label: 预期收益 / Expected Benefits
+ description: 说明这个功能会带来什么好处,会影响哪些用户群体 / Explain what benefits this feature would bring and which user groups it would affect
+ placeholder: |
+ 例如:
+ - 提高网络连接的稳定性和性能
+ - 简化大规模部署的管理复杂度
+ - 增强企业用户的安全性需求
+ - 降低新用户的学习成本
+
+ Example:
+ - Improve network connection stability and performance
+ - Simplify management complexity for large-scale deployments
+ - Enhance security requirements for enterprise users
+ - Reduce learning curve for new users
+
+ - type: textarea
+ id: technical-considerations
+ attributes:
+ label: 技术考虑 / Technical Considerations
+ description: 如果您了解技术细节,请分享相关的技术考虑或约束 / If you have technical knowledge, please share relevant technical considerations or constraints
+ placeholder: |
+ 例如:
+ - 可能需要修改网络协议栈
+ - 需要考虑跨平台兼容性
+ - 可能影响现有性能
+ - 依赖第三方库或协议
+
+ Example:
+ - May require modifications to network protocol stack
+ - Cross-platform compatibility needs consideration
+ - Potential impact on existing performance
+ - Dependencies on third-party libraries or protocols
+
- type: textarea
id: alternatives
attributes:
- label: 替代方案 / Alternatives considered
- description: "您考虑过的任何替代解决方案 / Any alternative solutions you've considered"
+ label: 备选方案 / Alternative Solutions
+ description: 您是否考虑过其他解决方案?是否有现有的替代方案? / Have you considered other solutions? Are there existing alternatives?
+ placeholder: |
+ 例如:
+ - 使用第三方工具 X 可以部分解决,但缺少 Y 功能
+ - 通过脚本workaround可以实现,但不够优雅
+ - 其他类似项目 Z 有这个功能,可以参考其实现
+
+ Example:
+ - Third-party tool X can partially solve this, but lacks Y functionality
+ - Can be achieved through script workarounds, but not elegant
+ - Similar project Z has this feature, could reference its implementation
+
+ - type: textarea
+ id: implementation-priority
+ attributes:
+ label: 实现优先级 / Implementation Priority
+ description: 这个功能对您有多重要?是否有时间要求? / How important is this feature to you? Any time requirements?
+ placeholder: |
+ 例如:
+ - 高优先级:阻碍了我们的生产部署
+ - 中优先级:会显著改善用户体验
+ - 低优先级:锦上添花的功能
+
+ Example:
+ - High priority: Blocking our production deployment
+ - Medium priority: Would significantly improve user experience
+ - Low priority: Nice-to-have feature
- type: textarea
- id: context
+ id: additional-context
attributes:
- label: 额外上下文 / Additional context
- description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
\ No newline at end of file
+ label: 补充信息 / Additional Context
+ description: 添加任何其他相关信息,如截图、链接、参考资料等 / Add any other relevant information such as screenshots, links, or references
+ placeholder: |
+ 例如:
+ - 相关的 RFC 或技术规范
+ - 其他项目的实现示例
+ - 用户调研或反馈数据
+ - 设计草图或流程图
+
+ Example:
+ - Relevant RFCs or technical specifications
+ - Implementation examples from other projects
+ - User research or feedback data
+ - Design sketches or flowcharts
\ No newline at end of file
diff --git a/.github/origin_wfs/Dockerfile b/.github/origin_wfs/Dockerfile
index 61fc84e9d..780d8fa5c 100644
--- a/.github/origin_wfs/Dockerfile
+++ b/.github/origin_wfs/Dockerfile
@@ -1,29 +1,35 @@
-FROM alpine:latest AS builder
+FROM alpine:latest AS base
+FROM base AS builder
ARG TARGETPLATFORM
COPY . /tmp/artifacts
-RUN mkdir -p /tmp/output; \
- cd /tmp/artifacts; \
- ARTIFACT_ARCH=""; \
+WORKDIR /tmp/output
+RUN ARTIFACT_ARCH=""; \
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
ARTIFACT_ARCH="x86_64"; \
+ elif [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then \
+ ARTIFACT_ARCH="armhf"; \
+ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
+ ARTIFACT_ARCH="armv7hf"; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
ARTIFACT_ARCH="aarch64"; \
+ elif [ "$TARGETPLATFORM" = "linux/riscv64" ]; then \
+ ARTIFACT_ARCH="riscv64"; \
else \
- echo "Unsupported architecture: $TARGETARCH"; \
+ echo "Unsupported architecture: $TARGETPLATFORM"; \
exit 1; \
fi; \
cp /tmp/artifacts/easytier-linux-${ARTIFACT_ARCH}/* /tmp/output;
-FROM alpine:latest
+FROM base
RUN apk add --no-cache tzdata tini
WORKDIR /app
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
# users can use "-e TZ=xxx" to adjust it
-ENV TZ Asia/Shanghai
+ENV TZ=Asia/Shanghai
# tcp
EXPOSE 11010/tcp
diff --git a/.github/origin_wfs/core.yml b/.github/origin_wfs/core.yml
index a10c5bb5c..7cb8de18c 100644
--- a/.github/origin_wfs/core.yml
+++ b/.github/origin_wfs/core.yml
@@ -40,12 +40,12 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Install pnpm
- uses: pnpm/action-setup@v3
+ uses: pnpm/action-setup@v4
with:
- version: 9
+ version: 10
run_install: false
- name: Get pnpm store directory
@@ -83,6 +83,9 @@ jobs:
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-x86_64
+ - TARGET: riscv64gc-unknown-linux-musl
+ OS: ubuntu-22.04
+ ARTIFACT_NAME: linux-riscv64
- TARGET: mips-unknown-linux-musl
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips
@@ -102,6 +105,10 @@ jobs:
OS: ubuntu-22.04
ARTIFACT_NAME: linux-arm
+ - TARGET: loongarch64-unknown-linux-musl
+ OS: ubuntu-24.04
+ ARTIFACT_NAME: linux-loongarch64
+
- TARGET: x86_64-apple-darwin
OS: macos-latest
ARTIFACT_NAME: macos-x86_64
@@ -157,7 +164,7 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup protoc
- uses: arduino/setup-protoc@v2
+ uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -167,6 +174,11 @@ jobs:
run: |
bash ./.github/workflows/install_rust.sh
+ # loongarch need llvm-18
+ if [[ $TARGET =~ ^loongarch.*$ ]]; then
+ sudo apt-get install -qq llvm-18 clang-18
+ export LLVM_CONFIG_PATH=/usr/lib/llvm-18/bin/llvm-config
+ fi
# we set the sysroot when sysroot is a dir
# this dir is a soft link generated by install_rust.sh
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
@@ -175,14 +187,19 @@ jobs:
fi
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
- cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier
+ cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
else
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
+ CORE_FEATURES="--features=mimalloc"
+ elif [[ $TARGET =~ ^riscv64.*$ ]]; then
+ CORE_FEATURES="--features=mimalloc"
+ else
+ CORE_FEATURES="--features=jemalloc"
fi
cargo build --release --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
- cargo build --release --target $TARGET
+ cargo build --release --target $TARGET $CORE_FEATURES
fi
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
@@ -212,8 +229,8 @@ jobs:
rustup set auto-self-update disable
- rustup install 1.86
- rustup default 1.86
+ rustup install 1.89
+ rustup default 1.89
export CC=clang
export CXX=clang++
@@ -221,7 +238,7 @@ jobs:
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
- cargo build --release --verbose --target $TARGET
+ cargo build --release --verbose --target $TARGET --features=mimalloc
- name: Compress
run: |
@@ -243,7 +260,7 @@ jobs:
TAG=$GITHUB_SHA
fi
- if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
+ if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ && ! $TARGET =~ ^riscv64.*$ ]]; then
UPX_VERSION=4.2.4
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
cp upx-${UPX_VERSION}-amd64_linux/upx .
diff --git a/.github/origin_wfs/docker.yml b/.github/origin_wfs/docker.yml
index 124b11d50..74000d01c 100644
--- a/.github/origin_wfs/docker.yml
+++ b/.github/origin_wfs/docker.yml
@@ -11,13 +11,18 @@ on:
image_tag:
description: 'Tag for this image build'
type: string
- default: 'v2.3.2'
+ default: 'v2.4.2'
required: true
mark_latest:
description: 'Mark this image as latest'
type: boolean
default: false
required: true
+ mark_unstable:
+ description: 'Mark this image as unstable'
+ type: boolean
+ default: false
+ required: true
jobs:
docker:
@@ -27,6 +32,13 @@ jobs:
-
name: Checkout
uses: actions/checkout@v4
+ -
+ name: Validate inputs
+ run: |
+ if [[ "${{ inputs.mark_latest }}" == "true" && "${{ inputs.mark_unstable }}" == "true" ]]; then
+ echo "Error: mark_latest and mark_unstable cannot both be true"
+ exit 1
+ fi
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -47,7 +59,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download artifact
id: download-artifact
- uses: dawidd6/action-download-artifact@v6
+ uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.run_id }}
@@ -56,14 +68,36 @@ jobs:
- name: List files
run: |
ls -l -R .
+ - name: Prepare Docker tags
+ id: tags
+ run: |
+ # Base tags with version
+ DOCKERHUB_TAGS="easytier/easytier:${{ inputs.image_tag }}"
+ GHCR_TAGS="ghcr.io/easytier/easytier:${{ inputs.image_tag }}"
+
+ # Add latest tags if requested
+ if [[ "${{ inputs.mark_latest }}" == "true" ]]; then
+ DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:latest"
+ GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:latest"
+ fi
+
+ # Add unstable tags if requested
+ if [[ "${{ inputs.mark_unstable }}" == "true" ]]; then
+ DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:unstable"
+ GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:unstable"
+ fi
+
+ # Combine all tags
+ ALL_TAGS="${DOCKERHUB_TAGS},${GHCR_TAGS}"
+
+ echo "tags=${ALL_TAGS}" >> $GITHUB_OUTPUT
+ echo "Generated tags: ${ALL_TAGS}"
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: ./docker_context
- platforms: linux/amd64,linux/arm64
+ platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
push: true
file: .github/workflows/Dockerfile
- tags: |
- easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
- ghcr.io/easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
+ tags: ${{ steps.tags.outputs.tags }}
diff --git a/.github/origin_wfs/gui.yml b/.github/origin_wfs/gui.yml
index 0045a8a03..146406c6e 100644
--- a/.github/origin_wfs/gui.yml
+++ b/.github/origin_wfs/gui.yml
@@ -29,7 +29,7 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
- paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
+ paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh"]'
build-gui:
strategy:
fail-fast: false
@@ -78,20 +78,11 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
+ - uses: actions/checkout@v3
+
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
- run: |
- sudo apt update
- sudo apt install -qq libwebkit2gtk-4.1-dev \
- build-essential \
- curl \
- wget \
- file \
- libgtk-3-dev \
- librsvg2-dev \
- libxdo-dev \
- libssl-dev \
- patchelf
+ run: bash ./.github/workflows/install_gui_dep.sh
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
@@ -128,20 +119,18 @@ jobs:
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- - uses: actions/checkout@v3
-
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Install pnpm
- uses: pnpm/action-setup@v3
+ uses: pnpm/action-setup@v4
with:
- version: 9
+ version: 10
run_install: false
- name: Get pnpm store directory
@@ -174,7 +163,7 @@ jobs:
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
- uses: arduino/setup-protoc@v2
+ uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/origin_wfs/install_rust.sh b/.github/origin_wfs/install_rust.sh
index 28de5f12e..0c7176cba 100644
--- a/.github/origin_wfs/install_rust.sh
+++ b/.github/origin_wfs/install_rust.sh
@@ -15,6 +15,8 @@ if [[ $OS =~ ^ubuntu.*$ ]]; then
# if target is mips or mipsel, we should use soft-float version of musl
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
MUSL_TARGET=${TARGET}sf
+ elif [[ $TARGET =~ ^riscv64gc-.*$ ]]; then
+ MUSL_TARGET=${TARGET/#riscv64gc-/riscv64-}
fi
if [[ $MUSL_TARGET =~ musl ]]; then
mkdir -p ./musl_gcc
@@ -29,8 +31,8 @@ fi
# see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable
-rustup install 1.86
-rustup default 1.86
+rustup install 1.89
+rustup default 1.89
# mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
diff --git a/.github/origin_wfs/mobile.yml b/.github/origin_wfs/mobile.yml
index 6d5000d57..b122c411f 100644
--- a/.github/origin_wfs/mobile.yml
+++ b/.github/origin_wfs/mobile.yml
@@ -56,7 +56,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'oracle'
- java-version: '20'
+ java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
@@ -72,12 +72,12 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Install pnpm
- uses: pnpm/action-setup@v3
+ uses: pnpm/action-setup@v4
with:
- version: 9
+ version: 10
run_install: false
- name: Get pnpm store directory
@@ -115,7 +115,7 @@ jobs:
rustup target add x86_64-linux-android
- name: Setup protoc
- uses: arduino/setup-protoc@v2
+ uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/origin_wfs/ohos.yml b/.github/origin_wfs/ohos.yml
new file mode 100644
index 000000000..87f22b1b6
--- /dev/null
+++ b/.github/origin_wfs/ohos.yml
@@ -0,0 +1,114 @@
+name: EasyTier OHOS
+
+on:
+ push:
+ branches: ["develop", "main", "releases/**"]
+ pull_request:
+ branches: ["develop", "main"]
+
+env:
+ CARGO_TERM_COLOR: always
+
+defaults:
+ run:
+ # necessary for windows
+ shell: bash
+
+jobs:
+ pre_job:
+ # continue-on-error: true # Uncomment once integration is finished
+ runs-on: ubuntu-latest
+ # Map a step output to a job output
+ outputs:
+ # do not skip push on branch starts with releases/
+ should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
+ steps:
+ - id: skip_check
+ uses: fkirc/skip-duplicate-actions@v5
+ with:
+ # All of these options are optional, so you can remove them if you are happy with the defaults
+ concurrent_skipping: 'same_content_newer'
+ skip_after_successful_duplicate: 'true'
+ cancel_others: 'true'
+ paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
+ build-ohos:
+ runs-on: ubuntu-latest
+ needs: pre_job
+ if: needs.pre_job.outputs.should_skip != 'true'
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ wget \
+ unzip \
+ git \
+ pkg-config
+ sudo apt-get clean
+
+ - name: Download and extract native SDK
+ working-directory: ../../../
+ run: |
+ echo $PWD
+ wget -q \
+ https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.aa
+ wget -q \
+ https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.ab
+ cat ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab > sdk.tar.gz
+ echo "Extracting native..."
+ mkdir sdk
+ tar -xzf sdk.tar.gz ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip
+ tar -xzf sdk.tar.gz ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip
+ unzip -qq ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip -d sdk
+ unzip -qq ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip -d sdk
+ ls -la sdk/native/llvm/bin/
+ rm -rf ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab ohos-sdk/
+
+ - name: Download and Extract Custom SDK
+ run: |
+ wget https://github.com/FrankHan052176/Easytier-OHOS-sdk/releases/download/v1/ohos-sdk.zip -O /tmp/ohos-sdk.zip
+ sudo unzip -o /tmp/ohos-sdk.zip -d /tmp/custom-sdk
+ sudo cp -rf /tmp/custom-sdk/linux/native/* $HOME/sdk/native
+ echo "Custom SDK files deployed to $HOME/sdk/native"
+ ls -a $HOME/sdk/native
+
+ - name: Setup build environment
+ run: |
+ echo "OHOS_NDK_HOME=$HOME/sdk" >> $GITHUB_ENV
+ echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
+
+ - name: Create clang wrapper script
+ run: |
+ sudo mkdir -p $OHOS_NDK_HOME/native/llvm
+ sudo tee $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh > /dev/null <<'EOF'
+ #!/bin/sh
+ exec $OHOS_NDK_HOME/native/llvm/bin/clang \
+ -target aarch64-linux-ohos \
+ --sysroot=$OHOS_NDK_HOME/native/sysroot \
+ -D__MUSL__ \
+ "$@"
+ EOF
+ sudo chmod +x $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh
+
+ - name: Build
+ working-directory: ./easytier-contrib/easytier-ohrs
+ run: |
+ sudo apt-get install -y llvm clang lldb lld
+ sudo apt-get install -y protobuf-compiler
+ bash ../../.github/workflows/install_rust.sh
+ source env.sh
+ cargo install ohrs
+ rustup target add aarch64-unknown-linux-ohos
+ cargo update easytier
+ ohrs doctor
+ ohrs build --release --arch aarch
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: easytier-ohos
+ path: ./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
+ retention-days: 5
+ if-no-files-found: error
diff --git a/.github/origin_wfs/release.yml b/.github/origin_wfs/release.yml
index 09791d71d..35643179a 100644
--- a/.github/origin_wfs/release.yml
+++ b/.github/origin_wfs/release.yml
@@ -21,7 +21,7 @@ on:
version:
description: 'Version for this release'
type: string
- default: 'v2.3.2'
+ default: 'v2.4.2'
required: true
make_latest:
description: 'Mark this release as latest'
@@ -42,7 +42,7 @@ jobs:
uses: actions/checkout@v4
- name: Download Core Artifact
- uses: dawidd6/action-download-artifact@v6
+ uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.core_run_id }}
@@ -50,7 +50,7 @@ jobs:
path: release_assets
- name: Download GUI Artifact
- uses: dawidd6/action-download-artifact@v6
+ uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.gui_run_id }}
@@ -58,7 +58,7 @@ jobs:
path: release_assets_nozip
- name: Download Mobile Artifact
- uses: dawidd6/action-download-artifact@v6
+ uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.mobile_run_id }}
diff --git a/.github/origin_wfs/test.yml b/.github/origin_wfs/test.yml
index e93573150..8ff4da265 100644
--- a/.github/origin_wfs/test.yml
+++ b/.github/origin_wfs/test.yml
@@ -28,7 +28,7 @@ jobs:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
- paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
+ paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
test:
runs-on: ubuntu-22.04
needs: pre_job
@@ -37,7 +37,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup protoc
- uses: arduino/setup-protoc@v2
+ uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -55,12 +55,12 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 21
+ node-version: 22
- name: Install pnpm
- uses: pnpm/action-setup@v3
+ uses: pnpm/action-setup@v4
with:
- version: 9
+ version: 10
run_install: false
- name: Get pnpm store directory
@@ -89,6 +89,24 @@ jobs:
./target
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
+ - name: Install GUI dependencies (Used by clippy)
+ run: |
+ bash ./.github/workflows/install_gui_dep.sh
+ bash ./.github/workflows/install_rust.sh
+ rustup component add rustfmt
+ rustup component add clippy
+
+ - name: Check formatting
+ if: ${{ !cancelled() }}
+ run: cargo fmt --all -- --check
+
+ - name: Check Clippy
+ if: ${{ !cancelled() }}
+ # NOTE: tauri need `dist` dir in build.rs
+ run: |
+ mkdir -p easytier-gui/dist
+ cargo clippy --all-targets --all-features --all -- -D warnings
+
- name: Run tests
run: |
sudo prlimit --pid $$ --nofile=1048576:1048576
diff --git a/.github/workflows/build_libs.yml b/.github/workflows/build_libs.yml
index 411328e39..ecfcd5a72 100644
--- a/.github/workflows/build_libs.yml
+++ b/.github/workflows/build_libs.yml
@@ -72,4 +72,4 @@ jobs:
release_binaries/easytier-linux-x86_64.so
release_binaries/easytier-linux-arm64.so
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/install_gui_dep.sh b/.github/workflows/install_gui_dep.sh
new file mode 100644
index 000000000..aa61b1988
--- /dev/null
+++ b/.github/workflows/install_gui_dep.sh
@@ -0,0 +1,11 @@
+sudo apt update
+sudo apt install -qq libwebkit2gtk-4.1-dev \
+ build-essential \
+ curl \
+ wget \
+ file \
+ libgtk-3-dev \
+ librsvg2-dev \
+ libxdo-dev \
+ libssl-dev \
+ patchelf
\ No newline at end of file
diff --git a/.github/workflows/ohos.yml b/.github/workflows/ohos.yml
new file mode 100644
index 000000000..87f22b1b6
--- /dev/null
+++ b/.github/workflows/ohos.yml
@@ -0,0 +1,114 @@
+name: EasyTier OHOS
+
+on:
+ push:
+ branches: ["develop", "main", "releases/**"]
+ pull_request:
+ branches: ["develop", "main"]
+
+env:
+ CARGO_TERM_COLOR: always
+
+defaults:
+ run:
+ # necessary for windows
+ shell: bash
+
+jobs:
+ pre_job:
+ # continue-on-error: true # Uncomment once integration is finished
+ runs-on: ubuntu-latest
+ # Map a step output to a job output
+ outputs:
+ # do not skip push on branch starts with releases/
+ should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
+ steps:
+ - id: skip_check
+ uses: fkirc/skip-duplicate-actions@v5
+ with:
+ # All of these options are optional, so you can remove them if you are happy with the defaults
+ concurrent_skipping: 'same_content_newer'
+ skip_after_successful_duplicate: 'true'
+ cancel_others: 'true'
+ paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-contrib/easytier-ohrs/**", ".github/workflows/ohos.yml", ".github/workflows/install_rust.sh"]'
+ build-ohos:
+ runs-on: ubuntu-latest
+ needs: pre_job
+ if: needs.pre_job.outputs.should_skip != 'true'
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ wget \
+ unzip \
+ git \
+ pkg-config
+ sudo apt-get clean
+
+ - name: Download and extract native SDK
+ working-directory: ../../../
+ run: |
+ echo $PWD
+ wget -q \
+ https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.aa
+ wget -q \
+ https://github.com/openharmony-rs/ohos-sdk/releases/download/v5.1.0/ohos-sdk-windows_linux-public.tar.gz.ab
+ cat ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab > sdk.tar.gz
+ echo "Extracting native..."
+ mkdir sdk
+ tar -xzf sdk.tar.gz ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip
+ tar -xzf sdk.tar.gz ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip
+ unzip -qq ohos-sdk/linux/native-linux-x64-5.1.0.107-Release.zip -d sdk
+ unzip -qq ohos-sdk/linux/toolchains-linux-x64-5.1.0.107-Release.zip -d sdk
+ ls -la sdk/native/llvm/bin/
+ rm -rf ohos-sdk-windows_linux-public.tar.gz.aa ohos-sdk-windows_linux-public.tar.gz.ab ohos-sdk/
+
+ - name: Download and Extract Custom SDK
+ run: |
+ wget https://github.com/FrankHan052176/Easytier-OHOS-sdk/releases/download/v1/ohos-sdk.zip -O /tmp/ohos-sdk.zip
+ sudo unzip -o /tmp/ohos-sdk.zip -d /tmp/custom-sdk
+ sudo cp -rf /tmp/custom-sdk/linux/native/* $HOME/sdk/native
+ echo "Custom SDK files deployed to $HOME/sdk/native"
+ ls -a $HOME/sdk/native
+
+ - name: Setup build environment
+ run: |
+ echo "OHOS_NDK_HOME=$HOME/sdk" >> $GITHUB_ENV
+ echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
+
+ - name: Create clang wrapper script
+ run: |
+ sudo mkdir -p $OHOS_NDK_HOME/native/llvm
+ sudo tee $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh > /dev/null <<'EOF'
+ #!/bin/sh
+ exec $OHOS_NDK_HOME/native/llvm/bin/clang \
+ -target aarch64-linux-ohos \
+ --sysroot=$OHOS_NDK_HOME/native/sysroot \
+ -D__MUSL__ \
+ "$@"
+ EOF
+ sudo chmod +x $OHOS_NDK_HOME/native/llvm/aarch64-unknown-linux-ohos-clang.sh
+
+ - name: Build
+ working-directory: ./easytier-contrib/easytier-ohrs
+ run: |
+ sudo apt-get install -y llvm clang lldb lld
+ sudo apt-get install -y protobuf-compiler
+ bash ../../.github/workflows/install_rust.sh
+ source env.sh
+ cargo install ohrs
+ rustup target add aarch64-unknown-linux-ohos
+ cargo update easytier
+ ohrs doctor
+ ohrs build --release --arch aarch
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: easytier-ohos
+ path: ./easytier-contrib/easytier-ohrs/dist/arm64-v8a/libeasytier_ohrs.so
+ retention-days: 5
+ if-no-files-found: error
diff --git a/.gitignore b/.gitignore
index b7bc5ca97..d3a7e4651 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ target-*/
.vscode
.idea
+/.direnv/
# perf & flamegraph
perf.data
@@ -37,6 +38,9 @@ node_modules
.vite
easytier-gui/src-tauri/*.dll
+/easytier-contrib/easytier-ohrs/dist/
+
+.direnv
et.db*
/easytier/config.toml
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index e69de29bb..000000000
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..d4cb38a2d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,225 @@
+# Contributing to EasyTier
+
+[中文版](CONTRIBUTING_zh.md)
+
+Thank you for your interest in contributing to EasyTier! This document provides guidelines and instructions for contributing to the project.
+
+## Table of Contents
+
+- [Development Environment Setup](#development-environment-setup)
+ - [Prerequisites](#prerequisites)
+ - [Installation Steps](#installation-steps)
+- [Project Structure](#project-structure)
+- [Build Guide](#build-guide)
+ - [Building Core](#building-core)
+ - [Building GUI](#building-gui)
+ - [Building Mobile](#building-mobile)
+- [Development Workflow](#development-workflow)
+- [Testing Guidelines](#testing-guidelines)
+- [Pull Request Guidelines](#pull-request-guidelines)
+- [Additional Resources](#additional-resources)
+
+## Development Environment Setup
+
+### Prerequisites
+
+#### Required Tools
+- Node.js v21 or higher
+- pnpm v9 or higher
+- Rust toolchain (version 1.89)
+- LLVM and Clang
+- Protoc (Protocol Buffers compiler)
+
+#### Platform-Specific Dependencies
+
+**Linux (Ubuntu/Debian)**
+```bash
+# Core build dependencies
+sudo apt-get update && sudo apt-get install -y \
+ musl-tools \
+ llvm \
+ clang \
+ protobuf-compiler
+
+# GUI build dependencies
+sudo apt install -y \
+ libwebkit2gtk-4.1-dev \
+ build-essential \
+ curl \
+ wget \
+ file \
+ libgtk-3-dev \
+ librsvg2-dev \
+ libxdo-dev \
+ libssl-dev \
+ libappindicator3-dev \
+ patchelf
+
+# Testing dependencies
+sudo apt install -y bridge-utils
+```
+
+**For Cross-Compilation**
+- musl-cross toolchain (for MIPS and other architectures)
+- Additional setup may be required (see `.github/workflows/` for details)
+
+**For Android Development**
+- Java 20
+- Android SDK (Build Tools 34.0.0)
+- Android NDK (26.0.10792818)
+
+### Installation Steps
+
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/EasyTier/EasyTier.git
+ cd EasyTier
+ ```
+
+2. Install dependencies:
+ ```bash
+ # Install Rust toolchain
+ rustup install 1.89
+ rustup default 1.89
+
+ # Install project dependencies
+ pnpm -r install
+ ```
+
+## Project Structure
+
+```
+easytier/ # Core functionality and libraries
+easytier-web/ # Web dashboard and frontend
+easytier-gui/ # Desktop GUI application
+.github/workflows/ # CI/CD configuration files
+```
+
+## Build Guide
+
+### Building Core
+
+```bash
+# Standard build
+cargo build --release
+
+# Platform-specific builds
+cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
+cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
+cargo build --release --target x86_64-apple-darwin # macOS x86_64
+cargo build --release --target aarch64-apple-darwin # macOS M1/M2
+cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
+```
+
+Build artifacts: `target/[target-triple]/release/`
+
+### Building GUI
+
+```bash
+# 1. Build frontend
+pnpm -r build
+
+# 2. Build GUI application
+cd easytier-gui
+
+# Linux
+pnpm tauri build --target x86_64-unknown-linux-gnu
+
+# macOS
+pnpm tauri build --target x86_64-apple-darwin # Intel
+pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
+
+# Windows
+pnpm tauri build --target x86_64-pc-windows-msvc # x64
+```
+
+Build artifacts: `easytier-gui/src-tauri/target/release/bundle/`
+
+### Building Mobile
+
+```bash
+# 1. Install Android targets
+rustup target add aarch64-linux-android
+rustup target add armv7-linux-androideabi
+rustup target add i686-linux-android
+rustup target add x86_64-linux-android
+
+# 2. Build Android application
+cd easytier-gui
+pnpm tauri android build
+```
+
+Build artifacts: `easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
+
+### Build Notes
+
+1. Cross-compilation for ARM/MIPS requires additional setup
+2. Windows builds need correct DLL files
+3. Check `.github/workflows/` for detailed build configurations
+
+## Development Workflow
+
+1. Create a feature branch from `develop`:
+ ```bash
+ git checkout develop
+ git checkout -b feature/your-feature-name
+ ```
+
+2. Make your changes following our coding standards
+
+3. Write or update tests as needed
+
+4. Use conventional commit messages:
+ ```
+ feat: add new feature
+ fix: resolve bug
+ docs: update documentation
+ test: add tests
+ chore: update dependencies
+ ```
+
+5. Submit a pull request to `develop`
+
+## Testing Guidelines
+
+### Running Tests
+
+```bash
+# Configure system (Linux)
+sudo modprobe br_netfilter
+sudo sysctl net.bridge.bridge-nf-call-iptables=0
+sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
+
+# Run tests
+cargo test --no-default-features --features=full --verbose
+```
+
+### Test Requirements
+
+- Write tests for new features
+- Maintain existing test coverage
+- Tests should be isolated and repeatable
+- Include both unit and integration tests
+
+## Pull Request Guidelines
+
+1. Target the `develop` branch
+2. Ensure all tests pass
+3. Include clear description and purpose
+4. Reference related issues
+5. Keep changes focused and atomic
+6. Update documentation as needed
+
+## Additional Resources
+
+- [Issue Tracker](https://github.com/EasyTier/EasyTier/issues)
+- [Project Documentation](https://github.com/EasyTier/EasyTier/wiki)
+
+## Questions or Need Help?
+
+Feel free to:
+- Open an issue for questions
+- Join our community discussions
+- Reach out to maintainers
+
+Thank you for contributing to EasyTier!
\ No newline at end of file
diff --git a/CONTRIBUTING_zh.md b/CONTRIBUTING_zh.md
new file mode 100644
index 000000000..7e663da62
--- /dev/null
+++ b/CONTRIBUTING_zh.md
@@ -0,0 +1,233 @@
+# EasyTier 贡献指南
+
+[English Version](CONTRIBUTING.md)
+
+感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。
+
+## 目录
+
+- [EasyTier 贡献指南](#easytier-贡献指南)
+ - [目录](#目录)
+ - [开发环境配置](#开发环境配置)
+ - [前置要求](#前置要求)
+ - [必需工具](#必需工具)
+ - [平台特定依赖](#平台特定依赖)
+ - [安装步骤](#安装步骤)
+ - [项目结构](#项目结构)
+ - [构建指南](#构建指南)
+ - [构建核心组件](#构建核心组件)
+ - [构建桌面应用](#构建桌面应用)
+ - [构建移动应用](#构建移动应用)
+ - [构建注意事项](#构建注意事项)
+ - [开发工作流](#开发工作流)
+ - [测试指南](#测试指南)
+ - [运行测试](#运行测试)
+ - [测试要求](#测试要求)
+ - [Pull Request 规范](#pull-request-规范)
+ - [其他资源](#其他资源)
+ - [需要帮助?](#需要帮助)
+
+## 开发环境配置
+
+### 前置要求
+
+#### 必需工具
+- Node.js v21 或更高版本
+- pnpm v9 或更高版本
+- Rust 工具链(版本 1.89)
+- LLVM 和 Clang
+- Protoc(Protocol Buffers 编译器)
+
+#### 平台特定依赖
+
+**Linux (Ubuntu/Debian)**
+```bash
+# 核心构建依赖
+sudo apt-get update && sudo apt-get install -y \
+ musl-tools \
+ llvm \
+ clang \
+ protobuf-compiler
+
+# GUI 构建依赖
+sudo apt install -y \
+ libwebkit2gtk-4.1-dev \
+ build-essential \
+ curl \
+ wget \
+ file \
+ libgtk-3-dev \
+ librsvg2-dev \
+ libxdo-dev \
+ libssl-dev \
+ libappindicator3-dev \
+ patchelf
+
+# 测试依赖
+sudo apt install -y bridge-utils
+```
+
+**交叉编译依赖**
+- musl-cross 工具链(用于 MIPS 和其他架构)
+- 可能需要额外配置(详见 `.github/workflows/` 目录)
+
+**Android 开发依赖**
+- Java 20
+- Android SDK(Build Tools 34.0.0)
+- Android NDK(26.0.10792818)
+
+### 安装步骤
+
+1. 克隆仓库:
+ ```bash
+ git clone https://github.com/EasyTier/EasyTier.git
+ cd EasyTier
+ ```
+
+2. 安装依赖:
+ ```bash
+ # 安装 Rust 工具链
+ rustup install 1.89
+ rustup default 1.89
+
+ # 安装项目依赖
+ pnpm -r install
+ ```
+
+## 项目结构
+
+```
+easytier/ # 核心功能和库
+easytier-web/ # Web 仪表盘和前端
+easytier-gui/ # 桌面 GUI 应用
+.github/workflows/ # CI/CD 配置文件
+```
+
+## 构建指南
+
+### 构建核心组件
+
+```bash
+# 标准构建
+cargo build --release
+
+# 特定平台构建
+cargo build --release --target x86_64-unknown-linux-musl # Linux x86_64
+cargo build --release --target aarch64-unknown-linux-musl # Linux ARM64
+cargo build --release --target x86_64-apple-darwin # macOS x86_64
+cargo build --release --target aarch64-apple-darwin # macOS M1/M2
+cargo build --release --target x86_64-pc-windows-msvc # Windows x86_64
+```
+
+构建产物位置:`target/[target-triple]/release/`
+
+### 构建桌面应用
+
+```bash
+# 1. 构建前端
+pnpm -r build
+
+# 2. 构建 GUI 应用
+cd easytier-gui
+
+# Linux
+pnpm tauri build --target x86_64-unknown-linux-gnu
+
+# macOS
+pnpm tauri build --target x86_64-apple-darwin # Intel
+pnpm tauri build --target aarch64-apple-darwin # Apple Silicon
+
+# Windows
+pnpm tauri build --target x86_64-pc-windows-msvc # x64
+```
+
+构建产物位置:`easytier-gui/src-tauri/target/release/bundle/`
+
+### 构建移动应用
+
+```bash
+# 1. 安装 Android 目标平台
+rustup target add aarch64-linux-android
+rustup target add armv7-linux-androideabi
+rustup target add i686-linux-android
+rustup target add x86_64-linux-android
+
+# 2. 构建 Android 应用
+cd easytier-gui
+pnpm tauri android build
+```
+
+构建产物位置:`easytier-gui/src-tauri/gen/android/app/build/outputs/apk/universal/release/`
+
+### 构建注意事项
+
+1. ARM/MIPS 的交叉编译需要额外配置
+2. Windows 构建需要正确的 DLL 文件
+3. 详细构建配置请参考 `.github/workflows/` 目录
+
+## 开发工作流
+
+1. 从 `develop` 分支创建特性分支:
+ ```bash
+ git checkout develop
+ git checkout -b feature/your-feature-name
+ ```
+
+2. 按照代码规范进行修改
+
+3. 编写或更新测试
+
+4. 使用规范的提交信息:
+ ```
+ feat: 添加新功能
+ fix: 修复问题
+ docs: 更新文档
+ test: 添加测试
+ chore: 更新依赖
+ ```
+
+5. 提交 Pull Request 到 `develop` 分支
+
+## 测试指南
+
+### 运行测试
+
+```bash
+# 配置系统(Linux)
+sudo modprobe br_netfilter
+sudo sysctl net.bridge.bridge-nf-call-iptables=0
+sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
+
+# 运行测试
+cargo test --no-default-features --features=full --verbose
+```
+
+### 测试要求
+
+- 为新功能编写测试
+- 维护现有测试覆盖率
+- 测试应该是独立且可重复的
+- 包含单元测试和集成测试
+
+## Pull Request 规范
+
+1. 目标分支为 `develop`
+2. 确保所有测试通过
+3. 包含清晰的描述和目的
+4. 关联相关的 issues
+5. 保持变更的原子性和聚焦性
+6. 及时更新相关文档
+
+## 其他资源
+
+- [问题追踪](https://github.com/EasyTier/EasyTier/issues)
+- [项目文档](https://github.com/EasyTier/EasyTier/wiki)
+
+## 需要帮助?
+
+欢迎:
+- 提出问题
+- 参与社区讨论
+- 联系维护者
+
+感谢您为 EasyTier 做出贡献!
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 7ad9c53a0..b6e0eef87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -221,11 +221,12 @@ dependencies = [
"core-graphics 0.23.2",
"image 0.25.2",
"log",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
"parking_lot",
"windows-sys 0.48.0",
+ "wl-clipboard-rs",
"x11rb",
]
@@ -283,7 +284,7 @@ version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
dependencies = [
- "brotli",
+ "brotli 7.0.0",
"flate2",
"futures-core",
"memchr",
@@ -535,7 +536,7 @@ dependencies = [
"http-body-util",
"hyper",
"hyper-util",
- "itoa 1.0.11",
+ "itoa",
"matchit",
"memchr",
"mime",
@@ -750,12 +751,6 @@ dependencies = [
"digest",
]
-[[package]]
-name = "block"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
-
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -771,7 +766,16 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
- "objc2",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
+dependencies = [
+ "objc2 0.6.1",
]
[[package]]
@@ -819,7 +823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc"
dependencies = [
"borsh-derive",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
]
[[package]]
@@ -843,7 +847,18 @@ checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
- "brotli-decompressor",
+ "brotli-decompressor 4.0.1",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor 5.0.0",
]
[[package]]
@@ -856,6 +871,16 @@ dependencies = [
"alloc-stdlib",
]
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
[[package]]
name = "bstr"
version = "1.10.0"
@@ -1021,23 +1046,23 @@ dependencies = [
[[package]]
name = "cargo_metadata"
-version = "0.18.1"
+version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
]
[[package]]
name = "cargo_toml"
-version = "0.17.2"
+version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
+checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
dependencies = [
"serde",
"toml 0.8.19",
@@ -1096,6 +1121,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -1197,6 +1228,15 @@ dependencies = [
"unicode-width 0.2.0",
]
+[[package]]
+name = "clap_complete"
+version = "4.5.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
+dependencies = [
+ "clap",
+]
+
[[package]]
name = "clap_derive"
version = "4.5.28"
@@ -1224,36 +1264,6 @@ dependencies = [
"error-code",
]
-[[package]]
-name = "cocoa"
-version = "0.26.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2"
-dependencies = [
- "bitflags 2.8.0",
- "block",
- "cocoa-foundation",
- "core-foundation 0.10.0",
- "core-graphics 0.24.0",
- "foreign-types 0.5.0",
- "libc",
- "objc",
-]
-
-[[package]]
-name = "cocoa-foundation"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
-dependencies = [
- "bitflags 2.8.0",
- "block",
- "core-foundation 0.10.0",
- "core-graphics-types 0.2.0",
- "libc",
- "objc",
-]
-
[[package]]
name = "codepage"
version = "0.1.2"
@@ -1519,15 +1529,15 @@ dependencies = [
[[package]]
name = "cssparser"
-version = "0.27.2"
+version = "0.29.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
dependencies = [
"cssparser-macros",
"dtoa-short",
- "itoa 0.4.8",
+ "itoa",
"matches",
- "phf 0.8.0",
+ "phf 0.10.1",
"proc-macro2",
"quote",
"smallvec",
@@ -1740,6 +1750,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "derive-new"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
@@ -1828,11 +1849,11 @@ dependencies = [
[[package]]
name = "dirs"
-version = "5.0.1"
+version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
- "dirs-sys 0.4.1",
+ "dirs-sys 0.5.0",
]
[[package]]
@@ -1842,20 +1863,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
- "redox_users",
+ "redox_users 0.4.5",
"winapi",
]
[[package]]
name = "dirs-sys"
-version = "0.4.1"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
- "redox_users",
- "windows-sys 0.48.0",
+ "redox_users 0.5.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1864,6 +1885,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.8.0",
+ "objc2 0.6.1",
+]
+
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -1904,6 +1935,12 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "dpi"
version = "0.1.1"
@@ -1942,10 +1979,11 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "easytier"
-version = "2.3.2"
+version = "2.4.2"
dependencies = [
"aes-gcm",
"anyhow",
+ "arc-swap",
"async-recursion",
"async-ringbuf",
"async-stream",
@@ -1961,6 +1999,7 @@ dependencies = [
"chrono",
"cidr",
"clap",
+ "clap_complete",
"crossbeam",
"dashmap",
"dbus",
@@ -1982,9 +2021,6 @@ dependencies = [
"http_req",
"humansize",
"humantime-serde",
- "jemalloc-ctl",
- "jemalloc-sys",
- "jemallocator",
"kcp-sys",
"lazy_static",
"libc",
@@ -1999,6 +2035,7 @@ dependencies = [
"network-interface",
"nix 0.29.0",
"once_cell",
+ "openssl",
"parking_lot",
"percent-encoding",
"petgraph 0.8.1",
@@ -2033,6 +2070,9 @@ dependencies = [
"tachyonix",
"thiserror 1.0.63",
"thunk-rs",
+ "tikv-jemalloc-ctl",
+ "tikv-jemalloc-sys",
+ "tikv-jemallocator",
"time",
"timedmap",
"tokio",
@@ -2052,8 +2092,10 @@ dependencies = [
"version-compare",
"which 7.0.3",
"wildmatch",
+ "winapi",
"windows 0.52.0",
"windows-service",
+ "windows-sys 0.52.0",
"winreg 0.52.0",
"zerocopy",
"zip",
@@ -2074,16 +2116,17 @@ dependencies = [
[[package]]
name = "easytier-gui"
-version = "2.3.2"
+version = "2.4.2"
dependencies = [
"anyhow",
"chrono",
"dashmap",
"dunce",
"easytier",
- "gethostname 0.5.0",
+ "gethostname 1.0.2",
+ "libc",
"once_cell",
- "privilege",
+ "security-framework-sys",
"serde",
"serde_json",
"tauri",
@@ -2099,6 +2142,8 @@ dependencies = [
"thunk-rs",
"tokio",
"uuid",
+ "winapi",
+ "windows 0.52.0",
]
[[package]]
@@ -2121,7 +2166,7 @@ dependencies = [
[[package]]
name = "easytier-web"
-version = "2.3.2"
+version = "2.4.2"
dependencies = [
"anyhow",
"async-trait",
@@ -2171,16 +2216,16 @@ dependencies = [
[[package]]
name = "embed-resource"
-version = "2.4.3"
+version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4edcacde9351c33139a41e3c97eb2334351a81a2791bebb0b243df837128f602"
+checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38"
dependencies = [
"cc",
"memchr",
"rustc_version",
- "toml 0.8.19",
+ "toml 0.9.2",
"vswhom",
- "winreg 0.52.0",
+ "winreg 0.55.0",
]
[[package]]
@@ -2827,6 +2872,16 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "gethostname"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
+dependencies = [
+ "rustix 1.0.7",
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -3300,16 +3355,14 @@ dependencies = [
[[package]]
name = "html5ever"
-version = "0.26.0"
+version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
dependencies = [
"log",
"mac",
"markup5ever",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
+ "match_token",
]
[[package]]
@@ -3320,7 +3373,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
- "itoa 1.0.11",
+ "itoa",
]
[[package]]
@@ -3413,7 +3466,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
- "itoa 1.0.11",
+ "itoa",
"pin-project-lite",
"smallvec",
"tokio",
@@ -3497,9 +3550,9 @@ dependencies = [
[[package]]
name = "ico"
-version = "0.3.0"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae"
+checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
dependencies = [
"byteorder",
"png",
@@ -3734,9 +3787,9 @@ dependencies = [
[[package]]
name = "infer"
-version = "0.16.0"
+version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
dependencies = [
"cfb",
]
@@ -3761,15 +3814,6 @@ dependencies = [
"generic-array",
]
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
-]
-
[[package]]
name = "ip_network"
version = "0.4.1"
@@ -3874,12 +3918,6 @@ dependencies = [
"either",
]
-[[package]]
-name = "itoa"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
-
[[package]]
name = "itoa"
version = "1.0.11"
@@ -3909,37 +3947,6 @@ dependencies = [
"system-deps",
]
-[[package]]
-name = "jemalloc-ctl"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cffc705424a344c054e135d12ee591402f4539245e8bbd64e6c9eaa9458b63c"
-dependencies = [
- "jemalloc-sys",
- "libc",
- "paste",
-]
-
-[[package]]
-name = "jemalloc-sys"
-version = "0.5.4+5.3.0-patched"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac6c1946e1cea1788cbfde01c993b52a10e2da07f4bac608228d1bed20bfebf2"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
-name = "jemallocator"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0de374a9f8e63150e6f5e8a60cc14c668226d7a347d8aee1a45766e3c4dd3bc"
-dependencies = [
- "jemalloc-sys",
- "libc",
-]
-
[[package]]
name = "jni"
version = "0.21.1"
@@ -4012,7 +4019,7 @@ dependencies = [
[[package]]
name = "kcp-sys"
version = "0.1.0"
-source = "git+https://github.com/EasyTier/kcp-sys#0f0a0558391ba391c089806c23f369651f6c9eeb"
+source = "git+https://github.com/EasyTier/kcp-sys?rev=0f0a0558391ba391c089806c23f369651f6c9eeb#0f0a0558391ba391c089806c23f369651f6c9eeb"
dependencies = [
"anyhow",
"auto_impl",
@@ -4044,14 +4051,13 @@ dependencies = [
[[package]]
name = "kuchikiki"
-version = "0.8.2"
+version = "0.8.8-speedreader"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
+checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
dependencies = [
"cssparser",
"html5ever",
- "indexmap 1.9.3",
- "matches",
+ "indexmap 2.7.1",
"selectors",
]
@@ -4152,9 +4158,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libmimalloc-sys"
-version = "0.1.42"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
+checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
dependencies = [
"cc",
"libc",
@@ -4279,15 +4285,6 @@ dependencies = [
"winreg 0.52.0",
]
-[[package]]
-name = "malloc_buf"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
-dependencies = [
- "libc",
-]
-
[[package]]
name = "managed"
version = "0.8.0"
@@ -4302,18 +4299,29 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
-version = "0.11.0"
+version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
dependencies = [
"log",
- "phf 0.10.1",
- "phf_codegen 0.10.0",
+ "phf 0.11.2",
+ "phf_codegen 0.11.3",
"string_cache",
"string_cache_codegen",
"tendril",
]
+[[package]]
+name = "match_token"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
[[package]]
name = "matchers"
version = "0.1.0"
@@ -4390,9 +4398,9 @@ dependencies = [
[[package]]
name = "mimalloc"
-version = "0.1.46"
+version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
+checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
dependencies = [
"libmimalloc-sys",
]
@@ -4471,21 +4479,22 @@ dependencies = [
[[package]]
name = "muda"
-version = "0.15.3"
+version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484"
+checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988"
dependencies = [
"crossbeam-channel",
"dpi",
"gtk",
"keyboard-types",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
"once_cell",
"png",
"serde",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
"windows-sys 0.59.0",
]
@@ -4700,6 +4709,18 @@ dependencies = [
"memoffset",
]
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.8.0",
+ "cfg-if",
+ "cfg_aliases 0.1.1",
+ "libc",
+]
+
[[package]]
name = "nix"
version = "0.29.0"
@@ -4708,7 +4729,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"libc",
"memoffset",
]
@@ -4721,7 +4742,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.8.0",
"cfg-if",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"libc",
"memoffset",
]
@@ -4894,23 +4915,11 @@ dependencies = [
"libc",
]
-[[package]]
-name = "objc"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
-dependencies = [
- "malloc_buf",
-]
-
[[package]]
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
-dependencies = [
- "cc",
-]
[[package]]
name = "objc2"
@@ -4922,6 +4931,16 @@ dependencies = [
"objc2-encode",
]
+[[package]]
+name = "objc2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
@@ -4929,37 +4948,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.8.0",
- "block2",
+ "block2 0.5.1",
"libc",
- "objc2",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-foundation",
- "objc2-quartz-core",
+ "objc2 0.5.2",
+ "objc2-core-data 0.2.2",
+ "objc2-core-image 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
]
[[package]]
-name = "objc2-cloud-kit"
-version = "0.2.2"
+name = "objc2-app-kit"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-core-location",
- "objc2-foundation",
+ "block2 0.6.1",
+ "libc",
+ "objc2 0.6.1",
+ "objc2-cloud-kit",
+ "objc2-core-data 0.3.1",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image 0.3.1",
+ "objc2-foundation 0.3.1",
+ "objc2-quartz-core 0.3.1",
]
[[package]]
-name = "objc2-contacts"
-version = "0.2.2"
+name = "objc2-cloud-kit"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
+checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "bitflags 2.8.0",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
]
[[package]]
@@ -4969,9 +4994,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
+dependencies = [
+ "bitflags 2.8.0",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags 2.8.0",
+ "dispatch2",
+ "objc2 0.6.1",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
+dependencies = [
+ "bitflags 2.8.0",
+ "dispatch2",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-io-surface",
]
[[package]]
@@ -4980,29 +5040,36 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
"objc2-metal",
]
[[package]]
-name = "objc2-core-location"
-version = "0.2.2"
+name = "objc2-core-image"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
+checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
dependencies = [
- "block2",
- "objc2",
- "objc2-contacts",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-encode"
-version = "4.0.3"
+version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
[[package]]
name = "objc2-foundation"
@@ -5011,115 +5078,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.8.0",
- "block2",
+ "block2 0.5.1",
"dispatch",
"libc",
- "objc2",
+ "objc2 0.5.2",
]
[[package]]
-name = "objc2-link-presentation"
-version = "0.2.2"
+name = "objc2-foundation"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
+checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
- "block2",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "bitflags 2.8.0",
+ "block2 0.6.1",
+ "libc",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
]
[[package]]
-name = "objc2-metal"
-version = "0.2.2"
+name = "objc2-io-surface"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
]
[[package]]
-name = "objc2-quartz-core"
+name = "objc2-metal"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-foundation",
- "objc2-metal",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
-name = "objc2-symbols"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
-dependencies = [
- "objc2",
- "objc2-foundation",
-]
-
-[[package]]
-name = "objc2-ui-kit"
+name = "objc2-quartz-core"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-cloud-kit",
- "objc2-core-data",
- "objc2-core-image",
- "objc2-core-location",
- "objc2-foundation",
- "objc2-link-presentation",
- "objc2-quartz-core",
- "objc2-symbols",
- "objc2-uniform-type-identifiers",
- "objc2-user-notifications",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal",
]
[[package]]
-name = "objc2-uniform-type-identifiers"
-version = "0.2.2"
+name = "objc2-quartz-core"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
+checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "bitflags 2.8.0",
+ "objc2 0.6.1",
+ "objc2-foundation 0.3.1",
]
[[package]]
-name = "objc2-user-notifications"
-version = "0.2.2"
+name = "objc2-ui-kit"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
+checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-core-location",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-web-kit"
-version = "0.2.2"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65"
+checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
"bitflags 2.8.0",
- "block2",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "block2 0.6.1",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
]
[[package]]
@@ -5190,6 +5238,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+[[package]]
+name = "openssl-src"
+version = "300.5.2+3.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "openssl-sys"
version = "0.9.103"
@@ -5198,6 +5255,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
+ "openssl-src",
"pkg-config",
"vcpkg",
]
@@ -5451,9 +5509,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
- "phf_macros 0.8.0",
"phf_shared 0.8.0",
- "proc-macro-hack",
]
[[package]]
@@ -5462,7 +5518,9 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
dependencies = [
+ "phf_macros 0.10.0",
"phf_shared 0.10.0",
+ "proc-macro-hack",
]
[[package]]
@@ -5487,12 +5545,12 @@ dependencies = [
[[package]]
name = "phf_codegen"
-version = "0.10.0"
+version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
- "phf_generator 0.10.0",
- "phf_shared 0.10.0",
+ "phf_generator 0.11.2",
+ "phf_shared 0.11.2",
]
[[package]]
@@ -5527,12 +5585,12 @@ dependencies = [
[[package]]
name = "phf_macros"
-version = "0.8.0"
+version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
dependencies = [
- "phf_generator 0.8.0",
- "phf_shared 0.8.0",
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
"proc-macro-hack",
"proc-macro2",
"quote",
@@ -5637,7 +5695,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap 2.7.1",
- "quick-xml",
+ "quick-xml 0.32.0",
"serde",
"time",
]
@@ -5833,18 +5891,6 @@ dependencies = [
"syn 2.0.87",
]
-[[package]]
-name = "privilege"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "765ec92721e112ffe07f5c06fb0654da0b708990888981d05cf12a7c9909df30"
-dependencies = [
- "libc",
- "security-framework-sys",
- "which 4.4.2",
- "windows-sys 0.48.0",
-]
-
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -6062,6 +6108,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quinn"
version = "0.11.8"
@@ -6069,7 +6124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
@@ -6332,6 +6387,17 @@ dependencies = [
"thiserror 1.0.63",
]
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror 2.0.11",
+]
+
[[package]]
name = "regex"
version = "1.10.6"
@@ -7109,22 +7175,20 @@ dependencies = [
[[package]]
name = "selectors"
-version = "0.22.0"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
dependencies = [
"bitflags 1.3.2",
"cssparser",
"derive_more",
"fxhash",
"log",
- "matches",
"phf 0.8.0",
"phf_codegen 0.8.0",
"precomputed-hash",
"servo_arc",
"smallvec",
- "thin-slice",
]
[[package]]
@@ -7184,7 +7248,7 @@ version = "1.0.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
dependencies = [
- "itoa 1.0.11",
+ "itoa",
"memchr",
"ryu",
"serde",
@@ -7196,7 +7260,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
- "itoa 1.0.11",
+ "itoa",
"serde",
]
@@ -7220,6 +7284,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_spanned"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -7227,7 +7300,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
- "itoa 1.0.11",
+ "itoa",
"ryu",
"serde",
]
@@ -7269,7 +7342,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e76bab63c3fd98d27c17f9cbce177f64a91f5e69ac04cafe04e1bb25d1dc3c"
dependencies = [
"indexmap 2.7.1",
- "itoa 1.0.11",
+ "itoa",
"libyml",
"log",
"memchr",
@@ -7328,8 +7401,8 @@ dependencies = [
[[package]]
name = "service-manager"
-version = "0.7.1"
-source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#13dae5e8160f91fdc9834d847165cc5ce0a72fb3"
+version = "0.8.0"
+source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b"
dependencies = [
"cfg-if",
"dirs 4.0.0",
@@ -7342,9 +7415,9 @@ dependencies = [
[[package]]
name = "servo_arc"
-version = "0.1.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
dependencies = [
"nodrop",
"stable_deref_trait",
@@ -7487,9 +7560,9 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.5.7"
+version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -7502,15 +7575,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd"
dependencies = [
"bytemuck",
- "cfg_aliases",
+ "cfg_aliases 0.2.1",
"core-graphics 0.23.2",
"foreign-types 0.5.0",
"js-sys",
"log",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
- "objc2-quartz-core",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core 0.2.2",
"raw-window-handle",
"redox_syscall",
"wasm-bindgen",
@@ -7697,7 +7770,7 @@ dependencies = [
"hex",
"hkdf",
"hmac",
- "itoa 1.0.11",
+ "itoa",
"log",
"md-5",
"memchr",
@@ -7742,7 +7815,7 @@ dependencies = [
"hkdf",
"hmac",
"home",
- "itoa 1.0.11",
+ "itoa",
"log",
"md-5",
"memchr",
@@ -8012,12 +8085,11 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tao"
-version = "0.30.6"
+version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "833b4d43383d76d5078d72f3acd977f47eb5b6751eb40baa665d13828e7b79df"
+checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
dependencies = [
"bitflags 2.8.0",
- "cocoa",
"core-foundation 0.10.0",
"core-graphics 0.24.0",
"crossbeam-channel",
@@ -8027,7 +8099,6 @@ dependencies = [
"gdkwayland-sys",
"gdkx11-sys",
"gtk",
- "instant",
"jni",
"lazy_static",
"libc",
@@ -8035,7 +8106,9 @@ dependencies = [
"ndk",
"ndk-context",
"ndk-sys",
- "objc",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-foundation 0.3.1",
"once_cell",
"parking_lot",
"raw-window-handle",
@@ -8043,8 +8116,8 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
- "windows 0.58.0",
- "windows-core 0.58.0",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
"windows-version",
"x11-dl",
]
@@ -8074,17 +8147,16 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
-version = "2.0.6"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3889b392db6d32a105d3757230ea0220090b8f94c90d3e60b6c5eb91178ab1b"
+checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7"
dependencies = [
"anyhow",
"bytes",
- "dirs 5.0.1",
+ "dirs 6.0.0",
"dunce",
"embed_plist",
- "futures-util",
- "getrandom 0.2.15",
+ "getrandom 0.3.2",
"glob",
"gtk",
"heck 0.5.0",
@@ -8095,9 +8167,10 @@ dependencies = [
"log",
"mime",
"muda",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-foundation 0.3.1",
+ "objc2-ui-kit",
"percent-encoding",
"plist",
"raw-window-handle",
@@ -8112,7 +8185,7 @@ dependencies = [
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
"tokio",
"tray-icon",
"url",
@@ -8120,18 +8193,18 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
- "windows 0.58.0",
+ "windows 0.61.3",
]
[[package]]
name = "tauri-build"
-version = "2.0.3"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
+checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs 5.0.1",
+ "dirs 6.0.0",
"glob",
"heck 0.5.0",
"json-patch",
@@ -8147,12 +8220,12 @@ dependencies = [
[[package]]
name = "tauri-codegen"
-version = "2.0.3"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf79faeecf301d3e969b1fae977039edb77a4c1f25cc0a961be298b54bff97cf"
+checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a"
dependencies = [
"base64 0.22.1",
- "brotli",
+ "brotli 8.0.1",
"ico",
"json-patch",
"plist",
@@ -8174,9 +8247,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
-version = "2.0.3"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c52027c8c5afb83166dacddc092ee8fff50772f9646d461d8c33ee887e447a03"
+checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -8188,9 +8261,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
-version = "2.0.3"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e753f2a30933a9bbf0a202fa47d7cc4a3401f06e8d6dcc53b79aa62954828c79"
+checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02"
dependencies = [
"anyhow",
"glob",
@@ -8205,24 +8278,23 @@ dependencies = [
[[package]]
name = "tauri-plugin-autostart"
-version = "2.0.1"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bba6bb936e0fd0a58ed958b49e2e423dd40949c9d9425cc991be996959e3838e"
+checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5"
dependencies = [
"auto-launch",
- "log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
]
[[package]]
name = "tauri-plugin-clipboard-manager"
-version = "2.0.2"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a66feaa0fb7fce8e5073323d11ca381c9da7ac06f458e42b9ff77364b76a360"
+checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca"
dependencies = [
"arboard",
"log",
@@ -8230,16 +8302,16 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
]
[[package]]
name = "tauri-plugin-os"
-version = "2.0.1"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbc5f23a86f37687c7f4fecfdc706b279087bc44f7a46702f7307ff1551ee03a"
+checksum = "05bccb4c6de4299beec5a9b070878a01bce9e2c945aa7a75bcea38bcba4c675d"
dependencies = [
- "gethostname 0.5.0",
+ "gethostname 1.0.2",
"log",
"os_info",
"serde",
@@ -8248,14 +8320,14 @@ dependencies = [
"sys-locale",
"tauri",
"tauri-plugin",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
]
[[package]]
name = "tauri-plugin-positioner"
-version = "2.0.2"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "647253ed516eb1dc5a7eefbfadf594ea5ec9cd8b506ef8896ed64621f9f1d264"
+checksum = "3a01e373ea3f3f5f46d40f434ba13bd12fa4833aabab50dfc09f6362bad27c95"
dependencies = [
"log",
"serde",
@@ -8263,14 +8335,14 @@ dependencies = [
"serde_repr",
"tauri",
"tauri-plugin",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
]
[[package]]
name = "tauri-plugin-process"
-version = "2.0.1"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae06a00087c148962a52814a2d7265b1a0505bced5ffb74f8c284a5f96a4d03d"
+checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
dependencies = [
"tauri",
"tauri-plugin",
@@ -8278,9 +8350,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
-version = "2.0.2"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ad7880c5586b6b2104be451e3d7fc0f3800c84bda69e9ba81c828f87cb34267"
+checksum = "2b9ffadec5c3523f11e8273465cacb3d86ea7652a28e6e2a2e9b5c182f791d25"
dependencies = [
"encoding_rs",
"log",
@@ -8293,22 +8365,22 @@ dependencies = [
"shared_child",
"tauri",
"tauri-plugin",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
"tokio",
]
[[package]]
name = "tauri-plugin-single-instance"
-version = "2.2.3"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1320af4d866a7fb5f5721d299d14d0dd9e4e6bc0359ff3e263124a2bf6814efa"
+checksum = "50a0e5a4ce43cb3a733c3aef85e8478bc769dac743c615e26639cbf5d953faf7"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.11",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
"zbus",
]
@@ -8324,36 +8396,40 @@ dependencies = [
[[package]]
name = "tauri-runtime"
-version = "2.1.1"
+version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784"
+checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676"
dependencies = [
+ "cookie",
"dpi",
"gtk",
"http",
"jni",
+ "objc2 0.6.1",
+ "objc2-ui-kit",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
"url",
- "windows 0.58.0",
+ "windows 0.61.3",
]
[[package]]
name = "tauri-runtime-wry"
-version = "2.1.2"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
+checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439"
dependencies = [
"gtk",
"http",
"jni",
"log",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-foundation 0.3.1",
+ "once_cell",
"percent-encoding",
"raw-window-handle",
"softbuffer",
@@ -8363,17 +8439,18 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
- "windows 0.58.0",
+ "windows 0.61.3",
"wry",
]
[[package]]
name = "tauri-utils"
-version = "2.1.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9271a88f99b4adea0dc71d0baca4505475a0bbd139fb135f62958721aaa8fe54"
+checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e"
dependencies = [
- "brotli",
+ "anyhow",
+ "brotli 8.0.1",
"cargo_metadata",
"ctor",
"dunce",
@@ -8406,12 +8483,13 @@ dependencies = [
[[package]]
name = "tauri-winres"
-version = "0.1.1"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
+checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4"
dependencies = [
"embed-resource",
- "toml 0.7.8",
+ "indexmap 2.7.1",
+ "toml 0.8.19",
]
[[package]]
@@ -8448,12 +8526,6 @@ dependencies = [
"windows-sys 0.59.0",
]
-[[package]]
-name = "thin-slice"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
-
[[package]]
name = "thiserror"
version = "1.0.63"
@@ -8506,8 +8578,8 @@ dependencies = [
[[package]]
name = "thunk-rs"
-version = "0.3.3"
-source = "git+https://github.com/easytier/thunk.git#5e8371a3100dbc18dda952a2036c6bd6fb0504db"
+version = "0.3.5"
+source = "git+https://github.com/easytier/thunk.git#cbbeec75a66b7b3cf0824ae890d9d06bcfb9d1f3"
[[package]]
name = "tiff"
@@ -8520,6 +8592,37 @@ dependencies = [
"weezl",
]
+[[package]]
+name = "tikv-jemalloc-ctl"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f21f216790c8df74ce3ab25b534e0718da5a1916719771d3fec23315c99e468b"
+dependencies = [
+ "libc",
+ "paste",
+ "tikv-jemalloc-sys",
+]
+
+[[package]]
+name = "tikv-jemalloc-sys"
+version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd3c60906412afa9c2b5b5a48ca6a5abe5736aec9eb48ad05037a677e52e4e2d"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "tikv-jemallocator"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cec5ff18518d81584f477e9bfdf957f5bb0979b0bac3af4ca30b5b3ae2d2865"
+dependencies = [
+ "libc",
+ "tikv-jemalloc-sys",
+]
+
[[package]]
name = "time"
version = "0.3.41"
@@ -8527,7 +8630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
- "itoa 1.0.11",
+ "itoa",
"libc",
"num-conv",
"num_threads",
@@ -8698,8 +8801,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.7",
+ "toml_datetime 0.6.8",
"toml_edit 0.19.15",
]
@@ -8710,11 +8813,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.7",
+ "toml_datetime 0.6.8",
"toml_edit 0.22.20",
]
+[[package]]
+name = "toml"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
+dependencies = [
+ "indexmap 2.7.1",
+ "serde",
+ "serde_spanned 1.0.0",
+ "toml_datetime 0.7.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.10",
+]
+
[[package]]
name = "toml_datetime"
version = "0.6.8"
@@ -8724,6 +8842,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "toml_datetime"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "toml_edit"
version = "0.19.15"
@@ -8732,8 +8859,8 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.7.1",
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.7",
+ "toml_datetime 0.6.8",
"winnow 0.5.40",
]
@@ -8744,7 +8871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap 2.7.1",
- "toml_datetime",
+ "toml_datetime 0.6.8",
"winnow 0.5.40",
]
@@ -8756,11 +8883,26 @@ checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
dependencies = [
"indexmap 2.7.1",
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.7",
+ "toml_datetime 0.6.8",
"winnow 0.6.18",
]
+[[package]]
+name = "toml_parser"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
+dependencies = [
+ "winnow 0.7.10",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
+
[[package]]
name = "tonic-build"
version = "0.12.1"
@@ -9020,25 +9162,39 @@ dependencies = [
[[package]]
name = "tray-icon"
-version = "0.19.1"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b"
+checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a"
dependencies = [
- "core-graphics 0.24.0",
"crossbeam-channel",
- "dirs 5.0.1",
+ "dirs 6.0.0",
"libappindicator",
"muda",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation 0.3.1",
"once_cell",
"png",
"serde",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
"windows-sys 0.59.0",
]
+[[package]]
+name = "tree_magic_mini"
+version = "3.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63"
+dependencies = [
+ "fnv",
+ "memchr",
+ "nom",
+ "once_cell",
+ "petgraph 0.6.5",
+]
+
[[package]]
name = "triomphe"
version = "0.1.13"
@@ -9065,8 +9221,7 @@ checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]]
name = "tun-easytier"
version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10dff0358b37ef593a74c9d2264a1df126e169d194878732a4f99ff7b01678bd"
+source = "git+https://github.com/EasyTier/rust-tun#12378839e7985283df0e4fb536b7137230356db5"
dependencies = [
"bytes",
"cfg-if",
@@ -9478,6 +9633,77 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "wayland-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix 0.38.34",
+ "scoped-tls",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
+dependencies = [
+ "bitflags 2.8.0",
+ "rustix 0.38.34",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.31.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
+dependencies = [
+ "bitflags 2.8.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols-wlr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6"
+dependencies = [
+ "bitflags 2.8.0",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
+dependencies = [
+ "proc-macro2",
+ "quick-xml 0.37.5",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
+dependencies = [
+ "pkg-config",
+]
+
[[package]]
name = "web-sys"
version = "0.3.70"
@@ -9581,16 +9807,16 @@ dependencies = [
[[package]]
name = "webview2-com"
-version = "0.33.0"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
+checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
- "windows 0.58.0",
- "windows-core 0.58.0",
- "windows-implement",
- "windows-interface",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-implement 0.60.0",
+ "windows-interface 0.59.1",
]
[[package]]
@@ -9606,13 +9832,13 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
-version = "0.33.0"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
+checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
- "thiserror 1.0.63",
- "windows 0.58.0",
- "windows-core 0.58.0",
+ "thiserror 2.0.11",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
]
[[package]]
@@ -9710,12 +9936,14 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "window-vibrancy"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8cdd6999298d969289d8078dae02ce798ad23452075985cccba8b6326711ecf"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [
- "cocoa",
- "objc",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
"raw-window-handle",
"windows-sys 0.59.0",
"windows-version",
@@ -9750,6 +9978,28 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -9765,13 +10015,37 @@ version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-result",
- "windows-strings",
+ "windows-implement 0.58.0",
+ "windows-interface 0.58.0",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement 0.60.0",
+ "windows-interface 0.59.1",
+ "windows-link",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link",
+ "windows-threading",
+]
+
[[package]]
name = "windows-implement"
version = "0.58.0"
@@ -9783,6 +10057,17 @@ dependencies = [
"syn 2.0.87",
]
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
[[package]]
name = "windows-interface"
version = "0.58.0"
@@ -9794,14 +10079,41 @@ dependencies = [
"syn 2.0.87",
]
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link",
+]
+
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
- "windows-result",
- "windows-strings",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
"windows-targets 0.52.6",
]
@@ -9814,6 +10126,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-service"
version = "0.7.0"
@@ -9831,10 +10152,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -9871,6 +10201,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -9910,13 +10249,38 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-version"
version = "0.1.1"
@@ -9944,6 +10308,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -9962,6 +10332,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -9980,12 +10356,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -10004,6 +10392,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -10022,6 +10416,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -10040,6 +10440,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -10058,6 +10464,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
[[package]]
name = "winnow"
version = "0.5.40"
@@ -10114,6 +10526,16 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "winsafe"
version = "0.0.19"
@@ -10142,6 +10564,26 @@ dependencies = [
"bitflags 2.8.0",
]
+[[package]]
+name = "wl-clipboard-rs"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb"
+dependencies = [
+ "derive-new",
+ "libc",
+ "log",
+ "nix 0.28.0",
+ "os_pipe",
+ "tempfile",
+ "thiserror 1.0.63",
+ "tree_magic_mini",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "wayland-protocols-wlr",
+]
+
[[package]]
name = "write16"
version = "1.0.0"
@@ -10156,12 +10598,13 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
-version = "0.46.1"
+version = "0.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f8c948dc5f7c23bd93ba03b85b7f679852589bb78e150424d993171e4ef7b73"
+checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
dependencies = [
"base64 0.22.1",
- "block2",
+ "block2 0.6.1",
+ "cookie",
"crossbeam-channel",
"dpi",
"dunce",
@@ -10174,9 +10617,10 @@ dependencies = [
"kuchikiki",
"libc",
"ndk",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.6.1",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
@@ -10185,12 +10629,13 @@ dependencies = [
"sha2",
"soup3",
"tao-macros",
- "thiserror 1.0.63",
+ "thiserror 2.0.11",
+ "url",
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
- "windows 0.58.0",
- "windows-core 0.58.0",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
"windows-version",
"x11-dl",
]
@@ -10301,9 +10746,9 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.7.0"
+version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88232b74ba057a0c85472ec1bae8a17569960be17da2d5e5ad30d5efe7ea6719"
+checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad"
dependencies = [
"async-broadcast",
"async-executor",
@@ -10334,9 +10779,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.7.0"
+version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6969c06899233334676e60da1675740539cf034ee472a6c5b5c54e50a0a554c9"
+checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659"
dependencies = [
"proc-macro-crate 3.2.0",
"proc-macro2",
diff --git a/Cargo.toml b/Cargo.toml
index 40a083326..4ab877171 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,9 @@ members = [
"easytier-contrib/easytier-ffi",
]
default-members = ["easytier", "easytier-web"]
+exclude = [
+ "easytier-contrib/easytier-ohrs", # it needs ohrs sdk
+]
[profile.dev]
panic = "unwind"
diff --git a/EasyTier.code-workspace b/EasyTier.code-workspace
index ac3cdf3a3..ebe44138b 100644
--- a/EasyTier.code-workspace
+++ b/EasyTier.code-workspace
@@ -3,13 +3,29 @@
{
"path": "."
},
+ {
+ "name": "core",
+ "path": "easytier"
+ },
{
"name": "gui",
"path": "easytier-gui"
},
{
- "name": "core",
- "path": "easytier"
+ "name": "web",
+ "path": "easytier-web"
+ },
+ {
+ "name": "ffi",
+ "path": "easytier-contrib/easytier-ffi"
+ },
+ {
+ "name": "magisk",
+ "path": "easytier-contrib/easytier-magisk"
+ },
+ {
+ "name": "openharmony",
+ "path": "easytier-contrib/easytier-ohrs"
},
{
"name": "vpnservice",
@@ -26,5 +42,9 @@
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnSaveMode": "modifications",
+ "editor.formatOnPaste": false,
+ "editor.formatOnType": true,
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index b55fcfc8c..4bc515ccd 100644
--- a/README.md
+++ b/README.md
@@ -11,263 +11,234 @@
[简体中文](/README_CN.md) | [English](/README.md)
-**Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.**
-
-EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
+> ✨ A simple, secure, decentralized virtual private network solution powered by Rust and Tokio
-
-
+
+
-## Features
-
-- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
-- **Safe**: Use WireGuard protocol to encrypt data.
-- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
-- **Cross-platform**: Supports MacOS/Linux/Windows/Android, will support IOS in the future. The executable file is statically linked, making deployment simple.
-- **Networking without public IP**: Supports networking using shared public nodes, refer to [Configuration Guide](#Networking-without-public-IP)
-- **NAT traversal**: Supports UDP-based NAT traversal, able to establish stable connections even in complex network environments.
-- **Subnet Proxy (Point-to-Network)**: Nodes can expose accessible network segments as proxies to the VPN subnet, allowing other nodes to access these subnets through the node.
-- **Smart Routing**: Selects links based on traffic to reduce latency and increase throughput.
-- **TCP Support**: Provides reliable data transmission through concurrent TCP links when UDP is limited, optimizing performance.
-- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
-- **IPv6 Support**: Supports networking using IPv6.
-- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
-- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
+📚 **[Full Documentation](https://easytier.cn/en/)** | 🖥️ **[Web Console](https://easytier.cn/web)** | 📝 **[Download Releases](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[Third Party Tools](https://easytier.cn/en/guide/installation_gui.html#third-party-graphical-interfaces)** | ❤️ **[Sponsor](#sponsor)**
-## Installation
+## Features
-1. **Download the precompiled binary file**
+### Core Features
- Visit the [GitHub Release page](https://github.com/EasyTier/EasyTier/releases) to download the binary file suitable for your operating system. Release includes both command-line programs and GUI programs in the compressed package.
+- 🔒 **Decentralized**: Nodes are equal and independent, no centralized services required
+- 🚀 **Easy to Use**: Multiple operation methods via web, client, and command line
+- 🌍 **Cross-Platform**: Supports Win/MacOS/Linux/FreeBSD/Android and X86/ARM/MIPS architectures
+- 🔐 **Secure**: AES-GCM or WireGuard encryption, prevents man-in-the-middle attacks
-2. **Install via crates.io**
+### Advanced Capabilities
- ```sh
- cargo install easytier
- ```
+- 🔌 **Efficient NAT Traversal**: Supports UDP and IPv6 traversal, works with NAT4-NAT4 networks
+- 🌐 **Subnet Proxy**: Nodes can share subnets for other nodes to access
+- 🔄 **Intelligent Routing**: Latency priority and automatic route selection for best network experience
+- ⚡ **High Performance**: Zero-copy throughout the entire link, supports TCP/UDP/WSS/WG protocols
-3. **Install from source code**
+### Network Optimization
- ```sh
- cargo install --git https://github.com/EasyTier/EasyTier.git easytier
- ```
+- 📊 **UDP Loss Resistance**: KCP/QUIC proxy optimizes latency and bandwidth in high packet loss environments
+- 🔧 **Web Management**: Easy configuration and monitoring through web interface
+- 🛠️ **Zero Config**: Simple deployment with statically linked executables
-4. **Install by Docker Compose**
+## Quick Start
- Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.
+### 📥 Installation
-5. **Install by script (For Linux Only)**
+Choose the installation method that best suits your needs:
- ```sh
- wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
- ```
+```bash
+# 1. Download pre-built binary (Recommended, All platforms supported)
+# Visit https://github.com/EasyTier/EasyTier/releases
- The script supports the following commands and options:
+# 2. Install via cargo (Latest development version)
+cargo install --git https://github.com/EasyTier/EasyTier.git easytier
- Commands:
- - `install`: Install EasyTier
- - `uninstall`: Uninstall EasyTier
- - `update`: Update EasyTier to the latest version
- - `help`: Show help message
+# 3. Install via Docker
+# See https://easytier.cn/en/guide/installation.html#installation-methods
- Options:
- - `--skip-folder-verify`: Skip folder verification during installation
- - `--skip-folder-fix`: Skip automatic folder path fixing
- - `--no-gh-proxy`: Disable GitHub proxy
- - `--gh-proxy`: Set custom GitHub proxy URL (default: https://ghfast.top/)
+# 4. Linux Quick Install
+wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
- Examples:
- ```sh
- # Show help
- bash /tmp/easytier.sh help
+# 5. MacOS via Homebrew
+brew tap brewforge/chinese
+brew install --cask easytier-gui
- # Install with options
- bash /tmp/easytier.sh install --skip-folder-verify
- bash /tmp/easytier.sh install --no-gh-proxy
- bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
+# 6. OpenWrt Luci Web UI
+# Visit https://github.com/EasyTier/luci-app-easytier
- # Update EasyTier
- bash /tmp/easytier.sh update
+# 7. (Optional) Install shell completions:
+easytier-core --gen-autocomplete fish > ~/.config/fish/completions/easytier-core.fish
+easytier-cli gen-autocomplete fish > ~/.config/fish/completions/easytier-cli.fish
- # Uninstall EasyTier
- bash /tmp/easytier.sh uninstall
- ```
+```
-6. **Install by Homebrew (For MacOS Only)**
+### 🚀 Basic Usage
- ```sh
- brew tap brewforge/chinese
- brew install --cask easytier-gui
- ```
+#### Quick Networking with Shared Nodes
-## Quick Start
+EasyTier supports quick networking using shared public nodes. When you don't have a public IP, you can use the free shared nodes provided by the EasyTier community. Nodes will automatically attempt NAT traversal and establish P2P connections. When P2P fails, data will be relayed through shared nodes.
-> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
+The currently deployed shared public node is `tcp://public.easytier.cn:11010`.
-Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
+When using shared nodes, each node entering the network needs to provide the same `--network-name` and `--network-secret` parameters as the unique identifier of the network.
-### Two-node Networking
+Taking two nodes as an example (Please use more complex network name to avoid conflicts):
-Assuming the network topology of the two nodes is as follows
+1. Run on Node A:
-```mermaid
-flowchart LR
+```bash
+# Run with administrator privileges
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```
-subgraph Node A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
-end
+2. Run on Node B:
-subgraph Node B
-nodeb[EasyTier\n10.144.144.2]
-end
+```bash
+# Run with administrator privileges
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```
-nodea <-----> nodeb
+After successful execution, you can check the network status using `easytier-cli`:
+```text
+| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
+| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
+| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.2-70e69a38~ |
+| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.2-70e69a38~ |
+| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.2-70e69a38~ |
```
-1. Execute on Node A:
+You can test connectivity between nodes:
- ```sh
- sudo easytier-core --ipv4 10.144.144.1
- ```
-
- Successful execution of the command will print the following.
+```bash
+# Test connectivity
+ping 10.126.126.1
+ping 10.126.126.2
+```
- 
+Note: If you cannot ping through, it may be that the firewall is blocking incoming traffic. Please turn off the firewall or add allow rules.
-2. Execute on Node B
+To improve availability, you can connect to multiple shared nodes simultaneously:
- ```sh
- sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
- ```
+```bash
+# Connect to multiple shared nodes
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010 -p udp://public.easytier.cn:11010
+```
-3. Test Connectivity
+Once your network is set up successfully, you can easily configure it to start automatically on system boot. Refer to the [One-Click Register Service guide](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) for step-by-step instructions on registering EasyTier as a system service.
- The two nodes should connect successfully and be able to communicate within the virtual subnet
+#### Decentralized Networking
- ```sh
- ping 10.144.144.2
- ```
+EasyTier is fundamentally decentralized, with no distinction between server and client. As long as one device can communicate with any node in the virtual network, it can join the virtual network. Here's how to set up a decentralized network:
- Use easytier-cli to view node information in the subnet
+1. Start First Node (Node A):
- ```sh
- easytier-cli peer
- ```
+```bash
+# Start the first node
+sudo easytier-core -i 10.144.144.1
+```
- 
+After startup, this node will listen on the following ports by default:
+- TCP: 11010
+- UDP: 11010
+- WebSocket: 11011
+- WebSocket SSL: 11012
+- WireGuard: 11013
- ```sh
- easytier-cli route
- ```
+2. Connect Second Node (Node B):
- 
+```bash
+# Connect to the first node using its public IP
+sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
+```
+3. Verify Connection:
- ```sh
- easytier-cli node
- ```
+```bash
+# Test connectivity
+ping 10.144.144.2
- 
+# View connected peers
+easytier-cli peer
----
+# View routing information
+easytier-cli route
-### Multi-node Networking
+# View local node information
+easytier-cli node
+```
-Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
+For more nodes to join the network, they can connect to any existing node in the network using the `-p` parameter:
-```sh
-sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
+```bash
+# Connect to any existing node using its public IP
+sudo easytier-core -i 10.144.144.3 -p udp://ANY_EXISTING_NODE_PUBLIC_IP:11010
```
-The `--peers` parameter can fill in the listening address of any node already in the virtual network.
+### 🔍 Advanced Features
----
+#### Subnet Proxy
-### Subnet Proxy (Point-to-Network) Configuration
-
-Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
+Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes:
```mermaid
flowchart LR
-subgraph Node A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
+subgraph Node A Public IP 22.1.1.1
+nodea[EasyTier 10.144.144.1]
end
subgraph Node B
-nodeb[EasyTier\n10.144.144.2]
+nodeb[EasyTier 10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
-
```
-Then the startup parameters for Node B's easytier are (new -n parameter)
+To share a subnet, add the `-n` parameter when starting EasyTier:
-```sh
-sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
+```bash
+# Share subnet 10.1.1.0/24 with other nodes
+sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
```
-Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. Node A can check whether the subnet proxy is effective through the following command.
-
-1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
-
- ```sh
- easytier-cli route
- ```
-
- 
-
-2. Test whether Node A can access nodes under the proxied subnet
-
- ```sh
- ping 10.1.1.2
- ```
-
----
-
-### Networking without Public IP
-
-EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.cn:11010``.
-
-When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
+Subnet proxy information will automatically sync to each node in the virtual network, and each node will automatically configure the corresponding route. You can verify the subnet proxy setup:
-Taking two nodes as an example, Node A executes:
+1. Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
-```sh
-sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```bash
+# View routing information
+easytier-cli route
```
-Node B executes
+
-```sh
-sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
-```
-
-After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
+2. Test if you can access nodes in the proxied subnet:
-### Use EasyTier with WireGuard Client
+```bash
+# Test connectivity to proxied subnet
+ping 10.1.1.2
+```
-EasyTier can be used as a WireGuard server to allow any device with WireGuard client installed to access the EasyTier network. For platforms currently unsupported by EasyTier (such as iOS, Android, etc.), this method can be used to connect to the EasyTier network.
+#### WireGuard Integration
-Assuming the network topology is as follows:
+EasyTier can act as a WireGuard server, allowing any device with a WireGuard client (including iOS and Android) to access the EasyTier network. Here's an example setup:
```mermaid
flowchart LR
-ios[[iPhone \n WireGuard Installed]]
+ios[[iPhone WireGuard Installed]]
-subgraph Node A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
+subgraph Node A Public IP 22.1.1.1
+nodea[EasyTier 10.144.144.1]
end
subgraph Node B
-nodeb[EasyTier\n10.144.144.2]
+nodeb[EasyTier 10.144.144.2]
end
id1[[10.1.1.0/24]]
@@ -275,86 +246,77 @@ id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
-To enable an iPhone to access the EasyTier network through Node A, the following configuration can be applied:
-
-Include the --vpn-portal parameter in the easytier-core command on Node A to specify the port that the WireGuard service listens on and the subnet used by the WireGuard network.
+1. Start EasyTier with WireGuard portal enabled:
-```sh
-# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
-sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
+```bash
+# Listen on 0.0.0.0:11013 and use 10.14.14.0/24 subnet for WireGuard clients
+sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
-After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
-
-```sh
-$> easytier-cli vpn-portal
-portal_name: wireguard
-
-############### client_config_start ###############
-
-[Interface]
-PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
-Address = 10.14.14.0/32 # should assign an ip from this cidr manually
-
-[Peer]
-PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
-AllowedIPs = 10.144.144.0/24,10.14.14.0/24
-Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
-PersistentKeepalive = 25
+2. Get WireGuard client configuration:
-############### client_config_end ###############
-
-connected_clients:
-[]
+```bash
+# Get WireGuard client configuration
+easytier-cli vpn-portal
```
-Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
-
-### Self-Hosted Public Server
+3. In the output configuration:
+ - Set `Interface.Address` to an available IP from the WireGuard subnet
+ - Set `Peer.Endpoint` to the public IP/domain of your EasyTier node
+ - Import the modified configuration into your WireGuard client
-Every virtual network (with same network name and secret) can act as a public server cluster. Nodes of other network can connect to arbitrary nodes in public server cluster to discover each other without public IP.
+#### Self-Hosted Public Shared Node
-Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
+You can run your own public shared node to help other nodes discover each other. A public shared node is just a regular EasyTier network (with same network name and secret) that other networks can connect to.
-You can also join the official public server cluster with following command:
+To run a public shared node:
+```bash
+# No need to specify IPv4 address for public shared nodes
+sudo easytier-core --network-name mysharednode --network-secret mysharednode
```
-sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
-```
-
-### Configurations
+## Related Projects
-You can use ``easytier-core --help`` to view all configuration items
+- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
+- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
+- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
+- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
-## Roadmap
+### Contact Us
-- [ ] Support features such TCP hole punching, KCP, FEC etc.
-- [ ] Support iOS.
+- 💬 **[Telegram Group](https://t.me/easytier)**
+- 👥 **[QQ Group: 949700262](https://qm.qq.com/cgi-bin/qm/qr?k=kC8YJ6Jb8vWJIDbZrZJB8pB5YZgPJA5-)**
-## Community and Contribution
+## License
-We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
+EasyTier is released under the [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
-## Related Projects and Resources
+## Sponsor
-- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
-- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
-- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
-- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
+CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
-## License
+
+
+
+
+
-EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
+Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
-## Contact
+
+
+
+
+
+
+
+
-- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
-- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
-- Telegram:https://t.me/easytier
-- QQ Group: 949700262
-## Sponsor
+If you find EasyTier helpful, please consider sponsoring us. Software development and maintenance require a lot of time and effort, and your sponsorship will help us better maintain and improve EasyTier.
-
-
+
+
+
+
diff --git a/README_CN.md b/README_CN.md
index 12aba9898..fba6bd68b 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -1,271 +1,243 @@
# EasyTier
+[](https://github.com/EasyTier/EasyTier/releases)
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[](https://github.com/EasyTier/EasyTier/commits/main)
[](https://github.com/EasyTier/EasyTier/issues)
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
+[](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
+[](https://deepwiki.com/EasyTier/EasyTier)
[简体中文](/README_CN.md) | [English](/README.md)
-**请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
-
-一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
+> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化的异地组网方案
-
-
+
+
-## 特点
+📚 **[完整文档](https://easytier.cn)** | 🖥️ **[Web 控制台](https://easytier.cn/web)** | 📝 **[下载发布版本](https://github.com/EasyTier/EasyTier/releases)** | 🧩 **[第三方工具](https://easytier.cn/guide/installation_gui.html#%E7%AC%AC%E4%B8%89%E6%96%B9%E5%9B%BE%E5%BD%A2%E7%95%8C%E9%9D%A2)** | ❤️ **[赞助](#赞助)**
-- **去中心化**:无需依赖中心化服务,节点平等且独立。
-- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
-- **高性能**:全链路零拷贝,性能与主流组网软件相当。
-- **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接,部署简单。
-- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
-- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
-- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
-- **智能路由**:根据流量智能选择链路,减少延迟,提高吞吐量。
-- **TCP 支持**:在 UDP 受限的情况下,通过并发 TCP 链接提供可靠的数据传输,优化性能。
-- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
-- **IPV6 支持**:支持利用 IPV6 组网。
-- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
-- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
+## 特性
-## 安装
+### 核心特性
-1. **下载预编译的二进制文件**
+- 🔒 **去中心化**:节点平等且独立,无需中心化服务
+- 🚀 **易于使用**:支持通过网页、客户端和命令行多种操作方式
+- 🌍 **跨平台**:支持 Win/MacOS/Linux/FreeBSD/Android 和 X86/ARM/MIPS 架构
+- 🔐 **安全**:AES-GCM 或 WireGuard 加密,防止中间人攻击
- 访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
+### 高级功能
-2. **通过 crates.io 安装**
+- 🔌 **高效 NAT 穿透**:支持 UDP 和 IPv6 穿透,可在 NAT4-NAT4 网络中工作
+- 🌐 **子网代理**:节点可以共享子网供其他节点访问
+- 🔄 **智能路由**:延迟优先和自动路由选择,提供最佳网络体验
+- ⚡ **高性能**:整个链路零拷贝,支持 TCP/UDP/WSS/WG 协议
- ```sh
- cargo install easytier
- ```
+### 网络优化
-3. **通过源码安装**
+- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
+- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
+- 🛠️ **零配置**:静态链接的可执行文件,简单部署
- ```sh
- cargo install --git https://github.com/EasyTier/EasyTier.git easytier
- ```
+## 快速开始
-4. **通过Docker Compose安装**
+### 📥 安装
- 请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
+选择最适合您需求的安装方式:
-5. **使用一键脚本安装 (仅适用于 Linux)**
+```bash
+# 1. 下载预编译二进制文件(推荐,支持所有平台)
+# 访问 https://github.com/EasyTier/EasyTier/releases
- ```sh
- wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
- ```
+# 2. 通过 cargo 安装(最新开发版本)
+cargo install --git https://github.com/EasyTier/EasyTier.git easytier
- 脚本支持以下命令和选项:
+# 3. 通过 Docker 安装
+# 参见 https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F
- 命令:
- - `install`: 安装 EasyTier
- - `uninstall`: 卸载 EasyTier
- - `update`: 更新 EasyTier 到最新版本
- - `help`: 显示帮助信息
+# 4. Linux 快速安装
+wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
- 选项:
- - `--skip-folder-verify`: 跳过安装过程中的文件夹验证
- - `--skip-folder-fix`: 跳过自动修复文件夹路径
- - `--no-gh-proxy`: 禁用 GitHub 代理
- - `--gh-proxy`: 设置自定义 GitHub 代理 URL (默认值: https://ghfast.top/)
+# 5. MacOS 通过 Homebrew 安装
+brew tap brewforge/chinese
+brew install --cask easytier-gui
- 示例:
- ```sh
- # 查看帮助
- bash /tmp/easytier.sh help
+# 6. OpenWrt Luci Web 界面
+# 访问 https://github.com/EasyTier/luci-app-easytier
- # 安装(带选项)
- bash /tmp/easytier.sh install --skip-folder-verify
- bash /tmp/easytier.sh install --no-gh-proxy
- bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
+# 7.(可选)安装 Shell 补全功能:
+# Fish 补全
+easytier-core --gen-autocomplete fish > ~/.config/fish/completions/easytier-core.fish
+easytier-cli gen-autocomplete fish > ~/.config/fish/completions/easytier-cli.fish
- # 更新 EasyTier
- bash /tmp/easytier.sh update
+```
- # 卸载 EasyTier
- bash /tmp/easytier.sh uninstall
- ```
+### 🚀 基本用法
-6. **使用 Homebrew 安装 (仅适用于 MacOS)**
+#### 使用共享节点快速组网
- ```sh
- brew tap brewforge/chinese
- brew install --cask easytier-gui
- ```
+EasyTier 支持使用共享公共节点快速组网。当您没有公网 IP 时,可以使用 EasyTier 社区提供的免费共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
-## 快速开始
+当前部署的共享公共节点是 `tcp://public.easytier.cn:11010`。
-> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
+使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name` 和 `--network-secret` 参数作为网络的唯一标识符。
-确保已按照 [安装指南](#安装) 安装 EasyTier,并且 easytier-core 和 easytier-cli 两个命令都已经可用。
+以两个节点为例(请使用更复杂的网络名称以避免冲突):
-### 双节点组网
+1. 在节点 A 上运行:
-假设双节点的网络拓扑如下
-
-```mermaid
-flowchart LR
+```bash
+# 以管理员权限运行
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```
-subgraph 节点 A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
-end
+2. 在节点 B 上运行:
-subgraph 节点 B
-nodeb[EasyTier\n10.144.144.2]
-end
+```bash
+# 以管理员权限运行
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```
-nodea <-----> nodeb
+执行成功后,可以使用 `easytier-cli` 检查网络状态:
+```text
+| ipv4 | hostname | cost | lat_ms | loss_rate | rx_bytes | tx_bytes | tunnel_proto | nat_type | id | version |
+| ------------ | -------------- | ----- | ------ | --------- | -------- | -------- | ------------ | -------- | ---------- | --------------- |
+| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.2-70e69a38~ |
+| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.2-70e69a38~ |
+| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.2-70e69a38~ |
```
-1. 在节点 A 上执行:
-
- ```sh
- sudo easytier-core --ipv4 10.144.144.1
- ```
+您可以测试节点之间的连通性:
- 命令执行成功会有如下打印。
+```bash
+# 测试连通性
+ping 10.126.126.1
+ping 10.126.126.2
+```
- 
+注意:如果无法 ping 通,可能是防火墙阻止了入站流量。请关闭防火墙或添加允许规则。
-2. 在节点 B 执行
+为了提高可用性,您可以同时连接多个共享节点:
- ```sh
- sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
- ```
+```bash
+# 连接多个共享节点
+sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010 -p udp://public.easytier.cn:11010
+```
-3. 测试联通性
+#### 去中心化组网
- 两个节点应成功连接并能够在虚拟子网内通信
+EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
- ```sh
- ping 10.144.144.2
- ```
+1. 启动第一个节点(节点 A):
- 使用 easytier-cli 查看子网中的节点信息
+```bash
+# 启动第一个节点
+sudo easytier-core -i 10.144.144.1
+```
- ```sh
- easytier-cli peer
- ```
+启动后,该节点将默认监听以下端口:
+- TCP:11010
+- UDP:11010
+- WebSocket:11011
+- WebSocket SSL:11012
+- WireGuard:11013
- 
+2. 连接第二个节点(节点 B):
- ```sh
- easytier-cli route
- ```
+```bash
+# 使用第一个节点的公网 IP 连接
+sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010
+```
- 
+3. 验证连接:
- ```sh
- easytier-cli node
- ```
+```bash
+# 测试连通性
+ping 10.144.144.2
- 
+# 查看已连接的对等节点
+easytier-cli peer
----
+# 查看路由信息
+easytier-cli route
-### 多节点组网
+# 查看本地节点信息
+easytier-cli node
+```
-基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
+更多节点要加入网络,可以使用 `-p` 参数连接到网络中的任何现有节点:
-```sh
-sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
+```bash
+# 使用任何现有节点的公网 IP 连接
+sudo easytier-core -i 10.144.144.3 -p udp://任何现有节点的公网IP:11010
```
-其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
-
----
+### 🔍 高级功能
-### 子网代理(点对网)配置
+#### 子网代理
-假设网络拓扑如下,节点 B 想将其可访问的子网 10.1.1.0/24 共享给其他节点。
+假设网络拓扑如下,节点 B 想要与其他节点共享其可访问的子网 10.1.1.0/24:
```mermaid
flowchart LR
-subgraph 节点 A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
+subgraph 节点 A 公网 IP 22.1.1.1
+nodea[EasyTier 10.144.144.1]
end
subgraph 节点 B
-nodeb[EasyTier\n10.144.144.2]
+nodeb[EasyTier 10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
-
```
-则节点 B 的 easytier 启动参数为(新增 -n 参数)
+要共享子网,在启动 EasyTier 时添加 `-n` 参数:
-```sh
-sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
+```bash
+# 与其他节点共享子网 10.1.1.0/24
+sudo easytier-core -i 10.144.144.2 -n 10.1.1.0/24
```
-子网代理信息会自动同步到虚拟网络的每个节点,各个节点会自动配置相应的路由,节点 A 可以通过如下命令检查子网代理是否生效。
-
-1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。
-
- ```sh
- easytier-cli route
- ```
-
- 
-
-2. 测试节点 A 是否可访问被代理子网下的节点
-
- ```sh
- ping 10.1.1.2
- ```
-
----
+子网代理信息将自动同步到虚拟网络中的每个节点,每个节点将自动配置相应的路由。您可以验证子网代理设置:
-### 无公网IP组网
+1. 检查路由信息是否已同步(proxy_cidrs 列显示代理的子网):
-EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
-
-使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
-
-以双节点为例,节点 A 执行:
-
-```sh
-sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
+```bash
+# 查看路由信息
+easytier-cli route
```
-节点 B 执行
+
-```sh
-sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
-```
+2. 测试是否可以访问代理子网中的节点:
-命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
-
----
-
-### 使用 WireGuard 客户端接入
+```bash
+# 测试到代理子网的连通性
+ping 10.1.1.2
+```
-EasyTier 可以用作 WireGuard 服务端,让任意安装了 WireGuard 客户端的设备访问 EasyTier 网络。对于目前 EasyTier 不支持的平台 (如 iOS、Android 等),可以使用这种方式接入 EasyTier 网络。
+#### WireGuard 集成
-假设网络拓扑如下:
+EasyTier 可以作为 WireGuard 服务器,允许任何安装了 WireGuard 客户端的设备(包括 iOS 和 Android)访问 EasyTier 网络。以下是设置示例:
```mermaid
flowchart LR
-ios[[iPhone \n 安装 WireGuard]]
+ios[[iPhone 已安装 WireGuard]]
-subgraph 节点 A IP 22.1.1.1
-nodea[EasyTier\n10.144.144.1]
+subgraph 节点 A 公网 IP 22.1.1.1
+nodea[EasyTier 10.144.144.1]
end
subgraph 节点 B
-nodeb[EasyTier\n10.144.144.2]
+nodeb[EasyTier 10.144.144.2]
end
id1[[10.1.1.0/24]]
@@ -273,88 +245,78 @@ id1[[10.1.1.0/24]]
ios <-.-> nodea <--> nodeb <-.-> id1
```
-我们需要 iPhone 通过节点 A 访问 EasyTier 网络,则可进行如下配置:
+1. 启动启用 WireGuard 门户的 EasyTier:
-在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
-
-```sh
-# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
-sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
+```bash
+# 在 0.0.0.0:11013 上监听,并使用 10.14.14.0/24 子网作为 WireGuard 客户端
+sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
-easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
-
-```sh
-$> easytier-cli vpn-portal
-portal_name: wireguard
-
-############### client_config_start ###############
-
-[Interface]
-PrivateKey = 9VDvlaIC9XHUvRuE06hD2CEDrtGF+0lDthgr9SZfIho=
-Address = 10.14.14.0/32 # should assign an ip from this cidr manually
-
-[Peer]
-PublicKey = zhrZQg4QdPZs8CajT3r4fmzcNsWpBL9ImQCUsnlXyGM=
-AllowedIPs = 10.144.144.0/24,10.14.14.0/24
-Endpoint = 0.0.0.0:11013 # should be the public ip(or domain) of the vpn server
-PersistentKeepalive = 25
-
-############### client_config_end ###############
+2. 获取 WireGuard 客户端配置:
-connected_clients:
-[]
+```bash
+# 获取 WireGuard 客户端配置
+easytier-cli vpn-portal
```
-使用 Client Config 前,需要将 Interface Address 和 Peer Endpoint 分别修改为客户端的 IP 和 EasyTier 节点的 IP。将配置文件导入 WireGuard 客户端,即可访问 EasyTier 网络。
+3. 在输出配置中:
+ - 将 `Interface.Address` 设置为 WireGuard 子网中的可用 IP
+ - 将 `Peer.Endpoint` 设置为您的 EasyTier 节点的公网 IP/域名
+ - 将修改后的配置导入到您的 WireGuard 客户端
----
+#### 自建公共共享节点
-### 自建公共中转服务器
+您可以运行自己的公共共享节点来帮助其他节点相互发现。公共共享节点只是一个普通的 EasyTier 网络(具有相同的网络名称和密钥),其他网络可以连接到它。
-每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此。
-
-运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
-
-也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
+要运行公共共享节点:
-```
-sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
+```bash
+# 公共共享节点无需指定 IPv4 地址
+sudo easytier-core --network-name mysharednode --network-secret mysharednode
```
-### 其他配置
+网络设置成功后,您可以轻松配置它以在系统启动时自动启动。请参阅 [一键注册服务指南](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) 了解如何将 EasyTier 注册为系统服务。
-可使用 ``easytier-core --help`` 查看全部配置项
+## 相关项目
-## 路线图
+- [ZeroTier](https://www.zerotier.com/):用于连接设备的全球虚拟网络。
+- [TailScale](https://tailscale.com/):旨在简化网络配置的 VPN 解决方案。
+- [vpncloud](https://github.com/dswd/vpncloud):一个 P2P 网状 VPN
+- [Candy](https://github.com/lanthora/candy):一个可靠、低延迟、反审查的虚拟专用网络
-- [ ] 完善文档和用户指南。
-- [ ] 支持 TCP 打洞、KCP、FEC 等特性。
-- [ ] 支持 iOS。
+### 联系我们
-## 社区和贡献
+- 💬 **[Telegram 群组](https://t.me/easytier)**
+- 👥 **[QQ 群:949700262](https://qm.qq.com/cgi-bin/qm/qr?k=kC8YJ6Jb8vWJIDbZrZJB8pB5YZgPJA5-)**
-我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
+## 许可证
-## 相关项目和资源
+EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可下发布。
-- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
-- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
-- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
-- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
+## 赞助
-## 许可证
+本项目的 CDN 加速和安全防护由腾讯云 EdgeOne 赞助。
-EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
+
+
+
+
+
-## 联系方式
+特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。
-- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
-- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
-- QQ 群: 949700262
-- Telegram:https://t.me/easytier
+
+
+
+
+
+
+
+
-## 赞助
+如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。
-
-
+
+
+
+
diff --git a/assets/alipay.png b/assets/alipay.png
new file mode 100644
index 000000000..3661eb5aa
Binary files /dev/null and b/assets/alipay.png differ
diff --git a/assets/config-page.png b/assets/config-page.png
new file mode 100644
index 000000000..010a46444
Binary files /dev/null and b/assets/config-page.png differ
diff --git a/assets/edgeone.png b/assets/edgeone.png
new file mode 100644
index 000000000..a5b2a8183
Binary files /dev/null and b/assets/edgeone.png differ
diff --git a/assets/langlang.png b/assets/langlang.png
new file mode 100644
index 000000000..6fc0bbb6f
Binary files /dev/null and b/assets/langlang.png differ
diff --git a/assets/raincloud.png b/assets/raincloud.png
new file mode 100644
index 000000000..d5f2a062f
Binary files /dev/null and b/assets/raincloud.png differ
diff --git a/assets/running-page.png b/assets/running-page.png
new file mode 100644
index 000000000..5f1cc1555
Binary files /dev/null and b/assets/running-page.png differ
diff --git a/assets/wechat.png b/assets/wechat.png
new file mode 100644
index 000000000..9183f1e48
Binary files /dev/null and b/assets/wechat.png differ
diff --git a/easytier-contrib/easytier-ffi/src/lib.rs b/easytier-contrib/easytier-ffi/src/lib.rs
index 4e2f66cad..1c2e5abd3 100644
--- a/easytier-contrib/easytier-ffi/src/lib.rs
+++ b/easytier-contrib/easytier-ffi/src/lib.rs
@@ -29,8 +29,39 @@ fn set_error_msg(msg: &str) {
msg_buf[..len].copy_from_slice(bytes);
}
+/// # Safety
+/// Set the tun fd
#[no_mangle]
-pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
+pub unsafe extern "C" fn set_tun_fd(
+ inst_name: *const std::ffi::c_char,
+ fd: std::ffi::c_int,
+) -> std::ffi::c_int {
+ let inst_name = unsafe {
+ assert!(!inst_name.is_null());
+ std::ffi::CStr::from_ptr(inst_name)
+ .to_string_lossy()
+ .into_owned()
+ };
+ if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
+ return -1;
+ }
+
+ let inst_id = *INSTANCE_NAME_ID_MAP
+ .get(&inst_name)
+ .as_ref()
+ .unwrap()
+ .value();
+
+ match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
+ Ok(_) => 0,
+ Err(_) => -1,
+ }
+}
+
+/// # Safety
+/// Get the last error message
+#[no_mangle]
+pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
let msg_buf = ERROR_MSG.lock().unwrap();
if msg_buf.is_empty() {
unsafe {
@@ -54,8 +85,10 @@ pub extern "C" fn free_string(s: *const std::ffi::c_char) {
}
}
+/// # Safety
+/// Parse the config
#[no_mangle]
-pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
+pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
@@ -71,8 +104,10 @@ pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_
0
}
+/// # Safety
+/// Run the network instance
#[no_mangle]
-pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
+pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
@@ -107,8 +142,10 @@ pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std:
0
}
+/// # Safety
+/// Retain the network instance
#[no_mangle]
-pub extern "C" fn retain_network_instance(
+pub unsafe extern "C" fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
@@ -144,13 +181,15 @@ pub extern "C" fn retain_network_instance(
return -1;
}
- let _ = INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
+ INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
0
}
+/// # Safety
+/// Collect the network infos
#[no_mangle]
-pub extern "C" fn collect_network_infos(
+pub unsafe extern "C" fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> std::ffi::c_int {
@@ -209,7 +248,9 @@ mod tests {
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
- assert_eq!(parse_config(cstr.as_ptr()), 0);
+ unsafe {
+ assert_eq!(parse_config(cstr.as_ptr()), 0);
+ }
}
#[test]
@@ -219,6 +260,8 @@ mod tests {
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
- assert_eq!(run_network_instance(cstr.as_ptr()), 0);
+ unsafe {
+ assert_eq!(run_network_instance(cstr.as_ptr()), 0);
+ }
}
}
diff --git a/easytier-contrib/easytier-magisk/module.prop b/easytier-contrib/easytier-magisk/module.prop
index 6316f4e6c..c0e822ab5 100644
--- a/easytier-contrib/easytier-magisk/module.prop
+++ b/easytier-contrib/easytier-magisk/module.prop
@@ -1,6 +1,6 @@
id=easytier_magisk
name=EasyTier_Magisk
-version=v2.3.2
+version=v2.4.2
versionCode=1
author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
diff --git a/easytier-contrib/easytier-ohrs/Cargo.lock b/easytier-contrib/easytier-ohrs/Cargo.lock
new file mode 100644
index 000000000..821de2203
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/Cargo.lock
@@ -0,0 +1,5778 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "arc-swap"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
+
+[[package]]
+name = "async-event"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1222afd3d2bce3995035054046a279ae7aa154d70d0766cea050073f3fd7ddf"
+dependencies = [
+ "loom 0.5.6",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "async-ringbuf"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2495ca646b600f2fb09278bdf28dd2227ad45cab155cd7a25d4fd2b7002952"
+dependencies = [
+ "futures-util",
+ "ringbuf",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "atomic-shim"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "auto_impl"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base62"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags 2.9.1",
+ "cexpr",
+ "clang-sys",
+ "itertools 0.13.0",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "boringtun-easytier"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f09b4d1ada8affba260cb185bbdf6d5acff42f924dea1a17f938cf3e8fbe475"
+dependencies = [
+ "aead",
+ "atomic-shim",
+ "base64 0.13.1",
+ "blake2",
+ "chacha20poly1305",
+ "hex",
+ "hmac",
+ "ip_network",
+ "ip_network_table",
+ "libc",
+ "nix 0.25.1",
+ "parking_lot",
+ "rand_core 0.6.4",
+ "ring",
+ "tracing",
+ "untrusted",
+ "x25519-dalek",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytecodec"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325"
+dependencies = [
+ "byteorder",
+ "trackable 0.2.24",
+]
+
+[[package]]
+name = "bytecount"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
+
+[[package]]
+name = "bytemuck"
+version = "1.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "bzip2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
+dependencies = [
+ "bzip2-sys",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.13+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "c2rust-bitfields"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41"
+dependencies = [
+ "c2rust-bitfields-derive",
+]
+
+[[package]]
+name = "c2rust-bitfields-derive"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3cbc102e2597c9744c8bd8c15915d554300601c91a079430d309816b0912545"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c"
+dependencies = [
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead",
+ "chacha20",
+ "cipher",
+ "poly1305",
+ "zeroize",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cidr"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdf600c45bd958cf2945c445264471cca8b6c8e67bc87b71affd6d7e5682621"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+ "unicase",
+ "unicode-width 0.2.1",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "codepage"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4"
+dependencies = [
+ "encoding_rs",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
+
+[[package]]
+name = "convert_case"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "crossbeam"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
+dependencies = [
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "dbus"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
+
+[[package]]
+name = "defmt"
+version = "0.3.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
+dependencies = [
+ "defmt 1.0.1",
+]
+
+[[package]]
+name = "defmt"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e"
+dependencies = [
+ "defmt-parser",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.12",
+]
+
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "diatomic-waker"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28025fb55a9d815acf7b0877555f437254f373036eec6ed265116c7a5c0825e9"
+dependencies = [
+ "loom 0.5.6",
+ "waker-fn",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "easytier"
+version = "2.4.2"
+source = "git+https://github.com/EasyTier/EasyTier.git#a4bb555fac1046d0099c44676fa9d0d8cca55c99"
+dependencies = [
+ "anyhow",
+ "async-recursion",
+ "async-ringbuf",
+ "async-stream",
+ "async-trait",
+ "atomic-shim",
+ "auto_impl",
+ "base64 0.22.1",
+ "bitflags 2.9.1",
+ "boringtun-easytier",
+ "bytecodec",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "cidr",
+ "clap",
+ "crossbeam",
+ "dashmap",
+ "dbus",
+ "derive_builder",
+ "easytier-rpc-build",
+ "encoding",
+ "futures",
+ "gethostname",
+ "git-version",
+ "globwalk",
+ "hashbrown 0.15.4",
+ "hickory-client",
+ "hickory-proto",
+ "hickory-resolver",
+ "hickory-server",
+ "http",
+ "http_req",
+ "humansize",
+ "humantime-serde",
+ "kcp-sys",
+ "machine-uid",
+ "mimalloc",
+ "multimap",
+ "netlink-packet-core",
+ "netlink-packet-route",
+ "netlink-packet-utils",
+ "netlink-sys",
+ "network-interface",
+ "nix 0.29.0",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "petgraph 0.8.2",
+ "pin-project-lite",
+ "pnet",
+ "prost",
+ "prost-build",
+ "prost-reflect",
+ "prost-reflect-build",
+ "prost-types",
+ "quinn",
+ "rand 0.8.5",
+ "rcgen",
+ "regex",
+ "reqwest",
+ "resolv-conf",
+ "ring",
+ "ringbuf",
+ "rust-i18n",
+ "rustls",
+ "serde",
+ "serde_json",
+ "service-manager",
+ "smoltcp",
+ "socket2",
+ "stun_codec",
+ "sys-locale",
+ "tabled",
+ "tachyonix",
+ "thiserror 1.0.69",
+ "thunk-rs",
+ "time",
+ "timedmap",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tokio-util",
+ "tokio-websockets",
+ "toml",
+ "tonic-build",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "tun-easytier",
+ "url",
+ "uuid",
+ "version-compare",
+ "which 7.0.3",
+ "wildmatch",
+ "winapi",
+ "windows 0.52.0",
+ "windows-service",
+ "windows-sys 0.52.0",
+ "winreg 0.52.0",
+ "zerocopy 0.7.35",
+ "zip",
+ "zstd",
+]
+
+[[package]]
+name = "easytier-ohrs"
+version = "0.1.0"
+dependencies = [
+ "easytier",
+ "napi-build-ohos",
+ "napi-derive-ohos",
+ "napi-ohos",
+ "ohos-hilog-binding",
+ "once_cell",
+ "serde_json",
+ "tracing",
+ "tracing-core",
+ "tracing-subscriber",
+ "uuid",
+]
+
+[[package]]
+name = "easytier-rpc-build"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24829168c28f6a448f57d18116c255dcbd2b8c25e76dbc60f6cd16d68ad2cf07"
+dependencies = [
+ "heck 0.5.0",
+ "prost-build",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
+dependencies = [
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
+]
+
+[[package]]
+name = "encoding-index-japanese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-korean"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-simpchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-singlebyte"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-index-tradchinese"
+version = "1.20141219.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
+dependencies = [
+ "encoding_index_tests",
+]
+
+[[package]]
+name = "encoding-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87b881ab2524b96a5ce932056c7482ba6152e2226fed3936b3e592adeb95ca6d"
+dependencies = [
+ "codepage",
+ "encoding_rs",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "encoding_index_tests"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "env_home"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "fastbloom"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8"
+dependencies = [
+ "getrandom 0.3.3",
+ "rand 0.9.1",
+ "siphasher",
+ "wide",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "libz-rs-sys",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.48.0",
+]
+
+[[package]]
+name = "generator"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "gethostname"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
+dependencies = [
+ "rustix 0.38.44",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "git-version"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19"
+dependencies = [
+ "git-version-macro",
+]
+
+[[package]]
+name = "git-version-macro"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "globset"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "heapless"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hickory-client"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c466cd63a4217d5b2b8e32f23f58312741ce96e3c84bf7438677d2baff0fc555"
+dependencies = [
+ "cfg-if",
+ "data-encoding",
+ "futures-channel",
+ "futures-util",
+ "hickory-proto",
+ "once_cell",
+ "radix_trie",
+ "rand 0.9.1",
+ "thiserror 2.0.12",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "hickory-proto"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
+dependencies = [
+ "async-trait",
+ "cfg-if",
+ "data-encoding",
+ "enum-as-inner",
+ "futures-channel",
+ "futures-io",
+ "futures-util",
+ "idna",
+ "ipnet",
+ "once_cell",
+ "rand 0.9.1",
+ "ring",
+ "serde",
+ "thiserror 2.0.12",
+ "tinyvec",
+ "tokio",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "hickory-resolver"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "hickory-proto",
+ "ipconfig",
+ "moka",
+ "once_cell",
+ "parking_lot",
+ "rand 0.9.1",
+ "resolv-conf",
+ "serde",
+ "smallvec",
+ "thiserror 2.0.12",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "hickory-server"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53e5fe811b941c74ee46b8818228bfd2bc2688ba276a0eaeb0f2c95ea3b2585"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "cfg-if",
+ "data-encoding",
+ "enum-as-inner",
+ "futures-util",
+ "hickory-proto",
+ "hickory-resolver",
+ "ipnet",
+ "prefix-trie",
+ "serde",
+ "thiserror 2.0.12",
+ "time",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http_req"
+version = "0.13.1"
+source = "git+https://github.com/EasyTier/http_req.git#b10aa9fc0db3067cc3d2174683a87250b80a1ea9"
+dependencies = [
+ "base64 0.22.1",
+ "rand 0.8.5",
+ "rustls",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "unicase",
+ "webpki",
+ "webpki-roots 0.26.11",
+ "zeroize",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "humantime"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
+
+[[package]]
+name = "humantime-serde"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c"
+dependencies = [
+ "humantime",
+ "serde",
+]
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata 0.4.9",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.4",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "ip_network"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1"
+
+[[package]]
+name = "ip_network_table"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0"
+dependencies = [
+ "ip_network",
+ "ip_network_table-deps-treebitmap",
+]
+
+[[package]]
+name = "ip_network_table-deps-treebitmap"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d"
+
+[[package]]
+name = "ipconfig"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
+dependencies = [
+ "socket2",
+ "widestring",
+ "windows-sys 0.48.0",
+ "winreg 0.50.0",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "ipnetwork"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "jobserver"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kcp-sys"
+version = "0.1.0"
+source = "git+https://github.com/EasyTier/kcp-sys#0f0a0558391ba391c089806c23f369651f6c9eeb"
+dependencies = [
+ "anyhow",
+ "auto_impl",
+ "bindgen",
+ "bitflags 2.9.1",
+ "bytes",
+ "cc",
+ "dashmap",
+ "parking_lot",
+ "rand 0.8.5",
+ "thiserror 2.0.12",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+ "zerocopy 0.7.35",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.174"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "liblzma"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0791ab7e08ccc8e0ce893f6906eb2703ed8739d8e89b57c0714e71bad09024c8"
+dependencies = [
+ "liblzma-sys",
+]
+
+[[package]]
+name = "liblzma-sys"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+]
+
+[[package]]
+name = "libz-rs-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
+dependencies = [
+ "zlib-rs",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if",
+ "generator 0.7.5",
+ "scoped-tls",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "loom"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
+dependencies = [
+ "cfg-if",
+ "generator 0.8.5",
+ "scoped-tls",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "machine-uid"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892"
+dependencies = [
+ "libc",
+ "winreg 0.52.0",
+]
+
+[[package]]
+name = "managed"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d"
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "memchr"
+version = "2.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mimalloc"
+version = "0.1.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40"
+dependencies = [
+ "libmimalloc-sys",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "moka"
+version = "0.12.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "loom 0.7.2",
+ "parking_lot",
+ "portable-atomic",
+ "rustc_version",
+ "smallvec",
+ "tagptr",
+ "thiserror 1.0.69",
+ "uuid",
+]
+
+[[package]]
+name = "multimap"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "napi-build-ohos"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ad5bf214216afe5b572da0bcd5cab932d17cbcca3dbe82991db0d765a764c8a"
+
+[[package]]
+name = "napi-derive-backend-ohos"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd974d6316c670078fa15276c6134e5b45142b393db350b24682ae613733cdac"
+dependencies = [
+ "convert_case",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "napi-derive-ohos"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3a8b89bbc39f81c472e76813dcd837f311aae7850a24a01d0bf5858221b1fd2"
+dependencies = [
+ "convert_case",
+ "napi-derive-backend-ohos",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "napi-ohos"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32036ede4ef064610304337831e9d49dac23e7edc4e9efd076c8259eab6d19a9"
+dependencies = [
+ "bitflags 2.9.1",
+ "chrono",
+ "ctor",
+ "encoding_rs",
+ "futures-core",
+ "indexmap",
+ "napi-sys-ohos",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "napi-sys-ohos"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e18642400316f886a6f153b2fbc48f5652d0e117803057005f89f0e48217d64"
+dependencies = [
+ "libloading",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework 2.11.1",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "netlink-packet-core"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "netlink-packet-utils",
+]
+
+[[package]]
+name = "netlink-packet-route"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e"
+dependencies = [
+ "anyhow",
+ "bitflags 2.9.1",
+ "byteorder",
+ "libc",
+ "log",
+ "netlink-packet-core",
+ "netlink-packet-utils",
+]
+
+[[package]]
+name = "netlink-packet-utils"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "paste",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "netlink-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
+dependencies = [
+ "bytes",
+ "libc",
+ "log",
+]
+
+[[package]]
+name = "network-interface"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3329f515506e4a2de3aa6e07027a6758e22e0f0e8eaf64fa47261cec2282602"
+dependencies = [
+ "cc",
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "no-std-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "normpath"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ohos-hilog-binding"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f360d22e965a34286283d36e8864fdfb04f443697641e8f6cbd64e670c3a3d5"
+dependencies = [
+ "libc",
+ "ohos-hilogs-sys",
+]
+
+[[package]]
+name = "ohos-hilogs-sys"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed07615005d0f8d7bcf901f89c8ff4870666a9bdb00382f588af383f40c160b7"
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+dependencies = [
+ "critical-section",
+ "portable-atomic",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "papergrid"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725"
+dependencies = [
+ "bytecount",
+ "fnv",
+ "unicode-width 0.1.11",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "pem"
+version = "3.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "petgraph"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "petgraph"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca"
+dependencies = [
+ "fixedbitset",
+ "hashbrown 0.15.4",
+ "indexmap",
+ "serde",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plist"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "pnet"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d"
+dependencies = [
+ "ipnetwork",
+ "pnet_base",
+ "pnet_datalink",
+ "pnet_packet",
+ "pnet_sys",
+ "pnet_transport",
+]
+
+[[package]]
+name = "pnet_base"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
+dependencies = [
+ "no-std-net",
+ "serde",
+]
+
+[[package]]
+name = "pnet_datalink"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7"
+dependencies = [
+ "ipnetwork",
+ "libc",
+ "pnet_base",
+ "pnet_sys",
+ "serde",
+ "winapi",
+]
+
+[[package]]
+name = "pnet_macros"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "pnet_macros_support"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab"
+dependencies = [
+ "pnet_base",
+]
+
+[[package]]
+name = "pnet_packet"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f"
+dependencies = [
+ "glob",
+ "pnet_base",
+ "pnet_macros",
+ "pnet_macros_support",
+]
+
+[[package]]
+name = "pnet_sys"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "pnet_transport"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf"
+dependencies = [
+ "libc",
+ "pnet_base",
+ "pnet_packet",
+ "pnet_sys",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy 0.8.26",
+]
+
+[[package]]
+name = "prefix-trie"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cf4c7c25f1dd66c76b451e9041a8cfce26e4ca754934fa7aed8d5a59a01d20"
+dependencies = [
+ "ipnet",
+ "num-traits",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
+dependencies = [
+ "heck 0.5.0",
+ "itertools 0.14.0",
+ "log",
+ "multimap",
+ "once_cell",
+ "petgraph 0.7.1",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 2.0.104",
+ "tempfile",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "prost-reflect"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2"
+dependencies = [
+ "once_cell",
+ "prost",
+ "prost-reflect-derive",
+ "prost-types",
+]
+
+[[package]]
+name = "prost-reflect-build"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123"
+dependencies = [
+ "prost-build",
+ "prost-reflect",
+]
+
+[[package]]
+name = "prost-reflect-derive"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.12",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
+dependencies = [
+ "bytes",
+ "fastbloom",
+ "getrandom 0.3.3",
+ "lru-slab",
+ "rand 0.9.1",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "slab",
+ "thiserror 2.0.12",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "rcgen"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48406db8ac1f3cbc7dcdb56ec355343817958a356ff430259bb07baf7607e1e1"
+dependencies = [
+ "pem",
+ "ring",
+ "time",
+ "yasna",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "reqwest"
+version = "0.12.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "resolv-conf"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ringbuf"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
+dependencies = [
+ "crossbeam-utils",
+ "portable-atomic",
+ "portable-atomic-util",
+]
+
+[[package]]
+name = "rust-i18n"
+version = "3.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
+dependencies = [
+ "globwalk",
+ "once_cell",
+ "regex",
+ "rust-i18n-macro",
+ "rust-i18n-support",
+ "smallvec",
+]
+
+[[package]]
+name = "rust-i18n-macro"
+version = "3.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
+dependencies = [
+ "glob",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "rust-i18n-support",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "rust-i18n-support"
+version = "3.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
+dependencies = [
+ "arc-swap",
+ "base62",
+ "globwalk",
+ "itertools 0.11.0",
+ "lazy_static",
+ "normpath",
+ "once_cell",
+ "proc-macro2",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "siphasher",
+ "toml",
+ "triomphe",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags 2.9.1",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 3.2.0",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework 3.2.0",
+ "security-framework-sys",
+ "webpki-root-certs 0.26.11",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "safe_arch"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "service-manager"
+version = "0.8.0"
+source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b"
+dependencies = [
+ "cfg-if",
+ "dirs",
+ "encoding-utils",
+ "encoding_rs",
+ "plist",
+ "which 4.4.2",
+ "xml-rs",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smoltcp"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb"
+dependencies = [
+ "bitflags 1.3.2",
+ "byteorder",
+ "cfg-if",
+ "defmt 0.3.100",
+ "heapless",
+ "managed",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "stun_codec"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b"
+dependencies = [
+ "bytecodec",
+ "byteorder",
+ "crc",
+ "hmac",
+ "md5",
+ "sha1",
+ "trackable 1.3.0",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tabled"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b"
+dependencies = [
+ "papergrid",
+ "tabled_derive",
+]
+
+[[package]]
+name = "tabled_derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tachyonix"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86c3eafa053bbcc63bb4bfc5eb26362a33ea0bc2e589f28bce00287d1c167d45"
+dependencies = [
+ "async-event",
+ "crossbeam-utils",
+ "diatomic-waker",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix 1.0.7",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
+dependencies = [
+ "rustix 1.0.7",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "thunk-rs"
+version = "0.3.4"
+source = "git+https://github.com/easytier/thunk.git#403f0d26d3d5bcfdfd76c23e36e517f19fe891e0"
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "timedmap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "825f6c8a18bc36d56a62f66af7296385b628c9c5543a8663d4c217fc920bfefd"
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.46.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-websockets"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "fastrand",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "httparse",
+ "ring",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "webpki-roots 0.26.11",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tonic-build"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "prost-types",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags 2.9.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
+dependencies = [
+ "crossbeam-channel",
+ "thiserror 1.0.69",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "time",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "trackable"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32"
+dependencies = [
+ "trackable 1.3.0",
+ "trackable_derive",
+]
+
+[[package]]
+name = "trackable"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae"
+dependencies = [
+ "trackable_derive",
+]
+
+[[package]]
+name = "trackable_derive"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "triomphe"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85"
+dependencies = [
+ "arc-swap",
+ "serde",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tun-easytier"
+version = "1.1.1"
+source = "git+https://github.com/EasyTier/rust-tun#12378839e7985283df0e4fb536b7137230356db5"
+dependencies = [
+ "bytes",
+ "cfg-if",
+ "futures-core",
+ "ipnet",
+ "libc",
+ "libloading",
+ "log",
+ "nix 0.29.0",
+ "thiserror 1.0.69",
+ "tokio",
+ "tokio-util",
+ "windows-sys 0.59.0",
+ "wintun",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "rand 0.9.1",
+ "serde",
+ "uuid-macro-internal",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "uuid-macro-internal"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "waker-fn"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
+dependencies = [
+ "webpki-root-certs 1.0.1",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.26.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
+dependencies = [
+ "webpki-roots 1.0.1",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "which"
+version = "7.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
+dependencies = [
+ "either",
+ "env_home",
+ "rustix 1.0.7",
+ "winsafe",
+]
+
+[[package]]
+name = "wide"
+version = "0.7.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "widestring"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
+
+[[package]]
+name = "wildmatch"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
+dependencies = [
+ "windows-core 0.52.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-service"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a"
+dependencies = [
+ "bitflags 2.9.1",
+ "widestring",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "winsafe"
+version = "0.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
+[[package]]
+name = "wintun"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae"
+dependencies = [
+ "c2rust-bitfields",
+ "libloading",
+ "log",
+ "thiserror 1.0.69",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "x25519-dalek"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+dependencies = [
+ "curve25519-dalek",
+ "rand_core 0.6.4",
+ "serde",
+ "zeroize",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda"
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive 0.7.35",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+dependencies = [
+ "zerocopy-derive 0.8.26",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "zip"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899"
+dependencies = [
+ "aes",
+ "arbitrary",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "deflate64",
+ "flate2",
+ "getrandom 0.3.3",
+ "hmac",
+ "indexmap",
+ "liblzma",
+ "memchr",
+ "pbkdf2",
+ "sha1",
+ "time",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
+
+[[package]]
+name = "zopfli"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/easytier-contrib/easytier-ohrs/Cargo.toml b/easytier-contrib/easytier-ohrs/Cargo.toml
new file mode 100644
index 000000000..45476f56d
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "easytier-ohrs"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+crate-type=["cdylib"]
+
+[dependencies]
+ohos-hilog-binding = {version = "*", features = ["redirect"]}
+easytier = { git = "https://github.com/EasyTier/EasyTier.git" }
+napi-derive-ohos = "1.0.4"
+napi-ohos = { version = "1.0.4", default-features = false, features = [
+ "serde-json",
+ "latin1",
+ "chrono_date",
+ "object_indexmap",
+ "tokio",
+ "async",
+ "tokio_rt",
+ "tokio_macros",
+ "tokio_io_util",
+ "deferred_trace",
+ "napi8",
+ "node_version_detect",
+ "web_stream",
+] }
+once_cell = "1.21.3"
+serde_json = "1.0.125"
+tracing-subscriber = "0.3.19"
+tracing-core = "0.1.33"
+tracing = "0.1.41"
+uuid = { version = "1.17.0", features = ["v4"] }
+
+[build-dependencies]
+napi-build-ohos = "1.0.4"
+[profile.dev]
+panic = "unwind"
+debug = true
+
+[profile.release]
+panic = "abort"
+lto = true
+codegen-units = 1
+opt-level = 3
+strip = true
diff --git a/easytier-contrib/easytier-ohrs/README.md b/easytier-contrib/easytier-ohrs/README.md
new file mode 100644
index 000000000..caeddc532
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/README.md
@@ -0,0 +1,65 @@
+# OpenHarmonyOS 项目构建说明
+
+本项目需要 OpenHarmonyOS SDK 和多个基础库支持才能成功编译。请按照以下步骤准备构建环境。
+如存在任何编译问题,请前往[Easytier for OHOS](https://github.com/FrankHan052176/EasyTier)
+
+## 前置要求
+
+### 1. 安装 OpenHarmonyOS SDK
+
+**SDK 下载链接**:
+[OpenHarmony 每日构建版本](https://ci.openharmony.cn/workbench/cicd/dailybuild/dailylist)
+
+**版本要求**:
+请选择版本号 **小于 OpenHarmony_5.1.0.58** 的 ohos-sdk-full 版本
+
+下载后请解压到适当位置(如 `/usr/local/ohos-sdk`),并记下安装路径。
+
+### 2. 编译依赖库
+在编译本项目前,需要先自行编译以下四个基础库:
+
+- glib
+- libffi
+- pcre2
+- zlib
+
+这些库需要使用 OpenHarmonyOS 的工具链进行交叉编译。
+
+## 环境配置
+
+### 1. 设置环境变量
+创建并运行以下脚本设置环境变量(请根据您的实际 SDK 安装路径修改):
+
+```bash
+#!/bin/bash
+# 请修改为您的实际 SDK 路径
+export OHOS_SDK_PATH="/usr/local/ohos-sdk/linux"
+export OHOS_TOOLCHAIN_DIR="${OHOS_SDK_PATH}/native/llvm"
+export TARGET_ARCH="aarch64-linux-ohos"
+export OHOS_SYSROOT="${OHOS_SDK_PATH}/native/sysroot"
+export CC="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang"
+export CXX="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang++"
+export AS="${OHOS_TOOLCHAIN_DIR}/bin/llvm-as"
+export AR="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ar"
+export LD="${OHOS_TOOLCHAIN_DIR}/bin/ld.lld"
+export RANLIB="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ranlib"
+export STRIP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-strip"
+export OBJDUMP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objdump"
+export OBJCOPY="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objcopy"
+export NM="${OHOS_TOOLCHAIN_DIR}/bin/llvm-nm"
+export CFLAGS="-fPIC -D__MUSL__=1 -march=armv8-a --target=${TARGET_ARCH} -Wno-error --sysroot=${OHOS_SYSROOT} -I${OHOS_SYSROOT}/usr/include/${TARGET_ARCH}"
+export CXXFLAGS="${CFLAGS}"
+export LDFLAGS="--sysroot=${OHOS_SYSROOT} -L${OHOS_SYSROOT}/usr/lib/${TARGET_ARCH} -fuse-ld=${LD}"
+export PKG_CONFIG_PATH="${OHOS_SYSROOT}/usr/lib/pkgconfig:${OHOS_SYSROOT}/usr/local/lib/pkgconfig"
+export PKG_CONFIG_LIBDIR="${OHOS_SYSROOT}/usr/lib:${OHOS_SYSROOT}/usr/local/lib"
+export PKG_CONFIG_SYSROOT_DIR="${OHOS_SYSROOT}"
+export HOST_TRIPLET="${TARGET_ARCH}"
+export BUILD_TRIPLET="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)"
+export PATH="${OHOS_TOOLCHAIN_DIR}/bin:${PATH}"
+
+echo "OpenHarmonyOS 环境变量已设置:"
+echo "OHOS_SDK_PATH: ${OHOS_SDK_PATH}"
+echo "OHOS_TOOLCHAIN_DIR: ${OHOS_TOOLCHAIN_DIR}"
+echo "OHOS_SYSROOT: ${OHOS_SYSROOT}"
+echo "PKG_CONFIG_PATH: ${PKG_CONFIG_PATH}"
+echo "PATH: ${PATH}"
diff --git a/easytier-contrib/easytier-ohrs/build.rs b/easytier-contrib/easytier-ohrs/build.rs
new file mode 100644
index 000000000..1320ceb28
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/build.rs
@@ -0,0 +1,3 @@
+fn main () {
+ napi_build_ohos::setup();
+}
\ No newline at end of file
diff --git a/easytier-contrib/easytier-ohrs/env.sh b/easytier-contrib/easytier-ohrs/env.sh
new file mode 100644
index 000000000..1736daadf
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/env.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+# 请修改为您的实际 SDK 路径
+export OHOS_TOOLCHAIN_DIR="${OHOS_NDK_HOME}/native/llvm"
+export TARGET_ARCH="aarch64-linux-ohos"
+export OHOS_SYSROOT="${OHOS_NDK_HOME}/native/sysroot"
+export CC="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang"
+export CXX="${OHOS_TOOLCHAIN_DIR}/bin/aarch64-unknown-linux-ohos-clang++"
+export AS="${OHOS_TOOLCHAIN_DIR}/bin/llvm-as"
+export AR="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ar"
+export LD="${OHOS_TOOLCHAIN_DIR}/bin/ld.lld"
+export RANLIB="${OHOS_TOOLCHAIN_DIR}/bin/llvm-ranlib"
+export STRIP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-strip"
+export OBJDUMP="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objdump"
+export OBJCOPY="${OHOS_TOOLCHAIN_DIR}/bin/llvm-objcopy"
+export NM="${OHOS_TOOLCHAIN_DIR}/bin/llvm-nm"
+export CFLAGS="-fPIC -D__MUSL__=1 -march=armv8-a --target=${TARGET_ARCH} -Wno-error --sysroot=${OHOS_SYSROOT} -I${OHOS_SYSROOT}/usr/include/${TARGET_ARCH}"
+export CXXFLAGS="${CFLAGS}"
+export LDFLAGS="--sysroot=${OHOS_SYSROOT} -L${OHOS_SYSROOT}/usr/lib/${TARGET_ARCH} -fuse-ld=${LD}"
+export PKG_CONFIG_PATH="${OHOS_SYSROOT}/usr/lib/pkgconfig:${OHOS_SYSROOT}/usr/local/lib/pkgconfig"
+export PKG_CONFIG_LIBDIR="${OHOS_SYSROOT}/usr/lib:${OHOS_SYSROOT}/usr/local/lib"
+export PKG_CONFIG_SYSROOT_DIR="${OHOS_SYSROOT}"
+export HOST_TRIPLET="${TARGET_ARCH}"
+export BUILD_TRIPLET="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)"
+export PATH="${OHOS_TOOLCHAIN_DIR}/bin:${PATH}"
+
+echo "OpenHarmonyOS 环境变量已设置:"
+echo "OHOS_SDK_PATH: ${OHOS_NDK_HOME}"
+echo "OHOS_TOOLCHAIN_DIR: ${OHOS_TOOLCHAIN_DIR}"
+echo "OHOS_SYSROOT: ${OHOS_SYSROOT}"
+echo "PKG_CONFIG_PATH: ${PKG_CONFIG_PATH}"
+echo "PATH: ${PATH}"
diff --git a/easytier-contrib/easytier-ohrs/src/lib.rs b/easytier-contrib/easytier-ohrs/src/lib.rs
new file mode 100644
index 000000000..e1e7518b8
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/src/lib.rs
@@ -0,0 +1,148 @@
+mod native_log;
+
+use easytier::common::config::{ConfigLoader, TomlConfigLoader};
+use easytier::instance_manager::NetworkInstanceManager;
+use easytier::launcher::ConfigSource;
+use napi_derive_ohos::napi;
+use ohos_hilog_binding::{hilog_debug, hilog_error};
+use std::format;
+use uuid::Uuid;
+
+static INSTANCE_MANAGER: once_cell::sync::Lazy =
+ once_cell::sync::Lazy::new(NetworkInstanceManager::new);
+
+#[napi(object)]
+pub struct KeyValuePair {
+ pub key: String,
+ pub value: String,
+}
+
+#[napi]
+pub fn set_tun_fd(
+ inst_id: String,
+ fd: i32,
+) -> bool {
+ match Uuid::try_parse(&inst_id) {
+ Ok(uuid) => {
+ match INSTANCE_MANAGER.set_tun_fd(&uuid, fd) {
+ Ok(_) => {
+ hilog_debug!("[Rust] set tun fd {} to {}.", fd, inst_id);
+ true
+ }
+ Err(e) => {
+ hilog_error!("[Rust] cant set tun fd {} to {}. {}", fd, inst_id, e);
+ false
+ }
+ }
+ }
+ Err(e) => {
+ hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
+ false
+ }
+ }
+}
+
+#[napi]
+pub fn parse_config(cfg_str: String) -> bool {
+ match TomlConfigLoader::new_from_str(&cfg_str) {
+ Ok(_) => {
+ true
+ }
+ Err(e) => {
+ hilog_error!("[Rust] parse config failed {}", e);
+ false
+ }
+ }
+}
+
+#[napi]
+pub fn run_network_instance(cfg_str: String) -> bool {
+ let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
+ Ok(cfg) => cfg,
+ Err(e) => {
+ hilog_error!("[Rust] parse config failed {}", e);
+ return false;
+ }
+ };
+
+ if INSTANCE_MANAGER.list_network_instance_ids().len() > 0 {
+ hilog_error!("[Rust] there is a running instance!");
+ return false;
+ }
+
+ let inst_id = cfg.get_id();
+ if INSTANCE_MANAGER
+ .list_network_instance_ids()
+ .contains(&inst_id)
+ {
+ return false;
+ }
+ INSTANCE_MANAGER
+ .run_network_instance(cfg, ConfigSource::FFI)
+ .unwrap();
+ true
+}
+
+#[napi]
+pub fn stop_network_instance(inst_names: Vec) {
+ INSTANCE_MANAGER
+ .delete_network_instance(
+ inst_names
+ .into_iter()
+ .filter_map(|s| Uuid::parse_str(&s).ok())
+ .collect(),
+ )
+ .unwrap();
+ hilog_debug!("[Rust] stop_network_instance");
+}
+
+#[napi]
+pub fn collect_network_infos() -> Vec {
+ let mut result = Vec::new();
+ match INSTANCE_MANAGER.collect_network_infos() {
+ Ok(map) => {
+ for (uuid, info) in map.iter() {
+ // convert value to json string
+ let value = match serde_json::to_string(&info) {
+ Ok(value) => value,
+ Err(e) => {
+ hilog_error!("[Rust] failed to serialize instance {} info: {}", uuid, e);
+ continue;
+ }
+ };
+ result.push(KeyValuePair {
+ key: uuid.clone().to_string(),
+ value: value.clone(),
+ });
+ }
+ }
+ Err(_) => {}
+ }
+ result
+}
+
+#[napi]
+pub fn collect_running_network() -> Vec {
+ INSTANCE_MANAGER
+ .list_network_instance_ids()
+ .clone()
+ .into_iter()
+ .map(|id| id.to_string())
+ .collect()
+}
+
+#[napi]
+pub fn is_running_network(inst_id: String) -> bool {
+ match Uuid::try_parse(&inst_id) {
+ Ok(uuid) => {
+ INSTANCE_MANAGER
+ .list_network_instance_ids()
+ .contains(&uuid)
+ }
+ Err(e) => {
+ hilog_error!("[Rust] cant covert {} to uuid. {}", inst_id, e);
+ false
+ }
+ }
+
+}
diff --git a/easytier-contrib/easytier-ohrs/src/native_log.rs b/easytier-contrib/easytier-ohrs/src/native_log.rs
new file mode 100644
index 000000000..221dce1cc
--- /dev/null
+++ b/easytier-contrib/easytier-ohrs/src/native_log.rs
@@ -0,0 +1,98 @@
+use std::collections::HashMap;
+use std::panic;
+use napi_derive_ohos::napi;
+use ohos_hilog_binding::{hilog_debug, hilog_error, hilog_info, hilog_warn, set_global_options, LogOptions};
+use tracing::{Event, Subscriber};
+use tracing_core::Level;
+use tracing_subscriber::layer::{Context, Layer};
+use tracing_subscriber::prelude::*;
+
+static INITIALIZED: std::sync::Once = std::sync::Once::new();
+fn panic_hook(info: &panic::PanicHookInfo) {
+ hilog_error!("RUST PANIC: {}", info);
+}
+
+#[napi]
+pub fn init_panic_hook() {
+ INITIALIZED.call_once(|| {
+ panic::set_hook(Box::new(panic_hook));
+ });
+}
+
+#[napi]
+pub fn hilog_global_options(
+ domain: u32,
+ tag: String,
+) {
+ ohos_hilog_binding::forward_stdio_to_hilog();
+ set_global_options(LogOptions{
+ domain,
+ tag: Box::leak(tag.clone().into_boxed_str()),
+ })
+}
+
+#[napi]
+pub fn init_tracing_subscriber() {
+ tracing_subscriber::registry()
+ .with(
+ CallbackLayer {
+ callback: Box::new(tracing_callback),
+ }
+ )
+ .init();
+}
+
+fn tracing_callback(event: &Event, fields: HashMap) {
+ let metadata = event.metadata();
+ #[cfg(target_env = "ohos")]
+ {
+ let loc = metadata.target().split("::").last().unwrap();
+ match *metadata.level() {
+ Level::TRACE => {
+ hilog_debug!("[{}] {:?}", loc, fields.values().collect::>());
+ }
+ Level::DEBUG => {
+ hilog_debug!("[{}] {:?}", loc, fields.values().collect::>());
+ }
+ Level::INFO => {
+ hilog_info!("[{}] {:?}", loc, fields.values().collect::>());
+ }
+ Level::WARN => {
+ hilog_warn!("[{}] {:?}", loc, fields.values().collect::>());
+ }
+ Level::ERROR => {
+ hilog_error!("[{}] {:?}", loc, fields.values().collect::>());
+ }
+ }
+ }
+}
+
+struct CallbackLayer {
+ callback: Box) + Send + Sync>,
+}
+
+impl Layer for CallbackLayer {
+ fn on_event(&self, event: &Event, _ctx: Context) {
+ // 使用 fmt::format::FmtSpan 提取字段值
+ let mut fields = HashMap::new();
+ let mut visitor = FieldCollector(&mut fields);
+ event.record(&mut visitor);
+ (self.callback)(event, fields);
+ }
+}
+
+struct FieldCollector<'a>(&'a mut HashMap);
+
+impl<'a> tracing::field::Visit for FieldCollector<'a> {
+ fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
+ self.0.insert(field.name().to_string(), value.to_string());
+ }
+
+ fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
+ self.0.insert(field.name().to_string(), value.to_string());
+ }
+
+ fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
+ self.0.insert(field.name().to_string(), format!("{:?}", value));
+ }
+}
\ No newline at end of file
diff --git a/easytier-gui/locales/cn.yml b/easytier-gui/locales/cn.yml
deleted file mode 100644
index 86c62701a..000000000
--- a/easytier-gui/locales/cn.yml
+++ /dev/null
@@ -1,120 +0,0 @@
-network: 网络
-networking_method: 网络方式
-public_server: 公共服务器
-manual: 手动
-standalone: 独立
-virtual_ipv4: 虚拟IPv4地址
-virtual_ipv4_dhcp: DHCP
-network_name: 网络名称
-network_secret: 网络密码
-public_server_url: 公共服务器地址
-peer_urls: 对等节点地址
-proxy_cidrs: 子网代理CIDR
-enable_vpn_portal: 启用VPN门户
-vpn_portal_listen_port: 监听端口
-vpn_portal_client_network: 客户端子网
-dev_name: TUN接口名称
-advanced_settings: 高级设置
-basic_settings: 基础设置
-listener_urls: 监听地址
-rpc_port: RPC端口
-config_network: 配置网络
-running: 运行中
-error_msg: 错误信息
-detail: 详情
-add_new_network: 添加新网络
-del_cur_network: 删除当前网络
-select_network: 选择网络
-network_instances: 网络实例
-instance_id: 实例ID
-network_infos: 网络信息
-parse_network_config: 解析网络配置
-retain_network_instance: 保留网络实例
-collect_network_infos: 收集网络信息
-settings: 设置
-exchange_language: Switch to English
-logging: 日志
-logging_level_info: 信息
-logging_level_debug: 调试
-logging_level_warn: 警告
-logging_level_trace: 跟踪
-logging_level_off: 关闭
-logging_open_dir: 打开日志目录
-logging_copy_dir: 复制日志路径
-disable_auto_launch: 关闭开机自启
-enable_auto_launch: 开启开机自启
-exit: 退出
-chips_placeholder: 例如: {0}, 按回车添加
-hostname_placeholder: '留空默认为主机名: {0}'
-dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
-off_text: 点击关闭
-on_text: 点击开启
-show_config: 显示配置
-edit_config: 编辑配置文件
-close: 关闭
-save: 保存
-config_saved: 配置已保存
-
-
-use_latency_first: 延迟优先模式
-my_node_info: 当前节点信息
-peer_count: 已连接
-upload: 上传
-download: 下载
-show_vpn_portal_config: 显示VPN门户配置
-vpn_portal_config: VPN门户配置
-show_event_log: 显示事件日志
-event_log: 事件日志
-peer_info: 节点信息
-hostname: 主机名
-route_cost: 路由
-latency: 延迟
-upload_bytes: 上传
-download_bytes: 下载
-loss_rate: 丢包率
-
-status:
- version: 内核版本
- local: 本机
- server: 服务器
- relay: 中继
-
-run_network: 运行网络
-stop_network: 停止网络
-network_running: 运行中
-network_stopped: 已停止
-dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中发生IP冲突,将自动更改IP。
-
-tray:
- show: 显示 / 隐藏
- exit: 退出
-
-about:
- title: 关于
- version: 版本
- author: 作者
- homepage: 主页
- license: 许可证
- description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
- check_update: 检查更新
-
-event:
- Unknown: 未知
- TunDeviceReady: Tun设备就绪
- TunDeviceError: Tun设备错误
- PeerAdded: 对端添加
- PeerRemoved: 对端移除
- PeerConnAdded: 对端连接添加
- PeerConnRemoved: 对端连接移除
- ListenerAdded: 监听器添加
- ListenerAddFailed: 监听器添加失败
- ListenerAcceptFailed: 监听器接受连接失败
- ConnectionAccepted: 连接已接受
- ConnectionError: 连接错误
- Connecting: 正在连接
- ConnectError: 连接错误
- VpnPortalClientConnected: VPN门户客户端已连接
- VpnPortalClientDisconnected: VPN门户客户端已断开连接
- DhcpIpv4Changed: DHCP IPv4地址更改
- DhcpIpv4Conflicted: DHCP IPv4地址冲突
- PortForwardAdded: 端口转发添加
diff --git a/easytier-gui/locales/en.yml b/easytier-gui/locales/en.yml
deleted file mode 100644
index b7cc3244a..000000000
--- a/easytier-gui/locales/en.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-network: Network
-networking_method: Networking Method
-public_server: Public Server
-manual: Manual
-standalone: Standalone
-virtual_ipv4: Virtual IPv4
-virtual_ipv4_dhcp: DHCP
-network_name: Network Name
-network_secret: Network Secret
-public_server_url: Public Server URL
-peer_urls: Peer URLs
-proxy_cidrs: Subnet Proxy CIDRs
-enable_vpn_portal: Enable VPN Portal
-vpn_portal_listen_port: VPN Portal Listen Port
-vpn_portal_client_network: Client Sub Network
-dev_name: TUN interface name
-advanced_settings: Advanced Settings
-basic_settings: Basic Settings
-listener_urls: Listener URLs
-rpc_port: RPC Port
-config_network: Config Network
-running: Running
-error_msg: Error Message
-detail: Detail
-add_new_network: New Network
-del_cur_network: Delete Current Network
-select_network: Select Network
-network_instances: Network Instances
-instance_id: Instance ID
-network_infos: Network Infos
-parse_network_config: Parse Network Config
-retain_network_instance: Retain Network Instance
-collect_network_infos: Collect Network Infos
-settings: Settings
-exchange_language: 切换中文
-logging: Logging
-logging_level_info: Info
-logging_level_debug: Debug
-logging_level_warn: Warn
-logging_level_trace: Trace
-logging_level_off: Off
-logging_open_dir: Open Log Directory
-logging_copy_dir: Copy Log Path
-disable_auto_launch: Disable Launch on Reboot
-enable_auto_launch: Enable Launch on Reboot
-exit: Exit
-use_latency_first: Latency First Mode
-chips_placeholder: 'e.g: {0}, press Enter to add'
-hostname_placeholder: 'Leave blank and default to host name: {0}'
-dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
-off_text: Press to disable
-on_text: Press to enable
-show_config: Show Config
-edit_config: Edit Config File
-close: Close
-save: Save
-config_saved: Configuration saved
-my_node_info: My Node Info
-peer_count: Connected
-upload: Upload
-download: Download
-show_vpn_portal_config: Show VPN Portal Config
-vpn_portal_config: VPN Portal Config
-show_event_log: Show Event Log
-event_log: Event Log
-peer_info: Peer Info
-route_cost: Route Cost
-hostname: Hostname
-latency: Latency
-upload_bytes: Upload
-download_bytes: Download
-loss_rate: Loss Rate
-
-status:
- version: Version
- local: Local
- server: Server
- relay: Relay
-
-run_network: Run Network
-stop_network: Stop Network
-network_running: running
-network_stopped: stopped
-dhcp_experimental_warning: Experimental warning! if there is an IP conflict in the network when using DHCP, the IP will be automatically changed.
-
-tray:
- show: Show / Hide
- exit: Exit
-
-about:
- title: About
- version: Version
- author: Author
- homepage: Homepage
- license: License
- description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
- check_update: Check Update
-
-event:
- Unknown: Unknown
- TunDeviceReady: TunDeviceReady
- TunDeviceError: TunDeviceError
- PeerAdded: PeerAdded
- PeerRemoved: PeerRemoved
- PeerConnAdded: PeerConnAdded
- PeerConnRemoved: PeerConnRemoved
- ListenerAdded: ListenerAdded
- ListenerAddFailed: ListenerAddFailed
- ListenerAcceptFailed: ListenerAcceptFailed
- ConnectionAccepted: ConnectionAccepted
- ConnectionError: ConnectionError
- Connecting: Connecting
- ConnectError: ConnectError
- VpnPortalClientConnected: VpnPortalClientConnected
- VpnPortalClientDisconnected: VpnPortalClientDisconnected
- DhcpIpv4Changed: DhcpIpv4Changed
- DhcpIpv4Conflicted: DhcpIpv4Conflicted
- PortForwardAdded: PortForwardAdded
diff --git a/easytier-gui/package.json b/easytier-gui/package.json
index efac0317a..f91d8fa2e 100644
--- a/easytier-gui/package.json
+++ b/easytier-gui/package.json
@@ -1,7 +1,7 @@
{
"name": "easytier-gui",
"type": "module",
- "version": "2.3.2",
+ "version": "2.4.2",
"private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": {
@@ -15,10 +15,10 @@
"dependencies": {
"@primevue/themes": "4.3.3",
"@tauri-apps/plugin-autostart": "2.0.0",
- "@tauri-apps/plugin-clipboard-manager": "2.0.0",
- "@tauri-apps/plugin-os": "2.0.0",
- "@tauri-apps/plugin-process": "2.0.0",
- "@tauri-apps/plugin-shell": "2.0.1",
+ "@tauri-apps/plugin-clipboard-manager": "2.3.0",
+ "@tauri-apps/plugin-os": "2.3.0",
+ "@tauri-apps/plugin-process": "2.3.0",
+ "@tauri-apps/plugin-shell": "2.3.0",
"@vueuse/core": "^11.2.0",
"aura": "link:@primevue\\themes\\aura",
"easytier-frontend-lib": "workspace:*",
@@ -33,8 +33,8 @@
"@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "4.3.3",
- "@tauri-apps/api": "2.1.0",
- "@tauri-apps/cli": "2.1.0",
+ "@tauri-apps/api": "2.7.0",
+ "@tauri-apps/cli": "2.7.1",
"@types/default-gateway": "^7.2.2",
"@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml
index 97dfab6fc..b4333ac9e 100644
--- a/easytier-gui/src-tauri/Cargo.toml
+++ b/easytier-gui/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "easytier-gui"
-version = "2.3.2"
+version = "2.4.2"
description = "EasyTier GUI"
authors = ["you"]
edition = "2021"
@@ -23,7 +23,7 @@ thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = f
[dependencies]
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
-tauri = { version = "=2.0.6", features = [
+tauri = { version = "2.7.0", features = [
"tray-icon",
"image-png",
"image-ico",
@@ -40,25 +40,32 @@ chrono = { version = "0.4.37", features = ["serde"] }
once_cell = "1.18.0"
dashmap = "6.0"
-
-privilege = "0.3"
-gethostname = "0.5"
+gethostname = "1.0.2"
dunce = "1.0.4"
-tauri-plugin-shell = "2.0"
-tauri-plugin-process = "2.0"
-tauri-plugin-clipboard-manager = "2.0"
-tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
+tauri-plugin-shell = "2.3.0"
+tauri-plugin-process = "2.3.0"
+tauri-plugin-clipboard-manager = "2.3.0"
+tauri-plugin-positioner = { version = "2.3.0", features = ["tray-icon"] }
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
-tauri-plugin-os = "2.0"
-tauri-plugin-autostart = "2.0"
+tauri-plugin-os = "2.3.0"
+tauri-plugin-autostart = "2.5.0"
uuid = "1.17.0"
+[target.'cfg(target_os = "windows")'.dependencies]
+windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
+winapi = { version = "0.3.9", features = ["securitybaseapi", "processthreadsapi"] }
+
+[target.'cfg(target_family = "unix")'.dependencies]
+libc = "0.2"
+
+[target.'cfg(target_os = "macos")'.dependencies]
+security-framework-sys = "2.9.0"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
-tauri-plugin-single-instance = "2.2.3"
+tauri-plugin-single-instance = "2.3.2"
diff --git a/easytier-gui/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/easytier-gui/src-tauri/gen/android/app/src/main/AndroidManifest.xml
index 2c1559ff5..d39174a00 100644
--- a/easytier-gui/src-tauri/gen/android/app/src/main/AndroidManifest.xml
+++ b/easytier-gui/src-tauri/gen/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,10 @@
+
+
+
+
+
+
= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent)
+ } else {
+ startService(serviceIntent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/easytier-gui/src-tauri/gen/android/app/src/main/java/com/kkrainbow/easytier/MainForegroundService.kt b/easytier-gui/src-tauri/gen/android/app/src/main/java/com/kkrainbow/easytier/MainForegroundService.kt
new file mode 100644
index 000000000..5b296265b
--- /dev/null
+++ b/easytier-gui/src-tauri/gen/android/app/src/main/java/com/kkrainbow/easytier/MainForegroundService.kt
@@ -0,0 +1,64 @@
+package com.kkrainbow.easytier
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import android.util.Log
+
+class MainForegroundService : Service() {
+ companion object {
+ const val CHANNEL_ID = "easytier_channel"
+ const val NOTIFICATION_ID = 1355
+ // You can add more constants if needed
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ createNotificationChannel()
+ val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("easytier Running")
+ .setContentText("easytier is available on localhost")
+ .setSmallIcon(android.R.drawable.ic_menu_manage)
+ .build()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ NOTIFICATION_ID,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ startForeground(NOTIFICATION_ID, notification)
+ }
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ try {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "easytier notice",
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager?.createNotificationChannel(channel)
+ } catch (e: Exception) {
+ Log.e("MainForegroundService", "Failed to create notification channel", e)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar
index e708b1c02..1b33c55ba 100755
Binary files a/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar and b/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
index a03ce6aa2..d4081da47 100644
--- a/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
+++ b/easytier-gui/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/easytier-gui/src-tauri/gen/android/gradlew b/easytier-gui/src-tauri/gen/android/gradlew
index 4f906e0c8..23d15a936 100755
--- a/easytier-gui/src-tauri/gen/android/gradlew
+++ b/easytier-gui/src-tauri/gen/android/gradlew
@@ -1,7 +1,7 @@
-#!/usr/bin/env sh
+#!/bin/sh
#
-# Copyright 2015 the original author or authors.
+# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,81 +15,115 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/easytier-gui/src-tauri/gen/android/gradlew.bat b/easytier-gui/src-tauri/gen/android/gradlew.bat
index 107acd32c..db3a6ac20 100755
--- a/easytier-gui/src-tauri/gen/android/gradlew.bat
+++ b/easytier-gui/src-tauri/gen/android/gradlew.bat
@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/easytier-gui/src-tauri/src/elevate/linux.rs b/easytier-gui/src-tauri/src/elevate/linux.rs
new file mode 100644
index 000000000..346e2cfb3
--- /dev/null
+++ b/easytier-gui/src-tauri/src/elevate/linux.rs
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Luis Liu. All rights reserved.
+ * Licensed under the MIT License. See License in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+use super::Command;
+use anyhow::{anyhow, Result};
+use std::env;
+use std::ffi::OsStr;
+use std::path::PathBuf;
+use std::process::{Command as StdCommand, Output};
+use std::str::FromStr;
+
+/// The implementation of state check and elevated executing varies on each platform
+impl Command {
+ /// Check the state the current program running
+ ///
+ /// Return `true` if the program is running as root, otherwise false
+ pub fn is_elevated() -> bool {
+ let uid = unsafe { libc::getuid() };
+ uid == 0
+ }
+
+ /// Prompting the user with a graphical OS dialog for the root password,
+ /// excuting the command with escalated privileges, and return the output
+ pub fn output(&self) -> Result {
+ let pkexec = PathBuf::from_str("/bin/pkexec")?;
+ let mut command = StdCommand::new(pkexec);
+ let display = env::var("DISPLAY");
+ let xauthority = env::var("XAUTHORITY");
+ let home = env::var("HOME");
+
+ command.arg("--disable-internal-agent");
+ if display.is_ok() || xauthority.is_ok() || home.is_ok() {
+ command.arg("env");
+ if let Ok(display) = display {
+ command.arg(format!("DISPLAY={}", display));
+ }
+ if let Ok(xauthority) = xauthority {
+ command.arg(format!("XAUTHORITY={}", xauthority));
+ }
+ if let Ok(home) = home {
+ command.arg(format!("HOME={}", home));
+ }
+ } else if self.cmd.get_envs().any(|(_, v)| v.is_some()) {
+ command.arg("env");
+ }
+ for (k, v) in self.cmd.get_envs() {
+ if let Some(value) = v {
+ command.arg(format!(
+ "{}={}",
+ k.to_str().ok_or(anyhow!("invalid key"))?,
+ value.to_str().ok_or(anyhow!("invalid value"))?
+ ));
+ }
+ }
+
+ command.arg(self.cmd.get_program());
+ let args: Vec<&OsStr> = self.cmd.get_args().collect();
+ if !args.is_empty() {
+ command.args(args);
+ }
+
+ let output = command.output()?;
+ Ok(output)
+ }
+}
diff --git a/easytier-gui/src-tauri/src/elevate/macos.rs b/easytier-gui/src-tauri/src/elevate/macos.rs
new file mode 100644
index 000000000..18b721d89
--- /dev/null
+++ b/easytier-gui/src-tauri/src/elevate/macos.rs
@@ -0,0 +1,182 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Luis Liu. All rights reserved.
+ * Licensed under the MIT License. See License in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+// Thanks to https://github.com/jorangreef/sudo-prompt/blob/master/index.js
+// MIT License
+//
+// Copyright (c) 2015 Joran Dirk Greef
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// ...
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use super::Command;
+use anyhow::Result;
+use std::env;
+use std::path::PathBuf;
+use std::process::{ExitStatus, Output};
+
+use std::ffi::{CString, OsString};
+use std::io;
+use std::mem;
+use std::os::unix::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+
+use libc::{fcntl, fileno, waitpid, EINTR, F_GETOWN};
+use security_framework_sys::authorization::{
+ errAuthorizationSuccess, kAuthorizationFlagDefaults, kAuthorizationFlagDestroyRights,
+ AuthorizationCreate, AuthorizationExecuteWithPrivileges, AuthorizationFree, AuthorizationRef,
+};
+
+const ENV_PATH: &str = "PATH";
+
+fn get_exe_path>(exe_name: P) -> Option {
+ let exe_name = exe_name.as_ref();
+ if exe_name.has_root() {
+ return Some(exe_name.into());
+ }
+
+ if let Ok(abs_path) = exe_name.canonicalize() {
+ if abs_path.is_file() {
+ return Some(abs_path);
+ }
+ }
+
+ env::var_os(ENV_PATH).and_then(|paths| {
+ env::split_paths(&paths)
+ .filter_map(|dir| {
+ let full_path = dir.join(exe_name);
+ if full_path.is_file() {
+ Some(full_path)
+ } else {
+ None
+ }
+ })
+ .next()
+ })
+}
+
+macro_rules! make_cstring {
+ ($s:expr) => {
+ match CString::new($s.as_bytes()) {
+ Ok(s) => s,
+ Err(_) => {
+ return Err(io::Error::new(io::ErrorKind::Other, "null byte in string"));
+ }
+ }
+ };
+}
+
+unsafe fn gui_runas(prog: *const i8, argv: *const *const i8) -> i32 {
+ let mut authref: AuthorizationRef = ptr::null_mut();
+ let mut pipe: *mut libc::FILE = ptr::null_mut();
+
+ if AuthorizationCreate(
+ ptr::null(),
+ ptr::null(),
+ kAuthorizationFlagDefaults,
+ &mut authref,
+ ) != errAuthorizationSuccess
+ {
+ return -1;
+ }
+ if AuthorizationExecuteWithPrivileges(
+ authref,
+ prog,
+ kAuthorizationFlagDefaults,
+ argv as *const *mut _,
+ &mut pipe,
+ ) != errAuthorizationSuccess
+ {
+ AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
+ return -1;
+ }
+
+ let pid = fcntl(fileno(pipe), F_GETOWN, 0);
+ let mut status = 0;
+ loop {
+ let r = waitpid(pid, &mut status, 0);
+ if r == -1 && io::Error::last_os_error().raw_os_error() == Some(EINTR) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ AuthorizationFree(authref, kAuthorizationFlagDestroyRights);
+ status
+}
+
+fn runas_root_gui(cmd: &Command) -> io::Result {
+ let exe: OsString = match get_exe_path(&cmd.cmd.get_program()) {
+ Some(exe) => exe.into(),
+ None => unsafe {
+ return Ok(mem::transmute(!0));
+ },
+ };
+ let prog = make_cstring!(exe);
+ let mut args = vec![];
+ for arg in cmd.cmd.get_args() {
+ args.push(make_cstring!(arg))
+ }
+ let mut argv: Vec<_> = args.iter().map(|x| x.as_ptr()).collect();
+ argv.push(ptr::null());
+
+ unsafe { Ok(mem::transmute(gui_runas(prog.as_ptr(), argv.as_ptr()))) }
+}
+
+/// The implementation of state check and elevated executing varies on each platform
+impl Command {
+ /// Check the state the current program running
+ ///
+ /// Return `true` if the program is running as root, otherwise false
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use elevated_command::Command;
+ ///
+ /// fn main() {
+ /// let is_elevated = Command::is_elevated();
+ ///
+ /// }
+ /// ```
+ pub fn is_elevated() -> bool {
+ let uid = unsafe { libc::getuid() };
+ let euid = unsafe { libc::geteuid() };
+
+ match (uid, euid) {
+ (0, 0) => true,
+ (_, 0) => true,
+ (_, _) => false,
+ }
+ }
+
+ /// Prompting the user with a graphical OS dialog for the root password,
+ /// excuting the command with escalated privileges, and return the output
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use elevated_command::Command;
+ /// use std::process::Command as StdCommand;
+ ///
+ /// fn main() {
+ /// let mut cmd = StdCommand::new("path to the application");
+ /// let elevated_cmd = Command::new(cmd);
+ /// let output = elevated_cmd.output().unwrap();
+ /// }
+ /// ```
+ pub fn output(&self) -> Result {
+ let status = runas_root_gui(self)?;
+ Ok(Output {
+ status,
+ stdout: Vec::new(),
+ stderr: Vec::new(),
+ })
+ }
+}
diff --git a/easytier-gui/src-tauri/src/elevate/mod.rs b/easytier-gui/src-tauri/src/elevate/mod.rs
new file mode 100644
index 000000000..594be6f71
--- /dev/null
+++ b/easytier-gui/src-tauri/src/elevate/mod.rs
@@ -0,0 +1,101 @@
+#![allow(dead_code)]
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Luis Liu. All rights reserved.
+ * Licensed under the MIT License. See License in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+use std::convert::From;
+use std::process::Command as StdCommand;
+
+/// Wrap of std::process::command and escalate privileges while executing
+pub struct Command {
+ cmd: StdCommand,
+ #[allow(dead_code)]
+ icon: Option>,
+ #[allow(dead_code)]
+ name: Option,
+}
+
+/// Command initialization shares the same logic across all the platforms
+impl Command {
+ /// Constructs a new `Command` from a std::process::Command
+ /// instance, it would read the following configuration from
+ /// the instance while executing:
+ ///
+ /// * The instance's path to the program
+ /// * The instance's arguments
+ /// * The instance's environment variables
+ ///
+ /// So far, the new `Command` would only take the environment variables explicitly
+ /// set by std::process::Command::env and std::process::Command::env,
+ /// without the ones inherited from the parent process
+ ///
+ /// And the environment variables would only be taken on Linux and MacOS,
+ /// they would be ignored on Windows
+ ///
+ /// Current working directory would be the following while executing the command:
+ /// - %SystemRoot%\System32 on Windows
+ /// - /root on Linux
+ /// - $TMPDIR/sudo_prompt_applet/applet.app/Contents/MacOS on MacOS
+ ///
+ /// To pass environment variables on Windows,
+ /// to inherit environment variables from the parent process and
+ /// to change the working directory will be supported in later versions
+ pub fn new(cmd: StdCommand) -> Self {
+ Self {
+ cmd,
+ icon: None,
+ name: None,
+ }
+ }
+
+ /// Consumes the `Take`, returning the wrapped std::process::Command
+ ///
+ /// # Examples
+ pub fn into_inner(self) -> StdCommand {
+ self.cmd
+ }
+
+ /// Gets a mutable reference to the underlying std::process::Command
+ pub fn get_ref(&self) -> &StdCommand {
+ &self.cmd
+ }
+
+ /// Gets a reference to the underlying std::process::Command
+ pub fn get_mut(&mut self) -> &mut StdCommand {
+ &mut self.cmd
+ }
+
+ /// Set the `icon` for the pop-up graphical OS dialog
+ pub fn icon(&mut self, icon: Vec) -> &mut Self {
+ self.icon = Some(icon);
+ self
+ }
+
+ /// Set the name for the pop-up graphical OS dialog
+ ///
+ /// This method is only applicable on `MacOS`
+ pub fn name(&mut self, name: String) -> &mut Self {
+ self.name = Some(name);
+ self
+ }
+}
+
+impl From for Command {
+ /// Converts from a std::process::Command
+ ///
+ /// It is similiar with the construct method
+ fn from(cmd: StdCommand) -> Self {
+ Self {
+ cmd,
+ icon: None,
+ name: None,
+ }
+ }
+}
+
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(target_os = "macos")]
+mod macos;
+#[cfg(target_os = "windows")]
+mod windows;
diff --git a/easytier-gui/src-tauri/src/elevate/windows.rs b/easytier-gui/src-tauri/src/elevate/windows.rs
new file mode 100644
index 000000000..7678cc3ac
--- /dev/null
+++ b/easytier-gui/src-tauri/src/elevate/windows.rs
@@ -0,0 +1,114 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Luis Liu. All rights reserved.
+ * Licensed under the MIT License. See License in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+use super::Command;
+use anyhow::Result;
+use std::mem;
+use std::os::windows::process::ExitStatusExt;
+use std::process::{ExitStatus, Output};
+use winapi::shared::minwindef::{DWORD, LPVOID};
+use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
+use winapi::um::securitybaseapi::GetTokenInformation;
+use winapi::um::winnt::{TokenElevation, HANDLE, TOKEN_ELEVATION, TOKEN_QUERY};
+use windows::core::{w, HSTRING, PCWSTR};
+use windows::Win32::Foundation::HWND;
+use windows::Win32::UI::Shell::ShellExecuteW;
+use windows::Win32::UI::WindowsAndMessaging::SW_HIDE;
+
+/// The implementation of state check and elevated executing varies on each platform
+impl Command {
+ /// Check the state the current program running
+ ///
+ /// Return `true` if the program is running as root, otherwise false
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use elevated_command::Command;
+ ///
+ /// fn main() {
+ /// let is_elevated = Command::is_elevated();
+ ///
+ /// }
+ /// ```
+ pub fn is_elevated() -> bool {
+ // Thanks to https://stackoverflow.com/a/8196291
+ unsafe {
+ let mut current_token_ptr: HANDLE = mem::zeroed();
+ let mut token_elevation: TOKEN_ELEVATION = mem::zeroed();
+ let token_elevation_type_ptr: *mut TOKEN_ELEVATION = &mut token_elevation;
+ let mut size: DWORD = 0;
+
+ let result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut current_token_ptr);
+
+ if result != 0 {
+ let result = GetTokenInformation(
+ current_token_ptr,
+ TokenElevation,
+ token_elevation_type_ptr as LPVOID,
+ mem::size_of::() as u32,
+ &mut size,
+ );
+ if result != 0 {
+ return token_elevation.TokenIsElevated != 0;
+ }
+ }
+ }
+ false
+ }
+
+ /// Prompting the user with a graphical OS dialog for the root password,
+ /// excuting the command with escalated privileges, and return the output
+ ///
+ /// On Windows, according to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutew#return-value,
+ /// Output.status.code() shoudl be greater than 32 if the function succeeds,
+ /// otherwise the value indicates the cause of the failure
+ ///
+ /// On Windows, Output.stdout and Output.stderr will always be empty as of now
+ ///
+ /// # Examples
+ ///
+ /// ```no_run
+ /// use elevated_command::Command;
+ /// use std::process::Command as StdCommand;
+ ///
+ /// fn main() {
+ /// let mut cmd = StdCommand::new("path to the application");
+ /// let elevated_cmd = Command::new(cmd);
+ /// let output = elevated_cmd.output().unwrap();
+ /// }
+ /// ```
+ pub fn output(&self) -> Result {
+ let args = self
+ .cmd
+ .get_args()
+ .map(|c| c.to_str().unwrap().to_string())
+ .collect::>();
+ let parameters = if args.is_empty() {
+ HSTRING::new()
+ } else {
+ let arg_str = args.join(" ");
+ HSTRING::from(arg_str)
+ };
+
+ // according to https://stackoverflow.com/a/38034535
+ // the cwd always point to %SystemRoot%\System32 and cannot be changed by settting lpdirectory param
+ let r = unsafe {
+ ShellExecuteW(
+ HWND(0),
+ w!("runas"),
+ &HSTRING::from(self.cmd.get_program()),
+ &HSTRING::from(parameters),
+ PCWSTR::null(),
+ SW_HIDE,
+ )
+ };
+ Ok(Output {
+ status: ExitStatus::from_raw(r.0 as u32),
+ stdout: Vec::::new(),
+ stderr: Vec::::new(),
+ })
+ }
+}
diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs
index b0a06e801..1333fe813 100644
--- a/easytier-gui/src-tauri/src/lib.rs
+++ b/easytier-gui/src-tauri/src/lib.rs
@@ -1,6 +1,8 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+mod elevate;
+
use std::collections::BTreeMap;
use easytier::{
@@ -128,18 +130,20 @@ fn toggle_window_visibility(app: &tauri::AppHandle) {
#[cfg(not(target_os = "android"))]
fn check_sudo() -> bool {
- use std::env::current_exe;
- let is_elevated = privilege::user::privileged();
+ let is_elevated = elevate::Command::is_elevated();
if !is_elevated {
- let Ok(exe) = current_exe() else {
- return true;
- };
+ let exe_path = std::env::var("APPIMAGE")
+ .ok()
+ .or_else(|| std::env::args().next())
+ .unwrap_or_default();
let args: Vec = std::env::args().collect();
- let mut elevated_cmd = privilege::runas::Command::new(exe);
+ let mut stdcmd = std::process::Command::new(&exe_path);
if args.contains(&AUTOSTART_ARG.to_owned()) {
- elevated_cmd.arg(AUTOSTART_ARG);
+ stdcmd.arg(AUTOSTART_ARG);
}
- let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
+ elevate::Command::new(stdcmd)
+ .output()
+ .expect("Failed to run elevated command");
}
is_elevated
}
@@ -209,7 +213,7 @@ pub fn run() {
// for tray icon, menu need to be built in js
#[cfg(not(target_os = "android"))]
let _tray_menu = TrayIconBuilder::with_id("main")
- .menu_on_left_click(false)
+ .show_menu_on_left_click(false)
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
diff --git a/easytier-gui/src-tauri/tauri.conf.json b/easytier-gui/src-tauri/tauri.conf.json
index e18fd16ab..907deded0 100644
--- a/easytier-gui/src-tauri/tauri.conf.json
+++ b/easytier-gui/src-tauri/tauri.conf.json
@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false
},
"productName": "easytier-gui",
- "version": "2.3.2",
+ "version": "2.4.2",
"identifier": "com.kkrainbow.easytier",
"plugins": {},
"app": {
diff --git a/easytier-gui/src/composables/mobile_vpn.ts b/easytier-gui/src/composables/mobile_vpn.ts
index a1058cc4d..a231bde95 100644
--- a/easytier-gui/src/composables/mobile_vpn.ts
+++ b/easytier-gui/src/composables/mobile_vpn.ts
@@ -115,6 +115,11 @@ function getRoutesForVpn(routes: Route[]): string[] {
async function onNetworkInstanceChange() {
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
const insts = networkStore.networkInstanceIds
+ const no_tun = networkStore.isNoTunEnabled(insts[0])
+ if (no_tun) {
+ await doStopVpn()
+ return
+ }
if (!insts) {
await doStopVpn()
return
@@ -132,14 +137,6 @@ async function onNetworkInstanceChange() {
return
}
- // if use no tun mode, stop the vpn service
- const no_tun = networkStore.isNoTunEnabled(insts[0])
- if (no_tun) {
- console.error('no tun mode, stop vpn service')
- await doStopVpn()
- return
- }
-
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
if (!network_length) {
network_length = 24
@@ -187,12 +184,26 @@ async function watchNetworkInstance() {
console.error('vpn service watch network instance')
}
+function isNoTunEnabled(instanceId: string | undefined) {
+ if (!instanceId) {
+ return false
+ }
+ const no_tun = networkStore.isNoTunEnabled(instanceId)
+ if (no_tun) {
+ return true
+ }
+ return false
+}
+
export async function initMobileVpnService() {
await registerVpnServiceListener()
await watchNetworkInstance()
}
-export async function prepareVpnService() {
+export async function prepareVpnService(instanceId: string) {
+ if (isNoTunEnabled(instanceId)) {
+ return
+ }
console.log('prepare vpn')
const prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret)))
diff --git a/easytier-gui/src/pages/index.vue b/easytier-gui/src/pages/index.vue
index 80fafa5c4..adfc58817 100644
--- a/easytier-gui/src/pages/index.vue
+++ b/easytier-gui/src/pages/index.vue
@@ -102,7 +102,7 @@ networkStore.$subscribe(async () => {
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
if (type() === 'android') {
- await prepareVpnService()
+ await prepareVpnService(cfg.instance_id)
networkStore.clearNetworkInstances()
}
else {
diff --git a/easytier-rpc-build/Cargo.toml b/easytier-rpc-build/Cargo.toml
index f4ca69324..3ed328ee7 100644
--- a/easytier-rpc-build/Cargo.toml
+++ b/easytier-rpc-build/Cargo.toml
@@ -8,7 +8,7 @@ repository = "https://github.com/EasyTier/EasyTier"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"]
-rust-version = "1.84.0"
+rust-version = "1.89.0"
license-file = "LICENSE"
readme = "README.md"
diff --git a/easytier-rpc-build/src/lib.rs b/easytier-rpc-build/src/lib.rs
index a61ff1b82..3d24dadb5 100644
--- a/easytier-rpc-build/src/lib.rs
+++ b/easytier-rpc-build/src/lib.rs
@@ -14,18 +14,11 @@ const NAMESPACE: &str = "easytier::proto::rpc_types";
///
/// See the crate-level documentation for more info.
#[allow(missing_copy_implementations)]
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
pub struct ServiceGenerator {
_private: (),
}
-impl ServiceGenerator {
- /// Create a new `ServiceGenerator` instance with the default options set.
- pub fn new() -> ServiceGenerator {
- ServiceGenerator { _private: () }
- }
-}
-
impl prost_build::ServiceGenerator for ServiceGenerator {
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
use std::fmt::Write;
@@ -78,7 +71,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
enum_methods,
" {name} = {index},",
name = method.proto_name,
- index = format!("{}", idx + 1)
+ index = idx + 1
)
.unwrap();
@@ -87,7 +80,7 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
" {index} => Ok({service_name}MethodDescriptor::{name}),",
service_name = service.name,
name = method.proto_name,
- index = format!("{}", idx + 1),
+ index = idx + 1,
)
.unwrap();
@@ -102,12 +95,12 @@ impl prost_build::ServiceGenerator for ServiceGenerator {
writeln!(
client_methods,
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
- {client_name}::{name}_inner(self.0.clone(), ctrl, input).await
+ {client_name}Client::{name}_inner(self.0.clone(), ctrl, input).await
}}"#,
name = method.name,
input_type = method.input_type,
output_type = method.output_type,
- client_name = format!("{}Client", service.name),
+ client_name = service.name,
namespace = NAMESPACE,
)
.unwrap();
diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml
index 041aee0ea..da682e916 100644
--- a/easytier-web/Cargo.toml
+++ b/easytier-web/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "easytier-web"
-version = "2.3.2"
+version = "2.4.2"
edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
diff --git a/easytier-web/build.rs b/easytier-web/build.rs
index 037ff6bdc..3de4b90a6 100644
--- a/easytier-web/build.rs
+++ b/easytier-web/build.rs
@@ -1,7 +1,10 @@
-fn main() {
- // enable thunk-rs when target os is windows and arch is x86_64 or i686
- #[cfg(target_os = "windows")]
- if !std::env::var("TARGET").unwrap_or_default().contains("aarch64"){
- thunk::thunk();
- }
-}
+fn main() {
+ // enable thunk-rs when target os is windows and arch is x86_64 or i686
+ #[cfg(target_os = "windows")]
+ if !std::env::var("TARGET")
+ .unwrap_or_default()
+ .contains("aarch64")
+ {
+ thunk::thunk();
+ }
+}
diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue
index 73ac0fb1f..71c6b687e 100644
--- a/easytier-web/frontend-lib/src/components/Config.vue
+++ b/easytier-web/frontend-lib/src/components/Config.vue
@@ -2,7 +2,13 @@
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
-import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
+import {
+ addRow,
+ DEFAULT_NETWORK_CONFIG,
+ NetworkConfig,
+ NetworkingMethod,
+ removeRow
+} from '../types/network'
import { defineProps, defineEmits, ref, } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -145,6 +151,7 @@ interface BoolFlag {
const bool_flags: BoolFlag[] = [
{ field: 'latency_first', help: 'latency_first_help' },
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
+ { field: 'disable_ipv6', help: 'disable_ipv6_help' },
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
{ field: 'enable_quic_proxy', help: 'enable_quic_proxy_help' },
@@ -162,6 +169,8 @@ const bool_flags: BoolFlag[] = [
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
]
+const portForwardProtocolOptions = ref(["tcp","udp"]);
+
@@ -415,6 +424,73 @@ const bool_flags: BoolFlag[] = [
+
+
+
+
+
+
+
+ {{ t('port_forwards_help') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml
index 96511d55f..ac8af2bd2 100644
--- a/easytier-web/frontend-lib/src/locales/cn.yaml
+++ b/easytier-web/frontend-lib/src/locales/cn.yaml
@@ -45,7 +45,7 @@ logging_copy_dir: 复制日志路径
disable_auto_launch: 关闭开机自启
enable_auto_launch: 开启开机自启
exit: 退出
-chips_placeholder: 例如: {0}, 按回车添加
+chips_placeholder: 例如: {0}, 输入后在下拉框中选择生效
hostname_placeholder: '留空默认为主机名: {0}'
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
off_text: 点击关闭
@@ -83,6 +83,9 @@ latency_first_help: 忽略中转跳数,选择总延迟最低的路径
use_smoltcp: 使用用户态协议栈
use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙问题导致无法子网代理 / KCP代理。
+disable_ipv6: 禁用IPv6
+disable_ipv6_help: 禁用此节点的IPv6功能,仅使用IPv4进行网络通信。
+
enable_kcp_proxy: 启用 KCP 代理
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
@@ -147,6 +150,12 @@ socks5_help: |
exit_nodes: 出口节点列表
exit_nodes_help: 转发所有流量的出口节点,虚拟IPv4地址,优先级由列表顺序决定
+
+port_forwards: 端口转发
+port_forwards_help: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
+port_forwards_bind_addr: "绑定地址,如:0.0.0.0"
+port_forwards_dst_addr: "目标地址,如:10.126.126.1"
+port_forwards_add_btn: "添加"
mtu: MTU
mtu_help: |
diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml
index e27aca893..a887eb44d 100644
--- a/easytier-web/frontend-lib/src/locales/en.yaml
+++ b/easytier-web/frontend-lib/src/locales/en.yaml
@@ -46,7 +46,7 @@ disable_auto_launch: Disable Launch on Reboot
enable_auto_launch: Enable Launch on Reboot
exit: Exit
use_latency_first: Latency First Mode
-chips_placeholder: 'e.g: {0}, press Enter to add'
+chips_placeholder: 'e.g: {0}, select from the dropdown after input'
hostname_placeholder: 'Leave blank and default to host name: {0}'
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
off_text: Press to disable
@@ -82,6 +82,9 @@ latency_first_help: Ignore hop count and select the path with the lowest total l
use_smoltcp: Use User-Space Protocol Stack
use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating system firewalls blocking subnet or KCP proxy functionality.
+disable_ipv6: Disable IPv6
+disable_ipv6_help: Disable IPv6 functionality for this node, only use IPv4 for network communication.
+
enable_kcp_proxy: Enable KCP Proxy
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
@@ -148,6 +151,12 @@ socks5_help: |
exit_nodes: Exit Nodes
exit_nodes_help: Exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list
+port_forwards: Port Forward
+port_forwards_help: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
+port_forwards_bind_addr: "Bind address, e.g.: 0.0.0.0"
+port_forwards_dst_addr: "Destination address, e.g.: 10.126.126.1"
+port_forwards_add_btn: "Add"
+
mtu: MTU
mtu_help: |
MTU of the TUN device, default is 1380 for non-encryption, 1360 for encryption. Range:400-1380
diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts
index 6487fc7ee..b6d1d4492 100644
--- a/easytier-web/frontend-lib/src/types/network.ts
+++ b/easytier-web/frontend-lib/src/types/network.ts
@@ -37,6 +37,7 @@ export interface NetworkConfig {
dev_name: string
use_smoltcp?: boolean
+ disable_ipv6?: boolean
enable_kcp_proxy?: boolean
disable_kcp_input?: boolean
enable_quic_proxy?: boolean
@@ -69,6 +70,8 @@ export interface NetworkConfig {
enable_private_mode?: boolean
rpc_portal_whitelists: string[]
+
+ port_forwards: PortForwardConfig[]
}
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
@@ -105,6 +108,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
dev_name: '',
use_smoltcp: false,
+ disable_ipv6: false,
enable_kcp_proxy: false,
disable_kcp_input: false,
enable_quic_proxy: false,
@@ -130,6 +134,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
enable_magic_dns: false,
enable_private_mode: false,
rpc_portal_whitelists: [],
+ port_forwards: [],
}
}
@@ -253,6 +258,30 @@ export interface PeerConnStats {
latency_us: number
}
+export interface PortForwardConfig {
+ bind_ip: string,
+ bind_port: number,
+ dst_ip: string,
+ dst_port: number,
+ proto: string
+}
+
+// 添加新行
+export const addRow = (rows: PortForwardConfig[]) => {
+ rows.push({
+ proto: 'tcp',
+ bind_ip: '',
+ bind_port: 65535,
+ dst_ip: '',
+ dst_port: 65535,
+ });
+};
+
+// 删除行
+export const removeRow = (index: number, rows: PortForwardConfig[]) => {
+ rows.splice(index, 1);
+};
+
export enum EventType {
TunDeviceReady = 'TunDeviceReady', // string
TunDeviceError = 'TunDeviceError', // string
diff --git a/easytier-web/src/client_manager/mod.rs b/easytier-web/src/client_manager/mod.rs
index 21458e855..f9e08024a 100644
--- a/easytier-web/src/client_manager/mod.rs
+++ b/easytier-web/src/client_manager/mod.rs
@@ -25,7 +25,7 @@ fn load_geoip_db(geoip_db: Option
) -> Option>>
match maxminddb::Reader::open_readfile(&path) {
Ok(reader) => {
tracing::info!("Successfully loaded GeoIP2 database from {}", path);
- return Some(reader);
+ Some(reader)
}
Err(err) => {
tracing::debug!("Failed to load GeoIP2 database from {}: {}", path, err);
@@ -207,10 +207,8 @@ impl ClientManager {
let region = city.subdivisions.map(|r| {
r.iter()
- .map(|x| x.names.as_ref())
- .flatten()
- .map(|x| x.get("zh-CN").or_else(|| x.get("en")))
- .flatten()
+ .filter_map(|x| x.names.as_ref())
+ .filter_map(|x| x.get("zh-CN").or_else(|| x.get("en")))
.map(|x| x.to_string())
.collect::>()
.join(",")
diff --git a/easytier-web/src/client_manager/session.rs b/easytier-web/src/client_manager/session.rs
index 877e7ce1f..97a22598c 100644
--- a/easytier-web/src/client_manager/session.rs
+++ b/easytier-web/src/client_manager/session.rs
@@ -94,14 +94,10 @@ impl SessionRpcService {
return Ok(HeartbeatResponse {});
};
- let machine_id: uuid::Uuid =
+ let machine_id: uuid::Uuid = req.machine_id.map(Into::into).ok_or(anyhow::anyhow!(
+ "Machine id is not set correctly, expect uuid but got: {:?}",
req.machine_id
- .clone()
- .map(Into::into)
- .ok_or(anyhow::anyhow!(
- "Machine id is not set correctly, expect uuid but got: {:?}",
- req.machine_id
- ))?;
+ ))?;
let user_id = storage
.db()
@@ -121,7 +117,7 @@ impl SessionRpcService {
if data.req.replace(req.clone()).is_none() {
assert!(data.storage_token.is_none());
data.storage_token = Some(StorageToken {
- token: req.user_token.clone().into(),
+ token: req.user_token.clone(),
client_url: data.client_url.clone(),
machine_id,
user_id,
diff --git a/easytier-web/src/client_manager/storage.rs b/easytier-web/src/client_manager/storage.rs
index 9acf6f610..cbbac7215 100644
--- a/easytier-web/src/client_manager/storage.rs
+++ b/easytier-web/src/client_manager/storage.rs
@@ -34,7 +34,7 @@ impl TryFrom for Storage {
type Error = ();
fn try_from(weak: Weak) -> Result {
- weak.upgrade().map(|inner| Storage(inner)).ok_or(())
+ weak.upgrade().map(Storage).ok_or(())
}
}
@@ -51,9 +51,7 @@ impl Storage {
machine_id: &uuid::Uuid,
client_url: &url::Url,
) {
- map.remove_if(&machine_id, |_, v| {
- v.storage_token.client_url == *client_url
- });
+ map.remove_if(machine_id, |_, v| v.storage_token.client_url == *client_url);
}
fn update_mid_to_client_info_map(
@@ -74,11 +72,7 @@ impl Storage {
}
pub fn update_client(&self, stoken: StorageToken, report_time: i64) {
- let inner = self
- .0
- .user_clients_map
- .entry(stoken.user_id)
- .or_insert_with(DashMap::new);
+ let inner = self.0.user_clients_map.entry(stoken.user_id).or_default();
let client_info = ClientInfo {
storage_token: stoken.clone(),
diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs
index a238bbf35..b5949567e 100644
--- a/easytier-web/src/main.rs
+++ b/easytier-web/src/main.rs
@@ -151,7 +151,7 @@ async fn get_dual_stack_listener(
} else {
None
};
- let v4_listener = if let Ok(_) = local_ipv4().await {
+ let v4_listener = if local_ipv4().await.is_ok() {
get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok()
} else {
None
diff --git a/easytier-web/src/restful/auth.rs b/easytier-web/src/restful/auth.rs
index 8bb3aad0c..9d0fffefd 100644
--- a/easytier-web/src/restful/auth.rs
+++ b/easytier-web/src/restful/auth.rs
@@ -137,7 +137,7 @@ mod post {
mod get {
use crate::restful::{
captcha::{
- captcha::spec::SpecCaptcha,
+ builder::spec::SpecCaptcha,
extension::{axum_tower_sessions::CaptchaAxumTowerSessionExt as _, CaptchaUtil},
NewCaptcha as _,
},
diff --git a/easytier-web/src/restful/captcha/base/captcha.rs b/easytier-web/src/restful/captcha/base/captcha.rs
index c7e45c875..3bed68e70 100644
--- a/easytier-web/src/restful/captcha/base/captcha.rs
+++ b/easytier-web/src/restful/captcha/base/captcha.rs
@@ -46,22 +46,22 @@ pub(crate) struct Captcha {
/// 验证码文本类型 The character type of the captcha
pub enum CaptchaType {
/// 字母数字混合
- TypeDefault = 1,
+ Default = 1,
/// 纯数字
- TypeOnlyNumber,
+ OnlyNumber,
/// 纯字母
- TypeOnlyChar,
+ OnlyChar,
/// 纯大写字母
- TypeOnlyUpper,
+ OnlyUpper,
/// 纯小写字母
- TypeOnlyLower,
+ OnlyLower,
/// 数字大写字母
- TypeNumAndUpper,
+ NumAndUpper,
}
/// 内置字体 Fonts shipped with the library
@@ -92,29 +92,29 @@ impl Captcha {
/// 生成随机验证码
pub fn alphas(&mut self) -> Vec {
let mut cs = vec!['\0'; self.len];
- for i in 0..self.len {
+ for cs_i in cs.iter_mut() {
match self.char_type {
- CaptchaType::TypeDefault => cs[i] = self.randoms.alpha(),
- CaptchaType::TypeOnlyNumber => {
- cs[i] = self.randoms.alpha_under(self.randoms.num_max_index)
+ CaptchaType::Default => *cs_i = self.randoms.alpha(),
+ CaptchaType::OnlyNumber => {
+ *cs_i = self.randoms.alpha_under(self.randoms.num_max_index)
}
- CaptchaType::TypeOnlyChar => {
- cs[i] = self
+ CaptchaType::OnlyChar => {
+ *cs_i = self
.randoms
.alpha_between(self.randoms.char_min_index, self.randoms.char_max_index)
}
- CaptchaType::TypeOnlyUpper => {
- cs[i] = self
+ CaptchaType::OnlyUpper => {
+ *cs_i = self
.randoms
.alpha_between(self.randoms.upper_min_index, self.randoms.upper_max_index)
}
- CaptchaType::TypeOnlyLower => {
- cs[i] = self
+ CaptchaType::OnlyLower => {
+ *cs_i = self
.randoms
.alpha_between(self.randoms.lower_min_index, self.randoms.lower_max_index)
}
- CaptchaType::TypeNumAndUpper => {
- cs[i] = self.randoms.alpha_under(self.randoms.upper_max_index)
+ CaptchaType::NumAndUpper => {
+ *cs_i = self.randoms.alpha_under(self.randoms.upper_max_index)
}
}
}
@@ -142,7 +142,7 @@ impl Captcha {
}
}
- pub fn get_font(&mut self) -> Arc {
+ pub fn get_font(&'_ mut self) -> Arc> {
if let Some(font) = font::get_font(&self.font_name) {
font
} else {
@@ -185,6 +185,7 @@ where
/// 特别地/In particular:
///
/// - 对算术验证码[ArithmeticCaptcha](crate::captcha::arithmetic::ArithmeticCaptcha)而言,这里的`len`是验证码中数字的数量。
+ ///
/// For [ArithmeticCaptcha](crate::captcha::arithmetic::ArithmeticCaptcha), the `len` presents the count of the digits
/// in the Captcha.
fn with_size_and_len(width: i32, height: i32, len: usize) -> Self;
@@ -226,7 +227,7 @@ impl NewCaptcha for Captcha {
let len = 5;
let width = 130;
let height = 48;
- let char_type = CaptchaType::TypeDefault;
+ let char_type = CaptchaType::Default;
let chars = None;
Self {
diff --git a/easytier-web/src/restful/captcha/base/randoms.rs b/easytier-web/src/restful/captcha/base/randoms.rs
index ed78579c7..956616d06 100644
--- a/easytier-web/src/restful/captcha/base/randoms.rs
+++ b/easytier-web/src/restful/captcha/base/randoms.rs
@@ -1,6 +1,4 @@
-
-use rand::{random};
-
+use rand::random;
/// 随机数工具类
pub(crate) struct Randoms {
diff --git a/easytier-web/src/restful/captcha/captcha/mod.rs b/easytier-web/src/restful/captcha/builder/mod.rs
similarity index 100%
rename from easytier-web/src/restful/captcha/captcha/mod.rs
rename to easytier-web/src/restful/captcha/builder/mod.rs
diff --git a/easytier-web/src/restful/captcha/captcha/spec.rs b/easytier-web/src/restful/captcha/builder/spec.rs
similarity index 100%
rename from easytier-web/src/restful/captcha/captcha/spec.rs
rename to easytier-web/src/restful/captcha/builder/spec.rs
diff --git a/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs b/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs
index 81597dbe9..a630282b3 100644
--- a/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs
+++ b/easytier-web/src/restful/captcha/extension/axum_tower_sessions.rs
@@ -10,7 +10,7 @@ use axum::response::Response;
use std::fmt::Debug;
use tower_sessions::Session;
-const CAPTCHA_KEY: &'static str = "ez-captcha";
+const CAPTCHA_KEY: &str = "ez-captcha";
/// Axum & Tower_Sessions
#[async_trait]
@@ -32,7 +32,7 @@ pub trait CaptchaAxumTowerSessionStaticExt {
/// Verify the Captcha code, and return whether user's code is correct.
async fn ver(code: &str, session: &Session) -> bool {
match session.get::(CAPTCHA_KEY).await {
- Ok(Some(ans)) => ans.to_ascii_lowercase() == code.to_ascii_lowercase(),
+ Ok(Some(ans)) => ans.eq_ignore_ascii_case(code),
_ => false,
}
}
diff --git a/easytier-web/src/restful/captcha/extension/mod.rs b/easytier-web/src/restful/captcha/extension/mod.rs
index c11a9766f..ada8a980b 100644
--- a/easytier-web/src/restful/captcha/extension/mod.rs
+++ b/easytier-web/src/restful/captcha/extension/mod.rs
@@ -1,7 +1,7 @@
pub mod axum_tower_sessions;
use super::base::captcha::AbstractCaptcha;
-use super::captcha::spec::SpecCaptcha;
+use super::builder::spec::SpecCaptcha;
use super::{CaptchaFont, NewCaptcha};
/// 验证码工具类 - Captcha Utils
diff --git a/easytier-web/src/restful/captcha/mod.rs b/easytier-web/src/restful/captcha/mod.rs
index 6f504e4d5..9fed9d914 100644
--- a/easytier-web/src/restful/captcha/mod.rs
+++ b/easytier-web/src/restful/captcha/mod.rs
@@ -117,7 +117,7 @@
#![allow(dead_code)]
pub(crate) mod base;
-pub mod captcha;
+pub mod builder;
pub mod extension;
mod utils;
diff --git a/easytier-web/src/restful/captcha/utils/color.rs b/easytier-web/src/restful/captcha/utils/color.rs
index a5345c787..bf9bde4c9 100644
--- a/easytier-web/src/restful/captcha/utils/color.rs
+++ b/easytier-web/src/restful/captcha/utils/color.rs
@@ -32,21 +32,24 @@ impl From<(u8, u8, u8)> for Color {
}
}
-impl Into<(u8, u8, u8, u8)> for Color {
- fn into(self) -> (u8, u8, u8, u8) {
+impl From for (u8, u8, u8, u8) {
+ fn from(val: Color) -> Self {
(
- (self.0 * 255.0) as u8,
- (self.1 * 255.0) as u8,
- (self.2 * 255.0) as u8,
- (self.3 * 255.0) as u8,
+ (val.0 * 255.0) as u8,
+ (val.1 * 255.0) as u8,
+ (val.2 * 255.0) as u8,
+ (val.3 * 255.0) as u8,
)
}
}
-impl Into for Color {
- fn into(self) -> u32 {
- let color: (u8, u8, u8, u8) = self.into();
- (color.0 as u32) << 24 + (color.1 as u32) << 16 + (color.2 as u32) << 8 + (color.3 as u32)
+impl From for u32 {
+ fn from(val: Color) -> Self {
+ let color: (u8, u8, u8, u8) = val.into();
+ (color.0 as u32)
+ << (24 + (color.1 as u32))
+ << (16 + (color.2 as u32))
+ << (8 + (color.3 as u32))
}
}
diff --git a/easytier-web/src/restful/captcha/utils/font.rs b/easytier-web/src/restful/captcha/utils/font.rs
index 6bce05ce0..5294fd320 100644
--- a/easytier-web/src/restful/captcha/utils/font.rs
+++ b/easytier-web/src/restful/captcha/utils/font.rs
@@ -11,7 +11,7 @@ struct FontAssets;
// pub(crate) static ref FONTS: RwLock>> = Default::default();
// }
-pub fn get_font(font_name: &str) -> Option> {
+pub fn get_font(font_name: &'_ str) -> Option>> {
// let fonts_cell = FONTS.get_or_init(|| Default::default());
// let guard = fonts_cell.read();
//
@@ -31,7 +31,7 @@ pub fn get_font(font_name: &str) -> Option> {
// }
}
-pub fn load_font(font_name: &str) -> Result, Box> {
+pub fn load_font(font_name: &'_ str) -> Result>, Box> {
match FontAssets::get(font_name) {
Some(assets) => {
let font = Font::try_from_vec(Vec::from(assets.data)).unwrap();
diff --git a/easytier-web/src/restful/mod.rs b/easytier-web/src/restful/mod.rs
index d21709179..ba29da685 100644
--- a/easytier-web/src/restful/mod.rs
+++ b/easytier-web/src/restful/mod.rs
@@ -143,7 +143,7 @@ impl RestfulServer {
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
};
- let machines = client_mgr.list_machine_by_user_id(user.id().clone()).await;
+ let machines = client_mgr.list_machine_by_user_id(user.id()).await;
Ok(GetSummaryJsonResp {
device_count: machines.len() as u32,
diff --git a/easytier-web/src/restful/network.rs b/easytier-web/src/restful/network.rs
index e8b08d16d..9a103cc53 100644
--- a/easytier-web/src/restful/network.rs
+++ b/easytier-web/src/restful/network.rs
@@ -8,7 +8,7 @@ use axum_login::AuthUser;
use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void;
use easytier::proto::rpc_types::controller::BaseController;
-use easytier::proto::web::*;
+use easytier::proto::{self, web::*};
use crate::client_manager::session::{Location, Session};
use crate::client_manager::ClientManager;
@@ -85,7 +85,7 @@ impl NetworkApi {
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
return Err((
StatusCode::UNAUTHORIZED,
- other_error(format!("No user id found")).into(),
+ other_error("No user id found".to_string()).into(),
));
};
Ok(user_id)
@@ -108,7 +108,7 @@ impl NetworkApi {
let Some(token) = result.get_token().await else {
return Err((
StatusCode::UNAUTHORIZED,
- other_error(format!("No token reported")).into(),
+ other_error("No token reported".to_string()).into(),
));
};
@@ -120,7 +120,7 @@ impl NetworkApi {
{
return Err((
StatusCode::FORBIDDEN,
- other_error(format!("Token mismatch")).into(),
+ other_error("Token mismatch".to_string()).into(),
));
}
@@ -177,7 +177,7 @@ impl NetworkApi {
.insert_or_update_user_network_config(
auth_session.user.as_ref().unwrap().id(),
machine_id,
- resp.inst_id.clone().unwrap_or_default().into(),
+ resp.inst_id.unwrap_or_default().into(),
serde_json::to_string(&config).unwrap(),
)
.await
@@ -248,7 +248,7 @@ impl NetworkApi {
.await
.map_err(convert_rpc_error)?;
- let running_inst_ids = ret.inst_ids.clone().into_iter().map(Into::into).collect();
+ let running_inst_ids = ret.inst_ids.clone().into_iter().collect();
// collect networks that are disabled
let disabled_inst_ids = client_mgr
@@ -261,7 +261,7 @@ impl NetworkApi {
.await
.map_err(convert_db_error)?
.iter()
- .filter_map(|x| x.network_instance_id.clone().try_into().ok())
+ .map(|x| Into::::into(x.network_instance_id.clone()))
.collect::>();
Ok(ListNetworkInstanceIdsJsonResp {
@@ -330,9 +330,8 @@ impl NetworkApi {
// not implement disable all
return Err((
StatusCode::NOT_IMPLEMENTED,
- other_error(format!("Not implemented")).into(),
- ))
- .into();
+ other_error("Not implemented".to_string()).into(),
+ ));
};
let sess = Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
diff --git a/easytier-web/src/restful/users.rs b/easytier-web/src/restful/users.rs
index 1e879ae50..b489a1454 100644
--- a/easytier-web/src/restful/users.rs
+++ b/easytier-web/src/restful/users.rs
@@ -76,32 +76,32 @@ impl Backend {
pub async fn register_new_user(&self, new_user: &RegisterNewUser) -> anyhow::Result<()> {
let hashed_password = password_auth::generate_hash(new_user.credentials.password.as_str());
- let mut txn = self.db.orm_db().begin().await?;
+ let txn = self.db.orm_db().begin().await?;
entity::users::ActiveModel {
username: Set(new_user.credentials.username.clone()),
password: Set(hashed_password.clone()),
..Default::default()
}
- .save(&mut txn)
+ .save(&txn)
.await?;
entity::users_groups::ActiveModel {
user_id: Set(entity::users::Entity::find()
.filter(entity::users::Column::Username.eq(new_user.credentials.username.as_str()))
- .one(&mut txn)
+ .one(&txn)
.await?
.unwrap()
.id),
group_id: Set(entity::groups::Entity::find()
.filter(entity::groups::Column::Name.eq("users"))
- .one(&mut txn)
+ .one(&txn)
.await?
.unwrap()
.id),
..Default::default()
}
- .save(&mut txn)
+ .save(&txn)
.await?;
txn.commit().await?;
diff --git a/easytier-web/src/web/mod.rs b/easytier-web/src/web/mod.rs
index 7f0590d10..eabe307ec 100644
--- a/easytier-web/src/web/mod.rs
+++ b/easytier-web/src/web/mod.rs
@@ -52,9 +52,7 @@ pub fn build_router(api_host: Option) -> Router {
router
};
- let router = router.fallback_service(service);
-
- router
+ router.fallback_service(service)
}
pub struct WebServer {
diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml
index 247f5c3b1..57b3298ac 100644
--- a/easytier/Cargo.toml
+++ b/easytier/Cargo.toml
@@ -3,12 +3,12 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier"
-version = "2.3.2"
+version = "2.4.2"
edition = "2021"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"]
-rust-version = "1.84.0"
+rust-version = "1.89.0"
license-file = "LICENSE"
readme = "README.md"
@@ -27,6 +27,7 @@ readme = "README.md"
name = "easytier"
path = "src/lib.rs"
crate-type = ["cdylib"]
+test = false
[dependencies]
lazy_static = "1.4.0"
@@ -43,6 +44,7 @@ tracing-appender = "0.2.3"
thiserror = "1.0"
auto_impl = "1.1.0"
crossbeam = "0.8.4"
+arc-swap = "1.7"
time = "0.3"
toml = "0.8.12"
chrono = { version = "0.4.37", features = ["serde"] }
@@ -88,11 +90,11 @@ http = { version = "1", default-features = false, features = [
tokio-rustls = { version = "0.26", default-features = false, optional = true }
# for tap device
-tun = { package = "tun-easytier", version = "1.1.1", features = [
+tun = { package = "tun-easytier", git="https://github.com/EasyTier/rust-tun", features = [
"async",
], optional = true }
# for net ns
-nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net"] }
+nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net", "fs"] }
uuid = { version = "1.5.0", features = [
"v4",
@@ -117,7 +119,7 @@ byteorder = "1.5.0"
# for proxy
cidr = { version = "0.2.2", features = ["serde"] }
-socket2 = "0.5.5"
+socket2 = { version = "0.5.10", features = ["all"] }
# for hole punching
stun_codec = "0.3.4"
@@ -135,6 +137,7 @@ clap = { version = "4.5.30", features = [
"wrap_help",
"env",
] }
+clap_complete = { version = "4.5.55" }
async-recursion = "1.0.5"
@@ -151,6 +154,7 @@ boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true
ring = { version = "0.17", optional = true }
bitflags = "2.5"
aes-gcm = { version = "0.10.3", optional = true }
+openssl = { version = "0.10", optional = true, features = ["vendored"] }
# for cli
tabled = "0.16"
@@ -192,7 +196,7 @@ service-manager = { git = "https://github.com/chipsenkbeil/service-manager-rs.gi
zstd = { version = "0.13" }
-kcp-sys = { git = "https://github.com/EasyTier/kcp-sys" }
+kcp-sys = { git = "https://github.com/EasyTier/kcp-sys", rev = "0f0a0558391ba391c089806c23f369651f6c9eeb" }
prost-reflect = { version = "0.14.5", default-features = false, features = [
"derive",
@@ -215,14 +219,6 @@ humantime-serde = "1.1.1"
multimap = "0.10.0"
version-compare = "0.2.0"
-jemallocator = { version = "0.5.4", optional = true }
-jemalloc-ctl = { version = "0.5.4", optional = true }
-jemalloc-sys = { version = "0.5.4", features = [
- "stats",
- "profiling",
- "unprefixed_malloc_on_supported_platforms",
-], optional = true }
-
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3"
@@ -249,6 +245,24 @@ windows = { version = "0.52.0", features = [
encoding = "0.2"
winreg = "0.52"
windows-service = "0.7.0"
+windows-sys = { version = "0.52", features = [
+ "Win32_NetworkManagement_IpHelper",
+ "Win32_NetworkManagement_Ndis",
+ "Win32_Networking_WinSock",
+ "Win32_Foundation"
+]}
+winapi = { version = "0.3.9", features = ["impl-default"] }
+
+[target.'cfg(not(windows))'.dependencies]
+jemallocator = { package = "tikv-jemallocator", version = "0.6.0", optional = true, features = [
+ "unprefixed_malloc_on_supported_platforms"
+] }
+jemalloc-ctl = { package = "tikv-jemalloc-ctl", version = "0.6.0", optional = true, features = [
+] }
+jemalloc-sys = { package = "tikv-jemalloc-sys", version = "0.6.0", features = [
+ "background_threads_runtime_support",
+ "background_threads",
+], optional = true }
[build-dependencies]
tonic-build = "0.12"
@@ -284,12 +298,12 @@ tokio-socks = "0.5.2"
[features]
-default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5", "quic"]
+default = ["wireguard", "websocket", "smoltcp", "tun", "socks5", "quic"]
full = [
"websocket",
"wireguard",
- "mimalloc",
"aes-gcm",
+ "openssl-crypto", # need openssl-dev libs
"smoltcp",
"tun",
"socks5",
@@ -298,6 +312,7 @@ wireguard = ["dep:boringtun", "dep:ring"]
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
mimalloc = ["dep:mimalloc"]
aes-gcm = ["dep:aes-gcm"]
+openssl-crypto = ["dep:openssl"]
tun = ["dep:tun"]
websocket = [
"dep:tokio-websockets",
@@ -308,4 +323,5 @@ websocket = [
]
smoltcp = ["dep:smoltcp", "dep:parking_lot"]
socks5 = ["dep:smoltcp"]
-jemalloc = ["dep:jemallocator", "dep:jemalloc-ctl", "dep:jemalloc-sys"]
+jemalloc = ["dep:jemallocator", "dep:jemalloc-sys"]
+jemalloc-prof = ["jemalloc", "dep:jemalloc-ctl", "jemalloc-ctl/stats", "jemalloc-sys/profiling", "jemalloc-sys/stats"]
diff --git a/easytier/build.rs b/easytier/build.rs
index aed1655b8..682738709 100644
--- a/easytier/build.rs
+++ b/easytier/build.rs
@@ -116,7 +116,7 @@ fn check_locale() {
if let Ok(globs) = globwalk::glob(locale_path) {
for entry in globs {
if let Err(e) = entry {
- println!("cargo:i18n-error={}", e);
+ println!("cargo:i18n-error={e}");
continue;
}
@@ -147,15 +147,17 @@ fn main() -> Result<(), Box> {
"src/proto/cli.proto",
"src/proto/web.proto",
"src/proto/magic_dns.proto",
+ "src/proto/acl.proto",
];
for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) {
- println!("cargo:rerun-if-changed={}", proto_file);
+ println!("cargo:rerun-if-changed={proto_file}");
}
let mut config = prost_build::Config::new();
config
.protoc_arg("--experimental_allow_proto3_optional")
+ .type_attribute(".acl", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".cli", "#[derive(serde::Serialize, serde::Deserialize)]")
@@ -171,7 +173,7 @@ fn main() -> Result<(), Box> {
.field_attribute(".web.NetworkConfig", "#[serde(default)]")
.service_generator(Box::new(rpc_build::ServiceGenerator::new()))
.btree_map(["."])
- .skip_debug(&[".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
+ .skip_debug([".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
config.compile_protos(&proto_files, &["src/proto/"])?;
diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml
index fd840074c..67a07437c 100644
--- a/easytier/locales/app.yml
+++ b/easytier/locales/app.yml
@@ -18,6 +18,9 @@ core_clap:
config_file:
en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
+ generate_completions:
+ en: "generate shell completions"
+ zh-CN: "生成 shell 补全脚本"
network_name:
en: "network name to identify this vpn network"
zh-CN: "用于标识此VPN网络的网络名称"
@@ -27,6 +30,9 @@ core_clap:
ipv4:
en: "ipv4 address of this vpn node, if empty, this node will only forward packets and no TUN device will be created"
zh-CN: "此VPN节点的IPv4地址,如果为空,则此节点将仅转发数据包,不会创建TUN设备"
+ ipv6:
+ en: "ipv6 address of this vpn node, can be used together with ipv4 for dual-stack operation"
+ zh-CN: "此VPN节点的IPv6地址,可与IPv4一起使用以进行双栈操作"
dhcp:
en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed."
zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。"
@@ -89,9 +95,15 @@ core_clap:
disable_encryption:
en: "disable encryption for peers communication, default is false, must be same with peers"
zh-CN: "禁用对等节点通信的加密,默认为false,必须与对等节点相同"
+ encryption_algorithm:
+ en: "encryption algorithm to use, supported: '', 'xor', 'chacha20', 'aes-gcm', 'aes-gcm-256', 'openssl-aes128-gcm', 'openssl-aes256-gcm', 'openssl-chacha20'. Empty string means default (aes-gcm)"
+ zh-CN: "要使用的加密算法,支持:''(默认aes-gcm)、'xor'、'chacha20'、'aes-gcm'、'aes-gcm-256'、'openssl-aes128-gcm'、'openssl-aes256-gcm'、'openssl-chacha20'"
multi_thread:
en: "use multi-thread runtime, default is single-thread"
zh-CN: "使用多线程运行时,默认为单线程"
+ multi_thread_count:
+ en: "the number of threads to use, default is 2, only effective when multi-thread is enabled, must be greater than 2"
+ zh-CN: "使用的线程数,默认为2,仅在多线程模式下有效。取值必须大于2"
disable_ipv6:
en: "do not use ipv6"
zh-CN: "不使用IPv6"
@@ -178,6 +190,21 @@ core_clap:
private_mode:
en: "if true, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node."
zh-CN: "如果为true,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转"
+ foreign_relay_bps_limit:
+ en: "the maximum bps limit for foreign network relay, default is no limit. unit: BPS (bytes per second)"
+ zh-CN: "作为共享节点时,限制非本地网络的流量转发速率,默认无限制,单位 BPS (字节每秒)"
+ tcp_whitelist:
+ en: "tcp port whitelist. Supports single ports (80) and ranges (8000-9000)"
+ zh-CN: "TCP 端口白名单。支持单个端口(80)和范围(8000-9000)"
+ udp_whitelist:
+ en: "udp port whitelist. Supports single ports (53) and ranges (5000-6000)"
+ zh-CN: "UDP 端口白名单。支持单个端口(53)和范围(5000-6000)"
+ disable_relay_kcp:
+ en: "if true, disable relay kcp packets. avoid consuming too many bandwidth. default is false"
+ zh-CN: "如果为true,则禁止节点转发 KCP 数据包,防止过度消耗流量。默认值为false"
+ enable_relay_foreign_network_kcp:
+ en: "if true, allow relay kcp packets from foreign network. default is false (not forward foreign network kcp packets)"
+ zh-CN: "如果为true,则作为共享节点时也可以转发其他网络的 KCP 数据包。默认值为false(不转发)"
core_app:
panic_backtrace_save:
diff --git a/easytier/src/arch/windows.rs b/easytier/src/arch/windows.rs
index 904de872c..1c7799cad 100644
--- a/easytier/src/arch/windows.rs
+++ b/easytier/src/arch/windows.rs
@@ -7,8 +7,9 @@ use windows::{
Win32::{
Foundation::{BOOL, FALSE},
NetworkManagement::WindowsFirewall::{
- INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_PRIVATE,
- NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT,
+ INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_DOMAIN,
+ NET_FW_PROFILE2_PRIVATE, NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN,
+ NET_FW_RULE_DIR_OUT,
},
Networking::WinSock::{
htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6,
@@ -164,7 +165,7 @@ impl Drop for ComInitializer {
pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> {
let _com = ComInitializer::new()?;
- // 创建防火墙策略实例
+ // Create firewall policy instance
let policy: INetFwPolicy2 = unsafe {
CoCreateInstance(
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
@@ -173,7 +174,7 @@ pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> {
)
}?;
- // 创建防火墙规则实例
+ // Create firewall rule instance
let rule: INetFwRule = unsafe {
CoCreateInstance(
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
@@ -182,7 +183,7 @@ pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> {
)
}?;
- // 设置规则属性
+ // Set rule properties
let exe_path = std::env::current_exe()
.with_context(|| "Failed to get current executable path when adding firewall rule")?
.to_string_lossy()
@@ -202,17 +203,19 @@ pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> {
rule.SetApplicationName(&app_path)?;
rule.SetAction(NET_FW_ACTION_ALLOW)?;
if inbound {
- rule.SetDirection(NET_FW_RULE_DIR_IN)?; // 允许入站连接
+ rule.SetDirection(NET_FW_RULE_DIR_IN)?; // Allow inbound connections
} else {
- rule.SetDirection(NET_FW_RULE_DIR_OUT)?; // 允许出站连接
+ rule.SetDirection(NET_FW_RULE_DIR_OUT)?; // Allow outbound connections
}
rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?;
- rule.SetProfiles(NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0)?;
+ rule.SetProfiles(
+ NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0 | NET_FW_PROFILE2_DOMAIN.0,
+ )?;
rule.SetGrouping(&BSTR::from("EasyTier"))?;
- // 获取规则集合并添加新规则
+ // Get rule collection and add new rule
let rules = policy.Rules()?;
- rules.Remove(&name)?; // 先删除同名规则
+ rules.Remove(&name)?; // Remove existing rule with same name first
rules.Add(&rule)?;
}
@@ -225,6 +228,266 @@ pub fn add_self_to_firewall_allowlist() -> anyhow::Result<()> {
Ok(())
}
+/// Add firewall rules for specified network interface to allow all traffic
+pub fn add_interface_to_firewall_allowlist(interface_name: &str) -> anyhow::Result<()> {
+ let _com = ComInitializer::new()?;
+
+ // Create firewall policy instance
+ let policy: INetFwPolicy2 = unsafe {
+ CoCreateInstance(
+ &windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
+ None,
+ CLSCTX_ALL,
+ )
+ }?;
+
+ tracing::info!(
+ "Adding comprehensive firewall rules for interface: {}",
+ interface_name
+ );
+
+ // Create rules for each protocol type
+ add_protocol_firewall_rules(&policy, interface_name, "TCP", 6)?; // TCP protocol number 6
+ tracing::debug!("Added TCP firewall rules for interface: {}", interface_name);
+
+ add_protocol_firewall_rules(&policy, interface_name, "UDP", 17)?; // UDP protocol number 17
+ tracing::debug!("Added UDP firewall rules for interface: {}", interface_name);
+
+ add_protocol_firewall_rules(&policy, interface_name, "ICMP", 1)?; // ICMP protocol number 1
+ tracing::debug!(
+ "Added ICMP firewall rules for interface: {}",
+ interface_name
+ );
+
+ // Add fallback rules for all protocols
+ add_all_protocols_firewall_rules(&policy, interface_name)?;
+ tracing::debug!(
+ "Added fallback all-protocols rules for interface: {}",
+ interface_name
+ );
+
+ tracing::info!(
+ "Successfully created all firewall rules for interface: {}",
+ interface_name
+ );
+
+ Ok(())
+}
+
+/// Add firewall rules for a specific protocol
+fn add_protocol_firewall_rules(
+ policy: &INetFwPolicy2,
+ interface_name: &str,
+ protocol_name: &str,
+ protocol_number: i32,
+) -> anyhow::Result<()> {
+ // Create rules for both inbound and outbound traffic
+ for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
+ // Create firewall rule instance
+ let rule: INetFwRule = unsafe {
+ CoCreateInstance(
+ &windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
+ None,
+ CLSCTX_ALL,
+ )
+ }?;
+
+ let rule_name = format!(
+ "EasyTier {} - {} Protocol ({})",
+ interface_name, protocol_name, direction_name
+ );
+ let description = format!(
+ "Allow {} traffic on EasyTier interface {}",
+ protocol_name, interface_name
+ );
+
+ let name_bstr = BSTR::from(&rule_name);
+ let desc_bstr = BSTR::from(&description);
+
+ unsafe {
+ rule.SetName(&name_bstr)?;
+ rule.SetDescription(&desc_bstr)?;
+ rule.SetProtocol(protocol_number)?;
+ rule.SetAction(NET_FW_ACTION_ALLOW)?;
+
+ if is_inbound {
+ rule.SetDirection(NET_FW_RULE_DIR_IN)?;
+ } else {
+ rule.SetDirection(NET_FW_RULE_DIR_OUT)?;
+ }
+
+ rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?;
+ rule.SetProfiles(
+ NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0 | NET_FW_PROFILE2_DOMAIN.0,
+ )?;
+ rule.SetGrouping(&BSTR::from("EasyTier"))?;
+
+ // Get rule collection and add new rule
+ let rules = policy.Rules()?;
+ rules.Remove(&name_bstr)?; // Remove existing rule with same name first
+ rules.Add(&rule)?;
+ }
+ }
+
+ Ok(())
+}
+
+/// Add fallback rules for all protocols
+fn add_all_protocols_firewall_rules(
+ policy: &INetFwPolicy2,
+ interface_name: &str,
+) -> anyhow::Result<()> {
+ // Create rules for both inbound and outbound traffic
+ for (is_inbound, direction_name) in [(true, "Inbound"), (false, "Outbound")] {
+ // Create firewall rule instance
+ let rule: INetFwRule = unsafe {
+ CoCreateInstance(
+ &windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
+ None,
+ CLSCTX_ALL,
+ )
+ }?;
+
+ let rule_name = format!(
+ "EasyTier {} - All Protocols ({})",
+ interface_name, direction_name
+ );
+ let description = format!(
+ "Allow all protocol traffic on EasyTier interface {}",
+ interface_name
+ );
+
+ let name_bstr = BSTR::from(&rule_name);
+ let desc_bstr = BSTR::from(&description);
+
+ unsafe {
+ rule.SetName(&name_bstr)?;
+ rule.SetDescription(&desc_bstr)?;
+ // Don't set protocol - allows all protocols by default
+ rule.SetAction(NET_FW_ACTION_ALLOW)?;
+
+ if is_inbound {
+ rule.SetDirection(NET_FW_RULE_DIR_IN)?;
+ } else {
+ rule.SetDirection(NET_FW_RULE_DIR_OUT)?;
+ }
+
+ rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?;
+ rule.SetProfiles(
+ NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0 | NET_FW_PROFILE2_DOMAIN.0,
+ )?;
+ rule.SetGrouping(&BSTR::from("EasyTier"))?;
+
+ // Get rule collection and add new rule
+ let rules = policy.Rules()?;
+ rules.Remove(&name_bstr)?; // Remove existing rule with same name first
+ rules.Add(&rule)?;
+ }
+ }
+
+ Ok(())
+}
+
+/// Remove firewall rules for specified interface
+pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<()> {
+ let _com = ComInitializer::new()?;
+
+ let policy: INetFwPolicy2 = unsafe {
+ CoCreateInstance(
+ &windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
+ None,
+ CLSCTX_ALL,
+ )
+ }?;
+
+ let rules = unsafe { policy.Rules()? };
+
+ // Remove protocol-specific rules
+ for protocol_name in ["TCP", "UDP", "ICMP"] {
+ for direction in ["Inbound", "Outbound"] {
+ let rule_name = format!(
+ "EasyTier {} - {} Protocol ({})",
+ interface_name, protocol_name, direction
+ );
+ let name_bstr = BSTR::from(&rule_name);
+ unsafe {
+ let _ = rules.Remove(&name_bstr); // Ignore errors, rule might not exist
+ }
+ }
+ }
+
+ // Remove fallback protocol rules
+ for direction in ["Inbound", "Outbound"] {
+ let rule_name = format!(
+ "EasyTier {} - All Protocols ({})",
+ interface_name, direction
+ );
+ let name_bstr = BSTR::from(&rule_name);
+ unsafe {
+ let _ = rules.Remove(&name_bstr); // Ignore errors, rule might not exist
+ }
+ }
+
+ Ok(())
+}
+
+/// List EasyTier firewall rules for specified interface (for debugging)
+#[allow(dead_code)]
+pub fn list_interface_firewall_rules(interface_name: &str) -> anyhow::Result> {
+ let _com = ComInitializer::new()?;
+
+ let policy: INetFwPolicy2 = unsafe {
+ CoCreateInstance(
+ &windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
+ None,
+ CLSCTX_ALL,
+ )
+ }?;
+
+ let rules = unsafe { policy.Rules()? };
+ let mut found_rules = Vec::new();
+
+ // Check protocol-specific rules
+ for protocol_name in ["TCP", "UDP", "ICMP"] {
+ for direction in ["Inbound", "Outbound"] {
+ let rule_name = format!(
+ "EasyTier {} - {} Protocol ({})",
+ interface_name, protocol_name, direction
+ );
+ if check_rule_exists(&rules, &rule_name)? {
+ found_rules.push(rule_name);
+ }
+ }
+ }
+
+ // Check fallback protocol rules
+ for direction in ["Inbound", "Outbound"] {
+ let rule_name = format!(
+ "EasyTier {} - All Protocols ({})",
+ interface_name, direction
+ );
+ if check_rule_exists(&rules, &rule_name)? {
+ found_rules.push(rule_name);
+ }
+ }
+
+ Ok(found_rules)
+}
+
+/// Check if a firewall rule with specified name exists
+fn check_rule_exists(
+ rules: &windows::Win32::NetworkManagement::WindowsFirewall::INetFwRules,
+ rule_name: &str,
+) -> anyhow::Result {
+ let name_bstr = BSTR::from(rule_name);
+ unsafe {
+ match rules.Item(&name_bstr) {
+ Ok(_) => Ok(true),
+ Err(_) => Ok(false),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -234,4 +497,71 @@ mod tests {
let res = add_self_to_firewall_allowlist();
assert!(res.is_ok());
}
+
+ #[test]
+ #[ignore] // Requires administrator privileges, ignored by default
+ fn test_interface_firewall_rules() {
+ let test_interface = "test_interface";
+
+ // Add firewall rules
+ let add_result = add_interface_to_firewall_allowlist(test_interface);
+ assert!(
+ add_result.is_ok(),
+ "Failed to add interface firewall rules: {:?}",
+ add_result
+ );
+
+ println!(
+ "✓ Added comprehensive firewall rules for interface: {}",
+ test_interface
+ );
+
+ // Verify rules were created
+ let rules = list_interface_firewall_rules(test_interface).unwrap();
+ println!("Created {} firewall rules:", rules.len());
+ for rule in &rules {
+ println!(" - {}", rule);
+ }
+
+ // Verify required protocol rules are all created
+ let expected_protocols = ["TCP", "UDP", "ICMP"];
+ let expected_directions = ["Inbound", "Outbound"];
+
+ for protocol in &expected_protocols {
+ for direction in &expected_directions {
+ let rule_name = format!(
+ "EasyTier {} - {} Protocol ({})",
+ test_interface, protocol, direction
+ );
+ assert!(
+ rules.contains(&rule_name),
+ "Missing required rule: {}",
+ rule_name
+ );
+ }
+ }
+
+ println!("✓ All required protocol rules (TCP/UDP/ICMP) are present");
+
+ // Remove firewall rules
+ let remove_result = remove_interface_firewall_rules(test_interface);
+ assert!(
+ remove_result.is_ok(),
+ "Failed to remove interface firewall rules: {:?}",
+ remove_result
+ );
+
+ // Verify rules were removed
+ let remaining_rules = list_interface_firewall_rules(test_interface).unwrap();
+ assert!(
+ remaining_rules.is_empty(),
+ "Some rules were not removed: {:?}",
+ remaining_rules
+ );
+
+ println!(
+ "✓ Successfully removed all firewall rules for interface: {}",
+ test_interface
+ );
+ }
}
diff --git a/easytier/src/common/acl_processor.rs b/easytier/src/common/acl_processor.rs
new file mode 100644
index 000000000..6f979e3c0
--- /dev/null
+++ b/easytier/src/common/acl_processor.rs
@@ -0,0 +1,1487 @@
+use std::{
+ collections::HashMap,
+ net::{IpAddr, SocketAddr},
+ str::FromStr as _,
+ sync::Arc,
+ time::{Duration, SystemTime, UNIX_EPOCH},
+};
+
+use crate::common::{config::ConfigLoader, global_ctx::ArcGlobalCtx, token_bucket::TokenBucket};
+use crate::proto::acl::*;
+use anyhow::Context as _;
+use dashmap::DashMap;
+use tokio::task::JoinSet;
+
+// Performance-optimized key for rate limiting to avoid string allocations
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct RateLimitKey {
+ pub chain_type: ChainType,
+ pub rule_priority: u32,
+}
+
+impl RateLimitKey {
+ pub fn new(chain_type: ChainType, rule_priority: u32) -> Self {
+ Self {
+ chain_type,
+ rule_priority,
+ }
+ }
+}
+
+// Performance-optimized rule identifier to avoid string allocations
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RuleId {
+ Priority(u32),
+ Stateful(u32),
+ Default,
+}
+
+impl RuleId {
+ /// Convert to string only when actually needed (lazy evaluation)
+ pub fn to_string_cached(&self) -> String {
+ match self {
+ RuleId::Priority(p) => p.to_string(),
+ RuleId::Stateful(p) => format!("stateful-{}", p),
+ RuleId::Default => "default".to_string(),
+ }
+ }
+
+ /// Get string representation for logging (optimized for hot path)
+ pub fn as_str(&self) -> String {
+ self.to_string_cached()
+ }
+}
+
+// Fast lookup structures for performance optimization
+#[derive(Debug, Clone)]
+pub struct FastLookupRule {
+ pub priority: u32,
+ pub protocol: Protocol,
+ pub src_ip_ranges: Vec,
+ pub dst_ip_ranges: Vec,
+ pub src_port_ranges: Vec<(u16, u16)>,
+ pub dst_port_ranges: Vec<(u16, u16)>,
+ pub action: Action,
+ pub enabled: bool,
+ pub stateful: bool,
+ pub rate_limit: u32,
+ pub burst_limit: u32,
+ pub rule_stats: Arc,
+}
+
+// Cache key combining packet info and chain type
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct AclCacheKey {
+ pub chain_type: ChainType,
+ pub protocol: Protocol,
+ pub src_ip: IpAddr,
+ pub dst_ip: IpAddr,
+ pub src_port: u16,
+ pub dst_port: u16,
+}
+
+impl AclCacheKey {
+ pub fn from_packet_info(packet_info: &PacketInfo, chain_type: ChainType) -> Self {
+ Self {
+ chain_type,
+ protocol: packet_info.protocol,
+ src_ip: packet_info.src_ip,
+ dst_ip: packet_info.dst_ip,
+ src_port: packet_info.src_port.unwrap_or(0),
+ dst_port: packet_info.dst_port.unwrap_or(0),
+ }
+ }
+}
+
+// Cache entry with timestamp for LRU cleanup
+#[derive(Debug, Clone)]
+pub struct AclCacheEntry {
+ pub action: Action,
+ pub matched_rule: RuleId,
+ pub last_access: u64,
+ // New fields to track rule characteristics for proper cache behavior
+ pub conn_track_key: Option,
+ pub rate_limit_keys: Vec,
+ pub chain_type: ChainType,
+ pub acl_result: Option,
+ pub rule_stats_vec: Vec>,
+}
+
+// Packet info extracted for ACL processing
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct PacketInfo {
+ pub src_ip: IpAddr,
+ pub dst_ip: IpAddr,
+ pub src_port: Option,
+ pub dst_port: Option,
+ pub protocol: Protocol,
+ pub packet_size: usize,
+}
+
+// ACL processing result
+#[derive(Debug, Clone)]
+pub struct AclResult {
+ pub action: Action,
+ pub matched_rule: Option,
+ pub should_log: bool,
+ pub log_context: Option,
+}
+
+impl AclResult {
+ /// Get matched rule as string (lazy evaluation)
+ pub fn matched_rule_string(&self) -> Option {
+ self.matched_rule.as_ref().map(|r| r.to_string_cached())
+ }
+
+ /// Get matched rule as string reference for logging (compatibility method)
+ pub fn matched_rule_str(&self) -> Option {
+ self.matched_rule.as_ref().map(|r| r.as_str())
+ }
+}
+
+// Context for lazy log message construction
+#[derive(Debug, Clone)]
+pub enum AclLogContext {
+ StatefulMatch {
+ src_ip: IpAddr,
+ dst_ip: IpAddr,
+ },
+ RuleMatch {
+ src_ip: IpAddr,
+ dst_ip: IpAddr,
+ action: Action,
+ },
+ DefaultDrop,
+ DefaultAllow,
+ UnsupportedChainType,
+ RateLimitDrop,
+}
+
+impl AclLogContext {
+ pub fn to_message(&self) -> String {
+ match self {
+ AclLogContext::StatefulMatch { src_ip, dst_ip } => {
+ format!("Stateful match: {} -> {}", src_ip, dst_ip)
+ }
+ AclLogContext::RuleMatch {
+ src_ip,
+ dst_ip,
+ action,
+ } => {
+ format!("Rule match: {} -> {} action: {:?}", src_ip, dst_ip, action)
+ }
+ AclLogContext::DefaultDrop => "No matching rule, default drop".to_string(),
+ AclLogContext::DefaultAllow => "No matching rule, default allow".to_string(),
+ AclLogContext::UnsupportedChainType => "Unsupported chain type".to_string(),
+ AclLogContext::RateLimitDrop => "Rate limit drop".to_string(),
+ }
+ }
+}
+
+pub type SharedState = (
+ Arc>,
+ Arc>>,
+ Arc>,
+);
+
+// High-performance ACL processor - No more internal locks!
+pub struct AclProcessor {
+ // Immutable rule vectors - no locks needed since they're never modified after creation
+ inbound_rules: Vec,
+ outbound_rules: Vec,
+ forward_rules: Vec,
+
+ default_inbound_action: Action,
+ default_outbound_action: Action,
+ default_forward_action: Action,
+
+ default_rule_stats: Arc,
+
+ // Connection tracking table - shared across different processor instances if needed
+ conn_track: Arc>,
+
+ // Rate limiting buckets per rule using TokenBucket with optimized keys
+ rate_limiters: Arc>>,
+
+ // Rule lookup cache with LRU cleanup
+ rule_cache: Arc>,
+ cache_max_size: usize,
+ cache_cleanup_interval: Duration,
+
+ // Statistics
+ stats: Arc>,
+
+ tasks: JoinSet<()>,
+}
+
+impl AclProcessor {
+ /// Create a new ACL processor with pre-built immutable rules
+ /// This is the main constructor that should be used
+ pub fn new(acl_config: Acl) -> Self {
+ Self::new_with_shared_state(acl_config, None, None, None)
+ }
+
+ /// Create a new ACL processor while preserving connection tracking and rate limiting state
+ /// This is useful for hot reloading where you want to preserve established connections
+ pub fn new_with_shared_state(
+ acl_config: Acl,
+ conn_track: Option>>,
+ rate_limiters: Option>>>,
+ stats: Option>>,
+ ) -> Self {
+ let (inbound_rules, outbound_rules, forward_rules) = Self::build_rules(&acl_config);
+ let (default_inbound_action, default_outbound_action, default_forward_action) =
+ Self::build_default_actions(&acl_config);
+ let tasks = JoinSet::new();
+
+ let mut processor = Self {
+ inbound_rules,
+ outbound_rules,
+ forward_rules,
+
+ default_inbound_action,
+ default_outbound_action,
+ default_forward_action,
+
+ default_rule_stats: Arc::new(RuleStats {
+ rule: None,
+ stat: Some(StatItem {
+ packet_count: 0,
+ byte_count: 0,
+ }),
+ }),
+ conn_track: conn_track.unwrap_or_else(|| Arc::new(DashMap::new())),
+ rate_limiters: rate_limiters.unwrap_or_else(|| Arc::new(DashMap::new())),
+ rule_cache: Arc::new(DashMap::new()), // Always start with fresh cache
+ cache_max_size: 10000, // Limit cache to 10k entries
+ cache_cleanup_interval: Duration::from_secs(20), // Cleanup every 5 minutes
+ stats: stats.unwrap_or_else(|| Arc::new(DashMap::new())),
+ tasks,
+ };
+
+ processor.start_cache_cleanup_task();
+ processor
+ }
+
+ fn build_default_actions(acl_config: &Acl) -> (Action, Action, Action) {
+ let default_inbound_action = acl_config
+ .acl_v1
+ .as_ref()
+ .and_then(|v1| {
+ v1.chains
+ .iter()
+ .find(|c| c.chain_type == ChainType::Inbound as i32)
+ })
+ .map(|c| c.default_action())
+ .unwrap_or(Action::Allow);
+
+ let default_outbound_action = acl_config
+ .acl_v1
+ .as_ref()
+ .and_then(|v1| {
+ v1.chains
+ .iter()
+ .find(|c| c.chain_type == ChainType::Outbound as i32)
+ })
+ .map(|c| c.default_action())
+ .unwrap_or(Action::Allow);
+
+ let default_forward_action = acl_config
+ .acl_v1
+ .as_ref()
+ .and_then(|v1| {
+ v1.chains
+ .iter()
+ .find(|c| c.chain_type == ChainType::Forward as i32)
+ })
+ .map(|c| c.default_action())
+ .unwrap_or(Action::Allow);
+
+ (
+ default_inbound_action,
+ default_outbound_action,
+ default_forward_action,
+ )
+ }
+
+ /// Build all rule vectors from configuration
+ fn build_rules(
+ acl_config: &Acl,
+ ) -> (
+ Vec,
+ Vec,
+ Vec,
+ ) {
+ let mut inbound_rules = Vec::new();
+ let mut outbound_rules = Vec::new();
+ let mut forward_rules = Vec::new();
+
+ // Build new rule vectors
+ if let Some(ref acl_v1) = acl_config.acl_v1 {
+ for chain in &acl_v1.chains {
+ if !chain.enabled {
+ continue;
+ }
+
+ let mut rules = chain
+ .rules
+ .iter()
+ .filter(|rule| rule.enabled)
+ .map(Self::convert_to_fast_lookup_rule)
+ .collect::>();
+
+ // Sort by priority (higher priority first)
+ rules.sort_by(|a, b| b.priority.cmp(&a.priority));
+
+ match chain.chain_type() {
+ ChainType::Inbound => inbound_rules.extend(rules),
+ ChainType::Outbound => outbound_rules.extend(rules),
+ ChainType::Forward => forward_rules.extend(rules),
+ _ => {}
+ }
+ }
+ }
+
+ tracing::info!(
+ "ACL rules built: {} inbound, {} outbound, {} forward",
+ inbound_rules.len(),
+ outbound_rules.len(),
+ forward_rules.len(),
+ );
+
+ (inbound_rules, outbound_rules, forward_rules)
+ }
+
+ /// Start periodic cache cleanup task
+ fn start_cache_cleanup_task(&mut self) {
+ let rule_cache = self.rule_cache.clone();
+ let cache_max_size = self.cache_max_size;
+ let cleanup_interval = self.cache_cleanup_interval;
+
+ self.tasks.spawn(async move {
+ let mut interval = tokio::time::interval(cleanup_interval);
+ loop {
+ interval.tick().await;
+ Self::cleanup_cache(&rule_cache, cache_max_size);
+ }
+ });
+
+ let conn_track = self.conn_track.clone();
+ self.tasks.spawn(async move {
+ let mut interval = tokio::time::interval(cleanup_interval);
+ loop {
+ interval.tick().await;
+ Self::cleanup_expired_connections(conn_track.clone(), 60);
+ }
+ });
+ }
+
+ /// Clean up cache using LRU strategy
+ fn cleanup_cache(cache: &DashMap, max_size: usize) {
+ let current_size = cache.len();
+ if current_size <= max_size {
+ return;
+ }
+
+ // Remove oldest entries (LRU cleanup)
+ let mut entries: Vec<(AclCacheKey, u64)> = cache
+ .iter()
+ .map(|entry| (entry.key().clone(), entry.value().last_access))
+ .collect();
+
+ // Sort by last_access (oldest first)
+ entries.sort_by_key(|(_, last_access)| *last_access);
+
+ // Remove oldest 20% of entries
+ let to_remove = current_size - max_size + (max_size / 5);
+ for (key, _) in entries.into_iter().take(to_remove) {
+ cache.remove(&key);
+ }
+
+ tracing::debug!(
+ "Cache cleanup completed: removed {} entries, current size: {}",
+ to_remove,
+ cache.len()
+ );
+ }
+
+ pub fn process_packet_with_cache_entry(
+ &self,
+ packet_info: &PacketInfo,
+ cache_entry: &AclCacheEntry,
+ ) -> AclResult {
+ for rate_limit_key in cache_entry.rate_limit_keys.iter() {
+ // bucket should already be created, so rate and burst are not important
+ if !self.check_rate_limit(rate_limit_key, 1, 1, false) {
+ return AclResult {
+ action: Action::Drop,
+ matched_rule: Some(cache_entry.matched_rule.clone()),
+ should_log: false,
+ log_context: Some(AclLogContext::RateLimitDrop),
+ };
+ }
+ }
+
+ if let Some(conn_track_key) = cache_entry.conn_track_key.as_ref() {
+ self.check_connection_state(conn_track_key, packet_info);
+ }
+
+ self.inc_cache_entry_stats(cache_entry, packet_info);
+
+ cache_entry.acl_result.clone().unwrap()
+ }
+
+ fn inc_cache_entry_stats(&self, cache_entry: &AclCacheEntry, packet_info: &PacketInfo) {
+ for rule_stats in cache_entry.rule_stats_vec.iter() {
+ // Use unsafe code to mutate the contents behind the Arc
+ let stat_ptr = rule_stats.stat.as_ref().unwrap() as *const StatItem as *mut StatItem;
+ unsafe {
+ (*stat_ptr).packet_count += 1;
+ (*stat_ptr).byte_count += packet_info.packet_size as u64;
+ }
+ }
+ }
+
+ pub fn get_rules_stats(&self) -> Vec {
+ let mut stats: Vec = Vec::new();
+ for rule in self.inbound_rules.iter() {
+ stats.push((*rule.rule_stats).clone());
+ }
+ for rule in self.outbound_rules.iter() {
+ stats.push((*rule.rule_stats).clone());
+ }
+ for rule in self.forward_rules.iter() {
+ stats.push((*rule.rule_stats).clone());
+ }
+ stats
+ }
+
+ /// Process a packet through ACL rules - Now lock-free!
+ pub fn process_packet(&self, packet_info: &PacketInfo, chain_type: ChainType) -> AclResult {
+ // Check cache first for performance
+ let cache_key = AclCacheKey::from_packet_info(packet_info, chain_type);
+
+ // If cache hit and can skip checks, return cached result
+ if let Some(mut cached) = self.rule_cache.get_mut(&cache_key) {
+ // Update last access time for LRU
+ cached.last_access = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ self.increment_stat(AclStatKey::CacheHits);
+ return self.process_packet_with_cache_entry(packet_info, &cached);
+ }
+
+ // Direct access to rules - no locks needed!
+ let rules = match chain_type {
+ ChainType::Inbound => &self.inbound_rules,
+ ChainType::Outbound => &self.outbound_rules,
+ ChainType::Forward => &self.forward_rules,
+ _ => {
+ return AclResult {
+ action: Action::Drop,
+ matched_rule: Some(RuleId::Default),
+ should_log: false,
+ log_context: Some(AclLogContext::UnsupportedChainType),
+ }
+ }
+ };
+
+ let mut cache_entry = AclCacheEntry {
+ action: Action::Allow,
+ matched_rule: RuleId::Default,
+ last_access: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ conn_track_key: None,
+ rate_limit_keys: vec![],
+ chain_type,
+ acl_result: None,
+ rule_stats_vec: vec![],
+ };
+
+ // Process rules in priority order
+ for rule in rules.iter() {
+ if !rule.enabled || !self.rule_matches(rule, packet_info) {
+ continue;
+ }
+
+ // Check rate limiting if configured
+ if rule.rate_limit > 0 {
+ let rule_key = RateLimitKey::new(chain_type, rule.priority);
+ cache_entry.rate_limit_keys.push(rule_key.clone());
+ cache_entry.rule_stats_vec.push(rule.rule_stats.clone());
+ if !self.check_rate_limit(&rule_key, rule.rate_limit, rule.burst_limit, true) {
+ // rate limited, drop packet
+ return AclResult {
+ action: Action::Drop,
+ matched_rule: Some(RuleId::Priority(rule.priority)),
+ should_log: false,
+ log_context: Some(AclLogContext::RateLimitDrop),
+ };
+ }
+ }
+
+ // Handle stateful connections if configured
+ if rule.stateful && rule.action == Action::Allow {
+ let conn_track_key = self.conn_track_key(packet_info);
+ self.check_connection_state(&conn_track_key, packet_info);
+ cache_entry.rule_stats_vec.push(rule.rule_stats.clone());
+ cache_entry.matched_rule = RuleId::Stateful(rule.priority);
+ cache_entry.conn_track_key = Some(conn_track_key);
+ cache_entry.acl_result = Some(AclResult {
+ action: Action::Allow,
+ matched_rule: Some(RuleId::Stateful(rule.priority)),
+ should_log: false,
+ log_context: Some(AclLogContext::StatefulMatch {
+ src_ip: packet_info.src_ip,
+ dst_ip: packet_info.dst_ip,
+ }),
+ });
+ } else {
+ // Rule matched, return action
+ cache_entry.rule_stats_vec.push(rule.rule_stats.clone());
+ cache_entry.matched_rule = RuleId::Priority(rule.priority);
+ cache_entry.acl_result = Some(AclResult {
+ action: rule.action,
+ matched_rule: Some(RuleId::Priority(rule.priority)),
+ should_log: false,
+ log_context: Some(AclLogContext::RuleMatch {
+ src_ip: packet_info.src_ip,
+ dst_ip: packet_info.dst_ip,
+ action: rule.action,
+ }),
+ });
+ }
+
+ // Cache the result with rule info
+ self.increment_stat(AclStatKey::RuleMatches);
+ self.inc_cache_entry_stats(&cache_entry, packet_info);
+ self.cache_result(&cache_key, cache_entry.clone());
+ return cache_entry.acl_result.clone().unwrap();
+ }
+
+ let default_action = match chain_type {
+ ChainType::Inbound => self.default_inbound_action,
+ ChainType::Outbound => self.default_outbound_action,
+ ChainType::Forward => self.default_forward_action,
+ _ => Action::Allow,
+ };
+
+ // No rule matched, return default drop
+ if default_action == Action::Drop {
+ self.increment_stat(AclStatKey::DefaultDrops);
+ } else {
+ self.increment_stat(AclStatKey::DefaultAllows);
+ }
+
+ let log_context = if default_action == Action::Drop {
+ AclLogContext::DefaultDrop
+ } else {
+ AclLogContext::DefaultAllow
+ };
+
+ cache_entry
+ .rule_stats_vec
+ .push(self.default_rule_stats.clone());
+ cache_entry.matched_rule = RuleId::Default;
+ cache_entry.acl_result = Some(AclResult {
+ action: default_action,
+ matched_rule: Some(RuleId::Default),
+ should_log: false,
+ log_context: Some(log_context),
+ });
+
+ // Cache the default result (no rule info)
+ self.inc_cache_entry_stats(&cache_entry, packet_info);
+ self.cache_result(&cache_key, cache_entry.clone());
+ cache_entry.acl_result.clone().unwrap()
+ }
+
+ /// Get shared state for preserving across hot reloads
+ pub fn get_shared_state(&self) -> SharedState {
+ (
+ self.conn_track.clone(),
+ self.rate_limiters.clone(),
+ self.stats.clone(),
+ )
+ }
+
+ /// Cache an ACL result
+ fn cache_result(&self, cache_key: &AclCacheKey, cache_entry: AclCacheEntry) {
+ self.rule_cache.insert(cache_key.clone(), cache_entry);
+
+ // Trigger cleanup if cache is getting too large
+ if self.rule_cache.len() > self.cache_max_size * 2 {
+ let cache = self.rule_cache.clone();
+ let max_size = self.cache_max_size;
+ Self::cleanup_cache(&cache, max_size);
+ }
+ }
+
+ /// Check if a rule matches the packet
+ fn rule_matches(&self, rule: &FastLookupRule, packet_info: &PacketInfo) -> bool {
+ // Protocol check
+ if rule.protocol != Protocol::Any && rule.protocol as i32 != packet_info.protocol as i32 {
+ return false;
+ }
+
+ // Source IP check
+ if !rule.src_ip_ranges.is_empty() {
+ let matches = rule
+ .src_ip_ranges
+ .iter()
+ .any(|cidr| match (cidr, packet_info.src_ip) {
+ (cidr::IpCidr::V4(v4_cidr), IpAddr::V4(v4_addr)) => v4_cidr.contains(&v4_addr),
+ (cidr::IpCidr::V6(v6_cidr), IpAddr::V6(v6_addr)) => v6_cidr.contains(&v6_addr),
+ _ => false,
+ });
+ if !matches {
+ return false;
+ }
+ }
+
+ // Destination IP check
+ if !rule.dst_ip_ranges.is_empty() {
+ let matches = rule
+ .dst_ip_ranges
+ .iter()
+ .any(|cidr| match (cidr, packet_info.dst_ip) {
+ (cidr::IpCidr::V4(v4_cidr), IpAddr::V4(v4_addr)) => v4_cidr.contains(&v4_addr),
+ (cidr::IpCidr::V6(v6_cidr), IpAddr::V6(v6_addr)) => v6_cidr.contains(&v6_addr),
+ _ => false,
+ });
+ if !matches {
+ return false;
+ }
+ }
+
+ // Source port check
+ if let Some(src_port) = packet_info.src_port {
+ if !rule.src_port_ranges.is_empty() {
+ let matches = rule
+ .src_port_ranges
+ .iter()
+ .any(|(start, end)| src_port >= *start && src_port <= *end);
+ if !matches {
+ return false;
+ }
+ }
+ }
+
+ // Destination port check
+ if let Some(dst_port) = packet_info.dst_port {
+ if !rule.dst_port_ranges.is_empty() {
+ let matches = rule
+ .dst_port_ranges
+ .iter()
+ .any(|(start, end)| dst_port >= *start && dst_port <= *end);
+ if !matches {
+ return false;
+ }
+ }
+ }
+
+ true
+ }
+
+ fn conn_track_key(&self, packet_info: &PacketInfo) -> String {
+ format!(
+ "{}:{}->{}:{}",
+ packet_info.src_ip,
+ packet_info.src_port.unwrap_or(0),
+ packet_info.dst_ip,
+ packet_info.dst_port.unwrap_or(0)
+ )
+ }
+
+ /// Check connection state for stateful rules
+ fn check_connection_state(&self, conn_track_key: &str, packet_info: &PacketInfo) {
+ self.conn_track
+ .entry(conn_track_key.to_string())
+ .and_modify(|x| {
+ x.last_seen = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+ x.packet_count += 1;
+ x.byte_count += packet_info.packet_size as u64;
+ x.state = ConnState::Established as i32;
+ })
+ .or_insert_with(|| ConnTrackEntry {
+ src_addr: Some(
+ SocketAddr::new(packet_info.src_ip, packet_info.src_port.unwrap_or(0)).into(),
+ ),
+ dst_addr: Some(
+ SocketAddr::new(packet_info.dst_ip, packet_info.dst_port.unwrap_or(0)).into(),
+ ),
+ protocol: packet_info.protocol as i32,
+ state: ConnState::New as i32,
+ created_at: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ last_seen: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs(),
+ packet_count: 1,
+ byte_count: packet_info.packet_size as u64,
+ });
+ }
+
+ /// Check rate limiting for a rule
+ fn check_rate_limit(
+ &self,
+ rule_key: &RateLimitKey,
+ rate: u32,
+ burst: u32,
+ allow_create: bool,
+ ) -> bool {
+ if rate == 0 {
+ return true; // No rate limiting
+ }
+
+ let bucket = self
+ .rate_limiters
+ .entry(rule_key.clone())
+ .or_insert_with(|| {
+ if !allow_create {
+ panic!("Rate limit bucket not found");
+ }
+ TokenBucket::new(burst as u64, rate as u64, Duration::from_millis(10))
+ })
+ .clone();
+
+ // Try to consume 1 token (1 packet)
+ bucket.try_consume(1)
+ }
+
+ /// Convert proto Rule to FastLookupRule
+ fn convert_to_fast_lookup_rule(rule: &Rule) -> FastLookupRule {
+ let src_ip_ranges = rule
+ .source_ips
+ .iter()
+ .filter_map(|x| Self::convert_ip_inet_to_cidr(x.as_str()))
+ .collect();
+
+ let dst_ip_ranges = rule
+ .destination_ips
+ .iter()
+ .filter_map(|x| Self::convert_ip_inet_to_cidr(x.as_str()))
+ .collect();
+
+ let src_port_ranges = rule
+ .source_ports
+ .iter()
+ .filter_map(|port_range| {
+ if let Some((start, end)) = parse_port_range(port_range) {
+ Some((start, end))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ let dst_port_ranges = rule
+ .ports
+ .iter()
+ .filter_map(|port_range| {
+ if let Some((start, end)) = parse_port_range(port_range) {
+ Some((start, end))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ FastLookupRule {
+ priority: rule.priority,
+ protocol: rule.protocol(),
+ src_ip_ranges,
+ dst_ip_ranges,
+ src_port_ranges,
+ dst_port_ranges,
+ action: rule.action(),
+ enabled: rule.enabled,
+ stateful: rule.stateful,
+ rate_limit: rule.rate_limit,
+ burst_limit: rule.burst_limit,
+ rule_stats: Arc::new(RuleStats {
+ rule: Some(rule.clone()),
+ stat: Some(StatItem {
+ packet_count: 0,
+ byte_count: 0,
+ }),
+ }),
+ }
+ }
+
+ /// Convert IpInet to CIDR for fast lookup
+ fn convert_ip_inet_to_cidr(input: &str) -> Option {
+ cidr::IpCidr::from_str(input).ok()
+ }
+
+ /// Increment statistics counter
+ pub fn increment_stat(&self, key: AclStatKey) {
+ self.stats
+ .entry(key)
+ .and_modify(|counter| *counter += 1)
+ .or_insert(1);
+ }
+
+ /// Get statistics
+ pub fn get_stats(&self) -> HashMap {
+ let mut stats = self
+ .stats
+ .iter()
+ .map(|entry| (entry.key().as_str(), *entry.value()))
+ .collect::>();
+
+ // Add cache statistics using enum keys
+ stats.insert(AclStatKey::CacheSize.as_str(), self.rule_cache.len() as u64);
+ stats.insert(
+ AclStatKey::CacheMaxSize.as_str(),
+ self.cache_max_size as u64,
+ );
+
+ stats
+ }
+
+ /// Clean up expired connection tracking entries
+ pub fn cleanup_expired_connections(
+ conn_track: Arc>,
+ timeout_secs: u64,
+ ) {
+ let current_time = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+ let keys_to_remove: Vec = conn_track
+ .iter()
+ .filter_map(|entry| {
+ if current_time - entry.last_seen > timeout_secs {
+ Some(entry.key().clone())
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ for key in keys_to_remove {
+ conn_track.remove(&key);
+ }
+ }
+
+ /// Get cache hit rate
+ pub fn get_cache_hit_rate(&self) -> f64 {
+ let cache_hits = self
+ .stats
+ .get(&AclStatKey::CacheHits)
+ .map(|v| *v.value())
+ .unwrap_or(0);
+ let total_requests = cache_hits
+ + self
+ .stats
+ .get(&AclStatKey::RuleMatches)
+ .map(|v| *v.value())
+ .unwrap_or(0);
+
+ if total_requests == 0 {
+ 0.0
+ } else {
+ cache_hits as f64 / total_requests as f64
+ }
+ }
+}
+
+// 新增辅助函数
+fn parse_port_start(port_strs: &[String]) -> Option {
+ port_strs
+ .iter()
+ .filter_map(|s| parse_port_range(s).map(|(start, _)| start))
+ .min()
+}
+fn parse_port_end(port_strs: &[String]) -> Option {
+ port_strs
+ .iter()
+ .filter_map(|s| parse_port_range(s).map(|(_, end)| end))
+ .max()
+}
+fn parse_port_range(s: &str) -> Option<(u16, u16)> {
+ if let Some((start, end)) = s.split_once('-') {
+ let start = start.trim().parse().ok()?;
+ let end = end.trim().parse().ok()?;
+ Some((start, end))
+ } else {
+ let port = s.trim().parse().ok()?;
+ Some((port, port))
+ }
+}
+
+// Statistics key enum for better performance
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum AclStatKey {
+ // Cache statistics
+ CacheHits,
+ CacheSize,
+ CacheMaxSize,
+ RuleMatches,
+ DefaultAllows,
+ DefaultDrops,
+
+ // Global packet statistics
+ PacketsTotal,
+ PacketsAllowed,
+ PacketsDropped,
+ PacketsNoop,
+
+ // Per-chain statistics
+ InboundPacketsTotal,
+ InboundPacketsAllowed,
+ InboundPacketsDropped,
+ InboundPacketsNoop,
+
+ OutboundPacketsTotal,
+ OutboundPacketsAllowed,
+ OutboundPacketsDropped,
+ OutboundPacketsNoop,
+
+ ForwardPacketsTotal,
+ ForwardPacketsAllowed,
+ ForwardPacketsDropped,
+ ForwardPacketsNoop,
+
+ UnknownPacketsTotal,
+ UnknownPacketsAllowed,
+ UnknownPacketsDropped,
+ UnknownPacketsNoop,
+}
+
+impl AclStatKey {
+ pub fn as_str(&self) -> String {
+ format!("{:?}", self)
+ }
+
+ pub fn from_chain_and_action(chain_type: ChainType, stat_type: AclStatType) -> Self {
+ match (chain_type, stat_type) {
+ (ChainType::Inbound, AclStatType::Total) => AclStatKey::InboundPacketsTotal,
+ (ChainType::Inbound, AclStatType::Allowed) => AclStatKey::InboundPacketsAllowed,
+ (ChainType::Inbound, AclStatType::Dropped) => AclStatKey::InboundPacketsDropped,
+ (ChainType::Inbound, AclStatType::Noop) => AclStatKey::InboundPacketsNoop,
+
+ (ChainType::Outbound, AclStatType::Total) => AclStatKey::OutboundPacketsTotal,
+ (ChainType::Outbound, AclStatType::Allowed) => AclStatKey::OutboundPacketsAllowed,
+ (ChainType::Outbound, AclStatType::Dropped) => AclStatKey::OutboundPacketsDropped,
+ (ChainType::Outbound, AclStatType::Noop) => AclStatKey::OutboundPacketsNoop,
+
+ (ChainType::Forward, AclStatType::Total) => AclStatKey::ForwardPacketsTotal,
+ (ChainType::Forward, AclStatType::Allowed) => AclStatKey::ForwardPacketsAllowed,
+ (ChainType::Forward, AclStatType::Dropped) => AclStatKey::ForwardPacketsDropped,
+ (ChainType::Forward, AclStatType::Noop) => AclStatKey::ForwardPacketsNoop,
+
+ (_, AclStatType::Total) => AclStatKey::UnknownPacketsTotal,
+ (_, AclStatType::Allowed) => AclStatKey::UnknownPacketsAllowed,
+ (_, AclStatType::Dropped) => AclStatKey::UnknownPacketsDropped,
+ (_, AclStatType::Noop) => AclStatKey::UnknownPacketsNoop,
+ }
+ }
+}
+
+pub struct AclRuleBuilder {
+ pub acl: Option,
+ pub tcp_whitelist: Vec,
+ pub udp_whitelist: Vec,
+ pub whitelist_priority: Option,
+}
+
+impl AclRuleBuilder {
+ fn parse_port_list(port_list: &[String]) -> anyhow::Result> {
+ let mut ports = Vec::new();
+
+ for port_spec in port_list {
+ if port_spec.contains('-') {
+ // Handle port range like "8000-9000"
+ let parts: Vec<&str> = port_spec.split('-').collect();
+ if parts.len() != 2 {
+ return Err(anyhow::anyhow!("Invalid port range format: {}", port_spec));
+ }
+
+ let start: u16 = parts[0]
+ .parse()
+ .with_context(|| format!("Invalid start port in range: {}", port_spec))?;
+ let end: u16 = parts[1]
+ .parse()
+ .with_context(|| format!("Invalid end port in range: {}", port_spec))?;
+
+ if start > end {
+ return Err(anyhow::anyhow!(
+ "Start port must be <= end port in range: {}",
+ port_spec
+ ));
+ }
+
+ // acl can handle port range
+ ports.push(port_spec.clone());
+ } else {
+ // Handle single port
+ let port: u16 = port_spec
+ .parse()
+ .with_context(|| format!("Invalid port number: {}", port_spec))?;
+ ports.push(port.to_string());
+ }
+ }
+
+ Ok(ports)
+ }
+
+ fn generate_acl_from_whitelists(&mut self) -> anyhow::Result<()> {
+ if self.tcp_whitelist.is_empty() && self.udp_whitelist.is_empty() {
+ return Ok(());
+ }
+
+ // Create inbound chain for whitelist rules
+ let mut inbound_chain = Chain {
+ name: "inbound_whitelist".to_string(),
+ chain_type: ChainType::Inbound as i32,
+ description: "Auto-generated inbound whitelist from CLI".to_string(),
+ enabled: true,
+ rules: vec![],
+ default_action: Action::Drop as i32, // Default deny
+ };
+
+ let mut rule_priority = self.whitelist_priority.unwrap_or(1000u32);
+
+ // Add TCP whitelist rules
+ if !self.tcp_whitelist.is_empty() {
+ let tcp_ports = Self::parse_port_list(&self.tcp_whitelist)?;
+ let tcp_rule = Rule {
+ name: "tcp_whitelist".to_string(),
+ description: "Auto-generated TCP whitelist rule".to_string(),
+ priority: rule_priority,
+ enabled: true,
+ protocol: Protocol::Tcp as i32,
+ ports: tcp_ports,
+ source_ips: vec![],
+ destination_ips: vec![],
+ source_ports: vec![],
+ action: Action::Allow as i32,
+ rate_limit: 0,
+ burst_limit: 0,
+ stateful: true,
+ };
+ inbound_chain.rules.push(tcp_rule);
+ rule_priority -= 1;
+ }
+
+ // Add UDP whitelist rules
+ if !self.udp_whitelist.is_empty() {
+ let udp_ports = Self::parse_port_list(&self.udp_whitelist)?;
+ let udp_rule = Rule {
+ name: "udp_whitelist".to_string(),
+ description: "Auto-generated UDP whitelist rule".to_string(),
+ priority: rule_priority,
+ enabled: true,
+ protocol: Protocol::Udp as i32,
+ ports: udp_ports,
+ source_ips: vec![],
+ destination_ips: vec![],
+ source_ports: vec![],
+ action: Action::Allow as i32,
+ rate_limit: 0,
+ burst_limit: 0,
+ stateful: false,
+ };
+ inbound_chain.rules.push(udp_rule);
+ }
+
+ if self.acl.is_none() {
+ self.acl = Some(Acl::default());
+ }
+
+ let acl = self.acl.as_mut().unwrap();
+
+ if let Some(ref mut acl_v1) = acl.acl_v1 {
+ acl_v1.chains.push(inbound_chain);
+ } else {
+ acl.acl_v1 = Some(AclV1 {
+ chains: vec![inbound_chain],
+ });
+ }
+
+ Ok(())
+ }
+
+ fn do_build(mut self) -> anyhow::Result> {
+ self.generate_acl_from_whitelists()?;
+ Ok(self.acl.clone())
+ }
+
+ pub fn build(global_ctx: &ArcGlobalCtx) -> anyhow::Result > {
+ let builder = AclRuleBuilder {
+ acl: global_ctx.config.get_acl(),
+ tcp_whitelist: global_ctx.config.get_tcp_whitelist(),
+ udp_whitelist: global_ctx.config.get_udp_whitelist(),
+ whitelist_priority: None,
+ };
+ builder.do_build()
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum AclStatType {
+ Total,
+ Allowed,
+ Dropped,
+ Noop,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::hash::{Hash, Hasher};
+ use std::net::{IpAddr, Ipv4Addr};
+
+ fn create_test_acl_config() -> Acl {
+ let mut acl_config = Acl::default();
+
+ let mut acl_v1 = AclV1::default();
+
+ // Create inbound chain
+ let mut chain = Chain {
+ name: "test_inbound".to_string(),
+ chain_type: ChainType::Inbound as i32,
+ enabled: true,
+ ..Default::default()
+ };
+
+ // Allow all rule
+ let rule = Rule {
+ name: "allow_all".to_string(),
+ priority: 100,
+ enabled: true,
+ action: Action::Allow as i32,
+ protocol: Protocol::Any as i32,
+ ..Default::default()
+ };
+
+ chain.rules.push(rule);
+ acl_v1.chains.push(chain);
+ acl_config.acl_v1 = Some(acl_v1);
+
+ acl_config
+ }
+
+ fn create_test_packet_info() -> PacketInfo {
+ PacketInfo {
+ src_ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
+ dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
+ src_port: Some(12345),
+ dst_port: Some(80),
+ protocol: Protocol::Tcp,
+ packet_size: 1024,
+ }
+ }
+
+ #[test]
+ fn test_acl_cache_key_creation() {
+ let packet_info = create_test_packet_info();
+ let cache_key = AclCacheKey::from_packet_info(&packet_info, ChainType::Inbound);
+
+ assert_eq!(cache_key.chain_type, ChainType::Inbound);
+ assert_eq!(cache_key.protocol, Protocol::Tcp);
+ assert_eq!(
+ cache_key.src_ip,
+ IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100))
+ );
+ assert_eq!(cache_key.dst_ip, IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
+ assert_eq!(cache_key.src_port, 12345);
+ assert_eq!(cache_key.dst_port, 80);
+ }
+
+ #[test]
+ fn test_acl_cache_key_equality() {
+ let packet_info1 = create_test_packet_info();
+ let packet_info2 = create_test_packet_info();
+
+ let key1 = AclCacheKey::from_packet_info(&packet_info1, ChainType::Inbound);
+ let key2 = AclCacheKey::from_packet_info(&packet_info2, ChainType::Inbound);
+
+ assert_eq!(key1, key2);
+
+ // Test hash consistency
+ use std::collections::hash_map::DefaultHasher;
+ let mut hasher1 = DefaultHasher::new();
+ let mut hasher2 = DefaultHasher::new();
+ key1.hash(&mut hasher1);
+ key2.hash(&mut hasher2);
+ assert_eq!(hasher1.finish(), hasher2.finish());
+ }
+
+ #[tokio::test]
+ async fn test_acl_processor_basic_functionality() {
+ let acl_config = create_test_acl_config();
+ let processor = AclProcessor::new(acl_config);
+ let packet_info = create_test_packet_info();
+
+ let result = processor.process_packet(&packet_info, ChainType::Inbound);
+
+ assert_eq!(result.action, Action::Allow);
+ assert!(result.matched_rule.is_some());
+ }
+
+ #[tokio::test]
+ async fn test_acl_cache_hit() {
+ let acl_config = create_test_acl_config();
+ let processor = AclProcessor::new(acl_config);
+ let packet_info = create_test_packet_info();
+
+ // First request - should be a cache miss
+ let result1 = processor.process_packet(&packet_info, ChainType::Inbound);
+
+ // Second request - should be a cache hit
+ let result2 = processor.process_packet(&packet_info, ChainType::Inbound);
+
+ assert_eq!(result1.action, result2.action);
+ assert_eq!(result1.matched_rule, result2.matched_rule);
+
+ // Check cache statistics
+ let stats = processor.get_stats();
+ assert_eq!(stats.get(&AclStatKey::CacheHits.as_str()).unwrap_or(&0), &1);
+ assert!(processor.get_cache_hit_rate() > 0.0);
+ }
+
+ #[tokio::test]
+ async fn test_lock_free_hot_reload_demo() {
+ println!("\n=== ACL 优化演示:无锁热加载 ===");
+
+ // 创建初始配置
+ let initial_config = create_test_acl_config();
+ let processor = AclProcessor::new(initial_config);
+ let packet_info = create_test_packet_info();
+
+ // 处理一些数据包
+ println!("1. 处理初始数据包...");
+ let result1 = processor.process_packet(&packet_info, ChainType::Inbound);
+ assert_eq!(result1.action, Action::Allow);
+ println!(" ✓ 数据包被允许通过");
+
+ // 获取共享状态
+ let (conn_track, rate_limiters, stats) = processor.get_shared_state();
+ println!("2. 保存连接跟踪和统计状态...");
+ println!(" ✓ 连接数: {}", conn_track.len());
+ println!(" ✓ 限流器数量: {}", rate_limiters.len());
+ println!(" ✓ 统计计数器数量: {}", stats.len());
+
+ // 创建新配置(模拟热加载)
+ let mut new_config = create_test_acl_config();
+ if let Some(ref mut acl_v1) = new_config.acl_v1 {
+ let drop_rule = Rule {
+ name: "drop_all".to_string(),
+ priority: 200,
+ enabled: true,
+ action: Action::Drop as i32,
+ protocol: Protocol::Any as i32,
+ ..Default::default()
+ };
+ acl_v1.chains[0].rules.push(drop_rule);
+ }
+
+ // 创建新的处理器实例(热加载)
+ println!("3. 执行热加载(创建新的处理器实例)...");
+ let new_processor = AclProcessor::new_with_shared_state(
+ new_config,
+ Some(conn_track.clone()),
+ Some(rate_limiters.clone()),
+ Some(stats.clone()),
+ );
+
+ // 验证新处理器的行为
+ let result2 = new_processor.process_packet(&packet_info, ChainType::Inbound);
+ assert_eq!(result2.action, Action::Drop); // 新规则应该拒绝
+ println!(" ✓ 新规则生效:数据包被拒绝");
+
+ // 验证状态被保留
+ let (new_conn_track, new_rate_limiters, new_stats) = new_processor.get_shared_state();
+ assert!(Arc::ptr_eq(&conn_track, &new_conn_track));
+ assert!(Arc::ptr_eq(&rate_limiters, &new_rate_limiters));
+ assert!(Arc::ptr_eq(&stats, &new_stats));
+ println!(" ✓ 连接状态和统计信息被完整保留");
+
+ println!("\n=== 性能优化效果 ===");
+ println!("✓ 无锁访问:处理器内部不再有任何锁");
+ println!("✓ 零拷贝:规则访问直接引用,无需克隆Arc");
+ println!("✓ 热加载:创建新实例替换,保留所有状态");
+ println!("✓ 内存效率:消除了多层Arc包装的开销");
+ }
+
+ #[tokio::test]
+ async fn test_performance_and_security_balance() {
+ // Create ACL config with different rule types
+ let mut acl_config = Acl::default();
+
+ let mut acl_v1 = AclV1::default();
+ let mut chain = Chain {
+ name: "performance_test".to_string(),
+ chain_type: ChainType::Inbound as i32,
+ enabled: true,
+ ..Default::default()
+ };
+
+ // 1. High-priority simple rule for UDP (can be cached efficiently)
+ let simple_rule = Rule {
+ name: "simple_udp".to_string(),
+ priority: 300,
+ enabled: true,
+ action: Action::Allow as i32,
+ protocol: Protocol::Udp as i32,
+ ..Default::default()
+ };
+ // No stateful or rate limit - can benefit from full cache optimization
+ chain.rules.push(simple_rule);
+
+ // 2. Medium-priority stateful + rate-limited rule for TCP (security critical)
+ let security_rule = Rule {
+ name: "security_tcp".to_string(),
+ priority: 200,
+ enabled: true,
+ action: Action::Allow as i32,
+ protocol: Protocol::Tcp as i32,
+ stateful: true,
+ rate_limit: 100,
+ burst_limit: 200,
+ ..Default::default()
+ };
+ chain.rules.push(security_rule);
+
+ // 3. Low-priority default allow rule for Any
+ let default_rule = Rule {
+ name: "default_allow".to_string(),
+ priority: 100,
+ enabled: true,
+ action: Action::Allow as i32,
+ protocol: Protocol::Any as i32,
+ ..Default::default()
+ };
+ chain.rules.push(default_rule);
+
+ acl_v1.chains.push(chain);
+ acl_config.acl_v1 = Some(acl_v1);
+
+ let processor = AclProcessor::new(acl_config);
+
+ // Test simple UDP packet (should hit high-priority simple rule and be cached)
+ let udp_packet = PacketInfo {
+ src_ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
+ dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
+ src_port: Some(12345),
+ dst_port: Some(53), // DNS
+ protocol: Protocol::Udp, // UDP
+ packet_size: 512,
+ };
+
+ // Test TCP packet (should hit stateful+rate-limited rule)
+ let tcp_packet = PacketInfo {
+ src_ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
+ dst_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
+ src_port: Some(12345),
+ dst_port: Some(80), // HTTP
+ protocol: Protocol::Tcp, // TCP
+ packet_size: 1024,
+ };
+
+ // Process UDP packets multiple times
+ println!("\n=== Performance Test Results ===");
+ for i in 1..=5 {
+ let result = processor.process_packet(&udp_packet, ChainType::Inbound);
+ assert_eq!(result.action, Action::Allow);
+ // UDP packets should match the highest priority rule that applies
+ // Since all rules allow "Any" protocol, UDP will match the highest priority one
+ println!(
+ "UDP packet {}: Allowed by rule (priority {:?})",
+ i, result.matched_rule
+ );
+ }
+
+ // Process TCP packets multiple times (stateful + rate limited)
+ for i in 1..=3 {
+ let result = processor.process_packet(&tcp_packet, ChainType::Inbound);
+ println!(
+ "TCP packet {}: {:?} by rule (priority {:?})",
+ i, result.action, result.matched_rule
+ );
+ }
+
+ let stats = processor.get_stats();
+ println!("\nStatistics:");
+ println!(
+ " Cache hits: {}",
+ stats.get(&AclStatKey::CacheHits.as_str()).unwrap_or(&0)
+ );
+ println!(
+ " Rule matches: {}",
+ stats.get(&AclStatKey::RuleMatches.as_str()).unwrap_or(&0)
+ );
+ println!(
+ " Cache hit rate: {:.1}%",
+ processor.get_cache_hit_rate() * 100.0
+ );
+
+ println!("\n✓ Stateful + rate-limited rules: Always processed for security");
+ println!("✓ Simple rules: Cached for performance");
+ println!(
+ "✓ Cache hit rate: {:.1}%",
+ processor.get_cache_hit_rate() * 100.0
+ );
+ }
+
+ #[test]
+ fn test_rate_limit_drop_log_context() {
+ // Test that RateLimitDrop log context is properly created
+ let context = AclLogContext::RateLimitDrop;
+ let message = context.to_message();
+ assert_eq!(message, "Rate limit drop");
+ }
+
+ #[tokio::test]
+ async fn test_rate_limit_drop_behavior() {
+ let mut acl_config = create_test_acl_config();
+
+ // Create a very restrictive rate-limited rule
+ if let Some(ref mut acl_v1) = acl_config.acl_v1 {
+ let rule = Rule {
+ name: "strict_rate_limit".to_string(),
+ priority: 200,
+ enabled: true,
+ action: Action::Allow as i32,
+ protocol: Protocol::Any as i32,
+ rate_limit: 1, // Allow only 1 packet per second
+ burst_limit: 1, // Burst of 1 packet
+ ..Default::default()
+ };
+ acl_v1.chains[0].rules.push(rule);
+ }
+
+ let processor = AclProcessor::new(acl_config);
+ let packet_info = create_test_packet_info();
+
+ // First request should be allowed
+ let result1 = processor.process_packet(&packet_info, ChainType::Inbound);
+ assert_eq!(result1.action, Action::Allow);
+ assert_eq!(result1.matched_rule, Some(RuleId::Priority(200)));
+
+ // Second request should be rate limited and dropped immediately
+ let result2 = processor.process_packet(&packet_info, ChainType::Inbound);
+ assert_eq!(result2.action, Action::Drop);
+ assert_eq!(result2.matched_rule, Some(RuleId::Priority(200)));
+ assert!(!result2.should_log);
+
+ // Verify the specific log context
+ assert!(matches!(
+ result2.log_context,
+ Some(AclLogContext::RateLimitDrop)
+ ));
+ }
+}
diff --git a/easytier/src/common/compressor.rs b/easytier/src/common/compressor.rs
index 7366378a9..6e8d0143a 100644
--- a/easytier/src/common/compressor.rs
+++ b/easytier/src/common/compressor.rs
@@ -21,6 +21,12 @@ pub trait Compressor {
pub struct DefaultCompressor {}
+impl Default for DefaultCompressor {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
impl DefaultCompressor {
pub fn new() -> Self {
DefaultCompressor {}
@@ -195,11 +201,11 @@ pub mod tests {
packet,
packet.payload_len()
);
- assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), true);
+ assert!(packet.peer_manager_header().unwrap().is_compressed());
compressor.decompress(&mut packet).await.unwrap();
assert_eq!(packet.payload(), text);
- assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false);
+ assert!(!packet.peer_manager_header().unwrap().is_compressed());
}
#[tokio::test]
@@ -215,10 +221,10 @@ pub mod tests {
.compress(&mut packet, CompressorAlgo::ZstdDefault)
.await
.unwrap();
- assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false);
+ assert!(!packet.peer_manager_header().unwrap().is_compressed());
compressor.decompress(&mut packet).await.unwrap();
assert_eq!(packet.payload(), text);
- assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), false);
+ assert!(!packet.peer_manager_header().unwrap().is_compressed());
}
}
diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs
index 1c1e8bf12..07f78945d 100644
--- a/easytier/src/common/config.rs
+++ b/easytier/src/common/config.rs
@@ -1,5 +1,6 @@
use std::{
- net::{Ipv4Addr, SocketAddr},
+ hash::Hasher,
+ net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{Arc, Mutex},
u64,
@@ -10,7 +11,10 @@ use cidr::IpCidr;
use serde::{Deserialize, Serialize};
use crate::{
- proto::common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
+ proto::{
+ acl::Acl,
+ common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
+ },
tunnel::generate_digest_from_str,
};
@@ -37,15 +41,87 @@ pub fn gen_default_flags() -> Flags {
bind_device: true,
enable_kcp_proxy: false,
disable_kcp_input: false,
- disable_relay_kcp: true,
+ disable_relay_kcp: false,
+ enable_relay_foreign_network_kcp: false,
accept_dns: false,
private_mode: false,
enable_quic_proxy: false,
disable_quic_input: false,
foreign_relay_bps_limit: u64::MAX,
+ multi_thread_count: 2,
+ encryption_algorithm: "aes-gcm".to_string(),
}
}
+pub enum EncryptionAlgorithm {
+ AesGcm,
+ Aes256Gcm,
+ Xor,
+ #[cfg(feature = "wireguard")]
+ ChaCha20,
+
+ #[cfg(feature = "openssl-crypto")]
+ OpensslAesGcm,
+ #[cfg(feature = "openssl-crypto")]
+ OpensslChacha20,
+ #[cfg(feature = "openssl-crypto")]
+ OpensslAes256Gcm,
+}
+
+impl std::fmt::Display for EncryptionAlgorithm {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::AesGcm => write!(f, "aes-gcm"),
+ Self::Aes256Gcm => write!(f, "aes-256-gcm"),
+ Self::Xor => write!(f, "xor"),
+ #[cfg(feature = "wireguard")]
+ Self::ChaCha20 => write!(f, "chacha20"),
+ #[cfg(feature = "openssl-crypto")]
+ Self::OpensslAesGcm => write!(f, "openssl-aes-gcm"),
+ #[cfg(feature = "openssl-crypto")]
+ Self::OpensslChacha20 => write!(f, "openssl-chacha20"),
+ #[cfg(feature = "openssl-crypto")]
+ Self::OpensslAes256Gcm => write!(f, "openssl-aes-256-gcm"),
+ }
+ }
+}
+
+impl TryFrom<&str> for EncryptionAlgorithm {
+ type Error = anyhow::Error;
+
+ fn try_from(value: &str) -> Result {
+ match value {
+ "aes-gcm" => Ok(Self::AesGcm),
+ "aes-256-gcm" => Ok(Self::Aes256Gcm),
+ "xor" => Ok(Self::Xor),
+ #[cfg(feature = "wireguard")]
+ "chacha20" => Ok(Self::ChaCha20),
+ #[cfg(feature = "openssl-crypto")]
+ "openssl-aes-gcm" => Ok(Self::OpensslAesGcm),
+ #[cfg(feature = "openssl-crypto")]
+ "openssl-chacha20" => Ok(Self::OpensslChacha20),
+ #[cfg(feature = "openssl-crypto")]
+ "openssl-aes-256-gcm" => Ok(Self::OpensslAes256Gcm),
+ _ => Err(anyhow::anyhow!("invalid encryption algorithm")),
+ }
+ }
+}
+
+pub fn get_avaliable_encrypt_methods() -> Vec<&'static str> {
+ let mut r = vec!["aes-gcm", "aes-256-gcm", "xor"];
+ if cfg!(feature = "wireguard") {
+ r.push("chacha20");
+ }
+ if cfg!(feature = "openssl-crypto") {
+ r.extend(vec![
+ "openssl-aes-gcm",
+ "openssl-chacha20",
+ "openssl-aes-256-gcm",
+ ]);
+ }
+ r
+}
+
#[auto_impl::auto_impl(Box, &)]
pub trait ConfigLoader: Send + Sync {
fn get_id(&self) -> uuid::Uuid;
@@ -63,10 +139,17 @@ pub trait ConfigLoader: Send + Sync {
fn get_ipv4(&self) -> Option;
fn set_ipv4(&self, addr: Option);
+ fn get_ipv6(&self) -> Option;
+ fn set_ipv6(&self, addr: Option);
+
fn get_dhcp(&self) -> bool;
fn set_dhcp(&self, dhcp: bool);
- fn add_proxy_cidr(&self, cidr: cidr::Ipv4Cidr, mapped_cidr: Option);
+ fn add_proxy_cidr(
+ &self,
+ cidr: cidr::Ipv4Cidr,
+ mapped_cidr: Option,
+ ) -> Result<(), anyhow::Error>;
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr);
fn get_proxy_cidrs(&self) -> Vec;
@@ -96,8 +179,8 @@ pub trait ConfigLoader: Send + Sync {
fn get_flags(&self) -> Flags;
fn set_flags(&self, flags: Flags);
- fn get_exit_nodes(&self) -> Vec;
- fn set_exit_nodes(&self, nodes: Vec);
+ fn get_exit_nodes(&self) -> Vec;
+ fn set_exit_nodes(&self, nodes: Vec);
fn get_routes(&self) -> Option>;
fn set_routes(&self, routes: Option>);
@@ -108,6 +191,15 @@ pub trait ConfigLoader: Send + Sync {
fn get_port_forwards(&self) -> Vec;
fn set_port_forwards(&self, forwards: Vec);
+ fn get_acl(&self) -> Option;
+ fn set_acl(&self, acl: Option);
+
+ fn get_tcp_whitelist(&self) -> Vec;
+ fn set_tcp_whitelist(&self, whitelist: Vec);
+
+ fn get_udp_whitelist(&self) -> Vec;
+ fn set_udp_whitelist(&self, whitelist: Vec);
+
fn dump(&self) -> String;
}
@@ -119,7 +211,7 @@ pub trait LoggingConfigLoader {
pub type NetworkSecretDigest = [u8; 32];
-#[derive(Debug, Clone, Deserialize, Serialize, Default, Eq, Hash)]
+#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkIdentity {
pub network_name: String,
pub network_secret: Option,
@@ -127,27 +219,53 @@ pub struct NetworkIdentity {
pub network_secret_digest: Option,
}
-impl PartialEq for NetworkIdentity {
- fn eq(&self, other: &Self) -> bool {
- if self.network_name != other.network_name {
- return false;
- }
+#[derive(Eq, PartialEq, Hash)]
+struct NetworkIdentityWithOnlyDigest {
+ network_name: String,
+ network_secret_digest: Option,
+}
- if self.network_secret.is_some()
- && other.network_secret.is_some()
- && self.network_secret != other.network_secret
- {
- return false;
+impl From for NetworkIdentityWithOnlyDigest {
+ fn from(identity: NetworkIdentity) -> Self {
+ if identity.network_secret_digest.is_some() {
+ Self {
+ network_name: identity.network_name,
+ network_secret_digest: identity.network_secret_digest,
+ }
+ } else if identity.network_secret.is_some() {
+ let mut network_secret_digest = [0u8; 32];
+ generate_digest_from_str(
+ &identity.network_name,
+ identity.network_secret.as_ref().unwrap(),
+ &mut network_secret_digest,
+ );
+ Self {
+ network_name: identity.network_name,
+ network_secret_digest: Some(network_secret_digest),
+ }
+ } else {
+ Self {
+ network_name: identity.network_name,
+ network_secret_digest: None,
+ }
}
+ }
+}
- if self.network_secret_digest.is_some()
- && other.network_secret_digest.is_some()
- && self.network_secret_digest != other.network_secret_digest
- {
- return false;
- }
+impl PartialEq for NetworkIdentity {
+ fn eq(&self, other: &Self) -> bool {
+ let self_with_digest = NetworkIdentityWithOnlyDigest::from(self.clone());
+ let other_with_digest = NetworkIdentityWithOnlyDigest::from(other.clone());
+ self_with_digest == other_with_digest
+ }
+}
+
+impl Eq for NetworkIdentity {}
- return true;
+impl std::hash::Hash for NetworkIdentity {
+ fn hash(&self, state: &mut H) {
+ let self_with_digest = NetworkIdentityWithOnlyDigest::from(self.clone());
+ self_with_digest.hash(state);
}
}
@@ -162,8 +280,10 @@ impl NetworkIdentity {
network_secret_digest: Some(network_secret_digest),
}
}
+}
- pub fn default() -> Self {
+impl Default for NetworkIdentity {
+ fn default() -> Self {
Self::new("default".to_string(), "".to_string())
}
}
@@ -216,7 +336,7 @@ pub struct VpnPortalConfig {
pub wireguard_listen: SocketAddr,
}
-#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub struct PortForwardConfig {
pub bind_addr: SocketAddr,
pub dst_addr: SocketAddr,
@@ -237,12 +357,12 @@ impl From for PortForwardConfig {
}
}
-impl Into for PortForwardConfig {
- fn into(self) -> PortForwardConfigPb {
+impl From for PortForwardConfigPb {
+ fn from(val: PortForwardConfig) -> Self {
PortForwardConfigPb {
- bind_addr: Some(self.bind_addr.into()),
- dst_addr: Some(self.dst_addr.into()),
- socket_type: match self.proto.to_lowercase().as_str() {
+ bind_addr: Some(val.bind_addr.into()),
+ dst_addr: Some(val.dst_addr.into()),
+ socket_type: match val.proto.to_lowercase().as_str() {
"tcp" => SocketType::Tcp as i32,
"udp" => SocketType::Udp as i32,
_ => SocketType::Tcp as i32,
@@ -258,11 +378,12 @@ struct Config {
instance_name: Option,
instance_id: Option,
ipv4: Option,
+ ipv6: Option,
dhcp: Option,
network_identity: Option,
listeners: Option>,
mapped_listeners: Option>,
- exit_nodes: Option>,
+ exit_nodes: Option>,
peer: Option>,
proxy_network: Option>,
@@ -282,6 +403,11 @@ struct Config {
#[serde(skip)]
flags_struct: Option,
+
+ acl: Option,
+
+ tcp_whitelist: Option>,
+ udp_whitelist: Option>,
}
#[derive(Debug, Clone)]
@@ -396,8 +522,7 @@ impl ConfigLoader for TomlConfigLoader {
locked_config
.ipv4
.as_ref()
- .map(|s| s.parse().ok())
- .flatten()
+ .and_then(|s| s.parse().ok())
.map(|c: cidr::Ipv4Inet| {
if c.network_length() == 32 {
cidr::Ipv4Inet::new(c.address(), 24).unwrap()
@@ -408,11 +533,16 @@ impl ConfigLoader for TomlConfigLoader {
}
fn set_ipv4(&self, addr: Option) {
- self.config.lock().unwrap().ipv4 = if let Some(addr) = addr {
- Some(addr.to_string())
- } else {
- None
- };
+ self.config.lock().unwrap().ipv4 = addr.map(|addr| addr.to_string());
+ }
+
+ fn get_ipv6(&self) -> Option {
+ let locked_config = self.config.lock().unwrap();
+ locked_config.ipv6.as_ref().and_then(|s| s.parse().ok())
+ }
+
+ fn set_ipv6(&self, addr: Option) {
+ self.config.lock().unwrap().ipv6 = addr.map(|addr| addr.to_string());
}
fn get_dhcp(&self) -> bool {
@@ -423,17 +553,23 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().dhcp = Some(dhcp);
}
- fn add_proxy_cidr(&self, cidr: cidr::Ipv4Cidr, mapped_cidr: Option) {
+ fn add_proxy_cidr(
+ &self,
+ cidr: cidr::Ipv4Cidr,
+ mapped_cidr: Option,
+ ) -> Result<(), anyhow::Error> {
let mut locked_config = self.config.lock().unwrap();
if locked_config.proxy_network.is_none() {
locked_config.proxy_network = Some(vec![]);
}
if let Some(mapped_cidr) = mapped_cidr.as_ref() {
- assert_eq!(
- cidr.network_length(),
- mapped_cidr.network_length(),
- "Mapped CIDR must have the same network length as the original CIDR",
- );
+ if cidr.network_length() != mapped_cidr.network_length() {
+ return Err(anyhow::anyhow!(
+ "Mapped CIDR must have the same network length as the original CIDR: {} != {}",
+ cidr.network_length(),
+ mapped_cidr.network_length()
+ ));
+ }
}
// insert if no duplicate
if !locked_config
@@ -453,6 +589,7 @@ impl ConfigLoader for TomlConfigLoader {
allow: None,
});
}
+ Ok(())
}
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr) {
@@ -479,7 +616,7 @@ impl ConfigLoader for TomlConfigLoader {
locked_config.instance_id = Some(id);
id
} else {
- locked_config.instance_id.as_ref().unwrap().clone()
+ *locked_config.instance_id.as_ref().unwrap()
}
}
@@ -493,7 +630,7 @@ impl ConfigLoader for TomlConfigLoader {
.unwrap()
.network_identity
.clone()
- .unwrap_or_else(NetworkIdentity::default)
+ .unwrap_or_default()
}
fn set_network_identity(&self, identity: NetworkIdentity) {
@@ -574,7 +711,7 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().flags_struct = Some(flags);
}
- fn get_exit_nodes(&self) -> Vec {
+ fn get_exit_nodes(&self) -> Vec {
self.config
.lock()
.unwrap()
@@ -583,7 +720,7 @@ impl ConfigLoader for TomlConfigLoader {
.unwrap_or_default()
}
- fn set_exit_nodes(&self, nodes: Vec) {
+ fn set_exit_nodes(&self, nodes: Vec) {
self.config.lock().unwrap().exit_nodes = Some(nodes);
}
@@ -616,6 +753,40 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().port_forward = Some(forwards);
}
+ fn get_acl(&self) -> Option {
+ self.config.lock().unwrap().acl.clone()
+ }
+
+ fn set_acl(&self, acl: Option) {
+ self.config.lock().unwrap().acl = acl;
+ }
+
+ fn get_tcp_whitelist(&self) -> Vec {
+ self.config
+ .lock()
+ .unwrap()
+ .tcp_whitelist
+ .clone()
+ .unwrap_or_default()
+ }
+
+ fn set_tcp_whitelist(&self, whitelist: Vec) {
+ self.config.lock().unwrap().tcp_whitelist = Some(whitelist);
+ }
+
+ fn get_udp_whitelist(&self) -> Vec {
+ self.config
+ .lock()
+ .unwrap()
+ .udp_whitelist
+ .clone()
+ .unwrap_or_default()
+ }
+
+ fn set_udp_whitelist(&self, whitelist: Vec) {
+ self.config.lock().unwrap().udp_whitelist = Some(whitelist);
+ }
+
fn dump(&self) -> String {
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
let default_flags_hashmap =
diff --git a/easytier/src/common/constants.rs b/easytier/src/common/constants.rs
index fffe2c6e9..3544b1390 100644
--- a/easytier/src/common/constants.rs
+++ b/easytier/src/common/constants.rs
@@ -8,14 +8,14 @@ macro_rules! define_global_var {
#[macro_export]
macro_rules! use_global_var {
($name:ident) => {
- crate::common::constants::$name.lock().unwrap().to_owned()
+ $crate::common::constants::$name.lock().unwrap().to_owned()
};
}
#[macro_export]
macro_rules! set_global_var {
($name:ident, $val:expr) => {
- *crate::common::constants::$name.lock().unwrap() = $val
+ *$crate::common::constants::$name.lock().unwrap() = $val
};
}
@@ -27,6 +27,8 @@ define_global_var!(MACHINE_UID, Option, None);
define_global_var!(MAX_DIRECT_CONNS_PER_PEER_IN_FOREIGN_NETWORK, u32, 3);
+define_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER, bool, true);
+
pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2;
pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\WorkDir";
diff --git a/easytier/src/common/defer.rs b/easytier/src/common/defer.rs
index 1132c9b75..c243d1ce8 100644
--- a/easytier/src/common/defer.rs
+++ b/easytier/src/common/defer.rs
@@ -12,7 +12,9 @@ impl Defer {
impl Drop for Defer {
fn drop(&mut self) {
- self.func.take().map(|f| f());
+ if let Some(f) = self.func.take() {
+ f()
+ }
}
}
diff --git a/easytier/src/common/dns.rs b/easytier/src/common/dns.rs
index 5ff4f081f..9e638323a 100644
--- a/easytier/src/common/dns.rs
+++ b/easytier/src/common/dns.rs
@@ -48,19 +48,15 @@ pub static RESOLVER: Lazy>>>
pub async fn resolve_txt_record(domain_name: &str) -> Result {
let r = RESOLVER.clone();
- let response = r.txt_lookup(domain_name).await.with_context(|| {
- format!(
- "txt_lookup failed, domain_name: {}",
- domain_name.to_string()
- )
- })?;
+ let response = r
+ .txt_lookup(domain_name)
+ .await
+ .with_context(|| format!("txt_lookup failed, domain_name: {}", domain_name))?;
- let txt_record = response.iter().next().with_context(|| {
- format!(
- "no txt record found, domain_name: {}",
- domain_name.to_string()
- )
- })?;
+ let txt_record = response
+ .iter()
+ .next()
+ .with_context(|| format!("no txt record found, domain_name: {}", domain_name))?;
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
tracing::info!(?txt_data, ?domain_name, "get txt record");
@@ -127,7 +123,7 @@ mod tests {
#[tokio::test]
async fn test_socket_addrs() {
- let url = url::Url::parse("tcp://public.easytier.cn:80").unwrap();
+ let url = url::Url::parse("tcp://github-ci-test.easytier.cn:80").unwrap();
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs: {:?}", addrs);
diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs
index 59df861c6..2c5e2cb25 100644
--- a/easytier/src/common/global_ctx.rs
+++ b/easytier/src/common/global_ctx.rs
@@ -5,7 +5,9 @@ use std::{
};
use crate::common::config::ProxyNetworkConfig;
+use crate::common::stats_manager::StatsManager;
use crate::common::token_bucket::TokenBucketManager;
+use crate::peers::acl_filter::AclFilter;
use crate::proto::cli::PeerConnInfo;
use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
use crossbeam::atomic::AtomicCell;
@@ -61,6 +63,7 @@ pub struct GlobalCtx {
event_bus: EventBus,
cached_ipv4: AtomicCell>,
+ cached_ipv6: AtomicCell >,
cached_proxy_cidrs: AtomicCell >>,
ip_collector: Mutex >>,
@@ -80,6 +83,10 @@ pub struct GlobalCtx {
quic_proxy_port: AtomicCell >,
token_bucket_manager: TokenBucketManager,
+
+ stats_manager: Arc,
+
+ acl_filter: Arc,
}
impl std::fmt::Debug for GlobalCtx {
@@ -97,7 +104,7 @@ impl std::fmt::Debug for GlobalCtx {
pub type ArcGlobalCtx = std::sync::Arc;
impl GlobalCtx {
- pub fn new(config_fs: impl ConfigLoader + 'static + Send + Sync) -> Self {
+ pub fn new(config_fs: impl ConfigLoader + 'static) -> Self {
let id = config_fs.get_id();
let network = config_fs.get_network_identity();
let net_ns = NetNS::new(config_fs.get_netns());
@@ -107,13 +114,15 @@ impl GlobalCtx {
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
- let enable_exit_node = config_fs.get_flags().enable_exit_node;
+ let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env = "ohos");
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
let no_tun = config_fs.get_flags().no_tun;
- let mut feature_flags = PeerFeatureFlag::default();
- feature_flags.kcp_input = !config_fs.get_flags().disable_kcp_input;
- feature_flags.no_relay_kcp = config_fs.get_flags().disable_relay_kcp;
+ let feature_flags = PeerFeatureFlag {
+ kcp_input: !config_fs.get_flags().disable_kcp_input,
+ no_relay_kcp: config_fs.get_flags().disable_relay_kcp,
+ ..Default::default()
+ };
GlobalCtx {
inst_name: config_fs.get_inst_name(),
@@ -124,6 +133,7 @@ impl GlobalCtx {
event_bus,
cached_ipv4: AtomicCell::new(None),
+ cached_ipv6: AtomicCell::new(None),
cached_proxy_cidrs: AtomicCell::new(None),
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
@@ -145,6 +155,10 @@ impl GlobalCtx {
quic_proxy_port: AtomicCell::new(None),
token_bucket_manager: TokenBucketManager::new(),
+
+ stats_manager: Arc::new(StatsManager::new()),
+
+ acl_filter: Arc::new(AclFilter::new()),
}
}
@@ -173,7 +187,7 @@ impl GlobalCtx {
{
Ok(())
} else {
- Err(anyhow::anyhow!("network {} not in whitelist", network_name).into())
+ Err(anyhow::anyhow!("network {} not in whitelist", network_name))
}
}
@@ -182,8 +196,8 @@ impl GlobalCtx {
return Some(ret);
}
let addr = self.config.get_ipv4();
- self.cached_ipv4.store(addr.clone());
- return addr;
+ self.cached_ipv4.store(addr);
+ addr
}
pub fn set_ipv4(&self, addr: Option) {
@@ -191,6 +205,20 @@ impl GlobalCtx {
self.cached_ipv4.store(None);
}
+ pub fn get_ipv6(&self) -> Option {
+ if let Some(ret) = self.cached_ipv6.load() {
+ return Some(ret);
+ }
+ let addr = self.config.get_ipv6();
+ self.cached_ipv6.store(addr);
+ addr
+ }
+
+ pub fn set_ipv6(&self, addr: Option) {
+ self.config.set_ipv6(addr);
+ self.cached_ipv6.store(None);
+ }
+
pub fn get_id(&self) -> uuid::Uuid {
self.config.get_id()
}
@@ -270,6 +298,29 @@ impl GlobalCtx {
key
}
+ pub fn get_256_key(&self) -> [u8; 32] {
+ let mut key = [0u8; 32];
+ let secret = self
+ .config
+ .get_network_identity()
+ .network_secret
+ .unwrap_or_default();
+ // fill key according to network secret
+ let mut hasher = DefaultHasher::new();
+ hasher.write(secret.as_bytes());
+ hasher.write(b"easytier-256bit-key"); // 添加固定盐值以区分128位和256位密钥
+
+ // 生成32字节密钥
+ for i in 0..4 {
+ let chunk_start = i * 8;
+ let chunk_end = chunk_start + 8;
+ hasher.write(&key[0..chunk_start]);
+ hasher.write(&[i as u8]); // 添加索引以确保每个8字节块都不同
+ key[chunk_start..chunk_end].copy_from_slice(&hasher.finish().to_be_bytes());
+ }
+ key
+ }
+
pub fn enable_exit_node(&self) -> bool {
self.enable_exit_node
}
@@ -301,6 +352,14 @@ impl GlobalCtx {
pub fn token_bucket_manager(&self) -> &TokenBucketManager {
&self.token_bucket_manager
}
+
+ pub fn stats_manager(&self) -> &Arc {
+ &self.stats_manager
+ }
+
+ pub fn get_acl_filter(&self) -> &Arc {
+ &self.acl_filter
+ }
}
#[cfg(test)]
@@ -319,18 +378,18 @@ pub mod tests {
let mut subscriber = global_ctx.subscribe();
let peer_id = new_peer_id();
- global_ctx.issue_event(GlobalCtxEvent::PeerAdded(peer_id.clone()));
- global_ctx.issue_event(GlobalCtxEvent::PeerRemoved(peer_id.clone()));
+ global_ctx.issue_event(GlobalCtxEvent::PeerAdded(peer_id));
+ global_ctx.issue_event(GlobalCtxEvent::PeerRemoved(peer_id));
global_ctx.issue_event(GlobalCtxEvent::PeerConnAdded(PeerConnInfo::default()));
global_ctx.issue_event(GlobalCtxEvent::PeerConnRemoved(PeerConnInfo::default()));
assert_eq!(
subscriber.recv().await.unwrap(),
- GlobalCtxEvent::PeerAdded(peer_id.clone())
+ GlobalCtxEvent::PeerAdded(peer_id)
);
assert_eq!(
subscriber.recv().await.unwrap(),
- GlobalCtxEvent::PeerRemoved(peer_id.clone())
+ GlobalCtxEvent::PeerRemoved(peer_id)
);
assert_eq!(
subscriber.recv().await.unwrap(),
@@ -347,7 +406,7 @@ pub mod tests {
) -> ArcGlobalCtx {
let config_fs = TomlConfigLoader::default();
config_fs.set_inst_name(format!("test_{}", config_fs.get_id()));
- config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default()));
+ config_fs.set_network_identity(network_identy.unwrap_or_default());
let ctx = Arc::new(GlobalCtx::new(config_fs));
ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
diff --git a/easytier/src/common/ifcfg/darwin.rs b/easytier/src/common/ifcfg/darwin.rs
index 2cf13bea6..3c751534a 100644
--- a/easytier/src/common/ifcfg/darwin.rs
+++ b/easytier/src/common/ifcfg/darwin.rs
@@ -1,8 +1,8 @@
use std::net::Ipv4Addr;
-use async_trait::async_trait;
-
use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
+use async_trait::async_trait;
+use cidr::{Ipv4Inet, Ipv6Inet};
pub struct MacIfConfiger {}
#[async_trait]
@@ -66,12 +66,17 @@ impl IfConfiguerTrait for MacIfConfiger {
.await
}
- async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
+ async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
if ip.is_none() {
run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await
} else {
run_shell_cmd(
- format!("ifconfig {} inet {} delete", name, ip.unwrap().to_string()).as_str(),
+ format!(
+ "ifconfig {} inet {} delete",
+ name,
+ ip.unwrap().address().to_string()
+ )
+ .as_str(),
)
.await
}
@@ -80,4 +85,60 @@ impl IfConfiguerTrait for MacIfConfiger {
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await
}
+
+ async fn add_ipv6_ip(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ run_shell_cmd(format!("ifconfig {} inet6 {}/{} add", name, address, cidr_prefix).as_str())
+ .await
+ }
+
+ async fn remove_ipv6(&self, name: &str, ip: Option) -> Result<(), Error> {
+ if let Some(ip) = ip {
+ run_shell_cmd(format!("ifconfig {} inet6 {} delete", name, ip.address()).as_str()).await
+ } else {
+ // Remove all IPv6 addresses is more complex on macOS, just succeed
+ Ok(())
+ }
+ }
+
+ async fn add_ipv6_route(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ cost: Option,
+ ) -> Result<(), Error> {
+ let cmd = if let Some(cost) = cost {
+ format!(
+ "route -n add -inet6 {}/{} -interface {} -hopcount {}",
+ address, cidr_prefix, name, cost
+ )
+ } else {
+ format!(
+ "route -n add -inet6 {}/{} -interface {}",
+ address, cidr_prefix, name
+ )
+ };
+ run_shell_cmd(cmd.as_str()).await
+ }
+
+ async fn remove_ipv6_route(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ run_shell_cmd(
+ format!(
+ "route -n delete -inet6 {}/{} -interface {}",
+ address, cidr_prefix, name
+ )
+ .as_str(),
+ )
+ .await
+ }
}
diff --git a/easytier/src/common/ifcfg/mod.rs b/easytier/src/common/ifcfg/mod.rs
index e779cacd8..f5bd223a8 100644
--- a/easytier/src/common/ifcfg/mod.rs
+++ b/easytier/src/common/ifcfg/mod.rs
@@ -1,15 +1,18 @@
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
mod darwin;
-#[cfg(any(target_os = "linux"))]
+#[cfg(target_os = "linux")]
mod netlink;
#[cfg(target_os = "windows")]
+mod win;
+#[cfg(target_os = "windows")]
mod windows;
mod route;
-use std::net::Ipv4Addr;
+use std::net::{Ipv4Addr, Ipv6Addr};
use async_trait::async_trait;
+use cidr::{Ipv4Inet, Ipv6Inet};
use tokio::process::Command;
use super::error::Error;
@@ -41,10 +44,38 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> {
Ok(())
}
+ async fn add_ipv6_route(
+ &self,
+ _name: &str,
+ _address: Ipv6Addr,
+ _cidr_prefix: u8,
+ _cost: Option,
+ ) -> Result<(), Error> {
+ Ok(())
+ }
+ async fn remove_ipv6_route(
+ &self,
+ _name: &str,
+ _address: Ipv6Addr,
+ _cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ Ok(())
+ }
+ async fn add_ipv6_ip(
+ &self,
+ _name: &str,
+ _address: Ipv6Addr,
+ _cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ Ok(())
+ }
async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> {
Ok(())
}
- async fn remove_ip(&self, _name: &str, _ip: Option) -> Result<(), Error> {
+ async fn remove_ip(&self, _name: &str, _ip: Option) -> Result<(), Error> {
+ Ok(())
+ }
+ async fn remove_ipv6(&self, _name: &str, _ip: Option) -> Result<(), Error> {
Ok(())
}
async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> {
@@ -110,7 +141,7 @@ pub struct DummyIfConfiger {}
#[async_trait]
impl IfConfiguerTrait for DummyIfConfiger {}
-#[cfg(any(target_os = "linux"))]
+#[cfg(target_os = "linux")]
pub type IfConfiger = netlink::NetlinkIfConfiger;
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
diff --git a/easytier/src/common/ifcfg/netlink.rs b/easytier/src/common/ifcfg/netlink.rs
index 80ecbd209..f51b83e13 100644
--- a/easytier/src/common/ifcfg/netlink.rs
+++ b/easytier/src/common/ifcfg/netlink.rs
@@ -8,7 +8,7 @@ use std::{
use anyhow::Context;
use async_trait::async_trait;
-use cidr::IpInet;
+use cidr::{IpInet, Ipv4Inet, Ipv6Inet};
use netlink_packet_core::{
NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable,
NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_EXCL, NLM_F_REQUEST,
@@ -85,14 +85,14 @@ fn send_netlink_req_and_wait_one_resp {
if e.code == NonZero::new(0) {
- return Ok(());
+ Ok(())
} else {
- return Err(e.to_io().into());
+ Err(e.to_io().into())
}
}
p => {
tracing::error!("Unexpected netlink response: {:?}", p);
- return Err(anyhow::anyhow!("Unexpected netlink response").into());
+ Err(anyhow::anyhow!("Unexpected netlink response").into())
}
}
}
@@ -194,6 +194,32 @@ impl NetlinkIfConfiger {
)
}
+ fn get_prefix_len_ipv6(name: &str, ip: Ipv6Addr) -> Result {
+ let addrs = Self::list_addresses(name)?;
+ for addr in addrs {
+ if addr.address() == IpAddr::V6(ip) {
+ return Ok(addr.network_length());
+ }
+ }
+ Err(Error::NotFound)
+ }
+
+ fn remove_one_ipv6(name: &str, ip: Ipv6Addr, prefix_len: u8) -> Result<(), Error> {
+ let mut message = AddressMessage::default();
+ message.header.prefix_len = prefix_len;
+ message.header.index = NetlinkIfConfiger::get_interface_index(name)?;
+ message.header.family = AddressFamily::Inet6;
+
+ message
+ .attributes
+ .push(AddressAttribute::Address(std::net::IpAddr::V6(ip)));
+
+ send_netlink_req_and_wait_one_resp::(
+ RouteNetlinkMessage::DelAddress(message),
+ true,
+ )
+ }
+
pub(crate) fn mtu_op>(
name: &str,
op: T,
@@ -237,8 +263,8 @@ impl NetlinkIfConfiger {
let (address, netmask) = match (address.family(), netmask.family()) {
(Some(Inet), Some(Inet)) => (
- IpAddr::V4(address.as_sockaddr_in().unwrap().ip().into()),
- IpAddr::V4(netmask.as_sockaddr_in().unwrap().ip().into()),
+ IpAddr::V4(address.as_sockaddr_in().unwrap().ip()),
+ IpAddr::V4(netmask.as_sockaddr_in().unwrap().ip()),
),
(Some(Inet6), Some(Inet6)) => (
IpAddr::V6(address.as_sockaddr_in6().unwrap().ip()),
@@ -307,7 +333,7 @@ impl NetlinkIfConfiger {
let mut resp = Vec::::new();
loop {
- if resp.len() == 0 {
+ if resp.is_empty() {
let (new_resp, _) = s.recv_from_full()?;
resp = new_resp;
}
@@ -447,7 +473,7 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
Ok(())
}
- async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
+ async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
if ip.is_none() {
let addrs = Self::list_addresses(name)?;
for addr in addrs {
@@ -457,8 +483,8 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
}
} else {
let ip = ip.unwrap();
- let prefix_len = Self::get_prefix_len(name, ip)?;
- Self::remove_one_ip(name, ip, prefix_len)?;
+ let prefix_len = Self::get_prefix_len(name, ip.address())?;
+ Self::remove_one_ip(name, ip.address(), prefix_len)?;
}
Ok(())
@@ -469,6 +495,106 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
Ok(())
}
+
+ async fn add_ipv6_ip(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ let mut message = AddressMessage::default();
+
+ message.header.prefix_len = cidr_prefix;
+ message.header.index = NetlinkIfConfiger::get_interface_index(name)?;
+ message.header.family = AddressFamily::Inet6;
+
+ message
+ .attributes
+ .push(AddressAttribute::Address(std::net::IpAddr::V6(address)));
+
+ // For IPv6, we don't need IFA_LOCAL or IFA_BROADCAST
+ send_netlink_req_and_wait_one_resp::(
+ RouteNetlinkMessage::NewAddress(message),
+ false,
+ )
+ }
+
+ async fn remove_ipv6(&self, name: &str, ip: Option) -> Result<(), Error> {
+ if ip.is_none() {
+ let addrs = Self::list_addresses(name)?;
+ for addr in addrs {
+ if let IpAddr::V6(ipv6) = addr.address() {
+ let prefix_len = addr.network_length();
+ Self::remove_one_ipv6(name, ipv6, prefix_len)?;
+ }
+ }
+ } else {
+ let ipv6 = ip.unwrap();
+ let prefix_len = Self::get_prefix_len_ipv6(name, ipv6.address())?;
+ Self::remove_one_ipv6(name, ipv6.address(), prefix_len)?;
+ }
+
+ Ok(())
+ }
+
+ async fn add_ipv6_route(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ cost: Option,
+ ) -> Result<(), Error> {
+ let mut message = RouteMessage::default();
+
+ message.header.address_family = AddressFamily::Inet6;
+ message.header.destination_prefix_length = cidr_prefix;
+ message.header.table = RouteHeader::RT_TABLE_MAIN;
+ message.header.protocol = RouteProtocol::Static;
+ message.header.scope = RouteScope::Universe;
+ message.header.kind = RouteType::Unicast;
+
+ // Add metric (cost) if specified
+ if let Some(cost) = cost {
+ message
+ .attributes
+ .push(RouteAttribute::Priority(cost as u32));
+ }
+
+ message
+ .attributes
+ .push(RouteAttribute::Oif(NetlinkIfConfiger::get_interface_index(
+ name,
+ )?));
+
+ message
+ .attributes
+ .push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
+
+ send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
+ }
+
+ async fn remove_ipv6_route(
+ &self,
+ name: &str,
+ address: std::net::Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ let routes = Self::list_routes()?;
+ let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
+
+ for msg in routes {
+ let other_route: Route = msg.clone().into();
+ if other_route.destination == std::net::IpAddr::V6(address)
+ && other_route.prefix == cidr_prefix
+ && other_route.ifindex == Some(ifidx)
+ {
+ send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?;
+ return Ok(());
+ }
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
diff --git a/easytier/src/common/ifcfg/win/luid.rs b/easytier/src/common/ifcfg/win/luid.rs
new file mode 100644
index 000000000..29af75c9d
--- /dev/null
+++ b/easytier/src/common/ifcfg/win/luid.rs
@@ -0,0 +1,745 @@
+//
+// Port supporting code from wireguard-windows, as used by 3rd-party/wireguard-go, to Rust
+// This file implements functionality similar to: wireguard-windows/tunnel/winipcfg/luid.go
+//
+// ATTENTION: NOT included are DNS() and SetDNS() - functions to query and set DNS servers for a network interface.
+//
+
+use super::netsh;
+use super::types::*;
+use cidr::Ipv4Inet;
+use cidr::Ipv6Inet;
+use std::net::{Ipv4Addr, Ipv6Addr};
+use std::ptr;
+use winapi::shared::{
+ guiddef::GUID, ifdef::NET_LUID, netioapi::*, nldef::*, winerror::*, ws2def::*, ws2ipdef::*,
+};
+
+pub struct InterfaceLuid {
+ luid: NET_LUID,
+}
+
+impl InterfaceLuid {
+ pub fn new(luid_value: u64) -> Self {
+ InterfaceLuid {
+ luid: winapi::shared::ifdef::NET_LUID_LH { Value: luid_value },
+ }
+ }
+
+ pub fn luid(&self) -> NET_LUID {
+ self.luid
+ }
+
+ /// get_ip_interface method retrieves IP information for the specified interface on the local computer.
+ pub fn get_ip_interface(
+ &self,
+ family: ADDRESS_FAMILY,
+ ) -> Result {
+ let mut row = MIB_IPINTERFACE_ROW::default();
+ unsafe { InitializeIpInterfaceEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.Family = family;
+
+ let result = unsafe { GetIpInterfaceEntry(&mut row) };
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-setipinterfaceentry
+ /// If only InterfaceIndex was specified, SetIpInterfaceEntry() will modify ipif with a correct InterfaceLuid
+ pub fn set_ip_interface(&self, ipif: *mut MIB_IPINTERFACE_ROW) -> Result<(), NETIO_STATUS> {
+ let result = unsafe { SetIpInterfaceEntry(ipif) };
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// get_interface method retrieves information for the specified adapter on the local computer.
+ /// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getifentry2
+ pub fn get_interface(&self) -> Result {
+ let mut row = MIB_IF_ROW2 {
+ InterfaceLuid: self.luid,
+ ..MIB_IF_ROW2::default()
+ };
+
+ let result = unsafe { GetIfEntry2(&mut row) };
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// GUID method converts a locally unique identifier (LUID) for a network interface to a globally unique identifier (GUID) for the interface.
+ /// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceluidtoguid
+ pub fn get_guid(&self) -> Result {
+ let mut interface_guid = GUID::default();
+
+ let result = unsafe { ConvertInterfaceLuidToGuid(&self.luid, &mut interface_guid) };
+
+ if NO_ERROR == result {
+ Ok(interface_guid)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// luid_from_guid function converts a globally unique identifier (GUID) for a network interface to the locally unique identifier (LUID) for the interface.
+ /// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceguidtoluid
+ pub fn luid_from_guid(interface_guid: &GUID) -> Result {
+ let mut interface_luid = NET_LUID::default();
+
+ let result = unsafe { ConvertInterfaceGuidToLuid(interface_guid, &mut interface_luid) };
+
+ if NO_ERROR == result {
+ Ok(Self {
+ luid: interface_luid,
+ })
+ } else {
+ Err(result)
+ }
+ }
+
+ /// luid_from_index function converts a local index for a network interface to the locally unique identifier (LUID) for the interface.
+ /// https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-convertinterfaceindextoluid
+ pub fn luid_from_index(interface_index: u32) -> Result {
+ let mut interface_luid = NET_LUID::default();
+
+ let result = unsafe { ConvertInterfaceIndexToLuid(interface_index, &mut interface_luid) };
+
+ if NO_ERROR == result {
+ Ok(Self {
+ luid: interface_luid,
+ })
+ } else {
+ Err(result)
+ }
+ }
+
+ /// get_from_ipv4_address method returns MibUnicastIPAddressRow struct that matches to provided 'ip' argument. Corresponds to GetUnicastIpAddressEntry
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getunicastipaddressentry)
+ pub fn get_from_ipv4_address(
+ &self,
+ ip: &Ipv4Addr,
+ ) -> Result {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ unsafe { *row.Address.Ipv4_mut() = convert_ipv4addr_to_sockaddr(ip) };
+
+ let result = unsafe { GetUnicastIpAddressEntry(&mut row) };
+
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// get_from_ipv6_address method returns MibUnicastIPAddressRow struct that matches to provided 'ip' argument. Corresponds to GetUnicastIpAddressEntry
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getunicastipaddressentry)
+ pub fn get_from_ipv6_address(
+ &self,
+ ip: &Ipv6Addr,
+ ) -> Result {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ unsafe { *row.Address.Ipv6_mut() = convert_ipv6addr_to_sockaddr(ip) };
+
+ let result = unsafe { GetUnicastIpAddressEntry(&mut row) };
+
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_ipv4_address method adds new unicast IP address to the interface. Corresponds to CreateUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
+ pub fn add_ipv4_address(&self, address: &Ipv4Inet) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe { *row.Address.Ipv4_mut() = convert_ipv4addr_to_sockaddr(&address.address()) };
+ row.OnLinkPrefixLength = address.network_length();
+
+ let result = unsafe { CreateUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_ipv6_address method adds new unicast IP address to the interface. Corresponds to CreateUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
+ pub fn add_ipv6_address(&self, address: &Ipv6Inet) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe { *row.Address.Ipv6_mut() = convert_ipv6addr_to_sockaddr(&address.address()) };
+ row.OnLinkPrefixLength = address.network_length();
+
+ let result = unsafe { CreateUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_ipv4_addresses method adds multiple new unicast IP addresses to the interface. Corresponds to CreateUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
+ pub fn add_ipv4_addresses(
+ &self,
+ addresses: impl IntoIterator- ,
+ ) -> Result<(), NETIO_STATUS> {
+ for ip in addresses.into_iter().enumerate() {
+ self.add_ipv4_address(&ip.1)?;
+ }
+ Ok(())
+ }
+
+ /// add_ipv6_addresses method adds multiple new unicast IP addresses to the interface. Corresponds to CreateUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createunicastipaddressentry).
+ pub fn add_ipv6_addresses(
+ &self,
+ addresses: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ for ip in addresses.into_iter().enumerate() {
+ self.add_ipv6_address(&ip.1)?;
+ }
+ Ok(())
+ }
+
+ /// set_ipv4_addresses method sets new unicast IP addresses to the interface.
+ pub fn set_ipv4_addresses(
+ &self,
+ addresses: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ self.flush_ipv4_addresses()?;
+ self.add_ipv4_addresses(addresses)?;
+ Ok(())
+ }
+
+ /// set_ipv6_addresses method sets new unicast IP addresses to the interface.
+ pub fn set_ipv6_addresses(
+ &self,
+ addresses: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ self.flush_ipv6_addresses()?;
+ self.add_ipv6_addresses(addresses)?;
+ Ok(())
+ }
+
+ /// delete_ipv4_address method deletes interface's unicast IP address. Corresponds to DeleteUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteunicastipaddressentry).
+ pub fn delete_ipv4_address(&self, address: &Ipv4Inet) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe { *row.Address.Ipv4_mut() = convert_ipv4addr_to_sockaddr(&address.address()) };
+ row.OnLinkPrefixLength = address.network_length();
+
+ let result = unsafe { DeleteUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// delete_ipv4_address method deletes interface's unicast IP address. Corresponds to DeleteUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteunicastipaddressentry).
+ pub fn delete_ipv4_address2(
+ &self,
+ address: *const SOCKADDR_IN,
+ prefix_len: u8,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ assert!(!address.is_null());
+ unsafe { *row.Address.Ipv4_mut() = *address };
+ row.OnLinkPrefixLength = prefix_len;
+
+ let result = unsafe { DeleteUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// delete_ipv6_address method deletes interface's unicast IP address. Corresponds to DeleteUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteunicastipaddressentry).
+ pub fn delete_ipv6_address(&self, address: &Ipv6Inet) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe { *row.Address.Ipv6_mut() = convert_ipv6addr_to_sockaddr(&address.address()) };
+ row.OnLinkPrefixLength = address.network_length();
+
+ let result = unsafe { DeleteUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// delete_ipv6_address method deletes interface's unicast IP address. Corresponds to DeleteUnicastIpAddressEntry function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteunicastipaddressentry).
+ pub fn delete_ipv6_address2(
+ &self,
+ address: *const SOCKADDR_IN6,
+ prefix_len: u8,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_UNICASTIPADDRESS_ROW::default();
+ unsafe { InitializeUnicastIpAddressEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.DadState = IpDadStatePreferred;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ assert!(!address.is_null());
+ unsafe { *row.Address.Ipv6_mut() = *address };
+ row.OnLinkPrefixLength = prefix_len;
+
+ let result = unsafe { DeleteUnicastIpAddressEntry(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// flush_ip_addresses method deletes all interface's unicast IP addresses.
+ pub fn flush_ip_addresses(&self, address_family: ADDRESS_FAMILY) -> Result<(), NETIO_STATUS> {
+ let mut p_table: PMIB_UNICASTIPADDRESS_TABLE = ptr::null_mut();
+ let result = unsafe { GetUnicastIpAddressTable(address_family, &mut p_table) };
+ if NO_ERROR != result {
+ return Err(result);
+ }
+
+ assert!(!p_table.is_null());
+ let num_entries = unsafe { *p_table }.NumEntries;
+ let x_table = unsafe { *p_table }.Table.as_ptr();
+ for i in 0..num_entries {
+ let current_entry = unsafe { x_table.add(i as _) };
+ if unsafe { (*current_entry).InterfaceLuid.Value } == self.luid.Value {
+ unsafe { DeleteUnicastIpAddressEntry(current_entry) };
+ }
+ }
+
+ unsafe { FreeMibTable(p_table as _) };
+
+ Ok(())
+ }
+
+ /// flush_ipv4_addresses method deletes all interface's unicast IP addresses.
+ pub fn flush_ipv4_addresses(&self) -> Result<(), NETIO_STATUS> {
+ self.flush_ip_addresses(AF_INET as _)
+ }
+
+ /// flush_ipv6_addresses method deletes all interface's unicast IP addresses.
+ pub fn flush_ipv6_addresses(&self) -> Result<(), NETIO_STATUS> {
+ self.flush_ip_addresses(AF_INET6 as _)
+ }
+
+ /// route_ipv4 method returns route determined with the input arguments. Corresponds to GetIpForwardEntry2 function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getipforwardentry2).
+ /// NOTE: If the corresponding route isn't found, the method will return error.
+ pub fn route_ipv4(
+ &self,
+ destination: &Ipv4Inet,
+ next_hop: &Ipv4Addr,
+ ) -> Result
{
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv4_mut() =
+ convert_ipv4addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv4_mut() = convert_ipv4addr_to_sockaddr(next_hop) };
+
+ let result = unsafe { GetIpForwardEntry2(&mut row) };
+
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// route_ipv6 method returns route determined with the input arguments. Corresponds to GetIpForwardEntry2 function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-getipforwardentry2).
+ /// NOTE: If the corresponding route isn't found, the method will return error.
+ pub fn route_ipv6(
+ &self,
+ destination: &Ipv6Inet,
+ next_hop: &Ipv6Addr,
+ ) -> Result {
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv6_mut() =
+ convert_ipv6addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv6_mut() = convert_ipv6addr_to_sockaddr(next_hop) };
+
+ let result = unsafe { GetIpForwardEntry2(&mut row) };
+
+ if NO_ERROR == result {
+ Ok(row)
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_route_ipv4 method adds a route to the interface. Corresponds to CreateIpForwardEntry2 function, with added splitDefault feature.
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createipforwardentry2)
+ pub fn add_route_ipv4(
+ &self,
+ destination: &Ipv4Inet,
+ next_hop: &Ipv4Addr,
+ metric: u32,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv4_mut() =
+ convert_ipv4addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv4_mut() = convert_ipv4addr_to_sockaddr(next_hop) };
+
+ row.Metric = metric;
+
+ let result = unsafe { CreateIpForwardEntry2(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_route_ipv6 method adds a route to the interface. Corresponds to CreateIpForwardEntry2 function, with added splitDefault feature.
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-createipforwardentry2)
+ pub fn add_route_ipv6(
+ &self,
+ destination: &Ipv6Inet,
+ next_hop: &Ipv6Addr,
+ metric: u32,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+ row.ValidLifetime = 0xffffffff;
+ row.PreferredLifetime = 0xffffffff;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv6_mut() =
+ convert_ipv6addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv6_mut() = convert_ipv6addr_to_sockaddr(next_hop) };
+
+ row.Metric = metric;
+
+ let result = unsafe { CreateIpForwardEntry2(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// add_routes_ipv4 method adds multiple routes to the interface
+ pub fn add_routes_ipv4(
+ &self,
+ routes_data: impl IntoIterator- ,
+ ) -> Result<(), NETIO_STATUS> {
+ for rd in routes_data.into_iter().enumerate() {
+ self.add_route_ipv4(&rd.1.destination, &rd.1.next_hop, rd.1.metric)?;
+ }
+ Ok(())
+ }
+
+ /// add_routes_ipv6 method adds multiple routes to the interface
+ pub fn add_routes_ipv6(
+ &self,
+ routes_data: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ for rd in routes_data.into_iter().enumerate() {
+ self.add_route_ipv6(&rd.1.destination, &rd.1.next_hop, rd.1.metric)?;
+ }
+ Ok(())
+ }
+
+ /// set_routes_ipv4 method sets (flush than add) multiple routes to the interface.
+ pub fn set_routes_ipv4(
+ &self,
+ routes_data: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ self.flush_routes_ipv4()?;
+ self.add_routes_ipv4(routes_data)?;
+ Ok(())
+ }
+
+ /// set_routes_ipv6 method sets (flush than add) multiple routes to the interface.
+ pub fn set_routes_ipv6(
+ &self,
+ routes_data: impl IntoIterator
- ,
+ ) -> Result<(), NETIO_STATUS> {
+ self.flush_routes_ipv6()?;
+ self.add_routes_ipv6(routes_data)?;
+ Ok(())
+ }
+
+ /// delete_route_ipv4 method deletes a route that matches the criteria. Corresponds to DeleteIpForwardEntry2 function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteipforwardentry2).
+ pub fn delete_route_ipv4(
+ &self,
+ destination: &Ipv4Inet,
+ next_hop: &Ipv4Addr,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv4_mut() =
+ convert_ipv4addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv4_mut() = convert_ipv4addr_to_sockaddr(next_hop) };
+
+ let result = unsafe { GetIpForwardEntry2(&mut row) };
+ if NO_ERROR != result {
+ return Err(result);
+ }
+
+ let result = unsafe { DeleteIpForwardEntry2(&row) };
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// delete_route_ipv6 method deletes a route that matches the criteria. Corresponds to DeleteIpForwardEntry2 function
+ /// (https://docs.microsoft.com/en-us/windows/desktop/api/netioapi/nf-netioapi-deleteipforwardentry2).
+ pub fn delete_route_ipv6(
+ &self,
+ destination: &Ipv6Inet,
+ next_hop: &Ipv6Addr,
+ ) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_IPFORWARD_ROW2::default();
+ unsafe { InitializeIpForwardEntry(&mut row) };
+
+ row.InterfaceLuid = self.luid;
+
+ unsafe {
+ *row.DestinationPrefix.Prefix.Ipv6_mut() =
+ convert_ipv6addr_to_sockaddr(&destination.address())
+ };
+ row.DestinationPrefix.PrefixLength = destination.network_length();
+
+ unsafe { *row.NextHop.Ipv6_mut() = convert_ipv6addr_to_sockaddr(next_hop) };
+
+ let result = unsafe { GetIpForwardEntry2(&mut row) };
+ if NO_ERROR != result {
+ return Err(result);
+ }
+
+ let result = unsafe { DeleteIpForwardEntry2(&row) };
+
+ if NO_ERROR == result {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// flush_routes method deletes all interface's routes.
+ /// It continues on failures, and returns the last error afterwards.
+ pub fn flush_routes(&self, address_family: ADDRESS_FAMILY) -> Result<(), NETIO_STATUS> {
+ let mut last_error: NETIO_STATUS = NO_ERROR;
+
+ let mut p_table: PMIB_IPFORWARD_TABLE2 = ptr::null_mut();
+ let result = unsafe { GetIpForwardTable2(address_family, &mut p_table) };
+ if NO_ERROR != result {
+ return Err(result);
+ }
+
+ assert!(!p_table.is_null());
+ let num_entries = unsafe { *p_table }.NumEntries;
+ let x_table = unsafe { *p_table }.Table.as_ptr();
+ for i in 0..num_entries {
+ let current_entry = unsafe { x_table.add(i as _) };
+ if unsafe { (*current_entry).InterfaceLuid.Value } == self.luid.Value {
+ let result = unsafe { DeleteIpForwardEntry2(current_entry) };
+ if NO_ERROR != result {
+ last_error = result;
+ }
+ }
+ }
+
+ unsafe { FreeMibTable(p_table as _) };
+
+ if NO_ERROR == last_error {
+ Ok(())
+ } else {
+ Err(result)
+ }
+ }
+
+ /// flush_routes_ipv4 method deletes all interface's routes.
+ /// It continues on failures, and returns the last error afterwards.
+ pub fn flush_routes_ipv4(&self) -> Result<(), NETIO_STATUS> {
+ self.flush_routes(AF_INET as _)
+ }
+
+ /// flush_routes_ipv6 method deletes all interface's routes.
+ /// It continues on failures, and returns the last error afterwards.
+ pub fn flush_routes_ipv6(&self) -> Result<(), NETIO_STATUS> {
+ self.flush_routes(AF_INET6 as _)
+ }
+
+ /// flush_dns method clears all DNS servers associated with the adapter.
+ fn flush_dns(&self, family: ADDRESS_FAMILY) -> Result<(), String> {
+ let ip_itf = match self.get_ip_interface(family) {
+ Ok(ip_itf) => ip_itf,
+ Err(_) => {
+ return Err(String::from("Failed to obtain interface"));
+ }
+ };
+
+ netsh::flush_dns(family, ip_itf.InterfaceIndex)
+ }
+
+ /// flush_dns_ipv4 method clears all DNS servers associated with the adapter.
+ pub fn flush_dns_ipv4(&self) -> Result<(), String> {
+ self.flush_dns(AF_INET as _)
+ }
+
+ /// flush_dns_ipv6 method clears all DNS servers associated with the adapter.
+ pub fn flush_dns_ipv6(&self) -> Result<(), String> {
+ self.flush_dns(AF_INET6 as _)
+ }
+
+ /// Sets MTU on the interface
+ /// TODO: Set IP and other things in here too, so the code is more organized
+ pub fn set_iface_config(&self, mtu: u32) -> Result<(), NETIO_STATUS> {
+ // SAFETY: Both NET_LUID_LH unions should be the same. We're just copying out
+ // the u64 value and re-wrapping it, since wintun doesn't refer to the windows
+ // crate's version of NET_LUID_LH.
+ self.try_set_mtu(AF_INET as ADDRESS_FAMILY, mtu)?;
+ self.try_set_mtu(AF_INET6 as ADDRESS_FAMILY, mtu)?;
+ Ok(())
+ }
+
+ fn try_set_mtu(&self, family: ADDRESS_FAMILY, mut mtu: u32) -> Result<(), NETIO_STATUS> {
+ let mut row = MIB_IPINTERFACE_ROW {
+ Family: family,
+ InterfaceLuid: self.luid,
+ ..Default::default()
+ };
+
+ // SAFETY: TODO
+ let error = unsafe { GetIpInterfaceEntry(&mut row) };
+ if error != NO_ERROR {
+ if family == (AF_INET6 as ADDRESS_FAMILY) && error == ERROR_NOT_FOUND {
+ tracing::debug!(?family, "Couldn't set MTU, maybe IPv6 is disabled.");
+ } else {
+ tracing::warn!(?family, "Couldn't set MTU: {}", error);
+ }
+ return Err(error);
+ }
+
+ if family == (AF_INET6 as ADDRESS_FAMILY) {
+ // ipv6 mtu must be at least 1280
+ mtu = 1280.max(mtu);
+ }
+
+ // https://stackoverflow.com/questions/54857292/setipinterfaceentry-returns-error-invalid-parameter
+ row.SitePrefixLength = 0;
+
+ row.NlMtu = mtu;
+
+ // SAFETY: TODO
+ let ret = unsafe { SetIpInterfaceEntry(&mut row) };
+ if NO_ERROR == ret {
+ Ok(())
+ } else {
+ Err(ret)
+ }
+ }
+}
diff --git a/easytier/src/common/ifcfg/win/mod.rs b/easytier/src/common/ifcfg/win/mod.rs
new file mode 100644
index 000000000..5c850f465
--- /dev/null
+++ b/easytier/src/common/ifcfg/win/mod.rs
@@ -0,0 +1,3 @@
+pub mod luid;
+pub mod netsh;
+pub mod types;
diff --git a/easytier/src/common/ifcfg/win/netsh.rs b/easytier/src/common/ifcfg/win/netsh.rs
new file mode 100644
index 000000000..267d404f6
--- /dev/null
+++ b/easytier/src/common/ifcfg/win/netsh.rs
@@ -0,0 +1,118 @@
+//
+// Port supporting code for wireguard-nt from wireguard-windows v0.5.3 to Rust
+// This file replicates the functionality of wireguard-windows/tunnel/winipcfg/netsh.go
+//
+
+use std::{
+ net::{Ipv4Addr, Ipv6Addr},
+ process::{Command, Stdio},
+};
+use winapi::shared::ws2def::{ADDRESS_FAMILY, AF_INET, AF_INET6};
+
+pub fn flush_dns(family: ADDRESS_FAMILY, if_index: u32) -> Result<(), String> {
+ let proto_name = match family as i32 {
+ AF_INET => "ipv4",
+ AF_INET6 => "ipv6",
+ _ => {
+ return Err(String::from("Invalid address family"));
+ }
+ };
+
+ //let netsh_params = format!("interface {proto} set dnsservers name={itf} source=static address=none validate=no register=both", proto=proto_name, itf=ip_itf.InterfaceIndex);
+ let ret_netsh = Command::new("netsh.exe")
+ .arg("interface")
+ .arg(proto_name)
+ .arg("set")
+ .arg("dnsservers")
+ .arg(format!("name={}", if_index))
+ .arg("source=static")
+ .arg("address=none")
+ .arg("validate=no")
+ .arg("register=both")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .output();
+ match ret_netsh {
+ Ok(output) => {
+ // netsh.exe returns error messages only and is silent upon success. BUT then it will return \r\n, so we need to look at the lines.
+ if let Ok(stdout_str) = String::from_utf8(output.stdout) {
+ if stdout_str.is_empty() || stdout_str == "\r\n" {
+ Ok(())
+ } else {
+ // TODO: ignore "There are no Domain Name Servers (DNS) configured on this computer."
+ // Is this string localized?
+ Err(stdout_str)
+ }
+ } else {
+ Err(String::from("Could not parse netsh output"))
+ }
+ }
+ Err(_) => Err(String::from("Failed to execute command")),
+ }
+}
+
+// Please execute flush_dns() first, as written in the original source code.
+fn add_dns(family: ADDRESS_FAMILY, if_index: u32, dnses: &[String]) -> Result<(), String> {
+ let proto_name = match family as i32 {
+ AF_INET => "ipv4",
+ AF_INET6 => "ipv6",
+ _ => {
+ return Err(String::from("Invalid address family"));
+ }
+ };
+
+ // "interface ipv4 add dnsservers name=%d address=%s validate=no"
+ let ret_netsh = Command::new("netsh.exe")
+ .arg("interface")
+ .arg(proto_name)
+ .arg("add")
+ .arg("dnsservers")
+ .arg(format!("name={}", if_index))
+ .arg(format!(
+ "address={}",
+ dnses
+ .iter()
+ .map(|x| x.to_string())
+ .collect::
>()
+ .join(",")
+ ))
+ .arg("validate=no")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .output();
+ match ret_netsh {
+ Ok(output) => {
+ // netsh.exe returns error messages only and is silent upon success. BUT then it will return \r\n, so we need to look at the lines.
+ if let Ok(stdout_str) = String::from_utf8(output.stdout) {
+ if stdout_str.is_empty() || stdout_str == "\r\n" {
+ Ok(())
+ } else {
+ // TODO: ignore "There are no Domain Name Servers (DNS) configured on this computer."
+ // Is this string localized?
+ Err(stdout_str)
+ }
+ } else {
+ Err(String::from("Could not parse netsh output"))
+ }
+ }
+ Err(_) => Err(String::from("Failed to execute command")),
+ }
+}
+
+pub fn add_dns_ipv4(if_index: u32, dnses: &[Ipv4Addr]) -> Result<(), String> {
+ flush_dns(AF_INET as _, if_index)?;
+ if dnses.is_empty() {
+ return Ok(());
+ }
+ let dnses_str: Vec = dnses.iter().map(|addr| addr.to_string()).collect();
+ add_dns(AF_INET as _, if_index, &dnses_str)
+}
+
+pub fn add_dns_ipv6(if_index: u32, dnses: &[Ipv6Addr]) -> Result<(), String> {
+ flush_dns(AF_INET6 as _, if_index)?;
+ if dnses.is_empty() {
+ return Ok(());
+ }
+ let dnses_str: Vec = dnses.iter().map(|addr| addr.to_string()).collect();
+ add_dns(AF_INET6 as _, if_index, &dnses_str)
+}
diff --git a/easytier/src/common/ifcfg/win/types.rs b/easytier/src/common/ifcfg/win/types.rs
new file mode 100644
index 000000000..ee030c408
--- /dev/null
+++ b/easytier/src/common/ifcfg/win/types.rs
@@ -0,0 +1,103 @@
+//
+// Port supporting code for wireguard-nt from wireguard-windows v0.5.3 to Rust
+// This file replicates parts of wireguard-windows/tunnel/winipcfg/types.go
+//
+
+use cidr::{Ipv4Inet, Ipv6Inet};
+use std::ffi::OsString;
+use std::net::{Ipv4Addr, Ipv6Addr};
+use std::os::windows::prelude::*;
+use winapi::shared::ws2def::*;
+use winapi::shared::ws2ipdef::*;
+
+#[derive(Hash, Eq, PartialEq, Debug)]
+pub struct RouteDataIpv4 {
+ pub destination: Ipv4Inet,
+ pub next_hop: Ipv4Addr,
+ pub metric: u32,
+}
+
+#[derive(Hash, Eq, PartialEq, Debug)]
+pub struct RouteDataIpv6 {
+ pub destination: Ipv6Inet,
+ pub next_hop: Ipv6Addr,
+ pub metric: u32,
+}
+
+/// This function converts std::net::Ipv4Addr to winapi::shared::inaddr::in_addr
+#[inline]
+pub fn convert_ipv4addr_to_inaddr(ip: &Ipv4Addr) -> winapi::shared::inaddr::in_addr {
+ let mut winaddr = winapi::shared::inaddr::in_addr::default();
+
+ let s_un_b = unsafe { winaddr.S_un.S_un_b_mut() };
+ s_un_b.s_b1 = ip.octets()[0];
+ s_un_b.s_b2 = ip.octets()[1];
+ s_un_b.s_b3 = ip.octets()[2];
+ s_un_b.s_b4 = ip.octets()[3];
+
+ winaddr
+}
+
+/// This function converts std::net::Ipv6Addr to winapi::shared::in6addr::in6_addr
+#[inline]
+pub fn convert_ipv6addr_to_inaddr(ip: &Ipv6Addr) -> winapi::shared::in6addr::in6_addr {
+ let mut winaddr = winapi::shared::in6addr::in6_addr::default();
+ let octets = ip.octets();
+ for i in 0..octets.len() {
+ unsafe { winaddr.u.Byte_mut()[i] = octets[i] };
+ }
+
+ winaddr
+}
+
+/// This function converts std::net::Ipv4Addr to winapi::shared::ws2def::SOCKADDR_IN
+pub fn convert_ipv4addr_to_sockaddr(ip: &Ipv4Addr) -> SOCKADDR_IN {
+ SOCKADDR_IN {
+ sin_family: AF_INET as ADDRESS_FAMILY,
+ sin_addr: convert_ipv4addr_to_inaddr(ip),
+ ..Default::default()
+ }
+}
+
+/// This function converts ipnet::Ipv6Addr to winapi::shared::ws2ipdef::SOCKADDR_IN6
+pub fn convert_ipv6addr_to_sockaddr(ip: &Ipv6Addr) -> SOCKADDR_IN6 {
+ SOCKADDR_IN6 {
+ sin6_family: AF_INET6 as ADDRESS_FAMILY,
+ sin6_addr: convert_ipv6addr_to_inaddr(ip),
+ ..Default::default()
+ }
+}
+
+/// This function converts winapi::shared::ws2def::SOCKADDR_IN to std::net::Ipv4Addr
+pub fn convert_sockaddr_to_ipv4addr(sockaddr: &SOCKADDR_IN) -> Ipv4Addr {
+ unsafe {
+ Ipv4Addr::new(
+ sockaddr.sin_addr.S_un.S_un_b().s_b1,
+ sockaddr.sin_addr.S_un.S_un_b().s_b2,
+ sockaddr.sin_addr.S_un.S_un_b().s_b3,
+ sockaddr.sin_addr.S_un.S_un_b().s_b4,
+ )
+ }
+}
+
+/// This function converts a null-terminated Windows Unicode PWCHAR/LPWSTR to an OsString
+pub fn u16_ptr_to_osstring(ptr: *const u16) -> OsString {
+ assert!(!ptr.is_null());
+ let len = (0..)
+ .take_while(|&i| unsafe { *ptr.offset(i) } != 0)
+ .count();
+ let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
+
+ OsString::from_wide(slice)
+}
+
+/// This function converts a null-terminated Windows PWCHAR/LPWSTR to a String
+pub fn u16_ptr_to_string(ptr: *const u16) -> String {
+ assert!(!ptr.is_null());
+ let len = (0..)
+ .take_while(|&i| unsafe { *ptr.offset(i) } != 0)
+ .count();
+ let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
+
+ String::from_utf16_lossy(slice)
+}
diff --git a/easytier/src/common/ifcfg/windows.rs b/easytier/src/common/ifcfg/windows.rs
index 5699104f5..3d5d27d4c 100644
--- a/easytier/src/common/ifcfg/windows.rs
+++ b/easytier/src/common/ifcfg/windows.rs
@@ -1,61 +1,185 @@
-use std::{io, net::Ipv4Addr};
+use crate::common::ifcfg::win::types::{RouteDataIpv4, RouteDataIpv6};
+use super::win::luid::InterfaceLuid;
use async_trait::async_trait;
+use cidr::{Ipv4Inet, Ipv6Inet};
+use std::{
+ io,
+ net::{Ipv4Addr, Ipv6Addr},
+ ptr::null_mut,
+};
+use windows_sys::Win32::{
+ Foundation::NO_ERROR,
+ NetworkManagement::IpHelper::{GetIfEntry, SetIfEntry, MIB_IFROW},
+ System::Diagnostics::Debug::{
+ FormatMessageW, FORMAT_MESSAGE_FROM_SYSTEM, FORMAT_MESSAGE_IGNORE_INSERTS,
+ },
+};
use winreg::{
enums::{HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE},
RegKey,
};
-use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait};
-
+use super::{Error, IfConfiguerTrait};
pub struct WindowsIfConfiger {}
+fn format_win_error(error: u32) -> String {
+ // use FormatMessageW to get the error message
+ let mut buffer = vec![0; 1024];
+ let size = buffer.len() as u32;
+ let flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
+
+ unsafe {
+ FormatMessageW(
+ flags,
+ null_mut(),
+ error,
+ 0,
+ buffer.as_mut_ptr() as *mut u16,
+ size,
+ null_mut(),
+ );
+ }
+ let str_end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
+ format!(
+ "{} (code: {})",
+ String::from_utf16_lossy(&buffer[..str_end])
+ .trim()
+ .to_string(),
+ error
+ )
+}
+
impl WindowsIfConfiger {
pub fn get_interface_index(name: &str) -> Option {
crate::arch::windows::find_interface_index(name).ok()
}
- async fn list_ipv4(name: &str) -> Result, Error> {
- use anyhow::Context;
- use network_interface::NetworkInterfaceConfig;
- use std::net::IpAddr;
- let ret = network_interface::NetworkInterface::show().with_context(|| "show interface")?;
- let addrs = ret
- .iter()
- .filter_map(|x| {
- if x.name != name {
- return None;
- }
- Some(x.addr.clone())
- })
- .flat_map(|x| x)
- .map(|x| x.ip())
- .filter_map(|x| {
- if let IpAddr::V4(ipv4) = x {
- Some(ipv4)
+ #[tracing::instrument(err, ret)]
+ async fn add_ip_address(name: &str, addr: Ipv4Inet) -> Result<(), Error> {
+ let if_index = Self::get_interface_index(name).ok_or(Error::NotFound)?;
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ luid.add_ipv4_address(&addr)
+ .map_err(|e| anyhow::anyhow!("Failed to add IPv4 address: {}", format_win_error(e)))?;
+ Ok(())
+ }
+
+ #[tracing::instrument(err, ret)]
+ async fn remove_ip_address(name: &str, addr: Option) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ if let Some(addr) = addr {
+ luid.delete_ipv4_address(&addr).map_err(|e| {
+ anyhow::anyhow!("Failed to delete IPv4 address: {}", format_win_error(e))
+ })?;
+ } else {
+ luid.flush_ipv4_addresses().map_err(|e| {
+ anyhow::anyhow!("Failed to flush IPv4 addresses: {}", format_win_error(e))
+ })?;
+ }
+ Ok(())
+ }
+
+ #[tracing::instrument(err, ret)]
+ async fn set_interface_status(name: &str, up: bool) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+
+ unsafe {
+ let mut if_row = MIB_IFROW {
+ wszName: [0; 256],
+ dwIndex: if_index,
+ dwType: 0,
+ dwMtu: 0,
+ dwSpeed: 0,
+ dwPhysAddrLen: 0,
+ bPhysAddr: [0; 8],
+ dwAdminStatus: if up { 1 } else { 2 }, // 1 = up, 2 = down
+ dwOperStatus: 0,
+ dwLastChange: 0,
+ dwInOctets: 0,
+ dwInUcastPkts: 0,
+ dwInNUcastPkts: 0,
+ dwInDiscards: 0,
+ dwInErrors: 0,
+ dwInUnknownProtos: 0,
+ dwOutOctets: 0,
+ dwOutUcastPkts: 0,
+ dwOutNUcastPkts: 0,
+ dwOutDiscards: 0,
+ dwOutErrors: 0,
+ dwOutQLen: 0,
+ dwDescrLen: 0,
+ bDescr: [0; 256],
+ };
+
+ if GetIfEntry(&mut if_row) == NO_ERROR {
+ if SetIfEntry(&if_row) == NO_ERROR {
+ Ok(())
} else {
- None
+ Err(anyhow::anyhow!("Failed to set interface status").into())
}
- })
- .collect::>();
+ } else {
+ Err(anyhow::anyhow!("Failed to get interface entry").into())
+ }
+ }
+ }
- Ok(addrs)
+ #[tracing::instrument(err, ret)]
+ async fn set_interface_mtu(name: &str, mtu: u32) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ luid.set_iface_config(mtu).map_err(|e| {
+ anyhow::anyhow!("Failed to set interface config: {}", format_win_error(e))
+ })?;
+ Ok(())
}
- async fn remove_one_ipv4(name: &str, ip: Ipv4Addr) -> Result<(), Error> {
- run_shell_cmd(
- format!(
- "netsh interface ipv4 delete address {} address={}",
- name,
- ip.to_string()
- )
- .as_str(),
- )
- .await
+ #[tracing::instrument(err, ret)]
+ async fn add_ipv6_address(name: &str, addr: Ipv6Inet) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ luid.add_ipv6_address(&addr)
+ .map_err(|e| anyhow::anyhow!("Failed to add IPv6 address: {}", format_win_error(e)))?;
+ Ok(())
+ }
+
+ #[tracing::instrument(err, ret)]
+ async fn remove_ipv6_address(name: &str, addr: Option) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ if let Some(addr) = addr {
+ luid.delete_ipv6_address(&addr).map_err(|e| {
+ anyhow::anyhow!("Failed to delete IPv6 address: {}", format_win_error(e))
+ })?;
+ } else {
+ luid.flush_ipv6_addresses().map_err(|e| {
+ anyhow::anyhow!("Failed to flush IPv6 addresses: {}", format_win_error(e))
+ })?;
+ }
+ Ok(())
}
}
-#[cfg(target_os = "windows")]
#[async_trait]
impl IfConfiguerTrait for WindowsIfConfiger {
async fn add_ipv4_route(
@@ -65,20 +189,20 @@ impl IfConfiguerTrait for WindowsIfConfiger {
cidr_prefix: u8,
cost: Option,
) -> Result<(), Error> {
- let Some(idx) = Self::get_interface_index(name) else {
+ let Some(if_index) = Self::get_interface_index(name) else {
return Err(Error::NotFound);
};
- run_shell_cmd(
- format!(
- "route ADD {} MASK {} 10.1.1.1 IF {} METRIC {}",
- address,
- cidr_to_subnet_mask(cidr_prefix),
- idx,
- cost.unwrap_or(9000)
- )
- .as_str(),
- )
- .await
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+
+ luid.add_routes_ipv4([RouteDataIpv4 {
+ destination: Ipv4Inet::new(address, cidr_prefix).unwrap(),
+ next_hop: Ipv4Addr::UNSPECIFIED,
+ metric: cost.unwrap_or(9000) as u32,
+ }])
+ .map_err(|e| anyhow::anyhow!("Failed to add route: {}", format_win_error(e)))?;
+ Ok(())
}
async fn remove_ipv4_route(
@@ -87,19 +211,18 @@ impl IfConfiguerTrait for WindowsIfConfiger {
address: Ipv4Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
- let Some(idx) = Self::get_interface_index(name) else {
+ let Some(if_index) = Self::get_interface_index(name) else {
return Err(Error::NotFound);
};
- run_shell_cmd(
- format!(
- "route DELETE {} MASK {} IF {}",
- address,
- cidr_to_subnet_mask(cidr_prefix),
- idx
- )
- .as_str(),
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+ luid.delete_route_ipv4(
+ &Ipv4Inet::new(address, cidr_prefix).unwrap(),
+ &Ipv4Addr::UNSPECIFIED,
)
- .await
+ .map_err(|e| anyhow::anyhow!("Failed to delete route: {}", format_win_error(e)))?;
+ Ok(())
}
async fn add_ipv4_ip(
@@ -108,39 +231,15 @@ impl IfConfiguerTrait for WindowsIfConfiger {
address: Ipv4Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
- run_shell_cmd(
- format!(
- "netsh interface ipv4 add address {} address={} mask={}",
- name,
- address,
- cidr_to_subnet_mask(cidr_prefix)
- )
- .as_str(),
- )
- .await
+ Self::add_ip_address(name, Ipv4Inet::new(address, cidr_prefix).unwrap()).await
}
async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> {
- run_shell_cmd(
- format!(
- "netsh interface set interface {} {}",
- name,
- if up { "enable" } else { "disable" }
- )
- .as_str(),
- )
- .await
+ Self::set_interface_status(name, up).await
}
- async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
- if ip.is_none() {
- for ip in Self::list_ipv4(name).await?.iter() {
- Self::remove_one_ipv4(name, *ip).await?;
- }
- Ok(())
- } else {
- Self::remove_one_ipv4(name, ip.unwrap()).await
- }
+ async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> {
+ Self::remove_ip_address(name, ip).await
}
async fn wait_interface_show(&self, name: &str) -> Result<(), Error> {
@@ -160,14 +259,66 @@ impl IfConfiguerTrait for WindowsIfConfiger {
}
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
- let _ = run_shell_cmd(
- format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(),
- )
- .await;
- run_shell_cmd(
- format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(),
+ Self::set_interface_mtu(name, mtu).await
+ }
+
+ async fn add_ipv6_ip(
+ &self,
+ name: &str,
+ address: Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ Self::add_ipv6_address(name, Ipv6Inet::new(address, cidr_prefix).unwrap()).await
+ }
+
+ async fn remove_ipv6(&self, name: &str, ip: Option) -> Result<(), Error> {
+ Self::remove_ipv6_address(name, ip).await
+ }
+
+ async fn add_ipv6_route(
+ &self,
+ name: &str,
+ address: Ipv6Addr,
+ cidr_prefix: u8,
+ cost: Option,
+ ) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+
+ luid.add_routes_ipv6([RouteDataIpv6 {
+ destination: Ipv6Inet::new(address, cidr_prefix).unwrap(),
+ next_hop: Ipv6Addr::UNSPECIFIED,
+ metric: cost.unwrap_or(9000) as u32,
+ }])
+ .map_err(|e| anyhow::anyhow!("Failed to add route: {}", format_win_error(e)))?;
+ Ok(())
+ }
+
+ async fn remove_ipv6_route(
+ &self,
+ name: &str,
+ address: Ipv6Addr,
+ cidr_prefix: u8,
+ ) -> Result<(), Error> {
+ let Some(if_index) = Self::get_interface_index(name) else {
+ return Err(Error::NotFound);
+ };
+
+ let luid = InterfaceLuid::luid_from_index(if_index).map_err(|e| {
+ anyhow::anyhow!("Failed to get interface luid: {}", format_win_error(e))
+ })?;
+
+ luid.delete_route_ipv6(
+ &Ipv6Inet::new(address, cidr_prefix).unwrap(),
+ &Ipv6Addr::UNSPECIFIED,
)
- .await
+ .map_err(|e| anyhow::anyhow!("Failed to delete route: {}", format_win_error(e)))?;
+ Ok(())
}
}
diff --git a/easytier/src/common/mod.rs b/easytier/src/common/mod.rs
index 308f60d00..7d2572c73 100644
--- a/easytier/src/common/mod.rs
+++ b/easytier/src/common/mod.rs
@@ -10,6 +10,7 @@ use tracing::Instrument;
use crate::{set_global_var, use_global_var};
+pub mod acl_processor;
pub mod compressor;
pub mod config;
pub mod constants;
@@ -21,6 +22,7 @@ pub mod ifcfg;
pub mod netns;
pub mod network;
pub mod scoped_task;
+pub mod stats_manager;
pub mod stun;
pub mod stun_codec_ext;
pub mod token_bucket;
@@ -138,8 +140,8 @@ pub fn get_machine_id() -> uuid::Uuid {
)))]
let gen_mid = None;
- if gen_mid.is_some() {
- return gen_mid.unwrap();
+ if let Some(mid) = gen_mid {
+ return mid;
}
let gen_mid = uuid::Uuid::new_v4();
diff --git a/easytier/src/common/netns.rs b/easytier/src/common/netns.rs
index 4f6ba3e11..27f5289fd 100644
--- a/easytier/src/common/netns.rs
+++ b/easytier/src/common/netns.rs
@@ -34,13 +34,12 @@ impl NetNSGuard {
return;
}
- let ns_path: String;
let name = name.unwrap();
- if name == ROOT_NETNS_NAME {
- ns_path = "/proc/1/ns/net".to_string();
+ let ns_path: String = if name == ROOT_NETNS_NAME {
+ "/proc/1/ns/net".to_string()
} else {
- ns_path = format!("/var/run/netns/{}", name);
- }
+ format!("/var/run/netns/{}", name)
+ };
let ns = std::fs::File::open(ns_path).unwrap();
tracing::info!(
diff --git a/easytier/src/common/network.rs b/easytier/src/common/network.rs
index 8396a3039..73dd97c66 100644
--- a/easytier/src/common/network.rs
+++ b/easytier/src/common/network.rs
@@ -16,14 +16,14 @@ struct InterfaceFilter {
iface: NetworkInterface,
}
-#[cfg(target_os = "android")]
+#[cfg(any(target_os = "android", target_env = "ohos"))]
impl InterfaceFilter {
async fn filter_iface(&self) -> bool {
true
}
}
-#[cfg(target_os = "linux")]
+#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
impl InterfaceFilter {
async fn is_tun_tap_device(&self) -> bool {
let path = format!("/sys/class/net/{}/tun_flags", self.iface.name);
@@ -211,7 +211,7 @@ impl IPCollector {
cached_ip_list.read().await.public_ipv6
);
- let sleep_sec = if !cached_ip_list.read().await.public_ipv4.is_none() {
+ let sleep_sec = if cached_ip_list.read().await.public_ipv4.is_some() {
CACHED_IP_LIST_TIMEOUT_SEC
} else {
3
@@ -252,14 +252,11 @@ impl IPCollector {
for iface in ifaces {
for ip in iface.ips {
let ip: std::net::IpAddr = ip.ip();
- match ip {
- std::net::IpAddr::V4(v4) => {
- if ip.is_loopback() || ip.is_multicast() {
- continue;
- }
- ret.interface_ipv4s.push(v4.into());
+ if let std::net::IpAddr::V4(v4) = ip {
+ if ip.is_loopback() || ip.is_multicast() {
+ continue;
}
- _ => {}
+ ret.interface_ipv4s.push(v4.into());
}
}
}
@@ -269,14 +266,11 @@ impl IPCollector {
for iface in ifaces {
for ip in iface.ips {
let ip: std::net::IpAddr = ip.ip();
- match ip {
- std::net::IpAddr::V6(v6) => {
- if v6.is_multicast() || v6.is_loopback() || v6.is_unicast_link_local() {
- continue;
- }
- ret.interface_ipv6s.push(v6.into());
+ if let std::net::IpAddr::V6(v6) = ip {
+ if v6.is_multicast() || v6.is_loopback() || v6.is_unicast_link_local() {
+ continue;
}
- _ => {}
+ ret.interface_ipv6s.push(v6.into());
}
}
}
diff --git a/easytier/src/common/stats_manager.rs b/easytier/src/common/stats_manager.rs
new file mode 100644
index 000000000..432901324
--- /dev/null
+++ b/easytier/src/common/stats_manager.rs
@@ -0,0 +1,886 @@
+use dashmap::DashMap;
+use serde::{Deserialize, Serialize};
+use std::cell::UnsafeCell;
+use std::fmt;
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+use tokio::time::interval;
+
+use crate::common::scoped_task::ScopedTask;
+
+/// Predefined metric names for type safety
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum MetricName {
+ /// RPC calls sent to peers
+ PeerRpcClientTx,
+ /// RPC calls received from peers
+ PeerRpcClientRx,
+ /// RPC calls sent to peers
+ PeerRpcServerTx,
+ /// RPC calls received from peers
+ PeerRpcServerRx,
+ /// RPC call duration in milliseconds
+ PeerRpcDuration,
+ /// RPC errors
+ PeerRpcErrors,
+
+ /// Traffic bytes sent
+ TrafficBytesTx,
+ /// Traffic bytes received
+ TrafficBytesRx,
+ /// Traffic bytes forwarded
+ TrafficBytesForwarded,
+ /// Traffic bytes sent to self
+ TrafficBytesSelfTx,
+ /// Traffic bytes received from self
+ TrafficBytesSelfRx,
+ /// Traffic bytes forwarded for foreign network, rx to local
+ TrafficBytesForeignForwardRx,
+ /// Traffic bytes forwarded for foreign network, tx from local
+ TrafficBytesForeignForwardTx,
+ /// Traffic bytes forwarded for foreign network, forward
+ TrafficBytesForeignForwardForwarded,
+
+ /// Traffic packets sent
+ TrafficPacketsTx,
+ /// Traffic packets received
+ TrafficPacketsRx,
+ /// Traffic packets forwarded
+ TrafficPacketsForwarded,
+ /// Traffic packets sent to self
+ TrafficPacketsSelfTx,
+ /// Traffic packets received from self
+ TrafficPacketsSelfRx,
+ /// Traffic packets forwarded for foreign network, rx to local
+ TrafficPacketsForeignForwardRx,
+ /// Traffic packets forwarded for foreign network, tx from local
+ TrafficPacketsForeignForwardTx,
+ /// Traffic packets forwarded for foreign network, forward
+ TrafficPacketsForeignForwardForwarded,
+
+ /// Compression bytes before compression
+ CompressionBytesRxBefore,
+ /// Compression bytes after compression
+ CompressionBytesRxAfter,
+ /// Compression bytes before compression
+ CompressionBytesTxBefore,
+ /// Compression bytes after compression
+ CompressionBytesTxAfter,
+}
+
+impl fmt::Display for MetricName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ MetricName::PeerRpcClientTx => write!(f, "peer_rpc_client_tx"),
+ MetricName::PeerRpcClientRx => write!(f, "peer_rpc_client_rx"),
+ MetricName::PeerRpcServerTx => write!(f, "peer_rpc_server_tx"),
+ MetricName::PeerRpcServerRx => write!(f, "peer_rpc_server_rx"),
+ MetricName::PeerRpcDuration => write!(f, "peer_rpc_duration_ms"),
+ MetricName::PeerRpcErrors => write!(f, "peer_rpc_errors"),
+
+ MetricName::TrafficBytesTx => write!(f, "traffic_bytes_tx"),
+ MetricName::TrafficBytesRx => write!(f, "traffic_bytes_rx"),
+ MetricName::TrafficBytesForwarded => write!(f, "traffic_bytes_forwarded"),
+ MetricName::TrafficBytesSelfTx => write!(f, "traffic_bytes_self_tx"),
+ MetricName::TrafficBytesSelfRx => write!(f, "traffic_bytes_self_rx"),
+ MetricName::TrafficBytesForeignForwardRx => {
+ write!(f, "traffic_bytes_foreign_forward_rx")
+ }
+ MetricName::TrafficBytesForeignForwardTx => {
+ write!(f, "traffic_bytes_foreign_forward_tx")
+ }
+ MetricName::TrafficBytesForeignForwardForwarded => {
+ write!(f, "traffic_bytes_foreign_forward_forwarded")
+ }
+
+ MetricName::TrafficPacketsTx => write!(f, "traffic_packets_tx"),
+ MetricName::TrafficPacketsRx => write!(f, "traffic_packets_rx"),
+ MetricName::TrafficPacketsForwarded => write!(f, "traffic_packets_forwarded"),
+ MetricName::TrafficPacketsSelfTx => write!(f, "traffic_packets_self_tx"),
+ MetricName::TrafficPacketsSelfRx => write!(f, "traffic_packets_self_rx"),
+ MetricName::TrafficPacketsForeignForwardRx => {
+ write!(f, "traffic_packets_foreign_forward_rx")
+ }
+ MetricName::TrafficPacketsForeignForwardTx => {
+ write!(f, "traffic_packets_foreign_forward_tx")
+ }
+ MetricName::TrafficPacketsForeignForwardForwarded => {
+ write!(f, "traffic_packets_foreign_forward_forwarded")
+ }
+
+ MetricName::CompressionBytesRxBefore => write!(f, "compression_bytes_rx_before"),
+ MetricName::CompressionBytesRxAfter => write!(f, "compression_bytes_rx_after"),
+ MetricName::CompressionBytesTxBefore => write!(f, "compression_bytes_tx_before"),
+ MetricName::CompressionBytesTxAfter => write!(f, "compression_bytes_tx_after"),
+ }
+ }
+}
+
+/// Predefined label types for type safety
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum LabelType {
+ /// Network Name
+ NetworkName(String),
+ /// Source peer ID
+ SrcPeerId(u32),
+ /// Destination peer ID
+ DstPeerId(u32),
+ /// Service name
+ ServiceName(String),
+ /// Method name
+ MethodName(String),
+ /// Protocol type
+ Protocol(String),
+ /// Direction (tx/rx)
+ Direction(String),
+ /// Compression algorithm
+ CompressionAlgo(String),
+ /// Error type
+ ErrorType(String),
+ /// Status
+ Status(String),
+}
+
+impl fmt::Display for LabelType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ LabelType::NetworkName(name) => write!(f, "network_name={}", name),
+ LabelType::SrcPeerId(id) => write!(f, "src_peer_id={}", id),
+ LabelType::DstPeerId(id) => write!(f, "dst_peer_id={}", id),
+ LabelType::ServiceName(name) => write!(f, "service_name={}", name),
+ LabelType::MethodName(name) => write!(f, "method_name={}", name),
+ LabelType::Protocol(proto) => write!(f, "protocol={}", proto),
+ LabelType::Direction(dir) => write!(f, "direction={}", dir),
+ LabelType::CompressionAlgo(algo) => write!(f, "compression_algo={}", algo),
+ LabelType::ErrorType(err) => write!(f, "error_type={}", err),
+ LabelType::Status(status) => write!(f, "status={}", status),
+ }
+ }
+}
+
+impl LabelType {
+ pub fn key(&self) -> &'static str {
+ match self {
+ LabelType::NetworkName(_) => "network_name",
+ LabelType::SrcPeerId(_) => "src_peer_id",
+ LabelType::DstPeerId(_) => "dst_peer_id",
+ LabelType::ServiceName(_) => "service_name",
+ LabelType::MethodName(_) => "method_name",
+ LabelType::Protocol(_) => "protocol",
+ LabelType::Direction(_) => "direction",
+ LabelType::CompressionAlgo(_) => "compression_algo",
+ LabelType::ErrorType(_) => "error_type",
+ LabelType::Status(_) => "status",
+ }
+ }
+
+ pub fn value(&self) -> String {
+ match self {
+ LabelType::NetworkName(name) => name.clone(),
+ LabelType::SrcPeerId(id) => id.to_string(),
+ LabelType::DstPeerId(id) => id.to_string(),
+ LabelType::ServiceName(name) => name.clone(),
+ LabelType::MethodName(name) => name.clone(),
+ LabelType::Protocol(proto) => proto.clone(),
+ LabelType::Direction(dir) => dir.clone(),
+ LabelType::CompressionAlgo(algo) => algo.clone(),
+ LabelType::ErrorType(err) => err.clone(),
+ LabelType::Status(status) => status.clone(),
+ }
+ }
+}
+
+/// Label represents a key-value pair for metric identification
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct Label {
+ pub key: String,
+ pub value: String,
+}
+
+impl Label {
+ pub fn new(key: impl Into, value: impl Into) -> Self {
+ Self {
+ key: key.into(),
+ value: value.into(),
+ }
+ }
+
+ pub fn from_label_type(label_type: &LabelType) -> Self {
+ Self {
+ key: label_type.key().to_string(),
+ value: label_type.value(),
+ }
+ }
+}
+
+/// LabelSet represents a collection of labels for a metric
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct LabelSet {
+ labels: Vec,
+}
+
+impl LabelSet {
+ pub fn new() -> Self {
+ Self { labels: Vec::new() }
+ }
+
+ pub fn with_label(mut self, key: impl Into, value: impl Into) -> Self {
+ self.labels.push(Label::new(key, value));
+ self.labels.sort_by(|a, b| a.key.cmp(&b.key)); // Keep labels sorted for consistent hashing
+ self
+ }
+
+ /// Add a typed label to the set
+ pub fn with_label_type(mut self, label_type: LabelType) -> Self {
+ self.labels.push(Label::from_label_type(&label_type));
+ self.labels.sort_by(|a, b| a.key.cmp(&b.key)); // Keep labels sorted for consistent hashing
+ self
+ }
+
+ /// Create a LabelSet from multiple LabelTypes
+ pub fn from_label_types(label_types: &[LabelType]) -> Self {
+ let mut labels = Vec::new();
+ for label_type in label_types {
+ labels.push(Label::from_label_type(label_type));
+ }
+ labels.sort_by(|a, b| a.key.cmp(&b.key)); // Keep labels sorted for consistent hashing
+ Self { labels }
+ }
+
+ pub fn labels(&self) -> &[Label] {
+ &self.labels
+ }
+
+ /// Generate a string key for this label set
+ pub fn to_key(&self) -> String {
+ if self.labels.is_empty() {
+ return String::new();
+ }
+
+ let mut parts = Vec::with_capacity(self.labels.len());
+ for label in &self.labels {
+ parts.push(format!("{}={}", label.key, label.value));
+ }
+ parts.join(",")
+ }
+}
+
+impl Default for LabelSet {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// UnsafeCounter provides a high-performance counter using UnsafeCell
+#[derive(Debug)]
+pub struct UnsafeCounter {
+ value: UnsafeCell,
+}
+
+impl Default for UnsafeCounter {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl UnsafeCounter {
+ pub fn new() -> Self {
+ Self {
+ value: UnsafeCell::new(0),
+ }
+ }
+
+ pub fn new_with_value(initial: u64) -> Self {
+ Self {
+ value: UnsafeCell::new(initial),
+ }
+ }
+
+ /// Increment the counter by the given amount
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is accessing this counter simultaneously.
+ pub unsafe fn add(&self, delta: u64) {
+ let ptr = self.value.get();
+ *ptr = (*ptr).saturating_add(delta);
+ }
+
+ /// Increment the counter by 1
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is accessing this counter simultaneously.
+ pub unsafe fn inc(&self) {
+ self.add(1);
+ }
+
+ /// Get the current value of the counter
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is modifying this counter simultaneously.
+ pub unsafe fn get(&self) -> u64 {
+ let ptr = self.value.get();
+ *ptr
+ }
+
+ /// Reset the counter to zero
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is accessing this counter simultaneously.
+ pub unsafe fn reset(&self) {
+ let ptr = self.value.get();
+ *ptr = 0;
+ }
+
+ /// Set the counter to a specific value
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is accessing this counter simultaneously.
+ pub unsafe fn set(&self, value: u64) {
+ let ptr = self.value.get();
+ *ptr = value;
+ }
+}
+
+// UnsafeCounter is Send + Sync because the safety is guaranteed by the caller
+unsafe impl Send for UnsafeCounter {}
+unsafe impl Sync for UnsafeCounter {}
+
+/// MetricData contains both the counter and last update timestamp
+/// Uses UnsafeCell for lock-free access
+#[derive(Debug)]
+struct MetricData {
+ counter: UnsafeCounter,
+ last_updated: UnsafeCell,
+}
+
+impl MetricData {
+ fn new() -> Self {
+ Self {
+ counter: UnsafeCounter::new(),
+ last_updated: UnsafeCell::new(Instant::now()),
+ }
+ }
+
+ fn new_with_value(initial: u64) -> Self {
+ Self {
+ counter: UnsafeCounter::new_with_value(initial),
+ last_updated: UnsafeCell::new(Instant::now()),
+ }
+ }
+
+ /// Update the last_updated timestamp
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is accessing this timestamp simultaneously.
+ unsafe fn touch(&self) {
+ let ptr = self.last_updated.get();
+ *ptr = Instant::now();
+ }
+
+ /// Get the last updated timestamp
+ /// # Safety
+ /// This method is unsafe because it uses UnsafeCell. The caller must ensure
+ /// that no other thread is modifying this timestamp simultaneously.
+ unsafe fn get_last_updated(&self) -> Instant {
+ let ptr = self.last_updated.get();
+ *ptr
+ }
+}
+
+// MetricData is Send + Sync because the safety is guaranteed by the caller
+unsafe impl Send for MetricData {}
+unsafe impl Sync for MetricData {}
+
+/// MetricKey uniquely identifies a metric with its name and labels
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+struct MetricKey {
+ name: MetricName,
+ labels: LabelSet,
+}
+
+impl MetricKey {
+ fn new(name: MetricName, labels: LabelSet) -> Self {
+ Self { name, labels }
+ }
+}
+
+impl fmt::Display for MetricKey {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let label_str = self.labels.to_key();
+ if label_str.is_empty() {
+ f.write_str(self.name.to_string().as_str())
+ } else {
+ f.write_str(format!("{}[{}]", self.name, label_str).as_str())
+ }
+ }
+}
+
+/// CounterHandle provides a safe interface to a MetricData
+/// It ensures thread-local access patterns for performance
+#[derive(Clone)]
+pub struct CounterHandle {
+ metric_data: Arc,
+ _key: MetricKey, // Keep key for debugging purposes
+}
+
+impl CounterHandle {
+ fn new(metric_data: Arc, key: MetricKey) -> Self {
+ Self {
+ metric_data,
+ _key: key,
+ }
+ }
+
+ /// Increment the counter by the given amount
+ pub fn add(&self, delta: u64) {
+ unsafe {
+ self.metric_data.counter.add(delta);
+ self.metric_data.touch();
+ }
+ }
+
+ /// Increment the counter by 1
+ pub fn inc(&self) {
+ unsafe {
+ self.metric_data.counter.inc();
+ self.metric_data.touch();
+ }
+ }
+
+ /// Get the current value of the counter
+ pub fn get(&self) -> u64 {
+ unsafe { self.metric_data.counter.get() }
+ }
+
+ /// Reset the counter to zero
+ pub fn reset(&self) {
+ unsafe {
+ self.metric_data.counter.reset();
+ self.metric_data.touch();
+ }
+ }
+
+ /// Set the counter to a specific value
+ pub fn set(&self, value: u64) {
+ unsafe {
+ self.metric_data.counter.set(value);
+ self.metric_data.touch();
+ }
+ }
+}
+
+/// MetricSnapshot represents a point-in-time view of a metric
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MetricSnapshot {
+ pub name: MetricName,
+ pub labels: LabelSet,
+ pub value: u64,
+}
+
+impl MetricSnapshot {
+ pub fn name_str(&self) -> String {
+ self.name.to_string()
+ }
+}
+
+/// StatsManager manages global statistics with high performance counters
+pub struct StatsManager {
+ counters: Arc>>,
+ cleanup_task: ScopedTask<()>,
+}
+
+impl StatsManager {
+ /// Create a new StatsManager
+ pub fn new() -> Self {
+ let counters = Arc::new(DashMap::new());
+
+ // Start cleanup task only if we're in a tokio runtime
+ let counters_clone = Arc::downgrade(&counters.clone());
+ let cleanup_task = tokio::spawn(async move {
+ let mut interval = interval(Duration::from_secs(60)); // Check every minute
+ loop {
+ interval.tick().await;
+
+ let cutoff_time = Instant::now() - Duration::from_secs(180); // 3 minutes
+
+ let Some(counters) = counters_clone.upgrade() else {
+ break;
+ };
+
+ // Remove entries that haven't been updated for 3 minutes
+ counters.retain(|_, metric_data: &mut Arc| unsafe {
+ metric_data.get_last_updated() > cutoff_time
+ });
+ }
+ });
+
+ Self {
+ counters,
+ cleanup_task: cleanup_task.into(),
+ }
+ }
+
+ /// Get or create a counter with the given name and labels
+ pub fn get_counter(&self, name: MetricName, labels: LabelSet) -> CounterHandle {
+ let key = MetricKey::new(name, labels);
+
+ let metric_data = self
+ .counters
+ .entry(key.clone())
+ .or_insert_with(|| Arc::new(MetricData::new()))
+ .clone();
+
+ CounterHandle::new(metric_data, key)
+ }
+
+ /// Get a counter with no labels
+ pub fn get_simple_counter(&self, name: MetricName) -> CounterHandle {
+ self.get_counter(name, LabelSet::new())
+ }
+
+ /// Get all metric snapshots
+ pub fn get_all_metrics(&self) -> Vec {
+ let mut metrics = Vec::new();
+
+ for entry in self.counters.iter() {
+ let key = entry.key();
+ let metric_data = entry.value();
+
+ let value = unsafe { metric_data.counter.get() };
+
+ metrics.push(MetricSnapshot {
+ name: key.name,
+ labels: key.labels.clone(),
+ value,
+ });
+ }
+
+ // Sort by metric name and then by labels for consistent output
+ metrics.sort_by(|a, b| {
+ a.name
+ .to_string()
+ .cmp(&b.name.to_string())
+ .then_with(|| a.labels.to_key().cmp(&b.labels.to_key()))
+ });
+
+ metrics
+ }
+
+ /// Get metrics filtered by name prefix
+ pub fn get_metrics_by_prefix(&self, prefix: &str) -> Vec {
+ self.get_all_metrics()
+ .into_iter()
+ .filter(|m| m.name.to_string().starts_with(prefix))
+ .collect()
+ }
+
+ /// Get a specific metric by name and labels
+ pub fn get_metric(&self, name: MetricName, labels: &LabelSet) -> Option {
+ let key = MetricKey::new(name, labels.clone());
+
+ if let Some(metric_data) = self.counters.get(&key) {
+ let value = unsafe { metric_data.counter.get() };
+ Some(MetricSnapshot {
+ name,
+ labels: labels.clone(),
+ value,
+ })
+ } else {
+ None
+ }
+ }
+
+ /// Clear all metrics
+ pub fn clear(&self) {
+ self.counters.clear();
+ }
+
+ /// Get the number of tracked metrics
+ pub fn metric_count(&self) -> usize {
+ self.counters.len()
+ }
+
+ /// Export metrics in Prometheus format
+ pub fn export_prometheus(&self) -> String {
+ let metrics = self.get_all_metrics();
+ let mut output = String::new();
+
+ let mut current_metric = String::new();
+
+ for metric in metrics {
+ let metric_name_str = metric.name.to_string();
+ if metric_name_str != current_metric {
+ if !current_metric.is_empty() {
+ output.push('\n');
+ }
+ output.push_str(&format!("# TYPE {} counter\n", metric_name_str));
+ current_metric = metric_name_str.clone();
+ }
+
+ if metric.labels.labels().is_empty() {
+ output.push_str(&format!("{} {}\n", metric_name_str, metric.value));
+ } else {
+ let label_str = metric
+ .labels
+ .labels()
+ .iter()
+ .map(|l| format!("{}=\"{}\"", l.key, l.value))
+ .collect::>()
+ .join(",");
+ output.push_str(&format!(
+ "{}{{{}}} {}\n",
+ metric_name_str, label_str, metric.value
+ ));
+ }
+ }
+
+ output
+ }
+}
+
+impl Default for StatsManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::common::stats_manager::{LabelSet, LabelType, MetricName, StatsManager};
+ use crate::proto::cli::{
+ GetPrometheusStatsRequest, GetPrometheusStatsResponse, GetStatsRequest, GetStatsResponse,
+ };
+ use std::collections::BTreeMap;
+
+ #[tokio::test]
+ async fn test_label_set() {
+ let labels = LabelSet::new()
+ .with_label("peer_id", "peer1")
+ .with_label("method", "ping");
+
+ assert_eq!(labels.to_key(), "method=ping,peer_id=peer1");
+ }
+
+ #[tokio::test]
+ async fn test_unsafe_counter() {
+ let counter = UnsafeCounter::new();
+
+ unsafe {
+ assert_eq!(counter.get(), 0);
+ counter.inc();
+ assert_eq!(counter.get(), 1);
+ counter.add(5);
+ assert_eq!(counter.get(), 6);
+ counter.set(10);
+ assert_eq!(counter.get(), 10);
+ counter.reset();
+ assert_eq!(counter.get(), 0);
+ }
+ }
+
+ #[tokio::test]
+ async fn test_stats_manager() {
+ let stats = StatsManager::new();
+
+ // Test simple counter
+ let counter1 = stats.get_simple_counter(MetricName::PeerRpcClientTx);
+ counter1.inc();
+ counter1.add(5);
+
+ // Test counter with labels
+ let labels = LabelSet::new()
+ .with_label("peer_id", "peer1")
+ .with_label("method", "ping");
+ let counter2 = stats.get_counter(MetricName::PeerRpcClientTx, labels.clone());
+ counter2.add(3);
+
+ // Check metrics
+ let metrics = stats.get_all_metrics();
+ assert_eq!(metrics.len(), 2);
+
+ // Find the simple counter
+ let simple_metric = metrics
+ .iter()
+ .find(|m| m.labels.labels().is_empty())
+ .unwrap();
+ assert_eq!(simple_metric.name, MetricName::PeerRpcClientTx);
+ assert_eq!(simple_metric.value, 6);
+
+ // Find the labeled counter
+ let labeled_metric = metrics
+ .iter()
+ .find(|m| !m.labels.labels().is_empty())
+ .unwrap();
+ assert_eq!(labeled_metric.name, MetricName::PeerRpcClientTx);
+ assert_eq!(labeled_metric.value, 3);
+ assert_eq!(labeled_metric.labels, labels);
+ }
+
+ #[tokio::test]
+ async fn test_prometheus_export() {
+ let stats = StatsManager::new();
+
+ let counter1 = stats.get_simple_counter(MetricName::TrafficBytesTx);
+ counter1.set(100);
+
+ let labels = LabelSet::new().with_label("status", "success");
+ let counter2 = stats.get_counter(MetricName::PeerRpcClientTx, labels);
+ counter2.set(50);
+
+ let prometheus_output = stats.export_prometheus();
+
+ assert!(prometheus_output.contains("# TYPE peer_rpc_client_tx counter"));
+ assert!(prometheus_output.contains("peer_rpc_client_tx{status=\"success\"} 50"));
+ assert!(prometheus_output.contains("# TYPE traffic_bytes_tx counter"));
+ assert!(prometheus_output.contains("traffic_bytes_tx 100"));
+ }
+
+ #[tokio::test]
+ async fn test_get_metric() {
+ let stats = StatsManager::new();
+
+ let labels = LabelSet::new().with_label("peer", "test");
+ let counter = stats.get_counter(MetricName::PeerRpcClientTx, labels.clone());
+ counter.set(42);
+
+ let metric = stats
+ .get_metric(MetricName::PeerRpcClientTx, &labels)
+ .unwrap();
+ assert_eq!(metric.value, 42);
+
+ let non_existent = stats.get_metric(MetricName::PeerRpcErrors, &LabelSet::new());
+ assert!(non_existent.is_none());
+ }
+
+ #[tokio::test]
+ async fn test_metrics_by_prefix() {
+ let stats = StatsManager::new();
+
+ stats
+ .get_simple_counter(MetricName::PeerRpcClientTx)
+ .set(10);
+ stats.get_simple_counter(MetricName::PeerRpcErrors).set(2);
+ stats
+ .get_simple_counter(MetricName::TrafficBytesTx)
+ .set(100);
+
+ let rpc_metrics = stats.get_metrics_by_prefix("peer_rpc");
+ assert_eq!(rpc_metrics.len(), 2);
+
+ let traffic_metrics = stats.get_metrics_by_prefix("traffic_");
+ assert_eq!(traffic_metrics.len(), 1);
+ }
+
+ #[tokio::test]
+ async fn test_cleanup_mechanism() {
+ let stats = StatsManager::new();
+
+ // 创建一些计数器
+ let counter1 = stats.get_simple_counter(MetricName::PeerRpcClientTx);
+ counter1.set(10);
+
+ let labels = LabelSet::new().with_label("test", "value");
+ let counter2 = stats.get_counter(MetricName::TrafficBytesTx, labels);
+ counter2.set(20);
+
+ // 验证计数器存在
+ assert_eq!(stats.metric_count(), 2);
+
+ // 注意:实际的清理测试需要等待3分钟,这在单元测试中不现实
+ // 这里我们只验证清理机制的基本结构是否正确
+ // 清理逻辑在后台线程中运行,会自动删除超过3分钟未更新的条目
+
+ // 验证计数器仍然可以正常工作
+ counter1.inc();
+ assert_eq!(counter1.get(), 11);
+
+ counter2.add(5);
+ assert_eq!(counter2.get(), 25);
+ }
+
+ #[tokio::test]
+ async fn test_stats_rpc_data_structures() {
+ // Test GetStatsRequest
+ let request = GetStatsRequest {};
+ assert_eq!(request, GetStatsRequest {});
+
+ // Test GetStatsResponse
+ let response = GetStatsResponse { metrics: vec![] };
+ assert!(response.metrics.is_empty());
+
+ // Test GetPrometheusStatsRequest
+ let prometheus_request = GetPrometheusStatsRequest {};
+ assert_eq!(prometheus_request, GetPrometheusStatsRequest {});
+
+ // Test GetPrometheusStatsResponse
+ let prometheus_response = GetPrometheusStatsResponse {
+ prometheus_text: "# Test metrics\n".to_string(),
+ };
+ assert_eq!(prometheus_response.prometheus_text, "# Test metrics\n");
+ }
+
+ #[tokio::test]
+ async fn test_metric_snapshot_creation() {
+ let stats_manager = StatsManager::new();
+
+ // Create some test metrics
+ let counter1 = stats_manager.get_counter(
+ MetricName::PeerRpcClientTx,
+ LabelSet::new()
+ .with_label_type(LabelType::SrcPeerId(123))
+ .with_label_type(LabelType::ServiceName("test_service".to_string())),
+ );
+ counter1.add(100);
+
+ let counter2 = stats_manager.get_counter(
+ MetricName::TrafficBytesTx,
+ LabelSet::new().with_label_type(LabelType::Protocol("tcp".to_string())),
+ );
+ counter2.add(1024);
+
+ // Get all metrics
+ let metrics = stats_manager.get_all_metrics();
+ assert_eq!(metrics.len(), 2);
+
+ // Verify the metrics can be converted to the format expected by RPC
+ for metric in metrics {
+ let mut labels = BTreeMap::new();
+ for label in metric.labels.labels() {
+ labels.insert(label.key.clone(), label.value.clone());
+ }
+
+ // This simulates what the RPC service would do
+ let _metric_snapshot = crate::proto::cli::MetricSnapshot {
+ name: metric.name.to_string(),
+ value: metric.value,
+ labels,
+ };
+ }
+ }
+
+ #[tokio::test]
+ async fn test_prometheus_export_format() {
+ let stats_manager = StatsManager::new();
+
+ // Create test metrics
+ let counter = stats_manager.get_counter(
+ MetricName::PeerRpcClientTx,
+ LabelSet::new()
+ .with_label_type(LabelType::SrcPeerId(123))
+ .with_label_type(LabelType::ServiceName("test".to_string())),
+ );
+ counter.add(42);
+
+ // Export to Prometheus format
+ let prometheus_text = stats_manager.export_prometheus();
+
+ println!("{}", prometheus_text);
+
+ // Verify the format
+ assert!(prometheus_text.contains("peer_rpc_client_tx"));
+ assert!(prometheus_text.contains("42"));
+ assert!(prometheus_text.contains("src_peer_id=\"123\""));
+ assert!(prometheus_text.contains("service_name=\"test\""));
+ }
+}
diff --git a/easytier/src/common/stun.rs b/easytier/src/common/stun.rs
index f390c540f..e9763b546 100644
--- a/easytier/src/common/stun.rs
+++ b/easytier/src/common/stun.rs
@@ -282,9 +282,7 @@ impl StunClient {
.with_context(|| "encode stun message")?;
tids.push(tid);
tracing::trace!(?message, ?msg, tid, "send stun request");
- self.socket
- .send_to(msg.as_slice().into(), &stun_host)
- .await?;
+ self.socket.send_to(msg.as_slice(), &stun_host).await?;
}
let now = Instant::now();
@@ -372,7 +370,7 @@ impl StunClientBuilder {
pub async fn stop(&mut self) {
self.task_set.abort_all();
- while let Some(_) = self.task_set.join_next().await {}
+ while self.task_set.join_next().await.is_some() {}
}
}
@@ -417,7 +415,7 @@ impl UdpNatTypeDetectResult {
return true;
}
}
- return false;
+ false
}
fn is_pat(&self) -> bool {
@@ -457,16 +455,16 @@ impl UdpNatTypeDetectResult {
if self.is_cone() {
if self.has_ip_changed_resp() {
if self.is_open_internet() {
- return NatType::OpenInternet;
+ NatType::OpenInternet
} else if self.is_pat() {
- return NatType::NoPat;
+ NatType::NoPat
} else {
- return NatType::FullCone;
+ NatType::FullCone
}
} else if self.has_port_changed_resp() {
- return NatType::Restricted;
+ NatType::Restricted
} else {
- return NatType::PortRestricted;
+ NatType::PortRestricted
}
} else if !self.stun_resps.is_empty() {
if self.public_ips().len() != 1
@@ -480,7 +478,7 @@ impl UdpNatTypeDetectResult {
.mapped_socket_addr
.is_none()
{
- return NatType::Symmetric;
+ NatType::Symmetric
} else {
let extra_bind_test = self.extra_bind_test.as_ref().unwrap();
let extra_port = extra_bind_test.mapped_socket_addr.unwrap().port();
@@ -488,15 +486,15 @@ impl UdpNatTypeDetectResult {
let max_port_diff = extra_port.saturating_sub(self.max_port());
let min_port_diff = self.min_port().saturating_sub(extra_port);
if max_port_diff != 0 && max_port_diff < 100 {
- return NatType::SymmetricEasyInc;
+ NatType::SymmetricEasyInc
} else if min_port_diff != 0 && min_port_diff < 100 {
- return NatType::SymmetricEasyDec;
+ NatType::SymmetricEasyDec
} else {
- return NatType::Symmetric;
+ NatType::Symmetric
}
}
} else {
- return NatType::Unknown;
+ NatType::Unknown
}
}
@@ -679,7 +677,7 @@ impl StunInfoCollectorTrait for StunInfoCollector {
.unwrap()
.clone()
.map(|x| x.collect_available_stun_server())
- .unwrap_or(vec![]);
+ .unwrap_or_default();
if stun_servers.is_empty() {
let mut host_resolver =
@@ -740,7 +738,7 @@ impl StunInfoCollector {
pub fn get_default_servers() -> Vec {
// NOTICE: we may need to choose stun stun server based on geo location
// stun server cross nation may return a external ip address with high latency and loss rate
- vec![
+ [
"txt:stun.easytier.cn",
"stun.miwifi.com",
"stun.chat.bilibili.com",
@@ -752,16 +750,16 @@ impl StunInfoCollector {
}
pub fn get_default_servers_v6() -> Vec {
- vec!["txt:stun-v6.easytier.cn"]
+ ["txt:stun-v6.easytier.cn"]
.iter()
.map(|x| x.to_string())
.collect()
}
- async fn get_public_ipv6(servers: &Vec