Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ snow = "0.9"
tokio = { version = "1.44", features = ["rt", "rt-multi-thread", "macros", "sync", "time", "socket2"] }
tun-rs = { version = "2.0.9", features = ["async_tokio"] }
socket2 = "0.5.9"
tokio-tungstenite = "0.26.2"
futures = "0.3.31"
ctrlc = "3.4"

# data
Expand Down
11 changes: 8 additions & 3 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ autobins = false

[features]
default = ["cli"]
udp = ["socket2"]
ws = ["tokio-tungstenite", "futures"]
cli = ["clap", "ctrlc", "anstyle"]

[[bin]]
Expand All @@ -20,12 +22,13 @@ shared = { workspace = true }
# IO
tokio = { workspace = true }
tun-rs = { workspace = true }
socket2 = { workspace = true}
pnet = "0.35.*"
socket2 = { workspace = true, optional = true}
ctrlc = { workspace = true, optional = true}
clap = { version = "4.5.23", features = ["derive"], optional = true}
anstyle = { version = "1.0", optional = true }

tokio-tungstenite = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
ipnetwork = "0.21.1"

# Crypto
snow = "0.9"
Expand All @@ -39,3 +42,5 @@ bincode = { workspace = true }
tracing-subscriber = { workspace = true }
tracing = { workspace = true }
tracing-appender = { workspace = true }
async-trait = "0.1.88"

2 changes: 1 addition & 1 deletion crates/client/src/bin/command/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl ConnectCmd {
debug!("stop signal not sent from Ctrl-C handler: {}", err);
}
}
thread::sleep(Duration::from_secs(1));
thread::sleep(Duration::from_secs(2));
process::exit(0);
}).expect("error setting Ctrl-C handler");

Expand Down
10 changes: 10 additions & 0 deletions crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,15 @@
//!
//!

#[cfg(not(any(
feature = "udp",
feature = "ws",
)))]
compile_error!(
"please enable one of the following transport backends with cargo's --features argument: \
udp, ws (e.g. --features=udp)"
);


pub mod network;
pub mod runtime;
237 changes: 153 additions & 84 deletions crates/client/src/network.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,73 @@
use std::net::{IpAddr};
use tracing::{info, warn};
use std::process::Command;
use crate::runtime::error::RuntimeError;
use anyhow::format_err;
use std::fmt::Write;
use std::str::FromStr;

pub enum RouteType {
Net,
Host,
pub struct RouteState {
dev: String,
default_gateway: Option<IpAddr>,
exclude: Vec<IpNetwork>,
}
use ipnetwork::{IpNetwork, NetworkSize};

pub struct DefaultGateway {
origin: IpAddr,
remote: IpAddr,
default: bool,
}
impl RouteState {
pub fn new(remote: IpAddr, dev: String) -> Self {
Self {
dev,
default_gateway: None,
exclude: vec![IpNetwork::from(remote)]
}
}

pub fn exclude(mut self, addr: IpNetwork) {
self.exclude.push(addr);
}

impl DefaultGateway {
pub fn create(new_gateway: &IpAddr, remote: &IpAddr, default: bool) -> Result<DefaultGateway, RuntimeError> {
let origin = default_gateway()
.map_err(|e| RuntimeError::Network(format!("getting default gateway: {}", e)))?;
info!("original default gateway: {}.", origin);
add_route(RouteType::Host, &remote.to_string(), &origin.to_string())
.map_err(|error| RuntimeError::Network(format!(
"failed to add route: {} -> {}: {}",
remote,
origin,
error
)))?;
pub fn build(mut self) -> anyhow::Result<RouteState> {
let (default_gateway, default_dev_name) = default_device().map_err(|e|
format_err!("failed to get default device: {}", e)
)?;
self.default_gateway = Some(default_gateway);
info!("default gateway: {} from dev {}", default_gateway, default_dev_name);
add_route(
&IpNetwork::from_str("0.0.0.0/1")?,
None,
&self.dev,
Some(1),
)?;
add_route(
&IpNetwork::from_str("128.0.0.0/1")?,
None,
&self.dev,
Some(1),
)?;

if default {
delete_route(RouteType::Net, "default");
add_route(RouteType::Net, "default", &new_gateway.to_string())
.map_err(|error| RuntimeError::Network(format!(
"failed to add new default route: {} (new) -> {} (old): {}",
new_gateway,
origin,
error
)))?;
for addr in self.exclude.iter() {
add_route(
addr,
Some(default_gateway),
&default_dev_name,
None
)?;
}

Ok(DefaultGateway {
origin,
remote: *remote,
default,
})
Ok(self)
}

pub fn restore(&mut self) {
if self.default {
delete_route(RouteType::Net, "default");
if let Err(e) = add_route(RouteType::Net, "default", &self.origin.to_string()) {
warn!("failed to restore default route: {}", e);
} else {
info!("restored default route: {}.", self.origin);
for addr in self.exclude.iter() {
match delete_route(
addr,
&self.default_gateway.expect(
"default gateway not set, cannot restore route (are you sure you called build?)"
),
) {
Ok(_) => {},
Err(e) => warn!("failed to restore route: {} via {}: {}", addr, self.default_gateway.unwrap(), e),
}
}
delete_route(RouteType::Host, &self.remote.to_string());
}
}

Expand All @@ -64,68 +77,95 @@ impl DefaultGateway {
// }
// }

pub fn delete_route(route_type: RouteType, route: &str) {
let mode = match route_type {
RouteType::Net => "-net",
RouteType::Host => "-host",
pub fn delete_route(route: &IpNetwork, via: &IpAddr,) -> anyhow::Result<()> {
info!("deleting route: {} via {}", route, via);

let (formated_route, _) = match route.size() {
NetworkSize::V4(32) | NetworkSize::V6(128) => (route.ip().to_string(), false),
_ => (route.to_string(), true),
};
info!("deleting route: {} {}.", mode, route);

let status = if cfg!(target_os = "linux") {
let check = Command::new("ip")
.arg("route")
.arg("show")
.arg(formated_route.clone())
.output()?;

if check.stdout.is_empty() {
warn!("route already deleted");
return Ok(());
}

Command::new("ip")
.arg("route")
.arg("del")
.arg(route)
.status()
.unwrap()
} else if cfg!(target_os = "macos") {
Command::new("route")
.arg("-n")
.arg("delete")
.arg(mode)
.arg(route)
.status()
.unwrap()
.arg(formated_route)
.arg("via")
.arg(via.to_string())
.status()?
} else {
unimplemented!("Unsupported OS");
};
if !status.success() {
warn!("failed to delete route: {}", status);
warn!("cant delete route: {}", status);
}
Ok(())
}

pub fn add_route(route_type: RouteType, route: &str, gateway: &str) -> anyhow::Result<()> {
let mode = match route_type {
RouteType::Net => "-net",
RouteType::Host => "-host",

fn add_route(route: &IpNetwork, via: Option<IpAddr>, dev: &str, metric: Option<usize>) -> anyhow::Result<()> {
let mut buffer = format!("adding route: {} ", route);
if let Some(via) = via {
write!(buffer, "via {} ", via)?;
}
write!(buffer, "dev {} ", dev)?;
if let Some(metric) = metric {
write!(buffer, "metric {}", metric)?;
}
info!("{}", buffer);

let (formated_route, _) = match route.size() {
NetworkSize::V4(32) | NetworkSize::V6(128) => (route.ip().to_string(), false),
_ => (route.to_string(), true),
};
info!("adding route: {} {} gateway {}.", mode, route, gateway);

let status = if cfg!(target_os = "linux") {
let check = Command::new("ip")
.arg("route")
.arg("show")
.arg(route)
.arg(formated_route.clone())
.output()?;

if !check.stdout.is_empty() {
warn!("route already exists");
return Ok(());
}

Command::new("ip")
.arg("route")
.arg("add")
.arg(route)
.arg("via")
.arg(gateway)
.status()?
} else if cfg!(target_os = "macos") {
Command::new("route")
.arg("-n")
.arg("add")
.arg(mode)
.arg(route)
.arg(gateway)
.status()?
let mut cmd = Command::new("ip");

cmd.arg("route").arg("add").arg(formated_route);

if let Some(via) = via {
cmd.arg("via").arg(via.to_string());
};

cmd.arg("dev").arg(dev);

if let Some(metric) = metric {
cmd.arg("metric").arg(metric.to_string());
};

cmd.status()?
// } else if cfg!(target_os = "macos") {
//
// Command::new("route")
// .arg("-n")
// .arg("add")
// .arg(if is_net { "-net" } else { "-host" })
// .arg(formated_route)
// .arg(gateway)
// .status()?
} else {
unimplemented!("Unsupported OS");
};
Expand All @@ -136,17 +176,46 @@ pub fn add_route(route_type: RouteType, route: &str, gateway: &str) -> anyhow::R
}
}

pub fn default_gateway() -> anyhow::Result<IpAddr> {
pub fn default_device() -> anyhow::Result<(IpAddr, String)> {
let cmd = if cfg!(target_os = "linux") {
"ip -4 route list 0/0 | awk '{print $3}'"
"ip -4 route list 0/0"
} else if cfg!(target_os = "macos") {
"route -n get default | grep gateway | awk '{print $2}'"
"route -n get default"
} else {
unimplemented!("Unsupported OS");
};

let output = Command::new("bash").arg("-c").arg(cmd).output()?;

if output.status.success() {
Ok(String::from_utf8(output.stdout)?.trim_end().parse()?)
let output_str = String::from_utf8(output.stdout)?;

if cfg!(target_os = "linux") {
for line in output_str.lines() {
if line.contains("default") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let ip: IpAddr = parts[2].parse()?;
let interface = parts[4].to_string();
return Ok((ip, interface));
}
}
}
}

// if cfg!(target_os = "macos") {
// for line in output_str.lines() {
// if line.contains("gateway") {
// let parts: Vec<&str> = line.split_whitespace().collect();
// if parts.len() >= 2 {
// let ip: IpAddr = parts[1].parse()?;
// return Ok((ip, String::from("unknown")));
// }
// }
// }
// }

Err(anyhow::anyhow!("Failed to parse output"))
} else {
Err(anyhow::anyhow!(String::from_utf8(output.stderr)?))
}
Expand Down
3 changes: 1 addition & 2 deletions crates/client/src/runtime/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod error;
mod worker;
mod transport;

use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
Expand Down Expand Up @@ -42,8 +43,6 @@ impl Runtime {
}

pub async fn run(&self) -> Result<(), RuntimeError> {
tracing::info!("Connecting to udp://{}", self.sock);

let worker = worker::create(
self.sock,
self.stop_tx.clone(),
Expand Down
Loading
Loading