Compare commits

...

497 Commits

Author SHA1 Message Date
Luna Yao 79b562cdc9 drop peer_mgr in time (#2064) 2026-04-06 11:31:05 +08:00
fanyang e3f089251c fix(ospf): mitigate route sync storm under connection flapping (#2063)
Addresses issue #2016 where nodes behind unstable networks
(e.g. campus firewalls) cause excessive traffic that can freeze
the remote node.

Two changes in peer_ospf_route.rs:

- Make do_sync_route_info only trigger reverse sync_now when
  incoming data actually changed the route table or foreign
  network state.  The previous unconditional sync_now created
  an A->B->A->B ping-pong cycle on every RPC exchange.

- Add exponential backoff (50ms..5s) to session_task retry loop.
  The previous fixed 50ms retry produced ~20 RPCs/s during
  sustained network instability.
2026-04-06 11:26:20 +08:00
fanyang cf6dcbc054 Fix IPv6 TCP tunnel display formatting (#1980)
Normalize composite tunnel display values before rendering peer and
debug output so IPv6 tunnel types no longer append `6` to the port.

- Preserve prefixes like `txt-` while converting tunnel schemes to
  their IPv6 form.
- Recover malformed values such as `txt-tcp://...:110106` into
  `txt-tcp6://...:11010`.
- Reuse the normalized remote address display in CLI debug output.
2026-04-05 22:12:55 +08:00
fanyang 2cf2b0fcac feat(cli): implement connector add/remove, drop peer stubs (#2058)
Implement the previously stubbed connector add/remove CLI commands
using PatchConfig RPC with InstanceConfigPatch.connectors, and
remove the peer add/remove stubs that had incorrect semantics.
2026-04-05 13:56:17 +08:00
dependabot[bot] aa0cca3bb6 build(deps): bump quinn-proto in /easytier-contrib/easytier-ohrs (#2059)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.13 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.13...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 13:16:33 +08:00
KKRainbow fb59f01058 fix: reconcile webhook-managed configs and make disable_p2p more intelligent (#2057)
* reconcile infra configs on webhook validate
* make disable_p2p more intelligent
* fix stats
2026-04-04 23:41:57 +08:00
Luna Yao e91a0da70a refactor: listener/connector protocol abstraction (#2026)
* fix listener protocol detection
* replace IpProtocol with IpNextHeaderProtocol
* use an enum to gather all listener schemes
* rename ListenerScheme to TunnelScheme; replace IpNextHeaderProtocols with socket2::Protocol
* move TunnelScheme to tunnel
* add IpScheme, simplify connector creation
* format; fix some typos; remove check_scheme_...;
* remove PROTO_PORT_OFFSET
* rename WSTunnel.. -> WsTunnel.., DNSTunnel.. -> DnsTunnel..
2026-04-04 10:55:58 +08:00
Luna Yao 9cc617ae4c ci: build rpm package (#2044)
* add rpm to ci
* rename build_filter to build-filter
* use prepare-pnpm action
2026-04-04 10:32:08 +08:00
韩嘉乐 e4b0f1f1bb Rename libeasytier_ohrs.so to libeasytier_release.so when build release package (#2056)
Rename shared library file for release.
2026-04-04 10:29:37 +08:00
Luna Yao 443c3ca0b3 fix: append address of reverse proxy to remote_addr (#2034)
* append address of reverse proxy to remote_addr
* validate proxy address in test
2026-03-30 16:48:23 +08:00
Luna Yao 55a0e5952c chore: use cfg_aliases for mobile (#2033) 2026-03-30 16:38:39 +08:00
KKRainbow 1dff388717 bump version to v2.6.0 (#2039) 2026-03-30 15:50:07 +08:00
Luna Yao 61c741f887 add BoxExt trait (#2036) 2026-03-30 13:25:53 +08:00
ParkGarden 01dd9a05c3 fix: 重构了 Magisk 模块的 easytier_core.sh, action.sh, uninstall.sh 三个脚本的逻辑,优化参数解析与进程管理,调整措辞 (#1964) 2026-03-30 13:18:42 +08:00
KKRainbow 8c19a2293c fix(windows): avoid pnet interface enumeration panic (#2031) 2026-03-29 23:16:44 +08:00
KKRainbow a1bec48dc9 fix android vpn permission grant (#2023)
* fix android vpn permission grant
* fix url input behaviour
2026-03-29 23:16:32 +08:00
KKRainbow 7e289865b2 fix(faketcp): avoid pnet interface lookup on windows (#2029) 2026-03-29 19:26:29 +08:00
fanyang 742c7edd57 fix: use default connection loss rate for peer stats (#2030) 2026-03-29 19:25:25 +08:00
Luna Yao b71a2889ef suppress clippy warnings when no feature flags are enabled (#2028) 2026-03-29 11:02:23 +08:00
KKRainbow bcd75d6ce3 Add instance recv limiter in peer conn (#2027) 2026-03-29 10:28:02 +08:00
Luna Yao d4c1b0e867 fix: read X-Forwarded-For from HTTP header of WS/WSS (#2019) 2026-03-28 22:20:46 +08:00
KKRainbow b037ea9c3f Relax private mode foreign network secret checks (#2022) 2026-03-28 22:19:23 +08:00
Luna Yao b5f475cd4c filter overlapped proxy cidr (#2024) 2026-03-28 09:40:05 +08:00
Luna Yao eaa4d2c7b8 test: use taiki-e/install-action for cargo-hack (#2020) 2026-03-28 00:07:59 +08:00
Luna Yao e160d9b048 ci: remove aes-gcm from check (#1925) 2026-03-27 22:48:22 +08:00
KKRainbow 0aeea39fbe refactor(gui): collapse public server and standalone into initial peer list (#2017)
The GUI exposed three networking modes: public server, manual, and standalone. In practice EasyTier does not have a server/client role distinction here. Those options only mapped to different peer bootstrap shapes, which made the product model misleading and pushed users toward a non-existent "public server" concept.

This change rewrites the shared configuration UX around initial nodes. Users now add or remove one or more initial node URLs directly, and the UI explains that EasyTier networking works like plugging in a cable: once a node connects to one or more existing nodes, it can join the mesh. Initial nodes may be self-hosted or shared by others.

To preserve compatibility, the frontend keeps the legacy fields and adds normalization helpers in the shared NetworkConfig layer. Old configs are read as initial_node_urls, while saves, runs, validation, config generation, and persisted GUI config sync still denormalize back into the current backend shape: zero initial nodes -> Standalone, one -> PublicServer, many -> Manual. This avoids any proto or backend API change while making old saved configs and imported TOML files load cleanly in the new UI.

Code changes:

- add initial_node_urls plus normalize/denormalize helpers in the shared frontend NetworkConfig model

- remove the mode switch and public-server/manual specific inputs from the shared Config component and replace them with a single initial-node list plus explanatory copy

- update Chinese and English locale strings for the new terminology

- normalize configs received from GUI/web backends and denormalize them before outbound API calls

- normalize GUI save-config events before storing them in localStorage so legacy payloads remain editable under the new model
2026-03-27 11:37:09 +08:00
KKRainbow e000636d83 feat(stats): add by-instance traffic metrics (#2011) 2026-03-26 13:46:33 +08:00
Luna Yao 8e4dc508bb test: improve test_txt_public_stun_server with timeout and retry mechanism (#2014) 2026-03-26 09:32:07 +08:00
Luna Yao e2684a93de refactor: use strum on EncryptionAlgorithm, use Xor as default when AesGcm not available (#1923) 2026-03-25 18:42:34 +08:00
KKRainbow 1d89ddbb16 Add lazy P2P demand tracking and need_p2p override (#2003)
- add lazy_p2p so nodes only start background P2P for peers that actually have recent business traffic
- add need_p2p so specific peers can still request eager background P2P even when other nodes enable lazy mode
- cover the new behavior with focused connector/peer-manager tests plus three-node integration tests that verify relay-to-direct route transition
2026-03-23 09:38:57 +08:00
KKRainbow 2bfdd44759 multi_fix: harden peer/session handling, tighten foreign-network trust, and improve web client metadata (#1999)
* machine-id should be scoped unbder same user-id
* feat: report device os metadata to console
* fix sync root key cause packet loss
* fix tun packet not invalid
* fix faketcp cause lat jitter
* fix some packet not decrypt
* fix peer info patch, improve performance of update self info
* fix foreign credential identity mismatch handling
2026-03-21 21:06:07 +08:00
Luna Yao 77966916c4 cargo: add used features for windows-sys (#1924) 2026-03-17 14:10:50 +08:00
TsXor 26b7455c1e ignores eol difference for auto-generated files (#1997) 2026-03-16 23:40:38 +08:00
KKRainbow 8922e7b991 fix: foreign credential handling and trusted key visibility (#1993)
* fix foreign credential handling
* allow list foreign network trusted keys
* fix(gui): delete removed config-server networks
* fix(web): reset managed instances on first sync
2026-03-16 22:19:31 +08:00
KKRainbow e6ac31fb20 feat(web): add webhook-managed machine access and multi-instance CLI support (#1989)
* feat: add webhook-managed access and multi-instance CLI support
* fix(foreign): verify credential of foreign credential peer
2026-03-15 12:08:50 +08:00
KKRainbow c8f3c5d6aa feat(credential): support custom credential ID generation (#1984)
introduces support for custom credential ID generation, allowing users to specify their own credential IDs instead of relying solely on auto-generated UUIDs.
2026-03-12 00:48:24 +08:00
KKRainbow 330659e449 feat(web): full-power RPC access + typed JSON proxy endpoint (#1983)
- extend web controller bindings to cover full RPC service set
- update rpc_service API wiring and session/controller integration
- generate trait-level json_call_method in rpc codegen
- route restful proxy-rpc requests via scoped typed clients
- add json-call regression tests and required Sync bound fixes~
2026-03-11 20:32:37 +08:00
Maxwell 80043df292 script: introduce EasyTier powershell installer (#1975) 2026-03-11 11:57:03 +08:00
KKRainbow ecd1ea6f8c feat(web): implement secure core-web tunnel with Noise protocol (#1976)
Implement end-to-end encryption for core-web connections using the
Noise protocol framework with the following changes:

Client-side (easytier/src/web_client/):
- Add security.rs module with Noise handshake implementation
- Add upgrade_client_tunnel() for client-side handshake
- Add Noise frame encryption/decryption via TunnelFilter
- Integrate GetFeature RPC for capability negotiation
- Support secure_mode option to enforce encrypted connections
- Handle graceful fallback for backward compatibility

Server-side (easytier-web/):
- Accept Noise handshake in client_manager
- Expose encryption support via GetFeature RPC

The implementation uses Noise_NN_25519_ChaChaPoly_SHA256 pattern for
encryption without authentication. Provides backward compatibility
with automatic fallback to plaintext connections.
2026-03-10 08:48:08 +08:00
KKRainbow 694b8d349d feat(credential): enforce signed credential distribution across mixed admin/shared topology (#1972) 2026-03-10 08:37:33 +08:00
KKRainbow ef44027f57 feat(credential): improve credential peer routing and visibility (#1971)
- improve credential peer filtering and related route lookup behavior
- expose credential peer information through CLI and API definitions
- add and refine tests for credential routing and peer interactions
2026-03-08 14:06:33 +08:00
KKRainbow f3db348b01 fix: resolve slow exit and reduce test timeouts (#1970)
- Explicitly shutdown tokio runtime on launcher cleanup to fix slow exit
- Add timeout to tunnel connector in tests to prevent hanging
- Reduce test wait durations from 5s to 100ms for faster test execution
- Bump num-bigint-dig from 0.8.4 to 0.8.6
2026-03-08 12:27:42 +08:00
KKRainbow c4eacf4591 feat(credential): implement credential peer auth and trust propagation (#1968)
- add credential manager and RPC/CLI for generate/list/revoke
- support credential-based Noise authentication and revocation handling
- propagate trusted credential metadata through OSPF route sync
- classify direct peers by auth level in session maintenance
- normalize sender credential flag for legacy non-secure compatibility
- add unit/integration tests for credential join, relay and revocation
2026-03-07 22:58:15 +08:00
KKRainbow 59d4475743 feat: relay peer end-to-end encryption via Noise IK handshake (#1960)
Enable encryption for non-direct nodes requiring relay forwarding.
When secure_mode is enabled, peers perform Noise IK handshake to
establish an encrypted PeerSession. Relay packets are encrypted at
the sender and decrypted at the receiver. Intermediate forwarding
nodes cannot read plaintext data.

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com>
2026-03-07 14:47:22 +08:00
KKRainbow 22b4c4be2c fix: guard macos-ne feature with target_os = "macos" in cfg expressions (#1962)
All 13 occurrences of `any(target_os = "ios", feature = "macos-ne")` are
replaced with `any(target_os = "ios", all(target_os = "macos", feature = "macos-ne"))`.

Previously, enabling `macos-ne` on non-macOS platforms (e.g. `--all-features`
on Linux) would incorrectly compile macOS/mobile-specific code paths, causing
build failures or wrong runtime behavior.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 00:06:21 +08:00
Luna Yao 5f31583a84 refactor: 使用 tracing 输出日志 (#1856)
* change all println to tracing
2026-03-04 09:52:23 +08:00
Mg Pig 1d25240d8c refactor(ui): extract URL input components and enhance UI responsiveness (#1819) 2026-03-04 09:49:15 +08:00
fanyang eeb507d6ea fix: register PeerCenterRpc in management API server so CLI peer-center works (#1929)
PeerCenterRpc was only registered in the per-instance peer-to-peer RPC
manager (domain = network_name), but not in the management API server
(domain = ""). The CLI connects to the management API with an empty
domain, causing "Invalid service name: PeerCenterRpc" errors.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 09:37:37 +08:00
fanyang 9e9916efa5 fix(connector): skip self-connection when peer shares local interface IPs (#1941)
When two EasyTier instances run on the same machine and share the same
network, the direct connector would expand a remote peer's 0.0.0.0
listener into local interface IPs and then attempt to connect to
itself, causing an infinite loop of failed connection attempts.

The existing `peer_id != my_peer_id` guard does not cover this case
because the two instances have different peer IDs despite sharing the
same physical network interfaces.

Fix by adding a self-connection check in `spawn_direct_connect_task`:
before spawning a connect task, compare the candidate (scheme, IP,
port) against the local running listeners. If a local listener matches
on all three dimensions — accounting for 0.0.0.0/:: wildcards by
checking membership in the local interface IP sets — the candidate is
silently dropped with a DEBUG log message.

The fix covers all four code paths:
- IPv4 unspecified (0.0.0.0) expansion loop
- IPv4 specific-address branch
- IPv6 unspecified (::) expansion loop
- IPv6 specific-address branch

The TESTING flag logic is untouched so existing unit tests are
unaffected.

* refactor(connector): replace is_self_connect closure with GlobalCtx::should_deny_proxy (#1954)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-04 09:36:35 +08:00
hello db6b9e3684 feat: core config server use last path segment as user name (#1931) 2026-03-03 18:24:28 +08:00
Mg Pig ff24332e23 feat(web): add OIDC SSO login support (#1943) 2026-03-03 18:23:31 +08:00
fanyang d4ff0b1767 build(deps): upgrade vite to 5.4.21 in frontend and gui packages (#1950) 2026-03-01 13:47:02 +08:00
Mg Pig 5716f7f16b fix(web): allow configuring listen address for API and web servers (#1919) (#1948) 2026-03-01 01:02:31 +08:00
fanyang e5bd8f9e24 build(deps): upgrade minimatch to 10.2.4 (#1949) 2026-02-28 22:40:47 +08:00
sky96111 b56bcfb4b0 fix: increase websocket peer connection timeout to 20 seconds (#1939)
- Add ws/wss protocols to long timeout list
2026-02-28 18:26:19 +08:00
fanyang fb95b4827c build(deps): bump axios from 1.11.0 to 1.13.6 in frontend packages (#1947)
Addresses security vulnerabilities in axios <1.13.5. Updates the
declared specifier to ^1.13.5 in all three frontend package.json
files and regenerates both npm and pnpm lock files (resolved: 1.13.6).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:17:18 +08:00
fanyang a8f7226195 fix(foreign_network): set avoid_relay_data when relay_data is false (#1935) 2026-02-25 09:30:24 +08:00
dependabot[bot] e6ee485352 build(deps-dev): bump vite from 5.4.10 to 5.4.21 in /easytier-web/frontend-lib (#1922)
* build(deps-dev): bump vite in /easytier-web/frontend-lib

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 22:47:29 +08:00
hello 73291a3a1c feat: Update Cargo.toml to add support for tls1.2 when use wss (#1917) 2026-02-20 18:01:21 +08:00
fanyang f737708f45 fix: avoid panic on malformed short tunnel packets (#1904) 2026-02-18 00:04:30 +08:00
fanyang aa24d09aa2 fix: replace stale magic DNS records on IP change (#1906)
Magic DNS updates are full snapshots, so appending routes keeps old IPs and returns duplicate A records. Replace each client's previous routes on update and add a regression test to ensure hostname resolution keeps only the latest IP.
2026-02-16 13:20:11 +08:00
fanyang fe4e77979d fix: avoid panic for quic peer urls using port 0 (#1905)
Prevent crashes when users input quic://...:0 by rejecting port 0 explicitly and propagating connect setup errors. Add a regression test to ensure invalid QUIC targets fail gracefully.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-14 17:10:29 +08:00
Chenx Dust 7a26640c26 feat: support macOS Network Extension (#1902)
* feat: support macOS Network Extension
* fix: disable macOS NE feature in cargo hack check
2026-02-14 14:54:36 +08:00
Mg Pig 5a777959e3 ui: clarify encryption checkbox description in locales (#1841) 2026-02-13 16:04:26 +08:00
Mg Pig 3512a80597 feat(web): add --disable-registration flag to disable user registration (#1881) 2026-02-13 16:03:11 +08:00
Zkitefly 011770a601 Update http_connector.rs (#1900) 2026-02-13 16:02:32 +08:00
Chenx Dust 6475724d2e fix: toggle_window_visibility with focus check (#1888)
* refactor: better logics for toggle_window_visibility
2026-02-11 16:50:36 +08:00
Mg Pig 85e9029577 feat: add Nix CI workflow and update flake.lock dependencies (#1872) 2026-02-10 18:11:35 +08:00
Luna Yao b6e292cce3 ci: use shared key for build workflow (#1868) 2026-02-04 09:48:55 +08:00
KKRainbow c58140fb47 update rust to 1.93 (#1865) 2026-02-04 09:48:43 +08:00
Luna Yao aebb7facfa drop permit reserved by poll_reserve (#1858) 2026-02-03 11:14:11 +08:00
Chenx Dust 1e2124cb99 fix: force set tun fd when received (#1860) 2026-02-03 11:13:31 +08:00
Chenx Dust e1cbd07d1f feat: separate zstd and faketcp into features (#1861)
* feat: separate faketcp into a feature
* fix: no need to initialize out_len
* feat: separate zstd into a feature
* clippy: remove unnecessary cast, because for unix size_t always equals usize
2026-02-03 11:12:33 +08:00
韩嘉乐 7750e81168 CI(ohos): add a condition to check for the publish code (#1863)
Added a condition to check for the presence of a release code when running the publish step
2026-02-03 11:11:45 +08:00
KKRainbow bf3edbd28f remove src modified flag from pm hdr (#1857) 2026-02-02 16:47:26 +08:00
Luna Yao cd2cf56358 refactor: handle quic proxy internally instead of use external udp port (#1743)
* deprecate quic_listen_port, add disable_relay_quic and enable_relay_foreign_network_quic
* add set_src_modified to TcpProxyForWrappedSrcTrait
* prioritize quic over kcp
2026-02-02 11:53:40 +08:00
KKRainbow 21f4a944a7 fix perf degraded because of impact of is_empty() of dashmap (#1854) 2026-02-01 08:51:18 +08:00
KKRainbow 9617005136 make udp->ring transmit reliable (#1851) 2026-01-31 17:23:45 +08:00
deddey c85d1d41b3 allow set TUN dev name on FreeBSD (#1823)
Also rename stale interfaces from previous runs before creating new ones.
Works around rust-tun reusing existing tun0 instead of configured name.

Tested on FreeBSD 14.1
2026-01-30 23:51:52 +08:00
KKRainbow 9e3c9228bb improve perf of remove_network in foreign net mgr (#1847) 2026-01-30 23:04:31 +08:00
Luna Yao acd7c85ff6 ci: speed up test with matrix (#1830)
* add an action to install pnpm packages
* add an action to prepare build environment
* rewrite test workflow, using composite actions and matrix
2026-01-30 22:21:27 +08:00
KKRainbow 8727221513 call remove_peer instead of remove_network when peer id not match (#1844) 2026-01-30 16:01:52 +08:00
Luna Yao cdedaf3f63 refactor(quic): remove quinn encryption (#1831)
* use quinn-plaintext
* remove server_cert in QUICTunnelListener
* remove some customized transport config
* leave max_concurrent_bidi_streams as default

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-30 10:21:59 +08:00
KKRainbow ffe5644ddc add token bucket limiter on peer conn recv (#1842)
We should limit peer conn recv to make sure we don't recv too much from peers.
2026-01-29 16:12:26 +08:00
Chenx Dust ccc684a9ab Fix: Fixed compilation issue after partially removing the feature flag (#1835) 2026-01-28 21:38:34 +08:00
fanyang 977e502150 feat(cli): add column truncation controls (#1838)
- drop low-priority columns when tables exceed terminal width
- truncate optional columns to fit remaining width
- add --no-trunc flag to disable truncation
- compute column widths using unicode display width

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 14:50:14 +08:00
Mg Pig 518d26b25f feat: add X-Network-Name header to HTTP connector requests (#1839)
This allows HTTP redirect servers to provide network-specific node
lists based on the client's network identity. Updated unit tests
to verify the header is correctly sent.
2026-01-28 14:48:45 +08:00
KKRainbow 101f416268 Introduce secure mode (part 1) (#1808)
Use noise protocol on handshake. Check peer's public key if needed. Also support rekey and replay attack prevention.

E2EE and temporary password will be implemented based on this.
2026-01-25 20:16:51 +08:00
Chenx Dust ffa08d1c43 feat: add peer_id in MyNodeInfo (#1821) 2026-01-22 22:44:37 +08:00
韩嘉乐 cf3f9169b7 CI(ohos): Enhance CI workflow for release package builds (#1812)
Added support for building and publishing release packages based on tags.
2026-01-20 12:25:10 +08:00
KKRainbow 8343cd5e76 fix config loss when run network (#1802) 2026-01-17 00:58:42 +08:00
KKRainbow 005b321f62 allow open rpc port in gui normal mode (#1795)
* allow open rpc port for gui normal mode
* downgrade dev tool console
2026-01-16 11:12:32 +08:00
KKRainbow 53264f67bf fix peer establish direct conn with subnet proxy to one of local interface (#1782)
* fix peer establish direct conn with subnet proxy to one of local interface

* fix peer mgr ref loop
2026-01-15 01:00:32 +08:00
韩嘉乐 f8b34e3c86 Merge pull request #1787 from EasyTier/FrankHan052176-patch-1
action[ohos] fix the cnt of commit in ohos.yml
2026-01-13 23:58:26 +08:00
韩嘉乐 ce1bdac2bc action[ohos] fix the cnt of commit in ohos.yml 2026-01-13 22:57:43 +08:00
Copilot bd8f01fb26 Add Nushell completion script generation support (#1756)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-11 18:41:02 +08:00
Chenx Dust b590700540 feat: support unix socket tunnel (for ios) (#1779)
Co-authored-by: Page Chen <pagechen04@gmail.com>
2026-01-11 16:37:32 +08:00
Chenx Dust 48c5c23f9b feat: support compile for iOS (#1777) 2026-01-11 16:36:58 +08:00
朝倉水希 f4f591d14c fix: outbound packet not dropped by acl (#1766) 2026-01-08 19:58:23 +08:00
Mg Pig 0c16e2211b feat(gui): persist and restore last used network instance ID (#1762) 2026-01-08 17:03:51 +08:00
Rinne 4bfea06a12 docs: update locales (#1755)
Co-authored-by: KKRainbow <443152178@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 11:08:32 +08:00
桜井 ホタル 057ee9f2c5 Resolves the issue of DNS resolution failure after installing KSU modules, resulting in inability to connect to nodes. (#1761) 2026-01-08 11:07:52 +08:00
Burning_TNT 7f48ca54a3 Implement requesting tun_fd with tokio channel. (#1734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-04 21:04:43 +08:00
hello ee5227130c feat: Update Cargo.toml for easytier-gui and android app to support tls1.2 (#1744) 2026-01-04 21:03:34 +08:00
韩嘉乐 2e0d9a2b54 Refactor EasyTier version resolution in workflow (#1747)
Updated the workflow to resolve the EasyTier version based on the latest commit and tag information.
2026-01-04 21:02:55 +08:00
编程小白 c5d732773f Convert dead URL to ASCII before socket address lookup (#1739) 2026-01-02 18:49:23 +08:00
狂男风 88a45d1156 use 80/443 as ws/wss default port (#1700) 2026-01-01 01:31:38 +08:00
KKRainbow 4e651a72f7 allow loopback src address in listener (#1730) 2026-01-01 00:41:56 +08:00
Mg Pig 7c563153ae fix: ensure proxy routes update correctly on NIC (#1729) 2025-12-31 22:36:45 +08:00
KKRainbow cb81c0df85 respond packet should not be dropped if request packet is already allowed (#1725) 2025-12-31 08:14:39 +08:00
21paradox 9c316ea01c fix socks5 and tcp forward mem leak (#1721)
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-12-31 00:01:44 +08:00
XuDaojie 541fc664e3 update[gui]:将macOS端的应用图标修改为Big Sur风格圆角图标 (#1723) 2025-12-30 22:22:39 +08:00
Mg Pig 18478b7c4b fix(android): update vpn routes when proxy cidrs change (#1717) 2025-12-30 19:26:42 +08:00
韩嘉乐 650323faef [Ohos] 仅在push时执行发布操作,避免流水线运行错误 (#1718) 2025-12-29 13:57:02 +08:00
狂男风 ed131272d4 fix(gui): open_log_dir not working (#1714)
* fix(mobile): open_log_dir not working on android
2025-12-28 23:20:29 +08:00
KKRainbow 39b056c87a bump version to v2.5.0 (#1715) 2025-12-28 23:19:30 +08:00
KKRainbow c19cd1bff3 add tcp hole punching (#1713)
add tcp hole punching and tcp stun test
2025-12-28 21:35:30 +08:00
狂男风 37531507db fix(mobile): logs unreachable on android (#1710) 2025-12-27 20:04:18 +08:00
KKRainbow ca9b4c58b1 fix windivert cause stack overflow (#1711) 2025-12-27 19:31:42 +08:00
KKRainbow 4341bcba5d improve faketcp, handle tcp GSO correctly (#1708)
Current implementation falsely drop GSO-merged tcp packet, and cause unexpected packet loss.
2025-12-26 23:46:17 +08:00
韩嘉乐 0be4ac1fa5 [Ohos] 使用Commit计数器替代Commit Hash作为版本尾缀 (#1703) 2025-12-25 20:42:43 +08:00
KKRainbow 28cd6da502 Add fake tcp tunnel (experimental) (#1673)
support faketcp to avoid tcp-over-tcp problem.
linux/macos/windows are supported.

better to be used in internet env, the maximum 
performance is majorly limited by windivert/raw socket.
2025-12-25 00:10:32 +08:00
狂男风 0712ef762d Fix logic error in relay network whitelist resolving (#1692) 2025-12-23 08:25:45 +08:00
韩嘉乐 eee7d7a1ed 增加Ohos流水线发布步骤执行条件 (#1695) 2025-12-22 21:40:28 +08:00
Burning_TNT 4c58def0db Make release.yml available in forks (#1689) 2025-12-21 21:13:53 +08:00
Momo c6a32e4467 fix: magic dns tld_dns_zone were not working properly (#1686)
* fix: magic dns tld_dns_zone failed to get updated
2025-12-21 21:13:39 +08:00
韩嘉乐 30f0ff16ca Merge pull request #1678 from EasyTier/ohpm
[鸿蒙] 在流水线中增加上传中心仓与上传华为云私仓流程,增加华为云流水线Webhook
2025-12-16 12:06:19 +08:00
FrankHan 38d117ee44 [鸿蒙] 在流水线中增加上传中心仓与上传华为云私仓流程,增加华为云流水线Webhook 2025-12-16 11:50:05 +08:00
KKRainbow 7aba65ea32 enhance port forward (#1662) 2025-12-09 22:16:16 +08:00
Tunglies fe4dff5df0 perf: simplify method signatures and reduce clone across multiple files (#1663) 2025-12-09 16:47:57 +08:00
KKRainbow 2bc51daa98 fix whitelist cause packets of other protocal dropped (#1660) 2025-12-08 21:56:27 +08:00
KKRainbow 838b6101b9 Make ospf route more effiencient (#1512)
Avoid iterate all peer info and conn list when building sync request.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 13:14:47 +08:00
韩嘉乐 056c9da781 [EasyTier-ohrs] Use NetworkConfig instead of TomlConfig, and add CompressionAlgorithm and EncryptionAlgorithm to NetworkConfig. (#1654) 2025-12-06 23:23:22 +08:00
datasone 2a656d6a0c fix(core): Fix sleep-wake reconnect by resetting alive_conn_urls (#1593)
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-12-05 14:31:08 +08:00
KKRainbow 43a650f9ab set FORCE_USE_CONN_LIST default to false (#1652)
this is falsely set to true and will casue compatibility issue
2025-12-05 00:26:04 +08:00
C.C. 88a55859ac fix(web): remove trailing slash from api base url (#1621) 2025-12-04 23:06:28 +08:00
dawn-lc d686c8721f feat(install): enhance installation script functionality (#1641)
* feat(install): enhance installation script functionality
* fix temp file extname
2025-12-04 23:06:06 +08:00
Mg Pig 0a718163fd feat(gui): GUI add support to connect to config server (#1596) 2025-12-04 23:05:36 +08:00
Mg Pig 53f279f5ff feat(core): Support environment variable parsing in config files (#1640) 2025-12-02 17:54:31 +08:00
Mg Pig ae6d929f4a fix(mobile): Add DHCP polling to fix Android VPN startup failure (#1628) 2025-12-01 01:13:05 +08:00
starrain bb82b3a5b0 fix(elevate): fix panic on NixOS (#1634) 2025-12-01 01:12:08 +08:00
Mg Pig 70b122fb91 feat(gui): macOS UX Improvements (#1631) 2025-12-01 01:11:36 +08:00
狂男风 67cba2c326 feat(mobile): Enhance the Magic DNS support via VpnService on Android (#1617)
* Add DNS route if accept_dns is enabled
* Update doStartVpn to accept optional DNS parameter
2025-11-27 16:53:40 +08:00
sky96111 b86692d009 fix(android): use network-assigned DNS when no DNS is provided (#1612) 2025-11-26 18:24:05 +08:00
狂男风 28e645a277 Add IPv6 address to VPN service (#1615) 2025-11-26 17:15:19 +08:00
Mg Pig 1f2517c731 feat(gui): add service and remote mode support (#1578)
This PR fundamentally restructures the EasyTier GUI, introducing support for service mode and remote mode, transforming it from a simple desktop application into a powerful network management terminal. This change allows users to persistently run the EasyTier core as a background service or remotely manage multiple EasyTier instances, greatly improving deployment flexibility and manageability.
2025-11-25 13:59:27 +08:00
Sijie.Sun b44053f496 support p2p-only mode (#1598) 2025-11-20 08:20:27 +08:00
Sijie.Sun 5b9ac65477 update readme (#1594) 2025-11-17 16:00:32 +08:00
Mg Pig d726d46a00 fix: Preserve disable_sym_hole_punching setting on edit (#1589) 2025-11-15 18:57:59 +08:00
Mg Pig 1273426009 feat: Enable core to use local config files while being managed via the web (#1540) 2025-11-08 20:32:00 +08:00
Sijie.Sun b50744690e easytier-web and uptime use mimalloc as allocator (#1559) 2025-11-08 11:07:33 +08:00
Tunglies 55b93454dc fix: clippy errors with stable toolchain and default features (#1553)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 20:08:39 +08:00
Mg Pig 89cc75f674 refactor: replace ConfigSource with bool parameter (#1516) 2025-11-04 13:48:10 +08:00
Mg Pig 6bb2fd9a15 feat(core): Refactor IDN and URL handling logic (#1533)
* feat(core): Refactor IDN and URL handling logic

* feat(tests): add dual_convert option for URL serialization in IDN tests
2025-11-03 22:15:40 +08:00
Mg Pig 8ab98bba8f feat(ui): make port forward config responsive (#1530) 2025-10-31 23:23:36 +08:00
韩嘉乐 26d002bc2b The flowback solution of HarmonyOS 5 failed due to the anti-loop mechanism. (#1514) 2025-10-25 00:17:24 +08:00
Sijie.Sun 71679e889a allow sync conn with conn list when conn bitmap is too large (#1508) 2025-10-23 08:11:36 +08:00
Sijie.Sun 7485f5f64e make sure event is triggered when peer conn remove (#1507) 2025-10-22 23:37:19 +08:00
Mg Pig bbe8f9f810 feat(ui): Display network names and optimize list loading (#1503) 2025-10-22 13:40:36 +08:00
Mg Pig eba9504fc2 refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component (#1489)
* refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component
* feat(gui): Add network config saving and refactor RemoteManagement
2025-10-20 22:07:01 +08:00
kuaifan 67ac9b00ff feat(gui): Optimize the data table column header style to prevent line breaks (#1497) 2025-10-19 16:50:17 +08:00
Sijie.Sun 3ffa6214ca fix subnet proxy deadloop (#1492)
* use LPM to determine subnet proxy dst.
* never allow subnet proxy traffic sending to self.
2025-10-19 15:46:51 +08:00
Mg Pig 6f278ab167 chore: update flake configuration (#1490) 2025-10-19 00:25:40 +08:00
Sijie.Sun f10b45a67c [easytier-uptime] support tag in node list (#1487) 2025-10-18 23:19:53 +08:00
Sijie.Sun cc8f35787e release dashmap memory (#1485) 2025-10-18 12:48:04 +08:00
Sijie.Sun 8f1786fa23 replace tachyonix with tokio mpsc in MpscTunnel (#1483)
tachyonix cannot correctly wakeup senders when the receiver is closed
and causing tasks deadlock and memory leak.
2025-10-17 00:09:13 +08:00
编程小白 70dddeace3 Fix support for Chinese domain names (#1462) 2025-10-15 21:00:05 +08:00
Mg Pig 8cc9da9d6d fix(web): fix generate and parse config methods broken in #1465 (#1476) 2025-10-14 15:13:20 +08:00
Luna Yao 5292b87275 Add quic-listen-port flag for customization of the port used by QUIC proxy (#1473) 2025-10-14 09:43:50 +08:00
Mg Pig 87b7b7ed7c refactor(web): Refactor web logic to extract reusable remote client management module (#1465) 2025-10-13 23:59:46 +08:00
imdingtalk 999a486928 Improve update in installation script, decrease downtime(#1422) 2025-10-13 23:52:37 +08:00
TaurusXin 627e989faa feat: show NAT type of all nodes in GUI (#1464) 2025-10-13 11:40:57 +08:00
Mg Pig af95312949 fix(acl): acl group cache add self group info (#1445) 2025-10-07 23:56:26 +08:00
Mg Pig a452c34390 fix(ohrs): update collect_network_infos to use synchronous method (#1444) 2025-10-04 23:12:38 +08:00
Mg Pig 4d5330fa0a refactor: get_running_info fn replace status polling with direct calls (#1441) 2025-10-04 21:43:34 +08:00
agusti moll 5e48626cb9 add tld-dns-zone for customizing top-level domain (TLD) zone (#1436) 2025-10-04 00:18:10 +08:00
阿瓦 ad7dc3a129 use plist as macos service management config generator (#1439) 2025-10-04 00:14:45 +08:00
niuhuan 92fab5aafa feat(ohos) build har package (#1440)
Co-authored-by: niuhuan <20847533+niuhuan@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-02 22:29:49 +08:00
Mg Pig 841d525913 refactor(rpc): Centralize RPC service and unify API (#1427)
This change introduces a major refactoring of the RPC service layer to improve modularity, unify the API, and simplify the overall architecture.

Key changes:
- Replaced per-network-instance RPC services with a single global RPC server, reducing resource usage and simplifying management.
- All clients (CLI, Web UI, etc.) now interact with EasyTier core through a unified RPC entrypoint, enabling consistent authentication and control.
- RPC implementation logic has been moved to `easytier/src/rpc_service/` and organized by functionality (e.g., `instance_manage.rs`, `peer_manage.rs`, `config.rs`) for better maintainability.
- Standardized Protobuf API definitions under `easytier/src/proto/` with an `api_` prefix (e.g., `cli.proto` → `api_instance.proto`) to provide a consistent interface.
- CLI commands now require explicit `--instance-id` or `--instance-name` when multiple network instances are running; the parameter is optional when only one instance exists.

BREAKING CHANGE:  
RPC portal configuration (`rpc_portal` and `rpc_portal_whitelist`) has been removed from per-instance configs and the Web UI. The RPC listen address must now be specified globally via the `--rpc-portal` command-line flag or the `ET_RPC_PORTAL` environment variable, as there is only one RPC service for the entire application.
2025-10-02 20:30:39 +08:00
Luna Yao d2efbbef04 refactor: change magicdns to internal redirect (#1428)
To resolve issue #1419, DNS request packets are read directly and responses are sent back internally instead of being forwarded to the listening port.

The DNS service on fake_ip (100.100.100.101) no longer supports DNS-over-TCP.

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-10-02 20:19:12 +08:00
Sijie.Sun 971ef82679 fix data not encrypted when no tun is enabled (#1435) 2025-10-01 11:16:24 +08:00
Mg Pig 020bf04ec4 refactor(config): unify runtime configuration management via ConfigRpc (#1397)
* refactor(config): unify runtime configuration management via ConfigRpc
* feat(tests): add config patch test and fix problem
2025-10-01 00:32:28 +08:00
韩嘉乐 4d91582fd8 Update ohos-rs (#1434) 2025-09-30 23:51:58 +08:00
Sijie.Sun e9b4dbce6e use cargo ndk in jni build script (#1424) 2025-09-28 23:18:51 +08:00
R0S 00fd02c739 正确的hostname 2025-09-26 21:17:14 +08:00
sijie.sun c0d2045e52 bump version to v2.4.5 2025-09-26 00:48:10 +08:00
ThermalEng 835cd407bf Update hotspot_iprule.sh, Support subnet forward for usb shared network (#1411) 2025-09-25 16:25:53 +08:00
Sijie.Sun f5ba5bb146 show traffic stats chart in web/gui (#1410) 2025-09-25 13:43:11 +08:00
Sijie.Sun 7a694257d9 add test for ipv6 wireguard vpn portal (#1408) 2025-09-25 08:24:56 +08:00
Sijie.Sun 67abf4446d fix socks5 panic (#1409) 2025-09-25 08:24:50 +08:00
Sijie.Sun 7035a3fef4 fix firewall rule not specify interface (#1407) 2025-09-25 00:11:26 +08:00
Sijie.Sun 4445916ba7 fix open log dir not work on gui (#1403) 2025-09-21 23:17:31 +08:00
Sijie.Sun a102a8bfc7 fix macos bind failed when addr is v6 (#1398) 2025-09-21 21:47:03 +08:00
Sijie.Sun c9e8c35e77 fix log dir not work; fix stun config from file not work; (#1393) 2025-09-20 00:20:08 +08:00
Sijie.Sun 1a1be8138a bump version to v2.4.4 (#1386) 2025-09-18 19:49:10 +08:00
Sijie.Sun e06e8a9e8a allow enable log with cli, limit log size (#1384)
* impl logger rpc
* use size based appender
* add log args
2025-09-18 16:35:12 +08:00
Sijie.Sun 56fd6e4ab6 fix wireguard listener (#1382)
* listen both v4 and v6 for wireguard portal
* fix panic when getting udp local addr

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-17 23:45:05 +08:00
Sijie.Sun 215db09925 avoid packets sending to non-exist peer causing route loop (#1378) 2025-09-17 09:52:53 +08:00
fanyang 9fff5e4fec Add config validation flag (#1376)
Add `--check-config` CLI option to validate configuration without
starting network
2025-09-16 22:58:07 +08:00
Sijie.Sun 802d3f78d7 distinct v6 and v4 tunnel in gui and cli (#1373) 2025-09-16 07:24:31 +08:00
Sijie.Sun 3593035eb9 fix networksetup timeout on some machine (#1372) 2025-09-15 23:33:43 +08:00
Sijie.Sun 757d76c9da fix stun server list empty when config is from web (#1371) 2025-09-15 22:52:58 +08:00
fanyang 445e68ddd1 Read config from stdin (#1354) 2025-09-13 21:21:30 +08:00
Sijie.Sun b540ec3f46 improve uptime (#1365) 2025-09-13 19:14:13 +08:00
Sijie.Sun 5c90431876 fix smoltcp attempt to subtract sequence numbers with underflow (#1360) 2025-09-13 15:03:04 +08:00
Sijie.Sun 793889c3b7 fix ospf ipv4 map error when ipv4 conflicted and changed (#1359) 2025-09-13 08:48:50 +08:00
Sijie.Sun eb42086f9c set correct route policy for udp/icmp (#1361) 2025-09-13 08:48:37 +08:00
Sijie.Sun d0efc40efb fix foreign network direct conn with mapped listeners (#1363) 2025-09-13 08:48:12 +08:00
fanyang ae704d1d5f Fix jemalloc warning on macOS (#1344)
fix:
```
-> % easytier-core
<jemalloc>: option background_thread currently supports pthread only
```

Reference: https://github.com/apache/arrow/pull/5729
2025-09-08 21:53:40 +08:00
fanyang 525dfd9fc1 cli: improve peer table display with shorter columns for small display (#1342)
- Add short column names for latency, loss rate, rx/tx bytes, tunnel protocol and NAT type
- Format loss rate as percentage with one decimal place
- Change table style from modern to markdown for better readability
2025-09-08 21:52:53 +08:00
Sijie.Sun 18bd178bbd update readme to fix script installation command (#1341) 2025-09-06 17:02:31 +08:00
fanyang 088155f6f3 core: hide default STUN servers from cli (#1334) 2025-09-06 15:53:34 +08:00
Sijie.Sun b750faa66f add android jni (#1340) 2025-09-06 13:49:42 +08:00
Sijie.Sun ef3309814d fix cli add port forward failed if initial forward list is empty (#1324) 2025-09-02 22:03:57 +08:00
fanyang b87a05b457 refactor: update custom STUN server settings (#1310)
* refactor: update global context STUN server initialization

Modified global context initialization to use a single StunInfoCollector
instance with properly configured IPv4 and IPv6 servers instead of
creating separate instances.

feat: add IPv6 STUN server configuration support

Added interface methods and config struct fields to support both IPv4
and IPv6 STUN server configuration. Modified getter and setter methods
to handle Option<Vec<String>> type for both server types.

feat: enhance StunInfoCollector with IPv6 support

Updated StunInfoCollector to support both IPv4 and IPv6 STUN servers.
Added new constructor that accepts both server types and methods to set
them independently.

feat: add CLI argument for IPv6 STUN servers

Added command line argument support for configuring IPv6 STUN servers.
Updated configuration setup to handle both IPv4 and IPv6 STUN server
settings.

docs: add localization for STUN server configuration

Added English and Chinese localization strings for the new STUN server
configuration options, including both IPv4 and IPv6 variants.
2025-09-02 21:46:37 +08:00
Joel Stodolski 754439f03c feat(gui): add macOS dock icon visibility control (#1328) 2025-09-02 17:32:18 +08:00
Sijie.Sun 2145ef40b9 fix ospf route panic (#1304) 2025-08-27 13:22:29 +08:00
Sijie.Sun a3806e0190 fix set ipv6 mtu may cause tun init error (#1300) 2025-08-27 09:57:32 +08:00
Sijie.Sun 0ceb58586b fix keepalive on accepted tcp proxy connection (#1302) 2025-08-26 23:30:30 +08:00
Sijie.Sun 719a1fe7cf bump version to 2.4.3 (#1296) 2025-08-26 12:22:08 +08:00
Sijie.Sun 671b8d5a0c fix quic transport (#1293) 2025-08-26 08:37:31 +08:00
fanyang e29206aef9 tray: place the exit menu item at bottom (#1291) 2025-08-25 12:47:43 +08:00
Sijie.Sun 3299a77da3 make magic dns domain check robust (#1288) 2025-08-24 18:24:42 +08:00
Sijie.Sun 0804fd6632 retry create tun device if it closed (#1279) 2025-08-24 15:25:09 +08:00
Sijie.Sun ea76114d50 fix kcp not work as expect (#1285) 2025-08-24 14:33:11 +08:00
Mg Pig 9304d3b227 feat(nix): refactor Flake and Migrate Android Support (#1280) 2025-08-24 00:53:42 +08:00
fanyang 78004de5e5 gui: sort peer list (#1278) 2025-08-24 00:53:32 +08:00
Sijie.Sun 5b7384fddd disable nat4 hole punch (#1277) 2025-08-22 23:33:21 +08:00
Mg Pig 08a92a53c3 feat(acl): add group-based ACL rules and related structures (#1265)
* feat(acl): add group-based ACL rules and related structures

* refactor(acl): optimize group handling with Arc and improve cache management

* refactor(acl): clippy

* feat(tests): add performance tests for generate_with_proof and verify methods

* feat: update group_trust_map to use HashMap for more secure group proofs

* refactor: refactor the logic of the trusted group getting and setting

* feat(acl): support kcp/quic use group acl

* feat(proxy): optimize group retrieval by IP in Kcp and Quic proxy handlers

* feat(tests): add group-based ACL tree node test

* always allow quic proxy traffic

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-08-22 22:25:00 +08:00
fanyang 34560af141 cli: put the local IP at the front (#1256) 2025-08-22 20:40:28 +08:00
Sijie.Sun 2e7e0088dd Revise QQ group details in README_CN.md (#1274) 2025-08-22 12:52:01 +08:00
Sijie.Sun d23366ea84 Update QQ Group contact information in README (#1275) 2025-08-22 12:51:50 +08:00
fanyang df7eb47593 Support tokio-console (#1259) 2025-08-21 11:41:42 +08:00
Sijie.Sun 839a28a3d5 avoid panic on smoltcp socket accept (#1272) 2025-08-21 09:30:51 +08:00
Sijie.Sun 9c6d1dabdf fix dead lock in tokio smoltcp (#1270) 2025-08-21 00:16:11 +08:00
Sijie.Sun e6ec7f405c introduce uptime monitor for easytier public nodes (#1250) 2025-08-20 22:59:44 +08:00
ThermalEng 8f37d4ef7c 增加magisk模块功能:热点局域网转发。 (#1252)
* 增加magisk模块功能:热点局域网转发。该功能由后台监控,热点打开,可自动增加转发规则。在三星fold3测试通过。

* 增加了默认tun名称的识别

1.防止配置文件没有配置dev_name的情况,按默认名称tun+在网络设备中查找;
2.一旦热点关闭,自动删除规则。

* 考虑到主程序已可通过模块开关来重新加载,将操作按钮用于转发开关。此外对状态栏信息进行了一些修饰,加入了转发状态的显示。
2025-08-20 20:49:25 +08:00
TigerBeanst c37af8c1be feat(easytier-magisk): add support for custom command args. (#1236) 2025-08-19 00:04:54 +08:00
Glavo 489661a2ce Fix #1255: Using mimalloc for Linux LoongArch64 (#1257) 2025-08-19 00:04:11 +08:00
Sijie.Sun fa3e208668 fix panic of std::Instant overflow (#1243) 2025-08-15 22:54:58 +08:00
21paradox 4d240efde9 add a android flake.nix for local development/test/build (#1237) 2025-08-15 16:59:11 +08:00
Sijie.Sun d9bcbd9b31 fix proxy traffic not count into traffic (#1229)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-14 00:05:12 +08:00
fanyang 35ff9b82fc Support custom STUN servers configuration (#1212)
* Support custom STUN servers

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 10:35:59 +08:00
Sijie.Sun a511abb613 fix docker file (#1219) 2025-08-11 18:09:16 +08:00
Sijie.Sun 1eec27b5ff bump version to 2.4.2 (#1218) 2025-08-11 09:03:13 +08:00
Sijie.Sun 1de7777a71 fix quic transport panic (#1216) 2025-08-11 08:30:59 +08:00
Sijie.Sun 975ca8bd9c Update docker workflow (#1217)
1. push all supported platform
2. support unstable tag
2025-08-10 23:36:50 +08:00
Sijie.Sun e43537939a clippy all codes (#1214)
1. clippy code
2. add fmt and clippy check in ci
2025-08-10 22:56:41 +08:00
CyiceK 0087ac3ffc feat(encrypt): Add XOR and ChaCha20 encryption with low-end device optimization and openssl support. (#1186)
Add ChaCha20 XOR algorithm, extend AES-GCM-256 capabilities, and integrate OpenSSL support.

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-08-09 18:53:55 +08:00
21paradox 7de4b33dd1 add FOREGROUND_SERVICE for no_tun mode, not using vpn service (#1203)
1. add FOREGROUND_SERVICE related code, connection not to be **blocked by android system** when apps running in background
2. no_tun mode not enabling vpnservice, makeing other app to use vpnservice

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-09 18:34:45 +08:00
Sijie.Sun 8ffc2f12e4 optimize the condition of enabling kcp (#1210) 2025-08-09 16:16:09 +08:00
FuturePrayer 37b24164b6 add portforward config to gui (#1198)
* Added port forwarding to the GUI interface
* Separated port forwarding into a separate drop-down menu
2025-08-09 09:50:09 +08:00
Sijie.Sun 8cdb27d43d add stats metrics (#1207)
support new cli command `easytier-cli stats`

It's useful to find out which components are consuming bandwidth.
2025-08-09 00:06:35 +08:00
Sijie.Sun efa17a7c10 fix dead loop in direct connecto if disable-p2p is enabled in dst (#1206) 2025-08-08 22:30:39 +08:00
Sijie.Sun 6d14e9e441 fix jemalloc prof feature (#1201) 2025-08-08 17:54:39 +08:00
fanyang e3e406dcde cli: sort peers by IPv4 and hostname (#1191)
* cli: sort entries by IPv4 and hostname

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-04 21:18:49 +08:00
sijie.sun d0a6c93c2c fix ipv6 packet routing and avoid route looping
properly handle ipv6 link local address and exit node.
2025-08-03 18:10:27 +08:00
sijie.sun 84bfac144c bump version to 2.4.1 2025-08-02 10:48:17 +08:00
Sijie.Sun 9eddb4b072 fix readme assets (#1182) 2025-08-01 23:58:01 +08:00
Sijie.Sun 4fca0f40fe update readme (#1181) 2025-08-01 23:52:27 +08:00
Sijie.Sun 43b9e6e6e9 fix macos elevate (#1177) 2025-08-01 09:36:10 +08:00
Sijie.Sun 583c768f40 fix exit code when error occcurs (#1173) 2025-07-30 23:05:22 +08:00
Tunglies b1b2421561 fix: compiling with socket2::Type::RAW not found on macOS #1168 (#1169) 2025-07-30 00:33:38 +08:00
Sijie.Sun 3d610c0f0f Some Improvements (#1172)
1. do not exit when dns query failed on et startup.
2. do not send secret digest to client when secret mismatch.
2025-07-29 23:05:38 +08:00
Sijie.Sun 2ec88da823 cli for port forward and tcp whitelist (#1165) 2025-07-29 09:30:47 +08:00
Mg Pig 5514de1187 chore: update flake configuration (#1163) 2025-07-29 00:26:05 +08:00
Glavo e70eed74e2 Add support for Linux RISC-V 64 (#1159) 2025-07-27 22:07:07 +08:00
Sijie.Sun 7dc5988620 avoid udp hole punch go through tun (#1155) 2025-07-26 14:39:03 +08:00
Sijie.Sun 354a4e1d7b fix acl not work with kcp&quic (#1152) 2025-07-26 14:38:10 +08:00
Sijie.Sun 5409c5bbe7 port range should not be converted to single port (#1154) 2025-07-26 14:13:13 +08:00
Sijie.Sun 33ff9554cd need encrypt rpc if dst is in peer map (#1151) 2025-07-25 22:28:47 +08:00
Sijie.Sun 975b4e7664 support loongarch (#1146) 2025-07-25 01:53:49 +08:00
Sijie.Sun 1f6a715939 releases/v2.4.0 (#1145)
* bump version to v2.4.0
* update tauri.
* allow try direct connect to public server
2025-07-25 00:16:15 +08:00
Sijie.Sun 8e7a8de5e5 Implement ACL (#1140)
1. get acl stats
```
./easytier-cli acl stats
AclStats:
  Global:
    CacheHits: 4
    CacheMaxSize: 10000
    CacheSize: 5
    DefaultAllows: 3
    InboundPacketsAllowed: 2
    InboundPacketsTotal: 2
    OutboundPacketsAllowed: 7
    OutboundPacketsTotal: 7
    PacketsAllowed: 9
    PacketsTotal: 9
    RuleMatches: 2
  ConnTrack:
    [src: 10.14.11.1:57444, dst: 10.14.11.2:1000, proto: Tcp, state: New, pkts: 1, bytes: 60, created: 2025-07-24 10:13:39 +08:00, last_seen: 2025-07-24 10:13:39 +08:00]
  Rules:
    [name: 'tcp_whitelist', prio: 1000, action: Allow, enabled: true, proto: Tcp, ports: ["1000"], src_ports: [], src_ips: [], dst_ips: [], stateful: true, rate: 0, burst: 0] [pkts: 2, bytes: 120]

  ```
2. use tcp/udp whitelist to block unexpected traffic.
   `sudo ./easytier-core -d --tcp-whitelist 1000`

3. use complete acl ability with config file:

```
[[acl.acl_v1.chains]]
name = "inbound_whitelist"
chain_type = 1
description = "Auto-generated inbound whitelist from CLI"
enabled = true
default_action = 2

[[acl.acl_v1.chains.rules]]
name = "tcp_whitelist"
description = "Auto-generated TCP whitelist rule"
priority = 1000
enabled = true
protocol = 1
ports = ["1000"]
source_ips = []
destination_ips = []
source_ports = []
action = 1
rate_limit = 0
burst_limit = 0
stateful = true

```
2025-07-24 22:13:45 +08:00
Sijie.Sun 4f53fccd25 fix bugs (#1138)
1. avoid dns query hangs the thread
2. avoid deadloop when stun query failed because of no ipv4 addr.
3. make quic input error non-fatal.
4. remove ring tunnel from connection map to avoid mem leak.
5. limit listener retry count.
2025-07-21 23:18:38 +08:00
Sijie.Sun 876d550f68 reduce memory usage (#1133)
Large memory usage comes from:

Mimalloc hold large thread cache, causing abort 13M+ usage.
QUIC endpoint occupy 3M when GRO is enabled.
Smoltcp 64 tcp listener use 2MB.
2025-07-20 19:15:28 +08:00
Sijie.Sun 2660ed5fda try create tun device if not exist (#1131) 2025-07-19 22:56:19 +08:00
Sijie.Sun 50c6f5ae6c add windows firewall for tun interface (#1130)
allow all icmp/tcp/udp on tun interface.
2025-07-19 20:38:44 +08:00
Sijie.Sun 85f0091056 fix latency first route of public server (#1129) 2025-07-19 18:16:53 +08:00
Sijie.Sun e25cd9be37 add disable ipv6 option to gui/web (#1127) 2025-07-19 11:07:57 +08:00
Sijie.Sun 1fb5ca9475 update issue template (#1126) 2025-07-18 23:50:02 +08:00
Sijie.Sun 7f3a9c021c close peer conn if remote addr is from virtual network (#1123) 2025-07-18 03:29:48 +08:00
liusen373 0427b48d75 Allows to modify Easytier's mapped listener at runtime via RPC (#1107)
* Add proto definition
* Implement and register the corresponding rpc service
* Parse command line parameters and call remote rpc service

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-07-17 20:37:05 +08:00
Jiangqiu Shen 0b729b99e7 add options to generate completions (#1103)
* add options to generate completions

use clap-complete crate to generate completions scripts: easytier-core --generate fish > ~/.config/fish/completions/easytier-core.fish

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-07-17 20:35:49 +08:00
Sijie.Sun 940238f158 socks5 and port forwarding (#1118) 2025-07-17 10:09:25 +08:00
Sijie.Sun 3f6c7ba1d2 update readme (#1102) 2025-07-10 00:34:34 +08:00
lazebird 0025973453 fix: cannot start gui on linux (#1090) 2025-07-07 22:59:11 +08:00
Rene Leonhardt c3a217c9d2 chore(ci): update GitHub Actions (#1088)
* chore(ci): update GitHub Actions
* update gradle-wrapper and revert UPX
* exclude cargo from dependabot and remove empty .gitmodules
2025-07-07 22:55:30 +08:00
Sijie.Sun 13c2e72871 fix incorrect config check (#1086) 2025-07-06 14:20:49 +08:00
Sijie.Sun 3c65594030 smoltcp use larger tx/rx buf size (#1085)
* smoltcp use larger tx/rx buf size
* fix direct conn check
2025-07-06 10:53:01 +08:00
Sijie.Sun f85b031402 handle close peer conn correctly (#1082) 2025-07-06 09:16:13 +08:00
Sijie.Sun ac3e994682 contributing.md (#1084) 2025-07-06 00:08:21 +08:00
Sijie.Sun 139f6b3c4c exclude ohos from workspace (#1080) 2025-07-05 18:44:37 +08:00
Sijie.Sun a4bb555fac use winapi to config ip and route (remove dep on netsh) (#1079)
On some windows machines can not execut netsh.
Also this avoid black cmd window when using gui.
2025-07-05 16:50:09 +08:00
DavHau d0cfc49806 Add support for IPv6 within VPN (#1061)
* add flake.nix with nix based dev shell
* add support for IPv6
* update thunk

---------

Co-authored-by: sijie.sun <sijie.sun@smartx.com>
2025-07-04 23:43:30 +08:00
韩嘉乐 01e491ec07 support ohos (#974)
* support ohos

---------

Co-authored-by: FrankHan <2777926911@qq.com>
2025-07-02 09:44:45 +08:00
Sijie.Sun bf021a9ead update gui placeholder text (#1062) 2025-06-27 08:29:44 +08:00
Sijie.Sun 70e69a382e allow set multithread count (#1056) 2025-06-26 02:19:33 +08:00
Sijie.Sun cd26d9f669 fix mem leak of token bucket (#1055) 2025-06-26 02:19:26 +08:00
Sijie.Sun 4fd0253e99 fix cargo install failure (#1054) 2025-06-25 21:55:44 +08:00
Sijie.Sun ebab70ca3b add geo info for in web device list (#1052) 2025-06-25 09:03:47 +08:00
Sijie.Sun ae4a158e36 web improve (#1047) 2025-06-24 09:09:52 +08:00
Mg Pig 760a1e6306 fix rpc_portal_whitelist from config file not working (#1042) 2025-06-23 00:50:41 +08:00
Sijie.Sun fded8b1de0 limit max conn count in foreign network manager (#1041) 2025-06-22 19:11:27 +08:00
Sijie.Sun 762d5cd392 blacklist the peers which disable p2p in hole-punching client (#1038) 2025-06-22 14:39:24 +08:00
dawn-lc 09ac79b9f3 fix uninstall.cmd (#1036) 2025-06-22 12:06:16 +08:00
dawn-lc 16f6fb0c59 add Windows Service install script 2025-06-21 15:57:55 +08:00
xzzpig 385e790600 simplify Textarea class in ConfigGenerator.vue 2025-06-21 14:56:40 +08:00
liusen373 95e4e5a931 Implement custom fmt::Debug for some prost_build generated structs
Currently implemented for:
1. common.Ipv4Addr
2. common.Ipv6Addr
3. common.UUID
2025-06-21 14:56:28 +08:00
sijie.sun e1bfec6fe2 add api_meta.js to frontend public 2025-06-19 23:40:57 +08:00
sijie.sun dde7a4dff1 bps limit should throttle kcp packet 2025-06-19 22:53:41 +08:00
Sijie.Sun 40601bd05b add bps limiter (#1015)
* add token bucket
* remove quinn-proto
2025-06-19 21:15:04 +08:00
chenxudong2020 72d5ed908e quic uses the bbr congestion control algorithm (#1010) 2025-06-18 23:17:52 +08:00
liusen373 72673a9d52 Add is_hole_punched flag to PeerConn (#1001) 2025-06-18 12:14:57 +08:00
tianxiayu007 327ccdcf38 installing by homebrew should use easytier-gui (#1004) 2025-06-18 11:06:26 +08:00
Sijie.Sun 8c2f96d1aa allow set machine uid with command line (#1009) 2025-06-18 11:02:29 +08:00
Sijie.Sun 34ba0bc95b add keepalive option for quic proxy (#1008)
avoid connection loss when idle
2025-06-17 23:39:56 +08:00
Mg Pig ed162c2e66 Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990)
* add method to create NetworkConfig from TomlConfigLoader
* allow web export/import toml config file and gui edit toml config
* Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web
2025-06-15 23:41:42 +08:00
Sijie.Sun 40b5fe9a54 support quic proxy (#993)
QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC.
QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. 
QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`.
2025-06-15 19:43:45 +08:00
Sijie.Sun 5a98fac395 Update core.yml,use upx4.2.4 (#991) 2025-06-14 23:04:55 +08:00
Sijie.Sun 0bab14cd72 use bulk compress instead of streaming to reduce mem usage (#985) 2025-06-14 14:55:48 +08:00
Mg Pig b407cfd9d4 Fixed the issue where the GUI would panic after using InstanceManager (#982)
Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-06-14 13:06:53 +08:00
Sijie.Sun 25dcdc652a support mapping subnet proxy (#978)
- **support mapping subproxy network cidr**
- **add command line option for proxy network mapping**
- **fix Instance leak in tests.
2025-06-14 11:42:45 +08:00
Sijie.Sun 950cb04534 remove macos default route on utun device (#976) 2025-06-12 22:24:34 +08:00
Sijie.Sun c07d1286ef internal stun server should use xor mapped addr (#975) 2025-06-12 08:09:59 +08:00
Mg Pig 8ddd153022 easytier-core支持多配置文件 (#964)
* 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中

* easytier-core支持多配置文件

* FFI复用instance manager

* 添加instance manager 单元测试
2025-06-11 23:17:09 +08:00
Sijie.Sun 870353c499 fix ospf route (#970)
- **fix deadlock in ospf route introducd by #958 **
- **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual**
- **allow multiple nodes broadcast same network ranges for subnet proxy**
- **bump version to v2.3.2**
2025-06-11 09:44:03 +08:00
BlackLuny ecebbecd3b add check for rpc packet fix #963 (#969) 2025-06-09 19:35:29 +08:00
Sijie.Sun f39fbb2ce2 ipv4-peerid table should use peer with least hop (#958)
sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list.
when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes.
2025-06-08 11:28:59 +08:00
Kiva ec56c0bc45 feat: allow using --proxy-forward-by-system together with --enable-exit-node (#957) 2025-06-07 22:27:57 +08:00
Mg Pig 20a6025075 Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) 2025-06-07 22:05:47 +08:00
BlackLuny 707963c0d9 Web dual stack (#953)
* reimplement easytier-web dual stack
* add protocol check for dual stack listener current only support tcp and udp
2025-06-07 22:05:11 +08:00
Kiva 3c7837692e fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) 2025-06-07 21:19:03 +08:00
Sijie.Sun f890812577 kcp connect retry (#952) 2025-06-07 12:24:11 +08:00
Sijie.Sun 47f3efe71b Create LICENSE (#951) 2025-06-07 10:56:54 +08:00
Sijie.Sun 6d88b10b14 remove LICENSE (#950) 2025-06-07 10:39:42 +08:00
Zisu Zhang d34a51739f Update default_port and sni logic to improve reverse proxy reachability (#947) 2025-06-07 08:19:31 +08:00
Sijie.Sun a6773aa549 zstd should reuse ctx to avoid huge mmap cost (#941) 2025-06-06 08:59:06 +08:00
Sijie.Sun 0314c66635 some improvements (#939)
1. ospf route conn map should also use version
2. treat nopat as cone
2025-06-05 22:49:57 +08:00
chenxudong2020 3fb172b4d2 Modify SNI logic: always use "localhost" as SNI to avoid IP blocking (#934) 2025-06-05 11:56:07 +08:00
Sijie.Sun 96fc19b803 fix minor bugs (#936)
1. update upx to v5.0.1 to avoid mips bug.
2. use latest mimalloc.
3. fix panic in ospf route
4. potential residual conn.
2025-06-05 11:55:44 +08:00
Wang Zeng 9f7ba8ab8f fix(easytier-gui): restore window correctly when clicking tray icon while minimized (#930)
以前在最小化窗口时单击托盘图标会错误地切换任务栏图标的可见性。

这一改变实现了预期的行为:
- 当窗口最小化时:单击托盘将窗口恢复到原始状态
- 当窗口可见时:托盘单击最小化到托盘
- 当窗口隐藏时:单击托盘恢复窗口

该修复通过提供标准的托盘交互行为来增强用户体验。包括必要的事件处理窗口状态转换。
2025-06-04 16:33:06 +08:00
Mg Pig e592e9f29a 节点信息组件添加隧道协议字段 (#931) 2025-06-04 09:22:58 +08:00
Sijie.Sun 4608bca998 improve performance of route generation (#914)
this may fix following problem:

1. cpu 100% when large number of nodes in network.
2. high cpu usage when large number of foreign networks.
3. packet loss when new node enters/exits.
4. old routes not cleand and show as an obloleted entry.
2025-06-02 20:12:27 +08:00
FuturePrayer b5dfc7374c add private mode (#897)
---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-06-02 06:47:17 +08:00
Mg Pig b469f8197a Supports customizing the API server address of the Web frontend through the --api-host parameter (#913) 2025-06-02 06:46:12 +08:00
Sijie.Sun 0a38a8ef4a fix musl download fail in ci action (#902)
https://github.com/orgs/community/discussions/27906

musl.cc banned microsoft ips

this patch replace musl.cc with https://github.com/cross-tools/musl-cross
2025-05-29 09:35:32 +08:00
Mg Pig e75be7801f easytier-web add websocket support (#901)
Co-authored-by: xzzpig <w2xzzig@hotmail.com>
2025-05-28 21:29:21 +08:00
Sijie.Sun 6c49bb1865 rename magisk kill.sh to action.sh (#893) 2025-05-27 09:32:40 +08:00
Sijie.Sun f9c24bc205 fix bugs (#892)
1. traffic stats not work.
2. magisk zip malformat
2025-05-27 09:28:28 +08:00
Mg Pig d7c3179c6e easytier-cli部分命令支持json输出 (#882)
* add cli options to json output
* add cli verbose output in json format for some sub command

- easytier-cli -v peer list
- easytier-cli -v peer list-foreign
- easytier-cli -o json peer list-foreign
- easytier-cli -v peer list-global-foreign
- easytier-cli -o json peer list-global-foreign
- easytier-cli -v route list
- easytier-cli -v connector
- easytier-cli -o json connector
- easytier-cli -o json stun
- easytier-cli -v proxy
- easytier-cli -v node info

---------

Co-authored-by: xzzpig <w2xzzig@hotmail.com>
2025-05-25 23:28:12 +08:00
Sijie.Sun b0fd37949a fix direct connector only select one listener (#875) 2025-05-25 13:56:08 +08:00
Sijie.Sun 29994b663a v6 hole punch (#873)
Some devices have ipv6 but don't allow input connection, this patch add hole punching for these devices.

- **add v6 hole punch msg to udp tunnel**
- **send hole punch packet when do ipv6 direct connect**
2025-05-24 22:57:33 +08:00
lzw-723 fc397c35c5 install script support openrc (#868) 2025-05-24 10:18:23 +08:00
Sijie.Sun 0f2b214918 fix web test (#872) 2025-05-24 01:22:25 +08:00
Sijie.Sun fec885c427 fix token mismatch when using web (#871) 2025-05-24 00:36:00 +08:00
Sijie.Sun 5a2fd4465c fix dns query (#864)
1. dns resolver should be global unique so dns cache can work. avoid dns query influence hole punching.
2. when system dns failed, fallback to hickory dns.
2025-05-23 10:34:28 +08:00
Sijie.Sun 83d1ecc4da bump version to v2.3.0 (#859)
also some improvements:

1. add magic dns option in gui.
2. allow icmp proxy fail on android
3. when no_tun is enabled, android do not start vpn service

Co-authored-by: Your Name <you@example.com>
2025-05-18 16:45:39 +08:00
Sijie.Sun 7c6daf7c56 Magic DNS and easytier-web improvements (#856)
1. dns add macos system config
2. allow easytier-web serve dashboard and api in same port
2025-05-18 16:34:35 +08:00
Sijie.Sun 28fe6257be magic dns (#813)
This patch implements:

1. A dns server that handles .et.net. zone in local and forward all other queries to system dns server.

2. A dns server instance which is a singleton in one machine, using one specific tcp port to be exclusive with each other. this instance is responsible for config system dns and run the dns server to handle dns queries.

3. A dns client instance that all easytier instance will run one, this instance will try to connect to dns server instance, and update the dns record in the dns server instance.

this pr only implements the system config for windows. linux & mac will do later.
2025-05-16 09:24:24 +08:00
Sijie.Sun 99430983bc Update README.md (#846)
Add deepwiki badge
2025-05-12 21:39:55 +08:00
Sijie.Sun d758a4958f fix panic cause segment fault (#843)
1. backtrace may fail on some platform such as armv7, should do it last in panic hook.
2. stun should not panic when bind v6 failed.
2025-05-11 21:34:24 +08:00
sijie.sun 95b12dda5a bump rust version to v1.86 2025-05-11 20:47:29 +08:00
Sijie.Sun 2675cf2d00 bump hickory-dns version to v0.25.2 (#839) 2025-05-11 08:46:31 +08:00
Sijie.Sun 72be46e8fa allow tcp port forward use kcp (#838) 2025-05-11 00:48:34 +08:00
loecom c5580feb64 add thunk-rs to support win7 (#812)
* add thunk-rs to support win7
---------

Co-authored-by: loecomm <loecom@qq.com>
2025-04-25 22:27:36 +08:00
伤月s 7e3819be86 新增magisk模块支持 (#786) 2025-04-24 12:21:05 +08:00
Char f0302f2be7 add default RPC portal (#803)
* add default RPC portal. auto choose port from 15888
2025-04-23 21:27:46 +08:00
Sijie.Sun b5f60f843d set web assets base dir from env (#793) 2025-04-19 22:56:20 +08:00
Sijie.Sun 6bdfb8b01f generate split js/css when building web (#792) 2025-04-19 22:38:56 +08:00
Sijie.Sun ef1d81a2a1 introduce ffi for easytier (#791) 2025-04-19 21:01:51 +08:00
L-Trump 739b4ee106 fix: avoid add ipv6 listener automatically for specified ipv4 listener (#782) 2025-04-16 21:58:39 +08:00
L-Trump 6a038e8a88 fix default listeners for config file (#777) 2025-04-13 09:38:45 +08:00
Qiao 72ea8a9f76 feat(install): enhance installation script functionality (#770)
* feat(install): enhance installation script functionality

- Add help information display feature
- Support custom GitHub proxy URL
- Add --no-gh-proxy option to disable GitHub proxy
- Optimize error messages and command prompts
- Fix typo (Commend -> Command)

* docs: update installation script documentation

---------

Co-authored-by: evanq <mail.qxw.im>
Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-04-10 18:14:33 +08:00
L-Trump 44d93648ee config from environment variables; CLI args override config file (#755)
* feat: configure through os environment variables
* feat: support CLI args overriding config file options
2025-04-10 18:14:10 +08:00
Sijie.Sun 75f7865769 fix gui memory leak (#768)
* upgrade primevue
* use card instead of panel
2025-04-10 10:02:04 +08:00
Sijie.Sun 01e3ad99ca optimize memory issues (#767)
* optimize memory issues

1. introduce jemalloc support, which can dump current memory usage
2. reduce the GlobalEvent broadcaster memory usage.
3. reduce tcp & udp tunnel memory usage

TODO: if peer conn tunnel hangs, the unbounded channel of peer rpc
may consume lots of memory, which should be improved.

* select a port from 15888+ when port is 0
2025-04-09 23:05:49 +08:00
m1m1sha 3c0d85c9db Merge pull request #750 from EasyTier/perf/fixed-default-rpc-port
perf: update default rpc_port value to 15888 in network configuration
2025-04-06 13:34:35 +08:00
Sijie.Sun b38991a14e Merge branch 'main' into perf/fixed-default-rpc-port 2025-04-06 13:09:06 +08:00
L-Trump 465269566b fix gui build ci (#756) 2025-04-06 11:47:14 +08:00
m1m1sha f103fc13d9 perf: update default rpc_port value to 15888 in network configuration 2025-04-05 10:17:16 +08:00
treasury1203 e5917fad4e docs: added a new tag badge and a link to it (#740) 2025-04-01 21:11:38 +08:00
kevin de8c89eb03 add binary file easytier-web-embed (#718)
* embed web dashboard into easytier-web
* add binary file easytier-web-embed
2025-04-01 10:03:58 +08:00
Sijie.Sun c142db301a port forward (#736)
* support tcp port forward
* support udp port forward
* command line option for port forward
2025-04-01 09:59:53 +08:00
kevin 8dc8c7d9e2 set hostname when connecting to config-server (#712) 2025-03-23 19:53:49 +08:00
kevin 2b909e04ea add ApiHost option for ConfigGenerator (#705) 2025-03-21 22:40:39 +08:00
Sijie.Sun e130c3f2e4 when gather v6 bind addrs should only rely on v6 range (#707) 2025-03-21 22:40:26 +08:00
3RDNature 3ad754879f Update install.sh (#706) 2025-03-21 19:27:37 +08:00
kevin fd2b3768e1 add mtu and mapped_listeners for web (#704) 2025-03-20 23:40:56 +08:00
Sijie.Sun 67cff12c76 fix gui compile (#701) 2025-03-20 00:55:14 +08:00
kevin c5ea7848b3 add disable_udp_hole_punching and hide passwd for web (#700)
* add disable_udp_hole_punching for web
* hide network_secret by default

---------

Co-authored-by: Sijie.Sun <sunsijie@buaa.edu.cn>
2025-03-19 23:57:09 +08:00
严浩 34365a096e fix(web_client): 将报告时间格式从字符串更改为RFC 3339格式 (#698) 2025-03-19 23:00:52 +08:00
Sijie.Sun d880dfbbca bump version to v2.2.4 (#697) 2025-03-19 17:23:15 +08:00
Sijie.Sun b46a200f8d connector should set bind addrs correctly (#696) 2025-03-19 10:47:43 +08:00
kevin 81490d0662 enable sni for tls client (#691)
* enable sni for tls client
* update test case
* fix public_ip parse bug
2025-03-19 01:15:34 +08:00
treasury1203 3d1e841cc5 Merge pull request #687 from treasury1203/patch-1
docs: (contributing)
2025-03-17 22:27:34 +08:00
sijie.sun f52936a103 bump version to v2.2.3 2025-03-17 22:24:19 +08:00
Sijie.Sun 23f69ce6a4 improve direct connector (#685)
* support ipv6 stun
* show interface and public ip in cli node info
* direct conn should keep trying unless already direct connected
* peer should use conn with smallest latency
* deprecate ipv6_listener, use -l instead
2025-03-17 10:46:14 +08:00
sijie.sun f84ae228fc fix some tailwind style not work 2025-03-16 11:45:18 +08:00
kevin 74c716ccaa fix web bugs 2025-03-15 14:52:09 +08:00
sijie.sun 445b02b2ca do not upload to oss 2025-03-15 00:16:12 +08:00
sijie.sun bb17ffa9fc fix wireguard not respond after idle for 120s 2025-03-15 00:16:12 +08:00
sijie.sun 389ea709ce fix smoltcp not wakeup closed socket 2025-03-15 00:16:12 +08:00
kevin c2f535ead4 import/export network config for web (#676)
* import/export network config for web
* add socks5 config for web
2025-03-12 23:19:56 +08:00
Sijie.Sun 0318f55322 add serde default to NetworkConfig (#675)
* add serde default to NetworkConfig

* set base z-index of event-dialog
2025-03-12 10:36:54 +08:00
kevin 1f4340e82f add configurable items for web/gui
enable_exit_node
relay_all_peer_rpc
multi_thread
proxy_forward_by_system
relay_network_whitelist
manual_routes
exit_nodes
2025-03-11 22:30:39 +08:00
loecom ed08707c98 easytier-web add tcp support
easytier-web add tcp support
2025-03-11 12:48:48 +08:00
sijie.sun 7397abcb94 txt connector should not rely on A record 2025-03-09 21:31:43 +08:00
sijie.sun 98d321f8ac fix kcp traffic not encrypted 2025-03-08 22:09:43 +08:00
sijie.sun e78b0ef869 test serializedly 2025-03-08 15:59:54 +08:00
sijie.sun 8d654330ac fix http_connector
1. use ipv4 first when connect to http server.
2. allow redirect to url like: http://tcp://p.com:11010
3. dns should also use long timeout
2025-03-08 15:59:54 +08:00
L-Trump 00d61333d3 allow proxy packets to be forwarded by system kernel 2025-03-08 12:56:49 +08:00
sijie.sun 03b55b61e7 support txt/srv record 2025-03-08 12:56:23 +08:00
sijie.sun 745e44cc87 allow using http connector for config server 2025-03-07 22:17:23 +08:00
sijie.sun 24213a874a make http connector timeout longer
http response may be slow, make its timeout longer.
2025-03-07 22:17:23 +08:00
sijie.sun 155f8a2ba2 make prost build smaller 2025-03-06 11:07:05 +08:00
sijie.sun 568dca6f9c fix memory leak 2025-03-06 11:07:05 +08:00
sijie.sun 673c34cf5a http redirector 2025-02-21 11:51:13 +08:00
sijie.sun 2050ed78d0 remove some dep 2025-02-21 11:51:13 +08:00
zhj9709 2632c44195 fix docker stop issue by using tini for graceful shutdown 2025-02-10 22:54:07 +08:00
Sijie.Sun 5449eabf2a Update docker.yml 2025-02-10 12:47:12 +08:00
sijie.sun dd5b00faf4 bump version to v2.2.2 2025-02-10 08:47:18 +08:00
sijie.sun 0caec3e4da fix label translate 2025-02-09 22:01:26 +08:00
sijie.sun e48e62cac0 fix tcp proxy not close properly 2025-02-09 22:01:09 +08:00
sijie.sun 06ebda2e2f update kcp-sys to fix unexpected disconnect 2025-02-09 00:30:27 +08:00
sijie.sun 53c449b9fb fix net2net kcp proxy 2025-02-08 23:11:10 +08:00
sijie.sun 51e0fac72c improve user experience
1. add config generator to easytier-web
2. add command to show tcp/kcp proxy entries
2025-02-07 23:59:36 +08:00
sijie.sun 32b1fe0893 netlink shoud remove route only when ifidx is same 2025-02-06 19:23:00 +08:00
sijie.sun 2af3b82e32 bump version to 2.2.1 2025-02-06 16:54:49 +08:00
sijie.sun eca1231831 fix help msg of kcp 2025-02-06 16:54:49 +08:00
sijie.sun e833c2a28b improve experience of subnet/kcp proxy
1. add self to windows firewall on windows
2. android always use smoltcp
2025-02-06 16:54:49 +08:00
Sijie.Sun 8b89a037e8 fix tcp incoming failure when kcp proxy is enabled (#601) 2025-02-06 09:08:34 +08:00
Sijie.Sun 1e821a03fe netlink route add should be exclusive (#596) 2025-02-04 23:01:13 +08:00
Sijie.Sun 66051967fe fix self peer route info not exist when starting (#595) 2025-02-04 21:35:14 +08:00
Sijie.Sun a63778854f use netlink instead of shell cmd to config ip (#593) 2025-02-03 15:13:50 +08:00
Sijie.Sun 4aea0821dd forward original peer info in ospf route (#589)
prost doesn't support unknown field, and these info may be lost when
they go through a old version node.
2025-01-27 20:38:22 +08:00
Sijie.Sun 08546925cc fix tests (#588)
fix proxy_three_node_disconnect_test and hole_punching_symmetric_only_random
2025-01-27 15:17:47 +08:00
Sijie.Sun d0f26d9303 bump version to 2.2.0 (#586) 2025-01-26 23:45:50 +08:00
Sijie.Sun 2a5d5ea4df make kcp proxy compitible with old version (#585)
* fix kcp not work with smoltcp
* check if dst kcp input is enabled
2025-01-26 16:22:10 +08:00
Sijie.Sun b69b122c8d add options to gui to enable kcp (#583)
* add test to kcp
* add options to gui to enable kcp
2025-01-26 13:31:20 +08:00
Sijie.Sun 55a39491cb feat/kcp (#580)
* support proxy tcp stream with kcp to improve experience of tcp over udp
* update rust version
* make subnet proxy route metrics lower in windows.
2025-01-26 00:41:15 +08:00
Sijie.Sun 1194ee1c2d fix peer manager stuck when sending large peer rpc (#572) 2025-01-17 06:50:21 +08:00
Sijie.Sun c23b544c34 tcp accept should retry when encoutering some kinds of error (#565)
* tcp accept should retry when encoutering some kinds of error

bump version to v2.1.2

* persistent temporary machine id
2025-01-14 08:55:48 +08:00
Sijie.Sun 9d76b86f49 fix bugs (#561)
1. if peers disconnected before stop session, may crash at the assert.
2. bind_device flag should take effect on manual connector.
2025-01-12 00:16:38 +08:00
Sijie.Sun bb0ccca3e5 allow manually specify public address of listeners (#556) 2025-01-10 09:25:14 +08:00
Sijie.Sun 306817ae9a allow listener retry listen (#554) 2025-01-09 00:01:41 +08:00
Sijie.Sun d2ec60e108 batch recv for udp proxy (#552) 2025-01-07 23:52:18 +08:00
Sijie.Sun e016aeddeb optimize pingpong conn close condition (#549)
if received some packets from peer, only close conn after enough
rounds of pingpong
2025-01-07 22:42:57 +08:00
Sijie.Sun a4419a31fd fix peer rpc compatibility issue (#548)
every rpc packet should contains descriptor if sent to old version et.
2025-01-06 23:30:56 +08:00
Sijie.Sun 34e4e907a9 bump version to v2.1.1 (#533) 2024-12-24 10:40:57 -05:00
Sijie.Sun 2f4a097787 fix android (#531) 2024-12-23 19:38:32 -05:00
Sijie.Sun f3de00be37 support pause a network (#528) 2024-12-23 09:29:59 +08:00
Sijie.Sun 4cf61f0d4a fix web show dup entry for same machine (#526) 2024-12-21 11:51:01 -05:00
Sijie.Sun 4e5915f98e save api host in local storage (#523) 2024-12-21 01:29:54 +08:00
Sijie.Sun 870eca9e9f optimize easytier-web (#522)
1. use default compress level for tower_http. the best level consume
lots of memory
2. add more help message and command line arg.
2024-12-21 01:27:39 +08:00
Sijie.Sun 25ed41caf5 use correct config server url (#519) 2024-12-20 00:21:22 +08:00
Sijie.Sun 4bb72b5606 fix rpc packet route before first route info exchange (#516)
* fix rpc packet route before first route info exchange
* fix install script
* update config server help
2024-12-19 09:25:25 +08:00
Sijie.Sun c4d8ea4fec refactor sym to cone punch (#515) 2024-12-18 23:56:47 +08:00
Sophon 8588c9201a Add windows arm64 target to CI GUI (#487) 2024-11-21 23:14:09 +08:00
Sophon dd2236c697 Add windows arm64 target to CI core (#486) 2024-11-21 15:27:24 +08:00
Sophon bc7c4d8cd0 Fix building on Windows ARM64 (#485)
Tested building natively on Windows ARM64, with Rust 1.82 and VS2022 toolchains.
Basic functionality of easytier-cli, easytier-core and easytier-web has been verified.
2024-11-21 10:32:03 +08:00
Sijie.Sun aed54f7318 support respond stun request in udp tunnel (#484)
we can use this to help the hole punching. (getting public mapped address stablely)
2024-11-20 23:45:06 +08:00
Sijie.Sun 86600c6315 version 2.1.0 (#482) 2024-11-19 23:46:02 +08:00
Sijie.Sun 3f47f37470 fix high cpu usage when client proto mismatch (#481)
before this patch, invalid packat received by tunnel reader may cause a dead loop in handshake.
2024-11-19 21:36:09 +08:00
Sijie.Sun 1324e6163e move toast to bottom right (#478) 2024-11-16 21:36:16 +08:00
Sijie.Sun 89093167c6 fix autocomplete not show dropdown (#477) 2024-11-16 21:31:19 +08:00
Sijie.Sun 15ad92aef2 fix no relay not work in local network (#476) 2024-11-16 14:36:17 +08:00
Sijie.Sun 6cdea38284 support compress for rpc and tun data (#473)
* support compress for rpc and tun data
* add compression layer to easytier-web
2024-11-16 11:23:18 +08:00
咸鱼而已 9d455e22fa 支持服务管理 (#456)
* Support running as a Windows service.

* Optimize startup logic contro

* When running in a Windows service environment, delegate the termination of the program to the win_service_event_loop function.

* Remove the use of std::ffi::OsString at the top.

* Support  service manager

* Move the service-related features to be implemented in easytier-cli.

* Add a command line option work-dir to specify the working directory
Adjust the error handling logic
2024-11-16 09:01:58 +08:00
Sijie.Sun 4fc3ff8ce8 gui use frontend-lib, fix memory leak (#467) 2024-11-10 23:03:40 +08:00
Sijie.Sun 88e6de9d7e make all frontend functions works (#466) 2024-11-10 11:06:58 +08:00
Sijie.Sun e948dbfcc1 Feat/web (Patchset 4) (#460)
support basic functions in frontend
1. create/del network
2. inspect network running status
2024-11-08 23:33:17 +08:00
Sijie.Sun 8aca5851f2 feat/web: Patchset 3 (#455)
https://apifox.com/apidoc/shared-ceda7a60-e817-4ea8-827b-de4e874dc45e

implement all backend API
2024-11-02 15:13:19 +08:00
Jiangqiu Shen 18da94bf33 make the panic message more useful (#437)
when panic happend, previouse panic info:
panic occurred: PanicHookInfo { payload: Any { .. }, location: Location { file: "easytier/src/easytier-core.rs", line: 680, col: 9 }, can_unwind: true, force_no_backtrace: false }

the new panic info:

panic occurred: payload:launcher error: "anyhow error: failed to add listener tcp://0.0.0.0:11010", location: Some(Location { file: "easytier/src/easytier-core.rs", line: 680, col: 9 })
backtrace saved to easytier-panic.log
2024-10-26 01:33:04 +08:00
咸鱼而已 1ac2e1c8e3 Support running as a Windows service. (#445)
When the program is started by the Service Control Manager (SCM), it needs to call StartServiceCtrlDispatcher (which corresponds to service_dispatcher::start in the windows-service crate) to inform the SCM of the service's entry function. The SCM then calls the service entry function passed to StartServiceCtrlDispatcher. The process calling StartServiceCtrlDispatcher will block until the service's status is set to Stopped. If the current program is not run through the SCM, StartServiceCtrlDispatcher will return the error ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, and the program will run according to its original mechanism. For more details about SCM, please refer to Microsoft's documentation.
2024-10-26 01:32:25 +08:00
Sijie.Sun a78b759741 feat/web (Patchset 2) (#444)
This patch implement a restful server without any auth.

usage:

```bash
# run easytier-web, which acts as an gateway and registry for all easytier-core
$> easytier-web

# run easytier-core and connect to easytier-web with a token
$> easytier-core --config-server udp://127.0.0.1:22020/fdsafdsa

# use restful api to list session
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/sessions
[{"token":"fdsafdsa","client_url":"udp://127.0.0.1:48915","machine_id":"de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f"}]%

# use restful api to run a network instance
$> curl -H "Content-Type: application/json" -X POST 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f -d '{"config": "listeners = [\"udp://0.0.0.0:12344\"]"}'

# use restful api to get network instance info
$> curl -H "Content-Type: application/json" -X GET 127.0.0.1:11211/api/v1/network/de3f5b8f-0f2f-d9d0-fb30-a2ac8951d92f/65437e50-b286-4098-a624-74429f2cb839 
```
2024-10-26 00:04:22 +08:00
fanyang b5c3726e67 Optimize building speed (#442)
Make easytier-cli and easytier-core link to the easytier library to
avoid duplicate linking of mods.
2024-10-24 16:21:35 +08:00
m1m1sha efee3707da 🐳 chore: use vue 3.4.38 (#438)
* 🐳 chore: use vue 3.4.38
2024-10-22 14:08:25 +08:00
Serenity bbd3453f36 ref: log files are suffix for easier viewing (#433) 2024-10-20 17:34:19 +08:00
Sijie.Sun 0bf42c53cc Feat/web (PatchSet 1) (#436)
* move rpc-build out of easytier dir and make it a independant project
* easytier core use launcher
* fix flags not print on launch
* allow launcher not fetch node info
* abstract out peer rpc impl
* fix arm gui ci. see https://github.com/actions/runner-images/pull/10807
* add easytier-web crate
* fix manual_connector test case
2024-10-19 18:10:02 +08:00
Sijie.Sun 2134bc9139 fix icmp/udp subnet proxy not work with public server relay (#431) 2024-10-17 00:22:42 +08:00
Xiao Tan 4df8d7e976 fix mtu parameter prompt (#429)
Co-authored-by: BH1XAQ <tanxiao@16iot.cn>
2024-10-16 23:16:09 +08:00
fanyang 70708b34cc Fix app not displayed when click on the dock icon under macOS (#424) 2024-10-14 21:33:48 +08:00
m1m1sha 949003ee1b Merge pull request #422 from EasyTier/chore/proxy
🐳 chore: proxy
2024-10-14 08:36:20 +08:00
m1m1sha db9df1df94 🐳 chore: proxy 2024-10-14 08:33:53 +08:00
sijie.sun 4dca25db86 bump version to 2.0.3 2024-10-13 12:49:01 +08:00
Sijie.Sun d87a440c04 fix 202 bugs (#418)
* fix peer rpc stop working because of mpsc tunnel close unexpectedly

* fix gui:

1. allow set network prefix for virtual ipv4
2. fix android crash
3. fix subnet proxy cannot be set on android
2024-10-13 11:59:16 +08:00
m1m1sha 55efd62798 Merge pull request #417 from EasyTier/perf/detail
🎈 perf: event log
2024-10-12 20:47:42 +08:00
m1m1sha 70a41275c1 feat: display
Display server tag and whether server supports relay
2024-10-12 20:17:45 +08:00
m1m1sha dd941681ce 🎈 perf: event log 2024-10-12 19:57:36 +08:00
m1m1sha 9824d0adaa Fix/UI detail (#414) 2024-10-12 00:36:57 +08:00
Sijie.Sun d2291628e0 mpsc tunnel may be stuck by slow tcp stream, should not panic for this (#406)
* mpsc tunnel may be stuck by slow tcp stream, should not panic for this
* disallow node connect to self
2024-10-11 00:12:14 +08:00
Sijie.Sun 7ab8cad1af allow use ipv4 address in any cidr (#404) 2024-10-10 10:28:48 +08:00
Sijie.Sun 2c017e0fc5 improve hole punch (#403)
* fix duplicated peer id (again)

* improve udp hole punch

1. always try cone punch for any nat type, tolerate fault stun type.
2. serializing all sym punch request, including server side.
2024-10-10 00:07:42 +08:00
Hs_Yeah d9453589ac Fix panic when DNS resolution for STUN server returns only IPv6 addrs. (#402) 2024-10-09 22:40:01 +08:00
Sijie.Sun e344372616 fix cone-to-cone punch (#401) 2024-10-09 22:39:06 +08:00
Sijie.Sun 63821e56bc fix udp buffer size, avoid packet loss (#399)
also bump version to 2.0.2
2024-10-08 22:01:15 +08:00
538 changed files with 119034 additions and 18791 deletions
+45 -15
View File
@@ -5,73 +5,103 @@ 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-linux-musl-gcc"
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"]
[target.mipsel-unknown-linux-musl]
linker = "mipsel-linux-muslsf-gcc"
linker = "mipsel-unknown-linux-muslsf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/lib",
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/lib",
"-L",
"./musl_gcc/mipsel-linux-muslsf-cross/lib/gcc/mipsel-linux-muslsf/11.2.1",
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/sysroot/usr/lib",
"-L",
"./musl_gcc/mipsel-unknown-linux-muslsf/lib/gcc/mipsel-unknown-linux-muslsf/15.1.0",
"-l",
"atomic",
"-l",
"ctz",
"-l",
"gcc",
]
[target.mips-unknown-linux-musl]
linker = "mips-linux-muslsf-gcc"
linker = "mips-unknown-linux-muslsf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/mips-linux-muslsf-cross/mips-linux-muslsf/lib",
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/lib",
"-L",
"./musl_gcc/mips-linux-muslsf-cross/lib/gcc/mips-linux-muslsf/11.2.1",
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/sysroot/usr/lib",
"-L",
"./musl_gcc/mips-unknown-linux-muslsf/lib/gcc/mips-unknown-linux-muslsf/15.1.0",
"-l",
"atomic",
"-l",
"ctz",
"-l",
"gcc",
]
[target.armv7-unknown-linux-musleabihf]
linker = "armv7l-linux-musleabihf-gcc"
linker = "armv7-unknown-linux-musleabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabi]
linker = "armv7m-linux-musleabi-gcc"
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-linux-musleabihf-gcc"
linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-linux-musleabihf-cross/arm-linux-musleabihf/lib",
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
"-L",
"./musl_gcc/arm-linux-musleabihf-cross/lib/gcc/arm-linux-musleabihf/11.2.1",
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
[target.arm-unknown-linux-musleabi]
linker = "arm-linux-musleabi-gcc"
linker = "arm-unknown-linux-musleabi-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-linux-musleabi-cross/arm-linux-musleabi/lib",
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
"-L",
"./musl_gcc/arm-linux-musleabi-cross/lib/gcc/arm-linux-musleabi/11.2.1",
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
+2
View File
@@ -0,0 +1,2 @@
PROFILE=$(cat .flake-profile 2>/dev/null)
use flake .#${PROFILE}
+95 -13
View File
@@ -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?
+156 -15
View File
@@ -3,36 +3,177 @@
name: 💡 新功能请求 / Feature Request
title: '[feat] '
description: 提出一个想法 / Suggest an idea
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
+43
View File
@@ -0,0 +1,43 @@
name: prepare-build
author: Luna
description: Prepare build environment
inputs:
web:
description: 'Whether to prepare the web build environment'
required: true
default: 'true'
gui:
description: 'Whether to prepare the GUI build environment'
required: true
default: 'true'
token:
description: 'GitHub token, used by setup-protoc action'
required: false
runs:
using: 'composite'
steps:
- run: mkdir -p easytier-gui/dist
shell: bash
- name: Setup Frontend Environment
if: ${{ inputs.web == 'true' }}
uses: ./.github/actions/prepare-pnpm
with:
build-filter: './easytier-web/*'
- name: Install GUI dependencies (Used by clippy)
if: ${{ inputs.gui == 'true' }}
run: |
bash ./.github/workflows/install_gui_dep.sh
shell: bash
- name: Install Rust
run: |
bash ./.github/workflows/install_rust.sh
shell: bash
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ inputs.token }}
+48
View File
@@ -0,0 +1,48 @@
name: 'Setup pnpm'
author: Luna
description: 'Setup Node.js, pnpm, and install dependencies'
inputs:
build-filter:
description: 'The filter argument for pnpm build (e.g. ./easytier-web/*)'
required: false
default: ''
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: 22
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install and build
shell: bash
run: |
pnpm -r install
if [ -n "${{ inputs.build-filter }}" ]; then
echo "Building with filter: ${{ inputs.build-filter }}"
pnpm -r --filter "${{ inputs.build-filter }}" build
else
echo "No build filter provided, building all packages"
pnpm -r build
fi
+15 -9
View File
@@ -1,29 +1,35 @@
FROM alpine:latest AS builder
FROM alpine:latest AS base
FROM base AS builder
ARG TARGETPLATFORM
COPY . /tmp/artifacts
RUN mkdir -p /tmp/output; \
cd /tmp/artifacts; \
ARTIFACT_ARCH=""; \
WORKDIR /tmp/output
RUN ARTIFACT_ARCH=""; \
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
ARTIFACT_ARCH="x86_64"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then \
ARTIFACT_ARCH="armhf"; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
ARTIFACT_ARCH="armv7hf"; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
ARTIFACT_ARCH="aarch64"; \
elif [ "$TARGETPLATFORM" = "linux/riscv64" ]; then \
ARTIFACT_ARCH="riscv64"; \
else \
echo "Unsupported architecture: $TARGETARCH"; \
echo "Unsupported architecture: $TARGETPLATFORM"; \
exit 1; \
fi; \
cp /tmp/artifacts/easytier-linux-${ARTIFACT_ARCH}/* /tmp/output;
FROM alpine:latest
FROM base
RUN apk add --no-cache tzdata
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
@@ -36,4 +42,4 @@ EXPOSE 11011/tcp
# wss
EXPOSE 11012/tcp
ENTRYPOINT ["easytier-core"]
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
+165 -58
View File
@@ -30,37 +30,62 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh", "easytier-web/**"]'
build_web:
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v5
- name: Setup Frontend Environment
uses: ./.github/actions/prepare-pnpm
with:
build-filter: './easytier-web/*'
- name: Archive artifact
uses: actions/upload-artifact@v5
with:
name: easytier-web-dashboard
path: |
easytier-web/frontend/dist/*
build:
strategy:
fail-fast: false
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest
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-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mipsel
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-latest
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
@@ -71,9 +96,15 @@ jobs:
- TARGET: x86_64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-i686
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
@@ -83,25 +114,34 @@ jobs:
TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }}
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
needs: pre_job
needs:
- pre_job
- build_web
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Cargo cache
uses: actions/cache@v4
- name: Download web artifact
uses: actions/download-artifact@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
name: easytier-web-dashboard
path: easytier-web/frontend/dist/
- uses: Swatinem/rust-cache@v2
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
with:
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
shared-key: "core-registry"
cache-targets: "false"
- 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 }}
@@ -110,26 +150,48 @@ jobs:
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
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
if [[ -d "./musl_gcc/sysroot" ]]; then
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)
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
cargo +nightly-2026-02-02 build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier --features=jemalloc
else
cargo build --release --verbose --target $TARGET
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
CORE_FEATURES="--features=mimalloc"
elif [[ $TARGET =~ ^riscv64.*$ || $TARGET =~ ^loongarch64.*$ || $TARGET =~ ^aarch64.*$ ]]; then
CORE_FEATURES="--features=mimalloc"
else
CORE_FEATURES="--features=jemalloc"
fi
cargo build --release --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
cargo build --release --target $TARGET $CORE_FEATURES
fi
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
- name: Build Core & Cli (X86_64 FreeBSD)
uses: cross-platform-actions/action@v0.23.0
uses: vmactions/freebsd-vm@670398e4236735b8b65805c3da44b7a511fb8b27
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
env:
TARGET: ${{ matrix.TARGET }}
with:
operating_system: freebsd
environment_variables: TARGET
architecture: x86-64
version: ${{ matrix.BSD_VERSION }}
shell: bash
memory: 5G
cpu_count: 4
envs: TARGET
release: ${{ matrix.BSD_VERSION }}
arch: x86_64
usesh: true
mem: 6144
cpu: 4
run: |
uname -a
echo $SHELL
@@ -138,27 +200,32 @@ jobs:
whoami
env | sort
sudo pkg install -y git protobuf
pkg install -y git protobuf llvm-devel sudo curl
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
. $HOME/.cargo/env
rustup set auto-self-update disable
rustup install 1.77
rustup default 1.77
rustup install 1.93
rustup default 1.93
export CC=clang
export CXX=clang++
export CARGO_TERM_COLOR=always
cargo build --release --verbose --target $TARGET
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
cargo build --release --verbose --target $TARGET --features=mimalloc
- name: Install UPX
if: ${{ matrix.OS != 'macos-latest' }}
uses: crazy-max/ghaction-upx@v3
with:
version: latest
install-only: true
mkdir -p built-bins/$TARGET/release/
mv ./target/$TARGET/release/easytier-web-embed ./built-bins/$TARGET/release/easytier-web-embed
mv ./target/$TARGET/release/easytier-web ./built-bins/$TARGET/release/easytier-web
mv ./target/$TARGET/release/easytier-core ./built-bins/$TARGET/release/easytier-core
mv ./target/$TARGET/release/easytier-cli ./built-bins/$TARGET/release/easytier-cli
# remove dirs to avoid copy many files back
rm -rf ./target ~/.cargo
mv ./built-bins ./target
- name: Compress
run: |
@@ -166,8 +233,14 @@ jobs:
# windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/Packet.dll ./artifacts/objects/
cp easytier/third_party/wintun.dll ./artifacts/objects/
case $TARGET in
x86_64*) ARCH_DIR=x86_64 ;;
i686*) ARCH_DIR=i686 ;;
aarch64*) ARCH_DIR=arm64 ;;
esac
if [[ -n "$ARCH_DIR" ]]; then
find "easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./artifacts/objects/ \;
fi
fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME
@@ -175,43 +248,77 @@ jobs:
TAG=$GITHUB_SHA
fi
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
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"
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
fi
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-${{ matrix.ARTIFACT_NAME }}
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
core-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build_web
- build
steps:
- name: Mark result as failed
if: needs.build.result != 'success'
run: exit 1
magisk_build:
needs:
- pre_job
- build_web
- build
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v5 # 必须先检出代码才能获取模块配置
# 下载二进制文件到独立目录
- name: Download Linux aarch64 binaries
uses: actions/download-artifact@v4
with:
name: easytier-linux-aarch64
path: ./downloaded-binaries/ # 独立目录避免冲突
# 将二进制文件复制到 Magisk 模块目录
- name: Prepare binaries
run: |
mkdir -p ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-core ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
# 上传生成的模块
- name: Upload Magisk Module
uses: actions/upload-artifact@v5
with:
name: Easytier-Magisk
path: |
./easytier-contrib/easytier-magisk
!./easytier-contrib/easytier-magisk/build.sh
!./easytier-contrib/easytier-magisk/magisk_update.json
if-no-files-found: error
+47 -5
View File
@@ -11,13 +11,18 @@ on:
image_tag:
description: 'Tag for this image build'
type: string
default: 'v1.2.0'
default: 'v2.6.0'
required: true
mark_latest:
description: 'Mark this image as latest'
type: boolean
default: false
required: true
mark_unstable:
description: 'Mark this image as unstable'
type: boolean
default: false
required: true
jobs:
docker:
@@ -26,7 +31,14 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
-
name: Validate inputs
run: |
if [[ "${{ inputs.mark_latest }}" == "true" && "${{ inputs.mark_unstable }}" == "true" ]]; then
echo "Error: mark_latest and mark_unstable cannot both be true"
exit 1
fi
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -39,9 +51,15 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: login github container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
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 }}
@@ -50,12 +68,36 @@ jobs:
- name: List files
run: |
ls -l -R .
- name: Prepare Docker tags
id: tags
run: |
# Base tags with version
DOCKERHUB_TAGS="easytier/easytier:${{ inputs.image_tag }}"
GHCR_TAGS="ghcr.io/easytier/easytier:${{ inputs.image_tag }}"
# Add latest tags if requested
if [[ "${{ inputs.mark_latest }}" == "true" ]]; then
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:latest"
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:latest"
fi
# Add unstable tags if requested
if [[ "${{ inputs.mark_unstable }}" == "true" ]]; then
DOCKERHUB_TAGS="${DOCKERHUB_TAGS},easytier/easytier:unstable"
GHCR_TAGS="${GHCR_TAGS},ghcr.io/easytier/easytier:unstable"
fi
# Combine all tags
ALL_TAGS="${DOCKERHUB_TAGS},${GHCR_TAGS}"
echo "tags=${ALL_TAGS}" >> $GITHUB_OUTPUT
echo "Generated tags: ${ALL_TAGS}"
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: ./docker_context
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/riscv64
push: true
file: .github/workflows/Dockerfile
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
tags: ${{ steps.tags.outputs.tags }}
+85 -88
View File
@@ -29,18 +29,18 @@ jobs:
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh", ".github/workflows/install_gui_dep.sh", "easytier-web/frontend-lib/**"]'
build-gui:
strategy:
fail-fast: false
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64
@@ -58,6 +58,16 @@ jobs:
GUI_TARGET: x86_64-pc-windows-msvc
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
GUI_TARGET: i686-pc-windows-msvc
ARTIFACT_NAME: windows-i686
runs-on: ${{ matrix.OS }}
env:
NAME: easytier
@@ -68,96 +78,94 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-node@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
(cd easytier-gui; pnpm install)
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: bash ./.github/workflows/install_gui_dep.sh
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: |
# see https://tauri.app/v1/guides/building/linux/
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ noble-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports noble-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt-get update && sudo apt-get upgrade -y
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
sudo apt update
sudo apt install aptitude
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu libsoup-3.0-dev:arm64 libjavascriptcoregtk-4.1-dev:arm64
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- name: Install rpm package (Linux target only)
if: ${{ contains(matrix.TARGET, '-linux-') }}
run: |
sudo apt update
sudo apt install -y rpm
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Setup Frontend Environment
uses: ./.github/actions/prepare-pnpm
- uses: Swatinem/rust-cache@v2
with:
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v3
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: copy correct DLLs
if: ${{ matrix.OS == 'windows-latest' }}
run: |
case $TARGET in
x86_64*) ARCH_DIR=x86_64 ;;
i686*) ARCH_DIR=i686 ;;
aarch64*) ARCH_DIR=arm64 ;;
esac
if [[ -n "$ARCH_DIR" ]]; then
find "./easytier/third_party/${ARCH_DIR}" -maxdepth 1 -type f \( -name "*.dll" -o -name "*.sys" \) -exec cp {} ./easytier-gui/src-tauri/ \;
fi
- name: Build GUI
if: ${{ matrix.GUI_TARGET != '' }}
uses: tauri-apps/tauri-action@v0
with:
projectPath: ./easytier-gui
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-latest' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ contains(matrix.TARGET, '-linux-') && contains(matrix.TARGET, 'aarch64') && '--bundles deb,rpm' || '' }}
- name: Compress
run: |
@@ -175,6 +183,7 @@ jobs:
mv ./target/$GUI_TARGET/release/bundle/dmg/*.dmg ./artifacts/objects/
elif [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$GUI_TARGET/release/bundle/deb/*.deb ./artifacts/objects/
mv ./target/$GUI_TARGET/release/bundle/rpm/*.rpm ./artifacts/objects/
if [[ $GUI_TARGET =~ ^x86_64.*$ ]]; then
# currently only x86 appimage is supported
mv ./target/$GUI_TARGET/release/bundle/appimage/*.AppImage ./artifacts/objects/
@@ -185,24 +194,12 @@ jobs:
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
gui-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
+11
View File
@@ -0,0 +1,11 @@
sudo apt update
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
+21 -47
View File
@@ -8,61 +8,35 @@
# dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev
# for easytier-gui
if [[ $GUI_TARGET != '' && $GUI_TARGET =~ ^x86_64.*$ ]]; then
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
# https://github.com/cross-tools/musl-cross/releases
# if "musl" is a substring of TARGET, we assume that we are using musl
MUSL_TARGET=$TARGET
# 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
# curl -s musl.cc | grep mipsel
case $TARGET in
mipsel-unknown-linux-musl)
MUSL_URI=mipsel-linux-muslsf
;;
mips-unknown-linux-musl)
MUSL_URI=mips-linux-muslsf
;;
aarch64-unknown-linux-musl)
MUSL_URI=aarch64-linux-musl
;;
armv7-unknown-linux-musleabihf)
MUSL_URI=armv7l-linux-musleabihf
;;
armv7-unknown-linux-musleabi)
MUSL_URI=armv7m-linux-musleabi
;;
arm-unknown-linux-musleabihf)
MUSL_URI=arm-linux-musleabihf
;;
arm-unknown-linux-musleabi)
MUSL_URI=arm-linux-musleabi
;;
esac
if [ -n "$MUSL_URI" ]; then
if [[ $MUSL_TARGET =~ musl ]]; then
mkdir -p ./musl_gcc
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
fi
fi
# see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable
rustup install 1.77
rustup default 1.77
rustup install 1.93
rustup default 1.93
# mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cd "$PWD/musl_gcc/${MUSL_URI}-cross/lib/gcc/${MUSL_URI}/11.2.1" || exit 255
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a
@@ -70,8 +44,8 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
ar x libgcc.a _ctzsi2.o _clz.o _bswapsi2.o
ar rcs libctz.a _ctzsi2.o _clz.o _bswapsi2.o
rustup toolchain install nightly-x86_64-unknown-linux-gnu
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
rustup toolchain install nightly-2026-02-02-x86_64-unknown-linux-gnu
rustup component add rust-src --toolchain nightly-2026-02-02-x86_64-unknown-linux-gnu
# https://github.com/rust-lang/rust/issues/128808
# remove it after Cargo or rustc fix this.
+12 -51
View File
@@ -36,7 +36,7 @@ jobs:
matrix:
include:
- TARGET: android
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: android
runs-on: ${{ matrix.OS }}
env:
@@ -47,7 +47,7 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Set current ref as env variable
run: |
@@ -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
@@ -70,41 +70,14 @@ jobs:
echo "$ANDROID_HOME/ndk/26.0.10792818/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
echo "NDK_HOME=$ANDROID_HOME/ndk/26.0.10792818/" > $GITHUB_ENV
- uses: actions/setup-node@v4
- name: Setup Frontend Environment
uses: ./.github/actions/prepare-pnpm
- uses: Swatinem/rust-cache@v2
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
(cd easytier-gui; pnpm install)
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# The prefix cache key, this can be changed to start a new cache manually.
# default: "v0-rust"
prefix-key: ""
- name: Install rust target
run: |
@@ -115,7 +88,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 }}
@@ -140,24 +113,12 @@ jobs:
rm -rf ./artifacts/objects/
- name: Archive artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: easytier-gui-${{ matrix.ARTIFACT_NAME }}
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
mobile-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
+30
View File
@@ -0,0 +1,30 @@
name: Nix Check
on:
push:
branches: ["main", "develop"]
paths:
- "**/*.nix"
- "flake.lock"
pull_request:
branches: ["main", "develop"]
paths:
- "**/*.nix"
- "flake.lock"
jobs:
check-full-shell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v6
- name: Check full devShell
run: nix develop .#full --command true
+225
View File
@@ -0,0 +1,225 @@
name: EasyTier OHOS
on:
push:
branches: ["develop", "main", "releases/**"]
tags:
- 'v*'
- '!*-pre'
pull_request:
branches: ["develop", "main"]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
defaults:
run:
# necessary for windows
shell: bash
jobs:
cargo_fmt_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: fmt check
working-directory: ./easytier-contrib/easytier-ohrs
run: |
bash ../../.github/workflows/install_rust.sh
rustup component add rustfmt
cargo fmt --all -- --check
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
env:
OHPM_PUBLISH_CODE: ${{ secrets.OHPM_PUBLISH_CODE }}
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v5
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
wget \
unzip \
git \
pkg-config curl libgl1-mesa-dev expect
sudo apt-get clean
- name: Resolve easytier version
run: |
set -e
UPSTREAM_REPO="https://github.com/EasyTier/EasyTier.git"
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || true
git fetch --unshallow upstream main || git fetch upstream main
git fetch --tags upstream --force
# 读取 cargo 版本
CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path easytier/Cargo.toml \
| jq -r '.packages[0].version')
# 获取 upstream/main 最新 tag
LAST_TAG=$(git describe --tags --abbrev=0 upstream/main 2>/dev/null || echo "")
LAST_TAG_VERSION="${LAST_TAG#v}"
# 语义版本比较
version_gt() {
[ "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ] && [ "$1" != "$2" ]
}
if [ -z "$LAST_TAG_VERSION" ]; then
BASE_VERSION="$CARGO_VERSION"
DIFF_COUNT=$(git rev-list --count upstream/main)
elif version_gt "$CARGO_VERSION" "$LAST_TAG_VERSION"; then
BASE_VERSION="$CARGO_VERSION"
DIFF_COUNT=0
else
BASE_VERSION="$LAST_TAG_VERSION"
DIFF_COUNT=$(git rev-list --count "${LAST_TAG}..upstream/main")
fi
COMMIT_HASH=$(git rev-parse --short upstream/main)
EASYTIER_VERSION="${BASE_VERSION}-${DIFF_COUNT}-${COMMIT_HASH}"
echo "EASYTIER_VERSION=$EASYTIER_VERSION"
echo "EASYTIER_VERSION=$EASYTIER_VERSION" >> $GITHUB_ENV
cd ./easytier-contrib/easytier-ohrs/package
jq --arg v "$EASYTIER_VERSION" '.version = $v' oh-package.json5 > oh-package.tmp.json5
mv oh-package.tmp.json5 oh-package.json5
- name: Generate CHANGELOG.md for current commit
working-directory: ./easytier-contrib/easytier-ohrs/package
run: |
{
echo "## easytier-ohrs ${EASYTIER_VERSION}"
echo
git log -1 --pretty=format:"- %s"
echo
} > CHANGELOG.md
- name: Setup HarmonyOS CLI tools
uses: ErBWs/setup-ohos@v1
- name: Download and Extract Custom SDK
run: |
wget https://github.com/FrankHan052176/Easytier-OHOS-sdk/releases/download/v1/ohos-sdk.zip -O /tmp/ohos-sdk.zip
sudo unzip -o /tmp/ohos-sdk.zip -d /tmp/custom-sdk
sudo cp -rf /tmp/custom-sdk/linux/native/* $OHOS_NDK_HOME/native
echo "Custom SDK files deployed to $OHOS_NDK_HOME/native"
ls -a $OHOS_NDK_HOME/native
- name: Setup build environment
run: |
echo "TARGET_ARCH=aarch64-linux-ohos" >> $GITHUB_ENV
- 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 latest Har
working-directory: ./easytier-contrib/easytier-ohrs
run: |
sudo apt-get install -y llvm clang lldb lld
sudo apt-get install -y protobuf-compiler
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
ohrs artifact
mv package.har easytier-ohrs.har
- name: Build Release Package
if: startsWith(github.ref, 'refs/tags/')
working-directory: ./easytier-contrib/easytier-ohrs
run: |
echo "🎉 Official Release detected. Building easytier-release..."
TAG_NAME="${{ github.ref_name }}"
TAG_VERSION="${TAG_NAME#v}"
echo "Release Version: $TAG_VERSION"
cd package
jq --arg v "$TAG_VERSION" '.name = "easytier-release" | .version = $v' oh-package.json5 > oh-package.tmp.json5 && mv oh-package.tmp.json5 oh-package.json5
cd ..
ohrs build --release --arch aarch
cd dist/arm64-v8a
mv libeasytier_ohrs.so libeasytier_release.so
cd ../..
ohrs artifact
mv package.har easytier-release.har
- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: easytier-ohos
path: |
./easytier-contrib/easytier-ohrs/easytier-ohrs.har
retention-days: 5
if-no-files-found: error
- name: Publish To Center Ohpm
working-directory: ./easytier-contrib/easytier-ohrs
env:
OHPM_PRIVATE_KEY: ${{ secrets.OHPM_PRIVATE_KEY }}
OHPM_KEY_PASSPHRASE: ${{ secrets.OHPM_KEY_PASSPHRASE }}
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
run: |
ohpm config set publish_id "$OHPM_PUBLISH_CODE"
ohpm config set publish_registry https://ohpm.openharmony.cn/ohpm
TMP_DIR=$(mktemp -d)
PRIVATE_KEY_FILE="$TMP_DIR/private_key"
printf '%s' "$OHPM_PRIVATE_KEY" > "$PRIVATE_KEY_FILE"
chmod 600 "$PRIVATE_KEY_FILE"
ohpm config set key_path $PRIVATE_KEY_FILE
unzip ohpm_crypto.zip -d /home/runner/work/
ohpm config set crypto_path /home/runner/work/ohpm_crypto
chmod 755 /home/runner/work/ohpm_crypto/*
PASSPHRASE="$(printf '%s' "$OHPM_KEY_PASSPHRASE" | tr -d '\r\n')"
ohpm config set key_passphrase "$PASSPHRASE"
ohpm publish easytier-ohrs.har
- name: Publish To Private Ohpm
working-directory: ./easytier-contrib/easytier-ohrs
if: ${{ env.OHPM_PUBLISH_CODE != '' && github.event_name == 'push' }}
run: |
printf '%s' "${{ secrets.CODEARTS_PRIVATE_OHPM }}" > ~/.ohpm/.ohpmrc
ohpm config set strict_ssl false
ohpm publish easytier-ohrs.har
if [ -f "easytier-release.har" ]; then
echo "🚀 Publishing Release package..."
ohpm publish easytier-release.har
fi
curl --header "Content-Type: application/json" --request POST --data "{}" ${{ secrets.CODEARTS_WEBHOOKS }}
+17 -14
View File
@@ -6,22 +6,19 @@ on:
core_run_id:
description: 'The run id of EasyTier-Core Action in EasyTier repo'
type: number
default: 10322498549
required: true
gui_run_id:
description: 'The run id of EasyTier-GUI Action in EasyTier repo'
type: number
default: 10322498557
required: true
mobile_run_id:
description: 'The run id of EasyTier-Mobile Action in EasyTier repo'
type: number
default: 10322498555
required: true
version:
description: 'Version for this release'
type: string
default: 'v2.0.1'
default: 'v2.6.0'
required: true
make_latest:
description: 'Mark this release as latest'
@@ -34,35 +31,34 @@ permissions:
jobs:
release:
if: contains('["KKRainbow"]', github.actor)
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- 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 }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
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 }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
path: release_assets_nozip
- name: Download GUI Artifact
uses: dawidd6/action-download-artifact@v6
- name: Download Mobile Artifact
uses: dawidd6/action-download-artifact@v11
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.mobile_run_id }}
repo: EasyTier/EasyTier
repo: ${{ github.repository }}
path: release_assets_nozip
- name: Zip release assets
@@ -78,7 +74,14 @@ jobs:
ls -l -R ./
chmod -R 755 .
for x in `ls`; do
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
if [ "$x" = "Easytier-Magisk" ]; then
# for Easytier-Magisk, make sure files are in the root of the zip
cd $x;
zip -r ../../zipped_assets/$x-${VERSION}.zip .;
cd ..;
else
zip -r ../zipped_assets/$x-${VERSION}.zip $x;
fi
done
- name: Release
+106 -19
View File
@@ -2,12 +2,14 @@ name: EasyTier Test
on:
push:
branches: ["develop", "main"]
branches: [ "develop", "main" ]
pull_request:
branches: ["develop", "main"]
branches: [ "develop", "main" ]
env:
CARGO_TERM_COLOR: always
# RUSTC_WRAPPER: "sccache"
# SCCACHE_GHA_ENABLED: "true"
defaults:
run:
@@ -28,40 +30,125 @@ jobs:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
skip_after_successful_duplicate: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
test:
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml", ".github/workflows/install_gui_dep.sh", ".github/workflows/install_rust.sh"]'
check:
name: Run linters & check
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Setup protoc
uses: arduino/setup-protoc@v2
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
gui: true
web: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2
- name: Install rustfmt and clippy
run: |
rustup component add rustfmt
rustup component add clippy
- uses: taiki-e/install-action@cargo-hack
- name: Check Cargo.lock is up to date
run: |
if ! cargo metadata --format-version 1 --locked --no-deps > /dev/null; then
echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock."
exit 1
fi
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check Clippy
run: cargo clippy --all-targets --features full --all -- -D warnings
- name: Check features
if: ${{ !cancelled() }}
run: cargo hack check --package easytier --each-feature --exclude-features macos-ne --verbose
pre-test:
name: Build test
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v5
- name: Prepare build environment
uses: ./.github/actions/prepare-build
with:
gui: true
web: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@nextest
- name: Archive test
run: cargo nextest archive --archive-file tests.tar.zst --package easytier --features full
- uses: actions/upload-artifact@v5
with:
name: tests
path: tests.tar.zst
retention-days: 1
test_matrix:
name: Test (${{ matrix.name }})
runs-on: ubuntu-latest
needs: [ pre_job, pre-test ]
if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
include:
- name: "easytier"
opts: "-E 'not test(tests::three_node)' --test-threads 1 --no-fail-fast"
- name: "three_node"
opts: "-E 'test(tests::three_node) and not test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
- name: "three_node::subnet_proxy_three_node_test"
opts: "-E 'test(subnet_proxy_three_node_test)' --test-threads 1 --no-fail-fast"
steps:
- uses: actions/checkout@v5
- name: Setup tools for test
run: sudo apt install bridge-utils
- name: Setup system for test
run: |
sudo modprobe br_netfilter
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
sudo ip addr add 2001:db8::2/64 dev lo
- name: Cargo cache
uses: actions/cache@v4
- uses: taiki-e/install-action@nextest
- name: Download tests
uses: actions/download-artifact@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
name: tests
- name: Run tests
run: |
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose
sudo chown -R $USER:$USER ./target
sudo chown -R $USER:$USER ~/.cargo
sudo prlimit --pid $$ --nofile=1048576:1048576
sudo -E env "PATH=$PATH" cargo nextest run --archive-file tests.tar.zst ${{ matrix.opts }}
test:
runs-on: ubuntu-latest
needs: [ pre_job, test_matrix ]
if: needs.pre_job.outputs.should_skip != 'true' && always()
steps:
- name: Mark result as failed
if: needs.test_matrix.result != 'success'
run: exit 1
+14
View File
@@ -11,6 +11,8 @@ target-*/
*.pdb
.vscode
/.idea
/.direnv/
# perf & flamegraph
perf.data
@@ -29,3 +31,15 @@ musl_gcc
# log
easytier-panic.log
# web
node_modules
.vite
easytier-gui/src-tauri/*.dll
easytier-gui/src-tauri/*.sys
/easytier-contrib/easytier-ohrs/dist/
.direnv
.flake-profile
View File
+225
View 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.93)
- 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.93
rustup default 1.93
# 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
View File
@@ -0,0 +1,233 @@
# EasyTier 贡献指南
[English Version](CONTRIBUTING.md)
感谢您对 EasyTier 项目的关注!本文档提供了参与项目贡献的指南和说明。
## 目录
- [EasyTier 贡献指南](#easytier-贡献指南)
- [目录](#目录)
- [开发环境配置](#开发环境配置)
- [前置要求](#前置要求)
- [必需工具](#必需工具)
- [平台特定依赖](#平台特定依赖)
- [安装步骤](#安装步骤)
- [项目结构](#项目结构)
- [构建指南](#构建指南)
- [构建核心组件](#构建核心组件)
- [构建桌面应用](#构建桌面应用)
- [构建移动应用](#构建移动应用)
- [构建注意事项](#构建注意事项)
- [开发工作流](#开发工作流)
- [测试指南](#测试指南)
- [运行测试](#运行测试)
- [测试要求](#测试要求)
- [Pull Request 规范](#pull-request-规范)
- [其他资源](#其他资源)
- [需要帮助?](#需要帮助)
## 开发环境配置
### 前置要求
#### 必需工具
- Node.js v21 或更高版本
- pnpm v9 或更高版本
- Rust 工具链(版本 1.93
- LLVM 和 Clang
- ProtocProtocol 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 SDKBuild Tools 34.0.0
- Android NDK26.0.10792818
### 安装步骤
1. 克隆仓库:
```bash
git clone https://github.com/EasyTier/EasyTier.git
cd EasyTier
```
2. 安装依赖:
```bash
# 安装 Rust 工具链
rustup install 1.93
rustup default 1.93
# 安装项目依赖
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 做出贡献!
Generated
+5224 -1207
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -1,12 +1,26 @@
[workspace]
resolver = "2"
members = ["easytier", "easytier-gui/src-tauri"]
default-members = ["easytier"]
members = [
"easytier",
"easytier-gui/src-tauri",
"easytier-rpc-build",
"easytier-web",
"easytier-contrib/easytier-ffi",
"easytier-contrib/easytier-uptime",
"easytier-contrib/easytier-android-jni",
]
default-members = ["easytier", "easytier-web"]
exclude = [
"easytier-contrib/easytier-ohrs", # it needs ohrs sdk
]
[profile.dev]
panic = "unwind"
debug = 2
[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
strip = true
+41 -71
View File
@@ -4,84 +4,54 @@
"path": "."
},
{
"name": "core",
"path": "easytier"
},
{
"name": "gui",
"path": "easytier-gui"
},
{
"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": "uptime",
"path": "easytier-contrib/easytier-uptime"
},
{
"name": "vpnservice",
"path": "tauri-plugin-vpnservice"
},
{
"name": "rpc-build",
"path": "easytier-rpc-build"
}
],
"settings": {
"eslint.useFlatConfig": true,
"i18n-ally.sourceLanguage": "cn",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications",
"editor.formatOnPaste": false,
"editor.formatOnType": true,
"[nix]": {
"editor.formatOnSave": false,
},
"eslint.rules.customizations": [
{
"rule": "style/*",
"severity": "off"
},
{
"rule": "style/eol-last",
"severity": "error"
},
{
"rule": "format/*",
"severity": "off"
},
{
"rule": "*-indent",
"severity": "off"
},
{
"rule": "*-spacing",
"severity": "off"
},
{
"rule": "*-spaces",
"severity": "off"
},
{
"rule": "*-order",
"severity": "off"
},
{
"rule": "*-dangle",
"severity": "off"
},
{
"rule": "*-newline",
"severity": "off"
},
{
"rule": "*quotes",
"severity": "off"
},
{
"rule": "*semi",
"severity": "off"
}
],
"eslint.validate": [
"code-workspace",
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"gql",
"graphql"
],
"i18n-ally.localesPaths": [
"easytier-gui/locales"
]
}
}
+133 -41
View File
@@ -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.
+238 -241
View File
@@ -1,240 +1,247 @@
# EasyTier
[![Github release](https://img.shields.io/github/v/tag/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/releases)
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[![GitHub Test Actions](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/EasyTier/EasyTier)
[简体中文](/README_CN.md) | [English](/README.md)
**Please visit the [EasyTier Official Website](https://www.easytier.top/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.
### 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://www.easytier.top/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
```
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
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
Linux (Recommended):
```bash
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
```
Assuming the network topology of the two nodes is as follows
Homebrew (MacOS/Linux):
```bash
brew tap brewforge/chinese
brew install --cask easytier-gui
```
Windows (Recommended, run with administrator privileges):
```powershell
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
```
Install via cargo (Latest development version):
```bash
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
[Install pre-built binary](https://github.com/EasyTier/EasyTier/releases) (Recommended, All platforms supported)
[Install via Docker](https://easytier.cn/en/guide/installation.html#installation-methods)
[Install OpenWrt ipk package](https://github.com/EasyTier/luci-app-easytier)
Additional steps:
[One-Click Register Service](https://easytier.cn/en/guide/network/oneclick-install-as-service.html) (Automatically start when the system boots and run in the background)
### 🚀 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.
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://<SharedNodeIP>:11010
```
2. Run on Node B:
```bash
# Run with administrator privileges
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<SharedNodeIP>: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.6.0-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-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://<SharedNodeIP1>:11010 -p udp://<SharedNodeIP2>: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.
![alt text](/assets/image-2.png)
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
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
### 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
```
![alt text](/assets/image-3.png)
2. Test whether Node A can access nodes under the proxied subnet
```sh
ping 10.1.1.2
```
---
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.top: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 -e tcp://public.easytier.top:11010
```bash
# View routing information
easytier-cli route
```
Node B executes
![Routing Information](/assets/image-3.png)
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top: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]]
@@ -242,88 +249,78 @@ 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.top: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
- [ ] Improve documentation and user guides.
- [ ] Support features such as encryption, TCP hole punching, etc.
- [ ] Support iOS.
- [ ] Support Web configuration management.
## 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]**
- No.1 [949700262](https://qm.qq.com/q/wFoTUChqZW)
- No.2 [837676408](https://qm.qq.com/q/4V33DrfgHe)
- No.3 [957189589](https://qm.qq.com/q/YNyTQjwlai)
## 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)
- Telegramhttps://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>
+236 -242
View File
@@ -1,241 +1,245 @@
# EasyTier
[![Github release](https://img.shields.io/github/v/tag/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/releases)
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[![GitHub Test Actions](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/EasyTier/EasyTier)
[简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 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 等协议进行节点间通信。
## 特性
## 安装
### 核心特性
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://www.easytier.top/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
- 📊 **UDP 丢包抗性**:KCP/QUIC 代理在高丢包环境下优化延迟和带宽
- 🔧 **Web 管理**:通过 Web 界面轻松配置和监控
- 🛠️ **零配置**:静态链接的可执行文件,简单部署
## 快速开始
> 下文仅描述命令行工具的使用,图形界面程序可参考下述概念自行配置。
### 📥 安装
确保已按照 [安装指南](#安装) 安装 EasyTier,并且 easytier-core 和 easytier-cli 两个命令都已经可用。
选择最适合您需求的安装方式:
### 双节点组网
Linux(推荐):
```bash
curl -fsSL "https://github.com/EasyTier/EasyTier/blob/main/script/install.sh?raw=true" | sudo bash -s install
```
假设双节点的网络拓扑如下
HomebrewMacOS/Linux):
```bash
brew tap brewforge/chinese
brew install --cask easytier-gui
```
Windows(推荐,请以管理员权限运行):
```powershell
irm "https://github.com/EasyTier/EasyTier/blob/main/script/install.ps1?raw=true" | iex
```
通过 cargo 安装(最新开发版本):
```bash
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
[下载预编译文件](https://github.com/EasyTier/EasyTier/releases)(推荐,支持所有平台)
[通过 Docker 安装](https://easytier.cn/guide/installation.html#%E5%AE%89%E8%A3%85%E6%96%B9%E5%BC%8F)
[安装 OpenWrt ipk 软件包](https://github.com/EasyTier/luci-app-easytier)
附加步骤:
[一键注册系统服务](https://easytier.cn/guide/network/oneclick-install-as-service.html)(系统启动时自动后台运行)
### 🚀 基本用法
#### 使用共享节点快速组网
EasyTier 支持使用共享节点快速组网。当您没有公网 IP 时,可以使用公共共享节点。节点会自动尝试 NAT 穿透并建立 P2P 连接。当 P2P 失败时,数据将通过共享节点中继。
使用共享节点时,每个进入网络的节点需要提供相同的 `--network-name``--network-secret` 参数作为网络的唯一标识符。
以两个节点为例(请使用更复杂的网络名称以避免冲突):
1. 在节点 A 上运行:
```bash
# 以管理员权限运行
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>:11010
```
2. 在节点 B 上运行:
```bash
# 以管理员权限运行
sudo easytier-core -d --network-name abc --network-secret abc -p tcp://<共享节点IP>: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.6.0-70e69a38~ |
| 10.126.126.2 | abc-2 | p2p | 3.452 | 0 | 17.33 kB | 20.42 kB | udp | FullCone | 390879727 | 2.6.0-70e69a38~ |
| | PublicServer_a | p2p | 27.796 | 0.000 | 50.01 kB | 67.46 kB | tcp | Unknown | 3771642457 | 2.6.0-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://<公共节点IP>:11010 -p udp://<公共节点IP>:11010
```
#### 去中心化组网
EasyTier 本质上是去中心化的,没有服务器和客户端的区分。只要一个设备能与虚拟网络中的任何节点通信,它就可以加入虚拟网络。以下是如何设置去中心化网络:
1. 启动第一个节点(节点 A):
```bash
# 启动第一个节点
sudo easytier-core -i 10.144.144.1
```
启动后,该节点将默认监听以下端口:
- TCP11010
- UDP11010
- WebSocket11011
- WebSocket SSL11012
- WireGuard11013
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
```
命令执行成功会有如下打印。
![alt text](/assets/image-2.png)
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
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
### 多节点组网
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
```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
```
![alt text](/assets/image-3.png)
2. 测试节点 A 是否可访问被代理子网下的节点
```sh
ping 10.1.1.2
```
---
### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.top:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010
```bash
# 查看路由信息
easytier-cli route
```
节点 B 执行
![路由信息](/assets/image-3.png)
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top: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]]
@@ -243,89 +247,79 @@ 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.top: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 解决方案。
- [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞等特性。
- [ ] 支持 iOS。
- [ ] 支持 Web 配置管理。
### 联系我们
## 社区和贡献
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [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/q/wFoTUChqZW)
- 二群 [837676408](https://qm.qq.com/q/4V33DrfgHe)
- 三群 [957189589](https://qm.qq.com/q/YNyTQjwlai)
## 许可证
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
- Telegramhttps://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>
+116
View File
@@ -0,0 +1,116 @@
# Android build environment
{
pkgs,
nixpkgs,
system,
}:
let
androidEnv = pkgs.callPackage "${nixpkgs}/pkgs/development/mobile/androidenv" {
inherit pkgs;
licenseAccepted = true;
};
includeAuto = pkgs.stdenv.hostPlatform.isx86_64 || pkgs.stdenv.hostPlatform.isDarwin;
ndkVersion = "26.1.10909125";
ndkVersions = [ ndkVersion ];
sdkArgs = {
includeNDK = true;
includeSources = true;
includeSystemImages = false;
includeEmulator = false;
inherit ndkVersions;
useGoogleAPIs = true;
useGoogleTVAddOns = true;
buildToolsVersions = [ "34.0.0" ];
numLatestPlatformVersions = 10;
includeExtras = [
"extras;google;gcm"
]
++ pkgs.lib.optionals includeAuto [
"extras;google;auto"
];
extraLicenses = [
"android-sdk-preview-license"
"android-googletv-license"
"android-sdk-arm-dbt-license"
"google-gdk-license"
"intel-android-extra-license"
"intel-android-sysimage-license"
"mips-android-sysimage-license"
];
};
androidComposition = androidEnv.composeAndroidPackages sdkArgs;
androidSdk = androidComposition.androidsdk;
platformTools = androidComposition.platform-tools;
cmake = androidComposition.cmake;
ndkHostTag =
if pkgs.stdenv.isLinux then
"linux-x86_64"
else if pkgs.stdenv.isDarwin then
"darwin-x86_64"
else
"";
ndkToolchain = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}/toolchains/llvm/prebuilt/${ndkHostTag}";
in
{
inherit
androidSdk
platformTools
cmake
ndkToolchain
ndkVersion
;
# List of packages required for Android development
packages = [
pkgs.jdk # openjdk 21
androidSdk
platformTools
cmake
pkgs.glibc_multi.dev
];
# Provide Rust extensions/targets for use by the upper-level flake
rust = {
extensions = [ "rust-std" ];
targets = [
"aarch64-linux-android"
"armv7-linux-androideabi"
"i686-linux-android"
"x86_64-linux-android"
];
};
# Android environment variables and shellHook
envVars = {
LANG = "C.UTF-8";
LC_ALL = "C.UTF-8";
JAVA_HOME = "${pkgs.jdk}/lib/openjdk";
ANDROID_SDK_ROOT = "${androidSdk}/libexec/android-sdk";
ANDROID_NDK_ROOT = "\${ANDROID_SDK_ROOT}/ndk-bundle";
NDK_HOME = "${androidSdk}/libexec/android-sdk/ndk/${ndkVersion}";
LIBCLANG_PATH = "${ndkToolchain}/lib";
KCP_SYS_EXTRA_HEADER_PATH = "${ndkToolchain}/lib/clang/19/include:${pkgs.glibc_multi.dev}/include";
ZSTD_SYS_STATIC = "1";
BINDGEN_EXTRA_CLANG_ARGS = "--sysroot=${ndkToolchain}/sysroot -I${ndkToolchain}/lib/clang/17/include ";
shellHook = ''
echo "Android environment activated"
export GRADLE_OPTS="-Dorg.gradle.project.android.aapt2FromMavenOverride=$(echo "$ANDROID_SDK_ROOT/build-tools/"*"/aapt2")"
cmake_root="$(echo "$ANDROID_SDK_ROOT/cmake/"*/)"
export PATH="$cmake_root/bin:$PATH"
unset NIX_CFLAGS_COMPILE
unset NIX_CFLAGS_COMPILE_FOR_BUILD
cat <<EOF > easytier-gui/local.properties
sdk.dir=$ANDROID_SDK_ROOT
ndk.dir=$ANDROID_NDK_ROOT
cmake.dir=$cmake_root
EOF
'';
};
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -0,0 +1,16 @@
[package]
name = "easytier-android-jni"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
jni = "0.21"
once_cell = "1.18.0"
log = "0.4"
android_logger = "0.13"
serde = { version = "1.0.220", features = ["derive"] }
serde_json = "1.0"
easytier = { path = "../../easytier" }
@@ -0,0 +1,267 @@
# EasyTier Android JNI
这是 EasyTier 的 Android JNI 绑定库,允许 Android 应用程序调用 EasyTier 的网络功能。
## 功能特性
- 🚀 完整的 EasyTier FFI 接口封装
- 📱 原生 Android JNI 支持
- 🔧 支持多种 Android 架构 (arm64-v8a, armeabi-v7a, x86, x86_64)
- 🛡️ 类型安全的 Java 接口
- 📝 详细的错误处理和日志记录
## 支持的架构
- `arm64-v8a` (aarch64-linux-android)
- `armeabi-v7a` (armv7-linux-androideabi)
- `x86` (i686-linux-android)
- `x86_64` (x86_64-linux-android)
## 构建要求
### 系统要求
- Rust 1.70+
- Android NDK r21+
- Linux/macOS 开发环境
### 环境设置
1. **安装 Rust**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
```
2. **安装 Android NDK**
- 下载 Android NDK: https://developer.android.com/ndk/downloads
- 解压到合适的目录
- 设置环境变量:
```bash
export ANDROID_NDK_ROOT=/path/to/android-ndk
```
3. **添加 Android 目标**
```bash
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
```
## 构建步骤
1. **克隆项目并进入目录**
```bash
cd /path/to/EasyTier/easytier-contrib/easytier-android-jni
```
2. **运行构建脚本**
```bash
./build.sh
```
3. **构建完成后,库文件将生成在 `target/android/` 目录下**
```
target/android/
├── arm64-v8a/
│ └── libeasytier_android_jni.so
├── armeabi-v7a/
│ └── libeasytier_android_jni.so
├── x86/
│ └── libeasytier_android_jni.so
└── x86_64/
└── libeasytier_android_jni.so
```
## Android 项目集成
### 1. 复制库文件
将生成的 `.so` 文件复制到您的 Android 项目中:
```
your-android-project/
└── src/main/
├── jniLibs/
│ ├── arm64-v8a/
│ │ └── libeasytier_android_jni.so
│ ├── armeabi-v7a/
│ │ └── libeasytier_android_jni.so
│ ├── x86/
│ │ └── libeasytier_android_jni.so
│ └── x86_64/
│ └── libeasytier_android_jni.so
└── java/
└── com/easytier/jni/
└── EasyTierJNI.java
```
### 2. 复制 Java 接口
将 `java/com/easytier/jni/EasyTierJNI.java` 复制到您的 Android 项目的相应包路径下。
### 3. 添加权限
在 `AndroidManifest.xml` 中添加必要的权限:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
```
## 使用示例
### 基本使用
```java
import com.easytier.jni.EasyTierJNI;
import java.util.Map;
public class EasyTierManager {
// 初始化网络实例
public void startNetwork() {
String config = """
inst_name = "my_instance"
network = "my_network"
""";
try {
// 解析配置
int result = EasyTierJNI.parseConfig(config);
if (result != 0) {
String error = EasyTierJNI.getLastError();
throw new RuntimeException("配置解析失败: " + error);
}
// 启动网络实例
result = EasyTierJNI.runNetworkInstance(config);
if (result != 0) {
String error = EasyTierJNI.getLastError();
throw new RuntimeException("网络实例启动失败: " + error);
}
System.out.println("EasyTier 网络实例启动成功");
} catch (RuntimeException e) {
System.err.println("启动失败: " + e.getMessage());
}
}
// 获取网络信息
public void getNetworkInfo() {
try {
Map<String, String> infos = EasyTierJNI.collectNetworkInfosAsMap(10);
for (Map.Entry<String, String> entry : infos.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
} catch (RuntimeException e) {
System.err.println("获取网络信息失败: " + e.getMessage());
}
}
// 停止所有实例
public void stopNetwork() {
try {
int result = EasyTierJNI.stopAllInstances();
if (result == 0) {
System.out.println("所有网络实例已停止");
}
} catch (RuntimeException e) {
System.err.println("停止网络失败: " + e.getMessage());
}
}
}
```
### VPN 服务集成
如果您要在 Android VPN 服务中使用:
```java
public class EasyTierVpnService extends VpnService {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 建立 VPN 连接
ParcelFileDescriptor vpnInterface = establishVpnInterface();
if (vpnInterface != null) {
int fd = vpnInterface.getFd();
// 设置 TUN 文件描述符
try {
EasyTierJNI.setTunFd("my_instance", fd);
} catch (RuntimeException e) {
Log.e("EasyTier", "设置 TUN FD 失败", e);
}
}
return START_STICKY;
}
private ParcelFileDescriptor establishVpnInterface() {
Builder builder = new Builder();
builder.setMtu(1500);
builder.addAddress("10.0.0.2", 24);
builder.addRoute("0.0.0.0", 0);
builder.setSession("EasyTier VPN");
return builder.establish();
}
}
```
## API 参考
### EasyTierJNI 类方法
| 方法 | 描述 | 参数 | 返回值 |
|------|------|------|--------|
| `parseConfig(String config)` | 解析 TOML 配置 | config: 配置字符串 | 0=成功, -1=失败 |
| `runNetworkInstance(String config)` | 启动网络实例 | config: 配置字符串 | 0=成功, -1=失败 |
| `setTunFd(String instanceName, int fd)` | 设置 TUN 文件描述符 | instanceName: 实例名, fd: 文件描述符 | 0=成功, -1=失败 |
| `retainNetworkInstance(String[] names)` | 保留指定实例 | names: 实例名数组 | 0=成功, -1=失败 |
| `collectNetworkInfos(int maxLength)` | 收集网络信息 | maxLength: 最大条目数 | 信息字符串数组 |
| `collectNetworkInfosAsMap(int maxLength)` | 收集网络信息为 Map | maxLength: 最大条目数 | Map<String, String> |
| `getLastError()` | 获取最后错误 | 无 | 错误消息字符串 |
| `stopAllInstances()` | 停止所有实例 | 无 | 0=成功, -1=失败 |
| `retainSingleInstance(String name)` | 保留单个实例 | name: 实例名 | 0=成功, -1=失败 |
## 故障排除
### 常见问题
1. **构建失败: "Android NDK not found"**
- 确保设置了 `ANDROID_NDK_ROOT` 环境变量
- 检查 NDK 路径是否正确
2. **运行时错误: "java.lang.UnsatisfiedLinkError"**
- 确保 `.so` 文件放在正确的 `jniLibs` 目录下
- 检查目标架构是否匹配
3. **配置解析失败**
- 检查 TOML 配置格式是否正确
- 使用 `getLastError()` 获取详细错误信息
### 调试技巧
- 启用 Android 日志查看 JNI 层的日志输出
- 使用 `adb logcat -s EasyTier-JNI` 查看相关日志
- 检查 `getLastError()` 返回的错误信息
## 许可证
本项目遵循与 EasyTier 主项目相同的许可证。
## 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目。
## 相关链接
- [EasyTier 主项目](https://github.com/EasyTier/EasyTier)
- [Android NDK 文档](https://developer.android.com/ndk)
- [Rust JNI 文档](https://docs.rs/jni/)
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# EasyTier Android JNI 构建脚本
# 用于编译适用于 Android 平台的 JNI 库
# 使用 cargo-ndk 工具简化 Android 编译过程
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
REPO_ROOT=$(git rev-parse --show-toplevel)
echo -e "${GREEN}EasyTier Android JNI 构建脚本 (使用 cargo-ndk)${NC}"
echo "=============================================="
# 检查 Rust 是否安装
if ! command -v rustc &> /dev/null; then
echo -e "${RED}错误: 未找到 Rust 编译器,请先安装 Rust${NC}"
exit 1
fi
# 检查 cargo 是否安装
if ! command -v cargo &> /dev/null; then
echo -e "${RED}错误: 未找到 Cargo,请先安装 Rust 工具链${NC}"
exit 1
fi
# 检查 cargo-ndk 是否安装
if ! cargo ndk --version &> /dev/null; then
echo -e "${YELLOW}cargo-ndk 未安装,正在安装...${NC}"
cargo install cargo-ndk
if ! cargo ndk --version &> /dev/null; then
echo -e "${RED}错误: cargo-ndk 安装失败${NC}"
exit 1
fi
fi
echo -e "${GREEN}cargo-ndk 版本: $(cargo ndk --version)${NC}"
# Android 目标架构映射 (cargo-ndk 使用的架构名称)
# ANDROID_TARGETS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
ANDROID_TARGETS=("arm64-v8a")
# Android 架构到 Rust target 的映射
declare -A TARGET_MAP
TARGET_MAP["arm64-v8a"]="aarch64-linux-android"
TARGET_MAP["armeabi-v7a"]="armv7-linux-androideabi"
TARGET_MAP["x86"]="i686-linux-android"
TARGET_MAP["x86_64"]="x86_64-linux-android"
# 检查并安装所需的 Rust target
echo -e "${YELLOW}检查并安装 Android 目标架构...${NC}"
for android_target in "${ANDROID_TARGETS[@]}"; do
rust_target="${TARGET_MAP[$android_target]}"
if ! rustup target list --installed | grep -q "$rust_target"; then
echo -e "${YELLOW}安装目标架构: $rust_target (for $android_target)${NC}"
rustup target add "$rust_target"
else
echo -e "${GREEN}目标架构已安装: $rust_target (for $android_target)${NC}"
fi
done
# 创建输出目录
OUTPUT_DIR="./target/android"
mkdir -p "$OUTPUT_DIR"
# 构建函数
build_for_target() {
local android_target=$1
echo -e "${YELLOW}构建目标: $android_target${NC}"
# 首先构建 easytier-ffi
echo -e "${YELLOW}构建 easytier-ffi for $android_target${NC}"
(cd $REPO_ROOT/easytier-contrib/easytier-ffi && cargo ndk -t $android_target build --release)
# 构建 JNI 库
cargo ndk -t $android_target build --release
# 复制库文件到输出目录
# cargo-ndk 使用 Rust target 名称作为目录名,而不是 Android 架构名称
rust_target="${TARGET_MAP[$android_target]}"
mkdir -p "$OUTPUT_DIR/$android_target"
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_android_jni.so" "$OUTPUT_DIR/$android_target/"
cp "$REPO_ROOT/target/$rust_target/release/libeasytier_ffi.so" "$OUTPUT_DIR/$android_target/"
echo -e "${GREEN}库文件已复制到: $OUTPUT_DIR/$android_target/${NC}"
}
# 检查 Android NDK (cargo-ndk 会自动处理 NDK 路径)
if [ -z "$ANDROID_NDK_ROOT" ] && [ -z "$ANDROID_NDK_HOME" ] && [ -z "$NDK_HOME" ]; then
echo -e "${YELLOW}警告: 未设置 Android NDK 环境变量${NC}"
echo "cargo-ndk 将尝试自动检测 NDK 路径"
echo "如果构建失败,请设置以下环境变量之一:"
echo " - ANDROID_NDK_ROOT"
echo " - ANDROID_NDK_HOME"
echo " - NDK_HOME"
else
if [ -n "$ANDROID_NDK_ROOT" ]; then
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_ROOT${NC}"
elif [ -n "$ANDROID_NDK_HOME" ]; then
echo -e "${GREEN}使用 Android NDK: $ANDROID_NDK_HOME${NC}"
elif [ -n "$NDK_HOME" ]; then
echo -e "${GREEN}使用 Android NDK: $NDK_HOME${NC}"
fi
fi
# 构建所有目标
echo -e "${YELLOW}开始构建所有目标架构...${NC}"
for target in "${ANDROID_TARGETS[@]}"; do
build_for_target "$target"
done
echo -e "${GREEN}构建完成!${NC}"
echo -e "${GREEN}所有库文件已生成到: $OUTPUT_DIR${NC}"
echo ""
echo "目录结构:"
ls -la "$OUTPUT_DIR"/*/
echo ""
echo -e "${YELLOW}使用说明:${NC}"
echo "1. 将生成的 .so 文件复制到您的 Android 项目的 src/main/jniLibs/ 目录下"
echo "2. 将 java/com/easytier/jni/EasyTierJNI.java 复制到您的 Android 项目中"
echo "3. 在您的 Android 代码中调用 EasyTierJNI 类的方法"
echo ""
echo -e "${GREEN}注意: 此脚本使用 cargo-ndk 工具,无需手动设置复杂的环境变量${NC}"
echo -e "${GREEN}cargo-ndk 会自动处理交叉编译所需的工具链配置${NC}"
@@ -0,0 +1,56 @@
# EasyTier Android JNI 示例配置文件
# 这是一个基本的配置示例,展示如何配置 EasyTier 网络实例
# 实例名称 (必需)
inst_name = "android_instance"
# 网络名称 (必需)
network = "my_easytier_network"
# 网络密钥 (可选,用于网络加密)
# network_secret = "your_secret_key_here"
# 监听地址 (可选)
# listeners = ["tcp://0.0.0.0:11010", "udp://0.0.0.0:11010"]
# 对等节点地址 (可选)
# peers = ["tcp://peer1.example.com:11010", "udp://peer2.example.com:11010"]
# 虚拟 IP 地址 (可选)
# ipv4 = "10.144.144.1"
# 主机名 (可选)
# hostname = "android-device"
# 启用 IPv6 (可选)
# ipv6 = "fd00::1"
# 代理网络 (可选)
# proxy_networks = ["192.168.1.0/24"]
# 退出节点 (可选)
# exit_nodes = ["peer1"]
# 启用加密 (可选)
# enable_encryption = true
# 启用 IPv4 转发 (可选)
# enable_ipv4 = true
# 启用 IPv6 转发 (可选)
# enable_ipv6 = false
# MTU 设置 (可选)
# mtu = 1420
# 日志级别 (可选: error, warn, info, debug, trace)
# log_level = "info"
# 禁用 P2P (可选)
# disable_p2p = false
# 使用多路径 (可选)
# use_multi_path = true
# 延迟优先 (可选)
# latency_first = false
@@ -0,0 +1,78 @@
package com.easytier.jni
/** EasyTier JNI 接口类 提供 Android 应用调用 EasyTier 网络功能的接口 */
object EasyTierJNI {
init {
// 加载本地库
System.loadLibrary("easytier_android_jni")
}
/**
* 设置 TUN 文件描述符
* @param instanceName 实例名称
* @param fd TUN 文件描述符
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun setTunFd(instanceName: String, fd: Int): Int
/**
* 解析配置字符串
* @param config TOML 格式的配置字符串
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当配置解析失败时抛出异常
*/
@JvmStatic external fun parseConfig(config: String): Int
/**
* 运行网络实例
* @param config TOML 格式的配置字符串
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当实例启动失败时抛出异常
*/
@JvmStatic external fun runNetworkInstance(config: String): Int
/**
* 保留指定的网络实例,停止其他实例
* @param instanceNames 要保留的实例名称数组,传入 null 或空数组将停止所有实例
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun retainNetworkInstance(instanceNames: Array<String>?): Int
/**
* 收集网络信息
* @param maxLength 最大返回条目数
* @return 包含网络信息的字符串数组,每个元素格式为 "key=value"
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic external fun collectNetworkInfos(maxLength: Int): String?
/**
* 获取最后的错误消息
* @return 错误消息字符串,如果没有错误则返回 null
*/
@JvmStatic external fun getLastError(): String?
/**
* 便利方法:停止所有网络实例
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic
fun stopAllInstances(): Int {
return retainNetworkInstance(null)
}
/**
* 便利方法:停止指定实例外的所有实例
* @param instanceName 要保留的实例名称
* @return 0 表示成功,-1 表示失败
* @throws RuntimeException 当操作失败时抛出异常
*/
@JvmStatic
fun retainSingleInstance(instanceName: String): Int {
return retainNetworkInstance(arrayOf(instanceName))
}
}
@@ -0,0 +1,252 @@
package com.easytier.jni
import android.app.Activity
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.squareup.moshi.Moshi
import com.squareup.wire.WireJsonAdapterFactory
import common.Ipv4Inet
import web.NetworkInstanceRunningInfoMap
fun parseIpv4InetToString(inet: Ipv4Inet?): String? {
val addr = inet?.address?.addr ?: return null
val networkLength = inet.network_length
// 将 int32 转换为 IPv4 字符串
val ip =
String.format(
"%d.%d.%d.%d",
(addr shr 24) and 0xFF,
(addr shr 16) and 0xFF,
(addr shr 8) and 0xFF,
addr and 0xFF
)
return "$ip/$networkLength"
}
/** EasyTier 管理类 负责管理 EasyTier 实例的生命周期、监控网络状态变化、控制 VpnService */
class EasyTierManager(
private val activity: Activity,
private val instanceName: String,
private val networkConfig: String
) {
companion object {
private const val TAG = "EasyTierManager"
private const val MONITOR_INTERVAL = 3000L // 3秒监控间隔
}
private val handler = Handler(Looper.getMainLooper())
private var isRunning = false
private var currentIpv4: String? = null
private var currentProxyCidrs: List<String> = emptyList()
private var vpnServiceIntent: Intent? = null
// JSON 解析器
private val moshi = Moshi.Builder().add(WireJsonAdapterFactory()).build()
private val adapter = moshi.adapter(NetworkInstanceRunningInfoMap::class.java)
// 监控任务
private val monitorRunnable =
object : Runnable {
override fun run() {
if (isRunning) {
monitorNetworkStatus()
handler.postDelayed(this, MONITOR_INTERVAL)
}
}
}
/** 启动 EasyTier 实例和监控 */
fun start() {
if (isRunning) {
Log.w(TAG, "EasyTier 实例已经在运行中")
return
}
try {
// 启动 EasyTier 实例
val result = EasyTierJNI.runNetworkInstance(networkConfig)
if (result == 0) {
isRunning = true
Log.i(TAG, "EasyTier 实例启动成功: $instanceName")
// 开始监控网络状态
handler.post(monitorRunnable)
} else {
Log.e(TAG, "EasyTier 实例启动失败: $result")
val error = EasyTierJNI.getLastError()
Log.e(TAG, "错误信息: $error")
}
} catch (e: Exception) {
Log.e(TAG, "启动 EasyTier 实例时发生异常", e)
}
}
/** 停止 EasyTier 实例和监控 */
fun stop() {
if (!isRunning) {
Log.w(TAG, "EasyTier 实例未在运行")
return
}
isRunning = false
// 停止监控任务
handler.removeCallbacks(monitorRunnable)
try {
// 停止 VpnService
stopVpnService()
// 停止 EasyTier 实例
EasyTierJNI.stopAllInstances()
Log.i(TAG, "EasyTier 实例已停止: $instanceName")
// 重置状态
currentIpv4 = null
currentProxyCidrs = emptyList()
} catch (e: Exception) {
Log.e(TAG, "停止 EasyTier 实例时发生异常", e)
}
}
/** 监控网络状态 */
private fun monitorNetworkStatus() {
try {
val infosJson = EasyTierJNI.collectNetworkInfos(10)
if (infosJson.isNullOrEmpty()) {
Log.d(TAG, "未获取到网络信息")
return
}
val networkInfoMap = parseNetworkInfo(infosJson)
val networkInfo = networkInfoMap?.map?.get(instanceName)
if (networkInfo == null) {
Log.d(TAG, "未找到实例 $instanceName 的网络信息")
return
}
Log.d(TAG, "网络信息: $networkInfo")
// 检查实例是否正在运行
if (!networkInfo.running) {
Log.w(TAG, "EasyTier 实例未运行: ${networkInfo.error_msg}")
return
}
val newIpv4Inet = networkInfo.my_node_info?.virtual_ipv4
if (newIpv4Inet == null) {
Log.w(TAG, "EasyTier No Ipv4: $networkInfo")
return
}
// 获取当前节点的 IPv4 地址
val newIpv4 = parseIpv4InetToString(newIpv4Inet)
// 获取所有节点的 proxy_cidrs
val newProxyCidrs = mutableListOf<String>()
networkInfo.routes?.forEach { route ->
route.proxy_cidrs?.let { cidrs -> newProxyCidrs.addAll(cidrs) }
}
// 检查是否有变化
val ipv4Changed = newIpv4 != currentIpv4
val proxyCidrsChanged = newProxyCidrs != currentProxyCidrs
if (ipv4Changed || proxyCidrsChanged) {
Log.i(TAG, "网络状态发生变化:")
Log.i(TAG, " IPv4: $currentIpv4 -> $newIpv4")
Log.i(TAG, " Proxy CIDRs: $currentProxyCidrs -> $newProxyCidrs")
// 更新状态
currentIpv4 = newIpv4
currentProxyCidrs = newProxyCidrs.toList()
// 重启 VpnService
if (newIpv4 != null) {
restartVpnService(newIpv4, newProxyCidrs)
}
} else {
Log.d(TAG, "网络状态无变化 - IPv4: $currentIpv4, Proxy CIDRs: ${currentProxyCidrs.size}")
}
} catch (e: Exception) {
Log.e(TAG, "监控网络状态时发生异常", e)
}
}
/** 解析网络信息 JSON */
private fun parseNetworkInfo(jsonString: String): NetworkInstanceRunningInfoMap? {
return try {
adapter.fromJson(jsonString)
} catch (e: Exception) {
Log.e(TAG, "解析网络信息失败", e)
null
}
}
/** 重启 VpnService */
private fun restartVpnService(ipv4: String, proxyCidrs: List<String>) {
try {
// 先停止现有的 VpnService
stopVpnService()
// 启动新的 VpnService
startVpnService(ipv4, proxyCidrs)
} catch (e: Exception) {
Log.e(TAG, "重启 VpnService 时发生异常", e)
}
}
/** 启动 VpnService */
private fun startVpnService(ipv4: String, proxyCidrs: List<String>) {
try {
val intent = Intent(activity, EasyTierVpnService::class.java)
intent.putExtra("ipv4_address", ipv4)
intent.putStringArrayListExtra("proxy_cidrs", ArrayList(proxyCidrs))
intent.putExtra("instance_name", instanceName)
activity.startService(intent)
vpnServiceIntent = intent
Log.i(TAG, "VpnService 已启动 - IPv4: $ipv4, Proxy CIDRs: $proxyCidrs")
} catch (e: Exception) {
Log.e(TAG, "启动 VpnService 时发生异常", e)
}
}
/** 停止 VpnService */
private fun stopVpnService() {
try {
vpnServiceIntent?.let { intent ->
activity.stopService(intent)
Log.i(TAG, "VpnService 已停止")
}
vpnServiceIntent = null
} catch (e: Exception) {
Log.e(TAG, "停止 VpnService 时发生异常", e)
}
}
/** 获取当前状态信息 */
fun getStatus(): EasyTierStatus {
return EasyTierStatus(
isRunning = isRunning,
instanceName = instanceName,
currentIpv4 = currentIpv4,
currentProxyCidrs = currentProxyCidrs.toList()
)
}
/** 状态数据类 */
data class EasyTierStatus(
val isRunning: Boolean,
val instanceName: String,
val currentIpv4: String?,
val currentProxyCidrs: List<String>
)
}
@@ -0,0 +1,143 @@
package com.easytier.jni
import android.content.Intent
import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
import kotlin.concurrent.thread
class EasyTierVpnService : VpnService() {
private var vpnInterface: ParcelFileDescriptor? = null
private var isRunning = false
private var instanceName: String? = null
companion object {
private const val TAG = "EasyTierVpnService"
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "VPN Service created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 获取传入的参数
val ipv4Address = intent?.getStringExtra("ipv4_address")
val proxyCidrs = intent?.getStringArrayListExtra("proxy_cidrs") ?: arrayListOf()
instanceName = intent?.getStringExtra("instance_name")
if (ipv4Address == null || instanceName == null) {
Log.e(TAG, "缺少必要参数: ipv4Address=$ipv4Address, instanceName=$instanceName")
stopSelf()
return START_NOT_STICKY
}
Log.i(
TAG,
"启动 VPN Service - IPv4: $ipv4Address, Proxy CIDRs: $proxyCidrs, Instance: $instanceName"
)
thread {
try {
setupVpnInterface(ipv4Address, proxyCidrs)
} catch (t: Throwable) {
Log.e(TAG, "VPN 设置失败", t)
stopSelf()
}
}
return START_STICKY
}
private fun setupVpnInterface(ipv4Address: String, proxyCidrs: List<String>) {
try {
// 解析 IPv4 地址和网络长度
val (ip, networkLength) = parseIpv4Address(ipv4Address)
// 1. 准备 VpnService.Builder
val builder = Builder()
builder.setSession("EasyTier VPN")
.addAddress(ip, networkLength)
.addDnsServer("223.5.5.5")
.addDnsServer("114.114.114.114")
.addDisallowedApplication("com.easytier.easytiervpn")
// 2. 添加路由表 - 为每个 proxy CIDR 添加路由
proxyCidrs.forEach { cidr ->
try {
val (routeIp, routeLength) = parseCidr(cidr)
builder.addRoute(routeIp, routeLength)
Log.d(TAG, "添加路由: $routeIp/$routeLength")
} catch (e: Exception) {
Log.w(TAG, "解析 CIDR 失败: $cidr", e)
}
}
// 3. 构建虚拟网络接口
vpnInterface = builder.establish()
if (vpnInterface == null) {
Log.e(TAG, "创建 VPN 接口失败")
return
}
Log.i(TAG, "VPN 接口创建成功")
// 4. 将 TUN 文件描述符传递给 EasyTier
instanceName?.let { name ->
val fd = vpnInterface!!.fd
val result = EasyTierJNI.setTunFd(name, fd)
if (result == 0) {
Log.i(TAG, "TUN 文件描述符设置成功: $fd")
} else {
Log.e(TAG, "TUN 文件描述符设置失败: $result")
}
}
isRunning = true
// 5. 保持服务运行
while (isRunning && vpnInterface != null) {
Thread.sleep(1000)
}
} catch (t: Throwable) {
Log.e(TAG, "VPN 接口设置过程中发生错误", t)
} finally {
cleanup()
}
}
/** 解析 IPv4 地址,返回 IP 和网络长度 */
private fun parseIpv4Address(ipv4Address: String): Pair<String, Int> {
return if (ipv4Address.contains("/")) {
val parts = ipv4Address.split("/")
Pair(parts[0], parts[1].toInt())
} else {
// 默认使用 /24 网络
Pair(ipv4Address, 24)
}
}
/** 解析 CIDR,返回 IP 和网络长度 */
private fun parseCidr(cidr: String): Pair<String, Int> {
val parts = cidr.split("/")
if (parts.size != 2) {
throw IllegalArgumentException("无效的 CIDR 格式: $cidr")
}
return Pair(parts[0], parts[1].toInt())
}
private fun cleanup() {
isRunning = false
vpnInterface?.close()
vpnInterface = null
Log.i(TAG, "VPN 接口已清理")
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "VPN Service destroyed")
cleanup()
}
}
@@ -0,0 +1,41 @@
# 使用说明
1. 需要将 proto 文件放入 app/src/main/proto
2. android/gradle/libs.versions.toml 中加入依赖
```
# Wire 核心运行时
android-wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version = "5.3.11" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
android-wire-moshi-adapter = { group = "com.squareup.wire", name = "wire-moshi-adapter", version = "5.3.11" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.9.0" }
```
3. build.gradle.kts 中加入
```
plugins {
...
alias(libs.plugins.wire)
}
dependencies {
...
implementation(libs.android.wire.runtime)
implementation(libs.android.wire.moshi.adapter)
implementation(libs.moshi)
}
...
wire {
kotlin {
rpcRole = "none"
}
}
```
4. 调用 easytier-contrib/easytier-android-jni/build.sh 生成 jni 和 ffi 的 so 文件。
并将生成的 so 文件放到 android/app/src/main/jniLibs/arm64-v8a 目录下。
5. 使用 EasyTierManager 可以拉起 EasyTier 实例并启动 Android VpnService 组件。
@@ -0,0 +1,319 @@
use easytier::proto::api::manage::{NetworkInstanceRunningInfo, NetworkInstanceRunningInfoMap};
use jni::objects::{JClass, JObjectArray, JString};
use jni::sys::{jint, jstring};
use jni::JNIEnv;
use once_cell::sync::Lazy;
use std::ffi::{CStr, CString};
use std::ptr;
// 定义 KeyValuePair 结构体
#[repr(C)]
#[derive(Clone, Copy)]
pub struct KeyValuePair {
pub key: *const std::ffi::c_char,
pub value: *const std::ffi::c_char,
}
// 声明外部 C 函数
extern "C" {
fn set_tun_fd(inst_name: *const std::ffi::c_char, fd: std::ffi::c_int) -> std::ffi::c_int;
fn get_error_msg(out: *mut *const std::ffi::c_char);
fn free_string(s: *const std::ffi::c_char);
fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int;
fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int;
fn collect_network_infos(infos: *mut KeyValuePair, max_length: usize) -> std::ffi::c_int;
}
// 初始化 Android 日志
static LOGGER_INIT: Lazy<()> = Lazy::new(|| {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("EasyTier-JNI"),
);
});
// 辅助函数:从 Java String 转换为 CString
fn jstring_to_cstring(env: &mut JNIEnv, jstr: &JString) -> Result<CString, String> {
let java_str = env
.get_string(jstr)
.map_err(|e| format!("Failed to get string: {:?}", e))?;
let rust_str = java_str.to_str().map_err(|_| "Invalid UTF-8".to_string())?;
CString::new(rust_str).map_err(|_| "String contains null byte".to_string())
}
// 辅助函数:获取错误消息
fn get_last_error() -> Option<String> {
unsafe {
let mut error_ptr: *const std::ffi::c_char = ptr::null();
get_error_msg(&mut error_ptr);
if error_ptr.is_null() {
None
} else {
let error_cstr = CStr::from_ptr(error_ptr);
let error_str = error_cstr.to_string_lossy().into_owned();
free_string(error_ptr);
Some(error_str)
}
}
}
// 辅助函数:抛出 Java 异常
fn throw_exception(env: &mut JNIEnv, message: &str) {
let _ = env.throw_new("java/lang/RuntimeException", message);
}
/// 设置 TUN 文件描述符
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_setTunFd(
mut env: JNIEnv,
_class: JClass,
inst_name: JString,
fd: jint,
) -> jint {
Lazy::force(&LOGGER_INIT);
let inst_name_cstr = match jstring_to_cstring(&mut env, &inst_name) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid instance name: {}", e));
return -1;
}
};
unsafe {
let result = set_tun_fd(inst_name_cstr.as_ptr(), fd);
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
result
}
}
/// 解析配置
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_parseConfig(
mut env: JNIEnv,
_class: JClass,
config: JString,
) -> jint {
Lazy::force(&LOGGER_INIT);
let config_cstr = match jstring_to_cstring(&mut env, &config) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid config string: {}", e));
return -1;
}
};
unsafe {
let result = parse_config(config_cstr.as_ptr());
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
result
}
}
/// 运行网络实例
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_runNetworkInstance(
mut env: JNIEnv,
_class: JClass,
config: JString,
) -> jint {
Lazy::force(&LOGGER_INIT);
let config_cstr = match jstring_to_cstring(&mut env, &config) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(&mut env, &format!("Invalid config string: {}", e));
return -1;
}
};
unsafe {
let result = run_network_instance(config_cstr.as_ptr());
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
result
}
}
/// 保持网络实例
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_retainNetworkInstance(
mut env: JNIEnv,
_class: JClass,
instance_names: JObjectArray,
) -> jint {
Lazy::force(&LOGGER_INIT);
// 处理 null 数组的情况
if instance_names.is_null() {
unsafe {
let result = retain_network_instance(ptr::null(), 0);
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
return result;
}
}
// 获取数组长度
let array_length = match env.get_array_length(&instance_names) {
Ok(len) => len as usize,
Err(e) => {
throw_exception(&mut env, &format!("Failed to get array length: {:?}", e));
return -1;
}
};
// 如果数组为空,停止所有实例
if array_length == 0 {
unsafe {
let result = retain_network_instance(ptr::null(), 0);
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
return result;
}
}
// 转换 Java 字符串数组为 C 字符串数组
let mut c_strings = Vec::with_capacity(array_length);
let mut c_string_ptrs = Vec::with_capacity(array_length);
for i in 0..array_length {
let java_string = match env.get_object_array_element(&instance_names, i as i32) {
Ok(obj) => obj,
Err(e) => {
throw_exception(
&mut env,
&format!("Failed to get array element {}: {:?}", i, e),
);
return -1;
}
};
if java_string.is_null() {
continue; // 跳过 null 元素
}
let jstring = JString::from(java_string);
let c_string = match jstring_to_cstring(&mut env, &jstring) {
Ok(cstr) => cstr,
Err(e) => {
throw_exception(
&mut env,
&format!("Invalid instance name at index {}: {}", i, e),
);
return -1;
}
};
c_string_ptrs.push(c_string.as_ptr());
c_strings.push(c_string); // 保持 CString 的所有权
}
unsafe {
let result = retain_network_instance(c_string_ptrs.as_ptr(), c_string_ptrs.len());
if result != 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
}
result
}
}
/// 收集网络信息
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_collectNetworkInfos(
mut env: JNIEnv,
_class: JClass,
) -> jstring {
Lazy::force(&LOGGER_INIT);
const MAX_INFOS: usize = 100;
let mut infos = vec![
KeyValuePair {
key: ptr::null(),
value: ptr::null(),
};
MAX_INFOS
];
unsafe {
let count = collect_network_infos(infos.as_mut_ptr(), MAX_INFOS);
if count < 0 {
if let Some(error) = get_last_error() {
throw_exception(&mut env, &error);
}
return ptr::null_mut();
}
let mut ret = NetworkInstanceRunningInfoMap::default();
// 使用 serde_json 构建 JSON
for info in infos.iter().take(count as usize) {
let key_ptr = info.key;
let val_ptr = info.value;
if key_ptr.is_null() || val_ptr.is_null() {
break;
}
let key = CStr::from_ptr(key_ptr).to_string_lossy();
let val = CStr::from_ptr(val_ptr).to_string_lossy();
let value = match serde_json::from_str::<NetworkInstanceRunningInfo>(val.as_ref()) {
Ok(v) => v,
Err(_) => {
throw_exception(&mut env, "Failed to parse JSON");
continue;
}
};
ret.map.insert(key.to_string(), value);
}
let json_str = serde_json::to_string(&ret).unwrap_or_else(|_| "{}".to_string());
match env.new_string(&json_str) {
Ok(jstr) => jstr.into_raw(),
Err(_) => {
throw_exception(&mut env, "Failed to create JSON string");
ptr::null_mut()
}
}
}
}
/// 获取最后的错误信息
#[no_mangle]
pub extern "system" fn Java_com_easytier_jni_EasyTierJNI_getLastError(
env: JNIEnv,
_class: JClass,
) -> jstring {
match get_last_error() {
Some(error) => match env.new_string(&error) {
Ok(jstr) => jstr.into_raw(),
Err(_) => ptr::null_mut(),
},
None => ptr::null_mut(),
}
}
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "easytier-ffi"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
easytier = { path = "../../easytier" }
once_cell = "1.18.0"
dashmap = "6.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
uuid = "1.17.0"
@@ -0,0 +1,159 @@
public class EasyTierFFI
{
// 导入 DLL 函数
private const string DllName = "easytier_ffi.dll";
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int parse_config([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int run_network_instance([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int retain_network_instance(IntPtr instNames, int length);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int collect_network_infos(IntPtr infos, int maxLength);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern void get_error_msg(out IntPtr errorMsg);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_string(IntPtr str);
// 定义 KeyValuePair 结构体
[StructLayout(LayoutKind.Sequential)]
public struct KeyValuePair
{
public IntPtr Key;
public IntPtr Value;
}
// 解析配置
public static void ParseConfig(string config)
{
if (string.IsNullOrEmpty(config))
{
throw new ArgumentException("Configuration string cannot be null or empty.");
}
int result = parse_config(config);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
// 启动网络实例
public static void RunNetworkInstance(string config)
{
if (string.IsNullOrEmpty(config))
{
throw new ArgumentException("Configuration string cannot be null or empty.");
}
int result = run_network_instance(config);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
// 保留网络实例
public static void RetainNetworkInstances(string[] instanceNames)
{
IntPtr[] namePointers = null;
IntPtr namesPtr = IntPtr.Zero;
try
{
if (instanceNames != null && instanceNames.Length > 0)
{
namePointers = new IntPtr[instanceNames.Length];
for (int i = 0; i < instanceNames.Length; i++)
{
if (string.IsNullOrEmpty(instanceNames[i]))
{
throw new ArgumentException("Instance name cannot be null or empty.");
}
namePointers[i] = Marshal.StringToHGlobalAnsi(instanceNames[i]);
}
namesPtr = Marshal.AllocHGlobal(Marshal.SizeOf<IntPtr>() * namePointers.Length);
Marshal.Copy(namePointers, 0, namesPtr, namePointers.Length);
}
int result = retain_network_instance(namesPtr, instanceNames?.Length ?? 0);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
finally
{
if (namePointers != null)
{
foreach (var ptr in namePointers)
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
}
}
if (namesPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(namesPtr);
}
}
}
// 收集网络信息
public static KeyValuePair<string, string>[] CollectNetworkInfos(int maxLength)
{
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf<KeyValuePair>() * maxLength);
try
{
int count = collect_network_infos(buffer, maxLength);
if (count < 0)
{
throw new Exception(GetErrorMessage());
}
var result = new KeyValuePair<string, string>[count];
for (int i = 0; i < count; i++)
{
var kv = Marshal.PtrToStructure<KeyValuePair>(buffer + i * Marshal.SizeOf<KeyValuePair>());
string key = Marshal.PtrToStringAnsi(kv.Key);
string value = Marshal.PtrToStringAnsi(kv.Value);
// 释放由 FFI 分配的字符串内存
free_string(kv.Key);
free_string(kv.Value);
result[i] = new KeyValuePair<string, string>(key, value);
}
return result;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
// 获取错误信息
private static string GetErrorMessage()
{
get_error_msg(out IntPtr errorMsgPtr);
if (errorMsgPtr == IntPtr.Zero)
{
return "Unknown error";
}
string errorMsg = Marshal.PtrToStringAnsi(errorMsgPtr);
free_string(errorMsgPtr); // 释放错误信息字符串
return errorMsg;
}
}
+267
View File
@@ -0,0 +1,267 @@
use std::sync::Mutex;
use dashmap::DashMap;
use easytier::{
common::config::{ConfigFileControl, ConfigLoader as _, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
};
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()));
#[repr(C)]
pub struct KeyValuePair {
pub key: *const std::ffi::c_char,
pub value: *const std::ffi::c_char,
}
fn set_error_msg(msg: &str) {
let bytes = msg.as_bytes();
let mut msg_buf = ERROR_MSG.lock().unwrap();
let len = bytes.len();
msg_buf.resize(len, 0);
msg_buf[..len].copy_from_slice(bytes);
}
/// # Safety
/// Set the tun fd
#[no_mangle]
pub unsafe extern "C" fn set_tun_fd(
inst_name: *const std::ffi::c_char,
fd: std::ffi::c_int,
) -> std::ffi::c_int {
let inst_name = unsafe {
assert!(!inst_name.is_null());
std::ffi::CStr::from_ptr(inst_name)
.to_string_lossy()
.into_owned()
};
if !INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
return -1;
}
let inst_id = *INSTANCE_NAME_ID_MAP
.get(&inst_name)
.as_ref()
.unwrap()
.value();
match INSTANCE_MANAGER.set_tun_fd(&inst_id, fd) {
Ok(_) => 0,
Err(_) => -1,
}
}
/// # Safety
/// Get the last error message
#[no_mangle]
pub unsafe extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
let msg_buf = ERROR_MSG.lock().unwrap();
if msg_buf.is_empty() {
unsafe {
*out = std::ptr::null();
}
return;
}
let cstr = std::ffi::CString::new(&msg_buf[..]).unwrap();
unsafe {
*out = cstr.into_raw();
}
}
#[no_mangle]
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
if s.is_null() {
return;
}
unsafe {
let _ = std::ffi::CString::from_raw(s as *mut std::ffi::c_char);
}
}
/// # Safety
/// Parse the config
#[no_mangle]
pub unsafe extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
set_error_msg(&format!("failed to parse config: {:?}", e));
return -1;
}
0
}
/// # Safety
/// Run the network instance
#[no_mangle]
pub unsafe extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => cfg,
Err(e) => {
set_error_msg(&format!("failed to parse config: {}", e));
return -1;
}
};
let inst_name = cfg.get_inst_name();
if INSTANCE_NAME_ID_MAP.contains_key(&inst_name) {
set_error_msg("instance already exists");
return -1;
}
let instance_id =
match INSTANCE_MANAGER.run_network_instance(cfg, false, ConfigFileControl::STATIC_CONFIG) {
Ok(id) => id,
Err(e) => {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
};
INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id);
0
}
/// # Safety
/// Retain the network instance
#[no_mangle]
pub unsafe extern "C" fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if length == 0 {
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;
}
let inst_names = unsafe {
assert!(!inst_names.is_null());
std::slice::from_raw_parts(inst_names, length)
.iter()
.map(|&name| {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>()
};
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;
}
INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k));
0
}
/// # Safety
/// Collect the network infos
#[no_mangle]
pub unsafe extern "C" fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> std::ffi::c_int {
if max_length == 0 {
return 0;
}
let infos = unsafe {
assert!(!infos.is_null());
std::slice::from_raw_parts_mut(infos, max_length)
};
let collected_infos = match INSTANCE_MANAGER.collect_network_infos_sync() {
Ok(infos) => infos,
Err(e) => {
set_error_msg(&format!("failed to collect network infos: {}", e));
return -1;
}
};
let mut index = 0;
for (instance_id, value) in collected_infos.iter() {
if index >= max_length {
break;
}
let Some(key) = INSTANCE_MANAGER.get_instance_name(instance_id) else {
continue;
};
// convert value to json string
let value = match serde_json::to_string(&value) {
Ok(value) => value,
Err(e) => {
set_error_msg(&format!("failed to serialize instance info: {}", e));
return -1;
}
};
infos[index] = KeyValuePair {
key: std::ffi::CString::new(key).unwrap().into_raw(),
value: std::ffi::CString::new(value).unwrap().into_raw(),
};
index += 1;
}
index as std::ffi::c_int
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
unsafe {
assert_eq!(parse_config(cstr.as_ptr()), 0);
}
}
#[test]
fn test_run_network_instance() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
unsafe {
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
}
}
}
@@ -0,0 +1,33 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "********************************"
ui_print " Please install Magisk v20.4+! "
ui_print "********************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0
@@ -0,0 +1,6 @@
# easytier_magisk版模块
magisk安装后重启
目录位置:/data/adb/modules/easytier_magisk
配置文件位置://data/adb/modules/easytier_magisk/config/config.toml
修改config.conf即可,修改后配置文件后去magisk app重新开关模块即可生效
@@ -0,0 +1,74 @@
#!/data/adb/magisk/busybox sh
MODDIR=${0%/*}
MODULE_PROP="${MODDIR}/module.prop"
IP_RULE_SCRIPT="${MODDIR}/hotspot_iprule.sh"
ET_STATUS=""
REDIR_STATUS=""
IS_RUNNING=false
# 确保辅助脚本有执行权限
chmod +x "${IP_RULE_SCRIPT}" 2>/dev/null
# 更新 module.prop 文件中的 description
update_module_description() {
local status_message=$1
# 检查 module.prop 文件存在且 description 发生变化了再写入
if [ -f "${MODULE_PROP}" ]; then
local current_desc=$(grep "^description=" "${MODULE_PROP}")
local new_desc="description=[状态] ${status_message}"
if [ "${current_desc}" != "${new_desc}" ]; then
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
fi
fi
}
# 判断程序启动状态
if [ -f "${MODDIR}/disable" ]; then
IS_RUNNING=false
ET_STATUS="主程序已关闭"
elif pgrep -f "${MODDIR}/easytier-core" >/dev/null; then
IS_RUNNING=true
if [ -f "${MODDIR}/config/command_args" ]; then
ET_STATUS="主程序正在运行(启动参数模式)"
else
ET_STATUS="主程序正在运行(配置文件模式)"
fi
elif [ -z "$ET_STATUS" ]; then
# 既没 disable 也没运行,说明是异常停止或未启动
ET_STATUS="主程序启动失败或未运行"
fi
# 无论主程序是否运行,都允许切换“开关文件”的状态,以便下次生效
if [ -f "${MODDIR}/enable_IP_rule" ]; then
rm -f "${MODDIR}/enable_IP_rule"
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
REDIR_STATUS="转发已禁用"
echo "热点子网转发已禁用"
echo "[ET-NAT] Action: IP rule disabled." >> "${MODDIR}/log.log"
else
touch "${MODDIR}/enable_IP_rule"
if [ "$IS_RUNNING" = true ]; then
"${IP_RULE_SCRIPT}" del >/dev/null 2>&1
"${IP_RULE_SCRIPT}" add_once
echo "转发规则将立即生效,无需重启"
else
echo "主程序未运行,转发规则将在下次启动时生效"
fi
REDIR_STATUS="转发已激活"
echo "----------------------------------"
echo "热点子网转发已激活"
echo "热点开启后将自动将热点加入转发网络"
echo "需要在配置中提前配置好 cidr 参数"
echo "----------------------------------"
echo "[ET-NAT] Action: IP rule enabled." >> "${MODDIR}/log.log"
fi
sync
update_module_description "${ET_STATUS}| ${REDIR_STATUS}"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
version=$(cat module.prop | grep 'version=' | awk -F '=' '{print $2}' | sed 's/ (.*//')
version='v'$(grep '^version =' ../../easytier/Cargo.toml | cut -d '"' -f 2)
if [ -z "$version" ]; then
echo "Error: 版本号不存在."
exit 1
fi
filename="easytier_magisk_${version}.zip"
echo $version
if [ -f "./easytier-core" ] && [ -f "./easytier-cli" ] && [ -f "./easytier-web" ]; then
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
else
wget -O "easytier_last.zip" https://github.com/EasyTier/EasyTier/releases/download/"$version"/easytier-linux-aarch64-"$version".zip
unzip -o easytier_last.zip -d ./
mv ./easytier-linux-aarch64/* ./
rm -rf ./easytier_last.zip
rm -rf ./easytier-linux-aarch64
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
fi
@@ -0,0 +1 @@
--config-server udp://127.0.0.1:22020/admin --machine-id easytier-magisk
@@ -0,0 +1,38 @@
instance_name = "default"
dhcp = false
#ipv4="本机ip"
listeners = [
"tcp://0.0.0.0:11010",
"udp://0.0.0.0:11010",
"wg://0.0.0.0:11011",
"ws://0.0.0.0:11011/",
"wss://0.0.0.0:11012/",
]
mapped_listeners = []
exit_nodes = []
rpc_portal = "0.0.0.0:15888"
[network_identity]
network_name = "default"
network_secret = ""
[[peer]]
#uri = "协议://中转ip:端口"
[flags]
default_protocol = "tcp"
dev_name = ""
enable_encryption = true
enable_ipv6 = true
mtu = 1380
latency_first = false
enable_exit_node = false
no_tun = false
use_smoltcp = false
foreign_network_whitelist = "*"
disable_p2p = false
relay_all_peer_rpc = false
disable_udp_hole_punching = false
disable_tcp_hole_punching = false
@@ -0,0 +1,19 @@
SKIPMOUNT=false
PROPFILE=true
POSTFSDATA=true
LATESTARTSERVICE=true
set_perm_recursive $MODPATH 0 0 0777 0777
ui_print "系统架构为:$ARCH"
ui_print "系统 SDK 版本:$API"
ui_print "EasyTier 安装位置:/data/adb/modules/easytier_magisk"
ui_print "配置文件位置:/data/adb/modules/easytier_magisk/config/config.toml"
ui_print "如需使用启动参数模式,请将 /data/adb/modules/easytier_magisk/config/command_args_sample 重命名为 command_args,并修改其中的内容"
ui_print "config 目录中存在 command_args 文件时,模块会自动忽略 config.toml 文件"
ui_print "----------------------------------"
ui_print "注意!启动参数文件中不能存在 \" 和 ',配置文件则没有这个限制"
ui_print "----------------------------------"
ui_print "修改配置后无需重启设备,在 Magisk 中禁用 EasyTier 模块,等待 10 秒后重新启用即可让新配置生效"
ui_print "点击 Magisk 中模块左下角的“操作”按钮可以禁用或激活热点子网转发,使用该功能前需要在配置中提前配置好 cidr 参数"
ui_print "模块安装完成,重启设备生效"
@@ -0,0 +1,112 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_FILE="${MODDIR}/config/config.toml"
COMMAND_ARGS="${MODDIR}/config/command_args"
LOG_FILE="${MODDIR}/log.log"
MODULE_PROP="${MODDIR}/module.prop"
EASYTIER="${MODDIR}/easytier-core"
# 处理获取到的设备型号中可能出现的空格
BRAND=$(getprop ro.product.brand | tr ' ' '-')
MODEL=$(getprop ro.product.model | tr ' ' '-')
DEVICE_HOSTNAME="${BRAND}-${MODEL}"
REDIR_STATUS=""
# 更新 module.prop 文件中的 description
update_module_description() {
local status_message=$1
# 检查 module.prop 文件存在且 description 发生变化了再写入
if [ -f "${MODULE_PROP}" ]; then
local current_desc=$(grep "^description=" "${MODULE_PROP}")
local new_desc="description=[状态] ${status_message}"
if [ "${current_desc}" != "${new_desc}" ]; then
sed -i "s#^description=.*#${new_desc}#" "${MODULE_PROP}"
fi
fi
}
# 检查并初始化 TUN 设备
if [ ! -e /dev/net/tun ]; then
if [ ! -d /dev/net ]; then
mkdir -p /dev/net
fi
ln -s /dev/tun /dev/net/tun
fi
while true; do
# 获取子网转发激活状态
if [ -f "${MODDIR}/enable_IP_rule" ]; then
REDIR_STATUS="转发已激活"
else
REDIR_STATUS="转发已禁用"
fi
# 检查模块是否被禁用
if [ -f "${MODDIR}/disable" ]; then
update_module_description "主程序已关闭 | ${REDIR_STATUS}"
if pgrep -f "${EASYTIER}" >/dev/null; then
echo "开关控制 $(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭"
pkill -f "${EASYTIER}"
fi
sleep 10s
continue
fi
# 检查进程是否已经在运行
if pgrep -f "${EASYTIER}" >/dev/null; then
sleep 10s
continue
fi
# 检查配置文件是否存在
if [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${COMMAND_ARGS}" ]; then
update_module_description "缺少配置文件或启动参数文件"
sleep 10s
continue
fi
# 如果 config 目录下存在 command_args 文件,则读取其中的内容作为启动参数
if [ -f "${COMMAND_ARGS}" ]; then
# 启动参数模式
CMD_CONTENT=$(tr '\r\n' ' ' < "${COMMAND_ARGS}")
if echo "${CMD_CONTENT}" | grep -q "\-\-hostname"; then
FINAL_ARGS="${CMD_CONTENT}"
else
FINAL_ARGS="${CMD_CONTENT} --hostname ${DEVICE_HOSTNAME}"
fi
TZ=Asia/Shanghai "${EASYTIER}" ${FINAL_ARGS} > "${LOG_FILE}" 2>&1 &
STR_MODE="启动参数模式"
# 否则读取 config.toml 的内容作为启动参数
else
# 配置文件模式
if grep -q "^[[:space:]]*hostname[[:space:]]*=" "${CONFIG_FILE}"; then
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" > "${LOG_FILE}" 2>&1 &
else
TZ=Asia/Shanghai "${EASYTIER}" -c "${CONFIG_FILE}" --hostname "${DEVICE_HOSTNAME}" > "${LOG_FILE}" 2>&1 &
fi
STR_MODE="配置文件模式"
fi
# 等待进程启动
sleep 5s
# 启动后的扫尾工作
if pgrep -f "${EASYTIER}" >/dev/null; then
if ! ip rule show | grep -q "lookup main"; then
ip rule add from all lookup main
fi
update_module_description "主程序正在运行(${STR_MODE}| ${REDIR_STATUS}"
else
update_module_description "主程序启动失败,请检查配置文件或启动参数"
fi
sleep 10s
done
@@ -0,0 +1,104 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_FILE="${MODDIR}/config/config.toml"
LOG_FILE="${MODDIR}/log.log"
ACTION="$1" # 参数:add add_once del
# 获取接口/IP
get_et_iface() {
awk '
BEGIN { IGNORECASE = 1 }
/^[[:space:]]*dev_name[[:space:]]*=/ {
val = $0
sub(/^[^=]*=[[:space:]]*/, "", val)
gsub(/[" \t]/, "", val)
print val
exit
}
' "$CONFIG_FILE"
}
get_tun_iface() {
ip link | awk -F': ' '/ tun[[:alnum:]]+/ {print $2; exit}'
}
get_hot_iface() {
ip link | awk -F': ' '/(^| )(swlan[[:alnum:]_]*|softap[[:alnum:]_]*|p2p-wlan[[:alnum:]_]*|ap[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
}
get_usb_iface() {
ip link | awk -F': ' '/(^| )(usb[[:alnum:]_]*|rndis[[:alnum:]_]*|eth[[:alnum:]_]*)\:/ {print $2; exit}' | cut -d'@' -f1 | head -n1
}
get_hot_cidr() {
ip -4 addr show dev "$1" | awk '/inet /{print $2; exit}'
}
set_nat_rules() {
ET_IFACE=$(get_et_iface)
[ -z "$ET_IFACE" ] && ET_IFACE="$(get_tun_iface)"
HOT_IFACE=$(get_hot_iface)
USB_IFACE=$(get_usb_iface)
HOT_CIDR=$(get_hot_cidr "$HOT_IFACE")
USB_CIDR=$(get_hot_cidr "$USB_IFACE")
# 如果热点关闭就删除自定义链
[ -n "$ET_IFACE" ] && { [ -n "$HOT_CIDR" ] || [ -n "$USB_CIDR" ]; } || return 1
# 创建自定义链(如不存在)
iptables -t nat -N ET_NAT 2>/dev/null
iptables -N ET_FWD 2>/dev/null
# 确保主链首条跳转到自定义链
iptables -t nat -C POSTROUTING -j ET_NAT 2>/dev/null || \
iptables -t nat -I POSTROUTING 1 -j ET_NAT
iptables -C FORWARD -j ET_FWD 2>/dev/null || \
iptables -I FORWARD 1 -j ET_FWD
# 添加规则
if [ -n "$HOT_CIDR" ]; then
iptables -t nat -A ET_NAT -s "$HOT_CIDR" -o "$ET_IFACE" -j MASQUERADE
iptables -A ET_FWD -i "$HOT_IFACE" -o "$ET_IFACE" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A ET_FWD -i "$ET_IFACE" -o "$HOT_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $HOT_IFACE $HOT_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
if [ -n "$USB_CIDR" ]; then
iptables -t nat -A ET_NAT -s "$USB_CIDR" -o "$ET_IFACE" -j MASQUERADE
iptables -A ET_FWD -i "$USB_IFACE" -o "$ET_IFACE" \
-m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A ET_FWD -i "$ET_IFACE" -o "$USB_IFACE" \
-m state --state ESTABLISHED,RELATED -j ACCEPT
echo "[ET-NAT] Rules applied: $USB_IFACE $USB_CIDR$ET_IFACE" >> "$LOG_FILE"
fi
}
flush_rules() {
iptables -t nat -F ET_NAT 2>/dev/null
iptables -F ET_FWD 2>/dev/null
echo "[ET-NAT] Custom chains flushed." >> "$LOG_FILE"
}
case "$ACTION" in
add)
set_nat_rules
echo "[ET-NAT] Guard started." >> "$LOG_FILE"
ip monitor link addr | while read -r _; do
if [ -f "${MODDIR}/enable_IP_rule" ]; then
flush_rules
set_nat_rules
fi
done
;;
add_once)
flush_rules
set_nat_rules
echo "[ET-NAT] One-time rules applied." >> "$LOG_FILE"
;;
del)
flush_rules
;;
*)
echo "Usage: $0 [add|del]"
exit 1
;;
esac
@@ -0,0 +1,6 @@
{
"version": "v1.0",
"versionCode": 1,
"zipUrl": "",
"changelog": ""
}
@@ -0,0 +1,7 @@
id=easytier_magisk
name=EasyTier_Magisk
version=v2.6.0
versionCode=1
author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
updateJson=https://raw.githubusercontent.com/EasyTier/EasyTier/refs/heads/main/easytier-contrib/easytier-magisk/magisk_update.json
@@ -0,0 +1,24 @@
#!/data/adb/magisk/busybox sh
MODDIR=${0%/*}
# MODDIR="$(dirname $(readlink -f "$0"))"
chmod 755 ${MODDIR}/*
# 等待系统启动成功
while [ "$(getprop sys.boot_completed)" != "1" ]; do
sleep 5s
done
# 防止系统挂起
echo "PowerManagerService.noSuspend" > /sys/power/wake_lock
# 修改模块描述
sed -i 's/$(description=)$[^"]*/\1[状态]关闭中/' "$MODDIR/module.prop"
# 等待 3 秒
sleep 3s
"${MODDIR}/easytier_core.sh" &
"${MODDIR}/hotspot_iprule.sh" add &
# easytier_core.sh 和 hotspot_iprule.sh 都有内部循环做守护,
# 所以这里不需要再做守护了
@@ -0,0 +1,2 @@
nameserver 114.114.114.114
nameserver 223.5.5.5
@@ -0,0 +1,5 @@
MODDIR=${0%/*}
pkill -f "${MODDIR}/easytier-core"
# 使用 ${MODDIR:?} 确保变量非空,避免执行 rm -rf /*
rm -rf "${MODDIR:?}/"*
@@ -0,0 +1,9 @@
dist/
target/
.DS_Store
.idea/
package/libs
*.har
Cargo.lock
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
[package]
name = "easytier-ohrs"
version = "0.1.0"
edition = "2024"
[lib]
crate-type=["cdylib"]
[dependencies]
ohos-hilog-binding = {version = "*", features = ["redirect"]}
easytier = { path = "../../easytier" }
napi-derive-ohos = "1.1"
napi-ohos = { version = "1.1", 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.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
[build-dependencies]
napi-build-ohos = "1.1"
[profile.dev]
panic = "unwind"
debug = true
[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
strip = true
+65
View 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
View File
@@ -0,0 +1,3 @@
fn main() {
napi_build_ohos::setup();
}
+31
View 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}"
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
# 0.0.1
- init package
+165
View File
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
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.
0. Additional Definitions.
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.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
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.
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".
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.
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.
1. Exception to Section 3 of the GNU GPL.
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. Conveying Modified Versions.
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:
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
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
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:
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.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
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:
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.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
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.
d) Do one of the following:
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.
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.
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.)
5. Combined Libraries.
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:
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.
+162
View File
@@ -0,0 +1,162 @@
# `easytier-ohrs`
## Install
use `ohpm` to install package.
```shell
ohpm install easytier-ohrs
```
## API
### collectNetworkInfos
```ts
collectNetworkInfos(): Array<KeyValuePair>
````
---
### collectRunningNetwork
```ts
collectRunningNetwork(): Array<string>
```
获取当前正在运行的网络实例名称列表。
---
### convertTomlToNetworkConfig
```ts
convertTomlToNetworkConfig(cfgStr: string): string
```
将 TOML 配置转换为 NetworkConfig。
* `cfgStr`TOML 配置内容
---
### defaultNetworkConfig
```ts
defaultNetworkConfig(): string
```
获取默认的网络配置(JSON 字符串),用于转换为object进行赋值。
---
### easytierVersion
```ts
easytierVersion(): string
```
获取 EasyTier 当前版本号。
---
### hilogGlobalOptions
```ts
hilogGlobalOptions(domain: number, tag: string): void
```
设置全局日志选项。
* `domain`:日志域 ID
* `tag`:日志标签
---
### initPanicHook
```ts
initPanicHook(): void
```
初始化 panic 钩子,用于将Rust侧的panic输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
---
### initTracingSubscriber
```ts
initTracingSubscriber(): void
```
初始化 tracing 日志订阅器,用于将Rust侧日志同步输出到hilog中,请先通过 hilogGlobalOptions 设置hilog的参数。
---
### isRunningNetwork
```ts
isRunningNetwork(instId: string): boolean
```
判断指定网络实例是否正在运行。
* `instId`:网络实例 ID
---
### parseNetworkConfig
```ts
parseNetworkConfig(cfgJson: string): boolean
```
校验网络配置(JSON 格式)是否合法。
* `cfgJson`:网络配置内容
---
### runNetworkInstance
```ts
runNetworkInstance(cfgJson: string): boolean
```
启动网络实例。
* `cfgJson`:网络配置(JSON
---
### setTunFd
```ts
setTunFd(instId: string, fd: number): boolean
```
为指定网络实例设置 TUN 设备文件描述符。
* `instId`:网络实例 ID
* `fd`TUN 设备文件描述符
---
### stopNetworkInstance
```ts
stopNetworkInstance(instNames: Array<string>): void
```
停止指定的网络实例。
* `instNames`:网络实例名称列表
## Usage
```ts
// todo
```
+4
View File
@@ -0,0 +1,4 @@
import * as api from "libeasytier_ohrs.so";
export * from 'libeasytier_ohrs.so';
export default api;
+20
View File
@@ -0,0 +1,20 @@
{
"license": "LGPL-3.0",
"author": "easytier",
"name": "easytier-ohrs",
"description": "EasyTier for OpenHarmonyOS",
"main": "index.ets",
"version": "0.0.1",
"types": "libs/index.d.ts",
"dependencies": {},
"compatibleSdkVersion": "17",
"compatibleSdkType": "OpenHarmony",
"obfuscated": false,
"nativeComponents": [
{
"name": "libeasytier_ohrs.so",
"compatibleSdkVersion": "17",
"compatibleSdkType": "OpenHarmony"
}
]
}
@@ -0,0 +1,7 @@
{
"module": {
"name": "easytier-ohrs",
"type": "har",
"deviceTypes": ["default", "tablet", "2in1"]
},
}
+185
View File
@@ -0,0 +1,185 @@
mod native_log;
use easytier::common::config::{ConfigFileControl, ConfigLoader, TomlConfigLoader};
use easytier::common::constants::EASYTIER_VERSION;
use easytier::instance_manager::NetworkInstanceManager;
use easytier::proto::api::manage::NetworkConfig;
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 easytier_version() -> String {
EASYTIER_VERSION.to_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 default_network_config() -> String {
match NetworkConfig::new_from_config(TomlConfigLoader::default()) {
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
Err(e) => {
hilog_error!("[Rust] default_network_config failed {}", e);
format!("ERROR {}", e)
}
}
}
#[napi]
pub fn convert_toml_to_network_config(cfg_str: String) -> String {
match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => match NetworkConfig::new_from_config(cfg) {
Ok(result) => serde_json::to_string(&result).unwrap_or_else(|e| format!("ERROR {}", e)),
Err(e) => {
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
format!("ERROR {}", e)
}
},
Err(e) => {
hilog_error!("[Rust] convert_toml_to_network_config failed {}", e);
format!("ERROR {}", e)
}
}
}
#[napi]
pub fn parse_network_config(cfg_json: String) -> bool {
match serde_json::from_str::<NetworkConfig>(&cfg_json) {
Ok(cfg) => match cfg.gen_config() {
Ok(toml) => {
hilog_debug!("[Rust] Convert to Toml {}", toml.dump());
true
}
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
false
}
},
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
false
}
}
}
#[napi]
pub fn run_network_instance(cfg_json: String) -> bool {
let cfg = match serde_json::from_str::<NetworkConfig>(&cfg_json) {
Ok(cfg) => match cfg.gen_config() {
Ok(toml) => toml,
Err(e) => {
hilog_error!("[Rust] parse config failed {}", e);
return false;
}
},
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, false, ConfigFileControl::STATIC_CONFIG)
.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_sync() {
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
}
}
}
@@ -0,0 +1,96 @@
use napi_derive_ohos::napi;
use ohos_hilog_binding::{
LogOptions, hilog_debug, hilog_error, hilog_info, hilog_warn, set_global_options,
};
use std::collections::HashMap;
use std::panic;
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));
}
}
+17
View File
@@ -0,0 +1,17 @@
# Development Environment Configuration
SERVER_HOST=127.0.0.1
SERVER_PORT=8080
DATABASE_PATH=uptime.db
DATABASE_MAX_CONNECTIONS=5
HEALTH_CHECK_INTERVAL=60
HEALTH_CHECK_TIMEOUT=15
HEALTH_CHECK_RETRIES=2
RUST_LOG=debug
LOG_LEVEL=debug
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=content-type,authorization
NODE_ENV=development
API_BASE_URL=/api
ENABLE_COMPRESSION=true
ENABLE_CORS=true
@@ -0,0 +1,17 @@
# Development Environment Configuration
SERVER_HOST=127.0.0.1
SERVER_PORT=8080
DATABASE_PATH=uptime.db
DATABASE_MAX_CONNECTIONS=5
HEALTH_CHECK_INTERVAL=60
HEALTH_CHECK_TIMEOUT=15
HEALTH_CHECK_RETRIES=2
RUST_LOG=debug
LOG_LEVEL=debug
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=content-type,authorization
NODE_ENV=development
API_BASE_URL=/api
ENABLE_COMPRESSION=true
ENABLE_CORS=true
@@ -0,0 +1,29 @@
# Server Configuration
SERVER_HOST=127.0.0.1
SERVER_PORT=8080
# Database Configuration
DATABASE_PATH=uptime.db
DATABASE_MAX_CONNECTIONS=10
# Health Check Configuration
HEALTH_CHECK_INTERVAL=30
HEALTH_CHECK_TIMEOUT=10
HEALTH_CHECK_RETRIES=3
# Logging Configuration
RUST_LOG=info
LOG_LEVEL=info
# CORS Configuration
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=content-type,authorization
# Production Configuration
NODE_ENV=development
API_BASE_URL=/api
# Security Configuration
ENABLE_COMPRESSION=true
ENABLE_CORS=true
@@ -0,0 +1,21 @@
# Production Environment Configuration
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
DATABASE_PATH=/var/lib/easytier-uptime/uptime.db
DATABASE_MAX_CONNECTIONS=20
HEALTH_CHECK_INTERVAL=30
HEALTH_CHECK_TIMEOUT=10
HEALTH_CHECK_RETRIES=3
RUST_LOG=info
LOG_LEVEL=info
CORS_ALLOWED_ORIGINS=https://yourdomain.com
CORS_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS
CORS_ALLOWED_HEADERS=content-type,authorization
NODE_ENV=production
API_BASE_URL=/api
ENABLE_COMPRESSION=true
ENABLE_CORS=true
# Security
SECRET_KEY=your-secret-key-here
JWT_SECRET=your-jwt-secret-here
@@ -0,0 +1,3 @@
*.db
*.db-shm
*.db-wal
@@ -0,0 +1,66 @@
[package]
name = "easytier-uptime"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
# Axum web framework
axum = { version = "0.8.4", features = ["macros"] }
axum-extra = { version = "0.10", features = ["query"] }
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
tower = "0.5"
# SeaORM dependencies
sea-orm = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
"with-chrono",
"with-uuid",
"with-json"
] }
sea-orm-migration = { version = "1.1" }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "chrono", "uuid"] }
# Validation
validator = { version = "0.18", features = ["derive"] }
thiserror = "1.0"
jsonwebtoken = "9.0"
# Configuration and serialization
serde_yaml = "0.9"
toml = "0.8"
# Network and async
async-trait = "0.1"
futures = "0.3"
tokio-util = { version = "0.7", features = ["full"] }
# Filesystem operations
tempfile = "3.8"
# Additional utilities
dashmap = "6.1.0"
clap = { version = "4.0", features = ["derive"] }
parking_lot = "0.12"
once_cell = "1.19"
# EasyTier core
easytier = { path = "../../easytier" }
mimalloc = { version = "*" }
# Testing
[dev-dependencies]
mockall = "0.12"
tokio-test = "0.4"
reqwest = "0.12"
+272
View File
@@ -0,0 +1,272 @@
# EasyTier Uptime Monitor
一个用于监控 EasyTier 实例健康状态和运行时间的系统。
## 功能特性
- 🏥 **健康监控**: 实时监控 EasyTier 节点的健康状态
- 📊 **数据统计**: 提供详细的运行时间和响应时间统计
- 🔧 **实例管理**: 管理多个 EasyTier 实例
- 🌐 **Web界面**: 直观的 Web 管理界面
- 🚨 **告警系统**: 支持健康状态异常告警
- 📈 **图表展示**: 可视化展示监控数据
## 系统架构
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Frontend │ │ Backend │ │ Database │
│ (Vue.js) │◄──►│ (Rust/Axum) │◄──►│ (SQLite) │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Dashboard │ │ │ │ API Routes │ │ │ │ Nodes │ │
│ │ Health View │ │ │ │ Health │ │ │ │ Health │ │
│ │ Node Mgmt │ │ │ │ Instances │ │ │ │ Instances │ │
│ │ Charts │ │ │ │ Scheduler │ │ │ │ Stats │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## 快速开始
### 环境要求
- **Rust**: 1.70+
- **Node.js**: 16+
- **npm**: 8+
### 开发环境
1. **克隆项目**
```bash
git clone <repository-url>
cd easytier-uptime
```
2. **启动开发环境**
```bash
./start-dev.sh
```
3. **访问应用**
- 前端界面: http://localhost:3000
- 后端API: http://localhost:8080
- 健康检查: http://localhost:8080/health
### 生产环境
1. **启动生产环境**
```bash
./start-prod.sh
```
2. **停止生产环境**
```bash
./stop-prod.sh
```
## 配置说明
### 环境变量
#### 后端配置 (.env)
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `SERVER_HOST` | `127.0.0.1` | 服务器监听地址 |
| `SERVER_PORT` | `8080` | 服务器端口 |
| `DATABASE_PATH` | `uptime.db` | 数据库文件路径 |
| `DATABASE_MAX_CONNECTIONS` | `10` | 数据库最大连接数 |
| `HEALTH_CHECK_INTERVAL` | `30` | 健康检查间隔(秒) |
| `HEALTH_CHECK_TIMEOUT` | `10` | 健康检查超时(秒) |
| `HEALTH_CHECK_RETRIES` | `3` | 健康检查重试次数 |
| `RUST_LOG` | `info` | 日志级别 |
| `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | 允许的跨域来源 |
| `ENABLE_CORS` | `true` | 是否启用CORS |
| `ENABLE_COMPRESSION` | `true` | 是否启用压缩 |
#### 前端配置 (frontend/.env)
| 变量名 | 默认值 | 说明 |
|--------|--------|------|
| `VITE_APP_TITLE` | `EasyTier Uptime Monitor` | 应用标题 |
| `VITE_API_BASE_URL` | `/api` | API基础URL |
| `VITE_APP_ENV` | `development` | 应用环境 |
| `VITE_ENABLE_DEV_TOOLS` | `true` | 是否启用开发工具 |
| `VITE_API_TIMEOUT` | `10000` | API超时时间(毫秒) |
## API 文档
### 健康检查
```http
GET /health
```
### 节点管理
```http
# 获取节点列表
GET /api/nodes
# 创建节点
POST /api/nodes
# 获取节点详情
GET /api/nodes/{id}
# 更新节点
PUT /api/nodes/{id}
# 删除节点
DELETE /api/nodes/{id}
```
### 健康记录
```http
# 获取节点健康历史
GET /api/nodes/{id}/health
# 获取节点健康统计
GET /api/nodes/{id}/health/stats
```
### 实例管理
```http
# 获取实例列表
GET /api/instances
# 创建实例
POST /api/instances
# 停止实例
DELETE /api/instances/{id}
```
## 测试
### 运行集成测试
```bash
./test-integration.sh
```
### 运行单元测试
```bash
cargo test
```
### 测试覆盖率
```bash
cargo tarpaulin
```
## 部署
### Docker 部署
```bash
# 构建镜像
docker build -t easytier-uptime .
# 运行容器
docker run -d -p 8080:8080 easytier-uptime
```
### 手动部署
1. **构建后端**
```bash
cargo build --release
```
2. **构建前端**
```bash
cd frontend
npm install
npm run build
cd ..
```
3. **配置环境**
```bash
cp .env.production .env
# 编辑 .env 文件
```
4. **启动服务**
```bash
./start-prod.sh
```
## 监控和日志
### 日志文件
- **后端日志**: `logs/backend.log`
- **前端日志**: `logs/frontend.log`
- **测试日志**: `test-results/`
### 健康检查
系统提供以下健康检查端点:
- `/health` - 基本健康检查
- `/api/health/stats` - 健康统计信息
- `/api/health/scheduler/status` - 调度器状态
## 故障排除
### 常见问题
1. **后端启动失败**
- 检查端口是否被占用
- 确认数据库文件权限
- 查看日志文件 `logs/backend.log`
2. **前端连接失败**
- 检查后端服务是否运行
- 确认API地址配置
- 检查CORS配置
3. **健康检查失败**
- 确认目标节点可访问
- 检查防火墙设置
- 验证健康检查配置
### 性能优化
1. **数据库优化**
- 定期清理过期数据
- 配置适当的连接池大小
- 使用索引优化查询
2. **前端优化**
- 启用代码分割
- 配置缓存策略
- 优化图片和资源
3. **网络优化**
- 启用压缩
- 配置CDN
- 优化API响应时间
## 贡献指南
1. Fork 项目
2. 创建特性分支
3. 提交更改
4. 推送到分支
5. 创建 Pull Request
## 许可证
MIT License
## 支持
如有问题或建议,请提交 Issue 或联系开发团队。
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
{
"name": "easytier-uptime-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.13.5",
"dayjs": "^1.11.13",
"easytier-uptime-frontend": "link:",
"element-plus": "^2.8.8",
"vue": "^3.5.18",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.4",
"vite": "^7.1.2"
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,340 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { healthApi } from './api'
import {
Monitor,
Plus,
CircleCheck,
CircleClose,
Loading,
Link
} from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const healthStatus = ref(null)
const loading = ref(false)
// 安全地打开外部链接
const openExternalLink = (url) => {
try {
if (typeof window !== 'undefined' && window.open) {
window.open(url, '_blank')
} else {
// 备用方案:创建一个临时链接元素
const link = document.createElement('a')
link.href = url
link.target = '_blank'
link.rel = 'noopener noreferrer'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
} catch (error) {
console.error('Failed to open external link:', error)
// 最后的备用方案:直接跳转
if (typeof window !== 'undefined') {
window.location.href = url
}
}
}
// 检查后端健康状态
const checkHealth = async () => {
try {
loading.value = true
const response = await healthApi.check()
healthStatus.value = response.success
} catch (error) {
healthStatus.value = false
console.error('Health check failed:', error)
} finally {
loading.value = false
}
}
// 导航菜单项
const menuItems = [
{
path: '/',
name: 'dashboard',
title: '节点监控',
icon: 'Monitor'
},
{
path: '/submit',
name: 'submit',
title: '提交节点',
icon: 'Plus'
}
]
// 根据当前路由计算默认激活的菜单项
const activeMenuIndex = computed(() => {
const p = route.path
if (p.startsWith('/submit')) return 'submit'
return 'dashboard'
})
// 处理菜单选择,避免返回 Promise 导致异步补丁问题
const handleMenuSelect = (key) => {
const item = menuItems.find((i) => i.name === key)
if (item && item.path) {
router.push(item.path)
}
}
onMounted(() => {
checkHealth()
// 定期检查健康状态
setInterval(checkHealth, 60000) // 每分钟检查一次
})
</script>
<template>
<div id="app">
<!-- 顶部导航栏 -->
<el-header class="app-header">
<div class="header-content">
<div class="logo-section">
<el-icon size="32" color="#409EFF">
<Monitor />
</el-icon>
<h1 class="app-title">EasyTier Uptime</h1>
</div>
<el-menu :default-active="activeMenuIndex" mode="horizontal" class="nav-menu"
@select="handleMenuSelect">
<el-menu-item v-for="item in menuItems" :key="item.name" :index="item.name">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.title }}</span>
</el-menu-item>
</el-menu>
<div class="header-actions">
<!-- 健康状态指示器 -->
<el-tooltip :content="healthStatus === null ? '检查中...' : healthStatus ? '服务正常' : '服务异常'" placement="bottom">
<div class="health-indicator">
<el-icon :color="healthStatus === null ? '#909399' : healthStatus ? '#67C23A' : '#F56C6C'"
:class="{ 'loading': loading }">
<CircleCheck v-if="healthStatus === true" />
<CircleClose v-else-if="healthStatus === false" />
<Loading v-else />
</el-icon>
</div>
</el-tooltip>
<!-- 管理员入口 -->
<el-button type="warning" link @click="() => router.push('/admin/login')">
管理员
</el-button>
<!-- GitHub链接 -->
<el-button type="primary" link @click="() => openExternalLink('https://github.com/EasyTier/EasyTier')">
<el-icon>
<Link />
</el-icon>
GitHub
</el-button>
</div>
</div>
</el-header>
<!-- 主要内容区域 -->
<el-main class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
<!-- 底部信息 -->
<el-footer class="app-footer">
<div class="footer-content">
<p>
© 2024 EasyTier Community |
<el-button type="primary" link size="small"
@click="() => openExternalLink('https://github.com/EasyTier/EasyTier')">
开源项目
</el-button>
|
<el-button type="primary" link size="small"
@click="() => openExternalLink('https://github.com/EasyTier/EasyTier/blob/main/README.md')">
使用文档
</el-button>
</p>
</div>
</el-footer>
</div>
</template>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
background-color: #f5f7fa;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 顶部导航栏 */
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 0;
height: 60px;
line-height: 60px;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
}
.app-title {
color: white;
font-size: 20px;
font-weight: 600;
margin: 0;
}
.nav-menu {
background: transparent;
border: none;
flex: 1;
justify-content: center;
}
.nav-menu .el-menu-item {
color: rgba(255, 255, 255, 0.8);
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.nav-menu .el-menu-item:hover,
.nav-menu .el-menu-item.is-active {
color: white;
background: rgba(255, 255, 255, 0.1);
border-bottom-color: white;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.health-indicator {
display: flex;
align-items: center;
cursor: pointer;
}
.health-indicator .loading {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 主要内容区域 */
.app-main {
flex: 1;
padding: 0;
background-color: #f5f7fa;
}
/* 页面切换动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 底部信息 */
.app-footer {
background: white;
border-top: 1px solid #e4e7ed;
text-align: center;
height: 50px;
line-height: 50px;
}
.footer-content p {
color: #909399;
font-size: 14px;
margin: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
padding: 0 10px;
}
.app-title {
font-size: 16px;
}
.nav-menu {
display: none;
}
.header-actions {
gap: 10px;
}
}
/* Element Plus 组件样式覆盖 */
.el-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-button {
border-radius: 6px;
}
.el-input {
border-radius: 6px;
}
.el-select {
border-radius: 6px;
}
</style>
@@ -0,0 +1,195 @@
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
// 保证数组参数使用 repeated keys 风格序列化:tags=a&tags=b
paramsSerializer: params => {
const usp = new URLSearchParams()
Object.entries(params || {}).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => usp.append(key, v))
} else if (value !== undefined && value !== null && value !== '') {
usp.append(key, value)
}
})
return usp.toString()
}
})
// 请求拦截器
api.interceptors.request.use(
config => {
// 只在管理员相关的API请求中添加token
if (config.url && config.url.includes('/api/admin/')) {
const token = localStorage.getItem('admin_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
response => {
// 直接返回完整的response对象,让各个API方法自己处理数据格式
return response
},
error => {
console.error('API Error Details:', {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
config: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers
}
})
return Promise.reject(error)
}
)
// 节点相关API
export const nodeApi = {
// 获取节点列表(支持传入 AbortController.signal 用于取消)
async getNodes(params = {}, options = {}) {
const response = await api.get('/api/nodes', { params, signal: options.signal })
return response.data
},
// 获取所有标签
async getAllTags() {
const response = await api.get('/api/tags')
return response.data
},
// 创建节点
async createNode(data) {
const response = await api.post('/api/nodes', data)
return response.data
},
// 获取单个节点
async getNode(id) {
const response = await api.get(`/api/nodes/${id}`)
return response.data
},
// 更新节点
async updateNode(id, data) {
const response = await api.put(`/api/nodes/${id}`, data)
return response.data
},
// 删除节点
async deleteNode(id) {
const response = await api.delete(`/api/nodes/${id}`)
return response.data
},
// 获取节点健康记录
async getNodeHealth(id, params = {}) {
const response = await api.get(`/api/nodes/${id}/health`, { params })
return response.data
},
// 获取节点健康统计
async getNodeHealthStats(id, params = {}) {
const response = await api.get(`/api/nodes/${id}/health/stats`, { params })
return response.data
},
// 测试节点连接
async testConnection(data) {
const response = await api.post('/api/test_connection', data)
return response.data
}
}
// 健康检查API
export const healthApi = {
async check() {
const response = await api.get('/health')
return response.data
}
}
// 管理员API
export const adminApi = {
// 管理员登录
async login(password) {
const response = await api.post('/api/admin/login', { password })
return response.data
},
// 验证token有效性
async verifyToken() {
const response = await api.get('/api/admin/verify')
return response.data
},
// 获取所有节点(包括未审批的)
async getNodes(params = {}) {
const response = await api.get('/api/admin/nodes', { params })
return response.data
},
// 审批节点
async approveNode(id) {
const response = await api.put(`/api/admin/nodes/${id}/approve`)
return response.data
},
// 撤销审批节点
async revokeApproval(id) {
const response = await api.put(`/api/admin/nodes/${id}/revoke`)
return response.data
},
// 删除节点
async deleteNode(id) {
const response = await api.delete(`/api/admin/nodes/${id}`)
return response.data
},
// 更新节点
async updateNode(id, data) {
const response = await api.put(`/api/admin/nodes/${id}`, data)
return response.data
},
// 兼容方法:获取所有节点(参数转换)
async getAllNodes(params = {}) {
const mapped = {
page: params.page,
per_page: params.page_size ?? params.per_page,
is_approved: params.approved ?? params.is_approved,
is_active: params.online ?? params.is_active,
protocol: params.protocol,
search: params.search,
tag: params.tag
}
// 移除未定义的字段
Object.keys(mapped).forEach(k => {
if (mapped[k] === undefined || mapped[k] === null || mapped[k] === '') {
delete mapped[k]
}
})
// 直接复用现有接口
const response = await api.get('/api/admin/nodes', { params: mapped })
return response.data
}
}
export default api
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,405 @@
<template>
<div class="health-timeline" :class="{ 'compact': compact }">
<div class="timeline-header">
<span class="timeline-title">最近24小时健康状态</span>
<div class="timeline-legend">
<span class="legend-item">
<span class="legend-dot perfect"></span>
<span class="legend-text">100%</span>
</span>
<span class="legend-item">
<span class="legend-dot excellent"></span>
<span class="legend-text">90-99%</span>
</span>
<span class="legend-item">
<span class="legend-dot good"></span>
<span class="legend-text">80-89%</span>
</span>
<span class="legend-item">
<span class="legend-dot fair"></span>
<span class="legend-text">60-79%</span>
</span>
<span class="legend-item">
<span class="legend-dot poor"></span>
<span class="legend-text">1-59%</span>
</span>
<span class="legend-item">
<span class="legend-dot unknown"></span>
<span class="legend-text">未知</span>
</span>
</div>
</div>
<div class="timeline-container" v-loading="loading">
<div class="timeline-grid">
<!-- 时间刻度 -->
<div class="time-labels">
<span v-for="(hour, idx) in timeLabels" :key="idx" class="time-label">
{{ hour }}
</span>
</div>
<!-- 健康状态条 -->
<div class="health-bars">
<div v-for="(segment, index) in healthSegments" :key="index" class="health-segment" :class="segment.status"
:style="{ width: segment.width + '%', backgroundColor: segment.color }" :title="getSegmentTooltip(segment)">
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="health-summary">
<div class="summary-item">
<span class="summary-value">{{ uptimePercentage }}%</span>
<span class="summary-label">在线率</span>
</div>
<div class="summary-item">
<span class="summary-value">{{ avgResponseTime }}ms</span>
<span class="summary-label">平均响应</span>
</div>
<div class="summary-item">
<span class="summary-value">{{ totalChecks }}</span>
<span class="summary-label">检查次数</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { nodeApi } from '../api'
import dayjs from 'dayjs'
const props = defineProps({
nodeInfo: {
type: Object,
required: true
},
compact: {
type: Boolean,
default: true
}
})
const loading = ref(false)
const avg_response_time = ref(0)
// 时间标签(24小时,每4小时一个标签)
const timeLabels = computed(() => {
const nodeInfo = props.nodeInfo
const granularity = nodeInfo.ring_granularity
const total_ring = nodeInfo.health_record_total_counter_ring
const totalDuration = granularity * total_ring.length
const now = dayjs(nodeInfo.last_check_time)
const startTime = now.subtract(totalDuration, 'second')
const labelCount = 6
const labelIntervalDuration = totalDuration / (labelCount - 1)
let labels = []
for (let i = 0; i < labelCount; i++) {
const time = startTime.add(i * labelIntervalDuration, 'second')
labels.push(time.format('HH:mm'))
}
return labels
})
const total_checks = computed(() => {
let total = 0
for (let i = 0; i < props.nodeInfo.health_record_total_counter_ring.length; i++) {
total += props.nodeInfo.health_record_total_counter_ring[i]
}
return total
})
const healthy_checks = computed(() => {
let total = 0
for (let i = 0; i < props.nodeInfo.health_record_healthy_counter_ring.length; i++) {
total += props.nodeInfo.health_record_healthy_counter_ring[i]
}
return total
})
const uptime_percentage = computed(() => {
return (healthy_checks.value / total_checks.value) * 100
})
// 根据成功率获取颜色
const getColorBySuccessRate = (rate) => {
if (rate === 1) {
return '#67c23a' // 100% 绿色
} else if (rate >= 0.9) {
return '#85ce61' // 90-99% 浅绿色
} else if (rate >= 0.8) {
return '#e6a23c' // 80-89% 橙色
} else if (rate >= 0.6) {
return '#f78989' // 60-79% 浅红色
} else if (rate > 0) {
return '#f56c6c' // 1-59% 红色
} else {
return '#c0c4cc' // 0% 或未知 灰色
}
}
// 健康状态分段
const healthSegments = computed(() => {
const nodeInfo = props.nodeInfo
const total_ring = nodeInfo.health_record_total_counter_ring
const healthy_ring = nodeInfo.health_record_healthy_counter_ring
const granularity = nodeInfo.ring_granularity
const totalDuration = granularity * total_ring.length
const segments = []
const now = dayjs(nodeInfo.last_check_time)
const startTime = now.subtract(totalDuration, 'second')
for (let i = total_ring.length - 1; i >= 0; i--) {
const total_counter = total_ring[i]
const healthy_counter = healthy_ring[i]
const currentTime = startTime.subtract((i + 1) * granularity, 'second')
const currentEndTime = currentTime.add(granularity, 'second')
let successRate = 0
let currentStatus = 'unknown'
if (total_counter !== 0) {
successRate = healthy_counter / total_counter
if (successRate === 1) {
currentStatus = 'perfect'
} else if (successRate >= 0.9) {
currentStatus = 'excellent'
} else if (successRate >= 0.8) {
currentStatus = 'good'
} else if (successRate >= 0.6) {
currentStatus = 'fair'
} else if (successRate > 0) {
currentStatus = 'poor'
} else {
currentStatus = 'failed'
}
}
segments.push({
status: currentStatus,
successRate: successRate,
color: getColorBySuccessRate(successRate),
width: (granularity / totalDuration) * 100,
duration: granularity / 60.0,
startTime: currentTime.format('HH:mm'),
endTime: currentEndTime.format('HH:mm'),
})
}
return segments
})
// 统计数据
const uptimePercentage = computed(() => {
return uptime_percentage.value.toFixed(1) || '0.0'
})
const avgResponseTime = computed(() => {
return (props.nodeInfo.last_response_time / 1000).toFixed(1) || '0.0'
})
const totalChecks = computed(() => {
return total_checks.value || 0
})
// 获取分段提示信息
const getSegmentTooltip = (segment) => {
const statusText = {
perfect: '完美',
excellent: '优秀',
good: '良好',
fair: '一般',
poor: '较差',
failed: '失败',
unknown: '未知'
}[segment.status] || '未知'
const successRateText = segment.successRate > 0 ? `${(segment.successRate * 100).toFixed(1)}%` : '0%'
return `${segment.startTime} - ${segment.endTime}: ${statusText} (${successRateText}) - ${Math.round(segment.duration)}分钟`
}
</script>
<style scoped>
.health-timeline {
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
margin-top: 8px;
border: 1px solid #e4e7ed;
}
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.timeline-title {
font-size: 13px;
font-weight: 500;
color: #606266;
}
.timeline-legend {
display: flex;
gap: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.legend-dot.perfect {
background-color: #67c23a;
}
.legend-dot.excellent {
background-color: #85ce61;
}
.legend-dot.good {
background-color: #e6a23c;
}
.legend-dot.fair {
background-color: #f78989;
}
.legend-dot.poor {
background-color: #f56c6c;
}
.legend-dot.unknown {
background-color: #c0c4cc;
}
.legend-text {
font-size: 11px;
color: #909399;
}
.timeline-container {
position: relative;
min-height: 60px;
}
.timeline-grid {
position: relative;
}
.time-labels {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.time-label {
font-size: 10px;
color: #c0c4cc;
font-family: monospace;
}
.health-bars {
display: flex;
height: 12px;
border-radius: 6px;
overflow: hidden;
background-color: #f0f0f0;
margin-bottom: 8px;
}
.health-segment {
height: 100%;
transition: all 0.3s ease;
cursor: pointer;
}
/* 颜色现在通过动态样式设置,不再需要这些CSS类 */
.health-segment:hover {
opacity: 0.8;
transform: scaleY(1.2);
}
.response-time-chart {
height: 30px;
margin-bottom: 8px;
}
.response-chart {
width: 100%;
height: 100%;
}
.health-summary {
display: flex;
justify-content: space-around;
padding-top: 8px;
border-top: 1px solid #e4e7ed;
}
.summary-item {
text-align: center;
}
.summary-value {
display: block;
font-size: 14px;
font-weight: 600;
color: #409eff;
line-height: 1;
}
.summary-label {
font-size: 10px;
color: #909399;
margin-top: 2px;
}
/* 紧凑模式 */
.health-timeline.compact {
padding: 8px;
}
.health-timeline.compact .timeline-header {
margin-bottom: 8px;
}
.health-timeline.compact .timeline-title {
font-size: 12px;
}
.health-timeline.compact .health-bars {
height: 8px;
margin-bottom: 6px;
}
.health-timeline.compact .health-summary {
padding-top: 6px;
}
.health-timeline.compact .summary-value {
font-size: 12px;
}
.health-timeline.compact .summary-label {
font-size: 9px;
}
</style>
@@ -0,0 +1,563 @@
<template>
<div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" label-position="left"
@submit.prevent="handleSubmit">
<el-form-item label="节点名称" prop="name" required>
<el-input v-model="form.name" placeholder="请输入节点名称,如:北京-联通-01" maxlength="100" show-word-limit clearable>
<template #prefix>
<el-icon>
<Monitor />
</el-icon>
</template>
</el-input>
<div class="form-tip">建议使用地区-运营商-编号的格式命名</div>
</el-form-item>
<el-row :gutter="20">
<el-col :span="16">
<el-form-item label="主机地址" prop="host" required>
<el-input v-model="form.host" placeholder="请输入IP地址或域名" clearable>
<template #prefix>
<el-icon>
<Location />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="端口" prop="port" required>
<el-input-number v-model="form.port" :min="1" :max="65535" placeholder="端口号" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="协议类型" prop="protocol" required>
<el-radio-group v-model="form.protocol">
<el-radio value="tcp">TCP</el-radio>
<el-radio value="udp">UDP</el-radio>
<el-radio value="ws">WebSocket</el-radio>
<el-radio value="wss">WebSocket Secure</el-radio>
</el-radio-group>
<div class="form-tip">选择节点支持的连接协议</div>
</el-form-item>
<el-form-item label="允许中转" prop="allow_relay" required>
<el-radio-group v-model="form.allow_relay">
<el-radio :value="true">允许中转数据</el-radio>
<el-radio :value="false">仅用于打洞</el-radio>
</el-radio-group>
<div class="form-tip">选择节点是否允许中转其他用户的数据流量</div>
</el-form-item>
<el-form-item label="网络名称" prop="network_name" required>
<el-input v-model="form.network_name" placeholder="请输入EasyTier网络名称" maxlength="100" clearable>
<template #prefix>
<el-icon>
<Connection />
</el-icon>
</template>
</el-input>
<div class="form-tip"> EasyTier network name 一致用于后端探活</div>
</el-form-item>
<el-form-item label="网络密码" prop="network_secret" required>
<el-input v-model="form.network_secret" type="password" placeholder="请输入网络密码" maxlength="100" clearable
show-password>
<template #prefix>
<el-icon>
<Lock />
</el-icon>
</template>
</el-input>
<div class="form-tip"> EasyTier network secret 一致</div>
</el-form-item>
<el-form-item label="最大网络数" prop="max_connections" required>
<el-input-number v-model="form.max_connections" :min="1" :max="10000" placeholder="最大网络数量"
style="width: 200px" />
<div class="form-tip">节点能够承载的最大网络数量</div>
</el-form-item>
<el-form-item label="节点描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="4" placeholder="请描述您的节点特点,如:地理位置、网络质量、使用限制等"
maxlength="500" show-word-limit />
<div class="form-tip">详细描述有助于用户选择合适的节点</div>
</el-form-item>
<!-- 新增标签管理仅在管理员编辑时显示 -->
<el-form-item v-if="props.showTags" label="标签" prop="tags">
<el-select v-model="form.tags" multiple filterable allow-create default-first-option :multiple-limit="10"
placeholder="输入后按回车添加,如:北京、联通、IPv6、高带宽">
<el-option v-for="opt in (form.tags || [])" :key="opt" :label="opt" :value="opt" />
</el-select>
<div class="form-tip">用于分类与检索建议 1-6 个标签每个不超过 32 字符</div>
</el-form-item>
<!-- 联系方式 -->
<el-form-item label="联系方式" prop="contact_info">
<div class="contact-section">
<el-form-item label="微信" prop="wechat">
<el-input v-model="form.wechat" placeholder="请输入微信号" maxlength="50" clearable>
<template #prefix>
<el-icon>
<ChatDotRound />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="QQ" prop="qq_number">
<el-input v-model="form.qq_number" placeholder="请输入QQ号" maxlength="20" clearable>
<template #prefix>
<el-icon>
<User />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="邮箱" prop="mail">
<el-input v-model="form.mail" placeholder="请输入邮箱地址" maxlength="100" clearable>
<template #prefix>
<el-icon>
<Message />
</el-icon>
</template>
</el-input>
</el-form-item>
<div class="form-tip">请至少填写一种联系方式便于节点问题时联系您仅管理员可见</div>
</div>
</el-form-item>
<!-- 连接测试 -->
<el-form-item label="连接测试">
<div class="test-section">
<el-button type="warning" @click="testConnection" :loading="testing" :disabled="!canTest">
<el-icon>
<Connection />
</el-icon>
测试连接
</el-button>
<div v-if="testResult" class="test-result">
<el-tag :type="testResult.success ? 'success' : 'danger'" size="large">
{{ testResult.success ? '连接成功' : '连接失败' }}
</el-tag>
<span v-if="testResult.message" class="test-message">
{{ testResult.message }}
</span>
</div>
</div>
<div class="form-tip">建议在提交前测试连接以确保节点可用</div>
</el-form-item>
<!-- 使用条款 -->
<el-form-item prop="agreed" v-if="props.showAgreement">
<el-checkbox v-model="form.agreed">
我已阅读并同意
<el-button type="primary" link @click="showTerms = true">
节点共享协议
</el-button>
</el-checkbox>
</el-form-item>
<!-- 提交按钮 -->
<el-form-item>
<div class="submit-section">
<el-button type="primary" size="large" @click="handleSubmit" :loading="submitting"
:disabled="!form.agreed && props.showAgreement">
<el-icon>
<Upload />
</el-icon>
提交节点
</el-button>
<el-button size="large" @click="resetFields">
<el-icon>
<RefreshLeft />
</el-icon>
重置表单
</el-button>
</div>
</el-form-item>
</el-form> <!-- 使用条款对话框 -->
<el-dialog v-model="showTerms" title="节点共享协议" width="600px">
<div class="terms-content">
<h3>1. 节点共享原则</h3>
<p> 节点提供者应确保节点的稳定性和可用性</p>
<p> 不得利用共享节点进行违法违规活动</p>
<p> 尊重其他用户的使用权益</p>
<h3>2. 服务质量要求</h3>
<p> 节点应保持7x24小时稳定运行</p>
<p> 网络延迟应控制在合理范围内</p>
<p> 及时处理连接问题和故障</p>
<h3>3. 数据安全</h3>
<p> 不得记录或泄露用户传输数据</p>
<p> 保护用户隐私和数据安全</p>
<p> 遵守相关法律法规</p>
<h3>4. 免责声明</h3>
<p> 平台不对节点服务质量承担责任</p>
<p> 用户使用节点服务的风险自担</p>
<p> 平台有权移除不符合要求的节点</p>
</div>
<template #footer>
<el-button @click="showTerms = false">关闭</el-button>
<el-button type="primary" @click="acceptTerms">同意并关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import {
Monitor,
Location,
PriceTag,
Connection,
Upload,
Edit,
RefreshLeft,
ChatDotRound,
User,
Message
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { nodeApi } from '../api'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({
name: '',
host: '',
port: 11010,
protocol: 'tcp',
allow_relay: true,
network_name: '',
network_secret: '',
max_connections: 100,
description: '',
wechat: '',
qq_number: '',
mail: '',
tags: [],
agreed: false
})
},
submitting: {
type: Boolean,
default: false
},
submitText: {
type: String,
default: '提交节点'
},
submitIcon: {
type: String,
default: 'Upload'
},
showConnectionTest: {
type: Boolean,
default: true
},
showAgreement: {
type: Boolean,
default: true
},
showCancel: {
type: Boolean,
default: false
},
// 新增:是否显示标签管理
showTags: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'submit', 'reset', 'cancel', 'show-terms'])
const formRef = ref()
const testing = ref(false)
const testResult = ref(null)
const showTerms = ref(false)
// 表单数据
const form = reactive({ ...props.modelValue })
// 监听props变化,更新表单数据
watch(() => props.modelValue, (newValue) => {
Object.assign(form, newValue)
}, { deep: true })
// 监听表单变化,向上传递
watch(form, (newValue) => {
emit('update:modelValue', { ...newValue })
}, { deep: true })
// 表单验证规则
const rules = {
name: [
{ required: true, message: '请输入节点名称', trigger: 'blur' },
{ min: 1, max: 100, message: '节点名称长度应在1-100个字符之间', trigger: 'blur' }
],
host: [
{ required: true, message: '请输入主机地址', trigger: 'blur' },
{ min: 1, max: 255, message: '主机地址长度应在1-255个字符之间', trigger: 'blur' },
{
pattern: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/,
message: '请输入有效的IP地址或域名',
trigger: 'blur'
}
],
port: [
{ required: true, message: '请输入端口号', trigger: 'blur' },
{ type: 'number', min: 1, max: 65535, message: '端口号应在1-65535之间', trigger: 'blur' }
],
protocol: [
{ required: true, message: '请选择协议类型', trigger: 'change' }
],
max_connections: [
{ required: true, message: '请输入最大连接数', trigger: 'blur' },
{ type: 'number', min: 1, max: 10000, message: '最大连接数应在1-10000之间', trigger: 'blur' }
],
version: [
{ max: 50, message: '版本信息长度不能超过50个字符', trigger: 'blur' }
],
description: [
{ max: 500, message: '描述长度不能超过500个字符', trigger: 'blur' }
],
wechat: [
{ max: 50, message: '微信号长度不能超过50个字符', trigger: 'blur' }
],
qq_number: [
{ max: 20, message: 'QQ号长度不能超过20个字符', trigger: 'blur' },
{ pattern: /^[1-9][0-9]{4,19}$/, message: '请输入有效的QQ号', trigger: 'blur' }
],
mail: [
{ max: 100, message: '邮箱地址长度不能超过100个字符', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
contact_info: [
{
validator: (rule, value, callback) => {
if (!form.wechat && !form.qq_number && !form.mail) {
callback(new Error('请至少填写一种联系方式'))
} else {
callback()
}
},
trigger: 'blur'
}
],
agreed: [
{
validator: (rule, value, callback) => {
if (!value) {
callback(new Error('请阅读并同意节点共享协议'))
} else {
callback()
}
},
trigger: 'change'
}
],
// 新增:标签规则(仅在显示标签管理时生效)
tags: [
{
validator: (rule, value, callback) => {
if (!props.showTags) {
callback()
return
}
if (!Array.isArray(form.tags)) {
callback(new Error('标签格式错误'))
return
}
if (form.tags.length > 10) {
callback(new Error('最多添加 10 个标签'))
return
}
for (const t of form.tags) {
const s = (t || '').trim()
if (s.length === 0) {
callback(new Error('标签不能为空'))
return
}
if (s.length > 32) {
callback(new Error('每个标签不超过 32 字符'))
return
}
}
callback()
},
trigger: 'change'
}
]
}
// 是否可以测试连接
const canTest = computed(() => {
return form.host && form.port && form.protocol && form.network_name && form.network_secret
})
const buildDataFromForm = () => {
const data = {
name: form.name || 'Test Node',
host: form.host,
port: form.port,
protocol: form.protocol,
description: form.description || null,
max_connections: form.max_connections || 100,
allow_relay: form.allow_relay,
network_name: form.network_name || null,
network_secret: form.network_secret || null,
wechat: form.wechat || null,
qq_number: form.qq_number || null,
mail: form.mail || null
}
// 仅在管理员编辑时附带标签
if (props.showTags) {
data.tags = Array.isArray(form.tags) ? form.tags : []
}
return data
}
// 测试连接
const testConnection = async () => {
if (!canTest.value) {
ElMessage.warning('请先填写主机地址、端口、协议、网络名称和网络密码')
return
}
testing.value = true
testResult.value = null
try {
// 构建测试数据
const testData = buildDataFromForm()
// 调用实际的连接测试API
const response = await nodeApi.testConnection(testData)
if (response.success) {
testResult.value = {
success: true,
message: '连接测试成功,节点可正常访问'
}
ElMessage.success('连接测试成功')
} else {
testResult.value = {
success: false,
message: response.error || '连接测试失败'
}
ElMessage.error('连接测试失败')
}
} catch (error) {
console.error('连接测试失败:', error)
testResult.value = {
success: false,
message: error.response?.data?.error || '测试过程中发生错误,请检查网络连接'
}
ElMessage.error('连接测试失败')
} finally {
testing.value = false
}
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (!valid) return
const submitData = buildDataFromForm()
emit('submit', submitData)
} catch (error) {
console.error('表单验证失败:', error)
}
}
// 重置表单
const resetFields = () => {
if (formRef.value) {
formRef.value.resetFields()
}
// 重置标签
if (props.showTags) {
form.tags = []
}
testResult.value = null
emit('reset')
}
const acceptTerms = () => {
form.agreed = true
showTerms.value = false
ElMessage.success('已同意节点共享协议')
}
// 暴露方法给父组件
defineExpose({
validate: () => formRef.value?.validate(),
resetFields: () => formRef.value?.resetFields()
})
</script>
<style scoped>
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.test-section {
display: flex;
align-items: center;
gap: 12px;
}
.test-result {
display: flex;
align-items: center;
gap: 8px;
}
.test-message {
font-size: 12px;
color: #606266;
}
.submit-section {
display: flex;
gap: 12px;
}
.contact-section {
width: 100%;
}
.contact-section .el-form-item {
margin-bottom: 16px;
}
.contact-section .el-form-item:last-of-type {
margin-bottom: 8px;
}
.contact-section .el-form-item__label {
font-size: 14px;
color: #606266;
font-weight: 500;
}
</style>
@@ -0,0 +1,22 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import router from './router'
import App from './App.vue'
import './style.css'
const app = createApp(App)
// 注册Element Plus
app.use(ElementPlus)
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 注册路由
app.use(router)
app.mount('#app')
@@ -0,0 +1,78 @@
import { createRouter, createWebHistory } from 'vue-router'
import NodeDashboard from '../views/NodeDashboard.vue'
import SubmitNode from '../views/SubmitNode.vue'
import AdminLogin from '../views/AdminLogin.vue'
import AdminDashboard from '../views/AdminDashboard.vue'
const routes = [
{
path: '/',
name: 'Dashboard',
component: NodeDashboard,
meta: {
title: '节点状态监控'
}
},
{
path: '/submit',
name: 'Submit',
component: SubmitNode,
meta: {
title: '提交共享节点'
}
},
{
path: '/admin/login',
name: 'AdminLogin',
component: AdminLogin,
meta: {
title: '管理员登录'
}
},
{
path: '/admin',
name: 'AdminDashboard',
component: AdminDashboard,
meta: {
title: '管理员面板',
requiresAuth: true
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - EasyTier Uptime`
}
// 检查管理员权限
if (to.meta.requiresAuth) {
const token = localStorage.getItem('admin_token')
if (!token) {
next('/admin/login')
return
}
// 验证token有效性
try {
const { adminApi } = await import('../api')
await adminApi.verifyToken()
} catch (error) {
console.error('Token verification failed:', error)
localStorage.removeItem('admin_token')
next('/admin/login')
return
}
}
next()
})
export default router
@@ -0,0 +1,243 @@
/* 自定义样式 */
:root {
--primary-color: #409EFF;
--success-color: #67C23A;
--warning-color: #E6A23C;
--danger-color: #F56C6C;
--info-color: #909399;
--text-primary: #303133;
--text-regular: #606266;
--text-secondary: #909399;
--text-placeholder: #C0C4CC;
--border-base: #DCDFE6;
--border-light: #E4E7ED;
--border-lighter: #EBEEF5;
--border-extra-light: #F2F6FC;
--background-base: #F5F7FA;
--background-light: #FAFAFA;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 工具类 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.mb-10 {
margin-bottom: 10px;
}
.mb-20 {
margin-bottom: 20px;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
.p-10 {
padding: 10px;
}
.p-20 {
padding: 20px;
}
/* 动画效果 */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-up {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式断点 */
@media (max-width: 768px) {
.mobile-hidden {
display: none !important;
}
}
@media (min-width: 769px) {
.desktop-hidden {
display: none !important;
}
}
/* 状态指示器 */
.status-online {
color: var(--success-color);
}
.status-offline {
color: var(--danger-color);
}
.status-warning {
color: var(--warning-color);
}
/* 卡片阴影效果 */
.card-shadow {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s;
}
.card-shadow:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
}
/* 加载状态 */
.loading-overlay {
position: relative;
}
.loading-overlay::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* 表格样式增强 */
.el-table .el-table__row:hover {
cursor: pointer;
}
/* 按钮组样式 */
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.button-group .el-button {
margin: 0;
}
/* 统计卡片样式 */
.stat-card {
text-align: center;
padding: 10px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-2px);
}
/* 标签样式 */
.tag-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* 描述列表样式 */
.description-list {
display: grid;
grid-template-columns: auto 1fr;
gap: 10px 20px;
align-items: center;
}
.description-list .label {
font-weight: 600;
color: var(--text-regular);
}
.description-list .value {
color: var(--text-primary);
}
@@ -0,0 +1,62 @@
// Deterministic tag color generator (pure frontend)
// Same tag => same color; different tags => different colors
function stringHash(str) {
const s = String(str)
let hash = 5381
for (let i = 0; i < s.length; i++) {
hash = (hash * 33) ^ s.charCodeAt(i)
}
return hash >>> 0 // ensure positive
}
function hslToRgb(h, s, l) {
// h,s,l in [0,1]
let r, g, b
if (s === 0) {
r = g = b = l // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1 / 3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1 / 3)
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]
}
function rgbToHex(r, g, b) {
const toHex = (v) => v.toString(16).padStart(2, '0')
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
}
export function getTagStyle(tag) {
const hash = stringHash(tag)
const hue = hash % 360 // 0-359
const saturation = 65 // percentage
const lightness = 47 // percentage
const rgb = hslToRgb(hue / 360, saturation / 100, lightness / 100)
const hex = rgbToHex(rgb[0], rgb[1], rgb[2])
// Perceived brightness for text color selection
const brightness = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114
const textColor = brightness > 160 ? '#1f1f1f' : '#ffffff'
return {
backgroundColor: hex,
borderColor: hex,
color: textColor
}
}
@@ -0,0 +1,631 @@
<template>
<div>
<el-container class="admin-dashboard">
<!-- 头部导航 -->
<el-header class="admin-header">
<div class="header-content">
<div class="flex">
<h1 class="header-title">管理员面板</h1>
</div>
<div class="header-actions">
<router-link to="/" class="nav-link">
返回首页
</router-link>
<el-button type="danger" @click="logout">
退出登录
</el-button>
</div>
</div>
</el-header>
<!-- 主要内容 -->
<el-main class="main-content">
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb-20">
<el-col :xs="24" :sm="12" :md="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon success">
<el-icon>
<Check />
</el-icon>
</div>
<div class="stat-info">
<div class="stat-label">已审批节点</div>
<div class="stat-value">{{ stats.approved }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon warning">
<el-icon>
<Clock />
</el-icon>
</div>
<div class="stat-info">
<div class="stat-label">待审批节点</div>
<div class="stat-value">{{ stats.pending }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon info">
<el-icon>
<DataAnalysis />
</el-icon>
</div>
<div class="stat-info">
<div class="stat-label">总节点数</div>
<div class="stat-value">{{ stats.total }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon success">
<el-icon>
<CircleCheck />
</el-icon>
</div>
<div class="stat-info">
<div class="stat-label">在线节点</div>
<div class="stat-value">{{ stats.active }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选器 -->
<el-card class="mb-20">
<template #header>
<span>筛选条件</span>
</template>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="审批状态">
<el-select v-model="filters.approved" @change="loadNodes" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="已审批" value="true" />
<el-option label="待审批" value="false" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="在线状态">
<el-select v-model="filters.active" @change="loadNodes" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="在线" value="true" />
<el-option label="离线" value="false" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="协议">
<el-select v-model="filters.protocol" @change="loadNodes" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="TCP" value="tcp" />
<el-option label="UDP" value="udp" />
<el-option label="WireGuard" value="wg" />
<el-option label="WebSocket" value="ws" />
<el-option label="WebSocket Secure" value="wss" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<el-form-item label="搜索">
<el-input v-model="filters.search" @input="debounceSearch" placeholder="搜索节点名称或主机" clearable />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 节点列表 -->
<el-card>
<template #header>
<div class="flex-between">
<div>
<h3>节点列表</h3>
<p class="text-secondary">管理所有共享节点</p>
</div>
</div>
</template>
<div v-if="loading" class="text-center p-20">
<el-icon class="is-loading" size="32">
<Loading />
</el-icon>
<p class="mt-10">加载中...</p>
</div>
<el-table v-else-if="nodes.length > 0" :data="nodes" stripe>
<el-table-column prop="name" label="节点名称" min-width="120">
<template #default="{ row }">
<div class="flex items-center">
<el-icon class="mr-2"
:color="row.is_active && row.is_approved ? '#67C23A' : !row.is_approved ? '#E6A23C' : '#F56C6C'">
<CircleCheck v-if="row.is_active && row.is_approved" />
<Clock v-else-if="!row.is_approved" />
<el-icon v-else></el-icon>
</el-icon>
<span>{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="host" label="主机地址" min-width="150">
<template #default="{ row }">
{{ row.host }}:{{ row.port }}
</template>
</el-table-column>
<el-table-column prop="protocol" label="协议" width="80">
<template #default="{ row }">
<el-tag :type="getProtocolType(row.protocol)" size="small">
{{ row.protocol.toUpperCase() }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_approved" label="审批状态" width="100">
<template #default="{ row }">
<el-tag :type="row.is_approved ? 'success' : 'warning'" size="small">
{{ row.is_approved ? '已审批' : '待审批' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_active" label="在线状态" width="100">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">
{{ row.is_active ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="150" show-overflow-tooltip />
<el-table-column prop="tags" label="标签" min-width="160">
<template #default="{ row }">
<div class="tags-list">
<el-tag v-for="(tag, idx) in row.tags" :key="tag + idx" size="small" class="tag-chip" :style="getTagStyle(tag)">
{{ tag }}
</el-tag>
<span v-if="!row.tags || row.tags.length === 0" class="text-muted"></span>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="editNode(row)">
编辑
</el-button>
<el-button v-if="!row.is_approved" type="success" size="small" @click="approveNode(row.id)">
审批
</el-button>
<el-button v-if="row.is_approved" type="warning" size="small" @click="revokeApproval(row.id)">
撤销
</el-button>
<el-button type="danger" size="small" @click="deleteNode(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无节点数据" />
</el-card>
</el-main>
</el-container>
<!-- 编辑节点对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑节点" width="800px" destroy-on-close>
<NodeForm v-if="editDialogVisible" v-model="editForm" :submitting="updating" submit-text="更新节点" submit-icon="Edit"
:show-connection-test="false" :show-agreement="false" :show-cancel="true" :show-tags="true"
@submit="handleUpdateNode" @cancel="editDialogVisible = false" @reset="resetEditForm" />
</el-dialog>
</div>
</template>
<script>
import { adminApi } from '../api'
import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Check, Clock, DataAnalysis, CircleCheck, Loading } from '@element-plus/icons-vue'
import NodeForm from '../components/NodeForm.vue'
import { getTagStyle } from '../utils/tagColor'
export default {
name: 'AdminDashboard',
components: {
Check,
Clock,
DataAnalysis,
CircleCheck,
Loading,
NodeForm
},
data() {
return {
loading: false,
nodes: [],
filters: {
approved: '',
active: '',
protocol: '',
search: ''
},
searchTimeout: null,
editDialogVisible: false,
editForm: {
name: '',
host: '',
port: 11010,
protocol: 'tcp',
version: '',
max_connections: 100,
description: '',
tags: []
},
editingNodeId: null,
updating: false
}
},
computed: {
stats() {
const total = this.nodes.length
const approved = this.nodes.filter(node => node.is_approved).length
const pending = this.nodes.filter(node => !node.is_approved).length
const active = this.nodes.filter(node => node.is_active).length
return {
total,
approved,
pending,
active
}
}
},
async mounted() {
// 先验证token有效性
try {
await adminApi.verifyToken()
await this.loadNodes()
} catch (error) {
console.error('Token verification failed in mounted:', error)
this.logout()
}
},
methods: {
getTagStyle,
async loadNodes() {
try {
this.loading = true
const params = {}
if (this.filters.approved !== '') {
params.approved = this.filters.approved
}
if (this.filters.active !== '') {
params.active = this.filters.active
}
if (this.filters.protocol) {
params.protocol = this.filters.protocol
}
if (this.filters.search) {
params.search = this.filters.search
}
const response = await adminApi.getNodes(params)
this.nodes = response.data?.items || []
} catch (error) {
console.error('加载节点失败:', error)
if (error.response?.status === 401) {
this.logout()
} else {
ElMessage.error('加载节点失败')
}
} finally {
this.loading = false
}
},
async approveNode(nodeId) {
try {
await ElMessageBox.confirm('确定要审批通过这个节点吗?', '确认审批', {
type: 'warning'
})
await adminApi.approveNode(nodeId)
ElMessage.success('审批成功')
await this.loadNodes()
} catch (error) {
if (error !== 'cancel') {
console.error('审批失败:', error)
ElMessage.error('审批失败')
}
}
},
async revokeApproval(nodeId) {
try {
await ElMessageBox.confirm('确定要撤销这个节点的审批吗?撤销后节点将变为待审批状态。', '确认撤销审批', {
type: 'warning'
})
await adminApi.revokeApproval(nodeId)
ElMessage.success('撤销审批成功')
await this.loadNodes()
} catch (error) {
if (error !== 'cancel') {
console.error('撤销审批失败:', error)
ElMessage.error('撤销审批失败')
}
}
},
async deleteNode(nodeId) {
try {
await ElMessageBox.confirm('确定要删除这个节点吗?此操作不可恢复!', '确认删除', {
type: 'warning'
})
await adminApi.deleteNode(nodeId)
ElMessage.success('删除成功')
await this.loadNodes()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}
},
editNode(node) {
this.editingNodeId = node.id
// 只取需要的字段,并复制 tags 数组以避免引用问题
this.editForm = {
id: node.id,
name: node.name,
host: node.host,
port: node.port,
protocol: node.protocol,
version: node.version,
max_connections: node.max_connections,
description: node.description || '',
allow_relay: node.allow_relay,
network_name: node.network_name,
network_secret: node.network_secret,
wechat: node.wechat,
qq_number: node.qq_number,
mail: node.mail,
tags: Array.isArray(node.tags) ? [...node.tags] : []
}
this.editDialogVisible = true
},
async handleUpdateNode(formData) {
try {
this.updating = true
// 确保提交包含 tags 字段(为空数组也传)
const payload = {
name: formData.name,
host: formData.host,
port: formData.port,
protocol: formData.protocol,
version: formData.version,
max_connections: formData.max_connections,
description: formData.description,
allow_relay: formData.allow_relay,
network_name: formData.network_name,
network_secret: formData.network_secret,
wechat: formData.wechat,
qq_number: formData.qq_number,
mail: formData.mail,
tags: Array.isArray(formData.tags) ? formData.tags : []
}
await adminApi.updateNode(this.editingNodeId, payload)
ElMessage.success('节点更新成功')
this.editDialogVisible = false
await this.loadNodes()
} catch (error) {
console.error('更新节点失败:', error)
ElMessage.error('更新节点失败')
} finally {
this.updating = false
}
},
resetEditForm() {
this.editForm = {
name: '',
host: '',
port: 11010,
protocol: 'tcp',
version: '',
max_connections: 100,
description: ''
}
},
debounceSearch() {
if (this.searchTimeout) {
clearTimeout(this.searchTimeout)
}
this.searchTimeout = setTimeout(() => {
this.loadNodes()
}, 500)
},
formatDate(dateString) {
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss')
},
getProtocolType(protocol) {
const typeMap = {
tcp: 'primary',
udp: 'success',
wg: 'warning',
ws: 'info',
wss: 'danger'
}
return typeMap[protocol] || 'info'
},
async logout() {
try {
await ElMessageBox.confirm('确定要退出登录吗?', '确认退出', {
type: 'warning'
})
localStorage.removeItem('admin_token')
this.$router.push('/admin/login')
} catch (error) {
// 用户取消
}
}
}
}
</script>
<style scoped>
.admin-dashboard {
min-height: 100vh;
}
.admin-header {
background: white;
border-bottom: 1px solid #e4e7ed;
padding: 0;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 100%;
}
.header-title {
margin: 0;
color: #303133;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.nav-link {
color: #409eff;
text-decoration: none;
}
.nav-link:hover {
color: #66b1ff;
}
.main-content {
background: #f5f7fa;
padding: 20px;
}
.mb-20 {
margin-bottom: 20px;
}
.stat-card {
position: relative;
overflow: hidden;
height: 100px;
}
.stat-content {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.stat-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.stat-label {
font-size: 12px;
color: #909399;
margin: 0 0 4px 0;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
line-height: 1;
margin: 0;
}
.stat-icon {
font-size: 28px;
opacity: 0.3;
margin-left: 16px;
}
.stat-icon.success {
color: #67c23a;
}
.stat-icon.warning {
color: #e6a23c;
}
.stat-icon.info {
color: #409eff;
}
.flex {
display: flex;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.items-center {
align-items: center;
}
.mr-2 {
margin-right: 8px;
}
.mt-10 {
margin-top: 10px;
}
.p-20 {
padding: 20px;
}
.text-center {
text-align: center;
}
.text-secondary {
color: #909399;
}
.tag-chip {
margin-right: 4px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More