Compare commits

...

228 Commits

Author SHA1 Message Date
Sijie.Sun
a6773aa549 zstd should reuse ctx to avoid huge mmap cost (#941) 2025-06-06 08:59:06 +08:00
Sijie.Sun
0314c66635 some improvements (#939)
1. ospf route conn map should also use version
2. treat nopat as cone
2025-06-05 22:49:57 +08:00
chenxudong2020
3fb172b4d2 Modify SNI logic: always use "localhost" as SNI to avoid IP blocking (#934) 2025-06-05 11:56:07 +08:00
Sijie.Sun
96fc19b803 fix minor bugs (#936)
1. update upx to v5.0.1 to avoid mips bug.
2. use latest mimalloc.
3. fix panic in ospf route
4. potential residual conn.
2025-06-05 11:55:44 +08:00
Wang Zeng
9f7ba8ab8f fix(easytier-gui): restore window correctly when clicking tray icon while minimized (#930)
以前在最小化窗口时单击托盘图标会错误地切换任务栏图标的可见性。

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

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

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

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

musl.cc banned microsoft ips

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

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

---------

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

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

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

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

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

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

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

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

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

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

* docs: update installation script documentation

---------

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

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

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

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

---------

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

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

bump version to v2.1.2

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

* Optimize startup logic contro

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

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

* Support  service manager

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

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

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

the new panic info:

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

usage:

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

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

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

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

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

* fix gui:

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

* improve udp hole punch

1. always try cone punch for any nat type, tolerate fault stun type.
2. serializing all sym punch request, including server side.
2024-10-10 00:07:42 +08:00
Hs_Yeah
d9453589ac Fix panic when DNS resolution for STUN server returns only IPv6 addrs. (#402) 2024-10-09 22:40:01 +08:00
Sijie.Sun
e344372616 fix cone-to-cone punch (#401) 2024-10-09 22:39:06 +08:00
Sijie.Sun
63821e56bc fix udp buffer size, avoid packet loss (#399)
also bump version to 2.0.2
2024-10-08 22:01:15 +08:00
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
334 changed files with 50197 additions and 12098 deletions

View File

@@ -6,72 +6,84 @@ rustflags = ["-C", "linker-flavor=ld.lld"]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
linker = "aarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"]
[target.mipsel-unknown-linux-musl]
linker = "mipsel-linux-muslsf-gcc"
linker = "mipsel-unknown-linux-muslsf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/lib",
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/lib",
"-L",
"./musl_gcc/mipsel-linux-muslsf-cross/lib/gcc/mipsel-linux-muslsf/11.2.1",
"./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/sysroot/usr/lib",
"-L",
"./musl_gcc/mipsel-unknown-linux-muslsf/lib/gcc/mipsel-unknown-linux-muslsf/15.1.0",
"-l",
"atomic",
"-l",
"ctz",
"-l",
"gcc",
]
[target.mips-unknown-linux-musl]
linker = "mips-linux-muslsf-gcc"
linker = "mips-unknown-linux-muslsf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/mips-linux-muslsf-cross/mips-linux-muslsf/lib",
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/lib",
"-L",
"./musl_gcc/mips-linux-muslsf-cross/lib/gcc/mips-linux-muslsf/11.2.1",
"./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/sysroot/usr/lib",
"-L",
"./musl_gcc/mips-unknown-linux-muslsf/lib/gcc/mips-unknown-linux-muslsf/15.1.0",
"-l",
"atomic",
"-l",
"ctz",
"-l",
"gcc",
]
[target.armv7-unknown-linux-musleabihf]
linker = "armv7l-linux-musleabihf-gcc"
linker = "armv7-unknown-linux-musleabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabi]
linker = "armv7m-linux-musleabi-gcc"
linker = "armv7-unknown-linux-musleabi-gcc"
rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabihf]
linker = "arm-linux-musleabihf-gcc"
linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-linux-musleabihf-cross/arm-linux-musleabihf/lib",
"./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
"-L",
"./musl_gcc/arm-linux-musleabihf-cross/lib/gcc/arm-linux-musleabihf/11.2.1",
"./musl_gcc/arm-unknown-linux-musleabihf/lib/gcc/arm-unknown-linux-musleabihf/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]
[target.arm-unknown-linux-musleabi]
linker = "arm-linux-musleabi-gcc"
linker = "arm-unknown-linux-musleabi-gcc"
rustflags = [
"-C",
"target-feature=+crt-static",
"-L",
"./musl_gcc/arm-linux-musleabi-cross/arm-linux-musleabi/lib",
"./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
"-L",
"./musl_gcc/arm-linux-musleabi-cross/lib/gcc/arm-linux-musleabi/11.2.1",
"./musl_gcc/arm-unknown-linux-musleabi/lib/gcc/arm-unknown-linux-musleabi/15.1.0",
"-l",
"atomic",
"-l",
"gcc",
]

53
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
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.

View File

@@ -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.

View File

@@ -18,9 +18,13 @@ RUN mkdir -p /tmp/output; \
FROM alpine:latest
RUN apk add --no-cache tzdata tini
WORKDIR /app
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
# users can use "-e TZ=xxx" to adjust it
ENV TZ Asia/Shanghai
# tcp
EXPOSE 11010/tcp
# udp
@@ -32,4 +36,4 @@ EXPOSE 11011/tcp
# wss
EXPOSE 11012/tcp
ENTRYPOINT ["easytier-core"]
ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]

View File

@@ -2,7 +2,7 @@ name: EasyTier Core
on:
push:
branches: ["develop", "main"]
branches: ["develop", "main", "releases/**"]
pull_request:
branches: ["develop", "main"]
@@ -20,43 +20,86 @@ jobs:
runs-on: ubuntu-latest
# Map a step output to a job output
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:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
build_web:
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r --filter "./easytier-web/*" build
- name: Archive artifact
uses: actions/upload-artifact@v4
with:
name: easytier-web-dashboard
path: |
easytier-web/frontend/dist/*
build:
strategy:
fail-fast: false
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-x86_64
- TARGET: mips-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mips
- TARGET: mipsel-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-mipsel
- TARGET: armv7-unknown-linux-musleabihf # raspberry pi 2-3-4, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7hf
- TARGET: armv7-unknown-linux-musleabi # raspberry pi 2-3-4, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armv7
- TARGET: arm-unknown-linux-musleabihf # raspberry pi 0-1, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-armhf
- TARGET: arm-unknown-linux-musleabi # raspberry pi 0-1, not tested
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: linux-arm
- TARGET: x86_64-apple-darwin
@@ -69,6 +112,17 @@ jobs:
- TARGET: x86_64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-i686
- TARGET: x86_64-unknown-freebsd
OS: ubuntu-22.04
ARTIFACT_NAME: freebsd-13.2-x86_64
BSD_VERSION: 13.2
runs-on: ${{ matrix.OS }}
env:
@@ -76,16 +130,25 @@ jobs:
TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }}
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
needs: pre_job
needs:
- pre_job
- build_web
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Download web artifact
uses: actions/download-artifact@v4
with:
node-version: 21
name: easytier-web-dashboard
path: easytier-web/frontend/dist/
- name: Cargo cache
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
uses: actions/cache@v4
with:
path: |
@@ -93,9 +156,6 @@ jobs:
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
@@ -103,28 +163,79 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Core & Cli
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
run: |
bash ./.github/workflows/install_rust.sh
# we set the sysroot when sysroot is a dir
# this dir is a soft link generated by install_rust.sh
# kcp-sys need this to gen ffi bindings. without this clang may fail to find some libc headers such as bits/libc-header-start.h
if [[ -d "./musl_gcc/sysroot" ]]; then
export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$(readlink -f ./musl_gcc/sysroot)
fi
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
else
if [[ $OS =~ ^windows.*$ ]]; then
SUFFIX=.exe
fi
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX"
cargo build --release --verbose --target $TARGET
fi
- name: Install UPX
if: ${{ matrix.OS != 'macos-latest' }}
uses: crazy-max/ghaction-upx@v3
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
- name: Build Core & Cli (X86_64 FreeBSD)
uses: vmactions/freebsd-vm@v1
if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
env:
TARGET: ${{ matrix.TARGET }}
with:
version: latest
install-only: true
envs: TARGET
release: ${{ matrix.BSD_VERSION }}
arch: x86_64
usesh: true
mem: 6144
cpu: 4
run: |
uname -a
echo $SHELL
pwd
ls -lah
whoami
env | sort
pkg install -y git protobuf llvm-devel sudo curl
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. $HOME/.cargo/env
rustup set auto-self-update disable
rustup install 1.86
rustup default 1.86
export CC=clang
export CXX=clang++
export CARGO_TERM_COLOR=always
cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed
mv ./target/$TARGET/release/easytier-web ./target/$TARGET/release/easytier-web-embed
cargo build --release --verbose --target $TARGET
- name: Compress
run: |
mkdir -p ./artifacts/objects/
# 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
cp easytier/third_party/Packet.dll ./artifacts/objects/
cp easytier/third_party/wintun.dll ./artifacts/objects/
cp easytier/third_party/*.dll ./artifacts/objects/
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^i686.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/i686/*.dll ./artifacts/objects/
elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
SUFFIX=.exe
cp easytier/third_party/arm64/*.dll ./artifacts/objects/
fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME
@@ -132,13 +243,20 @@ jobs:
TAG=$GITHUB_SHA
fi
if [[ $OS =~ ^ubuntu.*$ ]]; then
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
UPX_VERSION=5.0.1
curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf -
cp upx-${UPX_VERSION}-amd64_linux/upx .
./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX"
./upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX"
fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
fi
mv ./artifacts/objects/* ./artifacts/
rm -rf ./artifacts/objects/
@@ -150,25 +268,52 @@ jobs:
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/
no-delete-remote-files: true
retry: 5
core-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
needs:
- pre_job
- build_web
- build
steps:
- name: Mark result as failed
if: needs.build.result != 'success'
run: exit 1
magisk_build:
needs:
- pre_job
- build_web
- build
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4 # 必须先检出代码才能获取模块配置
# 下载二进制文件到独立目录
- name: Download Linux aarch64 binaries
uses: actions/download-artifact@v4
with:
name: easytier-linux-aarch64
path: ./downloaded-binaries/ # 独立目录避免冲突
# 将二进制文件复制到 Magisk 模块目录
- name: Prepare binaries
run: |
mkdir -p ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-core ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-cli ./easytier-contrib/easytier-magisk/
cp ./downloaded-binaries/easytier-web ./easytier-contrib/easytier-magisk/
# 上传生成的模块
- name: Upload Magisk Module
uses: actions/upload-artifact@v4
with:
name: Easytier-Magisk
path: |
./easytier-contrib/easytier-magisk
!./easytier-contrib/easytier-magisk/build.sh
!./easytier-contrib/easytier-magisk/magisk_update.json
if-no-files-found: error

View File

@@ -11,7 +11,7 @@ on:
image_tag:
description: 'Tag for this image build'
type: string
default: 'v1.2.0'
default: 'v2.3.1'
required: true
mark_latest:
description: 'Mark this image as latest'
@@ -39,6 +39,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: login github container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download artifact
id: download-artifact
uses: dawidd6/action-download-artifact@v6
@@ -58,4 +64,6 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
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/easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},

View File

@@ -2,7 +2,7 @@ name: EasyTier GUI
on:
push:
branches: ["develop", "main"]
branches: ["develop", "main", "releases/**"]
pull_request:
branches: ["develop", "main"]
@@ -20,14 +20,15 @@ jobs:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
concurrent_skipping: 'same_content_newer'
skip_after_successful_duplicate: 'true'
cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", "easytier-gui/**", ".github/workflows/gui.yml", ".github/workflows/install_rust.sh"]'
build-gui:
strategy:
@@ -35,11 +36,11 @@ jobs:
matrix:
include:
- TARGET: aarch64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
GUI_TARGET: aarch64-unknown-linux-gnu
ARTIFACT_NAME: linux-aarch64
- TARGET: x86_64-unknown-linux-musl
OS: ubuntu-latest
OS: ubuntu-22.04
GUI_TARGET: x86_64-unknown-linux-gnu
ARTIFACT_NAME: linux-x86_64
@@ -57,6 +58,16 @@ jobs:
GUI_TARGET: x86_64-pc-windows-msvc
ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc
OS: windows-latest
GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
GUI_TARGET: i686-pc-windows-msvc
ARTIFACT_NAME: windows-i686
runs-on: ${{ matrix.OS }}
env:
NAME: easytier
@@ -67,52 +78,20 @@ jobs:
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
(cd easytier-gui; pnpm install)
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
sudo apt update
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
@@ -141,20 +120,83 @@ jobs:
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt-get update && sudo apt-get upgrade -y
sudo apt install gcc-aarch64-linux-gnu
sudo apt install libwebkit2gtk-4.1-dev:arm64
sudo apt install libssl-dev:arm64
sudo apt update
sudo apt install aptitude
sudo aptitude install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64 \
libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64 \
libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- uses: actions/checkout@v3
- name: Set current ref as env variable
run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- uses: actions/setup-node@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r build
- name: Cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo
./target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install rust target
run: bash ./.github/workflows/install_rust.sh
- name: Setup protoc
uses: arduino/setup-protoc@v2
with:
# GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: copy correct DLLs
if: ${{ matrix.OS == 'windows-latest' }}
run: |
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/
elif [[ $GUI_TARGET =~ ^i686.*$ ]]; then
cp ./easytier/third_party/i686/*.dll ./easytier-gui/src-tauri/
else
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
fi
- name: Build GUI
if: ${{ matrix.GUI_TARGET != '' }}
uses: tauri-apps/tauri-action@v0
with:
projectPath: ./easytier-gui
# https://tauri.app/v1/guides/building/linux/#cross-compiling-tauri-applications-for-arm-based-devices
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-latest' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
args: --verbose --target ${{ matrix.GUI_TARGET }} ${{ matrix.OS == 'ubuntu-22.04' && contains(matrix.TARGET, 'aarch64') && '--bundles deb' || '' }}
- name: Compress
run: |
@@ -188,18 +230,6 @@ jobs:
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/gui
no-delete-remote-files: true
retry: 5
gui-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest

View File

@@ -8,61 +8,33 @@
# dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev
# for easytier-gui
if [[ $GUI_TARGET != '' && $GUI_TARGET =~ ^x86_64.*$ ]]; then
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
# https://github.com/cross-tools/musl-cross/releases
# if "musl" is a substring of TARGET, we assume that we are using musl
MUSL_TARGET=$TARGET
# if target is mips or mipsel, we should use soft-float version of musl
if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
MUSL_TARGET=${TARGET}sf
fi
# curl -s musl.cc | grep mipsel
case $TARGET in
mipsel-unknown-linux-musl)
MUSL_URI=mipsel-linux-muslsf
;;
mips-unknown-linux-musl)
MUSL_URI=mips-linux-muslsf
;;
aarch64-unknown-linux-musl)
MUSL_URI=aarch64-linux-musl
;;
armv7-unknown-linux-musleabihf)
MUSL_URI=armv7l-linux-musleabihf
;;
armv7-unknown-linux-musleabi)
MUSL_URI=armv7m-linux-musleabi
;;
arm-unknown-linux-musleabihf)
MUSL_URI=arm-linux-musleabihf
;;
arm-unknown-linux-musleabi)
MUSL_URI=arm-linux-musleabi
;;
esac
if [ -n "$MUSL_URI" ]; then
if [[ $MUSL_TARGET =~ musl ]]; then
mkdir -p ./musl_gcc
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/
wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/bin/*gcc /usr/bin/
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/include/ /usr/include/musl-cross
sudo ln -sf $(pwd)/musl_gcc/${MUSL_TARGET}/${MUSL_TARGET}/sysroot/ ./musl_gcc/sysroot
sudo chmod -R a+rwx ./musl_gcc
fi
fi
# see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable
rustup install 1.77
rustup default 1.77
rustup install 1.86
rustup default 1.86
# mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cd "$PWD/musl_gcc/${MUSL_URI}-cross/lib/gcc/${MUSL_URI}/11.2.1" || exit 255
cd "$PWD/musl_gcc/${MUSL_TARGET}/lib/gcc/${MUSL_TARGET}/15.1.0" || exit 255
# for panic-abort
cp libgcc_eh.a libunwind.a

View File

@@ -2,7 +2,7 @@ name: EasyTier Mobile
on:
push:
branches: ["develop", "main"]
branches: ["develop", "main", "releases/**"]
pull_request:
branches: ["develop", "main"]
@@ -20,14 +20,15 @@ jobs:
runs-on: ubuntu-latest
# Map a step output to a job output
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
should_skip: ${{ steps.skip_check.outputs.should_skip == 'true' && !startsWith(github.ref_name, 'releases/') }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
# All of these options are optional, so you can remove them if you are happy with the defaults
concurrent_skipping: 'never'
concurrent_skipping: 'same_content_newer'
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"]'
build-mobile:
strategy:
@@ -35,7 +36,7 @@ jobs:
matrix:
include:
- TARGET: android
OS: ubuntu-latest
OS: ubuntu-22.04
ARTIFACT_NAME: android
runs-on: ${{ matrix.OS }}
env:
@@ -48,6 +49,10 @@ jobs:
steps:
- 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
with:
distribution: 'oracle'
@@ -90,8 +95,8 @@ jobs:
- name: Install frontend dependencies
run: |
(cd easytier-gui; pnpm install)
(cd tauri-plugin-vpnservice; pnpm install; pnpm build)
pnpm -r install
pnpm -r build
- name: Cargo cache
uses: actions/cache@v4
@@ -141,18 +146,6 @@ jobs:
path: |
./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{ github.sha }}/mobile
no-delete-remote-files: true
retry: 5
mobile-result:
if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest

View File

@@ -19,9 +19,9 @@ on:
default: 10322498555
required: true
version:
description: 'version for this release'
description: 'Version for this release'
type: string
default: 'v1.2.2'
default: 'v2.3.1'
required: true
make_latest:
description: 'Mark this release as latest'
@@ -55,27 +55,37 @@ jobs:
github_token: ${{secrets.GITHUB_TOKEN}}
run_id: ${{ inputs.gui_run_id }}
repo: EasyTier/EasyTier
path: release_assets
path: release_assets_nozip
- name: Download GUI Artifact
- name: Download Mobile 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
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 .
mkdir ../zipped_assets
for x in `ls`; do
zip ../zipped_assets/$x-${VERSION}.zip $x/*;
if [ "$x" = "Easytier-Magisk" ]; then
# for Easytier-Magisk, make sure files are in the root of the zip
cd $x;
zip -r ../../zipped_assets/$x-${VERSION}.zip .;
cd ..;
else
zip -r ../zipped_assets/$x-${VERSION}.zip $x;
fi
done
- name: Release

View File

@@ -30,7 +30,7 @@ jobs:
skip_after_successful_duplicate: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/test.yml"]'
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
steps:
@@ -47,11 +47,40 @@ jobs:
- name: Setup system for test
run: |
sudo modprobe br_netfilter
sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
sudo ip addr add 2001:db8::2/64 dev lo
- uses: actions/setup-node@v4
with:
node-version: 21
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install frontend dependencies
run: |
pnpm -r install
pnpm -r --filter "./easytier-web/*" build
- name: Cargo cache
uses: actions/cache@v4
with:
@@ -62,6 +91,6 @@ jobs:
- name: Run tests
run: |
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose
sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture
sudo chown -R $USER:$USER ./target
sudo chown -R $USER:$USER ~/.cargo

8
.gitignore vendored
View File

@@ -11,6 +11,7 @@ target-*/
*.pdb
.vscode
/.idea
# perf & flamegraph
perf.data
@@ -29,3 +30,10 @@ musl_gcc
# log
easytier-panic.log
# web
node_modules
.vite
easytier-gui/src-tauri/*.dll

4846
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,13 @@
[workspace]
resolver = "2"
members = ["easytier", "easytier-gui/src-tauri"]
default-members = ["easytier"]
members = [
"easytier",
"easytier-gui/src-tauri",
"easytier-rpc-build",
"easytier-web",
"easytier-contrib/easytier-ffi",
]
default-members = ["easytier", "easytier-web"]
[profile.dev]
panic = "unwind"
@@ -10,4 +16,5 @@ panic = "unwind"
panic = "abort"
lto = true
codegen-units = 1
opt-level = 3
strip = true

View File

@@ -4,84 +4,27 @@
"path": "."
},
{
"name": "gui",
"path": "easytier-gui"
},
{
"name": "core",
"path": "easytier"
},
{
"name": "vpnservice",
"path": "tauri-plugin-vpnservice"
},
{
"name": "rpc-build",
"path": "easytier-rpc-build"
}
],
"settings": {
"eslint.experimental.useFlatConfig": true,
"i18n-ally.sourceLanguage": "cn",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
// Disable the default formatter
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
"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"
]
}
}

414
README.md
View File

@@ -1,28 +1,31 @@
# EasyTier
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
# EasyTier
[![Github release](https://img.shields.io/github/v/tag/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/releases)
[![GitHub](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[![GitHub Test Actions](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/test.yml)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/EasyTier/EasyTier)
[简体中文](/README_CN.md) | [English](/README.md)
**Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.**
**Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.**
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.
<p align="center">
<img src="assets/image-5.png" width="300">
<img src="assets/image-4.png" width="300">
</p>
## Features
## Features
- **Decentralized**: No need to rely on centralized services, nodes are equal and independent.
- **Safe**: Use WireGuard protocol to encrypt data.
- **High Performance**: Full-link zero-copy, with performance comparable to mainstream networking software.
- **Cross-platform**: Supports MacOS/Linux/Windows, 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)
- **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.
@@ -31,171 +34,226 @@
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
- **IPv6 Support**: Supports networking using IPv6.
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
## 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
cargo install easytier
```
3. **Install from source code**
3. **Install from source code**
```sh
cargo install --git https://github.com/EasyTier/EasyTier.git
cargo install --git https://github.com/EasyTier/EasyTier.git easytier
```
4. **Install by Docker Compose**
4. **Install by Docker Compose**
Please visit the [EasyTier Official Website](https://www.easytier.top/en/) to view the full documentation.
Please visit the [EasyTier Official Website](https://easytier.cn/en/) to view the full documentation.
5. **Install by script (For Linux Only)**
5. **Install by script (For Linux Only)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
You can also uninstall/update Easytier by the command "uninstall" or "update" of this script
## Quick Start
The script supports the following commands and options:
Commands:
- `install`: Install EasyTier
- `uninstall`: Uninstall EasyTier
- `update`: Update EasyTier to the latest version
- `help`: Show help message
Options:
- `--skip-folder-verify`: Skip folder verification during installation
- `--skip-folder-fix`: Skip automatic folder path fixing
- `--no-gh-proxy`: Disable GitHub proxy
- `--gh-proxy`: Set custom GitHub proxy URL (default: https://ghfast.top/)
Examples:
```sh
# Show help
bash /tmp/easytier.sh help
# Install with options
bash /tmp/easytier.sh install --skip-folder-verify
bash /tmp/easytier.sh install --no-gh-proxy
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
# Update EasyTier
bash /tmp/easytier.sh update
# Uninstall EasyTier
bash /tmp/easytier.sh uninstall
```
6. **Install by Homebrew (For MacOS Only)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
## 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:
> 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
sudo easytier-core --ipv4 10.144.144.1
```
Successful execution of the command will print the following.
![alt text](/assets/image-2.png)
2. Execute on Node B
2. Execute on Node B
```sh
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
```sh
ping 10.144.144.2
```
Use easytier-cli to view node information in the subnet
```sh
easytier-cli peer
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
### Multi-node Networking
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
```
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
---
### Subnet Proxy (Point-to-Network) Configuration
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
```mermaid
flowchart LR
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
end
subgraph Node B
nodeb[EasyTier\n10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
```
Then the startup parameters for Node B's easytier are (new -n parameter)
```sh
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.
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
### Multi-node Networking
Based on the two-node networking example just now, if more nodes need to join the virtual network, you can use the following command.
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
The `--peers` parameter can fill in the listening address of any node already in the virtual network.
---
### Subnet Proxy (Point-to-Network) Configuration
Assuming the network topology is as follows, Node B wants to share its accessible subnet 10.1.1.0/24 with other nodes.
```mermaid
flowchart LR
subgraph Node A IP 22.1.1.1
nodea[EasyTier\n10.144.144.1]
end
subgraph Node B
nodeb[EasyTier\n10.144.144.2]
end
id1[[10.1.1.0/24]]
nodea <--> nodeb <-.-> id1
```
Then the startup parameters for Node B's easytier are (new -n parameter)
```sh
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.
1. Check whether the routing information has been synchronized, the proxy_cidrs column shows the proxied subnets.
```sh
easytier-cli route
```
![alt text](/assets/image-3.png)
![alt text](/assets/image-3.png)
2. Test whether Node A can access nodes under the proxied subnet
```sh
ping 10.1.1.2
```
---
### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://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:
@@ -221,14 +279,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.
```
```sh
# The following parameters mean: listen on port 0.0.0.0:11013, and use the 10.14.14.0/24 subnet for WireGuard
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
After successfully starting easytier-core, use easytier-cli to obtain the WireGuard client configuration.
```
```sh
$> easytier-cli vpn-portal
portal_name: wireguard
@@ -252,45 +310,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.
# 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.
### 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
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.
# 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-9.png" width="300">
<img src="assets/image-9.png" width="300">

View File

@@ -8,7 +8,7 @@
[简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。**
**请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
@@ -22,7 +22,7 @@
- **去中心化**:无需依赖中心化服务,节点平等且独立。
- **安全**:支持利用 WireGuard 加密通信,也支持 AES-GCM 加密保护中转流量。
- **高性能**:全链路零拷贝,性能与主流组网软件相当。
- **跨平台**:支持 MacOS/Linux/Windows未来将支持 IOS 和 Android。可执行文件静态链接,部署简单。
- **跨平台**:支持 MacOS/Linux/Windows/Android,未来将支持 IOS。可执行文件静态链接部署简单。
- **无公网 IP 组网**:支持利用共享的公网节点组网,可参考 [配置指南](#无公网IP组网)
- **NAT 穿透**:支持基于 UDP 的 NAT 穿透,即使在复杂的网络环境下也能建立稳定的连接。
- **子网代理(点对网)**:节点可以将可访问的网段作为代理暴露给 VPN 子网,允许其他节点通过该节点访问这些子网。
@@ -31,6 +31,7 @@
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
## 安装
@@ -39,27 +40,64 @@
访问 [GitHub Release 页面](https://github.com/EasyTier/EasyTier/releases) 下载适用于您操作系统的二进制文件。Release 压缩包中同时包含命令行程序和图形界面程序。
2. **通过 crates.io 安装**
```sh
cargo install easytier
```
```sh
cargo install easytier
```
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.top/) 以查看完整的文档。
请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)**
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/easytier.sh" && bash /tmp/easytier.sh install
```
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级
```sh
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.sh" && bash /tmp/easytier.sh install
```
脚本支持以下命令和选项:
命令:
- `install`: 安装 EasyTier
- `uninstall`: 卸载 EasyTier
- `update`: 更新 EasyTier 到最新版本
- `help`: 显示帮助信息
选项:
- `--skip-folder-verify`: 跳过安装过程中的文件夹验证
- `--skip-folder-fix`: 跳过自动修复文件夹路径
- `--no-gh-proxy`: 禁用 GitHub 代理
- `--gh-proxy`: 设置自定义 GitHub 代理 URL (默认值: https://ghfast.top/)
示例:
```sh
# 查看帮助
bash /tmp/easytier.sh help
# 安装(带选项)
bash /tmp/easytier.sh install --skip-folder-verify
bash /tmp/easytier.sh install --no-gh-proxy
bash /tmp/easytier.sh install --gh-proxy https://your-proxy.com/
# 更新 EasyTier
bash /tmp/easytier.sh update
# 卸载 EasyTier
bash /tmp/easytier.sh uninstall
```
6. **使用 Homebrew 安装 (仅适用于 MacOS)**
```sh
brew tap brewforge/chinese
brew install --cask easytier
```
## 快速开始
@@ -87,34 +125,48 @@ nodea <-----> nodeb
```
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 执行
```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. 测试联通性
两个节点应成功连接并能够在虚拟子网内通信
```sh
ping 10.144.144.2
```
两个节点应成功连接并能够在虚拟子网内通信
使用 easytier-cli 查看子网中的节点信息
```sh
easytier-cli peer
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
ping 10.144.144.2
```
使用 easytier-cli 查看子网中的节点信息
```sh
easytier-cli peer
```
![alt text](/assets/image.png)
```sh
easytier-cli route
```
![alt text](/assets/image-1.png)
```sh
easytier-cli node
```
![alt text](assets/image-10.png)
---
@@ -122,11 +174,11 @@ nodea <-----> nodeb
基于刚才的双节点组网例子,如果有更多的节点需要加入虚拟网络,可以使用如下命令。
```
```sh
sudo easytier-core --ipv4 10.144.144.2 --peers udp://22.1.1.1:11010
```
其中 `--peers ` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
其中 `--peers` 参数可以填写任意一个已经在虚拟网络中的节点的监听地址。
---
@@ -161,35 +213,36 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
1. 检查路由信息是否已经同步proxy_cidrs 列展示了被代理的子网。
```sh
easytier-cli route
```
![alt text](/assets/image-3.png)
```sh
easytier-cli route
```
![alt text](/assets/image-3.png)
2. 测试节点 A 是否可访问被代理子网下的节点
```sh
ping 10.1.1.2
```
```sh
ping 10.1.1.2
```
---
### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://easytier.public.kkrainbow.top:11010``。
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行:
```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -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 执行
```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。
@@ -224,14 +277,14 @@ ios <-.-> nodea <--> nodeb <-.-> id1
在节点 A 的 easytier-core 命令中,加入 --vpn-portal 参数,指定 WireGuard 服务监听的端口,以及 WireGuard 网络使用的网段。
```
```sh
# 以下参数的含义为: 监听 0.0.0.0:11013 端口WireGuard 使用 10.14.14.0/24 网段
sudo easytier-core --ipv4 10.144.144.1 --vpn-portal wg://0.0.0.0:11013/10.14.14.0/24
```
easytier-core 启动成功后,使用 easytier-cli 获取 WireGuard Client 的配置。
```
```sh
$> easytier-cli vpn-portal
portal_name: wireguard
@@ -259,43 +312,49 @@ connected_clients:
### 自建公共中转服务器
每个节点都可作为其他用户网络的中转节点。不带任何参数直接启动 EasyTier 即可
每个虚拟网络(通过相同的网络名称和密钥建链)都可以充当公共服务器集群。其他网络的节点可以连接到公共服务器集群中的任意节点,无需公共 IP 即可发现彼此
运行自建的公共服务器集群与运行虚拟网络完全相同,不过可以跳过配置 ipv4 地址。
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
```
### 其他配置
可使用 ``easytier-core --help`` 查看全部配置项
# 路线图
## 路线图
- [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞等特性。
- [ ] 支持 Android、IOS 等移动平台
- [ ] 支持 Web 配置管理。
- [ ] 支持 TCP 打洞、KCP、FEC 等特性。
- [ ] 支持 iOS
# 社区和贡献
## 社区和贡献
我们欢迎并鼓励社区贡献!如果你想参与进来,请提交 [GitHub PR](https://github.com/EasyTier/EasyTier/pulls)。详细的贡献指南可以在 [CONTRIBUTING.md](https://github.com/EasyTier/EasyTier/blob/main/CONTRIBUTING.md) 中找到。
# 相关项目和资源
## 相关项目和资源
- [ZeroTier](https://www.zerotier.com/): 一个全球虚拟网络,用于连接设备。
- [TailScale](https://tailscale.com/): 一个旨在简化网络配置的 VPN 解决方案。
- [vpncloud](https://github.com/dswd/vpncloud): 一个 P2P Mesh VPN
- [Candy](https://github.com/lanthora/candy): 可靠、低延迟、抗审查的虚拟专用网络
# 许可证
## 许可证
EasyTier 根据 [Apache License 2.0](https://github.com/EasyTier/EasyTier/blob/main/LICENSE) 许可证发布。
# 联系方式
## 联系方式
- 提问或报告问题:[GitHub Issues](https://github.com/EasyTier/EasyTier/issues)
- 讨论和交流:[GitHub Discussions](https://github.com/EasyTier/EasyTier/discussions)
- QQ 群: 949700262
- Telegramhttps://t.me/easytier
# 赞助
## 赞助
<img src="assets/image-8.png" width="300">
<img src="assets/image-9.png" width="300">

BIN
assets/image-10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,16 @@
[package]
name = "easytier-ffi"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
easytier = { path = "../../easytier" }
once_cell = "1.18.0"
dashmap = "6.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"

View File

@@ -0,0 +1,159 @@
public class EasyTierFFI
{
// 导入 DLL 函数
private const string DllName = "easytier_ffi.dll";
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int parse_config([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int run_network_instance([MarshalAs(UnmanagedType.LPStr)] string cfgStr);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int retain_network_instance(IntPtr instNames, int length);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern int collect_network_infos(IntPtr infos, int maxLength);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern void get_error_msg(out IntPtr errorMsg);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
private static extern void free_string(IntPtr str);
// 定义 KeyValuePair 结构体
[StructLayout(LayoutKind.Sequential)]
public struct KeyValuePair
{
public IntPtr Key;
public IntPtr Value;
}
// 解析配置
public static void ParseConfig(string config)
{
if (string.IsNullOrEmpty(config))
{
throw new ArgumentException("Configuration string cannot be null or empty.");
}
int result = parse_config(config);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
// 启动网络实例
public static void RunNetworkInstance(string config)
{
if (string.IsNullOrEmpty(config))
{
throw new ArgumentException("Configuration string cannot be null or empty.");
}
int result = run_network_instance(config);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
// 保留网络实例
public static void RetainNetworkInstances(string[] instanceNames)
{
IntPtr[] namePointers = null;
IntPtr namesPtr = IntPtr.Zero;
try
{
if (instanceNames != null && instanceNames.Length > 0)
{
namePointers = new IntPtr[instanceNames.Length];
for (int i = 0; i < instanceNames.Length; i++)
{
if (string.IsNullOrEmpty(instanceNames[i]))
{
throw new ArgumentException("Instance name cannot be null or empty.");
}
namePointers[i] = Marshal.StringToHGlobalAnsi(instanceNames[i]);
}
namesPtr = Marshal.AllocHGlobal(Marshal.SizeOf<IntPtr>() * namePointers.Length);
Marshal.Copy(namePointers, 0, namesPtr, namePointers.Length);
}
int result = retain_network_instance(namesPtr, instanceNames?.Length ?? 0);
if (result < 0)
{
throw new Exception(GetErrorMessage());
}
}
finally
{
if (namePointers != null)
{
foreach (var ptr in namePointers)
{
if (ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(ptr);
}
}
}
if (namesPtr != IntPtr.Zero)
{
Marshal.FreeHGlobal(namesPtr);
}
}
}
// 收集网络信息
public static KeyValuePair<string, string>[] CollectNetworkInfos(int maxLength)
{
IntPtr buffer = Marshal.AllocHGlobal(Marshal.SizeOf<KeyValuePair>() * maxLength);
try
{
int count = collect_network_infos(buffer, maxLength);
if (count < 0)
{
throw new Exception(GetErrorMessage());
}
var result = new KeyValuePair<string, string>[count];
for (int i = 0; i < count; i++)
{
var kv = Marshal.PtrToStructure<KeyValuePair>(buffer + i * Marshal.SizeOf<KeyValuePair>());
string key = Marshal.PtrToStringAnsi(kv.Key);
string value = Marshal.PtrToStringAnsi(kv.Value);
// 释放由 FFI 分配的字符串内存
free_string(kv.Key);
free_string(kv.Value);
result[i] = new KeyValuePair<string, string>(key, value);
}
return result;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
// 获取错误信息
private static string GetErrorMessage()
{
get_error_msg(out IntPtr errorMsgPtr);
if (errorMsgPtr == IntPtr.Zero)
{
return "Unknown error";
}
string errorMsg = Marshal.PtrToStringAnsi(errorMsgPtr);
free_string(errorMsgPtr); // 释放错误信息字符串
return errorMsg;
}
}

View File

@@ -0,0 +1,199 @@
use std::sync::Mutex;
use dashmap::DashMap;
use easytier::{
common::config::{ConfigLoader as _, TomlConfigLoader},
launcher::NetworkInstance,
};
static INSTANCE_MAP: once_cell::sync::Lazy<DashMap<String, NetworkInstance>> =
once_cell::sync::Lazy::new(DashMap::new);
static ERROR_MSG: once_cell::sync::Lazy<Mutex<Vec<u8>>> =
once_cell::sync::Lazy::new(|| Mutex::new(Vec::new()));
#[repr(C)]
pub struct KeyValuePair {
pub key: *const std::ffi::c_char,
pub value: *const std::ffi::c_char,
}
fn set_error_msg(msg: &str) {
let bytes = msg.as_bytes();
let mut msg_buf = ERROR_MSG.lock().unwrap();
let len = bytes.len();
msg_buf.resize(len, 0);
msg_buf[..len].copy_from_slice(bytes);
}
#[no_mangle]
pub extern "C" fn get_error_msg(out: *mut *const std::ffi::c_char) {
let msg_buf = ERROR_MSG.lock().unwrap();
if msg_buf.is_empty() {
unsafe {
*out = std::ptr::null();
}
return;
}
let cstr = std::ffi::CString::new(&msg_buf[..]).unwrap();
unsafe {
*out = cstr.into_raw();
}
}
#[no_mangle]
pub extern "C" fn free_string(s: *const std::ffi::c_char) {
if s.is_null() {
return;
}
unsafe {
let _ = std::ffi::CString::from_raw(s as *mut std::ffi::c_char);
}
}
#[no_mangle]
pub extern "C" fn parse_config(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
if let Err(e) = TomlConfigLoader::new_from_str(&cfg_str) {
set_error_msg(&format!("failed to parse config: {:?}", e));
return -1;
}
0
}
#[no_mangle]
pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std::ffi::c_int {
let cfg_str = unsafe {
assert!(!cfg_str.is_null());
std::ffi::CStr::from_ptr(cfg_str)
.to_string_lossy()
.into_owned()
};
let cfg = match TomlConfigLoader::new_from_str(&cfg_str) {
Ok(cfg) => cfg,
Err(e) => {
set_error_msg(&format!("failed to parse config: {}", e));
return -1;
}
};
let inst_name = cfg.get_inst_name();
if INSTANCE_MAP.contains_key(&inst_name) {
set_error_msg("instance already exists");
return -1;
}
let mut instance = NetworkInstance::new(cfg);
if let Err(e) = instance.start().map_err(|e| e.to_string()) {
set_error_msg(&format!("failed to start instance: {}", e));
return -1;
}
INSTANCE_MAP.insert(inst_name, instance);
0
}
#[no_mangle]
pub extern "C" fn retain_network_instance(
inst_names: *const *const std::ffi::c_char,
length: usize,
) -> std::ffi::c_int {
if length == 0 {
INSTANCE_MAP.clear();
return 0;
}
let inst_names = unsafe {
assert!(!inst_names.is_null());
std::slice::from_raw_parts(inst_names, length)
.iter()
.map(|&name| {
assert!(!name.is_null());
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
})
.collect::<Vec<_>>()
};
let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k));
0
}
#[no_mangle]
pub extern "C" fn collect_network_infos(
infos: *mut KeyValuePair,
max_length: usize,
) -> std::ffi::c_int {
if max_length == 0 {
return 0;
}
let infos = unsafe {
assert!(!infos.is_null());
std::slice::from_raw_parts_mut(infos, max_length)
};
let mut index = 0;
for instance in INSTANCE_MAP.iter() {
if index >= max_length {
break;
}
let key = instance.key();
let Some(value) = instance.get_running_info() else {
continue;
};
// convert value to json string
let value = match serde_json::to_string(&value) {
Ok(value) => value,
Err(e) => {
set_error_msg(&format!("failed to serialize instance info: {}", e));
return -1;
}
};
infos[index] = KeyValuePair {
key: std::ffi::CString::new(key.clone()).unwrap().into_raw(),
value: std::ffi::CString::new(value).unwrap().into_raw(),
};
index += 1;
}
index as std::ffi::c_int
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_config() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
fdsafdsa
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
assert_eq!(parse_config(cstr.as_ptr()), 0);
}
#[test]
fn test_run_network_instance() {
let cfg_str = r#"
inst_name = "test"
network = "test_network"
"#;
let cstr = std::ffi::CString::new(cfg_str).unwrap();
assert_eq!(run_network_instance(cstr.as_ptr()), 0);
}
}

View File

@@ -0,0 +1,33 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "********************************"
ui_print " Please install Magisk v20.4+! "
ui_print "********************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0

View File

@@ -0,0 +1,6 @@
# easytier_magisk版模块
magisk安装后重启
目录位置:/data/adb/modules/easytier_magisk
配置文件位置://data/adb/modules/easytier_magisk/config/config.toml
修改config.conf即可修改后配置文件后去magisk app重新开关模块即可生效

View File

@@ -0,0 +1,14 @@
#!/data/adb/magisk/busybox sh
MODDIR=${0%/*}
# 查找 easytier-core 进程的 PID
PID=$(pgrep easytier-core)
# 检查是否找到了进程
if [ -z "$PID" ]; then
echo "easytier-core 进程未找到"
else
# 结束进程
kill $PID
echo "已结束 easytier-core 进程 (PID: $PID)"
fi

View File

@@ -0,0 +1,25 @@
#!/bin/sh
version=$(cat module.prop | grep 'version=' | awk -F '=' '{print $2}' | sed 's/ (.*//')
version='v'$(grep '^version =' ../../easytier/Cargo.toml | cut -d '"' -f 2)
if [ -z "$version" ]; then
echo "Error: 版本号不存在."
exit 1
fi
filename="easytier_magisk_${version}.zip"
echo $version
if [ -f "./easytier-core" ] && [ -f "./easytier-cli" ] && [ -f "./easytier-web" ]; then
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
else
wget -O "easytier_last.zip" https://github.com/EasyTier/EasyTier/releases/download/"$version"/easytier-linux-aarch64-"$version".zip
unzip -o easytier_last.zip -d ./
mv ./easytier-linux-aarch64/* ./
rm -rf ./easytier_last.zip
rm -rf ./easytier-linux-aarch64
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
fi

View File

@@ -0,0 +1,37 @@
instance_name = "default"
dhcp = false
#ipv4="本机ip"
listeners = [
"tcp://0.0.0.0:11010",
"udp://0.0.0.0:11010",
"wg://0.0.0.0:11011",
"ws://0.0.0.0:11011/",
"wss://0.0.0.0:11012/",
]
mapped_listeners = []
exit_nodes = []
rpc_portal = "0.0.0.0:15888"
[network_identity]
network_name = "default"
network_secret = ""
[[peer]]
#uri = "协议://中转ip:端口"
[flags]
default_protocol = "tcp"
dev_name = ""
enable_encryption = true
enable_ipv6 = true
mtu = 1380
latency_first = false
enable_exit_node = false
no_tun = false
use_smoltcp = false
foreign_network_whitelist = "*"
disable_p2p = false
relay_all_peer_rpc = false
disable_udp_hole_punching = false

View File

@@ -0,0 +1,7 @@
ui_print '安装完成'
ui_print '当前架构为' + $ARCH
ui_print '当前系统版本为' + $API
ui_print '安装目录为: /data/adb/modules/easytier_magisk'
ui_print '配置文件位置: /data/adb/modules/easytier_magisk/config/config.toml'
ui_print '修改后配置文件后在magisk app点击操作按钮即可生效'
ui_print '记得重启'

View File

@@ -0,0 +1,48 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_FILE="${MODDIR}/config/config.toml"
LOG_FILE="${MODDIR}/log.log"
MODULE_PROP="${MODDIR}/module.prop"
EASYTIER="${MODDIR}/easytier-core"
# 更新module.prop文件中的description
update_module_description() {
local status_message=$1
sed -i "/^description=/c\description=[状态]${status_message}" ${MODULE_PROP}
}
if [ ! -e /dev/net/tun ]; then
if [ ! -d /dev/net ]; then
mkdir -p /dev/net
fi
ln -s /dev/tun /dev/net/tun
fi
while true; do
if ls $MODDIR | grep -q "disable"; then
update_module_description "关闭中"
if pgrep -f 'easytier-core' >/dev/null; then
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在,正在关闭 ..."
pkill easytier-core # 关闭进程
fi
else
if ! pgrep -f 'easytier-core' >/dev/null; then
if [ ! -f "$CONFIG_FILE" ]; then
update_module_description "config.toml不存在"
sleep 3s
continue
fi
TZ=Asia/Shanghai ${EASYTIER} -c ${CONFIG_FILE} > ${LOG_FILE} &
sleep 5s # 等待easytier-core启动完成
update_module_description "已开启(不一定运行成功)"
ip rule add from all lookup main
else
echo "开关控制$(date "+%Y-%m-%d %H:%M:%S") 进程已存在"
fi
fi
sleep 3s # 暂停3秒后再次执行循环
done

View File

@@ -0,0 +1,6 @@
{
"version": "v1.0",
"versionCode": 1,
"zipUrl": "",
"changelog": ""
}

View File

@@ -0,0 +1,7 @@
id=easytier_magisk
name=EasyTier_Magisk
version=v2.3.1
versionCode=1
author=EasyTier
description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier)
updateJson=https://raw.githubusercontent.com/EasyTier/EasyTier/refs/heads/main/easytier-contrib/easytier-magisk/magisk_update.json

View File

@@ -0,0 +1,27 @@
#!/data/adb/magisk/busybox sh
MODDIR=${0%/*}
# MODDIR="$(dirname $(readlink -f "$0"))"
chmod 755 ${MODDIR}/*
# 等待系统启动成功
while [ "$(getprop sys.boot_completed)" != "1" ]; do
sleep 5s
done
# 防止系统挂起
echo "PowerManagerService.noSuspend" > /sys/power/wake_lock
# 修改模块描述
sed -i 's/$(description=)$[^"]*/\1[状态]关闭中/' "$MODDIR/module.prop"
# 等待 3 秒
sleep 3s
"${MODDIR}/easytier_core.sh" &
# 检查是否启用模块
while [ ! -f ${MODDIR}/disable ]; do
sleep 2
done
pkill easytier-core

View File

@@ -0,0 +1,2 @@
nameserver 114.114.114.114
nameserver 223.5.5.5

View File

@@ -0,0 +1,3 @@
MODDIR=${0%/*}
pkill easytier-core # 结束 easytier-core 进程
rm -rf $MODDIR/*

View File

@@ -1,2 +0,0 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@@ -1,5 +1,81 @@
{
"i18n-ally.localesPaths": [
"locales"
"cSpell.words": [
"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"
]
}
}

View File

@@ -14,6 +14,15 @@ npm install -g pnpm
### For Desktop (Win/Mac/Linux)
```
cd ../tauri-plugin-vpnservice
pnpm install
pnpm build
cd ../easytier-web/frontend-lib
pnpm install
pnpm build
cd ../../easytier-gui
pnpm install
pnpm tauri build
```
@@ -34,7 +43,6 @@ rustup target add aarch64-linux-android
install java 20
```
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 .
@@ -43,4 +51,4 @@ See [Gradle compatibility matrix](https://docs.gradle.org/current/userguide/comp
pnpm install
pnpm tauri android init
pnpm tauri android build
```
```

View File

@@ -13,6 +13,7 @@ proxy_cidrs: 子网代理CIDR
enable_vpn_portal: 启用VPN门户
vpn_portal_listen_port: 监听端口
vpn_portal_client_network: 客户端子网
dev_name: TUN接口名称
advanced_settings: 高级设置
basic_settings: 基础设置
listener_urls: 监听地址
@@ -45,11 +46,13 @@ 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: 上传
@@ -66,6 +69,12 @@ upload_bytes: 上传
download_bytes: 下载
loss_rate: 丢包率
status:
version: 内核版本
local: 本机
server: 服务器
relay: 中继
run_network: 运行网络
stop_network: 停止网络
network_running: 运行中
@@ -75,3 +84,33 @@ dhcp_experimental_warning: 实验性警告使用DHCP时如果组网环境中
tray:
show: 显示 / 隐藏
exit: 退出
about:
title: 关于
version: 版本
author: 作者
homepage: 主页
license: 许可证
description: 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
check_update: 检查更新
event:
Unknown: 未知
TunDeviceReady: Tun设备就绪
TunDeviceError: Tun设备错误
PeerAdded: 对端添加
PeerRemoved: 对端移除
PeerConnAdded: 对端连接添加
PeerConnRemoved: 对端连接移除
ListenerAdded: 监听器添加
ListenerAddFailed: 监听器添加失败
ListenerAcceptFailed: 监听器接受连接失败
ConnectionAccepted: 连接已接受
ConnectionError: 连接错误
Connecting: 正在连接
ConnectError: 连接错误
VpnPortalClientConnected: VPN门户客户端已连接
VpnPortalClientDisconnected: VPN门户客户端已断开连接
DhcpIpv4Changed: DHCP IPv4地址更改
DhcpIpv4Conflicted: DHCP IPv4地址冲突
PortForwardAdded: 端口转发添加

View File

@@ -13,6 +13,7 @@ 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
@@ -43,9 +44,10 @@ 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
@@ -66,6 +68,12 @@ upload_bytes: Upload
download_bytes: Download
loss_rate: Loss Rate
status:
version: Version
local: Local
server: Server
relay: Relay
run_network: Run Network
stop_network: Stop Network
network_running: running
@@ -75,3 +83,33 @@ dhcp_experimental_warning: Experimental warning! if there is an IP conflict in t
tray:
show: Show / Hide
exit: Exit
about:
title: About
version: Version
author: Author
homepage: Homepage
license: License
description: 'EasyTier is a simple, safe and decentralized VPN networking solution implemented with the Rust language and Tokio framework.'
check_update: Check Update
event:
Unknown: Unknown
TunDeviceReady: TunDeviceReady
TunDeviceError: TunDeviceError
PeerAdded: PeerAdded
PeerRemoved: PeerRemoved
PeerConnAdded: PeerConnAdded
PeerConnRemoved: PeerConnRemoved
ListenerAdded: ListenerAdded
ListenerAddFailed: ListenerAddFailed
ListenerAcceptFailed: ListenerAcceptFailed
ConnectionAccepted: ConnectionAccepted
ConnectionError: ConnectionError
Connecting: Connecting
ConnectError: ConnectError
VpnPortalClientConnected: VpnPortalClientConnected
VpnPortalClientDisconnected: VpnPortalClientDisconnected
DhcpIpv4Changed: DhcpIpv4Changed
DhcpIpv4Conflicted: DhcpIpv4Conflicted
PortForwardAdded: PortForwardAdded

View File

@@ -1,8 +1,9 @@
{
"name": "easytier-gui",
"type": "module",
"version": "1.2.2",
"version": "2.3.1",
"private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
@@ -12,49 +13,51 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix"
},
"dependencies": {
"@primevue/themes": "^4.0.4",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
"@tauri-apps/plugin-os": "2.0.0-rc.0",
"@tauri-apps/plugin-process": "2.0.0-rc.0",
"@tauri-apps/plugin-shell": "2.0.0-rc.0",
"aura": "link:@primevue/themes/aura",
"pinia": "^2.2.1",
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^4.0.4",
"tauri-plugin-vpnservice-api": "link:../tauri-plugin-vpnservice",
"vue": "^3.4.36",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.3"
"@primevue/themes": "4.3.3",
"@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0",
"@tauri-apps/plugin-process": "2.0.0",
"@tauri-apps/plugin-shell": "2.0.1",
"@vueuse/core": "^11.2.0",
"aura": "link:@primevue\\themes\\aura",
"easytier-frontend-lib": "workspace:*",
"ip-num": "1.5.1",
"pinia": "^2.2.4",
"primevue": "4.3.3",
"tauri-plugin-vpnservice-api": "workspace:*",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@antfu/eslint-config": "^2.24.1",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@primevue/auto-import-resolver": "^4.0.4",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tauri-apps/api": "2.0.0-rc.0",
"@tauri-apps/cli": "2.0.0-rc.1",
"@types/node": "^20.14.14",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-vue": "^5.1.2",
"@vue-macros/volar": "^0.19.1",
"@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "4.3.3",
"@tauri-apps/api": "2.1.0",
"@tauri-apps/cli": "2.1.0",
"@types/default-gateway": "^7.2.2",
"@types/node": "^22.7.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/volar": "0.30.5",
"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",
"internal-ip": "^8.0.0",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4",
"unplugin-auto-import": "^0.17.8",
"unplugin-vue-components": "^0.27.3",
"unplugin-vue-macros": "^2.11.4",
"postcss": "^8.4.47",
"tailwindcss": "=3.4.17",
"typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"unplugin-vue-macros": "^2.13.3",
"unplugin-vue-markdown": "^0.26.2",
"unplugin-vue-router": "^0.8.8",
"uuid": "^9.0.1",
"vite": "^5.3.5",
"vite-plugin-vue-devtools": "^7.3.7",
"unplugin-vue-router": "^0.10.8",
"uuid": "^10.0.0",
"vite": "^5.4.8",
"vite-plugin-vue-devtools": "^7.4.6",
"vite-plugin-vue-layouts": "^0.11.0",
"vue-i18n": "^9.13.1",
"vue-tsc": "^2.0.29"
"vue-i18n": "^10.0.0",
"vue-tsc": "^2.1.10"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
[build]
target = "x86_64-unknown-linux-gnu"
[target]

View File

@@ -1,6 +1,6 @@
[package]
name = "easytier-gui"
version = "1.2.2"
version = "2.3.1"
description = "EasyTier GUI"
authors = ["you"]
edition = "2021"
@@ -14,11 +14,20 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }
# enable thunk-rs when compiling for x86_64 or i686 windows
[target.x86_64-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[target.i686-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[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",
"image-png",
"image-ico",
"devtools",
] }
serde = { version = "1", features = ["derive"] }
@@ -35,17 +44,20 @@ dashmap = "6.0"
privilege = "0.3"
gethostname = "0.5"
auto-launch = "0.5.0"
dunce = "1.0.4"
tauri-plugin-shell = "2.0.0-rc"
tauri-plugin-process = "2.0.0-rc"
tauri-plugin-clipboard-manager = "2.0.0-rc"
tauri-plugin-positioner = { version = "2.0.0-rc", features = ["tray-icon"] }
tauri-plugin-shell = "2.0"
tauri-plugin-process = "2.0"
tauri-plugin-clipboard-manager = "2.0"
tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] }
tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
tauri-plugin-os = "2.0.0-rc"
tauri-plugin-os = "2.0"
tauri-plugin-autostart = "2.0"
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.2.3"

Binary file not shown.

View File

@@ -1,34 +1,12 @@
fn main() {
if !cfg!(debug_assertions) && cfg!(target_os = "windows") {
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();
}
}
fn main() {
// enable thunk-rs when target os is windows and arch is x86_64 or i686
#[cfg(target_os = "windows")]
if !std::env::var("TARGET")
.unwrap_or_default()
.contains("aarch64")
{
thunk::thunk();
}
tauri_build::build();
}

View File

@@ -1,4 +1,5 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
@@ -13,6 +14,7 @@
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-set-focus",
"core:window:allow-set-title",
"core:app:default",
"core:resources:default",
"core:menu:default",
@@ -24,7 +26,6 @@
"shell:default",
"process:default",
"clipboard-manager:default",
"core:tray:default",
"core:tray:allow-new",
"core:tray:allow-set-menu",
"core:tray:allow-set-title",
@@ -38,12 +39,16 @@
"vpnservice:allow-prepare-vpn",
"vpnservice:allow-start-vpn",
"vpnservice:allow-stop-vpn",
"vpnservice:allow-register-listener",
"vpnservice:allow-registerListener",
"os:default",
"os:allow-os-type",
"os:allow-arch",
"os:allow-hostname",
"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.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -3,175 +3,38 @@
use std::collections::BTreeMap;
use anyhow::Context;
#[cfg(not(target_os = "android"))]
use auto_launch::AutoLaunchBuilder;
use dashmap::DashMap;
use easytier::{
common::config::{
ConfigLoader, FileLoggerConfig, NetworkIdentity, PeerConfig, TomlConfigLoader,
VpnPortalConfig,
},
launcher::{NetworkInstance, NetworkInstanceRunningInfo},
common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader},
launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo},
utils::{self, NewFilterSender},
};
use serde::{Deserialize, Serialize};
use tauri::Manager as _;
#[derive(Deserialize, Serialize, PartialEq, Debug)]
enum NetworkingMethod {
PublicServer,
Manual,
Standalone,
}
impl Default for NetworkingMethod {
fn default() -> Self {
NetworkingMethod::PublicServer
}
}
pub const AUTOSTART_ARG: &str = "--autostart";
#[cfg(not(target_os = "android"))]
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>> =
once_cell::sync::Lazy::new(DashMap::new);
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
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
#[tauri::command]
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
@@ -181,10 +44,10 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
#[tauri::command]
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());
}
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 mut instance = NetworkInstance::new(cfg);
@@ -224,13 +87,9 @@ fn get_os_hostname() -> Result<String, 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]
fn set_logging_level(level: String) -> Result<(), String> {
#[allow(static_mut_refs)]
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
sender.send(level).map_err(|e| e.to_string())?;
Ok(())
@@ -249,7 +108,12 @@ fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> {
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or_default() {
let _ = window.hide();
if window.is_minimized().unwrap_or_default() {
let _ = window.unminimize();
let _ = window.set_focus();
} else {
let _ = window.hide();
}
} else {
let _ = window.show();
let _ = window.set_focus();
@@ -262,82 +126,19 @@ fn check_sudo() -> bool {
use std::env::current_exe;
let is_elevated = privilege::user::privileged();
if !is_elevated {
let Ok(my_exe) = current_exe() else {
let Ok(exe) = current_exe() else {
return true;
};
let mut elevated_cmd = privilege::runas::Command::new(my_exe);
let _ = elevated_cmd.force_prompt(true).gui(true).run();
let args: Vec<String> = std::env::args().collect();
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
}
#[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)]
pub fn run() {
#[cfg(not(target_os = "android"))]
@@ -345,12 +146,40 @@ pub fn run() {
use std::process;
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_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_vpnservice::init())
.plugin(tauri_plugin_vpnservice::init());
let app = builder
.setup(|app| {
// for logging config
let Ok(log_dir) = app.path().app_log_dir() else {
@@ -365,7 +194,10 @@ pub fn run() {
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
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
#[cfg(not(target_os = "android"))]
@@ -382,7 +214,7 @@ pub fn run() {
toggle_window_visibility(app);
}
})
.icon(tauri::image::Image::from_bytes(include_bytes!(
.icon(tauri::image::Image::from_bytes(include_bytes!(
"../icons/icon.png"
))?)
.icon_as_template(false)
@@ -396,9 +228,10 @@ pub fn run() {
retain_network_instance,
collect_network_infos,
get_os_hostname,
set_auto_launch_status,
set_logging_level,
set_tun_fd
set_tun_fd,
is_autostart,
easytier_version
])
.on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))]
@@ -408,6 +241,20 @@ pub fn run() {
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
.build(tauri::generate_context!())
.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);
}
_ => {}
});
}
}

View File

@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false
},
"productName": "easytier-gui",
"version": "1.2.2",
"version": "2.3.1",
"identifier": "com.kkrainbow.easytier",
"plugins": {},
"app": {

Binary file not shown.

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>
<RouterView />
</template>

View File

@@ -3,6 +3,7 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
@@ -20,15 +21,18 @@ declare global {
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const event2human: typeof import('./composables/utils')['event2human']
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname']
const h: typeof import('vue')['h']
const initMobileService: typeof import('./composables/mobile_vpn')['initMobileService']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject']
const isAutostart: typeof import('./composables/network')['isAutostart']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
@@ -41,10 +45,12 @@ declare global {
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
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 onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router/auto')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
@@ -56,6 +62,7 @@ declare global {
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
const provide: typeof import('vue')['provide']
@@ -77,6 +84,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const timeAgoCn: typeof import('./composables/utils')['timeAgoCn']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
@@ -87,11 +95,14 @@ declare global {
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router/auto')['useLink']
const useModel: typeof import('vue')['useModel']
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
const useRoute: typeof import('vue-router/auto')['useRoute']
const useRouter: typeof import('vue-router/auto')['useRouter']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTray: typeof import('./composables/tray')['useTray']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
@@ -101,7 +112,7 @@ declare global {
// for type re-export
declare global {
// @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')
}
// for vue template auto import
@@ -120,22 +131,22 @@ declare module 'vue' {
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
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 effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef<typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
@@ -145,8 +156,8 @@ declare module 'vue' {
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router/auto')['onBeforeRouteUpdate']>
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
@@ -158,6 +169,7 @@ declare module 'vue' {
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
@@ -168,7 +180,6 @@ declare module 'vue' {
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setAutoLaunchStatus: UnwrapRef<typeof import('./composables/network')['setAutoLaunchStatus']>
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
@@ -189,11 +200,14 @@ declare module 'vue' {
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
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 useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
readonly useRoute: UnwrapRef<typeof import('vue-router/auto')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router/auto')['useRouter']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>

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>

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>

View File

@@ -1,183 +1,202 @@
import { addPluginListener } from '@tauri-apps/api/core';
import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api';
import { Route } from '~/types/network';
import type { NetworkTypes } from 'easytier-frontend-lib'
import { addPluginListener } from '@tauri-apps/api/core'
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()
interface vpnStatus {
running: boolean
ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined
routes: string[]
running: boolean
ipv4Addr: string | null | undefined
ipv4Cidr: number | null | undefined
routes: string[]
}
var curVpnStatus: vpnStatus = {
running: false,
ipv4Addr: undefined,
ipv4Cidr: undefined,
routes: []
const curVpnStatus: vpnStatus = {
running: false,
ipv4Addr: undefined,
ipv4Cidr: undefined,
routes: [],
}
async function waitVpnStatus(target_status: boolean, timeout_sec: number) {
let start_time = Date.now()
while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
const start_time = Date.now()
while (curVpnStatus.running !== target_status) {
if (Date.now() - start_time > timeout_sec * 1000) {
throw new Error('wait vpn status timeout')
}
await new Promise(r => setTimeout(r, 50))
}
}
async function doStopVpn() {
if (!curVpnStatus.running) {
return
}
console.log('stop vpn')
let stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3)
if (!curVpnStatus.running) {
return
}
console.log('stop vpn')
const stop_ret = await stop_vpn()
console.log('stop vpn', JSON.stringify((stop_ret)))
await waitVpnStatus(false, 3)
curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = []
curVpnStatus.ipv4Addr = undefined
curVpnStatus.routes = []
}
async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
if (curVpnStatus.running) {
return
}
if (curVpnStatus.running) {
return
}
console.log('start vpn')
let start_ret = await start_vpn({
"ipv4Addr": ipv4Addr + '/' + cidr,
"routes": routes,
"disallowedApplications": ["com.kkrainbow.easytier"],
"mtu": 1300,
});
if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg)
}
await waitVpnStatus(true, 3)
console.log('start vpn service', ipv4Addr, cidr, routes)
const start_ret = await start_vpn({
ipv4Addr: `${ipv4Addr}/${cidr}`,
routes,
disallowedApplications: ['com.kkrainbow.easytier'],
mtu: 1300,
})
if (start_ret?.errorMsg?.length) {
throw new Error(start_ret.errorMsg)
}
await waitVpnStatus(true, 3)
curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes
curVpnStatus.ipv4Addr = ipv4Addr
curVpnStatus.routes = routes
}
async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true
if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
}
console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true
if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
}
}
async function onVpnServiceStop(payload: any) {
console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false
console.log('vpn service stop', JSON.stringify(payload))
curVpnStatus.running = false
}
async function registerVpnServiceListener() {
console.log('register vpn service listener')
await addPluginListener(
'vpnservice',
'vpn_service_start',
onVpnServiceStart
)
console.log('register vpn service listener')
await addPluginListener(
'vpnservice',
'vpn_service_start',
onVpnServiceStart,
)
await addPluginListener(
'vpnservice',
'vpn_service_stop',
onVpnServiceStop
)
await addPluginListener(
'vpnservice',
'vpn_service_stop',
onVpnServiceStop,
)
}
function getRoutesForVpn(routes: Route[]): string[] {
if (!routes) {
return []
}
if (!routes) {
return []
}
let ret = []
for (let r of routes) {
for (let cidr of r.proxy_cidrs) {
if (cidr.indexOf('/') === -1) {
cidr += '/32'
}
ret.push(cidr)
}
const ret = []
for (const r of routes) {
for (let cidr of r.proxy_cidrs) {
if (!cidr.includes('/')) {
cidr += '/32'
}
ret.push(cidr)
}
}
// sort and dedup
return Array.from(new Set(ret)).sort()
// sort and dedup
return Array.from(new Set(ret)).sort()
}
async function onNetworkInstanceChange() {
let insts = networkStore.networkInstanceIds
if (!insts) {
await doStopVpn()
return
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
const insts = networkStore.networkInstanceIds
if (!insts) {
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
}
// if use no tun mode, stop the vpn service
const no_tun = networkStore.isNoTunEnabled(insts[0])
if (no_tun) {
console.error('no tun mode, stop vpn service')
await doStopVpn()
return
}
let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
if (!network_length) {
network_length = 24
}
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]]
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
await doStopVpn()
return
try {
await doStartVpn(virtual_ip, 24, routes)
}
const virtual_ip = curNetworkInfo?.node_info?.virtual_ipv4
if (!virtual_ip || !virtual_ip.length) {
await doStopVpn()
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
catch (e) {
console.error('start vpn service failed, clear all network insts.', e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
}
}
}
async function watchNetworkInstance() {
var subscribe_running = false
networkStore.$subscribe(async () => {
if (subscribe_running) {
return
}
subscribe_running = true
try {
await onNetworkInstanceChange()
} catch (_) {
}
subscribe_running = false
})
let subscribe_running = false
networkStore.$subscribe(async () => {
if (subscribe_running) {
return
}
subscribe_running = true
try {
await onNetworkInstanceChange()
}
catch (_) {
}
subscribe_running = false
})
console.error('vpn service watch network instance')
}
export async function initMobileVpnService() {
await registerVpnServiceListener()
await watchNetworkInstance()
await registerVpnServiceListener()
await watchNetworkInstance()
}
export async function prepareVpnService() {
console.log('prepare vpn')
let prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg)
}
console.log('prepare vpn')
const prepare_ret = await prepare_vpn()
console.log('prepare vpn', JSON.stringify((prepare_ret)))
if (prepare_ret?.errorMsg?.length) {
throw new Error(prepare_ret.errorMsg)
}
}

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) {
return invoke<string>('parse_network_config', { cfg })
@@ -22,8 +24,8 @@ export async function getOsHostname() {
return await invoke<string>('get_os_hostname')
}
export async function setAutoLaunchStatus(enable: boolean) {
return await invoke<boolean>('set_auto_launch_status', { enable })
export async function isAutostart() {
return await invoke<boolean>('is_autostart')
}
export async function setLoggingLevel(level: string) {
@@ -33,3 +35,7 @@ export async function setLoggingLevel(level: string) {
export async function setTunFd(instanceId: string, fd: number) {
return await invoke('set_tun_fd', { instanceId, fd })
}
export async function getEasytierVersion() {
return await invoke<string>('easytier_version')
}

View File

@@ -1,6 +1,6 @@
import { getCurrentWindow } from '@tauri-apps/api/window'
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
import { TrayIcon } from '@tauri-apps/api/tray'
import { getCurrentWindow } from '@tauri-apps/api/window'
import pkg from '~/../package.json'
const DEFAULT_TRAY_NAME = 'main'
@@ -8,14 +8,15 @@ const DEFAULT_TRAY_NAME = 'main'
async function toggleVisibility() {
if (await getCurrentWindow().isVisible()) {
await getCurrentWindow().hide()
} else {
}
else {
await getCurrentWindow().show()
await getCurrentWindow().setFocus()
}
}
export async function useTray(init: boolean = false) {
let tray;
let tray
try {
tray = await TrayIcon.getById(DEFAULT_TRAY_NAME)
if (!tray) {
@@ -29,17 +30,18 @@ export async function useTray(init: boolean = false) {
}),
action: async () => {
toggleVisibility()
}
},
})
}
} catch (error) {
}
catch (error) {
console.warn('Error while creating tray icon:', error)
return null
}
if (init) {
tray.setTooltip(`EasyTier\n${pkg.version}`)
tray.setMenuOnLeftClick(false);
tray.setMenuOnLeftClick(false)
tray.setMenu(await Menu.new({
id: 'main',
items: await generateMenuItem(),
@@ -59,7 +61,7 @@ export async function generateMenuItem() {
export async function MenuItemExit(text: string) {
return await PredefinedMenuItem.new({
text: text,
text,
item: 'Quit',
})
}
@@ -69,14 +71,15 @@ export async function MenuItemShow(text: string) {
id: 'show',
text,
action: async () => {
await toggleVisibility();
await toggleVisibility()
},
})
}
export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | undefined = undefined) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
const menu = await Menu.new({
id: 'main',
items: items || await generateMenuItem(),
@@ -86,15 +89,17 @@ export async function setTrayMenu(items: (MenuItem | PredefinedMenuItem)[] | und
export async function setTrayRunState(isRunning: boolean = false) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
tray.setIcon(isRunning ? 'icons/icon-inactive.ico' : 'icons/icon.ico')
}
export async function setTrayTooltip(tooltip: string) {
if (tooltip) {
const tray = await useTray()
if (!tray) return
if (!tray)
return
tray.setTooltip(`EasyTier\n${pkg.version}\n${tooltip}`)
tray.setTitle(`EasyTier\n${pkg.version}\n${tooltip}`)
}
}
}

View File

@@ -1,16 +1,15 @@
import { setupLayouts } from 'virtual:generated-layouts'
import { createRouter, createWebHistory } from 'vue-router/auto'
import Aura from '@primevue/themes/aura'
import PrimeVue from 'primevue/config'
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 Aura from '@primevue/themes/aura'
import 'primeicons/primeicons.css'
import 'primeflex/primeflex.css'
import { i18n, loadLanguageAsync } from '~/modules/i18n'
import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch'
import 'easytier-frontend-lib/style.css'
if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => {
@@ -18,8 +17,9 @@ if (import.meta.env.PROD) {
event.key === 'F5'
|| (event.ctrlKey && event.key === 'r')
|| (event.metaKey && event.key === 'r')
)
) {
event.preventDefault()
}
})
document.addEventListener('contextmenu', (event) => {
@@ -28,29 +28,34 @@ if (import.meta.env.PROD) {
}
async function main() {
await loadLanguageAsync(localStorage.getItem('lang') || 'en')
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
const app = createApp(App)
const router = createRouter({
history: createWebHistory(),
extendRoutes: routes => setupLayouts(routes),
routes,
})
app.use(router)
app.use(createPinia())
app.use(i18n, { useScope: 'global' })
app.use(EasyTierFrontendLib)
// app.use(i18n, { useScope: 'global' })
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
prefix: 'p',
darkModeSelector: 'system',
cssLayer: false
}
}})
app.use(ToastService)
prefix: 'p',
darkModeSelector: 'system',
cssLayer: {
name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities',
},
},
},
})
app.use(ToastService as any)
app.mount('#app')
}

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> {
try {
const ret = await setAutoLaunchStatus(enable)
localStorage.setItem('auto_launch', JSON.stringify(ret))
return ret
} catch (e) {
console.error(e)
return false
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
try {
if (target_enable) {
await enable()
}
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 {
return localStorage.getItem('auto_launch') === 'true'
return localStorage.getItem('auto_launch') === 'true'
}

View File

@@ -1,24 +1,22 @@
<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 { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { useTray } from '~/composables/tray';
import { type } from '@tauri-apps/plugin-os';
import { getCurrentWindow } from '@tauri-apps/api/window'
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 visible = ref(false)
const aboutVisible = ref(false)
const tomlConfig = ref('')
useTray(true)
@@ -64,6 +62,27 @@ const toast = useToast()
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() {
networkStore.addNewNetwork()
networkStore.curNetwork = networkStore.lastNetwork
@@ -71,7 +90,6 @@ function addNewNetwork() {
networkStore.$subscribe(async () => {
networkStore.saveToLocalStorage()
networkStore.saveRunningInstanceIdsToLocalStorage()
try {
await parseNetworkConfig(networkStore.curNetwork)
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') {
await prepareVpnService()
networkStore.clearNetworkInstances()
} else {
}
else {
networkStore.removeNetworkInstance(cfg.instance_id)
}
@@ -95,6 +114,7 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
try {
await runNetworkInstance(cfg)
networkStore.addAutoStartInstId(cfg.instance_id)
}
catch (e: any) {
// console.error(e)
@@ -104,11 +124,12 @@ async function runNetworkCb(cfg: NetworkConfig, cb: () => void) {
cb()
}
async function stopNetworkCb(cfg: NetworkConfig, cb: () => void) {
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
// console.log('stopNetworkCb', cfg, cb)
cb()
networkStore.removeNetworkInstance(cfg.instance_id)
await retainNetworkInstance(networkStore.networkInstanceIds)
networkStore.removeAutoStartInstId(cfg.instance_id)
}
async function updateNetworkInfos() {
@@ -120,10 +141,13 @@ onMounted(async () => {
intervalId = window.setInterval(async () => {
await updateNetworkInfos()
}, 500)
await setTrayMenu([
await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show'))
])
window.setTimeout(async () => {
await setTrayMenu([
await MenuItemExit(t('tray.exit')),
await MenuItemShow(t('tray.show')),
])
}, 1000)
})
onUnmounted(() => clearInterval(intervalId))
@@ -139,10 +163,10 @@ const setting_menu_items = ref([
label: () => t('exchange_language'),
icon: 'pi pi-language',
command: async () => {
await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await I18nUtils.loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en'))
await setTrayMenu([
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',
items: (function () {
const levels = ['off', 'warn', 'info', 'debug', 'trace']
let items = []
for (let level of levels) {
const items = []
for (const level of levels) {
items.push({
label: () => t("logging_level_" + level) + (current_log_level === level ? ' ✓' : ''),
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
command: async () => {
current_log_level = level
await setLoggingLevel(level)
@@ -175,7 +199,7 @@ const setting_menu_items = ref([
label: () => t('logging_open_dir'),
icon: 'pi pi-folder-open',
command: async () => {
console.log("open log dir", await appLogDir())
// console.log('open log dir', await appLogDir())
await open(await appLogDir())
},
})
@@ -187,7 +211,14 @@ const setting_menu_items = ref([
},
})
return items
})()
})(),
},
{
label: () => t('about.title'),
icon: 'pi pi-at',
command: async () => {
aboutVisible.value = true
},
},
{
label: () => t('exit'),
@@ -202,34 +233,42 @@ function toggle_setting_menu(event: any) {
setting_menu.value.toggle(event)
}
onMounted(async () => {
onBeforeMount(async () => {
networkStore.loadFromLocalStorage()
if (getAutoLaunchStatus()) {
let prev_running_ids = loadRunningInstanceIdsFromLocalStorage()
for (let id of prev_running_ids) {
let cfg = networkStore.networkList.find((item) => item.instance_id === id)
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
getCurrentWindow().hide()
const autoStartIds = networkStore.autoStartInstIds
for (const id of autoStartIds) {
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
if (cfg) {
networkStore.addNetworkInstance(cfg.instance_id)
await runNetworkInstance(cfg)
}
}
}
})
onMounted(async () => {
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) {
return networkStore.networkInstanceIds.includes(id)
}
</script>
<script lang="ts">
</script>
<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%' }">
<Panel>
<ScrollPanel style="width: 100%; height: 300px">
@@ -237,50 +276,61 @@ function isRunning(id: string) {
</ScrollPanel>
</Panel>
<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" />
</div>
</Dialog>
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
<About />
</Dialog>
<div>
<Toolbar>
<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" />
</div>
</template>
<template #center>
<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">
<template #value="slotProps">
<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>
</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')" />
</div>
</template>
<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="mr-3">
<div class="mr-4">
{{ t('network_name') }}: {{ slotProps.option.network_name }}
</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')" />
</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
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 : '' }}
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)">
{{
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
}}
</div>
</div>
</template>
</Dropdown>
</Select>
</div>
</template>
@@ -295,19 +345,23 @@ function isRunning(id: string) {
<Panel class="h-full overflow-y-auto">
<Stepper :value="activeStep">
<StepList value="1">
<Step value="1">{{ t('config_network') }}</Step>
<Step value="2">{{ t('running') }}</Step>
<Step value="1">
{{ t('config_network') }}
</Step>
<Step value="2">
{{ t('running') }}
</Step>
</StepList>
<StepPanels value="1">
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
<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 v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
<div class="flex flex-column">
<Status :instance-id="networkStore.curNetworkId" />
<div class="flex flex-col">
<Status :cur-network-inst="curNetworkInst" />
</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"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
</div>
@@ -349,6 +403,10 @@ body {
margin: 0;
}
.p-select-overlay {
max-width: calc(100% - 2rem);
}
/*
.p-tabview-panel {

View File

@@ -1,24 +1,25 @@
import type { NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo } from '~/types/network'
import { DEFAULT_NETWORK_CONFIG } from '~/types/network'
import { NetworkTypes } from 'easytier-frontend-lib'
export const useNetworkStore = defineStore('networkStore', {
state: () => {
const networkList = [DEFAULT_NETWORK_CONFIG()]
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
return {
// for initially empty lists
networkList: networkList as NetworkConfig[],
networkList: networkList as NetworkTypes.NetworkConfig[],
// for data that is not yet loaded
curNetwork: networkList[0],
// 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: {
lastNetwork(): NetworkConfig {
lastNetwork(): NetworkTypes.NetworkConfig {
return this.networkList[this.networkList.length - 1]
},
@@ -26,7 +27,7 @@ export const useNetworkStore = defineStore('networkStore', {
return this.curNetwork.instance_id
},
networkInstances(): Array<NetworkInstance> {
networkInstances(): Array<NetworkTypes.NetworkInstance> {
return Object.values(this.instances)
},
@@ -37,7 +38,7 @@ export const useNetworkStore = defineStore('networkStore', {
actions: {
addNewNetwork() {
this.networkList.push(DEFAULT_NETWORK_CONFIG())
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
},
delCurNetwork() {
@@ -64,7 +65,7 @@ export const useNetworkStore = defineStore('networkStore', {
this.instances = {}
},
updateWithNetworkInfos(networkInfos: Record<string, NetworkInstanceRunningInfo>) {
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
this.networkInfos = networkInfos
for (const [instanceId, info] of Object.entries(networkInfos)) {
if (this.instances[instanceId] === undefined)
@@ -74,45 +75,68 @@ export const useNetworkStore = defineStore('networkStore', {
this.instances[instanceId].error_msg = info.error_msg || ''
this.instances[instanceId].detail = info
}
this.saveRunningInstanceIdsToLocalStorage()
},
loadFromLocalStorage() {
let networkList: NetworkConfig[]
let networkList: NetworkTypes.NetworkConfig[]
// if localStorage default is [{}], instanceId will be undefined
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
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
if (networkList.length === 0)
networkList = [DEFAULT_NETWORK_CONFIG()]
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
this.networkList = networkList
this.curNetwork = this.networkList[0]
this.loadAutoStartInstIdsFromLocalStorage()
},
saveToLocalStorage() {
localStorage.setItem('networkList', JSON.stringify(this.networkList))
},
saveRunningInstanceIdsToLocalStorage() {
let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running)
localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids))
}
saveAutoStartInstIdsToLocalStorage() {
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
},
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()
},
isNoTunEnabled(instanceId: string): boolean {
const cfg = this.networkList.find((cfg) => cfg.instance_id === instanceId)
if (!cfg)
return false
return cfg.no_tun ?? false
},
},
})
if (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 []
}
}

View File

@@ -16,7 +16,6 @@
font-weight: 400;
color: #0f0f0f;
background-color: white;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -46,3 +45,11 @@
border-radius: 4px;
background-color: #0000005d;
}
.p-password {
width: 100%;
}
.p-password>input {
width: 100%;
}

View File

@@ -12,7 +12,7 @@ declare module 'vue-router/auto-routes' {
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'unplugin-vue-router/types'
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router

View File

@@ -1,162 +0,0 @@
import { v4 as uuidv4 } from 'uuid'
export enum NetworkingMethod {
PublicServer = 'PublicServer',
Manual = 'Manual',
Standalone = 'Standalone',
}
export interface NetworkConfig {
instance_id: string
dhcp: boolean
virtual_ipv4: string
hostname?: string
network_name: string
network_secret: string
networking_method: NetworkingMethod
public_server_url: string
peer_urls: string[]
proxy_cidrs: string[]
enable_vpn_portal: boolean
vpn_portal_listen_port: number
vpn_portal_client_network_addr: string
vpn_portal_client_network_len: number
advanced_settings: boolean
listener_urls: string[]
rpc_port: number
}
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
return {
instance_id: uuidv4(),
dhcp: true,
virtual_ipv4: '',
network_name: 'easytier',
network_secret: '',
networking_method: NetworkingMethod.PublicServer,
public_server_url: 'tcp://easytier.public.kkrainbow.top:11010',
peer_urls: [],
proxy_cidrs: [],
enable_vpn_portal: false,
vpn_portal_listen_port: 22022,
vpn_portal_client_network_addr: '',
vpn_portal_client_network_len: 24,
advanced_settings: false,
listener_urls: [
'tcp://0.0.0.0:11010',
'udp://0.0.0.0:11010',
'wg://0.0.0.0:11011',
],
rpc_port: 0,
}
}
export interface NetworkInstance {
instance_id: string
running: boolean
error_msg: string
detail?: NetworkInstanceRunningInfo
}
export interface NetworkInstanceRunningInfo {
my_node_info: NodeInfo
events: Record<string, any>
node_info: NodeInfo
routes: Route[]
peers: PeerInfo[]
peer_route_pairs: PeerRoutePair[]
running: boolean
error_msg?: string
}
export interface NodeInfo {
virtual_ipv4: string
ips: {
public_ipv4: string
interface_ipv4s: string[]
public_ipv6: string
interface_ipv6s: string[]
listeners: {
serialization: string
scheme_end: number
username_end: number
host_start: number
host_end: number
host: any
port?: number
path_start: number
query_start?: number
fragment_start?: number
}[]
}
stun_info: StunInfo
listeners: string[]
vpn_portal_cfg?: string
}
export interface StunInfo {
udp_nat_type: number
tcp_nat_type: number
last_update_time: number
}
export interface Route {
peer_id: number
ipv4_addr: string
next_hop_peer_id: number
cost: number
proxy_cidrs: string[]
hostname: string
stun_info?: StunInfo
inst_id: string
}
export interface PeerInfo {
peer_id: number
conns: PeerConnInfo[]
}
export interface PeerConnInfo {
conn_id: string
my_peer_id: number
peer_id: number
features: string[]
tunnel?: TunnelInfo
stats?: PeerConnStats
loss_rate: number
}
export interface PeerRoutePair {
route: Route
peer?: PeerInfo
}
export interface TunnelInfo {
tunnel_type: string
local_addr: string
remote_addr: string
}
export interface PeerConnStats {
rx_bytes: number
tx_bytes: number
rx_packets: number
tx_packets: number
latency_us: number
}

View File

@@ -1,19 +1,35 @@
import { networkInterfaces } from 'node:os'
import path from 'node:path'
import { defineConfig } from 'vite'
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 process from 'node:process'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import VueDevTools from 'vite-plugin-vue-devtools'
import VueRouter from 'unplugin-vue-router/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
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 { PrimeVueResolver } from '@primevue/auto-import-resolver';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { internalIpV4Sync } from 'internal-ip';
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'
const host = process.env.TAURI_DEV_HOST;
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/
export default defineConfig(async () => ({
@@ -23,7 +39,6 @@ export default defineConfig(async () => ({
},
},
plugins: [
svelte(),
VueMacros({
plugins: {
vue: Vue({
@@ -101,7 +116,7 @@ export default defineConfig(async () => ({
hmr: host
? {
protocol: 'ws',
host: internalIpV4Sync(),
host: findIp(gateway4sync().gateway),
port: 1430,
}
: undefined,

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
easytier-rpc-build/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

View File

@@ -0,0 +1,3 @@
# Introduction
This is a protobuf rpc service stub generator for [EasyTier](https://github.com/EasyTier/EasyTier) project.

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(())
}
}

73
easytier-web/Cargo.toml Normal file
View File

@@ -0,0 +1,73 @@
[package]
name = "easytier-web"
version = "2.3.1"
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"
axum-embed = { version = "0.1.0", optional = true }
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"] }
[features]
default = []
embed = ["dep:axum-embed"]
# enable thunk-rs when compiling for x86_64 or i686 windows
[target.x86_64-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }
[target.i686-pc-windows-msvc.build-dependencies]
thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = false, features = ["win7"] }

7
easytier-web/build.rs Normal file
View File

@@ -0,0 +1,7 @@
fn main() {
// enable thunk-rs when target os is windows and arch is x86_64 or i686
#[cfg(target_os = "windows")]
if !std::env::var("TARGET").unwrap_or_default().contains("aarch64"){
thunk::thunk();
}
}

24
easytier-web/frontend-lib/.gitignore vendored Normal file
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?

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).

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>

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.3.3",
"@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.3.3",
"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.17",
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0",
"vue-tsc": "^2.1.10"
}
}

820
easytier-web/frontend-lib/pnpm-lock.yaml generated Normal file
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

View File

@@ -0,0 +1,7 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"postcss-nested": {},
},
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,416 @@
<script setup lang="ts">
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
import { 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
}
const exitNodesSuggestions = ref([''])
function searchExitNodesSuggestions(e: { query: string }) {
const ret = []
ret.push(e.query)
exitNodesSuggestions.value = ret
}
const whitelistSuggestions = ref([''])
function searchWhitelistSuggestions(e: { query: string }) {
const ret = []
ret.push(e.query)
whitelistSuggestions.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' },
{ field: 'enable_exit_node', help: 'enable_exit_node_help' },
{ field: 'relay_all_peer_rpc', help: 'relay_all_peer_rpc_help' },
{ field: 'multi_thread', help: 'multi_thread_help' },
{ field: 'proxy_forward_by_system', help: 'proxy_forward_by_system_help' },
{ field: 'disable_encryption', help: 'disable_encryption_help' },
{ field: 'disable_udp_hole_punching', help: 'disable_udp_hole_punching_help' },
{ field: 'enable_magic_dns', help: 'enable_magic_dns_help' },
{ field: 'enable_private_mode', help: 'enable_private_mode_help' },
]
</script>
<template>
<div class="frontend-lib">
<div class="flex flex-col h-full">
<div class="flex flex-col">
<div class="w-11/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>
<Password id="network_secret" v-model="curNetwork.network_secret"
aria-describedby="network_secret-help" toggleMask :feedback="false"/>
</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-[20rem] flex items-center" 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="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 basis-8/12 grow">
<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>
</div>
<div class="flex flex-col gap-2 basis-3/12 grow">
<InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
:min="0" :max="65535" fluid />
</div>
</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 class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex">
<label for="mtu">{{ t('mtu') }}</label>
<span class="pi pi-question-circle ml-2 self-center"
v-tooltip="t('mtu_help')"></span>
</div>
<InputNumber id="mtu" v-model="curNetwork.mtu" aria-describedby="mtu-help"
:format="false" :placeholder="t('mtu_placeholder')" :min="400" :max="1380" fluid/>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex">
<label for="relay_network_whitelist">{{ t('relay_network_whitelist') }}</label>
<span class="pi pi-question-circle ml-2 self-center"
v-tooltip="t('relay_network_whitelist_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_relay_network_whitelist" on-icon="pi pi-check" off-icon="pi pi-times"
:on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
<div v-if="curNetwork.enable_relay_network_whitelist" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<AutoComplete id="relay_network_whitelist" v-model="curNetwork.relay_network_whitelist"
:placeholder="t('relay_network_whitelist')" class="w-full" multiple fluid
:suggestions="whitelistSuggestions" @complete="searchWhitelistSuggestions" />
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-col gap-2 grow">
<div class="flex">
<label for="routes">{{ t('manual_routes') }}</label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('manual_routes_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_manual_routes" 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_manual_routes" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<AutoComplete id="routes" v-model="curNetwork.routes"
:placeholder="t('chips_placeholder', ['192.168.0.0/16'])" class="w-full" multiple fluid
:suggestions="inetSuggestions" @complete="searchInetSuggestions" />
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap ">
<div class="flex flex-col gap-2 grow">
<div class="flex">
<label for="socks5_port">{{ t('socks5') }}</label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('socks5_help')"></span>
</div>
<ToggleButton v-model="curNetwork.enable_socks5" 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_socks5" class="items-center flex flex-row gap-x-4">
<div class="min-w-64 w-full">
<InputNumber id="socks5_port" v-model="curNetwork.socks5_port" aria-describedby="rpc_port-help"
:format="false" :allow-empty="false" :min="0" :max="65535" class="w-full"/>
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 grow p-fluid">
<div class="flex">
<label for="exit_nodes">{{ t('exit_nodes') }}</label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('exit_nodes_help')"></span>
</div>
<AutoComplete id="exit_nodes" v-model="curNetwork.exit_nodes"
:placeholder="t('chips_placeholder', ['192.168.8.8'])" class="w-full" multiple fluid
:suggestions="exitNodesSuggestions" @complete="searchExitNodesSuggestions" />
</div>
</div>
<div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 grow p-fluid">
<div class="flex">
<label for="mapped_listeners">{{ t('mapped_listeners') }}</label>
<span class="pi pi-question-circle ml-2 self-center" v-tooltip="t('mapped_listeners_help')"></span>
</div>
<AutoComplete id="mapped_listeners" v-model="curNetwork.mapped_listeners"
:placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full"
multiple fluid :suggestions="peerSuggestions" @complete="searchPeerSuggestions" />
</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>

View File

@@ -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>

View File

@@ -1,31 +1,29 @@
<script setup lang="ts">
import type { NodeInfo } from '~/types/network'
const { t } = useI18n()
import { useTimeAgo } from '@vueuse/core'
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<{
instanceId?: string
curNetworkInst: NetworkInstance | null,
}>()
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 curNetworkInst = computed(() => {
return networkStore.networkInstances.find(n => n.instance_id === curNetwork.value.instance_id)
})
const { t } = useI18n()
const peerRouteInfos = computed(() => {
if (curNetworkInst.value)
return curNetworkInst.value.detail?.peer_route_pairs || []
if (props.curNetworkInst) {
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 []
})
@@ -33,8 +31,9 @@ const peerRouteInfos = computed(() => {
function routeCost(info: any) {
if (info.route) {
const cost = info.route.cost
return cost === 1 ? 'p2p' : `relay(${cost})`
return cost ? cost === 1 ? 'p2p' : `relay(${cost})` : t('status.local')
}
return '?'
}
@@ -73,34 +72,49 @@ function humanFileSize(bytes: number, si = false, dp = 1) {
return `${bytes.toFixed(dp)} ${units[u]}`
}
function latencyMs(info: any) {
function latencyMs(info: PeerRoutePair) {
let lat_us_sum = statsCommon(info, 'stats.latency_us')
if (lat_us_sum === undefined)
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`
}
function txBytes(info: any) {
function txBytes(info: PeerRoutePair) {
const tx = statsCommon(info, 'stats.tx_bytes')
return tx ? humanFileSize(tx) : ''
}
function rxBytes(info: any) {
function rxBytes(info: PeerRoutePair) {
const rx = statsCommon(info, 'stats.rx_bytes')
return rx ? humanFileSize(rx) : ''
}
function lossRate(info: any) {
function lossRate(info: PeerRoutePair) {
const lossRate = statsCommon(info, 'loss_rate')
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}` : ''
}
function tunnelProto(info: PeerRoutePair) {
return [...new Set(info.peer?.conns.map(c => c.tunnel?.tunnel_type))].join(',')
}
const myNodeInfo = computed(() => {
if (!curNetworkInst.value)
if (!props.curNetworkInst)
return {} as NodeInfo
return curNetworkInst.value.detail?.my_node_info
return props.curNetworkInst.detail?.my_node_info
})
interface Chip {
@@ -109,18 +123,26 @@ interface Chip {
}
const myNodeInfoChips = computed(() => {
if (!curNetworkInst.value)
if (!props.curNetworkInst)
return []
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)
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({
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
label: `Virtual IPv4: ${ipv4InetToString(my_node_info.virtual_ipv4)}`,
icon: '',
} as Chip)
@@ -128,7 +150,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv4s = my_node_info.ips?.interface_ipv4s
for (const [idx, ip] of local_ipv4s?.entries()) {
chips.push({
label: `Local IPv4 ${idx}: ${ip}`,
label: `Local IPv4 ${idx}: ${ipv4ToString(ip)}`,
icon: '',
} as Chip)
}
@@ -137,7 +159,7 @@ const myNodeInfoChips = computed(() => {
const local_ipv6s = my_node_info.ips?.interface_ipv6s
for (const [idx, ip] of local_ipv6s?.entries()) {
chips.push({
label: `Local IPv6 ${idx}: ${ip}`,
label: `Local IPv6 ${idx}: ${ipv6ToString(ip)}`,
icon: '',
} as Chip)
}
@@ -146,7 +168,15 @@ const myNodeInfoChips = computed(() => {
const public_ip = my_node_info.ips?.public_ipv4
if (public_ip) {
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: '',
} as Chip)
}
@@ -155,7 +185,7 @@ const myNodeInfoChips = computed(() => {
const listeners = my_node_info.listeners
for (const [idx, listener] of listeners?.entries()) {
chips.push({
label: `Listener ${idx}: ${listener}`,
label: `Listener ${idx}: ${listener.url}`,
icon: '',
} as Chip)
}
@@ -171,6 +201,8 @@ const myNodeInfoChips = computed(() => {
PortRestricted = 5,
Symmetric = 6,
SymUdpFirewall = 7,
SymmetricEasyInc = 8,
SymmetricEasyDec = 9,
};
const udpNatType: NatType = my_node_info.stun_info?.udp_nat_type
if (udpNatType !== undefined) {
@@ -183,6 +215,8 @@ const myNodeInfoChips = computed(() => {
[NatType.PortRestricted]: 'Port Restricted',
[NatType.Symmetric]: 'Symmetric',
[NatType.SymUdpFirewall]: 'Symmetric UDP Firewall',
[NatType.SymmetricEasyInc]: 'Symmetric Easy Inc',
[NatType.SymmetricEasyDec]: 'Symmetric Easy Dec',
}
chips.push({
@@ -261,28 +295,32 @@ function showVpnPortalConfig() {
}
function showEventLogs() {
const detail = curNetworkInst.value?.detail
const detail = props.curNetworkInst?.detail
if (!detail)
return
dialogContent.value = detail.events
dialogContent.value = detail.events.map((event: string) => JSON.parse(event))
dialogHeader.value = 'event_log'
dialogVisible.value = true
}
</script>
<template>
<div>
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" :style="{ width: '70%' }">
<Panel>
<ScrollPanel style="width: 100%; height: 400px">
<pre>{{ dialogContent }}</pre>
</ScrollPanel>
</Panel>
<Divider />
<div class="flex justify-content-end gap-2">
<Button type="button" :label="t('close')" @click="dialogVisible = false" />
</div>
<div class="frontend-lib">
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-full h-auto max-h-full"
:baseZIndex="2000">
<ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
<pre>{{ dialogContent }}</pre>
</ScrollPanel>
<Timeline v-else :value="dialogContent">
<template #opposite="slotProps">
<small class="text-surface-500 dark:text-surface-400">{{ useTimeAgo(Date.parse(slotProps.item.time))
}}</small>
</template>
<template #content="slotProps">
<HumanEvent :event="slotProps.item.event" />
</template>
</Timeline>
</Dialog>
<Card v-if="curNetworkInst?.error_msg">
@@ -290,7 +328,7 @@ function showEventLogs() {
Run Network Error
</template>
<template #content>
<div class="flex flex-column gap-y-5">
<div class="flex flex-col gap-y-5">
<div class="text-red-500">
{{ curNetworkInst.error_msg }}
</div>
@@ -304,12 +342,9 @@ function showEventLogs() {
{{ t('my_node_info') }}
</template>
<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="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid green"
>
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid green">
<div class="font-bold">
{{ t('peer_count') }}
</div>
@@ -318,10 +353,7 @@ function showEventLogs() {
</div>
</div>
<div
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid purple"
>
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid purple">
<div class="font-bold">
{{ t('upload') }}
</div>
@@ -330,10 +362,7 @@ function showEventLogs() {
</div>
</div>
<div
class="rounded-full w-32 h-32 flex flex-column align-items-center pt-4"
style="border: 1px solid fuchsia"
>
<div class="rounded-full w-32 h-32 flex flex-col items-center pt-6" style="border: 1px solid fuchsia">
<div class="font-bold">
{{ t('download') }}
</div>
@@ -343,11 +372,9 @@ function showEventLogs() {
</div>
</div>
<div class="flex flex-row align-items-center flex-wrap w-full max-h-40 overflow-scroll">
<Chip
v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
class="mr-2 mt-2 text-sm"
/>
<div class="flex flex-row items-center flex-wrap w-full max-h-40 overflow-scroll">
<Chip v-for="(chip, i) in myNodeInfoChips" :key="i" :label="chip.label" :icon="chip.icon"
class="mr-2 mt-2 text-sm" />
</div>
<div v-if="myNodeInfo" class="m-0 flex flex-row justify-center gap-x-5 text-sm">
@@ -365,17 +392,45 @@ function showEventLogs() {
{{ t('peer_info') }}
</template>
<template #content>
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-style="width: 100%">
<Column field="route.ipv4_addr" style="width: 100px;" :header="t('virtual_ipv4')" />
<Column field="route.hostname" style="max-width: 250px;" :header="t('hostname')" />
<Column :field="routeCost" style="width: 100px;" :header="t('route_cost')" />
<Column :field="latencyMs" style="width: 80px;" :header="t('latency')" />
<Column :field="txBytes" style="width: 80px;" :header="t('upload_bytes')" />
<Column :field="rxBytes" style="width: 80px;" :header="t('download_bytes')" />
<Column :field="lossRate" style="width: 100px;" :header="t('loss_rate')" />
<DataTable :value="peerRouteInfos" column-resize-mode="fit" table-class="w-full">
<Column :field="ipFormat" :header="t('virtual_ipv4')" />
<Column :header="t('hostname')">
<template #body="slotProps">
<div v-if="!slotProps.data.route.cost || !slotProps.data.route.feature_flag.is_public_server"
v-tooltip="slotProps.data.route.hostname">
{{
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="tunnelProto" :header="t('tunnel_proto')" />
<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>
</template>
</Card>
</template>
</div>
</template>
<style lang="postcss" scoped>
.p-timeline :deep(.p-timeline-event-opposite) {
@apply flex-none;
}
</style>

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