From 287ddf84429542c32f4e6f001a890a3b8da2df64 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 19 Jul 2025 22:13:35 +0800 Subject: [PATCH 01/10] Merge offical easytier to dev (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix compile issue --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen --- .cargo/config.toml | 10 + .envrc | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 108 +- .github/ISSUE_TEMPLATE/feature_request.yml | 169 +- .github/origin_wfs/Dockerfile | 12 +- .github/origin_wfs/core.yml | 8 +- .github/origin_wfs/docker.yml | 2 +- .github/origin_wfs/gui.yml | 8 +- .github/origin_wfs/mobile.yml | 10 +- .github/origin_wfs/release.yml | 6 +- .github/origin_wfs/test.yml | 8 +- .github/workflows/ohos.yml | 114 + .gitignore | 3 + .gitmodules | 0 CONTRIBUTING.md | 225 + CONTRIBUTING_zh.md | 225 + Cargo.lock | 51 +- Cargo.toml | 3 + EasyTier.code-workspace | 22 +- README.md | 406 +- README_CN.md | 413 +- assets/alipay.png | Bin 0 -> 7631 bytes assets/config-page.png | Bin 0 -> 93752 bytes assets/edgeone.png | Bin 0 -> 47460 bytes assets/langlang.png | Bin 0 -> 45029 bytes assets/running-page.png | Bin 0 -> 141645 bytes assets/wechat.png | Bin 0 -> 7158 bytes easytier-contrib/easytier-ffi/src/lib.rs | 24 + easytier-contrib/easytier-ohrs/Cargo.lock | 5778 +++++++++++++++++ easytier-contrib/easytier-ohrs/Cargo.toml | 46 + easytier-contrib/easytier-ohrs/README.md | 65 + easytier-contrib/easytier-ohrs/build.rs | 3 + easytier-contrib/easytier-ohrs/env.sh | 31 + easytier-contrib/easytier-ohrs/src/lib.rs | 148 + .../easytier-ohrs/src/native_log.rs | 98 + easytier-gui/locales/cn.yml | 120 - easytier-gui/locales/en.yml | 118 - easytier-gui/src-tauri/Cargo.toml | 3 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 +- easytier-gui/src-tauri/gen/android/gradlew | 286 +- .../src-tauri/gen/android/gradlew.bat | 41 +- easytier-gui/src-tauri/src/lib.rs | 18 +- .../frontend-lib/src/components/Config.vue | 1 + easytier-web/frontend-lib/src/locales/cn.yaml | 5 +- easytier-web/frontend-lib/src/locales/en.yaml | 5 +- .../frontend-lib/src/types/network.ts | 2 + easytier/Cargo.toml | 11 +- easytier/locales/app.yml | 12 + easytier/src/common/config.rs | 47 +- easytier/src/common/dns.rs | 2 +- easytier/src/common/global_ctx.rs | 18 +- easytier/src/common/ifcfg/darwin.rs | 69 +- easytier/src/common/ifcfg/mod.rs | 35 +- easytier/src/common/ifcfg/netlink.rs | 134 +- easytier/src/common/ifcfg/win/luid.rs | 745 +++ easytier/src/common/ifcfg/win/mod.rs | 3 + easytier/src/common/ifcfg/win/netsh.rs | 118 + easytier/src/common/ifcfg/win/types.rs | 103 + easytier/src/common/ifcfg/windows.rs | 337 +- easytier/src/common/network.rs | 4 +- easytier/src/common/token_bucket.rs | 78 +- easytier/src/connector/direct.rs | 5 +- easytier/src/connector/dns_connector.rs | 22 +- easytier/src/connector/mod.rs | 8 +- .../src/connector/udp_hole_punch/common.rs | 4 - easytier/src/connector/udp_hole_punch/mod.rs | 8 - easytier/src/easytier-cli.rs | 131 +- easytier/src/easytier-core.rs | 54 +- easytier/src/easytier_core.rs | 60 +- easytier/src/gateway/socks5.rs | 69 +- easytier/src/gateway/tcp_proxy.rs | 11 +- easytier/src/gateway/tokio_smoltcp/mod.rs | 9 +- easytier/src/helper.rs | 19 +- easytier/src/instance/dns_server/tests.rs | 2 +- easytier/src/instance/instance.rs | 121 +- easytier/src/instance/virtual_nic.rs | 111 +- easytier/src/instance_manager.rs | 2 +- easytier/src/launcher.rs | 51 +- easytier/src/lib.rs | 18 +- easytier/src/peers/foreign_network_manager.rs | 41 +- easytier/src/peers/peer_manager.rs | 290 +- easytier/src/peers/peer_map.rs | 12 +- easytier/src/peers/peer_ospf_route.rs | 38 +- easytier/src/peers/peer_rpc_service.rs | 5 + easytier/src/peers/route_trait.rs | 6 +- easytier/src/proto/cli.proto | 27 + easytier/src/proto/common.proto | 7 + easytier/src/proto/common.rs | 35 + easytier/src/proto/peer_rpc.proto | 1 + easytier/src/proto/rpc_impl/bidirect.rs | 2 +- easytier/src/proto/web.proto | 1 + easytier/src/tests/ipv6_test.rs | 66 + easytier/src/tests/mod.rs | 2 + easytier/src/tests/three_node.rs | 140 +- easytier/src/tunnel/common.rs | 7 +- easytier/src/vpn_portal/wireguard.rs | 4 +- flake.lock | 82 + flake.nix | 44 + 99 files changed, 10601 insertions(+), 1233 deletions(-) create mode 100644 .envrc create mode 100644 .github/workflows/ohos.yml delete mode 100644 .gitmodules create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING_zh.md create mode 100644 assets/alipay.png create mode 100644 assets/config-page.png create mode 100644 assets/edgeone.png create mode 100644 assets/langlang.png create mode 100644 assets/running-page.png create mode 100644 assets/wechat.png create mode 100644 easytier-contrib/easytier-ohrs/Cargo.lock create mode 100644 easytier-contrib/easytier-ohrs/Cargo.toml create mode 100644 easytier-contrib/easytier-ohrs/README.md create mode 100644 easytier-contrib/easytier-ohrs/build.rs create mode 100644 easytier-contrib/easytier-ohrs/env.sh create mode 100644 easytier-contrib/easytier-ohrs/src/lib.rs create mode 100644 easytier-contrib/easytier-ohrs/src/native_log.rs delete mode 100644 easytier-gui/locales/cn.yml delete mode 100644 easytier-gui/locales/en.yml create mode 100644 easytier/src/common/ifcfg/win/luid.rs create mode 100644 easytier/src/common/ifcfg/win/mod.rs create mode 100644 easytier/src/common/ifcfg/win/netsh.rs create mode 100644 easytier/src/common/ifcfg/win/types.rs create mode 100644 easytier/src/tests/ipv6_test.rs create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.cargo/config.toml b/.cargo/config.toml index a92f80e8c..99b3ae254 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,6 +5,16 @@ 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"] 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..147029f48 100644 --- a/.github/origin_wfs/Dockerfile +++ b/.github/origin_wfs/Dockerfile @@ -1,11 +1,11 @@ -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/arm64" ]; then \ @@ -16,14 +16,14 @@ RUN mkdir -p /tmp/output; \ 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..b200482d6 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 @@ -157,7 +157,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 }} diff --git a/.github/origin_wfs/docker.yml b/.github/origin_wfs/docker.yml index 124b11d50..8e4434827 100644 --- a/.github/origin_wfs/docker.yml +++ b/.github/origin_wfs/docker.yml @@ -47,7 +47,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 }} diff --git a/.github/origin_wfs/gui.yml b/.github/origin_wfs/gui.yml index 0045a8a03..aab8d4fe8 100644 --- a/.github/origin_wfs/gui.yml +++ b/.github/origin_wfs/gui.yml @@ -136,12 +136,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 @@ -174,7 +174,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/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/release.yml b/.github/origin_wfs/release.yml index 09791d71d..9fef36ac5 100644 --- a/.github/origin_wfs/release.yml +++ b/.github/origin_wfs/release.yml @@ -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..2d4010357 100644 --- a/.github/origin_wfs/test.yml +++ b/.github/origin_wfs/test.yml @@ -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 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..216c83b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,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..86d9471ce --- /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.86) +- 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 \ + libappindicator3-dev \ + 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 \ + 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.86 + rustup default 1.86 + + # 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..588fc0de2 --- /dev/null +++ b/CONTRIBUTING_zh.md @@ -0,0 +1,225 @@ +# EasyTier 贡献指南 + +[English Version](CONTRIBUTING.md) + +感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。 + +## 目录 + +- [开发环境配置](#开发环境配置) + - [前置要求](#前置要求) + - [安装步骤](#安装步骤) +- [项目结构](#项目结构) +- [构建指南](#构建指南) + - [构建核心组件](#构建核心组件) + - [构建桌面应用](#构建桌面应用) + - [构建移动应用](#构建移动应用) +- [开发工作流](#开发工作流) +- [测试指南](#测试指南) +- [Pull Request 规范](#pull-request-规范) +- [其他资源](#其他资源) + +## 开发环境配置 + +### 前置要求 + +#### 必需工具 +- Node.js v21 或更高版本 +- pnpm v9 或更高版本 +- Rust 工具链(版本 1.86) +- LLVM 和 Clang +- Protoc(Protocol Buffers 编译器) + +#### 平台特定依赖 + +**Linux (Ubuntu/Debian)** +```bash +# 核心构建依赖 +sudo apt-get update && sudo apt-get install -y \ + musl-tools \ + libappindicator3-dev \ + 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 \ + 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.86 + rustup default 1.86 + + # 安装项目依赖 + 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..9afe6d904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,6 +1197,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" @@ -1961,6 +1970,7 @@ dependencies = [ "chrono", "cidr", "clap", + "clap_complete", "crossbeam", "dashmap", "dbus", @@ -2052,8 +2062,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", @@ -2081,9 +2093,9 @@ dependencies = [ "dashmap", "dunce", "easytier", + "elevated-command", "gethostname 0.5.0", "once_cell", - "privilege", "serde", "serde_json", "tauri", @@ -2169,6 +2181,20 @@ dependencies = [ "serde", ] +[[package]] +name = "elevated-command" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c410eccdcc5b759704fdb6a792afe6b01ab8a062e2c003ff2567e2697a94aa" +dependencies = [ + "anyhow", + "base64 0.21.7", + "libc", + "log", + "winapi", + "windows 0.52.0", +] + [[package]] name = "embed-resource" version = "2.4.3" @@ -5833,18 +5859,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" @@ -7328,8 +7342,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", @@ -8506,8 +8520,8 @@ dependencies = [ [[package]] name = "thunk-rs" -version = "0.3.3" -source = "git+https://github.com/easytier/thunk.git#5e8371a3100dbc18dda952a2036c6bd6fb0504db" +version = "0.3.4" +source = "git+https://github.com/easytier/thunk.git#403f0d26d3d5bcfdfd76c23e36e517f19fe891e0" [[package]] name = "tiff" @@ -9065,8 +9079,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", 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..9e4f1acc8 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,7 @@ "i18n-ally.sortKeys": true, // Disable the default formatter "prettier.enable": false, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modifications", } } \ No newline at end of file diff --git a/README.md b/README.md index b55fcfc8c..ee3636b4c 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

- - +config page +running page

-## 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.3.2-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.3.2-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.3.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 +``` - ![alt text](/assets/image-2.png) +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 +``` - ![alt text](/assets/image.png) +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): - ![alt text](/assets/image-1.png) +```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 - ![alt text](assets/image-10.png) +# 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 - ``` - - ![alt text](/assets/image-3.png) - -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 +![Routing Information](/assets/image-3.png) -```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,73 @@ 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 - -You can use ``easytier-core --help`` to view all configuration items - -## Roadmap - -- [ ] Support features such TCP hole punching, KCP, FEC etc. -- [ ] Support iOS. -## Community and Contribution - -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). - -## Related Projects and Resources +## Related Projects - [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 -## License +### Contact Us -EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE). +- 💬 **[Telegram Group](https://t.me/easytier)** +- 👥 **[QQ Group: 949700262](https://qm.qq.com/cgi-bin/qm/qr?k=kC8YJ6Jb8vWJIDbZrZJB8pB5YZgPJA5-)** -## Contact +## License -- 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 +EasyTier is released under the [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE). ## Sponsor - - +CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne. + +

+ + EdgeOne Logo + +

+ +Special thanks to [Langlang Cloud](https://langlang.cloud/) for sponsoring our public servers. + +

+ + + +

+ +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..075599194 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,271 +1,243 @@ # EasyTier +[![Github release](https://img.shields.io/github/v/tag/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/releases) [![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) [![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main) [![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues) [![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml) [![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml) +[![GitHub Test Actions](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](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.3.2-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.3.2-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.3.2-70e69a38~ | ``` -1. 在节点 A 上执行: - - ```sh - sudo easytier-core --ipv4 10.144.144.1 - ``` +您可以测试节点之间的连通性: - 命令执行成功会有如下打印。 +```bash +# 测试连通性 +ping 10.126.126.1 +ping 10.126.126.2 +``` - ![alt text](/assets/image-2.png) +注意:如果无法 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 - ![alt text](/assets/image.png) +2. 连接第二个节点(节点 B): - ```sh - easytier-cli route - ``` +```bash +# 使用第一个节点的公网 IP 连接 +sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010 +``` - ![alt text](/assets/image-1.png) +3. 验证连接: - ```sh - easytier-cli node - ``` +```bash +# 测试连通性 +ping 10.144.144.2 - ![alt text](assets/image-10.png) +# 查看已连接的对等节点 +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 - ``` - - ![alt text](/assets/image-3.png) - -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 执行 +![路由信息](/assets/image-3.png) -```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,75 @@ 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://langlang.cloud/) 赞助我们的公共服务器。 -- 提问或报告问题:[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 0000000000000000000000000000000000000000..3661eb5aafdee1d6ffa60a7b8d079c67033ced31 GIT binary patch literal 7631 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⋙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

H`+fH|8PFI3p{NsoA`4Cc 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 Sd@4p76dazeelF{r5}E)E&TT#b literal 0 HcmV?d00001 diff --git a/assets/running-page.png b/assets/running-page.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1cc155545572c8d93c7eca8dd7fff4c198e1ed GIT binary patch literal 141645 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

tP6rVMb?J5il$~>( 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

*Q@K$7cMroQak|&3jc$CA_{%VWHs$ZP=hxe#o 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

K7UpUK$>l@ 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 E2O(~oYXATM 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/lib.rs b/easytier-gui/src-tauri/src/lib.rs index b0a06e801..101be71b1 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -128,18 +128,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 = elevated_command::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(); + elevated_command::Command::new(stdcmd) + .output() + .expect("Failed to run elevated command"); } is_elevated } diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 73ac0fb1f..75a2971b1 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -145,6 +145,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' }, diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 96511d55f..cc7a96df7 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 流量,降低传输延迟,提升传输速度。 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index e27aca893..7b4aabc36 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. diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 6487fc7ee..e1722dbe2 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 @@ -105,6 +106,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, diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 247f5c3b1..c8fb8f5f1 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -27,6 +27,7 @@ readme = "README.md" name = "easytier" path = "src/lib.rs" crate-type = ["cdylib"] +test = false [dependencies] lazy_static = "1.4.0" @@ -88,7 +89,7 @@ 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 @@ -135,6 +136,7 @@ clap = { version = "4.5.30", features = [ "wrap_help", "env", ] } +clap_complete = { version = "4.5.55" } async-recursion = "1.0.5" @@ -249,6 +251,13 @@ 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"] } [build-dependencies] tonic-build = "0.12" diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index fd840074c..5f2c17c6c 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将自动更改。" @@ -92,6 +98,9 @@ core_clap: 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 +187,9 @@ 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 (字节每秒)" core_app: panic_backtrace_save: diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 1c1e8bf12..d146d7005 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -43,6 +43,7 @@ pub fn gen_default_flags() -> Flags { enable_quic_proxy: false, disable_quic_input: false, foreign_relay_bps_limit: u64::MAX, + multi_thread_count: 2, } } @@ -63,10 +64,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; @@ -258,6 +266,7 @@ struct Config { instance_name: Option, instance_id: Option, ipv4: Option, + ipv6: Option, dhcp: Option, network_identity: Option, listeners: Option>, @@ -415,6 +424,23 @@ impl ConfigLoader for TomlConfigLoader { }; } + fn get_ipv6(&self) -> Option { + let locked_config = self.config.lock().unwrap(); + locked_config + .ipv6 + .as_ref() + .map(|s| s.parse().ok()) + .flatten() + } + + fn set_ipv6(&self, addr: Option) { + self.config.lock().unwrap().ipv6 = if let Some(addr) = addr { + Some(addr.to_string()) + } else { + None + }; + } + fn get_dhcp(&self) -> bool { self.config.lock().unwrap().dhcp.unwrap_or_default() } @@ -423,17 +449,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 +485,7 @@ impl ConfigLoader for TomlConfigLoader { allow: None, }); } + Ok(()) } fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr) { diff --git a/easytier/src/common/dns.rs b/easytier/src/common/dns.rs index 5ff4f081f..bf4990b11 100644 --- a/easytier/src/common/dns.rs +++ b/easytier/src/common/dns.rs @@ -127,7 +127,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..9fe50dabf 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -61,6 +61,7 @@ pub struct GlobalCtx { event_bus: EventBus, cached_ipv4: AtomicCell>, + cached_ipv6: AtomicCell>, cached_proxy_cidrs: AtomicCell>>, ip_collector: Mutex>>, @@ -107,7 +108,7 @@ 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; @@ -124,6 +125,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( @@ -191,6 +193,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.clone()); + return 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() } 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..37bc36455 100644 --- a/easytier/src/common/ifcfg/mod.rs +++ b/easytier/src/common/ifcfg/mod.rs @@ -3,13 +3,16 @@ mod darwin; #[cfg(any(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> { diff --git a/easytier/src/common/ifcfg/netlink.rs b/easytier/src/common/ifcfg/netlink.rs index 80ecbd209..4af723411 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, @@ -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, @@ -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..0e0c2e71b --- /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..6e140a5f1 --- /dev/null +++ b/easytier/src/common/ifcfg/win/mod.rs @@ -0,0 +1,3 @@ +pub mod netsh; +pub mod types; +pub mod luid; \ No newline at end of file diff --git a/easytier/src/common/ifcfg/win/netsh.rs b/easytier/src/common/ifcfg/win/netsh.rs new file mode 100644 index 000000000..dcf368c90 --- /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) +} \ No newline at end of file diff --git a/easytier/src/common/ifcfg/win/types.rs b/easytier/src/common/ifcfg/win/types.rs new file mode 100644 index 000000000..c0d90e6ae --- /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) +} \ No newline at end of file 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/network.rs b/easytier/src/common/network.rs index 8396a3039..e4d72f66f 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); diff --git a/easytier/src/common/token_bucket.rs b/easytier/src/common/token_bucket.rs index f5c710ab9..ebbcf898e 100644 --- a/easytier/src/common/token_bucket.rs +++ b/easytier/src/common/token_bucket.rs @@ -67,12 +67,16 @@ impl TokenBucket { }); // Start background refill task - let arc_clone = arc_self.clone(); + let weak_bucket = Arc::downgrade(&arc_self); + let refill_interval = arc_self.config.refill_interval; let refill_task = tokio::spawn(async move { - let mut interval = time::interval(arc_clone.config.refill_interval); + let mut interval = time::interval(refill_interval); loop { interval.tick().await; - arc_clone.refill(); + let Some(bucket) = weak_bucket.upgrade() else { + break; + }; + bucket.refill(); } }); @@ -167,9 +171,9 @@ impl TokenBucketManager { let retain_task = tokio::spawn(async move { loop { // Retain only buckets that are still in use - buckets_clone.retain(|_, bucket| Arc::::strong_count(bucket) <= 1); + buckets_clone.retain(|_, bucket| Arc::::strong_count(bucket) > 1); // Sleep for a while before next retention check - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_secs(5)).await; } }); @@ -190,6 +194,16 @@ impl TokenBucketManager { #[cfg(test)] mod tests { + use crate::{ + connector::udp_hole_punch::tests::create_mock_peer_manager_with_mock_stun, + peers::{ + foreign_network_manager::tests::create_mock_peer_manager_for_foreign_network, + tests::connect_peer_manager, + }, + proto::common::NatType, + tunnel::common::tests::wait_for_condition, + }; + use super::*; use tokio::time::{sleep, Duration}; @@ -309,4 +323,58 @@ mod tests { tokens ); } + + #[tokio::test] + async fn test_token_bucket_free() { + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + for i in 0..10 { + let pma_net1 = create_mock_peer_manager_for_foreign_network(&format!("net{}", i)).await; + + connect_peer_manager(pma_net1.clone(), pm_center1.clone()).await; + wait_for_condition( + || async { pma_net1.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + println!("net{}", i); + println!( + "buckets: {}", + pm_center1 + .get_global_ctx() + .token_bucket_manager() + .buckets + .len() + ); + + drop(pma_net1); + wait_for_condition( + || async { + pm_center1 + .get_foreign_network_manager() + .list_foreign_networks() + .await + .foreign_networks + .len() + == 0 + }, + Duration::from_secs(5), + ) + .await; + } + + // wait token bucket empty + wait_for_condition( + || async { + pm_center1 + .get_global_ctx() + .token_bucket_manager() + .buckets + .len() + == 0 + }, + Duration::from_secs(10), + ) + .await; + } } diff --git a/easytier/src/connector/direct.rs b/easytier/src/connector/direct.rs index 2fa0f2feb..2c8500485 100644 --- a/easytier/src/connector/direct.rs +++ b/easytier/src/connector/direct.rs @@ -211,10 +211,7 @@ impl DirectConnectorManagerData { dst_peer_id, peer_id ); - self.peer_manager - .get_peer_map() - .close_peer_conn(peer_id, &conn_id) - .await?; + self.peer_manager.close_peer_conn(peer_id, &conn_id).await?; return Err(Error::InvalidUrl(addr)); } diff --git a/easytier/src/connector/dns_connector.rs b/easytier/src/connector/dns_connector.rs index 97bc64d9d..bcc0d5392 100644 --- a/easytier/src/connector/dns_connector.rs +++ b/easytier/src/connector/dns_connector.rs @@ -168,13 +168,23 @@ impl DNSTunnelConnector { impl super::TunnelConnector for DNSTunnelConnector { async fn connect(&mut self) -> Result, TunnelError> { let mut conn = if self.addr.scheme() == "txt" { - self.handle_txt_record(self.addr.host_str().as_ref().unwrap()) - .await - .with_context(|| "get txt record url failed")? + self.handle_txt_record( + self.addr + .host_str() + .as_ref() + .ok_or(anyhow::anyhow!("host should not be empty in txt url"))?, + ) + .await + .with_context(|| "get txt record url failed")? } else if self.addr.scheme() == "srv" { - self.handle_srv_record(self.addr.host_str().as_ref().unwrap()) - .await - .with_context(|| "get srv record url failed")? + self.handle_srv_record( + self.addr + .host_str() + .as_ref() + .ok_or(anyhow::anyhow!("host should not be empty in srv url"))?, + ) + .await + .with_context(|| "get srv record url failed")? } else { return Err(anyhow::anyhow!( "unsupported dns scheme: {}, expecting txt or srv", diff --git a/easytier/src/connector/mod.rs b/easytier/src/connector/mod.rs index 746765c08..bd3a10d58 100644 --- a/easytier/src/connector/mod.rs +++ b/easytier/src/connector/mod.rs @@ -29,7 +29,7 @@ async fn set_bind_addr_for_peer_connector( is_ipv4: bool, ip_collector: &Arc, ) { - if cfg!(target_os = "android") { + if cfg!(any(target_os = "android", target_env = "ohos")) { return; } @@ -147,6 +147,12 @@ pub async fn create_connector_by_url( Box::new(connector) } "txt" | "srv" => { + if url.host_str().is_none() { + return Err(Error::InvalidUrl(format!( + "host should not be empty in txt or srv url: {}", + url + ))); + } let connector = dns_connector::DNSTunnelConnector::new(url, global_ctx.clone()); Box::new(connector) } diff --git a/easytier/src/connector/udp_hole_punch/common.rs b/easytier/src/connector/udp_hole_punch/common.rs index 33b400838..e89808d6f 100644 --- a/easytier/src/connector/udp_hole_punch/common.rs +++ b/easytier/src/connector/udp_hole_punch/common.rs @@ -477,10 +477,6 @@ impl PunchHoleServerCommon { self.listeners.lock().await.push(listener); } - pub(crate) async fn clear_udp_socket(&self) { - self.listeners.lock().await.clear(); - } - pub(crate) async fn find_listener(&self, addr: &SocketAddr) -> Option> { let all_listener_sockets = self.listeners.lock().await; diff --git a/easytier/src/connector/udp_hole_punch/mod.rs b/easytier/src/connector/udp_hole_punch/mod.rs index 3172407cc..261820a39 100644 --- a/easytier/src/connector/udp_hole_punch/mod.rs +++ b/easytier/src/connector/udp_hole_punch/mod.rs @@ -74,10 +74,6 @@ impl UdpHolePunchServer { both_easy_sym_server, }) } - - pub async fn clear_common(&self) { - self.common.clear_udp_socket().await; - } } #[async_trait::async_trait] @@ -542,10 +538,6 @@ impl UdpHolePunchConnector { Ok(()) } - pub async fn clear_udp_socket(&mut self) { - self.server.common.clear_udp_socket().await; - } - pub async fn run_as_server(&mut self) -> Result<(), Error> { self.peer_mgr .get_peer_rpc_mgr() diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index 996ea99d8..b896c23a2 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -11,8 +11,10 @@ use std::{ use anyhow::Context; use cidr::Ipv4Inet; -use clap::{command, Args, Parser, Subcommand}; +use clap::{command, Args, CommandFactory, Parser, Subcommand}; +use clap_complete::Shell; use humansize::format_size; +use rust_i18n::t; use service_manager::*; use tabled::settings::Style; use tokio::time::timeout; @@ -31,6 +33,7 @@ use easytier::{ PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory, + ManageMappedListenerRequest, MappedListenerManageRpc, MappedListenerManageRpcClientFactory, ListMappedListenerRequest, MappedListenerManageAction }, common::NatType, peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory}, @@ -72,6 +75,8 @@ enum SubCommand { Peer(PeerArgs), #[command(about = "manage connectors")] Connector(ConnectorArgs), + #[command(about = "manage mapped listeners")] + MappedListener(MappedListenerArgs), #[command(about = "do stun test")] Stun, #[command(about = "show route info")] @@ -86,6 +91,10 @@ enum SubCommand { Service(ServiceArgs), #[command(about = "show tcp/kcp proxy status")] Proxy, + #[command(about = t!("core_clap.generate_completions").to_string())] + GenAutocomplete{ + shell:Shell + }, } #[derive(clap::ValueEnum, Debug, Clone, PartialEq)] @@ -140,6 +149,26 @@ enum ConnectorSubCommand { List, } +#[derive(Args, Debug)] +struct MappedListenerArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum MappedListenerSubCommand { + /// Add Mapped Listerner + Add { + url: String + }, + /// Remove Mapped Listener + Remove { + url: String + }, + /// List Existing Mapped Listener + List, +} + #[derive(Subcommand, Debug)] enum NodeSubCommand { #[command(about = "show node info")] @@ -185,8 +214,11 @@ struct InstallArgs { #[arg(long)] display_name: Option, - #[arg(long, default_value = "false")] - disable_autostart: bool, + #[arg(long)] + disable_autostart: Option, + + #[arg(long)] + disable_restart_on_failure: Option, #[arg(long, help = "path to easytier-core binary")] core_path: Option, @@ -237,6 +269,18 @@ impl CommandHandler<'_> { .with_context(|| "failed to get connector manager client")?) } + async fn get_mapped_listener_manager_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get mapped listener manager client")?) + } + async fn get_peer_center_client( &self, ) -> Result>, Error> { @@ -695,8 +739,59 @@ impl CommandHandler<'_> { println!("response: {:#?}", response); Ok(()) } + + async fn handle_mapped_listener_list(&self) -> Result<(), Error> { + let client = self.get_mapped_listener_manager_client().await?; + let request = ListMappedListenerRequest::default(); + let response = client + .list_mapped_listener(BaseController::default(), request) + .await?; + if self.verbose || *self.output_format == OutputFormat::Json { + println!("{}", serde_json::to_string_pretty(&response.mappedlisteners)?); + return Ok(()); + } + println!("response: {:#?}", response); + Ok(()) + } + + async fn handle_mapped_listener_add(&self, url: &String) -> Result<(), Error> { + let url = Self::mapped_listener_validate_url(url)?; + let client = self.get_mapped_listener_manager_client().await?; + let request = ManageMappedListenerRequest { + action: MappedListenerManageAction::MappedListenerAdd as i32, + url: Some(url.into()) + }; + let _response = client + .manage_mapped_listener(BaseController::default(), request) + .await?; + Ok(()) + } + + async fn handle_mapped_listener_remove(&self, url: &String) -> Result<(), Error> { + let url = Self::mapped_listener_validate_url(url)?; + let client = self.get_mapped_listener_manager_client().await?; + let request = ManageMappedListenerRequest { + action: MappedListenerManageAction::MappedListenerRemove as i32, + url: Some(url.into()) + }; + let _response = client + .manage_mapped_listener(BaseController::default(), request) + .await?; + Ok(()) + } + + fn mapped_listener_validate_url(url: &String) -> Result { + let url = url::Url::parse(url)?; + if url.scheme() != "tcp" && url.scheme() != "udp" { + return Err(anyhow::anyhow!("Url ({url}) must start with tcp:// or udp://")) + } else if url.port().is_none() { + return Err(anyhow::anyhow!("Url ({url}) is missing port num")) + } + Ok(url) + } } +#[derive(Debug)] pub struct ServiceInstallOptions { pub program: PathBuf, pub args: Vec, @@ -704,6 +799,7 @@ pub struct ServiceInstallOptions { pub disable_autostart: bool, pub description: Option, pub display_name: Option, + pub disable_restart_on_failure: bool, } pub struct Service { lable: ServiceLabel, @@ -720,6 +816,8 @@ impl Service { let service_manager = ::native()?; let kind = ServiceManagerKind::native()?; + println!("service manager kind: {:?}", kind); + Ok(Self { lable: name.parse()?, kind, @@ -737,6 +835,7 @@ impl Service { username: None, working_directory: Some(options.work_directory.clone()), environment: None, + disable_restart_on_failure: options.disable_restart_on_failure, }; if self.status()? != ServiceStatus::NotInstalled { return Err(anyhow::anyhow!( @@ -977,7 +1076,10 @@ where #[tokio::main] #[tracing::instrument] async fn main() -> Result<(), Error> { + let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); + rust_i18n::set_locale(&locale); let cli = Cli::parse(); + let client = RpcClient::new(TcpTunnelConnector::new( format!("tcp://{}:{}", cli.rpc_portal.ip(), cli.rpc_portal.port()) .parse() @@ -1024,6 +1126,19 @@ async fn main() -> Result<(), Error> { handler.handle_connector_list().await?; } }, + SubCommand::MappedListener(mapped_listener_args) => match mapped_listener_args.sub_command { + Some(MappedListenerSubCommand::Add { url }) => { + handler.handle_mapped_listener_add(&url).await?; + println!("add mapped listener: {url}"); + } + Some(MappedListenerSubCommand::Remove { url }) => { + handler.handle_mapped_listener_remove(&url).await?; + println!("remove mapped listener: {url}"); + } + Some(MappedListenerSubCommand::List) | None => { + handler.handle_mapped_listener_list().await?; + } + }, SubCommand::Route(route_args) => match route_args.sub_command { Some(RouteSubCommand::List) | None => handler.handle_route_list().await?, Some(RouteSubCommand::Dump) => handler.handle_route_dump().await?, @@ -1231,10 +1346,14 @@ async fn main() -> Result<(), Error> { program: bin_path, args: bin_args, work_directory: work_dir, - disable_autostart: install_args.disable_autostart, + disable_autostart: install_args.disable_autostart.unwrap_or(false), description: Some(install_args.description), display_name: install_args.display_name, + disable_restart_on_failure: install_args + .disable_restart_on_failure + .unwrap_or(false), }; + println!("install_options: {:#?}", install_options); service.install(&install_options)?; } ServiceSubCommand::Uninstall => { @@ -1303,6 +1422,10 @@ async fn main() -> Result<(), Error> { print_output(&table_rows, &cli.output_format)?; } + SubCommand::GenAutocomplete { shell } => { + let mut cmd = Cli::command(); + easytier::print_completions(shell, &mut cmd, "easytier-cli"); + } } Ok(()) diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 2634ec848..8534b03f6 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -1,17 +1,15 @@ #![allow(dead_code)] use std::{ - net::{Ipv4Addr, SocketAddr}, - path::PathBuf, - process::ExitCode, - sync::Arc, + net::{Ipv4Addr, SocketAddr}, path::PathBuf, process::ExitCode, sync::Arc }; use anyhow::Context; use cidr::IpCidr; -use clap::Parser; +use clap::{CommandFactory, Parser}; -use crate::{ +use clap_complete::Shell; +use easytier::{ common::{ config::{ ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, @@ -119,6 +117,9 @@ struct Cli { #[command(flatten)] logging_options: LoggingOptions, + + #[clap(long, help = t!("core_clap.generate_completions").to_string())] + gen_autocomplete: Option, } #[derive(Parser, Debug)] @@ -145,6 +146,13 @@ struct NetworkOptions { )] ipv4: Option, + #[arg( + long, + env = "ET_IPV6", + help = t!("core_clap.ipv6").to_string() + )] + ipv6: Option, + #[arg( short, long, @@ -273,6 +281,13 @@ struct NetworkOptions { )] multi_thread: Option, + #[arg( + long, + env = "ET_MULTI_THREAD_COUNT", + help = t!("core_clap.multi_thread_count").to_string(), + )] + multi_thread_count: Option, + #[arg( long, env = "ET_DISABLE_IPV6", @@ -605,6 +620,12 @@ impl NetworkOptions { })?)) } + if let Some(ipv6) = &self.ipv6 { + cfg.set_ipv6(Some(ipv6.parse().with_context(|| { + format!("failed to parse ipv6 address: {}", ipv6) + })?)) + } + if !self.peers.is_empty() { let mut peers = cfg.get_peers(); peers.reserve(peers.len() + self.peers.len()); @@ -635,6 +656,7 @@ impl NetworkOptions { } if !self.mapped_listeners.is_empty() { + let mut errs = Vec::new(); cfg.set_mapped_listeners(Some( self.mapped_listeners .iter() @@ -645,12 +667,21 @@ impl NetworkOptions { }) .map(|s: url::Url| { if s.port().is_none() { - panic!("mapped listener port is missing: {}", s); + errs.push(anyhow::anyhow!("mapped listener port is missing: {}", s)); } s }) - .collect(), + .collect::>(), )); + if !errs.is_empty() { + return Err(anyhow::anyhow!( + "{}", + errs.iter() + .map(|x| format!("{}", x)) + .collect::>() + .join("\n") + )); + } } for n in self.proxy_networks.iter() { @@ -777,6 +808,7 @@ impl NetworkOptions { if let Some(dev_name) = &self.dev_name { f.dev_name = dev_name.clone() } + println!("mtu: {}, {:?}", f.mtu, self.mtu); if let Some(mtu) = self.mtu { f.mtu = mtu as u32; } @@ -816,6 +848,7 @@ impl NetworkOptions { f.foreign_relay_bps_limit = self .foreign_relay_bps_limit .unwrap_or(f.foreign_relay_bps_limit); + f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count); cfg.set_flags(f); if !self.exit_nodes.is_empty() { @@ -1123,6 +1156,11 @@ pub(crate) async fn main() -> ExitCode { let _monitor = std::thread::spawn(memory_monitor); let cli = Cli::parse(); + if let Some(shell) = cli.gen_autocomplete { + let mut cmd = Cli::command(); + easytier::print_completions(shell, &mut cmd, "easytier-core"); + return ExitCode::SUCCESS; + } let mut ret_code = 0; if let Err(e) = run_main(cli).await { diff --git a/easytier/src/easytier_core.rs b/easytier/src/easytier_core.rs index 1645a3e23..1070502bb 100644 --- a/easytier/src/easytier_core.rs +++ b/easytier/src/easytier_core.rs @@ -9,7 +9,8 @@ use std::{ use anyhow::Context; use cidr::IpCidr; -use clap::Parser; +use clap::{CommandFactory, Parser}; +use crate::instance_manager::NetworkInstanceManager; use crate::{ common::{ @@ -23,13 +24,14 @@ use crate::{ stun::MockStunInfoCollector, }, connector::create_connector_by_url, - instance_manager::NetworkInstanceManager, launcher::{add_proxy_network_to_config, ConfigSource}, + print_completions, proto::common::{CompressionAlgoPb, NatType}, tunnel::{IpVersion, PROTO_PORT_OFFSET}, utils::{init_logger, setup_panic_handler}, web_client, }; +use clap_complete::Shell; #[cfg(target_os = "windows")] windows_service::define_windows_service!(ffi_service_main, win_service_main); @@ -119,6 +121,9 @@ struct Cli { #[command(flatten)] logging_options: LoggingOptions, + + #[clap(long, help = t!("core_clap.generate_completions").to_string())] + gen_autocomplete: Option, } #[derive(Parser, Debug)] @@ -145,6 +150,13 @@ struct NetworkOptions { )] ipv4: Option, + #[arg( + long, + env = "ET_IPV6", + help = t!("core_clap.ipv6").to_string() + )] + ipv6: Option, + #[arg( short, long, @@ -273,6 +285,13 @@ struct NetworkOptions { )] multi_thread: Option, + #[arg( + long, + env = "ET_MULTI_THREAD_COUNT", + help = t!("core_clap.multi_thread_count").to_string(), + )] + multi_thread_count: Option, + #[arg( long, env = "ET_DISABLE_IPV6", @@ -605,6 +624,12 @@ impl NetworkOptions { })?)) } + if let Some(ipv6) = &self.ipv6 { + cfg.set_ipv6(Some(ipv6.parse().with_context(|| { + format!("failed to parse ipv6 address: {}", ipv6) + })?)) + } + if !self.peers.is_empty() { let mut peers = cfg.get_peers(); peers.reserve(peers.len() + self.peers.len()); @@ -635,6 +660,7 @@ impl NetworkOptions { } if !self.mapped_listeners.is_empty() { + let mut errs = Vec::new(); cfg.set_mapped_listeners(Some( self.mapped_listeners .iter() @@ -645,12 +671,21 @@ impl NetworkOptions { }) .map(|s: url::Url| { if s.port().is_none() { - panic!("mapped listener port is missing: {}", s); + errs.push(anyhow::anyhow!("mapped listener port is missing: {}", s)); } s }) - .collect(), + .collect::>(), )); + if !errs.is_empty() { + return Err(anyhow::anyhow!( + "{}", + errs.iter() + .map(|x| format!("{}", x)) + .collect::>() + .join("\n") + )); + } } for n in self.proxy_networks.iter() { @@ -816,6 +851,7 @@ impl NetworkOptions { f.foreign_relay_bps_limit = self .foreign_relay_bps_limit .unwrap_or(f.foreign_relay_bps_limit); + f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count); cfg.set_flags(f); if !self.exit_nodes.is_empty() { @@ -1129,6 +1165,11 @@ pub(crate) async fn main() -> ExitCode { let _monitor = std::thread::spawn(memory_monitor); let cli = Cli::parse(); + if let Some(shell) = cli.gen_autocomplete { + let mut cmd = Cli::command(); + print_completions(shell, &mut cmd, "easytier-core"); + return ExitCode::SUCCESS; + } let mut ret_code = 0; if let Err(e) = run_main(cli).await { @@ -1144,7 +1185,7 @@ pub(crate) async fn main() -> ExitCode { ExitCode::from(ret_code) } -// remember to comment code : init_logger(&cli.logging_options, false)?; client_tx.send(o).await.unwrap(); +// remember to comment code : init_logger(&cli.logging_options, false)?; pub(crate) async fn run(path: &str) -> u8 { let cli = Cli::parse_from(["app", &format!("-c{}", path)]); let mut ret_code = 0; @@ -1155,12 +1196,3 @@ pub(crate) async fn run(path: &str) -> u8 { ret_code } - -pub async fn init_instance(path: &str) { - use crate::helper::g_instance; - use crate::instance::instance::Instance; - let cli = Cli::parse_from(["app", &format!("-c{}", path)]); - let cfg = TomlConfigLoader::new(&cli.config_file.unwrap()[0]).unwrap(); - let mut guard = g_instance.write().await; - *guard = Some(Instance::new(cfg)); -} diff --git a/easytier/src/gateway/socks5.rs b/easytier/src/gateway/socks5.rs index 5c39583f3..7d33350cb 100644 --- a/easytier/src/gateway/socks5.rs +++ b/easytier/src/gateway/socks5.rs @@ -10,7 +10,7 @@ use kcp_sys::{endpoint::KcpEndpoint, stream::KcpStream}; use crate::{ common::{ config::PortForwardConfig, global_ctx::GlobalCtxEvent, join_joinset_background, - scoped_task::ScopedTask, + netns::NetNS, scoped_task::ScopedTask, }, gateway::{ fast_socks5::{ @@ -21,9 +21,12 @@ use crate::{ }, ip_reassembler::IpReassembler, kcp_proxy::NatDstKcpConnector, - tokio_smoltcp::{channel_device, Net, NetConfig}, + tokio_smoltcp::{channel_device, BufferSize, Net, NetConfig}, + }, + tunnel::{ + common::setup_sokcet2, + packet_def::{PacketType, ZCPacket}, }, - tunnel::packet_def::{PacketType, ZCPacket}, }; use anyhow::Context; use dashmap::DashMap; @@ -32,8 +35,7 @@ use pnet::packet::{ }; use tokio::{ io::{AsyncRead, AsyncWrite}, - net::TcpListener, - net::UdpSocket, + net::{TcpListener, TcpSocket, UdpSocket}, select, sync::{mpsc, Mutex}, task::JoinSet, @@ -250,6 +252,38 @@ impl AsyncTcpConnector for Socks5KcpConnector { } } +fn bind_tcp_socket(addr: SocketAddr, net_ns: NetNS) -> Result { + let _g = net_ns.guard(); + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + )?; + + setup_sokcet2(&socket2_socket, &addr)?; + + let socket = TcpSocket::from_std_stream(socket2_socket.into()); + + if let Err(e) = socket.set_nodelay(true) { + tracing::warn!(?e, "set_nodelay fail in listen"); + } + + Ok(socket.listen(1024)?) +} + +fn bind_udp_socket(addr: SocketAddr, net_ns: NetNS) -> Result { + let _g = net_ns.guard(); + let socket2_socket = socket2::Socket::new( + socket2::Domain::for_address(addr), + socket2::Type::DGRAM, + Some(socket2::Protocol::UDP), + )?; + + setup_sokcet2(&socket2_socket, &addr)?; + + Ok(UdpSocket::from_std(socket2_socket.into())?) +} + struct Socks5ServerNet { ipv4_addr: cidr::Ipv4Inet, auth: Option, @@ -301,7 +335,7 @@ impl Socks5ServerNet { let dst = ipv4.get_destination(); let packet = ZCPacket::new_with_payload(&data); - if let Err(e) = peer_manager.send_msg_ipv4(packet, dst).await { + if let Err(e) = peer_manager.send_msg_by_ip(packet, IpAddr::V4(dst)).await { tracing::error!("send to peer failed in smoltcp sender: {:?}", e); } } @@ -318,6 +352,11 @@ impl Socks5ServerNet { .parse() .unwrap(), vec![format!("{}", ipv4_addr.address()).parse().unwrap()], + Some(BufferSize { + tcp_rx_size: 1024 * 128, + tcp_tx_size: 1024 * 128, + ..Default::default() + }), ), ); @@ -550,10 +589,10 @@ impl Socks5Server { proxy_url.port().unwrap() ); - let listener = { - let _g = self.global_ctx.net_ns.guard(); - TcpListener::bind(bind_addr.parse::().unwrap()).await? - }; + let listener = bind_tcp_socket( + bind_addr.parse::().unwrap(), + self.global_ctx.net_ns.clone(), + )?; let net = self.net.clone(); self.tasks.lock().unwrap().spawn(async move { @@ -646,10 +685,7 @@ impl Socks5Server { bind_addr: SocketAddr, dst_addr: SocketAddr, ) -> Result<(), Error> { - let listener = { - let _g = self.global_ctx.net_ns.guard(); - TcpListener::bind(bind_addr).await? - }; + let listener = bind_tcp_socket(bind_addr, self.global_ctx.net_ns.clone())?; let net = self.net.clone(); let entries = self.entries.clone(); @@ -716,10 +752,7 @@ impl Socks5Server { bind_addr: SocketAddr, dst_addr: SocketAddr, ) -> Result<(), Error> { - let socket = { - let _g = self.global_ctx.net_ns.guard(); - Arc::new(UdpSocket::bind(bind_addr).await?) - }; + let socket = Arc::new(bind_udp_socket(bind_addr, self.global_ctx.net_ns.clone())?); let entries = self.entries.clone(); let net_ns = self.global_ctx.net_ns.clone(); diff --git a/easytier/src/gateway/tcp_proxy.rs b/easytier/src/gateway/tcp_proxy.rs index def3b739f..d56d90945 100644 --- a/easytier/src/gateway/tcp_proxy.rs +++ b/easytier/src/gateway/tcp_proxy.rs @@ -520,9 +520,11 @@ impl TcpProxy { #[cfg(feature = "smoltcp")] if self.global_ctx.get_flags().use_smoltcp || self.global_ctx.no_tun() - || cfg!(target_os = "android") + || cfg!(any(target_os = "android", target_env = "ohos")) { // use smoltcp network stack + + use crate::gateway::tokio_smoltcp::BufferSize; self.local_port .store(8899, std::sync::atomic::Ordering::Relaxed); @@ -557,7 +559,7 @@ impl TcpProxy { let dst = ipv4.get_destination(); let packet = ZCPacket::new_with_payload(&data); - if let Err(e) = peer_mgr.send_msg_ipv4(packet, dst).await { + if let Err(e) = peer_mgr.send_msg_by_ip(packet, IpAddr::V4(dst)).await { tracing::error!("send to peer failed in smoltcp sender: {:?}", e); } } @@ -573,6 +575,11 @@ impl TcpProxy { .parse() .unwrap(), vec![format!("{}", self.get_local_ip().unwrap()).parse().unwrap()], + Some(BufferSize { + tcp_rx_size: 1024 * 16, + tcp_tx_size: 1024 * 16, + ..Default::default() + }), ), ); net.set_any_ip(true); diff --git a/easytier/src/gateway/tokio_smoltcp/mod.rs b/easytier/src/gateway/tokio_smoltcp/mod.rs index 9f2f0350c..3187d8c7f 100644 --- a/easytier/src/gateway/tokio_smoltcp/mod.rs +++ b/easytier/src/gateway/tokio_smoltcp/mod.rs @@ -54,12 +54,17 @@ pub struct NetConfig { } impl NetConfig { - pub fn new(interface_config: Config, ip_addr: IpCidr, gateway: Vec) -> Self { + pub fn new( + interface_config: Config, + ip_addr: IpCidr, + gateway: Vec, + buffer_size: Option, + ) -> Self { Self { interface_config, ip_addr, gateway, - buffer_size: Default::default(), + buffer_size: buffer_size.unwrap_or_default(), } } } diff --git a/easytier/src/helper.rs b/easytier/src/helper.rs index 402c2118e..7972ead71 100644 --- a/easytier/src/helper.rs +++ b/easytier/src/helper.rs @@ -1,5 +1,5 @@ use crate::easytier_core; -use crate::instance::instance::Instance; +use crate::peers::peer_manager::PeerManager; use crate::peers::rpc_service::PeerManagerRpcService; use crate::proto::cli::{list_peer_route_pair, NodeInfo, PeerManageRpc, ShowNodeInfoRequest}; use crate::proto::rpc_types::controller::BaseController; @@ -8,14 +8,16 @@ use cidr::Ipv4Inet; use humansize::format_size; use lazy_static::lazy_static; use std::alloc::{alloc_zeroed, Layout}; -use std::option::Option; use std::ptr; use std::str::FromStr; +use std::sync::Arc; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; +use crate::instance_manager::NetworkInstanceManager; lazy_static! { - pub static ref g_instance: RwLock> = RwLock::new(None); + pub static ref g_peermanager: RwLock>> = RwLock::new(None); + pub static ref g_networkinstance: RwLock = RwLock::new(NetworkInstanceManager::new()); } lazy_static! { @@ -70,12 +72,6 @@ pub(crate) fn run(path: &str) { rt.block_on(easytier_core::run(path)); } -pub async fn clear_udp_socket() { - let guard = g_instance.read().await; - let inst = guard.as_ref().unwrap(); - inst.clear_udp_socket().await; -} - pub async fn get_stats() -> *mut u8 { #[derive(tabled::Tabled, serde::Serialize)] struct PeerTableItem { @@ -156,12 +152,11 @@ pub async fn get_stats() -> *mut u8 { } } - let guard = g_instance.read().await; + let guard = g_peermanager.read().await; if guard.is_none() { return get_buffer(); } - let inst = guard.as_ref().unwrap(); - let pm = inst.get_peer_manager(); + let pm = guard.as_ref().unwrap().clone(); let routes = pm.list_routes().await; let pmrs = PeerManagerRpcService::new(pm); let peers = pmrs.list_peers().await; diff --git a/easytier/src/instance/dns_server/tests.rs b/easytier/src/instance/dns_server/tests.rs index 6035cb4e5..19a426077 100644 --- a/easytier/src/instance/dns_server/tests.rs +++ b/easytier/src/instance/dns_server/tests.rs @@ -34,7 +34,7 @@ pub async fn prepare_env(dns_name: &str, tun_ip: Ipv4Inet) -> (Arc, let r = Arc::new(tokio::sync::Mutex::new(r)); let mut virtual_nic = NicCtx::new(peer_mgr.get_global_ctx(), &peer_mgr, r); - virtual_nic.run(tun_ip).await.unwrap(); + virtual_nic.run(Some(tun_ip), None).await.unwrap(); (peer_mgr, virtual_nic) } diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index c6b4189be..ee1d1a9a2 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -30,6 +30,11 @@ use crate::peers::rpc_service::PeerManagerRpcService; use crate::peers::{create_packet_recv_chan, recv_packet_from_chan, PacketRecvChanReceiver}; use crate::proto::cli::VpnPortalRpc; use crate::proto::cli::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo}; +use crate::proto::cli::{ + ListMappedListenerRequest, ListMappedListenerResponse, ManageMappedListenerRequest, + ManageMappedListenerResponse, MappedListener, MappedListenerManageAction, + MappedListenerManageRpc, +}; use crate::proto::common::TunnelInfo; use crate::proto::peer_rpc::PeerCenterRpcServer; use crate::proto::rpc_impl::standalone::{RpcServerHook, StandAloneServer}; @@ -89,8 +94,8 @@ impl IpProxy { self.tcp_proxy.start(true).await?; if let Err(e) = self.icmp_proxy.start().await { tracing::error!("start icmp proxy failed: {:?}", e); - if cfg!(not(target_os = "android")) { - // android may not support icmp proxy + if cfg!(not(any(target_os = "android", target_env = "ohos"))) { + // android and ohos not support icmp proxy return Err(e); } } @@ -267,6 +272,8 @@ impl Instance { peer_packet_sender.clone(), )); + peer_manager.set_allow_loopback_tunnel(false); + let listener_manager = Arc::new(Mutex::new(ListenerManager::new( global_ctx.clone(), peer_manager.clone(), @@ -477,14 +484,14 @@ impl Instance { continue; } - #[cfg(not(target_os = "android"))] + #[cfg(not(any(target_os = "android", target_env = "ohos")))] { let mut new_nic_ctx = NicCtx::new( global_ctx_c.clone(), &peer_manager_c, _peer_packet_receiver.clone(), ); - if let Err(e) = new_nic_ctx.run(ip).await { + if let Err(e) = new_nic_ctx.run(Some(ip), global_ctx_c.get_ipv6()).await { tracing::error!( ?current_dhcp_ip, ?candidate_ipv4_addr, @@ -531,25 +538,30 @@ impl Instance { Self::clear_nic_ctx(self.nic_ctx.clone(), self.peer_packet_receiver.clone()).await; if !self.global_ctx.config.get_flags().no_tun { - #[cfg(not(target_os = "android"))] - if let Some(ipv4_addr) = self.global_ctx.get_ipv4() { - let mut new_nic_ctx = NicCtx::new( - self.global_ctx.clone(), - &self.peer_manager, - self.peer_packet_receiver.clone(), - ); - new_nic_ctx.run(ipv4_addr).await?; - let ifname = new_nic_ctx.ifname().await; - Self::use_new_nic_ctx( - self.nic_ctx.clone(), - new_nic_ctx, - Self::create_magic_dns_runner( - self.peer_manager.clone(), - ifname, - ipv4_addr.clone(), - ), - ) - .await; + #[cfg(not(any(target_os = "android", target_env = "ohos")))] + { + let ipv4_addr = self.global_ctx.get_ipv4(); + let ipv6_addr = self.global_ctx.get_ipv6(); + + // Only run if we have at least one IP address (IPv4 or IPv6) + if ipv4_addr.is_some() || ipv6_addr.is_some() { + let mut new_nic_ctx = NicCtx::new( + self.global_ctx.clone(), + &self.peer_manager, + self.peer_packet_receiver.clone(), + ); + + new_nic_ctx.run(ipv4_addr, ipv6_addr).await?; + let ifname = new_nic_ctx.ifname().await; + + // Create Magic DNS runner only if we have IPv4 + let dns_runner = if let Some(ipv4) = ipv4_addr { + Self::create_magic_dns_runner(self.peer_manager.clone(), ifname, ipv4) + } else { + None + }; + Self::use_new_nic_ctx(self.nic_ctx.clone(), new_nic_ctx, dns_runner).await; + } } } @@ -710,6 +722,56 @@ impl Instance { } } + fn get_mapped_listener_manager_rpc_service( + &self, + ) -> impl MappedListenerManageRpc + Clone { + #[derive(Clone)] + pub struct MappedListenerManagerRpcService(Arc); + + #[async_trait::async_trait] + impl MappedListenerManageRpc for MappedListenerManagerRpcService { + type Controller = BaseController; + + async fn list_mapped_listener( + &self, + _: BaseController, + _request: ListMappedListenerRequest, + ) -> Result { + let mut ret = ListMappedListenerResponse::default(); + let urls = self.0.config.get_mapped_listeners(); + let mapped_listeners: Vec = urls + .into_iter() + .map(|u| MappedListener { + url: Some(u.into()), + }) + .collect(); + ret.mappedlisteners = mapped_listeners; + Ok(ret) + } + + async fn manage_mapped_listener( + &self, + _: BaseController, + req: ManageMappedListenerRequest, + ) -> Result { + let url: url::Url = req.url.ok_or(anyhow::anyhow!("url is empty"))?.into(); + + let urls = self.0.config.get_mapped_listeners(); + let mut set_urls: HashSet = urls.into_iter().collect(); + if req.action == MappedListenerManageAction::MappedListenerRemove as i32 { + set_urls.remove(&url); + } else if req.action == MappedListenerManageAction::MappedListenerAdd as i32 { + set_urls.insert(url); + } + let urls: Vec = set_urls.into_iter().collect(); + self.0.config.set_mapped_listeners(Some(urls)); + Ok(ManageMappedListenerResponse::default()) + } + } + + MappedListenerManagerRpcService(self.global_ctx.clone()) + } + async fn run_rpc_server(&mut self) -> Result<(), Error> { let Some(_) = self.global_ctx.config.get_rpc_portal() else { tracing::info!("rpc server not enabled, because rpc_portal is not set."); @@ -722,6 +784,7 @@ impl Instance { let conn_manager = self.conn_manager.clone(); let peer_center = self.peer_center.clone(); let vpn_portal_rpc = self.get_vpn_portal_rpc_service(); + let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service(); let s = self.rpc_server.as_mut().unwrap(); s.registry().register( @@ -737,6 +800,10 @@ impl Instance { .register(PeerCenterRpcServer::new(peer_center.get_rpc_service()), ""); s.registry() .register(VpnPortalRpcServer::new(vpn_portal_rpc), ""); + s.registry().register( + MappedListenerManageRpcServer::new(mapped_listener_manager_rpc), + "", + ); if let Some(ip_proxy) = self.ip_proxy.as_ref() { s.registry().register( @@ -796,11 +863,7 @@ impl Instance { self.peer_packet_receiver.clone() } - pub async fn clear_udp_socket(&self) { - self.udp_hole_puncher.lock().await.clear_udp_socket().await; - } - - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_env = "ohos"))] pub async fn setup_nic_ctx_for_android( nic_ctx: ArcNicCtx, global_ctx: ArcGlobalCtx, @@ -856,7 +919,7 @@ impl Drop for Instance { }; let now = std::time::Instant::now(); - while now.elapsed().as_secs() < 1 { + while now.elapsed().as_secs() < 10 { tokio::time::sleep(std::time::Duration::from_millis(50)).await; if pm.strong_count() == 0 { tracing::info!( diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index 8ee2dcf77..898380bea 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -1,7 +1,7 @@ use std::{ collections::BTreeSet, io, - net::Ipv4Addr, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, pin::Pin, sync::{Arc, Weak}, task::{Context, Poll}, @@ -23,9 +23,10 @@ use crate::{ use byteorder::WriteBytesExt as _; use bytes::{BufMut, BytesMut}; +use cidr::{Ipv4Inet, Ipv6Inet}; use futures::{lock::BiLock, ready, SinkExt, Stream, StreamExt}; use pin_project_lite::pin_project; -use pnet::packet::ipv4::Ipv4Packet; +use pnet::packet::{ipv4::Ipv4Packet, ipv6::Ipv6Packet}; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, sync::Mutex, @@ -110,7 +111,7 @@ enum PacketProtocol { // Note: the protocol in the packet information header is platform dependent. impl PacketProtocol { - #[cfg(any(target_os = "linux", target_os = "android"))] + #[cfg(any(target_os = "linux", target_os = "android", target_env = "ohos"))] fn into_pi_field(self) -> Result { use nix::libc; match self { @@ -328,7 +329,7 @@ impl VirtualNic { Ok(tun::create(&config)?) } - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_env = "ohos"))] pub async fn create_dev_for_android( &mut self, tun_fd: std::os::fd::RawFd, @@ -434,12 +435,26 @@ impl VirtualNic { Ok(()) } - pub async fn remove_ip(&self, ip: Option) -> Result<(), Error> { + pub async fn add_ipv6_route(&self, address: Ipv6Addr, cidr: u8) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv6_route(self.ifname(), address, cidr, None) + .await?; + Ok(()) + } + + pub async fn remove_ip(&self, ip: Option) -> Result<(), Error> { let _g = self.global_ctx.net_ns.guard(); self.ifcfg.remove_ip(self.ifname(), ip).await?; Ok(()) } + pub async fn remove_ipv6(&self, ip: Option) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg.remove_ipv6(self.ifname(), ip).await?; + Ok(()) + } + pub async fn add_ip(&self, ip: Ipv4Addr, cidr: i32) -> Result<(), Error> { let _g = self.global_ctx.net_ns.guard(); self.ifcfg @@ -448,6 +463,14 @@ impl VirtualNic { Ok(()) } + pub async fn add_ipv6(&self, ip: Ipv6Addr, cidr: i32) -> Result<(), Error> { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv6_ip(self.ifname(), ip, cidr as u8) + .await?; + Ok(()) + } + pub fn get_ifcfg(&self) -> impl IfConfiguerTrait { IfConfiger {} } @@ -496,6 +519,20 @@ impl NicCtx { Ok(()) } + pub async fn assign_ipv6_to_tun_device(&self, ipv6_addr: cidr::Ipv6Inet) -> Result<(), Error> { + let nic = self.nic.lock().await; + nic.link_up().await?; + nic.remove_ipv6(None).await?; + nic.add_ipv6(ipv6_addr.address(), ipv6_addr.network_length() as i32) + .await?; + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + nic.add_ipv6_route(ipv6_addr.first_address(), ipv6_addr.network_length()) + .await?; + } + Ok(()) + } + async fn do_forward_nic_to_peers_ipv4(ret: ZCPacket, mgr: &PeerManager) { if let Some(ipv4) = Ipv4Packet::new(ret.payload()) { if ipv4.get_version() != 4 { @@ -509,16 +546,53 @@ impl NicCtx { ); // TODO: use zero-copy - let send_ret = mgr.send_msg_ipv4(ret, dst_ipv4).await; + let send_ret = mgr.send_msg_by_ip(ret, IpAddr::V4(dst_ipv4)).await; if send_ret.is_err() { - tracing::trace!(?send_ret, "[USER_PACKET] send_msg_ipv4 failed") + tracing::trace!(?send_ret, "[USER_PACKET] send_msg failed") } } else { tracing::warn!(?ret, "[USER_PACKET] not ipv4 packet"); } } - fn do_forward_nic_to_peers( + async fn do_forward_nic_to_peers_ipv6(ret: ZCPacket, mgr: &PeerManager) { + if let Some(ipv6) = Ipv6Packet::new(ret.payload()) { + if ipv6.get_version() != 6 { + tracing::info!("[USER_PACKET] not ipv6 packet: {:?}", ipv6); + return; + } + let dst_ipv6 = ipv6.get_destination(); + tracing::trace!( + ?ret, + "[USER_PACKET] recv new packet from tun device and forward to peers." + ); + + // TODO: use zero-copy + let send_ret = mgr.send_msg_by_ip(ret, IpAddr::V6(dst_ipv6)).await; + if send_ret.is_err() { + tracing::trace!(?send_ret, "[USER_PACKET] send_msg failed") + } + } else { + tracing::warn!(?ret, "[USER_PACKET] not ipv6 packet"); + } + } + + async fn do_forward_nic_to_peers(ret: ZCPacket, mgr: &PeerManager) { + let payload = ret.payload(); + if payload.is_empty() { + return; + } + + match payload[0] >> 4 { + 4 => Self::do_forward_nic_to_peers_ipv4(ret, mgr).await, + 6 => Self::do_forward_nic_to_peers_ipv6(ret, mgr).await, + _ => { + tracing::warn!(?ret, "[USER_PACKET] unknown IP version"); + } + } + } + + fn do_forward_nic_to_peers_task( &mut self, mut stream: Pin>, ) -> Result<(), Error> { @@ -532,7 +606,7 @@ impl NicCtx { tracing::error!("read from nic failed: {:?}", ret); break; } - Self::do_forward_nic_to_peers_ipv4(ret.unwrap(), mgr.as_ref()).await; + Self::do_forward_nic_to_peers(ret.unwrap(), mgr.as_ref()).await; } panic!("nic stream closed"); }); @@ -647,7 +721,7 @@ impl NicCtx { Ok(()) } - pub async fn run(&mut self, ipv4_addr: cidr::Ipv4Inet) -> Result<(), Error> { + pub async fn run(&mut self, ipv4_addr: Option, ipv6_addr: Option) -> Result<(), Error> { let tunnel = { let mut nic = self.nic.lock().await; match nic.create_dev().await { @@ -681,16 +755,25 @@ impl NicCtx { let (stream, sink) = tunnel.split(); - self.do_forward_nic_to_peers(stream)?; + self.do_forward_nic_to_peers_task(stream)?; self.do_forward_peers_to_nic(sink); - self.assign_ipv4_to_tun_device(ipv4_addr).await?; + // Assign IPv4 address if provided + if let Some(ipv4_addr) = ipv4_addr { + self.assign_ipv4_to_tun_device(ipv4_addr).await?; + } + + // Assign IPv6 address if provided + if let Some(ipv6_addr) = ipv6_addr { + self.assign_ipv6_to_tun_device(ipv6_addr).await?; + } + self.run_proxy_cidrs_route_updater().await?; Ok(()) } - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_env = "ohos"))] pub async fn run_for_android(&mut self, tun_fd: std::os::fd::RawFd) -> Result<(), Error> { let tunnel = { let mut nic = self.nic.lock().await; @@ -710,7 +793,7 @@ impl NicCtx { let (stream, sink) = tunnel.split(); - self.do_forward_nic_to_peers(stream)?; + self.do_forward_nic_to_peers_task(stream)?; self.do_forward_peers_to_nic(sink); Ok(()) diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index fd3c7330a..bfa3d4a8a 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -13,7 +13,7 @@ use crate::{ }; pub struct NetworkInstanceManager { - instance_map: Arc>, + pub instance_map: Arc>, instance_stop_tasks: Arc>>, stop_check_notifier: Arc, } diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 3ba21a0ed..15682e278 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -3,7 +3,6 @@ use std::{ sync::{atomic::AtomicBool, Arc, RwLock}, }; -use crate::helper::{g_instance}; use crate::{ common::{ config::{ @@ -21,6 +20,7 @@ use crate::{ use anyhow::Context; use chrono::{DateTime, Local}; use tokio::{sync::broadcast, task::JoinSet}; +use crate::helper::g_peermanager; pub type MyNodeInfo = crate::proto::web::MyNodeInfo; @@ -95,7 +95,7 @@ impl EasyTierLauncher { } } - #[cfg(target_os = "android")] + #[cfg(any(target_os = "android", target_env = "ohos"))] async fn run_routine_for_android( instance: &Instance, data: &EasyTierData, @@ -137,17 +137,15 @@ impl EasyTierLauncher { ) -> Result<(), anyhow::Error> { let mut instance = Instance::new(cfg); let mut tasks = JoinSet::new(); - { - let guard = g_instance.read().await; - let peer_mgr = guard.as_ref().unwrap().get_peer_manager(); + let mut guard = g_peermanager.write().await; + *guard = Some(instance.get_peer_manager()); + } + { // Subscribe to global context events - let global_ctx = guard.as_ref().unwrap().get_global_ctx(); + let global_ctx = instance.get_global_ctx(); let data_c = data.clone(); - let global_ctx_c = instance.get_global_ctx(); - let peer_mgr_c = instance.get_peer_manager().clone(); - let vpn_portal = instance.get_vpn_portal_inst(); tasks.spawn(async move { let mut receiver = global_ctx.subscribe(); loop { @@ -169,9 +167,9 @@ impl EasyTierLauncher { // update my node info if fetch_node_info { let data_c = data.clone(); - let global_ctx_c = guard.as_ref().unwrap().get_global_ctx(); - let peer_mgr_c = peer_mgr.clone(); - let vpn_portal = guard.as_ref().unwrap().get_vpn_portal_inst(); + let global_ctx_c = instance.get_global_ctx(); + let peer_mgr_c = instance.get_peer_manager().clone(); + let vpn_portal = instance.get_vpn_portal_inst(); tasks.spawn(async move { loop { // Update TUN Device Name @@ -212,12 +210,16 @@ impl EasyTierLauncher { Self::run_routine_for_android(&instance, &data, &mut tasks).await; } + #[cfg(any(target_os = "android", target_env = "ohos"))] + Self::run_routine_for_android(&instance, &data, &mut tasks).await; + + instance.run().await?; + stop_signal.notified().await; + { - let mut w_guard = g_instance.write().await; - w_guard.as_mut().unwrap().run().await?; + let mut guard = g_peermanager.write().await; + *guard = None; } - - stop_signal.notified().await; tasks.abort_all(); drop(tasks); @@ -225,6 +227,8 @@ impl EasyTierLauncher { instance.clear_resources().await; drop(instance); + println!("[easytier_routine] Instance resource released!"); + Ok(()) } @@ -270,8 +274,9 @@ impl EasyTierLauncher { self.thread_handle = Some(std::thread::spawn(move || { let rt = if cfg.get_flags().multi_thread { + let worker_threads = 2.max(cfg.get_flags().multi_thread_count as usize); tokio::runtime::Builder::new_multi_thread() - .worker_threads(2) + .worker_threads(worker_threads) .enable_all() .build() } else { @@ -362,7 +367,7 @@ pub enum ConfigSource { pub struct NetworkInstance { config: TomlConfigLoader, - launcher: Option, + pub launcher: Option, config_source: ConfigSource, } @@ -496,7 +501,7 @@ pub fn add_proxy_network_to_config( } else { None }; - cfg.add_proxy_cidr(real_cidr, mapped_cidr); + cfg.add_proxy_cidr(real_cidr, mapped_cidr)?; Ok(()) } @@ -691,6 +696,10 @@ impl NetworkConfig { flags.use_smoltcp = use_smoltcp; } + if let Some(disable_ipv6) = self.disable_ipv6 { + flags.enable_ipv6 = !disable_ipv6; + } + if let Some(enable_kcp_proxy) = self.enable_kcp_proxy { flags.enable_kcp_proxy = enable_kcp_proxy; } @@ -866,6 +875,7 @@ impl NetworkConfig { result.latency_first = Some(flags.latency_first); result.dev_name = Some(flags.dev_name.clone()); result.use_smoltcp = Some(flags.use_smoltcp); + result.disable_ipv6 = Some(!flags.enable_ipv6); result.enable_kcp_proxy = Some(flags.enable_kcp_proxy); result.disable_kcp_input = Some(flags.disable_kcp_input); result.enable_quic_proxy = Some(flags.enable_quic_proxy); @@ -1016,7 +1026,7 @@ mod tests { } else { None }; - config.add_proxy_cidr(network, mapped_network); + config.add_proxy_cidr(network, mapped_network).unwrap(); } } @@ -1109,6 +1119,7 @@ mod tests { flags.latency_first = rng.gen_bool(0.5); flags.dev_name = format!("etun{}", rng.gen_range(0..10)); flags.use_smoltcp = rng.gen_bool(0.3); + flags.enable_ipv6 = rng.gen_bool(0.8); flags.enable_kcp_proxy = rng.gen_bool(0.5); flags.disable_kcp_input = rng.gen_bool(0.3); flags.enable_quic_proxy = rng.gen_bool(0.5); diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index b732e4163..7585f4686 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -1,5 +1,9 @@ #![allow(dead_code)] +use std::io; + +use clap::Command; +use clap_complete::Generator; #[macro_use] extern crate rust_i18n; @@ -24,8 +28,7 @@ mod helper; #[cfg(test)] mod tests; -use crate::easytier_core::init_instance; -use crate::helper::{clear_udp_socket, get_stats, get_token, run}; +use crate::helper::{get_stats, get_token, run}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::thread::sleep; @@ -34,6 +37,10 @@ use std::time::Duration; pub const VERSION: &str = common::constants::EASYTIER_VERSION; rust_i18n::i18n!("locales", fallback = "en"); +pub fn print_completions(generator: G, cmd: &mut Command, bin_name: &str) { + clap_complete::generate(generator, cmd, bin_name, &mut io::stdout()); +} + #[unsafe(no_mangle)] pub extern "C" fn start(config_path: *const c_char) { let c_str = unsafe { @@ -41,14 +48,7 @@ pub extern "C" fn start(config_path: *const c_char) { .to_str() .unwrap_or("Error decoding config_path") }; - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - init_instance(c_str).await; - }); run(c_str); - rt.block_on(async { - clear_udp_socket().await; - }); } #[unsafe(no_mangle)] diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index e06414855..4cc4fd975 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -10,7 +10,7 @@ use std::{ time::SystemTime, }; -use dashmap::DashMap; +use dashmap::{DashMap, DashSet}; use tokio::{ sync::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, @@ -367,14 +367,14 @@ impl Drop for ForeignNetworkEntry { struct ForeignNetworkManagerData { network_peer_maps: DashMap>, - peer_network_map: DashMap, + peer_network_map: DashMap>, network_peer_last_update: DashMap, accessor: Arc>, lock: std::sync::Mutex<()>, } impl ForeignNetworkManagerData { - fn get_peer_network(&self, peer_id: PeerId) -> Option { + fn get_peer_network(&self, peer_id: PeerId) -> Option> { self.peer_network_map.get(&peer_id).map(|v| v.clone()) } @@ -384,7 +384,10 @@ impl ForeignNetworkManagerData { fn remove_peer(&self, peer_id: PeerId, network_name: &String) { let _l = self.lock.lock().unwrap(); - self.peer_network_map.remove(&peer_id); + self.peer_network_map.remove_if(&peer_id, |_, v| { + let _ = v.remove(network_name); + v.is_empty() + }); if let Some(_) = self .network_peer_maps .remove_if(network_name, |_, v| v.peer_map.is_empty()) @@ -406,7 +409,10 @@ impl ForeignNetworkManagerData { fn remove_network(&self, network_name: &String) { let _l = self.lock.lock().unwrap(); - self.peer_network_map.retain(|_, v| v != network_name); + self.peer_network_map.iter().for_each(|v| { + v.value().remove(network_name); + }); + self.peer_network_map.retain(|_, v| !v.is_empty()); self.network_peer_maps.remove(network_name); self.network_peer_last_update.remove(network_name); } @@ -439,7 +445,9 @@ impl ForeignNetworkManagerData { .clone(); self.peer_network_map - .insert(dst_peer_id, network_identity.network_name.clone()); + .entry(dst_peer_id) + .or_insert_with(|| DashSet::new()) + .insert(network_identity.network_name.clone()); self.network_peer_last_update .insert(network_identity.network_name.clone(), SystemTime::now()); @@ -665,6 +673,23 @@ impl ForeignNetworkManager { Err(Error::RouteError(Some("network not found".to_string()))) } } + + pub async fn close_peer_conn( + &self, + peer_id: PeerId, + conn_id: &super::peer_conn::PeerConnId, + ) -> Result<(), Error> { + let network_names = self.data.get_peer_network(peer_id).unwrap_or_default(); + for network_name in network_names { + if let Some(entry) = self.data.get_network_entry(&network_name) { + let ret = entry.peer_map.close_peer_conn(peer_id, conn_id).await; + if ret.is_ok() || !matches!(ret.as_ref().unwrap_err(), Error::NotFound) { + return ret; + } + } + } + Err(Error::NotFound) + } } impl Drop for ForeignNetworkManager { @@ -675,7 +700,7 @@ impl Drop for ForeignNetworkManager { } #[cfg(test)] -mod tests { +pub mod tests { use crate::{ common::global_ctx::tests::get_mock_global_ctx_with_network, connector::udp_hole_punch::tests::{ @@ -711,7 +736,7 @@ mod tests { peer_mgr } - async fn create_mock_peer_manager_for_foreign_network(network: &str) -> Arc { + pub async fn create_mock_peer_manager_for_foreign_network(network: &str) -> Arc { create_mock_peer_manager_for_foreign_network_ext(network, network).await } diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index f93b082a7..0062761d0 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -1,7 +1,7 @@ use std::{ fmt::Debug, - net::Ipv4Addr, - sync::{Arc, Weak}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::{atomic::AtomicBool, Arc, Weak}, time::{Instant, SystemTime}, }; @@ -143,6 +143,8 @@ pub struct PeerManager { exit_nodes: Vec, reserved_my_peer_id_map: DashMap, + + allow_loopback_tunnel: AtomicBool, } impl Debug for PeerManager { @@ -271,9 +273,16 @@ impl PeerManager { exit_nodes, reserved_my_peer_id_map: DashMap::new(), + + allow_loopback_tunnel: AtomicBool::new(true), } } + pub fn set_allow_loopback_tunnel(&self, allow_loopback_tunnel: bool) { + self.allow_loopback_tunnel + .store(allow_loopback_tunnel, std::sync::atomic::Ordering::Relaxed); + } + fn build_foreign_network_manager_accessor( peer_map: &Arc, ) -> Box { @@ -334,6 +343,8 @@ impl PeerManager { pub fn has_directly_connected_conn(&self, peer_id: PeerId) -> bool { if let Some(peer) = self.peers.get_peer_by_id(peer_id) { peer.has_directly_connected_conn() + } else if self.foreign_network_client.get_peer_map().has_peer(peer_id) { + true } else { false } @@ -354,6 +365,65 @@ impl PeerManager { self.add_client_tunnel(t, true).await } + // avoid loop back to virtual network + fn check_remote_addr_not_from_virtual_network( + &self, + tunnel: &dyn Tunnel, + ) -> Result<(), anyhow::Error> { + tracing::info!("check remote addr not from virtual network"); + let Some(tunnel_info) = tunnel.info() else { + anyhow::bail!("tunnel info is not set"); + }; + let Some(src) = tunnel_info.remote_addr.map(url::Url::from) else { + anyhow::bail!("tunnel info remote addr is not set"); + }; + if src.scheme() == "ring" { + return Ok(()); + } + let src_host = match src.socket_addrs(|| Some(1)) { + Ok(addrs) => addrs, + Err(_) => { + // if the tunnel is not rely on ip address, skip check + return Ok(()); + } + }; + let virtual_ipv4 = self.global_ctx.get_ipv4().map(|ip| ip.network()); + let virtual_ipv6 = self.global_ctx.get_ipv6().map(|ip| ip.network()); + tracing::info!( + ?virtual_ipv4, + ?virtual_ipv6, + "check remote addr not from virtual network" + ); + for addr in src_host { + // if no-tun is enabled, the src ip of packet in virtual network is converted to loopback address + if addr.ip().is_loopback() + && !self + .allow_loopback_tunnel + .load(std::sync::atomic::Ordering::Relaxed) + { + anyhow::bail!("tunnel src host is loopback address"); + } + + match addr { + SocketAddr::V4(addr) => { + if let Some(virtual_ipv4) = virtual_ipv4 { + if virtual_ipv4.contains(&addr.ip()) { + anyhow::bail!("tunnel src host is from the virtual network (ignore this error please)"); + } + } + } + SocketAddr::V6(addr) => { + if let Some(virtual_ipv6) = virtual_ipv6 { + if virtual_ipv6.contains(&addr.ip()) { + anyhow::bail!("tunnel src host is from the virtual network (ignore this error please)"); + } + } + } + } + } + Ok(()) + } + #[tracing::instrument(ret)] pub async fn add_tunnel_as_server( &self, @@ -361,6 +431,8 @@ impl PeerManager { is_directly_connected: bool, ) -> Result<(), Error> { tracing::info!("add tunnel as server start"); + self.check_remote_addr_not_from_virtual_network(&tunnel)?; + let mut conn = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel); conn.do_handshake_as_server_ext(|peer, msg| { if msg.network_name @@ -862,6 +934,50 @@ impl PeerManager { } } } + #[cfg(target_env = "ohos")] + { + if dst_peers.is_empty() { + tracing::info!("no peer id for ipv4: {}, set exit_node for ohos", ipv4_addr); + dst_peers.push(self.my_peer_id.clone()); + is_exit_node = true; + } + } + (dst_peers, is_exit_node) + } + + pub async fn get_msg_dst_peer_ipv6(&self, ipv6_addr: &Ipv6Addr) -> (Vec, bool) { + let mut is_exit_node = false; + let mut dst_peers = vec![]; + let network_length = self + .global_ctx + .get_ipv6() + .map(|x| x.network_length()) + .unwrap_or(64); + let ipv6_inet = cidr::Ipv6Inet::new(*ipv6_addr, network_length).unwrap(); + if ipv6_addr.is_multicast() || *ipv6_addr == ipv6_inet.last_address() { + dst_peers.extend( + self.peers + .list_routes() + .await + .iter() + .map(|x| x.key().clone()), + ); + } else if let Some(peer_id) = self.peers.get_peer_id_by_ipv6(&ipv6_addr).await { + dst_peers.push(peer_id); + } else { + // For IPv6, we'll need to implement exit node support later + // For now, just try to find any available peer for routing + if dst_peers.is_empty() { + dst_peers.extend( + self.peers + .list_routes() + .await + .iter() + .map(|x| x.key().clone()), + ); + is_exit_node = true; + } + } (dst_peers, is_exit_node) } @@ -880,11 +996,11 @@ impl PeerManager { Ok(()) } - pub async fn send_msg_ipv4(&self, mut msg: ZCPacket, ipv4_addr: Ipv4Addr) -> Result<(), Error> { + pub async fn send_msg_by_ip(&self, mut msg: ZCPacket, ip_addr: IpAddr) -> Result<(), Error> { tracing::trace!( - "do send_msg in peer manager, msg: {:?}, ipv4_addr: {}", + "do send_msg in peer manager, msg: {:?}, ip_addr: {}", msg, - ipv4_addr + ip_addr ); msg.fill_peer_manager_hdr( @@ -904,10 +1020,13 @@ impl PeerManager { .await; } - let (dst_peers, is_exit_node) = self.get_msg_dst_peer(&ipv4_addr).await; + let (dst_peers, is_exit_node) = match ip_addr { + IpAddr::V4(ipv4_addr) => self.get_msg_dst_peer(&ipv4_addr).await, + IpAddr::V6(ipv6_addr) => self.get_msg_dst_peer_ipv6(&ipv6_addr).await, + }; if dst_peers.is_empty() { - tracing::info!("no peer id for ipv4: {}", ipv4_addr); + tracing::info!("no peer id for ip: {}", ip_addr); return Ok(()); } @@ -1081,6 +1200,35 @@ impl PeerManager { self.peer_rpc_mgr.rpc_server().registry().unregister_all(); } + + pub async fn close_peer_conn( + &self, + peer_id: PeerId, + conn_id: &PeerConnId, + ) -> Result<(), Error> { + let ret = self.peers.close_peer_conn(peer_id, conn_id).await; + tracing::info!("close_peer_conn in peer map: {:?}", ret); + if ret.is_ok() || !matches!(ret.as_ref().unwrap_err(), Error::NotFound) { + return ret; + } + + let ret = self + .foreign_network_client + .get_peer_map() + .close_peer_conn(peer_id, conn_id) + .await; + tracing::info!("close_peer_conn in foreign network client: {:?}", ret); + if ret.is_ok() || !matches!(ret.as_ref().unwrap_err(), Error::NotFound) { + return ret; + } + + let ret = self + .foreign_network_manager + .close_peer_conn(peer_id, conn_id) + .await; + tracing::info!("close_peer_conn in foreign network manager done: {:?}", ret); + ret + } } #[cfg(test)] @@ -1100,7 +1248,10 @@ mod tests { peer_manager::RouteAlgoType, peer_rpc::tests::register_service, route_trait::NextHopPolicy, - tests::{connect_peer_manager, wait_route_appear, wait_route_appear_with_cost}, + tests::{ + connect_peer_manager, create_mock_peer_manager_with_name, wait_route_appear, + wait_route_appear_with_cost, + }, }, proto::common::{CompressionAlgoPb, NatType, PeerFeatureFlag}, tunnel::{ @@ -1380,4 +1531,127 @@ mod tests { ) .await; } + + #[tokio::test] + async fn close_conn_in_peer_map() { + let peer_mgr_a = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let peer_mgr_b = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.clone()) + .await + .unwrap(); + + let conns = peer_mgr_a + .get_peer_map() + .list_peer_conns(peer_mgr_b.my_peer_id) + .await; + assert!(conns.is_some()); + let conn_info = conns.as_ref().unwrap().first().unwrap(); + + peer_mgr_a + .close_peer_conn(peer_mgr_b.my_peer_id, &conn_info.conn_id.parse().unwrap()) + .await + .unwrap(); + + wait_for_condition( + || async { + let peers = peer_mgr_a.list_peers().await; + peers.is_empty() + }, + Duration::from_secs(10), + ) + .await; + // a is client, b is server + } + + #[tokio::test] + async fn close_conn_in_foreign_network_client() { + let peer_mgr_server = create_mock_peer_manager_with_name("server".to_string()).await; + let peer_mgr_client = create_mock_peer_manager_with_name("client".to_string()).await; + connect_peer_manager(peer_mgr_client.clone(), peer_mgr_server.clone()).await; + wait_for_condition( + || async { + peer_mgr_client + .get_foreign_network_client() + .list_public_peers() + .await + .len() + == 1 + }, + Duration::from_secs(3), + ) + .await; + + let peer_id = peer_mgr_client + .foreign_network_client + .list_public_peers() + .await[0]; + let conns = peer_mgr_client + .foreign_network_client + .get_peer_map() + .list_peer_conns(peer_id) + .await; + assert!(conns.is_some()); + let conn_info = conns.as_ref().unwrap().first().unwrap(); + peer_mgr_client + .close_peer_conn(peer_id, &conn_info.conn_id.parse().unwrap()) + .await + .unwrap(); + + wait_for_condition( + || async { + peer_mgr_client + .get_foreign_network_client() + .list_public_peers() + .await + .len() + == 0 + }, + Duration::from_secs(10), + ) + .await; + } + + #[tokio::test] + async fn close_conn_in_foreign_network_manager() { + let peer_mgr_server = create_mock_peer_manager_with_name("server".to_string()).await; + let peer_mgr_client = create_mock_peer_manager_with_name("client".to_string()).await; + connect_peer_manager(peer_mgr_client.clone(), peer_mgr_server.clone()).await; + wait_for_condition( + || async { + peer_mgr_client + .get_foreign_network_client() + .list_public_peers() + .await + .len() + == 1 + }, + Duration::from_secs(3), + ) + .await; + + let conns = peer_mgr_server + .foreign_network_manager + .list_foreign_networks() + .await; + let client_info = conns.foreign_networks["client"].peers[0].clone(); + let conn_info = client_info.conns[0].clone(); + peer_mgr_server + .close_peer_conn(client_info.peer_id, &conn_info.conn_id.parse().unwrap()) + .await + .unwrap(); + + wait_for_condition( + || async { + peer_mgr_client + .get_foreign_network_client() + .list_public_peers() + .await + .len() + == 0 + }, + Duration::from_secs(10), + ) + .await; + } } diff --git a/easytier/src/peers/peer_map.rs b/easytier/src/peers/peer_map.rs index 4aa0abc54..9bd37bcad 100644 --- a/easytier/src/peers/peer_map.rs +++ b/easytier/src/peers/peer_map.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, sync::Arc}; +use std::{net::{Ipv4Addr, Ipv6Addr}, sync::Arc}; use anyhow::Context; use dashmap::DashMap; @@ -194,6 +194,16 @@ impl PeerMap { None } + pub async fn get_peer_id_by_ipv6(&self, ipv6: &Ipv6Addr) -> Option { + for route in self.routes.read().await.iter() { + let peer_id = route.get_peer_id_by_ipv6(ipv6).await; + if peer_id.is_some() { + return peer_id; + } + } + None + } + pub async fn get_route_peer_info(&self, peer_id: PeerId) -> Option { for route in self.routes.read().await.iter() { if let Some(info) = route.get_peer_info(peer_id).await { diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index f9d3adf9e..d1d99f7a7 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -1,7 +1,7 @@ use std::{ collections::BTreeSet, fmt::Debug, - net::Ipv4Addr, + net::{Ipv4Addr, Ipv6Addr}, sync::{ atomic::{AtomicBool, AtomicU32, Ordering}, Arc, Weak, @@ -125,6 +125,7 @@ impl RoutePeerInfo { peer_route_id: 0, network_length: 24, quic_port: None, + ipv6_addr: None, } } @@ -165,6 +166,7 @@ impl RoutePeerInfo { .unwrap_or(24), quic_port: global_ctx.get_quic_proxy_port().map(|x| x as u32), + ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()), }; let need_update_periodically = if let Ok(Ok(d)) = @@ -221,6 +223,8 @@ impl Into for RoutePeerInfo { next_hop_peer_id_latency_first: None, cost_latency_first: None, path_latency_latency_first: None, + + ipv6_addr: self.ipv6_addr.map(Into::into), } } } @@ -635,6 +639,7 @@ struct RouteTable { peer_infos: DashMap, next_hop_map: NextHopMap, ipv4_peer_id_map: DashMap, + ipv6_peer_id_map: DashMap, cidr_peer_id_map: DashMap, next_hop_map_version: AtomicVersion, } @@ -645,6 +650,7 @@ impl RouteTable { peer_infos: DashMap::new(), next_hop_map: DashMap::new(), ipv4_peer_id_map: DashMap::new(), + ipv6_peer_id_map: DashMap::new(), cidr_peer_id_map: DashMap::new(), next_hop_map_version: AtomicVersion::new(), } @@ -742,6 +748,10 @@ impl RouteTable { // remove ipv4 map for peers we cannot reach. self.next_hop_map.contains_key(v) }); + self.ipv6_peer_id_map.retain(|_, v| { + // remove ipv6 map for peers we cannot reach. + self.next_hop_map.contains_key(v) + }); self.cidr_peer_id_map.retain(|_, v| { // remove cidr map for peers we cannot reach. self.next_hop_map.contains_key(v) @@ -876,6 +886,17 @@ impl RouteTable { .or_insert(*peer_id); } + if let Some(ipv6_addr) = info.ipv6_addr.and_then(|x| x.address) { + self.ipv6_peer_id_map + .entry(ipv6_addr.into()) + .and_modify(|v| { + if *v != *peer_id && is_new_peer_better(*v) { + *v = *peer_id; + } + }) + .or_insert(*peer_id); + } + for cidr in info.proxy_cidrs.iter() { self.cidr_peer_id_map .entry(cidr.parse().unwrap()) @@ -2267,6 +2288,21 @@ impl Route for PeerRoute { None } + async fn get_peer_id_by_ipv6(&self, ipv6_addr: &Ipv6Addr) -> Option { + let route_table = &self.service_impl.route_table; + if let Some(peer_id) = route_table.ipv6_peer_id_map.get(ipv6_addr) { + return Some(*peer_id); + } + + // TODO: Add proxy support for IPv6 similar to IPv4 + // if let Some(peer_id) = route_table.get_peer_id_for_proxy_ipv6(ipv6_addr) { + // return Some(peer_id); + // } + + tracing::debug!(?ipv6_addr, "no peer id for ipv6"); + None + } + async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) { *self.service_impl.cost_calculator.write().unwrap() = Some(_cost_fn); self.service_impl.synced_route_info.version.inc(); diff --git a/easytier/src/peers/peer_rpc_service.rs b/easytier/src/peers/peer_rpc_service.rs index 8e3b5e73c..13eba761c 100644 --- a/easytier/src/peers/peer_rpc_service.rs +++ b/easytier/src/peers/peer_rpc_service.rs @@ -36,6 +36,11 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer { .chain(self.global_ctx.get_running_listeners().into_iter()) .map(Into::into) .collect(); + // remove et ipv6 from the interface ipv6 list + if let Some(et_ipv6) = self.global_ctx.get_ipv6() { + let et_ipv6: crate::proto::common::Ipv6Addr = et_ipv6.address().into(); + ret.interface_ipv6s.retain(|x| *x != et_ipv6); + } tracing::trace!( "get_ip_list: public_ipv4: {:?}, public_ipv6: {:?}, listeners: {:?}", ret.public_ipv4, diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index 21f6083e8..d18e5ba12 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -1,4 +1,4 @@ -use std::{net::Ipv4Addr, sync::Arc}; +use std::{net::{Ipv4Addr, Ipv6Addr}, sync::Arc}; use dashmap::DashMap; @@ -82,6 +82,10 @@ pub trait Route { None } + async fn get_peer_id_by_ipv6(&self, _ipv6: &Ipv6Addr) -> Option { + None + } + async fn list_peers_own_foreign_network( &self, _network_identity: &NetworkIdentity, diff --git a/easytier/src/proto/cli.proto b/easytier/src/proto/cli.proto index 0eafeb2e2..c73205d86 100644 --- a/easytier/src/proto/cli.proto +++ b/easytier/src/proto/cli.proto @@ -65,6 +65,8 @@ message Route { optional uint32 next_hop_peer_id_latency_first = 12; optional int32 cost_latency_first = 13; optional int32 path_latency_latency_first = 14; + + common.Ipv6Inet ipv6_addr = 15; } message PeerRoutePair { @@ -170,6 +172,31 @@ service ConnectorManageRpc { rpc ManageConnector(ManageConnectorRequest) returns (ManageConnectorResponse); } +message MappedListener { + common.Url url = 1; +} + +message ListMappedListenerRequest {} + +message ListMappedListenerResponse { repeated MappedListener mappedlisteners = 1; } + +enum MappedListenerManageAction { + MAPPED_LISTENER_ADD = 0; + MAPPED_LISTENER_REMOVE = 1; +} + +message ManageMappedListenerRequest { + MappedListenerManageAction action = 1; + common.Url url = 2; +} + +message ManageMappedListenerResponse {} + +service MappedListenerManageRpc { + rpc ListMappedListener(ListMappedListenerRequest) returns (ListMappedListenerResponse); + rpc ManageMappedListener(ManageMappedListenerRequest) returns (ManageMappedListenerResponse); +} + message VpnPortalInfo { string vpn_type = 1; string client_config = 2; diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index c9761e9e1..c6378e4b3 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -43,6 +43,8 @@ message FlagsInConfig { // a global relay limit, only work for foreign network uint64 foreign_relay_bps_limit = 26; + + uint32 multi_thread_count = 27; } message RpcDescriptor { @@ -137,6 +139,11 @@ message Ipv4Inet { uint32 network_length = 2; } +message Ipv6Inet { + Ipv6Addr address = 1; + uint32 network_length = 2; +} + message Url { string url = 1; } message SocketAddr { diff --git a/easytier/src/proto/common.rs b/easytier/src/proto/common.rs index b97c60abb..07d3d1c23 100644 --- a/easytier/src/proto/common.rs +++ b/easytier/src/proto/common.rs @@ -131,6 +131,41 @@ impl FromStr for Ipv4Inet { } } +impl From for Ipv6Inet { + fn from(value: cidr::Ipv6Inet) -> Self { + Ipv6Inet { + address: Some(value.address().into()), + network_length: value.network_length() as u32, + } + } +} + +impl From for cidr::Ipv6Inet { + fn from(value: Ipv6Inet) -> Self { + cidr::Ipv6Inet::new( + value.address.unwrap_or_default().into(), + value.network_length as u8, + ) + .unwrap() + } +} + +impl fmt::Display for Ipv6Inet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", cidr::Ipv6Inet::from(self.clone())) + } +} + +impl FromStr for Ipv6Inet { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(Ipv6Inet::from( + cidr::Ipv6Inet::from_str(s).with_context(|| "Failed to parse Ipv6Inet")?, + )) + } +} + impl From for Url { fn from(value: url::Url) -> Self { Url { diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 0ac05ca2a..0bfb432dc 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -24,6 +24,7 @@ message RoutePeerInfo { uint32 network_length = 13; optional uint32 quic_port = 14; + optional common.Ipv6Inet ipv6_addr = 15; } message PeerIdVersion { diff --git a/easytier/src/proto/rpc_impl/bidirect.rs b/easytier/src/proto/rpc_impl/bidirect.rs index 41f7e5730..231c3041b 100644 --- a/easytier/src/proto/rpc_impl/bidirect.rs +++ b/easytier/src/proto/rpc_impl/bidirect.rs @@ -139,7 +139,7 @@ impl BidirectRpcManager { server_tx.send(o).await.unwrap(); continue; } else if peer_manager_header.packet_type == PacketType::RpcResp as u8 { - let _ = client_tx.send(o).await; + client_tx.send(o).await.unwrap(); continue; } } diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index d29025159..195047aaa 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -42,6 +42,7 @@ message NetworkConfig { optional string dev_name = 20; optional bool use_smoltcp = 21; + optional bool disable_ipv6 = 47; optional bool enable_kcp_proxy = 22; optional bool disable_kcp_input = 23; optional bool disable_p2p = 24; diff --git a/easytier/src/tests/ipv6_test.rs b/easytier/src/tests/ipv6_test.rs new file mode 100644 index 000000000..d71dc0784 --- /dev/null +++ b/easytier/src/tests/ipv6_test.rs @@ -0,0 +1,66 @@ +use std::net::Ipv6Addr; + +use crate::{ + common::config::{ConfigLoader, TomlConfigLoader}, + common::global_ctx::tests::get_mock_global_ctx, + peers::peer_manager::RouteAlgoType, + proto::peer_rpc::RoutePeerInfo, +}; + +#[tokio::test] +async fn test_ipv6_config_support() { + let config = TomlConfigLoader::default(); + + // Test IPv6 configuration setting and getting + let ipv6_cidr = "fd00::1/64".parse().unwrap(); + config.set_ipv6(Some(ipv6_cidr)); + + assert_eq!(config.get_ipv6(), Some(ipv6_cidr)); +} + +#[tokio::test] +async fn test_global_ctx_ipv6() { + let global_ctx = get_mock_global_ctx(); + + // Test setting and getting IPv6 from global context + let ipv6_cidr = "fd00::1/64".parse().unwrap(); + global_ctx.set_ipv6(Some(ipv6_cidr)); + + assert_eq!(global_ctx.get_ipv6(), Some(ipv6_cidr)); +} + +#[tokio::test] +async fn test_route_peer_info_ipv6() { + let global_ctx = get_mock_global_ctx(); + + // Set IPv6 address in global context + let ipv6_cidr = "fd00::1/64".parse().unwrap(); + global_ctx.set_ipv6(Some(ipv6_cidr)); + + // Create RoutePeerInfo with IPv6 support + let peer_info = RoutePeerInfo::new(); + let updated_info = peer_info.update_self(123, 456, &global_ctx); + + // Verify IPv6 address is included + assert!(updated_info.ipv6_addr.is_some()); + let ipv6_addr: Ipv6Addr = updated_info.ipv6_addr.unwrap().address.unwrap().into(); + assert_eq!(ipv6_addr, ipv6_cidr.address()); +} + +#[tokio::test] +async fn test_peer_manager_ipv6() { + let global_ctx = get_mock_global_ctx(); + let (packet_sender, _packet_receiver) = tokio::sync::mpsc::channel(100); + let peer_mgr = crate::peers::peer_manager::PeerManager::new( + RouteAlgoType::Ospf, + global_ctx.clone(), + packet_sender, + ); + + // Test IPv6 address lookup for unknown address + let ipv6_addr = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 2); + let (peers, _is_self) = peer_mgr.get_msg_dst_peer_ipv6(&ipv6_addr).await; + + // Should return empty peers list for unknown IPv6 + assert!(peers.is_empty()); +} diff --git a/easytier/src/tests/mod.rs b/easytier/src/tests/mod.rs index 271dfed78..ad5bb5a03 100644 --- a/easytier/src/tests/mod.rs +++ b/easytier/src/tests/mod.rs @@ -1,6 +1,8 @@ #[cfg(target_os = "linux")] mod three_node; +mod ipv6_test; + use crate::common::PeerId; use crate::peers::peer_manager::PeerManager; diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 9284fbc55..901d67dfc 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -51,11 +51,17 @@ pub fn prepare_linux_namespaces() { add_ns_to_bridge("br_b", "net_d"); } -pub fn get_inst_config(inst_name: &str, ns: Option<&str>, ipv4: &str) -> TomlConfigLoader { +pub fn get_inst_config( + inst_name: &str, + ns: Option<&str>, + ipv4: &str, + ipv6: &str, +) -> TomlConfigLoader { let config = TomlConfigLoader::default(); config.set_inst_name(inst_name.to_owned()); config.set_netns(ns.map(|s| s.to_owned())); config.set_ipv4(Some(ipv4.parse().unwrap())); + config.set_ipv6(Some(ipv6.parse().unwrap())); config.set_listeners(vec![ "tcp://0.0.0.0:11010".parse().unwrap(), "udp://0.0.0.0:11010".parse().unwrap(), @@ -82,16 +88,19 @@ pub async fn init_three_node_ex TomlConfigLoader>( "inst1", Some("net_a"), "10.144.144.1", + "fd00::1/64", ))); let mut inst2 = Instance::new(cfg_cb(get_inst_config( "inst2", Some("net_b"), "10.144.144.2", + "fd00::2/64", ))); let mut inst3 = Instance::new(cfg_cb(get_inst_config( "inst3", Some("net_c"), "10.144.144.3", + "fd00::3/64", ))); inst1.run().await.unwrap(); @@ -232,6 +241,30 @@ async fn ping_test(from_netns: &str, target_ip: &str, payload_size: Option) -> bool { + let _g = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard(); + let code = tokio::process::Command::new("ip") + .args(&[ + "netns", + "exec", + from_netns, + "ping6", + "-c", + "1", + "-s", + payload_size.unwrap_or(56).to_string().as_str(), + "-W", + "1", + target_ip.to_string().as_str(), + ]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .await + .unwrap(); + code.code().unwrap() == 0 +} + #[rstest::rstest] #[tokio::test] #[serial_test::serial] @@ -250,12 +283,26 @@ pub async fn basic_three_node_test(#[values("tcp", "udp", "wg", "ws", "wss")] pr insts[0].get_peer_manager().list_routes().await, ); + // Test IPv4 connectivity wait_for_condition( || async { ping_test("net_c", "10.144.144.1", None).await }, Duration::from_secs(5000), ) .await; + // Test IPv6 connectivity + wait_for_condition( + || async { ping6_test("net_c", "fd00::1", None).await }, + Duration::from_secs(5), + ) + .await; + + wait_for_condition( + || async { ping6_test("net_a", "fd00::3", None).await }, + Duration::from_secs(5), + ) + .await; + drop_insts(insts).await; } @@ -401,7 +448,8 @@ pub async fn quic_proxy() { "udp", |cfg| { if cfg.get_inst_name() == "inst3" { - cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None); + cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None) + .unwrap(); } cfg }, @@ -451,11 +499,13 @@ pub async fn subnet_proxy_three_node_test( flags.disable_quic_input = disable_quic_input; flags.enable_quic_proxy = dst_enable_quic_proxy; cfg.set_flags(flags); - cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None); + cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None) + .unwrap(); cfg.add_proxy_cidr( "10.1.2.0/24".parse().unwrap(), Some("10.1.3.0/24".parse().unwrap()), - ); + ) + .unwrap(); } if cfg.get_inst_name() == "inst2" && relay_by_public_server { @@ -562,7 +612,12 @@ pub async fn proxy_three_node_disconnect_test(#[values("tcp", "wg")] proto: &str }; let insts = init_three_node(proto).await; - let mut inst4 = Instance::new(get_inst_config("inst4", Some("net_d"), "10.144.144.4")); + let mut inst4 = Instance::new(get_inst_config( + "inst4", + Some("net_d"), + "10.144.144.4", + "fd00::4/64", + )); if proto == "tcp" { inst4 .get_conn_manager() @@ -627,16 +682,7 @@ pub async fn proxy_three_node_disconnect_test(#[values("tcp", "wg")] proto: &str .iter() .find(|r| **r == inst4.peer_id()) .is_none(); - if !ret { - println!( - "conn info: {:?}", - insts[2] - .get_peer_manager() - .get_peer_map() - .list_peer_conns(inst4.peer_id()) - .await - ); - } + ret }, // 0 down, assume last packet is recv in -0.01 @@ -726,13 +772,23 @@ pub async fn udp_broadcast_test() { pub async fn foreign_network_forward_nic_data() { prepare_linux_namespaces(); - let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1", "fd00::1/64"); center_node_config .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); let mut center_inst = Instance::new(center_node_config); - let mut inst1 = Instance::new(get_inst_config("inst1", Some("net_b"), "10.144.145.1")); - let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_c"), "10.144.145.2")); + let mut inst1 = Instance::new(get_inst_config( + "inst1", + Some("net_b"), + "10.144.145.1", + "fd00:1::1/64", + )); + let mut inst2 = Instance::new(get_inst_config( + "inst2", + Some("net_c"), + "10.144.145.2", + "fd00:1::2/64", + )); center_inst.run().await.unwrap(); inst1.run().await.unwrap(); @@ -940,21 +996,26 @@ pub async fn foreign_network_functional_cluster() { crate::set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); prepare_linux_namespaces(); - let center_node_config1 = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + let center_node_config1 = get_inst_config("inst1", Some("net_a"), "10.144.144.1", "fd00::1/64"); center_node_config1 .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); let mut center_inst1 = Instance::new(center_node_config1); - let center_node_config2 = get_inst_config("inst2", Some("net_b"), "10.144.144.2"); + let center_node_config2 = get_inst_config("inst2", Some("net_b"), "10.144.144.2", "fd00::2/64"); center_node_config2 .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); let mut center_inst2 = Instance::new(center_node_config2); - let inst1_config = get_inst_config("inst1", Some("net_c"), "10.144.145.1"); + let inst1_config = get_inst_config("inst1", Some("net_c"), "10.144.145.1", "fd00:2::1/64"); inst1_config.set_listeners(vec![]); let mut inst1 = Instance::new(inst1_config); - let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_d"), "10.144.145.2")); + let mut inst2 = Instance::new(get_inst_config( + "inst2", + Some("net_d"), + "10.144.145.2", + "fd00:2::2/64", + )); center_inst1.run().await.unwrap(); center_inst2.run().await.unwrap(); @@ -1011,18 +1072,23 @@ pub async fn foreign_network_functional_cluster() { pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) { prepare_linux_namespaces(); - let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + let center_node_config = get_inst_config("inst1", Some("net_a"), "10.144.144.1", "fd00::1/64"); if is_foreign { center_node_config .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); } let mut center_inst = Instance::new(center_node_config); - let inst1_config = get_inst_config("inst1", Some("net_b"), "10.144.145.1"); + let inst1_config = get_inst_config("inst1", Some("net_b"), "10.144.145.1", "fd00:1::1/64"); inst1_config.set_listeners(vec![]); let mut inst1 = Instance::new(inst1_config); - let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_c"), "10.144.145.2")); + let mut inst2 = Instance::new(get_inst_config( + "inst2", + Some("net_c"), + "10.144.145.2", + "fd00:1::2/64", + )); center_inst.run().await.unwrap(); inst1.run().await.unwrap(); @@ -1118,7 +1184,8 @@ pub async fn port_forward_test( }, ]); } else if cfg.get_inst_name() == "inst3" { - cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None); + cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None) + .unwrap(); } let mut flags = cfg.get_flags(); flags.no_tun = no_tun; @@ -1240,3 +1307,24 @@ pub async fn relay_bps_limit_test(#[values(100, 200, 400, 800)] bps_limit: u64) drop_insts(insts).await; } + +#[tokio::test] +async fn avoid_tunnel_loop_back_to_virtual_network() { + let insts = init_three_node("udp").await; + + let tcp_connector = TcpTunnelConnector::new("tcp://10.144.144.2:11010".parse().unwrap()); + insts[0] + .get_peer_manager() + .try_direct_connect(tcp_connector) + .await + .unwrap_err(); + + let udp_connector = UdpTunnelConnector::new("udp://10.144.144.3:11010".parse().unwrap()); + insts[0] + .get_peer_manager() + .try_direct_connect(udp_connector) + .await + .unwrap_err(); + + drop_insts(insts).await; +} diff --git a/easytier/src/tunnel/common.rs b/easytier/src/tunnel/common.rs index 3a0803173..f0ea3b8a6 100644 --- a/easytier/src/tunnel/common.rs +++ b/easytier/src/tunnel/common.rs @@ -388,7 +388,12 @@ pub(crate) fn setup_sokcet2_ext( } } - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + #[cfg(any( + target_os = "android", + target_os = "fuchsia", + target_os = "linux", + target_env = "ohos" + ))] if let Some(dev_name) = bind_dev { tracing::trace!(dev_name = ?dev_name, "bind device"); socket2_socket.bind_device(Some(dev_name.as_bytes()))?; diff --git a/easytier/src/vpn_portal/wireguard.rs b/easytier/src/vpn_portal/wireguard.rs index 780e516e4..dca3ed016 100644 --- a/easytier/src/vpn_portal/wireguard.rs +++ b/easytier/src/vpn_portal/wireguard.rs @@ -1,5 +1,5 @@ use std::{ - net::{Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, }; @@ -128,7 +128,7 @@ impl WireGuardImpl { tracing::trace!(?i, "Received from wg client"); let dst = i.get_destination(); let _ = peer_mgr - .send_msg_ipv4(ZCPacket::new_with_payload(inner.as_ref()), dst) + .send_msg_by_ip(ZCPacket::new_with_payload(inner.as_ref()), IpAddr::V4(dst)) .await; } diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..ad1125c29 --- /dev/null +++ b/flake.lock @@ -0,0 +1,82 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1750741721, + "narHash": "sha256-Z0djmTa1YmnGMfE9jEe05oO4zggjDmxOGKwt844bUhE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4b1164c3215f018c4442463a27689d973cffd750", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1750905536, + "narHash": "sha256-Mo7yXM5IvMGNvJPiNkFsVT2UERmnvjsKgnY6UyDdySQ=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "2fa7c0aabd15fa0ccc1dc7e675a4fcf0272ad9a1", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..ceb754e6e --- /dev/null +++ b/flake.nix @@ -0,0 +1,44 @@ +{ + description = "EasyTier development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + + lib = nixpkgs.lib; + + rust = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + in + { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + rust + pkg-config + protobuf + ]; + + buildInputs = with pkgs; [ + zstd + ]; + + LIBCLANG_PATH = "${pkgs.libclang.lib}/lib"; + ZSTD_SYS_USE_PKG_CONFIG = true; + KCP_SYS_EXTRA_HEADER_PATH = "${pkgs.libclang.lib}/lib/clang/19/include:${pkgs.glibc.dev}/include"; + }; + }); +} From 3b239fd72c0628baa9455103e19f70f2ae4bdd51 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sun, 20 Jul 2025 13:29:48 +0800 Subject: [PATCH 02/10] remove mimalloc --- easytier/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index c8fb8f5f1..219538a36 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -293,11 +293,10 @@ 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", "smoltcp", "tun", From 592a030e0930fd9fd1ba14ee21bb5da76f118c55 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sun, 20 Jul 2025 14:19:47 +0800 Subject: [PATCH 03/10] clear CONNECTION_MAP resource --- easytier/src/instance/instance.rs | 2 ++ easytier/src/tunnel/ring.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index ee1d1a9a2..8cf93c0ea 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -901,6 +901,8 @@ impl Instance { if let Some(rpc_server) = self.rpc_server.take() { rpc_server.registry().unregister_all(); }; + let mut guard = crate::tunnel::ring::CONNECTION_MAP.lock(); + guard.await.clear() } } diff --git a/easytier/src/tunnel/ring.rs b/easytier/src/tunnel/ring.rs index e0dd13ae0..1e543e9c1 100644 --- a/easytier/src/tunnel/ring.rs +++ b/easytier/src/tunnel/ring.rs @@ -179,12 +179,12 @@ impl Debug for RingSink { } } -struct Connection { +pub struct Connection { client: Arc, server: Arc, } -static CONNECTION_MAP: Lazy>>>>> = +pub(crate) static CONNECTION_MAP: Lazy>>>>> = Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); #[derive(Debug)] From 768f64940c20d51759035f8a25f874f5a4b1c270 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 20 Jul 2025 14:40:38 +0800 Subject: [PATCH 04/10] Merge new Offical Easytier codes (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen --- easytier/Cargo.toml | 8 +- easytier/src/arch/windows.rs | 350 +++++++++++++++++- easytier/src/easytier-cli.rs | 267 +++++++------ easytier/src/easytier_core.rs | 56 ++- easytier/src/helper.rs | 8 +- easytier/src/instance/virtual_nic.rs | 162 +++++++- easytier/src/instance_manager.rs | 2 +- easytier/src/launcher.rs | 5 +- easytier/src/lib.rs | 4 +- easytier/src/peer_center/instance.rs | 147 ++++++-- easytier/src/peers/foreign_network_manager.rs | 17 + easytier/src/peers/peer.rs | 13 +- easytier/src/peers/peer_manager.rs | 10 +- easytier/src/peers/peer_map.rs | 35 +- easytier/src/peers/peer_ospf_route.rs | 2 +- easytier/src/peers/rpc_service.rs | 18 +- 16 files changed, 858 insertions(+), 246 deletions(-) diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 219538a36..ecad0efc4 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -89,11 +89,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", git = "https://github.com/EasyTier/rust-tun", 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", @@ -253,10 +253,10 @@ winreg = "0.52" windows-service = "0.7.0" windows-sys = { version = "0.52", features = [ "Win32_NetworkManagement_IpHelper", - "Win32_NetworkManagement_Ndis", + "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock", "Win32_Foundation" -] } +]} winapi = { version = "0.3.9", features = ["impl-default"] } [build-dependencies] 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/easytier-cli.rs b/easytier/src/easytier-cli.rs index b896c23a2..f8b465318 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -28,8 +28,10 @@ use easytier::{ cli::{ list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest, - ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListPeerRequest, - ListPeerResponse, ListRouteRequest, ListRouteResponse, NodeInfo, PeerManageRpc, + ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, + ListPeerRequest, ListPeerResponse, ListRouteRequest, ListRouteResponse, + ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc, + MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc, PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory, @@ -92,9 +94,7 @@ enum SubCommand { #[command(about = "show tcp/kcp proxy status")] Proxy, #[command(about = t!("core_clap.generate_completions").to_string())] - GenAutocomplete{ - shell:Shell - }, + GenAutocomplete { shell: Shell }, } #[derive(clap::ValueEnum, Debug, Clone, PartialEq)] @@ -158,13 +158,9 @@ struct MappedListenerArgs { #[derive(Subcommand, Debug)] enum MappedListenerSubCommand { /// Add Mapped Listerner - Add { - url: String - }, + Add { url: String }, /// Remove Mapped Listener - Remove { - url: String - }, + Remove { url: String }, /// List Existing Mapped Listener List, } @@ -616,109 +612,61 @@ impl CommandHandler<'_> { }); let route = p.route.clone().unwrap_or_default(); - if route.cost == 1 { - items.push(RouteTableItem { - ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), - hostname: route.hostname.clone(), - proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(), - - next_hop_ipv4: "DIRECT".to_string(), - next_hop_hostname: "".to_string(), - next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0), - path_len: route.cost, - path_latency: next_hop_pair.get_latency_ms().unwrap_or_default() as i32, - - next_hop_ipv4_lat_first: next_hop_pair_latency_first - .map(|pair| pair.route.clone().unwrap_or_default().ipv4_addr) - .unwrap_or_default() - .map(|ip| ip.to_string()) - .unwrap_or_default(), - next_hop_hostname_lat_first: next_hop_pair_latency_first - .map(|pair| pair.route.clone().unwrap_or_default().hostname) - .unwrap_or_default() - .clone(), - path_latency_lat_first: next_hop_pair_latency_first - .map(|pair| { - pair.route - .clone() - .unwrap_or_default() - .path_latency_latency_first - .unwrap_or_default() - }) - .unwrap_or_default(), - path_len_lat_first: next_hop_pair_latency_first - .map(|pair| { - pair.route - .clone() - .unwrap_or_default() - .cost_latency_first - .unwrap_or_default() - }) - .unwrap_or_default(), - - version: if route.version.is_empty() { - "unknown".to_string() - } else { - route.version.to_string() - }, - }); - } else { - items.push(RouteTableItem { - ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), - hostname: route.hostname.clone(), - proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(), - next_hop_ipv4: next_hop_pair + items.push(RouteTableItem { + ipv4: route.ipv4_addr.map(|ip| ip.to_string()).unwrap_or_default(), + hostname: route.hostname.clone(), + proxy_cidrs: route.proxy_cidrs.clone().join(",").to_string(), + next_hop_ipv4: if route.cost == 1 { + "DIRECT".to_string() + } else { + next_hop_pair .route .clone() .unwrap_or_default() .ipv4_addr .map(|ip| ip.to_string()) - .unwrap_or_default(), - next_hop_hostname: next_hop_pair + .unwrap_or_default() + }, + next_hop_hostname: if route.cost == 1 { + "DIRECT".to_string() + } else { + next_hop_pair .route .clone() .unwrap_or_default() .hostname - .clone(), - next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0), - path_len: route.cost, - path_latency: p.route.clone().unwrap_or_default().path_latency as i32, - - next_hop_ipv4_lat_first: next_hop_pair_latency_first + .clone() + }, + next_hop_lat: next_hop_pair.get_latency_ms().unwrap_or(0.0), + path_len: route.cost, + path_latency: route.path_latency, + + next_hop_ipv4_lat_first: if route.cost_latency_first.unwrap_or_default() == 1 { + "DIRECT".to_string() + } else { + next_hop_pair_latency_first .map(|pair| pair.route.clone().unwrap_or_default().ipv4_addr) .unwrap_or_default() .map(|ip| ip.to_string()) - .unwrap_or_default(), - next_hop_hostname_lat_first: next_hop_pair_latency_first + .unwrap_or_default() + }, + next_hop_hostname_lat_first: if route.cost_latency_first.unwrap_or_default() == 1 { + "DIRECT".to_string() + } else { + next_hop_pair_latency_first .map(|pair| pair.route.clone().unwrap_or_default().hostname) .unwrap_or_default() - .clone(), - path_latency_lat_first: next_hop_pair_latency_first - .map(|pair| { - pair.route - .clone() - .unwrap_or_default() - .path_latency_latency_first - .unwrap_or_default() - }) - .unwrap_or_default(), - path_len_lat_first: next_hop_pair_latency_first - .map(|pair| { - pair.route - .clone() - .unwrap_or_default() - .cost_latency_first - .unwrap_or_default() - }) - .unwrap_or_default(), - - version: if route.version.is_empty() { - "unknown".to_string() - } else { - route.version.to_string() - }, - }); - } + .clone() + }, + path_latency_lat_first: route.path_latency_latency_first.unwrap_or_default(), + path_len_lat_first: route.cost_latency_first.unwrap_or_default(), + + version: if route.version.is_empty() { + "unknown".to_string() + } else { + route.version.to_string() + }, + }); } print_output(&items, self.output_format)?; @@ -747,7 +695,10 @@ impl CommandHandler<'_> { .list_mapped_listener(BaseController::default(), request) .await?; if self.verbose || *self.output_format == OutputFormat::Json { - println!("{}", serde_json::to_string_pretty(&response.mappedlisteners)?); + println!( + "{}", + serde_json::to_string_pretty(&response.mappedlisteners)? + ); return Ok(()); } println!("response: {:#?}", response); @@ -759,7 +710,7 @@ impl CommandHandler<'_> { let client = self.get_mapped_listener_manager_client().await?; let request = ManageMappedListenerRequest { action: MappedListenerManageAction::MappedListenerAdd as i32, - url: Some(url.into()) + url: Some(url.into()), }; let _response = client .manage_mapped_listener(BaseController::default(), request) @@ -772,7 +723,7 @@ impl CommandHandler<'_> { let client = self.get_mapped_listener_manager_client().await?; let request = ManageMappedListenerRequest { action: MappedListenerManageAction::MappedListenerRemove as i32, - url: Some(url.into()) + url: Some(url.into()), }; let _response = client .manage_mapped_listener(BaseController::default(), request) @@ -783,9 +734,11 @@ impl CommandHandler<'_> { fn mapped_listener_validate_url(url: &String) -> Result { let url = url::Url::parse(url)?; if url.scheme() != "tcp" && url.scheme() != "udp" { - return Err(anyhow::anyhow!("Url ({url}) must start with tcp:// or udp://")) + return Err(anyhow::anyhow!( + "Url ({url}) must start with tcp:// or udp://" + )); } else if url.port().is_none() { - return Err(anyhow::anyhow!("Url ({url}) is missing port num")) + return Err(anyhow::anyhow!("Url ({url}) is missing port num")); } Ok(url) } @@ -1079,7 +1032,7 @@ async fn main() -> Result<(), Error> { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); rust_i18n::set_locale(&locale); let cli = Cli::parse(); - + let client = RpcClient::new(TcpTunnelConnector::new( format!("tcp://{}:{}", cli.rpc_portal.ip(), cli.rpc_portal.port()) .parse() @@ -1126,19 +1079,21 @@ async fn main() -> Result<(), Error> { handler.handle_connector_list().await?; } }, - SubCommand::MappedListener(mapped_listener_args) => match mapped_listener_args.sub_command { - Some(MappedListenerSubCommand::Add { url }) => { - handler.handle_mapped_listener_add(&url).await?; - println!("add mapped listener: {url}"); - } - Some(MappedListenerSubCommand::Remove { url }) => { - handler.handle_mapped_listener_remove(&url).await?; - println!("remove mapped listener: {url}"); - } - Some(MappedListenerSubCommand::List) | None => { - handler.handle_mapped_listener_list().await?; + SubCommand::MappedListener(mapped_listener_args) => { + match mapped_listener_args.sub_command { + Some(MappedListenerSubCommand::Add { url }) => { + handler.handle_mapped_listener_add(&url).await?; + println!("add mapped listener: {url}"); + } + Some(MappedListenerSubCommand::Remove { url }) => { + handler.handle_mapped_listener_remove(&url).await?; + println!("remove mapped listener: {url}"); + } + Some(MappedListenerSubCommand::List) | None => { + handler.handle_mapped_listener_list().await?; + } } - }, + } SubCommand::Route(route_args) => match route_args.sub_command { Some(RouteSubCommand::List) | None => handler.handle_route_list().await?, Some(RouteSubCommand::Dump) => handler.handle_route_dump().await?, @@ -1173,10 +1128,53 @@ async fn main() -> Result<(), Error> { GetGlobalPeerMapRequest::default(), ) .await?; + let route_infos = handler.list_peer_route_pair().await?; + struct PeerCenterNodeInfo { + hostname: String, + ipv4: String, + } + let node_id_to_node_info = DashMap::new(); + let node_info = handler + .get_peer_manager_client() + .await? + .show_node_info(BaseController::default(), ShowNodeInfoRequest::default()) + .await? + .node_info + .ok_or(anyhow::anyhow!("node info not found"))?; + node_id_to_node_info.insert( + node_info.peer_id, + PeerCenterNodeInfo { + hostname: node_info.hostname.clone(), + ipv4: node_info.ipv4_addr.clone(), + }, + ); + for route_info in route_infos { + let Some(peer_id) = route_info.route.as_ref().map(|x| x.peer_id) else { + continue; + }; + node_id_to_node_info.insert( + peer_id, + PeerCenterNodeInfo { + hostname: route_info + .route + .as_ref() + .map(|x| x.hostname.clone()) + .unwrap_or_default(), + ipv4: route_info + .route + .as_ref() + .and_then(|x| x.ipv4_addr) + .map(|x| x.to_string()) + .unwrap_or_default(), + }, + ); + } #[derive(tabled::Tabled, serde::Serialize)] struct PeerCenterTableItem { node_id: String, + hostname: String, + ipv4: String, #[tabled(rename = "direct_peers")] #[serde(skip_serializing)] direct_peers_str: String, @@ -1187,27 +1185,50 @@ async fn main() -> Result<(), Error> { #[derive(serde::Serialize)] struct DirectPeerItem { node_id: String, + hostname: String, + ipv4: String, latency_ms: i32, } let mut table_rows = vec![]; for (k, v) in resp.global_peer_map.iter() { let node_id = k; - let direct_peers_strs = v - .direct_peers - .iter() - .map(|(k, v)| format!("{}: {:?}ms", k, v.latency_ms,)) - .collect::>(); let direct_peers: Vec<_> = v .direct_peers .iter() .map(|(k, v)| DirectPeerItem { node_id: k.to_string(), + hostname: node_id_to_node_info + .get(k) + .map(|x| x.hostname.clone()) + .unwrap_or_default(), + ipv4: node_id_to_node_info + .get(k) + .map(|x| x.ipv4.clone()) + .unwrap_or_default(), latency_ms: v.latency_ms, }) .collect(); + let direct_peers_strs = direct_peers + .iter() + .map(|x| { + format!( + "{}({}[{}]): {}ms", + x.node_id, x.hostname, x.ipv4, x.latency_ms, + ) + }) + .collect::>(); + table_rows.push(PeerCenterTableItem { node_id: node_id.to_string(), + hostname: node_id_to_node_info + .get(node_id) + .map(|x| x.hostname.clone()) + .unwrap_or_default(), + ipv4: node_id_to_node_info + .get(node_id) + .map(|x| x.ipv4.clone()) + .unwrap_or_default(), direct_peers_str: direct_peers_strs.join("\n"), direct_peers, }); diff --git a/easytier/src/easytier_core.rs b/easytier/src/easytier_core.rs index 1070502bb..8bfb7fef3 100644 --- a/easytier/src/easytier_core.rs +++ b/easytier/src/easytier_core.rs @@ -1,37 +1,25 @@ #![allow(dead_code)] +use rust_i18n::t; use std::{ - net::{Ipv4Addr, SocketAddr}, - path::PathBuf, - process::ExitCode, - sync::Arc, + net::{Ipv4Addr, SocketAddr}, path::PathBuf, process::ExitCode, sync::Arc }; use anyhow::Context; use cidr::IpCidr; use clap::{CommandFactory, Parser}; -use crate::instance_manager::NetworkInstanceManager; - -use crate::{ - common::{ - config::{ - ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, - NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, - }, - constants::EASYTIER_VERSION, - global_ctx::GlobalCtx, - set_default_machine_id, - stun::MockStunInfoCollector, - }, - connector::create_connector_by_url, - launcher::{add_proxy_network_to_config, ConfigSource}, - print_completions, - proto::common::{CompressionAlgoPb, NatType}, - tunnel::{IpVersion, PROTO_PORT_OFFSET}, - utils::{init_logger, setup_panic_handler}, - web_client, -}; + use clap_complete::Shell; +use crate::{common::{ + config::{ + ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, + NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, + }, + constants::EASYTIER_VERSION, + global_ctx::GlobalCtx, + set_default_machine_id, + stun::MockStunInfoCollector, +}, connector::create_connector_by_url, instance_manager::NetworkInstanceManager, launcher::{add_proxy_network_to_config, ConfigSource}, print_completions, proto::common::{CompressionAlgoPb, NatType}, tunnel::{IpVersion, PROTO_PORT_OFFSET}, utils::{init_logger, setup_panic_handler}, web_client}; #[cfg(target_os = "windows")] windows_service::define_windows_service!(ffi_service_main, win_service_main); @@ -773,8 +761,8 @@ impl NetworkOptions { port_forward.host_str().expect("local bind host is missing"), port_forward.port().expect("local bind port is missing") ) - .parse() - .expect(format!("failed to parse local bind addr {}", example_str).as_str()); + .parse() + .expect(format!("failed to parse local bind addr {}", example_str).as_str()); let dst_addr = format!( "{}", @@ -784,8 +772,8 @@ impl NetworkOptions { .next() .expect(format!("remote destination addr is missing {}", example_str).as_str()) ) - .parse() - .expect(format!("failed to parse remote destination addr {}", example_str).as_str()); + .parse() + .expect(format!("failed to parse remote destination addr {}", example_str).as_str()); let port_forward_item = PortForwardConfig { bind_addr, @@ -812,6 +800,7 @@ impl NetworkOptions { if let Some(dev_name) = &self.dev_name { f.dev_name = dev_name.clone() } + println!("mtu: {}, {:?}", f.mtu, self.mtu); if let Some(mtu) = self.mtu { f.mtu = mtu as u32; } @@ -839,7 +828,7 @@ impl NetworkOptions { compression ), } - .into(); + .into(); } f.bind_device = self.bind_device.unwrap_or(f.bind_device); f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); @@ -1001,8 +990,8 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { "udp://config-server.easytier.cn:22020/{}", config_server_url_s ) - .parse() - .unwrap(), + .parse() + .unwrap(), }; let mut c_url = config_server_url.clone(); @@ -1185,6 +1174,7 @@ pub(crate) async fn main() -> ExitCode { ExitCode::from(ret_code) } + // remember to comment code : init_logger(&cli.logging_options, false)?; pub(crate) async fn run(path: &str) -> u8 { let cli = Cli::parse_from(["app", &format!("-c{}", path)]); @@ -1195,4 +1185,4 @@ pub(crate) async fn run(path: &str) -> u8 { } ret_code -} +} \ No newline at end of file diff --git a/easytier/src/helper.rs b/easytier/src/helper.rs index 7972ead71..de66df3f3 100644 --- a/easytier/src/helper.rs +++ b/easytier/src/helper.rs @@ -156,10 +156,10 @@ pub async fn get_stats() -> *mut u8 { if guard.is_none() { return get_buffer(); } - let pm = guard.as_ref().unwrap().clone(); - let routes = pm.list_routes().await; - let pmrs = PeerManagerRpcService::new(pm); - let peers = pmrs.list_peers().await; + let peer_mgr_c = guard.as_ref().unwrap().clone(); + let routes = peer_mgr_c.list_routes().await; + let pmrs = PeerManagerRpcService::new(peer_mgr_c.clone()); + let peers = PeerManagerRpcService::list_peers(&peer_mgr_c).await;; let peer_routes = list_peer_route_pair(peers, routes); let mut items: Vec = vec![]; let res = pmrs diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index 898380bea..cb5722c45 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -248,6 +248,23 @@ pub struct VirtualNic { ifcfg: Box, } +impl Drop for VirtualNic { + fn drop(&mut self) { + #[cfg(target_os = "windows")] + { + if let Some(ref ifname) = self.ifname { + // Try to clean up firewall rules, but don't panic in destructor + if let Err(e) = crate::arch::windows::remove_interface_firewall_rules(ifname) { + eprintln!( + "Warning: Failed to remove firewall rules for interface {}: {}", + ifname, e + ); + } + } + } + } +} + impl VirtualNic { pub fn new(global_ctx: ArcGlobalCtx) -> Self { Self { @@ -257,12 +274,113 @@ impl VirtualNic { } } + /// Check and create TUN device node if necessary on Linux systems + #[cfg(target_os = "linux")] + async fn ensure_tun_device_node() { + const TUN_DEV_PATH: &str = "/dev/net/tun"; + const TUN_DIR_PATH: &str = "/dev/net"; + + // Check if /dev/net/tun already exists + if tokio::fs::metadata(TUN_DEV_PATH).await.is_ok() { + tracing::debug!("TUN device node {} already exists", TUN_DEV_PATH); + return; + } + + tracing::info!( + "TUN device node {} not found, attempting to create", + TUN_DEV_PATH + ); + + // Check if TUN kernel module is available + let tun_module_available = tokio::fs::metadata("/proc/net/dev").await.is_ok() + && (tokio::fs::read_to_string("/proc/modules").await) + .map(|content| content.contains("tun")) + .unwrap_or(false); + + if !tun_module_available { + tracing::warn!("TUN kernel module may not be loaded"); + println!("⚠ Warning: TUN kernel module may not be available."); + println!(" You may need to load it with: sudo modprobe tun"); + } + + // Try to create /dev/net directory if it doesn't exist + if tokio::fs::metadata(TUN_DIR_PATH).await.is_err() { + if let Err(e) = tokio::fs::create_dir_all(TUN_DIR_PATH).await { + tracing::warn!( + "Failed to create directory {}: {}. Continuing anyway.", + TUN_DIR_PATH, + e + ); + println!( + "⚠ Warning: Failed to create directory {}. TUN device creation may fail.", + TUN_DIR_PATH + ); + println!( + " You may need to run with root privileges or manually create the TUN device." + ); + Self::print_troubleshooting_info(); + return; + } + tracing::info!("Created directory {}", TUN_DIR_PATH); + } + + // Try to create the TUN device node + // Major number 10, minor number 200 for /dev/net/tun + let dev_node = nix::sys::stat::makedev(10, 200); + + match nix::sys::stat::mknod( + TUN_DEV_PATH, + nix::sys::stat::SFlag::S_IFCHR, + nix::sys::stat::Mode::from_bits(0o600).unwrap(), + dev_node, + ) { + Ok(_) => { + tracing::info!("Successfully created TUN device node {}", TUN_DEV_PATH); + println!("✓ Created TUN device node {}", TUN_DEV_PATH); + } + Err(e) => { + tracing::warn!( + "Failed to create TUN device node {}: {}. Continuing anyway.", + TUN_DEV_PATH, + e + ); + println!( + "⚠ Warning: Failed to create TUN device node {}.", + TUN_DEV_PATH + ); + println!(" Error: {}", e); + Self::print_troubleshooting_info(); + } + } + } + + /// Print troubleshooting information for TUN device issues + #[cfg(target_os = "linux")] + fn print_troubleshooting_info() { + println!(" Possible solutions:"); + println!(" 1. Run with root privileges: sudo ./easytier-core [options]"); + println!(" 2. Manually create TUN device: sudo mkdir -p /dev/net && sudo mknod /dev/net/tun c 10 200"); + println!(" 3. Load TUN kernel module: sudo modprobe tun"); + println!(" 4. Use --no-tun flag if TUN functionality is not needed"); + println!(" 5. Check if your system/container supports TUN devices"); + println!(" Note: TUN functionality may still work if the kernel supports dynamic device creation."); + } + + /// For non-Linux systems, this is a no-op + #[cfg(not(target_os = "linux"))] + async fn ensure_tun_device_node() -> Result<(), Error> { + Ok(()) + } + async fn create_tun(&mut self) -> Result { let mut config = Configuration::default(); config.layer(Layer::L3); #[cfg(target_os = "linux")] { + // Check and create TUN device node if necessary (Linux only) + Self::ensure_tun_device_node().await; + let dev_name = self.global_ctx.get_flags().dev_name; if !dev_name.is_empty() { config.tun_name(format!("{}", dev_name)); @@ -414,6 +532,38 @@ impl VirtualNic { ); self.ifname = Some(ifname.to_owned()); + + #[cfg(target_os = "windows")] + { + // Add firewall rules for virtual NIC interface to allow all traffic + match crate::arch::windows::add_interface_to_firewall_allowlist(&ifname) { + Ok(_) => { + tracing::info!( + "Successfully configured Windows Firewall for interface: {}", + ifname + ); + tracing::info!( + "All protocols (TCP/UDP/ICMP) are now allowed on interface: {}", + ifname + ); + } + Err(e) => { + tracing::warn!("Failed to configure Windows Firewall for {}: {}", ifname, e); + println!( + "⚠ Warning: Failed to configure Windows Firewall for interface {}.", + ifname + ); + println!(" This may cause connectivity issues with ping and other network functions."); + println!( + " Please run as Administrator or manually configure Windows Firewall." + ); + println!( + " Alternatively, you can disable Windows Firewall for testing purposes." + ); + } + } + } + Ok(Box::new(ft)) } @@ -582,7 +732,7 @@ impl NicCtx { if payload.is_empty() { return; } - + match payload[0] >> 4 { 4 => Self::do_forward_nic_to_peers_ipv4(ret, mgr).await, 6 => Self::do_forward_nic_to_peers_ipv6(ret, mgr).await, @@ -721,7 +871,11 @@ impl NicCtx { Ok(()) } - pub async fn run(&mut self, ipv4_addr: Option, ipv6_addr: Option) -> Result<(), Error> { + pub async fn run( + &mut self, + ipv4_addr: Option, + ipv6_addr: Option, + ) -> Result<(), Error> { let tunnel = { let mut nic = self.nic.lock().await; match nic.create_dev().await { @@ -762,12 +916,12 @@ impl NicCtx { if let Some(ipv4_addr) = ipv4_addr { self.assign_ipv4_to_tun_device(ipv4_addr).await?; } - + // Assign IPv6 address if provided if let Some(ipv6_addr) = ipv6_addr { self.assign_ipv6_to_tun_device(ipv6_addr).await?; } - + self.run_proxy_cidrs_route_updater().await?; Ok(()) diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index bfa3d4a8a..fd3c7330a 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -13,7 +13,7 @@ use crate::{ }; pub struct NetworkInstanceManager { - pub instance_map: Arc>, + instance_map: Arc>, instance_stop_tasks: Arc>>, stop_check_notifier: Arc, } diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 15682e278..b7616e944 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -21,6 +21,7 @@ use anyhow::Context; use chrono::{DateTime, Local}; use tokio::{sync::broadcast, task::JoinSet}; use crate::helper::g_peermanager; +use crate::proto::cli::PeerManageRpc; pub type MyNodeInfo = crate::proto::web::MyNodeInfo; @@ -198,9 +199,7 @@ impl EasyTierLauncher { *data_c.my_node_info.write().unwrap() = node_info.clone(); *data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await; *data_c.peers.write().unwrap() = - PeerManagerRpcService::new(peer_mgr_c.clone()) - .list_peers() - .await; + PeerManagerRpcService::list_peers(&peer_mgr_c).await; tokio::time::sleep(std::time::Duration::from_secs(1)).await; } }); diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index 7585f4686..9c089a745 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -4,8 +4,6 @@ use std::io; use clap::Command; use clap_complete::Generator; -#[macro_use] -extern crate rust_i18n; mod arch; mod easytier_core; @@ -37,7 +35,7 @@ use std::time::Duration; pub const VERSION: &str = common::constants::EASYTIER_VERSION; rust_i18n::i18n!("locales", fallback = "en"); -pub fn print_completions(generator: G, cmd: &mut Command, bin_name: &str) { +pub fn print_completions(generator: G, cmd: &mut Command, bin_name:&str) { clap_complete::generate(generator, cmd, bin_name, &mut io::stdout()); } diff --git a/easytier/src/peer_center/instance.rs b/easytier/src/peer_center/instance.rs index d446024cc..d0a49a915 100644 --- a/easytier/src/peer_center/instance.rs +++ b/easytier/src/peer_center/instance.rs @@ -12,17 +12,19 @@ use tokio::task::JoinSet; use tracing::Instrument; use crate::{ - common::PeerId, + common::{global_ctx::GlobalCtx, PeerId}, peers::{ peer_manager::PeerManager, + peer_map::PeerMap, + peer_rpc::PeerRpcManager, route_trait::{RouteCostCalculator, RouteCostCalculatorInterface}, rpc_service::PeerManagerRpcService, }, proto::{ peer_rpc::{ - GetGlobalPeerMapRequest, GetGlobalPeerMapResponse, GlobalPeerMap, PeerCenterRpc, - PeerCenterRpcClientFactory, PeerCenterRpcServer, PeerInfoForGlobalMap, - ReportPeersRequest, ReportPeersResponse, + DirectConnectedPeerInfo, GetGlobalPeerMapRequest, GetGlobalPeerMapResponse, + GlobalPeerMap, PeerCenterRpc, PeerCenterRpcClientFactory, PeerCenterRpcServer, + PeerInfoForGlobalMap, ReportPeersRequest, ReportPeersResponse, }, rpc_types::{self, controller::BaseController}, }, @@ -30,8 +32,18 @@ use crate::{ use super::{server::PeerCenterServer, Digest, Error}; +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait PeerCenterPeerManagerTrait: Send + Sync + 'static { + async fn list_peers(&self) -> PeerInfoForGlobalMap; + fn my_peer_id(&self) -> PeerId; + fn get_global_ctx(&self) -> Arc; + fn get_rpc_mgr(&self) -> Weak; + async fn list_routes(&self) -> Vec; +} + struct PeerCenterBase { - peer_mgr: Weak, + peer_mgr: Arc, my_peer_id: PeerId, tasks: Mutex>, lock: Arc>, @@ -41,7 +53,7 @@ struct PeerCenterBase { static SERVICE_ID: u32 = 50; struct PeridicJobCtx { - peer_mgr: Weak, + peer_mgr: Arc, my_peer_id: PeerId, center_peer: AtomicCell, job_ctx: T, @@ -49,22 +61,17 @@ struct PeridicJobCtx { impl PeerCenterBase { pub async fn init(&self) -> Result<(), Error> { - let Some(peer_mgr) = self.peer_mgr.upgrade() else { + let Some(rpc_mgr) = self.peer_mgr.get_rpc_mgr().upgrade() else { return Err(Error::Shutdown); }; - - peer_mgr - .get_peer_rpc_mgr() - .rpc_server() - .registry() - .register( - PeerCenterRpcServer::new(PeerCenterServer::new(peer_mgr.my_peer_id())), - &peer_mgr.get_global_ctx().get_network_name(), - ); + rpc_mgr.rpc_server().registry().register( + PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())), + &self.peer_mgr.get_global_ctx().get_network_name(), + ); Ok(()) } - async fn select_center_peer(peer_mgr: &Arc) -> Option { + async fn select_center_peer(peer_mgr: &dyn PeerCenterPeerManagerTrait) -> Option { let peers = peer_mgr.list_routes().await; if peers.is_empty() { return None; @@ -109,19 +116,18 @@ impl PeerCenterBase { job_ctx, }); loop { - let Some(peer_mgr) = peer_mgr.upgrade() else { - tracing::error!("peer manager is shutdown, exit periodic job"); - return; - }; - let Some(center_peer) = Self::select_center_peer(&peer_mgr).await else { tracing::trace!("no center peer found, sleep 1 second"); tokio::time::sleep(Duration::from_secs(1)).await; continue; }; + let Some(rpc_mgr) = peer_mgr.get_rpc_mgr().upgrade() else { + tracing::error!("rpc manager is shutdown, exit periodic job"); + return; + }; + ctx.center_peer.store(center_peer.clone()); tracing::trace!(?center_peer, "run periodic job"); - let rpc_mgr = peer_mgr.get_peer_rpc_mgr(); let _g = lock.lock().await; let stub = rpc_mgr .rpc_client() @@ -148,10 +154,11 @@ impl PeerCenterBase { ); } - pub fn new(peer_mgr: Arc) -> Self { + pub fn new(peer_mgr: Arc) -> Self { + let my_peer_id = peer_mgr.my_peer_id(); PeerCenterBase { - peer_mgr: Arc::downgrade(&peer_mgr), - my_peer_id: peer_mgr.my_peer_id(), + peer_mgr, + my_peer_id, tasks: Mutex::new(JoinSet::new()), lock: Arc::new(Mutex::new(())), } @@ -190,7 +197,7 @@ impl PeerCenterRpc for PeerCenterInstanceService { } pub struct PeerCenterInstance { - peer_mgr: Arc, + peer_mgr: Arc, client: Arc, global_peer_map: Arc>, @@ -199,7 +206,7 @@ pub struct PeerCenterInstance { } impl PeerCenterInstance { - pub fn new(peer_mgr: Arc) -> Self { + pub fn new(peer_mgr: Arc) -> Self { PeerCenterInstance { peer_mgr: peer_mgr.clone(), client: Arc::new(PeerCenterBase::new(peer_mgr.clone())), @@ -286,15 +293,14 @@ impl PeerCenterInstance { async fn init_report_peers_job(&self) { struct Ctx { - service: PeerManagerRpcService, - + peer_mgr: Arc, last_report_peers: Mutex>, last_center_peer: AtomicCell, last_report_time: AtomicCell, } let ctx = Arc::new(Ctx { - service: PeerManagerRpcService::new(self.peer_mgr.clone()), + peer_mgr: self.peer_mgr.clone(), last_report_peers: Mutex::new(BTreeSet::new()), last_center_peer: AtomicCell::new(PeerId::default()), last_report_time: AtomicCell::new(Instant::now()), @@ -303,7 +309,7 @@ impl PeerCenterInstance { self.client .init_periodic_job(ctx, |client, ctx| async move { let my_node_id = ctx.my_peer_id; - let peers: PeerInfoForGlobalMap = ctx.job_ctx.service.list_peers().await.into(); + let peers = ctx.job_ctx.peer_mgr.list_peers().await; let peer_list = peers.direct_peers.keys().map(|k| *k).collect(); let job_ctx = &ctx.job_ctx; @@ -373,7 +379,7 @@ impl PeerCenterInstance { if let Some(cost) = self.directed_cost(src, dst) { return cost; } - self.directed_cost(dst, src).unwrap_or(100) + self.directed_cost(dst, src).unwrap_or(500) } fn begin_update(&mut self) { @@ -402,6 +408,81 @@ impl PeerCenterInstance { } } +#[async_trait::async_trait] +impl PeerCenterPeerManagerTrait for PeerManager { + async fn list_peers(&self) -> PeerInfoForGlobalMap { + PeerManagerRpcService::list_peers(self).await.into() + } + + fn my_peer_id(&self) -> PeerId { + self.get_peer_map().my_peer_id() + } + + fn get_global_ctx(&self) -> Arc { + self.get_peer_map().get_global_ctx() + } + + fn get_rpc_mgr(&self) -> Weak { + Arc::downgrade(&self.get_peer_rpc_mgr()) + } + + async fn list_routes(&self) -> Vec { + self.list_routes().await + } +} + +pub struct PeerMapWithPeerRpcManager { + pub peer_map: Arc, + pub rpc_mgr: Arc, +} + +#[async_trait::async_trait] +impl PeerCenterPeerManagerTrait for PeerMapWithPeerRpcManager { + async fn list_peers(&self) -> PeerInfoForGlobalMap { + // TODO: currently latency between public server cannot be calculated because one public-server pair + // has no connection between them. (hard to get latency from peer manager because it's hard to transfrom the peer id) + // but it's fine because we don't want to too much traffic between public servers. + let peers = self.peer_map.list_peers().await; + let mut ret = PeerInfoForGlobalMap::default(); + for peer in peers { + if let Some(conns) = self.peer_map.list_peer_conns(peer).await { + let Some(min_lat) = conns + .iter() + .map(|conn| conn.stats.as_ref().unwrap().latency_us) + .min() + else { + continue; + }; + + ret.direct_peers.insert( + peer, + DirectConnectedPeerInfo { + latency_ms: std::cmp::max(1, (min_lat as u32 / 1000) as i32), + }, + ); + } + } + + ret + } + + fn my_peer_id(&self) -> PeerId { + self.peer_map.my_peer_id() + } + + fn get_global_ctx(&self) -> Arc { + self.peer_map.get_global_ctx() + } + + fn get_rpc_mgr(&self) -> Weak { + Arc::downgrade(&self.rpc_mgr) + } + + async fn list_routes(&self) -> Vec { + self.peer_map.list_route_infos().await + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index 4cc4fd975..df69e36e0 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -29,6 +29,7 @@ use crate::{ token_bucket::TokenBucket, PeerId, }, + peer_center::instance::{PeerCenterInstance, PeerMapWithPeerRpcManager}, peers::route_trait::{Route, RouteInterface}, proto::{ cli::{ForeignNetworkEntryPb, ListForeignNetworkResponse, PeerInfo}, @@ -73,6 +74,8 @@ struct ForeignNetworkEntry { bps_limiter: Arc, + peer_center: Arc, + tasks: Mutex>, pub lock: Mutex<()>, @@ -116,6 +119,13 @@ impl ForeignNetworkEntry { .token_bucket_manager() .get_or_create(&network.network_name, limiter_config.into()); + let peer_center = Arc::new(PeerCenterInstance::new(Arc::new( + PeerMapWithPeerRpcManager { + peer_map: peer_map.clone(), + rpc_mgr: peer_rpc.clone(), + }, + ))); + Self { my_peer_id, @@ -134,6 +144,8 @@ impl ForeignNetworkEntry { tasks: Mutex::new(JoinSet::new()), + peer_center, + lock: Mutex::new(()), } } @@ -270,6 +282,10 @@ impl ForeignNetworkEntry { .await .unwrap(); + route + .set_route_cost_fn(self.peer_center.get_cost_calculator()) + .await; + self.peer_map.add_route(Arc::new(Box::new(route))).await; } @@ -351,6 +367,7 @@ impl ForeignNetworkEntry { self.prepare_route(accessor).await; self.start_packet_recv().await; self.peer_rpc.run(); + self.peer_center.init().await; } } diff --git a/easytier/src/peers/peer.rs b/easytier/src/peers/peer.rs index 3ef8884e1..dbdd3b8eb 100644 --- a/easytier/src/peers/peer.rs +++ b/easytier/src/peers/peer.rs @@ -201,14 +201,17 @@ impl Peer { } pub fn has_directly_connected_conn(&self) -> bool { - self.conns.iter().any(|entry|!(entry.value()).is_hole_punched()) + self.conns + .iter() + .any(|entry| !(entry.value()).is_hole_punched()) } pub fn get_directly_connections(&self) -> DashSet { - self.conns.iter() - .filter(|entry| !(entry.value()).is_hole_punched()) - .map(|entry|(entry.value()).get_conn_id()) - .collect() + self.conns + .iter() + .filter(|entry| !(entry.value()).is_hole_punched()) + .map(|entry| (entry.value()).get_conn_id()) + .collect() } pub fn get_default_conn_id(&self) -> PeerConnId { diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 0062761d0..91cabf997 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -8,7 +8,7 @@ use std::{ use anyhow::Context; use async_trait::async_trait; -use dashmap::{DashMap, DashSet}; +use dashmap::DashMap; use tokio::{ sync::{ @@ -1184,14 +1184,6 @@ impl PeerManager { } } - pub fn get_directly_connections(&self, peer_id: PeerId) -> DashSet { - if let Some(peer) = self.peers.get_peer_by_id(peer_id) { - return peer.get_directly_connections(); - } - - DashSet::new() - } - pub async fn clear_resources(&self) { let mut peer_pipeline = self.peer_packet_process_pipeline.write().await; peer_pipeline.clear(); diff --git a/easytier/src/peers/peer_map.rs b/easytier/src/peers/peer_map.rs index 9bd37bcad..eef23fd9f 100644 --- a/easytier/src/peers/peer_map.rs +++ b/easytier/src/peers/peer_map.rs @@ -1,7 +1,10 @@ -use std::{net::{Ipv4Addr, Ipv6Addr}, sync::Arc}; +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + sync::Arc, +}; use anyhow::Context; -use dashmap::DashMap; +use dashmap::{DashMap, DashSet}; use tokio::sync::RwLock; use crate::{ @@ -10,7 +13,10 @@ use crate::{ global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity}, PeerId, }, - proto::{cli::PeerConnInfo, peer_rpc::RoutePeerInfo}, + proto::{ + cli::{self, PeerConnInfo}, + peer_rpc::RoutePeerInfo, + }, tunnel::{packet_def::ZCPacket, TunnelError}, }; @@ -91,6 +97,14 @@ impl PeerMap { self.peer_map.get(&peer_id).map(|v| v.clone()) } + pub fn get_directly_connections_by_peer_id(&self, peer_id: PeerId) -> DashSet { + if let Some(peer) = self.get_peer_by_id(peer_id) { + return peer.get_directly_connections(); + } + + DashSet::new() + } + pub fn has_peer(&self, peer_id: PeerId) -> bool { peer_id == self.my_peer_id || self.peer_map.contains_key(&peer_id) } @@ -324,6 +338,13 @@ impl PeerMap { route_map } + pub async fn list_route_infos(&self) -> Vec { + for route in self.routes.read().await.iter() { + return route.list_routes().await; + } + vec![] + } + pub async fn need_relay_by_foreign_network(&self, dst_peer_id: PeerId) -> Result { // if gateway_peer_id is not connected to me, means need relay by foreign network let gateway_id = self @@ -343,6 +364,14 @@ impl PeerMap { .map(|v| (v.key().clone(), v.value().clone())) .collect() } + + pub fn my_peer_id(&self) -> PeerId { + self.my_peer_id + } + + pub fn get_global_ctx(&self) -> ArcGlobalCtx { + self.global_ctx.clone() + } } impl Drop for PeerMap { diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index d1d99f7a7..5c0a1d679 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -2264,7 +2264,7 @@ impl Route for PeerRoute { route.next_hop_peer_id_latency_first = next_hop_peer_latency_first.map(|x| x.next_hop_peer_id); - route.cost_latency_first = next_hop_peer_latency_first.map(|x| x.path_latency); + route.cost_latency_first = next_hop_peer_latency_first.map(|x| x.path_len as i32); route.path_latency_latency_first = next_hop_peer_latency_first.map(|x| x.path_latency); route.feature_flag = item.feature_flag.clone(); diff --git a/easytier/src/peers/rpc_service.rs b/easytier/src/peers/rpc_service.rs index 14f8f197f..9e588944d 100644 --- a/easytier/src/peers/rpc_service.rs +++ b/easytier/src/peers/rpc_service.rs @@ -22,17 +22,17 @@ impl PeerManagerRpcService { PeerManagerRpcService { peer_manager } } - pub async fn list_peers(&self) -> Vec { - let mut peers = self.peer_manager.get_peer_map().list_peers().await; + pub async fn list_peers(peer_manager: &PeerManager) -> Vec { + let mut peers = peer_manager.get_peer_map().list_peers().await; peers.extend( - self.peer_manager + peer_manager .get_foreign_network_client() .get_peer_map() .list_peers() .await .iter(), ); - let peer_map = self.peer_manager.get_peer_map(); + let peer_map = peer_manager.get_peer_map(); let mut peer_infos = Vec::new(); for peer in peers { let mut peer_info = PeerInfo::default(); @@ -41,17 +41,15 @@ impl PeerManagerRpcService { .get_peer_default_conn_id(peer) .await .map(Into::into); - peer_info.directly_connected_conns = self - .peer_manager - .get_directly_connections(peer) + peer_info.directly_connected_conns = peer_map + .get_directly_connections_by_peer_id(peer) .into_iter() .map(Into::into) .collect(); if let Some(conns) = peer_map.list_peer_conns(peer).await { peer_info.conns = conns; - } else if let Some(conns) = self - .peer_manager + } else if let Some(conns) = peer_manager .get_foreign_network_client() .get_peer_map() .list_peer_conns(peer) @@ -77,7 +75,7 @@ impl PeerManageRpc for PeerManagerRpcService { ) -> Result { let mut reply = ListPeerResponse::default(); - let peers = self.list_peers().await; + let peers = PeerManagerRpcService::list_peers(&self.peer_manager).await; for peer in peers { reply.peer_infos.push(peer); } From 823268bfcec2e4247671f9cc194db2ccfc0a332e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 24 Jul 2025 21:54:45 +0800 Subject: [PATCH 05/10] Merge code from offical repo (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) * reduce memory usage (#1133) Large memory usage comes from: Mimalloc hold large thread cache, causing abort 13M+ usage. QUIC endpoint occupy 3M when GRO is enabled. Smoltcp 64 tcp listener use 2MB. * fix bugs (#1138) 1. avoid dns query hangs the thread 2. avoid deadloop when stun query failed because of no ipv4 addr. 3. make quic input error non-fatal. 4. remove ring tunnel from connection map to avoid mem leak. 5. limit listener retry count. --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen --- .github/origin_wfs/core.yml | 13 ++- .github/origin_wfs/install_rust.sh | 4 +- CONTRIBUTING.md | 6 +- CONTRIBUTING_zh.md | 38 ++++--- Cargo.lock | 82 +++++++------- easytier-rpc-build/Cargo.toml | 2 +- easytier/Cargo.toml | 24 ++-- easytier/src/connector/direct.rs | 12 +- easytier/src/connector/manual.rs | 19 +++- easytier/src/connector/udp_hole_punch/mod.rs | 8 +- easytier/src/easytier-cli.rs | 1 + easytier/src/easytier-core.rs | 8 +- easytier/src/easytier_core.rs | 6 +- easytier/src/gateway/tcp_proxy.rs | 109 ++++++++++--------- easytier/src/helper.rs | 2 +- easytier/src/instance/instance.rs | 26 +++-- easytier/src/instance/listeners.rs | 7 +- easytier/src/launcher.rs | 46 +++++--- easytier/src/tunnel/quic.rs | 65 ++++++++++- easytier/src/tunnel/ring.rs | 29 +++-- 20 files changed, 325 insertions(+), 182 deletions(-) diff --git a/.github/origin_wfs/core.yml b/.github/origin_wfs/core.yml index b200482d6..f111f1df6 100644 --- a/.github/origin_wfs/core.yml +++ b/.github/origin_wfs/core.yml @@ -175,14 +175,17 @@ 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" + 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 +215,8 @@ jobs: rustup set auto-self-update disable - rustup install 1.86 - rustup default 1.86 + rustup install 1.87 + rustup default 1.87 export CC=clang export CXX=clang++ @@ -221,7 +224,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: | diff --git a/.github/origin_wfs/install_rust.sh b/.github/origin_wfs/install_rust.sh index 28de5f12e..c9d339794 100644 --- a/.github/origin_wfs/install_rust.sh +++ b/.github/origin_wfs/install_rust.sh @@ -29,8 +29,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.87 +rustup default 1.87 # mips/mipsel cannot add target from rustup, need compile by ourselves if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86d9471ce..3fb64b89e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides #### Required Tools - Node.js v21 or higher - pnpm v9 or higher -- Rust toolchain (version 1.86) +- Rust toolchain (version 1.87) - LLVM and Clang - Protoc (Protocol Buffers compiler) @@ -79,8 +79,8 @@ sudo apt install -y bridge-utils 2. Install dependencies: ```bash # Install Rust toolchain - rustup install 1.86 - rustup default 1.86 + rustup install 1.87 + rustup default 1.87 # Install project dependencies pnpm -r install diff --git a/CONTRIBUTING_zh.md b/CONTRIBUTING_zh.md index 588fc0de2..c2cbea0ce 100644 --- a/CONTRIBUTING_zh.md +++ b/CONTRIBUTING_zh.md @@ -6,18 +6,26 @@ ## 目录 -- [开发环境配置](#开发环境配置) - - [前置要求](#前置要求) - - [安装步骤](#安装步骤) -- [项目结构](#项目结构) -- [构建指南](#构建指南) - - [构建核心组件](#构建核心组件) - - [构建桌面应用](#构建桌面应用) - - [构建移动应用](#构建移动应用) -- [开发工作流](#开发工作流) -- [测试指南](#测试指南) -- [Pull Request 规范](#pull-request-规范) -- [其他资源](#其他资源) +- [EasyTier 贡献指南](#easytier-贡献指南) + - [目录](#目录) + - [开发环境配置](#开发环境配置) + - [前置要求](#前置要求) + - [必需工具](#必需工具) + - [平台特定依赖](#平台特定依赖) + - [安装步骤](#安装步骤) + - [项目结构](#项目结构) + - [构建指南](#构建指南) + - [构建核心组件](#构建核心组件) + - [构建桌面应用](#构建桌面应用) + - [构建移动应用](#构建移动应用) + - [构建注意事项](#构建注意事项) + - [开发工作流](#开发工作流) + - [测试指南](#测试指南) + - [运行测试](#运行测试) + - [测试要求](#测试要求) + - [Pull Request 规范](#pull-request-规范) + - [其他资源](#其他资源) + - [需要帮助?](#需要帮助) ## 开发环境配置 @@ -26,7 +34,7 @@ #### 必需工具 - Node.js v21 或更高版本 - pnpm v9 或更高版本 -- Rust 工具链(版本 1.86) +- Rust 工具链(版本 1.87) - LLVM 和 Clang - Protoc(Protocol Buffers 编译器) @@ -79,8 +87,8 @@ sudo apt install -y bridge-utils 2. 安装依赖: ```bash # 安装 Rust 工具链 - rustup install 1.86 - rustup default 1.86 + rustup install 1.87 + rustup default 1.87 # 安装项目依赖 pnpm -r install diff --git a/Cargo.lock b/Cargo.lock index 9afe6d904..3f9edcfd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,9 +1992,6 @@ dependencies = [ "http_req", "humansize", "humantime-serde", - "jemalloc-ctl", - "jemalloc-sys", - "jemallocator", "kcp-sys", "lazy_static", "libc", @@ -2043,6 +2040,9 @@ dependencies = [ "tachyonix", "thiserror 1.0.63", "thunk-rs", + "tikv-jemalloc-ctl", + "tikv-jemalloc-sys", + "tikv-jemallocator", "time", "timedmap", "tokio", @@ -3935,37 +3935,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" @@ -4038,7 +4007,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", @@ -4178,9 +4147,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", @@ -4416,9 +4385,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", ] @@ -8520,8 +8489,8 @@ dependencies = [ [[package]] name = "thunk-rs" -version = "0.3.4" -source = "git+https://github.com/easytier/thunk.git#403f0d26d3d5bcfdfd76c23e36e517f19fe891e0" +version = "0.3.5" +source = "git+https://github.com/easytier/thunk.git#cbbeec75a66b7b3cf0824ae890d9d06bcfb9d1f3" [[package]] name = "tiff" @@ -8534,6 +8503,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" diff --git a/easytier-rpc-build/Cargo.toml b/easytier-rpc-build/Cargo.toml index f4ca69324..d433b4608 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.87.0" license-file = "LICENSE" readme = "README.md" diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index ecad0efc4..2e1e42d23 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" authors = ["kkrainbow"] keywords = ["vpn", "p2p", "network", "easytier"] categories = ["network-programming", "command-line-utilities"] -rust-version = "1.84.0" +rust-version = "1.87.0" license-file = "LICENSE" readme = "README.md" @@ -194,7 +194,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", @@ -217,14 +217,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" @@ -259,6 +251,15 @@ windows-sys = { version = "0.52", features = [ ]} winapi = { version = "0.3.9", features = ["impl-default"] } +[target.'cfg(not(windows))'.dependencies] +jemallocator = { package = "tikv-jemallocator", version = "0.6.0", optional = true } +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" globwalk = "0.8.1" @@ -316,4 +317,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/src/connector/direct.rs b/easytier/src/connector/direct.rs index 2c8500485..b967d6cf9 100644 --- a/easytier/src/connector/direct.rs +++ b/easytier/src/connector/direct.rs @@ -12,7 +12,10 @@ use std::{ }; use crate::{ - common::{error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait, PeerId}, + common::{ + dns::socket_addrs, error::Error, global_ctx::ArcGlobalCtx, stun::StunInfoCollectorTrait, + PeerId, + }, peers::{ peer_conn::PeerConnId, peer_manager::PeerManager, @@ -281,14 +284,14 @@ impl DirectConnectorManagerData { } } - fn spawn_direct_connect_task( + async fn spawn_direct_connect_task( self: &Arc, dst_peer_id: PeerId, ip_list: &GetIpListResponse, listener: &url::Url, tasks: &mut JoinSet>, ) { - let Ok(mut addrs) = listener.socket_addrs(|| None) else { + let Ok(mut addrs) = socket_addrs(listener, || None).await else { tracing::error!(?listener, "failed to parse socket address from listener"); return; }; @@ -432,7 +435,8 @@ impl DirectConnectorManagerData { &ip_list, &listener, &mut tasks, - ); + ) + .await; listener_list.push(listener.clone().to_string()); available_listeners.pop(); diff --git a/easytier/src/connector/manual.rs b/easytier/src/connector/manual.rs index 1ece6f48d..2cce8836c 100644 --- a/easytier/src/connector/manual.rs +++ b/easytier/src/connector/manual.rs @@ -15,7 +15,7 @@ use tokio::{ }; use crate::{ - common::{join_joinset_background, PeerId}, + common::{dns::socket_addrs, join_joinset_background, PeerId}, peers::peer_conn::PeerConnId, proto::{ cli::{ @@ -242,7 +242,7 @@ impl ManualConnectorManager { tasks.lock().unwrap().spawn(async move { let reconn_ret = Self::conn_reconnect(data_clone.clone(), dead_url.clone(), connector.clone()).await; - sender.send(reconn_ret).await.unwrap(); + let _ = sender.send(reconn_ret).await; data_clone.reconnecting.remove(&dead_url).unwrap(); data_clone.connectors.insert(dead_url.clone(), connector); @@ -373,7 +373,20 @@ impl ManualConnectorManager { if u.scheme() == "ring" || u.scheme() == "txt" || u.scheme() == "srv" { ip_versions.push(IpVersion::Both); } else { - let addrs = u.socket_addrs(|| Some(1000))?; + let addrs = match socket_addrs(&u, || Some(1000)).await { + Ok(addrs) => addrs, + Err(e) => { + data.global_ctx.issue_event(GlobalCtxEvent::ConnectError( + dead_url.clone(), + format!("{:?}", IpVersion::Both), + format!("{:?}", e), + )); + return Err(Error::AnyhowError(anyhow::anyhow!( + "get ip from url failed: {:?}", + e + ))); + } + }; tracing::info!(?addrs, ?dead_url, "get ip from url done"); let mut has_ipv4 = false; let mut has_ipv6 = false; diff --git a/easytier/src/connector/udp_hole_punch/mod.rs b/easytier/src/connector/udp_hole_punch/mod.rs index 261820a39..1bdaa1548 100644 --- a/easytier/src/connector/udp_hole_punch/mod.rs +++ b/easytier/src/connector/udp_hole_punch/mod.rs @@ -270,7 +270,7 @@ impl UdpHoePunchConnectorData { #[tracing::instrument(skip(self))] async fn cone_to_cone(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { - let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000]); + let mut backoff = BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000]); loop { backoff.sleep_for_next_backoff().await; @@ -293,7 +293,8 @@ impl UdpHoePunchConnectorData { #[tracing::instrument(skip(self))] async fn sym_to_cone(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { - let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); + let mut backoff = + BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); let mut round = 0; let mut port_idx = rand::random(); @@ -338,7 +339,8 @@ impl UdpHoePunchConnectorData { #[tracing::instrument(skip(self))] async fn both_easy_sym(self: Arc, task_info: PunchTaskInfo) -> Result<(), Error> { - let mut backoff = BackOff::new(vec![0, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); + let mut backoff = + BackOff::new(vec![1000, 1000, 2000, 4000, 4000, 8000, 8000, 16000, 64000]); loop { backoff.sleep_for_next_backoff().await; diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index f8b465318..ecc428fb0 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -13,6 +13,7 @@ use anyhow::Context; use cidr::Ipv4Inet; use clap::{command, Args, CommandFactory, Parser, Subcommand}; use clap_complete::Shell; +use dashmap::DashMap; use humansize::format_size; use rust_i18n::t; use service_manager::*; diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 8534b03f6..350e20091 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -39,7 +39,7 @@ use mimalloc::MiMalloc; #[global_allocator] static GLOBAL_MIMALLOC: MiMalloc = MiMalloc; -#[cfg(feature = "jemalloc")] +#[cfg(feature = "jemalloc-prof")] use jemalloc_ctl::{epoch, stats, Access as _, AsName as _}; #[cfg(feature = "jemalloc")] @@ -47,7 +47,7 @@ use jemalloc_ctl::{epoch, stats, Access as _, AsName as _}; static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; fn set_prof_active(_active: bool) { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { const PROF_ACTIVE: &'static [u8] = b"prof.active\0"; let name = PROF_ACTIVE.name(); @@ -56,7 +56,7 @@ fn set_prof_active(_active: bool) { } fn dump_profile(_cur_allocated: usize) { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { const PROF_DUMP: &'static [u8] = b"prof.dump\0"; static mut PROF_DUMP_FILE_NAME: [u8; 128] = [0; 128]; @@ -1091,7 +1091,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { } fn memory_monitor() { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { let mut last_peak_size = 0; let e = epoch::mib().unwrap(); diff --git a/easytier/src/easytier_core.rs b/easytier/src/easytier_core.rs index 8bfb7fef3..82ae232ad 100644 --- a/easytier/src/easytier_core.rs +++ b/easytier/src/easytier_core.rs @@ -31,7 +31,7 @@ use mimalloc::MiMalloc; #[global_allocator] static GLOBAL_MIMALLOC: MiMalloc = MiMalloc; -#[cfg(feature = "jemalloc")] +#[cfg(feature = "jemalloc-prof")] use jemalloc_ctl::{epoch, stats, Access as _, AsName as _}; #[cfg(feature = "jemalloc")] @@ -39,7 +39,7 @@ use jemalloc_ctl::{epoch, stats, Access as _, AsName as _}; static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; fn set_prof_active(_active: bool) { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { const PROF_ACTIVE: &'static [u8] = b"prof.active\0"; let name = PROF_ACTIVE.name(); @@ -48,7 +48,7 @@ fn set_prof_active(_active: bool) { } fn dump_profile(_cur_allocated: usize) { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { const PROF_DUMP: &'static [u8] = b"prof.dump\0"; static mut PROF_DUMP_FILE_NAME: [u8; 128] = [0; 128]; diff --git a/easytier/src/gateway/tcp_proxy.rs b/easytier/src/gateway/tcp_proxy.rs index d56d90945..37299fd82 100644 --- a/easytier/src/gateway/tcp_proxy.rs +++ b/easytier/src/gateway/tcp_proxy.rs @@ -15,7 +15,6 @@ use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; use tokio::io::{copy_bidirectional, AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::net::{TcpListener, TcpSocket, TcpStream}; -use tokio::select; use tokio::sync::{mpsc, Mutex}; use tokio::task::JoinSet; use tokio::time::timeout; @@ -210,67 +209,62 @@ impl ProxyTcpStream { } } +type SmolTcpAcceptResult = Result<(tokio_smoltcp::TcpStream, SocketAddr)>; #[cfg(feature = "smoltcp")] struct SmolTcpListener { - listener_task: JoinSet<()>, - listen_count: usize, + stream_tx: mpsc::UnboundedSender, + stream_rx: mpsc::UnboundedReceiver, - stream_rx: mpsc::UnboundedReceiver>, + tasks: Arc>>, } #[cfg(feature = "smoltcp")] impl SmolTcpListener { - pub async fn new(net: Arc>>, listen_count: usize) -> Self { - let mut tasks = JoinSet::new(); + pub async fn new() -> Self { + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "smoltcp listener".to_owned()); let (tx, rx) = mpsc::unbounded_channel(); - let locked_net = net.lock().await; - for _ in 0..listen_count { - let mut tcp = locked_net - .as_ref() - .unwrap() - .tcp_bind("0.0.0.0:8899".parse().unwrap()) - .await - .unwrap(); - let tx = tx.clone(); - tasks.spawn(async move { - let mut not_listening_count = 0; - loop { - select! { - _ = tokio::time::sleep(Duration::from_secs(2)) => { - if tcp.is_listening() { - not_listening_count = 0; - continue; - } - - not_listening_count += 1; - if not_listening_count >= 2 { - tracing::error!("smol tcp listener not listening"); - tcp.relisten(); - } - } - accept_ret = tcp.accept() => { - tx.send(accept_ret.map_err(|e| { - anyhow::anyhow!("smol tcp listener accept failed: {:?}", e).into() - })) - .unwrap(); - not_listening_count = 0; - } - } - } - }); - } Self { - listener_task: tasks, - listen_count, + stream_tx: tx, stream_rx: rx, + tasks, } } - pub async fn accept(&mut self) -> Result<(tokio_smoltcp::TcpStream, SocketAddr)> { + pub async fn accept(&mut self) -> SmolTcpAcceptResult { self.stream_rx.recv().await.unwrap() } + + pub fn stream_tx(&self) -> mpsc::UnboundedSender { + self.stream_tx.clone() + } + + pub async fn add_listener( + tx: mpsc::UnboundedSender, + net: Arc>>, + tasks: Arc>>, + ) { + let locked_net = net.lock().await; + let mut tcp = locked_net + .as_ref() + .unwrap() + .tcp_bind("0.0.0.0:8899".parse().unwrap()) + .await + .unwrap(); + tasks.lock().unwrap().spawn(async move { + let ret = timeout(Duration::from_secs(10), tcp.accept()).await; + if let Ok(accept_ret) = ret { + tx.send(accept_ret.map_err(|e| { + anyhow::anyhow!("smol tcp listener accept failed: {:?}", e).into() + })) + .unwrap(); + } else { + tracing::error!("smol tcp listener accept timeout"); + } + }); + } } enum ProxyTcpListener { @@ -323,6 +317,7 @@ pub struct TcpProxy { smoltcp_stack_receiver: Arc>>>, #[cfg(feature = "smoltcp")] smoltcp_net: Arc>>, + smoltcp_listener_tx: std::sync::Mutex>>, enable_smoltcp: Arc, connector: C, @@ -332,10 +327,7 @@ pub struct TcpProxy { impl PeerPacketFilter for TcpProxy { async fn try_process_packet_from_peer(&self, mut packet: ZCPacket) -> Option { if let Some(_) = self.try_handle_peer_packet(&mut packet).await { - if self - .enable_smoltcp - .load(std::sync::atomic::Ordering::Relaxed) - { + if self.is_smoltcp_enabled() { let smoltcp_stack_sender = self.smoltcp_stack_sender.as_ref().unwrap(); if let Err(e) = smoltcp_stack_sender.try_send(packet) { tracing::error!("send to smoltcp stack failed: {:?}", e); @@ -455,6 +447,7 @@ impl TcpProxy { #[cfg(feature = "smoltcp")] smoltcp_net: Arc::new(Mutex::new(None)), + smoltcp_listener_tx: std::sync::Mutex::new(None), enable_smoltcp: Arc::new(AtomicBool::new(true)), @@ -584,7 +577,11 @@ impl TcpProxy { ); net.set_any_ip(true); self.smoltcp_net.lock().await.replace(net); - let tcp = SmolTcpListener::new(self.smoltcp_net.clone(), 64).await; + let tcp = SmolTcpListener::new().await; + self.smoltcp_listener_tx + .lock() + .unwrap() + .replace(tcp.stream_tx()); self.enable_smoltcp .store(true, std::sync::atomic::Ordering::Relaxed); @@ -865,6 +862,18 @@ impl TcpProxy { .syn_map .insert(src, Arc::new(NatDstEntry::new(src, real_dst, mapped_dst))); tracing::info!(src = ?src, ?real_dst, ?mapped_dst, old_entry = ?old_val, "tcp syn received"); + + // if smoltcp is enabled, add the listener to the net + if self.is_smoltcp_enabled() { + let smoltcp_listener_tx = self.smoltcp_listener_tx.lock().unwrap().clone().unwrap(); + SmolTcpListener::add_listener( + smoltcp_listener_tx, + self.smoltcp_net.clone(), + self.tasks.clone(), + ) + .await; + tracing::info!("smol tcp listener added for src: {:?}", src); + } } else if !self.addr_conn_map.contains_key(&src) && !self.syn_map.contains_key(&src) { // if not in syn map and addr conn map, may forwarding n2n packet return None; diff --git a/easytier/src/helper.rs b/easytier/src/helper.rs index de66df3f3..543d3ca29 100644 --- a/easytier/src/helper.rs +++ b/easytier/src/helper.rs @@ -159,7 +159,7 @@ pub async fn get_stats() -> *mut u8 { let peer_mgr_c = guard.as_ref().unwrap().clone(); let routes = peer_mgr_c.list_routes().await; let pmrs = PeerManagerRpcService::new(peer_mgr_c.clone()); - let peers = PeerManagerRpcService::list_peers(&peer_mgr_c).await;; + let peers = PeerManagerRpcService::list_peers(&peer_mgr_c).await; let peer_routes = list_peer_route_pair(peers, routes); let mut items: Vec = vec![]; let res = pmrs diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 8cf93c0ea..0741876fd 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -526,6 +526,19 @@ impl Instance { }); } + async fn run_quic_dst(&mut self) -> Result<(), Error> { + if !self.global_ctx.get_flags().enable_quic_proxy { + return Ok(()); + } + + let quic_dst = QUICProxyDst::new(self.global_ctx.clone())?; + quic_dst.start().await?; + self.global_ctx + .set_quic_proxy_port(Some(quic_dst.local_addr()?.port())); + self.quic_proxy_dst = Some(quic_dst); + Ok(()) + } + pub async fn run(&mut self) -> Result<(), Error> { self.listener_manager .lock() @@ -588,11 +601,12 @@ impl Instance { } if !self.global_ctx.get_flags().disable_quic_input { - let quic_dst = QUICProxyDst::new(self.global_ctx.clone())?; - quic_dst.start().await?; - self.global_ctx - .set_quic_proxy_port(Some(quic_dst.local_addr()?.port())); - self.quic_proxy_dst = Some(quic_dst); + if let Err(e) = self.run_quic_dst().await { + eprintln!( + "quic input start failed: {:?} (some platforms may not support)", + e + ); + } } // run after tun device created, so listener can bind to tun device, which may be required by win 10 @@ -901,8 +915,6 @@ impl Instance { if let Some(rpc_server) = self.rpc_server.take() { rpc_server.registry().unregister_all(); }; - let mut guard = crate::tunnel::ring::CONNECTION_MAP.lock(); - guard.await.clear() } } diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index da03f9963..19a36f092 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -179,11 +179,13 @@ impl ListenerManage peer_manager: Weak, global_ctx: ArcGlobalCtx, ) { + let mut err_count = 0; loop { let mut l = (creator)(); let _g = global_ctx.net_ns.guard(); match l.listen().await { Ok(_) => { + err_count = 0; global_ctx.add_running_listener(l.local_url()); global_ctx.issue_event(GlobalCtxEvent::ListenerAdded(l.local_url())); } @@ -193,8 +195,11 @@ impl ListenerManage l.local_url(), format!("error: {:?}, retry listen later...", e), )); + err_count += 1; + if err_count > 5 { + return; + } tokio::time::sleep(std::time::Duration::from_secs(1)).await; - continue; } } loop { diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index b7616e944..50df16992 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -21,7 +21,6 @@ use anyhow::Context; use chrono::{DateTime, Local}; use tokio::{sync::broadcast, task::JoinSet}; use crate::helper::g_peermanager; -use crate::proto::cli::PeerManageRpc; pub type MyNodeInfo = crate::proto::web::MyNodeInfo; @@ -145,23 +144,40 @@ impl EasyTierLauncher { { // Subscribe to global context events - let global_ctx = instance.get_global_ctx(); let data_c = data.clone(); + let global_ctx_c = instance.get_global_ctx(); + let peer_mgr_c = instance.get_peer_manager().clone(); + let vpn_portal = instance.get_vpn_portal_inst(); tasks.spawn(async move { - let mut receiver = global_ctx.subscribe(); loop { - match receiver.recv().await { - Ok(event) => { - Self::handle_easytier_event(event.clone(), &data_c).await; - } - Err(broadcast::error::RecvError::Closed) => { - break; - } - Err(broadcast::error::RecvError::Lagged(_)) => { - // do nothing currently - receiver = receiver.resubscribe(); - } - } + // Update TUN Device Name + *data_c.tun_dev_name.write().unwrap() = + global_ctx_c.get_flags().dev_name.clone(); + + let node_info = MyNodeInfo { + virtual_ipv4: global_ctx_c.get_ipv4().map(|ip| ip.into()), + hostname: global_ctx_c.get_hostname(), + version: EASYTIER_VERSION.to_string(), + ips: Some(global_ctx_c.get_ip_collector().collect_ip_addrs().await), + stun_info: Some(global_ctx_c.get_stun_info_collector().get_stun_info()), + listeners: global_ctx_c + .get_running_listeners() + .into_iter() + .map(Into::into) + .collect(), + vpn_portal_cfg: Some( + vpn_portal + .lock() + .await + .dump_client_config(peer_mgr_c.clone()) + .await, + ), + }; + *data_c.my_node_info.write().unwrap() = node_info.clone(); + *data_c.routes.write().unwrap() = peer_mgr_c.list_routes().await; + *data_c.peers.write().unwrap() = + PeerManagerRpcService::list_peers(&peer_mgr_c).await; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; } }); diff --git a/easytier/src/tunnel/quic.rs b/easytier/src/tunnel/quic.rs index b7fb0ffe4..559f226cd 100644 --- a/easytier/src/tunnel/quic.rs +++ b/easytier/src/tunnel/quic.rs @@ -2,7 +2,9 @@ //! //! Checkout the `README.md` for guidance. -use std::{error::Error, net::SocketAddr, sync::Arc, time::Duration}; +use std::{ + error::Error, io::IoSliceMut, net::SocketAddr, pin::Pin, sync::Arc, task::Poll, time::Duration, +}; use crate::tunnel::{ common::{FramedReader, FramedWriter, TunnelWrapper}, @@ -11,8 +13,8 @@ use crate::tunnel::{ use anyhow::Context; use quinn::{ - congestion::BbrConfig, crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, - ServerConfig, TransportConfig, + congestion::BbrConfig, crypto::rustls::QuicClientConfig, udp::RecvMeta, AsyncUdpSocket, + ClientConfig, Connection, Endpoint, EndpointConfig, ServerConfig, TransportConfig, UdpPoller, }; use super::{ @@ -35,6 +37,48 @@ pub fn configure_client() -> ClientConfig { client_config } +#[derive(Clone, Debug)] +struct NoGroAsyncUdpSocket { + inner: Arc, +} + +impl AsyncUdpSocket for NoGroAsyncUdpSocket { + fn create_io_poller(self: Arc) -> Pin> { + self.inner.clone().create_io_poller() + } + + fn try_send(&self, transmit: &quinn::udp::Transmit) -> std::io::Result<()> { + self.inner.try_send(transmit) + } + + /// Receive UDP datagrams, or register to be woken if receiving may succeed in the future + fn poll_recv( + &self, + cx: &mut std::task::Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [RecvMeta], + ) -> Poll> { + self.inner.poll_recv(cx, bufs, meta) + } + + /// Look up the local IP address and port used by this socket + fn local_addr(&self) -> std::io::Result { + self.inner.local_addr() + } + + fn may_fragment(&self) -> bool { + self.inner.may_fragment() + } + + fn max_transmit_segments(&self) -> usize { + self.inner.max_transmit_segments() + } + + fn max_receive_segments(&self) -> usize { + 1 + } +} + /// Constructs a QUIC endpoint configured to listen for incoming connections on a certain address /// and port. /// @@ -45,7 +89,20 @@ pub fn configure_client() -> ClientConfig { #[allow(unused)] pub fn make_server_endpoint(bind_addr: SocketAddr) -> Result<(Endpoint, Vec), Box> { let (server_config, server_cert) = configure_server()?; - let endpoint = Endpoint::server(server_config, bind_addr)?; + let socket = std::net::UdpSocket::bind(bind_addr)?; + let runtime = quinn::default_runtime() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "no async runtime found"))?; + let mut endpoint_config = EndpointConfig::default(); + endpoint_config.max_udp_payload_size(1200)?; + let socket = NoGroAsyncUdpSocket { + inner: runtime.wrap_udp_socket(socket)?, + }; + let endpoint = Endpoint::new_with_abstract_socket( + endpoint_config, + Some(server_config), + Arc::new(socket), + runtime, + )?; Ok((endpoint, server_cert)) } diff --git a/easytier/src/tunnel/ring.rs b/easytier/src/tunnel/ring.rs index 1e543e9c1..361685d28 100644 --- a/easytier/src/tunnel/ring.rs +++ b/easytier/src/tunnel/ring.rs @@ -12,10 +12,7 @@ use async_trait::async_trait; use futures::{Sink, SinkExt, Stream, StreamExt}; use once_cell::sync::Lazy; -use tokio::sync::{ - mpsc::{UnboundedReceiver, UnboundedSender}, - Mutex, -}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use uuid::Uuid; @@ -184,14 +181,17 @@ pub struct Connection { server: Arc, } -pub(crate) static CONNECTION_MAP: Lazy>>>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); +static CONNECTION_MAP: Lazy< + Arc>>>>, +> = Lazy::new(|| Arc::new(std::sync::Mutex::new(HashMap::new()))); #[derive(Debug)] pub struct RingTunnelListener { listerner_addr: url::Url, conn_sender: UnboundedSender>, conn_receiver: UnboundedReceiver>, + + key_in_conn_map: Option, } impl RingTunnelListener { @@ -201,6 +201,7 @@ impl RingTunnelListener { listerner_addr: key, conn_sender, conn_receiver, + key_in_conn_map: None, } } } @@ -244,10 +245,12 @@ impl RingTunnelListener { impl TunnelListener for RingTunnelListener { async fn listen(&mut self) -> Result<(), TunnelError> { tracing::info!("listen new conn of key: {}", self.listerner_addr); + let addr = self.get_addr().await?; CONNECTION_MAP .lock() - .await - .insert(self.get_addr().await?, self.conn_sender.clone()); + .unwrap() + .insert(addr, self.conn_sender.clone()); + self.key_in_conn_map = Some(addr); Ok(()) } @@ -276,6 +279,14 @@ impl TunnelListener for RingTunnelListener { } } +impl Drop for RingTunnelListener { + fn drop(&mut self) { + if let Some(addr) = self.key_in_conn_map { + CONNECTION_MAP.lock().unwrap().remove(&addr); + } + } +} + pub struct RingTunnelConnector { remote_addr: url::Url, } @@ -297,7 +308,7 @@ impl TunnelConnector for RingTunnelConnector { .await?; let entry = CONNECTION_MAP .lock() - .await + .unwrap() .get(&remote_addr) .unwrap() .clone(); From 6f3db8974d23556fba8d4f258ea9055eabd07efc Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 26 Jul 2025 12:32:46 +0800 Subject: [PATCH 06/10] Merge v2.4.0 codes (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) * reduce memory usage (#1133) Large memory usage comes from: Mimalloc hold large thread cache, causing abort 13M+ usage. QUIC endpoint occupy 3M when GRO is enabled. Smoltcp 64 tcp listener use 2MB. * fix bugs (#1138) 1. avoid dns query hangs the thread 2. avoid deadloop when stun query failed because of no ipv4 addr. 3. make quic input error non-fatal. 4. remove ring tunnel from connection map to avoid mem leak. 5. limit listener retry count. * Implement ACL (#1140) 1. get acl stats ``` ./easytier-cli acl stats AclStats: Global: CacheHits: 4 CacheMaxSize: 10000 CacheSize: 5 DefaultAllows: 3 InboundPacketsAllowed: 2 InboundPacketsTotal: 2 OutboundPacketsAllowed: 7 OutboundPacketsTotal: 7 PacketsAllowed: 9 PacketsTotal: 9 RuleMatches: 2 ConnTrack: [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00] Rules: [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120] ``` 2. use tcp/udp whitelist to block unexpected traffic. `sudo ./easytier-core -d --tcp-whitelist 1000` 3. use complete acl ability with config file: ``` [[acl.acl_v1.chains]] name = "inbound_whitelist" chain_type = 1 description = "Auto-generated inbound whitelist from CLI" enabled = true default_action = 2 [[acl.acl_v1.chains.rules]] name = "tcp_whitelist" description = "Auto-generated TCP whitelist rule" priority = 1000 enabled = true protocol = 1 ports = ["1000"] source_ips = [] destination_ips = [] source_ports = [] action = 1 rate_limit = 0 burst_limit = 0 stateful = true ``` * releases/v2.4.0 (#1145) * bump version to v2.4.0 * update tauri. * allow try direct connect to public server * support loongarch (#1146) * need encrypt rpc if dst is in peer map (#1151) --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen --- .cargo/config.toml | 4 + .github/origin_wfs/core.yml | 11 +- .github/origin_wfs/docker.yml | 2 +- .github/origin_wfs/release.yml | 2 +- Cargo.lock | 1200 ++++++++++----- README.md | 6 +- README_CN.md | 6 +- easytier-contrib/easytier-magisk/module.prop | 2 +- easytier-contrib/easytier-ohrs/Cargo.lock | 2 +- easytier-gui/package.json | 14 +- easytier-gui/src-tauri/Cargo.toml | 20 +- easytier-gui/src-tauri/src/lib.rs | 2 +- easytier-gui/src-tauri/tauri.conf.json | 2 +- easytier-web/Cargo.toml | 2 +- easytier/Cargo.toml | 3 +- easytier/build.rs | 2 + easytier/src/common/acl_processor.rs | 1334 +++++++++++++++++ easytier/src/common/config.rs | 18 +- easytier/src/common/constants.rs | 2 + easytier/src/common/global_ctx.rs | 11 +- easytier/src/common/mod.rs | 1 + easytier/src/connector/direct.rs | 22 +- easytier/src/easytier-cli.rs | 61 +- easytier/src/easytier-core.rs | 1177 --------------- easytier/src/easytier_core.rs | 206 ++- easytier/src/helper.rs | 23 +- .../instance/dns_server/server_instance.rs | 5 +- easytier/src/instance/instance.rs | 13 +- easytier/src/launcher.rs | 2 - easytier/src/peers/acl_filter.rs | 289 ++++ easytier/src/peers/foreign_network_manager.rs | 8 +- easytier/src/peers/mod.rs | 1 + easytier/src/peers/peer_manager.rs | 31 +- easytier/src/peers/rpc_service.rs | 28 +- easytier/src/proto/acl.proto | 127 ++ easytier/src/proto/acl.rs | 95 ++ easytier/src/proto/cli.proto | 11 + easytier/src/proto/common.proto | 28 +- easytier/src/proto/common.rs | 42 +- easytier/src/proto/mod.rs | 1 + easytier/src/tests/three_node.rs | 180 +++ easytier/src/utils.rs | 1 + pnpm-lock.yaml | 201 +-- .../permissions/autogenerated/reference.md | 2 + .../permissions/schemas/schema.json | 45 +- 45 files changed, 3463 insertions(+), 1782 deletions(-) create mode 100644 easytier/src/common/acl_processor.rs delete mode 100644 easytier/src/easytier-core.rs create mode 100644 easytier/src/peers/acl_filter.rs create mode 100644 easytier/src/proto/acl.proto create mode 100644 easytier/src/proto/acl.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 99b3ae254..1ffdf1d53 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -68,6 +68,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/.github/origin_wfs/core.yml b/.github/origin_wfs/core.yml index f111f1df6..e015624aa 100644 --- a/.github/origin_wfs/core.yml +++ b/.github/origin_wfs/core.yml @@ -102,6 +102,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 @@ -167,6 +171,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 @@ -246,7 +255,7 @@ jobs: TAG=$GITHUB_SHA fi - if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then + if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ ]]; 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 8e4434827..e73d97f3a 100644 --- a/.github/origin_wfs/docker.yml +++ b/.github/origin_wfs/docker.yml @@ -11,7 +11,7 @@ on: image_tag: description: 'Tag for this image build' type: string - default: 'v2.3.2' + default: 'v2.4.0' required: true mark_latest: description: 'Mark this image as latest' diff --git a/.github/origin_wfs/release.yml b/.github/origin_wfs/release.yml index 9fef36ac5..df8928f58 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.0' required: true make_latest: description: 'Mark this release as latest' diff --git a/Cargo.lock b/Cargo.lock index 3f9edcfd8..b431542e2 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" @@ -1233,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" @@ -1528,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", @@ -1749,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" @@ -1837,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]] @@ -1851,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.59.0", ] [[package]] @@ -1873,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" @@ -1913,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" @@ -1951,10 +1979,11 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "easytier" -version = "2.3.2" +version = "2.4.0" dependencies = [ "aes-gcm", "anyhow", + "arc-swap", "async-recursion", "async-ringbuf", "async-stream", @@ -2086,7 +2115,7 @@ dependencies = [ [[package]] name = "easytier-gui" -version = "2.3.2" +version = "2.4.0" dependencies = [ "anyhow", "chrono", @@ -2094,7 +2123,7 @@ dependencies = [ "dunce", "easytier", "elevated-command", - "gethostname 0.5.0", + "gethostname 1.0.2", "once_cell", "serde", "serde_json", @@ -2133,7 +2162,7 @@ dependencies = [ [[package]] name = "easytier-web" -version = "2.3.2" +version = "2.4.0" dependencies = [ "anyhow", "async-trait", @@ -2197,16 +2226,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]] @@ -2853,6 +2882,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" @@ -3326,16 +3365,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]] @@ -3346,7 +3383,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -3439,7 +3476,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.11", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -3523,9 +3560,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", @@ -3760,9 +3797,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", ] @@ -3787,15 +3824,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" @@ -3900,12 +3928,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" @@ -4039,14 +4061,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", ] @@ -4274,15 +4295,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" @@ -4297,18 +4309,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" @@ -4466,21 +4489,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", ] @@ -4695,6 +4719,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" @@ -4703,7 +4739,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "memoffset", ] @@ -4716,7 +4752,7 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.8.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.2.1", "libc", "memoffset", ] @@ -4889,23 +4925,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" @@ -4917,6 +4941,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" @@ -4924,37 +4958,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]] @@ -4964,9 +5004,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]] @@ -4975,146 +5050,134 @@ 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-foundation" -version = "0.2.2" +name = "objc2-exception-helper" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" dependencies = [ - "bitflags 2.8.0", - "block2", - "dispatch", - "libc", - "objc2", + "cc", ] [[package]] -name = "objc2-link-presentation" +name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "block2", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "bitflags 2.8.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", ] [[package]] -name = "objc2-metal" -version = "0.2.2" +name = "objc2-foundation" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.6.1", + "libc", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-quartz-core" -version = "0.2.2" +name = "objc2-io-surface" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.8.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "objc2 0.6.1", + "objc2-core-foundation", ] [[package]] -name = "objc2-symbols" +name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "objc2", - "objc2-foundation", + "bitflags 2.8.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[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]] @@ -5446,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]] @@ -5457,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]] @@ -5482,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]] @@ -5522,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", @@ -5632,7 +5695,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.7.1", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -6045,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" @@ -6052,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", @@ -6315,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" @@ -7092,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]] @@ -7167,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", @@ -7179,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", ] @@ -7203,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" @@ -7210,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", ] @@ -7252,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", @@ -7325,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", @@ -7485,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", @@ -7680,7 +7770,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "itoa 1.0.11", + "itoa", "log", "md-5", "memchr", @@ -7725,7 +7815,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa 1.0.11", + "itoa", "log", "md-5", "memchr", @@ -7995,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", @@ -8010,7 +8099,6 @@ dependencies = [ "gdkwayland-sys", "gdkx11-sys", "gtk", - "instant", "jni", "lazy_static", "libc", @@ -8018,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", @@ -8026,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", ] @@ -8057,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", @@ -8078,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", @@ -8095,7 +8185,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 1.0.63", + "thiserror 2.0.11", "tokio", "tray-icon", "url", @@ -8103,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", @@ -8130,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", @@ -8157,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", @@ -8171,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", @@ -8188,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", @@ -8213,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", @@ -8231,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", @@ -8246,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", @@ -8261,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", @@ -8276,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", ] @@ -8307,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", @@ -8346,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", @@ -8389,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]] @@ -8431,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" @@ -8541,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", @@ -8712,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", ] @@ -8724,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" @@ -8738,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" @@ -8746,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", ] @@ -8758,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", ] @@ -8770,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" @@ -9034,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" @@ -9491,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" @@ -9594,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]] @@ -9619,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]] @@ -9723,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", @@ -9763,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" @@ -9778,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" @@ -9796,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" @@ -9807,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", ] @@ -9827,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" @@ -9844,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" @@ -9884,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" @@ -9923,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" @@ -9957,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" @@ -9975,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" @@ -9993,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" @@ -10017,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" @@ -10035,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" @@ -10053,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" @@ -10071,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" @@ -10127,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" @@ -10155,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" @@ -10169,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", @@ -10187,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", @@ -10198,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", ] @@ -10314,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", @@ -10347,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/README.md b/README.md index ee3636b4c..1c3798e6c 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,9 @@ 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.3.2-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.3.2-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.3.2-70e69a38~ | +| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.0-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.0-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.0-70e69a38~ | ``` You can test connectivity between nodes: diff --git a/README_CN.md b/README_CN.md index 075599194..2323f3a56 100644 --- a/README_CN.md +++ b/README_CN.md @@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea ```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.3.2-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.3.2-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.3.2-70e69a38~ | +| 10.126.126.1 | abc-1 | Local | * | * | * | * | udp | FullCone | 439804259 | 2.4.0-70e69a38~ | +| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.0-70e69a38~ | +| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.0-70e69a38~ | ``` 您可以测试节点之间的连通性: diff --git a/easytier-contrib/easytier-magisk/module.prop b/easytier-contrib/easytier-magisk/module.prop index 6316f4e6c..e6b763d62 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.0 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 index e19454c57..c9233f566 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.lock +++ b/easytier-contrib/easytier-ohrs/Cargo.lock @@ -1010,7 +1010,7 @@ dependencies = [ [[package]] name = "easytier" -version = "2.3.2" +version = "2.4.0" source = "git+https://github.com/EasyTier/EasyTier.git#a4bb555fac1046d0099c44676fa9d0d8cca55c99" dependencies = [ "anyhow", diff --git a/easytier-gui/package.json b/easytier-gui/package.json index efac0317a..79373b930 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.0", "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 d5cccbe78..e5635a32d 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.0" 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", @@ -41,17 +41,17 @@ chrono = { version = "0.4.37", features = ["serde"] } once_cell = "1.18.0" dashmap = "6.0" elevated-command = "1.1.2" -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" @@ -60,4 +60,4 @@ uuid = "1.17.0" 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/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 101be71b1..ac84b26e0 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -211,7 +211,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..22a9502b7 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.0", "identifier": "com.kkrainbow.easytier", "plugins": {}, "app": { diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index 041aee0ea..eee8212d7 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.0" 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/Cargo.toml b/easytier/Cargo.toml index 2e1e42d23..3fe4c3c8a 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -3,7 +3,7 @@ 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.0" edition = "2021" authors = ["kkrainbow"] keywords = ["vpn", "p2p", "network", "easytier"] @@ -44,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"] } diff --git a/easytier/build.rs b/easytier/build.rs index aed1655b8..1987c8f34 100644 --- a/easytier/build.rs +++ b/easytier/build.rs @@ -147,6 +147,7 @@ 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()) { @@ -156,6 +157,7 @@ fn main() -> Result<(), Box> { 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)]") diff --git a/easytier/src/common/acl_processor.rs b/easytier/src/common/acl_processor.rs new file mode 100644 index 000000000..386aff964 --- /dev/null +++ b/easytier/src/common/acl_processor.rs @@ -0,0 +1,1334 @@ +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + str::FromStr as _, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use crate::common::token_bucket::TokenBucket; +use crate::proto::acl::*; +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(), + } + } +} + +// 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(|rule| Self::convert_to_fast_lookup_rule(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); + + return 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, + _ => { + 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.clone(), + 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, + ) -> ( + Arc>, + Arc>>, + Arc>, + ) { + ( + 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: &String, packet_info: &PacketInfo) { + self.conn_track + .entry(conn_track_key.clone()) + .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(|ip_inet| Self::convert_ip_inet_to_cidr(ip_inet)) + .collect(); + + let dst_ip_ranges = rule + .destination_ips + .iter() + .filter_map(|ip_inet| Self::convert_ip_inet_to_cidr(ip_inet)) + .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: &String) -> Option { + cidr::IpCidr::from_str(input.as_str()).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: &::prost::alloc::vec::Vec<::prost::alloc::string::String>, +) -> Option { + port_strs + .iter() + .filter_map(|s| parse_port_range(s).map(|(start, _)| start)) + .min() +} +fn parse_port_end( + port_strs: &::prost::alloc::vec::Vec<::prost::alloc::string::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, + } + } +} + +#[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::default(); + chain.name = "test_inbound".to_string(); + chain.chain_type = ChainType::Inbound as i32; + chain.enabled = true; + + // Allow all rule + let mut rule = Rule::default(); + rule.name = "allow_all".to_string(); + rule.priority = 100; + rule.enabled = true; + rule.action = Action::Allow as i32; + rule.protocol = Protocol::Any as i32; + + 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 mut drop_rule = Rule::default(); + drop_rule.name = "drop_all".to_string(); + drop_rule.priority = 200; + drop_rule.enabled = true; + drop_rule.action = Action::Drop as i32; + drop_rule.protocol = Protocol::Any as i32; + 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::default(); + chain.name = "performance_test".to_string(); + chain.chain_type = ChainType::Inbound as i32; + chain.enabled = true; + + // 1. High-priority simple rule for UDP (can be cached efficiently) + let mut simple_rule = Rule::default(); + simple_rule.name = "simple_udp".to_string(); + simple_rule.priority = 300; + simple_rule.enabled = true; + simple_rule.action = Action::Allow as i32; + simple_rule.protocol = Protocol::Udp as i32; + // 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 mut security_rule = Rule::default(); + security_rule.name = "security_tcp".to_string(); + security_rule.priority = 200; + security_rule.enabled = true; + security_rule.action = Action::Allow as i32; + security_rule.protocol = Protocol::Tcp as i32; + security_rule.stateful = true; + security_rule.rate_limit = 100; // 100 packets/sec + security_rule.burst_limit = 200; + chain.rules.push(security_rule); + + // 3. Low-priority default allow rule for Any + let mut default_rule = Rule::default(); + default_rule.name = "default_allow".to_string(); + default_rule.priority = 100; + default_rule.enabled = true; + default_rule.action = Action::Allow as i32; + default_rule.protocol = Protocol::Any as i32; + 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 mut rule = Rule::default(); + rule.name = "strict_rate_limit".to_string(); + rule.priority = 200; + rule.enabled = true; + rule.action = Action::Allow as i32; + rule.protocol = Protocol::Any as i32; + rule.rate_limit = 1; // Allow only 1 packet per second + rule.burst_limit = 1; // Burst of 1 packet + + 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/config.rs b/easytier/src/common/config.rs index d146d7005..a2d849571 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -10,7 +10,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, }; @@ -116,6 +119,9 @@ 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 dump(&self) -> String; } @@ -291,6 +297,8 @@ struct Config { #[serde(skip)] flags_struct: Option, + + acl: Option, } #[derive(Debug, Clone)] @@ -649,6 +657,14 @@ 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 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..7fb67ce98 100644 --- a/easytier/src/common/constants.rs +++ b/easytier/src/common/constants.rs @@ -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/global_ctx.rs b/easytier/src/common/global_ctx.rs index 9fe50dabf..98208fbad 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -6,6 +6,7 @@ use std::{ use crate::common::config::ProxyNetworkConfig; 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; @@ -81,6 +82,8 @@ pub struct GlobalCtx { quic_proxy_port: AtomicCell>, token_bucket_manager: TokenBucketManager, + + acl_filter: Arc, } impl std::fmt::Debug for GlobalCtx { @@ -108,7 +111,7 @@ impl GlobalCtx { let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers()); - let enable_exit_node = config_fs.get_flags().enable_exit_node || cfg!(target_env= "ohos"); + 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; @@ -147,6 +150,8 @@ impl GlobalCtx { quic_proxy_port: AtomicCell::new(None), token_bucket_manager: TokenBucketManager::new(), + + acl_filter: Arc::new(AclFilter::new()), } } @@ -317,6 +322,10 @@ impl GlobalCtx { pub fn token_bucket_manager(&self) -> &TokenBucketManager { &self.token_bucket_manager } + + pub fn get_acl_filter(&self) -> &Arc { + &self.acl_filter + } } #[cfg(test)] diff --git a/easytier/src/common/mod.rs b/easytier/src/common/mod.rs index 308f60d00..9bc5028d9 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; diff --git a/easytier/src/connector/direct.rs b/easytier/src/connector/direct.rs index b967d6cf9..ffda34be7 100644 --- a/easytier/src/connector/direct.rs +++ b/easytier/src/connector/direct.rs @@ -31,6 +31,7 @@ use crate::{ rpc_types::controller::BaseController, }, tunnel::{udp::UdpTunnelConnector, IpVersion}, + use_global_var, }; use crate::proto::cli::PeerConnInfo; @@ -57,12 +58,14 @@ pub trait PeerManagerForDirectConnector { impl PeerManagerForDirectConnector for PeerManager { async fn list_peers(&self) -> Vec { let mut ret = vec![]; + let allow_public_server = use_global_var!(DIRECT_CONNECT_TO_PUBLIC_SERVER); let routes = self.list_routes().await; - for r in routes - .iter() - .filter(|r| r.feature_flag.map(|r| !r.is_public_server).unwrap_or(true)) - { + for r in routes.iter().filter(|r| { + r.feature_flag + .map(|r| allow_public_server || !r.is_public_server) + .unwrap_or(true) + }) { ret.push(r.peer_id); } @@ -483,10 +486,17 @@ impl DirectConnectorManagerData { self.global_ctx.get_network_name(), ); - let ip_list = rpc_stub + let ip_list = match rpc_stub .get_ip_list(BaseController::default(), GetIpListRequest {}) .await - .with_context(|| format!("get ip list from peer {}", dst_peer_id))?; + .with_context(|| format!("get ip list from peer {}", dst_peer_id)) + { + Ok(ip_list) => ip_list, + Err(e) => { + tracing::error!(?e, "failed to get ip list from peer"); + continue; + } + }; tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list"); diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index ecc428fb0..b8b2e4a03 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -27,11 +27,12 @@ use easytier::{ }, proto::{ cli::{ - list_peer_route_pair, ConnectorManageRpc, ConnectorManageRpcClientFactory, - DumpRouteRequest, GetVpnPortalInfoRequest, ListConnectorRequest, - ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, - ListPeerRequest, ListPeerResponse, ListRouteRequest, ListRouteResponse, - ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc, + list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, ConnectorManageRpc, + ConnectorManageRpcClientFactory, DumpRouteRequest, GetAclStatsRequest, + GetVpnPortalInfoRequest, ListConnectorRequest, ListForeignNetworkRequest, + ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest, + ListPeerResponse, ListRouteRequest, ListRouteResponse, ManageMappedListenerRequest, + MappedListenerManageAction, MappedListenerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc, PeerManageRpcClientFactory, ShowNodeInfoRequest, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, @@ -94,6 +95,8 @@ enum SubCommand { Service(ServiceArgs), #[command(about = "show tcp/kcp proxy status")] Proxy, + #[command(about = "show ACL rules statistics")] + Acl(AclArgs), #[command(about = t!("core_clap.generate_completions").to_string())] GenAutocomplete { shell: Shell }, } @@ -180,6 +183,17 @@ struct NodeArgs { sub_command: Option, } +#[derive(Args, Debug)] +struct AclArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum AclSubCommand { + Stats, +} + #[derive(Args, Debug)] struct ServiceArgs { #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] @@ -302,6 +316,18 @@ impl CommandHandler<'_> { .with_context(|| "failed to get vpn portal client")?) } + async fn get_acl_manager_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .unwrap() + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get acl manager client")?) + } + async fn get_tcp_proxy_client( &self, transport_type: &str, @@ -689,6 +715,26 @@ impl CommandHandler<'_> { Ok(()) } + async fn handle_acl_stats(&self) -> Result<(), Error> { + let client = self.get_acl_manager_client().await?; + let request = GetAclStatsRequest::default(); + let response = client + .get_acl_stats(BaseController::default(), request) + .await?; + + if let Some(acl_stats) = response.acl_stats { + if self.output_format == &OutputFormat::Json { + println!("{}", serde_json::to_string_pretty(&acl_stats)?); + } else { + println!("{}", acl_stats); + } + } else { + println!("No ACL statistics available"); + } + + Ok(()) + } + async fn handle_mapped_listener_list(&self) -> Result<(), Error> { let client = self.get_mapped_listener_manager_client().await?; let request = ListMappedListenerRequest::default(); @@ -1444,6 +1490,11 @@ async fn main() -> Result<(), Error> { print_output(&table_rows, &cli.output_format)?; } + SubCommand::Acl(acl_args) => match &acl_args.sub_command { + Some(AclSubCommand::Stats) | None => { + handler.handle_acl_stats().await?; + } + }, SubCommand::GenAutocomplete { shell } => { let mut cmd = Cli::command(); easytier::print_completions(shell, &mut cmd, "easytier-cli"); diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs deleted file mode 100644 index 350e20091..000000000 --- a/easytier/src/easytier-core.rs +++ /dev/null @@ -1,1177 +0,0 @@ -#![allow(dead_code)] - -use std::{ - net::{Ipv4Addr, SocketAddr}, path::PathBuf, process::ExitCode, sync::Arc -}; - -use anyhow::Context; -use cidr::IpCidr; -use clap::{CommandFactory, Parser}; - -use clap_complete::Shell; -use easytier::{ - common::{ - config::{ - ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, - NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, - }, - constants::EASYTIER_VERSION, - global_ctx::GlobalCtx, - set_default_machine_id, - stun::MockStunInfoCollector, - }, - connector::create_connector_by_url, - instance_manager::NetworkInstanceManager, - launcher::{add_proxy_network_to_config, ConfigSource}, - proto::common::{CompressionAlgoPb, NatType}, - tunnel::{IpVersion, PROTO_PORT_OFFSET}, - utils::{init_logger, setup_panic_handler}, - web_client, -}; - -#[cfg(target_os = "windows")] -windows_service::define_windows_service!(ffi_service_main, win_service_main); - -#[cfg(all(feature = "mimalloc", not(feature = "jemalloc")))] -use mimalloc::MiMalloc; - -#[cfg(all(feature = "mimalloc", not(feature = "jemalloc")))] -#[global_allocator] -static GLOBAL_MIMALLOC: MiMalloc = MiMalloc; - -#[cfg(feature = "jemalloc-prof")] -use jemalloc_ctl::{epoch, stats, Access as _, AsName as _}; - -#[cfg(feature = "jemalloc")] -#[global_allocator] -static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; - -fn set_prof_active(_active: bool) { - #[cfg(feature = "jemalloc-prof")] - { - const PROF_ACTIVE: &'static [u8] = b"prof.active\0"; - let name = PROF_ACTIVE.name(); - name.write(_active).expect("Should succeed to set prof"); - } -} - -fn dump_profile(_cur_allocated: usize) { - #[cfg(feature = "jemalloc-prof")] - { - const PROF_DUMP: &'static [u8] = b"prof.dump\0"; - static mut PROF_DUMP_FILE_NAME: [u8; 128] = [0; 128]; - let file_name_str = format!( - "profile-{}-{}.out", - _cur_allocated, - chrono::Local::now().format("%Y-%m-%d-%H-%M-%S") - ); - // copy file name to PROF_DUMP - let file_name = file_name_str.as_bytes(); - let len = file_name.len(); - if len > 127 { - panic!("file name too long"); - } - unsafe { - PROF_DUMP_FILE_NAME[..len].copy_from_slice(file_name); - // set the last byte to 0 - PROF_DUMP_FILE_NAME[len] = 0; - - let name = PROF_DUMP.name(); - name.write(&PROF_DUMP_FILE_NAME[..len + 1]) - .expect("Should succeed to dump profile"); - println!("dump profile to: {}", file_name_str); - } - } -} - -#[derive(Parser, Debug)] -#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] -struct Cli { - #[arg( - short = 'w', - long, - env = "ET_CONFIG_SERVER", - help = t!("core_clap.config_server").to_string() - )] - config_server: Option, - - #[arg( - long, - env = "ET_MACHINE_ID", - help = t!("core_clap.machine_id").to_string() - )] - machine_id: Option, - - #[arg( - short, - long, - env = "ET_CONFIG_FILE", - value_delimiter = ',', - help = t!("core_clap.config_file").to_string(), - num_args = 1.., - )] - config_file: Option>, - - #[command(flatten)] - network_options: NetworkOptions, - - #[command(flatten)] - logging_options: LoggingOptions, - - #[clap(long, help = t!("core_clap.generate_completions").to_string())] - gen_autocomplete: Option, -} - -#[derive(Parser, Debug)] -struct NetworkOptions { - #[arg( - long, - env = "ET_NETWORK_NAME", - help = t!("core_clap.network_name").to_string(), - )] - network_name: Option, - - #[arg( - long, - env = "ET_NETWORK_SECRET", - help = t!("core_clap.network_secret").to_string(), - )] - network_secret: Option, - - #[arg( - short, - long, - env = "ET_IPV4", - help = t!("core_clap.ipv4").to_string() - )] - ipv4: Option, - - #[arg( - long, - env = "ET_IPV6", - help = t!("core_clap.ipv6").to_string() - )] - ipv6: Option, - - #[arg( - short, - long, - env = "ET_DHCP", - help = t!("core_clap.dhcp").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - dhcp: Option, - - #[arg( - short, - long, - env = "ET_PEERS", - value_delimiter = ',', - help = t!("core_clap.peers").to_string(), - num_args = 0.. - )] - peers: Vec, - - #[arg( - short, - long, - env = "ET_EXTERNAL_NODE", - help = t!("core_clap.external_node").to_string() - )] - external_node: Option, - - #[arg( - short = 'n', - long, - env = "ET_PROXY_NETWORKS", - value_delimiter = ',', - help = t!("core_clap.proxy_networks").to_string() - )] - proxy_networks: Vec, - - #[arg( - short, - long, - env = "ET_RPC_PORTAL", - help = t!("core_clap.rpc_portal").to_string(), - )] - rpc_portal: Option, - - #[arg( - long, - env = "ET_RPC_PORTAL_WHITELIST", - value_delimiter = ',', - help = t!("core_clap.rpc_portal_whitelist").to_string(), - )] - rpc_portal_whitelist: Option>, - - #[arg( - short, - long, - env = "ET_LISTENERS", - value_delimiter = ',', - help = t!("core_clap.listeners").to_string(), - num_args = 0.. - )] - listeners: Vec, - - #[arg( - long, - env = "ET_MAPPED_LISTENERS", - value_delimiter = ',', - help = t!("core_clap.mapped_listeners").to_string(), - num_args = 0.. - )] - mapped_listeners: Vec, - - #[arg( - long, - env = "ET_NO_LISTENER", - help = t!("core_clap.no_listener").to_string(), - default_value = "false", - )] - no_listener: bool, - - #[arg( - long, - env = "ET_HOSTNAME", - help = t!("core_clap.hostname").to_string() - )] - hostname: Option, - - #[arg( - short = 'm', - long, - env = "ET_INSTANCE_NAME", - help = t!("core_clap.instance_name").to_string(), - )] - instance_name: Option, - - #[arg( - long, - env = "ET_VPN_PORTAL", - help = t!("core_clap.vpn_portal").to_string() - )] - vpn_portal: Option, - - #[arg( - long, - env = "ET_DEFAULT_PROTOCOL", - help = t!("core_clap.default_protocol").to_string() - )] - default_protocol: Option, - - #[arg( - short = 'u', - long, - env = "ET_DISABLE_ENCRYPTION", - help = t!("core_clap.disable_encryption").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_encryption: Option, - - #[arg( - long, - env = "ET_MULTI_THREAD", - help = t!("core_clap.multi_thread").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - multi_thread: Option, - - #[arg( - long, - env = "ET_MULTI_THREAD_COUNT", - help = t!("core_clap.multi_thread_count").to_string(), - )] - multi_thread_count: Option, - - #[arg( - long, - env = "ET_DISABLE_IPV6", - help = t!("core_clap.disable_ipv6").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_ipv6: Option, - - #[arg( - long, - env = "ET_DEV_NAME", - help = t!("core_clap.dev_name").to_string() - )] - dev_name: Option, - - #[arg( - long, - env = "ET_MTU", - help = t!("core_clap.mtu").to_string() - )] - mtu: Option, - - #[arg( - long, - env = "ET_LATENCY_FIRST", - help = t!("core_clap.latency_first").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - latency_first: Option, - - #[arg( - long, - env = "ET_EXIT_NODES", - value_delimiter = ',', - help = t!("core_clap.exit_nodes").to_string(), - num_args = 0.. - )] - exit_nodes: Vec, - - #[arg( - long, - env = "ET_ENABLE_EXIT_NODE", - help = t!("core_clap.enable_exit_node").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - enable_exit_node: Option, - - #[arg( - long, - env = "ET_PROXY_FORWARD_BY_SYSTEM", - help = t!("core_clap.proxy_forward_by_system").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - proxy_forward_by_system: Option, - - #[arg( - long, - env = "ET_NO_TUN", - help = t!("core_clap.no_tun").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - no_tun: Option, - - #[arg( - long, - env = "ET_USE_SMOLTCP", - help = t!("core_clap.use_smoltcp").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - use_smoltcp: Option, - - #[arg( - long, - env = "ET_MANUAL_ROUTES", - value_delimiter = ',', - help = t!("core_clap.manual_routes").to_string(), - num_args = 0.. - )] - manual_routes: Option>, - - // if not in relay_network_whitelist: - // for foreign virtual network, will refuse the incoming connection - // for local virtual network, will refuse relaying tun packet - #[arg( - long, - env = "ET_RELAY_NETWORK_WHITELIST", - value_delimiter = ',', - help = t!("core_clap.relay_network_whitelist").to_string(), - num_args = 0.. - )] - relay_network_whitelist: Option>, - - #[arg( - long, - env = "ET_DISABLE_P2P", - help = t!("core_clap.disable_p2p").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_p2p: Option, - - #[arg( - long, - env = "ET_DISABLE_UDP_HOLE_PUNCHING", - help = t!("core_clap.disable_udp_hole_punching").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_udp_hole_punching: Option, - - #[arg( - long, - env = "ET_RELAY_ALL_PEER_RPC", - help = t!("core_clap.relay_all_peer_rpc").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - relay_all_peer_rpc: Option, - - #[cfg(feature = "socks5")] - #[arg( - long, - env = "ET_SOCKS5", - help = t!("core_clap.socks5").to_string() - )] - socks5: Option, - - #[arg( - long, - env = "ET_COMPRESSION", - help = t!("core_clap.compression").to_string(), - )] - compression: Option, - - #[arg( - long, - env = "ET_BIND_DEVICE", - help = t!("core_clap.bind_device").to_string() - )] - bind_device: Option, - - #[arg( - long, - env = "ET_ENABLE_KCP_PROXY", - help = t!("core_clap.enable_kcp_proxy").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - enable_kcp_proxy: Option, - - #[arg( - long, - env = "ET_DISABLE_KCP_INPUT", - help = t!("core_clap.disable_kcp_input").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_kcp_input: Option, - - #[arg( - long, - env = "ET_ENABLE_QUIC_PROXY", - help = t!("core_clap.enable_quic_proxy").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - enable_quic_proxy: Option, - - #[arg( - long, - env = "ET_DISABLE_QUIC_INPUT", - help = t!("core_clap.disable_quic_input").to_string(), - num_args = 0..=1, - default_missing_value = "true" - )] - disable_quic_input: Option, - - #[arg( - long, - env = "ET_PORT_FORWARD", - value_delimiter = ',', - help = t!("core_clap.port_forward").to_string(), - num_args = 1.. - )] - port_forward: Vec, - - #[arg( - long, - env = "ET_ACCEPT_DNS", - help = t!("core_clap.accept_dns").to_string(), - )] - accept_dns: Option, - - #[arg( - long, - env = "ET_PRIVATE_MODE", - help = t!("core_clap.private_mode").to_string(), - )] - private_mode: Option, - - #[arg( - long, - env = "ET_FOREIGN_RELAY_BPS_LIMIT", - help = t!("core_clap.foreign_relay_bps_limit").to_string(), - )] - foreign_relay_bps_limit: Option, -} - -#[derive(Parser, Debug)] -struct LoggingOptions { - #[arg( - long, - env = "ET_CONSOLE_LOG_LEVEL", - help = t!("core_clap.console_log_level").to_string() - )] - console_log_level: Option, - - #[arg( - long, - env = "ET_FILE_LOG_LEVEL", - help = t!("core_clap.file_log_level").to_string() - )] - file_log_level: Option, - - #[arg( - long, - env = "ET_FILE_LOG_DIR", - help = t!("core_clap.file_log_dir").to_string() - )] - file_log_dir: Option, -} - -rust_i18n::i18n!("locales", fallback = "en"); - -impl Cli { - fn parse_listeners(no_listener: bool, listeners: Vec) -> anyhow::Result> { - if no_listener || listeners.is_empty() { - return Ok(vec![]); - } - - let origin_listners = listeners; - let mut listeners: Vec = Vec::new(); - if origin_listners.len() == 1 { - if let Ok(port) = origin_listners[0].parse::() { - for (proto, offset) in PROTO_PORT_OFFSET { - listeners.push(format!("{}://0.0.0.0:{}", proto, port + *offset)); - } - return Ok(listeners); - } - } - - for l in &origin_listners { - let proto_port: Vec<&str> = l.split(':').collect(); - if proto_port.len() > 2 { - if let Ok(url) = l.parse::() { - listeners.push(url.to_string()); - } else { - panic!("failed to parse listener: {}", l); - } - } else { - let Some((proto, offset)) = PROTO_PORT_OFFSET - .iter() - .find(|(proto, _)| *proto == proto_port[0]) - else { - return Err(anyhow::anyhow!("unknown protocol: {}", proto_port[0])); - }; - - let port = if proto_port.len() == 2 { - proto_port[1].parse::().unwrap() - } else { - 11010 + offset - }; - - listeners.push(format!("{}://0.0.0.0:{}", proto, port)); - } - } - - Ok(listeners) - } - - fn parse_rpc_portal(rpc_portal: String) -> anyhow::Result { - if let Ok(port) = rpc_portal.parse::() { - return Ok(format!("0.0.0.0:{}", port).parse().unwrap()); - } - - Ok(rpc_portal.parse()?) - } -} - -impl NetworkOptions { - fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool { - if config_file_count == 1 { - return true; - } - let Some(network_name) = &self.network_name else { - return false; - }; - if cfg.get_network_identity().network_name == *network_name { - return true; - } - false - } - - fn merge_into(&self, cfg: &mut TomlConfigLoader) -> anyhow::Result<()> { - if self.hostname.is_some() { - cfg.set_hostname(self.hostname.clone()); - } - - let old_ns = cfg.get_network_identity(); - let network_name = self.network_name.clone().unwrap_or(old_ns.network_name); - let network_secret = self - .network_secret - .clone() - .unwrap_or(old_ns.network_secret.unwrap_or_default()); - cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret)); - - if let Some(dhcp) = self.dhcp { - cfg.set_dhcp(dhcp); - } - - if let Some(ipv4) = &self.ipv4 { - cfg.set_ipv4(Some(ipv4.parse().with_context(|| { - format!("failed to parse ipv4 address: {}", ipv4) - })?)) - } - - if let Some(ipv6) = &self.ipv6 { - cfg.set_ipv6(Some(ipv6.parse().with_context(|| { - format!("failed to parse ipv6 address: {}", ipv6) - })?)) - } - - if !self.peers.is_empty() { - let mut peers = cfg.get_peers(); - peers.reserve(peers.len() + self.peers.len()); - for p in &self.peers { - peers.push(PeerConfig { - uri: p - .parse() - .with_context(|| format!("failed to parse peer uri: {}", p))?, - }); - } - cfg.set_peers(peers); - } - - if self.no_listener || !self.listeners.is_empty() { - cfg.set_listeners( - Cli::parse_listeners(self.no_listener, self.listeners.clone())? - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(), - ); - } else if cfg.get_listeners() == None { - cfg.set_listeners( - Cli::parse_listeners(false, vec!["11010".to_string()])? - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(), - ); - } - - if !self.mapped_listeners.is_empty() { - let mut errs = Vec::new(); - cfg.set_mapped_listeners(Some( - self.mapped_listeners - .iter() - .map(|s| { - s.parse() - .with_context(|| format!("mapped listener is not a valid url: {}", s)) - .unwrap() - }) - .map(|s: url::Url| { - if s.port().is_none() { - errs.push(anyhow::anyhow!("mapped listener port is missing: {}", s)); - } - s - }) - .collect::>(), - )); - if !errs.is_empty() { - return Err(anyhow::anyhow!( - "{}", - errs.iter() - .map(|x| format!("{}", x)) - .collect::>() - .join("\n") - )); - } - } - - for n in self.proxy_networks.iter() { - add_proxy_network_to_config(n, &cfg)?; - } - - let rpc_portal = if let Some(r) = &self.rpc_portal { - Cli::parse_rpc_portal(r.clone()) - .with_context(|| format!("failed to parse rpc portal: {}", r))? - } else if let Some(r) = cfg.get_rpc_portal() { - r - } else { - Cli::parse_rpc_portal("0".into())? - }; - cfg.set_rpc_portal(rpc_portal); - - if let Some(rpc_portal_whitelist) = &self.rpc_portal_whitelist { - let mut whitelist = cfg.get_rpc_portal_whitelist().unwrap_or_else(|| Vec::new()); - for cidr in rpc_portal_whitelist { - whitelist.push((*cidr).clone()); - } - cfg.set_rpc_portal_whitelist(Some(whitelist)); - } - - if let Some(external_nodes) = self.external_node.as_ref() { - let mut old_peers = cfg.get_peers(); - old_peers.push(PeerConfig { - uri: external_nodes.parse().with_context(|| { - format!("failed to parse external node uri: {}", external_nodes) - })?, - }); - cfg.set_peers(old_peers); - } - - if let Some(inst_name) = &self.instance_name { - cfg.set_inst_name(inst_name.clone()); - } - - if let Some(vpn_portal) = self.vpn_portal.as_ref() { - let url: url::Url = vpn_portal - .parse() - .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?; - let host = url - .host_str() - .ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?; - let port = url - .port() - .ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?; - let client_cidr = url.path()[1..].parse().with_context(|| { - format!("failed to parse vpn portal client cidr: {}", url.path()) - })?; - let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap(); - cfg.set_vpn_portal_config(VpnPortalConfig { - wireguard_listen, - client_cidr, - }); - } - - if let Some(manual_routes) = self.manual_routes.as_ref() { - let mut routes = Vec::::with_capacity(manual_routes.len()); - for r in manual_routes { - routes.push( - r.parse() - .with_context(|| format!("failed to parse route: {}", r))?, - ); - } - cfg.set_routes(Some(routes)); - } - - #[cfg(feature = "socks5")] - if let Some(socks5_proxy) = self.socks5 { - cfg.set_socks5_portal(Some( - format!("socks5://0.0.0.0:{}", socks5_proxy) - .parse() - .unwrap(), - )); - } - - #[cfg(feature = "socks5")] - for port_forward in self.port_forward.iter() { - let example_str = ", example: udp://0.0.0.0:12345/10.126.126.1:12345"; - - let bind_addr = format!( - "{}:{}", - port_forward.host_str().expect("local bind host is missing"), - port_forward.port().expect("local bind port is missing") - ) - .parse() - .expect(format!("failed to parse local bind addr {}", example_str).as_str()); - - let dst_addr = format!( - "{}", - port_forward - .path_segments() - .expect(format!("remote destination addr is missing {}", example_str).as_str()) - .next() - .expect(format!("remote destination addr is missing {}", example_str).as_str()) - ) - .parse() - .expect(format!("failed to parse remote destination addr {}", example_str).as_str()); - - let port_forward_item = PortForwardConfig { - bind_addr, - dst_addr, - proto: port_forward.scheme().to_string(), - }; - - let mut old = cfg.get_port_forwards(); - old.push(port_forward_item); - cfg.set_port_forwards(old); - } - - let mut f = cfg.get_flags(); - if let Some(default_protocol) = &self.default_protocol { - f.default_protocol = default_protocol.clone() - }; - if let Some(v) = self.disable_encryption { - f.enable_encryption = !v; - } - if let Some(v) = self.disable_ipv6 { - f.enable_ipv6 = !v; - } - f.latency_first = self.latency_first.unwrap_or(f.latency_first); - if let Some(dev_name) = &self.dev_name { - f.dev_name = dev_name.clone() - } - println!("mtu: {}, {:?}", f.mtu, self.mtu); - if let Some(mtu) = self.mtu { - f.mtu = mtu as u32; - } - f.enable_exit_node = self.enable_exit_node.unwrap_or(f.enable_exit_node); - f.proxy_forward_by_system = self - .proxy_forward_by_system - .unwrap_or(f.proxy_forward_by_system); - f.no_tun = self.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun")); - f.use_smoltcp = self.use_smoltcp.unwrap_or(f.use_smoltcp); - if let Some(wl) = self.relay_network_whitelist.as_ref() { - f.relay_network_whitelist = wl.join(" "); - } - f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p); - f.disable_udp_hole_punching = self - .disable_udp_hole_punching - .unwrap_or(f.disable_udp_hole_punching); - f.relay_all_peer_rpc = self.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc); - f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread); - if let Some(compression) = &self.compression { - f.data_compress_algo = match compression.as_str() { - "none" => CompressionAlgoPb::None, - "zstd" => CompressionAlgoPb::Zstd, - _ => panic!( - "unknown compression algorithm: {}, supported: none, zstd", - compression - ), - } - .into(); - } - f.bind_device = self.bind_device.unwrap_or(f.bind_device); - f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); - f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input); - f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy); - f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input); - f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns); - f.private_mode = self.private_mode.unwrap_or(f.private_mode); - f.foreign_relay_bps_limit = self - .foreign_relay_bps_limit - .unwrap_or(f.foreign_relay_bps_limit); - f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count); - cfg.set_flags(f); - - if !self.exit_nodes.is_empty() { - cfg.set_exit_nodes(self.exit_nodes.clone()); - } - - Ok(()) - } -} - -impl LoggingConfigLoader for &LoggingOptions { - fn get_console_logger_config(&self) -> ConsoleLoggerConfig { - ConsoleLoggerConfig { - level: self.console_log_level.clone(), - } - } - - fn get_file_logger_config(&self) -> FileLoggerConfig { - FileLoggerConfig { - level: self.file_log_level.clone(), - dir: self.file_log_dir.clone(), - file: None, - } - } -} - -#[cfg(target_os = "windows")] -fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> { - use crate::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY; - use winreg::enums::*; - use winreg::RegKey; - - let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); - let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?; - let dir_pat_str = key.get_value::(service_name)?; - let dir_path = std::fs::canonicalize(dir_pat_str)?; - - std::env::set_current_dir(dir_path)?; - - Ok(()) -} - -#[cfg(target_os = "windows")] -fn win_service_event_loop( - stop_notify: std::sync::Arc, - cli: Cli, - status_handle: windows_service::service_control_handler::ServiceStatusHandle, -) { - use std::time::Duration; - use tokio::runtime::Runtime; - use windows_service::service::*; - - let normal_status = ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }; - let error_status = ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - exit_code: ServiceExitCode::ServiceSpecific(1u32), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }; - - std::thread::spawn(move || { - let rt = Runtime::new().unwrap(); - rt.block_on(async move { - tokio::select! { - res = run_main(cli) => { - match res { - Ok(_) => { - status_handle.set_service_status(normal_status).unwrap(); - std::process::exit(0); - } - Err(e) => { - status_handle.set_service_status(error_status).unwrap(); - eprintln!("error: {}", e); - } - } - }, - _ = stop_notify.notified() => { - _ = status_handle.set_service_status(normal_status); - std::process::exit(0); - } - } - }); - }); -} - -#[cfg(target_os = "windows")] -fn win_service_main(arg: Vec) { - use std::sync::Arc; - use std::time::Duration; - use tokio::sync::Notify; - use windows_service::service::*; - use windows_service::service_control_handler::*; - - _ = win_service_set_work_dir(&arg[0]); - - let cli = Cli::parse(); - - let stop_notify_send = Arc::new(Notify::new()); - let stop_notify_recv = Arc::clone(&stop_notify_send); - let event_handler = move |control_event| -> ServiceControlHandlerResult { - match control_event { - ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, - ServiceControl::Stop => { - stop_notify_send.notify_one(); - ServiceControlHandlerResult::NoError - } - _ => ServiceControlHandlerResult::NotImplemented, - } - }; - let status_handle = register(String::new(), event_handler).expect("register service fail"); - let next_status = ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Running, - controls_accepted: ServiceControlAccept::STOP, - exit_code: ServiceExitCode::Win32(0), - checkpoint: 0, - wait_hint: Duration::default(), - process_id: None, - }; - status_handle - .set_service_status(next_status) - .expect("set service status fail"); - - win_service_event_loop(stop_notify_recv, cli, status_handle); -} - -async fn run_main(cli: Cli) -> anyhow::Result<()> { - init_logger(&cli.logging_options, false)?; - - if cli.config_server.is_some() { - set_default_machine_id(cli.machine_id); - let config_server_url_s = cli.config_server.clone().unwrap(); - let config_server_url = match url::Url::parse(&config_server_url_s) { - Ok(u) => u, - Err(_) => format!( - "udp://config-server.easytier.cn:22020/{}", - config_server_url_s - ) - .parse() - .unwrap(), - }; - - let mut c_url = config_server_url.clone(); - c_url.set_path(""); - let token = config_server_url - .path_segments() - .and_then(|mut x| x.next()) - .map(|x| x.to_string()) - .unwrap_or_default(); - - println!( - "Entering config client mode...\n server: {}\n token: {}", - c_url, token, - ); - - println!("Official config website: https://easytier.cn/web"); - - if token.is_empty() { - panic!("empty token"); - } - - let config = TomlConfigLoader::default(); - let global_ctx = Arc::new(GlobalCtx::new(config)); - global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector { - udp_nat_type: NatType::Unknown, - })); - let mut flags = global_ctx.get_flags(); - flags.bind_device = false; - global_ctx.set_flags(flags); - let hostname = match cli.network_options.hostname { - None => gethostname::gethostname().to_string_lossy().to_string(), - Some(hostname) => hostname.to_string(), - }; - let _wc = web_client::WebClient::new( - create_connector_by_url(c_url.as_str(), &global_ctx, IpVersion::Both).await?, - token.to_string(), - hostname, - ); - tokio::signal::ctrl_c().await.unwrap(); - return Ok(()); - } - let manager = NetworkInstanceManager::new(); - let mut crate_cli_network = - cli.config_file.is_none() || cli.network_options.network_name.is_some(); - if let Some(config_files) = cli.config_file { - let config_file_count = config_files.len(); - for config_file in config_files { - let mut cfg = TomlConfigLoader::new(&config_file) - .with_context(|| format!("failed to load config file: {:?}", config_file))?; - - if cli.network_options.can_merge(&cfg, config_file_count) { - cli.network_options.merge_into(&mut cfg).with_context(|| { - format!("failed to merge config from cli: {:?}", config_file) - })?; - crate_cli_network = false; - } - - println!( - "Starting easytier from config file {:?} with config:", - config_file - ); - println!("############### TOML ###############\n"); - println!("{}", cfg.dump()); - println!("-----------------------------------"); - manager.run_network_instance(cfg, ConfigSource::File)?; - } - } - - if crate_cli_network { - let mut cfg = TomlConfigLoader::default(); - cli.network_options - .merge_into(&mut cfg) - .with_context(|| format!("failed to create config from cli"))?; - println!("Starting easytier from cli with config:"); - println!("############### TOML ###############\n"); - println!("{}", cfg.dump()); - println!("-----------------------------------"); - manager.run_network_instance(cfg, ConfigSource::Cli)?; - } - - tokio::select! { - _ = manager.wait() => { - } - _ = tokio::signal::ctrl_c() => { - println!("ctrl-c received, exiting..."); - } - } - Ok(()) -} - -fn memory_monitor() { - #[cfg(feature = "jemalloc-prof")] - { - let mut last_peak_size = 0; - let e = epoch::mib().unwrap(); - let allocated_stats = stats::allocated::mib().unwrap(); - - loop { - e.advance().unwrap(); - let new_heap_size = allocated_stats.read().unwrap(); - - println!( - "heap size: {} bytes, time: {}", - new_heap_size, - chrono::Local::now().format("%Y-%m-%d %H:%M:%S") - ); - - // dump every 75MB - if last_peak_size > 0 - && new_heap_size > last_peak_size - && new_heap_size - last_peak_size > 75 * 1024 * 1024 - { - println!( - "heap size increased: {} bytes, time: {}", - new_heap_size - last_peak_size, - chrono::Local::now().format("%Y-%m-%d %H:%M:%S") - ); - dump_profile(new_heap_size); - last_peak_size = new_heap_size; - } - - if last_peak_size == 0 { - last_peak_size = new_heap_size; - } - - std::thread::sleep(std::time::Duration::from_secs(5)); - } - } -} - -#[tokio::main(flavor = "current_thread")] -pub(crate) async fn main() -> ExitCode { - let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); - rust_i18n::set_locale(&locale); - setup_panic_handler(); - - #[cfg(target_os = "windows")] - match windows_service::service_dispatcher::start(String::new(), ffi_service_main) { - Ok(_) => std::thread::park(), - Err(e) => { - let should_panic = if let windows_service::Error::Winapi(ref io_error) = e { - io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT - } else { - true - }; - - if should_panic { - panic!("SCM start an error: {}", e); - } - } - }; - - set_prof_active(true); - let _monitor = std::thread::spawn(memory_monitor); - - let cli = Cli::parse(); - if let Some(shell) = cli.gen_autocomplete { - let mut cmd = Cli::command(); - easytier::print_completions(shell, &mut cmd, "easytier-core"); - return ExitCode::SUCCESS; - } - let mut ret_code = 0; - - if let Err(e) = run_main(cli).await { - eprintln!("error: {:?}", e); - ret_code = 1; - } - - println!("Stopping easytier..."); - - dump_profile(0); - set_prof_active(false); - - ExitCode::from(ret_code) -} diff --git a/easytier/src/easytier_core.rs b/easytier/src/easytier_core.rs index 82ae232ad..3c459a107 100644 --- a/easytier/src/easytier_core.rs +++ b/easytier/src/easytier_core.rs @@ -2,24 +2,40 @@ use rust_i18n::t; use std::{ - net::{Ipv4Addr, SocketAddr}, path::PathBuf, process::ExitCode, sync::Arc + net::{Ipv4Addr, SocketAddr}, + path::PathBuf, + process::ExitCode, + sync::Arc, }; use anyhow::Context; use cidr::IpCidr; use clap::{CommandFactory, Parser}; -use clap_complete::Shell; -use crate::{common::{ - config::{ - ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, - NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, +use crate::{ + common::{ + config::{ + ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, + NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, + }, + constants::EASYTIER_VERSION, + global_ctx::GlobalCtx, + set_default_machine_id, + stun::MockStunInfoCollector, + }, + connector::create_connector_by_url, + instance_manager::NetworkInstanceManager, + launcher::{add_proxy_network_to_config, ConfigSource}, + print_completions, + proto::{ + acl::{Acl, AclV1, Action, Chain, ChainType, Protocol, Rule}, + common::{CompressionAlgoPb, NatType}, }, - constants::EASYTIER_VERSION, - global_ctx::GlobalCtx, - set_default_machine_id, - stun::MockStunInfoCollector, -}, connector::create_connector_by_url, instance_manager::NetworkInstanceManager, launcher::{add_proxy_network_to_config, ConfigSource}, print_completions, proto::common::{CompressionAlgoPb, NatType}, tunnel::{IpVersion, PROTO_PORT_OFFSET}, utils::{init_logger, setup_panic_handler}, web_client}; + tunnel::{IpVersion, PROTO_PORT_OFFSET}, + utils::{init_logger, setup_panic_handler}, + web_client, +}; +use clap_complete::Shell; #[cfg(target_os = "windows")] windows_service::define_windows_service!(ffi_service_main, win_service_main); @@ -78,7 +94,7 @@ fn dump_profile(_cur_allocated: usize) { #[derive(Parser, Debug)] #[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] -struct Cli { +pub(crate) struct Cli { #[arg( short = 'w', long, @@ -492,6 +508,22 @@ struct NetworkOptions { help = t!("core_clap.foreign_relay_bps_limit").to_string(), )] foreign_relay_bps_limit: Option, + + #[arg( + long, + value_delimiter = ',', + help = "TCP port whitelist. Supports single ports (80) and ranges (8000-9000)", + num_args = 0.. + )] + tcp_whitelist: Vec, + + #[arg( + long, + value_delimiter = ',', + help = "UDP port whitelist. Supports single ports (53) and ranges (5000-6000)", + num_args = 0.. + )] + udp_whitelist: Vec, } #[derive(Parser, Debug)] @@ -589,6 +621,117 @@ impl NetworkOptions { false } + 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 + )); + } + + // Add individual ports in the range + for port in start..=end { + ports.push(port.to_string()); + } + } 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(&self) -> anyhow::Result> { + if self.tcp_whitelist.is_empty() && self.udp_whitelist.is_empty() { + return Ok(None); + } + + let mut acl = Acl { + acl_v1: Some(AclV1 { chains: vec![] }), + }; + + let acl_v1 = acl.acl_v1.as_mut().unwrap(); + + // 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 = 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); + } + + acl_v1.chains.push(inbound_chain); + Ok(Some(acl)) + } + fn merge_into(&self, cfg: &mut TomlConfigLoader) -> anyhow::Result<()> { if self.hostname.is_some() { cfg.set_hostname(self.hostname.clone()); @@ -761,8 +904,8 @@ impl NetworkOptions { port_forward.host_str().expect("local bind host is missing"), port_forward.port().expect("local bind port is missing") ) - .parse() - .expect(format!("failed to parse local bind addr {}", example_str).as_str()); + .parse() + .expect(format!("failed to parse local bind addr {}", example_str).as_str()); let dst_addr = format!( "{}", @@ -772,8 +915,8 @@ impl NetworkOptions { .next() .expect(format!("remote destination addr is missing {}", example_str).as_str()) ) - .parse() - .expect(format!("failed to parse remote destination addr {}", example_str).as_str()); + .parse() + .expect(format!("failed to parse remote destination addr {}", example_str).as_str()); let port_forward_item = PortForwardConfig { bind_addr, @@ -800,7 +943,6 @@ impl NetworkOptions { if let Some(dev_name) = &self.dev_name { f.dev_name = dev_name.clone() } - println!("mtu: {}, {:?}", f.mtu, self.mtu); if let Some(mtu) = self.mtu { f.mtu = mtu as u32; } @@ -828,7 +970,7 @@ impl NetworkOptions { compression ), } - .into(); + .into(); } f.bind_device = self.bind_device.unwrap_or(f.bind_device); f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); @@ -847,6 +989,11 @@ impl NetworkOptions { cfg.set_exit_nodes(self.exit_nodes.clone()); } + // Handle port whitelists by generating ACL configuration + if let Some(acl) = self.generate_acl_from_whitelists()? { + cfg.set_acl(Some(acl)); + } + Ok(()) } } @@ -978,8 +1125,8 @@ fn win_service_main(arg: Vec) { win_service_event_loop(stop_notify_recv, cli, status_handle); } -async fn run_main(cli: Cli) -> anyhow::Result<()> { - // init_logger(&cli.logging_options, false)?; +pub(crate) async fn run_main(cli: Cli) -> anyhow::Result<()> { + init_logger(&cli.logging_options, false)?; if cli.config_server.is_some() { set_default_machine_id(cli.machine_id); @@ -990,8 +1137,8 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { "udp://config-server.easytier.cn:22020/{}", config_server_url_s ) - .parse() - .unwrap(), + .parse() + .unwrap(), }; let mut c_url = config_server_url.clone(); @@ -1089,7 +1236,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { } fn memory_monitor() { - #[cfg(feature = "jemalloc")] + #[cfg(feature = "jemalloc-prof")] { let mut last_peak_size = 0; let e = epoch::mib().unwrap(); @@ -1173,16 +1320,3 @@ pub(crate) async fn main() -> ExitCode { ExitCode::from(ret_code) } - - -// remember to comment code : init_logger(&cli.logging_options, false)?; -pub(crate) async fn run(path: &str) -> u8 { - let cli = Cli::parse_from(["app", &format!("-c{}", path)]); - let mut ret_code = 0; - if let Err(e) = run_main(cli).await { - eprintln!("error: {:?}", e); - ret_code = 1; - } - - ret_code -} \ No newline at end of file diff --git a/easytier/src/helper.rs b/easytier/src/helper.rs index 543d3ca29..dba25e7e0 100644 --- a/easytier/src/helper.rs +++ b/easytier/src/helper.rs @@ -1,10 +1,12 @@ -use crate::easytier_core; +use crate::easytier_core::{run_main, Cli}; +use crate::instance_manager::NetworkInstanceManager; use crate::peers::peer_manager::PeerManager; use crate::peers::rpc_service::PeerManagerRpcService; use crate::proto::cli::{list_peer_route_pair, NodeInfo, PeerManageRpc, ShowNodeInfoRequest}; use crate::proto::rpc_types::controller::BaseController; use crate::utils::{cost_to_str, float_to_str, PeerRoutePair}; use cidr::Ipv4Inet; +use clap::Parser; use humansize::format_size; use lazy_static::lazy_static; use std::alloc::{alloc_zeroed, Layout}; @@ -13,11 +15,9 @@ use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; -use crate::instance_manager::NetworkInstanceManager; lazy_static! { pub static ref g_peermanager: RwLock>> = RwLock::new(None); - pub static ref g_networkinstance: RwLock = RwLock::new(NetworkInstanceManager::new()); } lazy_static! { @@ -69,7 +69,7 @@ pub fn get_token() -> CancellationToken { pub(crate) fn run(path: &str) { reset_token(); let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(easytier_core::run(path)); + rt.block_on(start_run(path)); } pub async fn get_stats() -> *mut u8 { @@ -183,3 +183,18 @@ where { serde_json::to_string(items).unwrap() } + +// 1. rename easytier-core.rs to easytier_core.rs +// 2. add codes: let token = &crate::helper::get_token(); _ = token.cancelled() => { println!("任务被取消"); } +// 3. add codes: return Ok(None); in init_logger +// 4. fix the import/package issue +async fn start_run(path: &str) -> u8 { + let cli = Cli::parse_from(["app", &format!("-c{}", path)]); + let mut ret_code = 0; + if let Err(e) = run_main(cli).await { + eprintln!("error: {:?}", e); + ret_code = 1; + } + + ret_code +} diff --git a/easytier/src/instance/dns_server/server_instance.rs b/easytier/src/instance/dns_server/server_instance.rs index 1e1f8f078..bbce45892 100644 --- a/easytier/src/instance/dns_server/server_instance.rs +++ b/easytier/src/instance/dns_server/server_instance.rs @@ -298,7 +298,10 @@ impl NicPacketFilter for MagicDnsServerInstanceData { #[async_trait::async_trait] impl RpcServerHook for MagicDnsServerInstanceData { - async fn on_new_client(&self, tunnel_info: Option)-> Result, anyhow::Error> { + async fn on_new_client( + &self, + tunnel_info: Option, + ) -> Result, anyhow::Error> { tracing::info!(?tunnel_info, "New client connected"); Ok(tunnel_info) } diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 0741876fd..70dccb1fc 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -609,6 +609,10 @@ impl Instance { } } + if let Some(acl) = self.global_ctx.config.get_acl() { + self.global_ctx.get_acl_filter().reload_rules(Some(&acl)); + } + // run after tun device created, so listener can bind to tun device, which may be required by win 10 self.ip_proxy = Some(IpProxy::new( self.get_global_ctx(), @@ -801,10 +805,11 @@ impl Instance { let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service(); let s = self.rpc_server.as_mut().unwrap(); - s.registry().register( - PeerManageRpcServer::new(PeerManagerRpcService::new(peer_mgr)), - "", - ); + let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone()); + s.registry() + .register(PeerManageRpcServer::new(peer_mgr_rpc_service.clone()), ""); + s.registry() + .register(AclManageRpcServer::new(peer_mgr_rpc_service), ""); s.registry().register( ConnectorManageRpcServer::new(ConnectorManagerRpcService(conn_manager)), "", diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 50df16992..cc70e2bc4 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -242,8 +242,6 @@ impl EasyTierLauncher { instance.clear_resources().await; drop(instance); - println!("[easytier_routine] Instance resource released!"); - Ok(()) } diff --git a/easytier/src/peers/acl_filter.rs b/easytier/src/peers/acl_filter.rs new file mode 100644 index 000000000..868734585 --- /dev/null +++ b/easytier/src/peers/acl_filter.rs @@ -0,0 +1,289 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::sync::atomic::Ordering; +use std::{ + net::IpAddr, + sync::{atomic::AtomicBool, Arc}, +}; + +use arc_swap::ArcSwap; +use pnet::packet::ipv6::Ipv6Packet; +use pnet::packet::{ + ip::IpNextHeaderProtocols, ipv4::Ipv4Packet, tcp::TcpPacket, udp::UdpPacket, Packet as _, +}; + +use crate::proto::acl::{AclStats, Protocol}; +use crate::tunnel::packet_def::PacketType; +use crate::{ + common::acl_processor::{AclProcessor, AclResult, AclStatKey, AclStatType, PacketInfo}, + proto::acl::{Acl, Action, ChainType}, + tunnel::packet_def::ZCPacket, +}; + +/// ACL filter that can be inserted into the packet processing pipeline +/// Optimized with lock-free hot reloading via atomic processor replacement +pub struct AclFilter { + // Use ArcSwap for lock-free atomic replacement during hot reload + acl_processor: ArcSwap, + acl_enabled: Arc, +} + +impl AclFilter { + pub fn new() -> Self { + Self { + acl_processor: ArcSwap::from(Arc::new(AclProcessor::new(Acl::default()))), + acl_enabled: Arc::new(AtomicBool::new(false)), + } + } + + /// Hot reload ACL rules by creating a new processor instance + /// Preserves connection tracking and rate limiting state across reloads + /// Now lock-free and doesn't require &mut self! + pub fn reload_rules(&self, acl_config: Option<&Acl>) { + let Some(acl_config) = acl_config else { + self.acl_enabled.store(false, Ordering::Relaxed); + return; + }; + + // Get current processor to extract shared state + let current_processor = self.acl_processor.load(); + let (conn_track, rate_limiters, stats) = current_processor.get_shared_state(); + + // Create new processor with preserved state + let new_processor = AclProcessor::new_with_shared_state( + acl_config.clone(), + Some(conn_track), + Some(rate_limiters), + Some(stats), + ); + + // Atomic replacement - this is completely lock-free! + self.acl_processor.store(Arc::new(new_processor)); + self.acl_enabled.store(true, Ordering::Relaxed); + + tracing::info!("ACL rules hot reloaded with preserved state (lock-free)"); + } + + /// Get current processor for processing packets + fn get_processor(&self) -> Arc { + self.acl_processor.load_full() + } + + pub fn get_stats(&self) -> AclStats { + let processor = self.get_processor(); + let global_stats = processor.get_stats(); + let (conn_track, _, _) = processor.get_shared_state(); + let rules_stats = processor.get_rules_stats(); + + AclStats { + global: global_stats.into_iter().map(|(k, v)| (k, v)).collect(), + conn_track: conn_track.iter().map(|x| x.value().clone()).collect(), + rules: rules_stats, + } + } + + /// Extract packet information for ACL processing + fn extract_packet_info(&self, packet: &ZCPacket) -> Option { + let payload = packet.payload(); + + let src_ip; + let dst_ip; + let src_port; + let dst_port; + let protocol; + + let ipv4_packet = Ipv4Packet::new(payload)?; + if ipv4_packet.get_version() == 4 { + src_ip = IpAddr::V4(ipv4_packet.get_source()); + dst_ip = IpAddr::V4(ipv4_packet.get_destination()); + protocol = ipv4_packet.get_next_level_protocol(); + + (src_port, dst_port) = match protocol { + IpNextHeaderProtocols::Tcp => { + let tcp_packet = TcpPacket::new(ipv4_packet.payload())?; + ( + Some(tcp_packet.get_source()), + Some(tcp_packet.get_destination()), + ) + } + IpNextHeaderProtocols::Udp => { + let udp_packet = UdpPacket::new(ipv4_packet.payload())?; + ( + Some(udp_packet.get_source()), + Some(udp_packet.get_destination()), + ) + } + _ => (None, None), + }; + } else if ipv4_packet.get_version() == 6 { + let ipv6_packet = Ipv6Packet::new(payload)?; + src_ip = IpAddr::V6(ipv6_packet.get_source()); + dst_ip = IpAddr::V6(ipv6_packet.get_destination()); + protocol = ipv6_packet.get_next_header(); + + (src_port, dst_port) = match protocol { + IpNextHeaderProtocols::Tcp => { + let tcp_packet = TcpPacket::new(ipv6_packet.payload())?; + ( + Some(tcp_packet.get_source()), + Some(tcp_packet.get_destination()), + ) + } + IpNextHeaderProtocols::Udp => { + let udp_packet = UdpPacket::new(ipv6_packet.payload())?; + ( + Some(udp_packet.get_source()), + Some(udp_packet.get_destination()), + ) + } + _ => (None, None), + }; + } else { + return None; + } + + let acl_protocol = match protocol { + IpNextHeaderProtocols::Tcp => Protocol::Tcp, + IpNextHeaderProtocols::Udp => Protocol::Udp, + IpNextHeaderProtocols::Icmp => Protocol::Icmp, + IpNextHeaderProtocols::Icmpv6 => Protocol::IcmPv6, + _ => Protocol::Unspecified, + }; + + Some(PacketInfo { + src_ip, + dst_ip, + src_port, + dst_port, + protocol: acl_protocol, + packet_size: payload.len(), + }) + } + + /// Process ACL result and log if needed + fn handle_acl_result( + &self, + result: &AclResult, + packet_info: &PacketInfo, + chain_type: ChainType, + processor: &AclProcessor, + ) { + if result.should_log { + if let Some(ref log_context) = result.log_context { + let log_message = log_context.to_message(); + tracing::info!( + src_ip = %packet_info.src_ip, + dst_ip = %packet_info.dst_ip, + src_port = packet_info.src_port, + dst_port = packet_info.dst_port, + protocol = ?packet_info.protocol, + action = ?result.action, + rule = result.matched_rule_str().as_deref().unwrap_or("unknown"), + chain_type = ?chain_type, + "ACL: {}", log_message + ); + } + } + + // Update global statistics in the ACL processor + match result.action { + Action::Allow => { + processor.increment_stat(AclStatKey::PacketsAllowed); + processor.increment_stat(AclStatKey::from_chain_and_action( + chain_type, + AclStatType::Allowed, + )); + tracing::trace!("ACL: Packet allowed"); + } + Action::Drop => { + processor.increment_stat(AclStatKey::PacketsDropped); + processor.increment_stat(AclStatKey::from_chain_and_action( + chain_type, + AclStatType::Dropped, + )); + tracing::debug!("ACL: Packet dropped"); + } + Action::Noop => { + processor.increment_stat(AclStatKey::PacketsNoop); + processor.increment_stat(AclStatKey::from_chain_and_action( + chain_type, + AclStatType::Noop, + )); + tracing::trace!("ACL: No operation"); + } + } + + // Track total packets processed per chain + processor.increment_stat(AclStatKey::from_chain_and_action( + chain_type, + AclStatType::Total, + )); + processor.increment_stat(AclStatKey::PacketsTotal); + } + + /// Common ACL processing logic + pub fn process_packet_with_acl( + &self, + packet: &ZCPacket, + is_in: bool, + my_ipv4: Option, + my_ipv6: Option, + ) -> bool { + if !self.acl_enabled.load(Ordering::Relaxed) { + return true; + } + + if packet.peer_manager_header().unwrap().packet_type != PacketType::Data as u8 { + return true; + } + + // Extract packet information + let packet_info = match self.extract_packet_info(packet) { + Some(info) => info, + None => { + tracing::warn!( + "Failed to extract packet info from {:?} packet, header: {:?}", + if is_in { "inbound" } else { "outbound" }, + packet.peer_manager_header() + ); + // allow all unknown packets + return true; + } + }; + + let chain_type = if is_in { + if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED) + || packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED) + { + ChainType::Inbound + } else { + ChainType::Forward + } + } else { + ChainType::Outbound + }; + + // Get current processor atomically + let processor = self.get_processor(); + + // Process through ACL rules + let acl_result = processor.process_packet(&packet_info, chain_type); + + self.handle_acl_result(&acl_result, &packet_info, chain_type, &processor); + + // Check if packet should be allowed + match acl_result.action { + Action::Allow | Action::Noop => true, + Action::Drop => { + tracing::trace!( + "ACL: Dropping {:?} packet from {} to {}, chain_type: {:?}", + packet_info.protocol, + packet_info.src_ip, + packet_info.dst_ip, + chain_type, + ); + + false + } + } + } +} diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index df69e36e0..479b38c2e 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -25,7 +25,6 @@ use crate::{ error::Error, global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent, NetworkIdentity}, join_joinset_background, - stun::MockStunInfoCollector, token_bucket::TokenBucket, PeerId, }, @@ -33,7 +32,7 @@ use crate::{ peers::route_trait::{Route, RouteInterface}, proto::{ cli::{ForeignNetworkEntryPb, ListForeignNetworkResponse, PeerInfo}, - common::{LimiterConfig, NatType}, + common::LimiterConfig, peer_rpc::DirectConnectorRpcServer, }, tunnel::packet_def::{PacketType, ZCPacket}, @@ -159,9 +158,8 @@ impl ForeignNetworkEntry { config.set_hostname(Some(format!("PublicServer_{}", global_ctx.get_hostname()))); let foreign_global_ctx = Arc::new(GlobalCtx::new(config)); - foreign_global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector { - udp_nat_type: NatType::Unknown, - })); + foreign_global_ctx + .replace_stun_info_collector(Box::new(global_ctx.get_stun_info_collector().clone())); let mut feature_flag = global_ctx.get_feature_flags(); feature_flag.is_public_server = true; diff --git a/easytier/src/peers/mod.rs b/easytier/src/peers/mod.rs index c3d9e1a75..fcccfe0c1 100644 --- a/easytier/src/peers/mod.rs +++ b/easytier/src/peers/mod.rs @@ -1,5 +1,6 @@ mod graph_algo; +pub mod acl_filter; pub mod peer; // pub mod peer_conn; pub mod peer_conn; diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 91cabf997..9507aebc0 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -81,12 +81,14 @@ impl PeerRpcManagerTransport for RpcTransport { async fn send(&self, mut msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> { let peers = self.peers.upgrade().ok_or(Error::Unknown)?; - // NOTE: if route info is not exchanged, this will return error. treat it as need relay - if !peers - .need_relay_by_foreign_network(dst_peer_id) + // NOTE: if route info is not exchanged, this will return None. treat it as public server. + let is_dst_peer_public_server = peers + .get_route_peer_info(dst_peer_id) .await - .unwrap_or(true) - { + .and_then(|x| x.feature_flag.map(|x| x.is_public_server)) + // if dst is directly connected, it's must not public server + .unwrap_or(!peers.has_peer(dst_peer_id)); + if !is_dst_peer_public_server { self.encryptor .encrypt(&mut msg) .with_context(|| "encrypt failed")?; @@ -573,6 +575,8 @@ impl PeerManager { let foreign_mgr = self.foreign_network_manager.clone(); let encryptor = self.encryptor.clone(); let compress_algo = self.data_compress_algo; + let acl_filter = self.global_ctx.get_acl_filter().clone(); + let global_ctx = self.global_ctx.clone(); self.tasks.lock().await.spawn(async move { tracing::trace!("start_peer_recv"); while let Ok(ret) = recv_packet_from_chan(&mut recv).await { @@ -631,6 +635,15 @@ impl PeerManager { continue; } + if !acl_filter.process_packet_with_acl( + &ret, + true, + global_ctx.get_ipv4().map(|x| x.address()), + global_ctx.get_ipv6().map(|x| x.address()), + ) { + continue; + } + let mut processed = false; let mut zc_packet = Some(ret); let mut idx = 0; @@ -845,6 +858,14 @@ impl PeerManager { } async fn run_nic_packet_process_pipeline(&self, data: &mut ZCPacket) { + if !self + .global_ctx + .get_acl_filter() + .process_packet_with_acl(data, false, None, None) + { + return; + } + for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() { let _ = pipeline.try_process_packet_from_nic(data).await; } diff --git a/easytier/src/peers/rpc_service.rs b/easytier/src/peers/rpc_service.rs index 9e588944d..e9913cca2 100644 --- a/easytier/src/peers/rpc_service.rs +++ b/easytier/src/peers/rpc_service.rs @@ -2,10 +2,10 @@ use std::sync::Arc; use crate::proto::{ cli::{ - DumpRouteRequest, DumpRouteResponse, ListForeignNetworkRequest, ListForeignNetworkResponse, - ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest, - ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, - ShowNodeInfoRequest, ShowNodeInfoResponse, + AclManageRpc, DumpRouteRequest, DumpRouteResponse, GetAclStatsRequest, GetAclStatsResponse, + ListForeignNetworkRequest, ListForeignNetworkResponse, ListGlobalForeignNetworkRequest, + ListGlobalForeignNetworkResponse, ListPeerRequest, ListPeerResponse, ListRouteRequest, + ListRouteResponse, PeerInfo, PeerManageRpc, ShowNodeInfoRequest, ShowNodeInfoResponse, }, rpc_types::{self, controller::BaseController}, }; @@ -134,3 +134,23 @@ impl PeerManageRpc for PeerManagerRpcService { }) } } + +#[async_trait::async_trait] +impl AclManageRpc for PeerManagerRpcService { + type Controller = BaseController; + + async fn get_acl_stats( + &self, + _: BaseController, + _request: GetAclStatsRequest, + ) -> Result { + let acl_stats = self + .peer_manager + .get_global_ctx() + .get_acl_filter() + .get_stats(); + Ok(GetAclStatsResponse { + acl_stats: Some(acl_stats), + }) + } +} diff --git a/easytier/src/proto/acl.proto b/easytier/src/proto/acl.proto new file mode 100644 index 000000000..393fc74f1 --- /dev/null +++ b/easytier/src/proto/acl.proto @@ -0,0 +1,127 @@ +syntax = "proto3"; + +import "common.proto"; + +package acl; + +// Enhanced protocol enum with more granular options +enum Protocol { + Unspecified = 0; + TCP = 1; + UDP = 2; + ICMP = 3; + ICMPv6 = 4; + Any = 5; +} + +enum Action { + Noop = 0; + Allow = 1; + Drop = 2; // Silent drop (no response) +} + +enum ChainType { + UnspecifiedChain = 0; + // send to this node + Inbound = 1; + // send from this node + Outbound = 2; + // subnet proxy + Forward = 3; +} + +// Time-based access control +message TimeWindow { + // Days of week: 0=Sunday, 1=Monday, ..., 6=Saturday + repeated uint32 days_of_week = 1; + // Time in minutes from midnight (0-1439) + uint32 start_time = 2; + uint32 end_time = 3; + // Timezone offset in minutes from UTC + int32 timezone_offset = 4; +} + +// Enhanced rule with priority and metadata +message Rule { + // Rule identification and metadata + string name = 1; // Human-readable rule name + string description = 2; // Rule description + uint32 priority = 3; // Higher number = higher priority (0-65535) + bool enabled = 4; // Rule enabled/disabled state + + // Core matching criteria + Protocol protocol = 5; + repeated string ports = 6; + repeated string source_ips = 7; // Source IP ranges + repeated string destination_ips = 8; // Destination IP ranges + + // Enhanced matching criteria + repeated string source_ports = 9; // Source port range + + // Action and logging + Action action = 10; + + // Rate limiting (packets per second) + uint32 rate_limit = 11; // 0 = no limit + uint32 burst_limit = 12; // Burst allowance + + // Connection tracking + bool stateful = 13; // Enable connection tracking +} + +// Rule chain with metadata and optimization hints +message Chain { + // Chain identification + string name = 1; // Human-readable chain name + ChainType chain_type = 2; + string description = 3; // Chain description + bool enabled = 4; // Chain enabled/disabled state + + // Rules in priority order (highest priority first) + repeated Rule rules = 5; + + // Default action when no rules match + Action default_action = 6; +} + +message AclV1 { repeated Chain chains = 1; } + +enum ConnState { + New = 0; + Established = 1; + Related = 2; + Invalid = 3; +} + +// Connection tracking entry for stateful ACLs +message ConnTrackEntry { + common.SocketAddr src_addr = 1; + common.SocketAddr dst_addr = 2; + Protocol protocol = 3; // IP protocol number (e.g., 6 = TCP, 17 = UDP) + ConnState state = 4; + uint64 created_at = 5; // Unix timestamp (seconds) + uint64 last_seen = 6; // Unix timestamp (seconds) + uint64 packet_count = 7; + uint64 byte_count = 8; +} + +// Top-level ACL configuration +message Acl { + AclV1 acl_v1 = 2; +} + +message StatItem { + uint64 packet_count = 1; + uint64 byte_count = 2; +} + +message RuleStats { + Rule rule = 1; + StatItem stat = 2; +} + +message AclStats { + repeated RuleStats rules = 1; + repeated ConnTrackEntry conn_track = 2; + map global = 3; +} diff --git a/easytier/src/proto/acl.rs b/easytier/src/proto/acl.rs new file mode 100644 index 000000000..6948ba3fc --- /dev/null +++ b/easytier/src/proto/acl.rs @@ -0,0 +1,95 @@ +use std::fmt::Display; + +include!(concat!(env!("OUT_DIR"), "/acl.rs")); + +impl Display for ConnTrackEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let src = self + .src_addr + .as_ref() + .map(|a| a.to_string()) + .unwrap_or_else(|| "-".to_string()); + let dst = self + .dst_addr + .as_ref() + .map(|a| a.to_string()) + .unwrap_or_else(|| "-".to_string()); + let last_seen = chrono::DateTime::::from_timestamp(self.last_seen as i64, 0) + .unwrap() + .with_timezone(&chrono::Local); + let created_at = chrono::DateTime::::from_timestamp(self.created_at as i64, 0) + .unwrap() + .with_timezone(&chrono::Local); + write!( + f, + "[src: {}, dst: {}, proto: {:?}, state: {:?}, pkts: {}, bytes: {}, created: {}, last_seen: {}]", + src, + dst, + Protocol::try_from(self.protocol).unwrap_or(Protocol::Unspecified), + ConnState::try_from(self.state).unwrap_or(ConnState::Invalid), + self.packet_count, + self.byte_count, + created_at, + last_seen + ) + } +} + +impl Display for Rule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[name: '{}', prio: {}, action: {:?}, enabled: {}, proto: {:?}, ports: {:?}, src_ports: {:?}, src_ips: {:?}, dst_ips: {:?}, stateful: {}, rate: {}, burst: {}]", + self.name, + self.priority, + Action::try_from(self.action).unwrap_or(Action::Noop), + self.enabled, + Protocol::try_from(self.protocol).unwrap_or(Protocol::Unspecified), + self.ports, + self.source_ports, + self.source_ips, + self.destination_ips, + self.stateful, + self.rate_limit, + self.burst_limit + ) + } +} + +impl Display for StatItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[pkts: {}, bytes: {}]", + self.packet_count, self.byte_count + ) + } +} + +impl Display for AclStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "AclStats:")?; + writeln!(f, " Global:")?; + for (k, v) in &self.global { + writeln!(f, " {}: {}", k, v)?; + } + writeln!(f, " ConnTrack:")?; + for entry in &self.conn_track { + writeln!(f, " {}", entry)?; + } + writeln!(f, " Rules:")?; + for rule_stat in &self.rules { + if let Some(rule) = &rule_stat.rule { + write!(f, " {} ", rule)?; + } else { + write!(f, " ")?; + } + if let Some(stat) = &rule_stat.stat { + writeln!(f, "{}", stat)?; + } else { + writeln!(f)?; + } + } + Ok(()) + } +} diff --git a/easytier/src/proto/cli.proto b/easytier/src/proto/cli.proto index c73205d86..847e48afb 100644 --- a/easytier/src/proto/cli.proto +++ b/easytier/src/proto/cli.proto @@ -2,6 +2,7 @@ syntax = "proto3"; import "common.proto"; import "peer_rpc.proto"; +import "acl.proto"; package cli; @@ -251,3 +252,13 @@ service TcpProxyRpc { rpc ListTcpProxyEntry(ListTcpProxyEntryRequest) returns (ListTcpProxyEntryResponse); } + +message GetAclStatsRequest {} + +message GetAclStatsResponse { + acl.AclStats acl_stats = 1; +} + +service AclManageRpc { + rpc GetAclStats(GetAclStatsRequest) returns (GetAclStatsResponse); +} diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index c6378e4b3..ad9da5b65 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -18,7 +18,8 @@ message FlagsInConfig { bool disable_p2p = 11; bool relay_all_peer_rpc = 12; bool disable_udp_hole_punching = 13; - // string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 instead + // string ipv6_listener = 14; [deprecated = true]; use -l udp://[::]:12345 + // instead bool multi_thread = 15; CompressionAlgoPb data_compress_algo = 16; bool bind_device = 17; @@ -144,6 +145,13 @@ message Ipv6Inet { uint32 network_length = 2; } +message IpInet { + oneof ip { + Ipv4Inet ipv4 = 1; + Ipv6Inet ipv6 = 2; + }; +} + message Url { string url = 1; } message SocketAddr { @@ -173,7 +181,7 @@ message PeerFeatureFlag { bool is_public_server = 1; bool avoid_relay_data = 2; bool kcp_input = 3; - bool no_relay_kcp = 4; + bool no_relay_kcp = 4; } enum SocketType { @@ -182,17 +190,17 @@ enum SocketType { } message PortForwardConfigPb { - SocketAddr bind_addr = 1; - SocketAddr dst_addr = 2; - SocketType socket_type = 3; + SocketAddr bind_addr = 1; + SocketAddr dst_addr = 2; + SocketType socket_type = 3; } -message ProxyDstInfo { - SocketAddr dst_addr = 1; -} +message ProxyDstInfo { SocketAddr dst_addr = 1; } message LimiterConfig { - optional uint64 burst_rate = 1; // default 1 means no burst (capacity is same with bps) + optional uint64 burst_rate = + 1; // default 1 means no burst (capacity is same with bps) optional uint64 bps = 2; // default 0 means no limit (unit is B/s) - optional uint64 fill_duration_ms = 3; // default 10ms, the period to fill the bucket + optional uint64 fill_duration_ms = + 3; // default 10ms, the period to fill the bucket } diff --git a/easytier/src/proto/common.rs b/easytier/src/proto/common.rs index 07d3d1c23..e30a7d7ea 100644 --- a/easytier/src/proto/common.rs +++ b/easytier/src/proto/common.rs @@ -1,4 +1,7 @@ -use std::{fmt, str::FromStr}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; use anyhow::Context; @@ -166,6 +169,43 @@ impl FromStr for Ipv6Inet { } } +impl From for IpInet { + fn from(value: cidr::IpInet) -> Self { + match value { + cidr::IpInet::V4(v4) => IpInet { + ip: Some(ip_inet::Ip::Ipv4(Ipv4Inet::from(v4))), + }, + cidr::IpInet::V6(v6) => IpInet { + ip: Some(ip_inet::Ip::Ipv6(Ipv6Inet::from(v6))), + }, + } + } +} + +impl From for cidr::IpInet { + fn from(value: IpInet) -> Self { + match value.ip { + Some(ip_inet::Ip::Ipv4(v4)) => cidr::IpInet::V4(v4.into()), + Some(ip_inet::Ip::Ipv6(v6)) => cidr::IpInet::V6(v6.into()), + None => panic!("IpInet is None"), + } + } +} + +impl Display for IpInet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", cidr::IpInet::from(self.clone())) + } +} + +impl FromStr for IpInet { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(IpInet::from(cidr::IpInet::from_str(s)?)) + } +} + impl From for Url { fn from(value: url::Url) -> Self { Url { diff --git a/easytier/src/proto/mod.rs b/easytier/src/proto/mod.rs index 51fe99aec..fd8b45524 100644 --- a/easytier/src/proto/mod.rs +++ b/easytier/src/proto/mod.rs @@ -1,6 +1,7 @@ pub mod rpc_impl; pub mod rpc_types; +pub mod acl; pub mod cli; pub mod common; pub mod error; diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 901d67dfc..ba407ea5f 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -1328,3 +1328,183 @@ async fn avoid_tunnel_loop_back_to_virtual_network() { drop_insts(insts).await; } + +#[tokio::test] +#[serial_test::serial] +pub async fn acl_rule_test_inbound() { + use crate::tunnel::{ + common::tests::_tunnel_pingpong_netns, + tcp::{TcpTunnelConnector, TcpTunnelListener}, + udp::{UdpTunnelConnector, UdpTunnelListener}, + }; + use rand::Rng; + let insts = init_three_node("udp").await; + + // 构造 ACL 配置 + use crate::proto::acl::*; + let mut acl = Acl::default(); + let mut acl_v1 = AclV1::default(); + + let mut chain = Chain::default(); + chain.name = "test_inbound".to_string(); + chain.chain_type = ChainType::Inbound as i32; + chain.enabled = true; + + // 禁止 8080 + let mut deny_rule = Rule::default(); + deny_rule.name = "deny_8080".to_string(); + deny_rule.priority = 200; + deny_rule.enabled = true; + deny_rule.action = Action::Drop as i32; + deny_rule.protocol = Protocol::Any as i32; + deny_rule.ports = vec!["8080".to_string()]; + chain.rules.push(deny_rule); + + // 允许其他 + let mut allow_rule = Rule::default(); + allow_rule.name = "allow_all".to_string(); + allow_rule.priority = 100; + allow_rule.enabled = true; + allow_rule.action = Action::Allow as i32; + allow_rule.protocol = Protocol::Any as i32; + allow_rule.stateful = true; + chain.rules.push(allow_rule); + + // 禁止 src ip 为 10.144.144.2 的流量 + let mut deny_rule = Rule::default(); + deny_rule.name = "deny_10.144.144.2".to_string(); + deny_rule.priority = 200; + deny_rule.enabled = true; + deny_rule.action = Action::Drop as i32; + deny_rule.protocol = Protocol::Any as i32; + deny_rule.source_ips = vec!["10.144.144.2/32".to_string()]; + chain.rules.push(deny_rule); + + acl_v1.chains.push(chain); + acl.acl_v1 = Some(acl_v1); + + // convert acl to to toml + let acl_toml = toml::to_string(&acl).unwrap(); + println!("ACL TOML: {}", acl_toml); + + insts[2] + .get_global_ctx() + .get_acl_filter() + .reload_rules(Some(&acl)); + + // TCP 测试部分 + { + // 2. 在 inst2 上监听 8080 和 8081 + let listener_8080 = TcpTunnelListener::new("tcp://0.0.0.0:8080".parse().unwrap()); + let listener_8081 = TcpTunnelListener::new("tcp://0.0.0.0:8081".parse().unwrap()); + let listener_8082 = TcpTunnelListener::new("tcp://0.0.0.0:8082".parse().unwrap()); + + // 3. inst1 作为客户端,尝试连接 inst2 的 8080(应被拒绝)和 8081(应被允许) + let connector_8080 = + TcpTunnelConnector::new(format!("tcp://{}:8080", "10.144.144.3").parse().unwrap()); + let connector_8081 = + TcpTunnelConnector::new(format!("tcp://{}:8081", "10.144.144.3").parse().unwrap()); + let connector_8082 = + TcpTunnelConnector::new(format!("tcp://{}:8082", "10.144.144.3").parse().unwrap()); + + // 4. 构造测试数据 + let mut buf = vec![0; 32]; + rand::thread_rng().fill(&mut buf[..]); + + // 5. 8081 应该可以 pingpong 成功 + _tunnel_pingpong_netns( + listener_8081, + connector_8081, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf.clone(), + ) + .await; + + // 6. 8080 应该连接失败(被 ACL 拦截) + let result = tokio::time::timeout( + std::time::Duration::from_millis(200), + _tunnel_pingpong_netns( + listener_8080, + connector_8080, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf.clone(), + ), + ) + .await; + + assert!(result.is_err(), "TCP 连接 8080 应被 ACL 拦截,不能成功"); + + // 7. 从 10.144.144.2 连接 8082 应该连接失败(被 ACL 拦截) + let result = tokio::time::timeout( + std::time::Duration::from_millis(200), + _tunnel_pingpong_netns( + listener_8082, + connector_8082, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_b".into())), + buf.clone(), + ), + ) + .await; + + assert!(result.is_err(), "TCP 连接 8082 应被 ACL 拦截,不能成功"); + + let stats = insts[2].get_global_ctx().get_acl_filter().get_stats(); + println!("stats: {:?}", stats); + } + + // UDP 测试部分 + { + // 1. 在 inst2 上监听 UDP 8080 和 8081 + let listener_8080 = UdpTunnelListener::new("udp://0.0.0.0:8080".parse().unwrap()); + let listener_8081 = UdpTunnelListener::new("udp://0.0.0.0:8081".parse().unwrap()); + + // 2. inst1 作为客户端,尝试连接 inst2 的 8080(应被拒绝)和 8081(应被允许) + let connector_8080 = + UdpTunnelConnector::new(format!("udp://{}:8080", "10.144.144.3").parse().unwrap()); + let connector_8081 = + UdpTunnelConnector::new(format!("udp://{}:8081", "10.144.144.3").parse().unwrap()); + + // 3. 构造测试数据 + let mut buf = vec![0; 32]; + rand::thread_rng().fill(&mut buf[..]); + + // 4. 8081 应该可以 pingpong 成功 + _tunnel_pingpong_netns( + listener_8081, + connector_8081, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf.clone(), + ) + .await; + + // 5. 8080 应该连接失败(被 ACL 拦截) + let result = tokio::time::timeout( + std::time::Duration::from_millis(200), + _tunnel_pingpong_netns( + listener_8080, + connector_8080, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + buf.clone(), + ), + ) + .await; + + assert!(result.is_err(), "UDP 连接 8080 应被 ACL 拦截,不能成功"); + + let stats = insts[2].get_global_ctx().get_acl_filter().get_stats(); + println!("stats: {}", stats); + } + + // remove acl, 8080 should succ + insts[2] + .get_global_ctx() + .get_acl_filter() + .reload_rules(None); + + drop_insts(insts).await; +} diff --git a/easytier/src/utils.rs b/easytier/src/utils.rs index 98b487d3d..5cf5ea751 100644 --- a/easytier/src/utils.rs +++ b/easytier/src/utils.rs @@ -26,6 +26,7 @@ pub fn init_logger( config: impl LoggingConfigLoader, need_reload: bool, ) -> Result, anyhow::Error> { + return Ok(None); let file_config = config.get_file_logger_config(); let file_level = file_config .level diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a38dfdfe3..7e30b830e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,17 +15,17 @@ importers: specifier: 2.0.0 version: 2.0.0 '@tauri-apps/plugin-clipboard-manager': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.3.0 + version: 2.3.0 '@tauri-apps/plugin-os': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.3.0 + version: 2.3.0 '@tauri-apps/plugin-process': - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.3.0 + version: 2.3.0 '@tauri-apps/plugin-shell': - specifier: 2.0.1 - version: 2.0.1 + specifier: 2.3.0 + version: 2.3.0 '@vueuse/core': specifier: ^11.2.0 version: 11.2.0(vue@3.5.12(typescript@5.6.3)) @@ -64,11 +64,11 @@ importers: specifier: 4.3.3 version: 4.3.3 '@tauri-apps/api': - specifier: 2.1.0 - version: 2.1.0 + specifier: 2.7.0 + version: 2.7.0 '@tauri-apps/cli': - specifier: 2.1.0 - version: 2.1.0 + specifier: 2.7.1 + version: 2.7.1 '@types/default-gateway': specifier: ^7.2.2 version: 7.2.2 @@ -921,8 +921,12 @@ packages: resolution: {integrity: sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==} engines: {node: '>= 16'} - '@intlify/message-compiler@12.0.0-alpha.2': - resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==} + '@intlify/message-compiler@12.0.0-alpha.3': + resolution: {integrity: sha512-mDDTN3gfYOHhBnpnlby19UHyvMaOnzdlpsIrxUfs44R/vCATfn8pMOkE8PXD2t410xkocEj3FpDcC9XC/0v4Dg==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.4': + resolution: {integrity: sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==} engines: {node: '>= 16'} '@intlify/message-compiler@9.14.4': @@ -933,8 +937,12 @@ packages: resolution: {integrity: sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==} engines: {node: '>= 16'} - '@intlify/shared@12.0.0-alpha.2': - resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==} + '@intlify/shared@12.0.0-alpha.3': + resolution: {integrity: sha512-ryaNYBvxQjyJUmVuBBg+HHUsmGnfxcEUPR0NCeG4/K9N2qtyFE35C80S15IN6iYFE2MGWLN7HfOSyg0MXZIc9w==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.4': + resolution: {integrity: sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==} engines: {node: '>= 16'} '@intlify/shared@9.14.4': @@ -1291,88 +1299,94 @@ packages: resolution: {integrity: sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==} engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} - '@tauri-apps/api@2.1.0': - resolution: {integrity: sha512-1w/JygZOiUtdOU7qart78MaB4/qayZ2heB793KhbZRS7I9q4sxXcXaB7He6uFlprD8w5TI9P8HCuEByCvWRtfw==} + '@tauri-apps/api@2.7.0': + resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} - '@tauri-apps/cli-darwin-arm64@2.1.0': - resolution: {integrity: sha512-ESc6J6CE8hl1yKH2vJ+ALF+thq4Be+DM1mvmTyUCQObvezNCNhzfS6abIUd3ou4x5RGH51ouiANeT3wekU6dCw==} + '@tauri-apps/cli-darwin-arm64@2.7.1': + resolution: {integrity: sha512-j2NXQN6+08G03xYiyKDKqbCV2Txt+hUKg0a8hYr92AmoCU8fgCjHyva/p16lGFGUG3P2Yu0xiNe1hXL9ZuRMzA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.1.0': - resolution: {integrity: sha512-TasHS442DFs8cSH2eUQzuDBXUST4ECjCd0yyP+zZzvAruiB0Bg+c8A+I/EnqCvBQ2G2yvWLYG8q/LI7c87A5UA==} + '@tauri-apps/cli-darwin-x64@2.7.1': + resolution: {integrity: sha512-CdYAefeM35zKsc91qIyKzbaO7FhzTyWKsE8hj7tEJ1INYpoh1NeNNyL/NSEA3Nebi5ilugioJ5tRK8ZXG8y3gw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.1.0': - resolution: {integrity: sha512-aP7ZBGNL4ny07Cbb6kKpUOSrmhcIK2KhjviTzYlh+pPhAptxnC78xQGD3zKQkTi2WliJLPmBYbOHWWQa57lQ9w==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.7.1': + resolution: {integrity: sha512-dnvyJrTA1UJxJjQ8q1N/gWomjP8Twij1BUQu2fdcT3OPpqlrbOk5R1yT0oD/721xoKNjroB5BXCsmmlykllxNg==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.1.0': - resolution: {integrity: sha512-ZTdgD5gLeMCzndMT2f358EkoYkZ5T+Qy6zPzU+l5vv5M7dHVN9ZmblNAYYXmoOuw7y+BY4X/rZvHV9pcGrcanQ==} + '@tauri-apps/cli-linux-arm64-gnu@2.7.1': + resolution: {integrity: sha512-FtBW6LJPNRTws3qyUc294AqCWU91l/H0SsFKq6q4Q45MSS4x6wxLxou8zB53tLDGEPx3JSoPLcDaSfPlSbyujQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-arm64-musl@2.1.0': - resolution: {integrity: sha512-NzwqjUCilhnhJzusz3d/0i0F1GFrwCQbkwR6yAHUxItESbsGYkZRJk0yMEWkg3PzFnyK4cWTlQJMEU52TjhEzA==} + '@tauri-apps/cli-linux-arm64-musl@2.7.1': + resolution: {integrity: sha512-/HXY0t4FHkpFzjeYS5c16mlA6z0kzn5uKLWptTLTdFSnYpr8FCnOP4Sdkvm2TDQPF2ERxXtNCd+WR/jQugbGnA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tauri-apps/cli-linux-x64-gnu@2.1.0': - resolution: {integrity: sha512-TyiIpMEtZxNOQmuFyfJwaaYbg3movSthpBJLIdPlKxSAB2BW0VWLY3/ZfIxm/G2YGHyREkjJvimzYE0i37PnMA==} + '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': + resolution: {integrity: sha512-GeW5lVI2GhhnaYckiDzstG2j2Jwlud5d2XefRGwlOK+C/bVGLT1le8MNPYK8wgRlpeK8fG1WnJJYD6Ke7YQ8bg==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.7.1': + resolution: {integrity: sha512-DprxKQkPxIPYwUgg+cscpv2lcIUhn2nxEPlk0UeaiV9vATxCXyytxr1gLcj3xgjGyNPlM0MlJyYaPy1JmRg1cA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-linux-x64-musl@2.1.0': - resolution: {integrity: sha512-/dQd0TlaxBdJACrR72DhynWftzHDaX32eBtS5WBrNJ+nnNb+znM3gON6nJ9tSE9jgDa6n1v2BkI/oIDtypfUXw==} + '@tauri-apps/cli-linux-x64-musl@2.7.1': + resolution: {integrity: sha512-KLlq3kOK7OUyDR757c0zQjPULpGZpLhNB0lZmZpHXvoOUcqZoCXJHh4dT/mryWZJp5ilrem5l8o9ngrDo0X1AA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tauri-apps/cli-win32-arm64-msvc@2.1.0': - resolution: {integrity: sha512-NdQJO7SmdYqOcE+JPU7bwg7+odfZMWO6g8xF9SXYCMdUzvM2Gv/AQfikNXz5yS7ralRhNFuW32i5dcHlxh4pDg==} + '@tauri-apps/cli-win32-arm64-msvc@2.7.1': + resolution: {integrity: sha512-dH7KUjKkSypCeWPiainHyXoES3obS+JIZVoSwSZfKq2gWgs48FY3oT0hQNYrWveE+VR4VoR3b/F3CPGbgFvksA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.1.0': - resolution: {integrity: sha512-f5h8gKT/cB8s1ticFRUpNmHqkmaLutT62oFDB7N//2YTXnxst7EpMIn1w+QimxTvTk2gcx6EcW6bEk/y2hZGzg==} + '@tauri-apps/cli-win32-ia32-msvc@2.7.1': + resolution: {integrity: sha512-1oeibfyWQPVcijOrTg709qhbXArjX3x1MPjrmA5anlygwrbByxLBcLXvotcOeULFcnH2FYUMMLLant8kgvwE5A==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.1.0': - resolution: {integrity: sha512-P/+LrdSSb5Xbho1LRP4haBjFHdyPdjWvGgeopL96OVtrFpYnfC+RctB45z2V2XxqFk3HweDDxk266btjttfjGw==} + '@tauri-apps/cli-win32-x64-msvc@2.7.1': + resolution: {integrity: sha512-D7Q9kDObutuirCNLxYQ7KAg2Xxg99AjcdYz/KuMw5HvyEPbkC9Q7JL0vOrQOrHEHxIQ2lYzFOZvKKoC2yyqXcg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.1.0': - resolution: {integrity: sha512-K2VhcKqBhAeS5pNOVdnR/xQRU6jwpgmkSL2ejHXcl0m+kaTggT0WRDQnFtPq6NljA7aE03cvwsbCAoFG7vtkJw==} + '@tauri-apps/cli@2.7.1': + resolution: {integrity: sha512-RcGWR4jOUEl92w3uvI0h61Llkfj9lwGD1iwvDRD2isMrDhOzjeeeVn9aGzeW1jubQ/kAbMYfydcA4BA0Cy733Q==} engines: {node: '>= 10'} hasBin: true '@tauri-apps/plugin-autostart@2.0.0': resolution: {integrity: sha512-NEwOQWVasZ8RczXkMLNJokRDujneuMH/UFA5t84DLkbNZUmiD3G7HZWhgSd1YQ0BFU9h9w+h2B/py3y6bzWg4Q==} - '@tauri-apps/plugin-clipboard-manager@2.0.0': - resolution: {integrity: sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==} + '@tauri-apps/plugin-clipboard-manager@2.3.0': + resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==} - '@tauri-apps/plugin-os@2.0.0': - resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==} + '@tauri-apps/plugin-os@2.3.0': + resolution: {integrity: sha512-dm3bDsMuUngpIQdJ1jaMkMfyQpHyDcaTIKTFaAMHoKeUd+Is3UHO2uzhElr6ZZkfytIIyQtSVnCWdW2Kc58f3g==} - '@tauri-apps/plugin-process@2.0.0': - resolution: {integrity: sha512-OYzi0GnkrF4NAnsHZU7U3tjSoP0PbeAlO7T1Z+vJoBUH9sFQ1NSLqWYWQyf8hcb3gVWe7P1JggjiskO+LST1ug==} + '@tauri-apps/plugin-process@2.3.0': + resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==} - '@tauri-apps/plugin-shell@2.0.1': - resolution: {integrity: sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==} + '@tauri-apps/plugin-shell@2.3.0': + resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==} '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -4404,8 +4418,8 @@ snapshots: '@intlify/bundle-utils@9.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@intlify/message-compiler': 12.0.0-alpha.2 - '@intlify/shared': 12.0.0-alpha.2 + '@intlify/message-compiler': 12.0.0-alpha.3 + '@intlify/shared': 12.0.0-alpha.3 acorn: 8.14.0 escodegen: 2.1.0 estree-walker: 2.0.2 @@ -4431,9 +4445,14 @@ snapshots: '@intlify/shared': 10.0.4 source-map-js: 1.2.1 - '@intlify/message-compiler@12.0.0-alpha.2': + '@intlify/message-compiler@12.0.0-alpha.3': dependencies: - '@intlify/shared': 12.0.0-alpha.2 + '@intlify/shared': 12.0.0-alpha.3 + source-map-js: 1.2.1 + + '@intlify/message-compiler@9.14.4': + dependencies: + '@intlify/shared': 9.14.4 source-map-js: 1.2.1 '@intlify/message-compiler@9.14.4': @@ -4443,7 +4462,9 @@ snapshots: '@intlify/shared@10.0.4': {} - '@intlify/shared@12.0.0-alpha.2': {} + '@intlify/shared@12.0.0-alpha.3': {} + + '@intlify/shared@9.14.4': {} '@intlify/shared@9.14.4': {} @@ -4451,8 +4472,8 @@ snapshots: dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.0)) '@intlify/bundle-utils': 9.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3))) - '@intlify/shared': 12.0.0-alpha.2 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@12.0.0-alpha.2)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3)) + '@intlify/shared': 12.0.0-alpha.3 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@12.0.0-alpha.3)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3)) '@rollup/pluginutils': 5.1.3(rollup@4.24.3) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) @@ -4475,11 +4496,11 @@ snapshots: - typescript - webpack-sources - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@12.0.0-alpha.2)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))': + '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@12.0.0-alpha.3)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))': dependencies: '@babel/parser': 7.26.2 optionalDependencies: - '@intlify/shared': 12.0.0-alpha.2 + '@intlify/shared': 12.0.0-alpha.3 '@vue/compiler-dom': 3.5.12 vue: 3.5.12(typescript@5.6.3) vue-i18n: 10.0.4(vue@3.5.12(typescript@5.6.3)) @@ -4794,70 +4815,74 @@ snapshots: '@tauri-apps/api@2.0.0-rc.0': {} - '@tauri-apps/api@2.1.0': {} + '@tauri-apps/api@2.7.0': {} + + '@tauri-apps/cli-darwin-arm64@2.7.1': + optional: true - '@tauri-apps/cli-darwin-arm64@2.1.0': + '@tauri-apps/cli-darwin-x64@2.7.1': optional: true - '@tauri-apps/cli-darwin-x64@2.1.0': + '@tauri-apps/cli-linux-arm-gnueabihf@2.7.1': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.1.0': + '@tauri-apps/cli-linux-arm64-gnu@2.7.1': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.1.0': + '@tauri-apps/cli-linux-arm64-musl@2.7.1': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.1.0': + '@tauri-apps/cli-linux-riscv64-gnu@2.7.1': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.1.0': + '@tauri-apps/cli-linux-x64-gnu@2.7.1': optional: true - '@tauri-apps/cli-linux-x64-musl@2.1.0': + '@tauri-apps/cli-linux-x64-musl@2.7.1': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.1.0': + '@tauri-apps/cli-win32-arm64-msvc@2.7.1': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.1.0': + '@tauri-apps/cli-win32-ia32-msvc@2.7.1': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.1.0': + '@tauri-apps/cli-win32-x64-msvc@2.7.1': optional: true - '@tauri-apps/cli@2.1.0': + '@tauri-apps/cli@2.7.1': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.1.0 - '@tauri-apps/cli-darwin-x64': 2.1.0 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.1.0 - '@tauri-apps/cli-linux-arm64-gnu': 2.1.0 - '@tauri-apps/cli-linux-arm64-musl': 2.1.0 - '@tauri-apps/cli-linux-x64-gnu': 2.1.0 - '@tauri-apps/cli-linux-x64-musl': 2.1.0 - '@tauri-apps/cli-win32-arm64-msvc': 2.1.0 - '@tauri-apps/cli-win32-ia32-msvc': 2.1.0 - '@tauri-apps/cli-win32-x64-msvc': 2.1.0 + '@tauri-apps/cli-darwin-arm64': 2.7.1 + '@tauri-apps/cli-darwin-x64': 2.7.1 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.7.1 + '@tauri-apps/cli-linux-arm64-gnu': 2.7.1 + '@tauri-apps/cli-linux-arm64-musl': 2.7.1 + '@tauri-apps/cli-linux-riscv64-gnu': 2.7.1 + '@tauri-apps/cli-linux-x64-gnu': 2.7.1 + '@tauri-apps/cli-linux-x64-musl': 2.7.1 + '@tauri-apps/cli-win32-arm64-msvc': 2.7.1 + '@tauri-apps/cli-win32-ia32-msvc': 2.7.1 + '@tauri-apps/cli-win32-x64-msvc': 2.7.1 '@tauri-apps/plugin-autostart@2.0.0': dependencies: - '@tauri-apps/api': 2.1.0 + '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-clipboard-manager@2.0.0': + '@tauri-apps/plugin-clipboard-manager@2.3.0': dependencies: - '@tauri-apps/api': 2.1.0 + '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-os@2.0.0': + '@tauri-apps/plugin-os@2.3.0': dependencies: - '@tauri-apps/api': 2.1.0 + '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-process@2.0.0': + '@tauri-apps/plugin-process@2.3.0': dependencies: - '@tauri-apps/api': 2.1.0 + '@tauri-apps/api': 2.7.0 - '@tauri-apps/plugin-shell@2.0.1': + '@tauri-apps/plugin-shell@2.3.0': dependencies: - '@tauri-apps/api': 2.1.0 + '@tauri-apps/api': 2.7.0 '@tybys/wasm-util@0.9.0': dependencies: diff --git a/tauri-plugin-vpnservice/permissions/autogenerated/reference.md b/tauri-plugin-vpnservice/permissions/autogenerated/reference.md index 57c0709b2..de55072e2 100644 --- a/tauri-plugin-vpnservice/permissions/autogenerated/reference.md +++ b/tauri-plugin-vpnservice/permissions/autogenerated/reference.md @@ -2,6 +2,8 @@ Default permissions for the plugin +#### This default permission set includes the following: + - `allow-ping` - `allow-start-vpn` diff --git a/tauri-plugin-vpnservice/permissions/schemas/schema.json b/tauri-plugin-vpnservice/permissions/schemas/schema.json index 1139bf13a..e6ba364bc 100644 --- a/tauri-plugin-vpnservice/permissions/schemas/schema.json +++ b/tauri-plugin-vpnservice/permissions/schemas/schema.json @@ -49,7 +49,7 @@ "minimum": 1.0 }, "description": { - "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", "type": [ "string", "null" @@ -111,7 +111,7 @@ "type": "string" }, "description": { - "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", "type": [ "string", "null" @@ -297,67 +297,80 @@ { "description": "Enables the ping command without any pre-configured scope.", "type": "string", - "const": "allow-ping" + "const": "allow-ping", + "markdownDescription": "Enables the ping command without any pre-configured scope." }, { "description": "Denies the ping command without any pre-configured scope.", "type": "string", - "const": "deny-ping" + "const": "deny-ping", + "markdownDescription": "Denies the ping command without any pre-configured scope." }, { "description": "Enables the prepare_vpn command without any pre-configured scope.", "type": "string", - "const": "allow-prepare-vpn" + "const": "allow-prepare-vpn", + "markdownDescription": "Enables the prepare_vpn command without any pre-configured scope." }, { "description": "Denies the prepare_vpn command without any pre-configured scope.", "type": "string", - "const": "deny-prepare-vpn" + "const": "deny-prepare-vpn", + "markdownDescription": "Denies the prepare_vpn command without any pre-configured scope." }, { "description": "Enables the registerListener command without any pre-configured scope.", "type": "string", - "const": "allow-registerListener" + "const": "allow-registerListener", + "markdownDescription": "Enables the registerListener command without any pre-configured scope." }, { "description": "Denies the registerListener command without any pre-configured scope.", "type": "string", - "const": "deny-registerListener" + "const": "deny-registerListener", + "markdownDescription": "Denies the registerListener command without any pre-configured scope." }, { "description": "Enables the register_listener command without any pre-configured scope.", "type": "string", - "const": "allow-register-listener" + "const": "allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." }, { "description": "Denies the register_listener command without any pre-configured scope.", "type": "string", - "const": "deny-register-listener" + "const": "deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." }, { "description": "Enables the start_vpn command without any pre-configured scope.", "type": "string", - "const": "allow-start-vpn" + "const": "allow-start-vpn", + "markdownDescription": "Enables the start_vpn command without any pre-configured scope." }, { "description": "Denies the start_vpn command without any pre-configured scope.", "type": "string", - "const": "deny-start-vpn" + "const": "deny-start-vpn", + "markdownDescription": "Denies the start_vpn command without any pre-configured scope." }, { "description": "Enables the stop_vpn command without any pre-configured scope.", "type": "string", - "const": "allow-stop-vpn" + "const": "allow-stop-vpn", + "markdownDescription": "Enables the stop_vpn command without any pre-configured scope." }, { "description": "Denies the stop_vpn command without any pre-configured scope.", "type": "string", - "const": "deny-stop-vpn" + "const": "deny-stop-vpn", + "markdownDescription": "Denies the stop_vpn command without any pre-configured scope." }, { - "description": "Default permissions for the plugin", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`\n- `allow-start-vpn`", "type": "string", - "const": "default" + "const": "default", + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`\n- `allow-start-vpn`" } ] } From 37eb1910dc9494b3791262a7e5467503726f6db1 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sat, 26 Jul 2025 12:48:36 +0800 Subject: [PATCH 07/10] update pipeline files --- .../{workflows => origin_wfs}/build_libs.yml | 0 .github/{workflows => origin_wfs}/ohos.yml | 0 .github/{origin_wfs => workflows}/core.yml | 201 +++++------------- .../{origin_wfs => workflows}/install_rust.sh | 0 4 files changed, 57 insertions(+), 144 deletions(-) rename .github/{workflows => origin_wfs}/build_libs.yml (100%) rename .github/{workflows => origin_wfs}/ohos.yml (100%) rename .github/{origin_wfs => workflows}/core.yml (57%) rename .github/{origin_wfs => workflows}/install_rust.sh (100%) diff --git a/.github/workflows/build_libs.yml b/.github/origin_wfs/build_libs.yml similarity index 100% rename from .github/workflows/build_libs.yml rename to .github/origin_wfs/build_libs.yml diff --git a/.github/workflows/ohos.yml b/.github/origin_wfs/ohos.yml similarity index 100% rename from .github/workflows/ohos.yml rename to .github/origin_wfs/ohos.yml diff --git a/.github/origin_wfs/core.yml b/.github/workflows/core.yml similarity index 57% rename from .github/origin_wfs/core.yml rename to .github/workflows/core.yml index e015624aa..6715f5fde 100644 --- a/.github/origin_wfs/core.yml +++ b/.github/workflows/core.yml @@ -2,9 +2,9 @@ name: EasyTier Core on: push: - branches: ["develop", "main", "releases/**"] + branches: ["dev", "main", "releases/**"] pull_request: - branches: ["develop", "main"] + branches: ["dev", "main"] env: CARGO_TERM_COLOR: always @@ -27,51 +27,10 @@ jobs: 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' + concurrent_skipping: "same_content_newer" + skip_after_successful_duplicate: "true" + cancel_others: "true" paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]' - build_web: - runs-on: ubuntu-latest - needs: pre_job - if: needs.pre_job.outputs.should_skip != 'true' - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install frontend dependencies - run: | - pnpm -r install - pnpm -r --filter "./easytier-web/*" build - - - name: Archive artifact - uses: actions/upload-artifact@v4 - with: - name: easytier-web-dashboard - path: | - easytier-web/frontend/dist/* build: strategy: fail-fast: false @@ -83,50 +42,50 @@ jobs: - TARGET: x86_64-unknown-linux-musl OS: ubuntu-22.04 ARTIFACT_NAME: linux-x86_64 - - TARGET: mips-unknown-linux-musl - OS: ubuntu-22.04 - ARTIFACT_NAME: linux-mips - - TARGET: mipsel-unknown-linux-musl - OS: ubuntu-22.04 - ARTIFACT_NAME: linux-mipsel - - TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested - OS: ubuntu-22.04 - ARTIFACT_NAME: linux-armv7hf - - TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested - OS: ubuntu-22.04 - ARTIFACT_NAME: linux-armv7 - - TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested - OS: ubuntu-22.04 - ARTIFACT_NAME: linux-armhf - - TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested - 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 - - TARGET: aarch64-apple-darwin - OS: macos-latest - ARTIFACT_NAME: macos-aarch64 + # - TARGET: mips-unknown-linux-musl + # OS: ubuntu-22.04 + # ARTIFACT_NAME: linux-mips + # - TARGET: mipsel-unknown-linux-musl + # OS: ubuntu-22.04 + # ARTIFACT_NAME: linux-mipsel + # - TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested + # OS: ubuntu-22.04 + # ARTIFACT_NAME: linux-armv7hf + # - TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested + # OS: ubuntu-22.04 + # ARTIFACT_NAME: linux-armv7 + # - TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested + # OS: ubuntu-22.04 + # ARTIFACT_NAME: linux-armhf + # - TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested + # 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 + # - TARGET: aarch64-apple-darwin + # OS: macos-latest + # ARTIFACT_NAME: macos-aarch64 - TARGET: x86_64-pc-windows-msvc OS: windows-latest ARTIFACT_NAME: windows-x86_64 - - TARGET: aarch64-pc-windows-msvc - OS: windows-latest - ARTIFACT_NAME: windows-arm64 - - TARGET: i686-pc-windows-msvc - OS: windows-latest - ARTIFACT_NAME: windows-i686 - - - TARGET: x86_64-unknown-freebsd - OS: ubuntu-22.04 - ARTIFACT_NAME: freebsd-13.2-x86_64 - BSD_VERSION: 13.2 + # - TARGET: aarch64-pc-windows-msvc + # OS: windows-latest + # ARTIFACT_NAME: windows-arm64 + # - TARGET: i686-pc-windows-msvc + # OS: windows-latest + # ARTIFACT_NAME: windows-i686 + + # - TARGET: x86_64-unknown-freebsd + # OS: ubuntu-22.04 + # ARTIFACT_NAME: freebsd-13.2-x86_64 + # BSD_VERSION: 13.2 runs-on: ${{ matrix.OS }} env: @@ -136,7 +95,6 @@ jobs: OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }} needs: - pre_job - - build_web if: needs.pre_job.outputs.should_skip != 'true' steps: - uses: actions/checkout@v3 @@ -145,14 +103,13 @@ jobs: run: | echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV - - name: Download web artifact - uses: actions/download-artifact@v4 - with: - name: easytier-web-dashboard - path: easytier-web/frontend/dist/ + # - name: Download web artifact + # uses: actions/download-artifact@v4 + # with: + # name: easytier-web-dashboard + # path: easytier-web/frontend/dist/ - name: Cargo cache - if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} uses: actions/cache@v4 with: path: | @@ -167,7 +124,6 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Build Core & Cli - if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} run: | bash ./.github/workflows/install_rust.sh @@ -187,66 +143,26 @@ jobs: cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc else if [[ $OS =~ ^windows.*$ ]]; then - SUFFIX=.exe + SUFFIX=.dll 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 $CORE_FEATURES fi - # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267) - - name: Build Core & Cli (X86_64 FreeBSD) - uses: vmactions/freebsd-vm@v1 - if: ${{ endsWith(matrix.TARGET, 'freebsd') }} - env: - TARGET: ${{ matrix.TARGET }} - with: - envs: TARGET - release: ${{ matrix.BSD_VERSION }} - arch: x86_64 - usesh: true - mem: 6144 - cpu: 4 - run: | - uname -a - echo $SHELL - pwd - ls -lah - whoami - env | sort - - pkg install -y git protobuf llvm-devel sudo curl - curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - . $HOME/.cargo/env - - rustup set auto-self-update disable - - rustup install 1.87 - rustup default 1.87 - - export CC=clang - export CXX=clang++ - export CARGO_TERM_COLOR=always - - 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 --features=mimalloc - - name: Compress run: | mkdir -p ./artifacts/objects/ # windows is the only OS using a different convention for executable file name if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then - SUFFIX=.exe + SUFFIX=.dll cp easytier/third_party/*.dll ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then - SUFFIX=.exe + SUFFIX=.dll cp easytier/third_party/i686/*.dll ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then - SUFFIX=.exe + SUFFIX=.dll cp easytier/third_party/arm64/*.dll ./artifacts/objects/ fi if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then @@ -285,7 +201,6 @@ jobs: runs-on: ubuntu-latest needs: - pre_job - - build_web - build steps: - name: Mark result as failed @@ -293,22 +208,21 @@ jobs: run: exit 1 magisk_build: - needs: + needs: - pre_job - - build_web - build if: needs.pre_job.outputs.should_skip != 'true' && always() runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置 + uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置 # 下载二进制文件到独立目录 - name: Download Linux aarch64 binaries uses: actions/download-artifact@v4 with: name: easytier-linux-aarch64 - path: ./downloaded-binaries/ # 独立目录避免冲突 + path: ./downloaded-binaries/ # 独立目录避免冲突 # 将二进制文件复制到 Magisk 模块目录 - name: Prepare binaries @@ -318,7 +232,6 @@ jobs: cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/ cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/ - # 上传生成的模块 - name: Upload Magisk Module uses: actions/upload-artifact@v4 diff --git a/.github/origin_wfs/install_rust.sh b/.github/workflows/install_rust.sh similarity index 100% rename from .github/origin_wfs/install_rust.sh rename to .github/workflows/install_rust.sh From 9b2fc8e5b9dd1ec255b3c2618238c774f5463659 Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Sun, 27 Jul 2025 23:02:21 +0800 Subject: [PATCH 08/10] Use old workflow --- .github/{workflows => origin_wfs}/core.yml | 208 +++++++++++++----- .../{workflows => origin_wfs}/install_rust.sh | 2 + .../{origin_wfs => workflows}/build_libs.yml | 2 +- 3 files changed, 153 insertions(+), 59 deletions(-) rename .github/{workflows => origin_wfs}/core.yml (56%) rename .github/{workflows => origin_wfs}/install_rust.sh (95%) rename .github/{origin_wfs => workflows}/build_libs.yml (97%) diff --git a/.github/workflows/core.yml b/.github/origin_wfs/core.yml similarity index 56% rename from .github/workflows/core.yml rename to .github/origin_wfs/core.yml index 6715f5fde..8eb464e4c 100644 --- a/.github/workflows/core.yml +++ b/.github/origin_wfs/core.yml @@ -2,9 +2,9 @@ name: EasyTier Core on: push: - branches: ["dev", "main", "releases/**"] + branches: ["develop", "main", "releases/**"] pull_request: - branches: ["dev", "main"] + branches: ["develop", "main"] env: CARGO_TERM_COLOR: always @@ -27,10 +27,51 @@ jobs: 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" + concurrent_skipping: 'same_content_newer' + skip_after_successful_duplicate: 'true' + cancel_others: 'true' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]' + build_web: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install frontend dependencies + run: | + pnpm -r install + pnpm -r --filter "./easytier-web/*" build + + - name: Archive artifact + uses: actions/upload-artifact@v4 + with: + name: easytier-web-dashboard + path: | + easytier-web/frontend/dist/* build: strategy: fail-fast: false @@ -42,50 +83,53 @@ jobs: - TARGET: x86_64-unknown-linux-musl OS: ubuntu-22.04 ARTIFACT_NAME: linux-x86_64 - # - TARGET: mips-unknown-linux-musl - # OS: ubuntu-22.04 - # ARTIFACT_NAME: linux-mips - # - TARGET: mipsel-unknown-linux-musl - # OS: ubuntu-22.04 - # ARTIFACT_NAME: linux-mipsel - # - TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested - # OS: ubuntu-22.04 - # ARTIFACT_NAME: linux-armv7hf - # - TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested - # OS: ubuntu-22.04 - # ARTIFACT_NAME: linux-armv7 - # - TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested - # OS: ubuntu-22.04 - # ARTIFACT_NAME: linux-armhf - # - TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested - # 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 - # - TARGET: aarch64-apple-darwin - # OS: macos-latest - # ARTIFACT_NAME: macos-aarch64 + - 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 + - TARGET: mipsel-unknown-linux-musl + OS: ubuntu-22.04 + ARTIFACT_NAME: linux-mipsel + - TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested + OS: ubuntu-22.04 + ARTIFACT_NAME: linux-armv7hf + - TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested + OS: ubuntu-22.04 + ARTIFACT_NAME: linux-armv7 + - TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested + OS: ubuntu-22.04 + ARTIFACT_NAME: linux-armhf + - TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested + 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 + - TARGET: aarch64-apple-darwin + OS: macos-latest + ARTIFACT_NAME: macos-aarch64 - TARGET: x86_64-pc-windows-msvc OS: windows-latest ARTIFACT_NAME: windows-x86_64 - # - TARGET: aarch64-pc-windows-msvc - # OS: windows-latest - # ARTIFACT_NAME: windows-arm64 - # - TARGET: i686-pc-windows-msvc - # OS: windows-latest - # ARTIFACT_NAME: windows-i686 - - # - TARGET: x86_64-unknown-freebsd - # OS: ubuntu-22.04 - # ARTIFACT_NAME: freebsd-13.2-x86_64 - # BSD_VERSION: 13.2 + - TARGET: aarch64-pc-windows-msvc + OS: windows-latest + ARTIFACT_NAME: windows-arm64 + - TARGET: i686-pc-windows-msvc + OS: windows-latest + ARTIFACT_NAME: windows-i686 + + - TARGET: x86_64-unknown-freebsd + OS: ubuntu-22.04 + ARTIFACT_NAME: freebsd-13.2-x86_64 + BSD_VERSION: 13.2 runs-on: ${{ matrix.OS }} env: @@ -95,6 +139,7 @@ jobs: OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }} needs: - pre_job + - build_web if: needs.pre_job.outputs.should_skip != 'true' steps: - uses: actions/checkout@v3 @@ -103,13 +148,14 @@ jobs: run: | echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV - # - name: Download web artifact - # uses: actions/download-artifact@v4 - # with: - # name: easytier-web-dashboard - # path: easytier-web/frontend/dist/ + - name: Download web artifact + uses: actions/download-artifact@v4 + with: + name: easytier-web-dashboard + path: easytier-web/frontend/dist/ - name: Cargo cache + if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} uses: actions/cache@v4 with: path: | @@ -124,6 +170,7 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Build Core & Cli + if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} run: | bash ./.github/workflows/install_rust.sh @@ -143,26 +190,68 @@ jobs: cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc else if [[ $OS =~ ^windows.*$ ]]; then - SUFFIX=.dll + 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 $CORE_FEATURES fi + # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267) + - name: Build Core & Cli (X86_64 FreeBSD) + uses: vmactions/freebsd-vm@v1 + if: ${{ endsWith(matrix.TARGET, 'freebsd') }} + env: + TARGET: ${{ matrix.TARGET }} + with: + envs: TARGET + release: ${{ matrix.BSD_VERSION }} + arch: x86_64 + usesh: true + mem: 6144 + cpu: 4 + run: | + uname -a + echo $SHELL + pwd + ls -lah + whoami + env | sort + + pkg install -y git protobuf llvm-devel sudo curl + curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + . $HOME/.cargo/env + + rustup set auto-self-update disable + + rustup install 1.87 + rustup default 1.87 + + export CC=clang + export CXX=clang++ + export CARGO_TERM_COLOR=always + + 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 --features=mimalloc + - name: Compress run: | mkdir -p ./artifacts/objects/ # windows is the only OS using a different convention for executable file name if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then - SUFFIX=.dll + SUFFIX=.exe cp easytier/third_party/*.dll ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then - SUFFIX=.dll + SUFFIX=.exe cp easytier/third_party/i686/*.dll ./artifacts/objects/ elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then - SUFFIX=.dll + SUFFIX=.exe cp easytier/third_party/arm64/*.dll ./artifacts/objects/ fi if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then @@ -171,7 +260,7 @@ jobs: TAG=$GITHUB_SHA fi - if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ && ! $TARGET =~ ^loongarch.*$ ]]; 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 . @@ -201,6 +290,7 @@ jobs: runs-on: ubuntu-latest needs: - pre_job + - build_web - build steps: - name: Mark result as failed @@ -208,21 +298,22 @@ jobs: run: exit 1 magisk_build: - needs: + needs: - pre_job + - build_web - build if: needs.pre_job.outputs.should_skip != 'true' && always() runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置 + uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置 # 下载二进制文件到独立目录 - name: Download Linux aarch64 binaries uses: actions/download-artifact@v4 with: name: easytier-linux-aarch64 - path: ./downloaded-binaries/ # 独立目录避免冲突 + path: ./downloaded-binaries/ # 独立目录避免冲突 # 将二进制文件复制到 Magisk 模块目录 - name: Prepare binaries @@ -232,6 +323,7 @@ jobs: cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/ cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/ + # 上传生成的模块 - name: Upload Magisk Module uses: actions/upload-artifact@v4 diff --git a/.github/workflows/install_rust.sh b/.github/origin_wfs/install_rust.sh similarity index 95% rename from .github/workflows/install_rust.sh rename to .github/origin_wfs/install_rust.sh index c9d339794..9eefcf05d 100644 --- a/.github/workflows/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 diff --git a/.github/origin_wfs/build_libs.yml b/.github/workflows/build_libs.yml similarity index 97% rename from .github/origin_wfs/build_libs.yml rename to .github/workflows/build_libs.yml index 411328e39..ecfcd5a72 100644 --- a/.github/origin_wfs/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 From 07a52a7128d196a8b9e5d24746ffddc1ae081c43 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 12 Aug 2025 23:12:18 +0800 Subject: [PATCH 09/10] V2.4.2 merge code and add isrunning (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) * reduce memory usage (#1133) Large memory usage comes from: Mimalloc hold large thread cache, causing abort 13M+ usage. QUIC endpoint occupy 3M when GRO is enabled. Smoltcp 64 tcp listener use 2MB. * fix bugs (#1138) 1. avoid dns query hangs the thread 2. avoid deadloop when stun query failed because of no ipv4 addr. 3. make quic input error non-fatal. 4. remove ring tunnel from connection map to avoid mem leak. 5. limit listener retry count. * Implement ACL (#1140) 1. get acl stats ``` ./easytier-cli acl stats AclStats: Global: CacheHits: 4 CacheMaxSize: 10000 CacheSize: 5 DefaultAllows: 3 InboundPacketsAllowed: 2 InboundPacketsTotal: 2 OutboundPacketsAllowed: 7 OutboundPacketsTotal: 7 PacketsAllowed: 9 PacketsTotal: 9 RuleMatches: 2 ConnTrack: [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00] Rules: [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120] ``` 2. use tcp/udp whitelist to block unexpected traffic. `sudo ./easytier-core -d --tcp-whitelist 1000` 3. use complete acl ability with config file: ``` [[acl.acl_v1.chains]] name = "inbound_whitelist" chain_type = 1 description = "Auto-generated inbound whitelist from CLI" enabled = true default_action = 2 [[acl.acl_v1.chains.rules]] name = "tcp_whitelist" description = "Auto-generated TCP whitelist rule" priority = 1000 enabled = true protocol = 1 ports = ["1000"] source_ips = [] destination_ips = [] source_ports = [] action = 1 rate_limit = 0 burst_limit = 0 stateful = true ``` * releases/v2.4.0 (#1145) * bump version to v2.4.0 * update tauri. * allow try direct connect to public server * support loongarch (#1146) * need encrypt rpc if dst is in peer map (#1151) * port range should not be converted to single port (#1154) * fix acl not work with kcp&quic (#1152) * avoid udp hole punch go through tun (#1155) * Add support for Linux RISC-V 64 (#1159) * Merge v2.4.0 codes (#16) * Merge offical easytier to dev (#8) * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix compile issue --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen * remove mimalloc * clear CONNECTION_MAP resource * Merge new Offical Easytier codes (#10) * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen * Merge code from offical repo (#12) * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) * reduce memory usage (#1133) Large memory usage comes from: Mimalloc hold large thread cache, causing abort 13M+ usage. QUIC endpoint occupy 3M when GRO is enabled. Smoltcp 64 tcp listener use 2MB. * fix bugs (#1138) 1. avoid dns query hangs the thread 2. avoid deadloop when stun query failed because of no ipv4 addr. 3. make quic input error non-fatal. 4. remove ring tunnel from connection map to avoid mem leak. 5. limit listener retry count. --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen * Merge v2.4.0 codes (#15) * Update default_port and sni logic to improve reverse proxy reachability (#947) * remove LICENSE (#950) * Create LICENSE (#951) * kcp connect retry (#952) * fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) * Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp * Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) * feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) * ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. * add check for rpc packet fix #963 (#969) * fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** * easytier-core支持多配置文件 (#964) * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 * internal stun server should use xor mapped addr (#975) * remove macos default route on utun device (#976) * support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. * Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun * use bulk compress instead of streaming to reduce mem usage (#985) * Update core.yml,use upx4.2.4 (#991) * support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. * Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web * add keepalive option for quic proxy (#1008) avoid connection loss when idle * allow set machine uid with command line (#1009) * installing by homebrew should use easytier-gui (#1004) * Add is_hole_punched flag to PeerConn (#1001) * quic uses the bbr congestion control algorithm (#1010) * add bps limiter (#1015) * add token bucket * remove quinn-proto * bps limit should throttle kcp packet * add api_meta.js to frontend public * Implement custom fmt::Debug for some prost_build generated structs Currently implemented for: 1. common.Ipv4Addr 2. common.Ipv6Addr 3. common.UUID * simplify Textarea class in ConfigGenerator.vue * add Windows Service install script * fix uninstall.cmd (#1036) * blacklist the peers which disable p2p in hole-punching client (#1038) * limit max conn count in foreign network manager (#1041) * fix rpc_portal_whitelist from config file not working (#1042) * web improve (#1047) * add geo info for in web device list (#1052) * fix cargo install failure (#1054) * fix mem leak of token bucket (#1055) * allow set multithread count (#1056) * update gui placeholder text (#1062) * support ohos (#974) * support ohos --------- Co-authored-by: FrankHan <2777926911@qq.com> * Add support for IPv6 within VPN (#1061) * add flake.nix with nix based dev shell * add support for IPv6 * update thunk --------- Co-authored-by: sijie.sun * use winapi to config ip and route (remove dep on netsh) (#1079) On some windows machines can not execut netsh. Also this avoid black cmd window when using gui. * exclude ohos from workspace (#1080) * contributing.md (#1084) * handle close peer conn correctly (#1082) * smoltcp use larger tx/rx buf size (#1085) * smoltcp use larger tx/rx buf size * fix direct conn check * fix incorrect config check (#1086) * chore(ci): update GitHub Actions (#1088) * chore(ci): update GitHub Actions * update gradle-wrapper and revert UPX * exclude cargo from dependabot and remove empty .gitmodules * fix: cannot start gui on linux (#1090) * update readme (#1102) * socks5 and port forwarding (#1118) * add options to generate completions (#1103) * add options to generate completions use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish --------- Co-authored-by: Sijie.Sun * Allows to modify Easytier's mapped listener at runtime via RPC (#1107) * Add proto definition * Implement and register the corresponding rpc service * Parse command line parameters and call remote rpc service --------- Co-authored-by: Sijie.Sun * close peer conn if remote addr is from virtual network (#1123) * update issue template (#1126) * add disable ipv6 option to gui/web (#1127) * fix latency first route of public server (#1129) * add windows firewall for tun interface (#1130) allow all icmp/tcp/udp on tun interface. * try create tun device if not exist (#1131) * reduce memory usage (#1133) Large memory usage comes from: Mimalloc hold large thread cache, causing abort 13M+ usage. QUIC endpoint occupy 3M when GRO is enabled. Smoltcp 64 tcp listener use 2MB. * fix bugs (#1138) 1. avoid dns query hangs the thread 2. avoid deadloop when stun query failed because of no ipv4 addr. 3. make quic input error non-fatal. 4. remove ring tunnel from connection map to avoid mem leak. 5. limit listener retry count. * Implement ACL (#1140) 1. get acl stats ``` ./easytier-cli acl stats AclStats: Global: CacheHits: 4 CacheMaxSize: 10000 CacheSize: 5 DefaultAllows: 3 InboundPacketsAllowed: 2 InboundPacketsTotal: 2 OutboundPacketsAllowed: 7 OutboundPacketsTotal: 7 PacketsAllowed: 9 PacketsTotal: 9 RuleMatches: 2 ConnTrack: [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00] Rules: [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120] ``` 2. use tcp/udp whitelist to block unexpected traffic. `sudo ./easytier-core -d --tcp-whitelist 1000` 3. use complete acl ability with config file: ``` [[acl.acl_v1.chains]] name = "inbound_whitelist" chain_type = 1 description = "Auto-generated inbound whitelist from CLI" enabled = true default_action = 2 [[acl.acl_v1.chains.rules]] name = "tcp_whitelist" description = "Auto-generated TCP whitelist rule" priority = 1000 enabled = true protocol = 1 ports = ["1000"] source_ips = [] destination_ips = [] source_ports = [] action = 1 rate_limit = 0 burst_limit = 0 stateful = true ``` * releases/v2.4.0 (#1145) * bump version to v2.4.0 * update tauri. * allow try direct connect to public server * support loongarch (#1146) * need encrypt rpc if dst is in peer map (#1151) --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen * update pipeline files * Use old workflow --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen * chore: update flake configuration (#1163) * cli for port forward and tcp whitelist (#1165) * Some Improvements (#1172) 1. do not exit when dns query failed on et startup. 2. do not send secret digest to client when secret mismatch. * fix: compiling with socket2::Type::RAW not found on macOS #1168 (#1169) * fix exit code when error occcurs (#1173) * fix macos elevate (#1177) * update readme (#1181) * fix readme assets (#1182) * bump version to 2.4.1 * fix ipv6 packet routing and avoid route looping properly handle ipv6 link local address and exit node. * cli: sort peers by IPv4 and hostname (#1191) * cli: sort entries by IPv4 and hostname Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix jemalloc prof feature (#1201) * fix dead loop in direct connecto if disable-p2p is enabled in dst (#1206) * add stats metrics (#1207) support new cli command `easytier-cli stats` It's useful to find out which components are consuming bandwidth. * add portforward config to gui (#1198) * Added port forwarding to the GUI interface * Separated port forwarding into a separate drop-down menu * optimize the condition of enabling kcp (#1210) * add FOREGROUND_SERVICE for no_tun mode, not using vpn service (#1203) 1. add FOREGROUND_SERVICE related code, connection not to be **blocked by android system** when apps running in background 2. no_tun mode not enabling vpnservice, makeing other app to use vpnservice Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(encrypt): Add XOR and ChaCha20 encryption with low-end device optimization and openssl support. (#1186) Add ChaCha20 XOR algorithm, extend AES-GCM-256 capabilities, and integrate OpenSSL support. --------- Co-authored-by: Sijie.Sun * clippy all codes (#1214) 1. clippy code 2. add fmt and clippy check in ci * Update docker workflow (#1217) 1. push all supported platform 2. support unstable tag * fix quic transport panic (#1216) * bump version to 2.4.2 (#1218) * fix docker file (#1219) * fix build issue and add is_running state --------- Co-authored-by: Zisu Zhang Co-authored-by: Sijie.Sun Co-authored-by: Kiva Co-authored-by: BlackLuny <602814112@qq.com> Co-authored-by: Mg Pig Co-authored-by: tianxiayu007 <1083010692@qq.com> Co-authored-by: liusen373 <52489720+liusen373@users.noreply.github.com> Co-authored-by: chenxudong2020 <872603935@qq.com> Co-authored-by: sijie.sun Co-authored-by: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Co-authored-by: 韩嘉乐 <2382008060@qq.com> Co-authored-by: FrankHan <2777926911@qq.com> Co-authored-by: DavHau Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Co-authored-by: lazebird Co-authored-by: Jiangqiu Shen Co-authored-by: Glavo Co-authored-by: Tunglies Co-authored-by: fanyang Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: FuturePrayer Co-authored-by: 21paradox Co-authored-by: CyiceK <36905062+CyiceK@users.noreply.github.com> --- .cargo/config.toml | 4 + .github/origin_wfs/Dockerfile | 8 +- .github/origin_wfs/core.yml | 4 +- .github/origin_wfs/docker.yml | 44 +- .github/origin_wfs/gui.yml | 19 +- .github/origin_wfs/install_rust.sh | 4 +- .github/origin_wfs/release.yml | 2 +- .github/origin_wfs/test.yml | 20 +- .github/workflows/install_gui_dep.sh | 11 + .github/workflows/ohos.yml | 114 ++ .gitignore | 1 + CONTRIBUTING.md | 8 +- CONTRIBUTING_zh.md | 8 +- Cargo.lock | 42 +- EasyTier.code-workspace | 2 + README.md | 12 +- README_CN.md | 11 +- assets/raincloud.png | Bin 0 -> 38138 bytes easytier-contrib/easytier-ffi/src/lib.rs | 51 +- easytier-contrib/easytier-magisk/module.prop | 2 +- easytier-contrib/easytier-ohrs/Cargo.lock | 2 +- easytier-gui/package.json | 2 +- easytier-gui/src-tauri/Cargo.toml | 12 +- .../android/app/src/main/AndroidManifest.xml | 10 + .../com/kkrainbow/easytier/MainActivity.kt | 19 +- .../easytier/MainForegroundService.kt | 64 + easytier-gui/src-tauri/src/elevate/linux.rs | 67 + easytier-gui/src-tauri/src/elevate/macos.rs | 182 +++ easytier-gui/src-tauri/src/elevate/mod.rs | 101 ++ easytier-gui/src-tauri/src/elevate/windows.rs | 114 ++ easytier-gui/src-tauri/src/lib.rs | 6 +- easytier-gui/src-tauri/tauri.conf.json | 2 +- easytier-gui/src/composables/mobile_vpn.ts | 29 +- easytier-gui/src/pages/index.vue | 2 +- easytier-rpc-build/Cargo.toml | 2 +- easytier-rpc-build/src/lib.rs | 17 +- easytier-web/Cargo.toml | 2 +- easytier-web/build.rs | 17 +- .../frontend-lib/src/components/Config.vue | 77 +- easytier-web/frontend-lib/src/locales/cn.yaml | 6 + easytier-web/frontend-lib/src/locales/en.yaml | 6 + .../frontend-lib/src/types/network.ts | 27 + easytier-web/src/client_manager/mod.rs | 8 +- easytier-web/src/client_manager/session.rs | 12 +- easytier-web/src/client_manager/storage.rs | 12 +- easytier-web/src/main.rs | 2 +- easytier-web/src/restful/auth.rs | 2 +- .../src/restful/captcha/base/captcha.rs | 41 +- .../src/restful/captcha/base/randoms.rs | 4 +- .../captcha/{captcha => builder}/mod.rs | 0 .../captcha/{captcha => builder}/spec.rs | 0 .../captcha/extension/axum_tower_sessions.rs | 4 +- .../src/restful/captcha/extension/mod.rs | 2 +- easytier-web/src/restful/captcha/mod.rs | 2 +- .../src/restful/captcha/utils/color.rs | 23 +- .../src/restful/captcha/utils/font.rs | 4 +- easytier-web/src/restful/mod.rs | 2 +- easytier-web/src/restful/network.rs | 19 +- easytier-web/src/restful/users.rs | 10 +- easytier-web/src/web/mod.rs | 4 +- easytier/Cargo.toml | 13 +- easytier/build.rs | 6 +- easytier/locales/app.yml | 15 + easytier/src/common/acl_processor.rs | 299 +++- easytier/src/common/compressor.rs | 14 +- easytier/src/common/config.rs | 224 ++- easytier/src/common/constants.rs | 4 +- easytier/src/common/defer.rs | 4 +- easytier/src/common/dns.rs | 20 +- easytier/src/common/global_ctx.rs | 62 +- easytier/src/common/ifcfg/mod.rs | 4 +- easytier/src/common/ifcfg/netlink.rs | 12 +- easytier/src/common/ifcfg/win/luid.rs | 2 +- easytier/src/common/ifcfg/win/mod.rs | 2 +- easytier/src/common/ifcfg/win/netsh.rs | 2 +- easytier/src/common/ifcfg/win/types.rs | 2 +- easytier/src/common/mod.rs | 5 +- easytier/src/common/netns.rs | 9 +- easytier/src/common/network.rs | 24 +- easytier/src/common/stats_manager.rs | 886 ++++++++++++ easytier/src/common/stun.rs | 51 +- easytier/src/common/token_bucket.rs | 16 +- easytier/src/connector/direct.rs | 78 +- easytier/src/connector/dns_connector.rs | 7 +- easytier/src/connector/http_connector.rs | 4 +- easytier/src/connector/manual.rs | 106 +- .../connector/udp_hole_punch/both_easy_sym.rs | 12 +- .../src/connector/udp_hole_punch/common.rs | 43 +- easytier/src/connector/udp_hole_punch/cone.rs | 14 +- easytier/src/connector/udp_hole_punch/mod.rs | 14 +- .../connector/udp_hole_punch/sym_to_cone.rs | 54 +- easytier/src/easytier-cli.rs | 535 ++++++- easytier/src/easytier-core.rs | 1254 +++++++++++++++++ easytier/src/easytier_core.rs | 232 ++- easytier/src/gateway/fast_socks5/mod.rs | 2 +- easytier/src/gateway/fast_socks5/server.rs | 8 +- .../gateway/fast_socks5/util/target_addr.rs | 12 +- easytier/src/gateway/icmp_proxy.rs | 69 +- easytier/src/gateway/ip_reassembler.rs | 43 +- easytier/src/gateway/kcp_proxy.rs | 128 +- easytier/src/gateway/mod.rs | 10 +- easytier/src/gateway/quic_proxy.rs | 42 +- easytier/src/gateway/socks5.rs | 194 +-- easytier/src/gateway/tcp_proxy.rs | 34 +- .../gateway/tokio_smoltcp/channel_device.rs | 10 +- easytier/src/gateway/tokio_smoltcp/device.rs | 11 +- easytier/src/gateway/tokio_smoltcp/mod.rs | 8 +- easytier/src/gateway/tokio_smoltcp/reactor.rs | 20 +- easytier/src/gateway/tokio_smoltcp/socket.rs | 8 +- easytier/src/gateway/udp_proxy.rs | 31 +- easytier/src/helper.rs | 5 + .../instance/dns_server/client_instance.rs | 7 +- easytier/src/instance/dns_server/config.rs | 6 +- easytier/src/instance/dns_server/runner.rs | 2 +- easytier/src/instance/dns_server/server.rs | 14 +- .../instance/dns_server/server_instance.rs | 21 +- .../dns_server/system_config/linux.rs | 17 +- easytier/src/instance/dns_server/tests.rs | 2 +- easytier/src/instance/instance.rs | 179 ++- easytier/src/instance/listeners.rs | 2 +- easytier/src/instance/mod.rs | 2 + easytier/src/instance/virtual_nic.rs | 31 +- easytier/src/instance_manager.rs | 54 +- easytier/src/launcher.rs | 104 +- easytier/src/lib.rs | 16 +- easytier/src/peer_center/instance.rs | 24 +- easytier/src/peer_center/server.rs | 2 +- easytier/src/peers/acl_filter.rs | 14 +- easytier/src/peers/encrypt/aes_gcm.rs | 18 +- easytier/src/peers/encrypt/mod.rs | 81 +- easytier/src/peers/encrypt/openssl_cipher.rs | 241 ++++ easytier/src/peers/encrypt/ring_aes_gcm.rs | 16 +- easytier/src/peers/encrypt/ring_chacha20.rs | 125 ++ easytier/src/peers/encrypt/xor_cipher.rs | 86 ++ easytier/src/peers/foreign_network_client.rs | 2 +- easytier/src/peers/foreign_network_manager.rs | 130 +- easytier/src/peers/graph_algo.rs | 41 +- easytier/src/peers/peer.rs | 6 +- easytier/src/peers/peer_conn.rs | 88 +- easytier/src/peers/peer_conn_ping.rs | 4 +- easytier/src/peers/peer_manager.rs | 317 +++-- easytier/src/peers/peer_map.rs | 22 +- easytier/src/peers/peer_ospf_route.rs | 164 +-- easytier/src/peers/peer_rpc.rs | 14 +- easytier/src/peers/route_trait.rs | 21 +- easytier/src/peers/rpc_service.rs | 94 +- easytier/src/peers/tests.rs | 2 +- easytier/src/proto/cli.proto | 63 + easytier/src/proto/cli.rs | 2 +- easytier/src/proto/common.proto | 8 +- easytier/src/proto/common.rs | 36 +- easytier/src/proto/error.rs | 2 + easytier/src/proto/rpc_impl/bidirect.rs | 30 +- easytier/src/proto/rpc_impl/client.rs | 107 +- easytier/src/proto/rpc_impl/packet.rs | 66 +- easytier/src/proto/rpc_impl/server.rs | 103 +- .../src/proto/rpc_impl/service_registry.rs | 14 + easytier/src/proto/rpc_types/handler.rs | 11 +- easytier/src/proto/tests.rs | 12 +- easytier/src/proto/web.proto | 9 + easytier/src/tests/mod.rs | 1 - easytier/src/tests/three_node.rs | 416 +++++- easytier/src/tunnel/common.rs | 24 +- easytier/src/tunnel/filter.rs | 14 +- easytier/src/tunnel/mod.rs | 8 +- easytier/src/tunnel/mpsc.rs | 9 +- easytier/src/tunnel/packet_def.rs | 8 +- easytier/src/tunnel/quic.rs | 43 +- easytier/src/tunnel/ring.rs | 18 +- easytier/src/tunnel/tcp.rs | 6 +- easytier/src/tunnel/udp.rs | 41 +- easytier/src/tunnel/websocket.rs | 18 +- easytier/src/tunnel/wireguard.rs | 16 +- easytier/src/utils.rs | 15 +- easytier/src/vpn_portal/wireguard.rs | 42 +- easytier/src/web_client/controller.rs | 7 +- easytier/src/web_client/mod.rs | 12 +- easytier/src/web_client/session.rs | 2 +- flake.lock | 12 +- flake.nix | 31 +- pnpm-lock.yaml | 15 - 181 files changed, 7342 insertions(+), 1796 deletions(-) create mode 100644 .github/workflows/install_gui_dep.sh create mode 100644 .github/workflows/ohos.yml create mode 100644 assets/raincloud.png create mode 100644 easytier-gui/src-tauri/gen/android/app/src/main/java/com/kkrainbow/easytier/MainForegroundService.kt create mode 100644 easytier-gui/src-tauri/src/elevate/linux.rs create mode 100644 easytier-gui/src-tauri/src/elevate/macos.rs create mode 100644 easytier-gui/src-tauri/src/elevate/mod.rs create mode 100644 easytier-gui/src-tauri/src/elevate/windows.rs rename easytier-web/src/restful/captcha/{captcha => builder}/mod.rs (100%) rename easytier-web/src/restful/captcha/{captcha => builder}/spec.rs (100%) create mode 100644 easytier/src/common/stats_manager.rs create mode 100644 easytier/src/easytier-core.rs create mode 100644 easytier/src/peers/encrypt/openssl_cipher.rs create mode 100644 easytier/src/peers/encrypt/ring_chacha20.rs create mode 100644 easytier/src/peers/encrypt/xor_cipher.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 1ffdf1d53..17288931a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,6 +19,10 @@ SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot" 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"] diff --git a/.github/origin_wfs/Dockerfile b/.github/origin_wfs/Dockerfile index 147029f48..780d8fa5c 100644 --- a/.github/origin_wfs/Dockerfile +++ b/.github/origin_wfs/Dockerfile @@ -8,10 +8,16 @@ 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; diff --git a/.github/origin_wfs/core.yml b/.github/origin_wfs/core.yml index 8eb464e4c..7cb8de18c 100644 --- a/.github/origin_wfs/core.yml +++ b/.github/origin_wfs/core.yml @@ -229,8 +229,8 @@ jobs: rustup set auto-self-update disable - rustup install 1.87 - rustup default 1.87 + rustup install 1.89 + rustup default 1.89 export CC=clang export CXX=clang++ diff --git a/.github/origin_wfs/docker.yml b/.github/origin_wfs/docker.yml index e73d97f3a..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.4.0' + 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 @@ -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 aab8d4fe8..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,8 +119,6 @@ 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 diff --git a/.github/origin_wfs/install_rust.sh b/.github/origin_wfs/install_rust.sh index 9eefcf05d..0c7176cba 100644 --- a/.github/origin_wfs/install_rust.sh +++ b/.github/origin_wfs/install_rust.sh @@ -31,8 +31,8 @@ fi # see https://github.com/rust-lang/rustup/issues/3709 rustup set auto-self-update disable -rustup install 1.87 -rustup default 1.87 +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/release.yml b/.github/origin_wfs/release.yml index df8928f58..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.4.0' + default: 'v2.4.2' required: true make_latest: description: 'Mark this release as latest' diff --git a/.github/origin_wfs/test.yml b/.github/origin_wfs/test.yml index 2d4010357..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 @@ -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/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 216c83b5b..d3a7e4651 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ target-*/ .vscode .idea +/.direnv/ # perf & flamegraph perf.data diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3fb64b89e..d4cb38a2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ Thank you for your interest in contributing to EasyTier! This document provides #### Required Tools - Node.js v21 or higher - pnpm v9 or higher -- Rust toolchain (version 1.87) +- Rust toolchain (version 1.89) - LLVM and Clang - Protoc (Protocol Buffers compiler) @@ -37,7 +37,6 @@ Thank you for your interest in contributing to EasyTier! This document provides # Core build dependencies sudo apt-get update && sudo apt-get install -y \ musl-tools \ - libappindicator3-dev \ llvm \ clang \ protobuf-compiler @@ -53,6 +52,7 @@ sudo apt install -y \ librsvg2-dev \ libxdo-dev \ libssl-dev \ + libappindicator3-dev \ patchelf # Testing dependencies @@ -79,8 +79,8 @@ sudo apt install -y bridge-utils 2. Install dependencies: ```bash # Install Rust toolchain - rustup install 1.87 - rustup default 1.87 + rustup install 1.89 + rustup default 1.89 # Install project dependencies pnpm -r install diff --git a/CONTRIBUTING_zh.md b/CONTRIBUTING_zh.md index c2cbea0ce..7e663da62 100644 --- a/CONTRIBUTING_zh.md +++ b/CONTRIBUTING_zh.md @@ -34,7 +34,7 @@ #### 必需工具 - Node.js v21 或更高版本 - pnpm v9 或更高版本 -- Rust 工具链(版本 1.87) +- Rust 工具链(版本 1.89) - LLVM 和 Clang - Protoc(Protocol Buffers 编译器) @@ -45,7 +45,6 @@ # 核心构建依赖 sudo apt-get update && sudo apt-get install -y \ musl-tools \ - libappindicator3-dev \ llvm \ clang \ protobuf-compiler @@ -61,6 +60,7 @@ sudo apt install -y \ librsvg2-dev \ libxdo-dev \ libssl-dev \ + libappindicator3-dev \ patchelf # 测试依赖 @@ -87,8 +87,8 @@ sudo apt install -y bridge-utils 2. 安装依赖: ```bash # 安装 Rust 工具链 - rustup install 1.87 - rustup default 1.87 + rustup install 1.89 + rustup default 1.89 # 安装项目依赖 pnpm -r install diff --git a/Cargo.lock b/Cargo.lock index b431542e2..b6e0eef87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1876,7 +1876,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1979,7 +1979,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "easytier" -version = "2.4.0" +version = "2.4.2" dependencies = [ "aes-gcm", "anyhow", @@ -2035,6 +2035,7 @@ dependencies = [ "network-interface", "nix 0.29.0", "once_cell", + "openssl", "parking_lot", "percent-encoding", "petgraph 0.8.1", @@ -2115,16 +2116,17 @@ dependencies = [ [[package]] name = "easytier-gui" -version = "2.4.0" +version = "2.4.2" dependencies = [ "anyhow", "chrono", "dashmap", "dunce", "easytier", - "elevated-command", "gethostname 1.0.2", + "libc", "once_cell", + "security-framework-sys", "serde", "serde_json", "tauri", @@ -2140,6 +2142,8 @@ dependencies = [ "thunk-rs", "tokio", "uuid", + "winapi", + "windows 0.52.0", ] [[package]] @@ -2162,7 +2166,7 @@ dependencies = [ [[package]] name = "easytier-web" -version = "2.4.0" +version = "2.4.2" dependencies = [ "anyhow", "async-trait", @@ -2210,20 +2214,6 @@ dependencies = [ "serde", ] -[[package]] -name = "elevated-command" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c410eccdcc5b759704fdb6a792afe6b01ab8a062e2c003ff2567e2697a94aa" -dependencies = [ - "anyhow", - "base64 0.21.7", - "libc", - "log", - "winapi", - "windows 0.52.0", -] - [[package]] name = "embed-resource" version = "3.0.5" @@ -5248,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" @@ -5256,6 +5255,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -7560,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", diff --git a/EasyTier.code-workspace b/EasyTier.code-workspace index 9e4f1acc8..ebe44138b 100644 --- a/EasyTier.code-workspace +++ b/EasyTier.code-workspace @@ -44,5 +44,7 @@ "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 1c3798e6c..4bc515ccd 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,9 @@ 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.0-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.0-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.0-70e69a38~ | +| 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~ | ``` You can test connectivity between nodes: @@ -302,14 +302,18 @@ CDN acceleration and security protection for this project are sponsored by Tence

-Special thanks to [Langlang Cloud](https://langlang.cloud/) for sponsoring our public servers. +Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.

+ + +

+ 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 2323f3a56..fba6bd68b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -106,9 +106,9 @@ sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.ea ```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.0-70e69a38~ | -| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.0-70e69a38~ | -| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.0-70e69a38~ | +| 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~ | ``` 您可以测试节点之间的连通性: @@ -303,12 +303,15 @@ EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)

-特别感谢 [浪浪云](https://langlang.cloud/) 赞助我们的公共服务器。 +特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。

+ + +

如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。 diff --git a/assets/raincloud.png b/assets/raincloud.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f2a062f6f52c43b52f0ce7d7381b313962a3d0 GIT binary patch literal 38138 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 std::ffi::c_int { @@ -43,18 +45,23 @@ pub extern "C" fn set_tun_fd( if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) { return -1; } - match INSTANCE_MANAGER.set_tun_fd(&INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap().value(), fd) { - Ok(_) => { - 0 - } - Err(_) => { - -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 extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) { +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 { @@ -78,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) @@ -95,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) @@ -131,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 { @@ -168,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 { @@ -233,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] @@ -243,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 e6b763d62..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.4.0 +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 index c9233f566..821de2203 100644 --- a/easytier-contrib/easytier-ohrs/Cargo.lock +++ b/easytier-contrib/easytier-ohrs/Cargo.lock @@ -1010,7 +1010,7 @@ dependencies = [ [[package]] name = "easytier" -version = "2.4.0" +version = "2.4.2" source = "git+https://github.com/EasyTier/EasyTier.git#a4bb555fac1046d0099c44676fa9d0d8cca55c99" dependencies = [ "anyhow", diff --git a/easytier-gui/package.json b/easytier-gui/package.json index 79373b930..f91d8fa2e 100644 --- a/easytier-gui/package.json +++ b/easytier-gui/package.json @@ -1,7 +1,7 @@ { "name": "easytier-gui", "type": "module", - "version": "2.4.0", + "version": "2.4.2", "private": true, "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4", "scripts": { diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index e5635a32d..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.4.0" +version = "2.4.2" description = "EasyTier GUI" authors = ["you"] edition = "2021" @@ -40,7 +40,6 @@ chrono = { version = "0.4.37", features = ["serde"] } once_cell = "1.18.0" dashmap = "6.0" -elevated-command = "1.1.2" gethostname = "1.0.2" dunce = "1.0.4" @@ -54,6 +53,15 @@ 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!! 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/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 ac84b26e0..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,7 +130,7 @@ fn toggle_window_visibility(app: &tauri::AppHandle) { #[cfg(not(target_os = "android"))] fn check_sudo() -> bool { - let is_elevated = elevated_command::Command::is_elevated(); + let is_elevated = elevate::Command::is_elevated(); if !is_elevated { let exe_path = std::env::var("APPIMAGE") .ok() @@ -139,7 +141,7 @@ fn check_sudo() -> bool { if args.contains(&AUTOSTART_ARG.to_owned()) { stdcmd.arg(AUTOSTART_ARG); } - elevated_command::Command::new(stdcmd) + elevate::Command::new(stdcmd) .output() .expect("Failed to run elevated command"); } diff --git a/easytier-gui/src-tauri/tauri.conf.json b/easytier-gui/src-tauri/tauri.conf.json index 22a9502b7..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.4.0", + "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 d433b4608..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.87.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 eee8212d7..da682e916 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easytier-web" -version = "2.4.0" +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 75a2971b1..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' @@ -163,6 +169,8 @@ const bool_flags: BoolFlag[] = [ { field: 'enable_private_mode', help: 'enable_private_mode_help' }, ] +const portForwardProtocolOptions = ref(["tcp","udp"]); +

R_UOiqkUNO3NboaIC5c_Xa)-28%) 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 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; + } + match INSTANCE_MANAGER.set_tun_fd(&INSTANCE_NAME_ID_MAP.get(&inst_name).unwrap().value(), fd) { + Ok(_) => { + 0 + } + Err(_) => { + -1 + } + } +} + #[no_mangle] pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) { let msg_buf = ERROR_MSG.lock().unwrap(); diff --git a/easytier-contrib/easytier-ohrs/Cargo.lock b/easytier-contrib/easytier-ohrs/Cargo.lock new file mode 100644 index 000000000..e19454c57 --- /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.3.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/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index 97dfab6fc..d5cccbe78 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -40,8 +40,7 @@ chrono = { version = "0.4.37", features = ["serde"] } once_cell = "1.18.0" dashmap = "6.0" - -privilege = "0.3" +elevated-command = "1.1.2" gethostname = "0.5" dunce = "1.0.4" 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 e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..1b33c55baabb587c669f562ae36f953de2481846 100755 GIT binary patch literal 43764 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