Compare commits

...

118 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
208 changed files with 15255 additions and 3622 deletions
+27 -15
View File
@@ -6,72 +6,84 @@ rustflags = ["-C", "linker-flavor=ld.lld"]
linker = "aarch64-linux-gnu-gcc" linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl] [target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc" linker = "aarch64-unknown-linux-musl-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(all(windows, target_env = "msvc"))'] [target.'cfg(all(windows, target_env = "msvc"))']
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.mipsel-unknown-linux-musl] [target.mipsel-unknown-linux-musl]
linker = "mipsel-linux-muslsf-gcc" linker = "mipsel-unknown-linux-muslsf-gcc"
rustflags = [ rustflags = [
"-C", "-C",
"target-feature=+crt-static", "target-feature=+crt-static",
"-L", "-L",
"./musl_gcc/mipsel-linux-muslsf-cross/mipsel-linux-muslsf/lib", "./musl_gcc/mipsel-unknown-linux-muslsf/mipsel-unknown-linux-muslsf/lib",
"-L", "-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", "-l",
"atomic", "atomic",
"-l", "-l",
"ctz", "ctz",
"-l",
"gcc",
] ]
[target.mips-unknown-linux-musl] [target.mips-unknown-linux-musl]
linker = "mips-linux-muslsf-gcc" linker = "mips-unknown-linux-muslsf-gcc"
rustflags = [ rustflags = [
"-C", "-C",
"target-feature=+crt-static", "target-feature=+crt-static",
"-L", "-L",
"./musl_gcc/mips-linux-muslsf-cross/mips-linux-muslsf/lib", "./musl_gcc/mips-unknown-linux-muslsf/mips-unknown-linux-muslsf/lib",
"-L", "-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", "-l",
"atomic", "atomic",
"-l", "-l",
"ctz", "ctz",
"-l",
"gcc",
] ]
[target.armv7-unknown-linux-musleabihf] [target.armv7-unknown-linux-musleabihf]
linker = "armv7l-linux-musleabihf-gcc" linker = "armv7-unknown-linux-musleabihf-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.armv7-unknown-linux-musleabi] [target.armv7-unknown-linux-musleabi]
linker = "armv7m-linux-musleabi-gcc" linker = "armv7-unknown-linux-musleabi-gcc"
rustflags = ["-C", "target-feature=+crt-static"] rustflags = ["-C", "target-feature=+crt-static"]
[target.arm-unknown-linux-musleabihf] [target.arm-unknown-linux-musleabihf]
linker = "arm-linux-musleabihf-gcc" linker = "arm-unknown-linux-musleabihf-gcc"
rustflags = [ rustflags = [
"-C", "-C",
"target-feature=+crt-static", "target-feature=+crt-static",
"-L", "-L",
"./musl_gcc/arm-linux-musleabihf-cross/arm-linux-musleabihf/lib", "./musl_gcc/arm-unknown-linux-musleabihf/arm-unknown-linux-musleabihf/lib",
"-L", "-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", "-l",
"atomic", "atomic",
"-l",
"gcc",
] ]
[target.arm-unknown-linux-musleabi] [target.arm-unknown-linux-musleabi]
linker = "arm-linux-musleabi-gcc" linker = "arm-unknown-linux-musleabi-gcc"
rustflags = [ rustflags = [
"-C", "-C",
"target-feature=+crt-static", "target-feature=+crt-static",
"-L", "-L",
"./musl_gcc/arm-linux-musleabi-cross/arm-linux-musleabi/lib", "./musl_gcc/arm-unknown-linux-musleabi/arm-unknown-linux-musleabi/lib",
"-L", "-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", "-l",
"atomic", "atomic",
"-l",
"gcc",
] ]
+2 -2
View File
@@ -18,7 +18,7 @@ RUN mkdir -p /tmp/output; \
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata tini
WORKDIR /app WORKDIR /app
COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin COPY --from=builder --chmod=755 /tmp/output/* /usr/local/bin
@@ -36,4 +36,4 @@ EXPOSE 11011/tcp
# wss # wss
EXPOSE 11012/tcp EXPOSE 11012/tcp
ENTRYPOINT ["easytier-core"] ENTRYPOINT ["/sbin/tini", "--", "easytier-core"]
+130 -39
View File
@@ -31,6 +31,47 @@ jobs:
skip_after_successful_duplicate: 'true' skip_after_successful_duplicate: 'true'
cancel_others: 'true' cancel_others: 'true'
paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]' paths: '["Cargo.toml", "Cargo.lock", "easytier/**", ".github/workflows/core.yml", ".github/workflows/install_rust.sh"]'
build_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: build:
strategy: strategy:
fail-fast: false fail-fast: false
@@ -71,10 +112,12 @@ jobs:
- TARGET: x86_64-pc-windows-msvc - TARGET: x86_64-pc-windows-msvc
OS: windows-latest OS: windows-latest
ARTIFACT_NAME: windows-x86_64 ARTIFACT_NAME: windows-x86_64
- TARGET: aarch64-pc-windows-msvc - TARGET: aarch64-pc-windows-msvc
OS: windows-latest OS: windows-latest
ARTIFACT_NAME: windows-arm64 ARTIFACT_NAME: windows-arm64
- TARGET: i686-pc-windows-msvc
OS: windows-latest
ARTIFACT_NAME: windows-i686
- TARGET: x86_64-unknown-freebsd - TARGET: x86_64-unknown-freebsd
OS: ubuntu-22.04 OS: ubuntu-22.04
@@ -87,7 +130,9 @@ jobs:
TARGET: ${{ matrix.TARGET }} TARGET: ${{ matrix.TARGET }}
OS: ${{ matrix.OS }} OS: ${{ matrix.OS }}
OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }} OSS_BUCKET: ${{ secrets.ALIYUN_OSS_BUCKET }}
needs: pre_job needs:
- pre_job
- build_web
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -96,7 +141,14 @@ jobs:
run: | run: |
echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV
- name: Download web artifact
uses: actions/download-artifact@v4
with:
name: easytier-web-dashboard
path: easytier-web/frontend/dist/
- name: Cargo cache - name: Cargo cache
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
@@ -114,26 +166,38 @@ jobs:
if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }}
run: | run: |
bash ./.github/workflows/install_rust.sh 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 if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier
else 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 cargo build --release --verbose --target $TARGET
fi fi
# Copied and slightly modified from @lmq8267 (https://github.com/lmq8267) # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267)
- name: Build Core & Cli (X86_64 FreeBSD) - name: Build Core & Cli (X86_64 FreeBSD)
uses: cross-platform-actions/action@v0.23.0 uses: vmactions/freebsd-vm@v1
if: ${{ endsWith(matrix.TARGET, 'freebsd') }} if: ${{ endsWith(matrix.TARGET, 'freebsd') }}
env: env:
TARGET: ${{ matrix.TARGET }} TARGET: ${{ matrix.TARGET }}
with: with:
operating_system: freebsd envs: TARGET
environment_variables: TARGET release: ${{ matrix.BSD_VERSION }}
architecture: x86-64 arch: x86_64
version: ${{ matrix.BSD_VERSION }} usesh: true
shell: bash mem: 6144
memory: 5G cpu: 4
cpu_count: 4
run: | run: |
uname -a uname -a
echo $SHELL echo $SHELL
@@ -142,40 +206,36 @@ jobs:
whoami whoami
env | sort env | sort
sudo pkg install -y git protobuf pkg install -y git protobuf llvm-devel sudo curl
curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y curl --proto 'https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env . $HOME/.cargo/env
rustup set auto-self-update disable rustup set auto-self-update disable
rustup install 1.77 rustup install 1.86
rustup default 1.77 rustup default 1.86
export CC=clang export CC=clang
export CXX=clang++ export CXX=clang++
export CARGO_TERM_COLOR=always 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 cargo build --release --verbose --target $TARGET
- name: Install UPX
if: ${{ matrix.OS != 'macos-latest' }}
uses: crazy-max/ghaction-upx@v3
with:
version: latest
install-only: true
- name: Compress - name: Compress
run: | run: |
mkdir -p ./artifacts/objects/ mkdir -p ./artifacts/objects/
# windows is the only OS using a different convention for executable file name # windows is the only OS using a different convention for executable file name
if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then if [[ $OS =~ ^windows.*$ && $TARGET =~ ^x86_64.*$ ]]; then
SUFFIX=.exe SUFFIX=.exe
cp easytier/third_party/Packet.dll ./artifacts/objects/ cp easytier/third_party/*.dll ./artifacts/objects/
cp easytier/third_party/wintun.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 elif [[ $OS =~ ^windows.*$ && $TARGET =~ ^aarch64.*$ ]]; then
SUFFIX=.exe SUFFIX=.exe
cp easytier/third_party/arm64/Packet.dll ./artifacts/objects/ cp easytier/third_party/arm64/*.dll ./artifacts/objects/
cp easytier/third_party/arm64/wintun.dll ./artifacts/objects/
fi fi
if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then if [[ $GITHUB_REF_TYPE =~ ^tag$ ]]; then
TAG=$GITHUB_REF_NAME TAG=$GITHUB_REF_NAME
@@ -184,14 +244,18 @@ jobs:
fi fi
if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then
upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX" UPX_VERSION=5.0.1
upx --lzma --best ./target/$TARGET/release/easytier-cli"$SUFFIX" 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 fi
mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/ mv ./target/$TARGET/release/easytier-core"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/ mv ./target/$TARGET/release/easytier-cli"$SUFFIX" ./artifacts/objects/
if [[ ! $TARGET =~ ^mips.*$ ]]; then if [[ ! $TARGET =~ ^mips.*$ ]]; then
mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/ mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./artifacts/objects/
mv ./target/$TARGET/release/easytier-web-embed"$SUFFIX" ./artifacts/objects/
fi fi
mv ./artifacts/objects/* ./artifacts/ mv ./artifacts/objects/* ./artifacts/
@@ -204,25 +268,52 @@ jobs:
path: | path: |
./artifacts/* ./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
core-result: core-result:
if: needs.pre_job.outputs.should_skip != 'true' && always() if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- pre_job - pre_job
- build_web
- build - build
steps: steps:
- name: Mark result as failed - name: Mark result as failed
if: needs.build.result != 'success' if: needs.build.result != 'success'
run: exit 1 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
+10 -2
View File
@@ -11,7 +11,7 @@ on:
image_tag: image_tag:
description: 'Tag for this image build' description: 'Tag for this image build'
type: string type: string
default: 'v1.2.0' default: 'v2.3.1'
required: true required: true
mark_latest: mark_latest:
description: 'Mark this image as latest' description: 'Mark this image as latest'
@@ -39,6 +39,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: login github container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download artifact - name: Download artifact
id: download-artifact id: download-artifact
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v6
@@ -58,4 +64,6 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
file: .github/workflows/Dockerfile file: .github/workflows/Dockerfile
tags: easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }}, tags: |
easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
ghcr.io/easytier/easytier:${{ inputs.image_tag }}${{ inputs.mark_latest && ',easytier/easytier:latest' || '' }},
+57 -46
View File
@@ -63,6 +63,11 @@ jobs:
GUI_TARGET: aarch64-pc-windows-msvc GUI_TARGET: aarch64-pc-windows-msvc
ARTIFACT_NAME: windows-arm64 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 }} runs-on: ${{ matrix.OS }}
env: env:
NAME: easytier NAME: easytier
@@ -73,6 +78,56 @@ jobs:
needs: pre_job needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true' if: needs.pre_job.outputs.should_skip != 'true'
steps: steps:
- name: Install GUI dependencies (x86 only)
if: ${{ matrix.TARGET == 'x86_64-unknown-linux-musl' }}
run: |
sudo apt update
sudo apt install -qq libwebkit2gtk-4.1-dev \
build-essential \
curl \
wget \
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: |
# see https://tauri.app/v1/guides/building/linux/
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt 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 - uses: actions/checkout@v3
- name: Set current ref as env variable - name: Set current ref as env variable
@@ -124,45 +179,13 @@ jobs:
# GitHub repo token to use to avoid rate limiter # GitHub repo token to use to avoid rate limiter
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install GUI cross compile (aarch64 only)
if: ${{ matrix.TARGET == 'aarch64-unknown-linux-musl' }}
run: |
# see https://tauri.app/v1/guides/building/linux/
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted" | sudo tee /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=amd64] http://security.ubuntu.com/ubuntu/ jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security multiverse" | sudo tee -a /etc/apt/sources.list
sudo dpkg --add-architecture arm64
sudo apt-get update
sudo apt-get install -y libgstreamer1.0-0:arm64 gstreamer1.0-plugins-base:arm64 gstreamer1.0-plugins-good:arm64
sudo apt-get install -y libgstreamer-gl1.0-0:arm64 libgstreamer-plugins-base1.0-0:arm64 libgstreamer-plugins-good1.0-0:arm64 libwebkit2gtk-4.1-0:arm64
sudo apt install -f -o Dpkg::Options::="--force-overwrite" libwebkit2gtk-4.1-dev:arm64 libssl-dev:arm64 gcc-aarch64-linux-gnu
echo "PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/" >> "$GITHUB_ENV"
- name: copy correct DLLs - name: copy correct DLLs
if: ${{ matrix.OS == 'windows-latest' }} if: ${{ matrix.OS == 'windows-latest' }}
run: | run: |
if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then if [[ $GUI_TARGET =~ ^aarch64.*$ ]]; then
cp ./easytier/third_party/arm64/*.dll ./easytier-gui/src-tauri/ 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 else
cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/ cp ./easytier/third_party/*.dll ./easytier-gui/src-tauri/
fi fi
@@ -207,18 +230,6 @@ jobs:
path: | path: |
./artifacts/* ./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
gui-result: gui-result:
if: needs.pre_job.outputs.should_skip != 'true' && always() if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
+17 -45
View File
@@ -8,61 +8,33 @@
# dependencies are only needed on ubuntu as that's the only place where # dependencies are only needed on ubuntu as that's the only place where
# we make cross-compilation # we make cross-compilation
if [[ $OS =~ ^ubuntu.*$ ]]; then if [[ $OS =~ ^ubuntu.*$ ]]; then
sudo apt-get update && sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf musl-tools libappindicator3-dev sudo apt-get update && sudo apt-get install -qq musl-tools libappindicator3-dev llvm clang
# for easytier-gui # https://github.com/cross-tools/musl-cross/releases
if [[ $GUI_TARGET != '' && $GUI_TARGET =~ ^x86_64.*$ ]]; then # if "musl" is a substring of TARGET, we assume that we are using musl
sudo apt install -qq libwebkit2gtk-4.1-dev \ MUSL_TARGET=$TARGET
build-essential \ # if target is mips or mipsel, we should use soft-float version of musl
curl \ if [[ $TARGET =~ ^mips.*$ || $TARGET =~ ^mipsel.*$ ]]; then
wget \ MUSL_TARGET=${TARGET}sf
file \
libgtk-3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
fi fi
# curl -s musl.cc | grep mipsel if [[ $MUSL_TARGET =~ musl ]]; then
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
mkdir -p ./musl_gcc mkdir -p ./musl_gcc
wget -c https://musl.cc/${MUSL_URI}-cross.tgz -P ./musl_gcc/ wget --inet4-only -c https://github.com/cross-tools/musl-cross/releases/download/20250520/${MUSL_TARGET}.tar.xz -P ./musl_gcc/
tar zxf ./musl_gcc/${MUSL_URI}-cross.tgz -C ./musl_gcc/ tar xf ./musl_gcc/${MUSL_TARGET}.tar.xz -C ./musl_gcc/
sudo ln -s $(pwd)/musl_gcc/${MUSL_URI}-cross/bin/*gcc /usr/bin/ 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
fi fi
# see https://github.com/rust-lang/rustup/issues/3709 # see https://github.com/rust-lang/rustup/issues/3709
rustup set auto-self-update disable rustup set auto-self-update disable
rustup install 1.77 rustup install 1.86
rustup default 1.77 rustup default 1.86
# mips/mipsel cannot add target from rustup, need compile by ourselves # mips/mipsel cannot add target from rustup, need compile by ourselves
if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then
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 # for panic-abort
cp libgcc_eh.a libunwind.a cp libgcc_eh.a libunwind.a
-12
View File
@@ -146,18 +146,6 @@ jobs:
path: | path: |
./artifacts/* ./artifacts/*
- name: Upload OSS
if: ${{ env.OSS_BUCKET != '' }}
uses: Menci/upload-to-oss@main
with:
access-key-id: ${{ secrets.ALIYUN_OSS_ACCESS_ID }}
access-key-secret: ${{ secrets.ALIYUN_OSS_ACCESS_KEY }}
endpoint: ${{ secrets.ALIYUN_OSS_ENDPOINT }}
bucket: ${{ secrets.ALIYUN_OSS_BUCKET }}
local-path: ./artifacts/
remote-path: /easytier-releases/${{env.GIT_DESC}}/easytier-gui-${{ matrix.ARTIFACT_NAME }}
no-delete-remote-files: true
retry: 5
mobile-result: mobile-result:
if: needs.pre_job.outputs.should_skip != 'true' && always() if: needs.pre_job.outputs.should_skip != 'true' && always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
+10 -3
View File
@@ -21,7 +21,7 @@ on:
version: version:
description: 'Version for this release' description: 'Version for this release'
type: string type: string
default: 'v2.1.0' default: 'v2.3.1'
required: true required: true
make_latest: make_latest:
description: 'Mark this release as latest' description: 'Mark this release as latest'
@@ -57,7 +57,7 @@ jobs:
repo: EasyTier/EasyTier repo: EasyTier/EasyTier
path: release_assets_nozip path: release_assets_nozip
- name: Download GUI Artifact - name: Download Mobile Artifact
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v6
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
@@ -78,7 +78,14 @@ jobs:
ls -l -R ./ ls -l -R ./
chmod -R 755 . chmod -R 755 .
for x in `ls`; do 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 done
- name: Release - name: Release
+30 -1
View File
@@ -47,11 +47,40 @@ jobs:
- name: Setup system for test - name: Setup system for test
run: | run: |
sudo modprobe br_netfilter
sudo sysctl net.bridge.bridge-nf-call-iptables=0 sudo sysctl net.bridge.bridge-nf-call-iptables=0
sudo sysctl net.bridge.bridge-nf-call-ip6tables=0 sudo sysctl net.bridge.bridge-nf-call-ip6tables=0
sudo sysctl net.ipv6.conf.lo.disable_ipv6=0 sudo sysctl net.ipv6.conf.lo.disable_ipv6=0
sudo ip addr add 2001:db8::2/64 dev lo 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 - name: Cargo cache
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -62,6 +91,6 @@ jobs:
- name: Run tests - name: Run tests
run: | 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 ./target
sudo chown -R $USER:$USER ~/.cargo sudo chown -R $USER:$USER ~/.cargo
Generated
+1443 -560
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,6 +1,12 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["easytier", "easytier-gui/src-tauri", "easytier-rpc-build", "easytier-web"] members = [
"easytier",
"easytier-gui/src-tauri",
"easytier-rpc-build",
"easytier-web",
"easytier-contrib/easytier-ffi",
]
default-members = ["easytier", "easytier-web"] default-members = ["easytier", "easytier-web"]
[profile.dev] [profile.dev]
@@ -10,3 +16,5 @@ panic = "unwind"
panic = "abort" panic = "abort"
lto = true lto = true
codegen-units = 1 codegen-units = 1
opt-level = 3
strip = true
+41 -10
View File
@@ -1,14 +1,17 @@
# EasyTier # 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](https://img.shields.io/github/license/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/blob/main/LICENSE)
[![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main) [![GitHub last commit](https://img.shields.io/github/last-commit/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/commits/main)
[![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues) [![GitHub issues](https://img.shields.io/github/issues/EasyTier/EasyTier)](https://github.com/EasyTier/EasyTier/issues)
[![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml) [![GitHub Core Actions](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/core.yml)
[![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml) [![GitHub GUI Actions](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml/badge.svg)](https://github.com/EasyTier/EasyTier/actions/workflows/gui.yml)
[![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) [简体中文](/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.
@@ -31,6 +34,7 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
- **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected. - **High Availability**: Supports multi-path and switches to healthy paths when high packet loss or network errors are detected.
- **IPv6 Support**: Supports networking using IPv6. - **IPv6 Support**: Supports networking using IPv6.
- **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC. - **Multiple Protocol Types**: Supports communication between nodes using protocols such as WebSocket and QUIC.
- **Web Management Interface**: Provides a [web-based management](https://easytier.cn/web) interface for easy configuration and monitoring.
## Installation ## Installation
@@ -52,7 +56,7 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
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)**
@@ -60,7 +64,36 @@ EasyTier is a simple, safe and decentralized VPN networking solution implemented
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.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 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)** 6. **Install by Homebrew (For MacOS Only)**
@@ -200,20 +233,20 @@ Subnet proxy information will automatically sync to each node in the virtual net
### Networking without Public IP ### Networking without Public IP
EasyTier supports networking using shared public nodes. The currently deployed shared public node is ``tcp://public.easytier.top:11010``. 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. 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: Taking two nodes as an example, Node A executes:
```sh ```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010 sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
``` ```
Node B executes Node B executes
```sh ```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010 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. After the command is successfully executed, Node A can access Node B through the virtual IP 10.144.144.2.
@@ -286,7 +319,7 @@ Run you own public server cluster is exactly same as running an virtual network,
You can also join the official public server cluster with following command: You can also join the official public server cluster with following command:
``` ```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010 sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
``` ```
@@ -296,10 +329,8 @@ You can use ``easytier-core --help`` to view all configuration items
## Roadmap ## Roadmap
- [ ] Improve documentation and user guides. - [ ] Support features such TCP hole punching, KCP, FEC etc.
- [ ] Support features such as encryption, TCP hole punching, etc.
- [ ] Support iOS. - [ ] Support iOS.
- [ ] Support Web configuration management.
## Community and Contribution ## Community and Contribution
+38 -9
View File
@@ -8,7 +8,7 @@
[简体中文](/README_CN.md) | [English](/README.md) [简体中文](/README_CN.md) | [English](/README.md)
**请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。** **请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。**
一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。 一个简单、安全、去中心化的内网穿透 VPN 组网方案,使用 Rust 语言和 Tokio 框架实现。
@@ -31,6 +31,7 @@
- **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。 - **高可用性**:支持多路径和在检测到高丢包率或网络错误时切换到健康路径。
- **IPV6 支持**:支持利用 IPV6 组网。 - **IPV6 支持**:支持利用 IPV6 组网。
- **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。 - **多协议类型**: 支持使用 WebSocket、QUIC 等协议进行节点间通信。
- **Web 管理界面**:支持通过 [Web 界面](https://easytier.cn)管理节点。
## 安装 ## 安装
@@ -52,7 +53,7 @@
4. **通过Docker Compose安装** 4. **通过Docker Compose安装**
请访问 [EasyTier 官网](https://www.easytier.top/) 以查看完整的文档。 请访问 [EasyTier 官网](https://easytier.cn/) 以查看完整的文档。
5. **使用一键脚本安装 (仅适用于 Linux)** 5. **使用一键脚本安装 (仅适用于 Linux)**
@@ -60,7 +61,36 @@
wget -O /tmp/easytier.sh "https://raw.githubusercontent.com/EasyTier/EasyTier/main/script/install.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
``` ```
使用本脚本安装的 Easytier 可以使用脚本的 uninstall/update 对其卸载/升级 脚本支持以下命令和选项:
命令:
- `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)** 6. **使用 Homebrew 安装 (仅适用于 MacOS)**
@@ -199,20 +229,20 @@ sudo easytier-core --ipv4 10.144.144.2 -n 10.1.1.0/24
### 无公网IP组网 ### 无公网IP组网
EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.top:11010``。 EasyTier 支持共享公网节点进行组网。目前已部署共享的公网节点 ``tcp://public.easytier.cn:11010``。
使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。 使用共享节点时,需要每个入网节点提供相同的 ``--network-name`` 和 ``--network-secret`` 参数,作为网络的唯一标识。
以双节点为例,节点 A 执行: 以双节点为例,节点 A 执行:
```sh ```sh
sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010 sudo easytier-core -i 10.144.144.1 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
``` ```
节点 B 执行 节点 B 执行
```sh ```sh
sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -e tcp://public.easytier.top:11010 sudo easytier-core --ipv4 10.144.144.2 --network-name abc --network-secret abc -p tcp://public.easytier.cn:11010
``` ```
命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。 命令执行成功后,节点 A 即可通过虚拟 IP 10.144.144.2 访问节点 B。
@@ -289,7 +319,7 @@ connected_clients:
也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡: 也可以使用以下命令加入官方公共服务器集群,后续将实现公共服务器集群的节点间负载均衡:
``` ```
sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.top:11010 sudo easytier-core --network-name easytier --network-secret easytier -p tcp://public.easytier.cn:11010
``` ```
### 其他配置 ### 其他配置
@@ -299,9 +329,8 @@ sudo easytier-core --network-name easytier --network-secret easytier -p tcp://pu
## 路线图 ## 路线图
- [ ] 完善文档和用户指南。 - [ ] 完善文档和用户指南。
- [ ] 支持 TCP 打洞等特性。 - [ ] 支持 TCP 打洞、KCP、FEC 等特性。
- [ ] 支持 iOS。 - [ ] 支持 iOS。
- [ ] 支持 Web 配置管理。
## 社区和贡献 ## 社区和贡献
+16
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"
@@ -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;
}
}
+199
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);
}
}
@@ -0,0 +1,33 @@
#!/sbin/sh
#################
# Initialization
#################
umask 022
# echo before loading util_functions
ui_print() { echo "$1"; }
require_new_magisk() {
ui_print "********************************"
ui_print " Please install Magisk v20.4+! "
ui_print "********************************"
exit 1
}
#########################
# Load util_functions.sh
#########################
OUTFD=$2
ZIPFILE=$3
mount /data 2>/dev/null
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
. /data/adb/magisk/util_functions.sh
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
install_module
exit 0
@@ -0,0 +1,6 @@
# easytier_magisk版模块
magisk安装后重启
目录位置:/data/adb/modules/easytier_magisk
配置文件位置://data/adb/modules/easytier_magisk/config/config.toml
修改config.conf即可,修改后配置文件后去magisk app重新开关模块即可生效
@@ -0,0 +1,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
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
version=$(cat module.prop | grep 'version=' | awk -F '=' '{print $2}' | sed 's/ (.*//')
version='v'$(grep '^version =' ../../easytier/Cargo.toml | cut -d '"' -f 2)
if [ -z "$version" ]; then
echo "Error: 版本号不存在."
exit 1
fi
filename="easytier_magisk_${version}.zip"
echo $version
if [ -f "./easytier-core" ] && [ -f "./easytier-cli" ] && [ -f "./easytier-web" ]; then
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
else
wget -O "easytier_last.zip" https://github.com/EasyTier/EasyTier/releases/download/"$version"/easytier-linux-aarch64-"$version".zip
unzip -o easytier_last.zip -d ./
mv ./easytier-linux-aarch64/* ./
rm -rf ./easytier_last.zip
rm -rf ./easytier-linux-aarch64
zip -r -o -X "$filename" ./ -x '.git/*' -x '.github/*' -x 'folder/*' -x 'build.sh' -x 'magisk_update.json'
fi
@@ -0,0 +1,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
@@ -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 '记得重启'
@@ -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
@@ -0,0 +1,6 @@
{
"version": "v1.0",
"versionCode": 1,
"zipUrl": "",
"changelog": ""
}
@@ -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
@@ -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
@@ -0,0 +1,2 @@
nameserver 114.114.114.114
nameserver 223.5.5.5
@@ -0,0 +1,3 @@
MODDIR=${0%/*}
pkill easytier-core # 结束 easytier-core 进程
rm -rf $MODDIR/*
+5 -1
View File
@@ -18,7 +18,11 @@ cd ../tauri-plugin-vpnservice
pnpm install pnpm install
pnpm build pnpm build
cd ../easytier-gui cd ../easytier-web/frontend-lib
pnpm install
pnpm build
cd ../../easytier-gui
pnpm install pnpm install
pnpm tauri build pnpm tauri build
``` ```
+1
View File
@@ -113,3 +113,4 @@ event:
VpnPortalClientDisconnected: VPN门户客户端已断开连接 VpnPortalClientDisconnected: VPN门户客户端已断开连接
DhcpIpv4Changed: DHCP IPv4地址更改 DhcpIpv4Changed: DHCP IPv4地址更改
DhcpIpv4Conflicted: DHCP IPv4地址冲突 DhcpIpv4Conflicted: DHCP IPv4地址冲突
PortForwardAdded: 端口转发添加
+1
View File
@@ -112,3 +112,4 @@ event:
VpnPortalClientDisconnected: VpnPortalClientDisconnected VpnPortalClientDisconnected: VpnPortalClientDisconnected
DhcpIpv4Changed: DhcpIpv4Changed DhcpIpv4Changed: DhcpIpv4Changed
DhcpIpv4Conflicted: DhcpIpv4Conflicted DhcpIpv4Conflicted: DhcpIpv4Conflicted
PortForwardAdded: PortForwardAdded
+8 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "easytier-gui", "name": "easytier-gui",
"type": "module", "type": "module",
"version": "2.1.0", "version": "2.3.1",
"private": true, "private": true,
"packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4", "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4",
"scripts": { "scripts": {
@@ -13,7 +13,7 @@
"lint:fix": "eslint . --ignore-pattern src-tauri --fix" "lint:fix": "eslint . --ignore-pattern src-tauri --fix"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.2.1", "@primevue/themes": "4.3.3",
"@tauri-apps/plugin-autostart": "2.0.0", "@tauri-apps/plugin-autostart": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0", "@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-os": "2.0.0", "@tauri-apps/plugin-os": "2.0.0",
@@ -24,7 +24,7 @@
"easytier-frontend-lib": "workspace:*", "easytier-frontend-lib": "workspace:*",
"ip-num": "1.5.1", "ip-num": "1.5.1",
"pinia": "^2.2.4", "pinia": "^2.2.4",
"primevue": "^4.2.1", "primevue": "4.3.3",
"tauri-plugin-vpnservice-api": "workspace:*", "tauri-plugin-vpnservice-api": "workspace:*",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
@@ -32,19 +32,21 @@
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^3.7.3", "@antfu/eslint-config": "^3.7.3",
"@intlify/unplugin-vue-i18n": "^5.2.0", "@intlify/unplugin-vue-i18n": "^5.2.0",
"@primevue/auto-import-resolver": "^4.1.0", "@primevue/auto-import-resolver": "4.3.3",
"@tauri-apps/api": "2.1.0", "@tauri-apps/api": "2.1.0",
"@tauri-apps/cli": "2.1.0", "@tauri-apps/cli": "2.1.0",
"@types/default-gateway": "^7.2.2",
"@types/node": "^22.7.4", "@types/node": "^22.7.4",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vue-macros/volar": "0.30.5", "@vue-macros/volar": "0.30.5",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cidr-tools": "^11.0.2",
"default-gateway": "^7.2.2",
"eslint": "^9.12.0", "eslint": "^9.12.0",
"eslint-plugin-format": "^0.1.2", "eslint-plugin-format": "^0.1.2",
"internal-ip": "^8.0.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.13", "tailwindcss": "=3.4.17",
"typescript": "^5.6.2", "typescript": "^5.6.2",
"unplugin-auto-import": "^0.18.3", "unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4", "unplugin-vue-components": "^0.27.4",
+11 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "easytier-gui" name = "easytier-gui"
version = "2.1.0" version = "2.3.1"
description = "EasyTier GUI" description = "EasyTier GUI"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"
@@ -14,8 +14,16 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] } 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] [dependencies]
tauri = { version = "2.1", features = [ # wry 0.47 may crash on android, see https://github.com/EasyTier/EasyTier/issues/527
tauri = { version = "=2.0.6", features = [
"tray-icon", "tray-icon",
"image-png", "image-png",
"image-ico", "image-ico",
@@ -52,4 +60,4 @@ tauri-plugin-autostart = "2.0"
custom-protocol = ["tauri/custom-protocol"] custom-protocol = ["tauri/custom-protocol"]
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-single-instance = "2.0.0-rc.0" tauri-plugin-single-instance = "2.2.3"
+9
View File
@@ -1,3 +1,12 @@
fn main() { 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(); tauri_build::build();
} }
@@ -39,7 +39,7 @@
"vpnservice:allow-prepare-vpn", "vpnservice:allow-prepare-vpn",
"vpnservice:allow-start-vpn", "vpnservice:allow-start-vpn",
"vpnservice:allow-stop-vpn", "vpnservice:allow-stop-vpn",
"vpnservice:allow-register-listener", "vpnservice:allow-registerListener",
"os:default", "os:default",
"os:allow-os-type", "os:allow-os-type",
"os:allow-arch", "os:allow-arch",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

+10 -2
View File
@@ -89,6 +89,7 @@ fn get_os_hostname() -> Result<String, String> {
#[tauri::command] #[tauri::command]
fn set_logging_level(level: String) -> Result<(), String> { fn set_logging_level(level: String) -> Result<(), String> {
#[allow(static_mut_refs)]
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() }; let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
sender.send(level).map_err(|e| e.to_string())?; sender.send(level).map_err(|e| e.to_string())?;
Ok(()) Ok(())
@@ -107,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>) { fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or_default() { if window.is_visible().unwrap_or_default() {
if window.is_minimized().unwrap_or_default() {
let _ = window.unminimize();
let _ = window.set_focus();
} else {
let _ = window.hide(); let _ = window.hide();
}
} else { } else {
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
@@ -141,7 +147,6 @@ pub fn run() {
process::exit(0); process::exit(0);
} }
#[cfg(not(target_os = "android"))]
utils::setup_panic_handler(); utils::setup_panic_handler();
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
@@ -189,7 +194,10 @@ pub fn run() {
let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else { let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else {
return Ok(()); return Ok(());
}; };
unsafe { LOGGER_LEVEL_SENDER.replace(logger_reinit) }; #[allow(static_mut_refs)]
unsafe {
LOGGER_LEVEL_SENDER.replace(logger_reinit)
};
// for tray icon, menu need to be built in js // for tray icon, menu need to be built in js
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
+1 -1
View File
@@ -17,7 +17,7 @@
"createUpdaterArtifacts": false "createUpdaterArtifacts": false
}, },
"productName": "easytier-gui", "productName": "easytier-gui",
"version": "2.1.0", "version": "2.3.1",
"identifier": "com.kkrainbow.easytier", "identifier": "com.kkrainbow.easytier",
"plugins": {}, "plugins": {},
"app": { "app": {
+14 -4
View File
@@ -49,9 +49,9 @@ async function doStartVpn(ipv4Addr: string, cidr: number, routes: string[]) {
return return
} }
console.log('start vpn') console.log('start vpn service', ipv4Addr, cidr, routes)
const start_ret = await start_vpn({ const start_ret = await start_vpn({
ipv4Addr: `${ipv4Addr}`, ipv4Addr: `${ipv4Addr}/${cidr}`,
routes, routes,
disallowedApplications: ['com.kkrainbow.easytier'], disallowedApplications: ['com.kkrainbow.easytier'],
mtu: 1300, mtu: 1300,
@@ -113,6 +113,7 @@ function getRoutesForVpn(routes: Route[]): string[] {
} }
async function onNetworkInstanceChange() { async function onNetworkInstanceChange() {
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
const insts = networkStore.networkInstanceIds const insts = networkStore.networkInstanceIds
if (!insts) { if (!insts) {
await doStopVpn() await doStopVpn()
@@ -131,6 +132,14 @@ async function onNetworkInstanceChange() {
return 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 let network_length = curNetworkInfo?.my_node_info?.virtual_ipv4.network_length
if (!network_length) { if (!network_length) {
network_length = 24 network_length = 24
@@ -142,7 +151,7 @@ async function onNetworkInstanceChange() {
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes) const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
if (ipChanged || routesChanged) { if (ipChanged || routesChanged) {
console.log('virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip) console.info('vpn service virtual ip changed', JSON.stringify(curVpnStatus), virtual_ip)
try { try {
await doStopVpn() await doStopVpn()
} }
@@ -154,7 +163,7 @@ async function onNetworkInstanceChange() {
await doStartVpn(virtual_ip, 24, routes) await doStartVpn(virtual_ip, 24, routes)
} }
catch (e) { catch (e) {
console.error('start vpn failed, clear all network insts.', e) console.error('start vpn service failed, clear all network insts.', e)
networkStore.clearNetworkInstances() networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds) await retainNetworkInstance(networkStore.networkInstanceIds)
} }
@@ -175,6 +184,7 @@ async function watchNetworkInstance() {
} }
subscribe_running = false subscribe_running = false
}) })
console.error('vpn service watch network instance')
} }
export async function initMobileVpnService() { export async function initMobileVpnService() {
+2 -2
View File
@@ -50,8 +50,8 @@ async function main() {
darkModeSelector: 'system', darkModeSelector: 'system',
cssLayer: { cssLayer: {
name: 'primevue', name: 'primevue',
order: 'tailwind-base, primevue, tailwind-utilities' order: 'tailwind-base, primevue, tailwind-utilities',
} },
}, },
}, },
}) })
+5
View File
@@ -250,7 +250,12 @@ onBeforeMount(async () => {
onMounted(async () => { onMounted(async () => {
if (type() === 'android') { if (type() === 'android') {
try {
await initMobileVpnService() await initMobileVpnService()
console.error("easytier init vpn service done")
} catch (e: any) {
console.error("easytier init vpn service failed", e)
}
} }
}) })
+7
View File
@@ -128,6 +128,13 @@ export const useNetworkStore = defineStore('networkStore', {
} }
this.saveAutoStartInstIdsToLocalStorage() 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
},
}, },
}) })
+8
View File
@@ -45,3 +45,11 @@
border-radius: 4px; border-radius: 4px;
background-color: #0000005d; background-color: #0000005d;
} }
.p-password {
width: 100%;
}
.p-password>input {
width: 100%;
}
+18 -2
View File
@@ -1,9 +1,11 @@
import { networkInterfaces } from 'node:os'
import path from 'node:path' import path from 'node:path'
import process from 'node:process' import process from 'node:process'
import VueI18n from '@intlify/unplugin-vue-i18n/vite' import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver' import { PrimeVueResolver } from '@primevue/auto-import-resolver'
import Vue from '@vitejs/plugin-vue' import Vue from '@vitejs/plugin-vue'
import { internalIpV4Sync } from 'internal-ip' import { containsCidr, parseCidr } from 'cidr-tools'
import { gateway4sync } from 'default-gateway'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import VueMacros from 'unplugin-vue-macros/vite' import VueMacros from 'unplugin-vue-macros/vite'
@@ -13,6 +15,20 @@ import { defineConfig } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools' import VueDevTools from 'vite-plugin-vue-devtools'
import Layouts from 'vite-plugin-vue-layouts' import Layouts from 'vite-plugin-vue-layouts'
function findIp(gateway: string) {
// Look for the matching interface in all local interfaces
console.log('gateway', gateway)
for (const addresses of Object.values(networkInterfaces())) {
if (!addresses)
continue
for (const { cidr } of addresses) {
if (cidr && containsCidr(cidr, gateway)) {
return parseCidr(cidr).ip
}
}
}
}
const host = process.env.TAURI_DEV_HOST const host = process.env.TAURI_DEV_HOST
// https://vitejs.dev/config/ // https://vitejs.dev/config/
@@ -100,7 +116,7 @@ export default defineConfig(async () => ({
hmr: host hmr: host
? { ? {
protocol: 'ws', protocol: 'ws',
host: internalIpV4Sync(), host: findIp(gateway4sync().gateway),
port: 1430, port: 1430,
} }
: undefined, : undefined,
+1 -1
View File
@@ -8,7 +8,7 @@ repository = "https://github.com/EasyTier/EasyTier"
authors = ["kkrainbow"] authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"] keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"] categories = ["network-programming", "command-line-utilities"]
rust-version = "1.77.0" rust-version = "1.84.0"
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"
+24 -3
View File
@@ -1,7 +1,8 @@
[package] [package]
name = "easytier-web" name = "easytier-web"
version = "0.1.0" version = "2.3.1"
edition = "2021" edition = "2021"
description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server."
[dependencies] [dependencies]
easytier = { path = "../easytier" } easytier = { path = "../easytier" }
@@ -17,13 +18,18 @@ axum = { version = "0.7", features = ["macros"] }
axum-login = { version = "0.16" } axum-login = { version = "0.16" }
password-auth = { version = "1.0.0" } password-auth = { version = "1.0.0" }
axum-messages = "0.7.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-sqlx-store = { version = "0.14.1", features = ["sqlite"] }
tower-sessions = { version = "0.13.0", default-features = false, features = [ tower-sessions = { version = "0.13.0", default-features = false, features = [
"signed", "signed",
] } ] }
tower-http = { version = "0.6", features = ["cors", "compression-full"] } tower-http = { version = "0.6", features = ["cors", "compression-full"] }
sqlx = { version = "0.8", features = ["sqlite"] } sqlx = { version = "0.8", features = ["sqlite"] }
sea-orm = { version = "1.1", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] } sea-orm = { version = "1.1", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"macros",
] }
sea-orm-migration = { version = "1.1" } sea-orm-migration = { version = "1.1" }
@@ -31,11 +37,13 @@ sea-orm-migration = { version = "1.1" }
rust-embed = { version = "8.5.0", features = ["debug-embed"] } rust-embed = { version = "8.5.0", features = ["debug-embed"] }
base64 = "0.22" base64 = "0.22"
rand = "0.8" rand = "0.8"
image = {version="0.24", default-features = false, features = ["png"]} image = { version = "0.24", default-features = false, features = ["png"] }
rusttype = "0.9.3" rusttype = "0.9.3"
imageproc = "0.23.0" imageproc = "0.23.0"
rust-i18n = "3"
sys-locale = "0.3"
clap = { version = "4.4.8", features = [ clap = { version = "4.4.8", features = [
"string", "string",
"unicode", "unicode",
@@ -50,3 +58,16 @@ uuid = { version = "1.5.0", features = [
"macro-diagnostics", "macro-diagnostics",
"serde", "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
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();
}
}
+3 -3
View File
@@ -18,14 +18,14 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.2.1", "@primevue/themes": "4.3.3",
"@vueuse/core": "^11.1.0", "@vueuse/core": "^11.1.0",
"aura": "link:@primevue\\themes\\aura", "aura": "link:@primevue\\themes\\aura",
"axios": "^1.7.7", "axios": "^1.7.7",
"floating-vue": "^5.2", "floating-vue": "^5.2",
"ip-num": "1.5.1", "ip-num": "1.5.1",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.2.1", "primevue": "4.3.3",
"tailwindcss-primeui": "^0.3.4", "tailwindcss-primeui": "^0.3.4",
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"uuid": "^11.0.2", "uuid": "^11.0.2",
@@ -40,7 +40,7 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2", "postcss-nested": "^7.0.2",
"tailwindcss": "^3.4.14", "tailwindcss": "=3.4.17",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"vite": "^5.4.10", "vite": "^5.4.10",
"vite-plugin-dts": "^4.3.0", "vite-plugin-dts": "^4.3.0",
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import InputGroup from 'primevue/inputgroup' import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon' import InputGroupAddon from 'primevue/inputgroupaddon'
import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button } from 'primevue' import { SelectButton, Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password } from 'primevue'
import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network' import { DEFAULT_NETWORK_CONFIG, NetworkConfig, NetworkingMethod } from '../types/network'
import { defineProps, defineEmits, ref, } from 'vue' import { defineProps, defineEmits, ref, } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -120,13 +120,53 @@ function searchListenerSuggestions(e: { query: string }) {
listenerSuggestions.value = ret 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> </script>
<template> <template>
<div class="frontend-lib"> <div class="frontend-lib">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="w-10/12 self-center "> <div class="w-11/12 self-center ">
<Panel :header="t('basic_settings')"> <Panel :header="t('basic_settings')">
<div class="flex flex-col gap-y-2"> <div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
@@ -159,8 +199,8 @@ function searchListenerSuggestions(e: { query: string }) {
</div> </div>
<div class="flex flex-col gap-2 basis-5/12 grow"> <div class="flex flex-col gap-2 basis-5/12 grow">
<label for="network_secret">{{ t('network_secret') }}</label> <label for="network_secret">{{ t('network_secret') }}</label>
<InputText id="network_secret" v-model="curNetwork.network_secret" <Password id="network_secret" v-model="curNetwork.network_secret"
aria-describedby="network_secret-help" /> aria-describedby="network_secret-help" toggleMask :feedback="false"/>
</div> </div>
</div> </div>
@@ -188,11 +228,18 @@ function searchListenerSuggestions(e: { query: string }) {
<Panel :header="t('advanced_settings')" toggleable collapsed> <Panel :header="t('advanced_settings')" toggleable collapsed>
<div class="flex flex-col gap-y-2"> <div class="flex flex-col gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap"> <div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 basis-5/12 grow"> <div class="flex flex-col gap-2 basis-5/12 grow">
<div class="flex items-center"> <label> {{ t('flags_switch') }} </label>
<Checkbox v-model="curNetwork.latency_first" input-id="use_latency_first" :binary="true" /> <div class="flex flex-row flex-wrap">
<label for="use_latency_first" class="ml-2"> {{ t('use_latency_first') }} </label>
<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>
</div> </div>
@@ -220,7 +267,8 @@ function searchListenerSuggestions(e: { query: string }) {
<ToggleButton v-model="curNetwork.enable_vpn_portal" on-icon="pi pi-check" off-icon="pi pi-times" <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" /> :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 v-if="curNetwork.enable_vpn_portal" class="items-center flex flex-row gap-x-4">
<div class="min-w-64"> <div class="flex flex-row gap-x-9 flex-wrap w-full">
<div class="flex flex-col gap-2 basis-8/12 grow">
<InputGroup> <InputGroup>
<InputText v-model="curNetwork.vpn_portal_client_network_addr" <InputText v-model="curNetwork.vpn_portal_client_network_addr"
:placeholder="t('vpn_portal_client_network')" /> :placeholder="t('vpn_portal_client_network')" />
@@ -228,9 +276,11 @@ function searchListenerSuggestions(e: { query: string }) {
<span>/{{ curNetwork.vpn_portal_client_network_len }}</span> <span>/{{ curNetwork.vpn_portal_client_network_len }}</span>
</InputGroupAddon> </InputGroupAddon>
</InputGroup> </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" <InputNumber v-model="curNetwork.vpn_portal_listen_port" :allow-empty="false" :format="false"
:min="0" :max="65535" class="w-8/12" fluid /> :min="0" :max="65535" fluid />
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -261,6 +311,97 @@ function searchListenerSuggestions(e: { query: string }) {
:placeholder="t('dev_name_placeholder')" /> :placeholder="t('dev_name_placeholder')" />
</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">
<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> </div>
</Panel> </Panel>
@@ -106,6 +106,10 @@ function ipFormat(info: PeerRoutePair) {
return ip ? `${IPv4.fromNumber(ip.address.addr)}/${ip.network_length}` : '' 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(() => { const myNodeInfo = computed(() => {
if (!props.curNetworkInst) if (!props.curNetworkInst)
return {} as NodeInfo return {} as NodeInfo
@@ -303,7 +307,8 @@ function showEventLogs() {
<template> <template>
<div class="frontend-lib"> <div class="frontend-lib">
<Dialog v-model:visible="dialogVisible" modal :header="t(dialogHeader)" class="w-2/3 h-auto"> <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'"> <ScrollPanel v-if="dialogHeader === 'vpn_portal_config'">
<pre>{{ dialogContent }}</pre> <pre>{{ dialogContent }}</pre>
</ScrollPanel> </ScrollPanel>
@@ -407,6 +412,7 @@ function showEventLogs() {
</template> </template>
</Column> </Column>
<Column :field="routeCost" :header="t('route_cost')" /> <Column :field="routeCost" :header="t('route_cost')" />
<Column :field="tunnelProto" :header="t('tunnel_proto')" />
<Column :field="latencyMs" :header="t('latency')" /> <Column :field="latencyMs" :header="t('latency')" />
<Column :field="txBytes" :header="t('upload_bytes')" /> <Column :field="txBytes" :header="t('upload_bytes')" />
<Column :field="rxBytes" :header="t('download_bytes')" /> <Column :field="rxBytes" :header="t('download_bytes')" />
@@ -64,11 +64,89 @@ event_log: 事件日志
peer_info: 节点信息 peer_info: 节点信息
hostname: 主机名 hostname: 主机名
route_cost: 路由 route_cost: 路由
tunnel_proto: 协议
latency: 延迟 latency: 延迟
upload_bytes: 上传 upload_bytes: 上传
download_bytes: 下载 download_bytes: 下载
loss_rate: 丢包率 loss_rate: 丢包率
flags_switch: 功能开关
latency_first: 开启延迟优先模式
latency_first_help: 忽略中转跳数,选择总延迟最低的路径
use_smoltcp: 使用用户态协议栈
use_smoltcp_help: 使用用户态 TCP/IP 协议栈,避免操作系统防火墙问题导致无法子网代理 / KCP代理。
enable_kcp_proxy: 启用 KCP 代理
enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟,提升传输速度。
disable_kcp_input: 禁用 KCP 输入
disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。
disable_p2p: 禁用 P2P
disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。
bind_device: 仅使用物理网卡
bind_device_help: 仅使用物理网卡,避免 EasyTier 通过其他虚拟网建立连接。
no_tun: 无 TUN 模式
no_tun_help: 不使用 TUN 网卡,适合无管理员权限时使用。本节点仅允许被访问。访问其他节点需要使用 SOCK5
enable_exit_node: 启用出口节点
enable_exit_node_help: 允许此节点成为出口节点
relay_all_peer_rpc: 转发RPC包
relay_all_peer_rpc_help: |
允许转发所有对等节点的RPC数据包,即使对等节点不在转发网络白名单中。
这可以帮助白名单外网络中的对等节点建立P2P连接。
multi_thread: 启用多线程
multi_thread_help: 使用多线程运行时
proxy_forward_by_system: 系统转发
proxy_forward_by_system_help: 通过系统内核转发子网代理数据包,禁用内置NAT
disable_encryption: 禁用加密
disable_encryption_help: 禁用对等节点通信的加密,默认为false,必须与对等节点相同
disable_udp_hole_punching: 禁用UDP打洞
disable_udp_hole_punching_help: 禁用UDP打洞功能
enable_magic_dns: 启用魔法DNS
enable_magic_dns_help: |
启用魔法DNS,允许通过EasyTier的DNS服务器访问其他节点的虚拟IPv4地址, 如 node1.et.net。
enable_private_mode: 启用私有模式
enable_private_mode_help: |
启用私有模式,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转。
relay_network_whitelist: 网络白名单
relay_network_whitelist_help: |
仅转发白名单网络的流量,支持通配符字符串。多个网络名称间可以使用英文空格间隔。
如果该参数为空,则禁用转发。默认允许所有网络。
例如:'*'(所有网络),'def*'(以def为前缀的网络),'net1 net2'(只允许net1和net2
manual_routes: 自定义路由
manual_routes_help: 手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16
socks5: socks5服务器
socks5_help: |
启用 socks5 服务器,允许 socks5 客户端访问虚拟网络. 格式: <端口>,例如:1080
exit_nodes: 出口节点列表
exit_nodes_help: 转发所有流量的出口节点,虚拟IPv4地址,优先级由列表顺序决定
mtu: MTU
mtu_help: |
TUN设备的MTU,默认为非加密时为1380,加密时为1360。范围:400-1380
mtu_placeholder: 留空为默认值1380
mapped_listeners: 监听映射
mapped_listeners_help: |
手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。
例如:tcp://123.123.123.123:11223,可以指定多个。
status: status:
version: 内核版本 version: 内核版本
local: 本机 local: 本机
@@ -113,3 +191,4 @@ event:
VpnPortalClientDisconnected: VPN门户客户端已断开连接 VpnPortalClientDisconnected: VPN门户客户端已断开连接
DhcpIpv4Changed: DHCP IPv4地址更改 DhcpIpv4Changed: DHCP IPv4地址更改
DhcpIpv4Conflicted: DHCP IPv4地址冲突 DhcpIpv4Conflicted: DHCP IPv4地址冲突
PortForwardAdded: 端口转发添加
@@ -62,12 +62,91 @@ show_event_log: Show Event Log
event_log: Event Log event_log: Event Log
peer_info: Peer Info peer_info: Peer Info
route_cost: Route Cost route_cost: Route Cost
tunnel_proto: Protocol
hostname: Hostname hostname: Hostname
latency: Latency latency: Latency
upload_bytes: Upload upload_bytes: Upload
download_bytes: Download download_bytes: Download
loss_rate: Loss Rate loss_rate: Loss Rate
flags_switch: Feature Switch
latency_first: Enable Latency-First Mode
latency_first_help: Ignore hop count and select the path with the lowest total latency
use_smoltcp: Use User-Space Protocol Stack
use_smoltcp_help: Use a user-space TCP/IP stack to avoid issues with operating system firewalls blocking subnet or KCP proxy functionality.
enable_kcp_proxy: Enable KCP Proxy
enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and boost transmission speed.
disable_kcp_input: Disable KCP Input
disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP.
disable_p2p: Disable P2P
disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server.
bind_device: Bind to Physical Device Only
bind_device_help: Use only the physical network interface to prevent EasyTier from connecting via virtual networks.
no_tun: No TUN Mode
no_tun_help: Do not use a TUN interface, suitable for environments without administrator privileges. This node is only accessible; accessing other nodes requires SOCKS5.
enable_exit_node: Enable Exit Node
enable_exit_node_help: Allow this node to be an exit node
relay_all_peer_rpc: Relay RPC Packets
relay_all_peer_rpc_help: |
Relay all peer rpc packets, even if the peer is not in the relay network whitelist.
This can help peers not in relay network whitelist to establish p2p connection.
multi_thread: Multi Thread
multi_thread_help: Use multi-thread runtime
proxy_forward_by_system: System Forward
proxy_forward_by_system_help: Forward packet to proxy networks via system kernel, disable internal nat for network proxy
disable_encryption: Disable Encryption
disable_encryption_help: Disable encryption for peers communication, default is false, must be same with peers
disable_udp_hole_punching: Disable UDP Hole Punching
disable_udp_hole_punching_help: Disable udp hole punching
enable_magic_dns: Enable Magic DNS
enable_magic_dns_help: |
Enable magic dns, all nodes in the network can access each other by domain name, e.g.: node1.et.net.
enable_private_mode: Enable Private Mode
enable_private_mode_help: |
Enable private mode, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node.
relay_network_whitelist: Network Whitelist
relay_network_whitelist_help: |
Only forward traffic from the whitelist networks, supporting wildcard strings, multiple network names can be separated by spaces.
If this parameter is empty, forwarding is disabled. By default, all networks are allowed.
e.g.: '*' (all networks), 'def*' (networks with the prefix 'def'), 'net1 net2' (only allow net1 and net2)
manual_routes: Manual Route
manual_routes_help: |
Assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.:192.168.0.0/16
socks5: Socks5 Server
socks5_help: |
Enable socks5 server, allow socks5 client to access virtual network. format: <port>, e.g.: 1080
exit_nodes: Exit Nodes
exit_nodes_help: Exit nodes to forward all traffic to, a virtual ipv4 address, priority is determined by the order of the list
mtu: MTU
mtu_help: |
MTU of the TUN device, default is 1380 for non-encryption, 1360 for encryption. Range:400-1380
mtu_placeholder: Leave blank as default value 1380
mapped_listeners: Map Listeners
mapped_listeners_help: |
Manually specify the public address of the listener, other nodes can use this address to connect to this node.
e.g.: tcp://123.123.123.123:11223, can specify multiple.
status: status:
version: Version version: Version
local: Local local: Local
@@ -112,3 +191,4 @@ event:
VpnPortalClientDisconnected: VpnPortalClientDisconnected VpnPortalClientDisconnected: VpnPortalClientDisconnected
DhcpIpv4Changed: DhcpIpv4Changed DhcpIpv4Changed: DhcpIpv4Changed
DhcpIpv4Conflicted: DhcpIpv4Conflicted DhcpIpv4Conflicted: DhcpIpv4Conflicted
PortForwardAdded: PortForwardAdded
@@ -1,5 +1,7 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { Md5 } from 'ts-md5' import { Md5 } from 'ts-md5'
import { UUID } from './utils';
import { NetworkConfig } from '../types/network';
export interface ValidateConfigResponse { export interface ValidateConfigResponse {
toml_config: string; toml_config: string;
@@ -31,6 +33,20 @@ export interface Summary {
device_count: number; device_count: number;
} }
export interface ListNetworkInstanceIdResponse {
running_inst_ids: Array<UUID>,
disabled_inst_ids: Array<UUID>,
}
export interface GenerateConfigRequest {
config: NetworkConfig;
}
export interface GenerateConfigResponse {
toml_config?: string;
error?: string;
}
export class ApiClient { export class ApiClient {
private client: AxiosInstance; private client: AxiosInstance;
private authFailedCb: Function | undefined; private authFailedCb: Function | undefined;
@@ -141,6 +157,17 @@ export class ApiClient {
return response.machines; return response.machines;
} }
public async list_deivce_instance_ids(machine_id: string): Promise<ListNetworkInstanceIdResponse> {
const response = await this.client.get<any, ListNetworkInstanceIdResponse>('/machines/' + machine_id + '/networks');
return response;
}
public async update_device_instance_state(machine_id: string, inst_id: string, disabled: boolean): Promise<undefined> {
await this.client.put<string>('/machines/' + machine_id + '/networks/' + inst_id, {
disabled: disabled,
});
}
public async get_network_info(machine_id: string, inst_id: string): Promise<any> { public async get_network_info(machine_id: string, inst_id: string): Promise<any> {
const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id); const response = await this.client.get<any, Record<string, any>>('/machines/' + machine_id + '/networks/info/' + inst_id);
return response.info.map; return response.info.map;
@@ -176,6 +203,18 @@ export class ApiClient {
public captcha_url() { public captcha_url() {
return this.client.defaults.baseURL + '/auth/captcha'; return this.client.defaults.baseURL + '/auth/captcha';
} }
public async generate_config(config: GenerateConfigRequest): Promise<GenerateConfigResponse> {
try {
const response = await this.client.post<any, GenerateConfigResponse>('/generate-config', config);
return response;
} catch (error) {
if (error instanceof AxiosError) {
return { error: error.response?.data };
}
return { error: 'Unknown error: ' + error };
}
}
} }
export default ApiClient; export default ApiClient;
+2 -2
View File
@@ -1,8 +1,6 @@
@import 'primeicons/primeicons.css'; @import 'primeicons/primeicons.css';
@import 'floating-vue/dist/style.css'; @import 'floating-vue/dist/style.css';
.frontend-lib {
@layer tailwind-base, primevue, tailwind-utilities; @layer tailwind-base, primevue, tailwind-utilities;
@layer tailwind-base { @layer tailwind-base {
@@ -51,4 +49,6 @@
background-color: #0000005d; background-color: #0000005d;
} }
.v-popper__inner {
white-space: pre-wrap;
} }
+57 -1
View File
@@ -35,6 +35,36 @@ export interface NetworkConfig {
latency_first: boolean latency_first: boolean
dev_name: string dev_name: string
use_smoltcp?: boolean
enable_kcp_proxy?: boolean
disable_kcp_input?: boolean
disable_p2p?: boolean
bind_device?: boolean
no_tun?: boolean
enable_exit_node?: boolean
relay_all_peer_rpc?: boolean
multi_thread?: boolean
proxy_forward_by_system?: boolean
disable_encryption?: boolean
disable_udp_hole_punching?: boolean
enable_relay_network_whitelist?: boolean
relay_network_whitelist: string[]
enable_manual_routes: boolean
routes: string[]
exit_nodes: string[]
enable_socks5?: boolean
socks5_port: number
mtu: number | null
mapped_listeners: string[]
enable_magic_dns?: boolean
enable_private_mode?: boolean
} }
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
@@ -67,8 +97,32 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
'wg://0.0.0.0:11011', 'wg://0.0.0.0:11011',
], ],
rpc_port: 0, rpc_port: 0,
latency_first: true, latency_first: false,
dev_name: '', dev_name: '',
use_smoltcp: false,
enable_kcp_proxy: false,
disable_kcp_input: false,
disable_p2p: false,
bind_device: true,
no_tun: false,
enable_exit_node: false,
relay_all_peer_rpc: false,
multi_thread: true,
proxy_forward_by_system: false,
disable_encryption: false,
disable_udp_hole_punching: false,
enable_relay_network_whitelist: false,
relay_network_whitelist: [],
enable_manual_routes: false,
routes: [],
exit_nodes: [],
enable_socks5: false,
socks5_port: 1080,
mtu: null,
mapped_listeners: [],
enable_magic_dns: false,
enable_private_mode: false,
} }
} }
@@ -215,4 +269,6 @@ export enum EventType {
DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null DhcpIpv4Changed = 'DhcpIpv4Changed', // ipv4 | null, ipv4 | null
DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null DhcpIpv4Conflicted = 'DhcpIpv4Conflicted', // ipv4 | null
PortForwardAdded = 'PortForwardAdded', // PortForwardConfigPb
} }
+1
View File
@@ -5,6 +5,7 @@
<link rel="icon" type="image/png" href="/easytier.png" /> <link rel="icon" type="image/png" href="/easytier.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EasyTier Dashboard</title> <title>EasyTier Dashboard</title>
<script src="/api_meta.js"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+3 -3
View File
@@ -9,11 +9,11 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@primevue/themes": "^4.2.1", "@primevue/themes": "4.3.3",
"aura": "link:@primevue/themes/aura", "aura": "link:@primevue/themes/aura",
"axios": "^1.7.7", "axios": "^1.7.7",
"easytier-frontend-lib": "workspace:*", "easytier-frontend-lib": "workspace:*",
"primevue": "^4.2.1", "primevue": "4.3.3",
"tailwindcss-primeui": "^0.3.4", "tailwindcss-primeui": "^0.3.4",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-router": "4" "vue-router": "4"
@@ -23,7 +23,7 @@
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.14", "tailwindcss": "=3.4.17",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^5.4.10", "vite": "^5.4.10",
"vite-plugin-singlefile": "^2.0.3", "vite-plugin-singlefile": "^2.0.3",
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { NetworkTypes } from 'easytier-frontend-lib';
import {computed, ref} from 'vue';
import { Api } from 'easytier-frontend-lib'
import {AutoComplete, Divider} from "primevue";
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
const apiHost = ref<string>(getInitialApiHost())
const apiHostSuggestions = ref<Array<string>>([])
const apiHostSearch = async (event: { query: string }) => {
apiHostSuggestions.value = [];
let hosts = cleanAndLoadApiHosts();
if (event.query) {
apiHostSuggestions.value.push(event.query);
}
hosts.forEach((host) => {
apiHostSuggestions.value.push(host.value);
});
}
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
saveApiHost(apiHost.value)
api.value?.generate_config({
config: config
}).then((res) => {
if (res.error) {
toml_config.value = res.error;
} else if (res.toml_config) {
toml_config.value = res.toml_config;
} else {
toml_config.value = "Api server returned an unexpected response";
}
});
};
</script>
<template>
<div class="flex items-center justify-center m-5">
<div class="sm:block md:flex w-full">
<div class="sm:w-full md:w-1/2 p-4">
<div class="flex flex-col">
<div class="w-11/12 self-center ">
<label>ApiHost</label>
<AutoComplete id="api-host" v-model="apiHost" dropdown :suggestions="apiHostSuggestions"
@complete="apiHostSearch" class="w-full" />
<Divider />
</div>
</div>
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
</div>
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
</div>
</div>
</div>
</template>
@@ -27,7 +27,7 @@ const loadDevices = async () => {
public_ip: device.client_url, public_ip: device.client_url,
running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)), running_network_instances: device.info?.running_network_instances.map((instance: any) => Utils.UuidToStr(instance)),
running_network_count: device.info?.running_network_instances.length, running_network_count: device.info?.running_network_instances.length,
report_time: device.info?.report_time, report_time: new Date(device.info?.report_time).toLocaleString(),
easytier_version: device.info?.easytier_version, easytier_version: device.info?.easytier_version,
machine_id: Utils.UuidToStr(device.info?.machine_id), machine_id: Utils.UuidToStr(device.info?.machine_id),
}); });
@@ -102,7 +102,7 @@ const selectedDeviceHostname = computed<string | undefined>(() => {
</DataTable> </DataTable>
<Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right" <Drawer v-model:visible="deviceManageVisible" :header="`Manage ${selectedDeviceHostname}`" position="right"
class="w-1/2 min-w-96"> :baseZIndex=1000 class="w-3/5 min-w-96">
<RouterView v-slot="{ Component }"> <RouterView v-slot="{ Component }">
<component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" /> <component :is="Component" :api="api" :deviceList="deviceList" @update="loadDevices" />
</RouterView> </RouterView>
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast } from 'primevue'; import {Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider} from 'primevue';
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib'; import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
const props = defineProps<{ const props = defineProps<{
@@ -27,15 +27,24 @@ const deviceInfo = computed<Utils.DeviceInfo | undefined | null>(() => {
return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null; return deviceId.value ? props.deviceList?.find((device) => device.machine_id === deviceId.value) : null;
}); });
const configFile = ref();
const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null); const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
const isEditing = ref(false); const isEditing = ref(false);
const showCreateNetworkDialog = ref(false); const showCreateNetworkDialog = ref(false);
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG()); const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
const instanceIdList = computed(() => { const instanceIdList = computed(() => {
let insts = deviceInfo.value?.running_network_instances || []; let insts = new Set(deviceInfo.value?.running_network_instances || []);
let options = insts.map((instance: string) => { let t = listInstanceIdResponse.value;
if (t) {
t.running_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
t.disabled_inst_ids.forEach((u) => insts.add(Utils.UuidToStr(u)));
}
let options = Array.from(insts).map((instance: string) => {
return { uuid: instance }; return { uuid: instance };
}); });
return options; return options;
@@ -51,6 +60,53 @@ const selectedInstanceId = computed({
} }
}); });
const needShowNetworkStatus = computed(() => {
if (!selectedInstanceId.value) {
// nothing selected
return false;
}
if (networkIsDisabled.value) {
// network is disabled
return false;
}
return true;
})
const networkIsDisabled = computed(() => {
if (!selectedInstanceId.value) {
return false;
}
return listInstanceIdResponse.value?.disabled_inst_ids.map(Utils.UuidToStr).includes(selectedInstanceId.value?.uuid);
});
watch(selectedInstanceId, async (newVal, oldVal) => {
if (newVal?.uuid !== oldVal?.uuid && networkIsDisabled.value) {
await loadDisabledNetworkConfig();
}
});
const disabledNetworkConfig = ref<NetworkTypes.NetworkConfig | undefined>(undefined);
const loadDisabledNetworkConfig = async () => {
disabledNetworkConfig.value = undefined;
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
let ret = await props.api?.get_network_config(deviceId.value, selectedInstanceId.value.uuid);
disabledNetworkConfig.value = ret;
}
const updateNetworkState = async (disabled: boolean) => {
if (!deviceId.value || !selectedInstanceId.value) {
return;
}
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
await loadNetworkInstanceIds();
}
const confirm = useConfirm(); const confirm = useConfirm();
const confirmDeleteNetwork = (event: any) => { const confirmDeleteNetwork = (event: any) => {
confirm.require({ confirm.require({
@@ -104,6 +160,7 @@ const createNewNetwork = async () => {
const newNetwork = () => { const newNetwork = () => {
newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG(); newNetworkConfig.value = NetworkTypes.DEFAULT_NETWORK_CONFIG();
newNetworkConfig.value.hostname = deviceInfo.value?.hostname;
isEditing.value = false; isEditing.value = false;
showCreateNetworkDialog.value = true; showCreateNetworkDialog.value = true;
} }
@@ -128,6 +185,15 @@ const editNetwork = async () => {
} }
} }
const loadNetworkInstanceIds = async () => {
if (!deviceId.value) {
return;
}
listInstanceIdResponse.value = await props.api?.list_deivce_instance_ids(deviceId.value);
console.debug("loadNetworkInstanceIds", listInstanceIdResponse.value);
}
const loadDeviceInfo = async () => { const loadDeviceInfo = async () => {
if (!deviceId.value || !instanceId.value) { if (!deviceId.value || !instanceId.value) {
return; return;
@@ -144,9 +210,68 @@ const loadDeviceInfo = async () => {
} as NetworkTypes.NetworkInstance; } as NetworkTypes.NetworkInstance;
} }
const exportConfig = async () => {
if (!deviceId.value || !instanceId.value) {
toast.add({ severity: 'error', summary: 'Error', detail: 'No network instance selected', life: 2000 });
return;
}
try {
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
delete ret.instance_id;
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
} catch (e: any) {
console.error(e);
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
return;
}
}
const importConfig = () => {
configFile.value.click();
}
const handleFileUpload = (event: Event) => {
const files = (event.target as HTMLInputElement).files;
const file = files ? files[0] : null;
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
let str = e.target?.result?.toString();
if(str){
const config = JSON.parse(str);
if(config === null || typeof config !== "object"){
throw new Error();
}
Object.assign(newNetworkConfig.value, config);
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
}
} catch (error) {
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
}
configFile.value.value = null;
}
reader.readAsText(file);
}
}
const exportJsonFile = (context: string, name: string) => {
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
let link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', name);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
let periodFunc = new Utils.PeriodicTask(async () => { let periodFunc = new Utils.PeriodicTask(async () => {
try { try {
await loadDeviceInfo(); await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
} catch (e) { } catch (e) {
console.debug(e); console.debug(e);
} }
@@ -163,9 +288,16 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
<ConfirmPopup></ConfirmPopup> <ConfirmPopup></ConfirmPopup>
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'" <Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
:style="{ width: '55rem' }"> :style="{ width: '55rem' }">
<div class="flex flex-col">
<div class="w-11/12 self-center ">
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
<Divider />
</div>
</div>
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config> <Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
</Dialog> </Dialog>
@@ -182,14 +314,33 @@ onUnmounted(() => {
<div class="gap-x-3 flex"> <div class="gap-x-3 flex">
<Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete" <Button @click="confirmDeleteNetwork($event)" icon="pi pi-minus" severity="danger" label="Delete"
iconPos="right" /> iconPos="right" />
<Button @click="exportConfig" icon="pi pi-file-export" severity="help" label="Export" iconPos="right" />
<Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" /> <Button @click="editNetwork" icon="pi pi-pen-to-square" label="Edit" iconPos="right" severity="info" />
<Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" /> <Button @click="newNetwork" icon="pi pi-plus" label="Create" iconPos="right" />
</div> </div>
</template> </template>
</Toolbar> </Toolbar>
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="!!selectedInstanceId"> <Divider />
<!-- For running network, show the status -->
<div v-if="needShowNetworkStatus">
<Status v-bind:cur-network-inst="curNetworkInfo" v-if="needShowNetworkStatus">
</Status> </Status>
<Divider />
<div class="text-center">
<Button @click="updateNetworkState(true)" label="Disable Network" severity="warn" />
</div>
</div>
<!-- For disabled network, show the config -->
<div v-if="networkIsDisabled">
<Config :cur-network="disabledNetworkConfig" @run-network="updateNetworkState(false)"
v-if="disabledNetworkConfig" />
<div v-else>
<div class="text-center text-xl"> Network is disabled, Loading config... </div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId"> <div class="grid grid-cols-1 gap-4 place-content-center h-full" v-if="!selectedInstanceId">
<div class="text-center text-xl"> Select or create a network instance to manage </div> <div class="text-center text-xl"> Select or create a network instance to manage </div>
+16 -6
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { Card, InputText, Password, Button, AutoComplete } from 'primevue'; import { Card, InputText, Password, Button, AutoComplete } from 'primevue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { Api } from 'easytier-frontend-lib'; import { Api } from 'easytier-frontend-lib';
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
defineProps<{ defineProps<{
isRegistering: boolean; isRegistering: boolean;
@@ -20,8 +21,10 @@ const registerPassword = ref('');
const captcha = ref(''); const captcha = ref('');
const captchaSrc = computed(() => api.value.captcha_url()); const captchaSrc = computed(() => api.value.captcha_url());
const onSubmit = async () => { const onSubmit = async () => {
// Add your login logic here // Add your login logic here
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: username.value, password: password.value, }; const credential: Api.Credential = { username: username.value, password: password.value, };
let ret = await api.value?.login(credential); let ret = await api.value?.login(credential);
if (ret.success) { if (ret.success) {
@@ -36,6 +39,7 @@ const onSubmit = async () => {
}; };
const onRegister = async () => { const onRegister = async () => {
saveApiHost(apiHost.value);
const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value }; const credential: Api.Credential = { username: registerUsername.value, password: registerPassword.value };
const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value }; const registerReq: Api.RegisterData = { credentials: credential, captcha: captcha.value };
let ret = await api.value?.register(registerReq); let ret = await api.value?.register(registerReq);
@@ -47,17 +51,23 @@ const onRegister = async () => {
} }
}; };
const defaultApiHost = 'https://config-server.easytier.cn' const apiHost = ref<string>(getInitialApiHost())
const apiHost = ref<string>(defaultApiHost)
const apiHostSuggestions = ref<Array<string>>([]) const apiHostSuggestions = ref<Array<string>>([])
const apiHostSearch = async (event: { query: string }) => { const apiHostSearch = async (event: { query: string }) => {
apiHostSuggestions.value = []; apiHostSuggestions.value = [];
let hosts = cleanAndLoadApiHosts();
if (event.query) { if (event.query) {
apiHostSuggestions.value.push(event.query); apiHostSuggestions.value.push(event.query);
} }
apiHostSuggestions.value.push(defaultApiHost); hosts.forEach((host) => {
apiHostSuggestions.value.push(host.value);
});
} }
onMounted(() => {
});
</script> </script>
<template> <template>
@@ -87,7 +97,7 @@ const apiHostSearch = async (event: { query: string }) => {
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Button label="Register" type="button" class="w-full" <Button label="Register" type="button" class="w-full"
@click="$router.replace({ name: 'register' })" severity="secondary" /> @click="saveApiHost(apiHost); $router.replace({ name: 'register' })" severity="secondary" />
</div> </div>
</form> </form>
@@ -111,7 +121,7 @@ const apiHostSearch = async (event: { query: string }) => {
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Button label="Back to Login" type="button" class="w-full" <Button label="Back to Login" type="button" class="w-full"
@click="$router.replace({ name: 'login' })" severity="secondary" /> @click="saveApiHost(apiHost); $router.replace({ name: 'login' })" severity="secondary" />
</div> </div>
</form> </form>
</template> </template>
+6 -1
View File
@@ -1,6 +1,6 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css'
import 'easytier-frontend-lib/style.css' import 'easytier-frontend-lib/style.css'
import './style.css'
import App from './App.vue' import App from './App.vue'
import EasytierFrontendLib from 'easytier-frontend-lib' import EasytierFrontendLib from 'easytier-frontend-lib'
import PrimeVue from 'primevue/config' import PrimeVue from 'primevue/config'
@@ -15,6 +15,7 @@ import DeviceManagement from './components/DeviceManagement.vue'
import Dashboard from './components/Dashboard.vue' import Dashboard from './components/Dashboard.vue'
import DialogService from 'primevue/dialogservice'; import DialogService from 'primevue/dialogservice';
import ToastService from 'primevue/toastservice'; import ToastService from 'primevue/toastservice';
import ConfigGenerator from './components/ConfigGenerator.vue'
const routes = [ const routes = [
{ {
@@ -66,6 +67,10 @@ const routes = [
} }
} }
}, },
{
path: '/config_generator',
component: ConfigGenerator,
}
] ]
const router = createRouter({ const router = createRouter({
@@ -0,0 +1,71 @@
interface ApiHost {
value: string;
usedAt: number;
}
let apiMeta: {
api_host: string;
} | undefined = (window as any).apiMeta;
// remove trailing slashes from the URL
const cleanUrl = (url: string) => url.replace(/\/+$/, '');
const defaultApiHost = cleanUrl(apiMeta?.api_host ?? `${location.origin}${location.pathname}`);
const isValidHttpUrl = (s: string): boolean => {
let url;
try {
url = new URL(s);
} catch (_) {
return false;
}
return url.protocol === "http:" || url.protocol === "https:";
};
const cleanAndLoadApiHosts = (): Array<ApiHost> => {
const maxHosts = 10;
const apiHosts = localStorage.getItem('apiHosts');
if (apiHosts) {
const hosts: Array<ApiHost> = JSON.parse(apiHosts);
// sort by usedAt
hosts.sort((a, b) => b.usedAt - a.usedAt);
// only keep the first 10
if (hosts.length > maxHosts) {
hosts.splice(maxHosts);
}
localStorage.setItem('apiHosts', JSON.stringify(hosts));
return hosts;
} else {
return [];
}
};
const saveApiHost = (host: string) => {
console.log('Save API Host:', host);
if (!isValidHttpUrl(host)) {
console.error('Invalid API Host:', host);
return;
}
let hosts = cleanAndLoadApiHosts();
const newHost: ApiHost = { value: host, usedAt: Date.now() };
hosts = hosts.filter((h) => h.value !== host);
hosts.push(newHost);
localStorage.setItem('apiHosts', JSON.stringify(hosts));
};
const getInitialApiHost = (): string => {
const hosts = cleanAndLoadApiHosts();
if (hosts.length > 0) {
return hosts[0].value;
} else {
saveApiHost(defaultApiHost)
return defaultApiHost;
}
};
export { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost }
+16 -3
View File
@@ -1,9 +1,22 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { viteSingleFile } from "vite-plugin-singlefile" // import { viteSingleFile } from "vite-plugin-singlefile"
const WEB_BASE_URL = process.env.WEB_BASE_URL || '';
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:11211';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: '', base: WEB_BASE_URL,
plugins: [vue(), viteSingleFile()], plugins: [vue(),/* viteSingleFile() */],
server: {
proxy: {
"/api": {
target: API_BASE_URL,
},
"/api_meta.js": {
target: API_BASE_URL,
},
}
}
}) })
+33
View File
@@ -0,0 +1,33 @@
_version: 2
cli:
db:
en: "path to the sqlite3 database file, used to save all the data"
zh-CN: "sqlite3 数据库文件路径, 用于保存所有数据"
console_log_level:
en: "The log level for the console logger. Possible values: trace, debug, info, warn, error"
zh-CN: "控制台日志级别。可能的值:trace, debug, info, warn, error"
file_log_level:
en: "The log level for the file logger. Possible values: trace, debug, info, warn, error"
zh-CN: "文件日志级别。可能的值:trace, debug, info, warn, error"
file_log_dir:
en: "The directory to save the log files, default is the current directory"
zh-CN: "保存日志文件的目录,默认为当前目录"
config_server_port:
en: "The port to listen for the config server, used by the easytier-core to connect to"
zh-CN: "配置服务器的监听端口,用于被 easytier-core 连接"
config_server_protocol:
en: "The protocol to listen for the config server, used by the easytier-core to connect to"
zh-CN: "配置服务器的监听协议,用于被 easytier-core 连接, 可能的值:udp, tcp"
api_server_port:
en: "The port to listen for the restful server, acting as ApiHost and used by the web frontend"
zh-CN: "restful 服务器的监听端口,作为 ApiHost 并被 web 前端使用"
web_server_port:
en: "The port to listen for the web dashboard server, default is same as the api server port"
zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同"
no_web:
en: "Do not run the web dashboard server"
zh-CN: "不运行 web dashboard 服务器"
api_host:
en: "The URL of the API server, used by the web frontend to connect to"
zh-CN: "API 服务器的 URL,用于 web 前端连接"
+19 -6
View File
@@ -10,7 +10,7 @@ use easytier::{
use session::Session; use session::Session;
use storage::{Storage, StorageToken}; use storage::{Storage, StorageToken};
use crate::db::Db; use crate::db::{Db, UserIdInDb};
#[derive(Debug)] #[derive(Debug)]
pub struct ClientManager { pub struct ClientManager {
@@ -86,15 +86,21 @@ impl ClientManager {
ret ret
} }
pub fn get_session_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<Arc<Session>> { pub fn get_session_by_machine_id(
let c_url = self.storage.get_client_url_by_machine_id(machine_id)?; &self,
user_id: UserIdInDb,
machine_id: &uuid::Uuid,
) -> Option<Arc<Session>> {
let c_url = self
.storage
.get_client_url_by_machine_id(user_id, machine_id)?;
self.client_sessions self.client_sessions
.get(&c_url) .get(&c_url)
.map(|item| item.value().clone()) .map(|item| item.value().clone())
} }
pub fn list_machine_by_token(&self, token: String) -> Vec<url::Url> { pub async fn list_machine_by_user_id(&self, user_id: UserIdInDb) -> Vec<url::Url> {
self.storage.list_token_clients(&token) self.storage.list_user_clients(user_id)
} }
pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> { pub async fn get_heartbeat_requests(&self, client_url: &url::Url) -> Option<HeartbeatRequest> {
@@ -118,6 +124,7 @@ mod tests {
}, },
web_client::WebClient, web_client::WebClient,
}; };
use sqlx::Executor;
use crate::{client_manager::ClientManager, db::Db}; use crate::{client_manager::ClientManager, db::Db};
@@ -127,8 +134,14 @@ mod tests {
let mut mgr = ClientManager::new(Db::memory_db().await); let mut mgr = ClientManager::new(Db::memory_db().await);
mgr.serve(Box::new(listener)).await.unwrap(); mgr.serve(Box::new(listener)).await.unwrap();
mgr.db()
.inner()
.execute("INSERT INTO users (username, password) VALUES ('test', 'test')")
.await
.unwrap();
let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap()); let connector = UdpTunnelConnector::new("udp://127.0.0.1:54333".parse().unwrap());
let _c = WebClient::new(connector, "test"); let _c = WebClient::new(connector, "test", "test");
wait_for_condition( wait_for_condition(
|| async { mgr.client_sessions.len() == 1 }, || async { mgr.client_sessions.len() == 1 },
+75 -19
View File
@@ -1,5 +1,6 @@
use std::{fmt::Debug, sync::Arc}; use std::{fmt::Debug, str::FromStr as _, sync::Arc};
use anyhow::Context;
use easytier::{ use easytier::{
common::scoped_task::ScopedTask, common::scoped_task::ScopedTask,
proto::{ proto::{
@@ -15,6 +16,8 @@ use easytier::{
}; };
use tokio::sync::{broadcast, RwLock}; use tokio::sync::{broadcast, RwLock};
use crate::db::ListNetworkProps;
use super::storage::{Storage, StorageToken, WeakRefStorage}; use super::storage::{Storage, StorageToken, WeakRefStorage};
#[derive(Debug)] #[derive(Debug)]
@@ -66,6 +69,66 @@ struct SessionRpcService {
data: SharedSessionData, data: SharedSessionData,
} }
impl SessionRpcService {
async fn handle_heartbeat(
&self,
req: HeartbeatRequest,
) -> rpc_types::error::Result<HeartbeatResponse> {
let mut data = self.data.write().await;
let Ok(storage) = Storage::try_from(data.storage.clone()) else {
tracing::error!("Failed to get storage");
return Ok(HeartbeatResponse {});
};
let machine_id: uuid::Uuid =
req.machine_id
.clone()
.map(Into::into)
.ok_or(anyhow::anyhow!(
"Machine id is not set correctly, expect uuid but got: {:?}",
req.machine_id
))?;
let user_id = storage
.db()
.get_user_id_by_token(req.user_token.clone())
.await
.with_context(|| {
format!(
"Failed to get user id by token from db: {:?}",
req.user_token
)
})?
.ok_or(anyhow::anyhow!(
"User not found by token: {:?}",
req.user_token
))?;
if data.req.replace(req.clone()).is_none() {
assert!(data.storage_token.is_none());
data.storage_token = Some(StorageToken {
token: req.user_token.clone().into(),
client_url: data.client_url.clone(),
machine_id,
user_id,
});
}
let Ok(report_time) = chrono::DateTime::<chrono::Local>::from_str(&req.report_time) else {
tracing::error!("Failed to parse report time: {:?}", req.report_time);
return Ok(HeartbeatResponse {});
};
storage.update_client(
data.storage_token.as_ref().unwrap().clone(),
report_time.timestamp(),
);
let _ = data.notifier.send(req);
Ok(HeartbeatResponse {})
}
}
#[async_trait::async_trait] #[async_trait::async_trait]
impl WebServerService for SessionRpcService { impl WebServerService for SessionRpcService {
type Controller = BaseController; type Controller = BaseController;
@@ -75,24 +138,13 @@ impl WebServerService for SessionRpcService {
_: BaseController, _: BaseController,
req: HeartbeatRequest, req: HeartbeatRequest,
) -> rpc_types::error::Result<HeartbeatResponse> { ) -> rpc_types::error::Result<HeartbeatResponse> {
let mut data = self.data.write().await; let ret = self.handle_heartbeat(req).await;
if data.req.replace(req.clone()).is_none() { if ret.is_err() {
assert!(data.storage_token.is_none()); tracing::warn!("Failed to handle heartbeat: {:?}", ret);
data.storage_token = Some(StorageToken { // sleep for a while to avoid client busy loop
token: req.user_token.clone().into(), tokio::time::sleep(std::time::Duration::from_secs(2)).await;
client_url: data.client_url.clone(),
machine_id: req
.machine_id
.clone()
.map(Into::into)
.unwrap_or(uuid::Uuid::new_v4()),
});
if let Ok(storage) = Storage::try_from(data.storage.clone()) {
storage.add_client(data.storage_token.as_ref().unwrap().clone());
} }
} ret
let _ = data.notifier.send(req);
Ok(HeartbeatResponse {})
} }
} }
@@ -196,7 +248,11 @@ impl Session {
let local_configs = match storage let local_configs = match storage
.db .db
.list_network_configs(user_id, Some(req.machine_id.unwrap().into()), true) .list_network_configs(
user_id,
Some(req.machine_id.unwrap().into()),
ListNetworkProps::EnabledOnly,
)
.await .await
{ {
Ok(configs) => configs, Ok(configs) => configs,
+70 -34
View File
@@ -1,8 +1,8 @@
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use dashmap::{DashMap, DashSet}; use dashmap::DashMap;
use crate::db::Db; use crate::db::{Db, UserIdInDb};
// use this to maintain Storage // use this to maintain Storage
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@@ -10,13 +10,19 @@ pub struct StorageToken {
pub token: String, pub token: String,
pub client_url: url::Url, pub client_url: url::Url,
pub machine_id: uuid::Uuid, pub machine_id: uuid::Uuid,
pub user_id: UserIdInDb,
}
#[derive(Debug, Clone)]
struct ClientInfo {
storage_token: StorageToken,
report_time: i64,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StorageInner { pub struct StorageInner {
// some map for indexing // some map for indexing
pub token_clients_map: DashMap<String, DashSet<url::Url>>, user_clients_map: DashMap<UserIdInDb, DashMap<uuid::Uuid, ClientInfo>>,
pub machine_client_url_map: DashMap<uuid::Uuid, DashSet<url::Url>>,
pub db: Db, pub db: Db,
} }
@@ -35,37 +41,58 @@ impl TryFrom<WeakRefStorage> for Storage {
impl Storage { impl Storage {
pub fn new(db: Db) -> Self { pub fn new(db: Db) -> Self {
Storage(Arc::new(StorageInner { Storage(Arc::new(StorageInner {
token_clients_map: DashMap::new(), user_clients_map: DashMap::new(),
machine_client_url_map: DashMap::new(),
db, db,
})) }))
} }
pub fn add_client(&self, stoken: StorageToken) { fn remove_mid_to_client_info_map(
map: &DashMap<uuid::Uuid, ClientInfo>,
machine_id: &uuid::Uuid,
client_url: &url::Url,
) {
map.remove_if(&machine_id, |_, v| {
v.storage_token.client_url == *client_url
});
}
fn update_mid_to_client_info_map(
map: &DashMap<uuid::Uuid, ClientInfo>,
client_info: &ClientInfo,
) {
map.entry(client_info.storage_token.machine_id)
.and_modify(|e| {
if e.report_time < client_info.report_time {
assert_eq!(
e.storage_token.machine_id,
client_info.storage_token.machine_id
);
*e = client_info.clone();
}
})
.or_insert(client_info.clone());
}
pub fn update_client(&self, stoken: StorageToken, report_time: i64) {
let inner = self let inner = self
.0 .0
.token_clients_map .user_clients_map
.entry(stoken.token) .entry(stoken.user_id)
.or_insert_with(DashSet::new); .or_insert_with(DashMap::new);
inner.insert(stoken.client_url.clone());
self.0 let client_info = ClientInfo {
.machine_client_url_map storage_token: stoken.clone(),
.entry(stoken.machine_id) report_time,
.or_insert_with(DashSet::new) };
.insert(stoken.client_url.clone());
Self::update_mid_to_client_info_map(&inner, &client_info);
} }
pub fn remove_client(&self, stoken: &StorageToken) { pub fn remove_client(&self, stoken: &StorageToken) {
self.0.token_clients_map.remove_if(&stoken.token, |_, set| {
set.remove(&stoken.client_url);
set.is_empty()
});
self.0 self.0
.machine_client_url_map .user_clients_map
.remove_if(&stoken.machine_id, |_, set| { .remove_if(&stoken.user_id, |_, set| {
set.remove(&stoken.client_url); Self::remove_mid_to_client_info_map(set, &stoken.machine_id, &stoken.client_url);
set.is_empty() set.is_empty()
}); });
} }
@@ -74,19 +101,28 @@ impl Storage {
Arc::downgrade(&self.0) Arc::downgrade(&self.0)
} }
pub fn get_client_url_by_machine_id(&self, machine_id: &uuid::Uuid) -> Option<url::Url> { pub fn get_client_url_by_machine_id(
self.0 &self,
.machine_client_url_map user_id: UserIdInDb,
.get(&machine_id) machine_id: &uuid::Uuid,
.map(|url| url.iter().next().map(|url| url.clone())) ) -> Option<url::Url> {
.flatten() self.0.user_clients_map.get(&user_id).and_then(|info_map| {
info_map
.get(machine_id)
.map(|info| info.storage_token.client_url.clone())
})
} }
pub fn list_token_clients(&self, token: &str) -> Vec<url::Url> { pub fn list_user_clients(&self, user_id: UserIdInDb) -> Vec<url::Url> {
self.0 self.0
.token_clients_map .user_clients_map
.get(token) .get(&user_id)
.map(|set| set.iter().map(|url| url.clone()).collect()) .map(|info_map| {
info_map
.iter()
.map(|info| info.value().storage_token.client_url.clone())
.collect()
})
.unwrap_or_default() .unwrap_or_default()
} }
+65 -7
View File
@@ -4,7 +4,7 @@ pub mod entity;
use entity::user_running_network_configs; use entity::user_running_network_configs;
use sea_orm::{ use sea_orm::{
sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait as _, prelude::Expr, sea_query::OnConflict, ColumnTrait as _, DatabaseConnection, DbErr, EntityTrait,
QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _, QueryFilter as _, SqlxSqliteConnector, TransactionTrait as _,
}; };
use sea_orm_migration::MigratorTrait as _; use sea_orm_migration::MigratorTrait as _;
@@ -12,7 +12,13 @@ use sqlx::{migrate::MigrateDatabase as _, types::chrono, Sqlite, SqlitePool};
use crate::migrator; use crate::migrator;
type UserIdInDb = i32; pub type UserIdInDb = i32;
pub enum ListNetworkProps {
All,
EnabledOnly,
DisabledOnly,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Db { pub struct Db {
@@ -115,17 +121,51 @@ impl Db {
Ok(()) Ok(())
} }
pub async fn update_network_config_state(
&self,
user_id: UserIdInDb,
network_inst_id: uuid::Uuid,
disabled: bool,
) -> Result<entity::user_running_network_configs::Model, DbErr> {
use entity::user_running_network_configs as urnc;
urnc::Entity::update_many()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.col_expr(urnc::Column::Disabled, Expr::value(disabled))
.col_expr(
urnc::Column::UpdateTime,
Expr::value(chrono::Local::now().fixed_offset()),
)
.exec(self.orm_db())
.await?;
urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id.to_string()))
.one(self.orm_db())
.await?
.ok_or(DbErr::RecordNotFound(format!(
"Network config not found for user {} and network instance {}",
user_id, network_inst_id
)))
}
pub async fn list_network_configs( pub async fn list_network_configs(
&self, &self,
user_id: UserIdInDb, user_id: UserIdInDb,
device_id: Option<uuid::Uuid>, device_id: Option<uuid::Uuid>,
only_enabled: bool, props: ListNetworkProps,
) -> Result<Vec<user_running_network_configs::Model>, DbErr> { ) -> Result<Vec<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc; use entity::user_running_network_configs as urnc;
let configs = urnc::Entity::find().filter(urnc::Column::UserId.eq(user_id)); let configs = urnc::Entity::find().filter(urnc::Column::UserId.eq(user_id));
let configs = if only_enabled { let configs = if matches!(
configs.filter(urnc::Column::Disabled.eq(false)) props,
ListNetworkProps::EnabledOnly | ListNetworkProps::DisabledOnly
) {
configs
.filter(urnc::Column::Disabled.eq(matches!(props, ListNetworkProps::DisabledOnly)))
} else { } else {
configs configs
}; };
@@ -140,6 +180,24 @@ impl Db {
Ok(configs) Ok(configs)
} }
pub async fn get_network_config(
&self,
user_id: UserIdInDb,
device_id: &uuid::Uuid,
network_inst_id: &String,
) -> Result<Option<user_running_network_configs::Model>, DbErr> {
use entity::user_running_network_configs as urnc;
let config = urnc::Entity::find()
.filter(urnc::Column::UserId.eq(user_id))
.filter(urnc::Column::DeviceId.eq(device_id.to_string()))
.filter(urnc::Column::NetworkInstanceId.eq(network_inst_id))
.one(self.orm_db())
.await?;
Ok(config)
}
pub async fn get_user_id<T: ToString>( pub async fn get_user_id<T: ToString>(
&self, &self,
user_name: T, user_name: T,
@@ -167,7 +225,7 @@ impl Db {
mod tests { mod tests {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _};
use crate::db::{entity::user_running_network_configs, Db}; use crate::db::{entity::user_running_network_configs, Db, ListNetworkProps};
#[tokio::test] #[tokio::test]
async fn test_user_network_config_management() { async fn test_user_network_config_management() {
@@ -209,7 +267,7 @@ mod tests {
assert_ne!(result.update_time, result2.update_time); assert_ne!(result.update_time, result2.update_time);
assert_eq!( assert_eq!(
db.list_network_configs(user_id, Some(device_id), true) db.list_network_configs(user_id, Some(device_id), ListNetworkProps::All)
.await .await
.unwrap() .unwrap()
.len(), .len(),
+165 -9
View File
@@ -1,11 +1,21 @@
#![allow(dead_code)] #![allow(dead_code)]
#[macro_use]
extern crate rust_i18n;
use std::sync::Arc; use std::sync::Arc;
use clap::Parser;
use easytier::{ use easytier::{
common::config::{ConfigLoader, ConsoleLoggerConfig, TomlConfigLoader}, common::{
tunnel::udp::UdpTunnelListener, config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader},
utils::init_logger, constants::EASYTIER_VERSION,
error::Error,
},
tunnel::{
tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener,
},
utils::{init_logger, setup_panic_handler},
}; };
mod client_manager; mod client_manager;
@@ -13,26 +23,172 @@ mod db;
mod migrator; mod migrator;
mod restful; mod restful;
#[cfg(feature = "embed")]
mod web;
rust_i18n::i18n!("locales", fallback = "en");
#[derive(Parser, Debug)]
#[command(name = "easytier-web", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
#[arg(short, long, default_value = "et.db", help = t!("cli.db").to_string())]
db: String,
#[arg(
long,
help = t!("cli.console_log_level").to_string(),
)]
console_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_level").to_string(),
)]
file_log_level: Option<String>,
#[arg(
long,
help = t!("cli.file_log_dir").to_string(),
)]
file_log_dir: Option<String>,
#[arg(
long,
short='c',
default_value = "22020",
help = t!("cli.config_server_port").to_string(),
)]
config_server_port: u16,
#[arg(
long,
short='p',
default_value = "udp",
help = t!("cli.config_server_protocol").to_string(),
)]
config_server_protocol: String,
#[arg(
long,
short='a',
default_value = "11211",
help = t!("cli.api_server_port").to_string(),
)]
api_server_port: u16,
#[cfg(feature = "embed")]
#[arg(
long,
short='l',
help = t!("cli.web_server_port").to_string(),
)]
web_server_port: Option<u16>,
#[cfg(feature = "embed")]
#[arg(
long,
help = t!("cli.no_web").to_string(),
default_value = "false"
)]
no_web: bool,
#[cfg(feature = "embed")]
#[arg(
long,
help = t!("cli.api_host").to_string()
)]
api_host: Option<url::Url>,
}
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
Ok(match l.scheme() {
"tcp" => Box::new(TcpTunnelListener::new(l.clone())),
"udp" => Box::new(UdpTunnelListener::new(l.clone())),
"ws" => Box::new(WSTunnelListener::new(l.clone())),
_ => {
return Err(Error::InvalidUrl(l.to_string()));
}
})
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
rust_i18n::set_locale(&locale);
setup_panic_handler();
let cli = Cli::parse();
let config = TomlConfigLoader::default(); let config = TomlConfigLoader::default();
config.set_console_logger_config(ConsoleLoggerConfig { config.set_console_logger_config(ConsoleLoggerConfig {
level: Some("trace".to_string()), level: cli.console_log_level,
});
config.set_file_logger_config(FileLoggerConfig {
dir: cli.file_log_dir,
level: cli.file_log_level,
file: None,
}); });
init_logger(config, false).unwrap(); init_logger(config, false).unwrap();
// let db = db::Db::new(":memory:").await.unwrap(); // let db = db::Db::new(":memory:").await.unwrap();
let db = db::Db::new("et.db").await.unwrap(); let db = db::Db::new(cli.db).await.unwrap();
let listener = UdpTunnelListener::new("udp://0.0.0.0:22020".parse().unwrap()); let listener = get_listener_by_url(
&format!(
"{}://0.0.0.0:{}",
cli.config_server_protocol, cli.config_server_port
)
.parse()
.unwrap(),
)
.unwrap();
let mut mgr = client_manager::ClientManager::new(db.clone()); let mut mgr = client_manager::ClientManager::new(db.clone());
mgr.serve(listener).await.unwrap(); mgr.serve(listener).await.unwrap();
let mgr = Arc::new(mgr); let mgr = Arc::new(mgr);
let mut restful_server = #[cfg(feature = "embed")]
restful::RestfulServer::new("0.0.0.0:11211".parse().unwrap(), mgr.clone(), db) let (web_router_restful, web_router_static) = if cli.no_web {
(None, None)
} else {
let web_router = web::build_router(cli.api_host.clone());
if cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port) {
(Some(web_router), None)
} else {
(None, Some(web_router))
}
};
#[cfg(not(feature = "embed"))]
let web_router_restful = None;
let _restful_server_tasks = restful::RestfulServer::new(
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
mgr.clone(),
db,
web_router_restful,
)
.await
.unwrap()
.start()
.await .await
.unwrap(); .unwrap();
restful_server.start().await.unwrap();
#[cfg(feature = "embed")]
let _web_server_task = if let Some(web_router) = web_router_static {
Some(
web::WebServer::new(
format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0))
.parse()
.unwrap(),
web_router,
)
.await
.unwrap()
.start()
.await
.unwrap(),
)
} else {
None
};
tokio::signal::ctrl_c().await.unwrap(); tokio::signal::ctrl_c().await.unwrap();
} }
+76 -29
View File
@@ -6,11 +6,14 @@ mod users;
use std::{net::SocketAddr, sync::Arc}; use std::{net::SocketAddr, sync::Arc};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::post;
use axum::{extract::State, routing::get, Json, Router}; use axum::{extract::State, routing::get, Json, Router};
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer}; use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
use axum_login::{login_required, AuthManagerLayerBuilder, AuthzBackend}; use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
use axum_messages::MessagesManagerLayer; use axum_messages::MessagesManagerLayer;
use easytier::common::config::ConfigLoader;
use easytier::common::scoped_task::ScopedTask; use easytier::common::scoped_task::ScopedTask;
use easytier::launcher::NetworkConfig;
use easytier::proto::rpc_types; use easytier::proto::rpc_types;
use network::NetworkApi; use network::NetworkApi;
use sea_orm::DbErr; use sea_orm::DbErr;
@@ -21,20 +24,26 @@ use tower_sessions::Expiry;
use tower_sessions_sqlx_store::SqliteStore; use tower_sessions_sqlx_store::SqliteStore;
use users::{AuthSession, Backend}; use users::{AuthSession, Backend};
use crate::client_manager::session::Session;
use crate::client_manager::storage::StorageToken; use crate::client_manager::storage::StorageToken;
use crate::client_manager::ClientManager; use crate::client_manager::ClientManager;
use crate::db::Db; use crate::db::Db;
/// Embed assets for web dashboard, build frontend first
#[cfg(feature = "embed")]
#[derive(rust_embed::RustEmbed, Clone)]
#[folder = "frontend/dist/"]
struct Assets;
pub struct RestfulServer { pub struct RestfulServer {
bind_addr: SocketAddr, bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>, client_mgr: Arc<ClientManager>,
db: Db, db: Db,
serve_task: Option<ScopedTask<()>>, // serve_task: Option<ScopedTask<()>>,
delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>, // delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
network_api: NetworkApi, network_api: NetworkApi,
web_router: Option<Router>,
} }
type AppStateInner = Arc<ClientManager>; type AppStateInner = Arc<ClientManager>;
@@ -48,6 +57,17 @@ struct GetSummaryJsonResp {
device_count: u32, device_count: u32,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct GenerateConfigRequest {
config: NetworkConfig,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct GenerateConfigResponse {
error: Option<String>,
toml_config: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Error { pub struct Error {
message: String, message: String,
@@ -73,6 +93,7 @@ impl RestfulServer {
bind_addr: SocketAddr, bind_addr: SocketAddr,
client_mgr: Arc<ClientManager>, client_mgr: Arc<ClientManager>,
db: Db, db: Db,
web_router: Option<Router>,
) -> anyhow::Result<Self> { ) -> anyhow::Result<Self> {
assert!(client_mgr.is_running()); assert!(client_mgr.is_running());
@@ -82,23 +103,13 @@ impl RestfulServer {
bind_addr, bind_addr,
client_mgr, client_mgr,
db, db,
serve_task: None, // serve_task: None,
delete_task: None, // delete_task: None,
network_api, network_api,
web_router,
}) })
} }
async fn get_session_by_machine_id(
client_mgr: &ClientManager,
machine_id: &uuid::Uuid,
) -> Result<Arc<Session>, HttpHandleError> {
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else {
return Err((StatusCode::NOT_FOUND, other_error("No such session").into()));
};
Ok(result)
}
async fn handle_list_all_sessions( async fn handle_list_all_sessions(
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
@@ -121,7 +132,7 @@ impl RestfulServer {
return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into())); return Err((StatusCode::UNAUTHORIZED, other_error("No such user").into()));
}; };
let machines = client_mgr.list_machine_by_token(user.tokens[0].clone()); let machines = client_mgr.list_machine_by_user_id(user.id().clone()).await;
Ok(GetSummaryJsonResp { Ok(GetSummaryJsonResp {
device_count: machines.len() as u32, device_count: machines.len() as u32,
@@ -129,7 +140,33 @@ impl RestfulServer {
.into()) .into())
} }
pub async fn start(&mut self) -> Result<(), anyhow::Error> { async fn handle_generate_config(
Json(req): Json<GenerateConfigRequest>,
) -> Result<Json<GenerateConfigResponse>, HttpHandleError> {
let config = req.config.gen_config();
match config {
Ok(c) => Ok(GenerateConfigResponse {
error: None,
toml_config: Some(c.dump()),
}
.into()),
Err(e) => Ok(GenerateConfigResponse {
error: Some(format!("{:?}", e)),
toml_config: None,
}
.into()),
}
}
pub async fn start(
mut self,
) -> Result<
(
ScopedTask<()>,
ScopedTask<tower_sessions::session_store::Result<()>>,
),
anyhow::Error,
> {
let listener = TcpListener::bind(self.bind_addr).await?; let listener = TcpListener::bind(self.bind_addr).await?;
// Session layer. // Session layer.
@@ -139,14 +176,13 @@ impl RestfulServer {
let session_store = SqliteStore::new(self.db.inner()); let session_store = SqliteStore::new(self.db.inner());
session_store.migrate().await?; session_store.migrate().await?;
self.delete_task.replace( let delete_task: ScopedTask<tower_sessions::session_store::Result<()>> =
tokio::task::spawn( tokio::task::spawn(
session_store session_store
.clone() .clone()
.continuously_delete_expired(tokio::time::Duration::from_secs(60)), .continuously_delete_expired(tokio::time::Duration::from_secs(60)),
) )
.into(), .into();
);
// Generate a cryptographic key to sign the session cookie. // Generate a cryptographic key to sign the session cookie.
let key = Key::generate(); let key = Key::generate();
@@ -167,7 +203,7 @@ impl RestfulServer {
.deflate(true) .deflate(true)
.gzip(true) .gzip(true)
.zstd(true) .zstd(true)
.quality(tower_http::compression::CompressionLevel::Best); .quality(tower_http::compression::CompressionLevel::Default);
let app = Router::new() let app = Router::new()
.route("/api/v1/summary", get(Self::handle_get_summary)) .route("/api/v1/summary", get(Self::handle_get_summary))
@@ -176,16 +212,27 @@ impl RestfulServer {
.route_layer(login_required!(Backend)) .route_layer(login_required!(Backend))
.merge(auth::router()) .merge(auth::router())
.with_state(self.client_mgr.clone()) .with_state(self.client_mgr.clone())
.route(
"/api/v1/generate-config",
post(Self::handle_generate_config),
)
.layer(MessagesManagerLayer) .layer(MessagesManagerLayer)
.layer(auth_layer) .layer(auth_layer)
.layer(tower_http::cors::CorsLayer::very_permissive()) .layer(tower_http::cors::CorsLayer::very_permissive())
.layer(compression_layer); .layer(compression_layer);
let task = tokio::spawn(async move { #[cfg(feature = "embed")]
axum::serve(listener, app).await.unwrap(); let app = if let Some(web_router) = self.web_router.take() {
}); app.merge(web_router)
self.serve_task = Some(task.into()); } else {
app
};
Ok(()) let serve_task: ScopedTask<()> = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
})
.into();
Ok((serve_task, delete_task))
} }
} }
+103 -28
View File
@@ -1,11 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use axum::extract::{Path, Query}; use axum::extract::Path;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::{delete, post}; use axum::routing::{delete, post};
use axum::{extract::State, routing::get, Json, Router}; use axum::{extract::State, routing::get, Json, Router};
use axum_login::AuthUser; use axum_login::AuthUser;
use dashmap::DashSet;
use easytier::launcher::NetworkConfig; use easytier::launcher::NetworkConfig;
use easytier::proto::common::Void; use easytier::proto::common::Void;
use easytier::proto::rpc_types::controller::BaseController; use easytier::proto::rpc_types::controller::BaseController;
@@ -13,6 +12,7 @@ use easytier::proto::web::*;
use crate::client_manager::session::Session; use crate::client_manager::session::Session;
use crate::client_manager::ClientManager; use crate::client_manager::ClientManager;
use crate::db::{ListNetworkProps, UserIdInDb};
use super::users::AuthSession; use super::users::AuthSession;
use super::{ use super::{
@@ -46,13 +46,21 @@ struct ColletNetworkInfoJsonReq {
inst_ids: Option<Vec<uuid::Uuid>>, inst_ids: Option<Vec<uuid::Uuid>>,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct UpdateNetworkStateJsonReq {
disabled: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
struct RemoveNetworkJsonReq { struct RemoveNetworkJsonReq {
inst_ids: Vec<uuid::Uuid>, inst_ids: Vec<uuid::Uuid>,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListNetworkInstanceIdsJsonResp(Vec<uuid::Uuid>); struct ListNetworkInstanceIdsJsonResp {
running_inst_ids: Vec<easytier::proto::common::Uuid>,
disabled_inst_ids: Vec<easytier::proto::common::Uuid>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)] #[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ListMachineItem { struct ListMachineItem {
@@ -72,12 +80,24 @@ impl NetworkApi {
Self {} Self {}
} }
fn get_user_id(auth_session: &AuthSession) -> Result<UserIdInDb, (StatusCode, Json<Error>)> {
let Some(user_id) = auth_session.user.as_ref().map(|x| x.id()) else {
return Err((
StatusCode::UNAUTHORIZED,
other_error(format!("No user id found")).into(),
));
};
Ok(user_id)
}
async fn get_session_by_machine_id( async fn get_session_by_machine_id(
auth_session: &AuthSession, auth_session: &AuthSession,
client_mgr: &ClientManager, client_mgr: &ClientManager,
machine_id: &uuid::Uuid, machine_id: &uuid::Uuid,
) -> Result<Arc<Session>, HttpHandleError> { ) -> Result<Arc<Session>, HttpHandleError> {
let Some(result) = client_mgr.get_session_by_machine_id(machine_id) else { let user_id = Self::get_user_id(auth_session)?;
let Some(result) = client_mgr.get_session_by_machine_id(user_id, machine_id) else {
return Err(( return Err((
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
other_error(format!("No such session: {}", machine_id)).into(), other_error(format!("No such session: {}", machine_id)).into(),
@@ -190,7 +210,7 @@ impl NetworkApi {
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
Path(machine_id): Path<uuid::Uuid>, Path(machine_id): Path<uuid::Uuid>,
Query(payload): Query<ColletNetworkInfoJsonReq>, Json(payload): Json<ColletNetworkInfoJsonReq>,
) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> { ) -> Result<Json<CollectNetworkInfoResponse>, HttpHandleError> {
let result = let result =
Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?; Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
@@ -226,10 +246,28 @@ impl NetworkApi {
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {}) .list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await .await
.map_err(convert_rpc_error)?; .map_err(convert_rpc_error)?;
Ok(
ListNetworkInstanceIdsJsonResp(ret.inst_ids.into_iter().map(Into::into).collect()) let running_inst_ids = ret.inst_ids.clone().into_iter().map(Into::into).collect();
.into(),
// collect networks that are disabled
let disabled_inst_ids = client_mgr
.db()
.list_network_configs(
auth_session.user.unwrap().id(),
Some(machine_id),
ListNetworkProps::DisabledOnly,
) )
.await
.map_err(convert_db_error)?
.iter()
.filter_map(|x| x.network_instance_id.clone().try_into().ok())
.collect::<Vec<_>>();
Ok(ListNetworkInstanceIdsJsonResp {
running_inst_ids,
disabled_inst_ids,
}
.into())
} }
async fn handle_remove_network_instance( async fn handle_remove_network_instance(
@@ -262,23 +300,13 @@ impl NetworkApi {
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
) -> Result<Json<ListMachineJsonResp>, HttpHandleError> { ) -> Result<Json<ListMachineJsonResp>, HttpHandleError> {
let tokens = auth_session let user_id = Self::get_user_id(&auth_session)?;
.user
.as_ref()
.map(|x| x.tokens.clone())
.unwrap_or_default();
let client_urls = DashSet::new(); let client_urls = client_mgr.list_machine_by_user_id(user_id).await;
for token in tokens {
let urls = client_mgr.list_machine_by_token(token);
for url in urls {
client_urls.insert(url);
}
}
let mut machines = vec![]; let mut machines = vec![];
for item in client_urls.iter() { for item in client_urls.iter() {
let client_url = item.key().clone(); let client_url = item.clone();
let session = client_mgr.get_heartbeat_requests(&client_url).await; let session = client_mgr.get_heartbeat_requests(&client_url).await;
machines.push(ListMachineItem { machines.push(ListMachineItem {
client_url: Some(client_url), client_url: Some(client_url),
@@ -289,6 +317,54 @@ impl NetworkApi {
Ok(Json(ListMachineJsonResp { machines })) Ok(Json(ListMachineJsonResp { machines }))
} }
async fn handle_update_network_state(
auth_session: AuthSession,
State(client_mgr): AppState,
Path((machine_id, inst_id)): Path<(uuid::Uuid, Option<uuid::Uuid>)>,
Json(payload): Json<UpdateNetworkStateJsonReq>,
) -> Result<(), HttpHandleError> {
let Some(inst_id) = inst_id else {
// not implement disable all
return Err((
StatusCode::NOT_IMPLEMENTED,
other_error(format!("Not implemented")).into(),
))
.into();
};
let sess = Self::get_session_by_machine_id(&auth_session, &client_mgr, &machine_id).await?;
let cfg = client_mgr
.db()
.update_network_config_state(auth_session.user.unwrap().id(), inst_id, payload.disabled)
.await
.map_err(convert_db_error)?;
let c = sess.scoped_rpc_client();
if payload.disabled {
c.delete_network_instance(
BaseController::default(),
DeleteNetworkInstanceRequest {
inst_ids: vec![inst_id.into()],
},
)
.await
.map_err(convert_rpc_error)?;
} else {
c.run_network_instance(
BaseController::default(),
RunNetworkInstanceRequest {
inst_id: Some(inst_id.into()),
config: Some(serde_json::from_str(&cfg.network_config).unwrap()),
},
)
.await
.map_err(convert_rpc_error)?;
}
Ok(())
}
async fn handle_get_network_config( async fn handle_get_network_config(
auth_session: AuthSession, auth_session: AuthSession,
State(client_mgr): AppState, State(client_mgr): AppState,
@@ -298,25 +374,24 @@ impl NetworkApi {
let db_row = client_mgr let db_row = client_mgr
.db() .db()
.list_network_configs(auth_session.user.unwrap().id(), Some(machine_id), false) .get_network_config(auth_session.user.unwrap().id(), &machine_id, &inst_id)
.await .await
.map_err(convert_db_error)? .map_err(convert_db_error)?
.iter()
.find(|x| x.network_instance_id == inst_id)
.map(|x| x.network_config.clone())
.ok_or(( .ok_or((
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
other_error(format!("No such network instance: {}", inst_id)).into(), other_error(format!("No such network instance: {}", inst_id)).into(),
))?; ))?;
Ok(serde_json::from_str::<NetworkConfig>(&db_row) Ok(
serde_json::from_str::<NetworkConfig>(&db_row.network_config)
.map_err(|e| { .map_err(|e| {
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
other_error(format!("Failed to parse network config: {:?}", e)).into(), other_error(format!("Failed to parse network config: {:?}", e)).into(),
) )
})? })?
.into()) .into(),
)
} }
pub fn build_route(&mut self) -> Router<AppStateInner> { pub fn build_route(&mut self) -> Router<AppStateInner> {
@@ -332,7 +407,7 @@ impl NetworkApi {
) )
.route( .route(
"/api/v1/machines/:machine-id/networks/:inst-id", "/api/v1/machines/:machine-id/networks/:inst-id",
delete(Self::handle_remove_network_instance), delete(Self::handle_remove_network_instance).put(Self::handle_update_network_state),
) )
.route( .route(
"/api/v1/machines/:machine-id/networks/info", "/api/v1/machines/:machine-id/networks/info",
+86
View File
@@ -0,0 +1,86 @@
use axum::{
extract::State,
http::header,
response::{IntoResponse, Response},
routing, Router,
};
use axum_embed::ServeEmbed;
use easytier::common::scoped_task::ScopedTask;
use rust_embed::RustEmbed;
use std::net::SocketAddr;
use tokio::net::TcpListener;
/// Embed assets for web dashboard, build frontend first
#[derive(RustEmbed, Clone)]
#[folder = "frontend/dist/"]
struct Assets;
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct ApiMetaResponse {
api_host: String,
}
async fn handle_api_meta(State(api_host): State<url::Url>) -> impl IntoResponse {
Response::builder()
.header(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)
.header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.header(header::PRAGMA, "no-cache")
.header(header::EXPIRES, "0")
.body(format!(
"window.apiMeta = {}",
serde_json::to_string(&ApiMetaResponse {
api_host: api_host.to_string()
})
.unwrap(),
))
.unwrap()
}
pub fn build_router(api_host: Option<url::Url>) -> Router {
let service = ServeEmbed::<Assets>::new();
let router = Router::new();
let router = if let Some(api_host) = api_host {
let sub_router = Router::new()
.route("/api_meta.js", routing::get(handle_api_meta))
.with_state(api_host);
router.merge(sub_router)
} else {
router
};
let router = router.fallback_service(service);
router
}
pub struct WebServer {
bind_addr: SocketAddr,
router: Router,
serve_task: Option<ScopedTask<()>>,
}
impl WebServer {
pub async fn new(bind_addr: SocketAddr, router: Router) -> anyhow::Result<Self> {
Ok(WebServer {
bind_addr,
router,
serve_task: None,
})
}
pub async fn start(self) -> Result<ScopedTask<()>, anyhow::Error> {
let listener = TcpListener::bind(self.bind_addr).await?;
let app = self.router;
let task = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
})
.into();
Ok(task)
}
}
+82 -23
View File
@@ -3,12 +3,12 @@ name = "easytier"
description = "A full meshed p2p VPN, connecting all your devices in one network with one command." description = "A full meshed p2p VPN, connecting all your devices in one network with one command."
homepage = "https://github.com/EasyTier/EasyTier" homepage = "https://github.com/EasyTier/EasyTier"
repository = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier"
version = "2.1.0" version = "2.3.1"
edition = "2021" edition = "2021"
authors = ["kkrainbow"] authors = ["kkrainbow"]
keywords = ["vpn", "p2p", "network", "easytier"] keywords = ["vpn", "p2p", "network", "easytier"]
categories = ["network-programming", "command-line-utilities"] categories = ["network-programming", "command-line-utilities"]
rust-version = "1.77.0" rust-version = "1.84.0"
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"
@@ -62,7 +62,6 @@ timedmap = "=1.0.1"
zerocopy = { version = "0.7.32", features = ["derive", "simd"] } zerocopy = { version = "0.7.32", features = ["derive", "simd"] }
bytes = "1.5.0" bytes = "1.5.0"
pin-project-lite = "0.2.13" pin-project-lite = "0.2.13"
atomicbox = "0.4.0"
tachyonix = "0.3.0" tachyonix = "0.3.0"
quinn = { version = "0.11.0", optional = true, features = ["ring"] } quinn = { version = "0.11.0", optional = true, features = ["ring"] }
@@ -89,7 +88,7 @@ tun = { package = "tun-easytier", version = "1.1.1", features = [
"async", "async",
], optional = true } ], optional = true }
# for net ns # for net ns
nix = { version = "0.27", features = ["sched", "socket", "ioctl"] } nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net"] }
uuid = { version = "1.5.0", features = [ uuid = { version = "1.5.0", features = [
"v4", "v4",
@@ -99,7 +98,6 @@ uuid = { version = "1.5.0", features = [
] } ] }
# for ring tunnel # for ring tunnel
crossbeam-queue = "0.3"
once_cell = "1.18.0" once_cell = "1.18.0"
# for rpc # for rpc
@@ -126,11 +124,12 @@ serde = { version = "1.0", features = ["derive"] }
pnet = { version = "0.35.0", features = ["serde"] } pnet = { version = "0.35.0", features = ["serde"] }
serde_json = "1" serde_json = "1"
clap = { version = "4.4.8", features = [ clap = { version = "4.5.30", features = [
"string", "string",
"unicode", "unicode",
"derive", "derive",
"wrap_help", "wrap_help",
"env",
] } ] }
async-recursion = "1.0.5" async-recursion = "1.0.5"
@@ -138,7 +137,8 @@ async-recursion = "1.0.5"
network-interface = "2.0" network-interface = "2.0"
# for ospf route # for ospf route
petgraph = "0.6.5" petgraph = "0.8.1"
hashbrown = "0.15.3"
# for wireguard # for wireguard
boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true } boringtun = { package = "boringtun-easytier", version = "0.6.1", optional = true }
@@ -154,21 +154,24 @@ humansize = "2.1.3"
base64 = "0.22" base64 = "0.22"
derivative = "2.2.0" mimalloc = { version = "*", optional = true }
mimalloc-rust = { version = "0.2.1", optional = true }
# for mips
indexmap = { version = "~1.9.3", optional = false, features = ["std"] }
# mips
atomic-shim = "0.2.0" atomic-shim = "0.2.0"
smoltcp = { version = "0.11.0", optional = true, default-features = false, features = [ smoltcp = { version = "0.12.0", optional = true, default-features = false, features = [
"std", "std",
"medium-ip", "medium-ip",
"proto-ipv4", "proto-ipv4",
"proto-ipv6", "proto-ipv6",
"proto-ipv4-fragmentation",
"fragmentation-buffer-size-8192",
"assembler-max-segment-count-16",
"reassembly-buffer-size-8192",
"reassembly-buffer-count-16",
"socket-tcp", "socket-tcp",
"socket-udp",
# "socket-tcp-cubic",
"async", "async",
] } ] }
parking_lot = { version = "0.12.0", optional = true } parking_lot = { version = "0.12.0", optional = true }
@@ -181,18 +184,62 @@ sys-locale = "0.3"
ringbuf = "0.4.5" ringbuf = "0.4.5"
async-ringbuf = "0.3.1" async-ringbuf = "0.3.1"
service-manager = {git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main"} service-manager = { git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main" }
async-compression = { version = "0.4.17", default-features = false, features = ["zstd", "tokio"] } zstd = { version = "0.13" }
kcp-sys = { git = "https://github.com/EasyTier/kcp-sys" }
prost-reflect = { version = "0.14.5", default-features = false, features = [
"derive",
] }
# for http connector
http_req = { git = "https://github.com/EasyTier/http_req.git", default-features = false, features = [
"rust-tls",
] }
# for dns connector
hickory-resolver = "0.25.2"
hickory-proto = "0.25.2"
# for magic dns
hickory-client = "0.25.2"
hickory-server = { version = "0.25.2", features = ["resolver"] }
derive_builder = "0.20.2"
humantime-serde = "1.1.1"
multimap = "0.10.0"
version-compare = "0.2.0"
jemallocator = { version = "0.5.4", optional = true }
jemalloc-ctl = { version = "0.5.4", optional = true }
jemalloc-sys = { version = "0.5.4", features = [
"stats",
"profiling",
"unprefixed_malloc_on_supported_platforms",
], optional = true }
[target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies]
machine-uid = "0.5.3" machine-uid = "0.5.3"
[target.'cfg(any(target_os = "linux"))'.dependencies]
netlink-sys = "0.8.7"
netlink-packet-route = "0.21.0"
netlink-packet-core = { version = "0.7.0" }
netlink-packet-utils = "0.5.2"
# for magic dns
resolv-conf = "0.7.3"
dbus = { version = "0.9.7", features = ["vendored"] }
which = "7.0.3"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = [ windows = { version = "0.52.0", features = [
"Win32_Networking_WinSock",
"Win32_NetworkManagement_IpHelper",
"Win32_Foundation", "Win32_Foundation",
"Win32_NetworkManagement_WindowsFirewall",
"Win32_System_Com",
"Win32_Networking",
"Win32_System_Ole",
"Win32_Networking_WinSock",
"Win32_System_IO", "Win32_System_IO",
] } ] }
encoding = "0.2" encoding = "0.2"
@@ -204,17 +251,28 @@ tonic-build = "0.12"
globwalk = "0.8.1" globwalk = "0.8.1"
regex = "1" regex = "1"
prost-build = "0.13.2" prost-build = "0.13.2"
rpc_build = { package = "easytier-rpc-build", version = "0.1.0", features = ["internal-namespace"] } rpc_build = { package = "easytier-rpc-build", version = "0.1.0", features = [
"internal-namespace",
] }
prost-reflect-build = { version = "0.14.0" }
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
reqwest = { version = "0.11", features = ["blocking"] } reqwest = { version = "0.12.12", features = ["blocking"] }
zip = "0.6.6" zip = "4.0.0"
# 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"] }
[dev-dependencies] [dev-dependencies]
serial_test = "3.0.0" serial_test = "3.0.0"
rstest = "0.18.2" rstest = "0.18.2"
futures-util = "0.3.30" futures-util = "0.3.30"
maplit = "1.0.2"
[target.'cfg(target_os = "linux")'.dev-dependencies] [target.'cfg(target_os = "linux")'.dev-dependencies]
defguard_wireguard_rs = "0.4.2" defguard_wireguard_rs = "0.4.2"
@@ -236,7 +294,7 @@ full = [
mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"] mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"]
wireguard = ["dep:boringtun", "dep:ring"] wireguard = ["dep:boringtun", "dep:ring"]
quic = ["dep:quinn", "dep:rustls", "dep:rcgen"] quic = ["dep:quinn", "dep:rustls", "dep:rcgen"]
mimalloc = ["dep:mimalloc-rust"] mimalloc = ["dep:mimalloc"]
aes-gcm = ["dep:aes-gcm"] aes-gcm = ["dep:aes-gcm"]
tun = ["dep:tun"] tun = ["dep:tun"]
websocket = [ websocket = [
@@ -248,3 +306,4 @@ websocket = [
] ]
smoltcp = ["dep:smoltcp", "dep:parking_lot"] smoltcp = ["dep:smoltcp", "dep:parking_lot"]
socks5 = ["dep:smoltcp"] socks5 = ["dep:smoltcp"]
jemalloc = ["dep:jemallocator", "dep:jemalloc-ctl", "dep:jemalloc-sys"]
+25 -7
View File
@@ -71,6 +71,8 @@ impl WindowsBuild {
if target.contains("x86_64") { if target.contains("x86_64") {
println!("cargo:rustc-link-search=native=easytier/third_party/"); println!("cargo:rustc-link-search=native=easytier/third_party/");
} else if target.contains("i686") {
println!("cargo:rustc-link-search=native=easytier/third_party/i686/");
} else if target.contains("aarch64") { } else if target.contains("aarch64") {
println!("cargo:rustc-link-search=native=easytier/third_party/arm64/"); println!("cargo:rustc-link-search=native=easytier/third_party/arm64/");
} }
@@ -125,23 +127,34 @@ fn check_locale() {
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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();
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
WindowsBuild::check_for_win(); WindowsBuild::check_for_win();
let proto_files_reflect = ["src/proto/peer_rpc.proto", "src/proto/common.proto"];
let proto_files = [ let proto_files = [
"src/proto/peer_rpc.proto",
"src/proto/common.proto",
"src/proto/error.proto", "src/proto/error.proto",
"src/proto/tests.proto", "src/proto/tests.proto",
"src/proto/cli.proto", "src/proto/cli.proto",
"src/proto/web.proto", "src/proto/web.proto",
"src/proto/magic_dns.proto",
]; ];
for proto_file in &proto_files { for proto_file in proto_files.iter().chain(proto_files_reflect.iter()) {
println!("cargo:rerun-if-changed={}", proto_file); println!("cargo:rerun-if-changed={}", proto_file);
} }
prost_build::Config::new() let mut config = prost_build::Config::new();
config
.protoc_arg("--experimental_allow_proto3_optional") .protoc_arg("--experimental_allow_proto3_optional")
.type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]") .type_attribute(".common", "#[derive(serde::Serialize, serde::Deserialize)]")
.type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]") .type_attribute(".error", "#[derive(serde::Serialize, serde::Deserialize)]")
@@ -155,10 +168,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]") .type_attribute("peer_rpc.PeerInfoForGlobalMap", "#[derive(Hash)]")
.type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]") .type_attribute("peer_rpc.ForeignNetworkRouteInfoKey", "#[derive(Hash, Eq)]")
.type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]") .type_attribute("common.RpcDescriptor", "#[derive(Hash, Eq)]")
.field_attribute(".web.NetworkConfig", "#[serde(default)]")
.service_generator(Box::new(rpc_build::ServiceGenerator::new())) .service_generator(Box::new(rpc_build::ServiceGenerator::new()))
.btree_map(["."]) .btree_map(["."]);
.compile_protos(&proto_files, &["src/proto/"])
.unwrap(); config.compile_protos(&proto_files, &["src/proto/"])?;
prost_reflect_build::Builder::new()
.file_descriptor_set_bytes("crate::proto::DESCRIPTOR_POOL_BYTES")
.compile_protos_with_config(config, &proto_files_reflect, &["src/proto/"])?;
check_locale(); check_locale();
Ok(()) Ok(())
+28 -4
View File
@@ -11,8 +11,8 @@ core_clap:
完整URL--config-server udp://127.0.0.1:22020/admin 完整URL--config-server udp://127.0.0.1:22020/admin
仅用户名:--config-server admin,将使用官方的服务器 仅用户名:--config-server admin,将使用官方的服务器
config_file: config_file:
en: "path to the config file, NOTE: if this is set, all other options will be ignored" en: "path to the config file, NOTE: the options set by cmdline args will override options in config file"
zh-CN: "配置文件路径,注意:如果设置了这个选项,其他所有选项都将被忽略" zh-CN: "配置文件路径,注意:命令行中的配置的选项会覆盖配置文件中的选项"
network_name: network_name:
en: "network name to identify this vpn network" en: "network name to identify this vpn network"
zh-CN: "用于标识此VPN网络的网络名称" zh-CN: "用于标识此VPN网络的网络名称"
@@ -96,12 +96,15 @@ core_clap:
enable_exit_node: enable_exit_node:
en: "allow this node to be an exit node" en: "allow this node to be an exit node"
zh-CN: "允许此节点成为出口节点" zh-CN: "允许此节点成为出口节点"
proxy_forward_by_system:
en: "forward packet to proxy networks via system kernel, disable internal nat for network proxy"
zh-CN: "通过系统内核转发子网代理数据包,禁用内置NAT"
no_tun: no_tun:
en: "do not create TUN device, can use subnet proxy to access node" en: "do not create TUN device, can use subnet proxy to access node"
zh-CN: "不创建TUN设备,可以使用子网代理访问节点" zh-CN: "不创建TUN设备,可以使用子网代理访问节点"
use_smoltcp: use_smoltcp:
en: "enable smoltcp stack for subnet proxy" en: "enable smoltcp stack for subnet proxy and kcp proxy"
zh-CN: "为子网代理启用smoltcp堆栈" zh-CN: "为子网代理和 KCP 代理启用smoltcp堆栈"
manual_routes: manual_routes:
en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16" en: "assign routes cidr manually, will disable subnet proxy and wireguard routes propagated from peers. e.g.: 192.168.0.0/16"
zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16" zh-CN: "手动分配路由CIDR,将禁用子网代理和从对等节点传播的wireguard路由。例如:192.168.0.0/16"
@@ -134,6 +137,27 @@ core_clap:
compression: compression:
en: "compression algorithm to use, support none, zstd. default is none" en: "compression algorithm to use, support none, zstd. default is none"
zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none" zh-CN: "要使用的压缩算法,支持 none、zstd。默认为 none"
mapped_listeners:
en: "manually specify the public address of the listener, other nodes can use this address to connect to this node. e.g.: tcp://123.123.123.123:11223, can specify multiple."
zh-CN: "手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。例如:tcp://123.123.123.123:11223,可以指定多个。"
bind_device:
en: "bind the connector socket to physical devices to avoid routing issues. e.g.: subnet proxy segment conflicts with a node's segment, after binding the physical device, it can communicate with the node normally."
zh-CN: "将连接器的套接字绑定到物理设备以避免路由问题。比如子网代理网段与某节点的网段冲突,绑定物理设备后可以与该节点正常通信。"
enable_kcp_proxy:
en: "proxy tcp streams with kcp, improving the latency and throughput on the network with udp packet loss."
zh-CN: "使用 KCP 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。"
disable_kcp_input:
en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved."
zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。"
port_forward:
en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple."
zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。"
accept_dns:
en: "if true, enable magic dns. with magic dns, you can access other nodes with a domain name, e.g.: <hostname>.et.net. magic dns will modify your system dns settings, enable it carefully."
zh-CN: "如果为true,则启用魔法DNS。使用魔法DNS,您可以使用域名访问其他节点,例如:<hostname>.et.net。魔法DNS将修改您的系统DNS设置,请谨慎启用。"
private_mode:
en: "if true, nodes with different network names or passwords from this network are not allowed to perform handshake or relay through this node."
zh-CN: "如果为true,则不允许使用了与本网络不相同的网络名称和密码的节点通过本节点进行握手或中转"
core_app: core_app:
panic_backtrace_save: panic_backtrace_save:
+119 -37
View File
@@ -1,26 +1,27 @@
use std::{ use std::{io, net::SocketAddr, os::windows::io::AsRawSocket};
ffi::c_void,
io::{self, ErrorKind},
mem,
net::SocketAddr,
os::windows::io::AsRawSocket,
ptr,
};
use anyhow::Context;
use network_interface::NetworkInterfaceConfig; use network_interface::NetworkInterfaceConfig;
use windows_sys::{ use windows::{
core::PCSTR, core::BSTR,
Win32::{ Win32::{
Foundation::{BOOL, FALSE}, Foundation::{BOOL, FALSE},
NetworkManagement::WindowsFirewall::{
INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_PRIVATE,
NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN, NET_FW_RULE_DIR_OUT,
},
Networking::WinSock::{ Networking::WinSock::{
htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6, htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6,
IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR, IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR,
}, },
System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_MULTITHREADED,
},
}, },
}; };
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> { pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
let handle = socket.as_raw_socket() as SOCKET; let handle = SOCKET(socket.as_raw_socket() as usize);
unsafe { unsafe {
// Ignoring UdpSocket's WSAECONNRESET error // Ignoring UdpSocket's WSAECONNRESET error
@@ -39,21 +40,18 @@ pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
let ret = WSAIoctl( let ret = WSAIoctl(
handle, handle,
SIO_UDP_CONNRESET, SIO_UDP_CONNRESET,
&enable as *const _ as *const c_void, Some(&enable as *const _ as *const std::ffi::c_void),
mem::size_of_val(&enable) as u32, std::mem::size_of_val(&enable) as u32,
ptr::null_mut(), None,
0, 0,
&mut bytes_returned as *mut _, &mut bytes_returned as *mut _,
ptr::null_mut(), None,
None, None,
); );
if ret == SOCKET_ERROR { if ret == SOCKET_ERROR {
use std::io::Error;
// Error occurs
let err_code = WSAGetLastError(); let err_code = WSAGetLastError();
return Err(Error::from_raw_os_error(err_code)); return Err(std::io::Error::from_raw_os_error(err_code.0));
} }
} }
@@ -63,7 +61,7 @@ pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
pub fn interface_count() -> io::Result<usize> { pub fn interface_count() -> io::Result<usize> {
let ifaces = network_interface::NetworkInterface::show().map_err(|e| { let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
io::Error::new( io::Error::new(
ErrorKind::NotFound, io::ErrorKind::NotFound,
format!("Failed to get interfaces. error: {}", e), format!("Failed to get interfaces. error: {}", e),
) )
})?; })?;
@@ -73,7 +71,7 @@ pub fn interface_count() -> io::Result<usize> {
pub fn find_interface_index(iface_name: &str) -> io::Result<u32> { pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
let ifaces = network_interface::NetworkInterface::show().map_err(|e| { let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
io::Error::new( io::Error::new(
ErrorKind::NotFound, io::ErrorKind::NotFound,
format!("Failed to get interfaces. {}, error: {}", iface_name, e), format!("Failed to get interfaces. {}, error: {}", iface_name, e),
) )
})?; })?;
@@ -82,7 +80,7 @@ pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
} }
tracing::error!("Failed to find interface index for {}", iface_name); tracing::error!("Failed to find interface index for {}", iface_name);
Err(io::Error::new( Err(io::Error::new(
ErrorKind::NotFound, io::ErrorKind::NotFound,
format!("{}", iface_name), format!("{}", iface_name),
)) ))
} }
@@ -92,7 +90,7 @@ pub fn set_ip_unicast_if<S: AsRawSocket>(
addr: &SocketAddr, addr: &SocketAddr,
iface: &str, iface: &str,
) -> io::Result<()> { ) -> io::Result<()> {
let handle = socket.as_raw_socket() as SOCKET; let handle = SOCKET(socket.as_raw_socket() as usize);
let if_index = find_interface_index(iface)?; let if_index = find_interface_index(iface)?;
@@ -100,30 +98,23 @@ pub fn set_ip_unicast_if<S: AsRawSocket>(
// https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options // https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
let ret = match addr { let ret = match addr {
SocketAddr::V4(..) => { SocketAddr::V4(..) => {
// Interface index is in network byte order for IPPROTO_IP.
let if_index = htonl(if_index); let if_index = htonl(if_index);
setsockopt( let if_index_bytes = if_index.to_ne_bytes();
handle, setsockopt(handle, IPPROTO_IP.0, IP_UNICAST_IF, Some(&if_index_bytes))
IPPROTO_IP as i32,
IP_UNICAST_IF as i32,
&if_index as *const _ as PCSTR,
mem::size_of_val(&if_index) as i32,
)
} }
SocketAddr::V6(..) => { SocketAddr::V6(..) => {
// Interface index is in host byte order for IPPROTO_IPV6. let if_index_bytes = if_index.to_ne_bytes();
setsockopt( setsockopt(
handle, handle,
IPPROTO_IPV6 as i32, IPPROTO_IPV6.0,
IPV6_UNICAST_IF as i32, IPV6_UNICAST_IF,
&if_index as *const _ as PCSTR, Some(&if_index_bytes),
mem::size_of_val(&if_index) as i32,
) )
} }
}; };
if ret == SOCKET_ERROR { if ret == SOCKET_ERROR {
let err = io::Error::from_raw_os_error(WSAGetLastError()); let err = std::io::Error::from_raw_os_error(WSAGetLastError().0);
tracing::error!( tracing::error!(
"set IP_UNICAST_IF / IPV6_UNICAST_IF interface: {}, index: {}, error: {}", "set IP_UNICAST_IF / IPV6_UNICAST_IF interface: {}, index: {}, error: {}",
iface, iface,
@@ -153,3 +144,94 @@ pub fn setup_socket_for_win<S: AsRawSocket>(
Ok(()) Ok(())
} }
struct ComInitializer;
impl ComInitializer {
fn new() -> windows::core::Result<Self> {
unsafe { CoInitializeEx(None, COINIT_MULTITHREADED)? };
Ok(Self)
}
}
impl Drop for ComInitializer {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
pub fn do_add_self_to_firewall_allowlist(inbound: bool) -> anyhow::Result<()> {
let _com = ComInitializer::new()?;
// 创建防火墙策略实例
let policy: INetFwPolicy2 = unsafe {
CoCreateInstance(
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
None,
CLSCTX_ALL,
)
}?;
// 创建防火墙规则实例
let rule: INetFwRule = unsafe {
CoCreateInstance(
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwRule,
None,
CLSCTX_ALL,
)
}?;
// 设置规则属性
let exe_path = std::env::current_exe()
.with_context(|| "Failed to get current executable path when adding firewall rule")?
.to_string_lossy()
.replace(r"\\?\", "");
let name = BSTR::from(format!(
"EasyTier {} ({})",
exe_path,
if inbound { "Inbound" } else { "Outbound" }
));
let desc = BSTR::from("Allow EasyTier to do subnet proxy and kcp proxy");
let app_path = BSTR::from(&exe_path);
unsafe {
rule.SetName(&name)?;
rule.SetDescription(&desc)?;
rule.SetApplicationName(&app_path)?;
rule.SetAction(NET_FW_ACTION_ALLOW)?;
if inbound {
rule.SetDirection(NET_FW_RULE_DIR_IN)?; // 允许入站连接
} else {
rule.SetDirection(NET_FW_RULE_DIR_OUT)?; // 允许出站连接
}
rule.SetEnabled(windows::Win32::Foundation::VARIANT_TRUE)?;
rule.SetProfiles(NET_FW_PROFILE2_PRIVATE.0 | NET_FW_PROFILE2_PUBLIC.0)?;
rule.SetGrouping(&BSTR::from("EasyTier"))?;
// 获取规则集合并添加新规则
let rules = policy.Rules()?;
rules.Remove(&name)?; // 先删除同名规则
rules.Add(&rule)?;
}
Ok(())
}
pub fn add_self_to_firewall_allowlist() -> anyhow::Result<()> {
do_add_self_to_firewall_allowlist(true)?;
do_add_self_to_firewall_allowlist(false)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_self_to_firewall_allowlist() {
let res = add_self_to_firewall_allowlist();
assert!(res.is_ok());
}
}
+45 -21
View File
@@ -1,5 +1,10 @@
use async_compression::tokio::write::{ZstdDecoder, ZstdEncoder}; use std::io::{Read, Write};
use tokio::io::AsyncWriteExt;
use dashmap::DashMap;
use std::cell::RefCell;
use zstd::stream::read::Decoder;
use zstd::stream::write::Encoder;
use zstd::zstd_safe::{CCtx, DCtx};
use zerocopy::{AsBytes as _, FromBytes as _}; use zerocopy::{AsBytes as _, FromBytes as _};
@@ -29,17 +34,20 @@ impl DefaultCompressor {
data: &[u8], data: &[u8],
compress_algo: CompressorAlgo, compress_algo: CompressorAlgo,
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
let buf = match compress_algo { match compress_algo {
CompressorAlgo::ZstdDefault => { CompressorAlgo::ZstdDefault => {
let mut o = ZstdEncoder::new(Vec::new()); let ret = CTX_MAP.with(|map_cell| {
o.write_all(data).await?; let map = map_cell.borrow();
o.shutdown().await?; let mut ctx_entry = map.entry(compress_algo).or_default();
o.into_inner() let writer = Vec::new();
let mut o = Encoder::with_context(writer, ctx_entry.value_mut());
o.write_all(data)?;
o.finish()
});
Ok(ret?)
}
CompressorAlgo::None => Ok(data.to_vec()),
} }
CompressorAlgo::None => data.to_vec(),
};
Ok(buf)
} }
pub async fn decompress_raw( pub async fn decompress_raw(
@@ -47,17 +55,17 @@ impl DefaultCompressor {
data: &[u8], data: &[u8],
compress_algo: CompressorAlgo, compress_algo: CompressorAlgo,
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
let buf = match compress_algo { match compress_algo {
CompressorAlgo::ZstdDefault => { CompressorAlgo::ZstdDefault => DCTX_MAP.with(|map_cell| {
let mut o = ZstdDecoder::new(Vec::new()); let map = map_cell.borrow();
o.write_all(data).await?; let mut ctx_entry = map.entry(compress_algo).or_default();
o.shutdown().await?; let mut decoder = Decoder::with_context(data, ctx_entry.value_mut());
o.into_inner() let mut output = Vec::new();
decoder.read_to_end(&mut output)?;
Ok(output)
}),
CompressorAlgo::None => Ok(data.to_vec()),
} }
CompressorAlgo::None => data.to_vec(),
};
Ok(buf)
} }
} }
@@ -146,6 +154,11 @@ impl Compressor for DefaultCompressor {
} }
} }
thread_local! {
static CTX_MAP: RefCell<DashMap<CompressorAlgo, CCtx<'static>>> = RefCell::new(DashMap::new());
static DCTX_MAP: RefCell<DashMap<CompressorAlgo, DCtx<'static>>> = RefCell::new(DashMap::new());
}
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use super::*; use super::*;
@@ -158,10 +171,21 @@ pub mod tests {
let compressor = DefaultCompressor {}; let compressor = DefaultCompressor {};
println!(
"Uncompressed packet: {:?}, len: {}",
packet,
packet.payload_len()
);
compressor compressor
.compress(&mut packet, CompressorAlgo::ZstdDefault) .compress(&mut packet, CompressorAlgo::ZstdDefault)
.await .await
.unwrap(); .unwrap();
println!(
"Compressed packet: {:?}, len: {}",
packet,
packet.payload_len()
);
assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), true); assert_eq!(packet.peer_manager_header().unwrap().is_compressed(), true);
compressor.decompress(&mut packet).await.unwrap(); compressor.decompress(&mut packet).await.unwrap();
+124 -34
View File
@@ -7,7 +7,10 @@ use std::{
use anyhow::Context; use anyhow::Context;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{proto::common::CompressionAlgoPb, tunnel::generate_digest_from_str}; use crate::{
proto::common::{CompressionAlgoPb, PortForwardConfigPb, SocketType},
tunnel::generate_digest_from_str,
};
pub type Flags = crate::proto::common::FlagsInConfig; pub type Flags = crate::proto::common::FlagsInConfig;
@@ -20,15 +23,21 @@ pub fn gen_default_flags() -> Flags {
mtu: 1380, mtu: 1380,
latency_first: false, latency_first: false,
enable_exit_node: false, enable_exit_node: false,
proxy_forward_by_system: false,
no_tun: false, no_tun: false,
use_smoltcp: false, use_smoltcp: false,
relay_network_whitelist: "*".to_string(), relay_network_whitelist: "*".to_string(),
disable_p2p: false, disable_p2p: false,
relay_all_peer_rpc: false, relay_all_peer_rpc: false,
disable_udp_hole_punching: false, disable_udp_hole_punching: false,
ipv6_listener: "udp://[::]:0".to_string(), multi_thread: true,
multi_thread: false,
data_compress_algo: CompressionAlgoPb::None.into(), data_compress_algo: CompressionAlgoPb::None.into(),
bind_device: true,
enable_kcp_proxy: false,
disable_kcp_input: false,
disable_relay_kcp: true,
accept_dns: false,
private_mode: false,
} }
} }
@@ -69,9 +78,12 @@ pub trait ConfigLoader: Send + Sync {
fn get_peers(&self) -> Vec<PeerConfig>; fn get_peers(&self) -> Vec<PeerConfig>;
fn set_peers(&self, peers: Vec<PeerConfig>); fn set_peers(&self, peers: Vec<PeerConfig>);
fn get_listeners(&self) -> Vec<url::Url>; fn get_listeners(&self) -> Option<Vec<url::Url>>;
fn set_listeners(&self, listeners: Vec<url::Url>); fn set_listeners(&self, listeners: Vec<url::Url>);
fn get_mapped_listeners(&self) -> Vec<url::Url>;
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>);
fn get_rpc_portal(&self) -> Option<SocketAddr>; fn get_rpc_portal(&self) -> Option<SocketAddr>;
fn set_rpc_portal(&self, addr: SocketAddr); fn set_rpc_portal(&self, addr: SocketAddr);
@@ -90,6 +102,9 @@ pub trait ConfigLoader: Send + Sync {
fn get_socks5_portal(&self) -> Option<url::Url>; fn get_socks5_portal(&self) -> Option<url::Url>;
fn set_socks5_portal(&self, addr: Option<url::Url>); fn set_socks5_portal(&self, addr: Option<url::Url>);
fn get_port_forwards(&self) -> Vec<PortForwardConfig>;
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>);
fn dump(&self) -> String; fn dump(&self) -> String;
} }
@@ -173,6 +188,41 @@ pub struct VpnPortalConfig {
pub wireguard_listen: SocketAddr, pub wireguard_listen: SocketAddr,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct PortForwardConfig {
pub bind_addr: SocketAddr,
pub dst_addr: SocketAddr,
pub proto: String,
}
impl From<PortForwardConfigPb> for PortForwardConfig {
fn from(config: PortForwardConfigPb) -> Self {
PortForwardConfig {
bind_addr: config.bind_addr.unwrap_or_default().into(),
dst_addr: config.dst_addr.unwrap_or_default().into(),
proto: match SocketType::try_from(config.socket_type) {
Ok(SocketType::Tcp) => "tcp".to_string(),
Ok(SocketType::Udp) => "udp".to_string(),
_ => "tcp".to_string(),
},
}
}
}
impl Into<PortForwardConfigPb> for PortForwardConfig {
fn into(self) -> PortForwardConfigPb {
PortForwardConfigPb {
bind_addr: Some(self.bind_addr.into()),
dst_addr: Some(self.dst_addr.into()),
socket_type: match self.proto.to_lowercase().as_str() {
"tcp" => SocketType::Tcp as i32,
"udp" => SocketType::Udp as i32,
_ => SocketType::Tcp as i32,
},
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
struct Config { struct Config {
netns: Option<String>, netns: Option<String>,
@@ -183,6 +233,7 @@ struct Config {
dhcp: Option<bool>, dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>, network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>, listeners: Option<Vec<url::Url>>,
mapped_listeners: Option<Vec<url::Url>>,
exit_nodes: Option<Vec<Ipv4Addr>>, exit_nodes: Option<Vec<Ipv4Addr>>,
peer: Option<Vec<PeerConfig>>, peer: Option<Vec<PeerConfig>>,
@@ -199,6 +250,8 @@ struct Config {
socks5_proxy: Option<url::Url>, socks5_proxy: Option<url::Url>,
port_forward: Option<Vec<PortForwardConfig>>,
flags: Option<serde_json::Map<String, serde_json::Value>>, flags: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(skip)] #[serde(skip)]
@@ -223,20 +276,23 @@ impl TomlConfigLoader {
config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default())); config.flags_struct = Some(Self::gen_flags(config.flags.clone().unwrap_or_default()));
Ok(TomlConfigLoader { let config = TomlConfigLoader {
config: Arc::new(Mutex::new(config)), config: Arc::new(Mutex::new(config)),
}) };
let old_ns = config.get_network_identity();
config.set_network_identity(NetworkIdentity::new(
old_ns.network_name,
old_ns.network_secret.unwrap_or_default(),
));
Ok(config)
} }
pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> { pub fn new(config_path: &PathBuf) -> Result<Self, anyhow::Error> {
let config_str = std::fs::read_to_string(config_path) let config_str = std::fs::read_to_string(config_path)
.with_context(|| format!("failed to read config file: {:?}", config_path))?; .with_context(|| format!("failed to read config file: {:?}", config_path))?;
let ret = Self::new_from_str(&config_str)?; let ret = Self::new_from_str(&config_str)?;
let old_ns = ret.get_network_identity();
ret.set_network_identity(NetworkIdentity::new(
old_ns.network_name,
old_ns.network_secret.unwrap_or_default(),
));
Ok(ret) Ok(ret)
} }
@@ -459,19 +515,27 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().peer = Some(peers); self.config.lock().unwrap().peer = Some(peers);
} }
fn get_listeners(&self) -> Vec<url::Url> { fn get_listeners(&self) -> Option<Vec<url::Url>> {
self.config self.config.lock().unwrap().listeners.clone()
.lock()
.unwrap()
.listeners
.clone()
.unwrap_or_default()
} }
fn set_listeners(&self, listeners: Vec<url::Url>) { fn set_listeners(&self, listeners: Vec<url::Url>) {
self.config.lock().unwrap().listeners = Some(listeners); self.config.lock().unwrap().listeners = Some(listeners);
} }
fn get_mapped_listeners(&self) -> Vec<url::Url> {
self.config
.lock()
.unwrap()
.mapped_listeners
.clone()
.unwrap_or_default()
}
fn set_mapped_listeners(&self, listeners: Option<Vec<url::Url>>) {
self.config.lock().unwrap().mapped_listeners = listeners;
}
fn get_rpc_portal(&self) -> Option<SocketAddr> { fn get_rpc_portal(&self) -> Option<SocketAddr> {
self.config.lock().unwrap().rpc_portal self.config.lock().unwrap().rpc_portal
} }
@@ -513,6 +577,35 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().exit_nodes = Some(nodes); self.config.lock().unwrap().exit_nodes = Some(nodes);
} }
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>> {
self.config.lock().unwrap().routes.clone()
}
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
self.config.lock().unwrap().routes = routes;
}
fn get_socks5_portal(&self) -> Option<url::Url> {
self.config.lock().unwrap().socks5_proxy.clone()
}
fn set_socks5_portal(&self, addr: Option<url::Url>) {
self.config.lock().unwrap().socks5_proxy = addr;
}
fn get_port_forwards(&self) -> Vec<PortForwardConfig> {
self.config
.lock()
.unwrap()
.port_forward
.clone()
.unwrap_or_default()
}
fn set_port_forwards(&self, forwards: Vec<PortForwardConfig>) {
self.config.lock().unwrap().port_forward = Some(forwards);
}
fn dump(&self) -> String { fn dump(&self) -> String {
let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap(); let default_flags_json = serde_json::to_string(&gen_default_flags()).unwrap();
let default_flags_hashmap = let default_flags_hashmap =
@@ -537,22 +630,6 @@ impl ConfigLoader for TomlConfigLoader {
config.flags = Some(flag_map); config.flags = Some(flag_map);
toml::to_string_pretty(&config).unwrap() toml::to_string_pretty(&config).unwrap()
} }
fn get_routes(&self) -> Option<Vec<cidr::Ipv4Cidr>> {
self.config.lock().unwrap().routes.clone()
}
fn set_routes(&self, routes: Option<Vec<cidr::Ipv4Cidr>>) {
self.config.lock().unwrap().routes = routes;
}
fn get_socks5_portal(&self) -> Option<url::Url> {
self.config.lock().unwrap().socks5_proxy.clone()
}
fn set_socks5_portal(&self, addr: Option<url::Url>) {
self.config.lock().unwrap().socks5_proxy = addr;
}
} }
#[cfg(test)] #[cfg(test)]
@@ -593,6 +670,11 @@ dir = "/tmp/easytier"
[console_logger] [console_logger]
level = "warn" level = "warn"
[[port_forward]]
bind_addr = "0.0.0.0:11011"
dst_addr = "192.168.94.33:11011"
proto = "tcp"
"#; "#;
let ret = TomlConfigLoader::new_from_str(config_str); let ret = TomlConfigLoader::new_from_str(config_str);
if let Err(e) = &ret { if let Err(e) = &ret {
@@ -613,6 +695,14 @@ level = "warn"
.collect::<Vec<String>>() .collect::<Vec<String>>()
); );
assert_eq!(
vec![PortForwardConfig {
bind_addr: "0.0.0.0:11011".parse().unwrap(),
dst_addr: "192.168.94.33:11011".parse().unwrap(),
proto: "tcp".to_string(),
}],
ret.get_port_forwards()
);
println!("{}", ret.dump()); println!("{}", ret.dump());
} }
} }
+134
View File
@@ -0,0 +1,134 @@
use std::net::SocketAddr;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Context;
use hickory_proto::runtime::TokioRuntimeProvider;
use hickory_proto::xfer::Protocol;
use hickory_resolver::config::{LookupIpStrategy, NameServerConfig, ResolverConfig, ResolverOpts};
use hickory_resolver::name_server::{GenericConnector, TokioConnectionProvider};
use hickory_resolver::system_conf::read_system_conf;
use hickory_resolver::{Resolver, TokioResolver};
use once_cell::sync::Lazy;
use tokio::net::lookup_host;
use super::error::Error;
pub fn get_default_resolver_config() -> ResolverConfig {
let mut default_resolve_config = ResolverConfig::new();
default_resolve_config.add_name_server(NameServerConfig::new(
"223.5.5.5:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config.add_name_server(NameServerConfig::new(
"180.184.1.1:53".parse().unwrap(),
Protocol::Udp,
));
default_resolve_config
}
pub static ALLOW_USE_SYSTEM_DNS_RESOLVER: Lazy<AtomicBool> = Lazy::new(|| AtomicBool::new(true));
pub static RESOLVER: Lazy<Arc<Resolver<GenericConnector<TokioRuntimeProvider>>>> =
Lazy::new(|| {
let system_cfg = read_system_conf();
let mut cfg = get_default_resolver_config();
let mut opt = ResolverOpts::default();
if let Ok(s) = system_cfg {
for ns in s.0.name_servers() {
cfg.add_name_server(ns.clone());
}
opt = s.1;
}
opt.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
let builder = TokioResolver::builder_with_config(cfg, TokioConnectionProvider::default())
.with_options(opt);
Arc::new(builder.build())
});
pub async fn resolve_txt_record(domain_name: &str) -> Result<String, Error> {
let r = RESOLVER.clone();
let response = r.txt_lookup(domain_name).await.with_context(|| {
format!(
"txt_lookup failed, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_record = response.iter().next().with_context(|| {
format!(
"no txt record found, domain_name: {}",
domain_name.to_string()
)
})?;
let txt_data = String::from_utf8_lossy(&txt_record.txt_data()[0]);
tracing::info!(?txt_data, ?domain_name, "get txt record");
Ok(txt_data.to_string())
}
pub async fn socket_addrs(
url: &url::Url,
default_port_number: impl Fn() -> Option<u16>,
) -> Result<Vec<SocketAddr>, Error> {
let host = url.host_str().ok_or(Error::InvalidUrl(url.to_string()))?;
let port = url
.port()
.or_else(default_port_number)
.ok_or(Error::InvalidUrl(url.to_string()))?;
// if host is an ip address, return it directly
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
return Ok(vec![SocketAddr::new(ip, port)]);
}
if ALLOW_USE_SYSTEM_DNS_RESOLVER.load(std::sync::atomic::Ordering::Relaxed) {
let socket_addr = format!("{}:{}", host, port);
match lookup_host(socket_addr).await {
Ok(a) => {
let a = a.collect();
tracing::debug!(?a, "system dns lookup done");
return Ok(a);
}
Err(e) => {
tracing::error!(?e, "system dns lookup failed");
}
}
}
// use hickory_resolver
let ret = RESOLVER.lookup_ip(host).await.with_context(|| {
format!(
"hickory dns lookup_ip failed, host: {}, port: {}",
host, port
)
})?;
Ok(ret
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect::<Vec<_>>())
}
#[cfg(test)]
mod tests {
use crate::defer;
use super::*;
#[tokio::test]
async fn test_socket_addrs() {
let url = url::Url::parse("tcp://public.easytier.cn:80").unwrap();
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs: {:?}", addrs);
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(false, std::sync::atomic::Ordering::Relaxed);
defer!(
ALLOW_USE_SYSTEM_DNS_RESOLVER.store(true, std::sync::atomic::Ordering::Relaxed);
);
let addrs = socket_addrs(&url, || Some(80)).await.unwrap();
assert_eq!(2, addrs.len(), "addrs: {:?}", addrs);
println!("addrs2: {:?}", addrs);
}
}
+62 -28
View File
@@ -5,7 +5,7 @@ use std::{
}; };
use crate::proto::cli::PeerConnInfo; use crate::proto::cli::PeerConnInfo;
use crate::proto::common::PeerFeatureFlag; use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb};
use crossbeam::atomic::AtomicCell; use crossbeam::atomic::AtomicCell;
use super::{ use super::{
@@ -42,6 +42,8 @@ pub enum GlobalCtxEvent {
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new) DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>), DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
PortForwardAdded(PortForwardConfigPb),
} }
pub type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>; pub type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
@@ -59,15 +61,16 @@ pub struct GlobalCtx {
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>, cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>, cached_proxy_cidrs: AtomicCell<Option<Vec<cidr::IpCidr>>>,
ip_collector: Arc<IPCollector>, ip_collector: Mutex<Option<Arc<IPCollector>>>,
hostname: String, hostname: Mutex<String>,
stun_info_collection: Box<dyn StunInfoCollectorTrait>, stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
running_listeners: Mutex<Vec<url::Url>>, running_listeners: Mutex<Vec<url::Url>>,
enable_exit_node: bool, enable_exit_node: bool,
proxy_forward_by_system: bool,
no_tun: bool, no_tun: bool,
feature_flags: AtomicCell<PeerFeatureFlag>, feature_flags: AtomicCell<PeerFeatureFlag>,
@@ -94,13 +97,18 @@ impl GlobalCtx {
let net_ns = NetNS::new(config_fs.get_netns()); let net_ns = NetNS::new(config_fs.get_netns());
let hostname = config_fs.get_hostname(); let hostname = config_fs.get_hostname();
let (event_bus, _) = tokio::sync::broadcast::channel(1024); let (event_bus, _) = tokio::sync::broadcast::channel(8);
let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers()); let stun_info_collection = Arc::new(StunInfoCollector::new_with_default_servers());
let enable_exit_node = config_fs.get_flags().enable_exit_node; let enable_exit_node = config_fs.get_flags().enable_exit_node;
let proxy_forward_by_system = config_fs.get_flags().proxy_forward_by_system;
let no_tun = config_fs.get_flags().no_tun; let no_tun = config_fs.get_flags().no_tun;
let mut feature_flags = PeerFeatureFlag::default();
feature_flags.kcp_input = !config_fs.get_flags().disable_kcp_input;
feature_flags.no_relay_kcp = config_fs.get_flags().disable_relay_kcp;
GlobalCtx { GlobalCtx {
inst_name: config_fs.get_inst_name(), inst_name: config_fs.get_inst_name(),
id, id,
@@ -112,18 +120,22 @@ impl GlobalCtx {
cached_ipv4: AtomicCell::new(None), cached_ipv4: AtomicCell::new(None),
cached_proxy_cidrs: AtomicCell::new(None), cached_proxy_cidrs: AtomicCell::new(None),
ip_collector: Arc::new(IPCollector::new(net_ns, stun_info_collection.clone())), ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
net_ns,
stun_info_collection.clone(),
)))),
hostname, hostname: Mutex::new(hostname),
stun_info_collection: Box::new(stun_info_collection), stun_info_collection: Mutex::new(stun_info_collection),
running_listeners: Mutex::new(Vec::new()), running_listeners: Mutex::new(Vec::new()),
enable_exit_node, enable_exit_node,
proxy_forward_by_system,
no_tun, no_tun,
feature_flags: AtomicCell::new(PeerFeatureFlag::default()), feature_flags: AtomicCell::new(feature_flags),
} }
} }
@@ -132,10 +144,13 @@ impl GlobalCtx {
} }
pub fn issue_event(&self, event: GlobalCtxEvent) { pub fn issue_event(&self, event: GlobalCtxEvent) {
if self.event_bus.receiver_count() != 0 { if let Err(e) = self.event_bus.send(event.clone()) {
self.event_bus.send(event).unwrap(); tracing::warn!(
} else { "Failed to send event: {:?}, error: {:?}, receiver count: {}",
tracing::warn!("No subscriber for event: {:?}", event); event,
e,
self.event_bus.receiver_count()
);
} }
} }
@@ -203,26 +218,30 @@ impl GlobalCtx {
} }
pub fn get_ip_collector(&self) -> Arc<IPCollector> { pub fn get_ip_collector(&self) -> Arc<IPCollector> {
self.ip_collector.clone() self.ip_collector.lock().unwrap().as_ref().unwrap().clone()
} }
pub fn get_hostname(&self) -> String { pub fn get_hostname(&self) -> String {
return self.hostname.clone(); return self.hostname.lock().unwrap().clone();
} }
pub fn get_stun_info_collector(&self) -> impl StunInfoCollectorTrait + '_ { pub fn set_hostname(&self, hostname: String) {
self.stun_info_collection.as_ref() *self.hostname.lock().unwrap() = hostname;
}
pub fn get_stun_info_collector(&self) -> Arc<dyn StunInfoCollectorTrait> {
self.stun_info_collection.lock().unwrap().clone()
} }
pub fn replace_stun_info_collector(&self, collector: Box<dyn StunInfoCollectorTrait>) { pub fn replace_stun_info_collector(&self, collector: Box<dyn StunInfoCollectorTrait>) {
// force replace the stun_info_collection without mut and drop the old one let arc_collector: Arc<dyn StunInfoCollectorTrait> = Arc::new(collector);
let ptr = &self.stun_info_collection as *const Box<dyn StunInfoCollectorTrait>; *self.stun_info_collection.lock().unwrap() = arc_collector.clone();
let ptr = ptr as *mut Box<dyn StunInfoCollectorTrait>;
unsafe { // rebuild the ip collector
std::ptr::drop_in_place(ptr); *self.ip_collector.lock().unwrap() = Some(Arc::new(IPCollector::new(
#[allow(invalid_reference_casting)] self.net_ns.clone(),
std::ptr::write(ptr, collector); arc_collector,
} )));
} }
pub fn get_running_listeners(&self) -> Vec<url::Url> { pub fn get_running_listeners(&self) -> Vec<url::Url> {
@@ -230,7 +249,10 @@ impl GlobalCtx {
} }
pub fn add_running_listener(&self, url: url::Url) { pub fn add_running_listener(&self, url: url::Url) {
self.running_listeners.lock().unwrap().push(url); let mut l = self.running_listeners.lock().unwrap();
if !l.contains(&url) {
l.push(url);
}
} }
pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> { pub fn get_vpn_portal_cidr(&self) -> Option<cidr::Ipv4Cidr> {
@@ -266,6 +288,10 @@ impl GlobalCtx {
self.enable_exit_node self.enable_exit_node
} }
pub fn proxy_forward_by_system(&self) -> bool {
self.proxy_forward_by_system
}
pub fn no_tun(&self) -> bool { pub fn no_tun(&self) -> bool {
self.no_tun self.no_tun
} }
@@ -281,7 +307,10 @@ impl GlobalCtx {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use crate::common::{config::TomlConfigLoader, new_peer_id}; use crate::{
common::{config::TomlConfigLoader, new_peer_id, stun::MockStunInfoCollector},
proto::common::NatType,
};
use super::*; use super::*;
@@ -321,7 +350,12 @@ pub mod tests {
let config_fs = TomlConfigLoader::default(); let config_fs = TomlConfigLoader::default();
config_fs.set_inst_name(format!("test_{}", config_fs.get_id())); config_fs.set_inst_name(format!("test_{}", config_fs.get_id()));
config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default())); config_fs.set_network_identity(network_identy.unwrap_or(NetworkIdentity::default()));
std::sync::Arc::new(GlobalCtx::new(config_fs))
let ctx = Arc::new(GlobalCtx::new(config_fs));
ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
udp_nat_type: NatType::Unknown,
}));
ctx
} }
pub fn get_mock_global_ctx() -> ArcGlobalCtx { pub fn get_mock_global_ctx() -> ArcGlobalCtx {

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