From d34a51739fcc60aea5b9ea611157b39da2bd994f Mon Sep 17 00:00:00 2001 From: Zisu Zhang Date: Sat, 7 Jun 2025 08:19:31 +0800 Subject: [PATCH 001/165] Update default_port and sni logic to improve reverse proxy reachability (#947) --- easytier/src/common/dns.rs | 9 +++++++++ easytier/src/tunnel/websocket.rs | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/easytier/src/common/dns.rs b/easytier/src/common/dns.rs index 46acd2c13..5ff4f081f 100644 --- a/easytier/src/common/dns.rs +++ b/easytier/src/common/dns.rs @@ -77,6 +77,15 @@ pub async fn socket_addrs( .port() .or_else(default_port_number) .ok_or(Error::InvalidUrl(url.to_string()))?; + // See https://github.com/EasyTier/EasyTier/pull/947 + let port = match port { + 0 => match url.scheme() { + "ws" => 80, + "wss" => 443, + _ => port, + }, + _ => port, + }; // if host is an ip address, return it directly if let Ok(ip) = host.parse::() { diff --git a/easytier/src/tunnel/websocket.rs b/easytier/src/tunnel/websocket.rs index f81d4d860..0ad98b304 100644 --- a/easytier/src/tunnel/websocket.rs +++ b/easytier/src/tunnel/websocket.rs @@ -202,8 +202,11 @@ impl WSTunnelConnector { init_crypto_provider(); let tls_conn = tokio_rustls::TlsConnector::from(Arc::new(get_insecure_tls_client_config())); - // Modify SNI logic: always use "localhost" as SNI to avoid IP blocking. - let sni = "localhost"; + // Modify SNI logic: use "localhost" as SNI for url without domain to avoid IP blocking. + let sni = match addr.domain() { + None => "localhost".to_string(), + Some(domain) => domain.to_string(), + }; let server_name = rustls::pki_types::ServerName::try_from(sni) .map_err(|_| TunnelError::InvalidProtocol("Invalid SNI".to_string()))?; let stream = tls_conn.connect(server_name, stream).await?; From 6d88b10b14d8ef52561cf5004902f0756f9fcb2f Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 7 Jun 2025 10:39:42 +0800 Subject: [PATCH 002/165] remove LICENSE (#950) --- LICENSE | 73 --------------------------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8cd40f38a..000000000 --- a/LICENSE +++ /dev/null @@ -1,73 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright 2023 sunsijie - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. From 47f3efe71b5735d960fecc3b739fdc97a49adb4b Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 7 Jun 2025 10:56:54 +0800 Subject: [PATCH 003/165] Create LICENSE (#951) --- LICENSE | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..0a041280b --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. From f8908125779069efbc08103d117279e422062544 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 7 Jun 2025 12:24:11 +0800 Subject: [PATCH 004/165] kcp connect retry (#952) --- easytier/src/gateway/kcp_proxy.rs | 65 +++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/easytier/src/gateway/kcp_proxy.rs b/easytier/src/gateway/kcp_proxy.rs index 5a9c82adc..163b63644 100644 --- a/easytier/src/gateway/kcp_proxy.rs +++ b/easytier/src/gateway/kcp_proxy.rs @@ -20,7 +20,7 @@ use pnet::packet::{ Packet as _, }; use prost::Message; -use tokio::{io::copy_bidirectional, task::JoinSet}; +use tokio::{io::copy_bidirectional, select, task::JoinSet}; use super::{ tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy}, @@ -134,21 +134,54 @@ impl NatDstConnector for NatDstKcpConnector { return Err(anyhow::anyhow!("no dst peer found for nat dst: {}", nat_dst).into()); } - let ret = self - .kcp_endpoint - .connect( - Duration::from_secs(10), - self.peer_mgr.my_peer_id(), - dst_peers[0], - Bytes::from(conn_data.encode_to_vec()), - ) - .await - .with_context(|| format!("failed to connect to nat dst: {}", nat_dst.to_string()))?; - - let stream = KcpStream::new(&self.kcp_endpoint, ret) - .ok_or(anyhow::anyhow!("failed to create kcp stream"))?; - - Ok(stream) + let mut connect_tasks: JoinSet> = JoinSet::new(); + let mut retry_remain = 5; + loop { + select! { + Some(Ok(Ok(ret))) = connect_tasks.join_next() => { + // just wait for the previous connection to finish + let stream = KcpStream::new(&self.kcp_endpoint, ret) + .ok_or(anyhow::anyhow!("failed to create kcp stream"))?; + return Ok(stream); + } + _ = tokio::time::sleep(Duration::from_millis(200)), if !connect_tasks.is_empty() && retry_remain > 0 => { + // no successful connection yet, trigger another connection attempt + } + else => { + // got error in connect_tasks, continue to retry + if retry_remain == 0 && connect_tasks.is_empty() { + break; + } + } + } + + // create a new connection task + if retry_remain == 0 { + continue; + } + retry_remain -= 1; + + let kcp_endpoint = self.kcp_endpoint.clone(); + let peer_mgr = self.peer_mgr.clone(); + let dst_peer = dst_peers[0]; + let conn_data_clone = conn_data.clone(); + + connect_tasks.spawn(async move { + kcp_endpoint + .connect( + Duration::from_secs(10), + peer_mgr.my_peer_id(), + dst_peer, + Bytes::from(conn_data_clone.encode_to_vec()), + ) + .await + .with_context(|| { + format!("failed to connect to nat dst: {}", nat_dst.to_string()) + }) + }); + } + + Err(anyhow::anyhow!("failed to connect to nat dst: {}", nat_dst).into()) } fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool { From 3c7837692e654b4741a49341fbdc2d00b1e01fe6 Mon Sep 17 00:00:00 2001 From: Kiva Date: Sat, 7 Jun 2025 21:19:03 +0800 Subject: [PATCH 005/165] fix(vpn-portal): wireguard peer table should be kept if the client roamed to another endpoint address (#954) --- easytier/src/vpn_portal/wireguard.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/easytier/src/vpn_portal/wireguard.rs b/easytier/src/vpn_portal/wireguard.rs index ba7978944..780e516e4 100644 --- a/easytier/src/vpn_portal/wireguard.rs +++ b/easytier/src/vpn_portal/wireguard.rs @@ -85,6 +85,7 @@ impl WireGuardImpl { let mut ip_registered = false; let remote_addr = info.remote_addr.clone(); + let endpoint_addr = remote_addr.clone().map(Into::into); peer_mgr .get_global_ctx() .issue_event(GlobalCtxEvent::VpnPortalClientConnected( @@ -115,10 +116,12 @@ impl WireGuardImpl { }; if !ip_registered { let client_entry = Arc::new(ClientEntry { - endpoint_addr: remote_addr.clone().map(Into::into), + endpoint_addr: endpoint_addr.clone(), sink: mpsc_tunnel.get_sink(), }); map_key = Some(i.get_source()); + // Be careful here: we may overwrite an existing entry if the client IP is reused, + // which is common when clients are behind NAT. wg_peer_ip_table.insert(i.get_source(), client_entry.clone()); ip_registered = true; } @@ -130,8 +133,17 @@ impl WireGuardImpl { } if map_key.is_some() { - tracing::info!(?map_key, "Removing wg client from table"); - wg_peer_ip_table.remove(&map_key.unwrap()); + // Remove the client from the wg_peer_ip_table only when its endpoint address is unchanged, + // or we may break clients behind NAT. + match wg_peer_ip_table.remove_if(&map_key.unwrap(), |_, entry| { + entry.endpoint_addr == endpoint_addr + }) { + Some(_) => tracing::info!(?map_key, "Removed wg client from table"), + None => tracing::info!( + ?map_key, + "The wg client changed its endpoint address, not removing from table" + ), + } } peer_mgr From 707963c0d9b5d5d5ac1adb8a126dd5a886e6cd97 Mon Sep 17 00:00:00 2001 From: BlackLuny <602814112@qq.com> Date: Sat, 7 Jun 2025 22:05:11 +0800 Subject: [PATCH 006/165] Web dual stack (#953) * reimplement easytier-web dual stack * add protocol check for dual stack listener current only support tcp and udp --- easytier-web/src/client_manager/mod.rs | 54 ++++++++++++++------------ easytier-web/src/main.rs | 51 ++++++++++++++++++------ 2 files changed, 69 insertions(+), 36 deletions(-) diff --git a/easytier-web/src/client_manager/mod.rs b/easytier-web/src/client_manager/mod.rs index d8354a348..9c4830eec 100644 --- a/easytier-web/src/client_manager/mod.rs +++ b/easytier-web/src/client_manager/mod.rs @@ -1,21 +1,24 @@ pub mod session; pub mod storage; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, +}; use dashmap::DashMap; -use easytier::{ - common::scoped_task::ScopedTask, proto::web::HeartbeatRequest, tunnel::TunnelListener, -}; +use easytier::{proto::web::HeartbeatRequest, tunnel::TunnelListener}; use session::Session; use storage::{Storage, StorageToken}; +use tokio::task::JoinSet; use crate::db::{Db, UserIdInDb}; #[derive(Debug)] pub struct ClientManager { - accept_task: Option>, - clear_task: Option>, + tasks: JoinSet<()>, + + listeners_cnt: Arc, client_sessions: Arc>>, storage: Storage, @@ -23,24 +26,35 @@ pub struct ClientManager { impl ClientManager { pub fn new(db: Db) -> Self { + let client_sessions = Arc::new(DashMap::new()); + let sessions: Arc>> = client_sessions.clone(); + let mut tasks = JoinSet::new(); + tasks.spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(15)).await; + sessions.retain(|_, session| session.is_running()); + } + }); ClientManager { - accept_task: None, - clear_task: None, + tasks, + + listeners_cnt: Arc::new(AtomicU32::new(0)), - client_sessions: Arc::new(DashMap::new()), + client_sessions, storage: Storage::new(db), } } - pub async fn serve( + pub async fn add_listener( &mut self, mut listener: L, ) -> Result<(), anyhow::Error> { listener.listen().await?; - + self.listeners_cnt.fetch_add(1, Ordering::Relaxed); let sessions = self.client_sessions.clone(); let storage = self.storage.weak_ref(); - let task = tokio::spawn(async move { + let listeners_cnt = self.listeners_cnt.clone(); + self.tasks.spawn(async move { while let Ok(tunnel) = listener.accept().await { let info = tunnel.info().unwrap(); let client_url: url::Url = info.remote_addr.unwrap().into(); @@ -49,24 +63,14 @@ impl ClientManager { session.serve(tunnel).await; sessions.insert(client_url, Arc::new(session)); } + listeners_cnt.fetch_sub(1, Ordering::Relaxed); }); - self.accept_task = Some(ScopedTask::from(task)); - - let sessions = self.client_sessions.clone(); - let task = tokio::spawn(async move { - loop { - tokio::time::sleep(std::time::Duration::from_secs(15)).await; - sessions.retain(|_, session| session.is_running()); - } - }); - self.clear_task = Some(ScopedTask::from(task)); - Ok(()) } pub fn is_running(&self) -> bool { - self.accept_task.is_some() && self.clear_task.is_some() + self.listeners_cnt.load(Ordering::Relaxed) > 0 } pub async fn list_sessions(&self) -> Vec { @@ -132,7 +136,7 @@ mod tests { async fn test_client() { let listener = UdpTunnelListener::new("udp://0.0.0.0:54333".parse().unwrap()); let mut mgr = ClientManager::new(Db::memory_db().await); - mgr.serve(Box::new(listener)).await.unwrap(); + mgr.add_listener(Box::new(listener)).await.unwrap(); mgr.db() .inner() diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 6a2e6d11d..3ad85bb68 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -11,6 +11,7 @@ use easytier::{ config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader}, constants::EASYTIER_VERSION, error::Error, + network::{local_ipv4, local_ipv6}, }, tunnel::{ tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener, @@ -111,6 +112,31 @@ pub fn get_listener_by_url(l: &url::Url) -> Result, Erro }) } +async fn get_dual_stack_listener( + protocol: &str, + port: u16, +) -> Result< + ( + Option>, + Option>, + ), + Error, +> { + let is_protocol_support_dual_stack = + protocol.trim().to_lowercase() == "tcp" || protocol.trim().to_lowercase() == "udp"; + let v6_listener = if is_protocol_support_dual_stack && local_ipv6().await.is_ok() { + get_listener_by_url(&format!("{}://[::0]:{}", protocol, port).parse().unwrap()).ok() + } else { + None + }; + let v4_listener = if let Ok(_) = local_ipv4().await { + get_listener_by_url(&format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap()).ok() + } else { + None + }; + Ok((v6_listener, v4_listener)) +} + #[tokio::main] async fn main() { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); @@ -131,18 +157,21 @@ async fn main() { // let db = db::Db::new(":memory:").await.unwrap(); let db = db::Db::new(cli.db).await.unwrap(); - - let listener = get_listener_by_url( - &format!( - "{}://0.0.0.0:{}", - cli.config_server_protocol, cli.config_server_port - ) - .parse() - .unwrap(), - ) - .unwrap(); let mut mgr = client_manager::ClientManager::new(db.clone()); - mgr.serve(listener).await.unwrap(); + let (v6_listener, v4_listener) = + get_dual_stack_listener(&cli.config_server_protocol, cli.config_server_port) + .await + .unwrap(); + if v4_listener.is_none() && v6_listener.is_none() { + panic!("Listen to both IPv4 and IPv6 failed"); + } + if let Some(listener) = v6_listener { + mgr.add_listener(listener).await.unwrap(); + } + if let Some(listener) = v4_listener { + mgr.add_listener(listener).await.unwrap(); + } + let mgr = Arc::new(mgr); #[cfg(feature = "embed")] From 20a6025075079abc3e3e8aa4ef7ed121c99dba67 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sat, 7 Jun 2025 22:05:47 +0800 Subject: [PATCH 007/165] Added RPC portal whitelist function, allowing only local access by default to enhance security (#929) --- .../frontend-lib/src/components/Config.vue | 9 + easytier-web/frontend-lib/src/locales/cn.yaml | 1 + easytier-web/frontend-lib/src/locales/en.yaml | 1 + .../frontend-lib/src/types/network.ts | 3 + easytier/locales/app.yml | 3 + easytier/src/common/config.rs | 13 ++ easytier/src/easytier-core.rs | 11 + .../instance/dns_server/server_instance.rs | 7 +- easytier/src/instance/instance.rs | 189 +++++++++++++++++- easytier/src/launcher.rs | 14 ++ easytier/src/proto/rpc_impl/standalone.rs | 15 +- easytier/src/proto/web.proto | 2 + 12 files changed, 260 insertions(+), 8 deletions(-) diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 5b4d1f7e6..e38fa3ea0 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -304,6 +304,15 @@ const bool_flags: BoolFlag[] = [ +
+
+ + +
+
+
diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index e0e16718e..9c3d241a7 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -18,6 +18,7 @@ advanced_settings: 高级设置 basic_settings: 基础设置 listener_urls: 监听地址 rpc_port: RPC端口 +rpc_portal_whitelists: RPC白名单 config_network: 配置网络 running: 运行中 error_msg: 错误信息 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index 1d6e167c6..bf9629f96 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -18,6 +18,7 @@ advanced_settings: Advanced Settings basic_settings: Basic Settings listener_urls: Listener URLs rpc_port: RPC Port +rpc_portal_whitelists: RPC Whitelist config_network: Config Network running: Running error_msg: Error Message diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 6f1af40b7..421d61f19 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -65,6 +65,8 @@ export interface NetworkConfig { enable_magic_dns?: boolean enable_private_mode?: boolean + + rpc_portal_whitelists: string[] } export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { @@ -123,6 +125,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { mapped_listeners: [], enable_magic_dns: false, enable_private_mode: false, + rpc_portal_whitelists: [], } } diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index e68a0e944..e5377c3d9 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -37,6 +37,9 @@ core_clap: rpc_portal: en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first" zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888" + rpc_portal_whitelist: + en: "rpc portal whitelist, only allow these addresses to access rpc portal, e.g.: 127.0.0.1,127.0.0.0/8,::1/128" + zh-CN: "RPC门户白名单,仅允许这些地址访问RPC门户,例如:127.0.0.1/32,127.0.0.0/8,::1/128" listeners: en: |+ listeners to accept connections, allow format: diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index cdb8be68f..9798b3239 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -5,6 +5,7 @@ use std::{ }; use anyhow::Context; +use cidr::IpCidr; use serde::{Deserialize, Serialize}; use crate::{ @@ -87,6 +88,9 @@ pub trait ConfigLoader: Send + Sync { fn get_rpc_portal(&self) -> Option; fn set_rpc_portal(&self, addr: SocketAddr); + fn get_rpc_portal_whitelist(&self) -> Option>; + fn set_rpc_portal_whitelist(&self, whitelist: Option>); + fn get_vpn_portal_config(&self) -> Option; fn set_vpn_portal_config(&self, config: VpnPortalConfig); @@ -243,6 +247,7 @@ struct Config { console_logger: Option, rpc_portal: Option, + rpc_portal_whitelist: Option>, vpn_portal_config: Option, @@ -544,6 +549,14 @@ impl ConfigLoader for TomlConfigLoader { self.config.lock().unwrap().rpc_portal = Some(addr); } + fn get_rpc_portal_whitelist(&self) -> Option> { + self.config.lock().unwrap().rpc_portal_whitelist.clone() + } + + fn set_rpc_portal_whitelist(&self, whitelist: Option>) { + self.config.lock().unwrap().rpc_portal_whitelist = whitelist; + } + fn get_vpn_portal_config(&self) -> Option { self.config.lock().unwrap().vpn_portal_config.clone() } diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index fa058e457..e209f37a1 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -11,6 +11,7 @@ use std::{ }; use anyhow::Context; +use cidr::IpCidr; use clap::Parser; use easytier::{ @@ -176,6 +177,14 @@ struct Cli { )] rpc_portal: Option, + #[arg( + long, + env = "ET_RPC_PORTAL_WHITELIST", + value_delimiter = ',', + help = t!("core_clap.rpc_portal_whitelist").to_string(), + )] + rpc_portal_whitelist: Option>, + #[arg( short, long, @@ -616,6 +625,8 @@ impl TryFrom<&Cli> for TomlConfigLoader { }; cfg.set_rpc_portal(rpc_portal); + cfg.set_rpc_portal_whitelist(cli.rpc_portal_whitelist.clone()); + if let Some(external_nodes) = cli.external_node.as_ref() { let mut old_peers = cfg.get_peers(); old_peers.push(PeerConfig { diff --git a/easytier/src/instance/dns_server/server_instance.rs b/easytier/src/instance/dns_server/server_instance.rs index f76954946..1e1f8f078 100644 --- a/easytier/src/instance/dns_server/server_instance.rs +++ b/easytier/src/instance/dns_server/server_instance.rs @@ -298,12 +298,13 @@ impl NicPacketFilter for MagicDnsServerInstanceData { #[async_trait::async_trait] impl RpcServerHook for MagicDnsServerInstanceData { - async fn on_new_client(&self, tunnel_info: Option) { - println!("New client connected: {:?}", tunnel_info); + async fn on_new_client(&self, tunnel_info: Option)-> Result, anyhow::Error> { + tracing::info!(?tunnel_info, "New client connected"); + Ok(tunnel_info) } async fn on_client_disconnected(&self, tunnel_info: Option) { - println!("Client disconnected: {:?}", tunnel_info); + tracing::info!(?tunnel_info, "Client disconnected"); let Some(tunnel_info) = tunnel_info else { return; }; diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 0b3d45574..e241f8f7a 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -1,11 +1,11 @@ use std::any::Any; use std::collections::HashSet; -use std::net::Ipv4Addr; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use anyhow::Context; -use cidr::Ipv4Inet; +use cidr::{IpCidr, Ipv4Inet}; use tokio::task::JoinHandle; use tokio::{sync::Mutex, task::JoinSet}; @@ -29,8 +29,9 @@ use crate::peers::rpc_service::PeerManagerRpcService; use crate::peers::{create_packet_recv_chan, recv_packet_from_chan, PacketRecvChanReceiver}; use crate::proto::cli::VpnPortalRpc; use crate::proto::cli::{GetVpnPortalInfoRequest, GetVpnPortalInfoResponse, VpnPortalInfo}; +use crate::proto::common::TunnelInfo; use crate::proto::peer_rpc::PeerCenterRpcServer; -use crate::proto::rpc_impl::standalone::StandAloneServer; +use crate::proto::rpc_impl::standalone::{RpcServerHook, StandAloneServer}; use crate::proto::rpc_types; use crate::proto::rpc_types::controller::BaseController; use crate::tunnel::tcp::TcpTunnelListener; @@ -155,6 +156,58 @@ impl NicCtxContainer { type ArcNicCtx = Arc>>; +pub struct InstanceRpcServerHook { + rpc_portal_whitelist: Vec, +} + +impl InstanceRpcServerHook { + pub fn new(rpc_portal_whitelist: Option>) -> Self { + let rpc_portal_whitelist = rpc_portal_whitelist + .unwrap_or_else(|| vec!["127.0.0.0/8".parse().unwrap(), "::1/128".parse().unwrap()]); + InstanceRpcServerHook { + rpc_portal_whitelist, + } + } +} + +#[async_trait::async_trait] +impl RpcServerHook for InstanceRpcServerHook { + async fn on_new_client( + &self, + tunnel_info: Option, + ) -> Result, anyhow::Error> { + let tunnel_info = tunnel_info.ok_or_else(|| anyhow::anyhow!("tunnel info is None"))?; + + let remote_url = tunnel_info + .remote_addr + .clone() + .ok_or_else(|| anyhow::anyhow!("remote_addr is None"))?; + + let url_str = &remote_url.url; + let url = url::Url::parse(url_str) + .map_err(|e| anyhow::anyhow!("Failed to parse remote URL '{}': {}", url_str, e))?; + + let host = url + .host_str() + .ok_or_else(|| anyhow::anyhow!("No host found in remote URL '{}'", url_str))?; + + let ip_addr: IpAddr = host + .parse() + .map_err(|e| anyhow::anyhow!("Failed to parse IP address '{}': {}", host, e))?; + + for cidr in &self.rpc_portal_whitelist { + if cidr.contains(&ip_addr) { + return Ok(Some(tunnel_info)); + } + } + return Err(anyhow::anyhow!( + "Rpc portal client IP {} not in whitelist: {:?}, ignoring client.", + ip_addr, + self.rpc_portal_whitelist + )); + } +} + pub struct Instance { inst_name: String, @@ -674,6 +727,10 @@ impl Instance { ); } + s.set_hook(Arc::new(InstanceRpcServerHook::new( + self.global_ctx.config.get_rpc_portal_whitelist(), + ))); + let _g = self.global_ctx.net_ns.guard(); Ok(s.serve().await.with_context(|| "rpc server start failed")?) } @@ -726,3 +783,129 @@ impl Instance { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::{instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook}; + + + #[tokio::test] + async fn test_rpc_portal_whitelist() { + use cidr::IpCidr; + + struct TestCase { + remote_url: String, + whitelist: Option>, + expected_result: bool, + } + + let test_cases:Vec = vec![ + // Test default whitelist (127.0.0.0/8, ::1/128) + TestCase { + remote_url: "tcp://127.0.0.1:15888".to_string(), + whitelist: None, + expected_result: true, + }, + TestCase { + remote_url: "tcp://127.1.2.3:15888".to_string(), + whitelist: None, + expected_result: true, + }, + TestCase { + remote_url: "tcp://192.168.1.1:15888".to_string(), + whitelist: None, + expected_result: false, + }, + + // Test custom whitelist + TestCase { + remote_url: "tcp://192.168.1.10:15888".to_string(), + whitelist: Some(vec![ + "192.168.1.0/24".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + ]), + expected_result: true, + }, + TestCase { + remote_url: "tcp://10.1.2.3:15888".to_string(), + whitelist: Some(vec![ + "192.168.1.0/24".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + ]), + expected_result: true, + }, + TestCase { + remote_url: "tcp://172.16.0.1:15888".to_string(), + whitelist: Some(vec![ + "192.168.1.0/24".parse().unwrap(), + "10.0.0.0/8".parse().unwrap(), + ]), + expected_result: false, + }, + + // Test empty whitelist (should reject all connections) + TestCase { + remote_url: "tcp://127.0.0.1:15888".to_string(), + whitelist: Some(vec![]), + expected_result: false, + }, + + // Test broad whitelist (0.0.0.0/0 and ::/0 accept all IP addresses) + TestCase { + remote_url: "tcp://8.8.8.8:15888".to_string(), + whitelist: Some(vec![ + "0.0.0.0/0".parse().unwrap(), + ]), + expected_result: true, + }, + + // Test edge case: specific IP whitelist + TestCase { + remote_url: "tcp://192.168.1.5:15888".to_string(), + whitelist: Some(vec![ + "192.168.1.5/32".parse().unwrap(), + ]), + expected_result: true, + }, + TestCase { + remote_url: "tcp://192.168.1.6:15888".to_string(), + whitelist: Some(vec![ + "192.168.1.5/32".parse().unwrap(), + ]), + expected_result: false, + }, + + // Test invalid URL (this case will fail during URL parsing) + TestCase { + remote_url: "invalid-url".to_string(), + whitelist: None, + expected_result: false, + }, + + // Test URL without IP address (this case will fail during IP parsing) + TestCase { + remote_url: "tcp://localhost:15888".to_string(), + whitelist: None, + expected_result: false, + }, + ]; + + for case in test_cases { + let hook = InstanceRpcServerHook::new(case.whitelist.clone()); + let tunnel_info = Some(crate::proto::common::TunnelInfo { + remote_addr: Some(crate::proto::common::Url { + url: case.remote_url.clone(), + }), + ..Default::default() + }); + + let result = hook.on_new_client(tunnel_info).await; + if case.expected_result { + assert!(result.is_ok(), "Expected success for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result); + } else { + assert!(result.is_err(), "Expected failure for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result); + } + } + + } +} \ No newline at end of file diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 1465eeb46..bd5ba7262 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -527,6 +527,20 @@ impl NetworkConfig { .with_context(|| format!("failed to parse rpc portal port: {:?}", self.rpc_port))?, ); + if self.rpc_portal_whitelists.is_empty() { + cfg.set_rpc_portal_whitelist(None); + } else { + cfg.set_rpc_portal_whitelist(Some( + self.rpc_portal_whitelists + .iter() + .map(|s| { + s.parse() + .with_context(|| format!("failed to parse rpc portal whitelist: {}", s)) + }) + .collect::, _>>()?, + )); + } + if self.enable_vpn_portal.unwrap_or_default() { let cidr = format!( "{}/{}", diff --git a/easytier/src/proto/rpc_impl/standalone.rs b/easytier/src/proto/rpc_impl/standalone.rs index 54ee94f49..32200abe8 100644 --- a/easytier/src/proto/rpc_impl/standalone.rs +++ b/easytier/src/proto/rpc_impl/standalone.rs @@ -21,7 +21,12 @@ use super::service_registry::ServiceRegistry; #[async_trait::async_trait] #[auto_impl::auto_impl(Arc, Box)] pub trait RpcServerHook: Send + Sync { - async fn on_new_client(&self, _tunnel_info: Option) {} + async fn on_new_client( + &self, + tunnel_info: Option, + ) -> Result, anyhow::Error> { + Ok(tunnel_info) + } async fn on_client_disconnected(&self, _tunnel_info: Option) {} } @@ -72,7 +77,13 @@ impl StandAloneServer { let inflight_server = inflight.clone(); let hook = hook.clone(); - hook.on_new_client(tunnel_info.clone()).await; + let tunnel_info = match hook.on_new_client(tunnel_info).await { + Ok(info) => info, + Err(e) => { + tracing::warn!(?e, "standalone hook.on_new_client failed"); + continue; + } + }; inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed); tasks.lock().unwrap().spawn(async move { diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index c0b4f37f1..c909ea503 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -66,6 +66,8 @@ message NetworkConfig { optional bool enable_magic_dns = 42; optional bool enable_private_mode = 43; + + repeated string rpc_portal_whitelists = 44; } message MyNodeInfo { From ec56c0bc4509e492a51a66907d2c18df237f63c6 Mon Sep 17 00:00:00 2001 From: Kiva Date: Sat, 7 Jun 2025 22:27:57 +0800 Subject: [PATCH 008/165] feat: allow using `--proxy-forward-by-system` together with `--enable-exit-node` (#957) --- easytier/src/instance/instance.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index e241f8f7a..c242cb8b7 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -71,7 +71,6 @@ impl IpProxy { async fn start(&self) -> Result<(), Error> { if (self.global_ctx.get_proxy_cidrs().is_empty() - || self.global_ctx.proxy_forward_by_system() || self.started.load(Ordering::Relaxed)) && !self.global_ctx.enable_exit_node() && !self.global_ctx.no_tun() @@ -79,6 +78,13 @@ impl IpProxy { return Ok(()); } + // Actually, if this node is enabled as an exit node, + // we still can use the system stack to forward packets. + if self.global_ctx.proxy_forward_by_system() + && !self.global_ctx.no_tun() { + return Ok(()); + } + self.started.store(true, Ordering::Relaxed); self.tcp_proxy.start(true).await?; if let Err(e) = self.icmp_proxy.start().await { From f39fbb2ce254c0209a63f5b204665d1d2cadc99c Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sun, 8 Jun 2025 11:28:59 +0800 Subject: [PATCH 009/165] ipv4-peerid table should use peer with least hop (#958) sometimes route table may not be updated in time, so some dead nodes are still showing in the peer list. when generating ipv4-peer table, we should avoid these dead devices overrides the entry of healthy nodes. --- easytier/src/instance/virtual_nic.rs | 11 +++++----- easytier/src/peers/peer_ospf_route.rs | 29 +++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index 029a6b681..b11342d40 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeSet, io, net::Ipv4Addr, pin::Pin, @@ -569,26 +570,26 @@ impl NicCtx { let ifname = nic.ifname().to_owned(); self.tasks.spawn(async move { - let mut cur_proxy_cidrs = vec![]; + let mut cur_proxy_cidrs = BTreeSet::new(); loop { - let mut proxy_cidrs = vec![]; + let mut proxy_cidrs = BTreeSet::new(); let routes = peer_mgr.list_routes().await; for r in routes { for cidr in r.proxy_cidrs { let Ok(cidr) = cidr.parse::() else { continue; }; - proxy_cidrs.push(cidr); + proxy_cidrs.insert(cidr); } } // add vpn portal cidr to proxy_cidrs if let Some(vpn_cfg) = global_ctx.config.get_vpn_portal_config() { - proxy_cidrs.push(vpn_cfg.client_cidr); + proxy_cidrs.insert(vpn_cfg.client_cidr); } if let Some(routes) = global_ctx.config.get_routes() { // if has manual routes, just override entire proxy_cidrs - proxy_cidrs = routes; + proxy_cidrs = routes.into_iter().collect(); } // if route is in cur_proxy_cidrs but not in proxy_cidrs, delete it. diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 704ad0382..6768b26e2 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -854,11 +854,36 @@ impl RouteTable { self.peer_infos.insert(*peer_id, info.clone()); + let is_new_peer_better = |old_peer_id: PeerId| -> bool { + let old_next_hop = self.get_next_hop(old_peer_id); + let new_next_hop = item.value(); + old_next_hop.is_none() + || new_next_hop.path_latency < old_next_hop.unwrap().path_latency + }; + if let Some(ipv4_addr) = info.ipv4_addr { - self.ipv4_peer_id_map.insert(ipv4_addr.into(), *peer_id); + self.ipv4_peer_id_map + .entry(ipv4_addr.into()) + .and_modify(|v| { + if *v != *peer_id && is_new_peer_better(*v) { + self.ipv4_peer_id_map.insert(ipv4_addr.into(), *peer_id); + } + }) + .or_insert(*peer_id); } for cidr in info.proxy_cidrs.iter() { + self.cidr_peer_id_map + .entry(cidr.parse().unwrap()) + .and_modify(|v| { + if *v != *peer_id && is_new_peer_better(*v) { + // if the next hop is not set or the new next hop is better, update it. + self.cidr_peer_id_map + .insert(cidr.parse().unwrap(), *peer_id); + } + }) + .or_insert(*peer_id); + self.cidr_peer_id_map .insert(cidr.parse().unwrap(), *peer_id); } @@ -1363,7 +1388,7 @@ impl PeerRouteServiceImpl { .dst_saved_conn_bitmap_version .get(&peer_id) .map(|item| item.get()); - if Some(*local_version) != peer_version { + if peer_version.is_none() || peer_version.unwrap() < *local_version { need_update = true; break; } From ecebbecd3b0c162a4901f9e9918c5cecd8192183 Mon Sep 17 00:00:00 2001 From: BlackLuny <602814112@qq.com> Date: Mon, 9 Jun 2025 19:35:29 +0800 Subject: [PATCH 010/165] add check for rpc packet fix #963 (#969) --- easytier/src/proto/rpc_impl/bidirect.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/easytier/src/proto/rpc_impl/bidirect.rs b/easytier/src/proto/rpc_impl/bidirect.rs index a673949f1..231c3041b 100644 --- a/easytier/src/proto/rpc_impl/bidirect.rs +++ b/easytier/src/proto/rpc_impl/bidirect.rs @@ -131,11 +131,14 @@ impl BidirectRpcManager { } }; - if o.peer_manager_header().unwrap().packet_type == PacketType::RpcReq as u8 { + let Some(peer_manager_header) = o.peer_manager_header() else { + tracing::error!("peer manager header not found"); + continue; + }; + if peer_manager_header.packet_type == PacketType::RpcReq as u8 { server_tx.send(o).await.unwrap(); continue; - } else if o.peer_manager_header().unwrap().packet_type == PacketType::RpcResp as u8 - { + } else if peer_manager_header.packet_type == PacketType::RpcResp as u8 { client_tx.send(o).await.unwrap(); continue; } From 870353c499a3c9fe9eb9b08c4add9f6f1a380840 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Wed, 11 Jun 2025 09:44:03 +0800 Subject: [PATCH 011/165] fix ospf route (#970) - **fix deadlock in ospf route introducd by #958 ** - **use random peer id for foreign network entry, because ospf route algo need peer id change after peer info version reset. this may interfere route propagation and cause node residual** - **allow multiple nodes broadcast same network ranges for subnet proxy** - **bump version to v2.3.2** --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- Cargo.lock | 6 +- easytier-contrib/easytier-magisk/module.prop | 2 +- easytier-gui/package.json | 2 +- easytier-gui/src-tauri/Cargo.toml | 2 +- easytier-gui/src-tauri/tauri.conf.json | 2 +- easytier-web/Cargo.toml | 2 +- easytier/Cargo.toml | 2 +- easytier/src/peers/foreign_network_manager.rs | 152 ++++++++++++++---- easytier/src/peers/peer_conn.rs | 36 +++++ easytier/src/peers/peer_manager.rs | 89 ++++++++-- easytier/src/peers/peer_map.rs | 16 ++ easytier/src/peers/peer_ospf_route.rs | 47 ++++-- easytier/src/peers/route_trait.rs | 10 ++ easytier/src/proto/cli.proto | 1 + easytier/src/proto/peer_rpc.proto | 1 + easytier/src/tests/three_node.rs | 14 +- 18 files changed, 316 insertions(+), 72 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e7691164a..124b11d50 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,7 +11,7 @@ on: image_tag: description: 'Tag for this image build' type: string - default: 'v2.3.1' + default: 'v2.3.2' required: true mark_latest: description: 'Mark this image as latest' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af3cbe440..09791d71d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ on: version: description: 'Version for this release' type: string - default: 'v2.3.1' + default: 'v2.3.2' required: true make_latest: description: 'Mark this release as latest' diff --git a/Cargo.lock b/Cargo.lock index bcdf1f8a7..fe0b05814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1942,7 +1942,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "easytier" -version = "2.3.1" +version = "2.3.2" dependencies = [ "aes-gcm", "anyhow", @@ -2070,7 +2070,7 @@ dependencies = [ [[package]] name = "easytier-gui" -version = "2.3.1" +version = "2.3.2" dependencies = [ "anyhow", "chrono", @@ -2116,7 +2116,7 @@ dependencies = [ [[package]] name = "easytier-web" -version = "2.3.1" +version = "2.3.2" dependencies = [ "anyhow", "async-trait", diff --git a/easytier-contrib/easytier-magisk/module.prop b/easytier-contrib/easytier-magisk/module.prop index 8df373b92..6316f4e6c 100644 --- a/easytier-contrib/easytier-magisk/module.prop +++ b/easytier-contrib/easytier-magisk/module.prop @@ -1,6 +1,6 @@ id=easytier_magisk name=EasyTier_Magisk -version=v2.3.1 +version=v2.3.2 versionCode=1 author=EasyTier description=easytier magisk module @EasyTier(https://github.com/EasyTier/EasyTier) diff --git a/easytier-gui/package.json b/easytier-gui/package.json index 5dc10bbe5..efac0317a 100644 --- a/easytier-gui/package.json +++ b/easytier-gui/package.json @@ -1,7 +1,7 @@ { "name": "easytier-gui", "type": "module", - "version": "2.3.1", + "version": "2.3.2", "private": true, "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4", "scripts": { diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index ef5c419a9..e23ae8f93 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easytier-gui" -version = "2.3.1" +version = "2.3.2" description = "EasyTier GUI" authors = ["you"] edition = "2021" diff --git a/easytier-gui/src-tauri/tauri.conf.json b/easytier-gui/src-tauri/tauri.conf.json index f2a947ab4..e18fd16ab 100644 --- a/easytier-gui/src-tauri/tauri.conf.json +++ b/easytier-gui/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ "createUpdaterArtifacts": false }, "productName": "easytier-gui", - "version": "2.3.1", + "version": "2.3.2", "identifier": "com.kkrainbow.easytier", "plugins": {}, "app": { diff --git a/easytier-web/Cargo.toml b/easytier-web/Cargo.toml index 90de5c8f9..2356a6ebf 100644 --- a/easytier-web/Cargo.toml +++ b/easytier-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easytier-web" -version = "2.3.1" +version = "2.3.2" edition = "2021" description = "Config server for easytier. easytier-core gets config from this and web frontend use it as restful api server." diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 190df0534..522732f85 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -3,7 +3,7 @@ name = "easytier" description = "A full meshed p2p VPN, connecting all your devices in one network with one command." homepage = "https://github.com/EasyTier/EasyTier" repository = "https://github.com/EasyTier/EasyTier" -version = "2.3.1" +version = "2.3.2" edition = "2021" authors = ["kkrainbow"] keywords = ["vpn", "p2p", "network", "easytier"] diff --git a/easytier/src/peers/foreign_network_manager.rs b/easytier/src/peers/foreign_network_manager.rs index df76d4f5b..749430674 100644 --- a/easytier/src/peers/foreign_network_manager.rs +++ b/easytier/src/peers/foreign_network_manager.rs @@ -70,13 +70,16 @@ struct ForeignNetworkEntry { packet_recv: Mutex>, tasks: Mutex>, + + pub lock: Mutex<()>, } impl ForeignNetworkEntry { fn new( network: NetworkIdentity, - global_ctx: ArcGlobalCtx, + // NOTICE: ospf route need my_peer_id be changed after restart. my_peer_id: PeerId, + global_ctx: ArcGlobalCtx, relay_data: bool, pm_packet_sender: PacketRecvChan, ) -> Self { @@ -114,6 +117,8 @@ impl ForeignNetworkEntry { packet_recv: Mutex::new(Some(packet_recv)), tasks: Mutex::new(JoinSet::new()), + + lock: Mutex::new(()), } } @@ -202,11 +207,7 @@ impl ForeignNetworkEntry { (peer_rpc, rpc_transport_sender) } - async fn prepare_route( - &self, - my_peer_id: PeerId, - accessor: Box, - ) { + async fn prepare_route(&self, accessor: Box) { struct Interface { my_peer_id: PeerId, peer_map: Weak, @@ -238,10 +239,14 @@ impl ForeignNetworkEntry { } } - let route = PeerRoute::new(my_peer_id, self.global_ctx.clone(), self.peer_rpc.clone()); + let route = PeerRoute::new( + self.my_peer_id, + self.global_ctx.clone(), + self.peer_rpc.clone(), + ); route .open(Box::new(Interface { - my_peer_id, + my_peer_id: self.my_peer_id, network_identity: self.network.clone(), peer_map: Arc::downgrade(&self.peer_map), accessor, @@ -317,8 +322,8 @@ impl ForeignNetworkEntry { }); } - async fn prepare(&self, my_peer_id: PeerId, accessor: Box) { - self.prepare_route(my_peer_id, accessor).await; + async fn prepare(&self, accessor: Box) { + self.prepare_route(accessor).await; self.start_packet_recv().await; self.peer_rpc.run(); } @@ -400,8 +405,8 @@ impl ForeignNetworkManagerData { new_added = true; Arc::new(ForeignNetworkEntry::new( network_identity.clone(), - global_ctx.clone(), my_peer_id, + global_ctx.clone(), relay_data, pm_packet_sender.clone(), )) @@ -417,9 +422,7 @@ impl ForeignNetworkManagerData { drop(l); if new_added { - entry - .prepare(my_peer_id, Box::new(self.accessor.clone())) - .await; + entry.prepare(Box::new(self.accessor.clone())).await; } (entry, new_added) @@ -467,6 +470,13 @@ impl ForeignNetworkManager { } } + pub fn get_network_peer_id(&self, network_name: &str) -> Option { + self.data + .network_peer_maps + .get(network_name) + .and_then(|v| Some(v.my_peer_id)) + } + pub async fn add_peer_conn(&self, peer_conn: PeerConn) -> Result<(), Error> { tracing::info!(peer_conn = ?peer_conn.get_conn_info(), network = ?peer_conn.get_network_identity(), "add new peer conn in foreign network manager"); @@ -483,7 +493,7 @@ impl ForeignNetworkManager { .data .get_or_insert_entry( &peer_conn.get_network_identity(), - self.my_peer_id, + peer_conn.get_my_peer_id(), peer_conn.get_peer_id(), !ret.is_err(), &self.global_ctx, @@ -491,17 +501,30 @@ impl ForeignNetworkManager { ) .await; - if entry.network != peer_conn.get_network_identity() { + let _g = entry.lock.lock().await; + + if entry.network != peer_conn.get_network_identity() + || entry.my_peer_id != peer_conn.get_my_peer_id() + { if new_added { self.data .remove_network(&entry.network.network_name.clone()); } - return Err(anyhow::anyhow!( - "network secret not match. exp: {:?} real: {:?}", - entry.network, - peer_conn.get_network_identity() - ) - .into()); + let err = if entry.my_peer_id != peer_conn.get_my_peer_id() { + anyhow::anyhow!( + "my peer id not match. exp: {:?} real: {:?}, need retry connect", + entry.my_peer_id, + peer_conn.get_my_peer_id() + ) + } else { + anyhow::anyhow!( + "network secret not match. exp: {:?} real: {:?}", + entry.network, + peer_conn.get_network_identity() + ) + }; + tracing::error!(?err, "foreign network entry not match, disconnect peer"); + return Err(err.into()); } if new_added { @@ -567,7 +590,8 @@ impl ForeignNetworkManager { .network_secret_digest .unwrap_or_default() .to_vec(), - ..Default::default() + my_peer_id_for_this_network: item.my_peer_id, + peers: Default::default(), }; for peer in item.peer_map.list_peers().await { let mut peer_info = PeerInfo::default(); @@ -614,8 +638,6 @@ impl Drop for ForeignNetworkManager { #[cfg(test)] mod tests { - use std::time::Duration; - use crate::{ common::global_ctx::tests::get_mock_global_ctx_with_network, connector::udp_hole_punch::tests::{ @@ -629,6 +651,7 @@ mod tests { set_global_var, tunnel::common::tests::wait_for_condition, }; + use std::time::Duration; use super::*; @@ -769,7 +792,10 @@ mod tests { .unwrap(); assert_eq!( - vec![pm_center.my_peer_id()], + vec![pm_center + .get_foreign_network_manager() + .get_network_peer_id("net1") + .unwrap()], pma_net1 .get_foreign_network_client() .get_peer_map() @@ -777,7 +803,10 @@ mod tests { .await ); assert_eq!( - vec![pm_center.my_peer_id()], + vec![pm_center + .get_foreign_network_manager() + .get_network_peer_id("net1") + .unwrap()], pmb_net1 .get_foreign_network_client() .get_peer_map() @@ -894,6 +923,75 @@ mod tests { .await; } + #[tokio::test] + async fn test_foreign_network_manager_cluster_simple() { + set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(pm_center1.clone(), pm_center2.clone()).await; + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pma_net1.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center2.clone()).await; + + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + + let pma_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + let pmb_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + connect_peer_manager(pma_net2.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net2.clone(), pm_center2.clone()).await; + + wait_route_appear(pma_net2.clone(), pmb_net2.clone()) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_foreign_network_manager_cluster_multiple_hops() { + set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + + let pm_center1 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center2 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center3 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + let pm_center4 = create_mock_peer_manager_with_mock_stun(NatType::Unknown).await; + + connect_peer_manager(pm_center1.clone(), pm_center2.clone()).await; + connect_peer_manager(pm_center2.clone(), pm_center3.clone()).await; + connect_peer_manager(pm_center3.clone(), pm_center4.clone()).await; + + let pma_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + let pmb_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pma_net1.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net1.clone(), pm_center3.clone()).await; + wait_route_appear(pma_net1.clone(), pmb_net1.clone()) + .await + .unwrap(); + let pmc_net1 = create_mock_peer_manager_for_foreign_network("net1").await; + connect_peer_manager(pmc_net1.clone(), pm_center4.clone()).await; + wait_route_appear(pma_net1.clone(), pmc_net1.clone()) + .await + .unwrap(); + + let pma_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + let pmb_net2 = create_mock_peer_manager_for_foreign_network("net2").await; + connect_peer_manager(pma_net2.clone(), pm_center1.clone()).await; + connect_peer_manager(pmb_net2.clone(), pm_center4.clone()).await; + wait_route_appear(pma_net2.clone(), pmb_net2.clone()) + .await + .unwrap(); + drop(pmb_net2); + wait_for_condition( + || async { pma_net2.list_routes().await.len() == 1 }, + Duration::from_secs(5), + ) + .await; + } + #[tokio::test] async fn test_foreign_network_manager_cluster() { set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); diff --git a/easytier/src/peers/peer_conn.rs b/easytier/src/peers/peer_conn.rs index b95798069..ca258c542 100644 --- a/easytier/src/peers/peer_conn.rs +++ b/easytier/src/peers/peer_conn.rs @@ -266,6 +266,31 @@ impl PeerConn { Ok(()) } + #[tracing::instrument(skip(handshake_recved))] + pub async fn do_handshake_as_server_ext( + &mut self, + mut handshake_recved: Fn, + ) -> Result<(), Error> + where + Fn: FnMut(&mut Self, &HandshakeRequest) -> Result<(), Error> + Send, + { + let rsp = self.wait_handshake_loop().await?; + + handshake_recved(self, &rsp)?; + + tracing::info!("handshake request: {:?}", rsp); + self.info = Some(rsp); + self.is_client = Some(false); + + self.send_handshake().await?; + + if self.get_peer_id() == self.my_peer_id { + Err(Error::WaitRespError("peer id conflict".to_owned())) + } else { + Ok(()) + } + } + #[tracing::instrument] pub async fn do_handshake_as_server(&mut self) -> Result<(), Error> { let rsp = self.wait_handshake_loop().await?; @@ -435,6 +460,17 @@ impl PeerConn { is_closed: self.close_event_notifier.is_closed(), } } + + pub fn set_peer_id(&mut self, peer_id: PeerId) { + if self.info.is_some() { + panic!("set_peer_id should only be called before handshake"); + } + self.my_peer_id = peer_id; + } + + pub fn get_my_peer_id(&self) -> PeerId { + self.my_peer_id + } } impl Drop for PeerConn { diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index 3a4ca3d6f..ccfffde02 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -144,6 +144,8 @@ pub struct PeerManager { // conns that are directly connected (which are not hole punched) directly_connected_conn_map: Arc>>, + + reserved_my_peer_id_map: DashMap, } impl Debug for PeerManager { @@ -272,6 +274,8 @@ impl PeerManager { exit_nodes, directly_connected_conn_map: Arc::new(DashMap::new()), + + reserved_my_peer_id_map: DashMap::new(), } } @@ -413,7 +417,7 @@ impl PeerManager { self.add_direct_tunnel(t).await } - #[tracing::instrument] + #[tracing::instrument(ret)] pub async fn add_tunnel_as_server( &self, tunnel: Box, @@ -421,18 +425,43 @@ impl PeerManager { ) -> Result<(), Error> { tracing::info!("add tunnel as server start"); let mut peer = PeerConn::new(self.my_peer_id, self.global_ctx.clone(), tunnel); - peer.do_handshake_as_server().await?; - if self.global_ctx.config.get_flags().private_mode - && peer.get_network_identity().network_name - != self.global_ctx.get_network_identity().network_name - { - return Err(Error::SecretKeyError( - "private mode is turned on, network identity not match".to_string(), - )); - } - if peer.get_network_identity().network_name - == self.global_ctx.get_network_identity().network_name - { + peer.do_handshake_as_server_ext(|peer, msg| { + if msg.network_name + == self.global_ctx.get_network_identity().network_name + { + return Ok(()); + } + + if self.global_ctx.config.get_flags().private_mode { + return Err(Error::SecretKeyError( + "private mode is turned on, network identity not match".to_string(), + )); + } + + let mut peer_id = self + .foreign_network_manager + .get_network_peer_id(&msg.network_name); + if peer_id.is_none() { + peer_id = Some(*self.reserved_my_peer_id_map.entry(msg.network_name.clone()).or_insert_with(|| { + rand::random::() + }).value()); + } + peer.set_peer_id(peer_id.clone().unwrap()); + + tracing::info!( + ?peer_id, + ?msg.network_name, + "handshake as server with foreign network, new peer id: {}, peer id in foreign manager: {:?}", + peer.get_my_peer_id(), peer_id + ); + + Ok(()) + }) + .await?; + + let peer_network_name = peer.get_network_identity().network_name.clone(); + + if peer_network_name == self.global_ctx.get_network_identity().network_name { let (peer_id, conn_id) = (peer.get_peer_id(), peer.get_conn_id()); self.add_new_peer_conn(peer).await?; if is_directly_connected { @@ -441,12 +470,15 @@ impl PeerManager { } else { self.foreign_network_manager.add_peer_conn(peer).await?; } + + self.reserved_my_peer_id_map.remove(&peer_network_name); + tracing::info!("add tunnel as server done"); Ok(()) } async fn try_handle_foreign_network_packet( - packet: ZCPacket, + mut packet: ZCPacket, my_peer_id: PeerId, peer_map: &PeerMap, foreign_network_mgr: &ForeignNetworkManager, @@ -463,6 +495,10 @@ impl PeerManager { let foreign_network_name = foreign_hdr.get_network_name(packet.payload()); let foreign_peer_id = foreign_hdr.get_dst_peer_id(); + let foreign_network_my_peer_id = + foreign_network_mgr.get_network_peer_id(&foreign_network_name); + + // NOTICE: the to peer id is modified by the src from foreign network my peer id to the origin my peer id if to_peer_id == my_peer_id { // packet sent from other peer to me, extract the inner packet and forward it if let Err(e) = foreign_network_mgr @@ -481,7 +517,27 @@ impl PeerManager { ); } Ok(()) - } else if from_peer_id == my_peer_id { + } else if Some(from_peer_id) == foreign_network_my_peer_id { + // to_peer_id is my peer id for the foreign network, need to convert to the origin my_peer_id of dst + let Some(to_peer_id) = peer_map + .get_origin_my_peer_id(&foreign_network_name, to_peer_id) + .await + else { + tracing::debug!( + ?foreign_network_name, + ?to_peer_id, + "cannot find origin my peer id for foreign network." + ); + return Err(packet); + }; + + // modify the to_peer id from foreign network my peer id to the origin my peer id + packet + .mut_peer_manager_header() + .unwrap() + .to_peer_id + .set(to_peer_id); + // packet is generated from foreign network mgr and should be forward to other peer if let Err(e) = peer_map .send_msg(packet, to_peer_id, NextHopPolicy::LeastHop) @@ -496,7 +552,7 @@ impl PeerManager { Ok(()) } else { - // target is not me, forward it + // target is not me, forward it. try get origin peer id Err(packet) } } @@ -717,6 +773,7 @@ impl PeerManager { last_update: Some(last_update.into()), version: 0, network_secret_digest: info.network_secret_digest.clone(), + my_peer_id_for_this_network: info.my_peer_id_for_this_network, }, ); } diff --git a/easytier/src/peers/peer_map.rs b/easytier/src/peers/peer_map.rs index 47e8e6ca2..f8cef8f10 100644 --- a/easytier/src/peers/peer_map.rs +++ b/easytier/src/peers/peer_map.rs @@ -204,6 +204,22 @@ impl PeerMap { None } + pub async fn get_origin_my_peer_id( + &self, + network_name: &str, + foreign_my_peer_id: PeerId, + ) -> Option { + for route in self.routes.read().await.iter() { + let origin_peer_id = route + .get_origin_my_peer_id(network_name, foreign_my_peer_id) + .await; + if origin_peer_id.is_some() { + return origin_peer_id; + } + } + None + } + pub fn is_empty(&self) -> bool { self.peer_map.is_empty() } diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 6768b26e2..8a641a909 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -857,8 +857,7 @@ impl RouteTable { let is_new_peer_better = |old_peer_id: PeerId| -> bool { let old_next_hop = self.get_next_hop(old_peer_id); let new_next_hop = item.value(); - old_next_hop.is_none() - || new_next_hop.path_latency < old_next_hop.unwrap().path_latency + old_next_hop.is_none() || new_next_hop.path_len < old_next_hop.unwrap().path_len }; if let Some(ipv4_addr) = info.ipv4_addr { @@ -866,7 +865,7 @@ impl RouteTable { .entry(ipv4_addr.into()) .and_modify(|v| { if *v != *peer_id && is_new_peer_better(*v) { - self.ipv4_peer_id_map.insert(ipv4_addr.into(), *peer_id); + *v = *peer_id; } }) .or_insert(*peer_id); @@ -878,14 +877,10 @@ impl RouteTable { .and_modify(|v| { if *v != *peer_id && is_new_peer_better(*v) { // if the next hop is not set or the new next hop is better, update it. - self.cidr_peer_id_map - .insert(cidr.parse().unwrap(), *peer_id); + *v = *peer_id; } }) .or_insert(*peer_id); - - self.cidr_peer_id_map - .insert(cidr.parse().unwrap(), *peer_id); } } } @@ -1084,6 +1079,7 @@ struct PeerRouteServiceImpl { route_table: RouteTable, route_table_with_cost: RouteTable, foreign_network_owner_map: DashMap>, + foreign_network_my_peer_id_map: DashMap<(String, PeerId), PeerId>, synced_route_info: SyncedRouteInfo, cached_local_conn_map: std::sync::Mutex, cached_local_conn_map_version: AtomicVersion, @@ -1104,6 +1100,10 @@ impl Debug for PeerRouteServiceImpl { .field("route_table_with_cost", &self.route_table_with_cost) .field("synced_route_info", &self.synced_route_info) .field("foreign_network_owner_map", &self.foreign_network_owner_map) + .field( + "foreign_network_my_peer_id_map", + &self.foreign_network_my_peer_id_map, + ) .field( "cached_local_conn_map", &self.cached_local_conn_map.lock().unwrap(), @@ -1127,6 +1127,7 @@ impl PeerRouteServiceImpl { route_table: RouteTable::new(), route_table_with_cost: RouteTable::new(), foreign_network_owner_map: DashMap::new(), + foreign_network_my_peer_id_map: DashMap::new(), synced_route_info: SyncedRouteInfo { peer_infos: DashMap::new(), @@ -1266,6 +1267,7 @@ impl PeerRouteServiceImpl { } fn update_foreign_network_owner_map(&self) { + self.foreign_network_my_peer_id_map.clear(); self.foreign_network_owner_map.clear(); for item in self.synced_route_info.foreign_network.iter() { let key = item.key(); @@ -1290,7 +1292,12 @@ impl PeerRouteServiceImpl { self.foreign_network_owner_map .entry(network_identity) .or_insert_with(|| Vec::new()) - .push(key.peer_id); + .push(entry.my_peer_id_for_this_network); + + self.foreign_network_my_peer_id_map.insert( + (key.network_name.clone(), entry.my_peer_id_for_this_network), + key.peer_id, + ); } } @@ -1529,8 +1536,6 @@ impl PeerRouteServiceImpl { req_dynamic_msg.set_field_by_name("peer_infos", Value::Message(peer_infos)); } - tracing::trace!(?req_dynamic_msg, "build_sync_route_raw_req"); - req_dynamic_msg } @@ -1646,7 +1651,12 @@ impl PeerRouteServiceImpl { } fn update_peer_info_last_update(&self) { - tracing::debug!(?self, "update_peer_info_last_update"); + tracing::debug!( + "update_peer_info_last_update, my_peer_id: {:?}, prev: {:?}, new: {:?}", + self.my_peer_id, + self.peer_info_last_update.load(), + std::time::Instant::now() + ); self.peer_info_last_update.store(std::time::Instant::now()); } @@ -2089,7 +2099,6 @@ impl PeerRoute { } } - #[tracing::instrument(skip(session_mgr))] async fn maintain_session_tasks( session_mgr: RouteSessionManager, service_impl: Arc, @@ -2097,7 +2106,6 @@ impl PeerRoute { session_mgr.maintain_sessions(service_impl).await; } - #[tracing::instrument(skip(session_mgr))] async fn update_my_peer_info_routine( service_impl: Arc, session_mgr: RouteSessionManager, @@ -2296,6 +2304,17 @@ impl Route for PeerRoute { .unwrap_or_default() } + async fn get_origin_my_peer_id( + &self, + network_name: &str, + foreign_my_peer_id: PeerId, + ) -> Option { + self.service_impl + .foreign_network_my_peer_id_map + .get(&(network_name.to_string(), foreign_my_peer_id)) + .map(|x| *x) + } + async fn get_feature_flag(&self, peer_id: PeerId) -> Option { self.service_impl .route_table diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index 80c653374..2dd7b8437 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -95,6 +95,16 @@ pub trait Route { Default::default() } + // my peer id in foreign network is different from the one in local network + // this function is used to get the peer id in local network + async fn get_origin_my_peer_id( + &self, + _network_name: &str, + _foreign_my_peer_id: PeerId, + ) -> Option { + None + } + async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) {} async fn get_feature_flag(&self, peer_id: PeerId) -> Option; diff --git a/easytier/src/proto/cli.proto b/easytier/src/proto/cli.proto index 973af4505..53ac82204 100644 --- a/easytier/src/proto/cli.proto +++ b/easytier/src/proto/cli.proto @@ -103,6 +103,7 @@ message ListForeignNetworkRequest {} message ForeignNetworkEntryPb { repeated PeerInfo peers = 1; bytes network_secret_digest = 2; + uint32 my_peer_id_for_this_network = 3; } message ListForeignNetworkResponse { diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 56a0354d8..0e1fd961a 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -46,6 +46,7 @@ message ForeignNetworkRouteInfoEntry { google.protobuf.Timestamp last_update = 2; uint32 version = 3; bytes network_secret_digest = 4; + uint32 my_peer_id_for_this_network = 5; } message RouteForeignNetworkInfos { diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 2f364f38c..e37b5d458 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -887,11 +887,17 @@ pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) { .get_foreign_network_client() .get_peer_map() }; + let center_inst_peer_id = if !is_foreign { + center_inst.peer_id() + } else { + center_inst + .get_peer_manager() + .get_foreign_network_manager() + .get_network_peer_id(&inst1.get_global_ctx().get_network_identity().network_name) + .unwrap() + }; - let conns = peer_map - .list_peer_conns(center_inst.peer_id()) - .await - .unwrap(); + let conns = peer_map.list_peer_conns(center_inst_peer_id).await.unwrap(); assert!(conns.len() >= 1); From 8ddd153022f6fe7c11627b3c9ac35918fab9e593 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Wed, 11 Jun 2025 23:17:09 +0800 Subject: [PATCH 012/165] =?UTF-8?q?easytier-core=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=20(#964)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 将web和gui允许多网络实例逻辑抽离到NetworkInstanceManager中 * easytier-core支持多配置文件 * FFI复用instance manager * 添加instance manager 单元测试 --- Cargo.lock | 16 +- easytier-contrib/easytier-ffi/Cargo.toml | 1 + easytier-contrib/easytier-ffi/src/lib.rs | 55 ++- easytier-gui/src-tauri/Cargo.toml | 1 + easytier-gui/src-tauri/src/lib.rs | 74 ++-- easytier-web/src/main.rs | 29 +- easytier/src/common/config.rs | 58 ++- easytier/src/easytier-core.rs | 411 +++++++------------ easytier/src/instance_manager.rs | 491 +++++++++++++++++++++++ easytier/src/launcher.rs | 65 +-- easytier/src/lib.rs | 1 + easytier/src/utils.rs | 21 +- easytier/src/web_client/controller.rs | 89 ++-- 13 files changed, 866 insertions(+), 446 deletions(-) create mode 100644 easytier/src/instance_manager.rs diff --git a/Cargo.lock b/Cargo.lock index fe0b05814..78ff92010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2066,6 +2066,7 @@ dependencies = [ "once_cell", "serde", "serde_json", + "uuid", ] [[package]] @@ -2094,6 +2095,7 @@ dependencies = [ "tauri-plugin-vpnservice", "thunk-rs", "tokio", + "uuid", ] [[package]] @@ -9230,21 +9232,23 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom 0.2.15", - "rand 0.8.5", + "getrandom 0.3.2", + "js-sys", + "rand 0.9.1", "serde", "uuid-macro-internal", + "wasm-bindgen", ] [[package]] name = "uuid-macro-internal" -version = "1.10.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" +checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7" dependencies = [ "proc-macro2", "quote", diff --git a/easytier-contrib/easytier-ffi/Cargo.toml b/easytier-contrib/easytier-ffi/Cargo.toml index d86091cee..fff8262ae 100644 --- a/easytier-contrib/easytier-ffi/Cargo.toml +++ b/easytier-contrib/easytier-ffi/Cargo.toml @@ -14,3 +14,4 @@ dashmap = "6.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1" +uuid = "1.17.0" diff --git a/easytier-contrib/easytier-ffi/src/lib.rs b/easytier-contrib/easytier-ffi/src/lib.rs index ab8a7b1f9..4e2f66cad 100644 --- a/easytier-contrib/easytier-ffi/src/lib.rs +++ b/easytier-contrib/easytier-ffi/src/lib.rs @@ -3,11 +3,14 @@ use std::sync::Mutex; use dashmap::DashMap; use easytier::{ common::config::{ConfigLoader as _, TomlConfigLoader}, - launcher::NetworkInstance, + instance_manager::NetworkInstanceManager, + launcher::ConfigSource, }; -static INSTANCE_MAP: once_cell::sync::Lazy> = +static INSTANCE_NAME_ID_MAP: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(DashMap::new); +static INSTANCE_MANAGER: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(NetworkInstanceManager::new); static ERROR_MSG: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| Mutex::new(Vec::new())); @@ -86,18 +89,20 @@ pub extern "C" fn run_network_instance(cfg_str: *const std::ffi::c_char) -> std: let inst_name = cfg.get_inst_name(); - if INSTANCE_MAP.contains_key(&inst_name) { + if INSTANCE_NAME_ID_MAP.contains_key(&inst_name) { set_error_msg("instance already exists"); return -1; } - let mut instance = NetworkInstance::new(cfg); - if let Err(e) = instance.start().map_err(|e| e.to_string()) { - set_error_msg(&format!("failed to start instance: {}", e)); - return -1; - } + let instance_id = match INSTANCE_MANAGER.run_network_instance(cfg, ConfigSource::FFI) { + Ok(id) => id, + Err(e) => { + set_error_msg(&format!("failed to start instance: {}", e)); + return -1; + } + }; - INSTANCE_MAP.insert(inst_name, instance); + INSTANCE_NAME_ID_MAP.insert(inst_name, instance_id); 0 } @@ -108,7 +113,11 @@ pub extern "C" fn retain_network_instance( length: usize, ) -> std::ffi::c_int { if length == 0 { - INSTANCE_MAP.clear(); + if let Err(e) = INSTANCE_MANAGER.retain_network_instance(Vec::new()) { + set_error_msg(&format!("failed to retain instances: {}", e)); + return -1; + } + INSTANCE_NAME_ID_MAP.clear(); return 0; } @@ -125,7 +134,17 @@ pub extern "C" fn retain_network_instance( .collect::>() }; - let _ = INSTANCE_MAP.retain(|k, _| inst_names.contains(k)); + let inst_ids: Vec = inst_names + .iter() + .filter_map(|name| INSTANCE_NAME_ID_MAP.get(name).map(|id| *id)) + .collect(); + + if let Err(e) = INSTANCE_MANAGER.retain_network_instance(inst_ids) { + set_error_msg(&format!("failed to retain instances: {}", e)); + return -1; + } + + let _ = INSTANCE_NAME_ID_MAP.retain(|k, _| inst_names.contains(k)); 0 } @@ -144,13 +163,20 @@ pub extern "C" fn collect_network_infos( std::slice::from_raw_parts_mut(infos, max_length) }; + let collected_infos = match INSTANCE_MANAGER.collect_network_infos() { + Ok(infos) => infos, + Err(e) => { + set_error_msg(&format!("failed to collect network infos: {}", e)); + return -1; + } + }; + let mut index = 0; - for instance in INSTANCE_MAP.iter() { + for (instance_id, value) in collected_infos.iter() { if index >= max_length { break; } - let key = instance.key(); - let Some(value) = instance.get_running_info() else { + let Some(key) = INSTANCE_MANAGER.get_network_instance_name(instance_id) else { continue; }; // convert value to json string @@ -181,7 +207,6 @@ mod tests { let cfg_str = r#" inst_name = "test" network = "test_network" - fdsafdsa "#; let cstr = std::ffi::CString::new(cfg_str).unwrap(); assert_eq!(parse_config(cstr.as_ptr()), 0); diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index e23ae8f93..97dfab6fc 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -53,6 +53,7 @@ tauri-plugin-positioner = { version = "2.0", features = ["tray-icon"] } tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" } tauri-plugin-os = "2.0" tauri-plugin-autostart = "2.0" +uuid = "1.17.0" [features] diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 517a2716e..d42faebec 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -3,10 +3,12 @@ use std::collections::BTreeMap; -use dashmap::DashMap; use easytier::{ - common::config::{ConfigLoader, FileLoggerConfig, TomlConfigLoader}, - launcher::{NetworkConfig, NetworkInstance, NetworkInstanceRunningInfo}, + common::config::{ + ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, + }, + launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo}, + instance_manager::NetworkInstanceManager, utils::{self, NewFilterSender}, }; @@ -17,8 +19,8 @@ pub const AUTOSTART_ARG: &str = "--autostart"; #[cfg(not(target_os = "android"))] use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; -static INSTANCE_MAP: once_cell::sync::Lazy> = - once_cell::sync::Lazy::new(DashMap::new); +static INSTANCE_MANAGER: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(NetworkInstanceManager::new); static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(Default::default); @@ -44,41 +46,39 @@ fn parse_network_config(cfg: NetworkConfig) -> Result { #[tauri::command] fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> { - if INSTANCE_MAP.contains_key(cfg.instance_id()) { - return Err("instance already exists".to_string()); - } let instance_id = cfg.instance_id().to_string(); - let cfg = cfg.gen_config().map_err(|e| e.to_string())?; - let mut instance = NetworkInstance::new(cfg); - instance.start().map_err(|e| e.to_string())?; - + INSTANCE_MANAGER + .run_network_instance(cfg, ConfigSource::GUI) + .map_err(|e| e.to_string())?; println!("instance {} started", instance_id); - INSTANCE_MAP.insert(instance_id, instance); Ok(()) } #[tauri::command] fn retain_network_instance(instance_ids: Vec) -> Result<(), String> { - let _ = INSTANCE_MAP.retain(|k, _| instance_ids.contains(k)); - println!( - "instance {:?} retained", - INSTANCE_MAP - .iter() - .map(|item| item.key().clone()) - .collect::>() - ); + let instance_ids = instance_ids + .into_iter() + .filter_map(|id| uuid::Uuid::parse_str(&id).ok()) + .collect(); + let retained = INSTANCE_MANAGER + .retain_network_instance(instance_ids) + .map_err(|e| e.to_string())?; + println!("instance {:?} retained", retained); Ok(()) } #[tauri::command] fn collect_network_infos() -> Result, String> { + let infos = INSTANCE_MANAGER + .collect_network_infos() + .map_err(|e| e.to_string())?; + let mut ret = BTreeMap::new(); - for instance in INSTANCE_MAP.iter() { - if let Some(info) = instance.get_running_info() { - ret.insert(instance.key().clone(), info); - } + for (uuid, info) in infos { + ret.insert(uuid.to_string(), info); } + Ok(ret) } @@ -97,10 +97,10 @@ fn set_logging_level(level: String) -> Result<(), String> { #[tauri::command] fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> { - let mut instance = INSTANCE_MAP - .get_mut(&instance_id) - .ok_or("instance not found")?; - instance.set_tun_fd(fd); + let uuid = uuid::Uuid::parse_str(&instance_id).map_err(|e| e.to_string())?; + INSTANCE_MANAGER + .set_tun_fd(&uuid, fd) + .map_err(|e| e.to_string())?; Ok(()) } @@ -185,13 +185,15 @@ pub fn run() { let Ok(log_dir) = app.path().app_log_dir() else { return Ok(()); }; - let config = TomlConfigLoader::default(); - config.set_file_logger_config(FileLoggerConfig { - dir: Some(log_dir.to_string_lossy().to_string()), - level: None, - file: None, - }); - let Ok(Some(logger_reinit)) = utils::init_logger(config, true) else { + let config = LoggingConfigBuilder::default() + .file_logger(FileLoggerConfig { + dir: Some(log_dir.to_string_lossy().to_string()), + level: None, + file: None, + }) + .build() + .map_err(|e| e.to_string())?; + let Ok(Some(logger_reinit)) = utils::init_logger(&config, true) else { return Ok(()); }; #[allow(static_mut_refs)] diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 3ad85bb68..852bf775f 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use clap::Parser; use easytier::{ common::{ - config::{ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, TomlConfigLoader}, + config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader}, constants::EASYTIER_VERSION, error::Error, network::{local_ipv4, local_ipv6}, @@ -101,6 +101,22 @@ struct Cli { api_host: Option, } +impl LoggingConfigLoader for &Cli { + fn get_console_logger_config(&self) -> ConsoleLoggerConfig { + ConsoleLoggerConfig { + level: self.console_log_level.clone(), + } + } + + fn get_file_logger_config(&self) -> FileLoggerConfig { + FileLoggerConfig { + dir: self.file_log_dir.clone(), + level: self.file_log_level.clone(), + file: None, + } + } +} + pub fn get_listener_by_url(l: &url::Url) -> Result, Error> { Ok(match l.scheme() { "tcp" => Box::new(TcpTunnelListener::new(l.clone())), @@ -144,16 +160,7 @@ async fn main() { setup_panic_handler(); let cli = Cli::parse(); - let config = TomlConfigLoader::default(); - config.set_console_logger_config(ConsoleLoggerConfig { - level: cli.console_log_level, - }); - config.set_file_logger_config(FileLoggerConfig { - dir: cli.file_log_dir, - level: cli.file_log_level, - file: None, - }); - init_logger(config, false).unwrap(); + init_logger(&cli, false).unwrap(); // let db = db::Db::new(":memory:").await.unwrap(); let db = db::Db::new(cli.db).await.unwrap(); diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 9798b3239..34004309e 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -71,11 +71,6 @@ pub trait ConfigLoader: Send + Sync { fn get_listener_uris(&self) -> Vec; - fn get_file_logger_config(&self) -> FileLoggerConfig; - fn set_file_logger_config(&self, config: FileLoggerConfig); - fn get_console_logger_config(&self) -> ConsoleLoggerConfig; - fn set_console_logger_config(&self, config: ConsoleLoggerConfig); - fn get_peers(&self) -> Vec; fn set_peers(&self, peers: Vec); @@ -112,6 +107,12 @@ pub trait ConfigLoader: Send + Sync { fn dump(&self) -> String; } +pub trait LoggingConfigLoader { + fn get_file_logger_config(&self) -> FileLoggerConfig; + + fn get_console_logger_config(&self) -> ConsoleLoggerConfig; +} + pub type NetworkSecretDigest = [u8; 32]; #[derive(Debug, Clone, Deserialize, Serialize, Default, Eq, Hash)] @@ -186,6 +187,24 @@ pub struct ConsoleLoggerConfig { pub level: Option, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, derive_builder::Builder)] +pub struct LoggingConfig { + #[builder(setter(into, strip_option), default = None)] + file_logger: Option, + #[builder(setter(into, strip_option), default = None)] + console_logger: Option, +} + +impl LoggingConfigLoader for &LoggingConfig { + fn get_file_logger_config(&self) -> FileLoggerConfig { + self.file_logger.clone().unwrap_or_default() + } + + fn get_console_logger_config(&self) -> ConsoleLoggerConfig { + self.console_logger.clone().unwrap_or_default() + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct VpnPortalConfig { pub client_cidr: cidr::Ipv4Cidr, @@ -243,9 +262,6 @@ struct Config { peer: Option>, proxy_network: Option>, - file_logger: Option, - console_logger: Option, - rpc_portal: Option, rpc_portal_whitelist: Option>, @@ -486,32 +502,6 @@ impl ConfigLoader for TomlConfigLoader { .unwrap_or_default() } - fn get_file_logger_config(&self) -> FileLoggerConfig { - self.config - .lock() - .unwrap() - .file_logger - .clone() - .unwrap_or_default() - } - - fn set_file_logger_config(&self, config: FileLoggerConfig) { - self.config.lock().unwrap().file_logger = Some(config); - } - - fn get_console_logger_config(&self) -> ConsoleLoggerConfig { - self.config - .lock() - .unwrap() - .console_logger - .clone() - .unwrap_or_default() - } - - fn set_console_logger_config(&self, config: ConsoleLoggerConfig) { - self.config.lock().unwrap().console_logger = Some(config); - } - fn get_peers(&self) -> Vec { self.config.lock().unwrap().peer.clone().unwrap_or_default() } diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index e209f37a1..e733a32b6 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -17,20 +17,17 @@ use clap::Parser; use easytier::{ common::{ config::{ - ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, NetworkIdentity, PeerConfig, - PortForwardConfig, TomlConfigLoader, VpnPortalConfig, + ConfigLoader, ConsoleLoggerConfig, FileLoggerConfig, LoggingConfigLoader, + NetworkIdentity, PeerConfig, PortForwardConfig, TomlConfigLoader, VpnPortalConfig, }, constants::EASYTIER_VERSION, - global_ctx::{EventBusSubscriber, GlobalCtx, GlobalCtxEvent}, - scoped_task::ScopedTask, + global_ctx::GlobalCtx, stun::MockStunInfoCollector, }, connector::create_connector_by_url, - launcher, - proto::{ - self, - common::{CompressionAlgoPb, NatType}, - }, + launcher::ConfigSource, + instance_manager::NetworkInstanceManager, + proto::common::{CompressionAlgoPb, NatType}, tunnel::{IpVersion, PROTO_PORT_OFFSET}, utils::{init_logger, setup_panic_handler}, web_client, @@ -106,10 +103,21 @@ struct Cli { short, long, env = "ET_CONFIG_FILE", - help = t!("core_clap.config_file").to_string() + value_delimiter = ',', + help = t!("core_clap.config_file").to_string(), + num_args = 1.., )] - config_file: Option, + config_file: Option>, + + #[command(flatten)] + network_options: NetworkOptions, + + #[command(flatten)] + logging_options: LoggingOptions, +} +#[derive(Parser, Debug)] +struct NetworkOptions { #[arg( long, env = "ET_NETWORK_NAME", @@ -212,27 +220,6 @@ struct Cli { )] no_listener: bool, - #[arg( - long, - env = "ET_CONSOLE_LOG_LEVEL", - help = t!("core_clap.console_log_level").to_string() - )] - console_log_level: Option, - - #[arg( - long, - env = "ET_FILE_LOG_LEVEL", - help = t!("core_clap.file_log_level").to_string() - )] - file_log_level: Option, - - #[arg( - long, - env = "ET_FILE_LOG_DIR", - help = t!("core_clap.file_log_dir").to_string() - )] - file_log_dir: Option, - #[arg( long, env = "ET_HOSTNAME", @@ -470,6 +457,30 @@ struct Cli { private_mode: Option, } +#[derive(Parser, Debug)] +struct LoggingOptions { + #[arg( + long, + env = "ET_CONSOLE_LOG_LEVEL", + help = t!("core_clap.console_log_level").to_string() + )] + console_log_level: Option, + + #[arg( + long, + env = "ET_FILE_LOG_LEVEL", + help = t!("core_clap.file_log_level").to_string() + )] + file_log_level: Option, + + #[arg( + long, + env = "ET_FILE_LOG_DIR", + help = t!("core_clap.file_log_dir").to_string() + )] + file_log_dir: Option, +} + rust_i18n::i18n!("locales", fallback = "en"); impl Cli { @@ -527,43 +538,47 @@ impl Cli { } } -impl TryFrom<&Cli> for TomlConfigLoader { - type Error = anyhow::Error; - - fn try_from(cli: &Cli) -> Result { - let cfg = if let Some(config_file) = &cli.config_file { - TomlConfigLoader::new(config_file) - .with_context(|| format!("failed to load config file: {:?}", cli.config_file))? - } else { - TomlConfigLoader::default() +impl NetworkOptions { + fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool { + if config_file_count == 1{ + return true; + } + let Some(network_name) = &self.network_name else { + return false; }; + if cfg.get_network_identity().network_name == *network_name { + return true; + } + false + } - if cli.hostname.is_some() { - cfg.set_hostname(cli.hostname.clone()); + fn merge_into(&self, cfg: &mut TomlConfigLoader) -> anyhow::Result<()> { + if self.hostname.is_some() { + cfg.set_hostname(self.hostname.clone()); } let old_ns = cfg.get_network_identity(); - let network_name = cli.network_name.clone().unwrap_or(old_ns.network_name); - let network_secret = cli + let network_name = self.network_name.clone().unwrap_or(old_ns.network_name); + let network_secret = self .network_secret .clone() .unwrap_or(old_ns.network_secret.unwrap_or_default()); cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret)); - if let Some(dhcp) = cli.dhcp { + if let Some(dhcp) = self.dhcp { cfg.set_dhcp(dhcp); } - if let Some(ipv4) = &cli.ipv4 { + if let Some(ipv4) = &self.ipv4 { cfg.set_ipv4(Some(ipv4.parse().with_context(|| { format!("failed to parse ipv4 address: {}", ipv4) })?)) } - if !cli.peers.is_empty() { + if !self.peers.is_empty() { let mut peers = cfg.get_peers(); - peers.reserve(peers.len() + cli.peers.len()); - for p in &cli.peers { + peers.reserve(peers.len() + self.peers.len()); + for p in &self.peers { peers.push(PeerConfig { uri: p .parse() @@ -573,9 +588,9 @@ impl TryFrom<&Cli> for TomlConfigLoader { cfg.set_peers(peers); } - if cli.no_listener || !cli.listeners.is_empty() { + if self.no_listener || !self.listeners.is_empty() { cfg.set_listeners( - Cli::parse_listeners(cli.no_listener, cli.listeners.clone())? + Cli::parse_listeners(self.no_listener, self.listeners.clone())? .into_iter() .map(|s| s.parse().unwrap()) .collect(), @@ -589,9 +604,9 @@ impl TryFrom<&Cli> for TomlConfigLoader { ); } - if !cli.mapped_listeners.is_empty() { + if !self.mapped_listeners.is_empty() { cfg.set_mapped_listeners(Some( - cli.mapped_listeners + self.mapped_listeners .iter() .map(|s| { s.parse() @@ -608,14 +623,14 @@ impl TryFrom<&Cli> for TomlConfigLoader { )); } - for n in cli.proxy_networks.iter() { + for n in self.proxy_networks.iter() { cfg.add_proxy_cidr( n.parse() .with_context(|| format!("failed to parse proxy network: {}", n))?, ); } - let rpc_portal = if let Some(r) = &cli.rpc_portal { + let rpc_portal = if let Some(r) = &self.rpc_portal { Cli::parse_rpc_portal(r.clone()) .with_context(|| format!("failed to parse rpc portal: {}", r))? } else if let Some(r) = cfg.get_rpc_portal() { @@ -625,9 +640,9 @@ impl TryFrom<&Cli> for TomlConfigLoader { }; cfg.set_rpc_portal(rpc_portal); - cfg.set_rpc_portal_whitelist(cli.rpc_portal_whitelist.clone()); + cfg.set_rpc_portal_whitelist(self.rpc_portal_whitelist.clone()); - if let Some(external_nodes) = cli.external_node.as_ref() { + if let Some(external_nodes) = self.external_node.as_ref() { let mut old_peers = cfg.get_peers(); old_peers.push(PeerConfig { uri: external_nodes.parse().with_context(|| { @@ -637,37 +652,11 @@ impl TryFrom<&Cli> for TomlConfigLoader { cfg.set_peers(old_peers); } - if cli.console_log_level.is_some() { - cfg.set_console_logger_config(ConsoleLoggerConfig { - level: cli.console_log_level.clone(), - }); - } - - if let Some(inst_name) = &cli.instance_name { + if let Some(inst_name) = &self.instance_name { cfg.set_inst_name(inst_name.clone()); } - if cli.file_log_dir.is_some() || cli.file_log_level.is_some() { - let inst_name = cfg.get_inst_name(); - let old_fl = cfg.get_file_logger_config(); - let file_log_dir = if cli.file_log_dir.is_some() { - &cli.file_log_dir - } else { - &old_fl.dir - }; - let file_log_level = if cli.file_log_level.is_some() { - &cli.file_log_level - } else { - &old_fl.level - }; - cfg.set_file_logger_config(FileLoggerConfig { - level: file_log_level.clone(), - dir: file_log_dir.clone(), - file: Some(format!("easytier-{}", inst_name)), - }); - } - - if let Some(vpn_portal) = cli.vpn_portal.as_ref() { + if let Some(vpn_portal) = self.vpn_portal.as_ref() { let url: url::Url = vpn_portal .parse() .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?; @@ -687,7 +676,7 @@ impl TryFrom<&Cli> for TomlConfigLoader { }); } - if let Some(manual_routes) = cli.manual_routes.as_ref() { + if let Some(manual_routes) = self.manual_routes.as_ref() { let mut routes = Vec::::with_capacity(manual_routes.len()); for r in manual_routes { routes.push( @@ -699,7 +688,7 @@ impl TryFrom<&Cli> for TomlConfigLoader { } #[cfg(feature = "socks5")] - if let Some(socks5_proxy) = cli.socks5 { + if let Some(socks5_proxy) = self.socks5 { cfg.set_socks5_portal(Some( format!("socks5://0.0.0.0:{}", socks5_proxy) .parse() @@ -708,7 +697,7 @@ impl TryFrom<&Cli> for TomlConfigLoader { } #[cfg(feature = "socks5")] - for port_forward in cli.port_forward.iter() { + for port_forward in self.port_forward.iter() { let example_str = ", example: udp://0.0.0.0:12345/10.126.126.1:12345"; let bind_addr = format!( @@ -742,38 +731,38 @@ impl TryFrom<&Cli> for TomlConfigLoader { } let mut f = cfg.get_flags(); - if let Some(default_protocol) = &cli.default_protocol { + if let Some(default_protocol) = &self.default_protocol { f.default_protocol = default_protocol.clone() }; - if let Some(v) = cli.disable_encryption { + if let Some(v) = self.disable_encryption { f.enable_encryption = !v; } - if let Some(v) = cli.disable_ipv6 { + if let Some(v) = self.disable_ipv6 { f.enable_ipv6 = !v; } - f.latency_first = cli.latency_first.unwrap_or(f.latency_first); - if let Some(dev_name) = &cli.dev_name { + f.latency_first = self.latency_first.unwrap_or(f.latency_first); + if let Some(dev_name) = &self.dev_name { f.dev_name = dev_name.clone() } - if let Some(mtu) = cli.mtu { + if let Some(mtu) = self.mtu { f.mtu = mtu as u32; } - f.enable_exit_node = cli.enable_exit_node.unwrap_or(f.enable_exit_node); - f.proxy_forward_by_system = cli + f.enable_exit_node = self.enable_exit_node.unwrap_or(f.enable_exit_node); + f.proxy_forward_by_system = self .proxy_forward_by_system .unwrap_or(f.proxy_forward_by_system); - f.no_tun = cli.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun")); - f.use_smoltcp = cli.use_smoltcp.unwrap_or(f.use_smoltcp); - if let Some(wl) = cli.relay_network_whitelist.as_ref() { + f.no_tun = self.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun")); + f.use_smoltcp = self.use_smoltcp.unwrap_or(f.use_smoltcp); + if let Some(wl) = self.relay_network_whitelist.as_ref() { f.relay_network_whitelist = wl.join(" "); } - f.disable_p2p = cli.disable_p2p.unwrap_or(f.disable_p2p); - f.disable_udp_hole_punching = cli + f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p); + f.disable_udp_hole_punching = self .disable_udp_hole_punching .unwrap_or(f.disable_udp_hole_punching); - f.relay_all_peer_rpc = cli.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc); - f.multi_thread = cli.multi_thread.unwrap_or(f.multi_thread); - if let Some(compression) = &cli.compression { + f.relay_all_peer_rpc = self.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc); + f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread); + if let Some(compression) = &self.compression { f.data_compress_algo = match compression.as_str() { "none" => CompressionAlgoPb::None, "zstd" => CompressionAlgoPb::Zstd, @@ -784,154 +773,35 @@ impl TryFrom<&Cli> for TomlConfigLoader { } .into(); } - f.bind_device = cli.bind_device.unwrap_or(f.bind_device); - f.enable_kcp_proxy = cli.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); - f.disable_kcp_input = cli.disable_kcp_input.unwrap_or(f.disable_kcp_input); - f.accept_dns = cli.accept_dns.unwrap_or(f.accept_dns); - f.private_mode = cli.private_mode.unwrap_or(f.private_mode); + f.bind_device = self.bind_device.unwrap_or(f.bind_device); + f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); + f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input); + f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns); + f.private_mode = self.private_mode.unwrap_or(f.private_mode); cfg.set_flags(f); - if !cli.exit_nodes.is_empty() { - cfg.set_exit_nodes(cli.exit_nodes.clone()); + if !self.exit_nodes.is_empty() { + cfg.set_exit_nodes(self.exit_nodes.clone()); } - Ok(cfg) + Ok(()) } } -fn print_event(msg: String) { - println!( - "{}: {}", - chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), - msg - ); -} - -fn peer_conn_info_to_string(p: proto::cli::PeerConnInfo) -> String { - format!( - "my_peer_id: {}, dst_peer_id: {}, tunnel_info: {:?}", - p.my_peer_id, p.peer_id, p.tunnel - ) -} - -#[tracing::instrument] -pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - loop { - if let Ok(e) = events.recv().await { - match e { - GlobalCtxEvent::PeerAdded(p) => { - print_event(format!("new peer added. peer_id: {}", p)); - } - - GlobalCtxEvent::PeerRemoved(p) => { - print_event(format!("peer removed. peer_id: {}", p)); - } - - GlobalCtxEvent::PeerConnAdded(p) => { - print_event(format!( - "new peer connection added. conn_info: {}", - peer_conn_info_to_string(p) - )); - } - - GlobalCtxEvent::PeerConnRemoved(p) => { - print_event(format!( - "peer connection removed. conn_info: {}", - peer_conn_info_to_string(p) - )); - } - - GlobalCtxEvent::ListenerAddFailed(p, msg) => { - print_event(format!( - "listener add failed. listener: {}, msg: {}", - p, msg - )); - } - - GlobalCtxEvent::ListenerAcceptFailed(p, msg) => { - print_event(format!( - "listener accept failed. listener: {}, msg: {}", - p, msg - )); - } - - GlobalCtxEvent::ListenerAdded(p) => { - if p.scheme() == "ring" { - continue; - } - print_event(format!("new listener added. listener: {}", p)); - } - - GlobalCtxEvent::ConnectionAccepted(local, remote) => { - print_event(format!( - "new connection accepted. local: {}, remote: {}", - local, remote - )); - } - - GlobalCtxEvent::ConnectionError(local, remote, err) => { - print_event(format!( - "connection error. local: {}, remote: {}, err: {}", - local, remote, err - )); - } - - GlobalCtxEvent::TunDeviceReady(dev) => { - print_event(format!("tun device ready. dev: {}", dev)); - } - - GlobalCtxEvent::TunDeviceError(err) => { - print_event(format!("tun device error. err: {}", err)); - } - - GlobalCtxEvent::Connecting(dst) => { - print_event(format!("connecting to peer. dst: {}", dst)); - } - - GlobalCtxEvent::ConnectError(dst, ip_version, err) => { - print_event(format!( - "connect to peer error. dst: {}, ip_version: {}, err: {}", - dst, ip_version, err - )); - } - - GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => { - print_event(format!( - "vpn portal client connected. portal: {}, client_addr: {}", - portal, client_addr - )); - } - - GlobalCtxEvent::VpnPortalClientDisconnected(portal, client_addr) => { - print_event(format!( - "vpn portal client disconnected. portal: {}, client_addr: {}", - portal, client_addr - )); - } - - GlobalCtxEvent::DhcpIpv4Changed(old, new) => { - print_event(format!("dhcp ip changed. old: {:?}, new: {:?}", old, new)); - } - - GlobalCtxEvent::DhcpIpv4Conflicted(ip) => { - print_event(format!("dhcp ip conflict. ip: {:?}", ip)); - } +impl LoggingConfigLoader for &LoggingOptions { + fn get_console_logger_config(&self) -> ConsoleLoggerConfig { + ConsoleLoggerConfig { + level: self.console_log_level.clone(), + } + } - GlobalCtxEvent::PortForwardAdded(cfg) => { - print_event(format!( - "port forward added. local: {}, remote: {}, proto: {}", - cfg.bind_addr.unwrap().to_string(), - cfg.dst_addr.unwrap().to_string(), - cfg.socket_type().as_str_name() - )); - } - } - } else { - events = events.resubscribe(); - } + fn get_file_logger_config(&self) -> FileLoggerConfig { + FileLoggerConfig { + level: self.file_log_level.clone(), + dir: self.file_log_dir.clone(), + file: None, } - }) + } } #[cfg(target_os = "windows")] @@ -1046,8 +916,7 @@ fn win_service_main(arg: Vec) { } async fn run_main(cli: Cli) -> anyhow::Result<()> { - let cfg = TomlConfigLoader::try_from(&cli)?; - init_logger(&cfg, false)?; + init_logger(&cli.logging_options, false)?; if cli.config_server.is_some() { let config_server_url_s = cli.config_server.clone().unwrap(); @@ -1088,7 +957,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { let mut flags = global_ctx.get_flags(); flags.bind_device = false; global_ctx.set_flags(flags); - let hostname = match cli.hostname { + let hostname = match cli.network_options.hostname { None => gethostname::gethostname().to_string_lossy().to_string(), Some(hostname) => hostname.to_string(), }; @@ -1100,19 +969,47 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> { tokio::signal::ctrl_c().await.unwrap(); return Ok(()); } + let manager = NetworkInstanceManager::new(); + let mut crate_cli_network = + cli.config_file.is_none() || cli.network_options.network_name.is_some(); + if let Some(config_files) = cli.config_file { + let config_file_count = config_files.len(); + for config_file in config_files { + let mut cfg = TomlConfigLoader::new(&config_file) + .with_context(|| format!("failed to load config file: {:?}", config_file))?; + + if cli.network_options.can_merge(&cfg, config_file_count) { + cli.network_options.merge_into(&mut cfg).with_context(|| { + format!("failed to merge config from cli: {:?}", config_file) + })?; + crate_cli_network = false; + } + + println!( + "Starting easytier from config file {:?} with config:", + config_file + ); + println!("############### TOML ###############\n"); + println!("{}", cfg.dump()); + println!("-----------------------------------"); + manager.run_network_instance(cfg, ConfigSource::File)?; + } + } - println!("Starting easytier with config:"); - println!("############### TOML ###############\n"); - println!("{}", cfg.dump()); - println!("-----------------------------------"); + if crate_cli_network { + let mut cfg = TomlConfigLoader::default(); + cli.network_options + .merge_into(&mut cfg) + .with_context(|| format!("failed to create config from cli"))?; + println!("Starting easytier from cli with config:"); + println!("############### TOML ###############\n"); + println!("{}", cfg.dump()); + println!("-----------------------------------"); + manager.run_network_instance(cfg, ConfigSource::Cli)?; + } - let mut l = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false); - let _t = ScopedTask::from(handle_event(l.start().unwrap())); tokio::select! { - e = l.wait() => { - if let Some(e) = e { - eprintln!("launcher error: {}", e); - } + _ = manager.wait() => { } _ = tokio::signal::ctrl_c() => { println!("ctrl-c received, exiting..."); diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs new file mode 100644 index 000000000..29eef0099 --- /dev/null +++ b/easytier/src/instance_manager.rs @@ -0,0 +1,491 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use dashmap::DashMap; + +use crate::{ + common::{ + config::{ConfigLoader, TomlConfigLoader}, + global_ctx::{EventBusSubscriber, GlobalCtxEvent}, + scoped_task::ScopedTask, + }, + launcher::{ConfigSource, NetworkInstance, NetworkInstanceRunningInfo}, + proto, +}; + +pub struct NetworkInstanceManager { + instance_map: Arc>, + instance_stop_tasks: Arc>>, + stop_check_notifier: Arc, +} + +impl NetworkInstanceManager { + pub fn new() -> Self { + NetworkInstanceManager { + instance_map: Arc::new(DashMap::new()), + instance_stop_tasks: Arc::new(DashMap::new()), + stop_check_notifier: Arc::new(tokio::sync::Notify::new()), + } + } + + fn start_instance_task(&self, instance_id: uuid::Uuid) -> Result<(), anyhow::Error> { + let instance = self + .instance_map + .get(&instance_id) + .ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?; + + if instance.get_config_source() == ConfigSource::FFI { + // FFI have no tokio runtime, so we don't need to spawn a task, and instance should be managed by the caller. + return Ok(()); + } + + let instance_stop_notifier = instance.get_stop_notifier(); + let instance_config_source = instance.get_config_source(); + let instance_event_receiver = match instance.get_config_source() { + ConfigSource::Cli | ConfigSource::File => Some(instance.subscribe_event()), + _ => None, + }; + + let instance_map = self.instance_map.clone(); + let instance_stop_tasks = self.instance_stop_tasks.clone(); + + let stop_check_notifier = self.stop_check_notifier.clone(); + self.instance_stop_tasks.insert( + instance_id, + ScopedTask::from(tokio::spawn(async move { + let Some(instance_stop_notifier) = instance_stop_notifier else { + return; + }; + let _t = if let Some(event) = instance_event_receiver.flatten() { + Some(ScopedTask::from(handle_event(instance_id, event))) + } else { + None + }; + instance_stop_notifier.notified().await; + if let Some(instance) = instance_map.get(&instance_id) { + if let Some(e) = instance.get_latest_error_msg() { + tracing::error!(?e, ?instance_id, "instance stopped with error"); + eprintln!("instance {} stopped with error: {}", instance_id, e); + } + } + match instance_config_source { + ConfigSource::Cli | ConfigSource::File => { + instance_map.remove(&instance_id); + } + ConfigSource::Web | ConfigSource::GUI | ConfigSource::FFI => {} + } + instance_stop_tasks.remove(&instance_id); + stop_check_notifier.notify_waiters(); + })), + ); + Ok(()) + } + + pub fn run_network_instance( + &self, + cfg: TomlConfigLoader, + source: ConfigSource, + ) -> Result { + let instance_id = cfg.get_id(); + if self.instance_map.contains_key(&instance_id) { + anyhow::bail!("instance {} already exists", instance_id); + } + + let mut instance = NetworkInstance::new(cfg, source); + instance.start()?; + + self.instance_map.insert(instance_id, instance); + self.start_instance_task(instance_id)?; + Ok(instance_id) + } + + pub fn retain_network_instance( + &self, + instance_ids: Vec, + ) -> Result, anyhow::Error> { + self.instance_map.retain(|k, _| instance_ids.contains(k)); + Ok(self.list_network_instance_ids()) + } + + pub fn delete_network_instance( + &self, + instance_ids: Vec, + ) -> Result, anyhow::Error> { + self.instance_map.retain(|k, _| !instance_ids.contains(k)); + Ok(self.list_network_instance_ids()) + } + + pub fn collect_network_infos( + &self, + ) -> Result, anyhow::Error> { + let mut ret = BTreeMap::new(); + for instance in self.instance_map.iter() { + if let Some(info) = instance.get_running_info() { + ret.insert(instance.key().clone(), info); + } + } + Ok(ret) + } + + pub fn list_network_instance_ids(&self) -> Vec { + self.instance_map + .iter() + .map(|item| item.key().clone()) + .collect() + } + + pub fn get_network_instance_name(&self, instance_id: &uuid::Uuid) -> Option { + self.instance_map + .get(instance_id) + .map(|instance| instance.value().get_inst_name()) + } + + pub fn set_tun_fd(&self, instance_id: &uuid::Uuid, fd: i32) -> Result<(), anyhow::Error> { + let mut instance = self + .instance_map + .get_mut(instance_id) + .ok_or_else(|| anyhow::anyhow!("instance not found"))?; + instance.set_tun_fd(fd); + Ok(()) + } + + pub async fn wait(&self) { + while self.instance_map.len() > 0 { + self.stop_check_notifier.notified().await; + } + } +} + +#[tracing::instrument] +fn handle_event( + instance_id: uuid::Uuid, + mut events: EventBusSubscriber, +) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + loop { + if let Ok(e) = events.recv().await { + match e { + GlobalCtxEvent::PeerAdded(p) => { + print_event(instance_id, format!("new peer added. peer_id: {}", p)); + } + + GlobalCtxEvent::PeerRemoved(p) => { + print_event(instance_id, format!("peer removed. peer_id: {}", p)); + } + + GlobalCtxEvent::PeerConnAdded(p) => { + print_event( + instance_id, + format!( + "new peer connection added. conn_info: {}", + peer_conn_info_to_string(p) + ), + ); + } + + GlobalCtxEvent::PeerConnRemoved(p) => { + print_event( + instance_id, + format!( + "peer connection removed. conn_info: {}", + peer_conn_info_to_string(p) + ), + ); + } + + GlobalCtxEvent::ListenerAddFailed(p, msg) => { + print_event( + instance_id, + format!("listener add failed. listener: {}, msg: {}", p, msg), + ); + } + + GlobalCtxEvent::ListenerAcceptFailed(p, msg) => { + print_event( + instance_id, + format!("listener accept failed. listener: {}, msg: {}", p, msg), + ); + } + + GlobalCtxEvent::ListenerAdded(p) => { + if p.scheme() == "ring" { + continue; + } + print_event(instance_id, format!("new listener added. listener: {}", p)); + } + + GlobalCtxEvent::ConnectionAccepted(local, remote) => { + print_event( + instance_id, + format!( + "new connection accepted. local: {}, remote: {}", + local, remote + ), + ); + } + + GlobalCtxEvent::ConnectionError(local, remote, err) => { + print_event( + instance_id, + format!( + "connection error. local: {}, remote: {}, err: {}", + local, remote, err + ), + ); + } + + GlobalCtxEvent::TunDeviceReady(dev) => { + print_event(instance_id, format!("tun device ready. dev: {}", dev)); + } + + GlobalCtxEvent::TunDeviceError(err) => { + print_event(instance_id, format!("tun device error. err: {}", err)); + } + + GlobalCtxEvent::Connecting(dst) => { + print_event(instance_id, format!("connecting to peer. dst: {}", dst)); + } + + GlobalCtxEvent::ConnectError(dst, ip_version, err) => { + print_event( + instance_id, + format!( + "connect to peer error. dst: {}, ip_version: {}, err: {}", + dst, ip_version, err + ), + ); + } + + GlobalCtxEvent::VpnPortalClientConnected(portal, client_addr) => { + print_event( + instance_id, + format!( + "vpn portal client connected. portal: {}, client_addr: {}", + portal, client_addr + ), + ); + } + + GlobalCtxEvent::VpnPortalClientDisconnected(portal, client_addr) => { + print_event( + instance_id, + format!( + "vpn portal client disconnected. portal: {}, client_addr: {}", + portal, client_addr + ), + ); + } + + GlobalCtxEvent::DhcpIpv4Changed(old, new) => { + print_event( + instance_id, + format!("dhcp ip changed. old: {:?}, new: {:?}", old, new), + ); + } + + GlobalCtxEvent::DhcpIpv4Conflicted(ip) => { + print_event(instance_id, format!("dhcp ip conflict. ip: {:?}", ip)); + } + + GlobalCtxEvent::PortForwardAdded(cfg) => { + print_event( + instance_id, + format!( + "port forward added. local: {}, remote: {}, proto: {}", + cfg.bind_addr.unwrap().to_string(), + cfg.dst_addr.unwrap().to_string(), + cfg.socket_type().as_str_name() + ), + ); + } + } + } else { + events = events.resubscribe(); + } + } + }) +} + +fn print_event(instance_id: uuid::Uuid, msg: String) { + println!( + "{}: [{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + instance_id, + msg + ); +} + +fn peer_conn_info_to_string(p: proto::cli::PeerConnInfo) -> String { + format!( + "my_peer_id: {}, dst_peer_id: {}, tunnel_info: {:?}", + p.my_peer_id, p.peer_id, p.tunnel + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::config::*; + + #[tokio::test] + async fn it_works() { + let manager = NetworkInstanceManager::new(); + let cfg_str = r#" + listeners = [] + "#; + + let port = crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found"); + + let instance_id1 = manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str) + .map(|c| { + c.set_listeners(vec![format!("tcp://0.0.0.0:{}", port).parse().unwrap()]); + c + }) + .unwrap(), + ConfigSource::Cli, + ) + .unwrap(); + let instance_id2 = manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::File, + ) + .unwrap(); + let instance_id3 = manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::GUI, + ) + .unwrap(); + let instance_id4 = manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::Web, + ) + .unwrap(); + let instance_id5 = manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::FFI, + ) + .unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; // to make instance actually started + + assert!(!crate::utils::check_tcp_available(port)); + + assert!(manager.instance_map.contains_key(&instance_id1)); + assert!(manager.instance_map.contains_key(&instance_id2)); + assert!(manager.instance_map.contains_key(&instance_id3)); + assert!(manager.instance_map.contains_key(&instance_id4)); + assert!(manager.instance_map.contains_key(&instance_id5)); + assert_eq!(manager.list_network_instance_ids().len(), 5); + assert_eq!(manager.instance_stop_tasks.len(), 4); // FFI instance does not have a stop task + + manager + .delete_network_instance(vec![instance_id3, instance_id4, instance_id5]) + .unwrap(); + assert!(!manager.instance_map.contains_key(&instance_id3)); + assert!(!manager.instance_map.contains_key(&instance_id4)); + assert!(!manager.instance_map.contains_key(&instance_id5)); + assert_eq!(manager.list_network_instance_ids().len(), 2); + } + + #[tokio::test] + async fn test_single_instance_failed() { + let free_tcp_port = + crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found"); + + for config_source in [ConfigSource::Cli, ConfigSource::File] { + let _port_holder = + std::net::TcpListener::bind(format!("0.0.0.0:{}", free_tcp_port)).unwrap(); + + let cfg_str = format!( + r#" + listeners = ["tcp://0.0.0.0:{}"] + "#, + free_tcp_port + ); + + let manager = NetworkInstanceManager::new(); + manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(), + config_source.clone(), + ) + .unwrap(); + + tokio::select! { + _ = manager.wait() => { + assert_eq!(manager.list_network_instance_ids().len(), 0); + } + _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => { + panic!("instance manager with single failed instance({:?}) should not running", config_source); + } + } + } + for config_source in [ConfigSource::Web, ConfigSource::GUI, ConfigSource::FFI] { + let _port_holder = + std::net::TcpListener::bind(format!("0.0.0.0:{}", free_tcp_port)).unwrap(); + + let cfg_str = format!( + r#" + listeners = ["tcp://0.0.0.0:{}"] + "#, + free_tcp_port + ); + + let manager = NetworkInstanceManager::new(); + manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(), + config_source.clone(), + ) + .unwrap(); + + assert_eq!(manager.list_network_instance_ids().len(), 1); + } + } + + #[tokio::test] + async fn test_multiple_instances_one_failed() { + let free_tcp_port = + crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found"); + + let manager = NetworkInstanceManager::new(); + let cfg_str = format!( + r#" + listeners = ["tcp://0.0.0.0:{}"] + [flags] + enable_ipv6 = false + "#, + free_tcp_port + ); + + manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(), + ConfigSource::Cli, + ) + .unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str.as_str()).unwrap(), + ConfigSource::Cli, + ) + .unwrap(); + + tokio::select! { + _ = manager.wait() => { + panic!("instance manager with multiple instances one failed should still running"); + } + _ = tokio::time::sleep(std::time::Duration::from_secs(2)) => { + assert_eq!(manager.list_network_instance_ids().len(), 1); + } + } + } +} diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index bd5ba7262..90e9ac0a2 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -1,6 +1,5 @@ use std::{ collections::VecDeque, - net::SocketAddr, sync::{atomic::AtomicBool, Arc, RwLock}, }; @@ -214,24 +213,18 @@ impl EasyTierLauncher { Ok(()) } - fn check_tcp_available(port: u16) -> bool { - let s = format!("0.0.0.0:{}", port).parse::().unwrap(); - std::net::TcpListener::bind(s).is_ok() - } - fn select_proper_rpc_port(cfg: &TomlConfigLoader) { let Some(mut f) = cfg.get_rpc_portal() else { return; }; if f.port() == 0 { - for i in 15888..15900 { - if Self::check_tcp_available(i) { - f.set_port(i); - cfg.set_rpc_portal(f); - break; - } - } + let Some(port) = crate::utils::find_free_tcp_port(15888..15900) else { + tracing::warn!("No free port found for RPC portal, skipping setting RPC portal"); + return; + }; + f.set_port(port); + cfg.set_rpc_portal(f); } } @@ -343,25 +336,40 @@ impl Drop for EasyTierLauncher { pub type NetworkInstanceRunningInfo = crate::proto::web::NetworkInstanceRunningInfo; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConfigSource { + Cli, + File, + Web, + GUI, + FFI, +} + pub struct NetworkInstance { config: TomlConfigLoader, launcher: Option, - fetch_node_info: bool, + config_source: ConfigSource, } impl NetworkInstance { - pub fn new(config: TomlConfigLoader) -> Self { + pub fn new(config: TomlConfigLoader, source: ConfigSource) -> Self { Self { config, launcher: None, - fetch_node_info: true, + config_source: source, } } - pub fn set_fetch_node_info(mut self, fetch_node_info: bool) -> Self { - self.fetch_node_info = fetch_node_info; - self + fn get_fetch_node_info(&self) -> bool { + match self.config_source { + ConfigSource::Cli | ConfigSource::File => false, + ConfigSource::Web | ConfigSource::GUI | ConfigSource::FFI => true, + } + } + + pub fn get_config_source(&self) -> ConfigSource { + self.config_source.clone() } pub fn is_easytier_running(&self) -> bool { @@ -395,6 +403,10 @@ impl NetworkInstance { }) } + pub fn get_inst_name(&self) -> String { + self.config.get_inst_name() + } + pub fn set_tun_fd(&mut self, tun_fd: i32) { if let Some(launcher) = self.launcher.as_ref() { launcher.data.tun_fd.write().unwrap().replace(tun_fd); @@ -406,7 +418,7 @@ impl NetworkInstance { return Ok(self.subscribe_event().unwrap()); } - let launcher = EasyTierLauncher::new(self.fetch_node_info); + let launcher = EasyTierLauncher::new(self.get_fetch_node_info()); self.launcher = Some(launcher); let ev = self.subscribe_event().unwrap(); @@ -418,7 +430,7 @@ impl NetworkInstance { Ok(ev) } - fn subscribe_event(&self) -> Option> { + pub fn subscribe_event(&self) -> Option> { if let Some(launcher) = self.launcher.as_ref() { Some(launcher.data.event_subscriber.read().unwrap().subscribe()) } else { @@ -426,9 +438,16 @@ impl NetworkInstance { } } - pub async fn wait(&self) -> Option { + pub fn get_stop_notifier(&self) -> Option> { + if let Some(launcher) = self.launcher.as_ref() { + Some(launcher.data.instance_stop_notifier.clone()) + } else { + None + } + } + + pub fn get_latest_error_msg(&self) -> Option { if let Some(launcher) = self.launcher.as_ref() { - launcher.data.instance_stop_notifier.notified().await; launcher.error_msg.read().unwrap().clone() } else { None diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index 23fa00bfc..793cf8fda 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -9,6 +9,7 @@ mod vpn_portal; pub mod common; pub mod connector; pub mod launcher; +pub mod instance_manager; pub mod peers; pub mod proto; pub mod tunnel; diff --git a/easytier/src/utils.rs b/easytier/src/utils.rs index e36365c8c..e705ca015 100644 --- a/easytier/src/utils.rs +++ b/easytier/src/utils.rs @@ -4,7 +4,7 @@ use anyhow::Context; use tracing::level_filters::LevelFilter; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; -use crate::common::{config::ConfigLoader, get_logger_timer_rfc3339}; +use crate::common::{config::LoggingConfigLoader, get_logger_timer_rfc3339}; pub type PeerRoutePair = crate::proto::cli::PeerRoutePair; @@ -23,7 +23,7 @@ pub fn float_to_str(f: f64, precision: usize) -> String { pub type NewFilterSender = std::sync::mpsc::Sender; pub fn init_logger( - config: impl ConfigLoader, + config: impl LoggingConfigLoader, need_reload: bool, ) -> Result, anyhow::Error> { let file_config = config.get_file_logger_config(); @@ -211,6 +211,21 @@ pub fn setup_panic_handler() { })); } +pub fn check_tcp_available(port: u16) -> bool { + use std::net::TcpListener; + let s = std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), port); + TcpListener::bind(s).is_ok() +} + +pub fn find_free_tcp_port(range: std::ops::Range) -> Option { + for port in range { + if check_tcp_available(port) { + return Some(port); + } + } + None +} + #[cfg(test)] mod tests { use crate::common::config::{self}; @@ -219,7 +234,7 @@ mod tests { async fn test_logger_reload() { println!("current working dir: {:?}", std::env::current_dir()); - let config = config::TomlConfigLoader::default(); + let config = config::LoggingConfigBuilder::default().build().unwrap(); let s = init_logger(&config, true).unwrap(); tracing::debug!("test not display debug"); s.unwrap().send(LevelFilter::DEBUG.to_string()).unwrap(); diff --git a/easytier/src/web_client/controller.rs b/easytier/src/web_client/controller.rs index 655dbf99b..45249c79e 100644 --- a/easytier/src/web_client/controller.rs +++ b/easytier/src/web_client/controller.rs @@ -1,11 +1,5 @@ -use std::collections::BTreeMap; - -use dashmap::DashMap; - use crate::{ - common::config::{ConfigLoader, TomlConfigLoader}, - launcher::NetworkInstance, - proto::{ + common::config::ConfigLoader, launcher::ConfigSource, instance_manager::NetworkInstanceManager, proto::{ rpc_types::{self, controller::BaseController}, web::{ CollectNetworkInfoRequest, CollectNetworkInfoResponse, DeleteNetworkInstanceRequest, @@ -14,13 +8,13 @@ use crate::{ RetainNetworkInstanceResponse, RunNetworkInstanceRequest, RunNetworkInstanceResponse, ValidateConfigRequest, ValidateConfigResponse, WebClientService, }, - }, + } }; pub struct Controller { token: String, hostname: String, - instance_map: DashMap, + manager: NetworkInstanceManager, } impl Controller { @@ -28,55 +22,12 @@ impl Controller { Controller { token, hostname, - instance_map: DashMap::new(), - } - } - - pub fn run_network_instance(&self, cfg: TomlConfigLoader) -> Result<(), anyhow::Error> { - let instance_id = cfg.get_id(); - if self.instance_map.contains_key(&instance_id) { - anyhow::bail!("instance {} already exists", instance_id); - } - - let mut instance = NetworkInstance::new(cfg); - instance.start()?; - - println!("instance {} started", instance_id); - self.instance_map.insert(instance_id, instance); - Ok(()) - } - - pub fn retain_network_instance( - &self, - instance_ids: Vec, - ) -> Result { - self.instance_map.retain(|k, _| instance_ids.contains(k)); - let remain = self - .instance_map - .iter() - .map(|item| item.key().clone().into()) - .collect::>(); - println!("instance {:?} retained", remain); - Ok(RetainNetworkInstanceResponse { - remain_inst_ids: remain, - }) - } - - pub fn collect_network_infos(&self) -> Result { - let mut map = BTreeMap::new(); - for instance in self.instance_map.iter() { - if let Some(info) = instance.get_running_info() { - map.insert(instance.key().to_string(), info); - } + manager: NetworkInstanceManager::new(), } - Ok(NetworkInstanceRunningInfoMap { map }) } pub fn list_network_instance_ids(&self) -> Vec { - self.instance_map - .iter() - .map(|item| item.key().clone()) - .collect() + self.manager.list_network_instance_ids() } pub fn token(&self) -> String { @@ -114,7 +65,8 @@ impl WebClientService for Controller { if let Some(inst_id) = req.inst_id { cfg.set_id(inst_id.into()); } - self.run_network_instance(cfg)?; + self.manager.run_network_instance(cfg, ConfigSource::Web)?; + println!("instance {} started", id); Ok(RunNetworkInstanceResponse { inst_id: Some(id.into()), }) @@ -125,7 +77,13 @@ impl WebClientService for Controller { _: BaseController, req: RetainNetworkInstanceRequest, ) -> Result { - Ok(self.retain_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?) + let remain = self + .manager + .retain_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?; + println!("instance {:?} retained", remain); + Ok(RetainNetworkInstanceResponse { + remain_inst_ids: remain.iter().map(|item| (*item).into()).collect(), + }) } async fn collect_network_info( @@ -133,7 +91,14 @@ impl WebClientService for Controller { _: BaseController, req: CollectNetworkInfoRequest, ) -> Result { - let mut ret = self.collect_network_infos()?; + let mut ret = NetworkInstanceRunningInfoMap { + map: self + .manager + .collect_network_infos()? + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + }; let include_inst_ids = req .inst_ids .iter() @@ -163,6 +128,7 @@ impl WebClientService for Controller { ) -> Result { Ok(ListNetworkInstanceResponse { inst_ids: self + .manager .list_network_instance_ids() .into_iter() .map(Into::into) @@ -176,11 +142,12 @@ impl WebClientService for Controller { _: BaseController, req: DeleteNetworkInstanceRequest, ) -> Result { - let mut inst_ids = self.list_network_instance_ids(); - inst_ids.retain(|id| !req.inst_ids.contains(&(id.clone().into()))); - self.retain_network_instance(inst_ids.clone())?; + let remain_inst_ids = self + .manager + .delete_network_instance(req.inst_ids.into_iter().map(Into::into).collect())?; + println!("instance {:?} retained", remain_inst_ids); Ok(DeleteNetworkInstanceResponse { - remain_inst_ids: inst_ids.into_iter().map(Into::into).collect(), + remain_inst_ids: remain_inst_ids.into_iter().map(Into::into).collect(), }) } } From c07d1286ef6cf058d43837e25ff0db3490c626b5 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Thu, 12 Jun 2025 08:09:59 +0800 Subject: [PATCH 013/165] internal stun server should use xor mapped addr (#975) --- easytier/src/tests/three_node.rs | 68 ++++++++++++++++++++++++++++++++ easytier/src/tunnel/udp.rs | 6 ++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index e37b5d458..dc8a1109a 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -839,6 +839,74 @@ pub async fn socks5_vpn_portal(#[values("10.144.144.1", "10.144.144.3")] dst_add tokio::join!(task).0.unwrap(); } +#[tokio::test] +#[serial_test::serial] +pub async fn foreign_network_functional_cluster() { + crate::set_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, 1); + prepare_linux_namespaces(); + + let center_node_config1 = get_inst_config("inst1", Some("net_a"), "10.144.144.1"); + center_node_config1 + .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); + let mut center_inst1 = Instance::new(center_node_config1); + + let center_node_config2 = get_inst_config("inst2", Some("net_b"), "10.144.144.2"); + center_node_config2 + .set_network_identity(NetworkIdentity::new("center".to_string(), "".to_string())); + let mut center_inst2 = Instance::new(center_node_config2); + + let inst1_config = get_inst_config("inst1", Some("net_c"), "10.144.145.1"); + inst1_config.set_listeners(vec![]); + let mut inst1 = Instance::new(inst1_config); + + let mut inst2 = Instance::new(get_inst_config("inst2", Some("net_d"), "10.144.145.2")); + + center_inst1.run().await.unwrap(); + center_inst2.run().await.unwrap(); + inst1.run().await.unwrap(); + inst2.run().await.unwrap(); + + center_inst1 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst2.id()).parse().unwrap(), + )); + + inst1 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst1.id()).parse().unwrap(), + )); + + inst2 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst2.id()).parse().unwrap(), + )); + + let peer_map_inst1 = inst1.get_peer_manager(); + println!("inst1 peer map: {:?}", peer_map_inst1.list_routes().await); + + wait_for_condition( + || async { ping_test("net_c", "10.144.145.2", None).await }, + Duration::from_secs(5), + ) + .await; + + // connect to two centers, ping should work + inst1 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", center_inst2.id()).parse().unwrap(), + )); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + wait_for_condition( + || async { ping_test("net_c", "10.144.145.2", None).await }, + Duration::from_secs(5), + ) + .await; +} + #[rstest::rstest] #[tokio::test] #[serial_test::serial] diff --git a/easytier/src/tunnel/udp.rs b/easytier/src/tunnel/udp.rs index 8496e6e43..422c41410 100644 --- a/easytier/src/tunnel/udp.rs +++ b/easytier/src/tunnel/udp.rs @@ -151,7 +151,7 @@ async fn respond_stun_packet( use crate::common::stun_codec_ext::*; use bytecodec::DecodeExt as _; use bytecodec::EncodeExt as _; - use stun_codec::rfc5389::attributes::MappedAddress; + use stun_codec::rfc5389::attributes::XorMappedAddress; use stun_codec::rfc5389::methods::BINDING; use stun_codec::{Message, MessageClass, MessageDecoder, MessageEncoder}; @@ -173,7 +173,9 @@ async fn respond_stun_packet( // we discard the prefix, make sure our implementation is not compatible with other stun client u32_to_tid(tid_to_u32(&tid)), ); - resp_msg.add_attribute(Attribute::MappedAddress(MappedAddress::new(addr.clone()))); + resp_msg.add_attribute(Attribute::XorMappedAddress(XorMappedAddress::new( + addr.clone(), + ))); let mut encoder = MessageEncoder::new(); let rsp_buf = encoder From 950cb04534a7fd71c292db7d0606aa0d47417bcc Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Thu, 12 Jun 2025 22:24:34 +0800 Subject: [PATCH 014/165] remove macos default route on utun device (#976) --- easytier/src/instance/virtual_nic.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index b11342d40..8ee2dcf77 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -658,6 +658,15 @@ impl NicCtx { let _ = RegistryManager::reg_change_catrgory_in_profile(&dev_name); } + #[cfg(any(target_os = "macos", target_os = "freebsd"))] + { + // remove the 10.0.0.0/24 route (which is added by rust-tun by default) + let _ = nic + .ifcfg + .remove_ipv4_route(&nic.ifname(), "10.0.0.0".parse().unwrap(), 24) + .await; + } + self.global_ctx .issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string())); ret From 25dcdc652aa8baf5b52561ff205796506bc3b11a Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 14 Jun 2025 11:42:45 +0800 Subject: [PATCH 015/165] support mapping subnet proxy (#978) - **support mapping subproxy network cidr** - **add command line option for proxy network mapping** - **fix Instance leak in tests. --- Cargo.lock | 29 ++--- easytier/Cargo.toml | 2 +- easytier/locales/app.yml | 10 +- easytier/src/common/config.rs | 37 +++--- easytier/src/common/global_ctx.rs | 26 +---- easytier/src/common/stun.rs | 15 ++- easytier/src/connector/manual.rs | 50 +++++---- easytier/src/easytier-core.rs | 9 +- easytier/src/gateway/icmp_proxy.rs | 96 ++++++++++++---- easytier/src/gateway/kcp_proxy.rs | 35 ++++-- easytier/src/gateway/mod.rs | 35 +++++- easytier/src/gateway/socks5.rs | 5 +- easytier/src/gateway/tcp_proxy.rs | 55 +++++---- easytier/src/gateway/udp_proxy.rs | 15 ++- easytier/src/instance/instance.rs | 105 +++++++++++++----- easytier/src/instance/listeners.rs | 17 ++- easytier/src/launcher.rs | 42 +++++-- easytier/src/peer_center/instance.rs | 31 ++++-- easytier/src/peer_center/mod.rs | 2 + easytier/src/peers/peer_manager.rs | 18 ++- easytier/src/peers/peer_ospf_route.rs | 4 +- .../src/proto/rpc_impl/service_registry.rs | 4 + easytier/src/tests/three_node.rs | 95 +++++++++++++--- 23 files changed, 521 insertions(+), 216 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78ff92010..fe3eecba3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4118,7 +4118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -6092,9 +6092,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -6513,24 +6513,25 @@ dependencies = [ [[package]] name = "rstest" -version = "0.18.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" dependencies = [ - "futures", "futures-timer", + "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.18.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" dependencies = [ "cfg-if", "glob", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "regex", @@ -6658,9 +6659,9 @@ checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] @@ -8542,9 +8543,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -9117,9 +9118,9 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 522732f85..dddb50b4d 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -270,7 +270,7 @@ thunk-rs = { git = "https://github.com/easytier/thunk.git", default-features = f [dev-dependencies] serial_test = "3.0.0" -rstest = "0.18.2" +rstest = "0.25.0" futures-util = "0.3.30" maplit = "1.0.2" diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index e5377c3d9..a3e985eee 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -32,8 +32,14 @@ core_clap: en: "use a public shared node to discover peers" zh-CN: "使用公共共享节点来发现对等节点" proxy_networks: - en: "export local networks to other peers in the vpn" - zh-CN: "将本地网络导出到VPN中的其他对等节点" + en: |+ + export local networks to other peers in the vpn, e.g.: 10.0.0.0/24. + also support mapping proxy network to other cidr, e.g.: 10.0.0.0/24->192.168.0.0/24 + other peers can access 10.0.0.1 with ip 192.168.0.1 + zh-CN: |+ + 将本地网络导出到VPN中的其他对等节点,例如:10.0.0.0/24。 + 还支持将代理网络映射到其他CIDR,例如:10.0.0.0/24->192.168.0.0/24 + 其他对等节点可以通过 IP 192.168.0.1 来访问 10.0.0.1 rpc_portal: en: "rpc portal address to listen for management. 0 means random port, 12345 means listen on 12345 of localhost, 0.0.0.0:12345 means listen on 12345 of all interfaces. default is 0 and will try 15888 first" zh-CN: "用于管理的RPC门户地址。0表示随机端口,12345表示在localhost的12345上监听,0.0.0.0:12345表示在所有接口的12345上监听。默认是0,首先尝试15888" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 34004309e..fee9f0e19 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -62,9 +62,9 @@ pub trait ConfigLoader: Send + Sync { fn get_dhcp(&self) -> bool; fn set_dhcp(&self, dhcp: bool); - fn add_proxy_cidr(&self, cidr: cidr::IpCidr); - fn remove_proxy_cidr(&self, cidr: cidr::IpCidr); - fn get_proxy_cidrs(&self) -> Vec; + fn add_proxy_cidr(&self, cidr: cidr::Ipv4Cidr, mapped_cidr: Option); + fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr); + fn get_proxy_cidrs(&self) -> Vec; fn get_network_identity(&self) -> NetworkIdentity; fn set_network_identity(&self, identity: NetworkIdentity); @@ -171,7 +171,8 @@ pub struct PeerConfig { #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct ProxyNetworkConfig { - pub cidr: String, + pub cidr: cidr::Ipv4Cidr, // the CIDR of the proxy network + pub mapped_cidr: Option, // allow remap the proxy CIDR to another CIDR pub allow: Option>, } @@ -418,50 +419,52 @@ impl ConfigLoader for TomlConfigLoader { self.config.lock().unwrap().dhcp = Some(dhcp); } - fn add_proxy_cidr(&self, cidr: cidr::IpCidr) { + fn add_proxy_cidr(&self, cidr: cidr::Ipv4Cidr, mapped_cidr: Option) { let mut locked_config = self.config.lock().unwrap(); if locked_config.proxy_network.is_none() { locked_config.proxy_network = Some(vec![]); } - let cidr_str = cidr.to_string(); + if let Some(mapped_cidr) = mapped_cidr.as_ref() { + assert_eq!( + cidr.network_length(), + mapped_cidr.network_length(), + "Mapped CIDR must have the same network length as the original CIDR", + ); + } // insert if no duplicate if !locked_config .proxy_network .as_ref() .unwrap() .iter() - .any(|c| c.cidr == cidr_str) + .any(|c| c.cidr == cidr) { locked_config .proxy_network .as_mut() .unwrap() .push(ProxyNetworkConfig { - cidr: cidr_str, + cidr, + mapped_cidr, allow: None, }); } } - fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) { + fn remove_proxy_cidr(&self, cidr: cidr::Ipv4Cidr) { let mut locked_config = self.config.lock().unwrap(); if let Some(proxy_cidrs) = &mut locked_config.proxy_network { - let cidr_str = cidr.to_string(); - proxy_cidrs.retain(|c| c.cidr != cidr_str); + proxy_cidrs.retain(|c| c.cidr != cidr); } } - fn get_proxy_cidrs(&self) -> Vec { + fn get_proxy_cidrs(&self) -> Vec { self.config .lock() .unwrap() .proxy_network .as_ref() - .map(|v| { - v.iter() - .map(|c| c.cidr.parse().unwrap()) - .collect::>() - }) + .cloned() .unwrap_or_default() } diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index baf8e939e..556d06831 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -4,6 +4,7 @@ use std::{ sync::{Arc, Mutex}, }; +use crate::common::config::ProxyNetworkConfig; use crate::proto::cli::PeerConnInfo; use crate::proto::common::{PeerFeatureFlag, PortForwardConfigPb}; use crossbeam::atomic::AtomicCell; @@ -59,7 +60,7 @@ pub struct GlobalCtx { event_bus: EventBus, cached_ipv4: AtomicCell>, - cached_proxy_cidrs: AtomicCell>>, + cached_proxy_cidrs: AtomicCell>>, ip_collector: Mutex>>, @@ -182,29 +183,6 @@ impl GlobalCtx { self.cached_ipv4.store(None); } - pub fn add_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { - self.config.add_proxy_cidr(cidr); - self.cached_proxy_cidrs.store(None); - Ok(()) - } - - pub fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { - self.config.remove_proxy_cidr(cidr); - self.cached_proxy_cidrs.store(None); - Ok(()) - } - - pub fn get_proxy_cidrs(&self) -> Vec { - if let Some(proxy_cidrs) = self.cached_proxy_cidrs.take() { - self.cached_proxy_cidrs.store(Some(proxy_cidrs.clone())); - return proxy_cidrs; - } - - let ret = self.config.get_proxy_cidrs(); - self.cached_proxy_cidrs.store(Some(ret.clone())); - ret - } - pub fn get_id(&self) -> uuid::Uuid { self.config.get_id() } diff --git a/easytier/src/common/stun.rs b/easytier/src/common/stun.rs index a250ae0a6..f390c540f 100644 --- a/easytier/src/common/stun.rs +++ b/easytier/src/common/stun.rs @@ -955,9 +955,18 @@ mod tests { async fn test_txt_public_stun_server() { let stun_servers = vec!["txt:stun.easytier.cn".to_string()]; let detector = UdpNatTypeDetector::new(stun_servers, 1); - let ret = detector.detect_nat_type(0).await; - println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type()); - assert!(!ret.unwrap().stun_resps.is_empty()); + for _ in 0..5 { + let ret = detector.detect_nat_type(0).await; + println!("{:#?}, {:?}", ret, ret.as_ref().unwrap().nat_type()); + if ret.is_ok() { + assert!(!ret.unwrap().stun_resps.is_empty()); + return; + } + } + debug_assert!( + false, + "should not reach here, stun server should be available" + ); } #[tokio::test] diff --git a/easytier/src/connector/manual.rs b/easytier/src/connector/manual.rs index edd737d7a..1ece6f48d 100644 --- a/easytier/src/connector/manual.rs +++ b/easytier/src/connector/manual.rs @@ -1,4 +1,7 @@ -use std::{collections::BTreeSet, sync::Arc}; +use std::{ + collections::BTreeSet, + sync::{Arc, Weak}, +}; use anyhow::Context; use dashmap::{DashMap, DashSet}; @@ -12,7 +15,7 @@ use tokio::{ }; use crate::{ - common::PeerId, + common::{join_joinset_background, PeerId}, peers::peer_conn::PeerConnId, proto::{ cli::{ @@ -53,7 +56,7 @@ struct ReconnResult { struct ConnectorManagerData { connectors: ConnectorMap, reconnecting: DashSet, - peer_manager: Arc, + peer_manager: Weak, alive_conn_urls: Arc>, // user removed connector urls removed_conn_urls: Arc>, @@ -78,7 +81,7 @@ impl ManualConnectorManager { data: Arc::new(ConnectorManagerData { connectors, reconnecting: DashSet::new(), - peer_manager, + peer_manager: Arc::downgrade(&peer_manager), alive_conn_urls: Arc::new(DashSet::new()), removed_conn_urls: Arc::new(DashSet::new()), net_ns: global_ctx.net_ns.clone(), @@ -190,20 +193,18 @@ impl ManualConnectorManager { tracing::warn!("event_recv lagged: {}, rebuild alive conn list", n); event_recv = event_recv.resubscribe(); data.alive_conn_urls.clear(); - for x in data - .peer_manager - .get_peer_map() - .get_alive_conns() - .iter() - .map(|x| { - x.tunnel - .clone() - .unwrap_or_default() - .remote_addr - .unwrap_or_default() - .to_string() - }) - { + let Some(pm) = data.peer_manager.upgrade() else { + tracing::warn!("peer manager is gone, exit"); + break; + }; + for x in pm.get_peer_map().get_alive_conns().iter().map(|x| { + x.tunnel + .clone() + .unwrap_or_default() + .remote_addr + .unwrap_or_default() + .to_string() + }) { data.alive_conn_urls.insert(x); } continue; @@ -222,6 +223,8 @@ impl ManualConnectorManager { use_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS), )); let (reconn_result_send, mut reconn_result_recv) = mpsc::channel(100); + let tasks = Arc::new(std::sync::Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "connector_reconnect_tasks".to_string()); loop { tokio::select! { @@ -237,7 +240,7 @@ impl ManualConnectorManager { let insert_succ = data.reconnecting.insert(dead_url.clone()); assert!(insert_succ); - tokio::spawn(async move { + tasks.lock().unwrap().spawn(async move { let reconn_ret = Self::conn_reconnect(data_clone.clone(), dead_url.clone(), connector.clone()).await; sender.send(reconn_ret).await.unwrap(); @@ -340,8 +343,13 @@ impl ManualConnectorManager { connector.lock().await.remote_url().clone(), )); tracing::info!("reconnect try connect... conn: {:?}", connector); - let (peer_id, conn_id) = data - .peer_manager + let Some(pm) = data.peer_manager.upgrade() else { + return Err(Error::AnyhowError(anyhow::anyhow!( + "peer manager is gone, cannot reconnect" + ))); + }; + + let (peer_id, conn_id) = pm .try_direct_connect(connector.lock().await.as_mut()) .await?; tracing::info!("reconnect succ: {} {} {}", peer_id, conn_id, dead_url); diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index e733a32b6..518412679 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -25,8 +25,8 @@ use easytier::{ stun::MockStunInfoCollector, }, connector::create_connector_by_url, - launcher::ConfigSource, instance_manager::NetworkInstanceManager, + launcher::{add_proxy_network_to_config, ConfigSource}, proto::common::{CompressionAlgoPb, NatType}, tunnel::{IpVersion, PROTO_PORT_OFFSET}, utils::{init_logger, setup_panic_handler}, @@ -540,7 +540,7 @@ impl Cli { impl NetworkOptions { fn can_merge(&self, cfg: &TomlConfigLoader, config_file_count: usize) -> bool { - if config_file_count == 1{ + if config_file_count == 1 { return true; } let Some(network_name) = &self.network_name else { @@ -624,10 +624,7 @@ impl NetworkOptions { } for n in self.proxy_networks.iter() { - cfg.add_proxy_cidr( - n.parse() - .with_context(|| format!("failed to parse proxy network: {}", n))?, - ); + add_proxy_network_to_config(n, &cfg)?; } let rpc_portal = if let Some(r) = &self.rpc_portal { diff --git a/easytier/src/gateway/icmp_proxy.rs b/easytier/src/gateway/icmp_proxy.rs index 55156e293..efde1de3d 100644 --- a/easytier/src/gateway/icmp_proxy.rs +++ b/easytier/src/gateway/icmp_proxy.rs @@ -1,7 +1,7 @@ use std::{ mem::MaybeUninit, net::{IpAddr, Ipv4Addr, SocketAddrV4}, - sync::Arc, + sync::{Arc, Weak}, thread, time::Duration, }; @@ -34,7 +34,7 @@ use super::{ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct IcmpNatKey { - dst_ip: std::net::IpAddr, + real_dst_ip: std::net::IpAddr, icmp_id: u16, icmp_seq: u16, } @@ -45,15 +45,22 @@ struct IcmpNatEntry { my_peer_id: PeerId, src_ip: IpAddr, start_time: std::time::Instant, + mapped_dst_ip: std::net::Ipv4Addr, } impl IcmpNatEntry { - fn new(src_peer_id: PeerId, my_peer_id: PeerId, src_ip: IpAddr) -> Result { + fn new( + src_peer_id: PeerId, + my_peer_id: PeerId, + src_ip: IpAddr, + mapped_dst_ip: Ipv4Addr, + ) -> Result { Ok(Self { src_peer_id, my_peer_id, src_ip, start_time: std::time::Instant::now(), + mapped_dst_ip, }) } } @@ -65,10 +72,10 @@ type NewPacketReceiver = tokio::sync::mpsc::UnboundedReceiver; #[derive(Debug)] pub struct IcmpProxy { global_ctx: ArcGlobalCtx, - peer_manager: Arc, + peer_manager: Weak, cidr_set: CidrSet, - socket: std::sync::Mutex>, + socket: std::sync::Mutex>>, nat_table: IcmpNatTable, @@ -78,7 +85,10 @@ pub struct IcmpProxy { icmp_sender: Arc>>>, } -fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit]) -> Result<(usize, IpAddr), Error> { +fn socket_recv( + socket: &Socket, + buf: &mut [MaybeUninit], +) -> Result<(usize, IpAddr), std::io::Error> { let (size, addr) = socket.recv_from(buf)?; let addr = match addr.as_socket() { None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), @@ -87,15 +97,32 @@ fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit]) -> Result<(usize, I Ok((size, addr)) } -fn socket_recv_loop(socket: Socket, nat_table: IcmpNatTable, sender: UnboundedSender) { +fn socket_recv_loop( + socket: Arc, + nat_table: IcmpNatTable, + sender: UnboundedSender, +) { let mut buf = [0u8; 8192]; let data: &mut [MaybeUninit] = unsafe { std::mem::transmute(&mut buf[..]) }; loop { - let Ok((len, peer_ip)) = socket_recv(&socket, data) else { - continue; + let (len, peer_ip) = match socket_recv(&socket, data) { + Ok((len, peer_ip)) => (len, peer_ip), + Err(e) => { + tracing::error!("recv icmp packet failed: {:?}", e); + if sender.is_closed() { + break; + } else { + continue; + } + } }; + if len <= 0 { + tracing::error!("recv empty packet, len: {}", len); + return; + } + if !peer_ip.is_ipv4() { continue; } @@ -114,7 +141,7 @@ fn socket_recv_loop(socket: Socket, nat_table: IcmpNatTable, sender: UnboundedSe } let key = IcmpNatKey { - dst_ip: peer_ip, + real_dst_ip: peer_ip, icmp_id: icmp_packet.get_identifier(), icmp_seq: icmp_packet.get_sequence_number(), }; @@ -128,12 +155,11 @@ fn socket_recv_loop(socket: Socket, nat_table: IcmpNatTable, sender: UnboundedSe continue; }; - let src_v4 = ipv4_packet.get_source(); let payload_len = len - ipv4_packet.get_header_length() as usize * 4; let id = ipv4_packet.get_identification(); let _ = compose_ipv4_packet( &mut buf[..], - &src_v4, + &v.mapped_dst_ip, &dest_ip, IpNextHeaderProtocols::Icmp, payload_len, @@ -176,7 +202,7 @@ impl IcmpProxy { let cidr_set = CidrSet::new(global_ctx.clone()); let ret = Self { global_ctx, - peer_manager, + peer_manager: Arc::downgrade(&peer_manager), cidr_set, socket: std::sync::Mutex::new(None), @@ -208,7 +234,7 @@ impl IcmpProxy { let socket = self.create_raw_socket(); match socket { Ok(socket) => { - self.socket.lock().unwrap().replace(socket); + self.socket.lock().unwrap().replace(Arc::new(socket)); } Err(e) => { tracing::warn!("create icmp socket failed: {:?}", e); @@ -241,7 +267,7 @@ impl IcmpProxy { let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); self.icmp_sender.lock().unwrap().replace(sender.clone()); if let Some(socket) = self.socket.lock().unwrap().as_ref() { - let socket = socket.try_clone()?; + let socket = socket.clone(); let nat_table = self.nat_table.clone(); thread::spawn(|| { socket_recv_loop(socket, nat_table, sender); @@ -254,7 +280,11 @@ impl IcmpProxy { while let Some(msg) = receiver.recv().await { let hdr = msg.peer_manager_header().unwrap(); let to_peer_id = hdr.to_peer_id.into(); - let ret = peer_manager.send_msg(msg, to_peer_id).await; + let Some(pm) = peer_manager.upgrade() else { + tracing::warn!("peer manager is gone, icmp proxy send loop exit"); + return; + }; + let ret = pm.send_msg(msg, to_peer_id).await; if ret.is_err() { tracing::error!("send icmp packet to peer failed: {:?}", ret); } @@ -271,9 +301,12 @@ impl IcmpProxy { } }); - self.peer_manager - .add_packet_process_pipeline(Box::new(self.clone())) - .await; + let Some(pm) = self.peer_manager.upgrade() else { + tracing::warn!("peer manager is gone, icmp proxy init failed"); + return Err(anyhow::anyhow!("peer manager is gone").into()); + }; + + pm.add_packet_process_pipeline(Box::new(self.clone())).await; Ok(()) } @@ -361,7 +394,11 @@ impl IcmpProxy { return None; } - if !self.cidr_set.contains_v4(ipv4.get_destination()) + let mut real_dst_ip = ipv4.get_destination(); + + if !self + .cidr_set + .contains_v4(ipv4.get_destination(), &mut real_dst_ip) && !is_exit_node && !(self.global_ctx.no_tun() && Some(ipv4.get_destination()) @@ -416,7 +453,7 @@ impl IcmpProxy { let icmp_seq = icmp_packet.get_sequence_number(); let key = IcmpNatKey { - dst_ip: ipv4.get_destination().into(), + real_dst_ip: real_dst_ip.into(), icmp_id, icmp_seq, }; @@ -425,6 +462,7 @@ impl IcmpProxy { hdr.from_peer_id.into(), hdr.to_peer_id.into(), ipv4.get_source().into(), + ipv4.get_destination(), ) .ok()?; @@ -432,10 +470,24 @@ impl IcmpProxy { tracing::info!("icmp nat table entry replaced: {:?}", old); } - if let Err(e) = self.send_icmp_packet(ipv4.get_destination(), &icmp_packet) { + if let Err(e) = self.send_icmp_packet(real_dst_ip, &icmp_packet) { tracing::error!("send icmp packet failed: {:?}", e); } Some(()) } } + +impl Drop for IcmpProxy { + fn drop(&mut self) { + tracing::info!( + "dropping icmp proxy, {:?}", + self.socket.lock().unwrap().as_ref() + ); + self.socket.lock().unwrap().as_ref().and_then(|s| { + tracing::info!("shutting down icmp socket"); + let _ = s.shutdown(std::net::Shutdown::Both); + Some(()) + }); + } +} diff --git a/easytier/src/gateway/kcp_proxy.rs b/easytier/src/gateway/kcp_proxy.rs index 163b63644..9d73f6aee 100644 --- a/easytier/src/gateway/kcp_proxy.rs +++ b/easytier/src/gateway/kcp_proxy.rs @@ -107,7 +107,7 @@ async fn handle_kcp_output( #[derive(Debug, Clone)] pub struct NatDstKcpConnector { pub(crate) kcp_endpoint: Arc, - pub(crate) peer_mgr: Arc, + pub(crate) peer_mgr: Weak, } #[async_trait::async_trait] @@ -120,10 +120,14 @@ impl NatDstConnector for NatDstKcpConnector { dst: Some(nat_dst.into()), }; + let Some(peer_mgr) = self.peer_mgr.upgrade() else { + return Err(anyhow::anyhow!("peer manager is not available").into()); + }; + let (dst_peers, _) = match nat_dst { SocketAddr::V4(addr) => { let ip = addr.ip(); - self.peer_mgr.get_msg_dst_peer(&ip).await + peer_mgr.get_msg_dst_peer(&ip).await } SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()), }; @@ -162,7 +166,7 @@ impl NatDstConnector for NatDstKcpConnector { retry_remain -= 1; let kcp_endpoint = self.kcp_endpoint.clone(); - let peer_mgr = self.peer_mgr.clone(); + let my_peer_id = peer_mgr.my_peer_id(); let dst_peer = dst_peers[0]; let conn_data_clone = conn_data.clone(); @@ -170,7 +174,7 @@ impl NatDstConnector for NatDstKcpConnector { kcp_endpoint .connect( Duration::from_secs(10), - peer_mgr.my_peer_id(), + my_peer_id, dst_peer, Bytes::from(conn_data_clone.encode_to_vec()), ) @@ -194,6 +198,7 @@ impl NatDstConnector for NatDstKcpConnector { _global_ctx: &GlobalCtx, hdr: &PeerManagerHeader, _ipv4: &Ipv4Packet, + _real_dst_ip: &mut Ipv4Addr, ) -> bool { return hdr.from_peer_id == hdr.to_peer_id; } @@ -301,7 +306,7 @@ impl KcpProxySrc { peer_manager.clone(), NatDstKcpConnector { kcp_endpoint: kcp_endpoint.clone(), - peer_mgr: peer_manager.clone(), + peer_mgr: Arc::downgrade(&peer_manager), }, ); @@ -342,6 +347,7 @@ pub struct KcpProxyDst { kcp_endpoint: Arc, peer_manager: Arc, proxy_entries: Arc>, + cidr_set: Arc, tasks: JoinSet<()>, } @@ -357,11 +363,12 @@ impl KcpProxyDst { output_receiver, false, )); - + let cidr_set = CidrSet::new(peer_manager.get_global_ctx()); Self { kcp_endpoint: Arc::new(kcp_endpoint), peer_manager, proxy_entries: Arc::new(DashMap::new()), + cidr_set: Arc::new(cidr_set), tasks, } } @@ -371,6 +378,7 @@ impl KcpProxyDst { mut kcp_stream: KcpStream, global_ctx: ArcGlobalCtx, proxy_entries: Arc>, + cidr_set: Arc, ) -> Result<()> { let mut conn_data = kcp_stream.conn_data().clone(); let parsed_conn_data = KcpConnData::decode(&mut conn_data) @@ -383,6 +391,16 @@ impl KcpProxyDst { ))? .into(); + match dst_socket.ip() { + IpAddr::V4(dst_v4_ip) => { + let mut real_ip = dst_v4_ip; + if cidr_set.contains_v4(dst_v4_ip, &mut real_ip) { + dst_socket.set_ip(real_ip.into()); + } + } + _ => {} + }; + let conn_id = kcp_stream.conn_id(); proxy_entries.insert( conn_id, @@ -424,6 +442,7 @@ impl KcpProxyDst { let kcp_endpoint = self.kcp_endpoint.clone(); let global_ctx = self.peer_manager.get_global_ctx().clone(); let proxy_entries = self.proxy_entries.clone(); + let cidr_set = self.cidr_set.clone(); self.tasks.spawn(async move { while let Ok(conn) = kcp_endpoint.accept().await { let stream = KcpStream::new(&kcp_endpoint, conn) @@ -432,8 +451,10 @@ impl KcpProxyDst { let global_ctx = global_ctx.clone(); let proxy_entries = proxy_entries.clone(); + let cidr_set = cidr_set.clone(); tokio::spawn(async move { - let _ = Self::handle_one_in_stream(stream, global_ctx, proxy_entries).await; + let _ = Self::handle_one_in_stream(stream, global_ctx, proxy_entries, cidr_set) + .await; }); } }); diff --git a/easytier/src/gateway/mod.rs b/easytier/src/gateway/mod.rs index 030b5b399..a3dfc7bab 100644 --- a/easytier/src/gateway/mod.rs +++ b/easytier/src/gateway/mod.rs @@ -1,3 +1,4 @@ +use dashmap::DashMap; use std::sync::{Arc, Mutex}; use tokio::task::JoinSet; @@ -20,8 +21,10 @@ pub mod kcp_proxy; #[derive(Debug)] pub(crate) struct CidrSet { global_ctx: ArcGlobalCtx, - cidr_set: Arc>>, + cidr_set: Arc>>, tasks: JoinSet<()>, + + mapped_to_real: Arc>, } impl CidrSet { @@ -30,6 +33,8 @@ impl CidrSet { global_ctx, cidr_set: Arc::new(Mutex::new(vec![])), tasks: JoinSet::new(), + + mapped_to_real: Arc::new(DashMap::new()), }; ret.run_cidr_updater(); ret @@ -38,15 +43,23 @@ impl CidrSet { fn run_cidr_updater(&mut self) { let global_ctx = self.global_ctx.clone(); let cidr_set = self.cidr_set.clone(); + let mapped_to_real = self.mapped_to_real.clone(); self.tasks.spawn(async move { let mut last_cidrs = vec![]; loop { - let cidrs = global_ctx.get_proxy_cidrs(); + let cidrs = global_ctx.config.get_proxy_cidrs(); if cidrs != last_cidrs { last_cidrs = cidrs.clone(); + mapped_to_real.clear(); cidr_set.lock().unwrap().clear(); for cidr in cidrs.iter() { - cidr_set.lock().unwrap().push(cidr.clone()); + let real_cidr = cidr.cidr; + let mapped = cidr.mapped_cidr.unwrap_or(real_cidr.clone()); + cidr_set.lock().unwrap().push(mapped.clone()); + + if mapped != real_cidr { + mapped_to_real.insert(mapped.clone(), real_cidr.clone()); + } } } tokio::time::sleep(std::time::Duration::from_secs(1)).await; @@ -54,11 +67,23 @@ impl CidrSet { }); } - pub fn contains_v4(&self, ip: std::net::Ipv4Addr) -> bool { - let ip = ip.into(); + pub fn contains_v4(&self, ipv4: std::net::Ipv4Addr, real_ip: &mut std::net::Ipv4Addr) -> bool { + let ip = ipv4.into(); let s = self.cidr_set.lock().unwrap(); for cidr in s.iter() { if cidr.contains(&ip) { + if let Some(real_cidr) = self.mapped_to_real.get(&cidr).map(|v| v.value().clone()) { + let origin_network_bits = real_cidr.first().address().to_bits(); + let network_mask = cidr.mask().to_bits(); + + let mut converted_ip = ipv4.to_bits(); + converted_ip &= !network_mask; + converted_ip |= origin_network_bits; + + *real_ip = std::net::Ipv4Addr::from(converted_ip); + } else { + *real_ip = ipv4; + } return true; } } diff --git a/easytier/src/gateway/socks5.rs b/easytier/src/gateway/socks5.rs index 0d8c99ccb..5c39583f3 100644 --- a/easytier/src/gateway/socks5.rs +++ b/easytier/src/gateway/socks5.rs @@ -237,12 +237,9 @@ impl AsyncTcpConnector for Socks5KcpConnector { let Some(kcp_endpoint) = self.kcp_endpoint.upgrade() else { return Err(anyhow::anyhow!("kcp endpoint is not ready").into()); }; - let Some(peer_mgr) = self.peer_mgr.upgrade() else { - return Err(anyhow::anyhow!("peer mgr is not ready").into()); - }; let c = NatDstKcpConnector { kcp_endpoint, - peer_mgr, + peer_mgr: self.peer_mgr.clone(), }; println!("connect to kcp endpoint, addr = {:?}", addr); let ret = c diff --git a/easytier/src/gateway/tcp_proxy.rs b/easytier/src/gateway/tcp_proxy.rs index 520318e67..ab10c8319 100644 --- a/easytier/src/gateway/tcp_proxy.rs +++ b/easytier/src/gateway/tcp_proxy.rs @@ -52,6 +52,7 @@ pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static { global_ctx: &GlobalCtx, hdr: &PeerManagerHeader, ipv4: &Ipv4Packet, + real_dst_ip: &mut Ipv4Addr, ) -> bool; fn transport_type(&self) -> TcpProxyEntryTransportType; } @@ -99,10 +100,11 @@ impl NatDstConnector for NatDstTcpConnector { global_ctx: &GlobalCtx, hdr: &PeerManagerHeader, ipv4: &Ipv4Packet, + real_dst_ip: &mut Ipv4Addr, ) -> bool { let is_exit_node = hdr.is_exit_node(); - if !cidr_set.contains_v4(ipv4.get_destination()) + if !cidr_set.contains_v4(ipv4.get_destination(), real_dst_ip) && !is_exit_node && !(global_ctx.no_tun() && Some(ipv4.get_destination()) @@ -125,7 +127,8 @@ type NatDstEntryState = TcpProxyEntryState; pub struct NatDstEntry { id: uuid::Uuid, src: SocketAddr, - dst: SocketAddr, + real_dst: SocketAddr, + mapped_dst: SocketAddr, start_time: Instant, start_time_local: chrono::DateTime, tasks: Mutex>, @@ -133,11 +136,12 @@ pub struct NatDstEntry { } impl NatDstEntry { - pub fn new(src: SocketAddr, dst: SocketAddr) -> Self { + pub fn new(src: SocketAddr, real_dst: SocketAddr, mapped_dst: SocketAddr) -> Self { Self { id: uuid::Uuid::new_v4(), src, - dst, + real_dst, + mapped_dst, start_time: Instant::now(), start_time_local: chrono::Local::now(), tasks: Mutex::new(JoinSet::new()), @@ -148,7 +152,7 @@ impl NatDstEntry { fn into_pb(&self, transport_type: TcpProxyEntryTransportType) -> TcpProxyEntry { TcpProxyEntry { src: Some(self.src.clone().into()), - dst: Some(self.dst.clone().into()), + dst: Some(self.real_dst.clone().into()), start_time: self.start_time_local.timestamp() as u64, state: self.state.load().into(), transport_type: transport_type.into(), @@ -396,7 +400,7 @@ impl NicPacketFilter for TcpProxy { drop(entry); assert_eq!(nat_entry.src, dst_addr); - let IpAddr::V4(ip) = nat_entry.dst.ip() else { + let IpAddr::V4(ip) = nat_entry.mapped_dst.ip() else { panic!("v4 nat entry src ip is not v4"); }; @@ -416,7 +420,7 @@ impl NicPacketFilter for TcpProxy { let dst = ip_packet.get_destination(); let mut tcp_packet = MutableTcpPacket::new(ip_packet.payload_mut()).unwrap(); - tcp_packet.set_source(nat_entry.dst.port()); + tcp_packet.set_source(nat_entry.real_dst.port()); Self::update_tcp_packet_checksum(&mut tcp_packet, &ip, &dst); drop(tcp_packet); @@ -537,7 +541,6 @@ impl TcpProxy { } } tracing::error!("smoltcp stack sink exited"); - panic!("smoltcp stack sink exited"); }); let peer_mgr = self.peer_manager.clone(); @@ -559,7 +562,6 @@ impl TcpProxy { } } tracing::error!("smoltcp stack stream exited"); - panic!("smoltcp stack stream exited"); }); let interface_config = smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ip); @@ -607,7 +609,7 @@ impl TcpProxy { let mut tcp_listener = self.get_proxy_listener().await?; let global_ctx = self.global_ctx.clone(); - let tasks = self.tasks.clone(); + let tasks = Arc::downgrade(&self.tasks); let syn_map = self.syn_map.clone(); let conn_map = self.conn_map.clone(); let addr_conn_map = self.addr_conn_map.clone(); @@ -644,7 +646,7 @@ impl TcpProxy { tracing::info!( ?socket_addr, "tcp connection accepted for proxy, nat dst: {:?}", - entry.dst + entry.real_dst ); assert_eq!(entry.state.load(), NatDstEntryState::SynReceived); @@ -658,6 +660,11 @@ impl TcpProxy { let old_nat_val = conn_map.insert(entry_clone.id, entry_clone.clone()); assert!(old_nat_val.is_none()); + let Some(tasks) = tasks.upgrade() else { + tracing::error!("tcp proxy tasks is dropped, exit accept loop"); + break; + }; + tasks.lock().unwrap().spawn(Self::connect_to_nat_dst( connector.clone(), global_ctx.clone(), @@ -697,14 +704,14 @@ impl TcpProxy { tracing::warn!("set_nodelay failed, ignore it: {:?}", e); } - let nat_dst = if Some(nat_entry.dst.ip()) + let nat_dst = if Some(nat_entry.real_dst.ip()) == global_ctx.get_ipv4().map(|ip| IpAddr::V4(ip.address())) { - format!("127.0.0.1:{}", nat_entry.dst.port()) + format!("127.0.0.1:{}", nat_entry.real_dst.port()) .parse() .unwrap() } else { - nat_entry.dst + nat_entry.real_dst }; let _guard = global_ctx.net_ns.guard(); @@ -818,10 +825,15 @@ impl TcpProxy { return None; } - if !self - .connector - .check_packet_from_peer(&self.cidr_set, &self.global_ctx, &hdr, &ipv4) - { + let mut real_dst_ip = ipv4.get_destination(); + + if !self.connector.check_packet_from_peer( + &self.cidr_set, + &self.global_ctx, + &hdr, + &ipv4, + &mut real_dst_ip, + ) { return None; } @@ -839,12 +851,13 @@ impl TcpProxy { if is_tcp_syn && !is_tcp_ack { let dest_ip = ip_packet.get_destination(); let dest_port = tcp_packet.get_destination(); - let dst = SocketAddr::V4(SocketAddrV4::new(dest_ip, dest_port)); + let mapped_dst = SocketAddr::V4(SocketAddrV4::new(dest_ip, dest_port)); + let real_dst = SocketAddr::V4(SocketAddrV4::new(real_dst_ip, dest_port)); let old_val = self .syn_map - .insert(src, Arc::new(NatDstEntry::new(src, dst))); - tracing::info!(src = ?src, dst = ?dst, old_entry = ?old_val, "tcp syn received"); + .insert(src, Arc::new(NatDstEntry::new(src, real_dst, mapped_dst))); + tracing::info!(src = ?src, ?real_dst, ?mapped_dst, old_entry = ?old_val, "tcp syn received"); } else if !self.addr_conn_map.contains_key(&src) && !self.syn_map.contains_key(&src) { // if not in syn map and addr conn map, may forwarding n2n packet return None; diff --git a/easytier/src/gateway/udp_proxy.rs b/easytier/src/gateway/udp_proxy.rs index 441e6b759..5e29f0f18 100644 --- a/easytier/src/gateway/udp_proxy.rs +++ b/easytier/src/gateway/udp_proxy.rs @@ -139,6 +139,8 @@ impl UdpNatEntry { self: Arc, mut packet_sender: Sender, virtual_ipv4: Ipv4Addr, + real_ipv4: Ipv4Addr, + mapped_ipv4: Ipv4Addr, ) { let (s, mut r) = tachyonix::channel(128); @@ -197,6 +199,10 @@ impl UdpNatEntry { src_v4.set_ip(virtual_ipv4); } + if *src_v4.ip() == real_ipv4 { + src_v4.set_ip(mapped_ipv4); + } + let Ok(_) = Self::compose_ipv4_packet( &self_clone, &mut packet_sender, @@ -266,7 +272,10 @@ impl UdpProxy { return None; } - if !self.cidr_set.contains_v4(ipv4.get_destination()) + let mut real_dst_ip = ipv4.get_destination(); + if !self + .cidr_set + .contains_v4(ipv4.get_destination(), &mut real_dst_ip) && !is_exit_node && !(self.global_ctx.no_tun() && Some(ipv4.get_destination()) @@ -322,6 +331,8 @@ impl UdpProxy { nat_entry.clone(), self.sender.clone(), self.global_ctx.get_ipv4().map(|x| x.address())?, + real_dst_ip, + ipv4.get_destination(), ))); } @@ -335,7 +346,7 @@ impl UdpProxy { .parse() .unwrap() } else { - SocketAddr::new(ipv4.get_destination().into(), udp_packet.get_destination()) + SocketAddr::new(real_dst_ip.into(), udp_packet.get_destination()) }; let send_ret = { diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index c242cb8b7..99cc12dcd 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -7,13 +7,13 @@ use std::sync::{Arc, Weak}; use anyhow::Context; use cidr::{IpCidr, Ipv4Inet}; -use tokio::task::JoinHandle; use tokio::{sync::Mutex, task::JoinSet}; use tokio_util::sync::CancellationToken; use crate::common::config::ConfigLoader; use crate::common::error::Error; use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent}; +use crate::common::scoped_task::ScopedTask; use crate::common::PeerId; use crate::connector::direct::DirectConnectorManager; use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager}; @@ -70,7 +70,7 @@ impl IpProxy { } async fn start(&self) -> Result<(), Error> { - if (self.global_ctx.get_proxy_cidrs().is_empty() + if (self.global_ctx.config.get_proxy_cidrs().is_empty() || self.started.load(Ordering::Relaxed)) && !self.global_ctx.enable_exit_node() && !self.global_ctx.no_tun() @@ -80,8 +80,7 @@ impl IpProxy { // Actually, if this node is enabled as an exit node, // we still can use the system stack to forward packets. - if self.global_ctx.proxy_forward_by_system() - && !self.global_ctx.no_tun() { + if self.global_ctx.proxy_forward_by_system() && !self.global_ctx.no_tun() { return Ok(()); } @@ -119,7 +118,7 @@ impl NicCtx { } struct MagicDnsContainer { - dns_runner_task: JoinHandle<()>, + dns_runner_task: ScopedTask<()>, dns_runner_cancel_token: CancellationToken, } @@ -140,7 +139,7 @@ impl NicCtxContainer { Self { nic_ctx: Some(Box::new(nic_ctx)), magic_dns: Some(MagicDnsContainer { - dns_runner_task: task, + dns_runner_task: task.into(), dns_runner_cancel_token: token, }), } @@ -400,7 +399,7 @@ impl Instance { // Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed. fn check_dhcp_ip_conflict(&self) { use rand::Rng; - let peer_manager_c = self.peer_manager.clone(); + let peer_manager_c = Arc::downgrade(&self.peer_manager.clone()); let global_ctx_c = self.get_global_ctx(); let nic_ctx = self.nic_ctx.clone(); let _peer_packet_receiver = self.peer_packet_receiver.clone(); @@ -411,6 +410,11 @@ impl Instance { loop { tokio::time::sleep(std::time::Duration::from_secs(next_sleep_time)).await; + let Some(peer_manager_c) = peer_manager_c.upgrade() else { + tracing::warn!("peer manager is dropped, stop dhcp check."); + return; + }; + // do not allocate ip if no peer connected let routes = peer_manager_c.list_routes().await; if routes.is_empty() { @@ -788,12 +792,56 @@ impl Instance { Self::use_new_nic_ctx(nic_ctx.clone(), new_nic_ctx, magic_dns_runner).await; Ok(()) } + + pub async fn clear_resources(&mut self) { + self.peer_manager.clear_resources().await; + let _ = self.nic_ctx.lock().await.take(); + if let Some(rpc_server) = self.rpc_server.take() { + rpc_server.registry().unregister_all(); + }; + } +} + +impl Drop for Instance { + fn drop(&mut self) { + let my_peer_id = self.peer_manager.my_peer_id(); + let pm = Arc::downgrade(&self.peer_manager); + let nic_ctx = self.nic_ctx.clone(); + if let Some(rpc_server) = self.rpc_server.take() { + rpc_server.registry().unregister_all(); + }; + tokio::spawn(async move { + nic_ctx.lock().await.take(); + if let Some(pm) = pm.upgrade() { + pm.clear_resources().await; + }; + + let now = std::time::Instant::now(); + while now.elapsed().as_secs() < 1 { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + if pm.strong_count() == 0 { + tracing::info!( + "Instance for peer {} dropped, all resources cleared.", + my_peer_id + ); + return; + } + } + + debug_assert!( + false, + "Instance for peer {} dropped, but resources not cleared in 1 seconds.", + my_peer_id + ); + }); + } } #[cfg(test)] mod tests { - use crate::{instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook}; - + use crate::{ + instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook, + }; #[tokio::test] async fn test_rpc_portal_whitelist() { @@ -805,7 +853,7 @@ mod tests { expected_result: bool, } - let test_cases:Vec = vec![ + let test_cases: Vec = vec![ // Test default whitelist (127.0.0.0/8, ::1/128) TestCase { remote_url: "tcp://127.0.0.1:15888".to_string(), @@ -822,7 +870,6 @@ mod tests { whitelist: None, expected_result: false, }, - // Test custom whitelist TestCase { remote_url: "tcp://192.168.1.10:15888".to_string(), @@ -848,46 +895,35 @@ mod tests { ]), expected_result: false, }, - // Test empty whitelist (should reject all connections) TestCase { remote_url: "tcp://127.0.0.1:15888".to_string(), whitelist: Some(vec![]), expected_result: false, }, - // Test broad whitelist (0.0.0.0/0 and ::/0 accept all IP addresses) TestCase { remote_url: "tcp://8.8.8.8:15888".to_string(), - whitelist: Some(vec![ - "0.0.0.0/0".parse().unwrap(), - ]), + whitelist: Some(vec!["0.0.0.0/0".parse().unwrap()]), expected_result: true, }, - // Test edge case: specific IP whitelist TestCase { remote_url: "tcp://192.168.1.5:15888".to_string(), - whitelist: Some(vec![ - "192.168.1.5/32".parse().unwrap(), - ]), + whitelist: Some(vec!["192.168.1.5/32".parse().unwrap()]), expected_result: true, }, TestCase { remote_url: "tcp://192.168.1.6:15888".to_string(), - whitelist: Some(vec![ - "192.168.1.5/32".parse().unwrap(), - ]), + whitelist: Some(vec!["192.168.1.5/32".parse().unwrap()]), expected_result: false, }, - // Test invalid URL (this case will fail during URL parsing) TestCase { remote_url: "invalid-url".to_string(), whitelist: None, expected_result: false, }, - // Test URL without IP address (this case will fail during IP parsing) TestCase { remote_url: "tcp://localhost:15888".to_string(), @@ -907,11 +943,22 @@ mod tests { let result = hook.on_new_client(tunnel_info).await; if case.expected_result { - assert!(result.is_ok(), "Expected success for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result); + assert!( + result.is_ok(), + "Expected success for remote_url:{},whitelist:{:?},but got: {:?}", + case.remote_url, + case.whitelist, + result + ); } else { - assert!(result.is_err(), "Expected failure for remote_url:{},whitelist:{:?},but got: {:?}", case.remote_url, case.whitelist, result); + assert!( + result.is_err(), + "Expected failure for remote_url:{},whitelist:{:?},but got: {:?}", + case.remote_url, + case.whitelist, + result + ); } } - } -} \ No newline at end of file +} diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index 26a2d9769..20af6ffff 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -1,4 +1,9 @@ -use std::{fmt::Debug, net::IpAddr, str::FromStr, sync::Arc}; +use std::{ + fmt::Debug, + net::IpAddr, + str::FromStr, + sync::{Arc, Weak}, +}; use anyhow::Context; use async_trait::async_trait; @@ -89,7 +94,7 @@ pub struct ListenerManager { global_ctx: ArcGlobalCtx, net_ns: NetNS, listeners: Vec, - peer_manager: Arc, + peer_manager: Weak, tasks: JoinSet<()>, } @@ -100,7 +105,7 @@ impl ListenerManage global_ctx: global_ctx.clone(), net_ns: global_ctx.net_ns.clone(), listeners: Vec::new(), - peer_manager, + peer_manager: Arc::downgrade(&peer_manager), tasks: JoinSet::new(), } } @@ -169,7 +174,7 @@ impl ListenerManage #[tracing::instrument(skip(creator))] async fn run_listener( creator: Arc, - peer_manager: Arc, + peer_manager: Weak, global_ctx: ArcGlobalCtx, ) { loop { @@ -221,6 +226,10 @@ impl ListenerManage let peer_manager = peer_manager.clone(); let global_ctx = global_ctx.clone(); tokio::spawn(async move { + let Some(peer_manager) = peer_manager.upgrade() else { + tracing::error!("peer manager is gone, cannot handle tunnel"); + return; + }; let server_ret = peer_manager.handle_tunnel(ret).await; if let Err(e) = &server_ret { global_ctx.issue_event(GlobalCtxEvent::ConnectionError( diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 90e9ac0a2..4f4b899f3 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -135,8 +135,6 @@ impl EasyTierLauncher { fetch_node_info: bool, ) -> Result<(), anyhow::Error> { let mut instance = Instance::new(cfg); - let peer_mgr = instance.get_peer_manager(); - let mut tasks = JoinSet::new(); // Subscribe to global context events @@ -164,7 +162,7 @@ impl EasyTierLauncher { if fetch_node_info { let data_c = data.clone(); let global_ctx_c = instance.get_global_ctx(); - let peer_mgr_c = peer_mgr.clone(); + let peer_mgr_c = instance.get_peer_manager().clone(); let vpn_portal = instance.get_vpn_portal_inst(); tasks.spawn(async move { loop { @@ -210,6 +208,9 @@ impl EasyTierLauncher { tasks.abort_all(); drop(tasks); + instance.clear_resources().await; + drop(instance); + Ok(()) } @@ -455,6 +456,36 @@ impl NetworkInstance { } } +pub fn add_proxy_network_to_config( + proxy_network: &str, + cfg: &TomlConfigLoader, +) -> Result<(), anyhow::Error> { + let parts: Vec<&str> = proxy_network.split("->").collect(); + let real_cidr = parts[0] + .parse() + .with_context(|| format!("failed to parse proxy network: {}", parts[0]))?; + + if parts.len() > 2 { + return Err(anyhow::anyhow!( + "invalid proxy network format: {}, support format: or ->, example: + 10.0.0.0/24 or 10.0.0.0/24->192.168.0.0/24", + proxy_network + )); + } + + let mapped_cidr = if parts.len() == 2 { + Some( + parts[1] + .parse() + .with_context(|| format!("failed to parse mapped network: {}", parts[1]))?, + ) + } else { + None + }; + cfg.add_proxy_cidr(real_cidr, mapped_cidr); + Ok(()) +} + pub type NetworkingMethod = crate::proto::web::NetworkingMethod; pub type NetworkConfig = crate::proto::web::NetworkConfig; @@ -534,10 +565,7 @@ impl NetworkConfig { cfg.set_listeners(listener_urls); for n in self.proxy_cidrs.iter() { - cfg.add_proxy_cidr( - n.parse() - .with_context(|| format!("failed to parse proxy network: {}", n))?, - ); + add_proxy_network_to_config(n, &cfg)?; } cfg.set_rpc_portal( diff --git a/easytier/src/peer_center/instance.rs b/easytier/src/peer_center/instance.rs index 82499c201..d446024cc 100644 --- a/easytier/src/peer_center/instance.rs +++ b/easytier/src/peer_center/instance.rs @@ -1,6 +1,6 @@ use std::{ collections::BTreeSet, - sync::Arc, + sync::{Arc, Weak}, time::{Duration, Instant}, }; @@ -31,7 +31,8 @@ use crate::{ use super::{server::PeerCenterServer, Digest, Error}; struct PeerCenterBase { - peer_mgr: Arc, + peer_mgr: Weak, + my_peer_id: PeerId, tasks: Mutex>, lock: Arc>, } @@ -40,20 +41,25 @@ struct PeerCenterBase { static SERVICE_ID: u32 = 50; struct PeridicJobCtx { - peer_mgr: Arc, + peer_mgr: Weak, + my_peer_id: PeerId, center_peer: AtomicCell, job_ctx: T, } impl PeerCenterBase { pub async fn init(&self) -> Result<(), Error> { - self.peer_mgr + let Some(peer_mgr) = self.peer_mgr.upgrade() else { + return Err(Error::Shutdown); + }; + + peer_mgr .get_peer_rpc_mgr() .rpc_server() .registry() .register( - PeerCenterRpcServer::new(PeerCenterServer::new(self.peer_mgr.my_peer_id())), - &self.peer_mgr.get_global_ctx().get_network_name(), + PeerCenterRpcServer::new(PeerCenterServer::new(peer_mgr.my_peer_id())), + &peer_mgr.get_global_ctx().get_network_name(), ); Ok(()) } @@ -91,17 +97,23 @@ impl PeerCenterBase { + Sync + 'static), ) -> () { - let my_peer_id = self.peer_mgr.my_peer_id(); + let my_peer_id = self.my_peer_id; let peer_mgr = self.peer_mgr.clone(); let lock = self.lock.clone(); self.tasks.lock().await.spawn( async move { let ctx = Arc::new(PeridicJobCtx { peer_mgr: peer_mgr.clone(), + my_peer_id, center_peer: AtomicCell::new(PeerId::default()), job_ctx, }); loop { + let Some(peer_mgr) = peer_mgr.upgrade() else { + tracing::error!("peer manager is shutdown, exit periodic job"); + return; + }; + let Some(center_peer) = Self::select_center_peer(&peer_mgr).await else { tracing::trace!("no center peer found, sleep 1 second"); tokio::time::sleep(Duration::from_secs(1)).await; @@ -138,7 +150,8 @@ impl PeerCenterBase { pub fn new(peer_mgr: Arc) -> Self { PeerCenterBase { - peer_mgr, + peer_mgr: Arc::downgrade(&peer_mgr), + my_peer_id: peer_mgr.my_peer_id(), tasks: Mutex::new(JoinSet::new()), lock: Arc::new(Mutex::new(())), } @@ -289,7 +302,7 @@ impl PeerCenterInstance { self.client .init_periodic_job(ctx, |client, ctx| async move { - let my_node_id = ctx.peer_mgr.my_peer_id(); + let my_node_id = ctx.my_peer_id; let peers: PeerInfoForGlobalMap = ctx.job_ctx.service.list_peers().await.into(); let peer_list = peers.direct_peers.keys().map(|k| *k).collect(); let job_ctx = &ctx.job_ctx; diff --git a/easytier/src/peer_center/mod.rs b/easytier/src/peer_center/mod.rs index 71d83cdba..f1e4ec775 100644 --- a/easytier/src/peer_center/mod.rs +++ b/easytier/src/peer_center/mod.rs @@ -19,6 +19,8 @@ pub enum Error { DigestMismatch, #[error("Not center server")] NotCenterServer, + #[error("Instance shutdown")] + Shutdown, } pub type Digest = u64; diff --git a/easytier/src/peers/peer_manager.rs b/easytier/src/peers/peer_manager.rs index ccfffde02..7f251a5ee 100644 --- a/easytier/src/peers/peer_manager.rs +++ b/easytier/src/peers/peer_manager.rs @@ -1101,9 +1101,16 @@ impl PeerManager { .unwrap_or_default(), proxy_cidrs: self .global_ctx + .config .get_proxy_cidrs() .into_iter() - .map(|x| x.to_string()) + .map(|x| { + if x.mapped_cidr.is_none() { + x.cidr.to_string() + } else { + format!("{}->{}", x.cidr, x.mapped_cidr.unwrap()) + } + }) .collect(), hostname: self.global_ctx.get_hostname(), stun_info: Some(self.global_ctx.get_stun_info_collector().get_stun_info()), @@ -1133,6 +1140,15 @@ impl PeerManager { .map(|x| x.clone()) .unwrap_or_default() } + + pub async fn clear_resources(&self) { + let mut peer_pipeline = self.peer_packet_process_pipeline.write().await; + peer_pipeline.clear(); + let mut nic_pipeline = self.nic_packet_process_pipeline.write().await; + nic_pipeline.clear(); + + self.peer_rpc_mgr.rpc_server().registry().unregister_all(); + } } #[cfg(test)] diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 8a641a909..5f92aaba4 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -139,10 +139,12 @@ impl RoutePeerInfo { cost: 0, ipv4_addr: global_ctx.get_ipv4().map(|x| x.address().into()), proxy_cidrs: global_ctx + .config .get_proxy_cidrs() .iter() + .map(|x| x.mapped_cidr.unwrap_or(x.cidr)) + .chain(global_ctx.get_vpn_portal_cidr()) .map(|x| x.to_string()) - .chain(global_ctx.get_vpn_portal_cidr().map(|x| x.to_string())) .collect(), hostname: Some(global_ctx.get_hostname()), udp_stun_info: global_ctx diff --git a/easytier/src/proto/rpc_impl/service_registry.rs b/easytier/src/proto/rpc_impl/service_registry.rs index 1ca440d56..6c970cf4f 100644 --- a/easytier/src/proto/rpc_impl/service_registry.rs +++ b/easytier/src/proto/rpc_impl/service_registry.rs @@ -96,6 +96,10 @@ impl ServiceRegistry { self.table.retain(|k, _| k.domain_name != domain_name); } + pub fn unregister_all(&self) { + self.table.clear(); + } + pub async fn call_method( &self, rpc_desc: RpcDescriptor, diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index dc8a1109a..e308999f5 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -188,6 +188,24 @@ pub async fn init_three_node_ex TomlConfigLoader>( vec![inst1, inst2, inst3] } +pub async fn drop_insts(insts: Vec) { + let mut set = JoinSet::new(); + for mut inst in insts { + set.spawn(async move { + inst.clear_resources().await; + let pm = Arc::downgrade(&inst.get_peer_manager()); + drop(inst); + let now = std::time::Instant::now(); + while now.elapsed().as_secs() < 5 && pm.strong_count() > 0 { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + + debug_assert_eq!(pm.strong_count(), 0, "PeerManager should be dropped"); + }); + } + while let Some(_) = set.join_next().await {} +} + async fn ping_test(from_netns: &str, target_ip: &str, payload_size: Option) -> bool { let _g = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard(); let code = tokio::process::Command::new("ip") @@ -233,14 +251,17 @@ pub async fn basic_three_node_test(#[values("tcp", "udp", "wg", "ws", "wss")] pr Duration::from_secs(5000), ) .await; + + drop_insts(insts).await; } -async fn subnet_proxy_test_udp() { +async fn subnet_proxy_test_udp(target_ip: &str) { use crate::tunnel::{common::tests::_tunnel_pingpong_netns, udp::UdpTunnelListener}; use rand::Rng; let udp_listener = UdpTunnelListener::new("udp://10.1.2.4:22233".parse().unwrap()); - let udp_connector = UdpTunnelConnector::new("udp://10.1.2.4:22233".parse().unwrap()); + let udp_connector = + UdpTunnelConnector::new(format!("udp://{}:22233", target_ip).parse().unwrap()); // NOTE: this should not excced udp tunnel max buffer size let mut buf = vec![0; 7 * 1024]; @@ -257,7 +278,8 @@ async fn subnet_proxy_test_udp() { // no fragment let udp_listener = UdpTunnelListener::new("udp://10.1.2.4:22233".parse().unwrap()); - let udp_connector = UdpTunnelConnector::new("udp://10.1.2.4:22233".parse().unwrap()); + let udp_connector = + UdpTunnelConnector::new(format!("udp://{}:22233", target_ip).parse().unwrap()); let mut buf = vec![0; 1 * 1024]; rand::thread_rng().fill(&mut buf[..]); @@ -305,12 +327,13 @@ async fn subnet_proxy_test_udp() { .await; } -async fn subnet_proxy_test_tcp() { +async fn subnet_proxy_test_tcp(target_ip: &str) { use crate::tunnel::{common::tests::_tunnel_pingpong_netns, tcp::TcpTunnelListener}; use rand::Rng; let tcp_listener = TcpTunnelListener::new("tcp://10.1.2.4:22223".parse().unwrap()); - let tcp_connector = TcpTunnelConnector::new("tcp://10.1.2.4:22223".parse().unwrap()); + let tcp_connector = + TcpTunnelConnector::new(format!("tcp://{}:22223", target_ip).parse().unwrap()); let mut buf = vec![0; 32]; rand::thread_rng().fill(&mut buf[..]); @@ -341,15 +364,15 @@ async fn subnet_proxy_test_tcp() { .await; } -async fn subnet_proxy_test_icmp() { +async fn subnet_proxy_test_icmp(target_ip: &str) { wait_for_condition( - || async { ping_test("net_a", "10.1.2.4", None).await }, + || async { ping_test("net_a", target_ip, None).await }, Duration::from_secs(5), ) .await; wait_for_condition( - || async { ping_test("net_a", "10.1.2.4", Some(5 * 1024)).await }, + || async { ping_test("net_a", target_ip, Some(5 * 1024)).await }, Duration::from_secs(5), ) .await; @@ -369,8 +392,8 @@ async fn subnet_proxy_test_icmp() { } #[rstest::rstest] -#[tokio::test] #[serial_test::serial] +#[tokio::test] pub async fn subnet_proxy_three_node_test( #[values("tcp", "udp", "wg")] proto: &str, #[values(true, false)] no_tun: bool, @@ -378,6 +401,7 @@ pub async fn subnet_proxy_three_node_test( #[values(true, false)] enable_kcp_proxy: bool, #[values(true, false)] disable_kcp_input: bool, #[values(true, false)] dst_enable_kcp_proxy: bool, + #[values(true, false)] test_mapped_cidr: bool, ) { let insts = init_three_node_ex( proto, @@ -388,7 +412,14 @@ pub async fn subnet_proxy_three_node_test( flags.disable_kcp_input = disable_kcp_input; flags.enable_kcp_proxy = dst_enable_kcp_proxy; cfg.set_flags(flags); - cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap()); + cfg.add_proxy_cidr( + "10.1.2.0/24".parse().unwrap(), + if test_mapped_cidr { + Some("10.1.3.0/24".parse().unwrap()) + } else { + None + }, + ); } if cfg.get_inst_name() == "inst2" && relay_by_public_server { @@ -410,19 +441,31 @@ pub async fn subnet_proxy_three_node_test( ) .await; - assert_eq!(insts[2].get_global_ctx().get_proxy_cidrs().len(), 1); + assert_eq!(insts[2].get_global_ctx().config.get_proxy_cidrs().len(), 1); wait_proxy_route_appear( &insts[0].get_peer_manager(), "10.144.144.3/24", insts[2].peer_id(), - "10.1.2.0/24", + if test_mapped_cidr { + "10.1.3.0/24" + } else { + "10.1.2.0/24" + }, ) .await; - subnet_proxy_test_icmp().await; - subnet_proxy_test_tcp().await; - subnet_proxy_test_udp().await; + let target_ip = if test_mapped_cidr { + "10.1.3.4" + } else { + "10.1.2.4" + }; + + subnet_proxy_test_icmp(target_ip).await; + subnet_proxy_test_tcp(target_ip).await; + subnet_proxy_test_udp(target_ip).await; + + drop_insts(insts).await; } #[rstest::rstest] @@ -464,6 +507,8 @@ pub async fn data_compress( Duration::from_secs(5), ) .await; + + drop_insts(_insts).await; } #[cfg(feature = "wireguard")] @@ -577,6 +622,8 @@ pub async fn proxy_three_node_disconnect_test(#[values("tcp", "wg")] proto: &str set_link_status("net_d", true); } + + drop_insts(insts).await; }); let (ret,) = tokio::join!(task); @@ -630,6 +677,8 @@ pub async fn udp_broadcast_test() { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), 2); + + drop_insts(_insts).await; } #[tokio::test] @@ -678,6 +727,8 @@ pub async fn foreign_network_forward_nic_data() { Duration::from_secs(5), ) .await; + + drop_insts(vec![center_inst, inst1, inst2]).await; } use std::{net::SocketAddr, str::FromStr}; @@ -778,6 +829,8 @@ pub async fn wireguard_vpn_portal() { Duration::from_secs(5), ) .await; + + drop_insts(insts).await; } #[cfg(feature = "wireguard")] @@ -837,6 +890,8 @@ pub async fn socks5_vpn_portal(#[values("10.144.144.1", "10.144.144.3")] dst_add drop(conn); tokio::join!(task).0.unwrap(); + + drop_insts(_insts).await; } #[tokio::test] @@ -886,6 +941,7 @@ pub async fn foreign_network_functional_cluster() { let peer_map_inst1 = inst1.get_peer_manager(); println!("inst1 peer map: {:?}", peer_map_inst1.list_routes().await); + drop(peer_map_inst1); wait_for_condition( || async { ping_test("net_c", "10.144.145.2", None).await }, @@ -905,6 +961,8 @@ pub async fn foreign_network_functional_cluster() { Duration::from_secs(5), ) .await; + + drop_insts(vec![center_inst1, center_inst2, inst1, inst2]).await; } #[rstest::rstest] @@ -974,6 +1032,9 @@ pub async fn manual_reconnector(#[values(true, false)] is_foreign: bool) { Duration::from_secs(5), ) .await; + + drop(peer_map); + drop_insts(vec![center_inst, inst1, inst2]).await; } #[rstest::rstest] @@ -1017,7 +1078,7 @@ pub async fn port_forward_test( }, ]); } else if cfg.get_inst_name() == "inst3" { - cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap()); + cfg.add_proxy_cidr("10.1.2.0/24".parse().unwrap(), None); } let mut flags = cfg.get_flags(); flags.no_tun = no_tun; @@ -1093,4 +1154,6 @@ pub async fn port_forward_test( buf, ) .await; + + drop_insts(_insts).await; } From b407cfd9d42a84fc6bfcb1034ac1640829193b77 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sat, 14 Jun 2025 13:06:53 +0800 Subject: [PATCH 016/165] Fixed the issue where the GUI would panic after using InstanceManager (#982) Co-authored-by: Sijie.Sun --- easytier/src/instance_manager.rs | 85 +++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/easytier/src/instance_manager.rs b/easytier/src/instance_manager.rs index 29eef0099..fd3c7330a 100644 --- a/easytier/src/instance_manager.rs +++ b/easytier/src/instance_manager.rs @@ -33,16 +33,27 @@ impl NetworkInstanceManager { .get(&instance_id) .ok_or_else(|| anyhow::anyhow!("instance {} not found", instance_id))?; - if instance.get_config_source() == ConfigSource::FFI { - // FFI have no tokio runtime, so we don't need to spawn a task, and instance should be managed by the caller. - return Ok(()); + match instance.get_config_source() { + ConfigSource::FFI | ConfigSource::GUI => { + // FFI and GUI have no tokio runtime, so we don't need to spawn a task + return Ok(()); + } + _ => { + if tokio::runtime::Handle::try_current().is_err() { + return Err(anyhow::anyhow!( + "tokio runtime not found, cannot start instance task" + )); + } + } } let instance_stop_notifier = instance.get_stop_notifier(); let instance_config_source = instance.get_config_source(); let instance_event_receiver = match instance.get_config_source() { - ConfigSource::Cli | ConfigSource::File => Some(instance.subscribe_event()), - _ => None, + ConfigSource::Cli | ConfigSource::File | ConfigSource::Web => { + Some(instance.subscribe_event()) + } + ConfigSource::GUI | ConfigSource::FFI => None, }; let instance_map = self.instance_map.clone(); @@ -381,7 +392,7 @@ mod tests { assert!(manager.instance_map.contains_key(&instance_id4)); assert!(manager.instance_map.contains_key(&instance_id5)); assert_eq!(manager.list_network_instance_ids().len(), 5); - assert_eq!(manager.instance_stop_tasks.len(), 4); // FFI instance does not have a stop task + assert_eq!(manager.instance_stop_tasks.len(), 3); // FFI and GUI instance does not have a stop task manager .delete_network_instance(vec![instance_id3, instance_id4, instance_id5]) @@ -392,6 +403,68 @@ mod tests { assert_eq!(manager.list_network_instance_ids().len(), 2); } + #[test] + fn test_no_tokio_runtime() { + let manager = NetworkInstanceManager::new(); + let cfg_str = r#" + listeners = [] + "#; + + let port = crate::utils::find_free_tcp_port(10012..65534).expect("no free tcp port found"); + + assert!(manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::Cli, + ) + .is_err()); + assert!(manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::File, + ) + .is_err()); + assert!(manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str) + .map(|c| { + c.set_listeners(vec![format!("tcp://0.0.0.0:{}", port).parse().unwrap()]); + c + }) + .unwrap(), + ConfigSource::GUI, + ) + .is_ok()); + assert!(manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::Web, + ) + .is_err()); + assert!(manager + .run_network_instance( + TomlConfigLoader::new_from_str(cfg_str).unwrap(), + ConfigSource::FFI, + ) + .is_ok()); + + std::thread::sleep(std::time::Duration::from_secs(1)); // wait instance actually started + + assert!(!crate::utils::check_tcp_available(port)); + + assert_eq!(manager.list_network_instance_ids().len(), 5); + assert_eq!( + manager + .instance_map + .iter() + .map(|item| item.is_easytier_running()) + .filter(|x| *x) + .count(), + 5 + ); // stop tasks failed not affect instance running status + assert_eq!(manager.instance_stop_tasks.len(), 0); + } + #[tokio::test] async fn test_single_instance_failed() { let free_tcp_port = From 0bab14cd72d9d8792f9d862d4793852afc36fbc8 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 14 Jun 2025 14:55:48 +0800 Subject: [PATCH 017/165] use bulk compress instead of streaming to reduce mem usage (#985) --- easytier/src/common/compressor.rs | 53 ++++++++++++++++++------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/easytier/src/common/compressor.rs b/easytier/src/common/compressor.rs index affb4dbcd..7366378a9 100644 --- a/easytier/src/common/compressor.rs +++ b/easytier/src/common/compressor.rs @@ -1,10 +1,7 @@ -use std::io::{Read, Write}; - +use anyhow::Context; use dashmap::DashMap; use std::cell::RefCell; -use zstd::stream::read::Decoder; -use zstd::stream::write::Encoder; -use zstd::zstd_safe::{CCtx, DCtx}; +use zstd::bulk; use zerocopy::{AsBytes as _, FromBytes as _}; @@ -35,17 +32,16 @@ impl DefaultCompressor { compress_algo: CompressorAlgo, ) -> Result, Error> { match compress_algo { - CompressorAlgo::ZstdDefault => { - let ret = CTX_MAP.with(|map_cell| { - let map = map_cell.borrow(); - let mut ctx_entry = map.entry(compress_algo).or_default(); - let writer = Vec::new(); - let mut o = Encoder::with_context(writer, ctx_entry.value_mut()); - o.write_all(data)?; - o.finish() - }); - Ok(ret?) - } + CompressorAlgo::ZstdDefault => CTX_MAP.with(|map_cell| { + let map = map_cell.borrow(); + let mut ctx_entry = map.entry(compress_algo).or_default(); + ctx_entry.compress(data).with_context(|| { + format!( + "Failed to compress data with algorithm: {:?}", + compress_algo + ) + }) + }), CompressorAlgo::None => Ok(data.to_vec()), } } @@ -59,10 +55,23 @@ impl DefaultCompressor { CompressorAlgo::ZstdDefault => DCTX_MAP.with(|map_cell| { let map = map_cell.borrow(); let mut ctx_entry = map.entry(compress_algo).or_default(); - let mut decoder = Decoder::with_context(data, ctx_entry.value_mut()); - let mut output = Vec::new(); - decoder.read_to_end(&mut output)?; - Ok(output) + for i in 1..=5 { + let mut len = data.len() * 2usize.pow(i); + if i == 5 && len < 64 * 1024 { + len = 64 * 1024; // Ensure a minimum buffer size + } + match ctx_entry.decompress(data, len) { + Ok(buf) => return Ok(buf), + Err(e) if e.to_string().contains("buffer is too small") => { + continue; // Try with a larger buffer + } + Err(e) => return Err(e.into()), + } + } + Err(anyhow::anyhow!( + "Failed to decompress data after multiple attempts with algorithm: {:?}", + compress_algo + )) }), CompressorAlgo::None => Ok(data.to_vec()), } @@ -155,8 +164,8 @@ impl Compressor for DefaultCompressor { } thread_local! { - static CTX_MAP: RefCell>> = RefCell::new(DashMap::new()); - static DCTX_MAP: RefCell>> = RefCell::new(DashMap::new()); + static CTX_MAP: RefCell>> = RefCell::new(DashMap::new()); + static DCTX_MAP: RefCell>> = RefCell::new(DashMap::new()); } #[cfg(test)] From 5a98fac395b80ed9bd59c4510b764520e1a54b9a Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sat, 14 Jun 2025 23:04:55 +0800 Subject: [PATCH 018/165] =?UTF-8?q?Update=20core.yml=EF=BC=8Cuse=20upx4.2.?= =?UTF-8?q?4=20(#991)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/core.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index da0de8b20..3c7eaecc5 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -244,7 +244,7 @@ jobs: fi if [[ $OS =~ ^ubuntu.*$ && ! $TARGET =~ ^.*freebsd$ ]]; then - UPX_VERSION=5.0.1 + UPX_VERSION=4.2.4 curl -L https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -s | tar xJvf - cp upx-${UPX_VERSION}-amd64_linux/upx . ./upx --lzma --best ./target/$TARGET/release/easytier-core"$SUFFIX" @@ -316,4 +316,4 @@ jobs: ./easytier-contrib/easytier-magisk !./easytier-contrib/easytier-magisk/build.sh !./easytier-contrib/easytier-magisk/magisk_update.json - if-no-files-found: error \ No newline at end of file + if-no-files-found: error From 40b5fe9a540d5bb9ff552d41e1887119f45e08c8 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Sun, 15 Jun 2025 19:43:45 +0800 Subject: [PATCH 019/165] support quic proxy (#993) QUIC proxy works like kcp proxy, it can proxy TCP streams and transfer data with QUIC. QUIC has better congestion algorithm (BBR) for network with both high loss rate and high bandwidth. QUIC proxy can be enabled by passing `--enable-quic-proxy` to easytier in the client side. The proxy status can be viewed by `easytier-cli proxy`. --- .github/workflows/core.yml | 6 +- .github/workflows/test.yml | 3 +- Cargo.lock | 145 ++++-- .../frontend-lib/src/components/Config.vue | 43 +- easytier-web/frontend-lib/src/locales/cn.yaml | 6 + easytier-web/frontend-lib/src/locales/en.yaml | 6 + .../frontend-lib/src/types/network.ts | 4 + easytier/Cargo.toml | 6 +- easytier/locales/app.yml | 6 + easytier/src/common/config.rs | 4 +- easytier/src/common/global_ctx.rs | 11 + easytier/src/easytier-cli.rs | 28 +- easytier/src/easytier-core.rs | 20 + easytier/src/gateway/kcp_proxy.rs | 81 ++-- easytier/src/gateway/mod.rs | 2 + easytier/src/gateway/quic_proxy.rs | 443 ++++++++++++++++++ easytier/src/gateway/tcp_proxy.rs | 4 + easytier/src/instance/instance.rs | 35 ++ easytier/src/instance/listeners.rs | 2 + easytier/src/launcher.rs | 8 + easytier/src/peers/peer_map.rs | 11 +- easytier/src/peers/peer_ospf_route.rs | 9 +- easytier/src/peers/route_trait.rs | 10 +- easytier/src/proto/cli.proto | 1 + easytier/src/proto/common.proto | 9 + easytier/src/proto/peer_rpc.proto | 2 + easytier/src/proto/web.proto | 3 + easytier/src/tests/three_node.rs | 88 +++- easytier/src/tunnel/packet_def.rs | 18 + easytier/src/tunnel/quic.rs | 4 +- 30 files changed, 852 insertions(+), 166 deletions(-) create mode 100644 easytier/src/gateway/quic_proxy.rs diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 3c7eaecc5..a10c5bb5c 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -175,14 +175,14 @@ jobs: fi if [[ $OS =~ ^ubuntu.*$ && $TARGET =~ ^mips.*$ ]]; then - cargo +nightly build -r --verbose --target $TARGET -Z build-std=std,panic_abort --no-default-features --features mips --package=easytier + cargo +nightly build -r --target $TARGET -Z build-std=std,panic_abort --package=easytier else if [[ $OS =~ ^windows.*$ ]]; then SUFFIX=.exe fi - cargo build --release --verbose --target $TARGET --package=easytier-web --features=embed + cargo build --release --target $TARGET --package=easytier-web --features=embed mv ./target/$TARGET/release/easytier-web"$SUFFIX" ./target/$TARGET/release/easytier-web-embed"$SUFFIX" - cargo build --release --verbose --target $TARGET + cargo build --release --target $TARGET fi # Copied and slightly modified from @lmq8267 (https://github.com/lmq8267) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9faa67bf4..e93573150 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,6 +91,7 @@ jobs: - name: Run tests run: | - sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 --nocapture + sudo prlimit --pid $$ --nofile=1048576:1048576 + sudo -E env "PATH=$PATH" cargo test --no-default-features --features=full --verbose -- --test-threads=1 sudo chown -R $USER:$USER ./target sudo chown -R $USER:$USER ~/.cargo diff --git a/Cargo.lock b/Cargo.lock index fe3eecba3..e3ceba876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastbloom" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" +dependencies = [ + "getrandom 0.3.2", + "rand 0.9.1", + "siphasher 1.0.1", + "wide", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -3923,20 +3935,6 @@ dependencies = [ "libc", ] -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror 1.0.63", - "walkdir", -] - [[package]] name = "jni" version = "0.21.1" @@ -4118,7 +4116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -4254,6 +4252,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -4504,7 +4508,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -6043,38 +6047,45 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.3" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", "socket2", - "thiserror 1.0.63", + "thiserror 2.0.11", "tokio", "tracing", + "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.6" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "rand 0.8.5", + "fastbloom", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.1", "ring", "rustc-hash", "rustls", + "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror 1.0.63", + "thiserror 2.0.11", "tinyvec", "tracing", + "web-time", ] [[package]] @@ -6694,9 +6705,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "ring", @@ -6708,15 +6719,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] @@ -6733,26 +6743,29 @@ name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-platform-verifier" -version = "0.3.3" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bda3f493b9abe5b93b3e7e3ecde0df292f2bd28c0296b90586ee0055ff5123" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation 0.9.4", + "core-foundation 0.10.0", "core-foundation-sys", - "jni 0.19.0", + "jni", "log", "once_cell", "rustls", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework", + "security-framework 3.2.0", "security-framework-sys", - "webpki-roots", - "winapi", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", ] [[package]] @@ -6763,9 +6776,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -7050,15 +7063,27 @@ dependencies = [ "core-foundation 0.9.4", "core-foundation-sys", "libc", - "num-bigint", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -7985,7 +8010,7 @@ dependencies = [ "gdkx11-sys", "gtk", "instant", - "jni 0.21.1", + "jni", "lazy_static", "libc", "log", @@ -8047,7 +8072,7 @@ dependencies = [ "heck 0.5.0", "http", "image 0.25.2", - "jni 0.21.1", + "jni", "libc", "log", "mime", @@ -8288,7 +8313,7 @@ dependencies = [ "dpi", "gtk", "http", - "jni 0.21.1", + "jni", "raw-window-handle", "serde", "serde_json", @@ -8306,7 +8331,7 @@ checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30" dependencies = [ "gtk", "http", - "jni 0.21.1", + "jni", "log", "objc2", "objc2-app-kit", @@ -9445,6 +9470,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -9499,6 +9534,24 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.0", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.3" @@ -10099,7 +10152,7 @@ dependencies = [ "html5ever", "http", "javascriptcore-rs", - "jni 0.21.1", + "jni", "kuchikiki", "libc", "ndk", diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index e38fa3ea0..3805d3e2d 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -147,6 +147,8 @@ const bool_flags: BoolFlag[] = [ { 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: 'enable_quic_proxy', help: 'enable_quic_proxy_help' }, + { field: 'disable_quic_input', help: 'disable_quic_input_help' }, { field: 'disable_p2p', help: 'disable_p2p_help' }, { field: 'bind_device', help: 'bind_device_help' }, { field: 'no_tun', help: 'no_tun_help' }, @@ -200,7 +202,7 @@ const bool_flags: BoolFlag[] = [
+ aria-describedby="network_secret-help" toggleMask :feedback="false" />
@@ -271,7 +273,7 @@ const bool_flags: BoolFlag[] = [
+ :placeholder="t('vpn_portal_client_network')" /> /{{ curNetwork.vpn_portal_client_network_len }} @@ -279,7 +281,7 @@ const bool_flags: BoolFlag[] = [
+ :min="0" :max="65535" fluid />
@@ -325,11 +327,10 @@ const bool_flags: BoolFlag[] = [
- +
- +
@@ -338,15 +339,15 @@ const bool_flags: BoolFlag[] = [
+ v-tooltip="t('relay_network_whitelist_help')">
- +
+ :placeholder="t('relay_network_whitelist')" class="w-full" multiple fluid + :suggestions="whitelistSuggestions" @complete="searchWhitelistSuggestions" />
@@ -359,12 +360,12 @@ const bool_flags: BoolFlag[] = [ + :on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
+ :placeholder="t('chips_placeholder', ['192.168.0.0/16'])" class="w-full" multiple fluid + :suggestions="inetSuggestions" @complete="searchInetSuggestions" />
@@ -377,11 +378,11 @@ const bool_flags: BoolFlag[] = [ + :on-label="t('off_text')" :off-label="t('on_text')" class="w-48" />
+ :format="false" :allow-empty="false" :min="0" :max="65535" class="w-full" />
@@ -394,8 +395,8 @@ const bool_flags: BoolFlag[] = [ + :placeholder="t('chips_placeholder', ['192.168.8.8'])" class="w-full" multiple fluid + :suggestions="exitNodesSuggestions" @complete="searchExitNodesSuggestions" /> @@ -406,8 +407,8 @@ const bool_flags: BoolFlag[] = [ + :placeholder="t('chips_placeholder', ['tcp://123.123.123.123:11223'])" class="w-full" multiple fluid + :suggestions="peerSuggestions" @complete="searchPeerSuggestions" /> diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 9c3d241a7..492bea168 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -85,6 +85,12 @@ enable_kcp_proxy_help: 将 TCP 流量转为 KCP 流量,降低传输延迟, disable_kcp_input: 禁用 KCP 输入 disable_kcp_input_help: 禁用 KCP 入站流量,其他开启 KCP 代理的节点仍然使用 TCP 连接到本节点。 +enable_quic_proxy: 启用 QUIC 代理 +enable_quic_proxy_help: 将 TCP 流量转为 QUIC 流量,降低传输延迟,提升传输速度。 + +disable_quic_input: 禁用 QUIC 输入 +disable_quic_input_help: 禁用 QUIC 入站流量,其他开启 QUIC 代理的节点仍然使用 TCP 连接到本节点。 + disable_p2p: 禁用 P2P disable_p2p_help: 禁用 P2P 模式,所有流量通过手动指定的服务器中转。 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index bf9629f96..bfef6e5e4 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -84,6 +84,12 @@ enable_kcp_proxy_help: Convert TCP traffic to KCP traffic to reduce latency and disable_kcp_input: Disable KCP Input disable_kcp_input_help: Disable inbound KCP traffic, while nodes with KCP proxy enabled continue to connect using TCP. +enable_quic_proxy: Enable QUIC Proxy +enable_quic_proxy_help: Convert TCP traffic to QUIC traffic to reduce latency and boost transmission speed. + +disable_quic_input: Disable QUIC Input +disable_quic_input_help: Disable inbound QUIC traffic, while nodes with QUIC proxy enabled continue to connect using TCP. + disable_p2p: Disable P2P disable_p2p_help: Disable P2P mode; route all traffic through a manually specified relay server. diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index 421d61f19..6487fc7ee 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -39,6 +39,8 @@ export interface NetworkConfig { use_smoltcp?: boolean enable_kcp_proxy?: boolean disable_kcp_input?: boolean + enable_quic_proxy?: boolean + disable_quic_input?: boolean disable_p2p?: boolean bind_device?: boolean no_tun?: boolean @@ -105,6 +107,8 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { use_smoltcp: false, enable_kcp_proxy: false, disable_kcp_input: false, + enable_quic_proxy: false, + disable_quic_input: false, disable_p2p: false, bind_device: true, no_tun: false, diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index dddb50b4d..69d60eaf6 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -64,7 +64,7 @@ bytes = "1.5.0" pin-project-lite = "0.2.13" tachyonix = "0.3.0" -quinn = { version = "0.11.0", optional = true, features = ["ring"] } +quinn = { version = "0.11.8", optional = true, features = ["ring"] } rustls = { version = "0.23.0", features = [ "ring", ], default-features = false, optional = true } @@ -280,9 +280,8 @@ tokio-socks = "0.5.2" [features] -default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5"] +default = ["wireguard", "mimalloc", "websocket", "smoltcp", "tun", "socks5", "quic"] full = [ - "quic", "websocket", "wireguard", "mimalloc", @@ -291,7 +290,6 @@ full = [ "tun", "socks5", ] -mips = ["aes-gcm", "mimalloc", "wireguard", "tun", "smoltcp", "socks5"] wireguard = ["dep:boringtun", "dep:ring"] quic = ["dep:quinn", "dep:rustls", "dep:rcgen"] mimalloc = ["dep:mimalloc"] diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index a3e985eee..120ffc8c8 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -158,6 +158,12 @@ core_clap: disable_kcp_input: en: "do not allow other nodes to use kcp to proxy tcp streams to this node. when a node with kcp proxy enabled accesses this node, the original tcp connection is preserved." zh-CN: "不允许其他节点使用 KCP 代理 TCP 流到此节点。开启 KCP 代理的节点访问此节点时,依然使用原始 TCP 连接。" + enable_quic_proxy: + en: "proxy tcp streams with QUIC, improving the latency and throughput on the network with udp packet loss." + zh-CN: "使用 QUIC 代理 TCP 流,提高在 UDP 丢包网络上的延迟和吞吐量。" + disable_quic_input: + en: "do not allow other nodes to use QUIC to proxy tcp streams to this node. when a node with QUIC proxy enabled accesses this node, the original tcp connection is preserved." + zh-CN: "不允许其他节点使用 QUIC 代理 TCP 流到此节点。开启 QUIC 代理的节点访问此节点时,依然使用原始 TCP 连接。" port_forward: en: "forward local port to remote port in virtual network. e.g.: udp://0.0.0.0:12345/10.126.126.1:23456, means forward local udp port 12345 to 10.126.126.1:23456 in the virtual network. can specify multiple." zh-CN: "将本地端口转发到虚拟网络中的远程端口。例如:udp://0.0.0.0:12345/10.126.126.1:23456,表示将本地UDP端口12345转发到虚拟网络中的10.126.126.1:23456。可以指定多个。" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index fee9f0e19..227668797 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -39,6 +39,8 @@ pub fn gen_default_flags() -> Flags { disable_relay_kcp: true, accept_dns: false, private_mode: false, + enable_quic_proxy: false, + disable_quic_input: false, } } @@ -437,7 +439,7 @@ impl ConfigLoader for TomlConfigLoader { .as_ref() .unwrap() .iter() - .any(|c| c.cidr == cidr) + .any(|c| c.cidr == cidr && c.mapped_cidr == mapped_cidr) { locked_config .proxy_network diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index 556d06831..266688ec7 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -75,6 +75,8 @@ pub struct GlobalCtx { no_tun: bool, feature_flags: AtomicCell, + + quic_proxy_port: AtomicCell>, } impl std::fmt::Debug for GlobalCtx { @@ -137,6 +139,7 @@ impl GlobalCtx { no_tun, feature_flags: AtomicCell::new(feature_flags), + quic_proxy_port: AtomicCell::new(None), } } @@ -281,6 +284,14 @@ impl GlobalCtx { pub fn set_feature_flags(&self, flags: PeerFeatureFlag) { self.feature_flags.store(flags); } + + pub fn get_quic_proxy_port(&self) -> Option { + self.quic_proxy_port.load() + } + + pub fn set_quic_proxy_port(&self, port: Option) { + self.quic_proxy_port.store(port); + } } #[cfg(test)] diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index 0c68771ce..996ea99d8 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -1083,7 +1083,8 @@ async fn main() -> Result<(), Error> { .iter() .map(|(k, v)| format!("{}: {:?}ms", k, v.latency_ms,)) .collect::>(); - let direct_peers: Vec<_> = v.direct_peers + let direct_peers: Vec<_> = v + .direct_peers .iter() .map(|(k, v)| DirectPeerItem { node_id: k.to_string(), @@ -1257,23 +1258,14 @@ async fn main() -> Result<(), Error> { } SubCommand::Proxy => { let mut entries = vec![]; - let client = handler.get_tcp_proxy_client("tcp").await?; - let ret = client - .list_tcp_proxy_entry(BaseController::default(), Default::default()) - .await; - entries.extend(ret.unwrap_or_default().entries); - - let client = handler.get_tcp_proxy_client("kcp_src").await?; - let ret = client - .list_tcp_proxy_entry(BaseController::default(), Default::default()) - .await; - entries.extend(ret.unwrap_or_default().entries); - - let client = handler.get_tcp_proxy_client("kcp_dst").await?; - let ret = client - .list_tcp_proxy_entry(BaseController::default(), Default::default()) - .await; - entries.extend(ret.unwrap_or_default().entries); + + for client_type in &["tcp", "kcp_src", "kcp_dst", "quic_src", "quic_dst"] { + let client = handler.get_tcp_proxy_client(client_type).await?; + let ret = client + .list_tcp_proxy_entry(BaseController::default(), Default::default()) + .await; + entries.extend(ret.unwrap_or_default().entries); + } if cli.verbose { println!("{}", serde_json::to_string_pretty(&entries)?); diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 518412679..39d53f6e5 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -433,6 +433,24 @@ struct NetworkOptions { )] disable_kcp_input: Option, + #[arg( + long, + env = "ET_ENABLE_QUIC_PROXY", + help = t!("core_clap.enable_quic_proxy").to_string(), + num_args = 0..=1, + default_missing_value = "true" + )] + enable_quic_proxy: Option, + + #[arg( + long, + env = "ET_DISABLE_QUIC_INPUT", + help = t!("core_clap.disable_quic_input").to_string(), + num_args = 0..=1, + default_missing_value = "true" + )] + disable_quic_input: Option, + #[arg( long, env = "ET_PORT_FORWARD", @@ -773,6 +791,8 @@ impl NetworkOptions { f.bind_device = self.bind_device.unwrap_or(f.bind_device); f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy); f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input); + f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy); + f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input); f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns); f.private_mode = self.private_mode.unwrap_or(f.private_mode); cfg.set_flags(f); diff --git a/easytier/src/gateway/kcp_proxy.rs b/easytier/src/gateway/kcp_proxy.rs index 9d73f6aee..d7dd395c9 100644 --- a/easytier/src/gateway/kcp_proxy.rs +++ b/easytier/src/gateway/kcp_proxy.rs @@ -124,19 +124,16 @@ impl NatDstConnector for NatDstKcpConnector { return Err(anyhow::anyhow!("peer manager is not available").into()); }; - let (dst_peers, _) = match nat_dst { - SocketAddr::V4(addr) => { - let ip = addr.ip(); - peer_mgr.get_msg_dst_peer(&ip).await - } + let dst_peer_id = match nat_dst { + SocketAddr::V4(addr) => peer_mgr.get_peer_map().get_peer_id_by_ipv4(addr.ip()).await, SocketAddr::V6(_) => return Err(anyhow::anyhow!("ipv6 is not supported").into()), }; - tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peers); + let Some(dst_peer) = dst_peer_id else { + return Err(anyhow::anyhow!("no peer found for nat dst: {}", nat_dst).into()); + }; - if dst_peers.len() != 1 { - return Err(anyhow::anyhow!("no dst peer found for nat dst: {}", nat_dst).into()); - } + tracing::trace!("kcp nat dst: {:?}, dst peers: {:?}", nat_dst, dst_peer); let mut connect_tasks: JoinSet> = JoinSet::new(); let mut retry_remain = 5; @@ -167,7 +164,6 @@ impl NatDstConnector for NatDstKcpConnector { let kcp_endpoint = self.kcp_endpoint.clone(); let my_peer_id = peer_mgr.my_peer_id(); - let dst_peer = dst_peers[0]; let conn_data_clone = conn_data.clone(); connect_tasks.spawn(async move { @@ -200,7 +196,7 @@ impl NatDstConnector for NatDstKcpConnector { _ipv4: &Ipv4Packet, _real_dst_ip: &mut Ipv4Addr, ) -> bool { - return hdr.from_peer_id == hdr.to_peer_id; + return hdr.from_peer_id == hdr.to_peer_id && hdr.is_kcp_src_modified(); } fn transport_type(&self) -> TcpProxyEntryTransportType { @@ -211,32 +207,41 @@ impl NatDstConnector for NatDstKcpConnector { #[derive(Clone)] struct TcpProxyForKcpSrc(Arc>); -pub struct KcpProxySrc { - kcp_endpoint: Arc, - peer_manager: Arc, - - tcp_proxy: TcpProxyForKcpSrc, - tasks: JoinSet<()>, +#[async_trait::async_trait] +pub(crate) trait TcpProxyForKcpSrcTrait: Send + Sync + 'static { + type Connector: NatDstConnector; + fn get_tcp_proxy(&self) -> &Arc>; + async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool; } -impl TcpProxyForKcpSrc { +#[async_trait::async_trait] +impl TcpProxyForKcpSrcTrait for TcpProxyForKcpSrc { + type Connector = NatDstKcpConnector; + + fn get_tcp_proxy(&self) -> &Arc> { + &self.0 + } + async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool { let peer_map: Arc = self.0.get_peer_manager().get_peer_map(); let Some(dst_peer_id) = peer_map.get_peer_id_by_ipv4(dst_ip).await else { return false; }; - let Some(feature_flag) = peer_map.get_peer_feature_flag(dst_peer_id).await else { + let Some(peer_info) = peer_map.get_route_peer_info(dst_peer_id).await else { return false; }; - feature_flag.kcp_input + peer_info.feature_flag.map(|x| x.kcp_input).unwrap_or(false) } } #[async_trait::async_trait] -impl NicPacketFilter for TcpProxyForKcpSrc { +impl> NicPacketFilter for T { async fn try_process_packet_from_nic(&self, zc_packet: &mut ZCPacket) -> bool { - let ret = self.0.try_process_packet_from_nic(zc_packet).await; + let ret = self + .get_tcp_proxy() + .try_process_packet_from_nic(zc_packet) + .await; if ret { return true; } @@ -263,29 +268,45 @@ impl NicPacketFilter for TcpProxyForKcpSrc { } } else { // if not syn packet, only allow established connection - if !self.0.is_tcp_proxy_connection(SocketAddr::new( - IpAddr::V4(ip_packet.get_source()), - tcp_packet.get_source(), - )) { + if !self + .get_tcp_proxy() + .is_tcp_proxy_connection(SocketAddr::new( + IpAddr::V4(ip_packet.get_source()), + tcp_packet.get_source(), + )) + { return false; } } - if let Some(my_ipv4) = self.0.get_global_ctx().get_ipv4() { + if let Some(my_ipv4) = self.get_tcp_proxy().get_global_ctx().get_ipv4() { // this is a net-to-net packet, only allow it when smoltcp is enabled // because the syn-ack packet will not be through and handled by the tun device when // the source ip is in the local network - if ip_packet.get_source() != my_ipv4.address() && !self.0.is_smoltcp_enabled() { + if ip_packet.get_source() != my_ipv4.address() + && !self.get_tcp_proxy().is_smoltcp_enabled() + { return false; } }; - zc_packet.mut_peer_manager_header().unwrap().to_peer_id = self.0.get_my_peer_id().into(); - + let hdr = zc_packet.mut_peer_manager_header().unwrap(); + hdr.to_peer_id = self.get_tcp_proxy().get_my_peer_id().into(); + if self.get_tcp_proxy().get_transport_type() == TcpProxyEntryTransportType::Kcp { + hdr.set_kcp_src_modified(true); + } true } } +pub struct KcpProxySrc { + kcp_endpoint: Arc, + peer_manager: Arc, + + tcp_proxy: TcpProxyForKcpSrc, + tasks: JoinSet<()>, +} + impl KcpProxySrc { pub async fn new(peer_manager: Arc) -> Self { let mut kcp_endpoint = create_kcp_endpoint(); diff --git a/easytier/src/gateway/mod.rs b/easytier/src/gateway/mod.rs index a3dfc7bab..c8d8af9e1 100644 --- a/easytier/src/gateway/mod.rs +++ b/easytier/src/gateway/mod.rs @@ -18,6 +18,8 @@ pub mod socks5; pub mod kcp_proxy; +pub mod quic_proxy; + #[derive(Debug)] pub(crate) struct CidrSet { global_ctx: ArcGlobalCtx, diff --git a/easytier/src/gateway/quic_proxy.rs b/easytier/src/gateway/quic_proxy.rs new file mode 100644 index 000000000..e6c6f5d09 --- /dev/null +++ b/easytier/src/gateway/quic_proxy.rs @@ -0,0 +1,443 @@ +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::{Arc, Mutex, Weak}; +use std::{net::SocketAddr, pin::Pin}; + +use anyhow::Context; +use dashmap::DashMap; +use pnet::packet::ipv4::Ipv4Packet; +use prost::Message as _; +use quinn::{Endpoint, Incoming}; +use tokio::io::{copy_bidirectional, AsyncRead, AsyncReadExt, AsyncWrite}; +use tokio::net::TcpStream; +use tokio::task::JoinSet; +use tokio::time::timeout; + +use crate::common::error::Result; +use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx}; +use crate::common::join_joinset_background; +use crate::defer; +use crate::gateway::kcp_proxy::TcpProxyForKcpSrcTrait; +use crate::gateway::tcp_proxy::{NatDstConnector, NatDstTcpConnector, TcpProxy}; +use crate::gateway::CidrSet; +use crate::peers::peer_manager::PeerManager; +use crate::proto::cli::{ + ListTcpProxyEntryRequest, ListTcpProxyEntryResponse, TcpProxyEntry, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, +}; +use crate::proto::common::ProxyDstInfo; +use crate::proto::rpc_types; +use crate::proto::rpc_types::controller::BaseController; +use crate::tunnel::packet_def::PeerManagerHeader; +use crate::tunnel::quic::{configure_client, make_server_endpoint}; + +pub struct QUICStream { + endpoint: Option, + connection: Option, + sender: quinn::SendStream, + receiver: quinn::RecvStream, +} + +impl AsyncRead for QUICStream { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let this = self.get_mut(); + Pin::new(&mut this.receiver).poll_read(cx, buf) + } +} + +impl AsyncWrite for QUICStream { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let this = self.get_mut(); + AsyncWrite::poll_write(Pin::new(&mut this.sender), cx, buf) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.get_mut(); + Pin::new(&mut this.sender).poll_flush(cx) + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.get_mut(); + Pin::new(&mut this.sender).poll_shutdown(cx) + } +} + +#[derive(Debug, Clone)] +pub struct NatDstQUICConnector { + pub(crate) peer_mgr: Weak, +} + +#[async_trait::async_trait] +impl NatDstConnector for NatDstQUICConnector { + type DstStream = QUICStream; + + #[tracing::instrument(skip(self), level = "debug", name = "NatDstQUICConnector::connect")] + async fn connect(&self, src: SocketAddr, nat_dst: SocketAddr) -> Result { + let Some(peer_mgr) = self.peer_mgr.upgrade() else { + return Err(anyhow::anyhow!("peer manager is not available").into()); + }; + + let IpAddr::V4(dst_ipv4) = nat_dst.ip() else { + return Err(anyhow::anyhow!("src must be an IPv4 address").into()); + }; + + let Some(dst_peer) = peer_mgr.get_peer_map().get_peer_id_by_ipv4(&dst_ipv4).await else { + return Err(anyhow::anyhow!("no peer found for dst: {}", nat_dst).into()); + }; + + let Some(dst_peer_info) = peer_mgr.get_peer_map().get_route_peer_info(dst_peer).await + else { + return Err(anyhow::anyhow!("no peer info found for dst peer: {}", dst_peer).into()); + }; + + let Some(dst_ipv4): Option = dst_peer_info.ipv4_addr.map(Into::into) else { + return Err(anyhow::anyhow!("no ipv4 found for dst peer: {}", dst_peer).into()); + }; + + let Some(quic_port) = dst_peer_info.quic_port else { + return Err(anyhow::anyhow!("no quic port found for dst peer: {}", dst_peer).into()); + }; + + let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap()) + .with_context(|| format!("failed to create QUIC endpoint for src: {}", src))?; + endpoint.set_default_client_config(configure_client()); + + // connect to server + let connection = { + let _g = peer_mgr.get_global_ctx().net_ns.guard(); + endpoint + .connect( + SocketAddr::new(dst_ipv4.into(), quic_port as u16), + "localhost", + ) + .unwrap() + .await + .with_context(|| { + format!( + "failed to connect to NAT destination {} from {}, real dst: {}", + nat_dst, src, dst_ipv4 + ) + })? + }; + + let (mut w, r) = connection + .open_bi() + .await + .with_context(|| "open_bi failed")?; + + let proxy_dst_info = ProxyDstInfo { + dst_addr: Some(nat_dst.into()), + }; + let proxy_dst_info_buf = proxy_dst_info.encode_to_vec(); + let buf_len = proxy_dst_info_buf.len() as u8; + w.write(&buf_len.to_le_bytes()) + .await + .with_context(|| "failed to write proxy dst info buf len to QUIC stream")?; + w.write(&proxy_dst_info_buf) + .await + .with_context(|| "failed to write proxy dst info to QUIC stream")?; + + Ok(QUICStream { + endpoint: Some(endpoint), + connection: Some(connection), + sender: w, + receiver: r, + }) + } + + fn check_packet_from_peer_fast(&self, _cidr_set: &CidrSet, _global_ctx: &GlobalCtx) -> bool { + true + } + + fn check_packet_from_peer( + &self, + _cidr_set: &CidrSet, + _global_ctx: &GlobalCtx, + hdr: &PeerManagerHeader, + _ipv4: &Ipv4Packet, + _real_dst_ip: &mut Ipv4Addr, + ) -> bool { + return hdr.from_peer_id == hdr.to_peer_id && !hdr.is_kcp_src_modified(); + } + + fn transport_type(&self) -> TcpProxyEntryTransportType { + TcpProxyEntryTransportType::Quic + } +} + +#[derive(Clone)] +struct TcpProxyForQUICSrc(Arc>); + +#[async_trait::async_trait] +impl TcpProxyForKcpSrcTrait for TcpProxyForQUICSrc { + type Connector = NatDstQUICConnector; + + fn get_tcp_proxy(&self) -> &Arc> { + &self.0 + } + + async fn check_dst_allow_kcp_input(&self, dst_ip: &Ipv4Addr) -> bool { + let peer_map: Arc = + self.0.get_peer_manager().get_peer_map(); + let Some(dst_peer_id) = peer_map.get_peer_id_by_ipv4(dst_ip).await else { + return false; + }; + let Some(peer_info) = peer_map.get_route_peer_info(dst_peer_id).await else { + return false; + }; + let Some(quic_port) = peer_info.quic_port else { + return false; + }; + quic_port > 0 + } +} + +pub struct QUICProxySrc { + peer_manager: Arc, + tcp_proxy: TcpProxyForQUICSrc, +} + +impl QUICProxySrc { + pub async fn new(peer_manager: Arc) -> Self { + let tcp_proxy = TcpProxy::new( + peer_manager.clone(), + NatDstQUICConnector { + peer_mgr: Arc::downgrade(&peer_manager), + }, + ); + + Self { + peer_manager, + tcp_proxy: TcpProxyForQUICSrc(tcp_proxy), + } + } + + pub async fn start(&self) { + self.peer_manager + .add_nic_packet_process_pipeline(Box::new(self.tcp_proxy.clone())) + .await; + self.peer_manager + .add_packet_process_pipeline(Box::new(self.tcp_proxy.0.clone())) + .await; + self.tcp_proxy.0.start(false).await.unwrap(); + } + + pub fn get_tcp_proxy(&self) -> Arc> { + self.tcp_proxy.0.clone() + } +} + +pub struct QUICProxyDst { + global_ctx: Arc, + endpoint: Arc, + proxy_entries: Arc>, + tasks: Arc>>, +} + +impl QUICProxyDst { + pub fn new(global_ctx: ArcGlobalCtx) -> Result { + let _g = global_ctx.net_ns.guard(); + let (endpoint, _) = make_server_endpoint("0.0.0.0:0".parse().unwrap()) + .map_err(|e| anyhow::anyhow!("failed to create QUIC endpoint: {}", e))?; + let tasks = Arc::new(Mutex::new(JoinSet::new())); + join_joinset_background(tasks.clone(), "QUICProxyDst tasks".to_string()); + Ok(Self { + global_ctx, + endpoint: Arc::new(endpoint), + proxy_entries: Arc::new(DashMap::new()), + tasks, + }) + } + + pub async fn start(&self) -> Result<()> { + let endpoint = self.endpoint.clone(); + let tasks = Arc::downgrade(&self.tasks.clone()); + let ctx = self.global_ctx.clone(); + let cidr_set = Arc::new(CidrSet::new(ctx.clone())); + let proxy_entries = self.proxy_entries.clone(); + + let task = async move { + loop { + match endpoint.accept().await { + Some(conn) => { + let Some(tasks) = tasks.upgrade() else { + tracing::warn!( + "QUICProxyDst tasks is not available, stopping accept loop" + ); + return; + }; + tasks + .lock() + .unwrap() + .spawn(Self::handle_connection_with_timeout( + conn, + ctx.clone(), + cidr_set.clone(), + proxy_entries.clone(), + )); + } + None => { + return; + } + } + } + }; + + self.tasks.lock().unwrap().spawn(task); + + Ok(()) + } + + pub fn local_addr(&self) -> Result { + self.endpoint.local_addr().map_err(Into::into) + } + + async fn handle_connection_with_timeout( + conn: Incoming, + ctx: Arc, + cidr_set: Arc, + proxy_entries: Arc>, + ) { + let remote_addr = conn.remote_address(); + defer!( + proxy_entries.remove(&remote_addr); + ); + let ret = timeout( + std::time::Duration::from_secs(10), + Self::handle_connection(conn, ctx, cidr_set, remote_addr, proxy_entries.clone()), + ) + .await; + + match ret { + Ok(Ok((mut quic_stream, mut tcp_stream))) => { + let ret = copy_bidirectional(&mut quic_stream, &mut tcp_stream).await; + tracing::info!( + "QUIC connection handled, result: {:?}, remote addr: {:?}", + ret, + quic_stream.connection.as_ref().map(|c| c.remote_address()) + ); + } + Ok(Err(e)) => { + tracing::error!("Failed to handle QUIC connection: {}", e); + } + Err(_) => { + tracing::warn!("Timeout while handling QUIC connection"); + } + } + } + + async fn handle_connection( + incoming: Incoming, + ctx: ArcGlobalCtx, + cidr_set: Arc, + proxy_entry_key: SocketAddr, + proxy_entries: Arc>, + ) -> Result<(QUICStream, TcpStream)> { + let conn = incoming.await.with_context(|| "accept failed")?; + let addr = conn.remote_address(); + tracing::info!("Accepted QUIC connection from {}", addr); + let (w, mut r) = conn.accept_bi().await.with_context(|| "accept_bi failed")?; + let len = r + .read_u8() + .await + .with_context(|| "failed to read proxy dst info buf len")?; + let mut buf = vec![0u8; len as usize]; + r.read_exact(&mut buf) + .await + .with_context(|| "failed to read proxy dst info")?; + + let proxy_dst_info = + ProxyDstInfo::decode(&buf[..]).with_context(|| "failed to decode proxy dst info")?; + + let dst_socket: SocketAddr = proxy_dst_info + .dst_addr + .map(Into::into) + .ok_or_else(|| anyhow::anyhow!("no dst addr in proxy dst info"))?; + + let SocketAddr::V4(mut dst_socket) = dst_socket else { + return Err(anyhow::anyhow!("NAT destination must be an IPv4 address").into()); + }; + + let mut real_ip = *dst_socket.ip(); + if cidr_set.contains_v4(*dst_socket.ip(), &mut real_ip) { + dst_socket.set_ip(real_ip); + } + + if Some(*dst_socket.ip()) == ctx.get_ipv4().map(|ip| ip.address()) && ctx.no_tun() { + dst_socket = format!("127.0.0.1:{}", dst_socket.port()).parse().unwrap(); + } + + proxy_entries.insert( + proxy_entry_key, + TcpProxyEntry { + src: Some(addr.into()), + dst: Some(SocketAddr::V4(dst_socket).into()), + start_time: chrono::Local::now().timestamp() as u64, + state: TcpProxyEntryState::ConnectingDst.into(), + transport_type: TcpProxyEntryTransportType::Quic.into(), + }, + ); + + let connector = NatDstTcpConnector {}; + + let dst_stream = { + let _g = ctx.net_ns.guard(); + connector + .connect("0.0.0.0:0".parse().unwrap(), dst_socket.into()) + .await? + }; + + if let Some(mut e) = proxy_entries.get_mut(&proxy_entry_key) { + e.state = TcpProxyEntryState::Connected.into(); + } + + let quic_stream = QUICStream { + endpoint: None, + connection: Some(conn), + sender: w, + receiver: r, + }; + + Ok((quic_stream, dst_stream)) + } +} + +#[derive(Clone)] +pub struct QUICProxyDstRpcService(Weak>); + +impl QUICProxyDstRpcService { + pub fn new(quic_proxy_dst: &QUICProxyDst) -> Self { + Self(Arc::downgrade(&quic_proxy_dst.proxy_entries)) + } +} + +#[async_trait::async_trait] +impl TcpProxyRpc for QUICProxyDstRpcService { + type Controller = BaseController; + async fn list_tcp_proxy_entry( + &self, + _: BaseController, + _request: ListTcpProxyEntryRequest, // Accept request of type HelloRequest + ) -> std::result::Result { + let mut reply = ListTcpProxyEntryResponse::default(); + if let Some(tcp_proxy) = self.0.upgrade() { + for item in tcp_proxy.iter() { + reply.entries.push(item.value().clone()); + } + } + Ok(reply) + } +} diff --git a/easytier/src/gateway/tcp_proxy.rs b/easytier/src/gateway/tcp_proxy.rs index ab10c8319..def3b739f 100644 --- a/easytier/src/gateway/tcp_proxy.rs +++ b/easytier/src/gateway/tcp_proxy.rs @@ -902,6 +902,10 @@ impl TcpProxy { } entries } + + pub fn get_transport_type(&self) -> TcpProxyEntryTransportType { + self.connector.transport_type() + } } #[derive(Clone)] diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 99cc12dcd..7360e0e35 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -20,6 +20,7 @@ use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManage use crate::connector::udp_hole_punch::UdpHolePunchConnector; use crate::gateway::icmp_proxy::IcmpProxy; use crate::gateway::kcp_proxy::{KcpProxyDst, KcpProxyDstRpcService, KcpProxySrc}; +use crate::gateway::quic_proxy::{QUICProxyDst, QUICProxyDstRpcService, QUICProxySrc}; use crate::gateway::tcp_proxy::{NatDstTcpConnector, TcpProxy, TcpProxyRpcService}; use crate::gateway::udp_proxy::UdpProxy; use crate::peer_center::instance::PeerCenterInstance; @@ -232,6 +233,9 @@ pub struct Instance { kcp_proxy_src: Option, kcp_proxy_dst: Option, + quic_proxy_src: Option, + quic_proxy_dst: Option, + peer_center: Arc, vpn_portal: Arc>>, @@ -312,6 +316,9 @@ impl Instance { kcp_proxy_src: None, kcp_proxy_dst: None, + quic_proxy_src: None, + quic_proxy_dst: None, + peer_center, vpn_portal: Arc::new(Mutex::new(Box::new(vpn_portal_inst))), @@ -562,6 +569,20 @@ impl Instance { self.kcp_proxy_dst = Some(dst_proxy); } + if self.global_ctx.get_flags().enable_quic_proxy { + let quic_src = QUICProxySrc::new(self.get_peer_manager()).await; + quic_src.start().await; + self.quic_proxy_src = Some(quic_src); + } + + if !self.global_ctx.get_flags().disable_quic_input { + let quic_dst = QUICProxyDst::new(self.global_ctx.clone())?; + quic_dst.start().await?; + self.global_ctx + .set_quic_proxy_port(Some(quic_dst.local_addr()?.port())); + self.quic_proxy_dst = Some(quic_dst); + } + // run after tun device created, so listener can bind to tun device, which may be required by win 10 self.ip_proxy = Some(IpProxy::new( self.get_global_ctx(), @@ -737,6 +758,20 @@ impl Instance { ); } + if let Some(quic_proxy) = self.quic_proxy_src.as_ref() { + s.registry().register( + TcpProxyRpcServer::new(TcpProxyRpcService::new(quic_proxy.get_tcp_proxy())), + "quic_src", + ); + } + + if let Some(quic_proxy) = self.quic_proxy_dst.as_ref() { + s.registry().register( + TcpProxyRpcServer::new(QUICProxyDstRpcService::new(quic_proxy)), + "quic_dst", + ); + } + s.set_hook(Arc::new(InstanceRpcServerHook::new( self.global_ctx.config.get_rpc_portal_whitelist(), ))); diff --git a/easytier/src/instance/listeners.rs b/easytier/src/instance/listeners.rs index 20af6ffff..da03f9963 100644 --- a/easytier/src/instance/listeners.rs +++ b/easytier/src/instance/listeners.rs @@ -142,6 +142,8 @@ impl ListenerManage if self.global_ctx.config.get_flags().enable_ipv6 && !is_url_host_ipv6(&l) && is_url_host_unspecified(&l) + // quic enables dual-stack by default, may conflict with v4 listener + && l.scheme() != "quic" { let mut ipv6_listener = l.clone(); ipv6_listener diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 4f4b899f3..7133a7f20 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -685,6 +685,14 @@ impl NetworkConfig { flags.disable_kcp_input = disable_kcp_input; } + if let Some(enable_quic_proxy) = self.enable_quic_proxy { + flags.enable_quic_proxy = enable_quic_proxy; + } + + if let Some(disable_quic_input) = self.disable_quic_input { + flags.disable_quic_input = disable_quic_input; + } + if let Some(disable_p2p) = self.disable_p2p { flags.disable_p2p = disable_p2p; } diff --git a/easytier/src/peers/peer_map.rs b/easytier/src/peers/peer_map.rs index f8cef8f10..9ab192789 100644 --- a/easytier/src/peers/peer_map.rs +++ b/easytier/src/peers/peer_map.rs @@ -10,7 +10,7 @@ use crate::{ global_ctx::{ArcGlobalCtx, GlobalCtxEvent, NetworkIdentity}, PeerId, }, - proto::{cli::PeerConnInfo, common::PeerFeatureFlag}, + proto::{cli::PeerConnInfo, peer_rpc::RoutePeerInfo}, tunnel::{packet_def::ZCPacket, TunnelError}, }; @@ -194,12 +194,11 @@ impl PeerMap { None } - pub async fn get_peer_feature_flag(&self, peer_id: PeerId) -> Option { + pub async fn get_route_peer_info(&self, peer_id: PeerId) -> Option { for route in self.routes.read().await.iter() { - let feature_flag = route.get_feature_flag(peer_id).await; - if feature_flag.is_some() { - return feature_flag; - }; + if let Some(info) = route.get_peer_info(peer_id).await { + return Some(info); + } } None } diff --git a/easytier/src/peers/peer_ospf_route.rs b/easytier/src/peers/peer_ospf_route.rs index 5f92aaba4..f9d3adf9e 100644 --- a/easytier/src/peers/peer_ospf_route.rs +++ b/easytier/src/peers/peer_ospf_route.rs @@ -33,7 +33,7 @@ use crate::{ }, peers::route_trait::{Route, RouteInterfaceBox}, proto::{ - common::{Ipv4Inet, NatType, PeerFeatureFlag, StunInfo}, + common::{Ipv4Inet, NatType, StunInfo}, peer_rpc::{ route_foreign_network_infos, ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, OspfRouteRpc, OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerIdVersion, @@ -124,6 +124,7 @@ impl RoutePeerInfo { feature_flag: None, peer_route_id: 0, network_length: 24, + quic_port: None, } } @@ -162,6 +163,8 @@ impl RoutePeerInfo { .get_ipv4() .map(|x| x.network_length() as u32) .unwrap_or(24), + + quic_port: global_ctx.get_quic_proxy_port().map(|x| x as u32), }; let need_update_periodically = if let Ok(Ok(d)) = @@ -2317,12 +2320,12 @@ impl Route for PeerRoute { .map(|x| *x) } - async fn get_feature_flag(&self, peer_id: PeerId) -> Option { + async fn get_peer_info(&self, peer_id: PeerId) -> Option { self.service_impl .route_table .peer_infos .get(&peer_id) - .and_then(|x| x.feature_flag.clone()) + .map(|x| x.clone()) } async fn get_peer_info_last_update_time(&self) -> Instant { diff --git a/easytier/src/peers/route_trait.rs b/easytier/src/peers/route_trait.rs index 2dd7b8437..21f6083e8 100644 --- a/easytier/src/peers/route_trait.rs +++ b/easytier/src/peers/route_trait.rs @@ -4,11 +4,9 @@ use dashmap::DashMap; use crate::{ common::{global_ctx::NetworkIdentity, PeerId}, - proto::{ - common::PeerFeatureFlag, - peer_rpc::{ - ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos, - }, + proto::peer_rpc::{ + ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, RouteForeignNetworkInfos, + RoutePeerInfo, }, }; @@ -107,7 +105,7 @@ pub trait Route { async fn set_route_cost_fn(&self, _cost_fn: RouteCostCalculator) {} - async fn get_feature_flag(&self, peer_id: PeerId) -> Option; + async fn get_peer_info(&self, peer_id: PeerId) -> Option; async fn get_peer_info_last_update_time(&self) -> std::time::Instant; diff --git a/easytier/src/proto/cli.proto b/easytier/src/proto/cli.proto index 53ac82204..0eafeb2e2 100644 --- a/easytier/src/proto/cli.proto +++ b/easytier/src/proto/cli.proto @@ -187,6 +187,7 @@ service VpnPortalRpc { enum TcpProxyEntryTransportType { TCP = 0; KCP = 1; + QUIC = 2; } enum TcpProxyEntryState { diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index 5066d2614..55db5d1ed 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -35,6 +35,11 @@ message FlagsInConfig { bool accept_dns = 22; // enable private mode bool private_mode = 23; + + // should we convert all tcp streams into quic streams + bool enable_quic_proxy = 24; + // does this peer allow quic input + bool disable_quic_input = 25; } message RpcDescriptor { @@ -171,3 +176,7 @@ message PortForwardConfigPb { SocketAddr dst_addr = 2; SocketType socket_type = 3; } + +message ProxyDstInfo { + SocketAddr dst_addr = 1; +} diff --git a/easytier/src/proto/peer_rpc.proto b/easytier/src/proto/peer_rpc.proto index 0e1fd961a..0ac05ca2a 100644 --- a/easytier/src/proto/peer_rpc.proto +++ b/easytier/src/proto/peer_rpc.proto @@ -22,6 +22,8 @@ message RoutePeerInfo { uint64 peer_route_id = 12; uint32 network_length = 13; + + optional uint32 quic_port = 14; } message PeerIdVersion { diff --git a/easytier/src/proto/web.proto b/easytier/src/proto/web.proto index c909ea503..d29025159 100644 --- a/easytier/src/proto/web.proto +++ b/easytier/src/proto/web.proto @@ -68,6 +68,9 @@ message NetworkConfig { optional bool enable_private_mode = 43; repeated string rpc_portal_whitelists = 44; + + optional bool enable_quic_proxy = 45; + optional bool disable_quic_input = 46; } message MyNodeInfo { diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index e308999f5..30353da35 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -222,6 +222,8 @@ async fn ping_test(from_netns: &str, target_ip: &str, payload_size: Option &mut Self { + let mut flags = PeerManagerHeaderFlags::from_bits(self.flags).unwrap(); + if modified { + flags.insert(PeerManagerHeaderFlags::KCP_SRC_MODIFIED); + } else { + flags.remove(PeerManagerHeaderFlags::KCP_SRC_MODIFIED); + } + self.flags = flags.bits(); + self + } + + pub fn is_kcp_src_modified(&self) -> bool { + PeerManagerHeaderFlags::from_bits(self.flags) + .unwrap() + .contains(PeerManagerHeaderFlags::KCP_SRC_MODIFIED) + } } #[repr(C, packed)] diff --git a/easytier/src/tunnel/quic.rs b/easytier/src/tunnel/quic.rs index 20d634423..798a6cd62 100644 --- a/easytier/src/tunnel/quic.rs +++ b/easytier/src/tunnel/quic.rs @@ -17,7 +17,7 @@ use super::{ IpVersion, Tunnel, TunnelConnector, TunnelError, TunnelListener, }; -fn configure_client() -> ClientConfig { +pub fn configure_client() -> ClientConfig { ClientConfig::new(Arc::new( QuicClientConfig::try_from(get_insecure_tls_client_config()).unwrap(), )) @@ -38,7 +38,7 @@ pub fn make_server_endpoint(bind_addr: SocketAddr) -> Result<(Endpoint, Vec) } /// Returns default server configuration along with its certificate. -fn configure_server() -> Result<(ServerConfig, Vec), Box> { +pub fn configure_server() -> Result<(ServerConfig, Vec), Box> { let (certs, key) = get_insecure_tls_cert(); let mut server_config = ServerConfig::with_single_cert(certs.clone(), key.into())?; From ed162c2e665d8f76da281730a1133863889b73a5 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Sun, 15 Jun 2025 23:41:42 +0800 Subject: [PATCH 020/165] Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990) * add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web --- easytier-gui/locales/cn.yml | 4 + easytier-gui/locales/en.yml | 3 + easytier-gui/src-tauri/src/lib.rs | 14 +- easytier-gui/src/App.vue | 1 + easytier-gui/src/auto-imports.d.ts | 2 + easytier-gui/src/composables/network.ts | 4 + easytier-gui/src/pages/index.vue | 24 +- easytier-gui/src/stores/network.ts | 6 + .../src/components/ConfigEditDialog.vue | 103 +++++ .../frontend-lib/src/components/index.ts | 1 + .../frontend-lib/src/easytier-frontend-lib.ts | 5 +- easytier-web/frontend-lib/src/locales/cn.yaml | 4 + easytier-web/frontend-lib/src/locales/en.yaml | 4 + easytier-web/frontend-lib/src/modules/api.ts | 21 + .../src/components/ConfigGenerator.vue | 45 +- .../src/components/DeviceManagement.vue | 156 ++++--- easytier-web/src/restful/mod.rs | 33 +- easytier/src/launcher.rs | 392 ++++++++++++++++++ 18 files changed, 740 insertions(+), 82 deletions(-) create mode 100644 easytier-web/frontend-lib/src/components/ConfigEditDialog.vue diff --git a/easytier-gui/locales/cn.yml b/easytier-gui/locales/cn.yml index 66f5de3e6..86c62701a 100644 --- a/easytier-gui/locales/cn.yml +++ b/easytier-gui/locales/cn.yml @@ -50,7 +50,11 @@ dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名 off_text: 点击关闭 on_text: 点击开启 show_config: 显示配置 +edit_config: 编辑配置文件 close: 关闭 +save: 保存 +config_saved: 配置已保存 + use_latency_first: 延迟优先模式 my_node_info: 当前节点信息 diff --git a/easytier-gui/locales/en.yml b/easytier-gui/locales/en.yml index 94d8178b5..b7cc3244a 100644 --- a/easytier-gui/locales/en.yml +++ b/easytier-gui/locales/en.yml @@ -51,7 +51,10 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n off_text: Press to disable on_text: Press to enable show_config: Show Config +edit_config: Edit Config File close: Close +save: Save +config_saved: Configuration saved my_node_info: My Node Info peer_count: Connected upload: Upload diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index d42faebec..b0a06e801 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -4,11 +4,9 @@ use std::collections::BTreeMap; use easytier::{ - common::config::{ - ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, - }, - launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo}, + common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader}, instance_manager::NetworkInstanceManager, + launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo}, utils::{self, NewFilterSender}, }; @@ -44,6 +42,13 @@ fn parse_network_config(cfg: NetworkConfig) -> Result { Ok(toml.dump()) } +#[tauri::command] +fn generate_network_config(toml_config: String) -> Result { + let config = TomlConfigLoader::new_from_str(&toml_config).map_err(|e| e.to_string())?; + let cfg = NetworkConfig::new_from_config(&config).map_err(|e| e.to_string())?; + Ok(cfg) +} + #[tauri::command] fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> { let instance_id = cfg.instance_id().to_string(); @@ -226,6 +231,7 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ parse_network_config, + generate_network_config, run_network_instance, retain_network_instance, collect_network_infos, diff --git a/easytier-gui/src/App.vue b/easytier-gui/src/App.vue index 818fba851..7088dce67 100644 --- a/easytier-gui/src/App.vue +++ b/easytier-gui/src/App.vue @@ -8,5 +8,6 @@ onBeforeMount(async () => { diff --git a/easytier-gui/src/auto-imports.d.ts b/easytier-gui/src/auto-imports.d.ts index c4a6782c3..18e781e20 100644 --- a/easytier-gui/src/auto-imports.d.ts +++ b/easytier-gui/src/auto-imports.d.ts @@ -23,6 +23,7 @@ declare global { const effectScope: typeof import('vue')['effectScope'] const event2human: typeof import('./composables/utils')['event2human'] const generateMenuItem: typeof import('./composables/tray')['generateMenuItem'] + const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig'] const getActivePinia: typeof import('pinia')['getActivePinia'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] @@ -134,6 +135,7 @@ declare module 'vue' { readonly defineStore: UnwrapRef readonly effectScope: UnwrapRef readonly generateMenuItem: UnwrapRef + readonly generateNetworkConfig: UnwrapRef readonly getActivePinia: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef diff --git a/easytier-gui/src/composables/network.ts b/easytier-gui/src/composables/network.ts index d603695e6..e0cc99460 100644 --- a/easytier-gui/src/composables/network.ts +++ b/easytier-gui/src/composables/network.ts @@ -8,6 +8,10 @@ export async function parseNetworkConfig(cfg: NetworkConfig) { return invoke('parse_network_config', { cfg }) } +export async function generateNetworkConfig(tomlConfig: string) { + return invoke('generate_network_config', { tomlConfig }) +} + export async function runNetworkInstance(cfg: NetworkConfig) { return invoke('run_network_instance', { cfg }) } diff --git a/easytier-gui/src/pages/index.vue b/easytier-gui/src/pages/index.vue index 0ba8b26c9..80fafa5c4 100644 --- a/easytier-gui/src/pages/index.vue +++ b/easytier-gui/src/pages/index.vue @@ -8,7 +8,7 @@ import { exit } from '@tauri-apps/plugin-process' import { open } from '@tauri-apps/plugin-shell' import TieredMenu from 'primevue/tieredmenu' import { useToast } from 'primevue/usetoast' -import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib' +import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib' import { isAutostart, setLoggingLevel } from '~/composables/network' import { useTray } from '~/composables/tray' @@ -23,7 +23,7 @@ useTray(true) const items = ref([ { - label: () => t('show_config'), + label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'), icon: 'pi pi-file-edit', command: async () => { try { @@ -262,6 +262,13 @@ onMounted(async () => { function isRunning(id: string) { return networkStore.networkInstanceIds.includes(id) } + +async function saveTomlConfig(tomlConfig: string) { + const config = await generateNetworkConfig(tomlConfig) + networkStore.replaceCurNetwork(config); + toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 }) + visible.value = false +} + diff --git a/easytier-web/frontend-lib/src/components/index.ts b/easytier-web/frontend-lib/src/components/index.ts index 26280255b..9d26d14a7 100644 --- a/easytier-web/frontend-lib/src/components/index.ts +++ b/easytier-web/frontend-lib/src/components/index.ts @@ -1,2 +1,3 @@ export { default as Config } from './Config.vue'; export { default as Status } from './Status.vue'; +export { default as ConfigEditDialog } from './ConfigEditDialog.vue'; diff --git a/easytier-web/frontend-lib/src/easytier-frontend-lib.ts b/easytier-web/frontend-lib/src/easytier-frontend-lib.ts index a6ed27c8f..4a69aeb62 100644 --- a/easytier-web/frontend-lib/src/easytier-frontend-lib.ts +++ b/easytier-web/frontend-lib/src/easytier-frontend-lib.ts @@ -1,7 +1,7 @@ import './style.css' import type { App } from 'vue'; -import { Config, Status } from "./components"; +import { Config, Status, ConfigEditDialog } from "./components"; import Aura from '@primevue/themes/aura' import PrimeVue from 'primevue/config' @@ -41,10 +41,11 @@ export default { }); app.component('Config', Config); + app.component('ConfigEditDialog', ConfigEditDialog); app.component('Status', Status); app.component('HumanEvent', HumanEvent); app.directive('tooltip', vTooltip as any); } }; -export { Config, Status, I18nUtils, NetworkTypes, Api, Utils }; +export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils }; diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 492bea168..bb1988190 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -51,7 +51,11 @@ dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名 off_text: 点击关闭 on_text: 点击开启 show_config: 显示配置 +edit_config: 编辑配置文件 +config_file: 配置文件 close: 关闭 +save: 保存 +config_saved: 配置已保存 use_latency_first: 延迟优先模式 my_node_info: 当前节点信息 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index bfef6e5e4..e89efb7a8 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -52,7 +52,11 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n off_text: Press to disable on_text: Press to enable show_config: Show Config +edit_config: Edit Config File +config_file: Config File close: Close +save: Save +config_saved: Configuration saved my_node_info: My Node Info peer_count: Connected upload: Upload diff --git a/easytier-web/frontend-lib/src/modules/api.ts b/easytier-web/frontend-lib/src/modules/api.ts index 699a79a87..5a0a1fbe1 100644 --- a/easytier-web/frontend-lib/src/modules/api.ts +++ b/easytier-web/frontend-lib/src/modules/api.ts @@ -47,6 +47,15 @@ export interface GenerateConfigResponse { error?: string; } +export interface ParseConfigRequest { + toml_config: string; +} + +export interface ParseConfigResponse { + config?: NetworkConfig; + error?: string; +} + export class ApiClient { private client: AxiosInstance; private authFailedCb: Function | undefined; @@ -215,6 +224,18 @@ export class ApiClient { return { error: 'Unknown error: ' + error }; } } + + public async parse_config(config: ParseConfigRequest): Promise { + try { + const response = await this.client.post('/parse-config', config); + return response; + } catch (error) { + if (error instanceof AxiosError) { + return { error: error.response?.data }; + } + return { error: 'Unknown error: ' + error }; + } + } } export default ApiClient; \ No newline at end of file diff --git a/easytier-web/frontend/src/components/ConfigGenerator.vue b/easytier-web/frontend/src/components/ConfigGenerator.vue index e9a8f7c46..dbfdaae12 100644 --- a/easytier-web/frontend/src/components/ConfigGenerator.vue +++ b/easytier-web/frontend/src/components/ConfigGenerator.vue @@ -2,12 +2,11 @@ import { NetworkTypes } from 'easytier-frontend-lib'; import {computed, ref} from 'vue'; import { Api } from 'easytier-frontend-lib' -import {AutoComplete, Divider} from "primevue"; +import {AutoComplete, Divider, Button, Textarea} from "primevue"; import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host" const api = computed(() => new Api.ApiClient(apiHost.value)); - const apiHost = ref(getInitialApiHost()) const apiHostSuggestions = ref>([]) const apiHostSearch = async (event: { query: string }) => { @@ -22,21 +21,44 @@ const apiHostSearch = async (event: { query: string }) => { } const newNetworkConfig = ref(NetworkTypes.DEFAULT_NETWORK_CONFIG()); -const toml_config = ref("Press 'Run Network' to generate TOML configuration"); +const toml_config = ref(""); +const errorMessage = ref(""); const generateConfig = (config: NetworkTypes.NetworkConfig) => { saveApiHost(apiHost.value) + errorMessage.value = ""; api.value?.generate_config({ config: config }).then((res) => { if (res.error) { - toml_config.value = res.error; + errorMessage.value = "Generation failed: " + res.error; } else if (res.toml_config) { toml_config.value = res.toml_config; } else { - toml_config.value = "Api server returned an unexpected response"; + errorMessage.value = "Api server returned an unexpected response"; } + }).catch(err => { + errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err)); + }); +}; + +const parseConfig = async () => { + try { + errorMessage.value = ""; + const res = await api.value?.parse_config({ + toml_config: toml_config.value }); + + if (res.error) { + errorMessage.value = "Parse failed: " + res.error; + } else if (res.config) { + newNetworkConfig.value = res.config; + } else { + errorMessage.value = "API returned an unexpected response"; + } + } catch (e) { + errorMessage.value = "Parse request failed: " + (e instanceof Error ? e.message : String(e)); + } }; @@ -55,8 +77,17 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => { -
-
{{ toml_config }}
+
+
{{ errorMessage }}
+ +
+
diff --git a/easytier-web/frontend/src/components/DeviceManagement.vue b/easytier-web/frontend/src/components/DeviceManagement.vue index 64f363fcf..a9549c842 100644 --- a/easytier-web/frontend/src/components/DeviceManagement.vue +++ b/easytier-web/frontend/src/components/DeviceManagement.vue @@ -1,6 +1,6 @@