Compare commits

...

154 Commits

Author SHA1 Message Date
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
Sijie.Sun 1be64223c8 ensure dst have session when we are initiator (#398)
* ensure dst have session when we are initiator

* bump version to 2.0.1
2024-10-08 21:05:46 +08:00
Sijie.Sun a08a8e7f4c update gui dep to resolve mem leak (#394) 2024-10-08 09:21:47 +08:00
Li Yang b31996230d add curl dependency check in the installation script (#391)
In many Linux containers, curl is not installed by default, just like
unzip.
2024-10-07 23:23:44 +08:00
Sijie.Sun 1e836501a8 serialize all sym hole punch (#390) 2024-10-07 23:04:49 +08:00
Sijie.Sun d4e59ffc40 fix listener may have no mapped addr (#389) 2024-10-07 12:15:20 +08:00
Sijie.Sun 37ceb77bf6 nat4-nat4 punch (#388)
this patch optimize the udp hole punch logic:

1. allow start punch hole before stun test complete.
2. add lock to symmetric punch, avoid conflict between concurrent hole punching task.
3. support punching hole for predictable nat4-nat4.
4. make backoff of retry reasonable
2024-10-06 22:49:18 +08:00
sijie.sun ba3da97ad4 fix ipv6 direct connector not work 2024-10-03 11:56:10 +08:00
sijie.sun 984ed8f6cf fix #367
introduce my peer route id and peer id is duplicated only when peer
route id is not same.

this problem occurs because update_self may increase my peer info
version and propagate to ther nodes.
2024-09-29 23:58:33 +08:00
sijie.sun c7895963e4 rollback some parameters 2024-09-29 23:17:46 +08:00
sijie.sun a0ece6ad4d fix public server address in readme 2024-09-29 21:35:16 +08:00
sijie.sun d0a3a40a0f fix bugs
add timeout for wss try_accept

public server should show stats

use default values for flags

bump version to 2.0.0
2024-09-29 17:49:14 +08:00
sijie.sun ff5ee8a05e support forward foreign network packet between peers 2024-09-29 10:31:29 +08:00
Hs_Yeah a50bcf3087 Fix IP address display in the status page of GUI
Signed-off-by: Hs_Yeah <bYeahq@gmail.com>
2024-09-27 15:58:02 +08:00
sijie.sun e0b364d3e2 use ubuntu 24.04 apt source
github action upgraded the ubuntu-latest to 24.04

https://github.com/actions/runner-images/pull/10687
2024-09-27 11:05:52 +08:00
sijie.sun 2496cf51c3 fix connection loss when traffic is huge 2024-09-26 23:49:01 +08:00
sijie.sun 7b4a01e7fb fix ring buffer stuck when using multi thread runtime 2024-09-26 14:34:33 +08:00
Hs_Yeah 3f9a1d8f2e Get dev_name from the global_ctx of each instance 2024-09-24 16:52:38 +08:00
Hs_Yeah 0b927bcc91 Add TUN device name setting support to easytier-gui 2024-09-24 16:52:38 +08:00
Hs_Yeah 92397bf7b6 Set Category of the TUN device's network profile to 1 in Windows Registry 2024-09-24 14:23:42 +08:00
sijie.sun d1e2e1db2b fix ospf foreign network info version 2024-09-23 13:42:25 +08:00
sijie.sun 783ba50c9e add cli command for global foreign network info 2024-09-23 00:03:57 +08:00
sijie.sun aca9a0e35b use ospf route to propogate foreign network info 2024-09-22 22:12:18 +08:00
liyang fb8d262554 Fix spelling errors 2024-09-22 20:58:37 +08:00
sijie.sun bd60cfc2a0 add feature flag to ospf route 2024-09-21 20:54:19 +08:00
sijie.sun 06afd221d5 make ping more smart 2024-09-21 18:00:52 +08:00
sijie.sun 0171fb35a4 fix upload oss 2024-09-21 00:24:58 +08:00
Jiangqiu Shen 99c47813c3 add the options to enable latency first or not
in the old behavior, the flags is not set, and it will be generated as default value in the first read. so the default value for the latency_first will be set to true according to the Default settings to Flag.

so the Vue code init the latency first to true.
2024-09-19 20:09:17 +08:00
sijie.sun 82f5dfd569 show nodes version correctly 2024-09-18 23:15:08 +08:00
sijie.sun 6d7edcd486 fix connect failed after setup one of sockets fails 2024-09-18 23:15:08 +08:00
M2kar 9f273dc887 modify compile command (#333)
* modify compile command

* fix(READMD.md): compile from git

* Update README_CN.md
2024-09-18 21:57:25 +08:00
Jiangqiu Shen ac9cfa5040 making cli parse code more ergonomic by remove some copy and unwrap (#347)
1. remove some unessesary copy in cli parse code of string
2. make some member function into non-member function to avoid taking the self reference.
3. use if let Some(..) instead of if xxx.is_some() to avoid copy and unwrap
2024-09-18 21:57:12 +08:00
Sijie.Sun 1b03223537 use customized rpc implementation, remove Tarpc & Tonic (#348)
This patch removes Tarpc & Tonic GRPC and implements a customized rpc framework, which can be used by peer rpc and cli interface.

web config server can also use this rpc framework.

moreover, rewrite the public server logic, use ospf route to implement public server based networking. this make public server mesh possible.
2024-09-18 21:55:28 +08:00
m1m1sha 0467b0a3dc Merge pull request #342 from EasyTier/ci/issue-template
🐎 ci: Modify Text
2024-09-15 22:39:11 +08:00
m1m1sha ba75167238 🐎 ci: Modify Text 2024-09-15 22:38:06 +08:00
m1m1sha 51e7daa26f Merge pull request #341 from EasyTier/ci/github-issue-template
🐎 ci: github issue template
2024-09-15 22:30:49 +08:00
m1m1sha 2ff653cc6f 🐎 ci: github issue template 2024-09-15 22:28:55 +08:00
m1m1sha cfe4d080d5 🐞 fix: GUI relay display error (#335) 2024-09-14 11:41:38 +08:00
M2kar 9b28ecde8e fix compile error due to rust version format (#332) 2024-09-14 11:40:46 +08:00
Sijie.Sun 096ed39d23 fix udp proxy disconn unexpectedly (#321) 2024-09-11 23:46:26 +08:00
m1m1sha 6ea3adcef8 feat: show version & local node (#318)
*  feat: version

Add display version information, incompatible with lower versions

* 🎈 perf: unknown

Unknown when there is no version number displayed

*  feat: Display local nodes

Display local nodes, incompatible with lower versions
2024-09-11 15:58:13 +08:00
m1m1sha 4342be29d7 Perf/front page (#316)
* 🐳 chore: dependencies

* 🐞 fix: minor style issues

fixed background white patches in dark mode
fixed the line height of the status label, which resulted in a bloated appearance

* 🌈 style: lint

*  feat: about
2024-09-11 09:13:00 +08:00
Sijie.Sun 1609c97574 fix panic when wireguard tunnel encounter udp recv error (#299) 2024-09-02 09:37:34 +08:00
Sijie.Sun f07b3ee9c6 fix punching task leak (#298)
the punching task creator doesn't check if the task is already
running, and may create many punching task to same peer node.

this patch also improve hole punching by checking hole punch packet
even if punch rpc is failed.
2024-08-31 14:37:34 +08:00
Sijie.Sun 2058dbc470 fix wg client hang after some time (#297)
wg portal doesn't know client disconnect causing msg overstocked in queue, make
entire peer packet process pipeline hang.
2024-08-31 12:44:12 +08:00
3RDNature 6964fb71fc Add a setting "disable_udp_hole_punch" to disable UDP hole punch function (#291)
It can solve #289 tentative.

Co-authored-by: 3rdnature <root@natureblog.net>
2024-08-29 11:34:30 +08:00
Jiangqiu Shen a8bb4ee7e5 Update Cargo.toml (#290)
fix compile error metioned in #286
2024-08-29 09:06:48 +08:00
严浩 3fcd74ce4e fix: Different network methods server URL display (#283)
Co-authored-by: 严浩 <i@oo1.dev>
2024-08-27 10:09:46 +08:00
Sijie.Sun 2b7ff0efc5 bump version to v1.2.3 and update readme (#280) 2024-08-25 13:45:18 +08:00
Sijie.Sun 5833541a6e set correct route cost for peers relayed by public server (#279) 2024-08-25 12:27:00 +08:00
Sijie.Sun 54c6418f97 only add necessary conn to alive urls (#277)
too many alive conns may cause high cpu usage and lagged broadcast
recv.
2024-08-25 11:12:01 +08:00
Sijie.Sun fc9aac42b4 fix release.yml, just skip zip gui & mobile artifect (#276) 2024-08-25 00:45:14 +08:00
Sijie.Sun 89b43684d8 add complete support for freebsd (#275)
add tun & websocket & wireguard support on freebsd
2024-08-25 00:44:45 +08:00
Sunakier 31b26222d3 feat: support multi-service management (#251)
feat: support config (only config file now)
2024-08-24 10:17:57 +08:00
Mrered Cio e4df03053e add MacOS Homebrew installation method (#273) 2024-08-24 10:13:30 +08:00
Sijie.Sun 833e7eca22 add command to show local node info (#271) 2024-08-23 11:50:11 +08:00
Sijie.Sun b7d85ad2ff update rust-i18n to v3.1.2 (#269) 2024-08-21 11:00:13 +08:00
Sijie.Sun 8793560e12 fix i18n, revert rust-i18n to v3.0.1 (#267) 2024-08-20 00:38:59 +08:00
Dingxuan Jiang 58e0e48d59 easytier-gui: prevent multiple instances (#265)
* easytier-gui: prevent multiple instances
* ignore single instance for Android and iOS
2024-08-19 12:25:36 +08:00
sijie.sun ad4cbbea6d fix socks5 access local virtual ip 2024-08-17 23:52:05 +08:00
sijie.sun db660ee3b1 add test for socks5 server 2024-08-17 21:39:19 +08:00
sijie.sun ae54a872ce support socks5 proxy
usage: --socks5 12345

create an socks5 server on port 12345, can be used by socks5 client to access
virtual network.
2024-08-17 13:17:38 +08:00
sijie.sun 2aa686f7ad use autostart plugin and hide window when autostart 2024-08-17 02:15:15 +08:00
sijie.sun ce10bf5e60 update tauri to rc2 2024-08-17 02:15:15 +08:00
Sijie.Sun 28ae9c447a Update Dockerfile to fix timezone 2024-08-17 00:00:32 +08:00
sijie.sun ff6da9bbec also setup panic handler on gui
this helps collect gui crash info.
2024-08-15 23:00:04 +08:00
sijie.sun 198c239399 set ipv6 mtu on windows
windows use different MTU for ipv4 / ipv6, we should set both.
2024-08-15 22:59:48 +08:00
sijie.sun 0fbbea963f forward foreign peer event to unbounded channel
if some events loss, may cause inconsistent foreign peer info.
2024-08-15 08:03:50 +08:00
sijie.sun 51165c54f5 smoltcp listener should bind multiple times
if smoltcp bind only once on tcp socket, it can only accept exactly
one syn packet in one round. other syn packets will be dropped and
client will receive a RST packet.
2024-08-13 23:01:34 +08:00
sijie.sun f14875aa3f bump version to v1.2.2 2024-08-13 00:46:32 +08:00
sijie.sun 6391dceb62 udpate release.yml 2024-08-13 00:46:32 +08:00
Sijie.Sun 29806b899a add draft release github action (#246) 2024-08-13 00:06:12 +08:00
Sunakier d63a3c01e4 Fix the installation description in the README (#243)
* fix README wording error
2024-08-11 19:43:54 +08:00
Sijie.Sun b6fb7ac962 add two cmd line option (#241)
1. disable_p2p: only using specified peers to relay packets.
2. relay_all_peer_rpc: allow relay route info for networks not in whitelist
2024-08-10 00:26:54 +08:00
Sunakier 1d22fdc972 Add one-click shell script (#196)
* Add one-click install shell script
2024-08-10 00:04:39 +08:00
Xiao Tan d135dd5a6f Revise the description of relay_network_whitelist (#235) 2024-08-09 23:52:05 +08:00
WillisXue 7cae63cb17 fix win tun name, clean up custom tun name (#234) 2024-08-08 23:03:41 +08:00
Sijie.Sun 232165eff3 fix bugs (#236) 2024-08-08 22:03:22 +08:00
Sijie.Sun 2bc4dd8c53 Update install_rust.sh (#237) 2024-08-08 20:07:22 +08:00
288 changed files with 40657 additions and 10124 deletions
+53
View File
@@ -0,0 +1,53 @@
# Copyright 2024-present Easytier Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
name: 🐞 问题报告 / Bug Report
title: '[bug] '
description: 报告一个问题 / Report a bug
labels: ['type: bug', 'status: needs triage']
body:
- type: markdown
attributes:
value: |
## 在提交问题之前 / First of all
1. 请先搜索有关此问题的 [现有问题](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue)。
1. Please search for [existing issues](https://github.com/EasyTier/EasyTier/issues?q=is%3Aissue) about this problem first.
2. 请确保所使用的 Easytier 版本都是最新的。
2. Make sure that all Easytier versions are up-to-date.
3. 请确保这是 EasyTier 的问题,而不是你正在使用的其他内容引起的问题。
3. Make sure it's an issue with EasyTier and not something else you are using.
4. 请记得遵守我们的社区准则并保持友好态度。
4. Remember to follow our community guidelines and be friendly.
- 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
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 重现步骤 / Reproduction
description: 能够重现行为的步骤或指向能够复现的存储库链接。 / A link to a reproduction repo or steps to reproduce the behaviour.
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
- type: textarea
id: expected-behavior
attributes:
label: 预期结果 / Expected behavior
description: 清楚地描述您期望发生的事情。 / A clear description of what you expected to happen.
- type: textarea
id: context
attributes:
label: 额外上下文 / Additional context
description: 在这里添加关于问题的任何其他上下文。 / Add any other context about the problem here.
@@ -0,0 +1,38 @@
# Copyright 2024-present Easytier Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
name: 💡 新功能请求 / Feature Request
title: '[feat] '
description: 提出一个想法 / Suggest an idea
labels: ['type: feature request']
body:
- type: textarea
id: problem
attributes:
label: 描述问题 / Describe the problem
description: 明确描述此功能将解决的问题 / A clear description of the problem this feature would solve
placeholder: "我总是在...感觉困惑 / I'm always frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "描述您想要的解决方案 / Describe the solution you'd like"
description: 明确说明您希望做出的改变 / A clear description of what change you would like
placeholder: '我希望... / I would like to...'
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 替代方案 / Alternatives considered
description: "您考虑过的任何替代解决方案 / Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
label: 额外上下文 / Additional context
description: 在此处添加有关问题的任何其他上下文。 / Add any other context about the problem here.
+4
View File
@@ -18,9 +18,13 @@ RUN mkdir -p /tmp/output; \
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache tzdata
WORKDIR /app WORKDIR /app
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
# users can use "-e TZ=xxx" to adjust it
ENV TZ Asia/Shanghai
# tcp # tcp
EXPOSE 11010/tcp EXPOSE 11010/tcp
# udp # udp
+79 -21
View File
@@ -2,7 +2,7 @@ name: EasyTier Core
on: on:
push: push:
branches: ["develop", "main"] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
@@ -20,14 +20,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }} # do not skip push on branch starts with releases/
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
steps: steps:
- id: skip_check - id: skip_check
uses: fkirc/skip-duplicate-actions@v5 uses: fkirc/skip-duplicate-actions@v5
with: with:
# All of these options are optional, so you can remove them if you are happy with the defaults # All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' 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"]'
build: build:
strategy: strategy:
@@ -35,28 +37,28 @@ jobs:
matrix: matrix:
include: include:
- TARGET: aarch64-unknown-linux-musl - TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-aarch64 ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl - TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-x86_64 ARTIFACT_NAME: linux-x86_64
- TARGET: mips-unknown-linux-musl - TARGET: mips-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl - TARGET: mipsel-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-mipsel ARTIFACT_NAME: linux-mipsel
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested - TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7hf ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested - TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7 ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested - TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-armhf ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested - TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: linux-arm ARTIFACT_NAME: linux-arm
- TARGET: x86_64-apple-darwin - TARGET: x86_64-apple-darwin
@@ -70,6 +72,15 @@ jobs:
OS: windows-latest OS: windows-latest
ARTIFACT_NAME: windows-x86_64 ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-arm64
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-22.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
runs-on: ${{ matrix.OS }} runs-on: ${{ matrix.OS }}
env: env:
NAME: easytier NAME: easytier
@@ -81,11 +92,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - name: Set current ref as env variable
with: run: |
node-version: 21 echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Cargo cache - name: Cargo cache
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
@@ -93,9 +105,6 @@ jobs:
./target ./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc - name: Setup protoc
uses: arduino/setup-protoc@v2 uses: arduino/setup-protoc@v2
with: with:
@@ -103,13 +112,55 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Core & Cli - name: Build Core & Cli
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
run: | run: |
bash ./.github/workflows/install_rust.sh
# 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
export KCP_SYS_EXTRA_HEADER_PATH=/usr/include/musl-cross
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then 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 build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
else else
cargo build --release --verbose --target $TARGET cargo build --release --verbose --target $TARGET
fi 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
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
run: |
uname -a
echo $SHELL
pwd
ls -lah
whoami
env | sort
sudo pkg install -y git protobuf llvm-devel
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
rustup set auto-self-update disable
rustup install 1.84
rustup default 1.84
export CC=clang
export CXX=clang++
export CARGO_TERM_COLOR=always
cargo build --release --verbose --target $TARGET
- name: Install UPX - name: Install UPX
if: ${{ matrix.OS != 'macos-latest' }} if: ${{ matrix.OS != 'macos-latest' }}
uses: crazy-max/ghaction-upx@v3 uses: crazy-max/ghaction-upx@v3
@@ -121,10 +172,14 @@ jobs:
run: | run: |
mkdir -p ./artifacts/objects/ mkdir -p ./artifacts/objects/
# windows is the only OS using a different convention for executable file name # windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ ]]; then if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
SUFFIX=.exe SUFFIX=.exe
cp easytier/third_party/Packet.dll ./artifacts/objects/ cp easytier/third_party/Packet.dll ./artifacts/objects/
cp easytier/third_party/wintun.dll ./artifacts/objects/ cp easytier/third_party/wintun.dll ./artifacts/objects/
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/arm64/Packet.dll ./artifacts/objects/
cp easytier/third_party/arm64/wintun.dll ./artifacts/objects/
fi fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME TAG=$GITHUB_REF_NAME
@@ -132,13 +187,16 @@ jobs:
TAG=$GITHUB_SHA TAG=$GITHUB_SHA
fi fi
if [[ $OS =~ ^ubuntu.*$ ]]; then if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX" upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX" upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
fi fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/ mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$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/
fi
mv ./artifacts/objects/* ./artifacts/ mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/ rm -rf ./artifacts/objects/
@@ -159,7 +217,7 @@ jobs:
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }} endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }} bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/ local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/ remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true no-delete-remote-files: true
retry: 5 retry: 5
core-result: core-result:
+10 -2
View File
@@ -11,7 +11,7 @@ on:
image_tag: image_tag:
description: 'Tag for this image build' description: 'Tag for this image build'
type: string type: string
default: 'v1.2.0' default: 'v2.2.2'
required: true required: true
mark_latest: mark_latest:
description: 'Mark this image as latest' description: 'Mark this image as latest'
@@ -39,6 +39,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} 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 - name: Download artifact
id: download-artifact id: download-artifact
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v6
@@ -58,4 +64,6 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
file: .github/workflows/Dockerfile file: .github/workflows/Dockerfile
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }}, tags: |
easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
ghcr.io/${{ github.actor }}/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
+46 -13
View File
@@ -2,7 +2,7 @@ name: EasyTier GUI
on: on:
push: push:
branches: ["develop", "main"] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
@@ -20,14 +20,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }} should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
steps: steps:
- id: skip_check - id: skip_check
uses: fkirc/skip-duplicate-actions@v5 uses: fkirc/skip-duplicate-actions@v5
with: with:
# All of these options are optional, so you can remove them if you are happy with the defaults # All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' 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"]'
build-gui: build-gui:
strategy: strategy:
@@ -35,11 +36,11 @@ jobs:
matrix: matrix:
include: include:
- TARGET: aarch64-unknown-linux-musl - TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
GUI_TARGET: aarch64-unknown-linux-gnu GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64 ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl - TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest OS: ubuntu-22.04
GUI_TARGET: x86_64-unknown-linux-gnu GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64 ARTIFACT_NAME: linux-x86_64
@@ -57,6 +58,11 @@ jobs:
GUI_TARGET: x86_64-pc-windows-msvc GUI_TARGET: x86_64-pc-windows-msvc
ARTIFACT_NAME: windows-x86_64 ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
runs-on: ${{ matrix.OS }} runs-on: ${{ matrix.OS }}
env: env:
NAME: easytier NAME: easytier
@@ -69,6 +75,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 21 node-version: 21
@@ -94,8 +104,8 @@ jobs:
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
(cd easytier-gui; pnpm install) pnpm -r install
(cd tauri-plugin-vpnservice; pnpm install; pnpm build) pnpm -r build
- name: Cargo cache - name: Cargo cache
uses: actions/cache@v4 uses: actions/cache@v4
@@ -114,6 +124,20 @@ jobs:
# GitHub repo token to use to avoid rate limiter # GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: |
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
- name: Install GUI cross compile (aarch64 only) - name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }} if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: | run: |
@@ -141,20 +165,29 @@ jobs:
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | 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 dpkg --add-architecture arm64
sudo apt-get update && sudo apt-get upgrade -y sudo apt-get update
sudo apt install gcc-aarch64-linux-gnu sudo apt-get install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64
sudo apt install libwebkit2gtk-4.1-dev:arm64 sudo apt-get install -y libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64
sudo apt install libssl-dev:arm64 sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV" echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV" echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- name: copy correct DLLs
if: ${{ matrix.OS == 'windows-latest' }}
run: |
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
else
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
fi
- name: Build GUI - name: Build GUI
if: ${{ matrix.GUI_TARGET != '' }} if: ${{ matrix.GUI_TARGET != '' }}
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
with: with:
projectPath: ./easytier-gui projectPath: ./easytier-gui
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices # 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 }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
- name: Compress - name: Compress
run: | run: |
@@ -197,7 +230,7 @@ jobs:
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }} endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }} bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/ local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/gui remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true no-delete-remote-files: true
retry: 5 retry: 5
gui-result: gui-result:
+11 -16
View File
@@ -8,20 +8,7 @@
# dependencies are only needed on ubuntu as that's the only place where # dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation # we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev llvm clang
# 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
fi
# curl -s musl.cc | grep mipsel # curl -s musl.cc | grep mipsel
case $TARGET in case $TARGET in
mipsel-unknown-linux-musl) mipsel-unknown-linux-musl)
@@ -52,13 +39,14 @@ if [[ $OS =~ ^ubuntu.*$ ]]; then
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -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/ tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/ sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/${MUSL_URI}/include/ /usr/include/musl-cross
fi fi
fi fi
# see https://github.com/rust-lang/rustup/issues/3709 # see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable rustup set auto-self-update disable
rustup install 1.77 rustup install 1.84
rustup default 1.77 rustup default 1.84
# mips/mipsel cannot add target from rustup, need compile by ourselves # mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
@@ -72,6 +60,13 @@ if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
rustup toolchain install nightly-x86_64-unknown-linux-gnu rustup toolchain install nightly-x86_64-unknown-linux-gnu
rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu
# https://github.com/rust-lang/rust/issues/128808
# remove it after Cargo or rustc fix this.
RUST_LIB_SRC=$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/
if [[ -f $RUST_LIB_SRC/library/Cargo.lock && ! -f $RUST_LIB_SRC/Cargo.lock ]]; then
cp -f $RUST_LIB_SRC/library/Cargo.lock $RUST_LIB_SRC/Cargo.lock
fi
else else
rustup target add $TARGET rustup target add $TARGET
if [[ $GUI_TARGET != '' ]]; then if [[ $GUI_TARGET != '' ]]; then
+12 -7
View File
@@ -2,7 +2,7 @@ name: EasyTier Mobile
on: on:
push: push:
branches: ["develop", "main"] branches: ["develop", "main", "releases/**"]
pull_request: pull_request:
branches: ["develop", "main"] branches: ["develop", "main"]
@@ -20,14 +20,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Map a step output to a job output # Map a step output to a job output
outputs: outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }} should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
steps: steps:
- id: skip_check - id: skip_check
uses: fkirc/skip-duplicate-actions@v5 uses: fkirc/skip-duplicate-actions@v5
with: with:
# All of these options are optional, so you can remove them if you are happy with the defaults # All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never' concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", "tauri-plugin-vpnservice/**", ".github/workflows/mobile.yml", ".github/workflows/install_rust.sh"]'
build-mobile: build-mobile:
strategy: strategy:
@@ -35,7 +36,7 @@ jobs:
matrix: matrix:
include: include:
- TARGET: android - TARGET: android
OS: ubuntu-latest OS: ubuntu-22.04
ARTIFACT_NAME: android ARTIFACT_NAME: android
runs-on: ${{ matrix.OS }} runs-on: ${{ matrix.OS }}
env: env:
@@ -48,6 +49,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
with: with:
distribution: 'oracle' distribution: 'oracle'
@@ -90,8 +95,8 @@ jobs:
- name: Install frontend dependencies - name: Install frontend dependencies
run: | run: |
(cd easytier-gui; pnpm install) pnpm -r install
(cd tauri-plugin-vpnservice; pnpm install; pnpm build) pnpm -r build
- name: Cargo cache - name: Cargo cache
uses: actions/cache@v4 uses: actions/cache@v4
@@ -150,7 +155,7 @@ jobs:
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }} endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }} bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/ local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/mobile remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true no-delete-remote-files: true
retry: 5 retry: 5
mobile-result: mobile-result:
+92
View File
@@ -0,0 +1,92 @@
name: EasyTier Release
on:
workflow_dispatch:
inputs:
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.2.2'
required: true
make_latest:
description: 'Mark this release as latest'
type: boolean
default: true
required: true
permissions:
contents: write
jobs:
release:
if: contains('["KKRainbow"]', github.actor)
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
- name: Download Core Artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.core_run_id }}
repo: EasyTier/EasyTier
path: release_assets
- name: Download GUI Artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.gui_run_id }}
repo: EasyTier/EasyTier
path: release_assets_nozip
- name: Download GUI Artifact
uses: dawidd6/action-download-artifact@v6
with:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.mobile_run_id }}
repo: EasyTier/EasyTier
path: release_assets_nozip
- name: Zip release assets
env:
VERSION: ${{ inputs.version }}
run: |
mkdir zipped_assets
find release_assets_nozip -type f -exec mv {} zipped_assets \;
ls -l -R ./zipped_assets
cd release_assets
ls -l -R ./
chmod -R 755 .
for x in `ls`; do
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
done
- name: Release
uses: softprops/action-gh-release@v2
with:
name: ${{ inputs.version }}
draft: true
files: |
./zipped_assets/*
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ inputs.version }}
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: pre_job needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
steps: steps:
+8
View File
@@ -11,6 +11,7 @@ target-*/
*.pdb *.pdb
.vscode .vscode
/.idea
# perf & flamegraph # perf & flamegraph
perf.data perf.data
@@ -29,3 +30,10 @@ musl_gcc
# log # log
easytier-panic.log easytier-panic.log
# web
node_modules
.vite
easytier-gui/src-tauri/*.dll
Generated
+2890 -708
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,7 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["easytier", "easytier-gui/src-tauri"] members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"]
default-members = ["easytier"] default-members = ["easytier", "easytier-web"]
[profile.dev] [profile.dev]
panic = "unwind" panic = "unwind"
@@ -10,4 +10,3 @@ panic = "unwind"
panic = "abort" panic = "abort"
lto = true lto = true
codegen-units = 1 codegen-units = 1
strip = true
+14 -71
View File
@@ -4,84 +4,27 @@
"path": "." "path": "."
}, },
{ {
"name": "gui",
"path": "easytier-gui" "path": "easytier-gui"
}, },
{ {
"name": "core",
"path": "easytier" "path": "easytier"
},
{
"name": "vpnservice",
"path": "tauri-plugin-vpnservice"
},
{
"name": "rpc-build",
"path": "easytier-rpc-build"
} }
], ],
"settings": { "settings": {
"eslint.experimental.useFlatConfig": true, "i18n-ally.sourceLanguage": "cn",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false, "prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"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"
]
} }
} }
+215 -172
View File
@@ -1,28 +1,28 @@
# EasyTier # EasyTier
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) [![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 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 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 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 GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[简体中文](/README_CN.md) | [English](/README.md) [简体中文](/README_CN.md) | [English](/README.md)
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.** **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. EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
<p align="center"> <p align="center">
<img src="assets/image-5.png" width="300"> <img src="assets/image-5.png" width="300">
<img src="assets/image-4.png" width="300"> <img src="assets/image-4.png" width="300">
</p> </p>
## Features ## Features
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent. - **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
- **Safe**: Use WireGuard protocol to encrypt data. - **Safe**: Use WireGuard protocol to encrypt data.
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software. - **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
- **Cross-platform**: Supports MacOS/Linux/Windows, will support IOS and Android in the future. The executable file is statically linked, making deployment simple. - **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) - **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. - **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. - **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.
@@ -31,160 +31,197 @@
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected. - **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. - **IPv6 Support**: Supports networking using IPv6.
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC. - **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
## Installation
1. **Download the precompiled binary file**
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.
2. **Install via crates.io**
## Installation
1. **Download the precompiled binary file**
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.
2. **Install via crates.io**
```sh ```sh
cargo install easytier cargo install easytier
``` ```
3. **Install from source code**
```sh
cargo install --git https://github.com/EasyTier/EasyTier.git
```
## 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. 3. **Install from source code**
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available. ```sh
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
### Two-node Networking ```
Assuming the network topology of the two nodes is as follows 4. **Install by Docker Compose**
```mermaid Please visit the [EasyTier Official Website](https://www.easytier.cn/en/) to view the full documentation.
flowchart LR
5. **Install by script (For Linux Only)**
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1] ```sh
end wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
subgraph Node B
nodeb[EasyTier\n10.144.144.2] You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
end
6. **Install by Homebrew (For MacOS Only)**
nodea <-----> nodeb
```sh
``` brew tap brewforge/chinese
brew install --cask easytier
1. Execute on Node A: ```
## 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.
Make sure EasyTier is installed according to the [Installation Guide](#Installation), and both easytier-core and easytier-cli commands are available.
### Two-node Networking
Assuming the network topology of the two nodes is as follows
```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]
end
nodea <-----> nodeb
```
1. Execute on Node A:
```sh ```sh
sudo easytier-core --ipv4 10.144.144.1 sudo easytier-core --ipv4 10.144.144.1
``` ```
Successful execution of the command will print the following. Successful execution of the command will print the following.
![alt text](/assets/image-2.png) ![alt text](/assets/image-2.png)
2. Execute on Node B 2. Execute on Node B
```sh ```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010 sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
``` ```
3. Test Connectivity 3. Test Connectivity
The two nodes should connect successfully and be able to communicate within the virtual subnet The two nodes should connect successfully and be able to communicate within the virtual subnet
```sh ```sh
ping 10.144.144.2 ping 10.144.144.2
``` ```
Use easytier-cli to view node information in the subnet Use easytier-cli to view node information in the subnet
```sh ```sh
easytier-cli peer easytier-cli peer
``` ```
![alt text](/assets/image.png) ![alt text](/assets/image.png)
```sh ```sh
easytier-cli route easytier-cli route
``` ```
![alt text](/assets/image-1.png) ![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
--- ---
### Multi-node Networking ### 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. 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 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. The `--peers` parameter can fill in the listening address of any node already in the virtual network.
--- ---
### Subnet Proxy (Point-to-Network) Configuration ### Subnet Proxy (Point-to-Network) Configuration
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes. Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
```mermaid ```mermaid
flowchart LR flowchart LR
subgraph Node A IP 22.1.1.1 subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1] nodea[EasyTier\n10.144.144.1]
end end
subgraph Node B subgraph Node B
nodeb[EasyTier\n10.144.144.2] nodeb[EasyTier\n10.144.144.2]
end end
id1[[10.1.1.0/24]] id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1 nodea <--> nodeb <-.-> id1
``` ```
Then the startup parameters for Node B's easytier are (new -n parameter) Then the startup parameters for Node B's easytier are (new -n parameter)
```sh ```sh
sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24 sudo easytier-core --ipv4 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. Node A can check whether the subnet proxy is effective through the following command.
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets. 1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
```sh ```sh
easytier-cli route easytier-cli route
``` ```
![alt text](/assets/image-3.png)
![alt text](/assets/image-3.png)
2. Test whether Node A can access nodes under the proxied subnet 2. Test whether Node A can access nodes under the proxied subnet
```sh ```sh
ping 10.1.1.2 ping 10.1.1.2
``` ```
---
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://easytier.public.kkrainbow.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://easytier.public.kkrainbow.top:11010
```
Node B executes
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010
```
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
### 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. ---
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.cn:11010``.
When using shared nodes, each node entering the network needs to provide the same ``--network-name`` and ``--network-secret`` parameters as the unique identifier of the network.
Taking two nodes as an example, Node A executes:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
Node B executes
```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
```
After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
### 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: Assuming the network topology is as follows:
@@ -210,14 +247,14 @@ To enable an iPhone to access the EasyTier network through Node A, the following
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. 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 # 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 sudo easytier-core --ipv4 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. After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
``` ```sh
$> easytier-cli vpn-portal $> easytier-cli vpn-portal
portal_name: wireguard portal_name: wireguard
@@ -241,45 +278,51 @@ connected_clients:
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. Before using the Client Config, you need to modify the Interface Address and Peer Endpoint to the client's IP and the IP of the EasyTier node, respectively. Import the configuration file into the WireGuard client to access the EasyTier network.
# Self-Hosted Public Server ### Self-Hosted Public Server
Each node can act as a relay node for other users' networks. Simply start EasyTier without any parameters. 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.
### 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 Android, IOS and other mobile platforms.
- [ ] 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
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
# License
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
# Sponsor Run you own public server cluster is exactly same as running an virtual network, except that you can skip config the ipv4 addr.
You can also join the official public server cluster with following command:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```
### Configurations
You can use ``easytier-core --help`` to view all configuration items
## Roadmap
- [ ] Support features such TCP hole punching, KCP, FEC etc.
- [ ] Support iOS.
## Community and Contribution
We welcome and encourage community contributions! If you want to get involved, please submit a [GitHub PR](https://github.com/EasyTier/EasyTier/pulls). Detailed contribution guidelines can be found in [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md).
## Related Projects and Resources
- [ZeroTier](https://www.zerotier.com/): A global virtual network for connecting devices.
- [TailScale](https://tailscale.com/): A VPN solution aimed at simplifying network configuration.
- [vpncloud](https://github.com/dswd/vpncloud): A P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): A reliable, low-latency, and anti-censorship virtual private network
## License
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
## Sponsor
<img src="assets/image-8.png" width="300"> <img src="assets/image-8.png" width="300">
<img src="assets/image-9.png" width="300"> <img src="assets/image-9.png" width="300">
+96 -54
View File
@@ -8,7 +8,7 @@
[简体中文](/README_CN.md) | [English](/README.md) [简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。** **请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
@@ -22,7 +22,7 @@
- **去中心化**:无需依赖中心化服务,节点平等且独立。 - **去中心化**:无需依赖中心化服务,节点平等且独立。
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。 - **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
- **高性能**:全链路零拷贝,性能与主流组网软件相当。 - **高性能**:全链路零拷贝,性能与主流组网软件相当。
- **跨平台**:支持 MacOS/Linux/Windows,未来将支持 IOS 和 Android。可执行文件静态链接,部署简单。 - **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接,部署简单。
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网) - **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。 - **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。 - **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
@@ -31,6 +31,7 @@
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。 - **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。 - **IPV6 支持**:支持利用 IPV6 组网。
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。 - **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
## 安装 ## 安装
@@ -39,15 +40,35 @@
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。 访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
2. **通过 crates.io 安装** 2. **通过 crates.io 安装**
```sh
cargo install easytier
```
```sh
cargo install easytier
```
3. **通过源码安装** 3. **通过源码安装**
```sh
cargo install --git https://github.com/EasyTier/EasyTier.git ```sh
``` cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
4. **通过Docker Compose安装**
请访问 [EasyTier 官网](https://www.easytier.cn/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
## 快速开始 ## 快速开始
@@ -75,34 +96,48 @@ nodea <-----> nodeb
``` ```
1. 在节点 A 上执行: 1. 在节点 A 上执行:
```sh
sudo easytier-core --ipv4 10.144.144.1
```
命令执行成功会有如下打印。
![alt text](/assets/image-2.png) ```sh
sudo easytier-core --ipv4 10.144.144.1
```
命令执行成功会有如下打印。
![alt text](/assets/image-2.png)
2. 在节点 B 执行 2. 在节点 B 执行
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010 ```sh
``` sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
3. 测试联通性 3. 测试联通性
两个节点应成功连接并能够在虚拟子网内通信 两个节点应成功连接并能够在虚拟子网内通信
```sh
ping 10.144.144.2
```
使用 easytier-cli 查看子网中的节点信息 ```sh
```sh ping 10.144.144.2
easytier-cli peer ```
```
![alt text](/assets/image.png) 使用 easytier-cli 查看子网中的节点信息
```sh
easytier-cli route ```sh
``` easytier-cli peer
![alt text](/assets/image-1.png) ```
![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)
--- ---
@@ -110,11 +145,11 @@ nodea <-----> nodeb
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。 基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
``` ```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010 sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
``` ```
其中 `--peers ` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。 其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
--- ---
@@ -149,35 +184,36 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。 1. 检查路由信息是否已经同步,proxy_cidrs 列展示了被代理的子网。
```sh ```sh
easytier-cli route easytier-cli route
``` ```
![alt text](/assets/image-3.png)
![alt text](/assets/image-3.png)
2. 测试节点 A 是否可访问被代理子网下的节点 2. 测试节点 A 是否可访问被代理子网下的节点
```sh ```sh
ping 10.1.1.2 ping 10.1.1.2
``` ```
--- ---
### 无公网IP组网 ### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://easytier.public.kkrainbow.top:11010``。 EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。 使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行: 以双节点为例,节点 A 执行:
```sh ```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010 sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
``` ```
节点 B 执行 节点 B 执行
```sh ```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://easytier.public.kkrainbow.top:11010 sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
``` ```
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。 命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
@@ -212,14 +248,14 @@ ios <-.-> nodea <--> nodeb <-.-> id1
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。 在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
``` ```sh
# 以下参数的含义为: 监听 0.0.0.0:11013 端口,WireGuard 使用 10.14.14.0/24 网段 # 以下参数的含义为: 监听 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 sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
``` ```
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。 easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
``` ```sh
$> easytier-cli vpn-portal $> easytier-cli vpn-portal
portal_name: wireguard portal_name: wireguard
@@ -247,43 +283,49 @@ connected_clients:
### 自建公共中转服务器 ### 自建公共中转服务器
每个节点都可作为其他用户网络的中转节点。不带任何参数直接启动 EasyTier 即可 每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此
运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```
### 其他配置 ### 其他配置
可使用 ``easytier-core --help`` 查看全部配置项 可使用 ``easytier-core --help`` 查看全部配置项
## 路线图
# 路线图
- [ ] 完善文档和用户指南。 - [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞等特性。 - [ ] 支持 TCP 打洞、KCP、FEC 等特性。
- [ ] 支持 Android、IOS 等移动平台 - [ ] 支持 iOS
- [ ] 支持 Web 配置管理。
# 社区和贡献 ## 社区和贡献
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。 我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
# 相关项目和资源 ## 相关项目和资源
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。 - [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。 - [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN - [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络 - [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
# 许可证 ## 许可证
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。 EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
# 联系方式 ## 联系方式
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues) - 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions) - 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
- QQ 群: 949700262 - QQ 群: 949700262
- Telegramhttps://t.me/easytier - Telegramhttps://t.me/easytier
# 赞助 ## 赞助
<img src="assets/image-8.png" width="300"> <img src="assets/image-8.png" width="300">
<img src="assets/image-9.png" width="300"> <img src="assets/image-9.png" width="300">
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

-2
View File
@@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false
+79 -3
View File
@@ -1,5 +1,81 @@
{ {
"i18n-ally.localesPaths": [ "cSpell.words": [
"locales" "easytier",
"Vite",
"vueuse",
"pinia",
"demi",
"antfu",
"iconify",
"intlify",
"vitejs",
"unplugin",
"pnpm"
],
"i18n-ally.localesPaths": "locales",
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{
"rule": "style/*",
"severity": "off"
},
{
"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"
}
],
// The following is optional.
// It's better to put under project setting `.vscode/settings.json`
// to avoid conflicts with working with different eslint configs
// that does not support all formats.
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
] ]
} }
+6 -2
View File
@@ -14,6 +14,11 @@ npm install -g pnpm
### For Desktop (Win/Mac/Linux) ### For Desktop (Win/Mac/Linux)
``` ```
cd ../tauri-plugin-vpnservice
pnpm install
pnpm build
cd ../easytier-gui
pnpm install pnpm install
pnpm tauri build pnpm tauri build
``` ```
@@ -34,7 +39,6 @@ rustup target add aarch64-linux-android
install java 20 install java 20
``` ```
Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts) Java version depend on gradle version specified in (easytier-gui\src-tauri\gen\android\build.gradle.kts)
See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail . See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html) for detail .
@@ -43,4 +47,4 @@ See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/comp
pnpm install pnpm install
pnpm tauri android init pnpm tauri android init
pnpm tauri android build pnpm tauri android build
``` ```
+38
View File
@@ -13,6 +13,7 @@ proxy_cidrs: 子网代理CIDR
enable_vpn_portal: 启用VPN门户 enable_vpn_portal: 启用VPN门户
vpn_portal_listen_port: 监听端口 vpn_portal_listen_port: 监听端口
vpn_portal_client_network: 客户端子网 vpn_portal_client_network: 客户端子网
dev_name: TUN接口名称
advanced_settings: 高级设置 advanced_settings: 高级设置
basic_settings: 基础设置 basic_settings: 基础设置
listener_urls: 监听地址 listener_urls: 监听地址
@@ -45,11 +46,13 @@ enable_auto_launch: 开启开机自启
exit: 退出 exit: 退出
chips_placeholder: 例如: {0}, 按回车添加 chips_placeholder: 例如: {0}, 按回车添加
hostname_placeholder: '留空默认为主机名: {0}' hostname_placeholder: '留空默认为主机名: {0}'
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
off_text: 点击关闭 off_text: 点击关闭
on_text: 点击开启 on_text: 点击开启
show_config: 显示配置 show_config: 显示配置
close: 关闭 close: 关闭
use_latency_first: 延迟优先模式
my_node_info: 当前节点信息 my_node_info: 当前节点信息
peer_count: 已连接 peer_count: 已连接
upload: 上传 upload: 上传
@@ -66,6 +69,12 @@ upload_bytes: 上传
download_bytes: 下载 download_bytes: 下载
loss_rate: 丢包率 loss_rate: 丢包率
status:
version: 内核版本
local: 本机
server: 服务器
relay: 中继
run_network: 运行网络 run_network: 运行网络
stop_network: 停止网络 stop_network: 停止网络
network_running: 运行中 network_running: 运行中
@@ -75,3 +84,32 @@ dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中
tray: tray:
show: 显示 / 隐藏 show: 显示 / 隐藏
exit: 退出 exit: 退出
about:
title: 关于
version: 版本
author: 作者
homepage: 主页
license: 许可证
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
check_update: 检查更新
event:
Unknown: 未知
TunDeviceReady: Tun设备就绪
TunDeviceError: Tun设备错误
PeerAdded: 对端添加
PeerRemoved: 对端移除
PeerConnAdded: 对端连接添加
PeerConnRemoved: 对端连接移除
ListenerAdded: 监听器添加
ListenerAddFailed: 监听器添加失败
ListenerAcceptFailed: 监听器接受连接失败
ConnectionAccepted: 连接已接受
ConnectionError: 连接错误
Connecting: 正在连接
ConnectError: 连接错误
VpnPortalClientConnected: VPN门户客户端已连接
VpnPortalClientDisconnected: VPN门户客户端已断开连接
DhcpIpv4Changed: DHCP IPv4地址更改
DhcpIpv4Conflicted: DHCP IPv4地址冲突
+38 -1
View File
@@ -13,6 +13,7 @@ proxy_cidrs: Subnet Proxy CIDRs
enable_vpn_portal: Enable VPN Portal enable_vpn_portal: Enable VPN Portal
vpn_portal_listen_port: VPN Portal Listen Port vpn_portal_listen_port: VPN Portal Listen Port
vpn_portal_client_network: Client Sub Network vpn_portal_client_network: Client Sub Network
dev_name: TUN interface name
advanced_settings: Advanced Settings advanced_settings: Advanced Settings
basic_settings: Basic Settings basic_settings: Basic Settings
listener_urls: Listener URLs listener_urls: Listener URLs
@@ -43,9 +44,10 @@ logging_copy_dir: Copy Log Path
disable_auto_launch: Disable Launch on Reboot disable_auto_launch: Disable Launch on Reboot
enable_auto_launch: Enable Launch on Reboot enable_auto_launch: Enable Launch on Reboot
exit: Exit exit: Exit
use_latency_first: Latency First Mode
chips_placeholder: 'e.g: {0}, press Enter to add' chips_placeholder: 'e.g: {0}, press Enter to add'
hostname_placeholder: 'Leave blank and default to host name: {0}' hostname_placeholder: 'Leave blank and default to host name: {0}'
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
off_text: Press to disable off_text: Press to disable
on_text: Press to enable on_text: Press to enable
show_config: Show Config show_config: Show Config
@@ -66,6 +68,12 @@ upload_bytes: Upload
download_bytes: Download download_bytes: Download
loss_rate: Loss Rate loss_rate: Loss Rate
status:
version: Version
local: Local
server: Server
relay: Relay
run_network: Run Network run_network: Run Network
stop_network: Stop Network stop_network: Stop Network
network_running: running network_running: running
@@ -75,3 +83,32 @@ dhcp_experimental_warning: Experimental warning! if there is an IP conflict in t
tray: tray:
show: Show / Hide show: Show / Hide
exit: Exit exit: Exit
about:
title: About
version: Version
author: Author
homepage: Homepage
license: License
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
check_update: Check Update
event:
Unknown: Unknown
TunDeviceReady: TunDeviceReady
TunDeviceError: TunDeviceError
PeerAdded: PeerAdded
PeerRemoved: PeerRemoved
PeerConnAdded: PeerConnAdded
PeerConnRemoved: PeerConnRemoved
ListenerAdded: ListenerAdded
ListenerAddFailed: ListenerAddFailed
ListenerAcceptFailed: ListenerAcceptFailed
ConnectionAccepted: ConnectionAccepted
ConnectionError: ConnectionError
Connecting: Connecting
ConnectError: ConnectError
VpnPortalClientConnected: VpnPortalClientConnected
VpnPortalClientDisconnected: VpnPortalClientDisconnected
DhcpIpv4Changed: DhcpIpv4Changed
DhcpIpv4Conflicted: DhcpIpv4Conflicted
+42 -37
View File
@@ -1,8 +1,9 @@
{ {
"name": "easytier-gui", "name": "easytier-gui",
"type": "module", "type": "module",
"version": "1.2.1", "version": "2.2.2",
"private": true, "private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
@@ -12,47 +13,51 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix" "lint:fix": "eslint . --ignore-pattern src-tauri --fix"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.0.4", "@primevue/themes": "^4.2.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0", "@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0-rc.0", "@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-process": "2.0.0-rc.0", "@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.0-rc.0", "@tauri-apps/plugin-process": "2.0.0",
"aura": "link:@primevue/themes/aura", "@tauri-apps/plugin-shell": "2.0.1",
"pinia": "^2.2.1", "@vueuse/core": "^11.2.0",
"primeflex": "^3.3.1", "aura": "link:@primevue\\themes\\aura",
"primeicons": "^7.0.0", "easytier-frontend-lib": "workspace:*",
"primevue": "^4.0.4", "ip-num": "1.5.1",
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice", "pinia": "^2.2.4",
"vue": "^3.4.36", "primevue": "^4.2.1",
"vue-i18n": "^9.13.1", "tauri-plugin-vpnservice-api": "workspace:*",
"vue-router": "^4.4.3" "vue": "^3.5.12",
"vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^2.24.1", "@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "^4.0.4", "@primevue/auto-import-resolver": "^4.1.0",
"@tauri-apps/api": "2.0.0-rc.0", "@tauri-apps/api": "2.1.0",
"@tauri-apps/cli": "2.0.0-rc.1", "@tauri-apps/cli": "2.1.0",
"@types/node": "^20.14.14", "@types/default-gateway": "^7.2.2",
"@types/uuid": "^9.0.8", "@types/node": "^22.7.4",
"@vitejs/plugin-vue": "^5.1.2", "@types/uuid": "^10.0.0",
"@vue-macros/volar": "^0.19.1", "@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/volar": "0.30.5",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.8.0", "cidr-tools": "^11.0.2",
"default-gateway": "^7.2.2",
"eslint": "^9.12.0",
"eslint-plugin-format": "^0.1.2", "eslint-plugin-format": "^0.1.2",
"postcss": "^8.4.41", "postcss": "^8.4.47",
"tailwindcss": "^3.4.7", "tailwindcss": "^3.4.13",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"unplugin-auto-import": "^0.17.8", "unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.3", "unplugin-vue-components": "^0.27.4",
"unplugin-vue-macros": "^2.11.4", "unplugin-vue-macros": "^2.13.3",
"unplugin-vue-markdown": "^0.26.2", "unplugin-vue-markdown": "^0.26.2",
"unplugin-vue-router": "^0.8.8", "unplugin-vue-router": "^0.10.8",
"uuid": "^9.0.1", "uuid": "^10.0.0",
"vite": "^5.3.5", "vite": "^5.4.8",
"vite-plugin-vue-devtools": "^7.3.7", "vite-plugin-vue-devtools": "^7.4.6",
"vite-plugin-vue-layouts": "^0.11.0", "vite-plugin-vue-layouts": "^0.11.0",
"vue-i18n": "^9.13.1", "vue-i18n": "^10.0.0",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.1.10"
} }
} }
+2537 -2031
View File
File diff suppressed because it is too large Load Diff
@@ -1,4 +0,0 @@
[build]
target = "x86_64-unknown-linux-gnu"
[target]
+13 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "easytier-gui" name = "easytier-gui"
version = "1.2.1" version = "2.2.2"
description = "EasyTier GUI" description = "EasyTier GUI"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@@ -15,10 +15,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2.0.0-rc", features = [] } tauri-build = { version = "2.0.0-rc", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0-rc", features = [ # wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
tauri = { version = "=2.0.6", features = [
"tray-icon", "tray-icon",
"image-png", "image-png",
"image-ico", "image-ico",
"devtools",
] } ] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -35,17 +37,20 @@ dashmap = "6.0"
privilege = "0.3" privilege = "0.3"
gethostname = "0.5" gethostname = "0.5"
auto-launch = "0.5.0"
dunce = "1.0.4" dunce = "1.0.4"
tauri-plugin-shell = "2.0.0-rc" tauri-plugin-shell = "2.0"
tauri-plugin-process = "2.0.0-rc" tauri-plugin-process = "2.0"
tauri-plugin-clipboard-manager = "2.0.0-rc" tauri-plugin-clipboard-manager = "2.0"
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] } tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" } tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
tauri-plugin-os = "2.0.0-rc" tauri-plugin-os = "2.0"
tauri-plugin-autostart = "2.0"
[features] [features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.0.0-rc.0"
Binary file not shown.
+3 -34
View File
@@ -1,34 +1,3 @@
fn main() { fn main() {
if !cfg!(debug_assertions) && cfg!(target_os = "windows") { tauri_build::build();
let mut windows = tauri_build::WindowsAttributes::new(); }
windows = windows.app_manifest(
r#"
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
"#,
);
tauri_build::try_build(tauri_build::Attributes::new().windows_attributes(windows))
.expect("failed to run build script");
} else {
tauri_build::build();
}
}
@@ -1,4 +1,5 @@
{ {
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "migrated", "identifier": "migrated",
"description": "permissions that were migrated from v1", "description": "permissions that were migrated from v1",
"local": true, "local": true,
@@ -13,6 +14,7 @@
"core:window:allow-show", "core:window:allow-show",
"core:window:allow-hide", "core:window:allow-hide",
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-set-title",
"core:app:default", "core:app:default",
"core:resources:default", "core:resources:default",
"core:menu:default", "core:menu:default",
@@ -24,7 +26,6 @@
"shell:default", "shell:default",
"process:default", "process:default",
"clipboard-manager:default", "clipboard-manager:default",
"core:tray:default",
"core:tray:allow-new", "core:tray:allow-new",
"core:tray:allow-set-menu", "core:tray:allow-set-menu",
"core:tray:allow-set-title", "core:tray:allow-set-title",
@@ -38,12 +39,16 @@
"vpnservice:allow-prepare-vpn", "vpnservice:allow-prepare-vpn",
"vpnservice:allow-start-vpn", "vpnservice:allow-start-vpn",
"vpnservice:allow-stop-vpn", "vpnservice:allow-stop-vpn",
"vpnservice:allow-register-listener", "vpnservice:allow-registerListener",
"os:default", "os:default",
"os:allow-os-type", "os:allow-os-type",
"os:allow-arch", "os:allow-arch",
"os:allow-hostname", "os:allow-hostname",
"os:allow-platform", "os:allow-platform",
"os:allow-locale" "os:allow-locale",
"autostart:default",
"autostart:allow-disable",
"autostart:allow-enable",
"autostart:allow-is-enabled"
] ]
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

+79 -237
View File
@@ -3,175 +3,38 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use anyhow::Context;
#[cfg(not(target_os = "android"))]
use auto_launch::AutoLaunchBuilder;
use dashmap::DashMap; use dashmap::DashMap;
use easytier::{ use easytier::{
common::config::{ common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader, launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
VpnPortalConfig,
},
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
utils::{self, NewFilterSender}, utils::{self, NewFilterSender},
}; };
use serde::{Deserialize, Serialize};
use tauri::Manager as _; use tauri::Manager as _;
pub const AUTOSTART_ARG: &str = "--autostart";
#[derive(Deserialize, Serialize, PartialEq, Debug)]
enum NetworkingMethod {
PublicServer,
Manual,
Standalone,
}
impl Default for NetworkingMethod {
fn default() -> Self {
NetworkingMethod::PublicServer
}
}
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
#[derive(Deserialize, Serialize, Debug, Default)]
struct NetworkConfig {
instance_id: String,
dhcp: bool,
virtual_ipv4: String,
hostname: Option<String>,
network_name: String,
network_secret: String,
networking_method: NetworkingMethod,
public_server_url: String,
peer_urls: Vec<String>,
proxy_cidrs: Vec<String>,
enable_vpn_portal: bool,
vpn_portal_listen_port: i32,
vpn_portal_client_network_addr: String,
vpn_portal_client_network_len: i32,
advanced_settings: bool,
listener_urls: Vec<String>,
rpc_port: i32,
}
impl NetworkConfig {
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error> {
let cfg = TomlConfigLoader::default();
cfg.set_id(
self.instance_id
.parse()
.with_context(|| format!("failed to parse instance id: {}", self.instance_id))?,
);
cfg.set_hostname(self.hostname.clone());
cfg.set_dhcp(self.dhcp);
cfg.set_inst_name(self.network_name.clone());
cfg.set_network_identity(NetworkIdentity::new(
self.network_name.clone(),
self.network_secret.clone(),
));
if !self.dhcp {
if self.virtual_ipv4.len() > 0 {
cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| {
format!("failed to parse ipv4 address: {}", self.virtual_ipv4)
})?))
}
}
match self.networking_method {
NetworkingMethod::PublicServer => {
cfg.set_peers(vec![PeerConfig {
uri: self.public_server_url.parse().with_context(|| {
format!(
"failed to parse public server uri: {}",
self.public_server_url
)
})?,
}]);
}
NetworkingMethod::Manual => {
let mut peers = vec![];
for peer_url in self.peer_urls.iter() {
if peer_url.is_empty() {
continue;
}
peers.push(PeerConfig {
uri: peer_url
.parse()
.with_context(|| format!("failed to parse peer uri: {}", peer_url))?,
});
}
cfg.set_peers(peers);
}
NetworkingMethod::Standalone => {}
}
let mut listener_urls = vec![];
for listener_url in self.listener_urls.iter() {
if listener_url.is_empty() {
continue;
}
listener_urls.push(
listener_url
.parse()
.with_context(|| format!("failed to parse listener uri: {}", listener_url))?,
);
}
cfg.set_listeners(listener_urls);
for n in self.proxy_cidrs.iter() {
cfg.add_proxy_cidr(
n.parse()
.with_context(|| format!("failed to parse proxy network: {}", n))?,
);
}
cfg.set_rpc_portal(
format!("127.0.0.1:{}", self.rpc_port)
.parse()
.with_context(|| format!("failed to parse rpc portal port: {}", self.rpc_port))?,
);
if self.enable_vpn_portal {
let cidr = format!(
"{}/{}",
self.vpn_portal_client_network_addr, self.vpn_portal_client_network_len
);
cfg.set_vpn_portal_config(VpnPortalConfig {
client_cidr: cidr
.parse()
.with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?,
wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port)
.parse()
.with_context(|| {
format!(
"failed to parse vpn portal wireguard listen port. {}",
self.vpn_portal_listen_port
)
})?,
});
}
Ok(cfg)
}
}
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> = static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
once_cell::sync::Lazy::new(DashMap::new); once_cell::sync::Lazy::new(DashMap::new);
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> = static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
once_cell::sync::Lazy::new(Default::default); once_cell::sync::Lazy::new(Default::default);
#[tauri::command]
fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string())
}
#[tauri::command]
fn is_autostart() -> Result<bool, String> {
let args: Vec<String> = std::env::args().collect();
println!("{:?}", args);
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
}
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command] #[tauri::command]
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> { fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
@@ -181,10 +44,10 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
#[tauri::command] #[tauri::command]
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> { fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
if INSTANCE_MAP.contains_key(&cfg.instance_id) { if INSTANCE_MAP.contains_key(cfg.instance_id()) {
return Err("instance already exists".to_string()); return Err("instance already exists".to_string());
} }
let instance_id = cfg.instance_id.clone(); let instance_id = cfg.instance_id().to_string();
let cfg = cfg.gen_config().map_err(|e| e.to_string())?; let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
let mut instance = NetworkInstance::new(cfg); let mut instance = NetworkInstance::new(cfg);
@@ -224,13 +87,9 @@ fn get_os_hostname() -> Result<String, String> {
Ok(gethostname::gethostname().to_string_lossy().to_string()) Ok(gethostname::gethostname().to_string_lossy().to_string())
} }
#[tauri::command]
fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result<bool, String> {
Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?)
}
#[tauri::command] #[tauri::command]
fn set_logging_level(level: String) -> Result<(), String> { fn set_logging_level(level: String) -> Result<(), String> {
#[allow(static_mut_refs)]
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() }; let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
sender.send(level).map_err(|e| e.to_string())?; sender.send(level).map_err(|e| e.to_string())?;
Ok(()) Ok(())
@@ -262,82 +121,19 @@ fn check_sudo() -> bool {
use std::env::current_exe; use std::env::current_exe;
let is_elevated = privilege::user::privileged(); let is_elevated = privilege::user::privileged();
if !is_elevated { if !is_elevated {
let Ok(my_exe) = current_exe() else { let Ok(exe) = current_exe() else {
return true; return true;
}; };
let mut elevated_cmd = privilege::runas::Command::new(my_exe); let args: Vec<String> = std::env::args().collect();
let _ = elevated_cmd.force_prompt(true).gui(true).run(); let mut elevated_cmd = privilege::runas::Command::new(exe);
if args.contains(&AUTOSTART_ARG.to_owned()) {
elevated_cmd.arg(AUTOSTART_ARG);
}
let _ = elevated_cmd.force_prompt(true).hide(true).gui(true).run();
} }
is_elevated is_elevated
} }
#[cfg(target_os = "android")]
pub fn init_launch(_app_handle: &tauri::AppHandle, _enable: bool) -> Result<bool, anyhow::Error> {
Ok(false)
}
/// init the auto launch
#[cfg(not(target_os = "android"))]
pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result<bool, anyhow::Error> {
use std::env::current_exe;
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow::anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow::anyhow!("failed to get app_path"))?
.to_string();
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
// use the /Applications/easytier-gui.app
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
}
})()
.unwrap_or(app_path);
#[cfg(target_os = "linux")]
let app_path = {
let appimage = _app_handle.env().appimage;
appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or(app_path)
};
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()
.with_context(|| "failed to build auto launch")?;
if enable && !auto.is_enabled().unwrap_or(false) {
// 避免重复设置登录项
let _ = auto.disable();
auto.enable()
.with_context(|| "failed to enable auto launch")?
} else if !enable {
let _ = auto.disable();
}
let enabled = auto.is_enabled()?;
Ok(enabled)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
@@ -345,12 +141,40 @@ pub fn run() {
use std::process; use std::process;
process::exit(0); process::exit(0);
} }
tauri::Builder::default()
utils::setup_panic_handler();
let mut builder = tauri::Builder::default();
#[cfg(not(target_os = "android"))]
{
use tauri_plugin_autostart::MacosLauncher;
builder = builder.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
Some(vec![AUTOSTART_ARG]),
));
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
app.webview_windows()
.values()
.next()
.expect("Sorry, no window found")
.set_focus()
.expect("Can't Bring Window to Focus");
}));
}
builder = builder
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_vpnservice::init()) .plugin(tauri_plugin_vpnservice::init());
let app = builder
.setup(|app| { .setup(|app| {
// for logging config // for logging config
let Ok(log_dir) = app.path().app_log_dir() else { let Ok(log_dir) = app.path().app_log_dir() else {
@@ -365,7 +189,10 @@ pub fn run() {
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else { let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
return Ok(()); return Ok(());
}; };
unsafe { LOGGER_LEVEL_SENDER.replace(logger_reinit) }; #[allow(static_mut_refs)]
unsafe {
LOGGER_LEVEL_SENDER.replace(logger_reinit)
};
// for tray icon, menu need to be built in js // for tray icon, menu need to be built in js
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
@@ -382,7 +209,7 @@ pub fn run() {
toggle_window_visibility(app); toggle_window_visibility(app);
} }
}) })
.icon(tauri::image::Image::from_bytes(include_bytes!( .icon(tauri::image::Image::from_bytes(include_bytes!(
"../icons/icon.png" "../icons/icon.png"
))?) ))?)
.icon_as_template(false) .icon_as_template(false)
@@ -396,9 +223,10 @@ pub fn run() {
retain_network_instance, retain_network_instance,
collect_network_infos, collect_network_infos,
get_os_hostname, get_os_hostname,
set_auto_launch_status,
set_logging_level, set_logging_level,
set_tun_fd set_tun_fd,
is_autostart,
easytier_version
]) ])
.on_window_event(|_win, event| match event { .on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
@@ -408,6 +236,20 @@ pub fn run() {
} }
_ => {} _ => {}
}) })
.run(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while running tauri application"); .unwrap();
#[cfg(not(target_os = "macos"))]
app.run(|_app, _event| {});
#[cfg(target_os = "macos")]
{
use tauri::RunEvent;
app.run(|app, event| match event {
RunEvent::Reopen { .. } => {
toggle_window_visibility(app);
}
_ => {}
});
}
} }
+1 -1
View File
@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false "createUpdaterArtifacts": false
}, },
"productName": "easytier-gui", "productName": "easytier-gui",
"version": "1.2.1", "version": "2.2.2",
"identifier": "com.kkrainbow.easytier", "identifier": "com.kkrainbow.easytier",
"plugins": {}, "plugins": {},
"app": { "app": {
Binary file not shown.
+9
View File
@@ -1,3 +1,12 @@
<script setup lang="ts">
import { getCurrentWindow } from '@tauri-apps/api/window'
import pkg from '~/../package.json'
onBeforeMount(async () => {
await getCurrentWindow().setTitle(`Easytier GUI: v${pkg.version}`)
})
</script>
<template> <template>
<RouterView /> <RouterView />
</template> </template>
+26 -12
View File
@@ -3,6 +3,7 @@
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
// biome-ignore lint: disable
export {} export {}
declare global { declare global {
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
@@ -20,15 +21,18 @@ declare global {
const definePage: typeof import('unplugin-vue-router/runtime')['definePage'] const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
const defineStore: typeof import('pinia')['defineStore'] const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope'] const effectScope: typeof import('vue')['effectScope']
const event2human: typeof import('./composables/utils')['event2human']
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem'] const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
const getActivePinia: typeof import('pinia')['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname'] const getOsHostname: typeof import('./composables/network')['getOsHostname']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService'] const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService'] const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const isAutostart: typeof import('./composables/network')['isAutostart']
const isProxy: typeof import('vue')['isProxy'] const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
@@ -41,10 +45,12 @@ declare global {
const mapWritableState: typeof import('pinia')['mapWritableState'] const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw'] const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick'] const nextTick: typeof import('vue')['nextTick']
const num2ipv4: typeof import('./composables/utils')['num2ipv4']
const num2ipv6: typeof import('./composables/utils')['num2ipv6']
const onActivated: typeof import('vue')['onActivated'] const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave'] const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate'] const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated'] const onDeactivated: typeof import('vue')['onDeactivated']
@@ -56,6 +62,7 @@ declare global {
const onServerPrefetch: typeof import('vue')['onServerPrefetch'] const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted'] const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated'] const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig'] const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService'] const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
@@ -77,6 +84,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef'] const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs'] const storeToRefs: typeof import('pinia')['storeToRefs']
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
const toRaw: typeof import('vue')['toRaw'] const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef'] const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs'] const toRefs: typeof import('vue')['toRefs']
@@ -87,11 +95,14 @@ declare global {
const useCssModule: typeof import('vue')['useCssModule'] const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars'] const useCssVars: typeof import('vue')['useCssVars']
const useI18n: typeof import('vue-i18n')['useI18n'] const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router/auto')['useLink'] const useLink: typeof import('vue-router/auto')['useLink']
const useModel: typeof import('vue')['useModel']
const useNetworkStore: typeof import('./stores/network')['useNetworkStore'] const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
const useRoute: typeof import('vue-router/auto')['useRoute'] const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter'] const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots'] const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTray: typeof import('./composables/tray')['useTray'] const useTray: typeof import('./composables/tray')['useTray']
const watch: typeof import('vue')['watch'] const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect'] const watchEffect: typeof import('vue')['watchEffect']
@@ -101,7 +112,7 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }
// for vue template auto import // for vue template auto import
@@ -120,22 +131,22 @@ declare module 'vue' {
readonly customRef: UnwrapRef<typeof import('vue')['customRef']> readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']> readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']> readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly definePage: UnwrapRef<typeof import('unplugin-vue-router/runtime')['definePage']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']> readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']> readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']> readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']> readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']> readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']> readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']> readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']> readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']> readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']> readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']> readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']> readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
@@ -145,8 +156,8 @@ declare module 'vue' {
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']> readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']> readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']> readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']> readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']> readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
@@ -158,6 +169,7 @@ declare module 'vue' {
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']> readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']> readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']> readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
@@ -168,7 +180,6 @@ declare module 'vue' {
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']> readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']> readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']> readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']> readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']> readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']> readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
@@ -189,11 +200,14 @@ declare module 'vue' {
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']> readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']> readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']> readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']> readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']> readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']> readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']> readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']> readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']> readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
readonly watch: UnwrapRef<typeof import('vue')['watch']> readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']> readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
+27
View File
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { getEasytierVersion } from '~/composables/network'
const { t } = useI18n()
const etVersion = ref('')
onMounted(async () => {
etVersion.value = await getEasytierVersion()
})
</script>
<template>
<Card>
<template #title>
Easytier - {{ t('about.version') }}: {{ etVersion }}
</template>
<template #content>
<p class="mb-1">
{{ t('about.description') }}
</p>
</template>
</Card>
</template>
<style scoped lang="postcss">
</style>
-262
View File
@@ -1,262 +0,0 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { getOsHostname } from '~/composables/network'
import { NetworkingMethod } from '~/types/network'
const { t } = useI18n()
import { ping } from 'tauri-plugin-vpnservice-api'
const props = defineProps<{
configInvalid?: boolean
instanceId?: string
}>()
defineEmits(['runNetwork'])
const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
{ value: NetworkingMethod.Manual, label: () => t('manual') },
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
])
const networkStore = useNetworkStore()
const curNetwork = computed(() => {
if (props.instanceId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const protos:{ [proto: string] : number; } = {'tcp': 11010, 'udp': 11010, 'wg':11011, 'ws': 11011, 'wss': 11012}
function searchUrlSuggestions(e: { query: string }): string[] {
const query = e.query
let ret = []
// if query match "^\w+:.*", then no proto prefix
if (query.match(/^\w+:.*/)) {
// if query is a valid url, then add to suggestions
try {
new URL(query)
ret.push(query)
} catch (e) {}
} else {
for (let proto in protos) {
let item = proto + '://' + query
// if query match ":\d+$", then no port suffix
if (!query.match(/:\d+$/)) {
item += ':' + protos[proto]
}
ret.push(item)
}
}
return ret
}
const publicServerSuggestions = ref([''])
const searchPresetPublicServers = (e: { query: string }) => {
const presetPublicServers = [
'tcp://easytier.public.kkrainbow.top:11010',
]
let query = e.query
// if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter((item) => item.includes(query))
// add additional suggestions
if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e))
}
publicServerSuggestions.value = ret
}
const peerSuggestions = ref([''])
const searchPeerSuggestions = (e: { query: string }) => {
peerSuggestions.value = searchUrlSuggestions(e)
}
const listenerSuggestions = ref([''])
const searchListenerSuggestiong = (e: { query: string }) => {
let ret = []
for (let proto in protos) {
let item = proto + '://0.0.0.0:';
// if query is a number, use it as port
if (e.query.match(/^\d+$/)) {
item += e.query
} else {
item += protos[proto]
}
if (item.includes(e.query)) {
ret.push(item)
}
}
if (ret.length === 0) {
ret.push(e.query)
}
listenerSuggestions.value = ret
}
function validateHostname() {
if (curNetwork.value.hostname) {
// eslint no-useless-escape
let name = curNetwork.value.hostname!.replaceAll(/[^\u4E00-\u9FA5a-zA-Z0-9\-]*/g, '')
if (name.length > 32)
name = name.substring(0, 32)
if (curNetwork.value.hostname !== name)
curNetwork.value.hostname = name
}
}
const osHostname = ref<string>('')
onMounted(async () => {
osHostname.value = await getOsHostname()
osHostname.value = await ping('ffdklsajflkdsjl') || ''
})
</script>
<template>
<div class="flex flex-column h-full">
<div class="flex flex-column">
<div class="w-10/12 self-center ">
<Message severity="warn">
{{ t('dhcp_experimental_warning') }}
</Message>
</div>
<div class="w-10/12 self-center ">
<Panel :header="t('basic_settings')">
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<div class="flex align-items-center" for="virtual_ip">
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
<label for="virtual_ip_auto" class="ml-2">
{{ t('virtual_ipv4_dhcp') }}
</label>
</div>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help" />
<InputGroupAddon>
<span>/24</span>
</InputGroupAddon>
</InputGroup>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_name">{{ t('network_name') }}</label>
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
</div>
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret"
aria-describedby=" network_secret-help" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="nm">{{ t('networking_method') }}</label>
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods" :option-label="(v) => v.label()" option-value="value"></SelectButton>
<div class="items-center flex flex-row p-fluid gap-x-1">
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions"/>
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer" :suggestions="publicServerSuggestions"
:virtualScrollerOptions="{ itemSize: 38 }" class="grow" dropdown @complete="searchPresetPublicServers" :completeOnFocus="true"
v-model="curNetwork.public_server_url"/>
</div>
</div>
</div>
</div>
</Panel>
<Divider />
<Panel :header="t('advanced_settings')" toggleable collapsed>
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label>
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [osHostname])" @blur="validateHostname" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="username">{{ t('proxy_cidrs') }}</label>
<Chips id="chips" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" separator=" " class="w-full" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-column gap-2 grow">
<label for="username">VPN Portal</label>
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48"/>
<div class="items-center flex flex-row gap-x-4" v-if="curNetwork.enable_vpn_portal">
<div class="min-w-64">
<InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false"
:format="false" :min="0" :max="65535" class="w-8" fluid/>
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label>
<AutoComplete id="listener_urls" :suggestions="listenerSuggestions"
class="w-full" dropdown @complete="searchListenerSuggestiong" :completeOnFocus="true"
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])"
v-model="curNetwork.listener_urls" multiple/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="rpc_port">{{ t('rpc_port') }}</label>
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="username-help"
:format="false" :min="0" :max="65535" />
</div>
</div>
</div>
</Panel>
<div class="flex pt-4 justify-content-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)" />
</div>
</div>
</div>
</div>
</template>
+142 -131
View File
@@ -1,183 +1,194 @@
import { addPluginListener } from '@tauri-apps/api/core'; import type { NetworkTypes } from 'easytier-frontend-lib'
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'; import { addPluginListener } from '@tauri-apps/api/core'
import { Route } from '~/types/network'; import { Utils } from 'easytier-frontend-lib'
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
type Route = NetworkTypes.Route
const networkStore = useNetworkStore() const networkStore = useNetworkStore()
interface vpnStatus { interface vpnStatus {
running: boolean running: boolean
ipv4Addr: string | null | undefined ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined ipv4Cidr: number | null | undefined
routes: string[] routes: string[]
} }
var curVpnStatus: vpnStatus = { const curVpnStatus: vpnStatus = {
running: false, running: false,
ipv4Addr: undefined, ipv4Addr: undefined,
ipv4Cidr: undefined, ipv4Cidr: undefined,
routes: [] routes: [],
} }
async function waitVpnStatus(target_status: boolean, timeout_sec: number) { async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
let start_time = Date.now() const start_time = Date.now()
while (curVpnStatus.running !== target_status) { while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) { if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout') throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
} }
await new Promise(r => setTimeout(r, 50))
}
} }
async function doStopVpn() { async function doStopVpn() {
if (!curVpnStatus.running) { if (!curVpnStatus.running) {
return return
} }
console.log('stop vpn') console.log('stop vpn')
let stop_ret = await stop_vpn() const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret))) console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3) await waitVpnStatus(false, 3)
curVpnStatus.ipv4Addr = undefined curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = [] curVpnStatus.routes = []
} }
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) { async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
if (curVpnStatus.running) { if (curVpnStatus.running) {
return return
} }
console.log('start vpn') console.log('start vpn service', ipv4Addr, cidr, routes)
let start_ret = await start_vpn({ const start_ret = await start_vpn({
"ipv4Addr": ipv4Addr + '/' + cidr, ipv4Addr: `${ipv4Addr}/${cidr}`,
"routes": routes, routes,
"disallowedApplications": ["com.kkrainbow.easytier"], disallowedApplications: ['com.kkrainbow.easytier'],
"mtu": 1300, mtu: 1300,
}); })
if (start_ret?.errorMsg?.length) { if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg) throw new Error(start_ret.errorMsg)
} }
await waitVpnStatus(true, 3) await waitVpnStatus(true, 3)
curVpnStatus.ipv4Addr = ipv4Addr curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes curVpnStatus.routes = routes
} }
async function onVpnServiceStart(payload: any) { async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload)) console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true curVpnStatus.running = true
if (payload.fd) { if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd) setTunFd(networkStore.networkInstanceIds[0], payload.fd)
} }
} }
async function onVpnServiceStop(payload: any) { async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload)) console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false curVpnStatus.running = false
} }
async function registerVpnServiceListener() { async function registerVpnServiceListener() {
console.log('register vpn service listener') console.log('register vpn service listener')
await addPluginListener( await addPluginListener(
'vpnservice', 'vpnservice',
'vpn_service_start', 'vpn_service_start',
onVpnServiceStart onVpnServiceStart,
) )
await addPluginListener( await addPluginListener(
'vpnservice', 'vpnservice',
'vpn_service_stop', 'vpn_service_stop',
onVpnServiceStop onVpnServiceStop,
) )
} }
function getRoutesForVpn(routes: Route[]): string[] { function getRoutesForVpn(routes: Route[]): string[] {
if (!routes) { if (!routes) {
return [] return []
} }
let ret = [] const ret = []
for (let r of routes) { for (const r of routes) {
for (let cidr of r.proxy_cidrs) { for (let cidr of r.proxy_cidrs) {
if (cidr.indexOf('/') === -1) { if (!cidr.includes('/')) {
cidr += '/32' cidr += '/32'
} }
ret.push(cidr) ret.push(cidr)
}
} }
}
// sort and dedup // sort and dedup
return Array.from(new Set(ret)).sort() return Array.from(new Set(ret)).sort()
} }
async function onNetworkInstanceChange() { async function onNetworkInstanceChange() {
let insts = networkStore.networkInstanceIds console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
if (!insts) { const insts = networkStore.networkInstanceIds
await doStopVpn() if (!insts) {
return await doStopVpn()
return
}
const curNetworkInfo = networkStore.networkInfos[insts[0]]
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
await doStopVpn()
return
}
const virtual_ip = Utils.ipv4ToString(curNetworkInfo?.my_node_info?.virtual_ipv4.address)
if (!virtual_ip || !virtual_ip.length) {
await doStopVpn()
return
}
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
if (!network_length) {
network_length = 24
}
const routes = getRoutesForVpn(curNetworkInfo?.routes)
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) {
console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try {
await doStopVpn()
}
catch (e) {
console.error(e)
} }
const curNetworkInfo = networkStore.networkInfos[insts[0]] try {
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) { await doStartVpn(virtual_ip, 24, routes)
await doStopVpn()
return
} }
catch (e) {
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4 console.error('start vpn service failed, clear all network insts.', e)
if (!virtual_ip || !virtual_ip.length) { networkStore.clearNetworkInstances()
await doStopVpn() await retainNetworkInstance(networkStore.networkInstanceIds)
return
}
const routes = getRoutesForVpn(curNetworkInfo?.routes)
var ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
var routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) {
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try {
await doStopVpn()
} catch (e) {
console.error(e)
}
try {
await doStartVpn(virtual_ip, 24, routes)
} catch (e) {
console.error("start vpn failed, clear all network insts.", e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
}
return
} }
}
} }
async function watchNetworkInstance() { async function watchNetworkInstance() {
var subscribe_running = false let subscribe_running = false
networkStore.$subscribe(async () => { networkStore.$subscribe(async () => {
if (subscribe_running) { if (subscribe_running) {
return return
} }
subscribe_running = true subscribe_running = true
try { try {
await onNetworkInstanceChange() await onNetworkInstanceChange()
} catch (_) { }
} catch (_) {
subscribe_running = false }
}) subscribe_running = false
})
console.error('vpn service watch network instance')
} }
export async function initMobileVpnService() { export async function initMobileVpnService() {
await registerVpnServiceListener() await registerVpnServiceListener()
await watchNetworkInstance() await watchNetworkInstance()
} }
export async function prepareVpnService() { export async function prepareVpnService() {
console.log('prepare vpn') console.log('prepare vpn')
let prepare_ret = await prepare_vpn() const prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret))) console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) { if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg) throw new Error(prepare_ret.errorMsg)
} }
} }
+10 -4
View File
@@ -1,6 +1,8 @@
import { invoke } from "@tauri-apps/api/core" import type { NetworkTypes } from 'easytier-frontend-lib'
import { invoke } from '@tauri-apps/api/core'
import type { NetworkConfig, NetworkInstanceRunningInfo } from '~/types/network' type NetworkConfig = NetworkTypes.NetworkConfig
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
export async function parseNetworkConfig(cfg: NetworkConfig) { export async function parseNetworkConfig(cfg: NetworkConfig) {
return invoke<string>('parse_network_config', { cfg }) return invoke<string>('parse_network_config', { cfg })
@@ -22,8 +24,8 @@ export async function getOsHostname() {
return await invoke<string>('get_os_hostname') return await invoke<string>('get_os_hostname')
} }
export async function setAutoLaunchStatus(enable: boolean) { export async function isAutostart() {
return await invoke<boolean>('set_auto_launch_status', { enable }) return await invoke<boolean>('is_autostart')
} }
export async function setLoggingLevel(level: string) { export async function setLoggingLevel(level: string) {
@@ -33,3 +35,7 @@ export async function setLoggingLevel(level: string) {
export async function setTunFd(instanceId: string, fd: number) { export async function setTunFd(instanceId: string, fd: number) {
return await invoke('set_tun_fd', { instanceId, fd }) return await invoke('set_tun_fd', { instanceId, fd })
} }
export async function getEasytierVersion() {
return await invoke<string>('easytier_version')
}
+17 -12
View File
@@ -1,6 +1,6 @@
import { getCurrentWindow } from '@tauri-apps/api/window'
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu' import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
import { TrayIcon } from '@tauri-apps/api/tray' import { TrayIcon } from '@tauri-apps/api/tray'
import { getCurrentWindow } from '@tauri-apps/api/window'
import pkg from '~/../package.json' import pkg from '~/../package.json'
const DEFAULT_TRAY_NAME = 'main' const DEFAULT_TRAY_NAME = 'main'
@@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
async function toggleVisibility() { async function toggleVisibility() {
if (await getCurrentWindow().isVisible()) { if (await getCurrentWindow().isVisible()) {
await getCurrentWindow().hide() await getCurrentWindow().hide()
} else { }
else {
await getCurrentWindow().show() await getCurrentWindow().show()
await getCurrentWindow().setFocus() await getCurrentWindow().setFocus()
} }
} }
export async function useTray(init: boolean = false) { export async function useTray(init: boolean = false) {
let tray; let tray
try { try {
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME) tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
if (!tray) { if (!tray) {
@@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
}), }),
action: async () => { action: async () => {
toggleVisibility() toggleVisibility()
} },
}) })
} }
} catch (error) { }
catch (error) {
console.warn('Error while creating tray icon:', error) console.warn('Error while creating tray icon:', error)
return null return null
} }
if (init) { if (init) {
tray.setTooltip(`EasyTier\n${pkg.version}`) tray.setTooltip(`EasyTier\n${pkg.version}`)
tray.setMenuOnLeftClick(false); tray.setMenuOnLeftClick(false)
tray.setMenu(await Menu.new({ tray.setMenu(await Menu.new({
id: 'main', id: 'main',
items: await generateMenuItem(), items: await generateMenuItem(),
@@ -59,7 +61,7 @@ export async function generateMenuItem() {
export async function MenuItemExit(text: string) { export async function MenuItemExit(text: string) {
return await PredefinedMenuItem.new({ return await PredefinedMenuItem.new({
text: text, text,
item: 'Quit', item: 'Quit',
}) })
} }
@@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
id: 'show', id: 'show',
text, text,
action: async () => { action: async () => {
await toggleVisibility(); await toggleVisibility()
}, },
}) })
} }
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) { export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
const menu = await Menu.new({ const menu = await Menu.new({
id: 'main', id: 'main',
items: items || await generateMenuItem(), items: items || await generateMenuItem(),
@@ -86,15 +89,17 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
export async function setTrayRunState(isRunning: boolean = false) { export async function setTrayRunState(isRunning: boolean = false) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico') tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
} }
export async function setTrayTooltip(tooltip: string) { export async function setTrayTooltip(tooltip: string) {
if (tooltip) { if (tooltip) {
const tray = await useTray() const tray = await useTray()
if (!tray) return if (!tray)
return
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`) tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`) tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
} }
} }
+24 -19
View File
@@ -1,16 +1,15 @@
import { setupLayouts } from 'virtual:generated-layouts' import Aura from '@primevue/themes/aura'
import { createRouter, createWebHistory } from 'vue-router/auto'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice' import ToastService from 'primevue/toastservice'
import App from '~/App.vue'
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
import App from '~/App.vue'
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
import '~/styles.css' import '~/styles.css'
import Aura from '@primevue/themes/aura' import 'easytier-frontend-lib/style.css'
import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => { document.addEventListener('keydown', (event) => {
@@ -18,8 +17,9 @@ if (import.meta.env.PROD) {
event.key === 'F5' event.key === 'F5'
|| (event.ctrlKey && event.key === 'r') || (event.ctrlKey && event.key === 'r')
|| (event.metaKey && event.key === 'r') || (event.metaKey && event.key === 'r')
) ) {
event.preventDefault() event.preventDefault()
}
}) })
document.addEventListener('contextmenu', (event) => { document.addEventListener('contextmenu', (event) => {
@@ -28,29 +28,34 @@ if (import.meta.env.PROD) {
} }
async function main() { async function main() {
await loadLanguageAsync(localStorage.getItem('lang') || 'en') await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync()) await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
const app = createApp(App) const app = createApp(App)
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
extendRoutes: routes => setupLayouts(routes), routes,
}) })
app.use(router) app.use(router)
app.use(createPinia()) app.use(createPinia())
app.use(i18n, { useScope: 'global' }) app.use(EasyTierFrontendLib)
// app.use(i18n, { useScope: 'global' })
app.use(PrimeVue, { app.use(PrimeVue, {
theme: { theme: {
preset: Aura, preset: Aura,
options: { options: {
prefix: 'p', prefix: 'p',
darkModeSelector: 'system', darkModeSelector: 'system',
cssLayer: false cssLayer: {
} name: 'primevue',
}}) order: 'tailwind-base, primevue, tailwind-utilities'
app.use(ToastService) }
},
},
})
app.use(ToastService as any)
app.mount('#app') app.mount('#app')
} }
+20 -10
View File
@@ -1,16 +1,26 @@
import { setAutoLaunchStatus } from "~/composables/network" import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
export async function loadAutoLaunchStatusAsync(enable: boolean): Promise<boolean> { export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
try { try {
const ret = await setAutoLaunchStatus(enable) if (target_enable) {
localStorage.setItem('auto_launch', JSON.stringify(ret)) await enable()
return ret
} catch (e) {
console.error(e)
return false
} }
else {
// 消除没有配置自启动时进行关闭操作报错
try {
await disable()
}
catch { }
}
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
return isEnabled()
}
catch (e) {
console.error(e)
return false
}
} }
export function getAutoLaunchStatusAsync(): boolean { export function getAutoLaunchStatusAsync(): boolean {
return localStorage.getItem('auto_launch') === 'true' return localStorage.getItem('auto_launch') === 'true'
} }
+116 -58
View File
@@ -1,24 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useToast } from 'primevue/usetoast'
import { exit } from '@tauri-apps/plugin-process';
import Config from '~/components/Config.vue'
import Status from '~/components/Status.vue'
import type { NetworkConfig } from '~/types/network'
import { loadLanguageAsync } from '~/modules/i18n'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
import { loadRunningInstanceIdsFromLocalStorage } from '~/stores/network'
import { setLoggingLevel } from '~/composables/network'
import TieredMenu from 'primevue/tieredmenu'
import { open } from '@tauri-apps/plugin-shell';
import { appLogDir } from '@tauri-apps/api/path' import { appLogDir } from '@tauri-apps/api/path'
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { useTray } from '~/composables/tray'; import { getCurrentWindow } from '@tauri-apps/api/window'
import { type } from '@tauri-apps/plugin-os'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { type } from '@tauri-apps/plugin-os'
import { exit } from '@tauri-apps/plugin-process'
import { open } from '@tauri-apps/plugin-shell'
import TieredMenu from 'primevue/tieredmenu'
import { useToast } from 'primevue/usetoast'
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
import { isAutostart, setLoggingLevel } from '~/composables/network'
import { useTray } from '~/composables/tray'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const visible = ref(false) const visible = ref(false)
const aboutVisible = ref(false)
const tomlConfig = ref('') const tomlConfig = ref('')
useTray(true) useTray(true)
@@ -64,6 +62,27 @@ const toast = useToast()
const networkStore = useNetworkStore() const networkStore = useNetworkStore()
const curNetworkConfig = computed(() => {
if (networkStore.curNetworkId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
console.log('curNetworkInst', ret)
if (ret === undefined) {
return null;
} else {
return ret;
}
})
function addNewNetwork() { function addNewNetwork() {
networkStore.addNewNetwork() networkStore.addNewNetwork()
networkStore.curNetwork = networkStore.lastNetwork networkStore.curNetwork = networkStore.lastNetwork
@@ -71,7 +90,6 @@ function addNewNetwork() {
networkStore.$subscribe(async () => { networkStore.$subscribe(async () => {
networkStore.saveToLocalStorage() networkStore.saveToLocalStorage()
networkStore.saveRunningInstanceIdsToLocalStorage()
try { try {
await parseNetworkConfig(networkStore.curNetwork) await parseNetworkConfig(networkStore.curNetwork)
messageBarSeverity.value = Severity.None messageBarSeverity.value = Severity.None
@@ -82,11 +100,12 @@ networkStore.$subscribe(async () => {
} }
}) })
async function runNetworkCb(cfg: NetworkConfig, cb: () => void) { async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
if (type() === 'android') { if (type() === 'android') {
await prepareVpnService() await prepareVpnService()
networkStore.clearNetworkInstances() networkStore.clearNetworkInstances()
} else { }
else {
networkStore.removeNetworkInstance(cfg.instance_id) networkStore.removeNetworkInstance(cfg.instance_id)
} }
@@ -95,6 +114,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
try { try {
await runNetworkInstance(cfg) await runNetworkInstance(cfg)
networkStore.addAutoStartInstId(cfg.instance_id)
} }
catch (e: any) { catch (e: any) {
// console.error(e) // console.error(e)
@@ -104,11 +124,12 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
cb() cb()
} }
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) { async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
// console.log('stopNetworkCb', cfg, cb) // console.log('stopNetworkCb', cfg, cb)
cb() cb()
networkStore.removeNetworkInstance(cfg.instance_id) networkStore.removeNetworkInstance(cfg.instance_id)
await retainNetworkInstance(networkStore.networkInstanceIds) await retainNetworkInstance(networkStore.networkInstanceIds)
networkStore.removeAutoStartInstId(cfg.instance_id)
} }
async function updateNetworkInfos() { async function updateNetworkInfos() {
@@ -120,10 +141,13 @@ onMounted(async () => {
intervalId = window.setInterval(async () => { intervalId = window.setInterval(async () => {
await updateNetworkInfos() await updateNetworkInfos()
}, 500) }, 500)
await setTrayMenu([
await MenuItemExit(t('tray.exit')), window.setTimeout(async () => {
await MenuItemShow(t('tray.show')) await setTrayMenu([
]) await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show')),
])
}, 1000)
}) })
onUnmounted(() => clearInterval(intervalId)) onUnmounted(() => clearInterval(intervalId))
@@ -139,10 +163,10 @@ const setting_menu_items = ref([
label: () => t('exchange_language'), label: () => t('exchange_language'),
icon: 'pi pi-language', icon: 'pi pi-language',
command: async () => { command: async () => {
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en')) await I18nUtils.loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await setTrayMenu([ await setTrayMenu([
await MenuItemExit(t('tray.exit')), await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show')) await MenuItemShow(t('tray.show')),
]) ])
}, },
}, },
@@ -158,10 +182,10 @@ const setting_menu_items = ref([
icon: 'pi pi-file', icon: 'pi pi-file',
items: (function () { items: (function () {
const levels = ['off', 'warn', 'info', 'debug', 'trace'] const levels = ['off', 'warn', 'info', 'debug', 'trace']
let items = [] const items = []
for (let level of levels) { for (const level of levels) {
items.push({ items.push({
label: () => t("logging_level_" + level) + (current_log_level === level ? ' ✓' : ''), label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
command: async () => { command: async () => {
current_log_level = level current_log_level = level
await setLoggingLevel(level) await setLoggingLevel(level)
@@ -175,7 +199,7 @@ const setting_menu_items = ref([
label: () => t('logging_open_dir'), label: () => t('logging_open_dir'),
icon: 'pi pi-folder-open', icon: 'pi pi-folder-open',
command: async () => { command: async () => {
console.log("open log dir", await appLogDir()) // console.log('open log dir', await appLogDir())
await open(await appLogDir()) await open(await appLogDir())
}, },
}) })
@@ -187,7 +211,14 @@ const setting_menu_items = ref([
}, },
}) })
return items return items
})() })(),
},
{
label: () => t('about.title'),
icon: 'pi pi-at',
command: async () => {
aboutVisible.value = true
},
}, },
{ {
label: () => t('exit'), label: () => t('exit'),
@@ -202,34 +233,42 @@ function toggle_setting_menu(event: any) {
setting_menu.value.toggle(event) setting_menu.value.toggle(event)
} }
onMounted(async () => { onBeforeMount(async () => {
networkStore.loadFromLocalStorage() networkStore.loadFromLocalStorage()
if (getAutoLaunchStatus()) { if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
let prev_running_ids = loadRunningInstanceIdsFromLocalStorage() getCurrentWindow().hide()
for (let id of prev_running_ids) { const autoStartIds = networkStore.autoStartInstIds
let cfg = networkStore.networkList.find((item) => item.instance_id === id) for (const id of autoStartIds) {
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
if (cfg) { if (cfg) {
networkStore.addNetworkInstance(cfg.instance_id) networkStore.addNetworkInstance(cfg.instance_id)
await runNetworkInstance(cfg) await runNetworkInstance(cfg)
} }
} }
} }
})
onMounted(async () => {
if (type() === 'android') { if (type() === 'android') {
await initMobileVpnService() try {
await initMobileVpnService()
console.error("easytier init vpn service done")
} catch (e: any) {
console.error("easytier init vpn service failed", e)
}
} }
}) })
function isRunning(id: string) { function isRunning(id: string) {
return networkStore.networkInstanceIds.includes(id) return networkStore.networkInstanceIds.includes(id)
} }
</script> </script>
<script lang="ts"> <script lang="ts">
</script> </script>
<template> <template>
<div id="root" class="flex flex-column"> <div id="root" class="flex flex-col">
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }"> <Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
<Panel> <Panel>
<ScrollPanel style="width: 100%; height: 300px"> <ScrollPanel style="width: 100%; height: 300px">
@@ -237,50 +276,61 @@ function isRunning(id: string) {
</ScrollPanel> </ScrollPanel>
</Panel> </Panel>
<Divider /> <Divider />
<div class="flex justify-content-end gap-2"> <div class="flex gap-2 justify-end">
<Button type="button" :label="t('close')" @click="visible = false" /> <Button type="button" :label="t('close')" @click="visible = false" />
</div> </div>
</Dialog> </Dialog>
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
<About />
</Dialog>
<div> <div>
<Toolbar> <Toolbar>
<template #start> <template #start>
<div class="flex align-items-center"> <div class="flex items-center">
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" /> <Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" @click="addNewNetwork" />
</div> </div>
</template> </template>
<template #center> <template #center>
<div class="min-w-40"> <div class="min-w-40">
<Dropdown v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false" <Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="w-full"> :placeholder="t('select_network')" class="w-full">
<template #value="slotProps"> <template #value="slotProps">
<div class="flex items-start content-center"> <div class="flex items-start content-center">
<div class="mr-3 flex-column"> <div class="mr-4 flex-col">
<span>{{ slotProps.value.network_name }}</span> <span>{{ slotProps.value.network_name }}</span>
</div> </div>
<Tag class="my-auto" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'" <Tag class="my-auto leading-3" :severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" /> :value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
</div> </div>
</template> </template>
<template #option="slotProps"> <template #option="slotProps">
<div class="flex flex-col items-start content-center"> <div class="flex flex-col items-start content-center max-w-full">
<div class="flex"> <div class="flex">
<div class="mr-3"> <div class="mr-4">
{{ t('network_name') }}: {{ slotProps.option.network_name }} {{ t('network_name') }}: {{ slotProps.option.network_name }}
</div> </div>
<Tag class="my-auto" :severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'" <Tag class="my-auto leading-3"
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" /> :value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
</div> </div>
<div>{{ slotProps.option.public_server_url }}</div> <div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis">
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
? slotProps.option.peer_urls.join(', ')
: slotProps.option.public_server_url }}
</div>
<div <div
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 !== '')"> v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
{{ networkStore.instances[slotProps.option.instance_id].detail {{
? networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4 : '' }} Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
}}
</div> </div>
</div> </div>
</template> </template>
</Dropdown> </Select>
</div> </div>
</template> </template>
@@ -295,19 +345,23 @@ function isRunning(id: string) {
<Panel class="h-full overflow-y-auto"> <Panel class="h-full overflow-y-auto">
<Stepper :value="activeStep"> <Stepper :value="activeStep">
<StepList value="1"> <StepList value="1">
<Step value="1">{{ t('config_network') }}</Step> <Step value="1">
<Step value="2">{{ t('running') }}</Step> {{ t('config_network') }}
</Step>
<Step value="2">
{{ t('running') }}
</Step>
</StepList> </StepList>
<StepPanels value="1"> <StepPanels value="1">
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1"> <StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None" <Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
@run-network="runNetworkCb($event, () => activateCallback('2'))" /> :cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
</StepPanel> </StepPanel>
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2"> <StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
<div class="flex flex-column"> <div class="flex flex-col">
<Status :instance-id="networkStore.curNetworkId" /> <Status :cur-network-inst="curNetworkInst" />
</div> </div>
<div class="flex pt-4 justify-content-center"> <div class="flex pt-6 justify-center">
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left" <Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" /> @click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
</div> </div>
@@ -349,6 +403,10 @@ body {
margin: 0; margin: 0;
} }
.p-select-overlay {
max-width: calc(100% - 2rem);
}
/* /*
.p-tabview-panel { .p-tabview-panel {
+44 -27
View File
@@ -1,24 +1,25 @@
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network' import { NetworkTypes } from 'easytier-frontend-lib'
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
export const useNetworkStore = defineStore('networkStore', { export const useNetworkStore = defineStore('networkStore', {
state: () => { state: () => {
const networkList = [DEFAULT_NETWORK_CONFIG()] const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
return { return {
// for initially empty lists // for initially empty lists
networkList: networkList as NetworkConfig[], networkList: networkList as NetworkTypes.NetworkConfig[],
// for data that is not yet loaded // for data that is not yet loaded
curNetwork: networkList[0], curNetwork: networkList[0],
// uuid -> instance // uuid -> instance
instances: {} as Record<string, NetworkInstance>, instances: {} as Record<string, NetworkTypes.NetworkInstance>,
networkInfos: {} as Record<string, NetworkInstanceRunningInfo>, networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
autoStartInstIds: [] as string[],
} }
}, },
getters: { getters: {
lastNetwork(): NetworkConfig { lastNetwork(): NetworkTypes.NetworkConfig {
return this.networkList[this.networkList.length - 1] return this.networkList[this.networkList.length - 1]
}, },
@@ -26,7 +27,7 @@ export const useNetworkStore = defineStore('networkStore', {
return this.curNetwork.instance_id return this.curNetwork.instance_id
}, },
networkInstances(): Array<NetworkInstance> { networkInstances(): Array<NetworkTypes.NetworkInstance> {
return Object.values(this.instances) return Object.values(this.instances)
}, },
@@ -37,7 +38,7 @@ export const useNetworkStore = defineStore('networkStore', {
actions: { actions: {
addNewNetwork() { addNewNetwork() {
this.networkList.push(DEFAULT_NETWORK_CONFIG()) this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
}, },
delCurNetwork() { delCurNetwork() {
@@ -64,7 +65,7 @@ export const useNetworkStore = defineStore('networkStore', {
this.instances = {} this.instances = {}
}, },
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) { updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
this.networkInfos = networkInfos this.networkInfos = networkInfos
for (const [instanceId, info] of Object.entries(networkInfos)) { for (const [instanceId, info] of Object.entries(networkInfos)) {
if (this.instances[instanceId] === undefined) if (this.instances[instanceId] === undefined)
@@ -74,45 +75,61 @@ export const useNetworkStore = defineStore('networkStore', {
this.instances[instanceId].error_msg = info.error_msg || '' this.instances[instanceId].error_msg = info.error_msg || ''
this.instances[instanceId].detail = info this.instances[instanceId].detail = info
} }
this.saveRunningInstanceIdsToLocalStorage()
}, },
loadFromLocalStorage() { loadFromLocalStorage() {
let networkList: NetworkConfig[] let networkList: NetworkTypes.NetworkConfig[]
// if localStorage default is [{}], instanceId will be undefined // if localStorage default is [{}], instanceId will be undefined
networkList = JSON.parse(localStorage.getItem('networkList') || '[]') networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
networkList = networkList.map((cfg) => { networkList = networkList.map((cfg) => {
return { ...DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkConfig return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
}) })
// prevent a empty list from localStorage, should not happen // prevent a empty list from localStorage, should not happen
if (networkList.length === 0) if (networkList.length === 0)
networkList = [DEFAULT_NETWORK_CONFIG()] networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
this.networkList = networkList this.networkList = networkList
this.curNetwork = this.networkList[0] this.curNetwork = this.networkList[0]
this.loadAutoStartInstIdsFromLocalStorage()
}, },
saveToLocalStorage() { saveToLocalStorage() {
localStorage.setItem('networkList', JSON.stringify(this.networkList)) localStorage.setItem('networkList', JSON.stringify(this.networkList))
}, },
saveRunningInstanceIdsToLocalStorage() { saveAutoStartInstIdsToLocalStorage() {
let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running) localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids)) },
}
loadAutoStartInstIdsFromLocalStorage() {
try {
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
}
catch (e) {
console.error(e)
this.autoStartInstIds = []
}
},
addAutoStartInstId(instanceId: string) {
if (!this.autoStartInstIds.includes(instanceId)) {
this.autoStartInstIds.push(instanceId)
}
this.saveAutoStartInstIdsToLocalStorage()
},
removeAutoStartInstId(instanceId: string) {
const idx = this.autoStartInstIds.indexOf(instanceId)
if (idx !== -1) {
this.autoStartInstIds.splice(idx, 1)
}
this.saveAutoStartInstIdsToLocalStorage()
},
}, },
}) })
if (import.meta.hot) if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot)) import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))
export function loadRunningInstanceIdsFromLocalStorage(): string[] {
try {
return JSON.parse(localStorage.getItem('runningInstanceIds') || '[]')
} catch (e) {
console.error(e)
return []
}
}
-1
View File
@@ -16,7 +16,6 @@
font-weight: 400; font-weight: 400;
color: #0f0f0f; color: #0f0f0f;
background-color: white;
font-synthesis: none; font-synthesis: none;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
+1 -1
View File
@@ -12,7 +12,7 @@ declare module 'vue-router/auto-routes' {
ParamValueOneOrMore, ParamValueOneOrMore,
ParamValueZeroOrMore, ParamValueZeroOrMore,
ParamValueZeroOrOne, ParamValueZeroOrOne,
} from 'unplugin-vue-router/types' } from 'vue-router'
/** /**
* Route name map generated by unplugin-vue-router * Route name map generated by unplugin-vue-router
+37 -14
View File
@@ -1,15 +1,35 @@
import { networkInterfaces } from 'node:os'
import path from 'node:path' import path from 'node:path'
import { defineConfig } from 'vite' import process from 'node:process'
import Vue from '@vitejs/plugin-vue'
import Layouts from 'vite-plugin-vue-layouts'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import VueI18n from '@intlify/unplugin-vue-i18n/vite' import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import VueDevTools from 'vite-plugin-vue-devtools' import { PrimeVueResolver } from '@primevue/auto-import-resolver'
import VueRouter from 'unplugin-vue-router/vite' import Vue from '@vitejs/plugin-vue'
import { containsCidr, parseCidr } from 'cidr-tools'
import { gateway4sync } from 'default-gateway'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import { VueRouterAutoImports } from 'unplugin-vue-router' import { VueRouterAutoImports } from 'unplugin-vue-router'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'; import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts'
function findIp(gateway: string) {
// Look for the matching interface in all local interfaces
console.log('gateway', gateway)
for (const addresses of Object.values(networkInterfaces())) {
if (!addresses)
continue
for (const { cidr } of addresses) {
if (cidr && containsCidr(cidr, gateway)) {
return parseCidr(cidr).ip
}
}
}
}
const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => ({ export default defineConfig(async () => ({
@@ -87,15 +107,18 @@ export default defineConfig(async () => ({
// 2. tauri expects a fixed port, fail if that port is not available // 2. tauri expects a fixed port, fail if that port is not available
server: { server: {
port: 1420, port: 1420,
host: '10.147.223.128', host: host || false,
strictPort: true, strictPort: true,
watch: { watch: {
// 3. tell vite to ignore watching `src-tauri` // 3. tell vite to ignore watching `src-tauri`
ignored: ['**/src-tauri/**'], ignored: ['**/src-tauri/**'],
}, },
hmr: { hmr: host
host: "10.147.223.128", ? {
protocol: "ws", protocol: 'ws',
}, host: findIp(gateway4sync().gateway),
port: 1430,
}
: undefined,
}, },
})) }))
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "easytier-rpc-build"
description = "Protobuf RPC Service Generator for EasyTier"
version = "0.1.0"
edition = "2021"
homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier"
authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"]
rust-version = "1.84.0"
license-file = "LICENSE"
readme = "README.md"
[dependencies]
heck = "0.5"
prost-build = "0.13"
[features]
default = []
internal-namespace = []
+1
View File
@@ -0,0 +1 @@
../LICENSE
+3
View File
@@ -0,0 +1,3 @@
# Introduction
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.
+387
View File
@@ -0,0 +1,387 @@
extern crate heck;
extern crate prost_build;
use std::fmt;
#[cfg(feature = "internal-namespace")]
const NAMESPACE: &str = "crate::proto::rpc_types";
#[cfg(not(feature = "internal-namespace"))]
const NAMESPACE: &str = "easytier::proto::rpc_types";
/// The service generator to be used with `prost-build` to generate RPC implementations for
/// `prost-simple-rpc`.
///
/// See the crate-level documentation for more info.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug)]
pub struct ServiceGenerator {
_private: (),
}
impl ServiceGenerator {
/// Create a new `ServiceGenerator` instance with the default options set.
pub fn new() -> ServiceGenerator {
ServiceGenerator { _private: () }
}
}
impl prost_build::ServiceGenerator for ServiceGenerator {
fn generate(&mut self, service: prost_build::Service, mut buf: &mut String) {
use std::fmt::Write;
let descriptor_name = format!("{}Descriptor", service.name);
let server_name = format!("{}Server", service.name);
let client_name = format!("{}Client", service.name);
let method_descriptor_name = format!("{}MethodDescriptor", service.name);
let mut trait_methods = String::new();
let mut enum_methods = String::new();
let mut list_enum_methods = String::new();
let mut client_methods = String::new();
let mut client_own_methods = String::new();
let mut match_name_methods = String::new();
let mut match_proto_name_methods = String::new();
let mut match_input_type_methods = String::new();
let mut match_input_proto_type_methods = String::new();
let mut match_output_type_methods = String::new();
let mut match_output_proto_type_methods = String::new();
let mut match_handle_methods = String::new();
let mut match_method_try_from = String::new();
for (idx, method) in service.methods.iter().enumerate() {
assert!(
!method.client_streaming,
"Client streaming not yet supported for method {}",
method.proto_name
);
assert!(
!method.server_streaming,
"Server streaming not yet supported for method {}",
method.proto_name
);
ServiceGenerator::write_comments(&mut trait_methods, 4, &method.comments).unwrap();
writeln!(
trait_methods,
r#" async fn {name}(&self, ctrl: Self::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}>;"#,
name = method.name,
input_type = method.input_type,
output_type = method.output_type,
namespace = NAMESPACE,
)
.unwrap();
ServiceGenerator::write_comments(&mut enum_methods, 4, &method.comments).unwrap();
writeln!(
enum_methods,
" {name} = {index},",
name = method.proto_name,
index = format!("{}", idx + 1)
)
.unwrap();
writeln!(
match_method_try_from,
" {index} => Ok({service_name}MethodDescriptor::{name}),",
service_name = service.name,
name = method.proto_name,
index = format!("{}", idx + 1),
)
.unwrap();
writeln!(
list_enum_methods,
" {service_name}MethodDescriptor::{name},",
service_name = service.name,
name = method.proto_name
)
.unwrap();
writeln!(
client_methods,
r#" async fn {name}(&self, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
{client_name}::{name}_inner(self.0.clone(), ctrl, input).await
}}"#,
name = method.name,
input_type = method.input_type,
output_type = method.output_type,
client_name = format!("{}Client", service.name),
namespace = NAMESPACE,
)
.unwrap();
writeln!(
client_own_methods,
r#" async fn {name}_inner(handler: H, ctrl: H::Controller, input: {input_type}) -> {namespace}::error::Result<{output_type}> {{
{namespace}::__rt::call_method(handler, ctrl, {method_descriptor_name}::{proto_name}, input).await
}}"#,
name = method.name,
method_descriptor_name = method_descriptor_name,
proto_name = method.proto_name,
input_type = method.input_type,
output_type = method.output_type,
namespace = NAMESPACE,
).unwrap();
let case = format!(
" {service_name}MethodDescriptor::{proto_name} => ",
service_name = service.name,
proto_name = method.proto_name
);
writeln!(match_name_methods, "{}{:?},", case, method.name).unwrap();
writeln!(match_proto_name_methods, "{}{:?},", case, method.proto_name).unwrap();
writeln!(
match_input_type_methods,
"{}::std::any::TypeId::of::<{}>(),",
case, method.input_type
)
.unwrap();
writeln!(
match_input_proto_type_methods,
"{}{:?},",
case, method.input_proto_type
)
.unwrap();
writeln!(
match_output_type_methods,
"{}::std::any::TypeId::of::<{}>(),",
case, method.output_type
)
.unwrap();
writeln!(
match_output_proto_type_methods,
"{}{:?},",
case, method.output_proto_type
)
.unwrap();
write!(
match_handle_methods,
r#"{} {{
let decoded: {input_type} = {namespace}::__rt::decode(input)?;
let ret = service.{name}(ctrl, decoded).await?;
{namespace}::__rt::encode(ret)
}}
"#,
case,
input_type = method.input_type,
name = method.name,
namespace = NAMESPACE,
)
.unwrap();
}
ServiceGenerator::write_comments(&mut buf, 0, &service.comments).unwrap();
write!(
buf,
r#"
#[async_trait::async_trait]
#[auto_impl::auto_impl(&, Arc, Box)]
pub trait {name} {{
type Controller: {namespace}::controller::Controller;
{trait_methods}
}}
/// A service descriptor for a `{name}`.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Default)]
pub struct {descriptor_name};
/// Methods available on a `{name}`.
///
/// This can be used as a key when routing requests for servers/clients of a `{name}`.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
pub enum {method_descriptor_name} {{
{enum_methods}
}}
impl std::convert::TryFrom<u8> for {method_descriptor_name} {{
type Error = {namespace}::error::Error;
fn try_from(value: u8) -> {namespace}::error::Result<Self> {{
match value {{
{match_method_try_from}
_ => Err({namespace}::error::Error::InvalidMethodIndex(value, "{name}".to_string())),
}}
}}
}}
/// A client for a `{name}`.
///
/// This implements the `{name}` trait by dispatching all method calls to the supplied `Handler`.
#[derive(Clone, Debug)]
pub struct {client_name}<H>(H) where H: {namespace}::handler::Handler;
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
/// Creates a new client instance that delegates all method calls to the supplied handler.
pub fn new(handler: H) -> {client_name}<H> {{
{client_name}(handler)
}}
}}
impl<H> {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
{client_own_methods}
}}
#[async_trait::async_trait]
impl<H> {name} for {client_name}<H> where H: {namespace}::handler::Handler<Descriptor = {descriptor_name}> {{
type Controller = H::Controller;
{client_methods}
}}
pub struct {client_name}Factory<C: {namespace}::controller::Controller>(std::marker::PhantomData<C>);
impl<C: {namespace}::controller::Controller> Clone for {client_name}Factory<C> {{
fn clone(&self) -> Self {{
Self(std::marker::PhantomData)
}}
}}
impl<C> {namespace}::__rt::RpcClientFactory for {client_name}Factory<C> where C: {namespace}::controller::Controller {{
type Descriptor = {descriptor_name};
type ClientImpl = Box<dyn {name}<Controller = C> + Send + 'static>;
type Controller = C;
fn new(handler: impl {namespace}::handler::Handler<Descriptor = Self::Descriptor, Controller = Self::Controller>) -> Self::ClientImpl {{
Box::new({client_name}::new(handler))
}}
}}
/// A server for a `{name}`.
///
/// This implements the `Server` trait by handling requests and dispatch them to methods on the
/// supplied `{name}`.
#[derive(Clone, Debug)]
pub struct {server_name}<A>(A) where A: {name} + Clone + Send + 'static;
impl<A> {server_name}<A> where A: {name} + Clone + Send + 'static {{
/// Creates a new server instance that dispatches all calls to the supplied service.
pub fn new(service: A) -> {server_name}<A> {{
{server_name}(service)
}}
async fn call_inner(
service: A,
method: {method_descriptor_name},
ctrl: A::Controller,
input: ::bytes::Bytes)
-> {namespace}::error::Result<::bytes::Bytes> {{
match method {{
{match_handle_methods}
}}
}}
}}
impl {namespace}::descriptor::ServiceDescriptor for {descriptor_name} {{
type Method = {method_descriptor_name};
fn name(&self) -> &'static str {{ {name:?} }}
fn proto_name(&self) -> &'static str {{ {proto_name:?} }}
fn package(&self) -> &'static str {{ {package:?} }}
fn methods(&self) -> &'static [Self::Method] {{
&[ {list_enum_methods} ]
}}
}}
#[async_trait::async_trait]
impl<A> {namespace}::handler::Handler for {server_name}<A>
where
A: {name} + Clone + Send + Sync + 'static {{
type Descriptor = {descriptor_name};
type Controller = A::Controller;
async fn call(
&self,
ctrl: A::Controller,
method: {method_descriptor_name},
input: ::bytes::Bytes)
-> {namespace}::error::Result<::bytes::Bytes> {{
{server_name}::call_inner(self.0.clone(), method, ctrl, input).await
}}
}}
impl {namespace}::descriptor::MethodDescriptor for {method_descriptor_name} {{
fn name(&self) -> &'static str {{
match *self {{
{match_name_methods}
}}
}}
fn proto_name(&self) -> &'static str {{
match *self {{
{match_proto_name_methods}
}}
}}
fn input_type(&self) -> ::std::any::TypeId {{
match *self {{
{match_input_type_methods}
}}
}}
fn input_proto_type(&self) -> &'static str {{
match *self {{
{match_input_proto_type_methods}
}}
}}
fn output_type(&self) -> ::std::any::TypeId {{
match *self {{
{match_output_type_methods}
}}
}}
fn output_proto_type(&self) -> &'static str {{
match *self {{
{match_output_proto_type_methods}
}}
}}
fn index(&self) -> u8 {{
*self as u8
}}
}}
"#,
name = service.name,
descriptor_name = descriptor_name,
server_name = server_name,
client_name = client_name,
method_descriptor_name = method_descriptor_name,
proto_name = service.proto_name,
package = service.package,
trait_methods = trait_methods,
enum_methods = enum_methods,
list_enum_methods = list_enum_methods,
client_own_methods = client_own_methods,
client_methods = client_methods,
match_name_methods = match_name_methods,
match_proto_name_methods = match_proto_name_methods,
match_input_type_methods = match_input_type_methods,
match_input_proto_type_methods = match_input_proto_type_methods,
match_output_type_methods = match_output_type_methods,
match_output_proto_type_methods = match_output_proto_type_methods,
match_handle_methods = match_handle_methods,
namespace = NAMESPACE,
).unwrap();
}
}
impl ServiceGenerator {
fn write_comments<W>(
mut write: W,
indent: usize,
comments: &prost_build::Comments,
) -> fmt::Result
where
W: fmt::Write,
{
for comment in &comments.leading {
for line in comment.lines().filter(|s| !s.is_empty()) {
writeln!(write, "{}///{}", " ".repeat(indent), line)?;
}
}
Ok(())
}
}
+57
View File
@@ -0,0 +1,57 @@
[package]
name = "easytier-web"
version = "2.2.2"
edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
[dependencies]
easytier = { path = "../easytier" }
tracing = { version = "0.1", features = ["log"] }
anyhow = { version = "1.0" }
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
dashmap = "6.1"
url = "2.2"
async-trait = "0.1"
axum = { version = "0.7", features = ["macros"] }
axum-login = { version = "0.16" }
password-auth = { version = "1.0.0" }
axum-messages = "0.7.0"
tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
tower-sessions = { version = "0.13.0", default-features = false, features = [
"signed",
] }
tower-http = { version = "0.6", features = ["cors", "compression-full"] }
sqlx = { version = "0.8", features = ["sqlite"] }
sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
sea-orm-migration = { version = "1.1" }
# for captcha
rust-embed = { version = "8.5.0", features = ["debug-embed"] }
base64 = "0.22"
rand = "0.8"
image = {version="0.24", default-features = false, features = ["png"]}
rusttype = "0.9.3"
imageproc = "0.23.0"
rust-i18n = "3"
sys-locale = "0.3"
clap = { version = "4.4.8", features = [
"string",
"unicode",
"derive",
"wrap_help",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.5.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
"serde",
] }
chrono = { version = "0.4.37", features = ["serde"] }
+24
View File
@@ -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?
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript 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 the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
+13
View File
@@ -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 + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
{
"name": "easytier-frontend-lib",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "./dist/easytier-frontend-lib.umd.cjs",
"module": "./dist/easytier-frontend-lib.js",
"exports": {
".": {
"import": "./dist/easytier-frontend-lib.js",
"require": "./dist/easytier-frontend-lib.umd.cjs"
},
"./*.css": "./dist/*.css"
},
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@primevue/themes": "^4.2.1",
"@vueuse/core": "^11.1.0",
"aura": "link:@primevue\\themes\\aura",
"axios": "^1.7.7",
"floating-vue": "^5.2",
"ip-num": "1.5.1",
"primeicons": "^7.0.0",
"primevue": "^4.2.1",
"tailwindcss-primeui": "^0.3.4",
"ts-md5": "^1.3.1",
"uuid": "^11.0.2",
"vue": "^3.5.12",
"vue-i18n": "^10.0.4"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0",
"@types/node": "^22.8.6",
"@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vue-tsc": "^2.1.10"
}
}
+820
View File
@@ -0,0 +1,820 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
vue:
specifier: ^3.5.12
version: 3.5.12(typescript@5.6.3)
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.1.4
version: 5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))
typescript:
specifier: ~5.6.2
version: 5.6.3
vite:
specifier: ^5.4.10
version: 5.4.10
vue-tsc:
specifier: ^2.1.8
version: 2.1.10(typescript@5.6.3)
packages:
'@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.26.2':
resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/types@7.26.0':
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
engines: {node: '>=6.9.0'}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.21.5':
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.21.5':
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.21.5':
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.21.5':
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.21.5':
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.21.5':
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.21.5':
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.21.5':
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.21.5':
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.21.5':
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.21.5':
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.21.5':
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.21.5':
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.21.5':
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.21.5':
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.21.5':
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.21.5':
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-x64@0.21.5':
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.21.5':
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.21.5':
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.21.5':
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.21.5':
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@rollup/rollup-android-arm-eabi@4.24.3':
resolution: {integrity: sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.24.3':
resolution: {integrity: sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.24.3':
resolution: {integrity: sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.24.3':
resolution: {integrity: sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.24.3':
resolution: {integrity: sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.24.3':
resolution: {integrity: sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
resolution: {integrity: sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
resolution: {integrity: sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.24.3':
resolution: {integrity: sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.24.3':
resolution: {integrity: sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
resolution: {integrity: sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
resolution: {integrity: sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.24.3':
resolution: {integrity: sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.24.3':
resolution: {integrity: sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.24.3':
resolution: {integrity: sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.24.3':
resolution: {integrity: sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.24.3':
resolution: {integrity: sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.24.3':
resolution: {integrity: sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
'@vitejs/plugin-vue@5.1.4':
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
vite: ^5.0.0
vue: ^3.2.25
'@volar/language-core@2.4.8':
resolution: {integrity: sha512-K/GxMOXGq997bO00cdFhTNuR85xPxj0BEEAy+BaqqayTmy9Tmhfgmq2wpJcVspRhcwfgPoE2/mEJa26emUhG/g==}
'@volar/source-map@2.4.8':
resolution: {integrity: sha512-jeWJBkC/WivdelMwxKkpFL811uH/jJ1kVxa+c7OvG48DXc3VrP7pplSWPP2W1dLMqBxD+awRlg55FQQfiup4cA==}
'@volar/typescript@2.4.8':
resolution: {integrity: sha512-6xkIYJ5xxghVBhVywMoPMidDDAFT1OoQeXwa27HSgJ6AiIKRe61RXLoik+14Z7r0JvnblXVsjsRLmCr42SGzqg==}
'@vue/compiler-core@3.5.12':
resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
'@vue/compiler-dom@3.5.12':
resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
'@vue/compiler-sfc@3.5.12':
resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
'@vue/compiler-ssr@3.5.12':
resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
'@vue/language-core@2.1.10':
resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
'@vue/reactivity@3.5.12':
resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
'@vue/runtime-core@3.5.12':
resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
'@vue/runtime-dom@3.5.12':
resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
'@vue/server-renderer@3.5.12':
resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
peerDependencies:
vue: 3.5.12
'@vue/shared@3.5.12':
resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
alien-signals@0.2.0:
resolution: {integrity: sha512-StlonZhBBrsPPwrDjiPAiVTf/rolxffLxVPT60Qv/t88BZ81BvUVzHgGqEFvJ1ii8HXtm1+zU2Icr59tfWEcag==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
hasBin: true
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
magic-string@0.30.12:
resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
postcss@8.4.47:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.24.3:
resolution: {integrity: sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
semver@7.6.3:
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
engines: {node: '>=10'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
typescript@5.6.3:
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
engines: {node: '>=14.17'}
hasBin: true
vite@5.4.10:
resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@types/node': ^18.0.0 || >=20.0.0
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
vue-tsc@2.1.10:
resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
vue@3.5.12:
resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
snapshots:
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-validator-identifier@7.25.9': {}
'@babel/parser@7.26.2':
dependencies:
'@babel/types': 7.26.0
'@babel/types@7.26.0':
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@esbuild/aix-ppc64@0.21.5':
optional: true
'@esbuild/android-arm64@0.21.5':
optional: true
'@esbuild/android-arm@0.21.5':
optional: true
'@esbuild/android-x64@0.21.5':
optional: true
'@esbuild/darwin-arm64@0.21.5':
optional: true
'@esbuild/darwin-x64@0.21.5':
optional: true
'@esbuild/freebsd-arm64@0.21.5':
optional: true
'@esbuild/freebsd-x64@0.21.5':
optional: true
'@esbuild/linux-arm64@0.21.5':
optional: true
'@esbuild/linux-arm@0.21.5':
optional: true
'@esbuild/linux-ia32@0.21.5':
optional: true
'@esbuild/linux-loong64@0.21.5':
optional: true
'@esbuild/linux-mips64el@0.21.5':
optional: true
'@esbuild/linux-ppc64@0.21.5':
optional: true
'@esbuild/linux-riscv64@0.21.5':
optional: true
'@esbuild/linux-s390x@0.21.5':
optional: true
'@esbuild/linux-x64@0.21.5':
optional: true
'@esbuild/netbsd-x64@0.21.5':
optional: true
'@esbuild/openbsd-x64@0.21.5':
optional: true
'@esbuild/sunos-x64@0.21.5':
optional: true
'@esbuild/win32-arm64@0.21.5':
optional: true
'@esbuild/win32-ia32@0.21.5':
optional: true
'@esbuild/win32-x64@0.21.5':
optional: true
'@jridgewell/sourcemap-codec@1.5.0': {}
'@rollup/rollup-android-arm-eabi@4.24.3':
optional: true
'@rollup/rollup-android-arm64@4.24.3':
optional: true
'@rollup/rollup-darwin-arm64@4.24.3':
optional: true
'@rollup/rollup-darwin-x64@4.24.3':
optional: true
'@rollup/rollup-freebsd-arm64@4.24.3':
optional: true
'@rollup/rollup-freebsd-x64@4.24.3':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.24.3':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.24.3':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.24.3':
optional: true
'@rollup/rollup-linux-arm64-musl@4.24.3':
optional: true
'@rollup/rollup-linux-powerpc64le-gnu@4.24.3':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.24.3':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.24.3':
optional: true
'@rollup/rollup-linux-x64-gnu@4.24.3':
optional: true
'@rollup/rollup-linux-x64-musl@4.24.3':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.24.3':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.24.3':
optional: true
'@rollup/rollup-win32-x64-msvc@4.24.3':
optional: true
'@types/estree@1.0.6': {}
'@vitejs/plugin-vue@5.1.4(vite@5.4.10)(vue@3.5.12(typescript@5.6.3))':
dependencies:
vite: 5.4.10
vue: 3.5.12(typescript@5.6.3)
'@volar/language-core@2.4.8':
dependencies:
'@volar/source-map': 2.4.8
'@volar/source-map@2.4.8': {}
'@volar/typescript@2.4.8':
dependencies:
'@volar/language-core': 2.4.8
path-browserify: 1.0.1
vscode-uri: 3.0.8
'@vue/compiler-core@3.5.12':
dependencies:
'@babel/parser': 7.26.2
'@vue/shared': 3.5.12
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.12':
dependencies:
'@vue/compiler-core': 3.5.12
'@vue/shared': 3.5.12
'@vue/compiler-sfc@3.5.12':
dependencies:
'@babel/parser': 7.26.2
'@vue/compiler-core': 3.5.12
'@vue/compiler-dom': 3.5.12
'@vue/compiler-ssr': 3.5.12
'@vue/shared': 3.5.12
estree-walker: 2.0.2
magic-string: 0.30.12
postcss: 8.4.47
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.12':
dependencies:
'@vue/compiler-dom': 3.5.12
'@vue/shared': 3.5.12
'@vue/compiler-vue2@2.7.16':
dependencies:
de-indent: 1.0.2
he: 1.2.0
'@vue/language-core@2.1.10(typescript@5.6.3)':
dependencies:
'@volar/language-core': 2.4.8
'@vue/compiler-dom': 3.5.12
'@vue/compiler-vue2': 2.7.16
'@vue/shared': 3.5.12
alien-signals: 0.2.0
minimatch: 9.0.5
muggle-string: 0.4.1
path-browserify: 1.0.1
optionalDependencies:
typescript: 5.6.3
'@vue/reactivity@3.5.12':
dependencies:
'@vue/shared': 3.5.12
'@vue/runtime-core@3.5.12':
dependencies:
'@vue/reactivity': 3.5.12
'@vue/shared': 3.5.12
'@vue/runtime-dom@3.5.12':
dependencies:
'@vue/reactivity': 3.5.12
'@vue/runtime-core': 3.5.12
'@vue/shared': 3.5.12
csstype: 3.1.3
'@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.6.3))':
dependencies:
'@vue/compiler-ssr': 3.5.12
'@vue/shared': 3.5.12
vue: 3.5.12(typescript@5.6.3)
'@vue/shared@3.5.12': {}
alien-signals@0.2.0: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
csstype@3.1.3: {}
de-indent@1.0.2: {}
entities@4.5.0: {}
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
'@esbuild/android-arm': 0.21.5
'@esbuild/android-arm64': 0.21.5
'@esbuild/android-x64': 0.21.5
'@esbuild/darwin-arm64': 0.21.5
'@esbuild/darwin-x64': 0.21.5
'@esbuild/freebsd-arm64': 0.21.5
'@esbuild/freebsd-x64': 0.21.5
'@esbuild/linux-arm': 0.21.5
'@esbuild/linux-arm64': 0.21.5
'@esbuild/linux-ia32': 0.21.5
'@esbuild/linux-loong64': 0.21.5
'@esbuild/linux-mips64el': 0.21.5
'@esbuild/linux-ppc64': 0.21.5
'@esbuild/linux-riscv64': 0.21.5
'@esbuild/linux-s390x': 0.21.5
'@esbuild/linux-x64': 0.21.5
'@esbuild/netbsd-x64': 0.21.5
'@esbuild/openbsd-x64': 0.21.5
'@esbuild/sunos-x64': 0.21.5
'@esbuild/win32-arm64': 0.21.5
'@esbuild/win32-ia32': 0.21.5
'@esbuild/win32-x64': 0.21.5
estree-walker@2.0.2: {}
fsevents@2.3.3:
optional: true
he@1.2.0: {}
magic-string@0.30.12:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.1
muggle-string@0.4.1: {}
nanoid@3.3.7: {}
path-browserify@1.0.1: {}
picocolors@1.1.1: {}
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.24.3:
dependencies:
'@types/estree': 1.0.6
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.24.3
'@rollup/rollup-android-arm64': 4.24.3
'@rollup/rollup-darwin-arm64': 4.24.3
'@rollup/rollup-darwin-x64': 4.24.3
'@rollup/rollup-freebsd-arm64': 4.24.3
'@rollup/rollup-freebsd-x64': 4.24.3
'@rollup/rollup-linux-arm-gnueabihf': 4.24.3
'@rollup/rollup-linux-arm-musleabihf': 4.24.3
'@rollup/rollup-linux-arm64-gnu': 4.24.3
'@rollup/rollup-linux-arm64-musl': 4.24.3
'@rollup/rollup-linux-powerpc64le-gnu': 4.24.3
'@rollup/rollup-linux-riscv64-gnu': 4.24.3
'@rollup/rollup-linux-s390x-gnu': 4.24.3
'@rollup/rollup-linux-x64-gnu': 4.24.3
'@rollup/rollup-linux-x64-musl': 4.24.3
'@rollup/rollup-win32-arm64-msvc': 4.24.3
'@rollup/rollup-win32-ia32-msvc': 4.24.3
'@rollup/rollup-win32-x64-msvc': 4.24.3
fsevents: 2.3.3
semver@7.6.3: {}
source-map-js@1.2.1: {}
typescript@5.6.3: {}
vite@5.4.10:
dependencies:
esbuild: 0.21.5
postcss: 8.4.47
rollup: 4.24.3
optionalDependencies:
fsevents: 2.3.3
vscode-uri@3.0.8: {}
vue-tsc@2.1.10(typescript@5.6.3):
dependencies:
'@volar/typescript': 2.4.8
'@vue/language-core': 2.1.10(typescript@5.6.3)
semver: 7.6.3
typescript: 5.6.3
vue@3.5.12(typescript@5.6.3):
dependencies:
'@vue/compiler-dom': 3.5.12
'@vue/compiler-sfc': 3.5.12
'@vue/runtime-dom': 3.5.12
'@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.6.3))
'@vue/shared': 3.5.12
optionalDependencies:
typescript: 5.6.3
@@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"postcss-nested": {},
},
}
@@ -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 @@
<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,297 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button } from 'primevue'
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
import { defineProps, defineEmits, ref, } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
configInvalid?: boolean
hostname?: string
}>()
defineEmits(['runNetwork'])
const curNetwork = defineModel('curNetwork', {
type: Object as () => NetworkConfig,
default: DEFAULT_NETWORK_CONFIG,
})
const { t } = useI18n()
const networking_methods = ref([
{ value: NetworkingMethod.PublicServer, label: () => t('public_server') },
{ value: NetworkingMethod.Manual, label: () => t('manual') },
{ value: NetworkingMethod.Standalone, label: () => t('standalone') },
])
const protos: { [proto: string]: number } = { tcp: 11010, udp: 11010, wg: 11011, ws: 11011, wss: 11012 }
function searchUrlSuggestions(e: { query: string }): string[] {
const query = e.query
const ret = []
// if query match "^\w+:.*", then no proto prefix
if (query.match(/^\w+:.*/)) {
// if query is a valid url, then add to suggestions
try {
// eslint-disable-next-line no-new
new URL(query)
ret.push(query)
}
catch { }
}
else {
for (const proto in protos) {
let item = `${proto}://${query}`
// if query match ":\d+$", then no port suffix
if (!query.match(/:\d+$/)) {
item += `:${protos[proto]}`
}
ret.push(item)
}
}
return ret
}
const publicServerSuggestions = ref([''])
function searchPresetPublicServers(e: { query: string }) {
const presetPublicServers = [
'tcp://public.easytier.top:11010',
]
const query = e.query
// if query is sub string of presetPublicServers, add to suggestions
let ret = presetPublicServers.filter(item => item.includes(query))
// add additional suggestions
if (query.length > 0) {
ret = ret.concat(searchUrlSuggestions(e))
}
publicServerSuggestions.value = ret
}
const peerSuggestions = ref([''])
function searchPeerSuggestions(e: { query: string }) {
peerSuggestions.value = searchUrlSuggestions(e)
}
const inetSuggestions = ref([''])
function searchInetSuggestions(e: { query: string }) {
if (e.query.search('/') >= 0) {
inetSuggestions.value = [e.query]
} else {
const ret = []
for (let i = 0; i < 32; i++) {
ret.push(`${e.query}/${i}`)
}
inetSuggestions.value = ret
}
}
const listenerSuggestions = ref([''])
function searchListenerSuggestions(e: { query: string }) {
const ret = []
for (const proto in protos) {
let item = `${proto}://0.0.0.0:`
// if query is a number, use it as port
if (e.query.match(/^\d+$/)) {
item += e.query
}
else {
item += protos[proto]
}
if (item.includes(e.query)) {
ret.push(item)
}
}
if (ret.length === 0) {
ret.push(e.query)
}
listenerSuggestions.value = ret
}
interface BoolFlag {
field: keyof NetworkConfig
help: string
}
const bool_flags: BoolFlag[] = [
{ field: 'latency_first', help: 'latency_first_help' },
{ field: 'use_smoltcp', help: 'use_smoltcp_help' },
{ field: 'enable_kcp_proxy', help: 'enable_kcp_proxy_help' },
{ field: 'disable_kcp_input', help: 'disable_kcp_input_help' },
{ field: 'disable_p2p', help: 'disable_p2p_help' },
{ field: 'bind_device', help: 'bind_device_help' },
{ field: 'no_tun', help: 'no_tun_help' },
]
</script>
<template>
<div class="frontend-lib">
<div class="flex flex-col h-full">
<div class="flex flex-col">
<div class="w-10/12 self-center ">
<Panel :header="t('basic_settings')">
<div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex items-center" for="virtual_ip">
<label class="mr-2"> {{ t('virtual_ipv4') }} </label>
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
<label for="virtual_ip_auto" class="ml-2">
{{ t('virtual_ipv4_dhcp') }}
</label>
</div>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help" />
<InputGroupAddon>
<span>/</span>
</InputGroupAddon>
<InputNumber v-model="curNetwork.network_length" :disabled="curNetwork.dhcp"
inputId="horizontal-buttons" showButtons :step="1" mode="decimal" :min="1" :max="32" fluid
class="max-w-20" />
</InputGroup>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="network_name">{{ t('network_name') }}</label>
<InputText id="network_name" v-model="curNetwork.network_name" aria-describedby="network_name-help" />
</div>
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret"
aria-describedby="network_secret-help" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="nm">{{ t('networking_method') }}</label>
<SelectButton v-model="curNetwork.networking_method" :options="networking_methods"
:option-label="(v) => v.label()" option-value="value" />
<div class="items-center flex flex-row p-fluid gap-x-1">
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.Manual" id="chips"
v-model="curNetwork.peer_urls" :placeholder="t('chips_placeholder', ['tcp://8.8.8.8:11010'])"
class="grow" multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
<AutoComplete v-if="curNetwork.networking_method === NetworkingMethod.PublicServer"
v-model="curNetwork.public_server_url" :suggestions="publicServerSuggestions"
:virtual-scroller-options="{ itemSize: 38 }" class="grow" dropdown :complete-on-focus="true"
@complete="searchPresetPublicServers" />
</div>
</div>
</div>
</div>
</Panel>
<Divider />
<Panel :header="t('advanced_settings')" toggleable collapsed>
<div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label> {{ t('flags_switch') }} </label>
<div class="flex flex-row flex-wrap">
<div class="basis-64 flex" v-for="flag in bool_flags">
<Checkbox v-model="curNetwork[flag.field]" :input-id="flag.field" :binary="true" />
<label :for="flag.field" class="ml-2"> {{ t(flag.field) }} </label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t(flag.help)"></span>
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="hostname">{{ t('hostname') }}</label>
<InputText id="hostname" v-model="curNetwork.hostname" aria-describedby="hostname-help" :format="true"
:placeholder="t('hostname_placeholder', [props.hostname])" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 grow p-fluid">
<label for="username">{{ t('proxy_cidrs') }}</label>
<AutoComplete id="subnet-proxy" v-model="curNetwork.proxy_cidrs"
:placeholder="t('chips_placeholder', ['10.0.0.0/24'])" class="w-full" multiple fluid
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-col gap-2 grow">
<label for="username">VPN Portal</label>
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
<div class="min-w-64">
<InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" />
<InputGroupAddon>
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon>
</InputGroup>
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
:min="0" :max="65535" class="w-8/12" fluid />
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label>
<AutoComplete id="listener_urls" v-model="curNetwork.listener_urls" :suggestions="listenerSuggestions"
class="w-full" dropdown :complete-on-focus="true"
:placeholder="t('chips_placeholder', ['tcp://1.1.1.1:11010'])" multiple
@complete="searchListenerSuggestions" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="rpc_port">{{ t('rpc_port') }}</label>
<InputNumber id="rpc_port" v-model="curNetwork.rpc_port" aria-describedby="rpc_port-help"
:format="false" :min="0" :max="65535" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<label for="dev_name">{{ t('dev_name') }}</label>
<InputText id="dev_name" v-model="curNetwork.dev_name" aria-describedby="dev_name-help" :format="true"
:placeholder="t('dev_name_placeholder')" />
</div>
</div>
</div>
</Panel>
<div class="flex pt-6 justify-center">
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
@click="$emit('runNetwork', curNetwork)" />
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,35 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { EventType } from '../types/network'
import { computed } from 'vue';
import { Fieldset } from 'primevue';
const props = defineProps<{
event: {
[key: string]: any
}
}>()
const { t } = useI18n()
const eventKey = computed(() => {
const key = Object.keys(props.event)[0]
return Object.keys(EventType).includes(key) ? key : 'Unknown'
})
const eventValue = computed(() => {
const value = props.event[eventKey.value]
return typeof value === 'object' ? value : value
})
</script>
<template>
<Fieldset :legend="t(`event.${eventKey}`)">
<template v-if="eventKey !== 'Unknown'">
<div v-if="event.DhcpIpv4Changed">
{{ `${eventValue[0]} -> ${eventValue[1]}` }}
</div>
<pre v-else>{{ eventValue }}</pre>
</template>
<pre v-else>{{ eventValue }}</pre>
</Fieldset>
</template>
@@ -1,31 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NodeInfo } from '~/types/network' import { useTimeAgo } from '@vueuse/core'
const { t } = useI18n() import { IPv4 } from 'ip-num/IPNumber'
import { NetworkInstance, type NodeInfo, type PeerRoutePair } from '../types/network'
import { useI18n } from 'vue-i18n';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { ipv4InetToString, ipv4ToString, ipv6ToString } from '../modules/utils';
import { DataTable, Column, Tag, Chip, Button, Dialog, ScrollPanel, Timeline, Divider, Card, } from 'primevue';
const props = defineProps<{ const props = defineProps<{
instanceId?: string curNetworkInst: NetworkInstance | null,
}>() }>()
const networkStore = useNetworkStore() const { t } = useI18n()
const curNetwork = computed(() => {
if (props.instanceId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === props.instanceId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const curNetworkInst = computed(() => {
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
})
const peerRouteInfos = computed(() => { const peerRouteInfos = computed(() => {
if (curNetworkInst.value) if (props.curNetworkInst) {
return curNetworkInst.value.detail?.peer_route_pairs || [] const my_node_info = props.curNetworkInst.detail?.my_node_info
return [{
route: {
ipv4_addr: my_node_info?.virtual_ipv4,
hostname: my_node_info?.hostname,
version: my_node_info?.version,
},
}, ...(props.curNetworkInst.detail?.peer_route_pairs || [])]
}
return [] return []
}) })
@@ -33,8 +31,9 @@ const peerRouteInfos = computed(() => {
function routeCost(info: any) { function routeCost(info: any) {
if (info.route) { if (info.route) {
const cost = info.route.cost const cost = info.route.cost
return cost === 1 ? 'p2p' : `relay(${cost})` return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
} }
return '?' return '?'
} }
@@ -73,34 +72,45 @@ function humanFileSize(bytes: number, si = false, dp = 1) {
return `${bytes.toFixed(dp)} ${units[u]}` return `${bytes.toFixed(dp)} ${units[u]}`
} }
function latencyMs(info: any) { function latencyMs(info: PeerRoutePair) {
let lat_us_sum = statsCommon(info, 'stats.latency_us') let lat_us_sum = statsCommon(info, 'stats.latency_us')
if (lat_us_sum === undefined) if (lat_us_sum === undefined)
return '' return ''
lat_us_sum = lat_us_sum / 1000 / info.peer.conns.length lat_us_sum = lat_us_sum / 1000 / info.peer!.conns.length
return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms` return `${lat_us_sum % 1 > 0 ? Math.round(lat_us_sum) + 1 : Math.round(lat_us_sum)}ms`
} }
function txBytes(info: any) { function txBytes(info: PeerRoutePair) {
const tx = statsCommon(info, 'stats.tx_bytes') const tx = statsCommon(info, 'stats.tx_bytes')
return tx ? humanFileSize(tx) : '' return tx ? humanFileSize(tx) : ''
} }
function rxBytes(info: any) { function rxBytes(info: PeerRoutePair) {
const rx = statsCommon(info, 'stats.rx_bytes') const rx = statsCommon(info, 'stats.rx_bytes')
return rx ? humanFileSize(rx) : '' return rx ? humanFileSize(rx) : ''
} }
function lossRate(info: any) { function lossRate(info: PeerRoutePair) {
const lossRate = statsCommon(info, 'loss_rate') const lossRate = statsCommon(info, 'loss_rate')
return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : '' return lossRate !== undefined ? `${Math.round(lossRate * 100)}%` : ''
} }
function version(info: PeerRoutePair) {
return info.route.version === '' ? 'unknown' : info.route.version
}
function ipFormat(info: PeerRoutePair) {
const ip = info.route.ipv4_addr
if (typeof ip === 'string')
return ip
return ip ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : ''
}
const myNodeInfo = computed(() => { const myNodeInfo = computed(() => {
if (!curNetworkInst.value) if (!props.curNetworkInst)
return {} as NodeInfo return {} as NodeInfo
return curNetworkInst.value.detail?.my_node_info return props.curNetworkInst.detail?.my_node_info
}) })
interface Chip { interface Chip {
@@ -109,18 +119,26 @@ interface Chip {
} }
const myNodeInfoChips = computed(() => { const myNodeInfoChips = computed(() => {
if (!curNetworkInst.value) if (!props.curNetworkInst)
return [] return []
const chips: Array<Chip> = [] const chips: Array<Chip> = []
const my_node_info = curNetworkInst.value.detail?.my_node_info const my_node_info = props.curNetworkInst.detail?.my_node_info
if (!my_node_info) if (!my_node_info)
return chips return chips
// virtual ipv4 // TUN Device Name
const dev_name = props.curNetworkInst.detail?.dev_name
if (dev_name) {
chips.push({
label: `TUN Device Name: ${dev_name}`,
icon: '',
} as Chip)
}
// virtual ipv4
chips.push({ chips.push({
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`, label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
icon: '', icon: '',
} as Chip) } as Chip)
@@ -128,7 +146,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv4s = my_node_info.ips?.interface_ipv4s const local_ipv4s = my_node_info.ips?.interface_ipv4s
for (const [idx, ip] of local_ipv4s?.entries()) { for (const [idx, ip] of local_ipv4s?.entries()) {
chips.push({ chips.push({
label: `Local IPv4 ${idx}: ${ip}`, label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
icon: '', icon: '',
} as Chip) } as Chip)
} }
@@ -137,7 +155,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv6s = my_node_info.ips?.interface_ipv6s const local_ipv6s = my_node_info.ips?.interface_ipv6s
for (const [idx, ip] of local_ipv6s?.entries()) { for (const [idx, ip] of local_ipv6s?.entries()) {
chips.push({ chips.push({
label: `Local IPv6 ${idx}: ${ip}`, label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
icon: '', icon: '',
} as Chip) } as Chip)
} }
@@ -146,7 +164,15 @@ const myNodeInfoChips = computed(() => {
const public_ip = my_node_info.ips?.public_ipv4 const public_ip = my_node_info.ips?.public_ipv4
if (public_ip) { if (public_ip) {
chips.push({ chips.push({
label: `Public IP: ${public_ip}`, label: `Public IP: ${IPv4.fromNumber(public_ip.addr)}`,
icon: '',
} as Chip)
}
const public_ipv6 = my_node_info.ips?.public_ipv6
if (public_ipv6) {
chips.push({
label: `Public IPv6: ${ipv6ToString(public_ipv6)}`,
icon: '', icon: '',
} as Chip) } as Chip)
} }
@@ -155,7 +181,7 @@ const myNodeInfoChips = computed(() => {
const listeners = my_node_info.listeners const listeners = my_node_info.listeners
for (const [idx, listener] of listeners?.entries()) { for (const [idx, listener] of listeners?.entries()) {
chips.push({ chips.push({
label: `Listener ${idx}: ${listener}`, label: `Listener ${idx}: ${listener.url}`,
icon: '', icon: '',
} as Chip) } as Chip)
} }
@@ -171,6 +197,8 @@ const myNodeInfoChips = computed(() => {
PortRestricted = 5, PortRestricted = 5,
Symmetric = 6, Symmetric = 6,
SymUdpFirewall = 7, SymUdpFirewall = 7,
SymmetricEasyInc = 8,
SymmetricEasyDec = 9,
}; };
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
if (udpNatType !== undefined) { if (udpNatType !== undefined) {
@@ -183,6 +211,8 @@ const myNodeInfoChips = computed(() => {
[NatType.PortRestricted]: 'Port Restricted', [NatType.PortRestricted]: 'Port Restricted',
[NatType.Symmetric]: 'Symmetric', [NatType.Symmetric]: 'Symmetric',
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall', [NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
} }
chips.push({ chips.push({
@@ -261,28 +291,31 @@ function showVpnPortalConfig() {
} }
function showEventLogs() { function showEventLogs() {
const detail = curNetworkInst.value?.detail const detail = props.curNetworkInst?.detail
if (!detail) if (!detail)
return return
dialogContent.value = detail.events dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
dialogHeader.value = 'event_log' dialogHeader.value = 'event_log'
dialogVisible.value = true dialogVisible.value = true
} }
</script> </script>
<template> <template>
<div> <div class="frontend-lib">
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" :style="{ width: '70%' }"> <Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto max-w-full">
<Panel> <ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
<ScrollPanel style="width: 100%; height: 400px"> <pre>{{ dialogContent }}</pre>
<pre>{{ dialogContent }}</pre> </ScrollPanel>
</ScrollPanel> <Timeline v-else :value="dialogContent">
</Panel> <template #opposite="slotProps">
<Divider /> <small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
<div class="flex justify-content-end gap-2"> }}</small>
<Button type="button" :label="t('close')" @click="dialogVisible = false" /> </template>
</div> <template #content="slotProps">
<HumanEvent :event="slotProps.item.event" />
</template>
</Timeline>
</Dialog> </Dialog>
<Card v-if="curNetworkInst?.error_msg"> <Card v-if="curNetworkInst?.error_msg">
@@ -290,7 +323,7 @@ function showEventLogs() {
Run Network Error Run Network Error
</template> </template>
<template #content> <template #content>
<div class="flex flex-column gap-y-5"> <div class="flex flex-col gap-y-5">
<div class="text-red-500"> <div class="text-red-500">
{{ curNetworkInst.error_msg }} {{ curNetworkInst.error_msg }}
</div> </div>
@@ -304,12 +337,9 @@ function showEventLogs() {
{{ t('my_node_info') }} {{ t('my_node_info') }}
</template> </template>
<template #content> <template #content>
<div class="flex w-full flex-column gap-y-5"> <div class="flex w-full flex-col gap-y-5">
<div class="m-0 flex flex-row justify-center gap-x-5"> <div class="m-0 flex flex-row justify-center gap-x-5">
<div <div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid green"
>
<div class="font-bold"> <div class="font-bold">
{{ t('peer_count') }} {{ t('peer_count') }}
</div> </div>
@@ -318,10 +348,7 @@ function showEventLogs() {
</div> </div>
</div> </div>
<div <div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid purple"
>
<div class="font-bold"> <div class="font-bold">
{{ t('upload') }} {{ t('upload') }}
</div> </div>
@@ -330,10 +357,7 @@ function showEventLogs() {
</div> </div>
</div> </div>
<div <div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid fuchsia"
>
<div class="font-bold"> <div class="font-bold">
{{ t('download') }} {{ t('download') }}
</div> </div>
@@ -343,11 +367,9 @@ function showEventLogs() {
</div> </div>
</div> </div>
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll"> <div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
<Chip <Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon" class="mr-2 mt-2 text-sm" />
class="mr-2 mt-2 text-sm"
/>
</div> </div>
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm"> <div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
@@ -365,17 +387,44 @@ function showEventLogs() {
{{ t('peer_info') }} {{ t('peer_info') }}
</template> </template>
<template #content> <template #content>
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-style="width: 100%"> <DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
<Column field="route.ipv4_addr" style="width: 100px;" :header="t('virtual_ipv4')" /> <Column :field="ipFormat" :header="t('virtual_ipv4')" />
<Column field="route.hostname" style="max-width: 250px;" :header="t('hostname')" /> <Column :header="t('hostname')">
<Column :field="routeCost" style="width: 100px;" :header="t('route_cost')" /> <template #body="slotProps">
<Column :field="latencyMs" style="width: 80px;" :header="t('latency')" /> <div v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
<Column :field="txBytes" style="width: 80px;" :header="t('upload_bytes')" /> v-tooltip="slotProps.data.route.hostname">
<Column :field="rxBytes" style="width: 80px;" :header="t('download_bytes')" /> {{
<Column :field="lossRate" style="width: 100px;" :header="t('loss_rate')" /> slotProps.data.route.hostname }}
</div>
<div v-else v-tooltip="slotProps.data.route.hostname" class="space-x-1">
<Tag v-if="slotProps.data.route.feature_flag.is_public_server" severity="info" value="Info">
{{ t('status.server') }}
</Tag>
<Tag v-if="slotProps.data.route.feature_flag.avoid_relay_data" severity="warn" value="Warn">
{{ t('status.relay') }}
</Tag>
</div>
</template>
</Column>
<Column :field="routeCost" :header="t('route_cost')" />
<Column :field="latencyMs" :header="t('latency')" />
<Column :field="txBytes" :header="t('upload_bytes')" />
<Column :field="rxBytes" :header="t('download_bytes')" />
<Column :field="lossRate" :header="t('loss_rate')" />
<Column :header="t('status.version')">
<template #body="slotProps">
<span>{{ version(slotProps.data) }}</span>
</template>
</Column>
</DataTable> </DataTable>
</template> </template>
</Card> </Card>
</template> </template>
</div> </div>
</template> </template>
<style lang="postcss" scoped>
.p-timeline :deep(.p-timeline-event-opposite) {
@apply flex-none;
}
</style>
@@ -0,0 +1,2 @@
export { default as Config } from './Config.vue';
export { default as Status } from './Status.vue';
@@ -0,0 +1,50 @@
import './style.css'
import type { App } from 'vue';
import { Config, Status } from "./components";
import Aura from '@primevue/themes/aura'
import PrimeVue from 'primevue/config'
import I18nUtils from './modules/i18n'
import * as NetworkTypes from './types/network'
import HumanEvent from './components/HumanEvent.vue';
// do not use primevue tooltip, it has serious memory leak issue
// https://github.com/primefaces/primevue/issues/5856
// import Tooltip from 'primevue/tooltip';
import { vTooltip } from 'floating-vue';
import * as Api from './modules/api';
import * as Utils from './modules/utils';
export default {
install: (app: App): void => {
app.use(I18nUtils.i18n, { useScope: 'global' })
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
prefix: 'p',
darkModeSelector: 'system',
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities'
}
},
},
zIndex: {
modal: 1100, //dialog, drawer
overlay: 1200, //select, popover
menu: 1300, //overlay menus
tooltip: 1400 //tooltip
}
});
app.component('Config', Config);
app.component('Status', Status);
app.component('HumanEvent', HumanEvent);
app.directive('tooltip', vTooltip as any);
}
};
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
@@ -0,0 +1,139 @@
network: 网络
networking_method: 网络方式
public_server: 公共服务器
manual: 手动
standalone: 独立
virtual_ipv4: 虚拟IPv4地址
virtual_ipv4_dhcp: DHCP
network_name: 网络名称
network_secret: 网络密码
public_server_url: 公共服务器地址
peer_urls: 对等节点地址
proxy_cidrs: 子网代理CIDR
enable_vpn_portal: 启用VPN门户
vpn_portal_listen_port: 监听端口
vpn_portal_client_network: 客户端子网
dev_name: TUN接口名称
advanced_settings: 高级设置
basic_settings: 基础设置
listener_urls: 监听地址
rpc_port: RPC端口
config_network: 配置网络
running: 运行中
error_msg: 错误信息
detail: 详情
add_new_network: 添加新网络
del_cur_network: 删除当前网络
select_network: 选择网络
network_instances: 网络实例
instance_id: 实例ID
network_infos: 网络信息
parse_network_config: 解析网络配置
retain_network_instance: 保留网络实例
collect_network_infos: 收集网络信息
settings: 设置
exchange_language: Switch to English
logging: 日志
logging_level_info: 信息
logging_level_debug: 调试
logging_level_warn: 警告
logging_level_trace: 跟踪
logging_level_off: 关闭
logging_open_dir: 打开日志目录
logging_copy_dir: 复制日志路径
disable_auto_launch: 关闭开机自启
enable_auto_launch: 开启开机自启
exit: 退出
chips_placeholder: 例如: {0}, 按回车添加
hostname_placeholder: '留空默认为主机名: {0}'
dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名称时,将会在设置TUN的IP时产生冲突,留空以自动生成随机名称
off_text: 点击关闭
on_text: 点击开启
show_config: 显示配置
close: 关闭
use_latency_first: 延迟优先模式
my_node_info: 当前节点信息
peer_count: 已连接
upload: 上传
download: 下载
show_vpn_portal_config: 显示VPN门户配置
vpn_portal_config: VPN门户配置
show_event_log: 显示事件日志
event_log: 事件日志
peer_info: 节点信息
hostname: 主机名
route_cost: 路由
latency: 延迟
upload_bytes: 上传
download_bytes: 下载
loss_rate: 丢包率
flags_switch: 功能开关
latency_first: 开启延迟优先模式
latency_first_help: 忽略中转跳数,选择总延迟最低的路径
use_smoltcp: 使用用户态协议栈
use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙问题导致无法子网代理 / KCP代理。
enable_kcp_proxy: 启用 KCP 代理
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
disable_kcp_input: 禁用 KCP 输入
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。
disable_p2p: 禁用 P2P
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
bind_device: 仅使用物理网卡
bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网建立连接。
no_tun: 无 TUN 模式
no_tun_help: 不使用 TUN 网卡,适合无管理员权限时使用。本节点仅允许被访问。访问其他节点需要使用 SOCK5
status:
version: 内核版本
local: 本机
server: 服务器
relay: 中继
run_network: 运行网络
stop_network: 停止网络
network_running: 运行中
network_stopped: 已停止
dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中发生IP冲突,将自动更改IP。
tray:
show: 显示 / 隐藏
exit: 退出
about:
title: 关于
version: 版本
author: 作者
homepage: 主页
license: 许可证
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
check_update: 检查更新
event:
Unknown: 未知
TunDeviceReady: Tun设备就绪
TunDeviceError: Tun设备错误
PeerAdded: 对端添加
PeerRemoved: 对端移除
PeerConnAdded: 对端连接添加
PeerConnRemoved: 对端连接移除
ListenerAdded: 监听器添加
ListenerAddFailed: 监听器添加失败
ListenerAcceptFailed: 监听器接受连接失败
ConnectionAccepted: 连接已接受
ConnectionError: 连接错误
Connecting: 正在连接
ConnectError: 连接错误
VpnPortalClientConnected: VPN门户客户端已连接
VpnPortalClientDisconnected: VPN门户客户端已断开连接
DhcpIpv4Changed: DHCP IPv4地址更改
DhcpIpv4Conflicted: DHCP IPv4地址冲突
@@ -0,0 +1,137 @@
network: Network
networking_method: Networking Method
public_server: Public Server
manual: Manual
standalone: Standalone
virtual_ipv4: Virtual IPv4
virtual_ipv4_dhcp: DHCP
network_name: Network Name
network_secret: Network Secret
public_server_url: Public Server URL
peer_urls: Peer URLs
proxy_cidrs: Subnet Proxy CIDRs
enable_vpn_portal: Enable VPN Portal
vpn_portal_listen_port: VPN Portal Listen Port
vpn_portal_client_network: Client Sub Network
dev_name: TUN interface name
advanced_settings: Advanced Settings
basic_settings: Basic Settings
listener_urls: Listener URLs
rpc_port: RPC Port
config_network: Config Network
running: Running
error_msg: Error Message
detail: Detail
add_new_network: New Network
del_cur_network: Delete Current Network
select_network: Select Network
network_instances: Network Instances
instance_id: Instance ID
network_infos: Network Infos
parse_network_config: Parse Network Config
retain_network_instance: Retain Network Instance
collect_network_infos: Collect Network Infos
settings: Settings
exchange_language: 切换中文
logging: Logging
logging_level_info: Info
logging_level_debug: Debug
logging_level_warn: Warn
logging_level_trace: Trace
logging_level_off: Off
logging_open_dir: Open Log Directory
logging_copy_dir: Copy Log Path
disable_auto_launch: Disable Launch on Reboot
enable_auto_launch: Enable Launch on Reboot
exit: Exit
use_latency_first: Latency First Mode
chips_placeholder: 'e.g: {0}, press Enter to add'
hostname_placeholder: 'Leave blank and default to host name: {0}'
dev_name_placeholder: 'Note: When multiple networks use the same TUN interface name at the same time, there will be a conflict when setting the TUN''s IP. Leave blank to automatically generate a random name.'
off_text: Press to disable
on_text: Press to enable
show_config: Show Config
close: Close
my_node_info: My Node Info
peer_count: Connected
upload: Upload
download: Download
show_vpn_portal_config: Show VPN Portal Config
vpn_portal_config: VPN Portal Config
show_event_log: Show Event Log
event_log: Event Log
peer_info: Peer Info
route_cost: Route Cost
hostname: Hostname
latency: Latency
upload_bytes: Upload
download_bytes: Download
loss_rate: Loss Rate
flags_switch: Feature Switch
latency_first: Enable Latency-First Mode
latency_first_help: Ignore hop count and select the path with the lowest total latency
use_smoltcp: Use User-Space Protocol Stack
use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating system firewalls blocking subnet or KCP proxy functionality.
enable_kcp_proxy: Enable KCP Proxy
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
disable_kcp_input: Disable KCP Input
disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP.
disable_p2p: Disable P2P
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
bind_device: Bind to Physical Device Only
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
no_tun: No TUN Mode
no_tun_help: Do not use a TUN interface, suitable for environments without administrator privileges. This node is only accessible; accessing other nodes requires SOCKS5.
status:
version: Version
local: Local
server: Server
relay: Relay
run_network: Run Network
stop_network: Stop Network
network_running: running
network_stopped: stopped
dhcp_experimental_warning: Experimental warning! if there is an IP conflict in the network when using DHCP, the IP will be automatically changed.
tray:
show: Show / Hide
exit: Exit
about:
title: About
version: Version
author: Author
homepage: Homepage
license: License
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
check_update: Check Update
event:
Unknown: Unknown
TunDeviceReady: TunDeviceReady
TunDeviceError: TunDeviceError
PeerAdded: PeerAdded
PeerRemoved: PeerRemoved
PeerConnAdded: PeerConnAdded
PeerConnRemoved: PeerConnRemoved
ListenerAdded: ListenerAdded
ListenerAddFailed: ListenerAddFailed
ListenerAcceptFailed: ListenerAcceptFailed
ConnectionAccepted: ConnectionAccepted
ConnectionError: ConnectionError
Connecting: Connecting
ConnectError: ConnectError
VpnPortalClientConnected: VpnPortalClientConnected
VpnPortalClientDisconnected: VpnPortalClientDisconnected
DhcpIpv4Changed: DhcpIpv4Changed
DhcpIpv4Conflicted: DhcpIpv4Conflicted
@@ -0,0 +1,220 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { Md5 } from 'ts-md5'
import { UUID } from './utils';
import { NetworkConfig } from '../types/network';
export interface ValidateConfigResponse {
toml_config: string;
}
// 定义接口返回的数据结构
export interface LoginResponse {
success: boolean;
message: string;
}
export interface RegisterResponse {
success: boolean;
message: string;
}
// 定义请求体数据结构
export interface Credential {
username: string;
password: string;
}
export interface RegisterData {
credentials: Credential;
captcha: string;
}
export interface Summary {
device_count: number;
}
export interface ListNetworkInstanceIdResponse {
running_inst_ids: Array<UUID>,
disabled_inst_ids: Array<UUID>,
}
export interface GenerateConfigRequest {
config: NetworkConfig;
}
export interface GenerateConfigResponse {
toml_config?: string;
error?: string;
}
export class ApiClient {
private client: AxiosInstance;
private authFailedCb: Function | undefined;
constructor(baseUrl: string, authFailedCb: Function | undefined = undefined) {
this.client = axios.create({
baseURL: baseUrl + '/api/v1',
withCredentials: true, // 如果需要支持跨域携带cookie
headers: {
'Content-Type': 'application/json',
},
});
this.authFailedCb = authFailedCb;
// 添加请求拦截器
this.client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
return config;
}, (error: any) => {
return Promise.reject(error);
});
// 添加响应拦截器
this.client.interceptors.response.use((response: AxiosResponse) => {
console.debug('Axios Response:', response);
return response.data; // 假设服务器返回的数据都在data属性中
}, (error: any) => {
if (error.response) {
let response: AxiosResponse = error.response;
if (response.status == 401 && this.authFailedCb) {
console.error('Unauthorized:', response.data);
this.authFailedCb();
} else {
// 请求已发出,但是服务器响应的状态码不在2xx范围
console.error('Response Error:', error.response.data);
}
} else if (error.request) {
// 请求已发出,但是没有收到响应
console.error('Request Error:', error.request);
} else {
// 发生了一些问题导致请求未发出
console.error('Error:', error.message);
}
return Promise.reject(error);
});
}
// 注册
public async register(data: RegisterData): Promise<RegisterResponse> {
try {
data.credentials.password = Md5.hashStr(data.credentials.password);
const response = await this.client.post<RegisterResponse>('/auth/register', data);
console.log("register response:", response);
return { success: true, message: 'Register success', };
} catch (error) {
if (error instanceof AxiosError) {
return { success: false, message: 'Failed to register, error: ' + JSON.stringify(error.response?.data), };
}
return { success: false, message: 'Unknown error, error: ' + error, };
}
}
// 登录
public async login(data: Credential): Promise<LoginResponse> {
try {
data.password = Md5.hashStr(data.password);
const response = await this.client.post<any>('/auth/login', data);
console.log("login response:", response);
return { success: true, message: 'Login success', };
} catch (error) {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
return { success: false, message: 'Invalid username or password', };
} else {
return { success: false, message: 'Unknown error, status code: ' + error.response?.status, };
}
}
return { success: false, message: 'Unknown error, error: ' + error, };
}
}
public async logout() {
await this.client.get('/auth/logout');
if (this.authFailedCb) {
this.authFailedCb();
}
}
public async change_password(new_password: string) {
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
}
public async check_login_status() {
try {
await this.client.get('/auth/check_login_status');
return true;
} catch (error) {
return false;
}
}
public async list_session() {
const response = await this.client.get('/sessions');
return response;
}
public async list_machines(): Promise<Array<any>> {
const response = await this.client.get<any, Record<string, Array<any>>>('/machines');
return response.machines;
}
public async list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
return response;
}
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
disabled: disabled,
});
}
public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
return response.info.map;
}
public async get_network_config(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/config/' + inst_id);
return response;
}
public async validate_config(machine_id: string, config: any): Promise<ValidateConfigResponse> {
const response = await this.client.post<any, ValidateConfigResponse>(`/machines/${machine_id}/validate-config`, {
config: config,
});
return response;
}
public async run_network(machine_id: string, config: any): Promise<undefined> {
await this.client.post<string>(`/machines/${machine_id}/networks`, {
config: config,
});
}
public async delete_network(machine_id: string, inst_id: string): Promise<undefined> {
await this.client.delete<string>(`/machines/${machine_id}/networks/${inst_id}`);
}
public async get_summary(): Promise<Summary> {
const response = await this.client.get<any, Summary>('/summary');
return response;
}
public captcha_url() {
return this.client.defaults.baseURL + '/auth/captcha';
}
public async generate_config(config: GenerateConfigRequest): Promise<GenerateConfigResponse> {
try {
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
}
export default ApiClient;
@@ -1,5 +1,8 @@
import type { Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import type { Locale } from 'vue-i18n'
import EnLocale from '../locales/en.yaml'
import CnLocale from '../locales/cn.yaml'
// Import i18n resources // Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import // https://vitejs.dev/guide/features.html#glob-import
@@ -10,10 +13,10 @@ export const i18n = createI18n({
messages: {}, messages: {},
}) })
const localesMap = Object.fromEntries( const localesMap = {
Object.entries(import.meta.glob('../../locales/*.yml')) "en": EnLocale,
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]), "cn": CnLocale,
) as Record<Locale, () => Promise<{ default: Record<string, string> }>> } as Record<string, any>
export const availableLocales = Object.keys(localesMap) export const availableLocales = Object.keys(localesMap)
@@ -38,13 +41,19 @@ export async function loadLanguageAsync(lang: string): Promise<Locale> {
let messages let messages
try { try {
messages = await localesMap[lang]() messages = localesMap[lang]
} }
catch { catch {
messages = await localesMap.en() messages = localesMap.en
} }
i18n.global.setLocaleMessage(lang, messages.default) i18n.global.setLocaleMessage(lang, messages)
loadedLanguages.push(lang) loadedLanguages.push(lang)
return setI18nLanguage(lang) return setI18nLanguage(lang)
} }
export default {
i18n,
localesMap,
loadLanguageAsync,
}
@@ -0,0 +1,108 @@
import { IPv4, IPv6 } from 'ip-num/IPNumber'
import { Ipv4Addr, Ipv4Inet, Ipv6Addr } from '../types/network'
export function ipv4ToString(ip: Ipv4Addr) {
return IPv4.fromNumber(ip.addr).toString()
}
export function ipv4InetToString(ip: Ipv4Inet | undefined) {
if (ip?.address === undefined) {
return 'undefined'
}
return `${ipv4ToString(ip.address)}/${ip.network_length}`
}
export function ipv6ToString(ip: Ipv6Addr) {
return IPv6.fromBigInt(
(BigInt(ip.part1) << BigInt(96))
+ (BigInt(ip.part2) << BigInt(64))
+ (BigInt(ip.part3) << BigInt(32))
+ BigInt(ip.part4),
)
}
function toHexString(uint64: bigint, padding = 9): string {
let hexString = uint64.toString(16);
while (hexString.length < padding) {
hexString = '0' + hexString;
}
return hexString;
}
function uint32ToUuid(part1: number, part2: number, part3: number, part4: number): string {
// 将两个 uint64 转换为 16 进制字符串
const part1Hex = toHexString(BigInt(part1), 8);
const part2Hex = toHexString(BigInt(part2), 8);
const part3Hex = toHexString(BigInt(part3), 8);
const part4Hex = toHexString(BigInt(part4), 8);
// 构造 UUID 格式字符串
const uuid = `${part1Hex.substring(0, 8)}-${part2Hex.substring(0, 4)}-${part2Hex.substring(4, 8)}-${part3Hex.substring(0, 4)}-${part3Hex.substring(4, 8)}${part4Hex.substring(0, 12)}`;
return uuid;
}
export interface UUID {
part1: number;
part2: number;
part3: number;
part4: number;
}
export function UuidToStr(uuid: UUID): string {
return uint32ToUuid(uuid.part1, uuid.part2, uuid.part3, uuid.part4);
}
export interface DeviceInfo {
hostname: string;
public_ip: string;
running_network_count: number;
report_time: string;
easytier_version: string;
running_network_instances?: Array<string>;
machine_id: string;
}
export function buildDeviceInfo(device: any): DeviceInfo {
let dev_info: DeviceInfo = {
hostname: device.info?.hostname,
public_ip: device.client_url,
running_network_instances: device.info?.running_network_instances.map((instance: any) => UuidToStr(instance)),
running_network_count: device.info?.running_network_instances.length,
report_time: device.info?.report_time,
easytier_version: device.info?.easytier_version,
machine_id: UuidToStr(device.info?.machine_id),
};
return dev_info;
}
// write a class to run a function periodically and can be stopped by calling stop(), use setTimeout to trigger the function
export class PeriodicTask {
private interval: number;
private task: (() => Promise<void>) | undefined;
private timer: any;
constructor(task: () => Promise<void>, interval: number) {
this.interval = interval;
this.task = task;
}
_runTaskHelper(nextInterval: number) {
this.timer = setTimeout(async () => {
if (this.task) {
await this.task();
this._runTaskHelper(this.interval);
}
}, nextInterval);
}
start() {
this._runTaskHelper(0);
}
stop() {
this.task = undefined;
clearTimeout(this.timer);
}
}
+54
View File
@@ -0,0 +1,54 @@
@import 'primeicons/primeicons.css';
@import 'floating-vue/dist/style.css';
.frontend-lib {
@layer tailwind-base, primevue, tailwind-utilities;
@layer tailwind-base {
@tailwind base;
}
@layer tailwind-utilities {
@tailwind components;
@tailwind utilities;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 12px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.card {
background: var(--surface-card);
padding: 2rem;
border-radius: 10px;
margin-bottom: 1rem;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
border-radius: 4px;
}
::-webkit-scrollbar-track {
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #0000005d;
}
}
@@ -1,9 +1,9 @@
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
export enum NetworkingMethod { export enum NetworkingMethod {
PublicServer = 'PublicServer', PublicServer = 0,
Manual = 'Manual', Manual = 1,
Standalone = 'Standalone', Standalone = 2,
} }
export interface NetworkConfig { export interface NetworkConfig {
@@ -11,6 +11,7 @@ export interface NetworkConfig {
dhcp: boolean dhcp: boolean
virtual_ipv4: string virtual_ipv4: string
network_length: number
hostname?: string hostname?: string
network_name: string network_name: string
network_secret: string network_secret: string
@@ -31,6 +32,16 @@ export interface NetworkConfig {
listener_urls: string[] listener_urls: string[]
rpc_port: number rpc_port: number
latency_first: boolean
dev_name: string
use_smoltcp?: boolean
enable_kcp_proxy?: boolean
disable_kcp_input?: boolean
disable_p2p?: boolean
bind_device?: boolean
no_tun?: boolean
} }
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
@@ -39,12 +50,13 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
dhcp: true, dhcp: true,
virtual_ipv4: '', virtual_ipv4: '',
network_length: 24,
network_name: 'easytier', network_name: 'easytier',
network_secret: '', network_secret: '',
networking_method: NetworkingMethod.PublicServer, networking_method: NetworkingMethod.PublicServer,
public_server_url: 'tcp://easytier.public.kkrainbow.top:11010', public_server_url: 'tcp://public.easytier.top:11010',
peer_urls: [], peer_urls: [],
proxy_cidrs: [], proxy_cidrs: [],
@@ -62,6 +74,15 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
'wg://0.0.0.0:11011', 'wg://0.0.0.0:11011',
], ],
rpc_port: 0, rpc_port: 0,
latency_first: false,
dev_name: '',
use_smoltcp: false,
enable_kcp_proxy: false,
disable_kcp_input: false,
disable_p2p: false,
bind_device: true,
no_tun: false,
} }
} }
@@ -75,9 +96,9 @@ export interface NetworkInstance {
} }
export interface NetworkInstanceRunningInfo { export interface NetworkInstanceRunningInfo {
dev_name: string
my_node_info: NodeInfo my_node_info: NodeInfo
events: Record<string, any> events: Array<string>,
node_info: NodeInfo
routes: Route[] routes: Route[]
peers: PeerInfo[] peers: PeerInfo[]
peer_route_pairs: PeerRoutePair[] peer_route_pairs: PeerRoutePair[]
@@ -85,13 +106,35 @@ export interface NetworkInstanceRunningInfo {
error_msg?: string error_msg?: string
} }
export interface Ipv4Addr {
addr: number
}
export interface Ipv4Inet {
address: Ipv4Addr
network_length: number
}
export interface Ipv6Addr {
part1: number
part2: number
part3: number
part4: number
}
export interface Url {
url: string
}
export interface NodeInfo { export interface NodeInfo {
virtual_ipv4: string virtual_ipv4: Ipv4Inet,
hostname: string
version: string
ips: { ips: {
public_ipv4: string public_ipv4: Ipv4Addr
interface_ipv4s: string[] interface_ipv4s: Ipv4Addr[]
public_ipv6: string public_ipv6: Ipv6Addr
interface_ipv6s: string[] interface_ipv6s: Ipv6Addr[]
listeners: { listeners: {
serialization: string serialization: string
scheme_end: number scheme_end: number
@@ -106,7 +149,7 @@ export interface NodeInfo {
}[] }[]
} }
stun_info: StunInfo stun_info: StunInfo
listeners: string[] listeners: Url[]
vpn_portal_cfg?: string vpn_portal_cfg?: string
} }
@@ -118,13 +161,14 @@ export interface StunInfo {
export interface Route { export interface Route {
peer_id: number peer_id: number
ipv4_addr: string ipv4_addr: Ipv4Inet | string | null
next_hop_peer_id: number next_hop_peer_id: number
cost: number cost: number
proxy_cidrs: string[] proxy_cidrs: string[]
hostname: string hostname: string
stun_info?: StunInfo stun_info?: StunInfo
inst_id: string inst_id: string
version: string
} }
export interface PeerInfo { export interface PeerInfo {
@@ -135,6 +179,7 @@ export interface PeerInfo {
export interface PeerConnInfo { export interface PeerConnInfo {
conn_id: string conn_id: string
my_peer_id: number my_peer_id: number
is_client: boolean
peer_id: number peer_id: number
features: string[] features: string[]
tunnel?: TunnelInfo tunnel?: TunnelInfo
@@ -160,3 +205,28 @@ export interface PeerConnStats {
tx_packets: number tx_packets: number
latency_us: number latency_us: number
} }
export enum EventType {
TunDeviceReady = 'TunDeviceReady', // string
TunDeviceError = 'TunDeviceError', // string
PeerAdded = 'PeerAdded', // number
PeerRemoved = 'PeerRemoved', // number
PeerConnAdded = 'PeerConnAdded', // PeerConnInfo
PeerConnRemoved = 'PeerConnRemoved', // PeerConnInfo
ListenerAdded = 'ListenerAdded', // any
ListenerAddFailed = 'ListenerAddFailed', // any, string
ListenerAcceptFailed = 'ListenerAcceptFailed', // any, string
ConnectionAccepted = 'ConnectionAccepted', // string, string
ConnectionError = 'ConnectionError', // string, string, string
Connecting = 'Connecting', // any
ConnectError = 'ConnectError', // string, string, string
VpnPortalClientConnected = 'VpnPortalClientConnected', // string, string
VpnPortalClientDisconnected = 'VpnPortalClientDisconnected', // string, string, string
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [require('tailwindcss-primeui')],
}
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"types": [
"@modyfi/vite-plugin-yaml/modules"
],
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+38
View File
@@ -0,0 +1,38 @@
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from "vite-plugin-dts"
import ViteYaml from '@modyfi/vite-plugin-yaml';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), dts({
tsconfigPath: './tsconfig.app.json',
}), ViteYaml()],
build: {
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, 'src/index.ts'),
name: 'easytier-frontend-lib',
// the proper extensions will be added
fileName: 'easytier-frontend-lib',
formats: ["es", "umd", "cjs"],
},
rollupOptions: {
input: {
main: resolve(__dirname, "src/easytier-frontend-lib.ts")
},
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue'],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
vue: 'Vue',
},
exports: "named"
},
},
},
})
+24
View File
@@ -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?
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript 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 the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/easytier.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EasyTier Dashboard</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
{
"name": "easytier-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@primevue/themes": "^4.2.1",
"aura": "link:@primevue/themes/aura",
"axios": "^1.7.7",
"easytier-frontend-lib": "workspace:*",
"primevue": "^4.2.1",
"tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.12",
"vue-router": "4"
},
"devDependencies": {
"@types/node": "^22.8.6",
"@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.14",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vite-plugin-singlefile": "^2.0.3",
"vue-tsc": "^2.1.10"
}
}

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