diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..548762c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "etherparse", + "nodelay", + "xvpn" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 72d56a8..762ce5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,36 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -85,6 +115,19 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -205,6 +248,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -221,6 +273,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -246,6 +304,36 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etherparse" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b119b9796ff800751a220394b8b3613f26dd30c48f254f6837e64c464872d1c7" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -285,6 +373,22 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -623,6 +727,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "paste" version = "1.0.15" @@ -635,6 +745,17 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -927,6 +1048,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aed038a26d2b6a7906a62b3d0c42fb77541b0a695b9c2b9489a0c25c093ce8b" dependencies = [ + "blocking", "byteorder", "bytes", "c2rust-bitfields", @@ -940,6 +1062,7 @@ dependencies = [ "nix 0.31.1", "route_manager", "scopeguard", + "tokio", "widestring", "windows-sys 0.61.2", "winreg", @@ -1469,6 +1592,7 @@ dependencies = [ "base64", "chrono", "clap", + "etherparse", "ipnet", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 291497e..21f1254 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,6 @@ anyhow = "1.0.102" uuid = { version = "1.21.0", features = ["v4", "serde"] } ipnet = { version = "2.11.0", features = ["serde"] } base64 = "0.22.1" -tun-rs = "2.8.2" +tun-rs = { version = "2.8.2", features = ["async"] } chrono = "0.4.44" +etherparse = "0.19.0" diff --git a/src/client.rs b/src/client.rs index 4cabe41..7409ba5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,5 @@ -use std::net::IpAddr; - use anyhow::Result; +use chrono::Utc; use clap::Args; use ipnet::Ipv4Net; use serde::{Deserialize, Serialize}; @@ -11,7 +10,21 @@ use tokio::{ time::Instant, }; -use crate::router::{CLIENT_REGISTER_TIMEOUT, CliRegMessages, RouterMessages, SERVER_PACKET_SIZE}; +use crate::{ + network::ip_match_network, + router::{CLIENT_REGISTER_TIMEOUT, CliRegMessages, RouterMessages, SERVER_PACKET_SIZE}, + tun::inti_tun_interface, +}; + +pub struct ClientStaTistic { + pub last_keep_alive: Option>, + pub keep_alive_count: usize, + pub total_data_received: usize, + pub total_data_sent: usize, + pub last_data_received: Option>, + pub last_data_sent: Option>, + pub latency_ms: Option, +} #[derive(Debug, Clone, Serialize, Deserialize, Args)] pub struct ClientCfg { @@ -19,9 +32,9 @@ pub struct ClientCfg { #[arg(long, short)] pub server: String, - /// The local interface IP address (example: 10.8.0.2). + /// The local interface IP address (example: 10.8.0.2/32). #[arg(long = "interface-ip")] - pub interface_ip: IpAddr, + pub interface_ip: Ipv4Net, /// The local interface name. #[arg(long = "interface-name", default_value = "xvpn0")] @@ -31,6 +44,10 @@ pub struct ClientCfg { /// Example: --local-route 1.1.1.1/32,10.0.0.0/24 #[arg(long = "local-route", visible_alias = "lr", value_delimiter = ',')] pub local_routes: Vec, + /// MTU for the TUN interface. + /// If not specified, the default MTU of the system will be used. + #[arg(long = "mtu", default_value = "1400")] + pub mtu: u16, } pub async fn start(config: ClientCfg) -> Result<()> { @@ -41,27 +58,51 @@ pub async fn start(config: ClientCfg) -> Result<()> { let (mut rx, mut tx) = stream.into_split(); // let client_stream = ClientStream::new(tx); - let mut buf = vec![0u8; SERVER_PACKET_SIZE]; - register_client(&mut rx, &mut tx, config, &mut buf).await?; + let mut vpn_buf = vec![0u8; SERVER_PACKET_SIZE]; + let mut tun_buf = vec![0u8; config.mtu as usize]; + register_client(&mut rx, &mut tx, &config, &mut vpn_buf).await?; + let tun_device = inti_tun_interface(&config).await?; println!("Client registration successful. Entering main loop to receive messages from router..."); loop { tokio::select! { - msg = rx.read(&mut buf) => { + msg = rx.read(&mut vpn_buf) => { match msg { Ok(0) => { println!("Connection to router closed by peer."); return Ok(()); } Ok(n) => { - println!("Received {} bytes from router: {:?}", n, RouterMessages::from_slice(&buf[..n])); + match RouterMessages::from_slice(&vpn_buf[..n]){ + RouterMessages::KeepAlive(timestamp) => { + println!("Received keep-alive message from router with timestamp: {}, delta {} ms", timestamp, (Utc::now().timestamp_micros() - timestamp).abs() as f64 / 1000.0); + } + + _ => println!("Received message from router: {:?}", RouterMessages::from_slice(&vpn_buf[..n])) + }; } Err(e) => { eprintln!("Error reading from router: {}", e); return Err(anyhow::anyhow!(format!("Error reading from router: {}", e))); } + } } - } + data = tun_device.recv(&mut tun_buf) => { + match data { + Ok(n) => { + let packet = etherparse::Ipv4HeaderSlice::from_slice(&tun_buf[..n])?; + let src = packet.source_addr(); + match ip_match_network(src, &config.local_routes).await { + Some(net) => println!("Source IP {} matches local route {}", src, net), + None => {}, + } + } + Err(e) => { + eprintln!("Error reading from TUN interface: {}", e); + return Err(anyhow::anyhow!(format!("Error reading from TUN interface: {}", e))); + } + } + } } } @@ -71,10 +112,10 @@ pub async fn start(config: ClientCfg) -> Result<()> { pub async fn register_client( rx: &mut OwnedReadHalf, tx: &mut OwnedWriteHalf, - config: ClientCfg, + config: &ClientCfg, buf: &mut [u8], ) -> Result<()> { - let register_msg = RouterMessages::CliReg(CliRegMessages::Reg(config)); + let register_msg = RouterMessages::CliReg(CliRegMessages::Reg(config.clone())); let mut client_registration_timeout = tokio::time::interval_at(Instant::now() + CLIENT_REGISTER_TIMEOUT, CLIENT_REGISTER_TIMEOUT); tx.write_all(®ister_msg.to_bytes()).await?; diff --git a/src/main.rs b/src/main.rs index 1a6858a..fc0751b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ pub mod client; pub mod config; +pub mod network; pub mod router; +pub mod tun; use clap::{Parser, Subcommand}; @@ -54,7 +56,7 @@ impl Display for OpModes { #[tokio::main] async fn main() -> anyhow::Result<()> { - // Check if some commanline or check if config file. + // Check if some commandline or check if config file. let commandline = if env::args().nth(1).is_some() { Cli::parse() } else { diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..5554f0f --- /dev/null +++ b/src/network.rs @@ -0,0 +1,11 @@ +use ipnet::Ipv4Net; +use std::net::Ipv4Addr; + +pub async fn ip_match_network(ip: Ipv4Addr, networks: &[Ipv4Net]) -> Option { + for net in networks { + if net.contains(&ip) { + return Some(*net); + } + } + None +} diff --git a/src/router.rs b/src/router.rs index 877afd9..e0924e1 100644 --- a/src/router.rs +++ b/src/router.rs @@ -27,16 +27,23 @@ pub trait ReceiverTrait {} pub enum RouterMessages { CliReg(CliRegMessages), KeepAlive(i64), - Data(Vec), + Data(VpnPacket), Quit(String), - Uknown(String), + Unknown(String), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpnPacket { + pub src_uuid: Uuid, + pub dst_uuid: Uuid, + pub payload: Vec, } impl RouterMessages { pub fn to_bytes(&self) -> Vec { serde_json::to_vec(self).expect("Unable to serialize RouteMessages") } pub fn from_slice(slice: &[u8]) -> Self { - serde_json::from_slice(slice).unwrap_or(RouterMessages::Uknown( + serde_json::from_slice(slice).unwrap_or(RouterMessages::Unknown( String::from_utf8(slice.to_vec()).unwrap_or_else(|b| format!("Invalid UTF-8: {:?}", b.as_bytes())), )) } @@ -47,7 +54,7 @@ pub enum CliRegMessages { Reg(ClientCfg), RegOk(Uuid), RegFailed(String), - Uknown(String), + Unknown(String), } impl CliRegMessages { @@ -55,7 +62,7 @@ impl CliRegMessages { serde_json::to_vec(self).expect("Unable to serialize RegisterMessages") } pub fn from_slice(slice: &[u8]) -> Self { - serde_json::from_slice(slice).unwrap_or(CliRegMessages::Uknown( + serde_json::from_slice(slice).unwrap_or(CliRegMessages::Unknown( String::from_utf8(slice.to_vec()).unwrap_or_else(|b| format!("Invalid UTF-8: {:?}", b.as_bytes())), )) } @@ -202,7 +209,6 @@ pub async fn handle_client(router: Router, stream: TcpStream) -> Result<()> { _= keep_alive_tick.tick() => { // Send keep-alive message to the client - println!("Sent keep-alive message to client"); vpn_client.send(RouterMessages::KeepAlive(Utc::now().timestamp_micros())).await?; } } diff --git a/src/tun.rs b/src/tun.rs new file mode 100644 index 0000000..378382c --- /dev/null +++ b/src/tun.rs @@ -0,0 +1,29 @@ +use anyhow::Result; +use tun_rs::{AsyncDevice, DeviceBuilder}; + +use crate::client::ClientCfg; + +pub async fn inti_tun_interface(config: &ClientCfg) -> Result { + println!( + "Initializing TUN interface with name: {}, IP: {}/{}, MTU: {}", + config.interface_name, + config.interface_ip.addr(), + config.interface_ip.netmask(), + config.mtu + ); + let device = match DeviceBuilder::new() + .name(&config.interface_name) + .ipv4(config.interface_ip.addr(), config.interface_ip.netmask(), None) + .mtu(config.mtu) + .build_async() + { + Ok(dev) => dev, + Err(e) => { + let msg = format!("Failed to create TUN interface: {:#}", e); + eprintln!("{}", msg); + return Err(anyhow::anyhow!(msg)); + } + }; + + Ok(device) +}