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 20, 2024
1 parent e548765 commit 6594c6d
Show file tree
Hide file tree
Showing 21 changed files with 864 additions and 181 deletions.
8 changes: 0 additions & 8 deletions mullvad-management-interface/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,6 @@ impl MullvadProxyClient {
.map(drop)
}

#[cfg(target_os = "linux")]
pub async fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> {
use futures::TryStreamExt;

Expand All @@ -604,7 +603,6 @@ impl MullvadProxyClient {
procs.try_collect().await.map_err(Error::Rpc)
}

#[cfg(target_os = "linux")]
pub async fn add_split_tunnel_process(&mut self, pid: i32) -> Result<()> {
self.0
.add_split_tunnel_process(pid)
Expand All @@ -613,7 +611,6 @@ impl MullvadProxyClient {
Ok(())
}

#[cfg(target_os = "linux")]
pub async fn remove_split_tunnel_process(&mut self, pid: i32) -> Result<()> {
self.0
.remove_split_tunnel_process(pid)
Expand All @@ -622,7 +619,6 @@ impl MullvadProxyClient {
Ok(())
}

#[cfg(target_os = "linux")]
pub async fn clear_split_tunnel_processes(&mut self) -> Result<()> {
self.0
.clear_split_tunnel_processes(())
Expand All @@ -631,7 +627,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 +636,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 +645,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 +653,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
35 changes: 25 additions & 10 deletions test/Cargo.lock

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

2 changes: 1 addition & 1 deletion test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ members = [
"test-runner",
"test-rpc",
"socks-server",
"am-i-mullvad",
"connection-checker",
]

[workspace.lints.rust]
Expand Down
33 changes: 0 additions & 33 deletions test/am-i-mullvad/src/main.rs

This file was deleted.

4 changes: 2 additions & 2 deletions test/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ if [[ $TARGET == x86_64-unknown-linux-gnu ]]; then
-e CARGO_HOME=/root/.cargo/registry \
-e CARGO_TARGET_DIR=/src/test/target \
mullvadvpn-app-tests \
/bin/bash -c "cd /src/test/; cargo build --bin test-runner --release --target ${TARGET}"
/bin/bash -c "cd /src/test/; cargo build --bin test-runner --bin connection-checker --release --target ${TARGET}"
else
cargo build \
--bin test-runner \
--bin am-i-mullvad \
--bin connection-checker \
--release --target "${TARGET}"
fi

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "am-i-mullvad"
name = "connection-checker"
description = "Simple cli for testing Mullvad VPN connections"
authors.workspace = true
repository.workspace = true
Expand All @@ -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.4", features = ["all"] }
36 changes: 36 additions & 0 deletions test/connection-checker/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 connection to am.i.mullvad (in millis).
#[clap(short, long, default_value = "3000")]
pub timeout: u64,

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

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

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

/// Target of <leak_tcp>, <leak_udp> or <leak_icmp>.
#[clap(long)]
pub leak: Option<SocketAddr>,

/// Timeout for leak check network connections (in millis).
#[clap(long, default_value = "1000")]
pub leak_timeout: u64,
}
2 changes: 2 additions & 0 deletions test/connection-checker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod cli;
pub mod net;
73 changes: 73 additions & 0 deletions test/connection-checker/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use clap::Parser;
use eyre::{eyre, Context};
use reqwest::blocking::Client;
use serde::Deserialize;
use std::{io::stdin, time::Duration};

use connection_checker::cli::Opt;
use connection_checker::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.leak {
if opt.leak_tcp {
let _ = send_tcp(opt, destination);
}
if opt.leak_udp {
let _ = send_udp(opt, destination);
}
if opt.leak_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 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}"))?;

if let Some(server) = &response.mullvad_exit_ip_hostname {
println!(
"You are connected to Mullvad (server {}). Your IP address is {}",
server, response.ip
);
Ok(true)
} else {
println!(
"You are not connected to Mullvad. Your IP address is {}",
response.ip
);
Ok(false)
}
}
78 changes: 78 additions & 0 deletions test/connection-checker/src/net.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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!("Leaking TCP packets to {destination}");

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

let timeout = Duration::from_millis(opt.leak_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!("Leaking UDP packets to {destination}");

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<()> {
eprintln!("Leaking IMCP packets to {destination}");

ping::ping(
destination,
Some(Duration::from_millis(opt.leak_timeout)),
None,
None,
None,
None,
)?;

Ok(())
}
Loading

0 comments on commit 6594c6d

Please sign in to comment.