Skip to content

Commit

Permalink
Test leaking TCP/UDP/ICMP packets in split tunnel
Browse files Browse the repository at this point in the history
  • Loading branch information
hulthe committed Mar 15, 2024
1 parent 4219b70 commit 69ff5fe
Show file tree
Hide file tree
Showing 12 changed files with 671 additions and 123 deletions.
4 changes: 0 additions & 4 deletions mullvad-management-interface/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,6 @@ impl MullvadProxyClient {
Ok(())
}

//#[cfg(target_os = "windows")]
pub async fn add_split_tunnel_app<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref().to_str().ok_or(Error::PathMustBeUtf8)?;
self.0
Expand All @@ -641,7 +640,6 @@ impl MullvadProxyClient {
Ok(())
}

//#[cfg(target_os = "windows")]
pub async fn remove_split_tunnel_app<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref().to_str().ok_or(Error::PathMustBeUtf8)?;
self.0
Expand All @@ -651,7 +649,6 @@ impl MullvadProxyClient {
Ok(())
}

//#[cfg(target_os = "windows")]
pub async fn clear_split_tunnel_apps(&mut self) -> Result<()> {
self.0
.clear_split_tunnel_apps(())
Expand All @@ -660,7 +657,6 @@ impl MullvadProxyClient {
Ok(())
}

//#[cfg(target_os = "windows")]
pub async fn set_split_tunnel_state(&mut self, state: bool) -> Result<()> {
self.0
.set_split_tunnel_state(state)
Expand Down
33 changes: 24 additions & 9 deletions test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test/am-i-mullvad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ rust-version.workspace = true
workspace = true

[dependencies]
clap = { workspace = true, features = ["derive"] }
color-eyre = "0.6.2"
eyre = "0.6.12"
ping = "0.5.2"
reqwest = { version = "0.11.24", default-features = false, features = ["blocking", "rustls-tls", "json"] }
serde = { version = "1.0.197", features = ["derive"] }
socket2 = { version = "0.5.3", features = ["all"] }
32 changes: 32 additions & 0 deletions test/am-i-mullvad/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use std::net::SocketAddr;

use clap::Parser;

/// CLI tool that queries <https://am.i.mullvad.net> to check if the machine is connected to
/// Mullvad VPN.
#[derive(Parser)]
pub struct Opt {
/// Interactive mode, press enter to check if you are Mullvad.
#[clap(short, long)]
pub interactive: bool,

/// Timeout for network connections (in millis).
#[clap(short, long, default_value = "2000")]
pub timeout: u64,

/// Try to send some junk data over TCP to <send_destination>.
#[clap(long, requires = "send_destination")]
pub send_tcp: bool,

/// Try to send some junk data over UDP to <send_destination>.
#[clap(long, requires = "send_destination")]
pub send_udp: bool,

/// Try to send ICMP request to <send_destination>.
#[clap(long, requires = "send_destination")]
pub send_icmp: bool,

/// Target of <send_tcp>, <send_udp> or <send_icmp>.
#[clap(short = 'd', long)]
pub send_destination: Option<SocketAddr>,
}
2 changes: 2 additions & 0 deletions test/am-i-mullvad/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod cli;
pub mod net;
60 changes: 50 additions & 10 deletions test/am-i-mullvad/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,59 @@
use clap::Parser;
use eyre::{eyre, Context};
use reqwest::blocking::get;
use reqwest::blocking::Client;
use serde::Deserialize;
use std::process;
use std::{io::stdin, time::Duration};

#[derive(Debug, Deserialize)]
struct Response {
ip: String,
mullvad_exit_ip_hostname: Option<String>,
}
use am_i_mullvad::cli::Opt;
use am_i_mullvad::net::{send_ping, send_tcp, send_udp};

fn main() -> eyre::Result<()> {
let opt = Opt::parse();
color_eyre::install()?;

if opt.interactive {
let stdin = stdin();
for line in stdin.lines() {
let _ = line.wrap_err("Failed to read from stdin")?;
test_connection(&opt)?;
}
} else {
test_connection(&opt)?;
}

Ok(())
}

fn test_connection(opt: &Opt) -> eyre::Result<bool> {
if let Some(destination) = opt.send_destination {
if opt.send_tcp {
let _ = send_tcp(opt, destination);
}
if opt.send_udp {
let _ = send_udp(opt, destination);
}
if opt.send_icmp {
let _ = send_ping(opt, destination.ip());
}
}
am_i_mullvad(opt)
}

/// Check if connected to Mullvad and print the result to stdout
fn am_i_mullvad(opt: &Opt) -> eyre::Result<bool> {
#[derive(Debug, Deserialize)]
struct Response {
ip: String,
mullvad_exit_ip_hostname: Option<String>,
}

let url = "https://am.i.mullvad.net/json";
let response: Response = get(url)

let client = Client::new();
let response: Response = client
.get(url)
.timeout(Duration::from_millis(opt.timeout))
.send()
.and_then(|r| r.json())
.wrap_err_with(|| eyre!("Failed to GET {url}"))?;

Expand All @@ -22,12 +62,12 @@ fn main() -> eyre::Result<()> {
"You are connected to Mullvad (server {}). Your IP address is {}",
server, response.ip
);
Ok(())
Ok(true)
} else {
println!(
"You are not connected to Mullvad. Your IP address is {}",
response.ip
);
process::exit(1)
Ok(false)
}
}
76 changes: 76 additions & 0 deletions test/am-i-mullvad/src/net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use eyre::{eyre, Context};
use std::{
io::Write,
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Duration,
};

use crate::cli::Opt;

pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> {
let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);

let family = match &destination {
SocketAddr::V4(_) => socket2::Domain::IPV4,
SocketAddr::V6(_) => socket2::Domain::IPV6,
};
let sock = socket2::Socket::new(family, socket2::Type::STREAM, Some(socket2::Protocol::TCP))
.wrap_err(eyre!("Failed to create TCP socket"))?;

eprintln!("Connecting from {bind_addr} to {destination}/TCP");

sock.bind(&socket2::SockAddr::from(bind_addr))
.wrap_err(eyre!("Failed to bind TCP socket to {bind_addr}"))?;

let timeout = Duration::from_millis(opt.timeout);
sock.set_write_timeout(Some(timeout))?;
sock.set_read_timeout(Some(timeout))?;

sock.connect_timeout(&socket2::SockAddr::from(destination), timeout)
.wrap_err(eyre!("Failed to connect to {destination}"))?;

let mut stream = std::net::TcpStream::from(sock);
stream
.write_all(b"hello there")
.wrap_err(eyre!("Failed to send message to {destination}"))?;

Ok(())
}

pub fn send_udp(_opt: &Opt, destination: SocketAddr) -> Result<(), eyre::Error> {
let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);

eprintln!("Connecting from {bind_addr} to {destination}/UDP");

let family = match &destination {
SocketAddr::V4(_) => socket2::Domain::IPV4,
SocketAddr::V6(_) => socket2::Domain::IPV6,
};
let sock = socket2::Socket::new(family, socket2::Type::DGRAM, Some(socket2::Protocol::UDP))
.wrap_err("Failed to create UDP socket")?;

sock.bind(&socket2::SockAddr::from(bind_addr))
.wrap_err(eyre!("Failed to bind UDP socket to {bind_addr}"))?;

//log::debug!("Send message from {bind_addr} to {destination}/UDP");

let std_socket = std::net::UdpSocket::from(sock);
std_socket
.send_to(b"Hello there!", destination)
.wrap_err(eyre!("Failed to send message to {destination}"))?;

Ok(())
}

pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> {
ping::ping(
destination,
Some(Duration::from_millis(opt.timeout)),
None,
None,
None,
None,
)?;

Ok(())
}
Loading

0 comments on commit 69ff5fe

Please sign in to comment.