Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
84bfac144c | ||
![]() |
9eddb4b072 | ||
![]() |
4fca0f40fe | ||
![]() |
43b9e6e6e9 | ||
![]() |
583c768f40 | ||
![]() |
b1b2421561 | ||
![]() |
3d610c0f0f | ||
![]() |
2ec88da823 | ||
![]() |
5514de1187 | ||
![]() |
e70eed74e2 | ||
![]() |
7dc5988620 | ||
![]() |
354a4e1d7b | ||
![]() |
5409c5bbe7 | ||
![]() |
33ff9554cd | ||
![]() |
975b4e7664 | ||
![]() |
1f6a715939 | ||
![]() |
8e7a8de5e5 | ||
![]() |
4f53fccd25 | ||
![]() |
876d550f68 | ||
![]() |
2660ed5fda | ||
![]() |
50c6f5ae6c | ||
![]() |
85f0091056 | ||
![]() |
e25cd9be37 | ||
![]() |
1fb5ca9475 | ||
![]() |
7f3a9c021c | ||
![]() |
0427b48d75 | ||
![]() |
0b729b99e7 | ||
![]() |
940238f158 | ||
![]() |
3f6c7ba1d2 | ||
![]() |
0025973453 | ||
![]() |
c3a217c9d2 | ||
![]() |
13c2e72871 | ||
![]() |
3c65594030 | ||
![]() |
f85b031402 | ||
![]() |
ac3e994682 | ||
![]() |
139f6b3c4c | ||
![]() |
a4bb555fac | ||
![]() |
d0cfc49806 | ||
![]() |
01e491ec07 | ||
![]() |
bf021a9ead | ||
![]() |
70e69a382e | ||
![]() |
cd26d9f669 | ||
![]() |
4fd0253e99 | ||
![]() |
ebab70ca3b | ||
![]() |
ae4a158e36 | ||
![]() |
760a1e6306 | ||
![]() |
fded8b1de0 | ||
![]() |
762d5cd392 | ||
![]() |
09ac79b9f3 | ||
![]() |
16f6fb0c59 | ||
![]() |
385e790600 | ||
![]() |
95e4e5a931 | ||
![]() |
e1bfec6fe2 | ||
![]() |
dde7a4dff1 | ||
![]() |
40601bd05b | ||
![]() |
72d5ed908e | ||
![]() |
72673a9d52 | ||
![]() |
327ccdcf38 | ||
![]() |
8c2f96d1aa | ||
![]() |
34ba0bc95b | ||
![]() |
ed162c2e66 | ||
![]() |
40b5fe9a54 | ||
![]() |
5a98fac395 | ||
![]() |
0bab14cd72 | ||
![]() |
b407cfd9d4 | ||
![]() |
25dcdc652a | ||
![]() |
950cb04534 | ||
![]() |
c07d1286ef | ||
![]() |
8ddd153022 | ||
![]() |
870353c499 | ||
![]() |
ecebbecd3b | ||
![]() |
f39fbb2ce2 | ||
![]() |
ec56c0bc45 | ||
![]() |
20a6025075 | ||
![]() |
707963c0d9 | ||
![]() |
3c7837692e | ||
![]() |
f890812577 | ||
![]() |
47f3efe71b | ||
![]() |
6d88b10b14 | ||
![]() |
d34a51739f |
@@ -5,10 +5,24 @@ rustflags = ["-C", "linker-flavor=ld.lld"]
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-ohos]
|
||||
ar = "/usr/local/ohos-sdk/linux/native/llvm/bin/llvm-ar"
|
||||
linker = "/home/runner/sdk/native/llvm/aarch64-unknown-linux-ohos-clang.sh"
|
||||
|
||||
[target.aarch64-unknown-linux-ohos.env]
|
||||
PKG_CONFIG_PATH = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib/pkgconfig:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib/pkgconfig"
|
||||
PKG_CONFIG_LIBDIR = "/usr/local/ohos-sdk/linux/native/sysroot/usr/lib:/usr/local/ohos-sdk/linux/native/sysroot/usr/local/lib"
|
||||
PKG_CONFIG_SYSROOT_DIR = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
SYSROOT = "/usr/local/ohos-sdk/linux/native/sysroot"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.riscv64gc-unknown-linux-musl]
|
||||
linker = "riscv64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
@@ -58,6 +72,10 @@ rustflags = ["-C", "target-feature=+crt-static"]
|
||||
linker = "armv7-unknown-linux-musleabi-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.loongarch64-unknown-linux-musl]
|
||||
linker = "loongarch64-unknown-linux-musl-gcc"
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
linker = "arm-unknown-linux-musleabihf-gcc"
|
||||
rustflags = [
|
||||
|
108
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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.
|
||||
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?
|
169
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -7,32 +7,173 @@ description: 提出一个想法 / Suggest an idea
|
||||
labels: ['type: feature request']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: 描述问题 / Describe the problem
|
||||
description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
|
||||
placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
|
||||
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: solution
|
||||
id: use-case
|
||||
attributes:
|
||||
label: "描述您想要的解决方案 / Describe the solution you'd like"
|
||||
description: 明确说明您希望做出的改变 / A clear description of what change you would like
|
||||
placeholder: '我希望... / I would like to...'
|
||||
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: current-limitations
|
||||
attributes:
|
||||
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: proposed-solution
|
||||
attributes:
|
||||
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: context
|
||||
id: implementation-priority
|
||||
attributes:
|
||||
label: 额外上下文 / Additional context
|
||||
description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
|
||||
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: additional-context
|
||||
attributes:
|
||||
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
|
12
.github/workflows/Dockerfile
vendored
@@ -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
|
||||
|
41
.github/workflows/core.yml
vendored
@@ -40,12 +40,12 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
@@ -83,6 +83,9 @@ jobs:
|
||||
- TARGET: x86_64-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-x86_64
|
||||
- TARGET: riscv64gc-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-riscv64
|
||||
- TARGET: mips-unknown-linux-musl
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-mips
|
||||
@@ -102,6 +105,10 @@ jobs:
|
||||
OS: ubuntu-22.04
|
||||
ARTIFACT_NAME: linux-arm
|
||||
|
||||
- TARGET: loongarch64-unknown-linux-musl
|
||||
OS: ubuntu-24.04
|
||||
ARTIFACT_NAME: linux-loongarch64
|
||||
|
||||
- TARGET: x86_64-apple-darwin
|
||||
OS: macos-latest
|
||||
ARTIFACT_NAME: macos-x86_64
|
||||
@@ -157,7 +164,7 @@ jobs:
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup protoc
|
||||
uses: arduino/setup-protoc@v2
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
# GitHub repo token to use to avoid rate limiter
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -167,6 +174,11 @@ jobs:
|
||||
run: |
|
||||
bash ./.github/workflows/install_rust.sh
|
||||
|
||||
# loongarch need llvm-18
|
||||
if [[ $TARGET =~ ^loongarch.*$ ]]; then
|
||||
sudo apt-get install -qq llvm-18 clang-18
|
||||
export LLVM_CONFIG_PATH=/usr/lib/llvm-18/bin/llvm-config
|
||||
fi
|
||||
# we set the sysroot when sysroot is a dir
|
||||
# this dir is a soft link generated by install_rust.sh
|
||||
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
|
||||
@@ -175,14 +187,19 @@ jobs:
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
|
||||
cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
|
||||
else
|
||||
if [[ $OS =~ ^windows.*$ ]]; then
|
||||
SUFFIX=.exe
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
elif [[ $TARGET =~ ^riscv64.*$ ]]; then
|
||||
CORE_FEATURES="--features=mimalloc"
|
||||
else
|
||||
CORE_FEATURES="--features=jemalloc"
|
||||
fi
|
||||
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
|
||||
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 --verbose --target $TARGET
|
||||
cargo build --release --target $TARGET $CORE_FEATURES
|
||||
fi
|
||||
|
||||
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
|
||||
@@ -212,8 +229,8 @@ jobs:
|
||||
|
||||
rustup set auto-self-update disable
|
||||
|
||||
rustup install 1.86
|
||||
rustup default 1.86
|
||||
rustup install 1.87
|
||||
rustup default 1.87
|
||||
|
||||
export CC=clang
|
||||
export CXX=clang++
|
||||
@@ -221,7 +238,7 @@ jobs:
|
||||
|
||||
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
|
||||
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
|
||||
cargo build --release --verbose --target $TARGET
|
||||
cargo build --release --verbose --target $TARGET --features=mimalloc
|
||||
|
||||
- name: Compress
|
||||
run: |
|
||||
@@ -243,8 +260,8 @@ jobs:
|
||||
TAG=$GITHUB_SHA
|
||||
fi
|
||||
|
||||
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
|
||||
UPX_VERSION=5.0.1
|
||||
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 .
|
||||
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
|
||||
|
4
.github/workflows/docker.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
image_tag:
|
||||
description: 'Tag for this image build'
|
||||
type: string
|
||||
default: 'v2.3.1'
|
||||
default: 'v2.4.1'
|
||||
required: true
|
||||
mark_latest:
|
||||
description: 'Mark this image as latest'
|
||||
@@ -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 }}
|
||||
|
8
.github/workflows/gui.yml
vendored
@@ -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 }}
|
||||
|
6
.github/workflows/install_rust.sh
vendored
@@ -15,6 +15,8 @@ if [[ $OS =~ ^ubuntu.*$ ]]; then
|
||||
# if target is mips or mipsel, we should use soft-float version of musl
|
||||
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET}sf
|
||||
elif [[ $TARGET =~ ^riscv64gc-.*$ ]]; then
|
||||
MUSL_TARGET=${TARGET/#riscv64gc-/riscv64-}
|
||||
fi
|
||||
if [[ $MUSL_TARGET =~ musl ]]; then
|
||||
mkdir -p ./musl_gcc
|
||||
@@ -29,8 +31,8 @@ fi
|
||||
|
||||
# see https://github.com/rust-lang/rustup/issues/3709
|
||||
rustup set auto-self-update disable
|
||||
rustup install 1.86
|
||||
rustup default 1.86
|
||||
rustup install 1.87
|
||||
rustup default 1.87
|
||||
|
||||
# mips/mipsel cannot add target from rustup, need compile by ourselves
|
||||
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
|
||||
|
10
.github/workflows/mobile.yml
vendored
@@ -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 }}
|
||||
|
114
.github/workflows/ohos.yml
vendored
Normal file
@@ -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
|
8
.github/workflows/release.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
version:
|
||||
description: 'Version for this release'
|
||||
type: string
|
||||
default: 'v2.3.1'
|
||||
default: 'v2.4.1'
|
||||
required: true
|
||||
make_latest:
|
||||
description: 'Mark this release as latest'
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Core Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.core_run_id }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
path: release_assets
|
||||
|
||||
- name: Download GUI Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.gui_run_id }}
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
path: release_assets_nozip
|
||||
|
||||
- name: Download Mobile Artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
run_id: ${{ inputs.mobile_run_id }}
|
||||
|
11
.github/workflows/test.yml
vendored
@@ -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
|
||||
@@ -91,6 +91,7 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture
|
||||
sudo prlimit --pid $$ --nofile=1048576:1048576
|
||||
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1
|
||||
sudo chown -R $USER:$USER ./target
|
||||
sudo chown -R $USER:$USER ~/.cargo
|
||||
|
4
.gitignore
vendored
@@ -12,6 +12,7 @@ target-*/
|
||||
|
||||
.vscode
|
||||
/.idea
|
||||
/.direnv/
|
||||
|
||||
# perf & flamegraph
|
||||
perf.data
|
||||
@@ -37,3 +38,6 @@ node_modules
|
||||
.vite
|
||||
|
||||
easytier-gui/src-tauri/*.dll
|
||||
/easytier-contrib/easytier-ohrs/dist/
|
||||
|
||||
.direnv
|
||||
|
0
.gitmodules
vendored
225
CONTRIBUTING.md
Normal file
@@ -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.87)
|
||||
- LLVM and Clang
|
||||
- Protoc (Protocol Buffers compiler)
|
||||
|
||||
#### Platform-Specific Dependencies
|
||||
|
||||
**Linux (Ubuntu/Debian)**
|
||||
```bash
|
||||
# Core build dependencies
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
musl-tools \
|
||||
llvm \
|
||||
clang \
|
||||
protobuf-compiler
|
||||
|
||||
# GUI build dependencies
|
||||
sudo apt install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libappindicator3-dev \
|
||||
patchelf
|
||||
|
||||
# Testing dependencies
|
||||
sudo apt install -y bridge-utils
|
||||
```
|
||||
|
||||
**For Cross-Compilation**
|
||||
- musl-cross toolchain (for MIPS and other architectures)
|
||||
- Additional setup may be required (see `.github/workflows/` for details)
|
||||
|
||||
**For Android Development**
|
||||
- Java 20
|
||||
- Android SDK (Build Tools 34.0.0)
|
||||
- Android NDK (26.0.10792818)
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/EasyTier/EasyTier.git
|
||||
cd EasyTier
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
# Install Rust toolchain
|
||||
rustup install 1.87
|
||||
rustup default 1.87
|
||||
|
||||
# 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!
|
233
CONTRIBUTING_zh.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# EasyTier 贡献指南
|
||||
|
||||
[English Version](CONTRIBUTING.md)
|
||||
|
||||
感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。
|
||||
|
||||
## 目录
|
||||
|
||||
- [EasyTier 贡献指南](#easytier-贡献指南)
|
||||
- [目录](#目录)
|
||||
- [开发环境配置](#开发环境配置)
|
||||
- [前置要求](#前置要求)
|
||||
- [必需工具](#必需工具)
|
||||
- [平台特定依赖](#平台特定依赖)
|
||||
- [安装步骤](#安装步骤)
|
||||
- [项目结构](#项目结构)
|
||||
- [构建指南](#构建指南)
|
||||
- [构建核心组件](#构建核心组件)
|
||||
- [构建桌面应用](#构建桌面应用)
|
||||
- [构建移动应用](#构建移动应用)
|
||||
- [构建注意事项](#构建注意事项)
|
||||
- [开发工作流](#开发工作流)
|
||||
- [测试指南](#测试指南)
|
||||
- [运行测试](#运行测试)
|
||||
- [测试要求](#测试要求)
|
||||
- [Pull Request 规范](#pull-request-规范)
|
||||
- [其他资源](#其他资源)
|
||||
- [需要帮助?](#需要帮助)
|
||||
|
||||
## 开发环境配置
|
||||
|
||||
### 前置要求
|
||||
|
||||
#### 必需工具
|
||||
- Node.js v21 或更高版本
|
||||
- pnpm v9 或更高版本
|
||||
- Rust 工具链(版本 1.87)
|
||||
- LLVM 和 Clang
|
||||
- Protoc(Protocol Buffers 编译器)
|
||||
|
||||
#### 平台特定依赖
|
||||
|
||||
**Linux (Ubuntu/Debian)**
|
||||
```bash
|
||||
# 核心构建依赖
|
||||
sudo apt-get update && sudo apt-get install -y \
|
||||
musl-tools \
|
||||
llvm \
|
||||
clang \
|
||||
protobuf-compiler
|
||||
|
||||
# GUI 构建依赖
|
||||
sudo apt install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libgtk-3-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libappindicator3-dev \
|
||||
patchelf
|
||||
|
||||
# 测试依赖
|
||||
sudo apt install -y bridge-utils
|
||||
```
|
||||
|
||||
**交叉编译依赖**
|
||||
- musl-cross 工具链(用于 MIPS 和其他架构)
|
||||
- 可能需要额外配置(详见 `.github/workflows/` 目录)
|
||||
|
||||
**Android 开发依赖**
|
||||
- Java 20
|
||||
- Android SDK(Build Tools 34.0.0)
|
||||
- Android NDK(26.0.10792818)
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆仓库:
|
||||
```bash
|
||||
git clone https://github.com/EasyTier/EasyTier.git
|
||||
cd EasyTier
|
||||
```
|
||||
|
||||
2. 安装依赖:
|
||||
```bash
|
||||
# 安装 Rust 工具链
|
||||
rustup install 1.87
|
||||
rustup default 1.87
|
||||
|
||||
# 安装项目依赖
|
||||
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 做出贡献!
|
1531
Cargo.lock
generated
@@ -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"
|
||||
|
@@ -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",
|
||||
}
|
||||
}
|
174
LICENSE
@@ -1,73 +1,165 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
0. Additional Definitions.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
4. Combined Works.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
d) Do one of the following:
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
Copyright 2023 sunsijie
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
5. Combined Libraries.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
496
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
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-5.png" width="300">
|
||||
<img src="assets/image-4.png" width="300">
|
||||
<img src="assets/config-page.png" width="300" alt="config page">
|
||||
<img src="assets/running-page.png" width="300" alt="running page">
|
||||
</p>
|
||||
|
||||
📚 **[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)**
|
||||
|
||||
## 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.
|
||||
### Core Features
|
||||
|
||||
## Installation
|
||||
- 🔒 **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
|
||||
|
||||
1. **Download the precompiled binary file**
|
||||
### Advanced Capabilities
|
||||
|
||||
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.
|
||||
- 🔌 **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
|
||||
|
||||
2. **Install via crates.io**
|
||||
### Network Optimization
|
||||
|
||||
```sh
|
||||
cargo install easytier
|
||||
```
|
||||
|
||||
3. **Install from source code**
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **Install by Docker Compose**
|
||||
|
||||
Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.
|
||||
|
||||
5. **Install by script (For Linux Only)**
|
||||
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
The script supports the following commands and options:
|
||||
|
||||
Commands:
|
||||
- `install`: Install EasyTier
|
||||
- `uninstall`: Uninstall EasyTier
|
||||
- `update`: Update EasyTier to the latest version
|
||||
- `help`: Show help message
|
||||
|
||||
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/)
|
||||
|
||||
Examples:
|
||||
```sh
|
||||
# Show help
|
||||
bash /tmp/easytier.sh help
|
||||
|
||||
# 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/
|
||||
|
||||
# Update EasyTier
|
||||
bash /tmp/easytier.sh update
|
||||
|
||||
# Uninstall EasyTier
|
||||
bash /tmp/easytier.sh uninstall
|
||||
```
|
||||
|
||||
6. **Install by Homebrew (For MacOS Only)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask 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
|
||||
|
||||
## Quick Start
|
||||
|
||||
> The following text only describes the use of the command-line tool; the GUI program can be configured by referring to the following concepts.
|
||||
### 📥 Installation
|
||||
|
||||
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
|
||||
Choose the installation method that best suits your needs:
|
||||
|
||||
### Two-node Networking
|
||||
```bash
|
||||
# 1. Download pre-built binary (Recommended, All platforms supported)
|
||||
# Visit https://github.com/EasyTier/EasyTier/releases
|
||||
|
||||
Assuming the network topology of the two nodes is as follows
|
||||
# 2. Install via cargo (Latest development version)
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
|
||||
# 3. Install via Docker
|
||||
# See https://easytier.cn/en/guide/installation.html#installation-methods
|
||||
|
||||
# 4. Linux Quick Install
|
||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
|
||||
|
||||
# 5. MacOS via Homebrew
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
|
||||
# 6. OpenWrt Luci Web UI
|
||||
# Visit https://github.com/EasyTier/luci-app-easytier
|
||||
|
||||
# 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
|
||||
|
||||
```
|
||||
|
||||
### 🚀 Basic Usage
|
||||
|
||||
#### Quick Networking with Shared Nodes
|
||||
|
||||
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 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.
|
||||
|
||||
Taking two nodes as an example (Please use more complex network name to avoid conflicts):
|
||||
|
||||
1. Run on Node A:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```
|
||||
|
||||
2. Run on Node B:
|
||||
|
||||
```bash
|
||||
# Run with administrator privileges
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```
|
||||
|
||||
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.1-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.1-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.1-70e69a38~ |
|
||||
```
|
||||
|
||||
You can test connectivity between nodes:
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
Note: If you cannot ping through, it may be that the firewall is blocking incoming traffic. Please turn off the firewall or add allow rules.
|
||||
|
||||
To improve availability, you can connect to multiple shared nodes simultaneously:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### Decentralized Networking
|
||||
|
||||
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:
|
||||
|
||||
1. Start First Node (Node A):
|
||||
|
||||
```bash
|
||||
# Start the first node
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
After startup, this node will listen on the following ports by default:
|
||||
- TCP: 11010
|
||||
- UDP: 11010
|
||||
- WebSocket: 11011
|
||||
- WebSocket SSL: 11012
|
||||
- WireGuard: 11013
|
||||
|
||||
2. Connect Second Node (Node B):
|
||||
|
||||
```bash
|
||||
# Connect to the first node using its public IP
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://FIRST_NODE_PUBLIC_IP:11010
|
||||
```
|
||||
|
||||
3. Verify Connection:
|
||||
|
||||
```bash
|
||||
# Test connectivity
|
||||
ping 10.144.144.2
|
||||
|
||||
# View connected peers
|
||||
easytier-cli peer
|
||||
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
|
||||
# View local node information
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
For more nodes to join the network, they can connect to any existing node in the network using the `-p` parameter:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 🔍 Advanced Features
|
||||
|
||||
#### Subnet Proxy
|
||||
|
||||
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<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. Execute on Node A:
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
Successful execution of the command will print the following.
|
||||
|
||||

|
||||
|
||||
2. Execute on Node B
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. Test Connectivity
|
||||
|
||||
The two nodes should connect successfully and be able to communicate within the virtual subnet
|
||||
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
Use easytier-cli to view node information in the subnet
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### Multi-node Networking
|
||||
|
||||
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph Node A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>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.
|
||||
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:
|
||||
|
||||
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
|
||||
1. Check if the routing information has been synchronized (the proxy_cidrs column shows the proxied subnets):
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. Test whether Node A can access nodes under the proxied subnet
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Networking without Public IP
|
||||
|
||||
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.cn:11010``.
|
||||
|
||||
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
|
||||
|
||||
Taking two nodes as an example, Node A executes:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```bash
|
||||
# View routing information
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||
Node B executes
|
||||

|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
2. Test if you can access nodes in the proxied subnet:
|
||||
|
||||
```bash
|
||||
# Test connectivity to proxied subnet
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
|
||||
#### WireGuard Integration
|
||||
|
||||
### Use EasyTier with WireGuard Client
|
||||
|
||||
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.
|
||||
|
||||
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<br/>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<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph Node B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
@@ -275,86 +246,77 @@ id1[[10.1.1.0/24]]
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
To enable an iPhone to access the EasyTier network through Node A, the following configuration can be applied:
|
||||
1. Start EasyTier with WireGuard portal enabled:
|
||||
|
||||
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.
|
||||
|
||||
```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.
|
||||
2. Get 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
|
||||
|
||||
############### 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.
|
||||
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
|
||||
|
||||
### Self-Hosted Public Server
|
||||
#### Self-Hosted Public Shared Node
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
|
||||
To run a public shared node:
|
||||
|
||||
You can also join the official public server cluster with following command:
|
||||
|
||||
```
|
||||
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
|
||||
```bash
|
||||
# No need to specify IPv4 address for public shared nodes
|
||||
sudo easytier-core --network-name mysharednode --network-secret mysharednode
|
||||
```
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
### Contact Us
|
||||
|
||||
- 💬 **[Telegram Group](https://t.me/easytier)**
|
||||
- 👥 **[QQ Group: 949700262](https://qm.qq.com/cgi-bin/qm/qr?k=kC8YJ6Jb8vWJIDbZrZJB8pB5YZgPJA5-)**
|
||||
|
||||
## License
|
||||
|
||||
EasyTier is released under the [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
## Contact
|
||||
|
||||
- Ask questions or report problems: [GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- Discussion and exchange: [GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- Telegram:https://t.me/easytier
|
||||
- QQ Group: 949700262
|
||||
EasyTier is released under the [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE).
|
||||
|
||||
## Sponsor
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200" alt="EdgeOne Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Special thanks to [Langlang Cloud](https://langlangy.cn/?i26c5a5) and [RainCloud](https://www.rainyun.com/NjM0NzQ1_) for sponsoring our public servers.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
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.
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
</p>
|
||||
|
504
README_CN.md
@@ -1,271 +1,243 @@
|
||||
# EasyTier
|
||||
|
||||
[](https://github.com/EasyTier/EasyTier/releases)
|
||||
[](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
|
||||
[](https://github.com/EasyTier/EasyTier/commits/main)
|
||||
[](https://github.com/EasyTier/EasyTier/issues)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
|
||||
[](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
|
||||
[](https://deepwiki.com/EasyTier/EasyTier)
|
||||
|
||||
[简体中文](/README_CN.md) | [English](/README.md)
|
||||
|
||||
**请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
|
||||
|
||||
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
|
||||
> ✨ 一个由 Rust 和 Tokio 驱动的简单、安全、去中心化的异地组网方案
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/image-6.png" width="300">
|
||||
<img src="assets/image-7.png" width="300">
|
||||
<img src="assets/config-page.png" width="300" alt="配置页面">
|
||||
<img src="assets/running-page.png" width="300" alt="运行页面">
|
||||
</p>
|
||||
|
||||
## 特点
|
||||
📚 **[完整文档](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. **通过源码安装**
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
|
||||
```
|
||||
|
||||
4. **通过Docker Compose安装**
|
||||
|
||||
请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
|
||||
|
||||
5. **使用一键脚本安装 (仅适用于 Linux)**
|
||||
|
||||
```sh
|
||||
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
|
||||
```
|
||||
|
||||
脚本支持以下命令和选项:
|
||||
|
||||
命令:
|
||||
- `install`: 安装 EasyTier
|
||||
- `uninstall`: 卸载 EasyTier
|
||||
- `update`: 更新 EasyTier 到最新版本
|
||||
- `help`: 显示帮助信息
|
||||
|
||||
选项:
|
||||
- `--skip-folder-verify`: 跳过安装过程中的文件夹验证
|
||||
- `--skip-folder-fix`: 跳过自动修复文件夹路径
|
||||
- `--no-gh-proxy`: 禁用 GitHub 代理
|
||||
- `--gh-proxy`: 设置自定义 GitHub 代理 URL (默认值: https://ghfast.top/)
|
||||
|
||||
示例:
|
||||
```sh
|
||||
# 查看帮助
|
||||
bash /tmp/easytier.sh help
|
||||
|
||||
# 安装(带选项)
|
||||
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/
|
||||
|
||||
# 更新 EasyTier
|
||||
bash /tmp/easytier.sh update
|
||||
|
||||
# 卸载 EasyTier
|
||||
bash /tmp/easytier.sh uninstall
|
||||
```
|
||||
|
||||
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
|
||||
|
||||
```sh
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier
|
||||
```
|
||||
- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
|
||||
- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
|
||||
- 🛠️ **零配置**:静态链接的可执行文件,简单部署
|
||||
|
||||
## 快速开始
|
||||
|
||||
> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
|
||||
### 📥 安装
|
||||
|
||||
确保已按照 [安装指南](#安装) 安装 EasyTier,并且 easytier-core 和 easytier-cli 两个命令都已经可用。
|
||||
选择最适合您需求的安装方式:
|
||||
|
||||
### 双节点组网
|
||||
```bash
|
||||
# 1. 下载预编译二进制文件(推荐,支持所有平台)
|
||||
# 访问 https://github.com/EasyTier/EasyTier/releases
|
||||
|
||||
假设双节点的网络拓扑如下
|
||||
# 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
|
||||
|
||||
# 4. Linux 快速安装
|
||||
wget -O- https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh | sudo bash
|
||||
|
||||
# 5. MacOS 通过 Homebrew 安装
|
||||
brew tap brewforge/chinese
|
||||
brew install --cask easytier-gui
|
||||
|
||||
# 6. OpenWrt Luci Web 界面
|
||||
# 访问 https://github.com/EasyTier/luci-app-easytier
|
||||
|
||||
# 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 支持使用共享公共节点快速组网。当您没有公网 IP 时,可以使用 EasyTier 社区提供的免费共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
|
||||
|
||||
当前部署的共享公共节点是 `tcp://public.easytier.cn:11010`。
|
||||
|
||||
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name` 和 `--network-secret` 参数作为网络的唯一标识符。
|
||||
|
||||
以两个节点为例(请使用更复杂的网络名称以避免冲突):
|
||||
|
||||
1. 在节点 A 上运行:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```
|
||||
|
||||
2. 在节点 B 上运行:
|
||||
|
||||
```bash
|
||||
# 以管理员权限运行
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```
|
||||
|
||||
执行成功后,可以使用 `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.1-70e69a38~ |
|
||||
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.4.1-70e69a38~ |
|
||||
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.4.1-70e69a38~ |
|
||||
```
|
||||
|
||||
您可以测试节点之间的连通性:
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.126.126.1
|
||||
ping 10.126.126.2
|
||||
```
|
||||
|
||||
注意:如果无法 ping 通,可能是防火墙阻止了入站流量。请关闭防火墙或添加允许规则。
|
||||
|
||||
为了提高可用性,您可以同时连接多个共享节点:
|
||||
|
||||
```bash
|
||||
# 连接多个共享节点
|
||||
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010 -p udp://public.easytier.cn:11010
|
||||
```
|
||||
|
||||
#### 去中心化组网
|
||||
|
||||
EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
|
||||
|
||||
1. 启动第一个节点(节点 A):
|
||||
|
||||
```bash
|
||||
# 启动第一个节点
|
||||
sudo easytier-core -i 10.144.144.1
|
||||
```
|
||||
|
||||
启动后,该节点将默认监听以下端口:
|
||||
- TCP:11010
|
||||
- UDP:11010
|
||||
- WebSocket:11011
|
||||
- WebSocket SSL:11012
|
||||
- WireGuard:11013
|
||||
|
||||
2. 连接第二个节点(节点 B):
|
||||
|
||||
```bash
|
||||
# 使用第一个节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.2 -p udp://第一个节点的公网IP:11010
|
||||
```
|
||||
|
||||
3. 验证连接:
|
||||
|
||||
```bash
|
||||
# 测试连通性
|
||||
ping 10.144.144.2
|
||||
|
||||
# 查看已连接的对等节点
|
||||
easytier-cli peer
|
||||
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
|
||||
# 查看本地节点信息
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||
更多节点要加入网络,可以使用 `-p` 参数连接到网络中的任何现有节点:
|
||||
|
||||
```bash
|
||||
# 使用任何现有节点的公网 IP 连接
|
||||
sudo easytier-core -i 10.144.144.3 -p udp://任何现有节点的公网IP:11010
|
||||
```
|
||||
|
||||
### 🔍 高级功能
|
||||
|
||||
#### 子网代理
|
||||
|
||||
假设网络拓扑如下,节点 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<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
end
|
||||
|
||||
nodea <-----> nodeb
|
||||
|
||||
```
|
||||
|
||||
1. 在节点 A 上执行:
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.1
|
||||
```
|
||||
|
||||
命令执行成功会有如下打印。
|
||||
|
||||

|
||||
|
||||
2. 在节点 B 执行
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
3. 测试联通性
|
||||
|
||||
两个节点应成功连接并能够在虚拟子网内通信
|
||||
|
||||
```sh
|
||||
ping 10.144.144.2
|
||||
```
|
||||
|
||||
使用 easytier-cli 查看子网中的节点信息
|
||||
|
||||
```sh
|
||||
easytier-cli peer
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
easytier-cli node
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 多节点组网
|
||||
|
||||
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
|
||||
```
|
||||
|
||||
其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
|
||||
|
||||
---
|
||||
|
||||
### 子网代理(点对网)配置
|
||||
|
||||
假设网络拓扑如下,节点 B 想将其可访问的子网 10.1.1.0/24 共享给其他节点。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph 节点 A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>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 列展示了被代理的子网。
|
||||
1. 检查路由信息是否已同步(proxy_cidrs 列显示代理的子网):
|
||||
|
||||
```sh
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||

|
||||
|
||||
2. 测试节点 A 是否可访问被代理子网下的节点
|
||||
|
||||
```sh
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 无公网IP组网
|
||||
|
||||
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
|
||||
|
||||
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
|
||||
|
||||
以双节点为例,节点 A 执行:
|
||||
|
||||
```sh
|
||||
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
```bash
|
||||
# 查看路由信息
|
||||
easytier-cli route
|
||||
```
|
||||
|
||||
节点 B 执行
|
||||

|
||||
|
||||
```sh
|
||||
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
|
||||
2. 测试是否可以访问代理子网中的节点:
|
||||
|
||||
```bash
|
||||
# 测试到代理子网的连通性
|
||||
ping 10.1.1.2
|
||||
```
|
||||
|
||||
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
|
||||
#### WireGuard 集成
|
||||
|
||||
---
|
||||
|
||||
### 使用 WireGuard 客户端接入
|
||||
|
||||
EasyTier 可以用作 WireGuard 服务端,让任意安装了 WireGuard 客户端的设备访问 EasyTier 网络。对于目前 EasyTier 不支持的平台 (如 iOS、Android 等),可以使用这种方式接入 EasyTier 网络。
|
||||
|
||||
假设网络拓扑如下:
|
||||
EasyTier 可以作为 WireGuard 服务器,允许任何安装了 WireGuard 客户端的设备(包括 iOS 和 Android)访问 EasyTier 网络。以下是设置示例:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
ios[[iPhone \n 安装 WireGuard]]
|
||||
ios[[iPhone<br/>已安装 WireGuard]]
|
||||
|
||||
subgraph 节点 A IP 22.1.1.1
|
||||
nodea[EasyTier\n10.144.144.1]
|
||||
subgraph 节点 A 公网 IP 22.1.1.1
|
||||
nodea[EasyTier<br/>10.144.144.1]
|
||||
end
|
||||
|
||||
subgraph 节点 B
|
||||
nodeb[EasyTier\n10.144.144.2]
|
||||
nodeb[EasyTier<br/>10.144.144.2]
|
||||
end
|
||||
|
||||
id1[[10.1.1.0/24]]
|
||||
@@ -273,88 +245,78 @@ id1[[10.1.1.0/24]]
|
||||
ios <-.-> nodea <--> nodeb <-.-> id1
|
||||
```
|
||||
|
||||
我们需要 iPhone 通过节点 A 访问 EasyTier 网络,则可进行如下配置:
|
||||
1. 启动启用 WireGuard 门户的 EasyTier:
|
||||
|
||||
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
|
||||
|
||||
```sh
|
||||
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段
|
||||
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```bash
|
||||
# 在 0.0.0.0:11013 上监听,并使用 10.14.14.0/24 子网作为 WireGuard 客户端
|
||||
sudo easytier-core -i 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
|
||||
```
|
||||
|
||||
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
|
||||
2. 获取 WireGuard 客户端配置:
|
||||
|
||||
```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 ###############
|
||||
|
||||
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。
|
||||
### 联系我们
|
||||
|
||||
## 社区和贡献
|
||||
|
||||
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
|
||||
|
||||
## 相关项目和资源
|
||||
|
||||
- [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): 可靠、低延迟、抗审查的虚拟专用网络
|
||||
- 💬 **[Telegram 群组](https://t.me/easytier)**
|
||||
- 👥 **[QQ 群:949700262](https://qm.qq.com/cgi-bin/qm/qr?k=kC8YJ6Jb8vWJIDbZrZJB8pB5YZgPJA5-)**
|
||||
|
||||
## 许可证
|
||||
|
||||
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
|
||||
|
||||
## 联系方式
|
||||
|
||||
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
|
||||
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
|
||||
- QQ 群: 949700262
|
||||
- Telegram:https://t.me/easytier
|
||||
EasyTier 在 [LGPL-3.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可下发布。
|
||||
|
||||
## 赞助
|
||||
|
||||
<img src="assets/image-8.png" width="300">
|
||||
<img src="assets/image-9.png" width="300">
|
||||
本项目的 CDN 加速和安全防护由腾讯云 EdgeOne 赞助。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://edgeone.ai/?from=github" target="_blank">
|
||||
<img src="assets/edgeone.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
特别感谢 [浪浪云](https://langlangy.cn/?i26c5a5) 和 [雨云](https://www.rainyun.com/NjM0NzQ1_) 赞助我们的公共服务器。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/langlang.png" width="200">
|
||||
</a>
|
||||
<a href="https://langlangy.cn/?i26c5a5" target="_blank">
|
||||
<img src="assets/raincloud.png" width="200">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
如果您觉得 EasyTier 有帮助,请考虑赞助我们。软件开发和维护需要大量的时间和精力,您的赞助将帮助我们更好地维护和改进 EasyTier。
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/wechat.png" width="200">
|
||||
<img src="assets/alipay.png" width="200">
|
||||
</p>
|
||||
|
BIN
assets/alipay.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/config-page.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
assets/edgeone.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
assets/langlang.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
assets/raincloud.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/running-page.png
Normal file
After Width: | Height: | Size: 138 KiB |
BIN
assets/wechat.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
@@ -14,3 +14,4 @@ dashmap = "6.0"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
uuid = "1.17.0"
|
||||
|
@@ -3,11 +3,14 @@ use std::sync::Mutex;
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader as _, TomlConfigLoader},
|
||||
launcher::NetworkInstance,
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::ConfigSource,
|
||||
};
|
||||
|
||||
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||
static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy<DashMap<String, uuid::Uuid>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
||||
|
||||
static ERROR_MSG: once_cell::sync::Lazy<Mutex<Vec<u8>>> =
|
||||
once_cell::sync::Lazy::new(|| Mutex::new(Vec::new()));
|
||||
@@ -26,6 +29,30 @@ fn set_error_msg(msg: &str) {
|
||||
msg_buf[..len].copy_from_slice(bytes);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_tun_fd(
|
||||
inst_name: *const std::ffi::c_char,
|
||||
fd: std::ffi::c_int,
|
||||
) -> std::ffi::c_int {
|
||||
let inst_name = unsafe {
|
||||
assert!(!inst_name.is_null());
|
||||
std::ffi::CStr::from_ptr(inst_name)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
return -1;
|
||||
}
|
||||
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();
|
||||
@@ -86,18 +113,20 @@ pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std:
|
||||
|
||||
let inst_name = cfg.get_inst_name();
|
||||
|
||||
if INSTANCE_MAP.contains_key(&inst_name) {
|
||||
if INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
|
||||
set_error_msg("instance already exists");
|
||||
return -1;
|
||||
}
|
||||
|
||||
let mut instance = NetworkInstance::new(cfg);
|
||||
if let Err(e) = instance.start().map_err(|e| e.to_string()) {
|
||||
let instance_id = match INSTANCE_MANAGER.run_network_instance(cfg, ConfigSource::FFI) {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to start instance: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
INSTANCE_MAP.insert(inst_name, instance);
|
||||
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
|
||||
|
||||
0
|
||||
}
|
||||
@@ -108,7 +137,11 @@ pub extern "C" fn retain_network_instance(
|
||||
length: usize,
|
||||
) -> std::ffi::c_int {
|
||||
if length == 0 {
|
||||
INSTANCE_MAP.clear();
|
||||
if let Err(e) = INSTANCE_MANAGER.retain_network_instance(Vec::new()) {
|
||||
set_error_msg(&format!("failed to retain instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
INSTANCE_NAME_ID_MAP.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -125,7 +158,17 @@ pub extern "C" fn retain_network_instance(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k));
|
||||
let inst_ids: Vec<uuid::Uuid> = inst_names
|
||||
.iter()
|
||||
.filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id))
|
||||
.collect();
|
||||
|
||||
if let Err(e) = INSTANCE_MANAGER.retain_network_instance(inst_ids) {
|
||||
set_error_msg(&format!("failed to retain instances: {}", e));
|
||||
return -1;
|
||||
}
|
||||
|
||||
let _ = INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
|
||||
|
||||
0
|
||||
}
|
||||
@@ -144,13 +187,20 @@ pub extern "C" fn collect_network_infos(
|
||||
std::slice::from_raw_parts_mut(infos, max_length)
|
||||
};
|
||||
|
||||
let collected_infos = match INSTANCE_MANAGER.collect_network_infos() {
|
||||
Ok(infos) => infos,
|
||||
Err(e) => {
|
||||
set_error_msg(&format!("failed to collect network infos: {}", e));
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
let mut index = 0;
|
||||
for instance in INSTANCE_MAP.iter() {
|
||||
for (instance_id, value) in collected_infos.iter() {
|
||||
if index >= max_length {
|
||||
break;
|
||||
}
|
||||
let key = instance.key();
|
||||
let Some(value) = instance.get_running_info() else {
|
||||
let Some(key) = INSTANCE_MANAGER.get_network_instance_name(instance_id) else {
|
||||
continue;
|
||||
};
|
||||
// convert value to json string
|
||||
@@ -181,7 +231,6 @@ mod tests {
|
||||
let cfg_str = r#"
|
||||
inst_name = "test"
|
||||
network = "test_network"
|
||||
fdsafdsa
|
||||
"#;
|
||||
let cstr = std::ffi::CString::new(cfg_str).unwrap();
|
||||
assert_eq!(parse_config(cstr.as_ptr()), 0);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
id=easytier_magisk
|
||||
name=EasyTier_Magisk
|
||||
version=v2.3.1
|
||||
version=v2.4.1
|
||||
versionCode=1
|
||||
author=EasyTier
|
||||
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
|
||||
|
5778
easytier-contrib/easytier-ohrs/Cargo.lock
generated
Normal file
46
easytier-contrib/easytier-ohrs/Cargo.toml
Normal file
@@ -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
|
65
easytier-contrib/easytier-ohrs/README.md
Normal file
@@ -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}"
|
3
easytier-contrib/easytier-ohrs/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main () {
|
||||
napi_build_ohos::setup();
|
||||
}
|
31
easytier-contrib/easytier-ohrs/env.sh
Normal file
@@ -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}"
|
148
easytier-contrib/easytier-ohrs/src/lib.rs
Normal file
@@ -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<NetworkInstanceManager> =
|
||||
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<String>) {
|
||||
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<KeyValuePair> {
|
||||
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<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
98
easytier-contrib/easytier-ohrs/src/native_log.rs
Normal file
@@ -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<String, String>) {
|
||||
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::<Vec<_>>());
|
||||
}
|
||||
Level::DEBUG => {
|
||||
hilog_debug!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::INFO => {
|
||||
hilog_info!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::WARN => {
|
||||
hilog_warn!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
Level::ERROR => {
|
||||
hilog_error!("[{}] {:?}", loc, fields.values().collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackLayer {
|
||||
callback: Box<dyn Fn(&Event, HashMap<String, String>) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for CallbackLayer {
|
||||
fn on_event(&self, event: &Event, _ctx: Context<S>) {
|
||||
// 使用 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<String, String>);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
@@ -1,116 +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: 显示配置
|
||||
close: 关闭
|
||||
|
||||
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: 端口转发添加
|
@@ -1,115 +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
|
||||
close: Close
|
||||
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
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "easytier-gui",
|
||||
"type": "module",
|
||||
"version": "2.3.1",
|
||||
"version": "2.4.1",
|
||||
"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",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-gui"
|
||||
version = "2.3.1"
|
||||
version = "2.4.1"
|
||||
description = "EasyTier GUI"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
@@ -23,7 +23,7 @@ thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = f
|
||||
|
||||
[dependencies]
|
||||
# wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
|
||||
tauri = { version = "=2.0.6", features = [
|
||||
tauri = { version = "2.7.0", features = [
|
||||
"tray-icon",
|
||||
"image-png",
|
||||
"image-ico",
|
||||
@@ -40,24 +40,32 @@ chrono = { version = "0.4.37", features = ["serde"] }
|
||||
|
||||
once_cell = "1.18.0"
|
||||
dashmap = "6.0"
|
||||
|
||||
privilege = "0.3"
|
||||
gethostname = "0.5"
|
||||
gethostname = "1.0.2"
|
||||
|
||||
dunce = "1.0.4"
|
||||
|
||||
tauri-plugin-shell = "2.0"
|
||||
tauri-plugin-process = "2.0"
|
||||
tauri-plugin-clipboard-manager = "2.0"
|
||||
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
|
||||
tauri-plugin-shell = "2.3.0"
|
||||
tauri-plugin-process = "2.3.0"
|
||||
tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-positioner = { version = "2.3.0", features = ["tray-icon"] }
|
||||
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
|
||||
tauri-plugin-os = "2.0"
|
||||
tauri-plugin-autostart = "2.0"
|
||||
tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-autostart = "2.5.0"
|
||||
uuid = "1.17.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
winapi = { version = "0.3.9", features = ["securitybaseapi", "processthreadsapi"] }
|
||||
|
||||
[target.'cfg(target_family = "unix")'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
security-framework-sys = "2.9.0"
|
||||
|
||||
[features]
|
||||
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-single-instance = "2.3.2"
|
||||
|
@@ -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
|
||||
|
298
easytier-gui/src-tauri/gen/android/gradlew
vendored
@@ -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
|
||||
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
|
||||
|
||||
# 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
|
||||
# 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\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
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" ;;
|
||||
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
|
||||
|
||||
# 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, 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.
|
||||
|
||||
# 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"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
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
|
||||
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
|
||||
# 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
|
||||
fi
|
||||
|
||||
|
||||
# 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" "$@"
|
||||
|
41
easytier-gui/src-tauri/gen/android/gradlew.bat
vendored
@@ -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
|
||||
|
97
easytier-gui/src-tauri/src/elevate/linux.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
///
|
||||
/// # 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() };
|
||||
if uid == 0 {
|
||||
true
|
||||
} else {
|
||||
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<Output> {
|
||||
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)
|
||||
}
|
||||
}
|
182
easytier-gui/src-tauri/src/elevate/macos.rs
Normal file
@@ -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<P: AsRef<Path>>(exe_name: P) -> Option<PathBuf> {
|
||||
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<ExitStatus> {
|
||||
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<Output> {
|
||||
let status = runas_root_gui(self)?;
|
||||
Ok(Output {
|
||||
status,
|
||||
stdout: Vec::new(),
|
||||
stderr: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
182
easytier-gui/src-tauri/src/elevate/mod.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
#![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<Vec<u8>>,
|
||||
#[allow(dead_code)]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use elevated_command::Command;
|
||||
/// use std::process::Command as StdCommand;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut cmd = StdCommand::new("path to the application");
|
||||
///
|
||||
/// cmd.arg("some arg");
|
||||
/// cmd.env("some key", "some value");
|
||||
///
|
||||
/// let elevated_cmd = Command::new(cmd);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new(cmd: StdCommand) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
icon: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the `Take`, returning the wrapped std::process::Command
|
||||
///
|
||||
/// # 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 cmd = elevated_cmd.into_inner();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn into_inner(self) -> StdCommand {
|
||||
self.cmd
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the underlying std::process::Command
|
||||
///
|
||||
/// # 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 cmd = elevated_cmd.get_ref();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_ref(&self) -> &StdCommand {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
/// Gets a reference to the underlying std::process::Command
|
||||
///
|
||||
/// # 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 cmd = elevated_cmd.get_mut();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get_mut(&mut self) -> &mut StdCommand {
|
||||
&mut self.cmd
|
||||
}
|
||||
|
||||
/// Set the `icon` for the pop-up graphical OS dialog
|
||||
///
|
||||
/// This method is only applicable on `MacOS`
|
||||
///
|
||||
/// # 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);
|
||||
/// elevated_cmd.icon(include_bytes!("path to the icon").to_vec());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn icon(&mut self, icon: Vec<u8>) -> &mut Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name for the pop-up graphical OS dialog
|
||||
///
|
||||
/// This method is only applicable on `MacOS`
|
||||
///
|
||||
/// # 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);
|
||||
/// elevated_cmd.name("some name".to_string());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn name(&mut self, name: String) -> &mut Self {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StdCommand> 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;
|
114
easytier-gui/src-tauri/src/elevate/windows.rs
Normal file
@@ -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::<winapi::um::winnt::TOKEN_ELEVATION_TYPE>() 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<Output> {
|
||||
let args = self
|
||||
.cmd
|
||||
.get_args()
|
||||
.map(|c| c.to_str().unwrap().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
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::<u8>::new(),
|
||||
stderr: Vec::<u8>::new(),
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,12 +1,14 @@
|
||||
// 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 dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
|
||||
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
|
||||
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
|
||||
utils::{self, NewFilterSender},
|
||||
};
|
||||
|
||||
@@ -17,8 +19,8 @@ pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
|
||||
once_cell::sync::Lazy::new(DashMap::new);
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
||||
|
||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||
once_cell::sync::Lazy::new(Default::default);
|
||||
@@ -42,43 +44,48 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
Ok(toml.dump())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String> {
|
||||
let config = TomlConfigLoader::new_from_str(&toml_config).map_err(|e| e.to_string())?;
|
||||
let cfg = NetworkConfig::new_from_config(&config).map_err(|e| e.to_string())?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||
if INSTANCE_MAP.contains_key(cfg.instance_id()) {
|
||||
return Err("instance already exists".to_string());
|
||||
}
|
||||
let instance_id = cfg.instance_id().to_string();
|
||||
|
||||
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
let mut instance = NetworkInstance::new(cfg);
|
||||
instance.start().map_err(|e| e.to_string())?;
|
||||
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, ConfigSource::GUI)
|
||||
.map_err(|e| e.to_string())?;
|
||||
println!("instance {} started", instance_id);
|
||||
INSTANCE_MAP.insert(instance_id, instance);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn retain_network_instance(instance_ids: Vec<String>) -> Result<(), String> {
|
||||
let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k));
|
||||
println!(
|
||||
"instance {:?} retained",
|
||||
INSTANCE_MAP
|
||||
.iter()
|
||||
.map(|item| item.key().clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
let instance_ids = instance_ids
|
||||
.into_iter()
|
||||
.filter_map(|id| uuid::Uuid::parse_str(&id).ok())
|
||||
.collect();
|
||||
let retained = INSTANCE_MANAGER
|
||||
.retain_network_instance(instance_ids)
|
||||
.map_err(|e| e.to_string())?;
|
||||
println!("instance {:?} retained", retained);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn collect_network_infos() -> Result<BTreeMap<String, NetworkInstanceRunningInfo>, String> {
|
||||
let infos = INSTANCE_MANAGER
|
||||
.collect_network_infos()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut ret = BTreeMap::new();
|
||||
for instance in INSTANCE_MAP.iter() {
|
||||
if let Some(info) = instance.get_running_info() {
|
||||
ret.insert(instance.key().clone(), info);
|
||||
}
|
||||
for (uuid, info) in infos {
|
||||
ret.insert(uuid.to_string(), info);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
@@ -97,10 +104,10 @@ fn set_logging_level(level: String) -> Result<(), String> {
|
||||
|
||||
#[tauri::command]
|
||||
fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> {
|
||||
let mut instance = INSTANCE_MAP
|
||||
.get_mut(&instance_id)
|
||||
.ok_or("instance not found")?;
|
||||
instance.set_tun_fd(fd);
|
||||
let uuid = uuid::Uuid::parse_str(&instance_id).map_err(|e| e.to_string())?;
|
||||
INSTANCE_MANAGER
|
||||
.set_tun_fd(&uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -123,18 +130,20 @@ fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn check_sudo() -> bool {
|
||||
use std::env::current_exe;
|
||||
let is_elevated = privilege::user::privileged();
|
||||
let is_elevated = elevate::Command::is_elevated();
|
||||
if !is_elevated {
|
||||
let Ok(exe) = current_exe() else {
|
||||
return true;
|
||||
};
|
||||
let exe_path = std::env::var("APPIMAGE")
|
||||
.ok()
|
||||
.or_else(|| std::env::args().next())
|
||||
.unwrap_or_default();
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut elevated_cmd = privilege::runas::Command::new(exe);
|
||||
let mut stdcmd = std::process::Command::new(&exe_path);
|
||||
if args.contains(&AUTOSTART_ARG.to_owned()) {
|
||||
elevated_cmd.arg(AUTOSTART_ARG);
|
||||
stdcmd.arg(AUTOSTART_ARG);
|
||||
}
|
||||
let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
|
||||
elevate::Command::new(stdcmd)
|
||||
.output()
|
||||
.expect("Failed to run elevated command");
|
||||
}
|
||||
is_elevated
|
||||
}
|
||||
@@ -185,13 +194,15 @@ pub fn run() {
|
||||
let Ok(log_dir) = app.path().app_log_dir() else {
|
||||
return Ok(());
|
||||
};
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_file_logger_config(FileLoggerConfig {
|
||||
let config = LoggingConfigBuilder::default()
|
||||
.file_logger(FileLoggerConfig {
|
||||
dir: Some(log_dir.to_string_lossy().to_string()),
|
||||
level: None,
|
||||
file: None,
|
||||
});
|
||||
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let Ok(Some(logger_reinit)) = utils::init_logger(&config, true) else {
|
||||
return Ok(());
|
||||
};
|
||||
#[allow(static_mut_refs)]
|
||||
@@ -202,7 +213,7 @@ pub fn run() {
|
||||
// for tray icon, menu need to be built in js
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let _tray_menu = TrayIconBuilder::with_id("main")
|
||||
.menu_on_left_click(false)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
@@ -224,6 +235,7 @@ pub fn run() {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
parse_network_config,
|
||||
generate_network_config,
|
||||
run_network_instance,
|
||||
retain_network_instance,
|
||||
collect_network_infos,
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"createUpdaterArtifacts": false
|
||||
},
|
||||
"productName": "easytier-gui",
|
||||
"version": "2.3.1",
|
||||
"version": "2.4.1",
|
||||
"identifier": "com.kkrainbow.easytier",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
|
@@ -8,5 +8,6 @@ onBeforeMount(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast position="bottom-right" />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
2
easytier-gui/src/auto-imports.d.ts
vendored
@@ -23,6 +23,7 @@ declare global {
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const event2human: typeof import('./composables/utils')['event2human']
|
||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
@@ -134,6 +135,7 @@ declare module 'vue' {
|
||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/network')['generateNetworkConfig']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
|
@@ -8,6 +8,10 @@ export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
}
|
||||
|
||||
export async function generateNetworkConfig(tomlConfig: string) {
|
||||
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||
}
|
||||
|
||||
export async function runNetworkInstance(cfg: NetworkConfig) {
|
||||
return invoke('run_network_instance', { cfg })
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import { exit } from '@tauri-apps/plugin-process'
|
||||
import { open } from '@tauri-apps/plugin-shell'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib'
|
||||
|
||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||
import { useTray } from '~/composables/tray'
|
||||
@@ -23,7 +23,7 @@ useTray(true)
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: () => t('show_config'),
|
||||
label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: async () => {
|
||||
try {
|
||||
@@ -262,6 +262,13 @@ onMounted(async () => {
|
||||
function isRunning(id: string) {
|
||||
return networkStore.networkInstanceIds.includes(id)
|
||||
}
|
||||
|
||||
async function saveTomlConfig(tomlConfig: string) {
|
||||
const config = await generateNetworkConfig(tomlConfig)
|
||||
networkStore.replaceCurNetwork(config);
|
||||
toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 })
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -269,17 +276,8 @@ function isRunning(id: string) {
|
||||
|
||||
<template>
|
||||
<div id="root" class="flex flex-col">
|
||||
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
||||
<Panel>
|
||||
<ScrollPanel style="width: 100%; height: 300px">
|
||||
<pre>{{ tomlConfig }}</pre>
|
||||
</ScrollPanel>
|
||||
</Panel>
|
||||
<Divider />
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||
</div>
|
||||
</Dialog>
|
||||
<ConfigEditDialog v-model:visible="visible" :cur-network="curNetworkConfig" :readonly="activeStep !== '1'"
|
||||
:save-config="saveTomlConfig" :generate-config="parseNetworkConfig" />
|
||||
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
|
@@ -48,6 +48,12 @@ export const useNetworkStore = defineStore('networkStore', {
|
||||
this.curNetwork = this.networkList[nextCurNetworkIdx]
|
||||
},
|
||||
|
||||
replaceCurNetwork(cfg: NetworkTypes.NetworkConfig) {
|
||||
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
|
||||
this.networkList[curNetworkIdx] = cfg
|
||||
this.curNetwork = cfg
|
||||
},
|
||||
|
||||
removeNetworkInstance(instanceId: string) {
|
||||
delete this.instances[instanceId]
|
||||
},
|
||||
|
@@ -15,8 +15,6 @@
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "easytier-web"
|
||||
version = "2.3.1"
|
||||
version = "2.4.1"
|
||||
edition = "2021"
|
||||
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
|
||||
|
||||
@@ -14,6 +14,9 @@ dashmap = "6.1"
|
||||
url = "2.2"
|
||||
async-trait = "0.1"
|
||||
|
||||
maxminddb = "0.24"
|
||||
once_cell = "1.18"
|
||||
|
||||
axum = { version = "0.7", features = ["macros"] }
|
||||
axum-login = { version = "0.16" }
|
||||
password-auth = { version = "1.0.0" }
|
||||
@@ -34,7 +37,7 @@ sea-orm-migration = { version = "1.1" }
|
||||
|
||||
|
||||
# for captcha
|
||||
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
|
||||
rust-embed = { version = "8.5.0", features = ["debug-embed", "include-exclude"] }
|
||||
base64 = "0.22"
|
||||
rand = "0.8"
|
||||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
|
@@ -145,8 +145,11 @@ 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' },
|
||||
{ field: 'disable_quic_input', help: 'disable_quic_input_help' },
|
||||
{ field: 'disable_p2p', help: 'disable_p2p_help' },
|
||||
{ field: 'bind_device', help: 'bind_device_help' },
|
||||
{ field: 'no_tun', help: 'no_tun_help' },
|
||||
@@ -200,7 +203,7 @@ const bool_flags: BoolFlag[] = [
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="network_secret">{{ t('network_secret') }}</label>
|
||||
<Password id="network_secret" v-model="curNetwork.network_secret"
|
||||
aria-describedby="network_secret-help" toggleMask :feedback="false"/>
|
||||
aria-describedby="network_secret-help" toggleMask :feedback="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -216,7 +219,7 @@ const bool_flags: BoolFlag[] = [
|
||||
|
||||
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
|
||||
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
|
||||
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
|
||||
class="grow" dropdown :complete-on-focus="false"
|
||||
@complete="searchPresetPublicServers" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,6 +307,15 @@ const bool_flags: BoolFlag[] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap w-full">
|
||||
<div class="flex flex-col gap-2 grow p-fluid">
|
||||
<label for="">{{ t('rpc_portal_whitelists') }}</label>
|
||||
<AutoComplete id="rpc_portal_whitelists" v-model="curNetwork.rpc_portal_whitelists"
|
||||
:placeholder="t('chips_placeholder', ['127.0.0.0/8'])" class="w-full" multiple fluid
|
||||
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-x-9 flex-wrap">
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<label for="dev_name">{{ t('dev_name') }}</label>
|
||||
@@ -316,11 +328,10 @@ const bool_flags: BoolFlag[] = [
|
||||
<div class="flex flex-col gap-2 basis-5/12 grow">
|
||||
<div class="flex">
|
||||
<label for="mtu">{{ t('mtu') }}</label>
|
||||
<span class="pi pi-question-circle ml-2 self-center"
|
||||
v-tooltip="t('mtu_help')"></span>
|
||||
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mtu_help')"></span>
|
||||
</div>
|
||||
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help"
|
||||
:format="false" :placeholder="t('mtu_placeholder')" :min="400" :max="1380" fluid/>
|
||||
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help" :format="false"
|
||||
:placeholder="t('mtu_placeholder')" :min="400" :max="1380" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -331,8 +342,8 @@ const bool_flags: BoolFlag[] = [
|
||||
<span class="pi pi-question-circle ml-2 self-center"
|
||||
v-tooltip="t('relay_network_whitelist_help')"></span>
|
||||
</div>
|
||||
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check"
|
||||
off-icon="pi pi-times" :on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
|
||||
<div v-if="curNetwork.enable_relay_network_whitelist" class="items-center flex flex-row gap-x-4">
|
||||
<div class="min-w-64 w-full">
|
||||
<AutoComplete id="relay_network_whitelist" v-model="curNetwork.relay_network_whitelist"
|
||||
@@ -372,7 +383,7 @@ const bool_flags: BoolFlag[] = [
|
||||
<div v-if="curNetwork.enable_socks5" class="items-center flex flex-row gap-x-4">
|
||||
<div class="min-w-64 w-full">
|
||||
<InputNumber id="socks5_port" v-model="curNetwork.socks5_port" aria-describedby="rpc_port-help"
|
||||
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full"/>
|
||||
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,8 +408,8 @@ const bool_flags: BoolFlag[] = [
|
||||
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mapped_listeners_help')"></span>
|
||||
</div>
|
||||
<AutoComplete id="mapped_listeners" v-model="curNetwork.mapped_listeners"
|
||||
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full"
|
||||
multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full" multiple fluid
|
||||
:suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
103
easytier-web/frontend-lib/src/components/ConfigEditDialog.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { NetworkConfig } from '../types/network';
|
||||
import { Divider, Button, Dialog, Textarea } from 'primevue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
generateConfig: {
|
||||
type: Object as () => (config: NetworkConfig) => Promise<string>,
|
||||
required: true,
|
||||
},
|
||||
saveConfig: {
|
||||
type: Object as () => (config: string) => Promise<void>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const curNetwork = defineModel('curNetwork', {
|
||||
type: Object as () => NetworkConfig | undefined,
|
||||
required: true,
|
||||
})
|
||||
|
||||
const visible = defineModel('visible', {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
watch([visible, curNetwork], async ([newVisible, newCurNetwork]) => {
|
||||
if (!newVisible) {
|
||||
tomlConfig.value = '';
|
||||
return;
|
||||
}
|
||||
if (!newCurNetwork) {
|
||||
tomlConfig.value = '';
|
||||
return;
|
||||
}
|
||||
const config = newCurNetwork;
|
||||
try {
|
||||
errorMessage.value = '';
|
||||
tomlConfig.value = await props.generateConfig(config);
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
|
||||
tomlConfig.value = '';
|
||||
}
|
||||
})
|
||||
onMounted(async () => {
|
||||
if (!visible.value) {
|
||||
return;
|
||||
}
|
||||
if (!curNetwork.value) {
|
||||
tomlConfig.value = '';
|
||||
return;
|
||||
}
|
||||
const config = curNetwork.value;
|
||||
try {
|
||||
tomlConfig.value = await props.generateConfig(config);
|
||||
errorMessage.value = '';
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
|
||||
tomlConfig.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
const handleConfigSave = async () => {
|
||||
if (props.readonly) return;
|
||||
try {
|
||||
await props.saveConfig(tomlConfig.value);
|
||||
visible.value = false;
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Failed to save config: ' + (e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
|
||||
const tomlConfig = ref<string>('')
|
||||
const tomlConfigRows = ref<number>(1);
|
||||
const errorMessage = ref<string>('');
|
||||
|
||||
watch(tomlConfig, (newValue) => {
|
||||
tomlConfigRows.value = newValue.split('\n').length;
|
||||
errorMessage.value = '';
|
||||
});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Dialog v-model:visible="visible" modal :header="t('config_file')" :style="{ width: '70%' }">
|
||||
<pre v-if="errorMessage"
|
||||
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||
<div class="flex w-full" style="max-height: 60vh; overflow-y: auto;">
|
||||
<Textarea v-model="tomlConfig" class="w-full h-full font-mono flex flex-col resize-none" :rows="tomlConfigRows"
|
||||
spellcheck="false" :readonly="props.readonly"></Textarea>
|
||||
</div>
|
||||
<Divider />
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button v-if="!props.readonly" type="button" :label="t('save')" @click="handleConfigSave" />
|
||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
@@ -1,2 +1,3 @@
|
||||
export { default as Config } from './Config.vue';
|
||||
export { default as Status } from './Status.vue';
|
||||
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import './style.css'
|
||||
|
||||
import type { App } from 'vue';
|
||||
import { Config, Status } from "./components";
|
||||
import { Config, Status, ConfigEditDialog } from "./components";
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import PrimeVue from 'primevue/config'
|
||||
|
||||
@@ -41,10 +41,11 @@ export default {
|
||||
});
|
||||
|
||||
app.component('Config', Config);
|
||||
app.component('ConfigEditDialog', ConfigEditDialog);
|
||||
app.component('Status', Status);
|
||||
app.component('HumanEvent', HumanEvent);
|
||||
app.directive('tooltip', vTooltip as any);
|
||||
}
|
||||
};
|
||||
|
||||
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||
|
@@ -18,6 +18,7 @@ advanced_settings: 高级设置
|
||||
basic_settings: 基础设置
|
||||
listener_urls: 监听地址
|
||||
rpc_port: RPC端口
|
||||
rpc_portal_whitelists: RPC白名单
|
||||
config_network: 配置网络
|
||||
running: 运行中
|
||||
error_msg: 错误信息
|
||||
@@ -44,13 +45,17 @@ 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: 点击关闭
|
||||
on_text: 点击开启
|
||||
show_config: 显示配置
|
||||
edit_config: 编辑配置文件
|
||||
config_file: 配置文件
|
||||
close: 关闭
|
||||
save: 保存
|
||||
config_saved: 配置已保存
|
||||
|
||||
use_latency_first: 延迟优先模式
|
||||
my_node_info: 当前节点信息
|
||||
@@ -78,12 +83,21 @@ 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 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
disable_kcp_input: 禁用 KCP 输入
|
||||
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。
|
||||
|
||||
enable_quic_proxy: 启用 QUIC 代理
|
||||
enable_quic_proxy_help: 将 TCP 流量转为 QUIC 流量,降低传输延迟,提升传输速度。
|
||||
|
||||
disable_quic_input: 禁用 QUIC 输入
|
||||
disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的节点仍然使用 TCP 连接到本节点。
|
||||
|
||||
disable_p2p: 禁用 P2P
|
||||
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
|
||||
|
||||
@@ -192,3 +206,116 @@ event:
|
||||
DhcpIpv4Changed: DHCP IPv4地址更改
|
||||
DhcpIpv4Conflicted: DHCP IPv4地址冲突
|
||||
PortForwardAdded: 端口转发添加
|
||||
|
||||
web:
|
||||
login:
|
||||
title: 登录
|
||||
username: 用户名
|
||||
password: 密码
|
||||
submit: 登录
|
||||
register: 注册
|
||||
remember_me: 记住我
|
||||
api_host: API主机
|
||||
captcha: 验证码
|
||||
back_to_login: 返回登录
|
||||
login: 登录
|
||||
|
||||
register:
|
||||
title: 注册
|
||||
username: 用户名
|
||||
password: 密码
|
||||
confirm_password: 确认密码
|
||||
submit: 注册
|
||||
login: 返回登录
|
||||
|
||||
main:
|
||||
dashboard: 仪表盘
|
||||
device_list: 设备列表
|
||||
device_management: 设备管理
|
||||
login_page: 登录页面
|
||||
settings: 设置
|
||||
logout: 退出登录
|
||||
language: 语言
|
||||
change_password: 修改密码
|
||||
|
||||
device:
|
||||
list: 设备列表
|
||||
management: 设备管理
|
||||
add: 添加设备
|
||||
delete: 删除设备
|
||||
refresh: 刷新
|
||||
status: 状态
|
||||
online: 在线
|
||||
offline: 离线
|
||||
last_seen: 最后在线
|
||||
no_devices: 未找到设备
|
||||
sort_by: 排序依据
|
||||
sort_direction: 排序方向
|
||||
show_detailed_view: 显示详情
|
||||
hide_detailed_view: 隐藏详情
|
||||
sort_by_hostname: 主机名
|
||||
sort_by_public_ip: 公网IP
|
||||
sort_by_version: 版本
|
||||
sort_by_networks: 网络数量
|
||||
sort_direction_asc: 当前升序,点击切换为降序
|
||||
sort_direction_desc: 当前降序,点击切换为升序
|
||||
hostname: 主机名
|
||||
public_ip: 公网IP
|
||||
networks: 网络数量
|
||||
last_report: 最后在线
|
||||
version: 版本
|
||||
machine_id: 机器ID
|
||||
unknown_location: 未知位置
|
||||
|
||||
device_management:
|
||||
edit_network: 编辑网络
|
||||
export_config: 导出配置
|
||||
delete_network: 删除网络
|
||||
network: 网络
|
||||
select_network: 选择网络
|
||||
create_network: 创建网络
|
||||
cancel_creation: 取消创建
|
||||
more_actions: 更多操作
|
||||
edit_as_file: 编辑为文件
|
||||
import_config: 导入配置
|
||||
create_new: 创建新网络
|
||||
network_status: 网络状态
|
||||
network_configuration: 网络配置
|
||||
loading_network_configuration: 加载网络配置
|
||||
no_network_selected: 未选择网络
|
||||
select_existing_network_or_create_new: 选择现有网络实例或创建新网络以管理网络设置
|
||||
disable_network: 禁用网络
|
||||
|
||||
network:
|
||||
title: 网络
|
||||
create: 创建网络
|
||||
delete: 删除网络
|
||||
start: 启动网络
|
||||
stop: 停止网络
|
||||
config: 网络配置
|
||||
status: 网络状态
|
||||
import: 导入配置
|
||||
export: 导出配置
|
||||
|
||||
common:
|
||||
confirm: 确认
|
||||
cancel: 取消
|
||||
save: 保存
|
||||
delete: 删除
|
||||
edit: 编辑
|
||||
refresh: 刷新
|
||||
loading: 加载中...
|
||||
error: 错误
|
||||
success: 成功
|
||||
warning: 警告
|
||||
info: 提示
|
||||
|
||||
settings:
|
||||
title: 设置
|
||||
change_password: 修改密码
|
||||
old_password: 旧密码
|
||||
new_password: 新密码
|
||||
confirm_password: 确认新密码
|
||||
language: 语言
|
||||
theme: 主题
|
||||
logout: 退出登录
|
@@ -18,6 +18,7 @@ advanced_settings: Advanced Settings
|
||||
basic_settings: Basic Settings
|
||||
listener_urls: Listener URLs
|
||||
rpc_port: RPC Port
|
||||
rpc_portal_whitelists: RPC Whitelist
|
||||
config_network: Config Network
|
||||
running: Running
|
||||
error_msg: Error Message
|
||||
@@ -45,13 +46,17 @@ 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
|
||||
on_text: Press to enable
|
||||
show_config: Show Config
|
||||
edit_config: Edit Config File
|
||||
config_file: Config File
|
||||
close: Close
|
||||
save: Save
|
||||
config_saved: Configuration saved
|
||||
my_node_info: My Node Info
|
||||
peer_count: Connected
|
||||
upload: Upload
|
||||
@@ -77,12 +82,21 @@ 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.
|
||||
|
||||
disable_kcp_input: Disable KCP Input
|
||||
disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP.
|
||||
|
||||
enable_quic_proxy: Enable QUIC Proxy
|
||||
enable_quic_proxy_help: Convert TCP traffic to QUIC traffic to reduce latency and boost transmission speed.
|
||||
|
||||
disable_quic_input: Disable QUIC Input
|
||||
disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC proxy enabled continue to connect using TCP.
|
||||
|
||||
disable_p2p: Disable P2P
|
||||
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
|
||||
|
||||
@@ -192,3 +206,116 @@ event:
|
||||
DhcpIpv4Changed: DhcpIpv4Changed
|
||||
DhcpIpv4Conflicted: DhcpIpv4Conflicted
|
||||
PortForwardAdded: PortForwardAdded
|
||||
|
||||
web:
|
||||
login:
|
||||
title: Login
|
||||
username: Username
|
||||
password: Password
|
||||
submit: Login
|
||||
register: Register
|
||||
remember_me: Remember Me
|
||||
api_host: API Host
|
||||
captcha: Captcha
|
||||
back_to_login: Back to Login
|
||||
login: Login
|
||||
|
||||
register:
|
||||
title: Register
|
||||
username: Username
|
||||
password: Password
|
||||
confirm_password: Confirm Password
|
||||
submit: Register
|
||||
login: Back to Login
|
||||
|
||||
main:
|
||||
dashboard: Dashboard
|
||||
device_list: Device List
|
||||
device_management: Device Management
|
||||
login_page: Login Page
|
||||
settings: Settings
|
||||
logout: Logout
|
||||
language: Language
|
||||
change_password: Change Password
|
||||
|
||||
device:
|
||||
list: Device List
|
||||
management: Device Management
|
||||
add: Add Device
|
||||
delete: Delete Device
|
||||
refresh: Refresh
|
||||
status: Status
|
||||
online: Online
|
||||
offline: Offline
|
||||
last_seen: Last Seen
|
||||
no_devices: No Devices Found
|
||||
sort_by: Sort By
|
||||
sort_direction: Sort Direction
|
||||
show_detailed_view: Show Details
|
||||
hide_detailed_view: Hide Details
|
||||
sort_by_hostname: Hostname
|
||||
sort_by_public_ip: Public IP
|
||||
sort_by_version: Version
|
||||
sort_by_networks: Network Count
|
||||
sort_direction_asc: Currently ascending, click to switch to descending
|
||||
sort_direction_desc: Currently descending, click to switch to ascending
|
||||
hostname: Hostname
|
||||
public_ip: Public IP
|
||||
networks: Network Count
|
||||
last_report: Last Seen
|
||||
version: Version
|
||||
machine_id: Machine ID
|
||||
unknown_location: Unknown Location
|
||||
|
||||
device_management:
|
||||
edit_network: Edit Network
|
||||
export_config: Export Config
|
||||
delete_network: Delete Network
|
||||
network: Network
|
||||
select_network: Select Network
|
||||
create_network: Create Network
|
||||
cancel_creation: Cancel Creation
|
||||
more_actions: More Actions
|
||||
edit_as_file: Edit as File
|
||||
import_config: Import Config
|
||||
create_new: Create New Network
|
||||
network_status: Network Status
|
||||
network_configuration: Network Configuration
|
||||
loading_network_configuration: Loading Network Configuration
|
||||
no_network_selected: No Network Selected
|
||||
select_existing_network_or_create_new: Select an existing network instance or create a new one to manage network settings
|
||||
disable_network: Disable Network
|
||||
|
||||
network:
|
||||
title: Network
|
||||
create: Create Network
|
||||
delete: Delete Network
|
||||
start: Start Network
|
||||
stop: Stop Network
|
||||
config: Network Config
|
||||
status: Network Status
|
||||
import: Import Config
|
||||
export: Export Config
|
||||
|
||||
common:
|
||||
confirm: Confirm
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
delete: Delete
|
||||
edit: Edit
|
||||
refresh: Refresh
|
||||
loading: Loading...
|
||||
error: Error
|
||||
success: Success
|
||||
warning: Warning
|
||||
info: Info
|
||||
|
||||
settings:
|
||||
title: Settings
|
||||
change_password: Change Password
|
||||
old_password: Old Password
|
||||
new_password: New Password
|
||||
confirm_password: Confirm New Password
|
||||
language: Language
|
||||
theme: Theme
|
||||
logout: Logout
|
@@ -47,6 +47,15 @@ export interface GenerateConfigResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigRequest {
|
||||
toml_config: string;
|
||||
}
|
||||
|
||||
export interface ParseConfigResponse {
|
||||
config?: NetworkConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authFailedCb: Function | undefined;
|
||||
@@ -215,6 +224,18 @@ export class ApiClient {
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
|
||||
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
|
||||
try {
|
||||
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
return { error: error.response?.data };
|
||||
}
|
||||
return { error: 'Unknown error: ' + error };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiClient;
|
@@ -22,6 +22,16 @@ export const availableLocales = Object.keys(localesMap)
|
||||
|
||||
const loadedLanguages: string[] = []
|
||||
|
||||
export function toggleLanguage() {
|
||||
const currentLang = localStorage.getItem('lang') || 'en'
|
||||
const newLang = currentLang === 'en' ? 'cn' : 'en'
|
||||
loadLanguageAsync(newLang)
|
||||
}
|
||||
|
||||
export function getCurrentLanguage() {
|
||||
return localStorage.getItem('lang') || 'en'
|
||||
}
|
||||
|
||||
function setI18nLanguage(lang: Locale) {
|
||||
i18n.global.locale.value = lang as any
|
||||
localStorage.setItem('lang', lang)
|
||||
@@ -56,4 +66,6 @@ export default {
|
||||
i18n,
|
||||
localesMap,
|
||||
loadLanguageAsync,
|
||||
toggleLanguage,
|
||||
getCurrentLanguage,
|
||||
}
|
||||
|
@@ -53,6 +53,12 @@ export function UuidToStr(uuid: UUID): string {
|
||||
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
country: string | undefined;
|
||||
city: string | undefined;
|
||||
region: string | undefined;
|
||||
}
|
||||
|
||||
export interface DeviceInfo {
|
||||
hostname: string;
|
||||
public_ip: string;
|
||||
@@ -61,6 +67,7 @@ export interface DeviceInfo {
|
||||
easytier_version: string;
|
||||
running_network_instances?: Array<string>;
|
||||
machine_id: string;
|
||||
location: Location | undefined;
|
||||
}
|
||||
|
||||
export function buildDeviceInfo(device: any): DeviceInfo {
|
||||
@@ -72,6 +79,7 @@ export function buildDeviceInfo(device: any): DeviceInfo {
|
||||
report_time: device.info?.report_time,
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: UuidToStr(device.info?.machine_id),
|
||||
location: device.location,
|
||||
};
|
||||
|
||||
return dev_info;
|
||||
|
@@ -18,8 +18,6 @@
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
@@ -37,8 +37,11 @@ export interface NetworkConfig {
|
||||
dev_name: string
|
||||
|
||||
use_smoltcp?: boolean
|
||||
disable_ipv6?: boolean
|
||||
enable_kcp_proxy?: boolean
|
||||
disable_kcp_input?: boolean
|
||||
enable_quic_proxy?: boolean
|
||||
disable_quic_input?: boolean
|
||||
disable_p2p?: boolean
|
||||
bind_device?: boolean
|
||||
no_tun?: boolean
|
||||
@@ -65,6 +68,8 @@ export interface NetworkConfig {
|
||||
|
||||
enable_magic_dns?: boolean
|
||||
enable_private_mode?: boolean
|
||||
|
||||
rpc_portal_whitelists: string[]
|
||||
}
|
||||
|
||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
@@ -101,8 +106,11 @@ 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,
|
||||
disable_quic_input: false,
|
||||
disable_p2p: false,
|
||||
bind_device: true,
|
||||
no_tun: false,
|
||||
@@ -123,6 +131,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||
mapped_listeners: [],
|
||||
enable_magic_dns: false,
|
||||
enable_private_mode: false,
|
||||
rpc_portal_whitelists: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/easytier.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>EasyTier Dashboard</title>
|
||||
<script src="/api_meta.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -16,7 +16,9 @@
|
||||
"primevue": "4.3.3",
|
||||
"tailwindcss-primeui": "^0.3.4",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "4"
|
||||
"vue-router": "4",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"@modyfi/vite-plugin-yaml": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.6",
|
||||
|
3
easytier-web/frontend/public/api_meta.js
Normal file
@@ -0,0 +1,3 @@
|
||||
window.apiMeta = {
|
||||
api_host: "https://config-server.easytier.cn"
|
||||
}
|
@@ -1,11 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { onMounted } from 'vue';
|
||||
import { Toast, DynamicDialog } from 'primevue';
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
});
|
||||
|
||||
</script>
|
||||
|
@@ -2,12 +2,11 @@
|
||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||
import {computed, ref} from 'vue';
|
||||
import { Api } from 'easytier-frontend-lib'
|
||||
import {AutoComplete, Divider} from "primevue";
|
||||
import {AutoComplete, Divider, Button, Textarea} from "primevue";
|
||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||
|
||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||
|
||||
|
||||
const apiHost = ref<string>(getInitialApiHost())
|
||||
const apiHostSuggestions = ref<Array<string>>([])
|
||||
const apiHostSearch = async (event: { query: string }) => {
|
||||
@@ -22,23 +21,46 @@ const apiHostSearch = async (event: { query: string }) => {
|
||||
}
|
||||
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
|
||||
const toml_config = ref<string>("");
|
||||
const errorMessage = ref<string>("");
|
||||
|
||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
saveApiHost(apiHost.value)
|
||||
errorMessage.value = "";
|
||||
api.value?.generate_config({
|
||||
config: config
|
||||
}).then((res) => {
|
||||
if (res.error) {
|
||||
toml_config.value = res.error;
|
||||
errorMessage.value = "Generation failed: " + res.error;
|
||||
} else if (res.toml_config) {
|
||||
toml_config.value = res.toml_config;
|
||||
} else {
|
||||
toml_config.value = "Api server returned an unexpected response";
|
||||
errorMessage.value = "Api server returned an unexpected response";
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
|
||||
});
|
||||
};
|
||||
|
||||
const parseConfig = async () => {
|
||||
try {
|
||||
errorMessage.value = "";
|
||||
const res = await api.value?.parse_config({
|
||||
toml_config: toml_config.value
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
errorMessage.value = "Parse failed: " + res.error;
|
||||
} else if (res.config) {
|
||||
newNetworkConfig.value = res.config;
|
||||
} else {
|
||||
errorMessage.value = "API returned an unexpected response";
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = "Parse request failed: " + (e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,8 +77,17 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||
</div>
|
||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||
</div>
|
||||
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
|
||||
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
|
||||
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||
<Textarea
|
||||
v-model="toml_config"
|
||||
spellcheck="false"
|
||||
class="w-full flex-grow p-2 whitespace-pre-wrap font-mono resize-none"
|
||||
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
|
||||
></Textarea>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
211
easytier-web/frontend/src/components/DeviceDetails.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import { Utils } from 'easytier-frontend-lib';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
// 定义组件接收的 props
|
||||
defineProps<{
|
||||
device: Utils.DeviceInfo;
|
||||
// 可以传入额外的样式类
|
||||
containerClass?: string;
|
||||
// 是否使用紧凑布局
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['device-details', containerClass, { 'compact': compact }]">
|
||||
<div class="detail-item hostname">
|
||||
<div class="detail-label">{{ t('web.device.hostname') }}</div>
|
||||
<div class="detail-value">{{ device.hostname }}</div>
|
||||
</div>
|
||||
<div class="detail-item public-ip">
|
||||
<div class="detail-label">{{ t('web.device.public_ip') }}</div>
|
||||
<div class="detail-value">{{ device.public_ip }}</div>
|
||||
</div>
|
||||
<div class="detail-item running-networks">
|
||||
<div class="detail-label">{{ t('web.device.networks') }}</div>
|
||||
<div class="detail-value">{{ device.running_network_count }}</div>
|
||||
</div>
|
||||
<div class="detail-item last-report">
|
||||
<div class="detail-label">{{ t('web.device.last_report') }}</div>
|
||||
<div class="detail-value">{{ device.report_time }}</div>
|
||||
</div>
|
||||
<div class="detail-item version">
|
||||
<div class="detail-label">{{ t('web.device.version') }}</div>
|
||||
<div class="detail-value">{{ device.easytier_version }}</div>
|
||||
</div>
|
||||
<div class="detail-item machine-id">
|
||||
<div class="detail-label">{{ t('web.device.machine_id') }}</div>
|
||||
<div class="detail-value">
|
||||
<span class="machine-id-value" :title="device.machine_id">{{ device.machine_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 基础布局 */
|
||||
.device-details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* 标准布局的详情项样式 */
|
||||
.detail-item {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--surface-border, #e9ecef);
|
||||
padding-bottom: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
background-color: var(--surface-hover, rgba(245, 247, 250, 0.5));
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #334155);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 紧凑布局样式 */
|
||||
.device-details.compact {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.compact .detail-item {
|
||||
padding: 0.3rem 0.2rem;
|
||||
display: grid;
|
||||
grid-template-columns: 40% 60%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.compact .detail-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: #3b82f6;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text-color-secondary, #475569);
|
||||
word-break: break-all;
|
||||
padding-left: 1rem;
|
||||
line-height: 1.4;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* 紧凑布局的标签和值样式 */
|
||||
.compact .detail-label::before {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.compact .detail-value {
|
||||
padding-left: 0.3rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 特定字段的样式 */
|
||||
.hostname .detail-label::before {
|
||||
background-color: #3b82f6;
|
||||
/* 蓝色 */
|
||||
}
|
||||
|
||||
.public-ip .detail-label::before {
|
||||
background-color: #10b981;
|
||||
/* 绿色 */
|
||||
}
|
||||
|
||||
.running-networks .detail-label::before {
|
||||
background-color: #f59e0b;
|
||||
/* 橙色 */
|
||||
}
|
||||
|
||||
.last-report .detail-label::before {
|
||||
background-color: #8b5cf6;
|
||||
/* 紫色 */
|
||||
}
|
||||
|
||||
.version .detail-label::before {
|
||||
background-color: #ec4899;
|
||||
/* 粉色 */
|
||||
}
|
||||
|
||||
.machine-id .detail-label::before {
|
||||
background-color: #6b7280;
|
||||
/* 灰色 */
|
||||
}
|
||||
|
||||
/* 机器ID特殊样式 */
|
||||
.machine-id-value {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.95rem;
|
||||
background-color: var(--surface-ground, #f1f5f9);
|
||||
color: var(--text-color, #1f2937);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--surface-border, #e2e8f0);
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 紧凑布局下的机器ID样式 */
|
||||
.compact .machine-id-value {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
/* 暗黑模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.detail-item {
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-item:hover {
|
||||
background-color: var(--surface-hover, rgba(30, 41, 59, 0.4));
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--text-color, #e2e8f0);
|
||||
}
|
||||
|
||||
.machine-id-value {
|
||||
background-color: var(--surface-ground, #1e293b);
|
||||
color: var(--text-color, #f1f5f9);
|
||||
border-color: var(--surface-border, #334155);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,13 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Button, Column, DataTable, Drawer, ProgressSpinner, useToast } from 'primevue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { Button, Drawer, ProgressSpinner, useToast, InputSwitch, Popover, Dropdown, Toolbar } from 'primevue';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Api, Utils } from 'easytier-frontend-lib';
|
||||
import DeviceDetails from './DeviceDetails.vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
declare const window: Window & typeof globalThis;
|
||||
|
||||
// 注册 Tooltip 指令
|
||||
const vTooltip = Tooltip;
|
||||
|
||||
const props = defineProps({
|
||||
api: Api.ApiClient,
|
||||
});
|
||||
|
||||
const detailPopover = ref();
|
||||
const selectedDevice = ref<Utils.DeviceInfo | null>(null);
|
||||
// 从 localStorage 读取显示详情状态,默认为 false
|
||||
const showDetailedView = ref(localStorage.getItem('deviceList.showDetailedView') === 'true');
|
||||
|
||||
// 监听显示详情状态变化,保存到 localStorage
|
||||
watch(showDetailedView, (newValue) => {
|
||||
localStorage.setItem('deviceList.showDetailedView', newValue.toString());
|
||||
});
|
||||
|
||||
const api = props.api;
|
||||
|
||||
const deviceList = ref<Array<Utils.DeviceInfo> | undefined>(undefined);
|
||||
@@ -22,15 +42,7 @@ const loadDevices = async () => {
|
||||
const resp = await api?.list_machines();
|
||||
let devices: Array<Utils.DeviceInfo> = [];
|
||||
for (const device of (resp || [])) {
|
||||
devices.push({
|
||||
hostname: device.info?.hostname,
|
||||
public_ip: device.client_url,
|
||||
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
|
||||
running_network_count: device.info?.running_network_instances.length,
|
||||
report_time: new Date(device.info?.report_time).toLocaleString(),
|
||||
easytier_version: device.info?.easytier_version,
|
||||
machine_id: Utils.UuidToStr(device.info?.machine_id),
|
||||
});
|
||||
devices.push(Utils.buildDeviceInfo(device));
|
||||
}
|
||||
console.debug("device list", deviceList.value);
|
||||
deviceList.value = devices;
|
||||
@@ -47,10 +59,14 @@ const periodFunc = new Utils.PeriodicTask(async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
// 初始化屏幕尺寸相关变量
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
const deviceManageVisible = computed<boolean>({
|
||||
@@ -66,45 +82,761 @@ const selectedDeviceHostname = computed<string | undefined>(() => {
|
||||
return deviceList.value?.find((device) => device.machine_id === selectedDeviceId.value)?.hostname;
|
||||
});
|
||||
|
||||
// 处理设备管理
|
||||
const handleDeviceManagement = (device: Utils.DeviceInfo) => {
|
||||
const instanceId = device.running_network_instances?.[0];
|
||||
router.push({
|
||||
name: 'deviceManagement',
|
||||
params: {
|
||||
deviceId: device.machine_id,
|
||||
instanceId: instanceId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 显示设备详情
|
||||
const showDeviceDetails = (device: Utils.DeviceInfo, event: Event) => {
|
||||
selectedDevice.value = device;
|
||||
detailPopover.value.toggle(event);
|
||||
};
|
||||
|
||||
// 检查是否为桌面设备
|
||||
const isDesktop = ref(false);
|
||||
// 检查是否为多卡片视图(一行可以放置多个卡片)
|
||||
const isMultiCardView = ref(false);
|
||||
|
||||
// 抽屉布局相关
|
||||
const drawerWidth = computed(() => {
|
||||
return isDesktop.value ? 'w-3/5 min-w-96' : 'w-full';
|
||||
});
|
||||
|
||||
const drawerPosition = computed(() => {
|
||||
return isDesktop.value ? 'right' : 'bottom';
|
||||
});
|
||||
|
||||
const drawerHeight = computed(() => {
|
||||
return isDesktop.value ? undefined : '100%';
|
||||
});
|
||||
|
||||
// 排序相关
|
||||
const sortOptions = ref([
|
||||
{ name: () => t('web.device.sort_by_hostname'), value: 'hostname', icon: 'pi pi-home' },
|
||||
{ name: () => t('web.device.sort_by_version'), value: 'version', icon: 'pi pi-tag' },
|
||||
{ name: () => t('web.device.sort_by_networks'), value: 'networks', icon: 'pi pi-sitemap' }
|
||||
]);
|
||||
const selectedSortOption = ref(sortOptions.value[0]);
|
||||
// 排序方向 (true为升序,false为降序)
|
||||
const ascending = ref(true);
|
||||
|
||||
// 切换排序方向
|
||||
const toggleSortDirection = () => {
|
||||
ascending.value = !ascending.value;
|
||||
};
|
||||
|
||||
// 排序函数
|
||||
const sortDevices = (devices: Array<Utils.DeviceInfo> | undefined) => {
|
||||
if (!devices) return [];
|
||||
|
||||
const sortField = selectedSortOption.value.value;
|
||||
const direction = ascending.value ? 1 : -1;
|
||||
|
||||
return [...devices].sort((a, b) => {
|
||||
let result = 0;
|
||||
|
||||
switch (sortField) {
|
||||
case 'hostname':
|
||||
result = a.hostname.localeCompare(b.hostname);
|
||||
break;
|
||||
case 'version':
|
||||
result = a.easytier_version.localeCompare(b.easytier_version);
|
||||
break;
|
||||
case 'networks':
|
||||
result = a.running_network_count - b.running_network_count;
|
||||
break;
|
||||
}
|
||||
|
||||
return result * direction;
|
||||
});
|
||||
};
|
||||
|
||||
// 排序后的设备列表
|
||||
const sortedDeviceList = computed(() => {
|
||||
return sortDevices(deviceList.value);
|
||||
});
|
||||
|
||||
// 保存resize事件处理函数的引用,以便正确移除
|
||||
const handleResize = () => {
|
||||
isDesktop.value = window.innerWidth >= 768;
|
||||
// 当容器宽度足够放置两个或更多卡片时,视为多卡片视图
|
||||
isMultiCardView.value = window.innerWidth >= 650;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 卡片容器 */
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
/* 确保子元素的绝对定位相对于此容器 */
|
||||
}
|
||||
|
||||
/* 设备卡片样式 */
|
||||
.device-card {
|
||||
border: 1px solid var(--surface-border, #e5e7eb);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--surface-card, white);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
color: var(--text-color, #1f2937);
|
||||
}
|
||||
|
||||
.device-details-popover {
|
||||
min-width: 280px;
|
||||
max-width: 350px;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
|
||||
/* Popover 样式 */
|
||||
:deep(.device-popover.p-popover) {
|
||||
min-width: 320px;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--card-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05));
|
||||
border: 1px solid var(--surface-border, #e5e7eb);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-content) {
|
||||
padding: 0;
|
||||
background-color: var(--surface-card, #ffffff);
|
||||
color: var(--text-color, #334155);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-arrow) {
|
||||
background-color: var(--surface-card, #ffffff);
|
||||
border-color: var(--surface-border, #e5e7eb);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header) {
|
||||
background-color: var(--surface-section, #f8fafc);
|
||||
border-bottom: 1px solid var(--surface-border, #e2e8f0);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header-close) {
|
||||
color: var(--text-color-secondary, #64748b);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header-close:hover) {
|
||||
background-color: var(--surface-hover, rgba(0, 0, 0, 0.04));
|
||||
color: var(--text-color, #334155);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.device-popover.p-popover) {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.25);
|
||||
border-color: var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-content) {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
color: var(--text-color, #f1f5f9);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-arrow) {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
border-color: var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header) {
|
||||
background-color: var(--surface-section, #0f172a);
|
||||
border-bottom: 1px solid var(--surface-border, #1e293b);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header-close) {
|
||||
color: var(--text-color-secondary, #94a3b8);
|
||||
}
|
||||
|
||||
:deep(.device-popover .p-popover-header-close:hover) {
|
||||
background-color: var(--surface-hover, rgba(255, 255, 255, 0.1));
|
||||
color: var(--text-color, #f1f5f9);
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
background-color: var(--surface-section, #0f172a);
|
||||
color: var(--text-color, #f1f5f9);
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--surface-section, #f8fafc);
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border, #e2e8f0);
|
||||
color: var(--text-color, #334155);
|
||||
}
|
||||
|
||||
/* 卡片内详情样式 */
|
||||
.card-details {
|
||||
background-color: var(--surface-ground, #f9fafb);
|
||||
}
|
||||
|
||||
/* 卡片内详情内容的特定样式 */
|
||||
:deep(.card-details-content) {
|
||||
padding: 0.15rem 0.1rem;
|
||||
}
|
||||
|
||||
/* 卡片中的紧凑详情内容 */
|
||||
:deep(.card-details-content) {
|
||||
padding: 0.15rem 0.1rem;
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-label) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-value) {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.card-details-content .detail-item) {
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-item:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-item:hover) {
|
||||
background-color: var(--surface-hover, rgba(30, 41, 59, 0.4));
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-label) {
|
||||
color: var(--text-color, #e2e8f0);
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-value) {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.card-details-content .detail-item) {
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-item:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-item:hover) {
|
||||
background-color: var(--surface-hover, rgba(30, 41, 59, 0.4));
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-label) {
|
||||
color: var(--text-color, #e2e8f0);
|
||||
}
|
||||
|
||||
:deep(.card-details-content .detail-value) {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保卡片在暗黑模式下有足够的对比度 */
|
||||
:deep(.device-card) {
|
||||
background-color: var(--surface-card, white);
|
||||
border-color: var(--surface-border, #e5e7eb);
|
||||
}
|
||||
|
||||
:deep(.card-header) {
|
||||
color: var(--text-color, #1f2937);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--text-color, #1f2937);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-color-secondary, #64748b);
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background-color: var(--primary-color, #3b82f6);
|
||||
color: #ffffff;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
background-color: var(--surface-card);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
box-shadow: var(--card-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sort-controls:hover {
|
||||
box-shadow: var(--card-shadow, 0 2px 5px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.sort-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
min-width: 6rem;
|
||||
max-width: 9rem;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.sort-direction-btn {
|
||||
font-size: 1rem;
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
|
||||
/* 暗黑模式样式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sort-controls {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sort-controls:hover {
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
:deep(.device-card) {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
border-color: var(--surface-border, #334155);
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
:deep(.card-header) {
|
||||
color: var(--text-color, #f1f5f9);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--text-color, #f1f5f9);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background-color: var(--primary-color, #4f46e5);
|
||||
}
|
||||
|
||||
:deep(.card-details) {
|
||||
background-color: var(--surface-ground, #0f172a);
|
||||
border-top: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
}
|
||||
|
||||
/* Popover 详情内容的特定样式 */
|
||||
:deep(.popover-details-content) {
|
||||
padding: 0.25rem 0.2rem;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
/* Popover 中的紧凑详情内容 */
|
||||
:deep(.popover-details-content) {
|
||||
padding: 0.25rem 0.2rem;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-label) {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-value) {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .machine-id-value) {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.popover-details-content .detail-item) {
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-item:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-item:hover) {
|
||||
background-color: var(--surface-hover, rgba(30, 41, 59, 0.4));
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-label) {
|
||||
color: var(--text-color, #e2e8f0);
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-value) {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.popover-details-content .detail-item) {
|
||||
border-bottom: 1px solid var(--surface-border, #334155);
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-item:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-item:hover) {
|
||||
background-color: var(--surface-hover, rgba(30, 41, 59, 0.4));
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-label) {
|
||||
color: var(--text-color, #e2e8f0);
|
||||
}
|
||||
|
||||
:deep(.popover-details-content .detail-value) {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端卡片样式 */
|
||||
@media (max-width: 768px) {
|
||||
.card-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 抽屉响应式样式 */
|
||||
:deep(.p-drawer) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.p-drawer.p-drawer-bottom) {
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.p-drawer.p-drawer-bottom .p-drawer-header) {
|
||||
padding-top: 1rem;
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
}
|
||||
|
||||
:deep(.p-drawer.p-drawer-bottom .p-drawer-content) {
|
||||
padding-bottom: 2rem;
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
}
|
||||
|
||||
/* 底部抽屉的拖动指示器 */
|
||||
:deep(.p-drawer.p-drawer-bottom .p-drawer-header::before) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 4rem;
|
||||
height: 4px;
|
||||
background-color: var(--surface-border);
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.p-drawer.p-drawer-bottom) {
|
||||
box-shadow: 0 -4px 12px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-fab-close-btn {
|
||||
/* 适配移动和桌面端,防止被内容遮挡 */
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.drawer-fab-close-btn:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
/* 排序控件在小屏幕下单独一行 */
|
||||
.sort-controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.sort-controls-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* 工具栏样式优化 */
|
||||
:deep(.p-dropdown) {
|
||||
background: transparent;
|
||||
border: 1px solid var(--surface-border);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
:deep(.p-dropdown:hover) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
:deep(.p-dropdown-panel) {
|
||||
.p-dropdown-items .p-dropdown-item {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-inputswitch) {
|
||||
.p-inputswitch-slider {
|
||||
background: var(--surface-200);
|
||||
}
|
||||
}
|
||||
|
||||
/* 确保所有按钮大小一致 */
|
||||
:deep(.p-button.p-button-icon-only) {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
/* 位置样式 */
|
||||
.location-icon {
|
||||
color: var(--pink-500);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
opacity: 0.9;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.location-separator {
|
||||
opacity: 0.5;
|
||||
font-weight: 300;
|
||||
margin: 0 0.1rem;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.location-text {
|
||||
color: var(--text-color-secondary, #cbd5e1);
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
color: var(--pink-400);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- 标题和工具栏 -->
|
||||
<div class="text-xl font-bold">
|
||||
<h1>{{ t('web.device.list') }}</h1>
|
||||
</div>
|
||||
|
||||
<Toolbar class="mb-4 p-3 gap-4 surface-0 border-1 surface-border rounded-md">
|
||||
<template #start>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="sort-by" class="text-sm text-500 hidden sm:block">{{ t('web.device.sort_by') }}:</label>
|
||||
<Dropdown id="sort-by" v-model="selectedSortOption" :options="sortOptions" optionLabel="name"
|
||||
class="sort-dropdown text-sm !min-w-[120px] sm:!min-w-[140px]" panelClass="text-sm">
|
||||
<template #value="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
<i :class="[slotProps.value.icon, 'text-600']"></i>
|
||||
<span class="text-600">{{ slotProps.value.name() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #option="slotProps">
|
||||
<div class="flex items-center gap-2">
|
||||
<i :class="[slotProps.option.icon, 'text-600']"></i>
|
||||
<span>{{ slotProps.option.name() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button :icon="ascending ? 'pi pi-sort-amount-up' : 'pi pi-sort-amount-down'" severity="secondary"
|
||||
text rounded class="sort-direction-btn min-w-[2.5rem] h-[2.5rem]"
|
||||
v-tooltip.top="ascending ? t('web.device.sort_direction_asc') : t('web.device.sort_direction_desc')"
|
||||
@click="toggleSortDirection" />
|
||||
</div>
|
||||
</template>
|
||||
<template #end>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="hidden sm:block border-r-1 surface-border h-4 mr-2"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="detailed-view" class="text-sm text-500 hidden sm:block">{{
|
||||
t('web.device.show_detailed_view') }}</label>
|
||||
<InputSwitch id="detailed-view" v-model="showDetailedView" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<div v-if="deviceList === undefined" class="w-full flex justify-center">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
|
||||
<DataTable :value="deviceList" tableStyle="min-width: 50rem" :metaKeySelection="true" sortField="hostname"
|
||||
:sortOrder="-1" v-if="deviceList !== undefined">
|
||||
<template #header>
|
||||
<div class="text-xl font-bold">Device List</div>
|
||||
</template>
|
||||
<div v-if="deviceList !== undefined">
|
||||
<!-- 卡片视图 (适用于所有屏幕尺寸) -->
|
||||
<div class="card-container">
|
||||
<div v-for="device in sortedDeviceList" :key="device.machine_id" class="device-card">
|
||||
<!-- 卡片头部 -->
|
||||
<div class="card-header">
|
||||
<!-- 上部区域:设备名称和版本徽章 -->
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<!-- 设备名称 -->
|
||||
<div class="font-semibold truncate card-title" :title="device.hostname">{{ device.hostname
|
||||
}}
|
||||
</div>
|
||||
|
||||
<Column field="hostname" header="Hostname" sortable style="width: 180px"></Column>
|
||||
<Column field="public_ip" header="Public IP" style="width: 150px"></Column>
|
||||
<Column field="running_network_count" header="Running Network Count" sortable style="width: 150px"></Column>
|
||||
<Column field="report_time" header="Report Time" sortable style="width: 150px"></Column>
|
||||
<Column field="easytier_version" header="EasyTier Version" sortable style="width: 150px"></Column>
|
||||
<Column class="w-24 !text-end">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-cog"
|
||||
@click="router.push({ name: 'deviceManagement', params: { deviceId: data.machine_id, instanceId: data.running_network_instances[0] } })"
|
||||
severity="secondary" rounded></Button>
|
||||
</template>
|
||||
</Column>
|
||||
<!-- 版本徽章 -->
|
||||
<div class="text-xs version-badge" v-tooltip="`EasyTier ${device.easytier_version}`">
|
||||
v{{ device.easytier_version.split('-')[0] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button icon="pi pi-refresh" label="Reload" severity="info" @click="loadDevices" />
|
||||
<!-- 下部区域:IP地址和操作按钮 -->
|
||||
<div class="flex justify-between items-center">
|
||||
<!-- IP地址和位置信息 -->
|
||||
<div class="text-sm truncate card-subtitle max-w-[60%] flex items-center gap-2"
|
||||
:title="device.location ? `${device.location.country}${device.location.region ? ' · ' + device.location.region : ''}${device.location.city ? ' · ' + device.location.city : ''}` : t('web.device.unknown_location')">
|
||||
<i class="pi pi-map-marker location-icon"></i>
|
||||
<span class="location-text">
|
||||
<template v-if="device.location">
|
||||
{{ device.location.country }}
|
||||
<template v-if="device.location.region">
|
||||
<span class="location-separator">·</span>
|
||||
{{ device.location.region }}
|
||||
</template>
|
||||
<template v-if="device.location.city">
|
||||
<span class="location-separator">·</span>
|
||||
{{ device.location.city }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('web.device.unknown_location') }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- 网络数量徽章 -->
|
||||
<span v-tooltip="t('web.device.network_count')"
|
||||
class="inline-flex items-center justify-center w-6 h-6 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
|
||||
{{ device.running_network_count }}
|
||||
</span>
|
||||
|
||||
<!-- 详情按钮 -->
|
||||
<Button v-tooltip="t('web.device.show_detailed_view')" icon="pi pi-info-circle"
|
||||
severity="info" text rounded class="w-9 h-9" v-if="!showDetailedView"
|
||||
@click="showDeviceDetails(device, $event)" />
|
||||
|
||||
<!-- 设置按钮 -->
|
||||
<Button icon="pi pi-cog" @click="handleDeviceManagement(device)" severity="secondary"
|
||||
rounded class="w-9 h-9" :title="`Manage ${device.hostname}`" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情区域 - 当开启详情显示时展示 -->
|
||||
<div v-if="showDetailedView" class="card-details border-t border-gray-200 fade-in">
|
||||
<DeviceDetails :device="device" containerClass="card-details-content" :compact="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局设备详情 Popover -->
|
||||
<Popover ref="detailPopover" :showCloseIcon="true" :closeOnEscape="true" :autoHide="false" appendTo="body"
|
||||
class="device-popover">
|
||||
<template v-if="selectedDevice">
|
||||
<div class="popover-header">
|
||||
<i class="pi pi-info-circle mr-2"></i>
|
||||
<span class="font-bold">设备详情</span>
|
||||
</div>
|
||||
<div class="device-details-popover">
|
||||
<DeviceDetails :device="selectedDevice" containerClass="popover-details-content" :compact="true" />
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
</Popover>
|
||||
|
||||
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
|
||||
:baseZIndex=1000 class="w-3/5 min-w-96">
|
||||
<Drawer v-model:visible="deviceManageVisible" :position="drawerPosition"
|
||||
:header="`Manage ${selectedDeviceHostname}`" :baseZIndex=1000 class="" :class="drawerWidth"
|
||||
:style="{ height: drawerHeight }">
|
||||
<template #container="{ closeCallback }">
|
||||
<div style="position: relative; height: 100%;" class="device-manage-drawer">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
|
||||
</RouterView>
|
||||
<Button icon="pi pi-times" rounded severity="danger"
|
||||
class="fixed z-50 right-6 bottom-6 shadow-lg drawer-fab-close-btn"
|
||||
style="width: 3.2rem; height: 3.2rem; font-size: 1.5rem;" @click="closeCallback" />
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import {Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider} from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
||||
import { IftaLabel, Select, Button, ConfirmPopup, useConfirm, useToast, Divider, Menu } from 'primevue';
|
||||
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
api: Api.ApiClient;
|
||||
@@ -33,6 +36,8 @@ const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
||||
|
||||
const isEditing = ref(false);
|
||||
const showCreateNetworkDialog = ref(false);
|
||||
const showConfigEditDialog = ref(false);
|
||||
const isCreatingNetwork = ref(false); // Flag to indicate if we're in network creation mode
|
||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||
|
||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||
@@ -103,7 +108,12 @@ const updateNetworkState = async (disabled: boolean) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disabled || !disabledNetworkConfig.value) {
|
||||
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
||||
} else if (disabledNetworkConfig.value) {
|
||||
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
|
||||
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
|
||||
}
|
||||
await loadNetworkInstanceIds();
|
||||
}
|
||||
|
||||
@@ -156,13 +166,19 @@ const createNewNetwork = async () => {
|
||||
}
|
||||
emits('update');
|
||||
showCreateNetworkDialog.value = false;
|
||||
isCreatingNetwork.value = false; // Exit creation mode after successful network creation
|
||||
}
|
||||
|
||||
const newNetwork = () => {
|
||||
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
|
||||
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
|
||||
isEditing.value = false;
|
||||
showCreateNetworkDialog.value = true;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
}
|
||||
|
||||
const cancelNetworkCreation = () => {
|
||||
isCreatingNetwork.value = false;
|
||||
}
|
||||
|
||||
const editNetwork = async () => {
|
||||
@@ -177,7 +193,8 @@ const editNetwork = async () => {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
console.debug("editNetwork", ret);
|
||||
newNetworkConfig.value = ret;
|
||||
showCreateNetworkDialog.value = true;
|
||||
// showCreateNetworkDialog.value = true; // Old dialog approach
|
||||
isCreatingNetwork.value = true; // Switch to creation mode instead
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to edit network, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
@@ -217,9 +234,15 @@ const exportConfig = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
delete ret.instance_id;
|
||||
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
|
||||
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||
delete networkConfig.instance_id;
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
|
||||
config: networkConfig
|
||||
});
|
||||
if (error) {
|
||||
throw { response: { data: error } };
|
||||
}
|
||||
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||
@@ -234,30 +257,34 @@ const importConfig = () => {
|
||||
const handleFileUpload = (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
const file = files ? files[0] : null;
|
||||
if (file) {
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
let str = e.target?.result?.toString();
|
||||
if(str){
|
||||
const config = JSON.parse(str);
|
||||
if(config === null || typeof config !== "object"){
|
||||
throw new Error();
|
||||
let tomlConfig = e.target?.result?.toString();
|
||||
if (!tomlConfig) return;
|
||||
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
}
|
||||
Object.assign(newNetworkConfig.value, config);
|
||||
|
||||
const config = resp.config;
|
||||
if (!config) return;
|
||||
|
||||
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
|
||||
Object.assign(newNetworkConfig.value, resp.config);
|
||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
|
||||
}
|
||||
configFile.value.value = null;
|
||||
}
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
const exportJsonFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
|
||||
const exportTomlFile = (context: string, name: string) => {
|
||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
|
||||
let link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.href = url;
|
||||
@@ -269,6 +296,58 @@ const exportJsonFile = (context: string, name: string) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
|
||||
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return tomlConfig ?? '';
|
||||
}
|
||||
|
||||
const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||
if (resp.error) {
|
||||
throw resp.error;
|
||||
};
|
||||
const config = resp.config;
|
||||
if (!config) {
|
||||
throw new Error("Parsed config is empty");
|
||||
}
|
||||
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||
if (networkIsDisabled.value) {
|
||||
disabledNetworkConfig.value = config;
|
||||
} else {
|
||||
newNetworkConfig.value = config;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式屏幕宽度
|
||||
const screenWidth = ref(window.innerWidth);
|
||||
const updateScreenWidth = () => {
|
||||
screenWidth.value = window.innerWidth;
|
||||
};
|
||||
|
||||
// 菜单引用和菜单项
|
||||
const menuRef = ref();
|
||||
const actionMenu = ref([
|
||||
{
|
||||
label: t('web.device_management.edit_network'),
|
||||
icon: 'pi pi-pencil',
|
||||
command: () => editNetwork()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.export_config'),
|
||||
icon: 'pi pi-download',
|
||||
command: () => exportConfig()
|
||||
},
|
||||
{
|
||||
label: t('web.device_management.delete_network'),
|
||||
icon: 'pi pi-trash',
|
||||
class: 'p-error',
|
||||
command: () => confirmDeleteNetwork(new Event('click'))
|
||||
}
|
||||
]);
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
try {
|
||||
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
||||
@@ -279,70 +358,253 @@ let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
periodFunc.start();
|
||||
|
||||
// 添加屏幕尺寸监听
|
||||
window.addEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
periodFunc.stop();
|
||||
|
||||
// 移除屏幕尺寸监听
|
||||
window.removeEventListener('resize', updateScreenWidth);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
|
||||
<div class="device-management">
|
||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||
<ConfirmPopup></ConfirmPopup>
|
||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
||||
:style="{ width: '55rem' }">
|
||||
<div class="flex flex-col">
|
||||
<div class="w-11/12 self-center ">
|
||||
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
||||
<Divider />
|
||||
</div>
|
||||
</div>
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</Dialog>
|
||||
|
||||
<Toolbar>
|
||||
<template #start>
|
||||
<IftaLabel>
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" inputId="dd-inst-id"
|
||||
placeholder="Select Instance" />
|
||||
<label class="mr-3" for="dd-inst-id">Network</label>
|
||||
<!-- 网络选择和操作按钮始终在同一行 -->
|
||||
<div class="network-header bg-surface-50 p-3 rounded-lg shadow-sm mb-1">
|
||||
<div class="flex flex-row justify-between items-center gap-2" style="align-items: center;">
|
||||
<!-- 网络选择 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<IftaLabel class="w-full">
|
||||
<Select v-model="selectedInstanceId" :options="instanceIdList" optionLabel="uuid" class="w-full"
|
||||
inputId="dd-inst-id" :placeholder="t('web.device_management.select_network')"
|
||||
:pt="{ root: { class: 'network-select-container' } }" />
|
||||
<label class="network-label mr-2 font-medium" for="dd-inst-id">{{
|
||||
t('web.device_management.network') }}</label>
|
||||
</IftaLabel>
|
||||
</template>
|
||||
|
||||
<template #end>
|
||||
<div class="gap-x-3 flex">
|
||||
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
|
||||
iconPos="right" />
|
||||
<Button @click="exportConfig" icon="pi pi-file-export" severity="help" label="Export" iconPos="right" />
|
||||
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
|
||||
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Toolbar>
|
||||
|
||||
<!-- 简化的按钮区域 - 无论屏幕大小都显示 -->
|
||||
<div class="flex gap-2 shrink-0 button-container items-center">
|
||||
<!-- Create/Cancel button based on state -->
|
||||
<Button v-if="!isCreatingNetwork" @click="newNetwork" icon="pi pi-plus"
|
||||
:label="screenWidth > 640 ? t('web.device_management.create_new') : undefined"
|
||||
:class="['create-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.create_network') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="primary" />
|
||||
|
||||
<Button v-else @click="cancelNetworkCreation" icon="pi pi-times"
|
||||
:label="screenWidth > 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
:class="['cancel-button', screenWidth <= 640 ? 'p-button-icon-only' : '']"
|
||||
:style="screenWidth <= 640 ? 'width: 3rem !important; height: 3rem !important; font-size: 1.2rem' : ''"
|
||||
:tooltip="screenWidth <= 640 ? t('web.device_management.cancel_creation') : undefined"
|
||||
tooltipOptions="{ position: 'bottom' }" severity="secondary" />
|
||||
|
||||
<!-- More actions menu -->
|
||||
<Menu ref="menuRef" :model="actionMenu" :popup="true" />
|
||||
<Button v-if="!isCreatingNetwork && selectedInstanceId" icon="pi pi-ellipsis-v"
|
||||
class="p-button-rounded flex items-center justify-center" severity="help"
|
||||
style="width: 3rem !important; height: 3rem !important; font-size: 1.2rem"
|
||||
@click="menuRef.toggle($event)" :aria-label="t('web.device_management.more_actions')"
|
||||
:tooltip="t('web.device_management.more_actions')" tooltipOptions="{ position: 'bottom' }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="network-content bg-surface-0 p-4 rounded-lg shadow-sm">
|
||||
<!-- Network Creation Form -->
|
||||
<div v-if="isCreatingNetwork" class="network-creation-container">
|
||||
<div class="network-creation-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-plus-circle text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ isEditing ? t('web.device_management.edit_network') :
|
||||
t('web.device_management.create_network') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex gap-2 flex-wrap justify-start mb-3">
|
||||
<Button @click="showConfigEditDialog = true" icon="pi pi-file-edit"
|
||||
:label="t('web.device_management.edit_as_file')" iconPos="left" severity="secondary" />
|
||||
<Button @click="importConfig" icon="pi pi-upload" :label="t('web.device_management.import_config')"
|
||||
iconPos="left" severity="help" />
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<!-- For running network, show the status -->
|
||||
<div v-if="needShowNetworkStatus">
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus">
|
||||
</Status>
|
||||
<Divider />
|
||||
<div class="text-center">
|
||||
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
|
||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||
</div>
|
||||
|
||||
<!-- Network Status (for running networks) -->
|
||||
<div v-else-if="needShowNetworkStatus" class="network-status-container">
|
||||
<div class="network-status-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-chart-line text-primary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_status') }}</h2>
|
||||
</div>
|
||||
|
||||
<Status v-bind:cur-network-inst="curNetworkInfo" class="mb-4"></Status>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<Button @click="updateNetworkState(true)" :label="t('web.device_management.disable_network')"
|
||||
severity="warning" icon="pi pi-power-off" iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- For disabled network, show the config -->
|
||||
<div v-if="networkIsDisabled">
|
||||
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)"
|
||||
v-if="disabledNetworkConfig" />
|
||||
<div v-else>
|
||||
<div class="text-center text-xl"> Network is disabled, Loading config... </div>
|
||||
<!-- Network Configuration (for disabled networks) -->
|
||||
<div v-else-if="networkIsDisabled" class="network-config-container">
|
||||
<div class="network-config-header flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-cog text-secondary text-xl"></i>
|
||||
<h2 class="text-xl font-medium">{{ t('web.device_management.network_configuration') }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="disabledNetworkConfig" class="mb-4">
|
||||
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)" />
|
||||
</div>
|
||||
<div v-else class="network-loading-placeholder text-center py-8">
|
||||
<i class="pi pi-spin pi-spinner text-3xl text-primary mb-3"></i>
|
||||
<div class="text-xl text-secondary">{{ t('web.device_management.loading_network_configuration') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
|
||||
<div class="text-center text-xl"> Select or create a network instance to manage </div>
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state flex flex-col items-center py-12">
|
||||
<i class="pi pi-sitemap text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('web.device_management.no_network_selected') }}
|
||||
</div>
|
||||
<p class="text-secondary text-center mb-6 max-w-md">
|
||||
{{ t('web.device_management.select_existing_network_or_create_new') }}
|
||||
</p>
|
||||
<Button @click="newNetwork" :label="t('web.device_management.create_network')" icon="pi pi-plus"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keep only the config edit dialogs -->
|
||||
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
|
||||
|
||||
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
|
||||
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-management {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.button-container {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
font-weight: 600;
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
/* 菜单样式定制 */
|
||||
:deep(.p-menu) {
|
||||
min-width: 12rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem) {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem-link) {
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem-icon) {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem.p-error .p-menuitem-text,
|
||||
.p-menu .p-menuitem.p-error .p-menuitem-icon) {
|
||||
color: var(--red-500);
|
||||
}
|
||||
|
||||
:deep(.p-menu .p-menuitem:hover.p-error .p-menuitem-link) {
|
||||
background-color: var(--red-50);
|
||||
}
|
||||
|
||||
/* 按钮图标样式 */
|
||||
:deep(.p-button-icon-only) {
|
||||
width: 2.5rem !important;
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
:deep(.p-button-icon-only .p-button-icon) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 网络选择相关样式 */
|
||||
.network-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.network-select-container) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Dark mode adaptations */
|
||||
:deep(.bg-surface-50) {
|
||||
background-color: var(--surface-50, #f8fafc);
|
||||
}
|
||||
|
||||
:deep(.bg-surface-0) {
|
||||
background-color: var(--surface-card, #ffffff);
|
||||
}
|
||||
|
||||
:deep(.text-primary) {
|
||||
color: var(--primary-color, #3b82f6);
|
||||
}
|
||||
|
||||
:deep(.text-secondary) {
|
||||
color: var(--text-color-secondary, #64748b);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.bg-surface-50) {
|
||||
background-color: var(--surface-ground, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.bg-surface-0) {
|
||||
background-color: var(--surface-card, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive design for mobile devices */
|
||||
@media (max-width: 768px) {
|
||||
.network-header {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* 在小屏幕上缩短网络标签文本 */
|
||||
.network-label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -3,8 +3,11 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { Api } from 'easytier-frontend-lib';
|
||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib';
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
isRegistering: boolean;
|
||||
@@ -74,57 +77,68 @@ onMounted(() => {
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">{{ isRegistering ? 'Register' : 'Login' }}
|
||||
<h2 class="text-2xl font-semibold text-center">{{ isRegistering ? t('web.login.register') :
|
||||
t('web.login.login') }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="p-field mb-4">
|
||||
<label for="api-host" class="block text-sm font-medium">Api Host</label>
|
||||
<label for="api-host" class="block text-sm font-medium">{{ t('web.login.api_host') }}</label>
|
||||
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
|
||||
@complete="apiHostSearch" class="w-full" />
|
||||
</div>
|
||||
<form v-if="!isRegistering" @submit.prevent="onSubmit" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="username" class="block text-sm font-medium">Username</label>
|
||||
<label for="username" class="block text-sm font-medium">{{ t('web.login.username') }}</label>
|
||||
<InputText id="username" v-model="username" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="password" class="block text-sm font-medium">Password</label>
|
||||
<label for="password" class="block text-sm font-medium">{{ t('web.login.password') }}</label>
|
||||
<Password id="password" v-model="password" required toggleMask :feedback="false" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Login" type="submit" class="w-full" />
|
||||
<Button :label="t('web.login.login')" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="button" class="w-full"
|
||||
<Button :label="t('web.login.register')" type="button" class="w-full"
|
||||
@click="saveApiHost(apiHost); $router.replace({ name: 'register' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form v-else @submit.prevent="onRegister" class="space-y-4">
|
||||
<div class="p-field">
|
||||
<label for="register-username" class="block text-sm font-medium">Username</label>
|
||||
<label for="register-username" class="block text-sm font-medium">{{ t('web.login.username')
|
||||
}}</label>
|
||||
<InputText id="register-username" v-model="registerUsername" required class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="register-password" class="block text-sm font-medium">Password</label>
|
||||
<label for="register-password" class="block text-sm font-medium">{{ t('web.login.password')
|
||||
}}</label>
|
||||
<Password id="register-password" v-model="registerPassword" required toggleMask
|
||||
:feedback="false" class="w-full" />
|
||||
</div>
|
||||
<div class="p-field">
|
||||
<label for="captcha" class="block text-sm font-medium">Captcha</label>
|
||||
<label for="captcha" class="block text-sm font-medium">{{ t('web.login.captcha') }}</label>
|
||||
<InputText id="captcha" v-model="captcha" required class="w-full" />
|
||||
<img :src="captchaSrc" alt="Captcha" class="mt-2 mb-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Register" type="submit" class="w-full" />
|
||||
<Button :label="t('web.login.register')" type="submit" class="w-full" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button label="Back to Login" type="button" class="w-full"
|
||||
<Button :label="t('web.login.back_to_login')" type="button" class="w-full"
|
||||
@click="saveApiHost(apiHost); $router.replace({ name: 'login' })" severity="secondary" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Button icon="pi pi-language" type="button" class="rounded-full absolute top-4 right-4 z-10"
|
||||
style="box-shadow: 0 2px 8px rgba(0,0,0,0.08);" severity="contrast"
|
||||
@click="I18nUtils.toggleLanguage" :aria-label="t('web.main.language')"
|
||||
:v-tooltip="t('web.main.language')" />
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Api, I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const api = computed<Api.ApiClient | undefined>(() => {
|
||||
@@ -21,14 +23,10 @@ const api = computed<Api.ApiClient | undefined>(() => {
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
onMounted(async () => {
|
||||
await I18nUtils.loadLanguageAsync('cn')
|
||||
});
|
||||
|
||||
const userMenu = ref();
|
||||
const userMenuItems = ref([
|
||||
{
|
||||
label: 'Change Password',
|
||||
label: t('web.main.change_password'),
|
||||
icon: 'pi pi-key',
|
||||
command: () => {
|
||||
console.log('File');
|
||||
@@ -45,7 +43,7 @@ const userMenuItems = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
label: t('web.main.logout'),
|
||||
icon: 'pi pi-sign-out',
|
||||
command: async () => {
|
||||
try {
|
||||
@@ -59,18 +57,58 @@ const userMenuItems = ref([
|
||||
])
|
||||
|
||||
const forceShowSideBar = ref(false)
|
||||
const sidebarRef = ref<HTMLElement>()
|
||||
const toggleButtonRef = ref<HTMLElement>()
|
||||
|
||||
// 处理点击外部区域关闭侧边栏
|
||||
const handleClickOutside = (event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// 如果侧边栏是隐藏的,不需要处理
|
||||
if (!forceShowSideBar.value) return;
|
||||
|
||||
// 检查点击是否在侧边栏内部或切换按钮上
|
||||
const isClickInsideSidebar = sidebarRef.value?.contains(target);
|
||||
const isClickOnToggleButton = toggleButtonRef.value?.contains(target);
|
||||
|
||||
// 如果点击在侧边栏外部且不在切换按钮上,则关闭侧边栏
|
||||
if (!isClickInsideSidebar && !isClickOnToggleButton) {
|
||||
forceShowSideBar.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换侧边栏显示状态
|
||||
const toggleSidebar = () => {
|
||||
forceShowSideBar.value = !forceShowSideBar.value;
|
||||
};
|
||||
|
||||
// 点击背景遮罩关闭侧边栏
|
||||
const closeSidebar = () => {
|
||||
forceShowSideBar.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 等待 DOM 渲染完成后添加事件监听器
|
||||
await nextTick();
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- https://flowbite.com/docs/components/sidebar/#sidebar-with-navbar -->
|
||||
<template>
|
||||
<nav class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
|
||||
<nav
|
||||
class="fixed top-0 z-50 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 top-navbar">
|
||||
<div class="px-3 py-3 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<div class="sm:hidden">
|
||||
<Button type="button" aria-haspopup="true" icon="pi pi-list" variant="text" size="large"
|
||||
severity="contrast" @click="forceShowSideBar = !forceShowSideBar" />
|
||||
<Button ref="toggleButtonRef" type="button" aria-haspopup="true" icon="pi pi-list"
|
||||
variant="text" size="large" severity="contrast" @click="toggleSidebar" />
|
||||
</div>
|
||||
<a href="https://easytier.top" class="flex ms-2 md:me-24">
|
||||
<img :src="Icon" class="h-9 me-3" alt="FlowBite Logo" />
|
||||
@@ -79,52 +117,27 @@ const forceShowSideBar = ref(false)
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="language-switch">
|
||||
<Button icon="pi pi-language" @click="I18nUtils.toggleLanguage" rounded severity="contrast" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center ms-3">
|
||||
<div>
|
||||
<Button type="button" @click="userMenu.toggle($event)" aria-haspopup="true"
|
||||
aria-controls="user-menu" icon="pi pi-user" raised rounded />
|
||||
<TieredMenu ref="userMenu" id="user-menu" :model="userMenuItems" popup />
|
||||
</div>
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm text-gray-900 dark:text-white" role="none">
|
||||
Neil Sims
|
||||
</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
|
||||
neil.sims@flowbite.com
|
||||
</p>
|
||||
</div>
|
||||
<ul class="py-1" role="none">
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Dashboard</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Earnings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside id="logo-sidebar"
|
||||
<!-- 背景遮罩 - 只在侧边栏显示时显示 -->
|
||||
<div v-if="forceShowSideBar" class="fixed inset-0 z-30 bg-black bg-opacity-50 sm:hidden" @click="closeSidebar">
|
||||
</div>
|
||||
|
||||
<aside ref="sidebarRef" id="logo-sidebar"
|
||||
class="fixed top-1 left-0 z-40 w-64 h-screen pt-20 transition-transform bg-white border-r border-gray-201 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
|
||||
:class="{ '-translate-x-full': !forceShowSideBar }" aria-label="Sidebar">
|
||||
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
|
||||
@@ -133,21 +146,21 @@ const forceShowSideBar = ref(false)
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'dashboard' })">
|
||||
<i class="pi pi-chart-pie text-xl"></i>
|
||||
<span class="mb-0.5">DashBoard</span>
|
||||
<span class="mb-0.5">{{ t('web.main.dashboard') }}</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'deviceList' })">
|
||||
<i class="pi pi-server text-xl"></i>
|
||||
<span class="mb-0.5">Devices</span>
|
||||
<span class="mb-0.5">{{ t('web.main.device_list') }}</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="text" class="w-full justify-start gap-x-3 pl-1.5 sidebar-button"
|
||||
severity="contrast" @click="router.push({ name: 'login' })">
|
||||
<i class="pi pi-sign-in text-xl"></i>
|
||||
<span class="mb-0.5">Login Page</span>
|
||||
<span class="mb-0.5">{{ t('web.main.login_page') }}</span>
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -155,7 +168,7 @@ const forceShowSideBar = ref(false)
|
||||
</aside>
|
||||
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700 mt-14">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" />
|
||||
|
@@ -6,6 +6,7 @@ import EasytierFrontendLib from 'easytier-frontend-lib'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import ConfirmationService from 'primevue/confirmationservice';
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import MainPage from './components/MainPage.vue'
|
||||
@@ -78,7 +79,12 @@ const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
createApp(App).use(PrimeVue,
|
||||
const app = createApp(App)
|
||||
|
||||
// Use i18n
|
||||
app.use(I18nUtils.i18n)
|
||||
|
||||
app.use(PrimeVue,
|
||||
{
|
||||
theme: {
|
||||
preset: Aura,
|
||||
|
@@ -23,11 +23,30 @@
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
/* 顶部导航栏安全区适配(如有 .top-navbar 类) */
|
||||
.top-navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 全屏内容适配移动端浏览器可视区 */
|
||||
.fullscreen-content {
|
||||
height: 100dvh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 如导航栏类名不同,请将 .top-navbar 替换为实际类名 */
|
||||
|
||||
.device-manage-drawer {
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ViteYaml from '@modyfi/vite-plugin-yaml'
|
||||
// import { viteSingleFile } from "vite-plugin-singlefile"
|
||||
|
||||
const WEB_BASE_URL = process.env.WEB_BASE_URL || '';
|
||||
@@ -8,7 +9,7 @@ const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:11211';
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: WEB_BASE_URL,
|
||||
plugins: [vue(),/* viteSingleFile() */],
|
||||
plugins: [vue(), ViteYaml(),/* viteSingleFile() */],
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
|
@@ -31,3 +31,6 @@ cli:
|
||||
api_host:
|
||||
en: "The URL of the API server, used by the web frontend to connect to"
|
||||
zh-CN: "API 服务器的 URL,用于 web 前端连接"
|
||||
geoip_db:
|
||||
en: "The path to the GeoIP2 database file, used to lookup the location of the client, default is the embedded file (only country information) , recommend https://github.com/P3TERX/GeoLite.mmdb"
|
||||
zh-CN: "GeoIP2 数据库文件路径,用于查找客户端的位置,默认为嵌入文件(仅国家信息),推荐 https://github.com/P3TERX/GeoLite.mmdb"
|
BIN
easytier-web/resources/geoip2-cn.mmdb
Normal file
After Width: | Height: | Size: 129 KiB |
@@ -1,72 +1,111 @@
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use easytier::{
|
||||
common::scoped_task::ScopedTask, proto::web::HeartbeatRequest, tunnel::TunnelListener,
|
||||
};
|
||||
use session::Session;
|
||||
use easytier::{proto::web::HeartbeatRequest, tunnel::TunnelListener};
|
||||
use maxminddb::geoip2;
|
||||
use session::{Location, Session};
|
||||
use storage::{Storage, StorageToken};
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::db::{Db, UserIdInDb};
|
||||
|
||||
#[derive(rust_embed::Embed)]
|
||||
#[folder = "resources/"]
|
||||
#[include = "geoip2-cn.mmdb"]
|
||||
struct GeoipDb;
|
||||
|
||||
fn load_geoip_db(geoip_db: Option<String>) -> Option<maxminddb::Reader<Vec<u8>>> {
|
||||
if let Some(path) = geoip_db {
|
||||
match maxminddb::Reader::open_readfile(&path) {
|
||||
Ok(reader) => {
|
||||
tracing::info!("Successfully loaded GeoIP2 database from {}", path);
|
||||
return Some(reader);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!("Failed to load GeoIP2 database from {}: {}", path, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let db = GeoipDb::get("geoip2-cn.mmdb").unwrap();
|
||||
let reader = maxminddb::Reader::from_source(db.data.to_vec()).ok()?;
|
||||
tracing::info!("Successfully loaded GeoIP2 database from embedded file");
|
||||
Some(reader)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientManager {
|
||||
accept_task: Option<ScopedTask<()>>,
|
||||
clear_task: Option<ScopedTask<()>>,
|
||||
tasks: JoinSet<()>,
|
||||
|
||||
listeners_cnt: Arc<AtomicU32>,
|
||||
|
||||
client_sessions: Arc<DashMap<url::Url, Arc<Session>>>,
|
||||
storage: Storage,
|
||||
|
||||
geoip_db: Arc<Option<maxminddb::Reader<Vec<u8>>>>,
|
||||
}
|
||||
|
||||
impl ClientManager {
|
||||
pub fn new(db: Db) -> Self {
|
||||
ClientManager {
|
||||
accept_task: None,
|
||||
clear_task: None,
|
||||
|
||||
client_sessions: Arc::new(DashMap::new()),
|
||||
storage: Storage::new(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve<L: TunnelListener + 'static>(
|
||||
&mut self,
|
||||
mut listener: L,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
listener.listen().await?;
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let storage = self.storage.weak_ref();
|
||||
let task = tokio::spawn(async move {
|
||||
while let Ok(tunnel) = listener.accept().await {
|
||||
let info = tunnel.info().unwrap();
|
||||
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||
println!("New session from {:?}", tunnel.info());
|
||||
let mut session = Session::new(storage.clone(), client_url.clone());
|
||||
session.serve(tunnel).await;
|
||||
sessions.insert(client_url, Arc::new(session));
|
||||
}
|
||||
});
|
||||
|
||||
self.accept_task = Some(ScopedTask::from(task));
|
||||
|
||||
let sessions = self.client_sessions.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
pub fn new(db: Db, geoip_db: Option<String>) -> Self {
|
||||
let client_sessions = Arc::new(DashMap::new());
|
||||
let sessions: Arc<DashMap<url::Url, Arc<Session>>> = client_sessions.clone();
|
||||
let mut tasks = JoinSet::new();
|
||||
tasks.spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
||||
sessions.retain(|_, session| session.is_running());
|
||||
}
|
||||
});
|
||||
self.clear_task = Some(ScopedTask::from(task));
|
||||
ClientManager {
|
||||
tasks,
|
||||
|
||||
listeners_cnt: Arc::new(AtomicU32::new(0)),
|
||||
|
||||
client_sessions,
|
||||
storage: Storage::new(db),
|
||||
geoip_db: Arc::new(load_geoip_db(geoip_db)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_listener<L: TunnelListener + 'static>(
|
||||
&mut self,
|
||||
mut listener: L,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
listener.listen().await?;
|
||||
self.listeners_cnt.fetch_add(1, Ordering::Relaxed);
|
||||
let sessions = self.client_sessions.clone();
|
||||
let storage = self.storage.weak_ref();
|
||||
let listeners_cnt = self.listeners_cnt.clone();
|
||||
let geoip_db = self.geoip_db.clone();
|
||||
self.tasks.spawn(async move {
|
||||
while let Ok(tunnel) = listener.accept().await {
|
||||
let info = tunnel.info().unwrap();
|
||||
let client_url: url::Url = info.remote_addr.unwrap().into();
|
||||
let location = Self::lookup_location(&client_url, geoip_db.clone());
|
||||
tracing::info!(
|
||||
"New session from {:?}, location: {:?}",
|
||||
client_url,
|
||||
location
|
||||
);
|
||||
let mut session = Session::new(storage.clone(), client_url.clone(), location);
|
||||
session.serve(tunnel).await;
|
||||
sessions.insert(client_url, Arc::new(session));
|
||||
}
|
||||
listeners_cnt.fetch_sub(1, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.accept_task.is_some() && self.clear_task.is_some()
|
||||
self.listeners_cnt.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
pub async fn list_sessions(&self) -> Vec<StorageToken> {
|
||||
@@ -108,9 +147,104 @@ impl ClientManager {
|
||||
s.data().read().await.req()
|
||||
}
|
||||
|
||||
pub async fn get_machine_location(&self, client_url: &url::Url) -> Option<Location> {
|
||||
let s = self.client_sessions.get(client_url)?.clone();
|
||||
s.data().read().await.location().cloned()
|
||||
}
|
||||
|
||||
pub fn db(&self) -> &Db {
|
||||
self.storage.db()
|
||||
}
|
||||
|
||||
fn lookup_location(
|
||||
client_url: &url::Url,
|
||||
geoip_db: Arc<Option<maxminddb::Reader<Vec<u8>>>>,
|
||||
) -> Option<Location> {
|
||||
let host = client_url.host_str()?;
|
||||
let ip: std::net::IpAddr = if let Ok(ip) = host.parse() {
|
||||
ip
|
||||
} else {
|
||||
tracing::debug!("Failed to parse host as IP address: {}", host);
|
||||
return None;
|
||||
};
|
||||
|
||||
// Skip lookup for private/special IPs
|
||||
let is_private = match ip {
|
||||
std::net::IpAddr::V4(ipv4) => {
|
||||
ipv4.is_private() || ipv4.is_loopback() || ipv4.is_unspecified()
|
||||
}
|
||||
std::net::IpAddr::V6(ipv6) => ipv6.is_loopback() || ipv6.is_unspecified(),
|
||||
};
|
||||
|
||||
if is_private {
|
||||
tracing::debug!("Skipping GeoIP lookup for special IP: {}", ip);
|
||||
let location = Location {
|
||||
country: "本地网络".to_string(),
|
||||
city: None,
|
||||
region: None,
|
||||
};
|
||||
return Some(location);
|
||||
}
|
||||
|
||||
let location = if let Some(db) = &*geoip_db {
|
||||
match db.lookup::<geoip2::City>(ip) {
|
||||
Ok(city) => {
|
||||
let country = city
|
||||
.country
|
||||
.and_then(|c| c.names)
|
||||
.and_then(|n| {
|
||||
n.get("zh-CN")
|
||||
.or_else(|| n.get("en"))
|
||||
.map(|s| s.to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "海外".to_string());
|
||||
|
||||
let city_name = city.city.and_then(|c| c.names).and_then(|n| {
|
||||
n.get("zh-CN")
|
||||
.or_else(|| n.get("en"))
|
||||
.map(|s| s.to_string())
|
||||
});
|
||||
|
||||
let region = city.subdivisions.map(|r| {
|
||||
r.iter()
|
||||
.map(|x| x.names.as_ref())
|
||||
.flatten()
|
||||
.map(|x| x.get("zh-CN").or_else(|| x.get("en")))
|
||||
.flatten()
|
||||
.map(|x| x.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
});
|
||||
|
||||
Location {
|
||||
country,
|
||||
city: city_name,
|
||||
region,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!("GeoIP lookup failed for {}: {}", ip, err);
|
||||
Location {
|
||||
country: "海外".to_string(),
|
||||
city: None,
|
||||
region: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"GeoIP database not available, using default location for {}",
|
||||
ip
|
||||
);
|
||||
Location {
|
||||
country: "海外".to_string(),
|
||||
city: None,
|
||||
region: None,
|
||||
}
|
||||
};
|
||||
|
||||
Some(location)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -131,8 +265,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_client() {
|
||||
let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap());
|
||||
let mut mgr = ClientManager::new(Db::memory_db().await);
|
||||
mgr.serve(Box::new(listener)).await.unwrap();
|
||||
let mut mgr = ClientManager::new(Db::memory_db().await, None);
|
||||
mgr.add_listener(Box::new(listener)).await.unwrap();
|
||||
|
||||
mgr.db()
|
||||
.inner()
|
||||
|
@@ -20,6 +20,13 @@ use crate::db::ListNetworkProps;
|
||||
|
||||
use super::storage::{Storage, StorageToken, WeakRefStorage};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Location {
|
||||
pub country: String,
|
||||
pub city: Option<String>,
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionData {
|
||||
storage: WeakRefStorage,
|
||||
@@ -28,10 +35,11 @@ pub struct SessionData {
|
||||
storage_token: Option<StorageToken>,
|
||||
notifier: broadcast::Sender<HeartbeatRequest>,
|
||||
req: Option<HeartbeatRequest>,
|
||||
location: Option<Location>,
|
||||
}
|
||||
|
||||
impl SessionData {
|
||||
fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||
fn new(storage: WeakRefStorage, client_url: url::Url, location: Option<Location>) -> Self {
|
||||
let (tx, _rx1) = broadcast::channel(2);
|
||||
|
||||
SessionData {
|
||||
@@ -40,6 +48,7 @@ impl SessionData {
|
||||
storage_token: None,
|
||||
notifier: tx,
|
||||
req: None,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +59,10 @@ impl SessionData {
|
||||
pub fn heartbeat_waiter(&self) -> broadcast::Receiver<HeartbeatRequest> {
|
||||
self.notifier.subscribe()
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&Location> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SessionData {
|
||||
@@ -165,8 +178,8 @@ impl Debug for Session {
|
||||
type SessionRpcClient = Box<dyn WebClientService<Controller = BaseController> + Send>;
|
||||
|
||||
impl Session {
|
||||
pub fn new(storage: WeakRefStorage, client_url: url::Url) -> Self {
|
||||
let session_data = SessionData::new(storage, client_url);
|
||||
pub fn new(storage: WeakRefStorage, client_url: url::Url, location: Option<Location>) -> Self {
|
||||
let session_data = SessionData::new(storage, client_url, location);
|
||||
let data = Arc::new(RwLock::new(session_data));
|
||||
|
||||
let rpc_mgr =
|
||||
|
@@ -8,9 +8,10 @@ use std::sync::Arc;
|
||||
use clap::Parser;
|
||||
use easytier::{
|
||||
common::{
|
||||
config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader},
|
||||
config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader},
|
||||
constants::EASYTIER_VERSION,
|
||||
error::Error,
|
||||
network::{local_ipv4, local_ipv6},
|
||||
},
|
||||
tunnel::{
|
||||
tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener,
|
||||
@@ -76,6 +77,12 @@ struct Cli {
|
||||
)]
|
||||
api_server_port: u16,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = t!("cli.geoip_db").to_string(),
|
||||
)]
|
||||
geoip_db: Option<String>,
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
#[arg(
|
||||
long,
|
||||
@@ -100,6 +107,22 @@ struct Cli {
|
||||
api_host: Option<url::Url>,
|
||||
}
|
||||
|
||||
impl LoggingConfigLoader for &Cli {
|
||||
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
|
||||
ConsoleLoggerConfig {
|
||||
level: self.console_log_level.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_logger_config(&self) -> FileLoggerConfig {
|
||||
FileLoggerConfig {
|
||||
dir: self.file_log_dir.clone(),
|
||||
level: self.file_log_level.clone(),
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
|
||||
Ok(match l.scheme() {
|
||||
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
|
||||
@@ -111,6 +134,31 @@ pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Erro
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_dual_stack_listener(
|
||||
protocol: &str,
|
||||
port: u16,
|
||||
) -> Result<
|
||||
(
|
||||
Option<Box<dyn TunnelListener>>,
|
||||
Option<Box<dyn TunnelListener>>,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let is_protocol_support_dual_stack =
|
||||
protocol.trim().to_lowercase() == "tcp" || protocol.trim().to_lowercase() == "udp";
|
||||
let v6_listener = if is_protocol_support_dual_stack && local_ipv6().await.is_ok() {
|
||||
get_listener_by_url(&format!("{}://[::0]:{}", protocol, port).parse().unwrap()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let v4_listener = if let Ok(_) = local_ipv4().await {
|
||||
get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((v6_listener, v4_listener))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
|
||||
@@ -118,31 +166,25 @@ async fn main() {
|
||||
setup_panic_handler();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let config = TomlConfigLoader::default();
|
||||
config.set_console_logger_config(ConsoleLoggerConfig {
|
||||
level: cli.console_log_level,
|
||||
});
|
||||
config.set_file_logger_config(FileLoggerConfig {
|
||||
dir: cli.file_log_dir,
|
||||
level: cli.file_log_level,
|
||||
file: None,
|
||||
});
|
||||
init_logger(config, false).unwrap();
|
||||
init_logger(&cli, false).unwrap();
|
||||
|
||||
// let db = db::Db::new(":memory:").await.unwrap();
|
||||
let db = db::Db::new(cli.db).await.unwrap();
|
||||
|
||||
let listener = get_listener_by_url(
|
||||
&format!(
|
||||
"{}://0.0.0.0:{}",
|
||||
cli.config_server_protocol, cli.config_server_port
|
||||
)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
let mut mgr = client_manager::ClientManager::new(db.clone(), cli.geoip_db);
|
||||
let (v6_listener, v4_listener) =
|
||||
get_dual_stack_listener(&cli.config_server_protocol, cli.config_server_port)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut mgr = client_manager::ClientManager::new(db.clone());
|
||||
mgr.serve(listener).await.unwrap();
|
||||
if v4_listener.is_none() && v6_listener.is_none() {
|
||||
panic!("Listen to both IPv4 and IPv6 failed");
|
||||
}
|
||||
if let Some(listener) = v6_listener {
|
||||
mgr.add_listener(listener).await.unwrap();
|
||||
}
|
||||
if let Some(listener) = v4_listener {
|
||||
mgr.add_listener(listener).await.unwrap();
|
||||
}
|
||||
|
||||
let mgr = Arc::new(mgr);
|
||||
|
||||
#[cfg(feature = "embed")]
|
||||
|
@@ -11,7 +11,7 @@ use axum::{extract::State, routing::get, Json, Router};
|
||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||
use axum_messages::MessagesManagerLayer;
|
||||
use easytier::common::config::ConfigLoader;
|
||||
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||
use easytier::common::scoped_task::ScopedTask;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::rpc_types;
|
||||
@@ -68,6 +68,17 @@ struct GenerateConfigResponse {
|
||||
toml_config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct ParseConfigRequest {
|
||||
toml_config: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
struct ParseConfigResponse {
|
||||
error: Option<String>,
|
||||
config: Option<NetworkConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Error {
|
||||
message: String,
|
||||
@@ -158,6 +169,25 @@ impl RestfulServer {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_parse_config(
|
||||
Json(req): Json<ParseConfigRequest>,
|
||||
) -> Result<Json<ParseConfigResponse>, HttpHandleError> {
|
||||
let config = TomlConfigLoader::new_from_str(&req.toml_config)
|
||||
.and_then(|config| NetworkConfig::new_from_config(&config));
|
||||
match config {
|
||||
Ok(c) => Ok(ParseConfigResponse {
|
||||
error: None,
|
||||
config: Some(c),
|
||||
}
|
||||
.into()),
|
||||
Err(e) => Ok(ParseConfigResponse {
|
||||
error: Some(format!("{:?}", e)),
|
||||
config: None,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(
|
||||
mut self,
|
||||
) -> Result<
|
||||
@@ -216,6 +246,7 @@ impl RestfulServer {
|
||||
"/api/v1/generate-config",
|
||||
post(Self::handle_generate_config),
|
||||
)
|
||||
.route("/api/v1/parse-config", post(Self::handle_parse_config))
|
||||
.layer(MessagesManagerLayer)
|
||||
.layer(auth_layer)
|
||||
.layer(tower_http::cors::CorsLayer::very_permissive())
|
||||
|
@@ -10,7 +10,7 @@ use easytier::proto::common::Void;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::proto::web::*;
|
||||
|
||||
use crate::client_manager::session::Session;
|
||||
use crate::client_manager::session::{Location, Session};
|
||||
use crate::client_manager::ClientManager;
|
||||
use crate::db::{ListNetworkProps, UserIdInDb};
|
||||
|
||||
@@ -66,6 +66,7 @@ struct ListNetworkInstanceIdsJsonResp {
|
||||
struct ListMachineItem {
|
||||
client_url: Option<url::Url>,
|
||||
info: Option<HeartbeatRequest>,
|
||||
location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
@@ -308,9 +309,11 @@ impl NetworkApi {
|
||||
for item in client_urls.iter() {
|
||||
let client_url = item.clone();
|
||||
let session = client_mgr.get_heartbeat_requests(&client_url).await;
|
||||
let location = client_mgr.get_machine_location(&client_url).await;
|
||||
machines.push(ListMachineItem {
|
||||
client_url: Some(client_url),
|
||||
info: session,
|
||||
location,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -3,12 +3,12 @@ name = "easytier"
|
||||
description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
|
||||
homepage = "https://github.com/EasyTier/EasyTier"
|
||||
repository = "https://github.com/EasyTier/EasyTier"
|
||||
version = "2.3.1"
|
||||
version = "2.4.1"
|
||||
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"
|
||||
|
||||
@@ -40,6 +40,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"] }
|
||||
@@ -64,7 +65,8 @@ bytes = "1.5.0"
|
||||
pin-project-lite = "0.2.13"
|
||||
tachyonix = "0.3.0"
|
||||
|
||||
quinn = { version = "0.11.0", optional = true, features = ["ring"] }
|
||||
quinn = { version = "0.11.8", optional = true, features = ["ring"] }
|
||||
|
||||
rustls = { version = "0.23.0", features = [
|
||||
"ring",
|
||||
], default-features = false, optional = true }
|
||||
@@ -84,11 +86,11 @@ http = { version = "1", default-features = false, features = [
|
||||
tokio-rustls = { version = "0.26", default-features = false, optional = true }
|
||||
|
||||
# for tap device
|
||||
tun = { package = "tun-easytier", version = "1.1.1", features = [
|
||||
tun = { package = "tun-easytier", git="https://github.com/EasyTier/rust-tun", features = [
|
||||
"async",
|
||||
], optional = true }
|
||||
# for net ns
|
||||
nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net"] }
|
||||
nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net", "fs"] }
|
||||
|
||||
uuid = { version = "1.5.0", features = [
|
||||
"v4",
|
||||
@@ -113,7 +115,7 @@ byteorder = "1.5.0"
|
||||
|
||||
# for proxy
|
||||
cidr = { version = "0.2.2", features = ["serde"] }
|
||||
socket2 = "0.5.5"
|
||||
socket2 = { version = "0.5.10", features = ["all"] }
|
||||
|
||||
# for hole punching
|
||||
stun_codec = "0.3.4"
|
||||
@@ -131,6 +133,7 @@ clap = { version = "4.5.30", features = [
|
||||
"wrap_help",
|
||||
"env",
|
||||
] }
|
||||
clap_complete = { version = "4.5.55" }
|
||||
|
||||
async-recursion = "1.0.5"
|
||||
|
||||
@@ -188,7 +191,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",
|
||||
@@ -211,14 +214,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"
|
||||
|
||||
@@ -245,6 +240,22 @@ windows = { version = "0.52.0", features = [
|
||||
encoding = "0.2"
|
||||
winreg = "0.52"
|
||||
windows-service = "0.7.0"
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_NetworkManagement_IpHelper",
|
||||
"Win32_NetworkManagement_Ndis",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Foundation"
|
||||
]}
|
||||
winapi = { version = "0.3.9", features = ["impl-default"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
jemallocator = { package = "tikv-jemallocator", version = "0.6.0", optional = true }
|
||||
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"
|
||||
@@ -270,7 +281,7 @@ thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = f
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3.0.0"
|
||||
rstest = "0.18.2"
|
||||
rstest = "0.25.0"
|
||||
futures-util = "0.3.30"
|
||||
maplit = "1.0.2"
|
||||
|
||||
@@ -280,18 +291,15 @@ tokio-socks = "0.5.2"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"]
|
||||
default = ["wireguard", "websocket", "smoltcp", "tun", "socks5", "quic"]
|
||||
full = [
|
||||
"quic",
|
||||
"websocket",
|
||||
"wireguard",
|
||||
"mimalloc",
|
||||
"aes-gcm",
|
||||
"smoltcp",
|
||||
"tun",
|
||||
"socks5",
|
||||
]
|
||||
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
|
||||
wireguard = ["dep:boringtun", "dep:ring"]
|
||||
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
|
||||
mimalloc = ["dep:mimalloc"]
|
||||
@@ -306,4 +314,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"]
|
||||
|
@@ -147,6 +147,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"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<dyn std::error::Error>> {
|
||||
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)]")
|
||||
@@ -170,7 +172,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
|
||||
.field_attribute(".web.NetworkConfig", "#[serde(default)]")
|
||||
.service_generator(Box::new(rpc_build::ServiceGenerator::new()))
|
||||
.btree_map(["."]);
|
||||
.btree_map(["."])
|
||||
.skip_debug(&[".common.Ipv4Addr", ".common.Ipv6Addr", ".common.UUID"]);
|
||||
|
||||
config.compile_protos(&proto_files, &["src/proto/"])?;
|
||||
|
||||
|
@@ -10,9 +10,17 @@ core_clap:
|
||||
配置服务器地址。允许格式:
|
||||
完整URL:--config-server udp://127.0.0.1:22020/admin
|
||||
仅用户名:--config-server admin,将使用官方的服务器
|
||||
machine_id:
|
||||
en: |+
|
||||
the machine id to identify this machine, used for config recovery after disconnection, must be unique and fixed. default is from system.
|
||||
zh-CN: |+
|
||||
Web 配置服务器通过 machine id 来识别机器,用于断线重连后的配置恢复,需要保证唯一且固定不变。默认从系统获得。
|
||||
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网络的网络名称"
|
||||
@@ -22,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将自动更改。"
|
||||
@@ -32,11 +43,20 @@ core_clap:
|
||||
en: "use a public shared node to discover peers"
|
||||
zh-CN: "使用公共共享节点来发现对等节点"
|
||||
proxy_networks:
|
||||
en: "export local networks to other peers in the vpn"
|
||||
zh-CN: "将本地网络导出到VPN中的其他对等节点"
|
||||
en: |+
|
||||
export local networks to other peers in the vpn, e.g.: 10.0.0.0/24.
|
||||
also support mapping proxy network to other cidr, e.g.: 10.0.0.0/24->192.168.0.0/24
|
||||
other peers can access 10.0.0.1 with ip 192.168.0.1
|
||||
zh-CN: |+
|
||||
将本地网络导出到VPN中的其他对等节点,例如:10.0.0.0/24。
|
||||
还支持将代理网络映射到其他CIDR,例如:10.0.0.0/24->192.168.0.0/24
|
||||
其他对等节点可以通过 IP 192.168.0.1 来访问 10.0.0.1
|
||||
rpc_portal:
|
||||
en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first"
|
||||
zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888"
|
||||
rpc_portal_whitelist:
|
||||
en: "rpc portal whitelist, only allow these addresses to access rpc portal, e.g.: 127.0.0.1,127.0.0.0/8,::1/128"
|
||||
zh-CN: "RPC门户白名单,仅允许这些地址访问RPC门户,例如:127.0.0.1/32,127.0.0.0/8,::1/128"
|
||||
listeners:
|
||||
en: |+
|
||||
listeners to accept connections, allow format:
|
||||
@@ -78,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"
|
||||
@@ -149,6 +172,12 @@ core_clap:
|
||||
disable_kcp_input:
|
||||
en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved."
|
||||
zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。"
|
||||
enable_quic_proxy:
|
||||
en: "proxy tcp streams with QUIC, improving the latency and throughput on the network with udp packet loss."
|
||||
zh-CN: "使用 QUIC 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。"
|
||||
disable_quic_input:
|
||||
en: "do not allow other nodes to use QUIC to proxy tcp streams to this node. when a node with QUIC proxy enabled accesses this node, the original tcp connection is preserved."
|
||||
zh-CN: "不允许其他节点使用 QUIC 代理 TCP 流到此节点。开启 QUIC 代理的节点访问此节点时,依然使用原始 TCP 连接。"
|
||||
port_forward:
|
||||
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
|
||||
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
|
||||
@@ -158,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:
|
||||
|
@@ -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<Vec<String>> {
|
||||
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<bool> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
1476
easytier/src/common/acl_processor.rs
Normal file
@@ -1,10 +1,7 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use anyhow::Context;
|
||||
use dashmap::DashMap;
|
||||
use std::cell::RefCell;
|
||||
use zstd::stream::read::Decoder;
|
||||
use zstd::stream::write::Encoder;
|
||||
use zstd::zstd_safe::{CCtx, DCtx};
|
||||
use zstd::bulk;
|
||||
|
||||
use zerocopy::{AsBytes as _, FromBytes as _};
|
||||
|
||||
@@ -35,17 +32,16 @@ impl DefaultCompressor {
|
||||
compress_algo: CompressorAlgo,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
match compress_algo {
|
||||
CompressorAlgo::ZstdDefault => {
|
||||
let ret = CTX_MAP.with(|map_cell| {
|
||||
CompressorAlgo::ZstdDefault => CTX_MAP.with(|map_cell| {
|
||||
let map = map_cell.borrow();
|
||||
let mut ctx_entry = map.entry(compress_algo).or_default();
|
||||
let writer = Vec::new();
|
||||
let mut o = Encoder::with_context(writer, ctx_entry.value_mut());
|
||||
o.write_all(data)?;
|
||||
o.finish()
|
||||
});
|
||||
Ok(ret?)
|
||||
}
|
||||
ctx_entry.compress(data).with_context(|| {
|
||||
format!(
|
||||
"Failed to compress data with algorithm: {:?}",
|
||||
compress_algo
|
||||
)
|
||||
})
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
}
|
||||
@@ -59,10 +55,23 @@ impl DefaultCompressor {
|
||||
CompressorAlgo::ZstdDefault => DCTX_MAP.with(|map_cell| {
|
||||
let map = map_cell.borrow();
|
||||
let mut ctx_entry = map.entry(compress_algo).or_default();
|
||||
let mut decoder = Decoder::with_context(data, ctx_entry.value_mut());
|
||||
let mut output = Vec::new();
|
||||
decoder.read_to_end(&mut output)?;
|
||||
Ok(output)
|
||||
for i in 1..=5 {
|
||||
let mut len = data.len() * 2usize.pow(i);
|
||||
if i == 5 && len < 64 * 1024 {
|
||||
len = 64 * 1024; // Ensure a minimum buffer size
|
||||
}
|
||||
match ctx_entry.decompress(data, len) {
|
||||
Ok(buf) => return Ok(buf),
|
||||
Err(e) if e.to_string().contains("buffer is too small") => {
|
||||
continue; // Try with a larger buffer
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to decompress data after multiple attempts with algorithm: {:?}",
|
||||
compress_algo
|
||||
))
|
||||
}),
|
||||
CompressorAlgo::None => Ok(data.to_vec()),
|
||||
}
|
||||
@@ -155,8 +164,8 @@ impl Compressor for DefaultCompressor {
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CTX_MAP: RefCell<DashMap<CompressorAlgo, CCtx<'static>>> = RefCell::new(DashMap::new());
|
||||
static DCTX_MAP: RefCell<DashMap<CompressorAlgo, DCtx<'static>>> = RefCell::new(DashMap::new());
|
||||
static CTX_MAP: RefCell<DashMap<CompressorAlgo, bulk::Compressor<'static>>> = RefCell::new(DashMap::new());
|
||||
static DCTX_MAP: RefCell<DashMap<CompressorAlgo, bulk::Decompressor<'static>>> = RefCell::new(DashMap::new());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@@ -2,13 +2,18 @@ use std::{
|
||||
net::{Ipv4Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
u64,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -38,6 +43,10 @@ pub fn gen_default_flags() -> Flags {
|
||||
disable_relay_kcp: true,
|
||||
accept_dns: false,
|
||||
private_mode: false,
|
||||
enable_quic_proxy: false,
|
||||
disable_quic_input: false,
|
||||
foreign_relay_bps_limit: u64::MAX,
|
||||
multi_thread_count: 2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,23 +67,25 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet>;
|
||||
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>);
|
||||
|
||||
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
|
||||
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
|
||||
|
||||
fn get_dhcp(&self) -> bool;
|
||||
fn set_dhcp(&self, dhcp: bool);
|
||||
|
||||
fn add_proxy_cidr(&self, cidr: cidr::IpCidr);
|
||||
fn remove_proxy_cidr(&self, cidr: cidr::IpCidr);
|
||||
fn get_proxy_cidrs(&self) -> Vec<cidr::IpCidr>;
|
||||
fn add_proxy_cidr(
|
||||
&self,
|
||||
cidr: cidr::Ipv4Cidr,
|
||||
mapped_cidr: Option<cidr::Ipv4Cidr>,
|
||||
) -> Result<(), anyhow::Error>;
|
||||
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr);
|
||||
fn get_proxy_cidrs(&self) -> Vec<ProxyNetworkConfig>;
|
||||
|
||||
fn get_network_identity(&self) -> NetworkIdentity;
|
||||
fn set_network_identity(&self, identity: NetworkIdentity);
|
||||
|
||||
fn get_listener_uris(&self) -> Vec<url::Url>;
|
||||
|
||||
fn get_file_logger_config(&self) -> FileLoggerConfig;
|
||||
fn set_file_logger_config(&self, config: FileLoggerConfig);
|
||||
fn get_console_logger_config(&self) -> ConsoleLoggerConfig;
|
||||
fn set_console_logger_config(&self, config: ConsoleLoggerConfig);
|
||||
|
||||
fn get_peers(&self) -> Vec<PeerConfig>;
|
||||
fn set_peers(&self, peers: Vec<PeerConfig>);
|
||||
|
||||
@@ -87,6 +98,9 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_rpc_portal(&self) -> Option<SocketAddr>;
|
||||
fn set_rpc_portal(&self, addr: SocketAddr);
|
||||
|
||||
fn get_rpc_portal_whitelist(&self) -> Option<Vec<IpCidr>>;
|
||||
fn set_rpc_portal_whitelist(&self, whitelist: Option<Vec<IpCidr>>);
|
||||
|
||||
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig>;
|
||||
fn set_vpn_portal_config(&self, config: VpnPortalConfig);
|
||||
|
||||
@@ -105,9 +119,24 @@ pub trait ConfigLoader: Send + Sync {
|
||||
fn get_port_forwards(&self) -> Vec<PortForwardConfig>;
|
||||
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>);
|
||||
|
||||
fn get_acl(&self) -> Option<Acl>;
|
||||
fn set_acl(&self, acl: Option<Acl>);
|
||||
|
||||
fn get_tcp_whitelist(&self) -> Vec<String>;
|
||||
fn set_tcp_whitelist(&self, whitelist: Vec<String>);
|
||||
|
||||
fn get_udp_whitelist(&self) -> Vec<String>;
|
||||
fn set_udp_whitelist(&self, whitelist: Vec<String>);
|
||||
|
||||
fn dump(&self) -> String;
|
||||
}
|
||||
|
||||
pub trait LoggingConfigLoader {
|
||||
fn get_file_logger_config(&self) -> FileLoggerConfig;
|
||||
|
||||
fn get_console_logger_config(&self) -> ConsoleLoggerConfig;
|
||||
}
|
||||
|
||||
pub type NetworkSecretDigest = [u8; 32];
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default, Eq, Hash)]
|
||||
@@ -166,7 +195,8 @@ pub struct PeerConfig {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ProxyNetworkConfig {
|
||||
pub cidr: String,
|
||||
pub cidr: cidr::Ipv4Cidr, // the CIDR of the proxy network
|
||||
pub mapped_cidr: Option<cidr::Ipv4Cidr>, // allow remap the proxy CIDR to another CIDR
|
||||
pub allow: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -182,13 +212,31 @@ pub struct ConsoleLoggerConfig {
|
||||
pub level: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)]
|
||||
pub struct LoggingConfig {
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
file_logger: Option<FileLoggerConfig>,
|
||||
#[builder(setter(into, strip_option), default = None)]
|
||||
console_logger: Option<ConsoleLoggerConfig>,
|
||||
}
|
||||
|
||||
impl LoggingConfigLoader for &LoggingConfig {
|
||||
fn get_file_logger_config(&self) -> FileLoggerConfig {
|
||||
self.file_logger.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
|
||||
self.console_logger.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct VpnPortalConfig {
|
||||
pub client_cidr: cidr::Ipv4Cidr,
|
||||
pub wireguard_listen: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct PortForwardConfig {
|
||||
pub bind_addr: SocketAddr,
|
||||
pub dst_addr: SocketAddr,
|
||||
@@ -230,6 +278,7 @@ struct Config {
|
||||
instance_name: Option<String>,
|
||||
instance_id: Option<uuid::Uuid>,
|
||||
ipv4: Option<String>,
|
||||
ipv6: Option<String>,
|
||||
dhcp: Option<bool>,
|
||||
network_identity: Option<NetworkIdentity>,
|
||||
listeners: Option<Vec<url::Url>>,
|
||||
@@ -239,10 +288,8 @@ struct Config {
|
||||
peer: Option<Vec<PeerConfig>>,
|
||||
proxy_network: Option<Vec<ProxyNetworkConfig>>,
|
||||
|
||||
file_logger: Option<FileLoggerConfig>,
|
||||
console_logger: Option<ConsoleLoggerConfig>,
|
||||
|
||||
rpc_portal: Option<SocketAddr>,
|
||||
rpc_portal_whitelist: Option<Vec<IpCidr>>,
|
||||
|
||||
vpn_portal_config: Option<VpnPortalConfig>,
|
||||
|
||||
@@ -256,6 +303,11 @@ struct Config {
|
||||
|
||||
#[serde(skip)]
|
||||
flags_struct: Option<Flags>,
|
||||
|
||||
acl: Option<Acl>,
|
||||
|
||||
tcp_whitelist: Option<Vec<String>>,
|
||||
udp_whitelist: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -389,6 +441,23 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
};
|
||||
}
|
||||
|
||||
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
|
||||
let locked_config = self.config.lock().unwrap();
|
||||
locked_config
|
||||
.ipv6
|
||||
.as_ref()
|
||||
.map(|s| s.parse().ok())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
|
||||
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()
|
||||
}
|
||||
@@ -397,50 +466,59 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().dhcp = Some(dhcp);
|
||||
}
|
||||
|
||||
fn add_proxy_cidr(&self, cidr: cidr::IpCidr) {
|
||||
fn add_proxy_cidr(
|
||||
&self,
|
||||
cidr: cidr::Ipv4Cidr,
|
||||
mapped_cidr: Option<cidr::Ipv4Cidr>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut locked_config = self.config.lock().unwrap();
|
||||
if locked_config.proxy_network.is_none() {
|
||||
locked_config.proxy_network = Some(vec![]);
|
||||
}
|
||||
let cidr_str = cidr.to_string();
|
||||
if let Some(mapped_cidr) = mapped_cidr.as_ref() {
|
||||
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
|
||||
.proxy_network
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|c| c.cidr == cidr_str)
|
||||
.any(|c| c.cidr == cidr && c.mapped_cidr == mapped_cidr)
|
||||
{
|
||||
locked_config
|
||||
.proxy_network
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.push(ProxyNetworkConfig {
|
||||
cidr: cidr_str,
|
||||
cidr,
|
||||
mapped_cidr,
|
||||
allow: None,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) {
|
||||
fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr) {
|
||||
let mut locked_config = self.config.lock().unwrap();
|
||||
if let Some(proxy_cidrs) = &mut locked_config.proxy_network {
|
||||
let cidr_str = cidr.to_string();
|
||||
proxy_cidrs.retain(|c| c.cidr != cidr_str);
|
||||
proxy_cidrs.retain(|c| c.cidr != cidr);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_proxy_cidrs(&self) -> Vec<cidr::IpCidr> {
|
||||
fn get_proxy_cidrs(&self) -> Vec<ProxyNetworkConfig> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.proxy_network
|
||||
.as_ref()
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.map(|c| c.cidr.parse().unwrap())
|
||||
.collect::<Vec<cidr::IpCidr>>()
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -481,32 +559,6 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_file_logger_config(&self) -> FileLoggerConfig {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.file_logger
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_file_logger_config(&self, config: FileLoggerConfig) {
|
||||
self.config.lock().unwrap().file_logger = Some(config);
|
||||
}
|
||||
|
||||
fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.console_logger
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_console_logger_config(&self, config: ConsoleLoggerConfig) {
|
||||
self.config.lock().unwrap().console_logger = Some(config);
|
||||
}
|
||||
|
||||
fn get_peers(&self) -> Vec<PeerConfig> {
|
||||
self.config.lock().unwrap().peer.clone().unwrap_or_default()
|
||||
}
|
||||
@@ -544,6 +596,14 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().rpc_portal = Some(addr);
|
||||
}
|
||||
|
||||
fn get_rpc_portal_whitelist(&self) -> Option<Vec<IpCidr>> {
|
||||
self.config.lock().unwrap().rpc_portal_whitelist.clone()
|
||||
}
|
||||
|
||||
fn set_rpc_portal_whitelist(&self, whitelist: Option<Vec<IpCidr>>) {
|
||||
self.config.lock().unwrap().rpc_portal_whitelist = whitelist;
|
||||
}
|
||||
|
||||
fn get_vpn_portal_config(&self) -> Option<VpnPortalConfig> {
|
||||
self.config.lock().unwrap().vpn_portal_config.clone()
|
||||
}
|
||||
@@ -606,6 +666,40 @@ impl ConfigLoader for TomlConfigLoader {
|
||||
self.config.lock().unwrap().port_forward = Some(forwards);
|
||||
}
|
||||
|
||||
fn get_acl(&self) -> Option<Acl> {
|
||||
self.config.lock().unwrap().acl.clone()
|
||||
}
|
||||
|
||||
fn set_acl(&self, acl: Option<Acl>) {
|
||||
self.config.lock().unwrap().acl = acl;
|
||||
}
|
||||
|
||||
fn get_tcp_whitelist(&self) -> Vec<String> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.tcp_whitelist
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_tcp_whitelist(&self, whitelist: Vec<String>) {
|
||||
self.config.lock().unwrap().tcp_whitelist = Some(whitelist);
|
||||
}
|
||||
|
||||
fn get_udp_whitelist(&self) -> Vec<String> {
|
||||
self.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.udp_whitelist
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_udp_whitelist(&self, whitelist: Vec<String>) {
|
||||
self.config.lock().unwrap().udp_whitelist = Some(whitelist);
|
||||
}
|
||||
|
||||
fn dump(&self) -> String {
|
||||
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
|
||||
let default_flags_hashmap =
|
||||
|