From 9779923b87d1f01517e0ce599a6288a815f6d1f5 Mon Sep 17 00:00:00 2001 From: "sijie.sun" Date: Sat, 23 Sep 2023 01:53:45 +0000 Subject: [PATCH] Initial Version --- .cargo/config | 3 + .gitignore | 26 + .gitmodules | 0 Cargo.toml | 17 + LICENSE | 73 +++ README.md | 78 +++ easytier-cli/Cargo.toml | 15 + easytier-cli/src/main.rs | 171 ++++++ easytier-core/Cargo.toml | 105 ++++ easytier-core/build.rs | 95 +++ easytier-core/proto/cli.proto | 116 ++++ easytier-core/src/common/config_fs.rs | 161 +++++ easytier-core/src/common/constants.rs | 28 + easytier-core/src/common/error.rs | 43 ++ easytier-core/src/common/global_ctx.rs | 259 ++++++++ easytier-core/src/common/ifcfg.rs | 312 ++++++++++ easytier-core/src/common/mod.rs | 9 + easytier-core/src/common/netns.rs | 118 ++++ easytier-core/src/common/network.rs | 218 +++++++ easytier-core/src/common/rkyv_util.rs | 54 ++ easytier-core/src/common/stun.rs | 433 +++++++++++++ easytier-core/src/connector/direct.rs | 325 ++++++++++ easytier-core/src/connector/manual.rs | 384 ++++++++++++ easytier-core/src/connector/mod.rs | 73 +++ easytier-core/src/connector/udp_hole_punch.rs | 523 ++++++++++++++++ easytier-core/src/gateway/icmp_proxy.rs | 301 +++++++++ easytier-core/src/gateway/mod.rs | 51 ++ easytier-core/src/gateway/tcp_proxy.rs | 402 ++++++++++++ easytier-core/src/instance/instance.rs | 413 +++++++++++++ easytier-core/src/instance/listeners.rs | 150 +++++ easytier-core/src/instance/mod.rs | 4 + easytier-core/src/instance/tun_codec.rs | 179 ++++++ easytier-core/src/instance/virtual_nic.rs | 203 +++++++ easytier-core/src/main.rs | 103 ++++ easytier-core/src/peers/mod.rs | 14 + easytier-core/src/peers/packet.rs | 205 +++++++ easytier-core/src/peers/peer.rs | 218 +++++++ easytier-core/src/peers/peer_conn.rs | 484 +++++++++++++++ easytier-core/src/peers/peer_manager.rs | 539 ++++++++++++++++ easytier-core/src/peers/peer_map.rs | 140 +++++ easytier-core/src/peers/peer_rpc.rs | 509 ++++++++++++++++ easytier-core/src/peers/rip_route.rs | 480 +++++++++++++++ easytier-core/src/peers/route_trait.rs | 36 ++ easytier-core/src/peers/rpc_service.rs | 55 ++ easytier-core/src/peers/tests.rs | 60 ++ easytier-core/src/rpc/cli.rs | 1 + easytier-core/src/rpc/lib.rs | 4 + easytier-core/src/rpc/peer.rs | 20 + easytier-core/src/tests/mod.rs | 174 ++++++ easytier-core/src/tests/three_node.rs | 227 +++++++ easytier-core/src/tunnels/codec.rs | 54 ++ easytier-core/src/tunnels/common.rs | 399 ++++++++++++ easytier-core/src/tunnels/mod.rs | 159 +++++ easytier-core/src/tunnels/ring_tunnel.rs | 391 ++++++++++++ easytier-core/src/tunnels/stats.rs | 101 +++ easytier-core/src/tunnels/tcp_tunnel.rs | 284 +++++++++ easytier-core/src/tunnels/tunnel_filter.rs | 228 +++++++ easytier-core/src/tunnels/udp_tunnel.rs | 574 ++++++++++++++++++ scripts/deploy.sh | 29 + scripts/local_init.sh | 10 + third_party/Packet.dll | Bin 0 -> 220032 bytes third_party/Packet.lib | Bin 0 -> 8290 bytes third_party/wintun.dll | Bin 0 -> 427552 bytes 63 files changed, 10840 insertions(+) create mode 100644 .cargo/config create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 easytier-cli/Cargo.toml create mode 100644 easytier-cli/src/main.rs create mode 100644 easytier-core/Cargo.toml create mode 100644 easytier-core/build.rs create mode 100644 easytier-core/proto/cli.proto create mode 100644 easytier-core/src/common/config_fs.rs create mode 100644 easytier-core/src/common/constants.rs create mode 100644 easytier-core/src/common/error.rs create mode 100644 easytier-core/src/common/global_ctx.rs create mode 100644 easytier-core/src/common/ifcfg.rs create mode 100644 easytier-core/src/common/mod.rs create mode 100644 easytier-core/src/common/netns.rs create mode 100644 easytier-core/src/common/network.rs create mode 100644 easytier-core/src/common/rkyv_util.rs create mode 100644 easytier-core/src/common/stun.rs create mode 100644 easytier-core/src/connector/direct.rs create mode 100644 easytier-core/src/connector/manual.rs create mode 100644 easytier-core/src/connector/mod.rs create mode 100644 easytier-core/src/connector/udp_hole_punch.rs create mode 100644 easytier-core/src/gateway/icmp_proxy.rs create mode 100644 easytier-core/src/gateway/mod.rs create mode 100644 easytier-core/src/gateway/tcp_proxy.rs create mode 100644 easytier-core/src/instance/instance.rs create mode 100644 easytier-core/src/instance/listeners.rs create mode 100644 easytier-core/src/instance/mod.rs create mode 100644 easytier-core/src/instance/tun_codec.rs create mode 100644 easytier-core/src/instance/virtual_nic.rs create mode 100644 easytier-core/src/main.rs create mode 100644 easytier-core/src/peers/mod.rs create mode 100644 easytier-core/src/peers/packet.rs create mode 100644 easytier-core/src/peers/peer.rs create mode 100644 easytier-core/src/peers/peer_conn.rs create mode 100644 easytier-core/src/peers/peer_manager.rs create mode 100644 easytier-core/src/peers/peer_map.rs create mode 100644 easytier-core/src/peers/peer_rpc.rs create mode 100644 easytier-core/src/peers/rip_route.rs create mode 100644 easytier-core/src/peers/route_trait.rs create mode 100644 easytier-core/src/peers/rpc_service.rs create mode 100644 easytier-core/src/peers/tests.rs create mode 100644 easytier-core/src/rpc/cli.rs create mode 100644 easytier-core/src/rpc/lib.rs create mode 100644 easytier-core/src/rpc/peer.rs create mode 100644 easytier-core/src/tests/mod.rs create mode 100644 easytier-core/src/tests/three_node.rs create mode 100644 easytier-core/src/tunnels/codec.rs create mode 100644 easytier-core/src/tunnels/common.rs create mode 100644 easytier-core/src/tunnels/mod.rs create mode 100644 easytier-core/src/tunnels/ring_tunnel.rs create mode 100644 easytier-core/src/tunnels/stats.rs create mode 100644 easytier-core/src/tunnels/tcp_tunnel.rs create mode 100644 easytier-core/src/tunnels/tunnel_filter.rs create mode 100644 easytier-core/src/tunnels/udp_tunnel.rs create mode 100644 scripts/deploy.sh create mode 100644 scripts/local_init.sh create mode 100644 third_party/Packet.dll create mode 100644 third_party/Packet.lib create mode 100644 third_party/wintun.dll diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..6b80765 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,3 @@ +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa0a365 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ +target-*/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.vscode + +# perf & flamegraph +perf.data +perf.data.old +flamegraph.svg + +root-target + +nohup.out diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6ac2945 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] +resolver= "2" +members = [ + "easytier-core", + "easytier-cli" +] + +default-members = [ + "easytier-core", + "easytier-cli" +] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8cd40f3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0135e6 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# EasyTier + +```diff +! NOTICE: THIS SOFTWARE IS STILL BEGIN DEVELOPPING, ONLY POC VERSION IS PROVIDED +``` + +A simple out-of-box alternative of ZeroTier & TailScale in rust. Bring all devices to one virtual net. + +this software can serve as a substitute for Zerotier or TailScale in certain scenarios due to following features: + +1. Easy to deploy. + + No roles (moons/derp or other dedicated relay server). All nodes are equal and rely on some p2p algorithms to communicate. + +2. Smart route decision. + + Use links charged by traffic to reduce latency but free links for high throughout app. + +3. Break the UDP throttling. + + Try use TCP to achieve better performance under enviraonment with throttled UDP. Also use some methods to avoid HOL-blocking of TCP over TCP. + +4. High availibility. + + Support multipath and switching to healthy paths when high packet loss rate or network error are detected + +EasyTIer also have following common features which are already supported by other softwares, but may be easier to use. + +1. Multi-platform support. + +2. Effcient P2P hole punching, both UDP & TCP. + +3. High performance. Try use multiple wan interface. + +5. Subnet Route. node can advertise and proxy one or more subnets, so other nodes can directy access these subnets without any iptables/nft trickys. + + +# Usage + +Currently a server with public ip is needed. + +run first node on the public node: + +``` +sudo easytier-core --ipv4 +``` + +run other nodes + +``` +sudo easytier-core --ipv4 --peers tcp://:11010 +``` + +use cli tool to inspect nodes with direct link. + +``` +easytier-cli peer +``` + +cli tool can also be used to inspect the route table + +``` +easytier-cli route +``` + + +# RoadMap + +- [x] Windows / Mac / Linux support +- [x] TCP / UDP tunnel. +- [x] NAT Traverse with relaying. +- [x] NAT Traverse with UDP hole punching. + +- [ ] Support shared public server. So users can use it without a public server. +- [ ] Encryption. With noise framework or other method. +- [ ] Support mobile platforms. +- [ ] Broadcast & Multicast support. +- [ ] UI tools. diff --git a/easytier-cli/Cargo.toml b/easytier-cli/Cargo.toml new file mode 100644 index 0000000..9ffd316 --- /dev/null +++ b/easytier-cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "easytier-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3" +tokio = { version = "1", features = ["full"] } +anyhow = "1.0" +easytier-core = { path = "../easytier-core" } +clap = { version = "4.4.8", features = ["unicode", "derive", "wrap_help"] } +tonic = "0.10" +thiserror = "1.0" diff --git a/easytier-cli/src/main.rs b/easytier-cli/src/main.rs new file mode 100644 index 0000000..15b96e7 --- /dev/null +++ b/easytier-cli/src/main.rs @@ -0,0 +1,171 @@ +use clap::{command, Args, Parser, Subcommand}; +use easytier_rpc::{ + connector_manage_rpc_client::ConnectorManageRpcClient, + peer_manage_rpc_client::PeerManageRpcClient, ListConnectorRequest, ListPeerRequest, + ListRouteRequest, +}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// the instance name + #[arg(short = 'n', long, default_value = "default")] + instance_name: String, + + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum SubCommand { + Peer(PeerArgs), + Connector(ConnectorArgs), + Route, +} + +#[derive(Args, Debug)] +struct PeerArgs { + #[arg(short, long)] + ipv4: Option, + + #[arg(short, long)] + peers: Vec, + + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum PeerSubCommand { + Add, + Remove, + List, +} + +#[derive(Args, Debug)] +struct ConnectorArgs { + #[arg(short, long)] + ipv4: Option, + + #[arg(short, long)] + peers: Vec, + + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum ConnectorSubCommand { + Add, + Remove, + List, +} + +#[derive(thiserror::Error, Debug)] +enum Error { + #[error("tonic transport error")] + TonicTransportError(#[from] tonic::transport::Error), + #[error("tonic rpc error")] + TonicRpcError(#[from] tonic::Status), +} + +struct CommandHandler { + addr: String, +} + +impl CommandHandler { + async fn get_peer_manager_client( + &self, + ) -> Result, Error> { + Ok(PeerManageRpcClient::connect(self.addr.clone()).await?) + } + + async fn get_connector_manager_client( + &self, + ) -> Result, Error> { + Ok(ConnectorManageRpcClient::connect(self.addr.clone()).await?) + } + + #[allow(dead_code)] + fn handle_peer_add(&self, _args: PeerArgs) { + println!("add peer"); + } + + #[allow(dead_code)] + fn handle_peer_remove(&self, _args: PeerArgs) { + println!("remove peer"); + } + + async fn handle_peer_list(&self, _args: PeerArgs) -> Result<(), Error> { + let mut client = self.get_peer_manager_client().await?; + let request = tonic::Request::new(ListPeerRequest::default()); + let response = client.list_peer(request).await?; + println!("response: {:#?}", response.into_inner()); + Ok(()) + } + + async fn handle_route_list(&self) -> Result<(), Error> { + let mut client = self.get_peer_manager_client().await?; + let request = tonic::Request::new(ListRouteRequest::default()); + let response = client.list_route(request).await?; + println!("response: {:#?}", response.into_inner()); + Ok(()) + } + + async fn handle_connector_list(&self) -> Result<(), Error> { + let mut client = self.get_connector_manager_client().await?; + let request = tonic::Request::new(ListConnectorRequest::default()); + let response = client.list_connector(request).await?; + println!("response: {:#?}", response.into_inner()); + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let cli = Cli::parse(); + println!("cli: {:?}", cli); + + let handler = CommandHandler { + addr: "http://127.0.0.1:15888".to_string(), + }; + + match cli.sub_command { + Some(SubCommand::Peer(peer_args)) => match peer_args.sub_command { + Some(PeerSubCommand::Add) => { + println!("add peer"); + } + Some(PeerSubCommand::Remove) => { + println!("remove peer"); + } + Some(PeerSubCommand::List) => { + handler.handle_peer_list(peer_args).await?; + } + None => { + handler.handle_peer_list(peer_args).await?; + } + }, + Some(SubCommand::Connector(conn_args)) => match conn_args.sub_command { + Some(ConnectorSubCommand::Add) => { + println!("add connector"); + } + Some(ConnectorSubCommand::Remove) => { + println!("remove connector"); + } + Some(ConnectorSubCommand::List) => { + handler.handle_connector_list().await?; + } + None => { + handler.handle_connector_list().await?; + } + }, + Some(SubCommand::Route) => { + handler.handle_route_list().await?; + } + None => { + println!("list peer"); + } + } + + Ok(()) +} diff --git a/easytier-core/Cargo.toml b/easytier-core/Cargo.toml new file mode 100644 index 0000000..601cadc --- /dev/null +++ b/easytier-core/Cargo.toml @@ -0,0 +1,105 @@ +[package] +name = "easytier-core" +version = "0.1.0" +edition = "2021" +authors = ["easytier"] +rust-version = "1.75" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "easytier_rpc" +path = "src/rpc/lib.rs" + +[dependencies] +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2.3" +log = "0.4" +thiserror = "1.0" +auto_impl = "1.1.0" +crossbeam = "0.8.4" + +gethostname = "0.4.3" + +futures = "0.3" + +tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1" +tokio-util = { version = "0.7.9", features = ["codec", "net"] } + +async-stream = "0.3.5" +async-trait = "0.1.74" + +dashmap = "5.5.3" +timedmap = "1.0.1" + +# for tap device +tun = { version = "0.6.1", features = ["async"] } +# for net ns +nix = { version = "0.27", features = ["sched", "socket", "ioctl"] } + +uuid = { version = "1.5.0", features = [ + "v4", + "fast-rng", + "macro-diagnostics", + "serde", +] } + +# for ring tunnel +crossbeam-queue = "0.3" +once_cell = "1.18.0" + +# for packet +rkyv = { "version" = "0.7.42", features = ["validation", "archive_le"] } + +# for rpc +tonic = "0.10" +prost = "0.12" +anyhow = "1.0" +tarpc = { version = "0.32", features = ["tokio1", "serde1"] } +bincode = "1.3" + +url = "2.5.0" + +# for tun packet +byteorder = "1.5.0" + +# for proxy +cidr = "0.2.2" +socket2 = "0.5.5" + +# for hole punching +stun-format = { git = "https://github.com/KKRainbow/stun-format.git", features = [ + "fmt", + "rfc3489", + "iana", +] } +rand = "0.8.5" + +[dependencies.serde] +version = "1.0" +features = ["derive"] + +[dependencies.pnet] +version = "0.34.0" +features = ["serde"] + +[dependencies.clap] +version = "4.4" +features = ["derive"] + +[dependencies.public-ip] +version = "0.2" +features = ["default"] + +[build-dependencies] +tonic-build = "0.10" + +[target.'cfg(windows)'.build-dependencies] +reqwest = { version = "0.11", features = ["blocking"] } +zip = "*" + + +[dev-dependencies] +serial_test = "*" diff --git a/easytier-core/build.rs b/easytier-core/build.rs new file mode 100644 index 0000000..e69e706 --- /dev/null +++ b/easytier-core/build.rs @@ -0,0 +1,95 @@ +#[cfg(target_os = "windows")] +use std::{ + env, + fs::File, + io::{copy, Cursor}, + path::PathBuf, +}; + +#[cfg(target_os = "windows")] +struct WindowsBuild {} + +#[cfg(target_os = "windows")] +impl WindowsBuild { + fn check_protoc_exist() -> Option { + let path = env::var_os("PROTOC").map(PathBuf::from); + if path.is_some() && path.as_ref().unwrap().exists() { + return path; + } + + let path = env::var_os("PATH").unwrap_or_default(); + for p in env::split_paths(&path) { + let p = p.join("protoc"); + if p.exists() { + return Some(p); + } + } + + None + } + + fn get_cargo_target_dir() -> Result> { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); + let profile = std::env::var("PROFILE")?; + let mut target_dir = None; + let mut sub_path = out_dir.as_path(); + while let Some(parent) = sub_path.parent() { + if parent.ends_with(&profile) { + target_dir = Some(parent); + break; + } + sub_path = parent; + } + let target_dir = target_dir.ok_or("not found")?; + Ok(target_dir.to_path_buf()) + } + + fn download_protoc() -> PathBuf { + println!("cargo:info=use exist protoc: {:?}", "k"); + let out_dir = Self::get_cargo_target_dir().unwrap(); + let fname = out_dir.join("protoc"); + if fname.exists() { + println!("cargo:info=use exist protoc: {:?}", fname); + return fname; + } + + println!("cargo:info=need download protoc, please wait..."); + + let url = "https://github.com/protocolbuffers/protobuf/releases/download/v26.0-rc1/protoc-26.0-rc-1-win64.zip"; + let response = reqwest::blocking::get(url).unwrap(); + println!("{:?}", response); + let mut content = response + .bytes() + .map(|v| v.to_vec()) + .map(Cursor::new) + .map(zip::ZipArchive::new) + .unwrap() + .unwrap(); + let protoc_zipped_file = content.by_name("bin/protoc.exe").unwrap(); + let mut content = protoc_zipped_file; + + copy(&mut content, &mut File::create(&fname).unwrap()).unwrap(); + + fname + } + + pub fn check_for_win() { + // add third_party dir to link search path + println!("cargo:rustc-link-search=native=third_party/"); + let protoc_path = if let Some(o) = Self::check_protoc_exist() { + println!("cargo:info=use os exist protoc: {:?}", o); + o + } else { + Self::download_protoc() + }; + std::env::set_var("PROTOC", protoc_path); + } +} + +fn main() -> Result<(), Box> { + #[cfg(target_os = "windows")] + WindowsBuild::check_for_win(); + + tonic_build::compile_protos("proto/cli.proto")?; + Ok(()) +} diff --git a/easytier-core/proto/cli.proto b/easytier-core/proto/cli.proto new file mode 100644 index 0000000..a72f63d --- /dev/null +++ b/easytier-core/proto/cli.proto @@ -0,0 +1,116 @@ +syntax = "proto3"; +package cli; + +message Status { + int32 code = 1; + string message = 2; +} + +message PeerConnStats { + uint64 rx_bytes = 1; + uint64 tx_bytes = 2; + + uint64 rx_packets = 3; + uint64 tx_packets = 4; + + uint64 latency_us = 5; +} + +message TunnelInfo { + string tunnel_type = 1; + string local_addr = 2; + string remote_addr = 3; +} + +message PeerConnInfo { + string conn_id = 1; + string my_node_id = 2; + string peer_id = 3; + repeated string features = 4; + TunnelInfo tunnel = 5; + PeerConnStats stats = 6; +} + +message PeerInfo { + string peer_id = 1; + repeated PeerConnInfo conns = 2; +} + +message ListPeerRequest {} + +message ListPeerResponse { + repeated PeerInfo peer_infos = 1; +} + +enum NatType { + // has NAT; but own a single public IP, port is not changed + Unknown = 0; + OpenInternet = 1; + NoPAT = 2; + FullCone = 3; + Restricted = 4; + PortRestricted = 5; + Symmetric = 6; + SymUdpFirewall = 7; +} + +message StunInfo { + NatType udp_nat_type = 1; + NatType tcp_nat_type = 2; + int64 last_update_time = 3; +} + +message Route { + string peer_id = 1; + string ipv4_addr = 2; + string next_hop_peer_id = 3; + int32 cost = 4; + repeated string proxy_cidrs = 5; + string hostname = 6; + StunInfo stun_info = 7; +} + +message ListRouteRequest {} + +message ListRouteResponse { + repeated Route routes = 1; +} + +service PeerManageRpc { + rpc ListPeer (ListPeerRequest) returns (ListPeerResponse); + rpc ListRoute (ListRouteRequest) returns (ListRouteResponse); +} + +enum ConnectorStatus { + CONNECTED = 0; + DISCONNECTED = 1; + CONNECTING = 2; +} + +message Connector { + string url = 1; + ConnectorStatus status = 2; +} + +message ListConnectorRequest {} + +message ListConnectorResponse { + repeated Connector connectors = 1; +} + +enum ConnectorManageAction { + ADD = 0; + REMOVE = 1; +} + +message ManageConnectorRequest { + ConnectorManageAction action = 1; + string url = 2; +} + +message ManageConnectorResponse { } + +service ConnectorManageRpc { + rpc ListConnector (ListConnectorRequest) returns (ListConnectorResponse); + rpc ManageConnector (ManageConnectorRequest) returns (ManageConnectorResponse); +} diff --git a/easytier-core/src/common/config_fs.rs b/easytier-core/src/common/config_fs.rs new file mode 100644 index 0000000..7022143 --- /dev/null +++ b/easytier-core/src/common/config_fs.rs @@ -0,0 +1,161 @@ +// use filesystem as a config store + +use std::{ + ffi::OsStr, + io::Write, + path::{Path, PathBuf}, +}; + +static DEFAULT_BASE_DIR: &str = "/var/lib/easytier"; +static DIR_ROOT_CONFIG_FILE_NAME: &str = "__root__"; + +pub struct ConfigFs { + _db_name: String, + db_path: PathBuf, +} + +impl ConfigFs { + pub fn new(db_name: &str) -> Self { + Self::new_with_dir(db_name, DEFAULT_BASE_DIR) + } + + pub fn new_with_dir(db_name: &str, dir: &str) -> Self { + let p = Path::new(OsStr::new(dir)).join(OsStr::new(db_name)); + std::fs::create_dir_all(&p).unwrap(); + ConfigFs { + _db_name: db_name.to_string(), + db_path: p, + } + } + + pub fn get(&self, key: &str) -> Result { + let path = self.db_path.join(OsStr::new(key)); + // if path is dir, read the DIR_ROOT_CONFIG_FILE_NAME in it + if path.is_dir() { + let path = path.join(OsStr::new(DIR_ROOT_CONFIG_FILE_NAME)); + std::fs::read_to_string(path) + } else if path.is_file() { + return std::fs::read_to_string(path); + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "key not found", + )); + } + } + + pub fn list_keys(&self, key: &str) -> Result, std::io::Error> { + let path = self.db_path.join(OsStr::new(key)); + let mut keys = Vec::new(); + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + let key = path.file_name().unwrap().to_str().unwrap().to_string(); + if key != DIR_ROOT_CONFIG_FILE_NAME { + keys.push(key); + } + } + Ok(keys) + } + + #[allow(dead_code)] + pub fn remove(&self, key: &str) -> Result<(), std::io::Error> { + let path = self.db_path.join(OsStr::new(key)); + // if path is dir, remove the DIR_ROOT_CONFIG_FILE_NAME in it + if path.is_dir() { + std::fs::remove_dir_all(path) + } else if path.is_file() { + return std::fs::remove_file(path); + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "key not found", + )); + } + } + + pub fn add_dir(&self, key: &str) -> Result { + let path = self.db_path.join(OsStr::new(key)); + // if path is dir, write the DIR_ROOT_CONFIG_FILE_NAME in it + if path.is_file() { + Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "key already exists", + )) + } else { + std::fs::create_dir_all(&path)?; + return std::fs::File::create(path.join(OsStr::new(DIR_ROOT_CONFIG_FILE_NAME))); + } + } + + pub fn add_file(&self, key: &str) -> Result { + let path = self.db_path.join(OsStr::new(key)); + let base_dir = path.parent().unwrap(); + if !path.is_file() { + std::fs::create_dir_all(base_dir)?; + } + std::fs::File::create(path) + } + + pub fn get_or_add( + &self, + key: &str, + val_fn: F, + add_dir: bool, + ) -> Result + where + F: FnOnce() -> String, + { + let get_ret = self.get(key); + match get_ret { + Ok(v) => Ok(v), + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + let val = val_fn(); + if add_dir { + let mut f = self.add_dir(key)?; + f.write_all(val.as_bytes())?; + } else { + let mut f = self.add_file(key)?; + f.write_all(val.as_bytes())?; + } + Ok(val) + } else { + Err(e) + } + } + } + } + + #[allow(dead_code)] + pub fn get_or_add_dir(&self, key: &str, val_fn: F) -> Result + where + F: FnOnce() -> String, + { + self.get_or_add(key, val_fn, true) + } + + pub fn get_or_add_file(&self, key: &str, val_fn: F) -> Result + where + F: FnOnce() -> String, + { + self.get_or_add(key, val_fn, false) + } + + pub fn get_or_default(&self, key: &str, default: F) -> Result + where + F: FnOnce() -> String, + { + let get_ret = self.get(key); + match get_ret { + Ok(v) => Ok(v), + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(default()) + } else { + Err(e) + } + } + } + } +} diff --git a/easytier-core/src/common/constants.rs b/easytier-core/src/common/constants.rs new file mode 100644 index 0000000..2f36d4a --- /dev/null +++ b/easytier-core/src/common/constants.rs @@ -0,0 +1,28 @@ +pub const DIRECT_CONNECTOR_SERVICE_ID: u32 = 1; +pub const DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC: u64 = 60; +pub const DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC: u64 = 60; + +macro_rules! define_global_var { + ($name:ident, $type:ty, $init:expr) => { + pub static $name: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new($init)); + }; +} + +#[macro_export] +macro_rules! use_global_var { + ($name:ident) => { + crate::common::constants::$name.lock().await.to_owned() + }; +} + +#[macro_export] +macro_rules! set_global_var { + ($name:ident, $val:expr) => { + *crate::common::constants::$name.lock().await = $val + }; +} + +define_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, u64, 1000); + +pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2; diff --git a/easytier-core/src/common/error.rs b/easytier-core/src/common/error.rs new file mode 100644 index 0000000..c741ed3 --- /dev/null +++ b/easytier-core/src/common/error.rs @@ -0,0 +1,43 @@ +use std::{io, result}; + +use thiserror::Error; + +use crate::tunnels; + +#[derive(Error, Debug)] +pub enum Error { + #[error("io error")] + IOError(#[from] io::Error), + #[error("rust tun error {0}")] + TunError(#[from] tun::Error), + #[error("tunnel error {0}")] + TunnelError(#[from] tunnels::TunnelError), + #[error("Peer has no conn, PeerId: {0}")] + PeerNoConnectionError(uuid::Uuid), + #[error("RouteError: {0}")] + RouteError(String), + #[error("Not found")] + NotFound, + #[error("Invalid Url: {0}")] + InvalidUrl(String), + #[error("Shell Command error: {0}")] + ShellCommandError(String), + // #[error("Rpc listen error: {0}")] + // RpcListenError(String), + #[error("Rpc connect error: {0}")] + RpcConnectError(String), + #[error("Rpc error: {0}")] + RpcClientError(#[from] tarpc::client::RpcError), + #[error("Timeout error: {0}")] + Timeout(#[from] tokio::time::error::Elapsed), + #[error("url in blacklist")] + UrlInBlacklist, + #[error("unknown data store error")] + Unknown, + #[error("anyhow error: {0}")] + AnyhowError(#[from] anyhow::Error), +} + +pub type Result = result::Result; + +// impl From for std:: diff --git a/easytier-core/src/common/global_ctx.rs b/easytier-core/src/common/global_ctx.rs new file mode 100644 index 0000000..eadfa67 --- /dev/null +++ b/easytier-core/src/common/global_ctx.rs @@ -0,0 +1,259 @@ +use std::{io::Write, sync::Arc}; + +use crossbeam::atomic::AtomicCell; +use easytier_rpc::PeerConnInfo; + +use super::{ + config_fs::ConfigFs, + netns::NetNS, + network::IPCollector, + stun::{StunInfoCollector, StunInfoCollectorTrait}, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum GlobalCtxEvent { + PeerAdded, + PeerRemoved, + PeerConnAdded(PeerConnInfo), + PeerConnRemoved(PeerConnInfo), +} + +type EventBus = tokio::sync::broadcast::Sender; +type EventBusSubscriber = tokio::sync::broadcast::Receiver; + +pub struct GlobalCtx { + pub inst_name: String, + pub id: uuid::Uuid, + pub config_fs: ConfigFs, + pub net_ns: NetNS, + + event_bus: EventBus, + + cached_ipv4: AtomicCell>, + cached_proxy_cidrs: AtomicCell>>, + + ip_collector: Arc, + + hotname: AtomicCell>, + + stun_info_collection: Box, +} + +impl std::fmt::Debug for GlobalCtx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GlobalCtx") + .field("inst_name", &self.inst_name) + .field("id", &self.id) + .field("net_ns", &self.net_ns.name()) + .field("event_bus", &"EventBus") + .field("ipv4", &self.cached_ipv4) + .finish() + } +} + +pub type ArcGlobalCtx = std::sync::Arc; + +impl GlobalCtx { + pub fn new(inst_name: &str, config_fs: ConfigFs, net_ns: NetNS) -> Self { + let id = config_fs + .get_or_add_file("inst_id", || uuid::Uuid::new_v4().to_string()) + .unwrap(); + let id = uuid::Uuid::parse_str(&id).unwrap(); + + let (event_bus, _) = tokio::sync::broadcast::channel(100); + + // NOTICE: we may need to choose stun stun server based on geo location + // stun server cross nation may return a external ip address with high latency and loss rate + let default_stun_servers = vec![ + "stun.miwifi.com:3478".to_string(), + "stun.qq.com:3478".to_string(), + "stun.chat.bilibili.com:3478".to_string(), + "fwa.lifesizecloud.com:3478".to_string(), + "stun.isp.net.au:3478".to_string(), + "stun.nextcloud.com:3478".to_string(), + "stun.freeswitch.org:3478".to_string(), + "stun.voip.blackberry.com:3478".to_string(), + "stunserver.stunprotocol.org:3478".to_string(), + "stun.sipnet.com:3478".to_string(), + "stun.radiojar.com:3478".to_string(), + "stun.sonetel.com:3478".to_string(), + "stun.voipgate.com:3478".to_string(), + "stun.counterpath.com:3478".to_string(), + "180.235.108.91:3478".to_string(), + "193.22.2.248:3478".to_string(), + ]; + + GlobalCtx { + inst_name: inst_name.to_string(), + id, + config_fs, + net_ns: net_ns.clone(), + event_bus, + cached_ipv4: AtomicCell::new(None), + cached_proxy_cidrs: AtomicCell::new(None), + + ip_collector: Arc::new(IPCollector::new(net_ns)), + + hotname: AtomicCell::new(None), + + stun_info_collection: Box::new(StunInfoCollector::new(default_stun_servers)), + } + } + + pub fn subscribe(&self) -> EventBusSubscriber { + self.event_bus.subscribe() + } + + pub fn issue_event(&self, event: GlobalCtxEvent) { + if self.event_bus.receiver_count() != 0 { + self.event_bus.send(event).unwrap(); + } else { + log::warn!("No subscriber for event: {:?}", event); + } + } + + pub fn get_ipv4(&self) -> Option { + if let Some(ret) = self.cached_ipv4.load() { + return Some(ret); + } + + let Ok(addr) = self.config_fs.get("ipv4") else { + return None; + }; + + let Ok(addr) = addr.parse() else { + tracing::error!("invalid ipv4 addr: {}", addr); + return None; + }; + + self.cached_ipv4.store(Some(addr)); + return Some(addr); + } + + pub fn set_ipv4(&mut self, addr: std::net::Ipv4Addr) { + self.config_fs + .add_file("ipv4") + .unwrap() + .write_all(addr.to_string().as_bytes()) + .unwrap(); + + self.cached_ipv4.store(None); + } + + pub fn add_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { + let escaped_cidr = cidr.to_string().replace("/", "_"); + self.config_fs + .add_file(&format!("proxy_cidrs/{}", escaped_cidr))?; + self.cached_proxy_cidrs.store(None); + Ok(()) + } + + pub fn remove_proxy_cidr(&self, cidr: cidr::IpCidr) -> Result<(), std::io::Error> { + let escaped_cidr = cidr.to_string().replace("/", "_"); + self.config_fs + .remove(&format!("proxy_cidrs/{}", escaped_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 Ok(keys) = self.config_fs.list_keys("proxy_cidrs") else { + return vec![]; + }; + + let mut ret = Vec::new(); + for key in keys.iter() { + let key = key.replace("_", "/"); + let Ok(cidr) = key.parse() else { + tracing::error!("invalid proxy cidr: {}", key); + continue; + }; + ret.push(cidr); + } + + self.cached_proxy_cidrs.store(Some(ret.clone())); + ret + } + + pub fn get_ip_collector(&self) -> Arc { + self.ip_collector.clone() + } + + pub fn get_hostname(&self) -> Option { + if let Some(hostname) = self.hotname.take() { + self.hotname.store(Some(hostname.clone())); + return Some(hostname); + } + + let hostname = gethostname::gethostname().to_string_lossy().to_string(); + self.hotname.store(Some(hostname.clone())); + return Some(hostname); + } + + pub fn get_stun_info_collector(&self) -> impl StunInfoCollectorTrait + '_ { + self.stun_info_collection.as_ref() + } + + #[cfg(test)] + pub fn replace_stun_info_collector(&self, collector: Box) { + // force replace the stun_info_collection without mut and drop the old one + let ptr = &self.stun_info_collection as *const Box; + let ptr = ptr as *mut Box; + unsafe { + std::ptr::drop_in_place(ptr); + std::ptr::write(ptr, collector); + } + } + + pub fn get_id(&self) -> uuid::Uuid { + self.id + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[tokio::test] + async fn test_global_ctx() { + let config_fs = ConfigFs::new("/tmp/easytier"); + let net_ns = NetNS::new(None); + let global_ctx = GlobalCtx::new("test", config_fs, net_ns); + + let mut subscriber = global_ctx.subscribe(); + global_ctx.issue_event(GlobalCtxEvent::PeerAdded); + global_ctx.issue_event(GlobalCtxEvent::PeerRemoved); + global_ctx.issue_event(GlobalCtxEvent::PeerConnAdded(PeerConnInfo::default())); + global_ctx.issue_event(GlobalCtxEvent::PeerConnRemoved(PeerConnInfo::default())); + + assert_eq!(subscriber.recv().await.unwrap(), GlobalCtxEvent::PeerAdded); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerRemoved + ); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerConnAdded(PeerConnInfo::default()) + ); + assert_eq!( + subscriber.recv().await.unwrap(), + GlobalCtxEvent::PeerConnRemoved(PeerConnInfo::default()) + ); + } + + pub fn get_mock_global_ctx() -> ArcGlobalCtx { + let node_id = uuid::Uuid::new_v4(); + let config_fs = ConfigFs::new_with_dir(node_id.to_string().as_str(), "/tmp/easytier"); + let net_ns = NetNS::new(None); + std::sync::Arc::new(GlobalCtx::new( + format!("test_{}", node_id).as_str(), + config_fs, + net_ns, + )) + } +} diff --git a/easytier-core/src/common/ifcfg.rs b/easytier-core/src/common/ifcfg.rs new file mode 100644 index 0000000..a50324d --- /dev/null +++ b/easytier-core/src/common/ifcfg.rs @@ -0,0 +1,312 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; +use tokio::process::Command; + +use super::error::Error; + +#[async_trait] +pub trait IfConfiguerTrait { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error>; + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error>; + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error>; + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error>; + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error>; + async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> { + return Ok(()); + } +} + +fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr { + if prefix_length > 32 { + panic!("Invalid CIDR prefix length"); + } + + let subnet_mask: u32 = (!0u32) + .checked_shl(32 - u32::from(prefix_length)) + .unwrap_or(0); + Ipv4Addr::new( + ((subnet_mask >> 24) & 0xFF) as u8, + ((subnet_mask >> 16) & 0xFF) as u8, + ((subnet_mask >> 8) & 0xFF) as u8, + (subnet_mask & 0xFF) as u8, + ) +} + +async fn run_shell_cmd(cmd: &str) -> Result<(), Error> { + let cmd_out = if cfg!(target_os = "windows") { + Command::new("cmd").arg("/C").arg(cmd).output().await? + } else { + Command::new("sh").arg("-c").arg(cmd).output().await? + }; + let stdout = String::from_utf8_lossy(cmd_out.stdout.as_slice()); + let stderr = String::from_utf8_lossy(cmd_out.stderr.as_slice()); + let ec = cmd_out.status.code(); + let succ = cmd_out.status.success(); + tracing::info!(?cmd, ?ec, ?succ, ?stdout, ?stderr, "run shell cmd"); + + if !cmd_out.status.success() { + return Err(Error::ShellCommandError( + stdout.to_string() + &stderr.to_string(), + )); + } + Ok(()) +} + +pub struct MacIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for MacIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n add {} -netmask {} -interface {} -hopcount 7", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n delete {} -netmask {} -interface {}", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "ifconfig {} {:?}/{:?} 10.8.8.8 up", + name, address, cidr_prefix, + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd(format!("ifconfig {} {}", name, if up { "up" } else { "down" }).as_str()) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await + } else { + run_shell_cmd( + format!("ifconfig {} inet {} delete", name, ip.unwrap().to_string()).as_str(), + ) + .await + } + } +} + +pub struct LinuxIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for LinuxIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "ip route add {}/{} dev {} metric 65535", + address, cidr_prefix, name + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd(format!("ip route del {}/{} dev {}", address, cidr_prefix, name).as_str()) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd(format!("ip addr add {:?}/{:?} dev {}", address, cidr_prefix, name).as_str()) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd(format!("ip link set {} {}", name, if up { "up" } else { "down" }).as_str()) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + run_shell_cmd(format!("ip addr flush dev {}", name).as_str()).await + } else { + run_shell_cmd( + format!("ip addr del {:?} dev {}", ip.unwrap().to_string(), name).as_str(), + ) + .await + } + } +} + +pub struct WindowsIfConfiger {} + +#[async_trait] +impl IfConfiguerTrait for WindowsIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route add {} mask {} {}", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route delete {} mask {} {}", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface ipv4 add address {} address={} mask={}", + name, + address, + cidr_to_subnet_mask(cidr_prefix) + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface set interface {} {}", + name, + if up { "enable" } else { "disable" } + ) + .as_str(), + ) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + run_shell_cmd(format!("netsh interface ipv4 delete address {}", name).as_str()).await + } else { + run_shell_cmd( + format!( + "netsh interface ipv4 delete address {} address={}", + name, + ip.unwrap().to_string() + ) + .as_str(), + ) + .await + } + } + + async fn wait_interface_show(&self, name: &str) -> Result<(), Error> { + Ok( + tokio::time::timeout(std::time::Duration::from_secs(10), async move { + loop { + let Ok(_) = run_shell_cmd( + format!("netsh interface ipv4 show interfaces {}", name).as_str(), + ) + .await + else { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + continue; + }; + break; + } + }) + .await?, + ) + } +} + +#[cfg(target_os = "macos")] +pub type IfConfiger = MacIfConfiger; + +#[cfg(target_os = "linux")] +pub type IfConfiger = LinuxIfConfiger; + +#[cfg(target_os = "windows")] +pub type IfConfiger = WindowsIfConfiger; diff --git a/easytier-core/src/common/mod.rs b/easytier-core/src/common/mod.rs new file mode 100644 index 0000000..54eed4c --- /dev/null +++ b/easytier-core/src/common/mod.rs @@ -0,0 +1,9 @@ +pub mod config_fs; +pub mod constants; +pub mod error; +pub mod global_ctx; +pub mod ifcfg; +pub mod netns; +pub mod network; +pub mod rkyv_util; +pub mod stun; diff --git a/easytier-core/src/common/netns.rs b/easytier-core/src/common/netns.rs new file mode 100644 index 0000000..71c4a4b --- /dev/null +++ b/easytier-core/src/common/netns.rs @@ -0,0 +1,118 @@ +use futures::Future; +use once_cell::sync::Lazy; +use tokio::sync::Mutex; + +#[cfg(target_os = "linux")] +use nix::sched::{setns, CloneFlags}; +#[cfg(target_os = "linux")] +use std::os::fd::AsFd; + +pub struct NetNSGuard { + #[cfg(target_os = "linux")] + old_ns: Option, +} + +type NetNSLock = Mutex<()>; +static LOCK: Lazy = Lazy::new(|| Mutex::new(())); +pub static ROOT_NETNS_NAME: &str = "_root_ns"; + +#[cfg(target_os = "linux")] +impl NetNSGuard { + pub fn new(ns: Option) -> Box { + let old_ns = if ns.is_some() { + let old_ns = if cfg!(target_os = "linux") { + Some(std::fs::File::open("/proc/self/ns/net").unwrap()) + } else { + None + }; + Self::switch_ns(ns); + old_ns + } else { + None + }; + Box::new(NetNSGuard { old_ns }) + } + + fn switch_ns(name: Option) { + if name.is_none() { + return; + } + + let ns_path: String; + let name = name.unwrap(); + if name == ROOT_NETNS_NAME { + ns_path = "/proc/1/ns/net".to_string(); + } else { + ns_path = format!("/var/run/netns/{}", name); + } + + let ns = std::fs::File::open(ns_path).unwrap(); + log::info!( + "[INIT NS] switching to new ns_name: {:?}, ns_file: {:?}", + name, + ns + ); + + setns(ns.as_fd(), CloneFlags::CLONE_NEWNET).unwrap(); + } +} + +#[cfg(target_os = "linux")] +impl Drop for NetNSGuard { + fn drop(&mut self) { + if self.old_ns.is_none() { + return; + } + log::info!("[INIT NS] switching back to old ns, ns: {:?}", self.old_ns); + setns( + self.old_ns.as_ref().unwrap().as_fd(), + CloneFlags::CLONE_NEWNET, + ) + .unwrap(); + } +} + +#[cfg(not(target_os = "linux"))] +impl NetNSGuard { + pub fn new(_ns: Option) -> Box { + Box::new(NetNSGuard {}) + } +} + +#[derive(Clone)] +pub struct NetNS { + name: Option, +} + +impl NetNS { + pub fn new(name: Option) -> Self { + NetNS { name } + } + + pub async fn run_async(&self, f: F) -> Ret + where + F: FnOnce() -> Fut, + Fut: Future, + { + // TODO: do we really need this lock + // let _lock = LOCK.lock().await; + let _guard = NetNSGuard::new(self.name.clone()); + f().await + } + + pub fn run(&self, f: F) -> Ret + where + F: FnOnce() -> Ret, + { + let _guard = NetNSGuard::new(self.name.clone()); + f() + } + + pub fn guard(&self) -> Box { + NetNSGuard::new(self.name.clone()) + } + + pub fn name(&self) -> Option { + self.name.clone() + } +} diff --git a/easytier-core/src/common/network.rs b/easytier-core/src/common/network.rs new file mode 100644 index 0000000..ded53d4 --- /dev/null +++ b/easytier-core/src/common/network.rs @@ -0,0 +1,218 @@ +use std::{ops::Deref, sync::Arc}; + +use easytier_rpc::peer::GetIpListResponse; +use pnet::datalink::NetworkInterface; +use tokio::{ + sync::{Mutex, RwLock}, + task::JoinSet, +}; + +use super::{constants::DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC, netns::NetNS}; + +struct InterfaceFilter { + iface: NetworkInterface, +} + +#[cfg(target_os = "linux")] +impl InterfaceFilter { + async fn is_iface_bridge(&self) -> bool { + let path = format!("/sys/class/net/{}/bridge", self.iface.name); + tokio::fs::metadata(&path).await.is_ok() + } + + async fn is_iface_phsical(&self) -> bool { + let path = format!("/sys/class/net/{}/device", self.iface.name); + tokio::fs::metadata(&path).await.is_ok() + } + + async fn filter_iface(&self) -> bool { + tracing::trace!( + "filter linux iface: {:?}, is_point_to_point: {}, is_loopback: {}, is_up: {}, is_lower_up: {}, is_bridge: {}, is_physical: {}", + self.iface, + self.iface.is_point_to_point(), + self.iface.is_loopback(), + self.iface.is_up(), + self.iface.is_lower_up(), + self.is_iface_bridge().await, + self.is_iface_phsical().await, + ); + + !self.iface.is_point_to_point() + && !self.iface.is_loopback() + && self.iface.is_up() + && self.iface.is_lower_up() + && (self.is_iface_bridge().await || self.is_iface_phsical().await) + } +} + +#[cfg(target_os = "macos")] +impl InterfaceFilter { + async fn is_interface_physical(interface_name: &str) -> bool { + let output = tokio::process::Command::new("networksetup") + .args(&["-listallhardwareports"]) + .output() + .await + .expect("Failed to execute command"); + + let stdout = std::str::from_utf8(&output.stdout).expect("Invalid UTF-8"); + + let lines: Vec<&str> = stdout.lines().collect(); + + for i in 0..lines.len() { + let line = lines[i]; + + if line.contains("Device:") && line.contains(interface_name) { + let next_line = lines[i + 1]; + if next_line.contains("Virtual Interface") { + return false; + } else { + return true; + } + } + } + + false + } + + async fn filter_iface(&self) -> bool { + !self.iface.is_point_to_point() + && !self.iface.is_loopback() + && self.iface.is_up() + && Self::is_interface_physical(&self.iface.name).await + } +} + +#[cfg(target_os = "windows")] +impl InterfaceFilter { + async fn filter_iface(&self) -> bool { + !self.iface.is_point_to_point() && !self.iface.is_loopback() && self.iface.is_up() + } +} + +pub async fn local_ipv4() -> std::io::Result { + let socket = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; + socket.connect("8.8.8.8:80").await?; + let addr = socket.local_addr()?; + match addr.ip() { + std::net::IpAddr::V4(ip) => Ok(ip), + std::net::IpAddr::V6(_) => Err(std::io::Error::new( + std::io::ErrorKind::AddrNotAvailable, + "no ipv4 address", + )), + } +} + +pub async fn local_ipv6() -> std::io::Result { + let socket = tokio::net::UdpSocket::bind("[::]:0").await?; + socket + .connect("[2001:4860:4860:0000:0000:0000:0000:8888]:80") + .await?; + let addr = socket.local_addr()?; + match addr.ip() { + std::net::IpAddr::V6(ip) => Ok(ip), + std::net::IpAddr::V4(_) => Err(std::io::Error::new( + std::io::ErrorKind::AddrNotAvailable, + "no ipv4 address", + )), + } +} + +pub struct IPCollector { + cached_ip_list: Arc>, + collect_ip_task: Mutex>, + net_ns: NetNS, +} + +impl IPCollector { + pub fn new(net_ns: NetNS) -> Self { + Self { + cached_ip_list: Arc::new(RwLock::new(GetIpListResponse::new())), + collect_ip_task: Mutex::new(JoinSet::new()), + net_ns, + } + } + + pub async fn collect_ip_addrs(&self) -> GetIpListResponse { + let mut task = self.collect_ip_task.lock().await; + if task.is_empty() { + let cached_ip_list = self.cached_ip_list.clone(); + *cached_ip_list.write().await = + Self::do_collect_ip_addrs(false, self.net_ns.clone()).await; + let net_ns = self.net_ns.clone(); + task.spawn(async move { + loop { + let ip_addrs = Self::do_collect_ip_addrs(true, net_ns.clone()).await; + *cached_ip_list.write().await = ip_addrs; + tokio::time::sleep(std::time::Duration::from_secs( + DIRECT_CONNECTOR_IP_LIST_TIMEOUT_SEC, + )) + .await; + } + }); + } + + return self.cached_ip_list.read().await.deref().clone(); + } + + #[tracing::instrument(skip(net_ns))] + async fn do_collect_ip_addrs(with_public: bool, net_ns: NetNS) -> GetIpListResponse { + let mut ret = easytier_rpc::peer::GetIpListResponse { + public_ipv4: "".to_string(), + interface_ipv4s: vec![], + public_ipv6: "".to_string(), + interface_ipv6s: vec![], + }; + + if with_public { + if let Some(v4_addr) = + public_ip::addr_with(public_ip::http::ALL, public_ip::Version::V4).await + { + ret.public_ipv4 = v4_addr.to_string(); + } + + if let Some(v6_addr) = public_ip::addr_v6().await { + ret.public_ipv6 = v6_addr.to_string(); + } + } + + let _g = net_ns.guard(); + let ifaces = pnet::datalink::interfaces(); + for iface in ifaces { + let f = InterfaceFilter { + iface: iface.clone(), + }; + + if !f.filter_iface().await { + continue; + } + + for ip in iface.ips { + let ip: std::net::IpAddr = ip.ip(); + if ip.is_loopback() || ip.is_multicast() { + continue; + } + if ip.is_ipv4() { + ret.interface_ipv4s.push(ip.to_string()); + } else if ip.is_ipv6() { + ret.interface_ipv6s.push(ip.to_string()); + } + } + } + + if let Ok(v4_addr) = local_ipv4().await { + tracing::trace!("got local ipv4: {}", v4_addr); + if !ret.interface_ipv4s.contains(&v4_addr.to_string()) { + ret.interface_ipv4s.push(v4_addr.to_string()); + } + } + + if let Ok(v6_addr) = local_ipv6().await { + tracing::trace!("got local ipv6: {}", v6_addr); + if !ret.interface_ipv6s.contains(&v6_addr.to_string()) { + ret.interface_ipv6s.push(v6_addr.to_string()); + } + } + + ret + } +} diff --git a/easytier-core/src/common/rkyv_util.rs b/easytier-core/src/common/rkyv_util.rs new file mode 100644 index 0000000..6588121 --- /dev/null +++ b/easytier-core/src/common/rkyv_util.rs @@ -0,0 +1,54 @@ +use rkyv::{ + validation::{validators::DefaultValidator, CheckTypeError}, + vec::ArchivedVec, + Archive, CheckBytes, Serialize, +}; +use tokio_util::bytes::{Bytes, BytesMut}; + +pub fn decode_from_bytes_checked<'a, T: Archive>( + bytes: &'a [u8], +) -> Result<&'a T::Archived, CheckTypeError>> +where + T::Archived: CheckBytes>, +{ + rkyv::check_archived_root::(bytes) +} + +pub fn decode_from_bytes<'a, T: Archive>( + bytes: &'a [u8], +) -> Result<&'a T::Archived, CheckTypeError>> +where + T::Archived: CheckBytes>, +{ + // rkyv::check_archived_root::(bytes) + unsafe { Ok(rkyv::archived_root::(bytes)) } +} + +// allow deseraial T to Bytes +pub fn encode_to_bytes(val: &T) -> Bytes +where + T: Serialize>, +{ + let ret = rkyv::to_bytes::<_, N>(val).unwrap(); + // let mut r = BytesMut::new(); + // r.extend_from_slice(&ret); + // r.freeze() + ret.into_boxed_slice().into() +} + +pub fn extract_bytes_from_archived_vec(raw_data: &Bytes, archived_data: &ArchivedVec) -> Bytes { + let ptr_range = archived_data.as_ptr_range(); + let offset = ptr_range.start as usize - raw_data.as_ptr() as usize; + let len = ptr_range.end as usize - ptr_range.start as usize; + return raw_data.slice(offset..offset + len); +} + +pub fn extract_bytes_mut_from_archived_vec( + raw_data: &mut BytesMut, + archived_data: &ArchivedVec, +) -> BytesMut { + let ptr_range = archived_data.as_ptr_range(); + let offset = ptr_range.start as usize - raw_data.as_ptr() as usize; + let len = ptr_range.end as usize - ptr_range.start as usize; + raw_data.split_off(offset).split_to(len) +} diff --git a/easytier-core/src/common/stun.rs b/easytier-core/src/common/stun.rs new file mode 100644 index 0000000..9a5e939 --- /dev/null +++ b/easytier-core/src/common/stun.rs @@ -0,0 +1,433 @@ +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; +use std::sync::Arc; +use std::time::Duration; + +use crossbeam::atomic::AtomicCell; +use easytier_rpc::{NatType, StunInfo}; +use stun_format::Attr; +use tokio::net::{lookup_host, UdpSocket}; +use tokio::sync::RwLock; +use tokio::task::JoinSet; + +use crate::common::error::Error; + +struct Stun { + stun_server: String, + req_repeat: u8, + resp_timeout: Duration, +} + +#[derive(Debug, Clone, Copy)] +struct BindRequestResponse { + source_addr: SocketAddr, + mapped_socket_addr: Option, + changed_socket_addr: Option, + + ip_changed: bool, + port_changed: bool, +} + +impl BindRequestResponse { + pub fn get_mapped_addr_no_check(&self) -> &SocketAddr { + self.mapped_socket_addr.as_ref().unwrap() + } +} + +impl Stun { + pub fn new(stun_server: String) -> Self { + Self { + stun_server, + req_repeat: 3, + resp_timeout: Duration::from_millis(3000), + } + } + + async fn wait_stun_response<'a, const N: usize>( + &self, + buf: &'a mut [u8; N], + udp: &UdpSocket, + tids: &Vec, + ) -> Result<(stun_format::Msg<'a>, SocketAddr), Error> { + let mut now = tokio::time::Instant::now(); + let deadline = now + self.resp_timeout; + + while now < deadline { + let mut udp_buf = [0u8; 1500]; + let (len, remote_addr) = + tokio::time::timeout(deadline - now, udp.recv_from(udp_buf.as_mut_slice())) + .await??; + now = tokio::time::Instant::now(); + + if len < 20 { + continue; + } + + // TODO:: we cannot borrow `buf` directly in udp recv_from, so we copy it here + unsafe { std::ptr::copy(udp_buf.as_ptr(), buf.as_ptr() as *mut u8, len) }; + + let msg = stun_format::Msg::<'a>::from(&buf[..]); + tracing::trace!(b = ?&udp_buf[..len], ?msg, ?tids, "recv stun response"); + + if msg.typ().is_none() || msg.tid().is_none() { + continue; + } + + if matches!( + msg.typ().as_ref().unwrap(), + stun_format::MsgType::BindingResponse + ) && tids.contains(msg.tid().as_ref().unwrap()) + { + return Ok((msg, remote_addr)); + } + } + + Err(Error::Unknown) + } + + fn stun_addr(addr: stun_format::SocketAddr) -> SocketAddr { + match addr { + stun_format::SocketAddr::V4(ip, port) => { + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), port)) + } + stun_format::SocketAddr::V6(ip, port) => { + SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::from(ip), port, 0, 0)) + } + } + } + + fn extrace_mapped_addr(msg: &stun_format::Msg) -> Option { + let mut mapped_addr = None; + for x in msg.attrs_iter() { + match x { + Attr::MappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(Self::stun_addr(addr)); + } + } + Attr::XorMappedAddress(addr) => { + if mapped_addr.is_none() { + let _ = mapped_addr.insert(Self::stun_addr(addr)); + } + } + _ => {} + } + } + mapped_addr + } + + fn extract_changed_addr(msg: &stun_format::Msg) -> Option { + let mut changed_addr = None; + for x in msg.attrs_iter() { + match x { + Attr::ChangedAddress(addr) => { + if changed_addr.is_none() { + let _ = changed_addr.insert(Self::stun_addr(addr)); + } + } + _ => {} + } + } + changed_addr + } + + pub async fn bind_request( + &self, + source_port: u16, + change_ip: bool, + change_port: bool, + ) -> Result { + let stun_host = lookup_host(&self.stun_server) + .await? + .next() + .ok_or(Error::NotFound)?; + // let udp_socket = socket2::Socket::new( + // match stun_host { + // SocketAddr::V4(..) => socket2::Domain::IPV4, + // SocketAddr::V6(..) => socket2::Domain::IPV6, + // }, + // socket2::Type::DGRAM, + // Some(socket2::Protocol::UDP), + // )?; + // udp_socket.set_reuse_port(true)?; + // udp_socket.set_reuse_address(true)?; + let udp = UdpSocket::bind(format!("0.0.0.0:{}", source_port)).await?; + + // repeat req in case of packet loss + let mut tids = vec![]; + for _ in 0..self.req_repeat { + let mut buf = [0u8; 28]; + // memset buf + unsafe { std::ptr::write_bytes(buf.as_mut_ptr(), 0, buf.len()) }; + let mut msg = stun_format::MsgBuilder::from(buf.as_mut_slice()); + msg.typ(stun_format::MsgType::BindingRequest).unwrap(); + let tid = rand::random::(); + msg.tid(tid as u128).unwrap(); + if change_ip || change_port { + msg.add_attr(Attr::ChangeRequest { + change_ip, + change_port, + }) + .unwrap(); + } + + tids.push(tid as u128); + tracing::trace!(b = ?msg.as_bytes(), tid, "send stun request"); + udp.send_to(msg.as_bytes(), &stun_host).await?; + } + + tracing::trace!("waiting stun response"); + let mut buf = [0; 1620]; + let (msg, recv_addr) = self.wait_stun_response(&mut buf, &udp, &tids).await?; + + let changed_socket_addr = Self::extract_changed_addr(&msg); + let ip_changed = stun_host.ip() != recv_addr.ip(); + let port_changed = stun_host.port() != recv_addr.port(); + + let resp = BindRequestResponse { + source_addr: udp.local_addr()?, + mapped_socket_addr: Self::extrace_mapped_addr(&msg), + changed_socket_addr, + ip_changed, + port_changed, + }; + + tracing::info!( + ?stun_host, + ?recv_addr, + ?changed_socket_addr, + "finish stun bind request" + ); + + Ok(resp) + } +} + +struct UdpNatTypeDetector { + stun_servers: Vec, +} + +impl UdpNatTypeDetector { + pub fn new(stun_servers: Vec) -> Self { + Self { stun_servers } + } + + async fn get_udp_nat_type(&self, mut source_port: u16) -> NatType { + // Like classic STUN (rfc3489). Detect NAT behavior for UDP. + // Modified from rfc3489. Requires at least two STUN servers. + let mut ret_test1_1 = None; + let mut ret_test1_2 = None; + let mut ret_test2 = None; + let mut ret_test3 = None; + + if source_port == 0 { + let udp = UdpSocket::bind("0.0.0.0:0").await.unwrap(); + source_port = udp.local_addr().unwrap().port(); + } + + let mut succ = false; + for server_ip in &self.stun_servers { + let stun = Stun::new(server_ip.clone()); + let ret = stun.bind_request(source_port, false, false).await; + if ret.is_err() { + // Try another STUN server + continue; + } + if ret_test1_1.is_none() { + ret_test1_1 = ret.ok(); + continue; + } + ret_test1_2 = ret.ok(); + let ret = stun.bind_request(source_port, true, true).await; + if let Ok(resp) = ret { + if !resp.ip_changed || !resp.port_changed { + // Try another STUN server + continue; + } + } + ret_test2 = ret.ok(); + ret_test3 = stun.bind_request(source_port, false, true).await.ok(); + succ = true; + break; + } + + if !succ { + return NatType::Unknown; + } + + tracing::info!( + ?ret_test1_1, + ?ret_test1_2, + ?ret_test2, + ?ret_test3, + "finish stun test, try to detect nat type" + ); + + let ret_test1_1 = ret_test1_1.unwrap(); + let ret_test1_2 = ret_test1_2.unwrap(); + + if ret_test1_1.mapped_socket_addr != ret_test1_2.mapped_socket_addr { + return NatType::Symmetric; + } + + if ret_test1_1.mapped_socket_addr.is_some() + && ret_test1_1.source_addr == ret_test1_1.mapped_socket_addr.unwrap() + { + if !ret_test2.is_none() { + return NatType::OpenInternet; + } else { + return NatType::SymUdpFirewall; + } + } else { + if let Some(ret_test2) = ret_test2 { + if source_port == ret_test2.get_mapped_addr_no_check().port() + && source_port == ret_test1_1.get_mapped_addr_no_check().port() + { + return NatType::NoPat; + } else { + return NatType::FullCone; + } + } else { + if !ret_test3.is_none() { + return NatType::Restricted; + } else { + return NatType::PortRestricted; + } + } + } + } +} + +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait StunInfoCollectorTrait: Send + Sync { + fn get_stun_info(&self) -> StunInfo; + async fn get_udp_port_mapping(&self, local_port: u16) -> Result; +} + +pub struct StunInfoCollector { + stun_servers: Arc>>, + udp_nat_type: Arc>, + redetect_notify: Arc, + tasks: JoinSet<()>, +} + +#[async_trait::async_trait] +impl StunInfoCollectorTrait for StunInfoCollector { + fn get_stun_info(&self) -> StunInfo { + let (typ, time) = self.udp_nat_type.load(); + StunInfo { + udp_nat_type: typ as i32, + tcp_nat_type: 0, + last_update_time: time.elapsed().as_secs() as i64, + } + } + + async fn get_udp_port_mapping(&self, local_port: u16) -> Result { + let stun_servers = self.stun_servers.read().await.clone(); + for server in stun_servers.iter() { + let stun = Stun::new(server.clone()); + let Ok(ret) = stun.bind_request(local_port, false, false).await else { + tracing::warn!(?server, "stun bind request failed"); + continue; + }; + if let Some(mapped_addr) = ret.mapped_socket_addr { + return Ok(mapped_addr); + } + } + Err(Error::NotFound) + } +} + +impl StunInfoCollector { + pub fn new(stun_servers: Vec) -> Self { + let mut ret = Self { + stun_servers: Arc::new(RwLock::new(stun_servers)), + udp_nat_type: Arc::new(AtomicCell::new(( + NatType::Unknown, + std::time::Instant::now(), + ))), + redetect_notify: Arc::new(tokio::sync::Notify::new()), + tasks: JoinSet::new(), + }; + + ret.start_stun_routine(); + + ret + } + + fn start_stun_routine(&mut self) { + let stun_servers = self.stun_servers.clone(); + let udp_nat_type = self.udp_nat_type.clone(); + let redetect_notify = self.redetect_notify.clone(); + self.tasks.spawn(async move { + loop { + let detector = UdpNatTypeDetector::new(stun_servers.read().await.clone()); + let ret = detector.get_udp_nat_type(0).await; + udp_nat_type.store((ret, std::time::Instant::now())); + + let sleep_sec = match ret { + NatType::Unknown => 15, + _ => 60, + }; + tracing::info!(?ret, ?sleep_sec, "finish udp nat type detect"); + + tokio::select! { + _ = redetect_notify.notified() => {} + _ = tokio::time::sleep(Duration::from_secs(sleep_sec)) => {} + } + } + }); + } + + pub fn update_stun_info(&self) { + self.redetect_notify.notify_one(); + } + + pub async fn set_stun_servers(&self, stun_servers: Vec) { + *self.stun_servers.write().await = stun_servers; + self.update_stun_info(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_stun_bind_request() { + // miwifi / qq seems not correctly responde to change_ip and change_port, they always try to change the src ip and port. + // let stun = Stun::new("stun.counterpath.com:3478".to_string()); + let stun = Stun::new("180.235.108.91:3478".to_string()); + // let stun = Stun::new("193.22.2.248:3478".to_string()); + + // let stun = Stun::new("stun.chat.bilibili.com:3478".to_string()); + // let stun = Stun::new("stun.miwifi.com:3478".to_string()); + + let rs = stun.bind_request(12345, true, true).await.unwrap(); + assert!(rs.ip_changed); + assert!(rs.port_changed); + + let rs = stun.bind_request(12345, true, false).await.unwrap(); + assert!(rs.ip_changed); + assert!(!rs.port_changed); + + let rs = stun.bind_request(12345, false, true).await.unwrap(); + assert!(!rs.ip_changed); + assert!(rs.port_changed); + + let rs = stun.bind_request(12345, false, false).await.unwrap(); + assert!(!rs.ip_changed); + assert!(!rs.port_changed); + } + + #[tokio::test] + async fn test_udp_nat_type_detect() { + let detector = UdpNatTypeDetector::new(vec![ + "stun.counterpath.com:3478".to_string(), + "180.235.108.91:3478".to_string(), + ]); + let ret = detector.get_udp_nat_type(0).await; + + assert_eq!(ret, NatType::FullCone); + } +} diff --git a/easytier-core/src/connector/direct.rs b/easytier-core/src/connector/direct.rs new file mode 100644 index 0000000..1c58b77 --- /dev/null +++ b/easytier-core/src/connector/direct.rs @@ -0,0 +1,325 @@ +// try connect peers directly, with either its public ip or lan ip + +use std::sync::Arc; + +use crate::{ + common::{ + constants::{self, DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC}, + error::Error, + global_ctx::ArcGlobalCtx, + network::IPCollector, + }, + peers::{peer_manager::PeerManager, peer_rpc::PeerRpcManager, PeerId}, +}; + +use easytier_rpc::{peer::GetIpListResponse, PeerConnInfo}; +use tokio::{task::JoinSet, time::timeout}; +use tracing::Instrument; + +use super::create_connector_by_url; + +#[tarpc::service] +pub trait DirectConnectorRpc { + async fn get_ip_list() -> GetIpListResponse; +} + +#[async_trait::async_trait] +pub trait PeerManagerForDirectConnector { + async fn list_peers(&self) -> Vec; + async fn list_peer_conns(&self, peer_id: &PeerId) -> Option>; + fn get_peer_rpc_mgr(&self) -> Arc; +} + +#[async_trait::async_trait] +impl PeerManagerForDirectConnector for PeerManager { + async fn list_peers(&self) -> Vec { + let mut ret = vec![]; + + let routes = self.list_routes().await; + for r in routes.iter() { + ret.push(r.peer_id.parse().unwrap()); + } + + ret + } + + async fn list_peer_conns(&self, peer_id: &PeerId) -> Option> { + self.get_peer_map().list_peer_conns(peer_id).await + } + + fn get_peer_rpc_mgr(&self) -> Arc { + self.get_peer_rpc_mgr() + } +} + +#[derive(Clone)] +struct DirectConnectorManagerRpcServer { + // TODO: this only cache for one src peer, should make it global + ip_list_collector: Arc, +} + +#[tarpc::server] +impl DirectConnectorRpc for DirectConnectorManagerRpcServer { + async fn get_ip_list(self, _: tarpc::context::Context) -> GetIpListResponse { + return self.ip_list_collector.collect_ip_addrs().await; + } +} + +impl DirectConnectorManagerRpcServer { + pub fn new(ip_collector: Arc) -> Self { + Self { + ip_list_collector: ip_collector, + } + } +} + +#[derive(Hash, Eq, PartialEq, Clone)] +struct DstBlackListItem(PeerId, String); + +struct DirectConnectorManagerData { + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + dst_blacklist: timedmap::TimedMap, +} + +impl std::fmt::Debug for DirectConnectorManagerData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DirectConnectorManagerData") + .field("peer_manager", &self.peer_manager) + .finish() + } +} + +pub struct DirectConnectorManager { + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + data: Arc, + + tasks: JoinSet<()>, +} + +impl DirectConnectorManager { + pub fn new( + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + ) -> Self { + Self { + my_node_id, + global_ctx: global_ctx.clone(), + data: Arc::new(DirectConnectorManagerData { + global_ctx, + peer_manager, + dst_blacklist: timedmap::TimedMap::new(), + }), + tasks: JoinSet::new(), + } + } + + pub fn run(&mut self) { + self.run_as_server(); + self.run_as_client(); + } + + pub fn run_as_server(&mut self) { + self.data.peer_manager.get_peer_rpc_mgr().run_service( + constants::DIRECT_CONNECTOR_SERVICE_ID, + DirectConnectorManagerRpcServer::new(self.global_ctx.get_ip_collector()).serve(), + ); + } + + pub fn run_as_client(&mut self) { + let data = self.data.clone(); + let my_node_id = self.my_node_id.clone(); + self.tasks.spawn( + async move { + loop { + let peers = data.peer_manager.list_peers().await; + let mut tasks = JoinSet::new(); + for peer_id in peers { + if peer_id == my_node_id { + continue; + } + tasks.spawn(Self::do_try_direct_connect(data.clone(), peer_id)); + } + + while let Some(task_ret) = tasks.join_next().await { + tracing::trace!(?task_ret, "direct connect task ret"); + } + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + .instrument(tracing::info_span!("direct_connector_client", my_id = ?self.my_node_id)), + ); + } + + async fn do_try_connect_to_ip( + data: Arc, + dst_peer_id: PeerId, + addr: String, + ) -> Result<(), Error> { + data.dst_blacklist.cleanup(); + if data + .dst_blacklist + .contains(&DstBlackListItem(dst_peer_id.clone(), addr.clone())) + { + tracing::trace!("try_connect_to_ip failed, addr in blacklist: {}", addr); + return Err(Error::UrlInBlacklist); + } + + let connector = create_connector_by_url(&addr, data.global_ctx.get_ip_collector()).await?; + let (peer_id, conn_id) = timeout( + std::time::Duration::from_secs(5), + data.peer_manager.try_connect(connector), + ) + .await??; + + // let (peer_id, conn_id) = data.peer_manager.try_connect(connector).await?; + + if peer_id != dst_peer_id { + tracing::info!( + "connect to ip succ: {}, but peer id mismatch, expect: {}, actual: {}", + addr, + dst_peer_id, + peer_id + ); + data.peer_manager + .get_peer_map() + .close_peer_conn(&peer_id, &conn_id) + .await?; + return Err(Error::InvalidUrl(addr)); + } + Ok(()) + } + + #[tracing::instrument] + async fn try_connect_to_ip( + data: Arc, + dst_peer_id: PeerId, + addr: String, + ) { + let ret = Self::do_try_connect_to_ip(data.clone(), dst_peer_id, addr.clone()).await; + if let Err(e) = ret { + if !matches!(e, Error::UrlInBlacklist) { + tracing::info!( + "try_connect_to_ip failed: {:?}, peer_id: {}", + e, + dst_peer_id + ); + data.dst_blacklist.insert( + DstBlackListItem(dst_peer_id.clone(), addr.clone()), + (), + std::time::Duration::from_secs(DIRECT_CONNECTOR_BLACKLIST_TIMEOUT_SEC), + ); + } + } else { + log::info!("try_connect_to_ip success, peer_id: {}", dst_peer_id); + } + } + + #[tracing::instrument] + async fn do_try_direct_connect( + data: Arc, + dst_peer_id: PeerId, + ) -> Result<(), Error> { + let peer_manager = data.peer_manager.clone(); + // check if we have direct connection with dst_peer_id + if let Some(c) = peer_manager.list_peer_conns(&dst_peer_id).await { + // currently if we have any type of direct connection (udp or tcp), we will not try to connect + if !c.is_empty() { + return Ok(()); + } + } + + log::trace!("try direct connect to peer: {}", dst_peer_id); + + let ip_list = peer_manager + .get_peer_rpc_mgr() + .do_client_rpc_scoped(1, dst_peer_id, |c| async { + let client = + DirectConnectorRpcClient::new(tarpc::client::Config::default(), c).spawn(); + let ip_list = client.get_ip_list(tarpc::context::current()).await; + tracing::info!(ip_list = ?ip_list, dst_peer_id = ?dst_peer_id, "got ip list"); + ip_list + }) + .await?; + + let mut tasks = JoinSet::new(); + ip_list.interface_ipv4s.iter().for_each(|ip| { + let addr = format!("{}://{}:{}", "tcp", ip, 11010); + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr, + )); + }); + + let addr = format!("{}://{}:{}", "tcp", ip_list.public_ipv4.clone(), 11010); + tasks.spawn(Self::try_connect_to_ip( + data.clone(), + dst_peer_id.clone(), + addr, + )); + + while let Some(ret) = tasks.join_next().await { + if let Err(e) = ret { + log::error!("join direct connect task failed: {:?}", e); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + connector::direct::DirectConnectorManager, + instance::listeners::ListenerManager, + peers::tests::{ + connect_peer_manager, create_mock_peer_manager, wait_route_appear, + wait_route_appear_with_cost, + }, + tunnels::tcp_tunnel::TcpTunnelListener, + }; + + #[tokio::test] + async fn direct_connector_basic_test() { + let p_a = create_mock_peer_manager().await; + let p_b = create_mock_peer_manager().await; + let p_c = create_mock_peer_manager().await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + + wait_route_appear(p_a.clone(), p_c.my_node_id()) + .await + .unwrap(); + + let mut dm_a = + DirectConnectorManager::new(p_a.my_node_id(), p_a.get_global_ctx(), p_a.clone()); + let mut dm_c = + DirectConnectorManager::new(p_c.my_node_id(), p_c.get_global_ctx(), p_c.clone()); + + dm_a.run_as_client(); + dm_c.run_as_server(); + + let mut lis_c = ListenerManager::new( + p_c.my_node_id(), + p_c.get_global_ctx().net_ns.clone(), + p_c.clone(), + ); + + lis_c + .add_listener(TcpTunnelListener::new( + "tcp://0.0.0.0:11010".parse().unwrap(), + )) + .await + .unwrap(); + + lis_c.run().await.unwrap(); + + wait_route_appear_with_cost(p_a.clone(), p_c.my_node_id(), Some(1)) + .await + .unwrap(); + } +} diff --git a/easytier-core/src/connector/manual.rs b/easytier-core/src/connector/manual.rs new file mode 100644 index 0000000..0be083f --- /dev/null +++ b/easytier-core/src/connector/manual.rs @@ -0,0 +1,384 @@ +use std::{collections::BTreeSet, sync::Arc}; + +use dashmap::{DashMap, DashSet}; +use easytier_rpc::{ + connector_manage_rpc_server::ConnectorManageRpc, Connector, ConnectorStatus, + ListConnectorRequest, ManageConnectorRequest, +}; +use tokio::{ + sync::{broadcast::Receiver, mpsc, Mutex}, + task::JoinSet, + time::timeout, +}; + +use crate::{ + common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, + netns::NetNS, + }, + connector::set_bind_addr_for_peer_connector, + peers::peer_manager::PeerManager, + tunnels::{Tunnel, TunnelConnector}, + use_global_var, +}; + +use super::create_connector_by_url; + +type ConnectorMap = Arc>>; + +#[derive(Debug, Clone)] +struct ReconnResult { + dead_url: String, + peer_id: uuid::Uuid, + conn_id: uuid::Uuid, +} + +struct ConnectorManagerData { + connectors: ConnectorMap, + reconnecting: DashSet, + peer_manager: Arc, + alive_conn_urls: Arc>>, + // user removed connector urls + removed_conn_urls: Arc>, + net_ns: NetNS, + global_ctx: ArcGlobalCtx, +} + +pub struct ManualConnectorManager { + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + data: Arc, + tasks: JoinSet<()>, +} + +impl ManualConnectorManager { + pub fn new( + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + ) -> Self { + let connectors = Arc::new(DashMap::new()); + let tasks = JoinSet::new(); + let event_subscriber = global_ctx.subscribe(); + + let mut ret = Self { + my_node_id, + global_ctx: global_ctx.clone(), + data: Arc::new(ConnectorManagerData { + connectors, + reconnecting: DashSet::new(), + peer_manager, + alive_conn_urls: Arc::new(Mutex::new(BTreeSet::new())), + removed_conn_urls: Arc::new(DashSet::new()), + net_ns: global_ctx.net_ns.clone(), + global_ctx, + }), + tasks, + }; + + ret.tasks + .spawn(Self::conn_mgr_routine(ret.data.clone(), event_subscriber)); + + ret + } + + pub fn add_connector(&self, connector: T) + where + T: TunnelConnector + Send + Sync + 'static, + { + log::info!("add_connector: {}", connector.remote_url()); + self.data + .connectors + .insert(connector.remote_url().into(), Box::new(connector)); + } + + pub async fn add_connector_by_url(&self, url: &str) -> Result<(), Error> { + self.add_connector(create_connector_by_url(url, self.global_ctx.get_ip_collector()).await?); + Ok(()) + } + + pub async fn remove_connector(&self, url: &str) -> Result<(), Error> { + log::info!("remove_connector: {}", url); + if !self.list_connectors().await.iter().any(|x| x.url == url) { + return Err(Error::NotFound); + } + self.data.removed_conn_urls.insert(url.into()); + Ok(()) + } + + pub async fn list_connectors(&self) -> Vec { + let conn_urls: BTreeSet = self + .data + .connectors + .iter() + .map(|x| x.key().clone().into()) + .collect(); + + let dead_urls: BTreeSet = Self::collect_dead_conns(self.data.clone()) + .await + .into_iter() + .collect(); + + let mut ret = Vec::new(); + + for conn_url in conn_urls { + let mut status = ConnectorStatus::Connected; + if dead_urls.contains(&conn_url) { + status = ConnectorStatus::Disconnected; + } + ret.insert( + 0, + Connector { + url: conn_url, + status: status.into(), + }, + ); + } + + let reconnecting_urls: BTreeSet = self + .data + .reconnecting + .iter() + .map(|x| x.clone().into()) + .collect(); + + for conn_url in reconnecting_urls { + ret.insert( + 0, + Connector { + url: conn_url, + status: ConnectorStatus::Connecting.into(), + }, + ); + } + + ret + } + + async fn conn_mgr_routine( + data: Arc, + mut event_recv: Receiver, + ) { + log::warn!("conn_mgr_routine started"); + let mut reconn_interval = tokio::time::interval(std::time::Duration::from_millis( + use_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS), + )); + let mut reconn_tasks = JoinSet::new(); + let (reconn_result_send, mut reconn_result_recv) = mpsc::channel(100); + + loop { + tokio::select! { + event = event_recv.recv() => { + if let Ok(event) = event { + Self::handle_event(&event, data.clone()).await; + } else { + log::warn!("event_recv closed"); + panic!("event_recv closed"); + } + } + + _ = reconn_interval.tick() => { + let dead_urls = Self::collect_dead_conns(data.clone()).await; + if dead_urls.is_empty() { + continue; + } + for dead_url in dead_urls { + let data_clone = data.clone(); + let sender = reconn_result_send.clone(); + let (_, connector) = data.connectors.remove(&dead_url).unwrap(); + let insert_succ = data.reconnecting.insert(dead_url.clone()); + assert!(insert_succ); + reconn_tasks.spawn(async move { + sender.send(Self::conn_reconnect(data_clone.clone(), dead_url, connector).await).await.unwrap(); + }); + } + log::info!("reconn_interval tick, done"); + } + + ret = reconn_result_recv.recv() => { + log::warn!("reconn_tasks done, out: {:?}", ret); + let _ = reconn_tasks.join_next().await.unwrap(); + } + } + } + } + + async fn handle_event(event: &GlobalCtxEvent, data: Arc) { + match event { + GlobalCtxEvent::PeerConnAdded(conn_info) => { + let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone(); + data.alive_conn_urls.lock().await.insert(addr); + log::warn!("peer conn added: {:?}", conn_info); + } + + GlobalCtxEvent::PeerConnRemoved(conn_info) => { + let addr = conn_info.tunnel.as_ref().unwrap().remote_addr.clone(); + data.alive_conn_urls.lock().await.remove(&addr); + log::warn!("peer conn removed: {:?}", conn_info); + } + + GlobalCtxEvent::PeerAdded => todo!(), + GlobalCtxEvent::PeerRemoved => todo!(), + } + } + + fn handle_remove_connector(data: Arc) { + let remove_later = DashSet::new(); + for it in data.removed_conn_urls.iter() { + let url = it.key(); + if let Some(_) = data.connectors.remove(url) { + log::warn!("connector: {}, removed", url); + continue; + } else if data.reconnecting.contains(url) { + log::warn!("connector: {}, reconnecting, remove later.", url); + remove_later.insert(url.clone()); + continue; + } else { + log::warn!("connector: {}, not found", url); + } + } + data.removed_conn_urls.clear(); + for it in remove_later.iter() { + data.removed_conn_urls.insert(it.key().clone()); + } + } + + async fn collect_dead_conns(data: Arc) -> BTreeSet { + Self::handle_remove_connector(data.clone()); + + let curr_alive = data.alive_conn_urls.lock().await.clone(); + let all_urls: BTreeSet = data + .connectors + .iter() + .map(|x| x.key().clone().into()) + .collect(); + &all_urls - &curr_alive + } + + async fn conn_reconnect( + data: Arc, + dead_url: String, + connector: Box, + ) -> Result { + let connector = Arc::new(Mutex::new(Some(connector))); + let net_ns = data.net_ns.clone(); + + log::info!("reconnect: {}", dead_url); + + let connector_clone = connector.clone(); + let data_clone = data.clone(); + let url_clone = dead_url.clone(); + let ip_collector = data.global_ctx.get_ip_collector(); + let reconn_task = async move { + let mut locked = connector_clone.lock().await; + let conn = locked.as_mut().unwrap(); + // TODO: should support set v6 here, use url in connector array + set_bind_addr_for_peer_connector(conn, true, &ip_collector).await; + let _g = net_ns.guard(); + log::info!("reconnect try connect... conn: {:?}", conn); + let tunnel = conn.connect().await?; + log::info!("reconnect get tunnel succ: {:?}", tunnel); + assert_eq!( + url_clone, + tunnel.info().unwrap().remote_addr, + "info: {:?}", + tunnel.info() + ); + let (peer_id, conn_id) = data_clone.peer_manager.add_client_tunnel(tunnel).await?; + log::info!("reconnect succ: {} {} {}", peer_id, conn_id, url_clone); + Ok(ReconnResult { + dead_url: url_clone, + peer_id, + conn_id, + }) + }; + + let ret = timeout(std::time::Duration::from_secs(1), reconn_task).await; + log::info!("reconnect: {} done, ret: {:?}", dead_url, ret); + + let conn = connector.lock().await.take().unwrap(); + data.reconnecting.remove(&dead_url).unwrap(); + data.connectors.insert(dead_url.clone(), conn); + + ret? + } +} + +pub struct ConnectorManagerRpcService(pub Arc); + +#[tonic::async_trait] +impl ConnectorManageRpc for ConnectorManagerRpcService { + async fn list_connector( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + let mut ret = easytier_rpc::ListConnectorResponse::default(); + let connectors = self.0.list_connectors().await; + ret.connectors = connectors; + Ok(tonic::Response::new(ret)) + } + + async fn manage_connector( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let req = request.into_inner(); + let url = url::Url::parse(&req.url) + .map_err(|_| tonic::Status::invalid_argument("invalid url"))?; + if req.action == easytier_rpc::ConnectorManageAction::Remove as i32 { + self.0.remove_connector(url.path()).await.map_err(|e| { + tonic::Status::invalid_argument(format!("remove connector failed: {:?}", e)) + })?; + return Ok(tonic::Response::new( + easytier_rpc::ManageConnectorResponse::default(), + )); + } else { + self.0 + .add_connector_by_url(url.as_str()) + .await + .map_err(|e| { + tonic::Status::invalid_argument(format!("add connector failed: {:?}", e)) + })?; + } + Ok(tonic::Response::new( + easytier_rpc::ManageConnectorResponse::default(), + )) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + peers::tests::create_mock_peer_manager, + set_global_var, + tunnels::{Tunnel, TunnelError}, + }; + + use super::*; + + #[tokio::test] + async fn test_reconnect_with_connecting_addr() { + set_global_var!(MANUAL_CONNECTOR_RECONNECT_INTERVAL_MS, 1); + + let peer_mgr = create_mock_peer_manager().await; + let my_node_id = uuid::Uuid::new_v4(); + let mgr = ManualConnectorManager::new(my_node_id, peer_mgr.get_global_ctx(), peer_mgr); + + struct MockConnector {} + #[async_trait::async_trait] + impl TunnelConnector for MockConnector { + fn remote_url(&self) -> url::Url { + url::Url::parse("tcp://aa.com").unwrap() + } + async fn connect(&mut self) -> Result, TunnelError> { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + Err(TunnelError::CommonError("fake error".into())) + } + } + + mgr.add_connector(MockConnector {}); + + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } +} diff --git a/easytier-core/src/connector/mod.rs b/easytier-core/src/connector/mod.rs new file mode 100644 index 0000000..f398d91 --- /dev/null +++ b/easytier-core/src/connector/mod.rs @@ -0,0 +1,73 @@ +use std::{ + net::{SocketAddr, SocketAddrV4, SocketAddrV6}, + sync::Arc, +}; + +use crate::{ + common::{error::Error, network::IPCollector}, + tunnels::{ + ring_tunnel::RingTunnelConnector, tcp_tunnel::TcpTunnelConnector, + udp_tunnel::UdpTunnelConnector, TunnelConnector, + }, +}; + +pub mod direct; +pub mod manual; +pub mod udp_hole_punch; + +async fn set_bind_addr_for_peer_connector( + connector: &mut impl TunnelConnector, + is_ipv4: bool, + ip_collector: &Arc, +) { + let ips = ip_collector.collect_ip_addrs().await; + if is_ipv4 { + let mut bind_addrs = vec![]; + for ipv4 in ips.interface_ipv4s { + let socket_addr = SocketAddrV4::new(ipv4.parse().unwrap(), 0).into(); + bind_addrs.push(socket_addr); + } + connector.set_bind_addrs(bind_addrs); + } else { + let mut bind_addrs = vec![]; + for ipv6 in ips.interface_ipv6s { + let socket_addr = SocketAddrV6::new(ipv6.parse().unwrap(), 0, 0, 0).into(); + bind_addrs.push(socket_addr); + } + connector.set_bind_addrs(bind_addrs); + } + let _ = connector; +} + +pub async fn create_connector_by_url( + url: &str, + ip_collector: Arc, +) -> Result, Error> { + let url = url::Url::parse(url).map_err(|_| Error::InvalidUrl(url.to_owned()))?; + match url.scheme() { + "tcp" => { + let dst_addr = + crate::tunnels::check_scheme_and_get_socket_addr::(&url, "tcp")?; + let mut connector = TcpTunnelConnector::new(url); + set_bind_addr_for_peer_connector(&mut connector, dst_addr.is_ipv4(), &ip_collector) + .await; + return Ok(Box::new(connector)); + } + "udp" => { + let dst_addr = + crate::tunnels::check_scheme_and_get_socket_addr::(&url, "udp")?; + let mut connector = UdpTunnelConnector::new(url); + set_bind_addr_for_peer_connector(&mut connector, dst_addr.is_ipv4(), &ip_collector) + .await; + return Ok(Box::new(connector)); + } + "ring" => { + crate::tunnels::check_scheme_and_get_socket_addr::(&url, "ring")?; + let connector = RingTunnelConnector::new(url); + return Ok(Box::new(connector)); + } + _ => { + return Err(Error::InvalidUrl(url.into())); + } + } +} diff --git a/easytier-core/src/connector/udp_hole_punch.rs b/easytier-core/src/connector/udp_hole_punch.rs new file mode 100644 index 0000000..cef878e --- /dev/null +++ b/easytier-core/src/connector/udp_hole_punch.rs @@ -0,0 +1,523 @@ +use std::{net::SocketAddr, sync::Arc}; + +use anyhow::Context; +use crossbeam::atomic::AtomicCell; +use easytier_rpc::NatType; +use rand::{seq::SliceRandom, Rng, SeedableRng}; +use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet}; +use tracing::Instrument; + +use crate::{ + common::{ + constants, error::Error, global_ctx::ArcGlobalCtx, rkyv_util::encode_to_bytes, + stun::StunInfoCollectorTrait, + }, + peers::{peer_manager::PeerManager, PeerId}, + tunnels::{ + udp_tunnel::{UdpPacket, UdpTunnelConnector, UdpTunnelListener}, + Tunnel, TunnelConnCounter, TunnelListener, + }, +}; + +use super::direct::PeerManagerForDirectConnector; + +#[tarpc::service] +pub trait UdpHolePunchService { + async fn try_punch_hole(local_mapped_addr: SocketAddr) -> Option; +} + +#[derive(Debug)] +struct UdpHolePunchListener { + socket: Arc, + tasks: JoinSet<()>, + running: Arc>, + mapped_addr: SocketAddr, + conn_counter: Arc>, + + listen_time: std::time::Instant, + last_select_time: AtomicCell, + last_connected_time: Arc>, +} + +impl UdpHolePunchListener { + async fn get_avail_port() -> Result { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + Ok(socket.local_addr()?.port()) + } + + pub async fn new(peer_mgr: Arc) -> Result { + let port = Self::get_avail_port().await?; + let listen_url = format!("udp://0.0.0.0:{}", port); + + let gctx = peer_mgr.get_global_ctx(); + let stun_info_collect = gctx.get_stun_info_collector(); + let mapped_addr = stun_info_collect.get_udp_port_mapping(port).await?; + + let mut listener = UdpTunnelListener::new(listen_url.parse().unwrap()); + + listener.listen().await?; + let socket = listener.get_socket().unwrap(); + + let running = Arc::new(AtomicCell::new(true)); + let running_clone = running.clone(); + + let last_connected_time = Arc::new(AtomicCell::new(std::time::Instant::now())); + let last_connected_time_clone = last_connected_time.clone(); + + let conn_counter = listener.get_conn_counter(); + let mut tasks = JoinSet::new(); + + tasks.spawn(async move { + while let Ok(conn) = listener.accept().await { + last_connected_time_clone.store(std::time::Instant::now()); + tracing::warn!(?conn, "udp hole punching listener got peer connection"); + if let Err(e) = peer_mgr.add_tunnel_as_server(conn).await { + tracing::error!(?e, "failed to add tunnel as server in hole punch listener"); + } + } + + running_clone.store(false); + }); + + tracing::warn!(?mapped_addr, ?socket, "udp hole punching listener started"); + + Ok(Self { + tasks, + socket, + running, + mapped_addr, + conn_counter, + + listen_time: std::time::Instant::now(), + last_select_time: AtomicCell::new(std::time::Instant::now()), + last_connected_time, + }) + } + + pub async fn get_socket(&self) -> Arc { + self.last_select_time.store(std::time::Instant::now()); + self.socket.clone() + } +} + +#[derive(Debug)] +struct UdpHolePunchConnectorData { + global_ctx: ArcGlobalCtx, + peer_mgr: Arc, + listeners: Arc>>, +} + +#[derive(Clone)] +struct UdpHolePunchRpcServer { + data: Arc, + + tasks: Arc>>, +} + +#[tarpc::server] +impl UdpHolePunchService for UdpHolePunchRpcServer { + async fn try_punch_hole( + self, + _: tarpc::context::Context, + local_mapped_addr: SocketAddr, + ) -> Option { + let (socket, mapped_addr) = self.select_listener().await?; + tracing::warn!(?local_mapped_addr, ?mapped_addr, "start hole punching"); + + let my_udp_nat_type = self + .data + .global_ctx + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type; + + // if we are restricted, we need to send hole punching resp to client + if my_udp_nat_type == NatType::PortRestricted as i32 + || my_udp_nat_type == NatType::Restricted as i32 + { + // send punch msg to local_mapped_addr for 3 seconds, 3.3 packet per second + self.tasks.lock().await.spawn(async move { + for _ in 0..10 { + tracing::info!(?local_mapped_addr, "sending hole punching packet"); + // generate a 128 bytes vec with random data + let mut rng = rand::rngs::StdRng::from_entropy(); + let mut buf = vec![0u8; 128]; + rng.fill(&mut buf[..]); + + let udp_packet = UdpPacket::new_hole_punch_packet(buf); + let udp_packet_bytes = encode_to_bytes::<_, 256>(&udp_packet); + let _ = socket + .send_to(udp_packet_bytes.as_ref(), local_mapped_addr) + .await; + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + } + }); + } + + Some(mapped_addr) + } +} + +impl UdpHolePunchRpcServer { + pub fn new(data: Arc) -> Self { + Self { + data, + tasks: Arc::new(Mutex::new(JoinSet::new())), + } + } + + async fn select_listener(&self) -> Option<(Arc, SocketAddr)> { + let all_listener_sockets = &self.data.listeners; + + // remove listener that not have connection in for 20 seconds + all_listener_sockets.lock().await.retain(|listener| { + listener.last_connected_time.load().elapsed().as_secs() < 20 + && listener.conn_counter.get() > 0 + }); + + let mut use_last = false; + if all_listener_sockets.lock().await.len() < 4 { + tracing::warn!("creating new udp hole punching listener"); + all_listener_sockets.lock().await.push( + UdpHolePunchListener::new(self.data.peer_mgr.clone()) + .await + .ok()?, + ); + use_last = true; + } + + let locked = all_listener_sockets.lock().await; + + let listener = if use_last { + locked.last()? + } else { + locked.choose(&mut rand::rngs::StdRng::from_entropy())? + }; + + Some((listener.get_socket().await, listener.mapped_addr)) + } +} + +pub struct UdpHolePunchConnector { + data: Arc, + tasks: JoinSet<()>, +} + +// Currently support: +// Symmetric -> Full Cone +// Any Type of Full Cone -> Any Type of Full Cone + +// if same level of full cone, node with smaller peer_id will be the initiator +// if different level of full cone, node with more strict level will be the initiator + +impl UdpHolePunchConnector { + pub fn new(global_ctx: ArcGlobalCtx, peer_mgr: Arc) -> Self { + Self { + data: Arc::new(UdpHolePunchConnectorData { + global_ctx, + peer_mgr, + listeners: Arc::new(Mutex::new(Vec::new())), + }), + tasks: JoinSet::new(), + } + } + + pub async fn run_as_client(&mut self) -> Result<(), Error> { + let data = self.data.clone(); + self.tasks.spawn(async move { + Self::main_loop(data).await; + }); + + Ok(()) + } + + pub async fn run_as_server(&mut self) -> Result<(), Error> { + self.data.peer_mgr.get_peer_rpc_mgr().run_service( + constants::UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID, + UdpHolePunchRpcServer::new(self.data.clone()).serve(), + ); + + Ok(()) + } + + pub async fn run(&mut self) -> Result<(), Error> { + self.run_as_client().await?; + self.run_as_server().await?; + + Ok(()) + } + + async fn collect_peer_to_connect(data: Arc) -> Vec { + let mut peers_to_connect = Vec::new(); + + // do not do anything if: + // 1. our stun test has not finished + // 2. our nat type is OpenInternet or NoPat, which means we can wait other peers to connect us + let my_nat_type = data + .global_ctx + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type; + + let my_nat_type = NatType::try_from(my_nat_type).unwrap(); + + if my_nat_type == NatType::Unknown + || my_nat_type == NatType::OpenInternet + || my_nat_type == NatType::NoPat + { + return peers_to_connect; + } + + // collect peer list from peer manager and do some filter: + // 1. peers without direct conns; + // 2. peers is full cone (any restricted type); + for route in data.peer_mgr.list_routes().await.iter() { + let Some(peer_stun_info) = route.stun_info.as_ref() else { + continue; + }; + let Ok(peer_nat_type) = NatType::try_from(peer_stun_info.udp_nat_type) else { + continue; + }; + + let peer_id: PeerId = route.peer_id.parse().unwrap(); + let conns = data.peer_mgr.list_peer_conns(&peer_id).await; + if conns.is_some() && conns.unwrap().len() > 0 { + continue; + } + + // if peer is symmetric ignore it because we cannot connect to it + // if peer is open internet or no pat, direct connector will connecto to it + if peer_nat_type == NatType::Unknown + || peer_nat_type == NatType::OpenInternet + || peer_nat_type == NatType::NoPat + || peer_nat_type == NatType::Symmetric + || peer_nat_type == NatType::SymUdpFirewall + { + continue; + } + + // if we are symmetric, we can only connect to full cone + // TODO: can also connect to restricted full cone, with some extra work + if (my_nat_type == NatType::Symmetric || my_nat_type == NatType::SymUdpFirewall) + && peer_nat_type != NatType::FullCone + { + continue; + } + + // if we have smae level of full cone, node with smaller peer_id will be the initiator + if my_nat_type == peer_nat_type { + if data.global_ctx.id > peer_id { + continue; + } + } else { + // if we have different level of full cone + // we will be the initiator if we have more strict level + if my_nat_type < peer_nat_type { + continue; + } + } + + tracing::info!( + ?peer_id, + ?peer_nat_type, + ?my_nat_type, + ?data.global_ctx.id, + "found peer to do hole punching" + ); + + peers_to_connect.push(peer_id); + } + + peers_to_connect + } + + #[tracing::instrument] + async fn do_hole_punching( + data: Arc, + dst_peer_id: PeerId, + ) -> Result, anyhow::Error> { + tracing::info!(?dst_peer_id, "start hole punching"); + // client: choose a local udp port, and get the pubic mapped port from stun server + let socket = UdpSocket::bind("0.0.0.0:0").await.with_context(|| "")?; + let local_socket_addr = socket.local_addr()?; + let local_port = socket.local_addr()?.port(); + drop(socket); // drop the socket to release the port + + let local_mapped_addr = data + .global_ctx + .get_stun_info_collector() + .get_udp_port_mapping(local_port) + .await + .with_context(|| "failed to get udp port mapping")?; + + // client -> server: tell server the mapped port, server will return the mapped address of listening port. + let Some(remote_mapped_addr) = data + .peer_mgr + .get_peer_rpc_mgr() + .do_client_rpc_scoped( + constants::UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID, + dst_peer_id, + |c| async { + let client = + UdpHolePunchServiceClient::new(tarpc::client::Config::default(), c).spawn(); + let remote_mapped_addr = client + .try_punch_hole(tarpc::context::current(), local_mapped_addr) + .await; + tracing::info!(?remote_mapped_addr, ?dst_peer_id, "got remote mapped addr"); + remote_mapped_addr + }, + ) + .await? + else { + return Err(anyhow::anyhow!("failed to get remote mapped addr")); + }; + + // server: will send some punching resps, total 10 packets. + // client: use the socket to create UdpTunnel with UdpTunnelConnector + // NOTICE: UdpTunnelConnector will ignore the punching resp packet sent by remote. + + let connector = UdpTunnelConnector::new( + format!( + "udp://{}:{}", + remote_mapped_addr.ip(), + remote_mapped_addr.port() + ) + .to_string() + .parse() + .unwrap(), + ); + + let socket = UdpSocket::bind(local_socket_addr) + .await + .with_context(|| "")?; + Ok(connector + .try_connect_with_socket(socket) + .await + .with_context(|| "UdpTunnelConnector failed to connect remote")?) + } + + async fn main_loop(data: Arc) { + loop { + let peers_to_connect = Self::collect_peer_to_connect(data.clone()).await; + tracing::trace!(?peers_to_connect, "peers to connect"); + if peers_to_connect.len() == 0 { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + continue; + } + + let mut tasks: JoinSet> = JoinSet::new(); + for peer_id in peers_to_connect { + let data = data.clone(); + tasks.spawn( + async move { + let tunnel = Self::do_hole_punching(data.clone(), peer_id) + .await + .with_context(|| "failed to do hole punching")?; + + let _ = + data.peer_mgr + .add_client_tunnel(tunnel) + .await + .with_context(|| { + "failed to add tunnel as client in hole punch connector" + })?; + + Ok(()) + } + .instrument(tracing::info_span!("doing hole punching client", ?peer_id)), + ); + } + + while let Some(res) = tasks.join_next().await { + if let Err(e) = res { + tracing::error!(?e, "failed to join hole punching job"); + continue; + } + + match res.unwrap() { + Err(e) => { + tracing::error!(?e, "failed to do hole punching job"); + } + Ok(_) => { + tracing::info!("hole punching job succeed"); + } + } + } + + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use easytier_rpc::{NatType, StunInfo}; + + use crate::{ + common::{error::Error, stun::StunInfoCollectorTrait}, + connector::udp_hole_punch::UdpHolePunchConnector, + peers::{ + peer_manager::PeerManager, + tests::{ + connect_peer_manager, create_mock_peer_manager, wait_route_appear, + wait_route_appear_with_cost, + }, + }, + tests::enable_log, + }; + + struct MockStunInfoCollector { + udp_nat_type: NatType, + } + + #[async_trait::async_trait] + impl StunInfoCollectorTrait for MockStunInfoCollector { + fn get_stun_info(&self) -> StunInfo { + StunInfo { + udp_nat_type: self.udp_nat_type as i32, + tcp_nat_type: NatType::Unknown as i32, + last_update_time: std::time::Instant::now().elapsed().as_secs() as i64, + } + } + + async fn get_udp_port_mapping(&self, port: u16) -> Result { + Ok(format!("127.0.0.1:{}", port).parse().unwrap()) + } + } + + async fn create_mock_peer_manager_with_mock_stun(udp_nat_type: NatType) -> Arc { + let p_a = create_mock_peer_manager().await; + let collector = Box::new(MockStunInfoCollector { udp_nat_type }); + p_a.get_global_ctx().replace_stun_info_collector(collector); + p_a + } + + #[tokio::test] + async fn hole_punching() { + enable_log(); + let p_a = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + let p_b = create_mock_peer_manager_with_mock_stun(NatType::Symmetric).await; + let p_c = create_mock_peer_manager_with_mock_stun(NatType::PortRestricted).await; + connect_peer_manager(p_a.clone(), p_b.clone()).await; + connect_peer_manager(p_b.clone(), p_c.clone()).await; + + wait_route_appear(p_a.clone(), p_c.my_node_id()) + .await + .unwrap(); + + println!("{:?}", p_a.list_routes().await); + + let mut hole_punching_a = UdpHolePunchConnector::new(p_a.get_global_ctx(), p_a.clone()); + let mut hole_punching_c = UdpHolePunchConnector::new(p_c.get_global_ctx(), p_c.clone()); + + hole_punching_a.run().await.unwrap(); + hole_punching_c.run().await.unwrap(); + + wait_route_appear_with_cost(p_a.clone(), p_c.my_node_id(), Some(1)) + .await + .unwrap(); + println!("{:?}", p_a.list_routes().await); + } +} diff --git a/easytier-core/src/gateway/icmp_proxy.rs b/easytier-core/src/gateway/icmp_proxy.rs new file mode 100644 index 0000000..ce19975 --- /dev/null +++ b/easytier-core/src/gateway/icmp_proxy.rs @@ -0,0 +1,301 @@ +use std::{ + mem::MaybeUninit, + net::{IpAddr, Ipv4Addr, SocketAddrV4}, + sync::Arc, + thread, +}; + +use pnet::packet::{ + icmp::{self, IcmpTypes}, + ip::IpNextHeaderProtocols, + ipv4::{self, Ipv4Packet, MutableIpv4Packet}, + Packet, +}; +use socket2::Socket; +use tokio::{ + sync::{mpsc::UnboundedSender, Mutex}, + task::JoinSet, +}; +use tokio_util::bytes::Bytes; +use tracing::Instrument; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx}, + peers::{ + packet, + peer_manager::{PeerManager, PeerPacketFilter}, + PeerId, + }, +}; + +use super::CidrSet; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct IcmpNatKey { + dst_ip: std::net::IpAddr, + icmp_id: u16, + icmp_seq: u16, +} + +#[derive(Debug)] +struct IcmpNatEntry { + src_peer_id: PeerId, + my_peer_id: PeerId, + src_ip: IpAddr, + start_time: std::time::Instant, +} + +impl IcmpNatEntry { + fn new(src_peer_id: PeerId, my_peer_id: PeerId, src_ip: IpAddr) -> Result { + Ok(Self { + src_peer_id, + my_peer_id, + src_ip, + start_time: std::time::Instant::now(), + }) + } +} + +type IcmpNatTable = Arc>; +type NewPacketSender = tokio::sync::mpsc::UnboundedSender; +type NewPacketReceiver = tokio::sync::mpsc::UnboundedReceiver; + +#[derive(Debug)] +pub struct IcmpProxy { + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + + cidr_set: CidrSet, + socket: socket2::Socket, + + nat_table: IcmpNatTable, + + tasks: Mutex>, +} + +fn socket_recv(socket: &Socket, buf: &mut [MaybeUninit]) -> Result<(usize, IpAddr), Error> { + let (size, addr) = socket.recv_from(buf)?; + let addr = match addr.as_socket() { + None => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + Some(add) => add.ip(), + }; + Ok((size, addr)) +} + +fn socket_recv_loop( + socket: Socket, + nat_table: IcmpNatTable, + sender: UnboundedSender, +) { + let mut buf = [0u8; 4096]; + let data: &mut [MaybeUninit] = unsafe { std::mem::transmute(&mut buf[12..]) }; + + loop { + let Ok((len, peer_ip)) = socket_recv(&socket, data) else { + continue; + }; + + if !peer_ip.is_ipv4() { + continue; + } + + let Some(mut ipv4_packet) = MutableIpv4Packet::new(&mut buf[12..12 + len]) else { + continue; + }; + + let Some(icmp_packet) = icmp::echo_reply::EchoReplyPacket::new(ipv4_packet.payload()) + else { + continue; + }; + + if icmp_packet.get_icmp_type() != IcmpTypes::EchoReply { + continue; + } + + let key = IcmpNatKey { + dst_ip: peer_ip, + icmp_id: icmp_packet.get_identifier(), + icmp_seq: icmp_packet.get_sequence_number(), + }; + + let Some((_, v)) = nat_table.remove(&key) else { + continue; + }; + + // send packet back to the peer where this request origin. + let IpAddr::V4(dest_ip) = v.src_ip else { + continue; + }; + + ipv4_packet.set_destination(dest_ip); + ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); + + let peer_packet = packet::Packet::new_data_packet( + v.my_peer_id, + v.src_peer_id, + &ipv4_packet.to_immutable().packet(), + ); + + if let Err(e) = sender.send(peer_packet) { + tracing::error!("send icmp packet to peer failed: {:?}, may exiting..", e); + break; + } + } +} + +#[async_trait::async_trait] +impl PeerPacketFilter for IcmpProxy { + async fn try_process_packet_from_peer( + &self, + packet: &packet::ArchivedPacket, + _: &Bytes, + ) -> Option<()> { + let _ = self.global_ctx.get_ipv4()?; + + let packet::ArchivedPacketBody::Data(x) = &packet.body else { + return None; + }; + + let ipv4 = Ipv4Packet::new(&x.data)?; + + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Icmp + { + return None; + } + + if !self.cidr_set.contains_v4(ipv4.get_destination()) { + return None; + } + + let icmp_packet = icmp::echo_request::EchoRequestPacket::new(&ipv4.payload())?; + + if icmp_packet.get_icmp_type() != IcmpTypes::EchoRequest { + // drop it because we do not support other icmp types + tracing::trace!("unsupported icmp type: {:?}", icmp_packet.get_icmp_type()); + return Some(()); + } + + let icmp_id = icmp_packet.get_identifier(); + let icmp_seq = icmp_packet.get_sequence_number(); + + let key = IcmpNatKey { + dst_ip: ipv4.get_destination().into(), + icmp_id, + icmp_seq, + }; + + if packet.to_peer.is_none() { + return None; + } + + let value = IcmpNatEntry::new( + packet.from_peer.to_uuid(), + packet.to_peer.as_ref().unwrap().to_uuid(), + ipv4.get_source().into(), + ) + .ok()?; + + if let Some(old) = self.nat_table.insert(key, value) { + tracing::info!("icmp nat table entry replaced: {:?}", old); + } + + if let Err(e) = self.send_icmp_packet(ipv4.get_destination(), &icmp_packet) { + tracing::error!("send icmp packet failed: {:?}", e); + } + + Some(()) + } +} + +impl IcmpProxy { + pub fn new( + global_ctx: ArcGlobalCtx, + peer_manager: Arc, + ) -> Result, Error> { + let cidr_set = CidrSet::new(global_ctx.clone()); + + let _g = global_ctx.net_ns.guard(); + let socket = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::RAW, + Some(socket2::Protocol::ICMPV4), + )?; + socket.bind(&socket2::SockAddr::from(SocketAddrV4::new( + std::net::Ipv4Addr::UNSPECIFIED, + 0, + )))?; + + let ret = Self { + global_ctx, + peer_manager, + cidr_set, + socket, + + nat_table: Arc::new(dashmap::DashMap::new()), + tasks: Mutex::new(JoinSet::new()), + }; + + Ok(Arc::new(ret)) + } + + pub async fn start(self: &Arc) -> Result<(), Error> { + self.start_icmp_proxy().await?; + self.start_nat_table_cleaner().await?; + Ok(()) + } + + async fn start_nat_table_cleaner(self: &Arc) -> Result<(), Error> { + let nat_table = self.nat_table.clone(); + self.tasks.lock().await.spawn( + async move { + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + nat_table.retain(|_, v| v.start_time.elapsed().as_secs() < 20); + } + } + .instrument(tracing::info_span!("icmp proxy nat table cleaner")), + ); + Ok(()) + } + + async fn start_icmp_proxy(self: &Arc) -> Result<(), Error> { + let socket = self.socket.try_clone()?; + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); + let nat_table = self.nat_table.clone(); + thread::spawn(|| { + socket_recv_loop(socket, nat_table, sender); + }); + + let peer_manager = self.peer_manager.clone(); + self.tasks.lock().await.spawn( + async move { + while let Some(msg) = receiver.recv().await { + let to_peer_id: uuid::Uuid = msg.to_peer.as_ref().unwrap().clone().into(); + let ret = peer_manager.send_msg(msg.into(), &to_peer_id).await; + if ret.is_err() { + tracing::error!("send icmp packet to peer failed: {:?}", ret); + } + } + } + .instrument(tracing::info_span!("icmp proxy send loop")), + ); + + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + Ok(()) + } + + fn send_icmp_packet( + &self, + dst_ip: Ipv4Addr, + icmp_packet: &icmp::echo_request::EchoRequestPacket, + ) -> Result<(), Error> { + self.socket.send_to( + icmp_packet.packet(), + &SocketAddrV4::new(dst_ip.into(), 0).into(), + )?; + + Ok(()) + } +} diff --git a/easytier-core/src/gateway/mod.rs b/easytier-core/src/gateway/mod.rs new file mode 100644 index 0000000..07b2815 --- /dev/null +++ b/easytier-core/src/gateway/mod.rs @@ -0,0 +1,51 @@ +use dashmap::DashSet; +use std::sync::Arc; +use tokio::task::JoinSet; + +use crate::common::global_ctx::ArcGlobalCtx; + +pub mod icmp_proxy; +pub mod tcp_proxy; + +#[derive(Debug)] +struct CidrSet { + global_ctx: ArcGlobalCtx, + cidr_set: Arc>, + tasks: JoinSet<()>, +} + +impl CidrSet { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + let mut ret = Self { + global_ctx, + cidr_set: Arc::new(DashSet::new()), + tasks: JoinSet::new(), + }; + ret.run_cidr_updater(); + ret + } + + fn run_cidr_updater(&mut self) { + let global_ctx = self.global_ctx.clone(); + let cidr_set = self.cidr_set.clone(); + self.tasks.spawn(async move { + let mut last_cidrs = vec![]; + loop { + let cidrs = global_ctx.get_proxy_cidrs(); + if cidrs != last_cidrs { + last_cidrs = cidrs.clone(); + cidr_set.clear(); + for cidr in cidrs.iter() { + cidr_set.insert(cidr.clone()); + } + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + } + + pub fn contains_v4(&self, ip: std::net::Ipv4Addr) -> bool { + let ip = ip.into(); + return self.cidr_set.iter().any(|cidr| cidr.contains(&ip)); + } +} diff --git a/easytier-core/src/gateway/tcp_proxy.rs b/easytier-core/src/gateway/tcp_proxy.rs new file mode 100644 index 0000000..8802e94 --- /dev/null +++ b/easytier-core/src/gateway/tcp_proxy.rs @@ -0,0 +1,402 @@ +use crossbeam::atomic::AtomicCell; +use dashmap::DashMap; +use pnet::packet::ip::IpNextHeaderProtocols; +use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet}; +use pnet::packet::tcp::{ipv4_checksum, MutableTcpPacket}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::atomic::AtomicU16; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::io::copy_bidirectional; +use tokio::net::{TcpListener, TcpSocket, TcpStream}; +use tokio::sync::Mutex; +use tokio::task::JoinSet; +use tokio_util::bytes::{Bytes, BytesMut}; +use tracing::Instrument; + +use crate::common::error::Result; +use crate::common::global_ctx::GlobalCtx; +use crate::common::netns::NetNS; +use crate::peers::packet::{self, ArchivedPacket}; +use crate::peers::peer_manager::{NicPacketFilter, PeerManager, PeerPacketFilter}; + +use super::CidrSet; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum NatDstEntryState { + // receive syn packet but not start connecting to dst + SynReceived, + // connecting to dst + ConnectingDst, + // connected to dst + Connected, + // connection closed + Closed, +} + +#[derive(Debug)] +pub struct NatDstEntry { + id: uuid::Uuid, + src: SocketAddr, + dst: SocketAddr, + start_time: Instant, + tasks: Mutex>, + state: AtomicCell, +} + +impl NatDstEntry { + pub fn new(src: SocketAddr, dst: SocketAddr) -> Self { + Self { + id: uuid::Uuid::new_v4(), + src, + dst, + start_time: Instant::now(), + tasks: Mutex::new(JoinSet::new()), + state: AtomicCell::new(NatDstEntryState::SynReceived), + } + } +} + +type ArcNatDstEntry = Arc; + +type SynSockMap = Arc>; +type ConnSockMap = Arc>; +// peer src addr to nat entry, when respond tcp packet, should modify the tcp src addr to the nat entry's dst addr +type AddrConnSockMap = Arc>; + +#[derive(Debug)] +pub struct TcpProxy { + global_ctx: Arc, + peer_manager: Arc, + local_port: AtomicU16, + + tasks: Arc>>, + + syn_map: SynSockMap, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + + cidr_set: CidrSet, +} + +#[async_trait::async_trait] +impl PeerPacketFilter for TcpProxy { + async fn try_process_packet_from_peer(&self, packet: &ArchivedPacket, _: &Bytes) -> Option<()> { + let ipv4_addr = self.global_ctx.get_ipv4()?; + + let packet::ArchivedPacketBody::Data(x) = &packet.body else { + return None; + }; + + let ipv4 = Ipv4Packet::new(&x.data)?; + if ipv4.get_version() != 4 || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { + return None; + } + + if !self.cidr_set.contains_v4(ipv4.get_destination()) { + return None; + } + + tracing::trace!(ipv4 = ?ipv4, cidr_set = ?self.cidr_set, "proxy tcp packet received"); + + let mut packet_buffer = BytesMut::with_capacity(x.data.len()); + packet_buffer.extend_from_slice(&x.data.to_vec()); + + let (ip_buffer, tcp_buffer) = + packet_buffer.split_at_mut(ipv4.get_header_length() as usize * 4); + + let mut ip_packet = MutableIpv4Packet::new(ip_buffer).unwrap(); + let mut tcp_packet = MutableTcpPacket::new(tcp_buffer).unwrap(); + + let is_tcp_syn = tcp_packet.get_flags() & pnet::packet::tcp::TcpFlags::SYN != 0; + if is_tcp_syn { + let source_ip = ip_packet.get_source(); + let source_port = tcp_packet.get_source(); + let src = SocketAddr::V4(SocketAddrV4::new(source_ip, source_port)); + + 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 old_val = self + .syn_map + .insert(src, Arc::new(NatDstEntry::new(src, dst))); + tracing::trace!(src = ?src, dst = ?dst, old_entry = ?old_val, "tcp syn received"); + } + + ip_packet.set_destination(ipv4_addr); + tcp_packet.set_destination(self.get_local_port()); + Self::update_ipv4_packet_checksum(&mut ip_packet, &mut tcp_packet); + + tracing::trace!(ip_packet = ?ip_packet, tcp_packet = ?tcp_packet, "tcp packet forwarded"); + + if let Err(e) = self + .peer_manager + .get_nic_channel() + .send(packet_buffer.freeze()) + .await + { + tracing::error!("send to nic failed: {:?}", e); + } + + Some(()) + } +} + +#[async_trait::async_trait] +impl NicPacketFilter for TcpProxy { + async fn try_process_packet_from_nic(&self, mut data: BytesMut) -> BytesMut { + let Some(my_ipv4) = self.global_ctx.get_ipv4() else { + return data; + }; + + let header_len = { + let Some(ipv4) = &Ipv4Packet::new(&data[..]) else { + return data; + }; + + if ipv4.get_version() != 4 + || ipv4.get_source() != my_ipv4 + || ipv4.get_next_level_protocol() != IpNextHeaderProtocols::Tcp + { + return data; + } + + ipv4.get_header_length() as usize * 4 + }; + + let (ip_buffer, tcp_buffer) = data.split_at_mut(header_len); + let mut ip_packet = MutableIpv4Packet::new(ip_buffer).unwrap(); + let mut tcp_packet = MutableTcpPacket::new(tcp_buffer).unwrap(); + + if tcp_packet.get_source() != self.get_local_port() { + return data; + } + + let dst_addr = SocketAddr::V4(SocketAddrV4::new( + ip_packet.get_destination(), + tcp_packet.get_destination(), + )); + + tracing::trace!(dst_addr = ?dst_addr, "tcp packet try find entry"); + let entry = if let Some(entry) = self.addr_conn_map.get(&dst_addr) { + entry + } else { + let Some(syn_entry) = self.syn_map.get(&dst_addr) else { + return data; + }; + syn_entry + }; + let nat_entry = entry.clone(); + drop(entry); + assert_eq!(nat_entry.src, dst_addr); + + let IpAddr::V4(ip) = nat_entry.dst.ip() else { + panic!("v4 nat entry src ip is not v4"); + }; + + ip_packet.set_source(ip); + tcp_packet.set_source(nat_entry.dst.port()); + Self::update_ipv4_packet_checksum(&mut ip_packet, &mut tcp_packet); + + tracing::trace!(dst_addr = ?dst_addr, nat_entry = ?nat_entry, packet = ?ip_packet, "tcp packet after modified"); + + data + } +} + +impl TcpProxy { + pub fn new(global_ctx: Arc, peer_manager: Arc) -> Arc { + Arc::new(Self { + global_ctx: global_ctx.clone(), + peer_manager, + + local_port: AtomicU16::new(0), + tasks: Arc::new(Mutex::new(JoinSet::new())), + + syn_map: Arc::new(DashMap::new()), + conn_map: Arc::new(DashMap::new()), + addr_conn_map: Arc::new(DashMap::new()), + + cidr_set: CidrSet::new(global_ctx), + }) + } + + fn update_ipv4_packet_checksum( + ipv4_packet: &mut MutableIpv4Packet, + tcp_packet: &mut MutableTcpPacket, + ) { + tcp_packet.set_checksum(ipv4_checksum( + &tcp_packet.to_immutable(), + &ipv4_packet.get_source(), + &ipv4_packet.get_destination(), + )); + + ipv4_packet.set_checksum(pnet::packet::ipv4::checksum(&ipv4_packet.to_immutable())); + } + + pub async fn start(self: &Arc) -> Result<()> { + self.run_syn_map_cleaner().await?; + self.run_listener().await?; + self.peer_manager + .add_packet_process_pipeline(Box::new(self.clone())) + .await; + self.peer_manager + .add_nic_packet_process_pipeline(Box::new(self.clone())) + .await; + + Ok(()) + } + + async fn run_syn_map_cleaner(&self) -> Result<()> { + let syn_map = self.syn_map.clone(); + let tasks = self.tasks.clone(); + let syn_map_cleaner_task = async move { + loop { + syn_map.retain(|_, entry| { + if entry.start_time.elapsed() > Duration::from_secs(30) { + tracing::warn!(entry = ?entry, "syn nat entry expired"); + entry.state.store(NatDstEntryState::Closed); + false + } else { + true + } + }); + tokio::time::sleep(Duration::from_secs(10)).await; + } + }; + tasks.lock().await.spawn(syn_map_cleaner_task); + + Ok(()) + } + + async fn run_listener(&self) -> Result<()> { + // bind on both v4 & v6 + let listen_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); + + let net_ns = self.global_ctx.net_ns.clone(); + let tcp_listener = net_ns + .run_async(|| async { TcpListener::bind(&listen_addr).await }) + .await?; + + self.local_port.store( + tcp_listener.local_addr()?.port(), + std::sync::atomic::Ordering::Relaxed, + ); + + let tasks = self.tasks.clone(); + let syn_map = self.syn_map.clone(); + let conn_map = self.conn_map.clone(); + let addr_conn_map = self.addr_conn_map.clone(); + let accept_task = async move { + tracing::info!(listener = ?tcp_listener, "tcp connection start accepting"); + + let conn_map = conn_map.clone(); + while let Ok((tcp_stream, socket_addr)) = tcp_listener.accept().await { + let Some(entry) = syn_map.get(&socket_addr) else { + tracing::error!("tcp connection from unknown source: {:?}", socket_addr); + continue; + }; + assert_eq!(entry.state.load(), NatDstEntryState::SynReceived); + + let entry_clone = entry.clone(); + drop(entry); + syn_map.remove_if(&socket_addr, |_, entry| entry.id == entry_clone.id); + + entry_clone.state.store(NatDstEntryState::ConnectingDst); + + let _ = addr_conn_map.insert(entry_clone.src, entry_clone.clone()); + let old_nat_val = conn_map.insert(entry_clone.id, entry_clone.clone()); + assert!(old_nat_val.is_none()); + + tasks.lock().await.spawn(Self::connect_to_nat_dst( + net_ns.clone(), + tcp_stream, + conn_map.clone(), + addr_conn_map.clone(), + entry_clone, + )); + } + tracing::error!("nat tcp listener exited"); + panic!("nat tcp listener exited"); + }; + self.tasks + .lock() + .await + .spawn(accept_task.instrument(tracing::info_span!("tcp_proxy_listener"))); + + Ok(()) + } + + fn remove_entry_from_all_conn_map( + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + conn_map.remove(&nat_entry.id); + addr_conn_map.remove_if(&nat_entry.src, |_, entry| entry.id == nat_entry.id); + } + + async fn connect_to_nat_dst( + net_ns: NetNS, + src_tcp_stream: TcpStream, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + if let Err(e) = src_tcp_stream.set_nodelay(true) { + tracing::warn!("set_nodelay failed, ignore it: {:?}", e); + } + + let _guard = net_ns.guard(); + let socket = TcpSocket::new_v4().unwrap(); + if let Err(e) = socket.set_nodelay(true) { + tracing::warn!("set_nodelay failed, ignore it: {:?}", e); + } + let Ok(Ok(dst_tcp_stream)) = tokio::time::timeout( + Duration::from_secs(10), + TcpSocket::new_v4().unwrap().connect(nat_entry.dst), + ) + .await + else { + tracing::error!("connect to dst failed: {:?}", nat_entry); + nat_entry.state.store(NatDstEntryState::Closed); + Self::remove_entry_from_all_conn_map(conn_map, addr_conn_map, nat_entry); + return; + }; + drop(_guard); + + assert_eq!(nat_entry.state.load(), NatDstEntryState::ConnectingDst); + nat_entry.state.store(NatDstEntryState::Connected); + + Self::handle_nat_connection( + src_tcp_stream, + dst_tcp_stream, + conn_map, + addr_conn_map, + nat_entry, + ) + .await; + } + + async fn handle_nat_connection( + mut src_tcp_stream: TcpStream, + mut dst_tcp_stream: TcpStream, + conn_map: ConnSockMap, + addr_conn_map: AddrConnSockMap, + nat_entry: ArcNatDstEntry, + ) { + let nat_entry_clone = nat_entry.clone(); + nat_entry.tasks.lock().await.spawn(async move { + let ret = copy_bidirectional(&mut src_tcp_stream, &mut dst_tcp_stream).await; + tracing::trace!(nat_entry = ?nat_entry_clone, ret = ?ret, "nat tcp connection closed"); + nat_entry_clone.state.store(NatDstEntryState::Closed); + + Self::remove_entry_from_all_conn_map(conn_map, addr_conn_map, nat_entry_clone); + }); + } + + pub fn get_local_port(&self) -> u16 { + self.local_port.load(std::sync::atomic::Ordering::Relaxed) + } +} diff --git a/easytier-core/src/instance/instance.rs b/easytier-core/src/instance/instance.rs new file mode 100644 index 0000000..bd2b44b --- /dev/null +++ b/easytier-core/src/instance/instance.rs @@ -0,0 +1,413 @@ +use std::borrow::BorrowMut; +use std::io::Write; +use std::sync::Arc; + +use futures::StreamExt; +use pnet::packet::ethernet::EthernetPacket; +use pnet::packet::ipv4::Ipv4Packet; + +use tokio::{sync::Mutex, task::JoinSet}; +use tokio_util::bytes::{Bytes, BytesMut}; +use tonic::transport::Server; +use uuid::Uuid; + +use crate::common::config_fs::ConfigFs; +use crate::common::error::Error; +use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx}; +use crate::common::netns::NetNS; +use crate::connector::direct::DirectConnectorManager; +use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager}; +use crate::connector::udp_hole_punch::UdpHolePunchConnector; +use crate::gateway::icmp_proxy::IcmpProxy; +use crate::gateway::tcp_proxy::TcpProxy; +use crate::peers::peer_manager::PeerManager; +use crate::peers::rip_route::BasicRoute; +use crate::peers::rpc_service::PeerManagerRpcService; +use crate::tunnels::SinkItem; + +use tokio_stream::wrappers::ReceiverStream; + +use super::listeners::ListenerManager; +use super::virtual_nic; + +pub struct InstanceConfigWriter { + config: ConfigFs, +} + +impl InstanceConfigWriter { + pub fn new(inst_name: &str) -> Self { + InstanceConfigWriter { + config: ConfigFs::new(inst_name), + } + } + + pub fn set_ns(self, net_ns: Option) -> Self { + let net_ns_in_conf = if let Some(net_ns) = net_ns { + net_ns + } else { + "".to_string() + }; + + self.config + .add_file("net_ns") + .unwrap() + .write_all(net_ns_in_conf.as_bytes()) + .unwrap(); + + self + } + + pub fn set_addr(self, addr: String) -> Self { + self.config + .add_file("ipv4") + .unwrap() + .write_all(addr.as_bytes()) + .unwrap(); + self + } +} + +pub struct Instance { + inst_name: String, + + id: uuid::Uuid, + + virtual_nic: Option>, + peer_packet_receiver: Option>, + + tasks: JoinSet<()>, + + peer_manager: Arc, + listener_manager: Arc>>, + conn_manager: Arc, + direct_conn_manager: Arc, + udp_hole_puncher: Arc>, + + tcp_proxy: Arc, + icmp_proxy: Arc, + + global_ctx: ArcGlobalCtx, +} + +impl Instance { + pub fn new(inst_name: &str) -> Self { + let config = ConfigFs::new(inst_name); + let net_ns_in_conf = config.get_or_default("net_ns", || "".to_string()).unwrap(); + let net_ns = NetNS::new(if net_ns_in_conf.is_empty() { + None + } else { + Some(net_ns_in_conf.clone()) + }); + + let addr = config + .get_or_default("ipv4", || "10.144.144.10".to_string()) + .unwrap(); + + log::info!( + "[INIT] instance creating. inst_name: {}, addr: {}, netns: {}", + inst_name, + addr, + net_ns_in_conf + ); + + let (peer_packet_sender, peer_packet_receiver) = tokio::sync::mpsc::channel(100); + + let global_ctx = Arc::new(GlobalCtx::new(inst_name, config, net_ns.clone())); + + let id = global_ctx.get_id(); + + let peer_manager = Arc::new(PeerManager::new( + global_ctx.clone(), + peer_packet_sender.clone(), + )); + + let listener_manager = Arc::new(Mutex::new(ListenerManager::new( + id, + net_ns.clone(), + peer_manager.clone(), + ))); + + let conn_manager = Arc::new(ManualConnectorManager::new( + id, + global_ctx.clone(), + peer_manager.clone(), + )); + + let mut direct_conn_manager = + DirectConnectorManager::new(id, global_ctx.clone(), peer_manager.clone()); + direct_conn_manager.run(); + + let udp_hole_puncher = UdpHolePunchConnector::new(global_ctx.clone(), peer_manager.clone()); + + let arc_tcp_proxy = TcpProxy::new(global_ctx.clone(), peer_manager.clone()); + let arc_icmp_proxy = IcmpProxy::new(global_ctx.clone(), peer_manager.clone()).unwrap(); + + Instance { + inst_name: inst_name.to_string(), + id, + + virtual_nic: None, + peer_packet_receiver: Some(ReceiverStream::new(peer_packet_receiver)), + + tasks: JoinSet::new(), + peer_manager, + listener_manager, + conn_manager, + direct_conn_manager: Arc::new(direct_conn_manager), + udp_hole_puncher: Arc::new(Mutex::new(udp_hole_puncher)), + + tcp_proxy: arc_tcp_proxy, + icmp_proxy: arc_icmp_proxy, + + global_ctx, + } + } + + pub fn get_conn_manager(&self) -> Arc { + self.conn_manager.clone() + } + + async fn do_forward_nic_to_peers_ipv4(ret: BytesMut, mgr: &PeerManager) { + if let Some(ipv4) = Ipv4Packet::new(&ret) { + if ipv4.get_version() != 4 { + tracing::info!("[USER_PACKET] not ipv4 packet: {:?}", ipv4); + } + let dst_ipv4 = ipv4.get_destination(); + tracing::trace!( + ?ret, + "[USER_PACKET] recv new packet from tun device and forward to peers." + ); + let send_ret = mgr.send_msg_ipv4(ret, dst_ipv4).await; + if send_ret.is_err() { + tracing::trace!(?send_ret, "[USER_PACKET] send_msg_ipv4 failed") + } + } else { + tracing::warn!(?ret, "[USER_PACKET] not ipv4 packet"); + } + } + + async fn do_forward_nic_to_peers_ethernet(mut ret: BytesMut, mgr: &PeerManager) { + if let Some(eth) = EthernetPacket::new(&ret) { + log::warn!("begin to forward: {:?}, type: {}", eth, eth.get_ethertype()); + Self::do_forward_nic_to_peers_ipv4(ret.split_off(14), mgr).await; + } else { + log::warn!("not ipv4 packet: {:?}", ret); + } + } + + fn do_forward_nic_to_peers(&mut self) -> Result<(), Error> { + // read from nic and write to corresponding tunnel + let nic = self.virtual_nic.as_ref().unwrap(); + let nic = nic.clone(); + let mgr = self.peer_manager.clone(); + + self.tasks.spawn(async move { + let mut stream = nic.pin_recv_stream(); + while let Some(ret) = stream.next().await { + if ret.is_err() { + log::error!("read from nic failed: {:?}", ret); + break; + } + Self::do_forward_nic_to_peers_ipv4(ret.unwrap(), mgr.as_ref()).await; + // Self::do_forward_nic_to_peers_ethernet(ret.into(), mgr.as_ref()).await; + } + }); + + Ok(()) + } + + fn do_forward_peers_to_nic( + tasks: &mut JoinSet<()>, + nic: Arc, + channel: Option>, + ) { + tasks.spawn(async move { + let send = nic.pin_send_stream(); + let channel = channel.unwrap(); + let ret = channel + .map(|packet| { + log::trace!( + "[USER_PACKET] forward packet from peers to nic. packet: {:?}", + packet + ); + Ok(packet) + }) + .forward(send) + .await; + if ret.is_err() { + panic!("do_forward_tunnel_to_nic"); + } + }); + } + + pub async fn run(&mut self) -> Result<(), Error> { + let ipv4_addr = self.global_ctx.get_ipv4().unwrap(); + + let mut nic = virtual_nic::VirtualNic::new(self.get_global_ctx()) + .create_dev() + .await? + .link_up() + .await? + .remove_ip(None) + .await? + .add_ip(ipv4_addr, 24) + .await?; + + if cfg!(target_os = "macos") { + nic = nic.add_route(ipv4_addr, 24).await?; + } + + self.virtual_nic = Some(Arc::new(nic)); + + self.do_forward_nic_to_peers().unwrap(); + Self::do_forward_peers_to_nic( + self.tasks.borrow_mut(), + self.virtual_nic.as_ref().unwrap().clone(), + self.peer_packet_receiver.take(), + ); + + self.listener_manager + .lock() + .await + .prepare_listeners() + .await?; + self.listener_manager.lock().await.run().await?; + self.peer_manager.run().await?; + + let route = BasicRoute::new(self.id(), self.global_ctx.clone()); + self.peer_manager.set_route(route).await; + + self.run_rpc_server().unwrap(); + + self.tcp_proxy.start().await.unwrap(); + self.icmp_proxy.start().await.unwrap(); + self.run_proxy_cidrs_route_updater(); + + self.udp_hole_puncher.lock().await.run().await?; + + Ok(()) + } + + pub fn get_peer_manager(&self) -> Arc { + self.peer_manager.clone() + } + + pub async fn close_peer_conn(&mut self, peer_id: &Uuid, conn_id: &Uuid) -> Result<(), Error> { + self.peer_manager + .get_peer_map() + .close_peer_conn(peer_id, conn_id) + .await?; + Ok(()) + } + + pub async fn wait(&mut self) { + while let Some(ret) = self.tasks.join_next().await { + log::info!("task finished: {:?}", ret); + ret.unwrap(); + } + } + + pub fn id(&self) -> uuid::Uuid { + self.id + } + + fn run_rpc_server(&mut self) -> Result<(), Box> { + let addr = "0.0.0.0:15888".parse()?; + let peer_mgr = self.peer_manager.clone(); + let conn_manager = self.conn_manager.clone(); + let net_ns = self.global_ctx.net_ns.clone(); + + self.tasks.spawn(async move { + let _g = net_ns.guard(); + log::info!("[INIT RPC] start rpc server. addr: {}", addr); + Server::builder() + .add_service( + easytier_rpc::peer_manage_rpc_server::PeerManageRpcServer::new( + PeerManagerRpcService::new(peer_mgr), + ), + ) + .add_service( + easytier_rpc::connector_manage_rpc_server::ConnectorManageRpcServer::new( + ConnectorManagerRpcService(conn_manager.clone()), + ), + ) + .serve(addr) + .await + .unwrap(); + }); + Ok(()) + } + + fn run_proxy_cidrs_route_updater(&mut self) { + let peer_mgr = self.peer_manager.clone(); + let net_ns = self.global_ctx.net_ns.clone(); + let nic = self.virtual_nic.as_ref().unwrap().clone(); + + self.tasks.spawn(async move { + let mut cur_proxy_cidrs = vec![]; + loop { + let mut proxy_cidrs = vec![]; + 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); + } + } + + // if route is in cur_proxy_cidrs but not in proxy_cidrs, delete it. + for cidr in cur_proxy_cidrs.iter() { + if proxy_cidrs.contains(cidr) { + continue; + } + + let _g = net_ns.guard(); + let ret = nic + .get_ifcfg() + .remove_ipv4_route( + nic.ifname(), + cidr.first_address(), + cidr.network_length(), + ) + .await; + + if ret.is_err() { + tracing::trace!( + cidr = ?cidr, + err = ?ret, + "remove route failed.", + ); + } + } + + for cidr in proxy_cidrs.iter() { + if cur_proxy_cidrs.contains(cidr) { + continue; + } + let _g = net_ns.guard(); + let ret = nic + .get_ifcfg() + .add_ipv4_route(nic.ifname(), cidr.first_address(), cidr.network_length()) + .await; + + if ret.is_err() { + tracing::trace!( + cidr = ?cidr, + err = ?ret, + "add route failed.", + ); + } + } + + cur_proxy_cidrs = proxy_cidrs; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + } + + pub fn get_global_ctx(&self) -> ArcGlobalCtx { + self.global_ctx.clone() + } +} diff --git a/easytier-core/src/instance/listeners.rs b/easytier-core/src/instance/listeners.rs new file mode 100644 index 0000000..47309c3 --- /dev/null +++ b/easytier-core/src/instance/listeners.rs @@ -0,0 +1,150 @@ +use std::{fmt::Debug, sync::Arc}; + +use async_trait::async_trait; +use tokio::{sync::Mutex, task::JoinSet}; + +use crate::{ + common::{error::Error, netns::NetNS}, + peers::peer_manager::PeerManager, + tunnels::{ + ring_tunnel::RingTunnelListener, tcp_tunnel::TcpTunnelListener, + udp_tunnel::UdpTunnelListener, Tunnel, TunnelListener, + }, +}; + +#[async_trait] +pub trait TunnelHandlerForListener { + async fn handle_tunnel(&self, tunnel: Box) -> Result<(), Error>; +} + +#[async_trait] +impl TunnelHandlerForListener for PeerManager { + #[tracing::instrument] + async fn handle_tunnel(&self, tunnel: Box) -> Result<(), Error> { + self.add_tunnel_as_server(tunnel).await + } +} + +pub struct ListenerManager { + my_node_id: uuid::Uuid, + net_ns: NetNS, + listeners: Vec>>, + peer_manager: Arc, + + tasks: JoinSet<()>, +} + +impl ListenerManager { + pub fn new(my_node_id: uuid::Uuid, net_ns: NetNS, peer_manager: Arc) -> Self { + Self { + my_node_id, + net_ns, + listeners: Vec::new(), + peer_manager, + tasks: JoinSet::new(), + } + } + + pub async fn prepare_listeners(&mut self) -> Result<(), Error> { + self.add_listener(UdpTunnelListener::new( + "udp://0.0.0.0:11010".parse().unwrap(), + )) + .await?; + self.add_listener(TcpTunnelListener::new( + "tcp://0.0.0.0:11010".parse().unwrap(), + )) + .await?; + self.add_listener(RingTunnelListener::new( + format!("ring://{}", self.my_node_id).parse().unwrap(), + )) + .await?; + Ok(()) + } + + pub async fn add_listener(&mut self, listener: Listener) -> Result<(), Error> + where + Listener: TunnelListener + 'static, + { + let listener = Arc::new(Mutex::new(listener)); + self.listeners.push(listener); + Ok(()) + } + + #[tracing::instrument] + async fn run_listener(listener: Arc>, peer_manager: Arc) { + let mut l = listener.lock().await; + while let Ok(ret) = l.accept().await { + tracing::info!(ret = ?ret, "conn accepted"); + let server_ret = peer_manager.handle_tunnel(ret).await; + if let Err(e) = &server_ret { + tracing::error!(error = ?e, "handle conn error"); + } + } + } + + pub async fn run(&mut self) -> Result<(), Error> { + for listener in &self.listeners { + let _guard = self.net_ns.guard(); + log::warn!("run listener: {:?}", listener); + listener.lock().await.listen().await?; + self.tasks.spawn(Self::run_listener( + listener.clone(), + self.peer_manager.clone(), + )); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use futures::{SinkExt, StreamExt}; + use tokio::time::timeout; + + use crate::tunnels::{ring_tunnel::RingTunnelConnector, TunnelConnector}; + + use super::*; + + #[derive(Debug)] + struct MockListenerHandler {} + + #[async_trait] + impl TunnelHandlerForListener for MockListenerHandler { + async fn handle_tunnel(&self, _tunnel: Box) -> Result<(), Error> { + let data = "abc"; + _tunnel.pin_sink().send(data.into()).await.unwrap(); + Err(Error::Unknown) + } + } + + #[tokio::test] + async fn handle_error_in_accept() { + let net_ns = NetNS::new(None); + let handler = Arc::new(MockListenerHandler {}); + let mut listener_mgr = + ListenerManager::new(uuid::Uuid::new_v4(), net_ns.clone(), handler.clone()); + + let ring_id = format!("ring://{}", uuid::Uuid::new_v4()); + + listener_mgr + .add_listener(RingTunnelListener::new(ring_id.parse().unwrap())) + .await + .unwrap(); + listener_mgr.run().await.unwrap(); + + let connect_once = |ring_id| async move { + let tunnel = RingTunnelConnector::new(ring_id).connect().await.unwrap(); + assert_eq!(tunnel.pin_stream().next().await.unwrap().unwrap(), "abc"); + tunnel + }; + + timeout(std::time::Duration::from_secs(1), async move { + connect_once(ring_id.parse().unwrap()).await; + // handle tunnel fail should not impact the second connect + connect_once(ring_id.parse().unwrap()).await; + }) + .await + .unwrap(); + } +} diff --git a/easytier-core/src/instance/mod.rs b/easytier-core/src/instance/mod.rs new file mode 100644 index 0000000..da4814b --- /dev/null +++ b/easytier-core/src/instance/mod.rs @@ -0,0 +1,4 @@ +pub mod instance; +pub mod listeners; +pub mod tun_codec; +pub mod virtual_nic; diff --git a/easytier-core/src/instance/tun_codec.rs b/easytier-core/src/instance/tun_codec.rs new file mode 100644 index 0000000..e07448d --- /dev/null +++ b/easytier-core/src/instance/tun_codec.rs @@ -0,0 +1,179 @@ +use std::io; + +use byteorder::{NativeEndian, NetworkEndian, WriteBytesExt}; +use tokio_util::bytes::{BufMut, Bytes, BytesMut}; +use tokio_util::codec::{Decoder, Encoder}; + +/// A packet protocol IP version +#[derive(Debug, Clone, Copy, Default)] +enum PacketProtocol { + #[default] + IPv4, + IPv6, + Other(u8), +} + +// Note: the protocol in the packet information header is platform dependent. +impl PacketProtocol { + #[cfg(any(target_os = "linux", target_os = "android"))] + fn into_pi_field(self) -> Result { + use nix::libc; + match self { + PacketProtocol::IPv4 => Ok(libc::ETH_P_IP as u16), + PacketProtocol::IPv6 => Ok(libc::ETH_P_IPV6 as u16), + PacketProtocol::Other(_) => Err(io::Error::new( + io::ErrorKind::Other, + "neither an IPv4 nor IPv6 packet", + )), + } + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + fn into_pi_field(self) -> Result { + use nix::libc; + match self { + PacketProtocol::IPv4 => Ok(libc::PF_INET as u16), + PacketProtocol::IPv6 => Ok(libc::PF_INET6 as u16), + PacketProtocol::Other(_) => Err(io::Error::new( + io::ErrorKind::Other, + "neither an IPv4 nor IPv6 packet", + )), + } + } + + #[cfg(target_os = "windows")] + fn into_pi_field(self) -> Result { + unimplemented!() + } +} + +#[derive(Debug)] +pub enum TunPacketBuffer { + Bytes(Bytes), + BytesMut(BytesMut), +} + +impl From for Bytes { + fn from(buf: TunPacketBuffer) -> Self { + match buf { + TunPacketBuffer::Bytes(bytes) => bytes, + TunPacketBuffer::BytesMut(bytes) => bytes.freeze(), + } + } +} + +impl AsRef<[u8]> for TunPacketBuffer { + fn as_ref(&self) -> &[u8] { + match self { + TunPacketBuffer::Bytes(bytes) => bytes.as_ref(), + TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(), + } + } +} + +/// A Tun Packet to be sent or received on the TUN interface. +#[derive(Debug)] +pub struct TunPacket(PacketProtocol, TunPacketBuffer); + +/// Infer the protocol based on the first nibble in the packet buffer. +fn infer_proto(buf: &[u8]) -> PacketProtocol { + match buf[0] >> 4 { + 4 => PacketProtocol::IPv4, + 6 => PacketProtocol::IPv6, + p => PacketProtocol::Other(p), + } +} + +impl TunPacket { + /// Create a new `TunPacket` based on a byte slice. + pub fn new(buffer: TunPacketBuffer) -> TunPacket { + let proto = infer_proto(buffer.as_ref()); + TunPacket(proto, buffer) + } + + /// Return this packet's bytes. + pub fn get_bytes(&self) -> &[u8] { + match &self.1 { + TunPacketBuffer::Bytes(bytes) => bytes.as_ref(), + TunPacketBuffer::BytesMut(bytes) => bytes.as_ref(), + } + } + + pub fn into_bytes(self) -> Bytes { + match self.1 { + TunPacketBuffer::Bytes(bytes) => bytes, + TunPacketBuffer::BytesMut(bytes) => bytes.freeze(), + } + } + + pub fn into_bytes_mut(self) -> BytesMut { + match self.1 { + TunPacketBuffer::Bytes(_) => panic!("cannot into_bytes_mut from bytes"), + TunPacketBuffer::BytesMut(bytes) => bytes, + } + } +} + +/// A TunPacket Encoder/Decoder. +pub struct TunPacketCodec(bool, i32); + +impl TunPacketCodec { + /// Create a new `TunPacketCodec` specifying whether the underlying + /// tunnel Device has enabled the packet information header. + pub fn new(pi: bool, mtu: i32) -> TunPacketCodec { + TunPacketCodec(pi, mtu) + } +} + +impl Decoder for TunPacketCodec { + type Item = TunPacket; + type Error = io::Error; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { + if buf.is_empty() { + return Ok(None); + } + + let mut pkt = buf.split_to(buf.len()); + + // reserve enough space for the next packet + if self.0 { + buf.reserve(self.1 as usize + 4); + } else { + buf.reserve(self.1 as usize); + } + + // if the packet information is enabled we have to ignore the first 4 bytes + if self.0 { + let _ = pkt.split_to(4); + } + + let proto = infer_proto(pkt.as_ref()); + Ok(Some(TunPacket(proto, TunPacketBuffer::BytesMut(pkt)))) + } +} + +impl Encoder for TunPacketCodec { + type Error = io::Error; + + fn encode(&mut self, item: TunPacket, dst: &mut BytesMut) -> Result<(), Self::Error> { + dst.reserve(item.get_bytes().len() + 4); + match item { + TunPacket(proto, bytes) if self.0 => { + // build the packet information header comprising of 2 u16 + // fields: flags and protocol. + let mut buf = Vec::::with_capacity(4); + + // flags is always 0 + buf.write_u16::(0)?; + // write the protocol as network byte order + buf.write_u16::(proto.into_pi_field()?)?; + + dst.put_slice(&buf); + dst.put(Bytes::from(bytes)); + } + TunPacket(_, bytes) => dst.put(Bytes::from(bytes)), + } + Ok(()) + } +} diff --git a/easytier-core/src/instance/virtual_nic.rs b/easytier-core/src/instance/virtual_nic.rs new file mode 100644 index 0000000..0e3994a --- /dev/null +++ b/easytier-core/src/instance/virtual_nic.rs @@ -0,0 +1,203 @@ +use std::{net::Ipv4Addr, pin::Pin}; + +use crate::{ + common::{ + error::Result, + global_ctx::ArcGlobalCtx, + ifcfg::{IfConfiger, IfConfiguerTrait}, + }, + tunnels::{ + codec::BytesCodec, common::FramedTunnel, DatagramSink, DatagramStream, Tunnel, TunnelError, + }, +}; + +use futures::{SinkExt, StreamExt}; +use tokio_util::{bytes::Bytes, codec::Framed}; +use tun::Device; + +use super::tun_codec::{TunPacket, TunPacketCodec}; + +pub struct VirtualNic { + dev_name: String, + queue_num: usize, + + global_ctx: ArcGlobalCtx, + + ifname: Option, + tun: Option>, + ifcfg: Box, +} + +impl VirtualNic { + pub fn new(global_ctx: ArcGlobalCtx) -> Self { + Self { + dev_name: "".to_owned(), + queue_num: 1, + global_ctx, + ifname: None, + tun: None, + ifcfg: Box::new(IfConfiger {}), + } + } + + pub fn set_dev_name(mut self, dev_name: &str) -> Result { + self.dev_name = dev_name.to_owned(); + Ok(self) + } + + pub fn set_queue_num(mut self, queue_num: usize) -> Result { + self.queue_num = queue_num; + Ok(self) + } + + async fn create_dev_ret_err(&mut self) -> Result<()> { + let mut config = tun::Configuration::default(); + let has_packet_info = cfg!(target_os = "macos"); + config.layer(tun::Layer::L3); + + #[cfg(target_os = "linux")] + { + config.platform(|config| { + // detect protocol by ourselves for cross platform + config.packet_information(false); + }); + config.name(self.dev_name.clone()); + } + + if self.queue_num != 1 { + todo!("queue_num != 1") + } + config.queues(self.queue_num); + config.up(); + + let dev = { + let _g = self.global_ctx.net_ns.guard(); + tun::create_as_async(&config)? + }; + let ifname = dev.get_ref().name()?; + self.ifcfg.wait_interface_show(ifname.as_str()).await?; + + let ft: Box = if has_packet_info { + let framed = Framed::new(dev, TunPacketCodec::new(true, 2500)); + let (sink, stream) = framed.split(); + + let new_stream = stream.map(|item| match item { + Ok(item) => Ok(item.into_bytes_mut()), + Err(err) => { + println!("tun stream error: {:?}", err); + Err(TunnelError::TunError(err.to_string())) + } + }); + + let new_sink = Box::pin(sink.with(|item: Bytes| async move { + if false { + return Err(TunnelError::TunError("tun sink error".to_owned())); + } + Ok(TunPacket::new(super::tun_codec::TunPacketBuffer::Bytes( + item, + ))) + })); + + Box::new(FramedTunnel::new(new_stream, new_sink, None)) + } else { + let framed = Framed::new(dev, BytesCodec::new(2500)); + let (sink, stream) = framed.split(); + Box::new(FramedTunnel::new(stream, sink, None)) + }; + + self.ifname = Some(ifname.to_owned()); + self.tun = Some(ft); + + Ok(()) + } + + pub async fn create_dev(mut self) -> Result { + self.create_dev_ret_err().await?; + Ok(self) + } + + pub fn ifname(&self) -> &str { + self.ifname.as_ref().unwrap().as_str() + } + + pub async fn link_up(self) -> Result { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg.set_link_status(self.ifname(), true).await?; + Ok(self) + } + + pub async fn add_route(self, address: Ipv4Addr, cidr: u8) -> Result { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv4_route(self.ifname(), address, cidr) + .await?; + Ok(self) + } + + pub async fn remove_ip(self, ip: Option) -> Result { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg.remove_ip(self.ifname(), ip).await?; + Ok(self) + } + + pub async fn add_ip(self, ip: Ipv4Addr, cidr: i32) -> Result { + let _g = self.global_ctx.net_ns.guard(); + self.ifcfg + .add_ipv4_ip(self.ifname(), ip, cidr as u8) + .await?; + Ok(self) + } + + pub fn pin_recv_stream(&self) -> Pin> { + self.tun.as_ref().unwrap().pin_stream() + } + + pub fn pin_send_stream(&self) -> Pin> { + self.tun.as_ref().unwrap().pin_sink() + } + + pub fn get_ifcfg(&self) -> &dyn IfConfiguerTrait { + self.ifcfg.as_ref() + } +} +#[cfg(test)] +mod tests { + use crate::{ + common::{error::Error, global_ctx::tests::get_mock_global_ctx}, + tests::enable_log, + }; + + use super::VirtualNic; + + async fn run_test_helper() -> Result { + let dev = VirtualNic::new(get_mock_global_ctx()).create_dev().await?; + + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + dev.link_up() + .await? + .remove_ip(None) + .await? + .add_ip("10.144.111.1".parse().unwrap(), 24) + .await + } + + #[tokio::test] + async fn tun_test() { + enable_log(); + let _dev = run_test_helper().await.unwrap(); + + // let mut stream = nic.pin_recv_stream(); + // while let Some(item) = stream.next().await { + // println!("item: {:?}", item); + // } + + // let framed = dev.into_framed(); + // let (mut s, mut b) = framed.split(); + // loop { + // let tmp = b.next().await.unwrap().unwrap(); + // let tmp = EthernetPacket::new(tmp.get_bytes()); + // println!("ret: {:?}", tmp.unwrap()); + // } + } +} diff --git a/easytier-core/src/main.rs b/easytier-core/src/main.rs new file mode 100644 index 0000000..0bde9c9 --- /dev/null +++ b/easytier-core/src/main.rs @@ -0,0 +1,103 @@ +#![allow(dead_code)] + +#[cfg(test)] +mod tests; + +use clap::Parser; + +mod common; +mod connector; +mod gateway; +mod instance; +mod peers; +mod tunnels; + +use instance::instance::{Instance, InstanceConfigWriter}; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// the instance name + #[arg(short = 'n', long, default_value = "default")] + instance_name: String, + + /// specify the network namespace, default is the root namespace + #[arg(long)] + net_ns: Option, + + #[arg(short, long)] + ipv4: Option, + + #[arg(short, long)] + peers: Vec, +} + +fn init_logger() { + // logger to rolling file + let file_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env() + .unwrap(); + let file_appender = tracing_appender::rolling::Builder::new() + .rotation(tracing_appender::rolling::Rotation::DAILY) + .max_log_files(5) + .filename_prefix("core.log") + .build("/var/log/easytier") + .expect("failed to initialize rolling file appender"); + let mut file_layer = tracing_subscriber::fmt::layer(); + file_layer.set_ansi(false); + let file_layer = file_layer + .with_writer(file_appender) + .with_filter(file_filter); + + // logger to console + let console_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .from_env() + .unwrap(); + let console_layer = tracing_subscriber::fmt::layer() + .pretty() + .with_writer(std::io::stderr) + .with_filter(console_filter); + + tracing_subscriber::Registry::default() + .with(console_layer) + .with(file_layer) + .init(); +} + +#[tokio::main(flavor = "current_thread")] +#[tracing::instrument] +pub async fn main() { + init_logger(); + + let cli = Cli::parse(); + tracing::info!(cli = ?cli, "cli args parsed"); + + let cfg = InstanceConfigWriter::new(cli.instance_name.as_str()).set_ns(cli.net_ns.clone()); + if let Some(ipv4) = &cli.ipv4 { + cfg.set_addr(ipv4.clone()); + } + + let mut inst = Instance::new(cli.instance_name.as_str()); + + let mut events = inst.get_global_ctx().subscribe(); + tokio::spawn(async move { + while let Ok(e) = events.recv().await { + log::warn!("event: {:?}", e); + } + }); + + inst.run().await.unwrap(); + + for peer in cli.peers { + inst.get_conn_manager() + .add_connector_by_url(peer.as_str()) + .await + .unwrap(); + } + + inst.wait().await; +} diff --git a/easytier-core/src/peers/mod.rs b/easytier-core/src/peers/mod.rs new file mode 100644 index 0000000..aa6e5f8 --- /dev/null +++ b/easytier-core/src/peers/mod.rs @@ -0,0 +1,14 @@ +pub mod packet; +pub mod peer; +pub mod peer_conn; +pub mod peer_manager; +pub mod peer_map; +pub mod peer_rpc; +pub mod rip_route; +pub mod route_trait; +pub mod rpc_service; + +#[cfg(test)] +pub mod tests; + +pub type PeerId = uuid::Uuid; diff --git a/easytier-core/src/peers/packet.rs b/easytier-core/src/peers/packet.rs new file mode 100644 index 0000000..c1127c7 --- /dev/null +++ b/easytier-core/src/peers/packet.rs @@ -0,0 +1,205 @@ +use rkyv::{Archive, Deserialize, Serialize}; +use tokio_util::bytes::Bytes; + +use crate::common::rkyv_util::{decode_from_bytes, encode_to_bytes}; + +const MAGIC: u32 = 0xd1e1a5e1; +const VERSION: u32 = 1; + +#[derive(Archive, Deserialize, Serialize, PartialEq, Clone)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct UUID(uuid::Bytes); + +// impl Debug for UUID +impl std::fmt::Debug for UUID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let uuid = uuid::Uuid::from_bytes(self.0); + write!(f, "{}", uuid) + } +} + +impl From for UUID { + fn from(uuid: uuid::Uuid) -> Self { + UUID(*uuid.as_bytes()) + } +} + +impl From for uuid::Uuid { + fn from(uuid: UUID) -> Self { + uuid::Uuid::from_bytes(uuid.0) + } +} + +impl ArchivedUUID { + pub fn to_uuid(&self) -> uuid::Uuid { + uuid::Uuid::from_bytes(self.0) + } +} + +impl From<&ArchivedUUID> for UUID { + fn from(uuid: &ArchivedUUID) -> Self { + UUID(uuid.0) + } +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct HandShake { + pub magic: u32, + pub my_peer_id: UUID, + pub version: u32, + pub features: Vec, + // pub interfaces: Vec, +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +#[archive_attr(derive(Debug))] +pub struct RoutePacket { + pub route_id: u8, + pub body: Vec, +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub enum CtrlPacketBody { + HandShake(HandShake), + RoutePacket(RoutePacket), + Ping, + Pong, + TaRpc(u32, bool, Vec), // u32: service_id, bool: is_req, Vec: rpc body +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct DataPacketBody { + pub data: Vec, +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub enum PacketBody { + Ctrl(CtrlPacketBody), + Data(DataPacketBody), +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct Packet { + pub from_peer: UUID, + pub to_peer: Option, + pub body: PacketBody, +} + +impl Packet { + pub fn decode(v: &[u8]) -> &ArchivedPacket { + decode_from_bytes::(v).unwrap() + } +} + +impl From for Bytes { + fn from(val: Packet) -> Self { + encode_to_bytes::<_, 4096>(&val) + } +} + +impl Packet { + pub fn new_handshake(from_peer: uuid::Uuid) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: None, + body: PacketBody::Ctrl(CtrlPacketBody::HandShake(HandShake { + magic: MAGIC, + my_peer_id: from_peer.into(), + version: VERSION, + features: Vec::new(), + })), + } + } + + pub fn new_data_packet(from_peer: uuid::Uuid, to_peer: uuid::Uuid, data: &[u8]) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: Some(to_peer.into()), + body: PacketBody::Data(DataPacketBody { + data: data.to_vec(), + }), + } + } + + pub fn new_route_packet( + from_peer: uuid::Uuid, + to_peer: uuid::Uuid, + route_id: u8, + data: &[u8], + ) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: Some(to_peer.into()), + body: PacketBody::Ctrl(CtrlPacketBody::RoutePacket(RoutePacket { + route_id, + body: data.to_vec(), + })), + } + } + + pub fn new_ping_packet(from_peer: uuid::Uuid, to_peer: uuid::Uuid) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: Some(to_peer.into()), + body: PacketBody::Ctrl(CtrlPacketBody::Ping), + } + } + + pub fn new_pong_packet(from_peer: uuid::Uuid, to_peer: uuid::Uuid) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: Some(to_peer.into()), + body: PacketBody::Ctrl(CtrlPacketBody::Pong), + } + } + + pub fn new_tarpc_packet( + from_peer: uuid::Uuid, + to_peer: uuid::Uuid, + service_id: u32, + is_req: bool, + body: Vec, + ) -> Self { + Packet { + from_peer: from_peer.into(), + to_peer: Some(to_peer.into()), + body: PacketBody::Ctrl(CtrlPacketBody::TaRpc(service_id, is_req, body)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn serialize() { + let a = "abcde"; + let out = Packet::new_data_packet(uuid::Uuid::new_v4(), uuid::Uuid::new_v4(), a.as_bytes()); + // let out = T::new(a.as_bytes()); + let out_bytes: Bytes = out.into(); + println!("out str: {:?}", a.as_bytes()); + println!("out bytes: {:?}", out_bytes); + + let archived = Packet::decode(&out_bytes[..]); + println!("in packet: {:?}", archived); + } +} diff --git a/easytier-core/src/peers/peer.rs b/easytier-core/src/peers/peer.rs new file mode 100644 index 0000000..e56a1b7 --- /dev/null +++ b/easytier-core/src/peers/peer.rs @@ -0,0 +1,218 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use easytier_rpc::PeerConnInfo; + +use tokio::{ + select, + sync::{mpsc, Mutex}, + task::JoinHandle, +}; +use tokio_util::bytes::Bytes; +use tracing::Instrument; +use uuid::Uuid; + +use crate::common::{ + error::Error, + global_ctx::{ArcGlobalCtx, GlobalCtxEvent}, +}; + +use super::peer_conn::PeerConn; + +type ArcPeerConn = Arc>; +type ConnMap = Arc>; + +pub struct Peer { + pub peer_node_id: uuid::Uuid, + conns: ConnMap, + global_ctx: ArcGlobalCtx, + + packet_recv_chan: mpsc::Sender, + + close_event_sender: mpsc::Sender, + close_event_listener: JoinHandle<()>, + + shutdown_notifier: Arc, +} + +impl Peer { + pub fn new( + peer_node_id: uuid::Uuid, + packet_recv_chan: mpsc::Sender, + global_ctx: ArcGlobalCtx, + ) -> Self { + let conns: ConnMap = Arc::new(DashMap::new()); + let (close_event_sender, mut close_event_receiver) = mpsc::channel(10); + let shutdown_notifier = Arc::new(tokio::sync::Notify::new()); + + let conns_copy = conns.clone(); + let shutdown_notifier_copy = shutdown_notifier.clone(); + let global_ctx_copy = global_ctx.clone(); + let close_event_listener = tokio::spawn( + async move { + loop { + select! { + ret = close_event_receiver.recv() => { + if ret.is_none() { + break; + } + let ret = ret.unwrap(); + tracing::warn!( + ?peer_node_id, + ?ret, + "notified that peer conn is closed", + ); + + if let Some((_, conn)) = conns_copy.remove(&ret) { + global_ctx_copy.issue_event(GlobalCtxEvent::PeerConnRemoved( + conn.lock().await.get_conn_info(), + )); + } + } + + _ = shutdown_notifier_copy.notified() => { + close_event_receiver.close(); + tracing::warn!(?peer_node_id, "peer close event listener notified"); + } + } + } + tracing::info!("peer {} close event listener exit", peer_node_id); + } + .instrument(tracing::info_span!( + "peer_close_event_listener", + ?peer_node_id, + )), + ); + + Peer { + peer_node_id, + conns: conns.clone(), + packet_recv_chan, + global_ctx, + + close_event_sender, + close_event_listener, + + shutdown_notifier, + } + } + + pub async fn add_peer_conn(&self, mut conn: PeerConn) { + conn.set_close_event_sender(self.close_event_sender.clone()); + conn.start_recv_loop(self.packet_recv_chan.clone()); + self.global_ctx + .issue_event(GlobalCtxEvent::PeerConnAdded(conn.get_conn_info())); + self.conns + .insert(conn.get_conn_id(), Arc::new(Mutex::new(conn))); + } + + pub async fn send_msg(&self, msg: Bytes) -> Result<(), Error> { + let Some(conn) = self.conns.iter().next() else { + return Err(Error::PeerNoConnectionError(self.peer_node_id)); + }; + + let conn_clone = conn.clone(); + drop(conn); + conn_clone.lock().await.send_msg(msg).await?; + + Ok(()) + } + + pub async fn close_peer_conn(&self, conn_id: &Uuid) -> Result<(), Error> { + let has_key = self.conns.contains_key(conn_id); + if !has_key { + return Err(Error::NotFound); + } + self.close_event_sender.send(conn_id.clone()).await.unwrap(); + Ok(()) + } + + pub async fn list_peer_conns(&self) -> Vec { + let mut conns = vec![]; + for conn in self.conns.iter() { + // do not lock here, otherwise it will cause dashmap deadlock + conns.push(conn.clone()); + } + + let mut ret = Vec::new(); + for conn in conns { + ret.push(conn.lock().await.get_conn_info()); + } + ret + } +} + +// pritn on drop +impl Drop for Peer { + fn drop(&mut self) { + self.shutdown_notifier.notify_one(); + tracing::info!("peer {} drop", self.peer_node_id); + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use tokio::{sync::mpsc, time::timeout}; + + use crate::{ + common::{config_fs::ConfigFs, global_ctx::GlobalCtx, netns::NetNS}, + peers::peer_conn::PeerConn, + tunnels::ring_tunnel::create_ring_tunnel_pair, + }; + + use super::Peer; + + #[tokio::test] + async fn close_peer() { + let (local_packet_send, _local_packet_recv) = mpsc::channel(10); + let (remote_packet_send, _remote_packet_recv) = mpsc::channel(10); + let global_ctx = Arc::new(GlobalCtx::new( + "test", + ConfigFs::new("/tmp/easytier-test"), + NetNS::new(None), + )); + let local_peer = Peer::new(uuid::Uuid::new_v4(), local_packet_send, global_ctx.clone()); + let remote_peer = Peer::new(uuid::Uuid::new_v4(), remote_packet_send, global_ctx.clone()); + + let (local_tunnel, remote_tunnel) = create_ring_tunnel_pair(); + let mut local_peer_conn = + PeerConn::new(local_peer.peer_node_id, global_ctx.clone(), local_tunnel); + let mut remote_peer_conn = + PeerConn::new(remote_peer.peer_node_id, global_ctx.clone(), remote_tunnel); + + assert!(!local_peer_conn.handshake_done()); + assert!(!remote_peer_conn.handshake_done()); + + let (a, b) = tokio::join!( + local_peer_conn.do_handshake_as_client(), + remote_peer_conn.do_handshake_as_server() + ); + a.unwrap(); + b.unwrap(); + + let local_conn_id = local_peer_conn.get_conn_id(); + + local_peer.add_peer_conn(local_peer_conn).await; + remote_peer.add_peer_conn(remote_peer_conn).await; + + assert_eq!(local_peer.list_peer_conns().await.len(), 1); + assert_eq!(remote_peer.list_peer_conns().await.len(), 1); + + let close_handler = + tokio::spawn(async move { local_peer.close_peer_conn(&local_conn_id).await }); + + // wait for remote peer conn close + timeout(std::time::Duration::from_secs(5), async { + while (&remote_peer).list_peer_conns().await.len() != 0 { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + }) + .await + .unwrap(); + + println!("wait for close handler"); + close_handler.await.unwrap().unwrap(); + } +} diff --git a/easytier-core/src/peers/peer_conn.rs b/easytier-core/src/peers/peer_conn.rs new file mode 100644 index 0000000..fd1eb1a --- /dev/null +++ b/easytier-core/src/peers/peer_conn.rs @@ -0,0 +1,484 @@ +use std::{pin::Pin, sync::Arc}; + +use easytier_rpc::{PeerConnInfo, PeerConnStats}; +use futures::{SinkExt, StreamExt}; +use pnet::datalink::NetworkInterface; + +use tokio::{ + sync::{broadcast, mpsc}, + task::JoinSet, + time::{timeout, Duration}, +}; + +use tokio_util::{ + bytes::{Bytes, BytesMut}, + sync::PollSender, +}; +use tracing::Instrument; + +use crate::{ + common::global_ctx::ArcGlobalCtx, + define_tunnel_filter_chain, + tunnels::{ + stats::{Throughput, WindowLatency}, + tunnel_filter::StatsRecorderTunnelFilter, + DatagramSink, Tunnel, TunnelError, + }, +}; + +use super::packet::{self, ArchivedCtrlPacketBody, ArchivedHandShake, Packet}; + +pub type PacketRecvChan = mpsc::Sender; + +macro_rules! wait_response { + ($stream: ident, $out_var:ident, $pattern:pat_param => $value:expr) => { + let rsp_vec = timeout(Duration::from_secs(1), $stream.next()).await; + if rsp_vec.is_err() { + return Err(TunnelError::WaitRespError( + "wait handshake response timeout".to_owned(), + )); + } + let rsp_vec = rsp_vec.unwrap().unwrap()?; + + let $out_var; + let rsp_bytes = Packet::decode(&rsp_vec); + match &rsp_bytes.body { + $pattern => $out_var = $value, + _ => { + log::error!( + "unexpected packet: {:?}, pattern: {:?}", + rsp_bytes, + stringify!($pattern) + ); + return Err(TunnelError::WaitRespError("unexpected packet".to_owned())); + } + } + }; +} + +pub struct PeerInfo { + magic: u32, + pub my_peer_id: uuid::Uuid, + version: u32, + pub features: Vec, + pub interfaces: Vec, +} + +impl<'a> From<&ArchivedHandShake> for PeerInfo { + fn from(hs: &ArchivedHandShake) -> Self { + PeerInfo { + magic: hs.magic.into(), + my_peer_id: hs.my_peer_id.to_uuid(), + version: hs.version.into(), + features: hs.features.iter().map(|x| x.to_string()).collect(), + interfaces: Vec::new(), + } + } +} + +define_tunnel_filter_chain!(PeerConnTunnel, stats = StatsRecorderTunnelFilter); + +pub struct PeerConn { + conn_id: uuid::Uuid, + + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + + sink: Pin>, + tunnel: Box, + + tasks: JoinSet>, + + info: Option, + + close_event_sender: Option>, + + ctrl_resp_sender: broadcast::Sender, + + latency_stats: Arc, + throughput: Arc, +} + +enum PeerConnPacketType { + Data(Bytes), + CtrlReq(Bytes), + CtrlResp(Bytes), +} + +static CTRL_REQ_PACKET_PREFIX: &[u8] = &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]; +static CTRL_RESP_PACKET_PREFIX: &[u8] = &[0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf1]; + +impl PeerConn { + pub fn new(node_id: uuid::Uuid, global_ctx: ArcGlobalCtx, tunnel: Box) -> Self { + let (ctrl_sender, _ctrl_receiver) = broadcast::channel(100); + let peer_conn_tunnel = PeerConnTunnel::new(); + let tunnel = peer_conn_tunnel.wrap_tunnel(tunnel); + + PeerConn { + conn_id: uuid::Uuid::new_v4(), + + my_node_id: node_id, + global_ctx, + + sink: tunnel.pin_sink(), + tunnel: Box::new(tunnel), + + tasks: JoinSet::new(), + + info: None, + close_event_sender: None, + + ctrl_resp_sender: ctrl_sender, + + latency_stats: Arc::new(WindowLatency::new(15)), + throughput: peer_conn_tunnel.stats.get_throughput().clone(), + } + } + + pub fn get_conn_id(&self) -> uuid::Uuid { + self.conn_id + } + + pub async fn do_handshake_as_server(&mut self) -> Result<(), TunnelError> { + let mut stream = self.tunnel.pin_stream(); + let mut sink = self.tunnel.pin_sink(); + + wait_response!(stream, hs_req, packet::ArchivedPacketBody::Ctrl(ArchivedCtrlPacketBody::HandShake(x)) => x); + self.info = Some(PeerInfo::from(hs_req)); + log::info!("handshake request: {:?}", hs_req); + + let hs_req = self + .global_ctx + .net_ns + .run(|| packet::Packet::new_handshake(self.my_node_id)); + sink.send(hs_req.into()).await?; + + Ok(()) + } + + pub async fn do_handshake_as_client(&mut self) -> Result<(), TunnelError> { + let mut stream = self.tunnel.pin_stream(); + let mut sink = self.tunnel.pin_sink(); + + let hs_req = self + .global_ctx + .net_ns + .run(|| packet::Packet::new_handshake(self.my_node_id)); + sink.send(hs_req.into()).await?; + + wait_response!(stream, hs_rsp, packet::ArchivedPacketBody::Ctrl(ArchivedCtrlPacketBody::HandShake(x)) => x); + self.info = Some(PeerInfo::from(hs_rsp)); + log::info!("handshake response: {:?}", hs_rsp); + + Ok(()) + } + + pub fn handshake_done(&self) -> bool { + self.info.is_some() + } + + async fn do_pingpong_once( + my_node_id: uuid::Uuid, + peer_id: uuid::Uuid, + sink: &mut Pin>, + receiver: &mut broadcast::Receiver, + ) -> Result { + // should add seq here. so latency can be calculated more accurately + let req = Self::build_ctrl_msg( + packet::Packet::new_ping_packet(my_node_id, peer_id).into(), + true, + ); + log::trace!("send ping packet: {:?}", req); + sink.send(req).await?; + + let now = std::time::Instant::now(); + + // wait until we get a pong packet in ctrl_resp_receiver + let resp = timeout(Duration::from_secs(4), async { + loop { + match receiver.recv().await { + Ok(p) => { + if let packet::ArchivedPacketBody::Ctrl( + packet::ArchivedCtrlPacketBody::Pong, + ) = &Packet::decode(&p).body + { + break; + } + } + Err(e) => { + log::warn!("recv pong resp error: {:?}", e); + return Err(TunnelError::WaitRespError( + "recv pong resp error".to_owned(), + )); + } + } + } + Ok(()) + }) + .await; + + if resp.is_err() { + return Err(TunnelError::WaitRespError( + "wait ping response timeout".to_owned(), + )); + } + + if resp.as_ref().unwrap().is_err() { + return Err(resp.unwrap().err().unwrap()); + } + + Ok(now.elapsed().as_micros()) + } + + fn start_pingpong(&mut self) { + let mut sink = self.tunnel.pin_sink(); + let my_node_id = self.my_node_id; + let peer_id = self.get_peer_id(); + let receiver = self.ctrl_resp_sender.subscribe(); + let close_event_sender = self.close_event_sender.clone().unwrap(); + let conn_id = self.conn_id; + let latency_stats = self.latency_stats.clone(); + + self.tasks.spawn(async move { + //sleep 1s + tokio::time::sleep(Duration::from_secs(1)).await; + loop { + let mut receiver = receiver.resubscribe(); + if let Ok(lat) = + Self::do_pingpong_once(my_node_id, peer_id, &mut sink, &mut receiver).await + { + log::trace!( + "pingpong latency: {}us, my_node_id: {}, peer_id: {}", + lat, + my_node_id, + peer_id + ); + latency_stats.record_latency(lat as u64); + + tokio::time::sleep(Duration::from_secs(1)).await; + } else { + break; + } + } + + log::warn!( + "pingpong task exit, my_node_id: {}, peer_id: {}", + my_node_id, + peer_id, + ); + + if let Err(e) = close_event_sender.send(conn_id).await { + log::warn!("close event sender error: {:?}", e); + } + + Ok(()) + }); + } + + fn get_packet_type(mut bytes_item: Bytes) -> PeerConnPacketType { + if bytes_item.starts_with(CTRL_REQ_PACKET_PREFIX) { + PeerConnPacketType::CtrlReq(bytes_item.split_off(CTRL_REQ_PACKET_PREFIX.len())) + } else if bytes_item.starts_with(CTRL_RESP_PACKET_PREFIX) { + PeerConnPacketType::CtrlResp(bytes_item.split_off(CTRL_RESP_PACKET_PREFIX.len())) + } else { + PeerConnPacketType::Data(bytes_item) + } + } + + fn handle_ctrl_req_packet( + bytes_item: Bytes, + conn_info: &PeerConnInfo, + ) -> Result { + let packet = Packet::decode(&bytes_item); + match packet.body { + packet::ArchivedPacketBody::Ctrl(packet::ArchivedCtrlPacketBody::Ping) => { + log::trace!("recv ping packet: {:?}", packet); + Ok(Self::build_ctrl_msg( + packet::Packet::new_pong_packet( + conn_info.my_node_id.parse().unwrap(), + conn_info.peer_id.parse().unwrap(), + ) + .into(), + false, + )) + } + _ => { + log::error!("unexpected packet: {:?}", packet); + Err(TunnelError::CommonError("unexpected packet".to_owned())) + } + } + } + + pub fn start_recv_loop(&mut self, packet_recv_chan: PacketRecvChan) { + let mut stream = self.tunnel.pin_stream(); + let mut sink = self.tunnel.pin_sink(); + let mut sender = PollSender::new(packet_recv_chan.clone()); + let close_event_sender = self.close_event_sender.clone().unwrap(); + let conn_id = self.conn_id; + let ctrl_sender = self.ctrl_resp_sender.clone(); + let conn_info = self.get_conn_info(); + let conn_info_for_instrument = self.get_conn_info(); + + self.tasks.spawn( + async move { + tracing::info!("start recving peer conn packet"); + while let Some(ret) = stream.next().await { + if ret.is_err() { + tracing::error!(error = ?ret, "peer conn recv error"); + if let Err(close_ret) = sink.close().await { + tracing::error!(error = ?close_ret, "peer conn sink close error, ignore it"); + } + if let Err(e) = close_event_sender.send(conn_id).await { + tracing::error!(error = ?e, "peer conn close event send error"); + } + return Err(ret.err().unwrap()); + } + + match Self::get_packet_type(ret.unwrap().into()) { + PeerConnPacketType::Data(item) => sender.send(item).await.unwrap(), + PeerConnPacketType::CtrlReq(item) => { + let ret = Self::handle_ctrl_req_packet(item, &conn_info).unwrap(); + if let Err(e) = sink.send(ret).await { + tracing::error!(?e, "peer conn send req error"); + } + } + PeerConnPacketType::CtrlResp(item) => { + if let Err(e) = ctrl_sender.send(item) { + tracing::error!(?e, "peer conn send ctrl resp error"); + } + } + } + } + tracing::info!("end recving peer conn packet"); + Ok(()) + } + .instrument( + tracing::info_span!("peer conn recv loop", conn_info = ?conn_info_for_instrument), + ), + ); + + self.start_pingpong(); + } + + pub async fn send_msg(&mut self, msg: Bytes) -> Result<(), TunnelError> { + self.sink.send(msg).await + } + + fn build_ctrl_msg(msg: Bytes, is_req: bool) -> Bytes { + let prefix: &'static [u8] = if is_req { + CTRL_REQ_PACKET_PREFIX + } else { + CTRL_RESP_PACKET_PREFIX + }; + let mut new_msg = BytesMut::new(); + new_msg.reserve(prefix.len() + msg.len()); + new_msg.extend_from_slice(prefix); + new_msg.extend_from_slice(&msg); + new_msg.into() + } + + pub fn get_peer_id(&self) -> uuid::Uuid { + self.info.as_ref().unwrap().my_peer_id + } + + pub fn set_close_event_sender(&mut self, sender: mpsc::Sender) { + self.close_event_sender = Some(sender); + } + + pub fn get_stats(&self) -> PeerConnStats { + PeerConnStats { + latency_us: self.latency_stats.get_latency_us(), + + tx_bytes: self.throughput.tx_bytes(), + rx_bytes: self.throughput.rx_bytes(), + + tx_packets: self.throughput.tx_packets(), + rx_packets: self.throughput.rx_packets(), + } + } + + pub fn get_conn_info(&self) -> PeerConnInfo { + PeerConnInfo { + conn_id: self.conn_id.to_string(), + my_node_id: self.my_node_id.to_string(), + peer_id: self.get_peer_id().to_string(), + features: self.info.as_ref().unwrap().features.clone(), + tunnel: self.tunnel.info(), + stats: Some(self.get_stats()), + } + } +} + +impl Drop for PeerConn { + fn drop(&mut self) { + let mut sink = self.tunnel.pin_sink(); + tokio::spawn(async move { + let ret = sink.close().await; + tracing::info!(error = ?ret, "peer conn tunnel closed."); + }); + log::info!("peer conn {:?} drop", self.conn_id); + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use crate::common::config_fs::ConfigFs; + use crate::common::global_ctx::GlobalCtx; + use crate::common::netns::NetNS; + use crate::tunnels::tunnel_filter::{PacketRecorderTunnelFilter, TunnelWithFilter}; + + #[tokio::test] + async fn peer_conn_handshake() { + use crate::tunnels::ring_tunnel::create_ring_tunnel_pair; + let (c, s) = create_ring_tunnel_pair(); + + let c_recorder = Arc::new(PacketRecorderTunnelFilter::new()); + let s_recorder = Arc::new(PacketRecorderTunnelFilter::new()); + + let c = TunnelWithFilter::new(c, c_recorder.clone()); + let s = TunnelWithFilter::new(s, s_recorder.clone()); + + let c_uuid = uuid::Uuid::new_v4(); + let s_uuid = uuid::Uuid::new_v4(); + + let mut c_peer = PeerConn::new( + c_uuid, + Arc::new(GlobalCtx::new( + "c", + ConfigFs::new_with_dir("c", "/tmp"), + NetNS::new(None), + )), + Box::new(c), + ); + + let mut s_peer = PeerConn::new( + s_uuid, + Arc::new(GlobalCtx::new( + "c", + ConfigFs::new_with_dir("c", "/tmp"), + NetNS::new(None), + )), + Box::new(s), + ); + + let (c_ret, s_ret) = tokio::join!( + c_peer.do_handshake_as_client(), + s_peer.do_handshake_as_server() + ); + + c_ret.unwrap(); + s_ret.unwrap(); + + assert_eq!(c_recorder.sent.lock().unwrap().len(), 1); + assert_eq!(c_recorder.received.lock().unwrap().len(), 1); + + assert_eq!(s_recorder.sent.lock().unwrap().len(), 1); + assert_eq!(s_recorder.received.lock().unwrap().len(), 1); + + assert_eq!(c_peer.get_peer_id(), s_uuid); + assert_eq!(s_peer.get_peer_id(), c_uuid); + } +} diff --git a/easytier-core/src/peers/peer_manager.rs b/easytier-core/src/peers/peer_manager.rs new file mode 100644 index 0000000..b81aac1 --- /dev/null +++ b/easytier-core/src/peers/peer_manager.rs @@ -0,0 +1,539 @@ +use std::{ + fmt::Debug, + net::Ipv4Addr, + sync::{atomic::AtomicU8, Arc}, +}; + +use async_trait::async_trait; +use futures::{StreamExt, TryFutureExt}; + +use tokio::{ + sync::{ + mpsc::{self, UnboundedReceiver, UnboundedSender}, + Mutex, RwLock, + }, + task::JoinSet, +}; +use tokio_stream::wrappers::ReceiverStream; +use tokio_util::bytes::{Bytes, BytesMut}; + +use uuid::Uuid; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx, rkyv_util::extract_bytes_from_archived_vec}, + peers::{ + packet::{self}, + peer_conn::PeerConn, + peer_rpc::PeerRpcManagerTransport, + route_trait::RouteInterface, + }, + tunnels::{SinkItem, Tunnel, TunnelConnector}, +}; + +use super::{ + peer_map::PeerMap, + peer_rpc::PeerRpcManager, + route_trait::{ArcRoute, Route}, + PeerId, +}; + +struct RpcTransport { + my_peer_id: uuid::Uuid, + peers: Arc, + + packet_recv: Mutex>, + peer_rpc_tspt_sender: UnboundedSender, + + route: Arc>>, +} + +#[async_trait::async_trait] +impl PeerRpcManagerTransport for RpcTransport { + fn my_peer_id(&self) -> Uuid { + self.my_peer_id + } + + async fn send(&self, msg: Bytes, dst_peer_id: &uuid::Uuid) -> Result<(), Error> { + let route = self.route.lock().await; + if route.is_none() { + log::error!("no route info when send rpc msg"); + return Err(Error::RouteError("No route info".to_string())); + } + + self.peers + .send_msg(msg, dst_peer_id, route.as_ref().unwrap().clone()) + .map_err(|e| e.into()) + .await + } + + async fn recv(&self) -> Result { + if let Some(o) = self.packet_recv.lock().await.recv().await { + Ok(o) + } else { + Err(Error::Unknown) + } + } +} + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait PeerPacketFilter { + async fn try_process_packet_from_peer( + &self, + packet: &packet::ArchivedPacket, + data: &Bytes, + ) -> Option<()>; +} + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait NicPacketFilter { + async fn try_process_packet_from_nic(&self, data: BytesMut) -> BytesMut; +} + +type BoxPeerPacketFilter = Box; +type BoxNicPacketFilter = Box; + +pub struct PeerManager { + my_node_id: uuid::Uuid, + global_ctx: ArcGlobalCtx, + nic_channel: mpsc::Sender, + + tasks: Arc>>, + + packet_recv: Arc>>>, + + peers: Arc, + route: Arc>>, + cur_route_id: AtomicU8, + + peer_rpc_mgr: Arc, + peer_rpc_tspt: Arc, + + peer_packet_process_pipeline: Arc>>, + nic_packet_process_pipeline: Arc>>, +} + +impl Debug for PeerManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerManager") + .field("my_node_id", &self.my_node_id) + .field("instance_name", &self.global_ctx.inst_name) + .field("net_ns", &self.global_ctx.net_ns.name()) + .field("cur_route_id", &self.cur_route_id) + .finish() + } +} + +impl PeerManager { + pub fn new(global_ctx: ArcGlobalCtx, nic_channel: mpsc::Sender) -> Self { + let (packet_send, packet_recv) = mpsc::channel(100); + let peers = Arc::new(PeerMap::new(packet_send.clone())); + + // TODO: remove these because we have impl pipeline processor. + let (peer_rpc_tspt_sender, peer_rpc_tspt_recv) = mpsc::unbounded_channel(); + let rpc_tspt = Arc::new(RpcTransport { + my_peer_id: global_ctx.get_id(), + peers: peers.clone(), + packet_recv: Mutex::new(peer_rpc_tspt_recv), + peer_rpc_tspt_sender, + route: Arc::new(Mutex::new(None)), + }); + + PeerManager { + my_node_id: global_ctx.get_id(), + global_ctx, + nic_channel, + + tasks: Arc::new(Mutex::new(JoinSet::new())), + + packet_recv: Arc::new(Mutex::new(Some(packet_recv))), + + peers: peers.clone(), + route: Arc::new(Mutex::new(None)), + cur_route_id: AtomicU8::new(0), + + peer_rpc_mgr: Arc::new(PeerRpcManager::new(rpc_tspt.clone())), + peer_rpc_tspt: rpc_tspt, + + peer_packet_process_pipeline: Arc::new(RwLock::new(Vec::new())), + nic_packet_process_pipeline: Arc::new(RwLock::new(Vec::new())), + } + } + + pub async fn add_client_tunnel(&self, tunnel: Box) -> Result<(Uuid, Uuid), Error> { + let mut peer = PeerConn::new(self.my_node_id, self.global_ctx.clone(), tunnel); + peer.do_handshake_as_client().await?; + let conn_id = peer.get_conn_id(); + let peer_id = peer.get_peer_id(); + self.peers + .add_new_peer_conn(peer, self.global_ctx.clone()) + .await; + Ok((peer_id, conn_id)) + } + + #[tracing::instrument] + pub async fn try_connect(&self, mut connector: C) -> Result<(Uuid, Uuid), Error> + where + C: TunnelConnector + Debug, + { + let ns = self.global_ctx.net_ns.clone(); + let t = ns + .run_async(|| async move { connector.connect().await }) + .await?; + self.add_client_tunnel(t).await + } + + #[tracing::instrument] + pub async fn add_tunnel_as_server(&self, tunnel: Box) -> Result<(), Error> { + tracing::info!("add tunnel as server start"); + let mut peer = PeerConn::new(self.my_node_id, self.global_ctx.clone(), tunnel); + peer.do_handshake_as_server().await?; + self.peers + .add_new_peer_conn(peer, self.global_ctx.clone()) + .await; + tracing::info!("add tunnel as server done"); + Ok(()) + } + + async fn start_peer_recv(&self) { + let mut recv = ReceiverStream::new(self.packet_recv.lock().await.take().unwrap()); + let my_node_id = self.my_node_id; + let peers = self.peers.clone(); + let arc_route = self.route.clone(); + let pipe_line = self.peer_packet_process_pipeline.clone(); + self.tasks.lock().await.spawn(async move { + log::trace!("start_peer_recv"); + while let Some(ret) = recv.next().await { + log::trace!("peer recv a packet...: {:?}", ret); + let packet = packet::Packet::decode(&ret); + let from_peer_uuid = packet.from_peer.to_uuid(); + let to_peer_uuid = packet.to_peer.as_ref().unwrap().to_uuid(); + if to_peer_uuid != my_node_id { + let locked_arc_route = arc_route.lock().await; + if locked_arc_route.is_none() { + log::error!("no route info after recv a packet"); + continue; + } + + let route = locked_arc_route.as_ref().unwrap().clone(); + drop(locked_arc_route); + log::trace!( + "need forward: to_peer_uuid: {:?}, my_uuid: {:?}", + to_peer_uuid, + my_node_id + ); + let ret = peers + .send_msg(ret.clone(), &to_peer_uuid, route.clone()) + .await; + if ret.is_err() { + log::error!( + "forward packet error: {:?}, dst: {:?}, from: {:?}", + ret, + to_peer_uuid, + from_peer_uuid + ); + } + } else { + let mut processed = false; + for pipeline in pipe_line.read().await.iter().rev() { + if let Some(_) = pipeline.try_process_packet_from_peer(&packet, &ret).await + { + processed = true; + break; + } + } + if !processed { + tracing::error!("unexpected packet: {:?}", ret); + } + } + } + panic!("done_peer_recv"); + }); + } + + pub async fn add_packet_process_pipeline(&self, pipeline: BoxPeerPacketFilter) { + // newest pipeline will be executed first + self.peer_packet_process_pipeline + .write() + .await + .push(pipeline); + } + + pub async fn add_nic_packet_process_pipeline(&self, pipeline: BoxNicPacketFilter) { + // newest pipeline will be executed first + self.nic_packet_process_pipeline + .write() + .await + .push(pipeline); + } + + async fn init_packet_process_pipeline(&self) { + use packet::ArchivedPacketBody; + + // for tun/tap ip/eth packet. + struct NicPacketProcessor { + nic_channel: mpsc::Sender, + } + #[async_trait::async_trait] + impl PeerPacketFilter for NicPacketProcessor { + async fn try_process_packet_from_peer( + &self, + packet: &packet::ArchivedPacket, + data: &Bytes, + ) -> Option<()> { + if let packet::ArchivedPacketBody::Data(x) = &packet.body { + // TODO: use a function to get the body ref directly for zero copy + self.nic_channel + .send(extract_bytes_from_archived_vec(&data, &x.data)) + .await + .unwrap(); + Some(()) + } else { + None + } + } + } + self.add_packet_process_pipeline(Box::new(NicPacketProcessor { + nic_channel: self.nic_channel.clone(), + })) + .await; + + // for peer manager router packet + struct RoutePacketProcessor { + route: Arc>>, + } + #[async_trait::async_trait] + impl PeerPacketFilter for RoutePacketProcessor { + async fn try_process_packet_from_peer( + &self, + packet: &packet::ArchivedPacket, + data: &Bytes, + ) -> Option<()> { + if let ArchivedPacketBody::Ctrl(packet::ArchivedCtrlPacketBody::RoutePacket( + route_packet, + )) = &packet.body + { + let r = self.route.lock().await; + match r.as_ref() { + Some(x) => { + let x = x.clone(); + drop(r); + x.handle_route_packet( + packet.from_peer.to_uuid(), + extract_bytes_from_archived_vec(&data, &route_packet.body), + ) + .await; + } + None => { + log::error!("no route info when handle route packet"); + } + } + Some(()) + } else { + None + } + } + } + self.add_packet_process_pipeline(Box::new(RoutePacketProcessor { + route: self.route.clone(), + })) + .await; + + // for peer rpc packet + struct PeerRpcPacketProcessor { + peer_rpc_tspt_sender: UnboundedSender, + } + + #[async_trait::async_trait] + impl PeerPacketFilter for PeerRpcPacketProcessor { + async fn try_process_packet_from_peer( + &self, + packet: &packet::ArchivedPacket, + data: &Bytes, + ) -> Option<()> { + if let ArchivedPacketBody::Ctrl(packet::ArchivedCtrlPacketBody::TaRpc(..)) = + &packet.body + { + self.peer_rpc_tspt_sender.send(data.clone()).unwrap(); + Some(()) + } else { + None + } + } + } + self.add_packet_process_pipeline(Box::new(PeerRpcPacketProcessor { + peer_rpc_tspt_sender: self.peer_rpc_tspt.peer_rpc_tspt_sender.clone(), + })) + .await; + } + + pub async fn set_route(&self, route: T) + where + T: Route + Send + Sync + 'static, + { + struct Interface { + my_node_id: uuid::Uuid, + peers: Arc, + } + + #[async_trait] + impl RouteInterface for Interface { + async fn list_peers(&self) -> Vec { + self.peers.list_peers_with_conn().await + } + async fn send_route_packet( + &self, + msg: Bytes, + route_id: u8, + dst_peer_id: &PeerId, + ) -> Result<(), Error> { + self.peers + .send_msg_directly( + packet::Packet::new_route_packet( + self.my_node_id, + *dst_peer_id, + route_id, + &msg, + ) + .into(), + dst_peer_id, + ) + .await + } + } + + let my_node_id = self.my_node_id; + let route_id = route + .open(Box::new(Interface { + my_node_id, + peers: self.peers.clone(), + })) + .await + .unwrap(); + + self.cur_route_id + .store(route_id, std::sync::atomic::Ordering::Relaxed); + let arc_route: ArcRoute = Arc::new(Box::new(route)); + + self.route.lock().await.replace(arc_route.clone()); + + self.peer_rpc_tspt + .route + .lock() + .await + .replace(arc_route.clone()); + } + + pub async fn list_routes(&self) -> Vec { + let route_info = self.route.lock().await; + if route_info.is_none() { + return Vec::new(); + } + + let route = route_info.as_ref().unwrap().clone(); + drop(route_info); + route.list_routes().await + } + + async fn run_nic_packet_process_pipeline(&self, mut data: BytesMut) -> BytesMut { + for pipeline in self.nic_packet_process_pipeline.read().await.iter().rev() { + data = pipeline.try_process_packet_from_nic(data).await; + } + data + } + + pub async fn send_msg(&self, msg: Bytes, dst_peer_id: &PeerId) -> Result<(), Error> { + self.peer_rpc_tspt.send(msg, dst_peer_id).await + } + + pub async fn send_msg_ipv4(&self, msg: BytesMut, ipv4_addr: Ipv4Addr) -> Result<(), Error> { + let route_info = self.route.lock().await; + if route_info.is_none() { + log::error!("no route info"); + return Err(Error::RouteError("No route info".to_string())); + } + + let route = route_info.as_ref().unwrap().clone(); + drop(route_info); + + log::trace!( + "do send_msg in peer manager, msg: {:?}, ipv4_addr: {}", + msg, + ipv4_addr + ); + + match route.get_peer_id_by_ipv4(&ipv4_addr).await { + Some(peer_id) => { + let msg = self.run_nic_packet_process_pipeline(msg).await; + self.peers + .send_msg( + packet::Packet::new_data_packet(self.my_node_id, peer_id, &msg).into(), + &peer_id, + route.clone(), + ) + .await?; + log::trace!( + "do send_msg in peer manager done, dst_peer_id: {:?}", + peer_id + ); + } + None => { + log::trace!("no peer id for ipv4: {}", ipv4_addr); + return Ok(()); + } + } + + Ok(()) + } + + async fn run_clean_peer_without_conn_routine(&self) { + let peer_map = self.peers.clone(); + self.tasks.lock().await.spawn(async move { + loop { + let mut to_remove = vec![]; + + for peer_id in peer_map.list_peers().await { + let conns = peer_map.list_peer_conns(&peer_id).await; + if conns.is_none() || conns.as_ref().unwrap().is_empty() { + to_remove.push(peer_id); + } + } + + for peer_id in to_remove { + peer_map.close_peer(&peer_id).await.unwrap(); + } + + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + } + }); + } + + pub async fn run(&self) -> Result<(), Error> { + self.init_packet_process_pipeline().await; + self.start_peer_recv().await; + self.peer_rpc_mgr.run(); + self.run_clean_peer_without_conn_routine().await; + Ok(()) + } + + pub fn get_peer_map(&self) -> Arc { + self.peers.clone() + } + + pub fn get_peer_rpc_mgr(&self) -> Arc { + self.peer_rpc_mgr.clone() + } + + pub fn my_node_id(&self) -> uuid::Uuid { + self.my_node_id + } + + pub fn get_global_ctx(&self) -> ArcGlobalCtx { + self.global_ctx.clone() + } + + pub fn get_nic_channel(&self) -> mpsc::Sender { + self.nic_channel.clone() + } +} diff --git a/easytier-core/src/peers/peer_map.rs b/easytier-core/src/peers/peer_map.rs new file mode 100644 index 0000000..7ee5c3b --- /dev/null +++ b/easytier-core/src/peers/peer_map.rs @@ -0,0 +1,140 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use easytier_rpc::PeerConnInfo; +use tokio::sync::mpsc; +use tokio_util::bytes::Bytes; + +use crate::{ + common::{error::Error, global_ctx::ArcGlobalCtx}, + tunnels::TunnelError, +}; + +use super::{peer::Peer, peer_conn::PeerConn, route_trait::ArcRoute, PeerId}; + +pub struct PeerMap { + peer_map: DashMap>, + packet_send: mpsc::Sender, +} + +impl PeerMap { + pub fn new(packet_send: mpsc::Sender) -> Self { + PeerMap { + peer_map: DashMap::new(), + packet_send, + } + } + + async fn add_new_peer(&self, peer: Peer) { + self.peer_map.insert(peer.peer_node_id, Arc::new(peer)); + } + + pub async fn add_new_peer_conn(&self, peer_conn: PeerConn, global_ctx: ArcGlobalCtx) { + let peer_id = peer_conn.get_peer_id(); + let no_entry = self.peer_map.get(&peer_id).is_none(); + if no_entry { + let new_peer = Peer::new(peer_id, self.packet_send.clone(), global_ctx); + new_peer.add_peer_conn(peer_conn).await; + self.add_new_peer(new_peer).await; + } else { + let peer = self.peer_map.get(&peer_id).unwrap().clone(); + peer.add_peer_conn(peer_conn).await; + } + } + + fn get_peer_by_id(&self, peer_id: &PeerId) -> Option> { + self.peer_map.get(peer_id).map(|v| v.clone()) + } + + pub async fn send_msg_directly( + &self, + msg: Bytes, + dst_peer_id: &uuid::Uuid, + ) -> Result<(), Error> { + match self.get_peer_by_id(dst_peer_id) { + Some(peer) => { + peer.send_msg(msg).await?; + } + None => { + log::error!("no peer for dst_peer_id: {}", dst_peer_id); + return Ok(()); + } + } + + Ok(()) + } + + pub async fn send_msg( + &self, + msg: Bytes, + dst_peer_id: &uuid::Uuid, + route: ArcRoute, + ) -> Result<(), Error> { + // get route info + let gateway_peer_id = route.get_next_hop(dst_peer_id).await; + + if gateway_peer_id.is_none() { + log::error!("no gateway for dst_peer_id: {}", dst_peer_id); + return Ok(()); + } + + let gateway_peer_id = gateway_peer_id.unwrap(); + self.send_msg_directly(msg, &gateway_peer_id).await?; + + Ok(()) + } + + pub async fn list_peers(&self) -> Vec { + let mut ret = Vec::new(); + for item in self.peer_map.iter() { + let peer_id = item.key(); + ret.push(*peer_id); + } + ret + } + + pub async fn list_peers_with_conn(&self) -> Vec { + let mut ret = Vec::new(); + let peers = self.list_peers().await; + for peer_id in peers.iter() { + let Some(peer) = self.get_peer_by_id(peer_id) else { + continue; + }; + if peer.list_peer_conns().await.len() > 0 { + ret.push(*peer_id); + } + } + ret + } + + pub async fn list_peer_conns(&self, peer_id: &PeerId) -> Option> { + if let Some(p) = self.get_peer_by_id(peer_id) { + Some(p.list_peer_conns().await) + } else { + return None; + } + } + + pub async fn close_peer_conn( + &self, + peer_id: &PeerId, + conn_id: &uuid::Uuid, + ) -> Result<(), Error> { + if let Some(p) = self.get_peer_by_id(peer_id) { + p.close_peer_conn(conn_id).await + } else { + return Err(Error::NotFound); + } + } + + pub async fn close_peer(&self, peer_id: &PeerId) -> Result<(), TunnelError> { + let remove_ret = self.peer_map.remove(peer_id); + tracing::info!( + ?peer_id, + has_old_value = ?remove_ret.is_some(), + peer_ref_counter = ?remove_ret.map(|v| Arc::strong_count(&v.1)), + "peer is closed" + ); + Ok(()) + } +} diff --git a/easytier-core/src/peers/peer_rpc.rs b/easytier-core/src/peers/peer_rpc.rs new file mode 100644 index 0000000..daadb29 --- /dev/null +++ b/easytier-core/src/peers/peer_rpc.rs @@ -0,0 +1,509 @@ +use std::sync::Arc; + +use dashmap::DashMap; +use futures::{SinkExt, StreamExt}; +use rkyv::Deserialize; +use tarpc::{server::Channel, transport::channel::UnboundedChannel}; +use tokio::{ + sync::mpsc::{self, UnboundedSender}, + task::JoinSet, +}; +use tokio_util::bytes::Bytes; +use tracing::Instrument; + +use crate::{common::error::Error, peers::packet::Packet}; + +use super::packet::{CtrlPacketBody, PacketBody}; + +type PeerRpcServiceId = u32; + +#[async_trait::async_trait] +#[auto_impl::auto_impl(Arc)] +pub trait PeerRpcManagerTransport: Send + Sync + 'static { + fn my_peer_id(&self) -> uuid::Uuid; + async fn send(&self, msg: Bytes, dst_peer_id: &uuid::Uuid) -> Result<(), Error>; + async fn recv(&self) -> Result; +} + +type PacketSender = UnboundedSender; + +struct PeerRpcEndPoint { + peer_id: uuid::Uuid, + packet_sender: PacketSender, + tasks: JoinSet<()>, +} + +type PeerRpcEndPointCreator = Box PeerRpcEndPoint + Send + Sync + 'static>; +#[derive(Hash, Eq, PartialEq, Clone)] +struct PeerRpcClientCtxKey(uuid::Uuid, PeerRpcServiceId); + +// handle rpc request from one peer +pub struct PeerRpcManager { + service_map: Arc>, + tasks: JoinSet<()>, + tspt: Arc>, + + service_registry: Arc>, + peer_rpc_endpoints: Arc>, + + client_resp_receivers: Arc>, +} + +impl std::fmt::Debug for PeerRpcManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PeerRpcManager") + .field("node_id", &self.tspt.my_peer_id()) + .finish() + } +} + +#[derive(Debug)] +struct TaRpcPacketInfo { + from_peer: uuid::Uuid, + to_peer: uuid::Uuid, + service_id: PeerRpcServiceId, + is_req: bool, + content: Vec, +} + +impl PeerRpcManager { + pub fn new(tspt: impl PeerRpcManagerTransport) -> Self { + Self { + service_map: Arc::new(DashMap::new()), + tasks: JoinSet::new(), + tspt: Arc::new(Box::new(tspt)), + + service_registry: Arc::new(DashMap::new()), + peer_rpc_endpoints: Arc::new(DashMap::new()), + + client_resp_receivers: Arc::new(DashMap::new()), + } + } + + pub fn run_service(self: &Self, service_id: PeerRpcServiceId, s: S) -> () + where + S: tarpc::server::Serve + Clone + Send + Sync + 'static, + Req: Send + 'static + serde::Serialize + for<'a> serde::Deserialize<'a>, + S::Resp: + Send + std::fmt::Debug + 'static + serde::Serialize + for<'a> serde::Deserialize<'a>, + S::Fut: Send + 'static, + { + let tspt = self.tspt.clone(); + let creator = Box::new(move |peer_id: uuid::Uuid| { + let mut tasks = JoinSet::new(); + let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel::(); + let (mut client_transport, server_transport) = tarpc::transport::channel::unbounded(); + let server = tarpc::server::BaseChannel::with_defaults(server_transport); + + let my_peer_id_clone = tspt.my_peer_id(); + let peer_id_clone = peer_id.clone(); + + let o = server.execute(s.clone()); + tasks.spawn(o); + + let tspt = tspt.clone(); + tasks.spawn(async move { + let mut cur_req_uuid = None; + loop { + tokio::select! { + Some(resp) = client_transport.next() => { + tracing::trace!(resp = ?resp, "recv packet from client"); + if resp.is_err() { + tracing::warn!(err = ?resp.err(), + "[PEER RPC MGR] client_transport in server side got channel error, ignore it."); + continue; + } + let resp = resp.unwrap(); + + if cur_req_uuid.is_none() { + tracing::error!("[PEER RPC MGR] cur_req_uuid is none, ignore this resp"); + continue; + } + + let serialized_resp = bincode::serialize(&resp); + if serialized_resp.is_err() { + tracing::error!(error = ?serialized_resp.err(), "serialize resp failed"); + continue; + } + + let msg = Packet::new_tarpc_packet( + tspt.my_peer_id(), + cur_req_uuid.take().unwrap(), + service_id, + false, + serialized_resp.unwrap(), + ); + + if let Err(e) = tspt.send(msg.into(), &peer_id).await { + tracing::error!(error = ?e, peer_id = ?peer_id, service_id = ?service_id, "send resp to peer failed"); + } + } + Some(packet) = packet_receiver.recv() => { + let info = Self::parse_rpc_packet(&packet); + if let Err(e) = info { + tracing::error!(error = ?e, packet = ?packet, "parse rpc packet failed"); + continue; + } + let info = info.unwrap(); + + assert_eq!(info.service_id, service_id); + cur_req_uuid = Some(packet.from_peer.clone().into()); + + tracing::trace!("recv packet from peer, packet: {:?}", packet); + + let decoded_ret = bincode::deserialize(&info.content.as_slice()); + if let Err(e) = decoded_ret { + tracing::error!(error = ?e, "decode rpc packet failed"); + continue; + } + let decoded: tarpc::ClientMessage = decoded_ret.unwrap(); + + if let Err(e) = client_transport.send(decoded).await { + tracing::error!(error = ?e, "send to req to client transport failed"); + } + } + else => { + tracing::warn!("[PEER RPC MGR] service runner destroy, peer_id: {}, service_id: {}", peer_id, service_id); + } + } + } + }.instrument(tracing::info_span!("service_runner", my_id = ?my_peer_id_clone, peer_id = ?peer_id_clone, service_id = ?service_id))); + + tracing::info!( + "[PEER RPC MGR] create new service endpoint for peer {}, service {}", + peer_id, + service_id + ); + + return PeerRpcEndPoint { + peer_id, + packet_sender, + tasks, + }; + // let resp = client_transport.next().await; + }); + + if let Some(_) = self.service_registry.insert(service_id, creator) { + panic!( + "[PEER RPC MGR] service {} is already registered", + service_id + ); + } + + log::info!( + "[PEER RPC MGR] register service {} succeed, my_node_id {}", + service_id, + self.tspt.my_peer_id() + ) + } + + fn parse_rpc_packet(packet: &Packet) -> Result { + match &packet.body { + PacketBody::Ctrl(CtrlPacketBody::TaRpc(id, is_req, body)) => Ok(TaRpcPacketInfo { + from_peer: packet.from_peer.clone().into(), + to_peer: packet.to_peer.clone().unwrap().into(), + service_id: *id, + is_req: *is_req, + content: body.clone(), + }), + _ => Err(Error::ShellCommandError("invalid packet".to_owned())), + } + } + + pub fn run(&self) { + let tspt = self.tspt.clone(); + let service_registry = self.service_registry.clone(); + let peer_rpc_endpoints = self.peer_rpc_endpoints.clone(); + let client_resp_receivers = self.client_resp_receivers.clone(); + tokio::spawn(async move { + loop { + let o = tspt.recv().await.unwrap(); + let packet = Packet::decode(&o); + let packet: Packet = packet.deserialize(&mut rkyv::Infallible).unwrap(); + let info = Self::parse_rpc_packet(&packet).unwrap(); + + if info.is_req { + if !service_registry.contains_key(&info.service_id) { + log::warn!( + "service {} not found, my_node_id: {}", + info.service_id, + tspt.my_peer_id() + ); + continue; + } + + let endpoint = peer_rpc_endpoints + .entry((info.to_peer, info.service_id)) + .or_insert_with(|| { + service_registry.get(&info.service_id).unwrap()(info.from_peer) + }); + + endpoint.packet_sender.send(packet).unwrap(); + } else { + if let Some(a) = client_resp_receivers + .get(&PeerRpcClientCtxKey(info.from_peer, info.service_id)) + { + log::trace!("recv resp: {:?}", packet); + if let Err(e) = a.send(packet) { + tracing::error!(error = ?e, "send resp to client failed"); + } + } else { + log::warn!("client resp receiver not found, info: {:?}", info); + } + } + } + }); + } + + #[tracing::instrument(skip(f))] + pub async fn do_client_rpc_scoped( + &self, + service_id: PeerRpcServiceId, + dst_peer_id: uuid::Uuid, + f: impl FnOnce(UnboundedChannel) -> Fut, + ) -> RpcRet + where + CM: serde::Serialize + for<'a> serde::Deserialize<'a> + Send + Sync + 'static, + Req: serde::Serialize + for<'a> serde::Deserialize<'a> + Send + Sync + 'static, + Fut: std::future::Future, + { + let mut tasks = JoinSet::new(); + let (packet_sender, mut packet_receiver) = mpsc::unbounded_channel::(); + let (client_transport, server_transport) = + tarpc::transport::channel::unbounded::(); + + let (mut server_s, mut server_r) = server_transport.split(); + + let tspt = self.tspt.clone(); + tasks.spawn(async move { + while let Some(a) = server_r.next().await { + if a.is_err() { + tracing::error!(error = ?a.err(), "channel error"); + continue; + } + + let a = bincode::serialize(&a.unwrap()); + if a.is_err() { + tracing::error!(error = ?a.err(), "bincode serialize failed"); + continue; + } + + let a = Packet::new_tarpc_packet( + tspt.my_peer_id(), + dst_peer_id, + service_id, + true, + a.unwrap(), + ); + + if let Err(e) = tspt.send(a.into(), &dst_peer_id).await { + tracing::error!(error = ?e, dst_peer_id = ?dst_peer_id, "send to peer failed"); + } + } + + tracing::warn!("[PEER RPC MGR] server trasport read aborted"); + }); + + tasks.spawn(async move { + while let Some(packet) = packet_receiver.recv().await { + tracing::trace!("tunnel recv: {:?}", packet); + + let info = PeerRpcManager::parse_rpc_packet(&packet); + if let Err(e) = info { + tracing::error!(error = ?e, "parse rpc packet failed"); + continue; + } + + let decoded = bincode::deserialize(&info.unwrap().content.as_slice()); + if let Err(e) = decoded { + tracing::error!(error = ?e, "decode rpc packet failed"); + continue; + } + + if let Err(e) = server_s.send(decoded.unwrap()).await { + tracing::error!(error = ?e, "send to rpc server channel failed"); + } + } + + tracing::warn!("[PEER RPC MGR] server packet read aborted"); + }); + + let _insert_ret = self + .client_resp_receivers + .insert(PeerRpcClientCtxKey(dst_peer_id, service_id), packet_sender); + + f(client_transport).await + } + + pub fn my_peer_id(&self) -> uuid::Uuid { + self.tspt.my_peer_id() + } +} + +#[cfg(test)] +mod tests { + use futures::{SinkExt, StreamExt}; + use tokio_util::bytes::Bytes; + + use crate::{ + common::error::Error, + peers::{ + peer_rpc::PeerRpcManager, + tests::{connect_peer_manager, create_mock_peer_manager, wait_route_appear}, + }, + tunnels::{self, ring_tunnel::create_ring_tunnel_pair}, + }; + + use super::PeerRpcManagerTransport; + + #[tarpc::service] + pub trait TestRpcService { + async fn hello(s: String) -> String; + } + + #[derive(Clone)] + struct MockService { + prefix: String, + } + + #[tarpc::server] + impl TestRpcService for MockService { + async fn hello(self, _: tarpc::context::Context, s: String) -> String { + format!("{} {}", self.prefix, s) + } + } + + #[tokio::test] + async fn peer_rpc_basic_test() { + struct MockTransport { + tunnel: Box, + my_peer_id: uuid::Uuid, + } + + #[async_trait::async_trait] + impl PeerRpcManagerTransport for MockTransport { + fn my_peer_id(&self) -> uuid::Uuid { + self.my_peer_id + } + async fn send(&self, msg: Bytes, _dst_peer_id: &uuid::Uuid) -> Result<(), Error> { + println!("rpc mgr send: {:?}", msg); + self.tunnel.pin_sink().send(msg).await.unwrap(); + Ok(()) + } + async fn recv(&self) -> Result { + let ret = self.tunnel.pin_stream().next().await.unwrap(); + println!("rpc mgr recv: {:?}", ret); + return ret.map(|v| v.freeze()).map_err(|_| Error::Unknown); + } + } + + let (ct, st) = create_ring_tunnel_pair(); + + let server_rpc_mgr = PeerRpcManager::new(MockTransport { + tunnel: st, + my_peer_id: uuid::Uuid::new_v4(), + }); + server_rpc_mgr.run(); + let s = MockService { + prefix: "hello".to_owned(), + }; + server_rpc_mgr.run_service(1, s.serve()); + + let client_rpc_mgr = PeerRpcManager::new(MockTransport { + tunnel: ct, + my_peer_id: uuid::Uuid::new_v4(), + }); + client_rpc_mgr.run(); + + let ret = client_rpc_mgr + .do_client_rpc_scoped(1, server_rpc_mgr.my_peer_id(), |c| async { + let c = TestRpcServiceClient::new(tarpc::client::Config::default(), c).spawn(); + let ret = c.hello(tarpc::context::current(), "abc".to_owned()).await; + ret + }) + .await; + + println!("ret: {:?}", ret); + assert_eq!(ret.unwrap(), "hello abc"); + } + + #[tokio::test] + async fn test_rpc_with_peer_manager() { + let peer_mgr_a = create_mock_peer_manager().await; + let peer_mgr_b = create_mock_peer_manager().await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.my_node_id()) + .await + .unwrap(); + + assert_eq!(peer_mgr_a.get_peer_map().list_peers().await.len(), 1); + assert_eq!( + peer_mgr_a.get_peer_map().list_peers().await[0], + peer_mgr_b.my_node_id() + ); + + let s = MockService { + prefix: "hello".to_owned(), + }; + peer_mgr_b.get_peer_rpc_mgr().run_service(1, s.serve()); + + let ip_list = peer_mgr_a + .get_peer_rpc_mgr() + .do_client_rpc_scoped(1, peer_mgr_b.my_node_id(), |c| async { + let c = TestRpcServiceClient::new(tarpc::client::Config::default(), c).spawn(); + let ret = c.hello(tarpc::context::current(), "abc".to_owned()).await; + ret + }) + .await; + + println!("ip_list: {:?}", ip_list); + assert_eq!(ip_list.as_ref().unwrap(), "hello abc"); + } + + #[tokio::test] + async fn test_multi_service_with_peer_manager() { + let peer_mgr_a = create_mock_peer_manager().await; + let peer_mgr_b = create_mock_peer_manager().await; + connect_peer_manager(peer_mgr_a.clone(), peer_mgr_b.clone()).await; + wait_route_appear(peer_mgr_a.clone(), peer_mgr_b.my_node_id()) + .await + .unwrap(); + + assert_eq!(peer_mgr_a.get_peer_map().list_peers().await.len(), 1); + assert_eq!( + peer_mgr_a.get_peer_map().list_peers().await[0], + peer_mgr_b.my_node_id() + ); + + let s = MockService { + prefix: "hello_a".to_owned(), + }; + peer_mgr_b.get_peer_rpc_mgr().run_service(1, s.serve()); + let b = MockService { + prefix: "hello_b".to_owned(), + }; + peer_mgr_b.get_peer_rpc_mgr().run_service(2, b.serve()); + + let ip_list = peer_mgr_a + .get_peer_rpc_mgr() + .do_client_rpc_scoped(1, peer_mgr_b.my_node_id(), |c| async { + let c = TestRpcServiceClient::new(tarpc::client::Config::default(), c).spawn(); + let ret = c.hello(tarpc::context::current(), "abc".to_owned()).await; + ret + }) + .await; + + assert_eq!(ip_list.as_ref().unwrap(), "hello_a abc"); + + let ip_list = peer_mgr_a + .get_peer_rpc_mgr() + .do_client_rpc_scoped(2, peer_mgr_b.my_node_id(), |c| async { + let c = TestRpcServiceClient::new(tarpc::client::Config::default(), c).spawn(); + let ret = c.hello(tarpc::context::current(), "abc".to_owned()).await; + ret + }) + .await; + + assert_eq!(ip_list.as_ref().unwrap(), "hello_b abc"); + } +} diff --git a/easytier-core/src/peers/rip_route.rs b/easytier-core/src/peers/rip_route.rs new file mode 100644 index 0000000..c0efd23 --- /dev/null +++ b/easytier-core/src/peers/rip_route.rs @@ -0,0 +1,480 @@ +use std::{net::Ipv4Addr, sync::Arc, time::Duration}; + +use async_trait::async_trait; +use dashmap::DashMap; +use easytier_rpc::{NatType, StunInfo}; +use rkyv::{Archive, Deserialize, Serialize}; +use tokio::{sync::Mutex, task::JoinSet}; +use tokio_util::bytes::Bytes; +use tracing::Instrument; +use uuid::Uuid; + +use crate::{ + common::{ + error::Error, + global_ctx::ArcGlobalCtx, + rkyv_util::{decode_from_bytes, encode_to_bytes}, + stun::StunInfoCollectorTrait, + }, + peers::{ + packet::{self, UUID}, + route_trait::{Route, RouteInterfaceBox}, + PeerId, + }, +}; + +#[derive(Archive, Deserialize, Serialize, Clone, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct SyncPeerInfo { + // means next hop in route table. + pub peer_id: UUID, + pub cost: u32, + pub ipv4_addr: Option, + pub proxy_cidrs: Vec, + pub hostname: Option, + pub udp_stun_info: i8, +} + +impl SyncPeerInfo { + pub fn new_self(from_peer: UUID, global_ctx: &ArcGlobalCtx) -> Self { + SyncPeerInfo { + peer_id: from_peer, + cost: 0, + ipv4_addr: global_ctx.get_ipv4(), + proxy_cidrs: global_ctx + .get_proxy_cidrs() + .iter() + .map(|x| x.to_string()) + .collect(), + hostname: global_ctx.get_hostname(), + udp_stun_info: global_ctx + .get_stun_info_collector() + .get_stun_info() + .udp_nat_type as i8, + } + } + + pub fn clone_for_route_table(&self, next_hop: &UUID, cost: u32, from: &Self) -> Self { + SyncPeerInfo { + peer_id: next_hop.clone(), + cost, + ipv4_addr: from.ipv4_addr.clone(), + proxy_cidrs: from.proxy_cidrs.clone(), + hostname: from.hostname.clone(), + udp_stun_info: from.udp_stun_info, + } + } +} + +#[derive(Archive, Deserialize, Serialize, Clone, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub struct SyncPeer { + pub myself: SyncPeerInfo, + pub neighbors: Vec, +} + +impl SyncPeer { + pub fn new( + from_peer: UUID, + _to_peer: UUID, + neighbors: Vec, + global_ctx: ArcGlobalCtx, + ) -> Self { + SyncPeer { + myself: SyncPeerInfo::new_self(from_peer, &global_ctx), + neighbors, + } + } +} + +struct SyncPeerFromRemote { + packet: SyncPeer, + last_update: std::time::Instant, +} + +type SyncPeerFromRemoteMap = Arc>; + +#[derive(Clone, Debug)] +struct RouteTable { + route_info: DashMap, + ipv4_peer_id_map: DashMap, + cidr_peer_id_map: DashMap, +} + +impl RouteTable { + fn new() -> Self { + RouteTable { + route_info: DashMap::new(), + ipv4_peer_id_map: DashMap::new(), + cidr_peer_id_map: DashMap::new(), + } + } + + fn copy_from(&self, other: &Self) { + self.route_info.clear(); + for item in other.route_info.iter() { + let (k, v) = item.pair(); + self.route_info.insert(*k, v.clone()); + } + + self.ipv4_peer_id_map.clear(); + for item in other.ipv4_peer_id_map.iter() { + let (k, v) = item.pair(); + self.ipv4_peer_id_map.insert(*k, *v); + } + + self.cidr_peer_id_map.clear(); + for item in other.cidr_peer_id_map.iter() { + let (k, v) = item.pair(); + self.cidr_peer_id_map.insert(*k, *v); + } + } +} + +pub struct BasicRoute { + my_peer_id: packet::UUID, + global_ctx: ArcGlobalCtx, + interface: Arc>>, + + route_table: Arc, + + sync_peer_from_remote: SyncPeerFromRemoteMap, + + tasks: Mutex>, + + need_sync_notifier: Arc, +} + +impl BasicRoute { + pub fn new(my_peer_id: Uuid, global_ctx: ArcGlobalCtx) -> Self { + BasicRoute { + my_peer_id: my_peer_id.into(), + global_ctx, + interface: Arc::new(Mutex::new(None)), + + route_table: Arc::new(RouteTable::new()), + + sync_peer_from_remote: Arc::new(DashMap::new()), + tasks: Mutex::new(JoinSet::new()), + + need_sync_notifier: Arc::new(tokio::sync::Notify::new()), + } + } + + fn update_route_table( + my_id: packet::UUID, + sync_peer_reqs: SyncPeerFromRemoteMap, + route_table: Arc, + ) { + tracing::trace!(my_id = ?my_id, route_table = ?route_table, "update route table"); + + let new_route_table = Arc::new(RouteTable::new()); + for item in sync_peer_reqs.iter() { + Self::update_route_table_with_req( + my_id.clone(), + &item.value().packet, + new_route_table.clone(), + ); + } + + route_table.copy_from(&new_route_table); + } + + fn update_route_table_with_req( + my_id: packet::UUID, + packet: &SyncPeer, + route_table: Arc, + ) { + let peer_id = packet.myself.peer_id.clone(); + let update = |cost: u32, peer_info: &SyncPeerInfo| { + let node_id: uuid::Uuid = peer_info.peer_id.clone().into(); + let ret = route_table + .route_info + .entry(node_id.clone().into()) + .and_modify(|info| { + if info.cost > cost { + *info = info.clone_for_route_table(&peer_id, cost, &peer_info); + } + }) + .or_insert( + peer_info + .clone() + .clone_for_route_table(&peer_id, cost, &peer_info), + ) + .value() + .clone(); + + if ret.cost > 32 { + log::error!( + "cost too large: {}, may lost connection, remove it", + ret.cost + ); + route_table.route_info.remove(&node_id); + } + + log::trace!( + "update route info, to: {:?}, gateway: {:?}, cost: {}, peer: {:?}", + node_id, + peer_id, + cost, + &peer_info + ); + + if let Some(ipv4) = peer_info.ipv4_addr { + route_table + .ipv4_peer_id_map + .insert(ipv4.clone(), node_id.clone().into()); + } + + for cidr in peer_info.proxy_cidrs.iter() { + let cidr: cidr::IpCidr = cidr.parse().unwrap(); + route_table + .cidr_peer_id_map + .insert(cidr, node_id.clone().into()); + } + }; + + for neighbor in packet.neighbors.iter() { + if neighbor.peer_id == my_id { + continue; + } + update(neighbor.cost + 1, &neighbor); + log::trace!("route info: {:?}", neighbor); + } + + // add the sender peer to route info + update(1, &packet.myself); + + log::trace!("my_id: {:?}, current route table: {:?}", my_id, route_table); + } + + async fn send_sync_peer_request( + interface: &RouteInterfaceBox, + my_peer_id: packet::UUID, + global_ctx: ArcGlobalCtx, + peer_id: PeerId, + route_table: Arc, + ) -> Result<(), Error> { + let mut route_info_copy: Vec = Vec::new(); + // copy the route info + for item in route_table.route_info.iter() { + let (k, v) = item.pair(); + route_info_copy.push(v.clone().clone_for_route_table(&(*k).into(), v.cost, &v)); + } + let msg = SyncPeer::new(my_peer_id, peer_id.into(), route_info_copy, global_ctx); + // TODO: this may exceed the MTU of the tunnel + interface + .send_route_packet(encode_to_bytes::<_, 4096>(&msg), 1, &peer_id) + .await + } + + async fn sync_peer_periodically(&self) { + let route_table = self.route_table.clone(); + let global_ctx = self.global_ctx.clone(); + let my_peer_id = self.my_peer_id.clone(); + let interface = self.interface.clone(); + let notifier = self.need_sync_notifier.clone(); + self.tasks.lock().await.spawn( + async move { + loop { + let lockd_interface = interface.lock().await; + let interface = lockd_interface.as_ref().unwrap(); + let peers = interface.list_peers().await; + for peer in peers.iter() { + let ret = Self::send_sync_peer_request( + interface, + my_peer_id.clone(), + global_ctx.clone(), + *peer, + route_table.clone(), + ) + .await; + + match &ret { + Ok(_) => { + log::trace!("send sync peer request to peer: {}", peer); + } + Err(Error::PeerNoConnectionError(_)) => { + log::trace!("peer {} no connection", peer); + } + Err(e) => { + log::error!( + "send sync peer request to peer: {} error: {:?}", + peer, + e + ); + } + }; + } + tokio::select! { + _ = notifier.notified() => { + log::trace!("sync peer request triggered by notifier"); + } + _ = tokio::time::sleep(Duration::from_secs(1)) => { + log::trace!("sync peer request triggered by timeout"); + } + } + } + } + .instrument( + tracing::info_span!("sync_peer_periodically", my_id = ?self.my_peer_id, global_ctx = ?self.global_ctx), + ), + ); + } + + async fn check_expired_sync_peer_from_remote(&self) { + let route_table = self.route_table.clone(); + let my_peer_id = self.my_peer_id.clone(); + let sync_peer_from_remote = self.sync_peer_from_remote.clone(); + let notifier = self.need_sync_notifier.clone(); + self.tasks.lock().await.spawn(async move { + loop { + let mut need_update_route = false; + let now = std::time::Instant::now(); + let mut need_remove = Vec::new(); + for item in sync_peer_from_remote.iter() { + let (k, v) = item.pair(); + if now.duration_since(v.last_update).as_secs() > 5 { + need_update_route = true; + need_remove.insert(0, k.clone()); + } + } + + for k in need_remove.iter() { + log::warn!("remove expired sync peer: {:?}", k); + sync_peer_from_remote.remove(k); + } + + if need_update_route { + Self::update_route_table( + my_peer_id.clone(), + sync_peer_from_remote.clone(), + route_table.clone(), + ); + notifier.notify_one(); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + } + + fn get_peer_id_for_proxy(&self, ipv4: &Ipv4Addr) -> Option { + let ipv4 = std::net::IpAddr::V4(*ipv4); + for item in self.route_table.cidr_peer_id_map.iter() { + let (k, v) = item.pair(); + if k.contains(&ipv4) { + return Some(*v); + } + } + None + } +} + +#[async_trait] +impl Route for BasicRoute { + async fn open(&self, interface: RouteInterfaceBox) -> Result { + *self.interface.lock().await = Some(interface); + self.sync_peer_periodically().await; + self.check_expired_sync_peer_from_remote().await; + Ok(1) + } + + async fn close(&self) {} + + #[tracing::instrument(skip(self, packet), fields(my_id = ?self.my_peer_id, ctx = ?self.global_ctx))] + async fn handle_route_packet(&self, src_peer_id: uuid::Uuid, packet: Bytes) { + let packet = decode_from_bytes::(&packet).unwrap(); + let p: SyncPeer = packet.deserialize(&mut rkyv::Infallible).unwrap(); + let mut updated = true; + assert_eq!(packet.myself.peer_id.to_uuid(), src_peer_id); + self.sync_peer_from_remote + .entry(packet.myself.peer_id.to_uuid()) + .and_modify(|v| { + if v.packet == *packet { + updated = false; + } else { + v.packet = p.clone(); + } + v.last_update = std::time::Instant::now(); + }) + .or_insert(SyncPeerFromRemote { + packet: p.clone(), + last_update: std::time::Instant::now(), + }); + + if updated { + Self::update_route_table( + self.my_peer_id.clone(), + self.sync_peer_from_remote.clone(), + self.route_table.clone(), + ); + self.need_sync_notifier.notify_one(); + } + } + + async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option { + if let Some(peer_id) = self.route_table.ipv4_peer_id_map.get(ipv4_addr) { + return Some(*peer_id); + } + + if let Some(peer_id) = self.get_peer_id_for_proxy(ipv4_addr) { + return Some(peer_id); + } + + log::info!("no peer id for ipv4: {}", ipv4_addr); + return None; + } + + async fn get_next_hop(&self, dst_peer_id: &PeerId) -> Option { + match self.route_table.route_info.get(dst_peer_id) { + Some(info) => { + return Some(info.peer_id.clone().into()); + } + None => { + log::error!("no route info for dst_peer_id: {}", dst_peer_id); + return None; + } + } + } + + async fn list_routes(&self) -> Vec { + let mut routes = Vec::new(); + + let parse_route_info = |real_peer_id: &Uuid, route_info: &SyncPeerInfo| { + let mut route = easytier_rpc::Route::default(); + route.ipv4_addr = if let Some(ipv4_addr) = route_info.ipv4_addr { + ipv4_addr.to_string() + } else { + "".to_string() + }; + route.peer_id = real_peer_id.to_string(); + route.next_hop_peer_id = Uuid::from(route_info.peer_id.clone()).to_string(); + route.cost = route_info.cost as i32; + route.proxy_cidrs = route_info.proxy_cidrs.clone(); + route.hostname = if let Some(hostname) = &route_info.hostname { + hostname.clone() + } else { + "".to_string() + }; + + let mut stun_info = StunInfo::default(); + if let Ok(udp_nat_type) = NatType::try_from(route_info.udp_stun_info as i32) { + stun_info.set_udp_nat_type(udp_nat_type); + } + route.stun_info = Some(stun_info); + + route + }; + + self.route_table.route_info.iter().for_each(|item| { + routes.push(parse_route_info(item.key(), item.value())); + }); + + routes + } +} diff --git a/easytier-core/src/peers/route_trait.rs b/easytier-core/src/peers/route_trait.rs new file mode 100644 index 0000000..e328724 --- /dev/null +++ b/easytier-core/src/peers/route_trait.rs @@ -0,0 +1,36 @@ +use std::{net::Ipv4Addr, sync::Arc}; + +use async_trait::async_trait; +use tokio_util::bytes::Bytes; + +use crate::common::error::Error; + +use super::PeerId; + +#[async_trait] +pub trait RouteInterface { + async fn list_peers(&self) -> Vec; + async fn send_route_packet( + &self, + msg: Bytes, + route_id: u8, + dst_peer_id: &PeerId, + ) -> Result<(), Error>; +} + +pub type RouteInterfaceBox = Box; + +#[async_trait] +pub trait Route { + async fn open(&self, interface: RouteInterfaceBox) -> Result; + async fn close(&self); + + async fn get_peer_id_by_ipv4(&self, ipv4: &Ipv4Addr) -> Option; + async fn get_next_hop(&self, peer_id: &PeerId) -> Option; + + async fn handle_route_packet(&self, src_peer_id: PeerId, packet: Bytes); + + async fn list_routes(&self) -> Vec; +} + +pub type ArcRoute = Arc>; diff --git a/easytier-core/src/peers/rpc_service.rs b/easytier-core/src/peers/rpc_service.rs new file mode 100644 index 0000000..5814f34 --- /dev/null +++ b/easytier-core/src/peers/rpc_service.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use easytier_rpc::peer_manage_rpc_server::PeerManageRpc; +use easytier_rpc::{ListPeerRequest, ListPeerResponse, ListRouteRequest, ListRouteResponse}; +use tonic::{Request, Response, Status}; + +use super::peer_manager::PeerManager; + +pub struct PeerManagerRpcService { + peer_manager: Arc, +} + +impl PeerManagerRpcService { + pub fn new(peer_manager: Arc) -> Self { + PeerManagerRpcService { peer_manager } + } +} + +#[tonic::async_trait] +impl PeerManageRpc for PeerManagerRpcService { + async fn list_peer( + &self, + _request: Request, // Accept request of type HelloRequest + ) -> Result, Status> { + let mut reply = ListPeerResponse::default(); + + let peers = self.peer_manager.get_peer_map().list_peers().await; + for peer in peers { + let mut peer_info = easytier_rpc::PeerInfo::default(); + peer_info.peer_id = peer.to_string(); + + if let Some(conns) = self + .peer_manager + .get_peer_map() + .list_peer_conns(&peer) + .await + { + peer_info.conns = conns; + } + + reply.peer_infos.push(peer_info); + } + + Ok(Response::new(reply)) + } + + async fn list_route( + &self, + _request: Request, // Accept request of type HelloRequest + ) -> Result, Status> { + let mut reply = ListRouteResponse::default(); + reply.routes = self.peer_manager.list_routes().await; + Ok(Response::new(reply)) + } +} diff --git a/easytier-core/src/peers/tests.rs b/easytier-core/src/peers/tests.rs new file mode 100644 index 0000000..dda7a83 --- /dev/null +++ b/easytier-core/src/peers/tests.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use crate::{ + common::{error::Error, global_ctx::tests::get_mock_global_ctx}, + peers::rip_route::BasicRoute, + tunnels::ring_tunnel::create_ring_tunnel_pair, +}; + +use super::peer_manager::PeerManager; + +pub async fn create_mock_peer_manager() -> Arc { + let (s, _r) = tokio::sync::mpsc::channel(1000); + let peer_mgr = Arc::new(PeerManager::new(get_mock_global_ctx(), s)); + peer_mgr + .set_route(BasicRoute::new( + peer_mgr.my_node_id(), + peer_mgr.get_global_ctx(), + )) + .await; + peer_mgr.run().await.unwrap(); + peer_mgr +} + +pub async fn connect_peer_manager(client: Arc, server: Arc) { + let (a_ring, b_ring) = create_ring_tunnel_pair(); + let a_mgr_copy = client.clone(); + tokio::spawn(async move { + a_mgr_copy.add_client_tunnel(a_ring).await.unwrap(); + }); + let b_mgr_copy = server.clone(); + tokio::spawn(async move { + b_mgr_copy.add_tunnel_as_server(b_ring).await.unwrap(); + }); +} + +pub async fn wait_route_appear_with_cost( + peer_mgr: Arc, + node_id: uuid::Uuid, + cost: Option, +) -> Result<(), Error> { + let now = std::time::Instant::now(); + while now.elapsed().as_secs() < 5 { + let route = peer_mgr.list_routes().await; + if route.iter().any(|r| { + r.peer_id.clone().parse::().unwrap() == node_id + && (cost.is_none() || r.cost == cost.unwrap()) + }) { + return Ok(()); + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + return Err(Error::NotFound); +} + +pub async fn wait_route_appear( + peer_mgr: Arc, + node_id: uuid::Uuid, +) -> Result<(), Error> { + wait_route_appear_with_cost(peer_mgr, node_id, None).await +} diff --git a/easytier-core/src/rpc/cli.rs b/easytier-core/src/rpc/cli.rs new file mode 100644 index 0000000..9c39b3e --- /dev/null +++ b/easytier-core/src/rpc/cli.rs @@ -0,0 +1 @@ +tonic::include_proto!("cli"); // The string specified here must match the proto package name diff --git a/easytier-core/src/rpc/lib.rs b/easytier-core/src/rpc/lib.rs new file mode 100644 index 0000000..546641b --- /dev/null +++ b/easytier-core/src/rpc/lib.rs @@ -0,0 +1,4 @@ +pub mod cli; +pub use cli::*; + +pub mod peer; diff --git a/easytier-core/src/rpc/peer.rs b/easytier-core/src/rpc/peer.rs new file mode 100644 index 0000000..02951b3 --- /dev/null +++ b/easytier-core/src/rpc/peer.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct GetIpListResponse { + pub public_ipv4: String, + pub interface_ipv4s: Vec, + pub public_ipv6: String, + pub interface_ipv6s: Vec, +} + +impl GetIpListResponse { + pub fn new() -> Self { + GetIpListResponse { + public_ipv4: "".to_string(), + interface_ipv4s: vec![], + public_ipv6: "".to_string(), + interface_ipv6s: vec![], + } + } +} diff --git a/easytier-core/src/tests/mod.rs b/easytier-core/src/tests/mod.rs new file mode 100644 index 0000000..83353f6 --- /dev/null +++ b/easytier-core/src/tests/mod.rs @@ -0,0 +1,174 @@ +mod three_node; + +pub fn get_guest_veth_name(net_ns: &str) -> &str { + Box::leak(format!("veth_{}_g", net_ns).into_boxed_str()) +} + +pub fn get_host_veth_name(net_ns: &str) -> &str { + Box::leak(format!("veth_{}_h", net_ns).into_boxed_str()) +} + +pub fn del_netns(name: &str) { + // del veth host + let _ = std::process::Command::new("ip") + .args(&["link", "del", get_host_veth_name(name)]) + .output(); + + let _ = std::process::Command::new("ip") + .args(&["netns", "del", name]) + .output(); +} + +pub fn create_netns(name: &str, ipv4: &str) { + // create netns + let _ = std::process::Command::new("ip") + .args(&["netns", "add", name]) + .output() + .unwrap(); + + // set lo up + let _ = std::process::Command::new("ip") + .args(&["netns", "exec", name, "ip", "link", "set", "lo", "up"]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(&[ + "link", + "add", + get_host_veth_name(name), + "type", + "veth", + "peer", + "name", + get_guest_veth_name(name), + ]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(&["link", "set", get_guest_veth_name(name), "netns", name]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(&[ + "netns", + "exec", + name, + "ip", + "link", + "set", + get_guest_veth_name(name), + "up", + ]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(&["link", "set", get_host_veth_name(name), "up"]) + .output() + .unwrap(); + + let _ = std::process::Command::new("ip") + .args(&[ + "netns", + "exec", + name, + "ip", + "addr", + "add", + ipv4, + "dev", + get_guest_veth_name(name), + ]) + .output() + .unwrap(); +} + +pub fn prepare_bridge(name: &str) { + // del bridge with brctl + let _ = std::process::Command::new("brctl") + .args(&["delbr", name]) + .output(); + + // create new br + let _ = std::process::Command::new("brctl") + .args(&["addbr", name]) + .output(); +} + +pub fn add_ns_to_bridge(br_name: &str, ns_name: &str) { + // use brctl to add ns to bridge + let _ = std::process::Command::new("brctl") + .args(&["addif", br_name, get_host_veth_name(ns_name)]) + .output() + .unwrap(); + + // set bridge up + let _ = std::process::Command::new("ip") + .args(&["link", "set", br_name, "up"]) + .output() + .unwrap(); +} + +pub fn enable_log() { + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing::level_filters::LevelFilter::INFO.into()) + .from_env() + .unwrap(); + tracing_subscriber::fmt::fmt() + .pretty() + .with_env_filter(filter) + .init(); +} + +fn check_route(ipv4: &str, dst_peer_id: uuid::Uuid, routes: Vec) { + let mut found = false; + for r in routes.iter() { + if r.ipv4_addr == ipv4.to_string() { + found = true; + assert_eq!(r.peer_id, dst_peer_id.to_string(), "{:?}", routes); + } + } + assert!(found); +} + +async fn wait_proxy_route_appear( + mgr: &std::sync::Arc, + ipv4: &str, + dst_peer_id: uuid::Uuid, + proxy_cidr: &str, +) { + let now = std::time::Instant::now(); + loop { + for r in mgr.list_routes().await.iter() { + let r = r; + if r.proxy_cidrs.contains(&proxy_cidr.to_owned()) { + assert_eq!(r.peer_id, dst_peer_id.to_string()); + assert_eq!(r.ipv4_addr, ipv4); + return; + } + } + if now.elapsed().as_secs() > 5 { + panic!("wait proxy route appear timeout"); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } +} + +fn set_link_status(net_ns: &str, up: bool) { + let _ = std::process::Command::new("ip") + .args(&[ + "netns", + "exec", + net_ns, + "ip", + "link", + "set", + get_guest_veth_name(net_ns), + if up { "up" } else { "down" }, + ]) + .output() + .unwrap(); +} diff --git a/easytier-core/src/tests/three_node.rs b/easytier-core/src/tests/three_node.rs new file mode 100644 index 0000000..fbcec17 --- /dev/null +++ b/easytier-core/src/tests/three_node.rs @@ -0,0 +1,227 @@ +use super::*; + +use crate::{ + common::netns::{NetNS, ROOT_NETNS_NAME}, + instance::instance::{Instance, InstanceConfigWriter}, + tunnels::{ + common::tests::_tunnel_pingpong_netns, + ring_tunnel::RingTunnelConnector, + tcp_tunnel::{TcpTunnelConnector, TcpTunnelListener}, + udp_tunnel::UdpTunnelConnector, + }, +}; + +pub fn prepare_linux_namespaces() { + del_netns("net_a"); + del_netns("net_b"); + del_netns("net_c"); + del_netns("net_d"); + + create_netns("net_a", "10.1.1.1/24"); + create_netns("net_b", "10.1.1.2/24"); + create_netns("net_c", "10.1.2.3/24"); + create_netns("net_d", "10.1.2.4/24"); + + prepare_bridge("br_a"); + prepare_bridge("br_b"); + + add_ns_to_bridge("br_a", "net_a"); + add_ns_to_bridge("br_a", "net_b"); + add_ns_to_bridge("br_b", "net_c"); + add_ns_to_bridge("br_b", "net_d"); +} + +pub async fn prepare_inst_configs() { + InstanceConfigWriter::new("inst1") + .set_ns(Some("net_a".into())) + .set_addr("10.144.144.1".to_owned()); + + InstanceConfigWriter::new("inst2") + .set_ns(Some("net_b".into())) + .set_addr("10.144.144.2".to_owned()); + + InstanceConfigWriter::new("inst3") + .set_ns(Some("net_c".into())) + .set_addr("10.144.144.3".to_owned()); +} + +pub async fn init_three_node(proto: &str) -> Vec { + log::set_max_level(log::LevelFilter::Info); + prepare_linux_namespaces(); + + prepare_inst_configs().await; + + let mut inst1 = Instance::new("inst1"); + let mut inst2 = Instance::new("inst2"); + let mut inst3 = Instance::new("inst3"); + + inst1.run().await.unwrap(); + inst2.run().await.unwrap(); + inst3.run().await.unwrap(); + + if proto == "tcp" { + inst2 + .get_conn_manager() + .add_connector(TcpTunnelConnector::new( + "tcp://10.1.1.1:11010".parse().unwrap(), + )); + } else { + inst2 + .get_conn_manager() + .add_connector(UdpTunnelConnector::new( + "udp://10.1.1.1:11010".parse().unwrap(), + )); + } + + inst2 + .get_conn_manager() + .add_connector(RingTunnelConnector::new( + format!("ring://{}", inst3.id()).parse().unwrap(), + )); + + // wait inst2 have two route. + let now = std::time::Instant::now(); + loop { + if inst2.get_peer_manager().list_routes().await.len() == 2 { + break; + } + if now.elapsed().as_secs() > 5 { + panic!("wait inst2 have two route timeout"); + } + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + + vec![inst1, inst2, inst3] +} + +#[tokio::test] +#[serial_test::serial] +pub async fn basic_three_node_test_tcp() { + let insts = init_three_node("tcp").await; + + check_route( + "10.144.144.2", + insts[1].id(), + insts[0].get_peer_manager().list_routes().await, + ); + + check_route( + "10.144.144.3", + insts[2].id(), + insts[0].get_peer_manager().list_routes().await, + ); +} + +#[tokio::test] +#[serial_test::serial] +pub async fn basic_three_node_test_udp() { + let insts = init_three_node("udp").await; + + check_route( + "10.144.144.2", + insts[1].id(), + insts[0].get_peer_manager().list_routes().await, + ); + + check_route( + "10.144.144.3", + insts[2].id(), + insts[0].get_peer_manager().list_routes().await, + ); +} + +#[tokio::test] +#[serial_test::serial] +pub async fn tcp_proxy_three_node_test() { + let insts = init_three_node("tcp").await; + + insts[2] + .get_global_ctx() + .add_proxy_cidr("10.1.2.0/24".parse().unwrap()) + .unwrap(); + assert_eq!(insts[2].get_global_ctx().get_proxy_cidrs().len(), 1); + + wait_proxy_route_appear( + &insts[0].get_peer_manager(), + "10.144.144.3", + insts[2].id(), + "10.1.2.0/24", + ) + .await; + + // wait updater + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + + 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()); + + _tunnel_pingpong_netns( + tcp_listener, + tcp_connector, + NetNS::new(Some("net_d".into())), + NetNS::new(Some("net_a".into())), + ) + .await; +} + +#[tokio::test] +#[serial_test::serial] +pub async fn icmp_proxy_three_node_test() { + let insts = init_three_node("tcp").await; + + insts[2] + .get_global_ctx() + .add_proxy_cidr("10.1.2.0/24".parse().unwrap()) + .unwrap(); + assert_eq!(insts[2].get_global_ctx().get_proxy_cidrs().len(), 1); + + wait_proxy_route_appear( + &insts[0].get_peer_manager(), + "10.144.144.3", + insts[2].id(), + "10.1.2.0/24", + ) + .await; + + // wait updater + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + + // send ping with shell in net_a to net_d + let _g = NetNS::new(Some(ROOT_NETNS_NAME.to_owned())).guard(); + let code = tokio::process::Command::new("ip") + .args(&[ + "netns", "exec", "net_a", "ping", "-c", "1", "-W", "1", "10.1.2.4", + ]) + .status() + .await + .unwrap(); + assert_eq!(code.code().unwrap(), 0); +} + +#[tokio::test] +#[serial_test::serial] +pub async fn proxy_three_node_disconnect_test() { + InstanceConfigWriter::new("inst4") + .set_ns(Some("net_d".into())) + .set_addr("10.144.144.4".to_owned()); + + let mut inst4 = Instance::new("inst4"); + inst4 + .get_conn_manager() + .add_connector(TcpTunnelConnector::new( + "tcp://10.1.2.3:11010".parse().unwrap(), + )); + inst4.run().await.unwrap(); + + tokio::spawn(async { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + set_link_status("net_d", false); + tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + set_link_status("net_d", true); + } + }); + + // TODO: add some traffic here, also should check route & peer list + tokio::time::sleep(tokio::time::Duration::from_secs(35)).await; +} diff --git a/easytier-core/src/tunnels/codec.rs b/easytier-core/src/tunnels/codec.rs new file mode 100644 index 0000000..4f91196 --- /dev/null +++ b/easytier-core/src/tunnels/codec.rs @@ -0,0 +1,54 @@ +use std::result::Result; +use tokio::io; +use tokio_util::{ + bytes::{BufMut, Bytes, BytesMut}, + codec::{Decoder, Encoder}, +}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct BytesCodec { + capacity: usize, +} + +impl BytesCodec { + /// Creates a new `BytesCodec` for shipping around raw bytes. + pub fn new(capacity: usize) -> BytesCodec { + BytesCodec { capacity } + } +} + +impl Decoder for BytesCodec { + type Item = BytesMut; + type Error = io::Error; + + fn decode(&mut self, buf: &mut BytesMut) -> Result, io::Error> { + if !buf.is_empty() { + let len = buf.len(); + let ret = Some(buf.split_to(len)); + buf.reserve(self.capacity); + Ok(ret) + } else { + Ok(None) + } + } +} + +impl Encoder for BytesCodec { + type Error = io::Error; + + fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> { + buf.reserve(data.len()); + buf.put(data); + Ok(()) + } +} + +impl Encoder for BytesCodec { + type Error = io::Error; + + fn encode(&mut self, data: BytesMut, buf: &mut BytesMut) -> Result<(), io::Error> { + buf.reserve(data.len()); + buf.put(data); + Ok(()) + } +} diff --git a/easytier-core/src/tunnels/common.rs b/easytier-core/src/tunnels/common.rs new file mode 100644 index 0000000..cabf14d --- /dev/null +++ b/easytier-core/src/tunnels/common.rs @@ -0,0 +1,399 @@ +use std::{ + collections::VecDeque, + net::IpAddr, + sync::Arc, + task::{ready, Context, Poll}, +}; + +use async_stream::stream; +use futures::{Future, FutureExt, Sink, SinkExt, Stream, StreamExt}; +use tokio::{sync::Mutex, time::error::Elapsed}; + +use std::pin::Pin; + +use crate::tunnels::{SinkError, TunnelError}; + +use super::{DatagramSink, DatagramStream, SinkItem, StreamT, Tunnel, TunnelInfo}; + +pub struct FramedTunnel { + read: Arc>, + write: Arc>, + + info: Option, +} + +impl FramedTunnel +where + R: Stream> + Send + Sync + Unpin + 'static, + W: Sink + Send + Sync + Unpin + 'static, + RE: std::error::Error + std::fmt::Debug + Send + Sync + 'static, + WE: std::error::Error + std::fmt::Debug + Send + Sync + 'static + From, +{ + pub fn new(read: R, write: W, info: Option) -> Self { + FramedTunnel { + read: Arc::new(Mutex::new(read)), + write: Arc::new(Mutex::new(write)), + info, + } + } + + pub fn new_tunnel_with_info(read: R, write: W, info: TunnelInfo) -> Box { + Box::new(FramedTunnel::new(read, write, Some(info))) + } + + pub fn recv_stream(&self) -> impl DatagramStream { + let read = self.read.clone(); + let info = self.info.clone(); + stream! { + loop { + let read_ret = read.lock().await.next().await; + if read_ret.is_none() { + tracing::info!(?info, "read_ret is none"); + yield Err(TunnelError::CommonError("recv stream closed".to_string())); + } else { + let read_ret = read_ret.unwrap(); + if read_ret.is_err() { + let err = read_ret.err().unwrap(); + tracing::info!(?info, "recv stream read error"); + yield Err(TunnelError::CommonError(err.to_string())); + } else { + yield Ok(read_ret.unwrap()); + } + } + } + } + } + + pub fn send_sink(&self) -> impl DatagramSink { + struct SendSink { + write: Arc>, + max_buffer_size: usize, + sending_buffers: Option>, + send_task: + Option> + Send + Sync + 'static>>>, + close_task: + Option> + Send + Sync + 'static>>>, + } + + impl SendSink + where + W: Sink + Send + Sync + Unpin + 'static, + WE: std::error::Error + std::fmt::Debug + Send + Sync + From, + { + fn try_send_buffser( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + if self.send_task.is_none() { + let mut buffers = self.sending_buffers.take().unwrap(); + let tun = self.write.clone(); + let send_task = async move { + if buffers.is_empty() { + return Ok(()); + } + + let mut locked_tun = tun.lock_owned().await; + while let Some(buf) = buffers.front() { + log::trace!( + "try_send buffer, len: {:?}, buf: {:?}", + buffers.len(), + &buf + ); + let timeout_task = tokio::time::timeout( + std::time::Duration::from_secs(1), + locked_tun.send(buf.clone()), + ); + let send_res = timeout_task.await; + let Ok(send_res) = send_res else { + // panic!("send timeout"); + let err = send_res.err().unwrap(); + return Err(err.into()); + }; + let Ok(_) = send_res else { + let err = send_res.err().unwrap(); + println!("send error: {:?}", err); + return Err(err); + }; + buffers.pop_front(); + } + return Ok(()); + }; + self.send_task = Some(Box::pin(send_task)); + } + + let ret = ready!(self.send_task.as_mut().unwrap().poll_unpin(cx)); + self.send_task = None; + self.sending_buffers = Some(VecDeque::new()); + return Poll::Ready(ret); + } + } + + impl Sink for SendSink + where + W: Sink + Send + Sync + Unpin + 'static, + WE: std::error::Error + std::fmt::Debug + Send + Sync + From, + { + type Error = SinkError; + + fn poll_ready( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let self_mut = self.get_mut(); + let sending_buf = self_mut.sending_buffers.as_ref(); + // if sending_buffers is None, must already be doing flush + if sending_buf.is_none() || sending_buf.unwrap().len() > self_mut.max_buffer_size { + return self_mut.poll_flush_unpin(cx); + } else { + return Poll::Ready(Ok(())); + } + } + + fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> { + assert!(self.send_task.is_none()); + let self_mut = self.get_mut(); + self_mut.sending_buffers.as_mut().unwrap().push_back(item); + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let self_mut = self.get_mut(); + let ret = self_mut.try_send_buffser(cx); + match ret { + Poll::Ready(Ok(())) => Poll::Ready(Ok(())), + Poll::Ready(Err(e)) => Poll::Ready(Err(SinkError::CommonError(e.to_string()))), + Poll::Pending => { + return Poll::Pending; + } + } + } + + fn poll_close( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let self_mut = self.get_mut(); + if self_mut.close_task.is_none() { + let tun = self_mut.write.clone(); + let close_task = async move { + let mut locked_tun = tun.lock_owned().await; + return locked_tun.close().await; + }; + self_mut.close_task = Some(Box::pin(close_task)); + } + + let ret = ready!(self_mut.close_task.as_mut().unwrap().poll_unpin(cx)); + self_mut.close_task = None; + + if ret.is_err() { + return Poll::Ready(Err(SinkError::CommonError( + ret.err().unwrap().to_string(), + ))); + } else { + return Poll::Ready(Ok(())); + } + } + } + + SendSink { + write: self.write.clone(), + max_buffer_size: 1000, + sending_buffers: Some(VecDeque::new()), + send_task: None, + close_task: None, + } + } +} + +impl Tunnel for FramedTunnel +where + R: Stream> + Send + Sync + Unpin + 'static, + W: Sink + Send + Sync + Unpin + 'static, + RE: std::error::Error + std::fmt::Debug + Send + Sync + 'static, + WE: std::error::Error + std::fmt::Debug + Send + Sync + 'static + From, +{ + fn stream(&self) -> Box { + Box::new(self.recv_stream()) + } + + fn sink(&self) -> Box { + Box::new(self.send_sink()) + } + + fn info(&self) -> Option { + if self.info.is_none() { + None + } else { + Some(self.info.clone().unwrap()) + } + } +} + +pub struct TunnelWithCustomInfo { + tunnel: Box, + info: TunnelInfo, +} + +impl TunnelWithCustomInfo { + pub fn new(tunnel: Box, info: TunnelInfo) -> Self { + TunnelWithCustomInfo { tunnel, info } + } +} + +impl Tunnel for TunnelWithCustomInfo { + fn stream(&self) -> Box { + self.tunnel.stream() + } + + fn sink(&self) -> Box { + self.tunnel.sink() + } + + fn info(&self) -> Option { + Some(self.info.clone()) + } +} + +pub(crate) fn get_interface_name_by_ip(local_ip: &IpAddr) -> Option { + let ifaces = pnet::datalink::interfaces(); + for iface in ifaces { + for ip in iface.ips { + if ip.ip() == *local_ip { + return Some(iface.name); + } + } + } + None +} + +pub mod tests { + use std::time::Instant; + + use futures::SinkExt; + use tokio_stream::StreamExt; + use tokio_util::bytes::{BufMut, Bytes, BytesMut}; + + use crate::{ + common::netns::NetNS, + tunnels::{close_tunnel, TunnelConnector, TunnelListener}, + }; + + pub async fn _tunnel_echo_server(tunnel: Box, once: bool) { + let mut recv = Box::into_pin(tunnel.stream()); + let mut send = Box::into_pin(tunnel.sink()); + + while let Some(ret) = recv.next().await { + if ret.is_err() { + log::trace!("recv error: {:?}", ret.err().unwrap()); + break; + } + let res = ret.unwrap(); + log::trace!("recv a msg, try echo back: {:?}", res); + send.send(Bytes::from(res)).await.unwrap(); + if once { + break; + } + } + log::warn!("echo server exit..."); + } + + pub(crate) async fn _tunnel_pingpong(listener: L, connector: C) + where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + _tunnel_pingpong_netns(listener, connector, NetNS::new(None), NetNS::new(None)).await + } + + pub(crate) async fn _tunnel_pingpong_netns( + mut listener: L, + mut connector: C, + l_netns: NetNS, + c_netns: NetNS, + ) where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + l_netns + .run_async(|| async { + listener.listen().await.unwrap(); + }) + .await; + + let lis = tokio::spawn(async move { + let ret = listener.accept().await.unwrap(); + assert_eq!( + ret.info().unwrap().local_addr, + listener.local_url().to_string() + ); + _tunnel_echo_server(ret, false).await + }); + + let tunnel = c_netns.run_async(|| connector.connect()).await.unwrap(); + + assert_eq!( + tunnel.info().unwrap().remote_addr, + connector.remote_url().to_string() + ); + + let mut send = tunnel.pin_sink(); + let mut recv = tunnel.pin_stream(); + let send_data = Bytes::from("abc"); + send.send(send_data).await.unwrap(); + let ret = tokio::time::timeout(tokio::time::Duration::from_secs(1), recv.next()) + .await + .unwrap() + .unwrap() + .unwrap(); + println!("echo back: {:?}", ret); + assert_eq!(ret, Bytes::from("abc")); + + close_tunnel(&tunnel).await.unwrap(); + + if connector.remote_url().scheme() == "udp" { + lis.abort(); + } else { + // lis should finish in 1 second + let ret = tokio::time::timeout(tokio::time::Duration::from_secs(1), lis).await; + assert!(ret.is_ok()); + } + } + + pub(crate) async fn _tunnel_bench(mut listener: L, mut connector: C) + where + L: TunnelListener + Send + Sync + 'static, + C: TunnelConnector + Send + Sync + 'static, + { + listener.listen().await.unwrap(); + + let lis = tokio::spawn(async move { + let ret = listener.accept().await.unwrap(); + _tunnel_echo_server(ret, false).await + }); + + let tunnel = connector.connect().await.unwrap(); + + let mut send = tunnel.pin_sink(); + let mut recv = tunnel.pin_stream(); + + // prepare a 4k buffer with random data + let mut send_buf = BytesMut::new(); + for _ in 0..64 { + send_buf.put_i128(rand::random::()); + } + + let now = Instant::now(); + let mut count = 0; + while now.elapsed().as_secs() < 3 { + send.send(send_buf.clone().freeze()).await.unwrap(); + let _ = recv.next().await.unwrap().unwrap(); + count += 1; + } + println!("bps: {}", (count / 1024) * 4 / now.elapsed().as_secs()); + + lis.abort(); + } +} diff --git a/easytier-core/src/tunnels/mod.rs b/easytier-core/src/tunnels/mod.rs new file mode 100644 index 0000000..2f4db32 --- /dev/null +++ b/easytier-core/src/tunnels/mod.rs @@ -0,0 +1,159 @@ +pub mod codec; +pub mod common; +pub mod ring_tunnel; +pub mod stats; +pub mod tcp_tunnel; +pub mod tunnel_filter; +pub mod udp_tunnel; + +use std::{fmt::Debug, net::SocketAddr, pin::Pin, sync::Arc}; + +use async_trait::async_trait; +use easytier_rpc::TunnelInfo; +use futures::{Sink, SinkExt, Stream}; + +use thiserror::Error; +use tokio_util::bytes::{Bytes, BytesMut}; + +#[derive(Error, Debug)] +pub enum TunnelError { + #[error("Error: {0}")] + CommonError(String), + #[error("io error")] + IOError(#[from] std::io::Error), + #[error("wait resp error")] + WaitRespError(String), + #[error("Connect Error: {0}")] + ConnectError(String), + #[error("Invalid Protocol: {0}")] + InvalidProtocol(String), + #[error("Invalid Addr: {0}")] + InvalidAddr(String), + #[error("Tun Error: {0}")] + TunError(String), + #[error("timeout")] + Timeout(#[from] tokio::time::error::Elapsed), +} + +pub type StreamT = BytesMut; +pub type StreamItem = Result; +pub type SinkItem = Bytes; +pub type SinkError = TunnelError; + +pub trait DatagramStream: Stream + Send + Sync {} +impl DatagramStream for T where T: Stream + Send + Sync {} +pub trait DatagramSink: Sink + Send + Sync {} +impl DatagramSink for T where T: Sink + Send + Sync {} + +#[auto_impl::auto_impl(Box, Arc)] +pub trait Tunnel: Send + Sync { + fn stream(&self) -> Box; + fn sink(&self) -> Box; + + fn pin_stream(&self) -> Pin> { + Box::into_pin(self.stream()) + } + + fn pin_sink(&self) -> Pin> { + Box::into_pin(self.sink()) + } + + fn info(&self) -> Option; +} + +pub async fn close_tunnel(t: &Box) -> Result<(), TunnelError> { + t.pin_sink().close().await +} + +#[auto_impl::auto_impl(Arc)] +pub trait TunnelConnCounter: 'static + Send + Sync + Debug { + fn get(&self) -> u32; +} + +#[async_trait] +#[auto_impl::auto_impl(Box)] +pub trait TunnelListener: Send + Sync { + async fn listen(&mut self) -> Result<(), TunnelError>; + async fn accept(&mut self) -> Result, TunnelError>; + fn local_url(&self) -> url::Url; + fn get_conn_counter(&self) -> Arc> { + #[derive(Debug)] + struct FakeTunnelConnCounter {} + impl TunnelConnCounter for FakeTunnelConnCounter { + fn get(&self) -> u32 { + 0 + } + } + Arc::new(Box::new(FakeTunnelConnCounter {})) + } +} + +#[async_trait] +#[auto_impl::auto_impl(Box)] +pub trait TunnelConnector { + async fn connect(&mut self) -> Result, TunnelError>; + fn remote_url(&self) -> url::Url; + fn set_bind_addrs(&mut self, _addrs: Vec) {} +} + +pub fn build_url_from_socket_addr(addr: &String, scheme: &str) -> url::Url { + url::Url::parse(format!("{}://{}", scheme, addr).as_str()).unwrap() +} + +impl std::fmt::Debug for dyn Tunnel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tunnel") + .field("info", &self.info()) + .finish() + } +} + +impl std::fmt::Debug for dyn TunnelConnector + Sync + Send { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TunnelConnector") + .field("remote_url", &self.remote_url()) + .finish() + } +} + +impl std::fmt::Debug for dyn TunnelListener { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TunnelListener") + .field("local_url", &self.local_url()) + .finish() + } +} + +pub(crate) trait FromUrl { + fn from_url(url: url::Url) -> Result + where + Self: Sized; +} + +pub(crate) fn check_scheme_and_get_socket_addr( + url: &url::Url, + scheme: &str, +) -> Result +where + T: FromUrl, +{ + if url.scheme() != scheme { + return Err(TunnelError::InvalidProtocol(url.scheme().to_string())); + } + + Ok(T::from_url(url.clone())?) +} + +impl FromUrl for SocketAddr { + fn from_url(url: url::Url) -> Result { + Ok(url.socket_addrs(|| None)?.pop().unwrap()) + } +} + +impl FromUrl for uuid::Uuid { + fn from_url(url: url::Url) -> Result { + let o = url.host_str().unwrap(); + let o = uuid::Uuid::parse_str(o).map_err(|e| TunnelError::InvalidAddr(e.to_string()))?; + Ok(o) + } +} diff --git a/easytier-core/src/tunnels/ring_tunnel.rs b/easytier-core/src/tunnels/ring_tunnel.rs new file mode 100644 index 0000000..b26f1b3 --- /dev/null +++ b/easytier-core/src/tunnels/ring_tunnel.rs @@ -0,0 +1,391 @@ +use std::{ + collections::HashMap, + sync::{atomic::AtomicBool, Arc}, + task::Poll, +}; + +use async_stream::stream; +use crossbeam_queue::ArrayQueue; + +use async_trait::async_trait; +use futures::Sink; +use once_cell::sync::Lazy; +use tokio::sync::{Mutex, Notify}; + +use futures::FutureExt; +use tokio_util::bytes::BytesMut; +use uuid::Uuid; + +use crate::tunnels::{SinkError, SinkItem}; + +use super::{ + build_url_from_socket_addr, check_scheme_and_get_socket_addr, DatagramSink, DatagramStream, + Tunnel, TunnelConnector, TunnelError, TunnelInfo, TunnelListener, +}; + +static RING_TUNNEL_CAP: usize = 1000; + +pub struct RingTunnel { + id: Uuid, + ring: Arc>, + consume_notify: Arc, + produce_notify: Arc, + closed: Arc, +} + +impl RingTunnel { + pub fn new(cap: usize) -> Self { + RingTunnel { + id: Uuid::new_v4(), + ring: Arc::new(ArrayQueue::new(cap)), + consume_notify: Arc::new(Notify::new()), + produce_notify: Arc::new(Notify::new()), + closed: Arc::new(AtomicBool::new(false)), + } + } + + pub fn new_with_id(id: Uuid, cap: usize) -> Self { + let mut ret = Self::new(cap); + ret.id = id; + ret + } + + fn recv_stream(&self) -> impl DatagramStream { + let ring = self.ring.clone(); + let produce_notify = self.produce_notify.clone(); + let consume_notify = self.consume_notify.clone(); + let closed = self.closed.clone(); + let id = self.id; + stream! { + loop { + if closed.load(std::sync::atomic::Ordering::Relaxed) { + log::warn!("ring recv tunnel {:?} closed", id); + yield Err(TunnelError::CommonError("Closed".to_owned())); + } + match ring.pop() { + Some(v) => { + let mut out = BytesMut::new(); + out.extend_from_slice(&v); + consume_notify.notify_one(); + log::trace!("id: {}, recv buffer, len: {:?}, buf: {:?}", id, v.len(), &v); + yield Ok(out); + }, + None => { + log::trace!("waiting recv buffer, id: {}", id); + produce_notify.notified().await; + } + } + } + } + } + + fn send_sink(&self) -> impl DatagramSink { + let ring = self.ring.clone(); + let produce_notify = self.produce_notify.clone(); + let consume_notify = self.consume_notify.clone(); + let closed = self.closed.clone(); + let id = self.id; + + // type T = RingTunnel; + + use tokio::task::JoinHandle; + + struct T { + ring: RingTunnel, + wait_consume_task: Option>, + } + + impl T { + fn wait_ring_consume( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + expected_size: usize, + ) -> std::task::Poll<()> { + let self_mut = self.get_mut(); + if self_mut.ring.ring.len() <= expected_size { + return Poll::Ready(()); + } + if self_mut.wait_consume_task.is_none() { + let id = self_mut.ring.id; + let consume_notify = self_mut.ring.consume_notify.clone(); + let ring = self_mut.ring.ring.clone(); + let task = async move { + log::trace!( + "waiting ring consume done, expected_size: {}, id: {}", + expected_size, + id + ); + while ring.len() > expected_size { + consume_notify.notified().await; + } + log::trace!( + "ring consume done, expected_size: {}, id: {}", + expected_size, + id + ); + }; + self_mut.wait_consume_task = Some(tokio::spawn(task)); + } + let task = self_mut.wait_consume_task.as_mut().unwrap(); + match task.poll_unpin(cx) { + Poll::Ready(_) => { + self_mut.wait_consume_task = None; + Poll::Ready(()) + } + Poll::Pending => Poll::Pending, + } + } + } + + impl Sink for T { + type Error = SinkError; + + fn poll_ready( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let expected_size = self.ring.ring.capacity() - 1; + match self.wait_ring_consume(cx, expected_size) { + Poll::Ready(_) => Poll::Ready(Ok(())), + Poll::Pending => Poll::Pending, + } + } + + fn start_send( + self: std::pin::Pin<&mut Self>, + item: SinkItem, + ) -> Result<(), Self::Error> { + log::trace!("id: {}, send buffer, buf: {:?}", self.ring.id, &item); + self.ring.ring.push(item).unwrap(); + self.ring.produce_notify.notify_one(); + Ok(()) + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.ring + .closed + .store(true, std::sync::atomic::Ordering::Relaxed); + log::warn!("ring tunnel send {:?} closed", self.ring.id); + self.ring.produce_notify.notify_one(); + Poll::Ready(Ok(())) + } + } + + T { + ring: RingTunnel { + id, + ring, + consume_notify, + produce_notify, + closed, + }, + wait_consume_task: None, + } + } +} + +struct Connection { + client: RingTunnel, + server: RingTunnel, + connect_notify: Arc, +} + +impl Tunnel for RingTunnel { + fn stream(&self) -> Box { + Box::new(self.recv_stream()) + } + + fn sink(&self) -> Box { + Box::new(self.send_sink()) + } + + fn info(&self) -> Option { + None + // Some(TunnelInfo { + // tunnel_type: "ring".to_owned(), + // local_addr: format!("ring://{}", self.id), + // remote_addr: format!("ring://{}", self.id), + // }) + } +} + +static CONNECTION_MAP: Lazy>>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +#[derive(Debug)] +pub struct RingTunnelListener { + listerner_addr: url::Url, +} + +impl RingTunnelListener { + pub fn new(key: url::Url) -> Self { + RingTunnelListener { + listerner_addr: key, + } + } +} +struct ConnectionForServer { + conn: Arc, +} + +impl Tunnel for ConnectionForServer { + fn stream(&self) -> Box { + Box::new(self.conn.server.recv_stream()) + } + + fn sink(&self) -> Box { + Box::new(self.conn.client.send_sink()) + } + + fn info(&self) -> Option { + Some(TunnelInfo { + tunnel_type: "ring".to_owned(), + local_addr: build_url_from_socket_addr(&self.conn.server.id.into(), "ring").into(), + remote_addr: build_url_from_socket_addr(&self.conn.client.id.into(), "ring").into(), + }) + } +} + +struct ConnectionForClient { + conn: Arc, +} + +impl Tunnel for ConnectionForClient { + fn stream(&self) -> Box { + Box::new(self.conn.client.recv_stream()) + } + + fn sink(&self) -> Box { + Box::new(self.conn.server.send_sink()) + } + + fn info(&self) -> Option { + Some(TunnelInfo { + tunnel_type: "ring".to_owned(), + local_addr: build_url_from_socket_addr(&self.conn.client.id.into(), "ring").into(), + remote_addr: build_url_from_socket_addr(&self.conn.server.id.into(), "ring").into(), + }) + } +} + +impl RingTunnelListener { + async fn add_connection(listener_addr: uuid::Uuid) { + CONNECTION_MAP.lock().await.insert( + listener_addr.clone(), + Arc::new(Connection { + client: RingTunnel::new(RING_TUNNEL_CAP), + server: RingTunnel::new_with_id(listener_addr.clone(), RING_TUNNEL_CAP), + connect_notify: Arc::new(Notify::new()), + }), + ); + } + + fn get_addr(&self) -> Result { + check_scheme_and_get_socket_addr::(&self.listerner_addr, "ring") + } +} + +#[async_trait] +impl TunnelListener for RingTunnelListener { + async fn listen(&mut self) -> Result<(), TunnelError> { + log::info!("listen new conn of key: {}", self.listerner_addr); + Self::add_connection(self.get_addr()?).await; + Ok(()) + } + + async fn accept(&mut self) -> Result, TunnelError> { + log::info!("waiting accept new conn of key: {}", self.listerner_addr); + let val = CONNECTION_MAP + .lock() + .await + .get(&self.get_addr()?) + .unwrap() + .clone(); + val.connect_notify.notified().await; + CONNECTION_MAP.lock().await.remove(&self.get_addr()?); + Self::add_connection(self.get_addr()?).await; + log::info!("accept new conn of key: {}", self.listerner_addr); + Ok(Box::new(ConnectionForServer { conn: val })) + } + + fn local_url(&self) -> url::Url { + self.listerner_addr.clone() + } +} + +pub struct RingTunnelConnector { + remote_addr: url::Url, +} + +impl RingTunnelConnector { + pub fn new(remote_addr: url::Url) -> Self { + RingTunnelConnector { remote_addr } + } +} + +#[async_trait] +impl TunnelConnector for RingTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + let val = CONNECTION_MAP + .lock() + .await + .get(&check_scheme_and_get_socket_addr::( + &self.remote_addr, + "ring", + )?) + .unwrap() + .clone(); + val.connect_notify.notify_one(); + log::info!("connecting"); + Ok(Box::new(ConnectionForClient { conn: val })) + } + + fn remote_url(&self) -> url::Url { + self.remote_addr.clone() + } +} + +pub fn create_ring_tunnel_pair() -> (Box, Box) { + let conn = Arc::new(Connection { + client: RingTunnel::new(RING_TUNNEL_CAP), + server: RingTunnel::new(RING_TUNNEL_CAP), + connect_notify: Arc::new(Notify::new()), + }); + ( + Box::new(ConnectionForServer { conn: conn.clone() }), + Box::new(ConnectionForClient { conn: conn }), + ) +} + +#[cfg(test)] +mod tests { + use crate::tunnels::common::tests::{_tunnel_bench, _tunnel_pingpong}; + + use super::*; + + #[tokio::test] + async fn ring_pingpong() { + let id: url::Url = format!("ring://{}", Uuid::new_v4()).parse().unwrap(); + let listener = RingTunnelListener::new(id.clone()); + let connector = RingTunnelConnector::new(id.clone()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn ring_bench() { + let id: url::Url = format!("ring://{}", Uuid::new_v4()).parse().unwrap(); + let listener = RingTunnelListener::new(id.clone()); + let connector = RingTunnelConnector::new(id); + _tunnel_bench(listener, connector).await + } +} diff --git a/easytier-core/src/tunnels/stats.rs b/easytier-core/src/tunnels/stats.rs new file mode 100644 index 0000000..8937f49 --- /dev/null +++ b/easytier-core/src/tunnels/stats.rs @@ -0,0 +1,101 @@ +use std::sync::atomic::{AtomicU32, AtomicU64}; + +pub struct WindowLatency { + latency_us_window: Vec, + latency_us_window_index: AtomicU32, + latency_us_window_size: AtomicU32, +} + +impl WindowLatency { + pub fn new(window_size: u32) -> Self { + Self { + latency_us_window: (0..window_size).map(|_| AtomicU64::new(0)).collect(), + latency_us_window_index: AtomicU32::new(0), + latency_us_window_size: AtomicU32::new(window_size), + } + } + + pub fn record_latency(&self, latency_us: u64) { + let index = self + .latency_us_window_index + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let index = index + % self + .latency_us_window_size + .load(std::sync::atomic::Ordering::Relaxed); + self.latency_us_window[index as usize] + .store(latency_us, std::sync::atomic::Ordering::Relaxed); + } + + pub fn get_latency_us(&self) -> u64 { + let window_size = self + .latency_us_window_size + .load(std::sync::atomic::Ordering::Relaxed); + let mut sum = 0; + let mut count = 0; + for i in 0..window_size { + let latency_us = + self.latency_us_window[i as usize].load(std::sync::atomic::Ordering::Relaxed); + if latency_us > 0 { + sum += latency_us; + count += 1; + } + } + + if count == 0 { + 0 + } else { + sum / count + } + } +} + +pub struct Throughput { + tx_bytes: AtomicU64, + rx_bytes: AtomicU64, + + tx_packets: AtomicU64, + rx_packets: AtomicU64, +} + +impl Throughput { + pub fn new() -> Self { + Self { + tx_bytes: AtomicU64::new(0), + rx_bytes: AtomicU64::new(0), + + tx_packets: AtomicU64::new(0), + rx_packets: AtomicU64::new(0), + } + } + + pub fn tx_bytes(&self) -> u64 { + self.tx_bytes.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn rx_bytes(&self) -> u64 { + self.rx_bytes.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn tx_packets(&self) -> u64 { + self.tx_packets.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn rx_packets(&self) -> u64 { + self.rx_packets.load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn record_tx_bytes(&self, bytes: u64) { + self.tx_bytes + .fetch_add(bytes, std::sync::atomic::Ordering::Relaxed); + self.tx_packets + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + pub fn record_rx_bytes(&self, bytes: u64) { + self.rx_bytes + .fetch_add(bytes, std::sync::atomic::Ordering::Relaxed); + self.rx_packets + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } +} diff --git a/easytier-core/src/tunnels/tcp_tunnel.rs b/easytier-core/src/tunnels/tcp_tunnel.rs new file mode 100644 index 0000000..49ff70d --- /dev/null +++ b/easytier-core/src/tunnels/tcp_tunnel.rs @@ -0,0 +1,284 @@ +use std::net::SocketAddr; + +use async_trait::async_trait; +use futures::{stream::FuturesUnordered, StreamExt}; +use tokio::net::{TcpListener, TcpSocket, TcpStream}; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +use super::{ + check_scheme_and_get_socket_addr, common::FramedTunnel, Tunnel, TunnelInfo, TunnelListener, +}; + +#[derive(Debug)] +pub struct TcpTunnelListener { + addr: url::Url, + listener: Option, +} + +impl TcpTunnelListener { + pub fn new(addr: url::Url) -> Self { + TcpTunnelListener { + addr, + listener: None, + } + } +} + +#[async_trait] +impl TunnelListener for TcpTunnelListener { + async fn listen(&mut self) -> Result<(), super::TunnelError> { + let addr = check_scheme_and_get_socket_addr::(&self.addr, "tcp")?; + + let socket = if addr.is_ipv4() { + TcpSocket::new_v4()? + } else { + TcpSocket::new_v6()? + }; + + socket.set_reuseaddr(true)?; + #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] + socket.set_reuseport(true)?; + socket.bind(addr)?; + + self.listener = Some(socket.listen(1024)?); + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + let listener = self.listener.as_ref().unwrap(); + let (stream, _) = listener.accept().await?; + stream.set_nodelay(true).unwrap(); + let info = TunnelInfo { + tunnel_type: "tcp".to_owned(), + local_addr: self.local_url().into(), + remote_addr: super::build_url_from_socket_addr(&stream.peer_addr()?.to_string(), "tcp") + .into(), + }; + + let (r, w) = tokio::io::split(stream); + Ok(FramedTunnel::new_tunnel_with_info( + FramedRead::new(r, LengthDelimitedCodec::new()), + FramedWrite::new(w, LengthDelimitedCodec::new()), + info, + )) + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } +} + +fn get_tunnel_with_tcp_stream( + stream: TcpStream, + remote_url: url::Url, +) -> Result, super::TunnelError> { + stream.set_nodelay(true).unwrap(); + + let info = TunnelInfo { + tunnel_type: "tcp".to_owned(), + local_addr: super::build_url_from_socket_addr(&stream.local_addr()?.to_string(), "tcp") + .into(), + remote_addr: remote_url.into(), + }; + + let (r, w) = tokio::io::split(stream); + Ok(Box::new(FramedTunnel::new_tunnel_with_info( + FramedRead::new(r, LengthDelimitedCodec::new()), + FramedWrite::new(w, LengthDelimitedCodec::new()), + info, + ))) +} + +#[derive(Debug)] +pub struct TcpTunnelConnector { + addr: url::Url, + + bind_addrs: Vec, +} + +impl TcpTunnelConnector { + pub fn new(addr: url::Url) -> Self { + TcpTunnelConnector { + addr, + bind_addrs: vec![], + } + } + + async fn connect_with_default_bind(&mut self) -> Result, super::TunnelError> { + tracing::info!(addr = ?self.addr, "connect tcp start"); + let addr = check_scheme_and_get_socket_addr::(&self.addr, "tcp")?; + let stream = TcpStream::connect(addr).await?; + tracing::info!(addr = ?self.addr, "connect tcp succ"); + return get_tunnel_with_tcp_stream(stream, self.addr.clone().into()); + } + + async fn connect_with_custom_bind( + &mut self, + is_ipv4: bool, + ) -> Result, super::TunnelError> { + let mut futures = FuturesUnordered::new(); + let dst_addr = check_scheme_and_get_socket_addr::(&self.addr, "tcp")?; + + for bind_addr in self.bind_addrs.iter() { + let socket = if is_ipv4 { + TcpSocket::new_v4()? + } else { + TcpSocket::new_v6()? + }; + socket.set_reuseaddr(true)?; + + #[cfg(all(unix, not(target_os = "solaris"), not(target_os = "illumos")))] + socket.set_reuseport(true)?; + + socket.bind(*bind_addr)?; + // linux does not use interface of bind_addr to send packet, so we need to bind device + // mac can handle this with bind correctly + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + if let Some(dev_name) = super::common::get_interface_name_by_ip(&bind_addr.ip()) { + tracing::trace!(dev_name = ?dev_name, "bind device"); + socket.bind_device(Some(dev_name.as_bytes()))?; + } + futures.push(socket.connect(dst_addr.clone())); + } + + let Some(ret) = futures.next().await else { + return Err(super::TunnelError::CommonError( + "join connect futures failed".to_owned(), + )); + }; + + return get_tunnel_with_tcp_stream(ret?, self.addr.clone().into()); + } +} + +#[async_trait] +impl super::TunnelConnector for TcpTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + if self.bind_addrs.is_empty() { + self.connect_with_default_bind().await + } else if self.bind_addrs[0].is_ipv4() { + self.connect_with_custom_bind(true).await + } else { + self.connect_with_custom_bind(false).await + } + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } +} + +#[cfg(test)] +mod tests { + use futures::SinkExt; + + use crate::tunnels::{ + common::tests::{_tunnel_bench, _tunnel_pingpong}, + TunnelConnector, + }; + + use super::*; + + #[tokio::test] + async fn tcp_pingpong() { + let listener = TcpTunnelListener::new("tcp://0.0.0.0:11011".parse().unwrap()); + let connector = TcpTunnelConnector::new("tcp://127.0.0.1:11011".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn tcp_bench() { + let listener = TcpTunnelListener::new("tcp://0.0.0.0:11012".parse().unwrap()); + let connector = TcpTunnelConnector::new("tcp://127.0.0.1:11012".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn tcp_bench_with_bind() { + let listener = TcpTunnelListener::new("tcp://127.0.0.1:11013".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11013".parse().unwrap()); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + #[should_panic] + async fn tcp_bench_with_bind_fail() { + let listener = TcpTunnelListener::new("tcp://127.0.0.1:11014".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11014".parse().unwrap()); + connector.set_bind_addrs(vec!["10.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + // test slow send lock in framed tunnel + #[tokio::test] + async fn tcp_multiple_sender_and_slow_receiver() { + // console_subscriber::init(); + let mut listener = TcpTunnelListener::new("tcp://127.0.0.1:11014".parse().unwrap()); + let mut connector = TcpTunnelConnector::new("tcp://127.0.0.1:11014".parse().unwrap()); + + listener.listen().await.unwrap(); + let t1 = tokio::spawn(async move { + let t = listener.accept().await.unwrap(); + let mut stream = t.pin_stream(); + + let now = tokio::time::Instant::now(); + + while let Some(Ok(_)) = stream.next().await { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + if now.elapsed().as_secs() > 5 { + break; + } + } + + tracing::info!("t1 exit"); + }); + + let tunnel = connector.connect().await.unwrap(); + let mut sink1 = tunnel.pin_sink(); + let t2 = tokio::spawn(async move { + for i in 0..1000000 { + let a = sink1.send(b"hello".to_vec().into()).await; + if a.is_err() { + tracing::info!(?a, "t2 exit with err"); + break; + } + + if i % 5000 == 0 { + tracing::info!(i, "send2 1000"); + } + } + + tracing::info!("t2 exit"); + }); + + let mut sink2 = tunnel.pin_sink(); + let t3 = tokio::spawn(async move { + for i in 0..1000000 { + let a = sink2.send(b"hello".to_vec().into()).await; + if a.is_err() { + tracing::info!(?a, "t3 exit with err"); + break; + } + + if i % 5000 == 0 { + tracing::info!(i, "send2 1000"); + } + } + + tracing::info!("t3 exit"); + }); + + let t4 = tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + tracing::info!("closing"); + let close_ret = tunnel.pin_sink().close().await; + tracing::info!("closed {:?}", close_ret); + }); + + let _ = tokio::join!(t1, t2, t3, t4); + } +} diff --git a/easytier-core/src/tunnels/tunnel_filter.rs b/easytier-core/src/tunnels/tunnel_filter.rs new file mode 100644 index 0000000..05d57ce --- /dev/null +++ b/easytier-core/src/tunnels/tunnel_filter.rs @@ -0,0 +1,228 @@ +use std::{ + sync::Arc, + task::{Context, Poll}, +}; + +use easytier_rpc::TunnelInfo; +use futures::{Sink, SinkExt, Stream, StreamExt}; + +use self::stats::Throughput; + +use super::*; +use crate::tunnels::{DatagramSink, DatagramStream, SinkError, SinkItem, StreamItem, Tunnel}; + +pub trait TunnelFilter { + fn before_send(&self, data: SinkItem) -> Result; + fn after_received(&self, data: StreamItem) -> Result; +} + +pub struct TunnelWithFilter { + inner: T, + filter: Arc, +} + +impl Tunnel for TunnelWithFilter +where + T: Tunnel + Send + Sync + 'static, + F: TunnelFilter + Send + Sync + 'static, +{ + fn sink(&self) -> Box { + struct SinkWrapper { + sink: Pin>, + filter: Arc, + } + impl Sink for SinkWrapper + where + F: TunnelFilter + Send + Sync + 'static, + { + type Error = SinkError; + + fn poll_ready( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_ready_unpin(cx) + } + + fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> { + let item = self.filter.before_send(item)?; + self.get_mut().sink.start_send_unpin(item) + } + + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_flush_unpin(cx) + } + + fn poll_close( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.get_mut().sink.poll_close_unpin(cx) + } + } + + Box::new(SinkWrapper { + sink: self.inner.pin_sink(), + filter: self.filter.clone(), + }) + } + + fn stream(&self) -> Box { + struct StreamWrapper { + stream: Pin>, + filter: Arc, + } + impl Stream for StreamWrapper + where + F: TunnelFilter + Send + Sync + 'static, + { + type Item = StreamItem; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let self_mut = self.get_mut(); + match self_mut.stream.poll_next_unpin(cx) { + Poll::Ready(Some(ret)) => { + Poll::Ready(Some(self_mut.filter.after_received(ret))) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } + } + + Box::new(StreamWrapper { + stream: self.inner.pin_stream(), + filter: self.filter.clone(), + }) + } + + fn info(&self) -> Option { + self.inner.info() + } +} + +impl TunnelWithFilter +where + T: Tunnel + Send + Sync + 'static, + F: TunnelFilter + Send + Sync + 'static, +{ + pub fn new(inner: T, filter: Arc) -> Self { + Self { inner, filter } + } +} + +pub struct PacketRecorderTunnelFilter { + pub received: Arc>>, + pub sent: Arc>>, +} + +impl TunnelFilter for PacketRecorderTunnelFilter { + fn before_send(&self, data: SinkItem) -> Result { + self.received.lock().unwrap().push(data.clone()); + Ok(data) + } + + fn after_received(&self, data: StreamItem) -> Result { + match data { + Ok(v) => { + self.sent.lock().unwrap().push(v.clone().into()); + Ok(v) + } + Err(e) => Err(e), + } + } +} + +impl PacketRecorderTunnelFilter { + pub fn new() -> Self { + Self { + received: Arc::new(std::sync::Mutex::new(Vec::new())), + sent: Arc::new(std::sync::Mutex::new(Vec::new())), + } + } +} + +pub struct StatsRecorderTunnelFilter { + throughput: Arc, +} + +impl TunnelFilter for StatsRecorderTunnelFilter { + fn before_send(&self, data: SinkItem) -> Result { + self.throughput.record_tx_bytes(data.len() as u64); + Ok(data) + } + + fn after_received(&self, data: StreamItem) -> Result { + match data { + Ok(v) => { + self.throughput.record_rx_bytes(v.len() as u64); + Ok(v) + } + Err(e) => Err(e), + } + } +} + +impl StatsRecorderTunnelFilter { + pub fn new() -> Self { + Self { + throughput: Arc::new(Throughput::new()), + } + } + + pub fn get_throughput(&self) -> Arc { + self.throughput.clone() + } +} + +#[macro_export] +macro_rules! define_tunnel_filter_chain { + ($type_name:ident $(, $field_name:ident = $filter_type:ty)+) => ( + pub struct $type_name { + $($field_name: std::sync::Arc<$filter_type>,)+ + } + + impl $type_name { + pub fn new() -> Self { + Self { + $($field_name: std::sync::Arc::new(<$filter_type>::new()),)+ + } + } + + pub fn wrap_tunnel(&self, tunnel: impl Tunnel + 'static) -> impl Tunnel { + $( + let tunnel = crate::tunnels::tunnel_filter::TunnelWithFilter::new(tunnel, self.$field_name.clone()); + )+ + tunnel + } + } + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tunnels::ring_tunnel::RingTunnel; + #[tokio::test] + async fn test_nested_filter() { + define_tunnel_filter_chain!( + Filter, + a = PacketRecorderTunnelFilter, + b = PacketRecorderTunnelFilter, + c = PacketRecorderTunnelFilter + ); + + let filter = Filter::new(); + let tunnel = filter.wrap_tunnel(RingTunnel::new(1)); + + let mut s = tunnel.pin_sink(); + s.send(Bytes::from("hello")).await.unwrap(); + + assert_eq!(1, filter.a.received.lock().unwrap().len()); + assert_eq!(1, filter.b.received.lock().unwrap().len()); + assert_eq!(1, filter.c.received.lock().unwrap().len()); + } +} diff --git a/easytier-core/src/tunnels/udp_tunnel.rs b/easytier-core/src/tunnels/udp_tunnel.rs new file mode 100644 index 0000000..1e96b01 --- /dev/null +++ b/easytier-core/src/tunnels/udp_tunnel.rs @@ -0,0 +1,574 @@ +use std::{fmt::Debug, pin::Pin, sync::Arc}; + +use async_trait::async_trait; +use dashmap::DashMap; +use easytier_rpc::TunnelInfo; +use futures::{stream::FuturesUnordered, SinkExt, StreamExt}; +use rkyv::{Archive, Deserialize, Serialize}; +use std::net::SocketAddr; +use tokio::{net::UdpSocket, sync::Mutex, task::JoinSet}; +use tokio_util::{ + bytes::{Buf, Bytes, BytesMut}, + udp::UdpFramed, +}; +use tracing::Instrument; + +use crate::{ + common::rkyv_util::{self, encode_to_bytes}, + tunnels::{build_url_from_socket_addr, close_tunnel, TunnelConnCounter, TunnelConnector}, +}; + +use super::{ + codec::BytesCodec, + common::{FramedTunnel, TunnelWithCustomInfo}, + ring_tunnel::create_ring_tunnel_pair, + DatagramSink, DatagramStream, Tunnel, TunnelListener, +}; + +pub const UDP_DATA_MTU: usize = 2500; + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +// Derives can be passed through to the generated type: +#[archive_attr(derive(Debug))] +pub enum UdpPacketPayload { + Syn, + Sack, + HolePunch(Vec), + Data(Vec), +} + +#[derive(Archive, Deserialize, Serialize, Debug)] +#[archive(compare(PartialEq), check_bytes)] +#[archive_attr(derive(Debug))] +pub struct UdpPacket { + pub conn_id: u32, + pub payload: UdpPacketPayload, +} + +impl UdpPacket { + pub fn new_data_packet(conn_id: u32, data: Vec) -> Self { + Self { + conn_id, + payload: UdpPacketPayload::Data(data), + } + } + + pub fn new_hole_punch_packet(data: Vec) -> Self { + Self { + conn_id: 0, + payload: UdpPacketPayload::HolePunch(data), + } + } + + pub fn new_syn_packet(conn_id: u32) -> Self { + Self { + conn_id, + payload: UdpPacketPayload::Syn, + } + } + + pub fn new_sack_packet(conn_id: u32) -> Self { + Self { + conn_id, + payload: UdpPacketPayload::Sack, + } + } +} + +fn try_get_data_payload(mut buf: BytesMut, conn_id: u32) -> Option { + let Ok(udp_packet) = rkyv_util::decode_from_bytes_checked::(&buf) else { + tracing::warn!(?buf, "udp decode error"); + return None; + }; + + if udp_packet.conn_id != conn_id.clone() { + tracing::warn!(?udp_packet, ?conn_id, "udp conn id not match"); + return None; + } + + let ArchivedUdpPacketPayload::Data(payload) = &udp_packet.payload else { + tracing::warn!(?udp_packet, "udp payload not data"); + return None; + }; + + let ptr_range = payload.as_ptr_range(); + let offset = ptr_range.start as usize - buf.as_ptr() as usize; + let len = ptr_range.end as usize - ptr_range.start as usize; + buf.advance(offset); + buf.truncate(len); + tracing::trace!(?offset, ?len, ?buf, "udp payload data"); + + Some(buf) +} + +fn get_tunnel_from_socket( + socket: Arc, + addr: SocketAddr, + conn_id: u32, +) -> Box { + let udp = UdpFramed::new(socket.clone(), BytesCodec::new(UDP_DATA_MTU)); + let (sink, stream) = udp.split(); + + let recv_addr = addr; + let stream = stream.filter_map(move |v| async move { + tracing::trace!(?v, "udp stream recv something"); + if v.is_err() { + tracing::warn!(?v, "udp stream error"); + return Some(Err(super::TunnelError::CommonError( + "udp stream error".to_owned(), + ))); + } + + let (buf, addr) = v.unwrap(); + assert_eq!(addr, recv_addr.clone()); + Some(Ok(try_get_data_payload(buf, conn_id.clone())?)) + }); + let stream = Box::pin(stream); + + let sender_addr = addr; + let sink = Box::pin(sink.with(move |v: Bytes| async move { + if false { + return Err(super::TunnelError::CommonError("udp sink error".to_owned())); + } + + // TODO: two copy here, how to avoid? + let udp_packet = UdpPacket::new_data_packet(conn_id, v.to_vec()); + tracing::trace!(?udp_packet, ?v, "udp send packet"); + let v = encode_to_bytes::<_, UDP_DATA_MTU>(&udp_packet); + + Ok((v, sender_addr)) + })); + + FramedTunnel::new_tunnel_with_info( + stream, + sink, + // TODO: this remote addr is not a url + super::TunnelInfo { + tunnel_type: "udp".to_owned(), + local_addr: super::build_url_from_socket_addr( + &socket.local_addr().unwrap().to_string(), + "udp", + ) + .into(), + remote_addr: super::build_url_from_socket_addr(&addr.to_string(), "udp").into(), + }, + ) +} + +struct StreamSinkPair( + Pin>, + Pin>, + u32, +); +type ArcStreamSinkPair = Arc>; + +pub struct UdpTunnelListener { + addr: url::Url, + socket: Option>, + + sock_map: Arc>, + forward_tasks: Arc>>, + + conn_recv: tokio::sync::mpsc::Receiver>, + conn_send: Option>>, +} + +impl UdpTunnelListener { + pub fn new(addr: url::Url) -> Self { + let (conn_send, conn_recv) = tokio::sync::mpsc::channel(100); + Self { + addr, + socket: None, + sock_map: Arc::new(DashMap::new()), + forward_tasks: Arc::new(Mutex::new(JoinSet::new())), + conn_recv, + conn_send: Some(conn_send), + } + } + + async fn try_forward_packet( + sock_map: &DashMap, + buf: BytesMut, + addr: SocketAddr, + ) -> Result<(), super::TunnelError> { + let entry = sock_map.get_mut(&addr); + if entry.is_none() { + log::warn!("udp forward packet: {:?}, {:?}, no entry", addr, buf); + return Ok(()); + } + + log::trace!("udp forward packet: {:?}, {:?}", addr, buf); + let entry = entry.unwrap(); + let pair = entry.value().clone(); + drop(entry); + + let Some(buf) = try_get_data_payload(buf, pair.lock().await.2) else { + return Ok(()); + }; + pair.lock().await.1.send(buf.freeze()).await?; + Ok(()) + } + + async fn handle_connect( + socket: Arc, + addr: SocketAddr, + forward_tasks: Arc>>, + sock_map: Arc>, + local_url: url::Url, + conn_id: u32, + ) -> Result, super::TunnelError> { + tracing::info!(?conn_id, ?addr, "udp connection accept handling",); + + let udp_packet = UdpPacket::new_sack_packet(conn_id); + let sack_buf = encode_to_bytes::<_, UDP_DATA_MTU>(&udp_packet); + socket.send_to(&sack_buf, addr).await?; + + let (ctunnel, stunnel) = create_ring_tunnel_pair(); + let udp_tunnel = get_tunnel_from_socket(socket.clone(), addr, conn_id); + let ss_pair = StreamSinkPair(ctunnel.pin_stream(), ctunnel.pin_sink(), conn_id); + let addr_copy = addr.clone(); + sock_map.insert(addr, Arc::new(Mutex::new(ss_pair))); + let ctunnel_stream = ctunnel.pin_stream(); + forward_tasks.lock().await.spawn(async move { + let ret = ctunnel_stream + .map(|v| { + tracing::trace!(?v, "udp stream recv something in forward task"); + if v.is_err() { + return Err(super::TunnelError::CommonError( + "udp stream error".to_owned(), + )); + } + Ok(v.unwrap().freeze()) + }) + .forward(udp_tunnel.pin_sink()) + .await; + if let None = sock_map.remove(&addr_copy) { + log::warn!("udp forward packet: {:?}, no entry", addr_copy); + } + close_tunnel(&ctunnel).await.unwrap(); + log::warn!("udp connection forward done: {:?}, {:?}", addr_copy, ret); + }); + + Ok(Box::new(TunnelWithCustomInfo::new( + stunnel, + TunnelInfo { + tunnel_type: "udp".to_owned(), + local_addr: local_url.into(), + remote_addr: build_url_from_socket_addr(&addr.to_string(), "udp").into(), + }, + ))) + } + + pub fn get_socket(&self) -> Option> { + self.socket.clone() + } +} + +#[async_trait] +impl TunnelListener for UdpTunnelListener { + async fn listen(&mut self) -> Result<(), super::TunnelError> { + let addr = super::check_scheme_and_get_socket_addr::(&self.addr, "udp")?; + self.socket = Some(Arc::new(UdpSocket::bind(addr).await?)); + + let socket = self.socket.as_ref().unwrap().clone(); + let forward_tasks = self.forward_tasks.clone(); + let sock_map = self.sock_map.clone(); + let conn_send = self.conn_send.take().unwrap(); + let local_url = self.local_url().clone(); + self.forward_tasks.lock().await.spawn( + async move { + loop { + let mut buf = BytesMut::new(); + buf.resize(2500, 0); + let (_size, addr) = socket.recv_from(&mut buf).await.unwrap(); + let _ = buf.split_off(_size); + log::trace!( + "udp recv packet: {:?}, buf: {:?}, size: {}", + addr, + buf, + _size + ); + + let Ok(udp_packet) = rkyv_util::decode_from_bytes_checked::(&buf) + else { + tracing::warn!(?buf, "udp decode error in forward task"); + continue; + }; + + if matches!(udp_packet.payload, ArchivedUdpPacketPayload::Syn) { + let conn = Self::handle_connect( + socket.clone(), + addr, + forward_tasks.clone(), + sock_map.clone(), + local_url.clone(), + udp_packet.conn_id.into(), + ) + .await + .unwrap(); + if let Err(e) = conn_send.send(conn).await { + tracing::warn!(?e, "udp send conn to accept channel error"); + } + } else { + Self::try_forward_packet(sock_map.as_ref(), buf, addr) + .await + .unwrap(); + } + } + } + .instrument(tracing::info_span!("udp forward task", ?self.socket)), + ); + + // let forward_tasks_clone = self.forward_tasks.clone(); + // tokio::spawn(async move { + // loop { + // let mut locked_forward_tasks = forward_tasks_clone.lock().await; + // tokio::select! { + // ret = locked_forward_tasks.join_next() => { + // tracing::warn!(?ret, "udp forward task exit"); + // } + // else => { + // drop(locked_forward_tasks); + // tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + // continue; + // } + // } + // } + // }); + + Ok(()) + } + + async fn accept(&mut self) -> Result, super::TunnelError> { + log::info!("start udp accept: {:?}", self.addr); + while let Some(conn) = self.conn_recv.recv().await { + return Ok(conn); + } + return Err(super::TunnelError::CommonError( + "udp accept error".to_owned(), + )); + } + + fn local_url(&self) -> url::Url { + self.addr.clone() + } + + fn get_conn_counter(&self) -> Arc> { + struct UdpTunnelConnCounter { + sock_map: Arc>, + } + + impl Debug for UdpTunnelConnCounter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UdpTunnelConnCounter") + .field("sock_map_len", &self.sock_map.len()) + .finish() + } + } + + impl TunnelConnCounter for UdpTunnelConnCounter { + fn get(&self) -> u32 { + self.sock_map.len() as u32 + } + } + + Arc::new(Box::new(UdpTunnelConnCounter { + sock_map: self.sock_map.clone(), + })) + } +} + +pub struct UdpTunnelConnector { + addr: url::Url, + bind_addrs: Vec, +} + +impl UdpTunnelConnector { + pub fn new(addr: url::Url) -> Self { + Self { + addr, + bind_addrs: vec![], + } + } + + async fn wait_sack( + socket: &UdpSocket, + addr: SocketAddr, + conn_id: u32, + ) -> Result<(), super::TunnelError> { + let mut buf = BytesMut::new(); + buf.resize(128, 0); + + let (usize, recv_addr) = tokio::time::timeout( + tokio::time::Duration::from_secs(3), + socket.recv_from(&mut buf), + ) + .await??; + + if recv_addr != addr { + return Err(super::TunnelError::ConnectError(format!( + "udp connect error, unexpected sack addr: {:?}, {:?}", + recv_addr, addr + ))); + } + + let _ = buf.split_off(usize); + + let Ok(udp_packet) = rkyv_util::decode_from_bytes_checked::(&buf) else { + tracing::warn!(?buf, "udp decode error in wait sack"); + return Err(super::TunnelError::ConnectError(format!( + "udp connect error, decode error. buf: {:?}", + buf + ))); + }; + + if conn_id != udp_packet.conn_id { + return Err(super::TunnelError::ConnectError(format!( + "udp connect error, conn id not match. conn_id: {:?}, {:?}", + conn_id, udp_packet.conn_id + ))); + } + + if !matches!(udp_packet.payload, ArchivedUdpPacketPayload::Sack) { + return Err(super::TunnelError::ConnectError(format!( + "udp connect error, unexpected payload. payload: {:?}", + udp_packet.payload + ))); + } + + Ok(()) + } + + async fn wait_sack_loop( + socket: &UdpSocket, + addr: SocketAddr, + conn_id: u32, + ) -> Result<(), super::TunnelError> { + while let Err(err) = Self::wait_sack(socket, addr, conn_id).await { + tracing::warn!(?err, "udp wait sack error"); + } + Ok(()) + } + + pub async fn try_connect_with_socket( + &self, + socket: UdpSocket, + ) -> Result, super::TunnelError> { + let addr = super::check_scheme_and_get_socket_addr::(&self.addr, "udp")?; + log::warn!("udp connect: {:?}", self.addr); + + // send syn + let conn_id = rand::random(); + let udp_packet = UdpPacket::new_syn_packet(conn_id); + let b = encode_to_bytes::<_, UDP_DATA_MTU>(&udp_packet); + let ret = socket.send_to(&b, &addr).await?; + tracing::warn!(?udp_packet, ?ret, "udp send syn"); + + // wait sack + tokio::time::timeout( + tokio::time::Duration::from_secs(3), + Self::wait_sack_loop(&socket, addr, conn_id), + ) + .await??; + + // sack done + let local_addr = socket.local_addr().unwrap().to_string(); + Ok(Box::new(TunnelWithCustomInfo::new( + get_tunnel_from_socket(Arc::new(socket), addr, conn_id), + TunnelInfo { + tunnel_type: "udp".to_owned(), + local_addr: super::build_url_from_socket_addr(&local_addr, "udp").into(), + remote_addr: self.remote_url().into(), + }, + ))) + } + + async fn connect_with_default_bind(&mut self) -> Result, super::TunnelError> { + let socket = UdpSocket::bind("0.0.0.0:0").await?; + return self.try_connect_with_socket(socket).await; + } + + async fn connect_with_custom_bind(&mut self) -> Result, super::TunnelError> { + let mut futures = FuturesUnordered::new(); + + for bind_addr in self.bind_addrs.iter() { + let socket = UdpSocket::bind(*bind_addr).await?; + + // linux does not use interface of bind_addr to send packet, so we need to bind device + // mac can handle this with bind correctly + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + if let Some(dev_name) = super::common::get_interface_name_by_ip(&bind_addr.ip()) { + tracing::trace!(dev_name = ?dev_name, "bind device"); + socket.bind_device(Some(dev_name.as_bytes()))?; + } + + futures.push(self.try_connect_with_socket(socket)); + } + + let Some(ret) = futures.next().await else { + return Err(super::TunnelError::CommonError( + "join connect futures failed".to_owned(), + )); + }; + + return ret; + } +} + +#[async_trait] +impl super::TunnelConnector for UdpTunnelConnector { + async fn connect(&mut self) -> Result, super::TunnelError> { + if self.bind_addrs.is_empty() { + self.connect_with_default_bind().await + } else { + self.connect_with_custom_bind().await + } + } + + fn remote_url(&self) -> url::Url { + self.addr.clone() + } + + fn set_bind_addrs(&mut self, addrs: Vec) { + self.bind_addrs = addrs; + } +} + +#[cfg(test)] +mod tests { + use crate::tunnels::common::tests::{_tunnel_bench, _tunnel_pingpong}; + + use super::*; + + #[tokio::test] + async fn udp_pingpong() { + let listener = UdpTunnelListener::new("udp://0.0.0.0:5556".parse().unwrap()); + let connector = UdpTunnelConnector::new("udp://127.0.0.1:5556".parse().unwrap()); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + async fn udp_bench() { + let listener = UdpTunnelListener::new("udp://0.0.0.0:5555".parse().unwrap()); + let connector = UdpTunnelConnector::new("udp://127.0.0.1:5555".parse().unwrap()); + _tunnel_bench(listener, connector).await + } + + #[tokio::test] + async fn udp_bench_with_bind() { + let listener = UdpTunnelListener::new("udp://127.0.0.1:5554".parse().unwrap()); + let mut connector = UdpTunnelConnector::new("udp://127.0.0.1:5554".parse().unwrap()); + connector.set_bind_addrs(vec!["127.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } + + #[tokio::test] + #[should_panic] + async fn udp_bench_with_bind_fail() { + let listener = UdpTunnelListener::new("udp://127.0.0.1:5553".parse().unwrap()); + let mut connector = UdpTunnelConnector::new("udp://127.0.0.1:5553".parse().unwrap()); + connector.set_bind_addrs(vec!["10.0.0.1:0".parse().unwrap()]); + _tunnel_pingpong(listener, connector).await + } +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..ecd5f81 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,29 @@ +CWD=$(dirname `cargo locate-project | jq '.root' -r`) +cargo build --release --all-targets + +TARGET_HOSTS=("root@public.kkrainbow.top" "root@47.242.85.82" "root@192.168.60.162") +CARGO_BUILD_OUTPUT_DIR="$CWD/target/release" + +copy_bin_to_remote() { + local TARGET_HOST=$1 + scp ${CARGO_BUILD_OUTPUT_DIR}/easytier-core $TARGET_HOST:/tmp/easytier-core & + scp ${CARGO_BUILD_OUTPUT_DIR}/easytier-cli $TARGET_HOST:/tmp/easytier-cli & +} + + +for TARGET_HOST in ${TARGET_HOSTS[@]}; do + ssh $TARGET_HOST "killall easytier-core" + copy_bin_to_remote $TARGET_HOST +done + +wait + +run_with_args() { + local TARGET_HOST=$1 + local ARGS=$2 + ssh $TARGET_HOST "nohup /tmp/easytier-core $ARGS > /tmp/easytier-core.log 2>&1 &" +} + +run_with_args "root@192.168.60.162" "--ipv4 10.144.144.10 --peers tcp://public.kkrainbow.top:11010" +run_with_args "root@public.kkrainbow.top" "--ipv4 10.144.144.20" +run_with_args "root@47.242.85.82" "--ipv4 10.144.144.30 --peers tcp://public.kkrainbow.top:11010" diff --git a/scripts/local_init.sh b/scripts/local_init.sh new file mode 100644 index 0000000..4d7f420 --- /dev/null +++ b/scripts/local_init.sh @@ -0,0 +1,10 @@ +ip netns add net_a +ip netns add net_b +ip netns add net_c +ip link add veth0 type veth peer name veth1 +ip link set veth0 netns net_a +ip link set veth1 netns net_b +ip netns exec net_a ip addr add 10.144.145.1/24 dev veth0 +ip netns exec net_b ip addr add 10.144.145.2/24 dev veth1 +ip netns exec net_a ip link set veth0 up +ip netns exec net_b ip link set veth1 up diff --git a/third_party/Packet.dll b/third_party/Packet.dll new file mode 100644 index 0000000000000000000000000000000000000000..7e1bf1206a781cf6db41cc41bf59854e63dd8f6a GIT binary patch literal 220032 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

l6@4tc8N5VA~I#-O;aK8jwH}g?6 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 S2U$OlRhm)2P)^SQ)BgZ@5?ieR literal 0 HcmV?d00001