Skip to content

Commit 17e30b5

Browse files
committed
Test leaking TCP/UDP/ICMP packets in split tunnel
1 parent 4219b70 commit 17e30b5

File tree

18 files changed

+819
-159
lines changed

18 files changed

+819
-159
lines changed

mullvad-management-interface/src/client.rs

-8
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,6 @@ impl MullvadProxyClient {
591591
.map(drop)
592592
}
593593

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

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

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

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

625-
#[cfg(target_os = "linux")]
626622
pub async fn clear_split_tunnel_processes(&mut self) -> Result<()> {
627623
self.0
628624
.clear_split_tunnel_processes(())
@@ -631,7 +627,6 @@ impl MullvadProxyClient {
631627
Ok(())
632628
}
633629

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

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

654-
//#[cfg(target_os = "windows")]
655648
pub async fn clear_split_tunnel_apps(&mut self) -> Result<()> {
656649
self.0
657650
.clear_split_tunnel_apps(())
@@ -660,7 +653,6 @@ impl MullvadProxyClient {
660653
Ok(())
661654
}
662655

663-
//#[cfg(target_os = "windows")]
664656
pub async fn set_split_tunnel_state(&mut self, state: bool) -> Result<()> {
665657
self.0
666658
.set_split_tunnel_state(state)

test/Cargo.lock

+25-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ members = [
1212
"test-runner",
1313
"test-rpc",
1414
"socks-server",
15-
"am-i-mullvad",
15+
"connection-checker",
1616
]
1717

1818
[workspace.lints.rust]

test/am-i-mullvad/src/main.rs

-33
This file was deleted.

test/build.sh

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ if [[ $TARGET == x86_64-unknown-linux-gnu ]]; then
1717
-e CARGO_HOME=/root/.cargo/registry \
1818
-e CARGO_TARGET_DIR=/src/test/target \
1919
mullvadvpn-app-tests \
20-
/bin/bash -c "cd /src/test/; cargo build --bin test-runner --release --target ${TARGET}"
20+
/bin/bash -c "cd /src/test/; cargo build --bin test-runner --bin connection-checker --release --target ${TARGET}"
2121
else
2222
cargo build \
2323
--bin test-runner \
24-
--bin am-i-mullvad \
24+
--bin connection-checker \
2525
--release --target "${TARGET}"
2626
fi
2727

test/am-i-mullvad/Cargo.toml test/connection-checker/Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "am-i-mullvad"
2+
name = "connection-checker"
33
description = "Simple cli for testing Mullvad VPN connections"
44
authors.workspace = true
55
repository.workspace = true
@@ -11,7 +11,10 @@ rust-version.workspace = true
1111
workspace = true
1212

1313
[dependencies]
14+
clap = { workspace = true, features = ["derive"] }
1415
color-eyre = "0.6.2"
1516
eyre = "0.6.12"
17+
ping = "0.5.2"
1618
reqwest = { version = "0.11.24", default-features = false, features = ["blocking", "rustls-tls", "json"] }
1719
serde = { version = "1.0.197", features = ["derive"] }
20+
socket2 = { version = "0.5.4", features = ["all"] }

test/connection-checker/src/cli.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::net::SocketAddr;
2+
3+
use clap::Parser;
4+
5+
/// CLI tool that queries <https://am.i.mullvad.net> to check if the machine is connected to
6+
/// Mullvad VPN.
7+
#[derive(Parser)]
8+
pub struct Opt {
9+
/// Interactive mode, press enter to check if you are Mullvad.
10+
#[clap(short, long)]
11+
pub interactive: bool,
12+
13+
/// Timeout for network connection to am.i.mullvad (in millis).
14+
#[clap(short, long, default_value = "3000")]
15+
pub timeout: u64,
16+
17+
/// Try to send some junk data over TCP to <leak>.
18+
#[clap(long, requires = "leak")]
19+
pub leak_tcp: bool,
20+
21+
/// Try to send some junk data over UDP to <leak>.
22+
#[clap(long, requires = "leak")]
23+
pub leak_udp: bool,
24+
25+
/// Try to send ICMP request to <leak>.
26+
#[clap(long, requires = "leak")]
27+
pub leak_icmp: bool,
28+
29+
/// Target of <leak_tcp>, <leak_udp> or <leak_icmp>.
30+
#[clap(long)]
31+
pub leak: Option<SocketAddr>,
32+
33+
/// Timeout for leak check network connections (in millis).
34+
#[clap(long, default_value = "1000")]
35+
pub leak_timeout: u64,
36+
}

test/connection-checker/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod cli;
2+
pub mod net;

test/connection-checker/src/main.rs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use clap::Parser;
2+
use eyre::{eyre, Context};
3+
use reqwest::blocking::Client;
4+
use serde::Deserialize;
5+
use std::{io::stdin, time::Duration};
6+
7+
use connection_checker::cli::Opt;
8+
use connection_checker::net::{send_ping, send_tcp, send_udp};
9+
10+
fn main() -> eyre::Result<()> {
11+
let opt = Opt::parse();
12+
color_eyre::install()?;
13+
14+
if opt.interactive {
15+
let stdin = stdin();
16+
for line in stdin.lines() {
17+
let _ = line.wrap_err("Failed to read from stdin")?;
18+
test_connection(&opt)?;
19+
}
20+
} else {
21+
test_connection(&opt)?;
22+
}
23+
24+
Ok(())
25+
}
26+
27+
fn test_connection(opt: &Opt) -> eyre::Result<bool> {
28+
if let Some(destination) = opt.leak {
29+
if opt.leak_tcp {
30+
let _ = send_tcp(opt, destination);
31+
}
32+
if opt.leak_udp {
33+
let _ = send_udp(opt, destination);
34+
}
35+
if opt.leak_icmp {
36+
let _ = send_ping(opt, destination.ip());
37+
}
38+
}
39+
am_i_mullvad(opt)
40+
}
41+
42+
/// Check if connected to Mullvad and print the result to stdout
43+
fn am_i_mullvad(opt: &Opt) -> eyre::Result<bool> {
44+
#[derive(Debug, Deserialize)]
45+
struct Response {
46+
ip: String,
47+
mullvad_exit_ip_hostname: Option<String>,
48+
}
49+
50+
let url = "https://am.i.mullvad.net/json";
51+
52+
let client = Client::new();
53+
let response: Response = client
54+
.get(url)
55+
.timeout(Duration::from_millis(opt.timeout))
56+
.send()
57+
.and_then(|r| r.json())
58+
.wrap_err_with(|| eyre!("Failed to GET {url}"))?;
59+
60+
if let Some(server) = &response.mullvad_exit_ip_hostname {
61+
println!(
62+
"You are connected to Mullvad (server {}). Your IP address is {}",
63+
server, response.ip
64+
);
65+
Ok(true)
66+
} else {
67+
println!(
68+
"You are not connected to Mullvad. Your IP address is {}",
69+
response.ip
70+
);
71+
Ok(false)
72+
}
73+
}

test/connection-checker/src/net.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use eyre::{eyre, Context};
2+
use std::{
3+
io::Write,
4+
net::{IpAddr, Ipv4Addr, SocketAddr},
5+
time::Duration,
6+
};
7+
8+
use crate::cli::Opt;
9+
10+
pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> {
11+
let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);
12+
13+
let family = match &destination {
14+
SocketAddr::V4(_) => socket2::Domain::IPV4,
15+
SocketAddr::V6(_) => socket2::Domain::IPV6,
16+
};
17+
let sock = socket2::Socket::new(family, socket2::Type::STREAM, Some(socket2::Protocol::TCP))
18+
.wrap_err(eyre!("Failed to create TCP socket"))?;
19+
20+
eprintln!("Leaking TCP packets to {destination}");
21+
22+
sock.bind(&socket2::SockAddr::from(bind_addr))
23+
.wrap_err(eyre!("Failed to bind TCP socket to {bind_addr}"))?;
24+
25+
let timeout = Duration::from_millis(opt.leak_timeout);
26+
sock.set_write_timeout(Some(timeout))?;
27+
sock.set_read_timeout(Some(timeout))?;
28+
29+
sock.connect_timeout(&socket2::SockAddr::from(destination), timeout)
30+
.wrap_err(eyre!("Failed to connect to {destination}"))?;
31+
32+
let mut stream = std::net::TcpStream::from(sock);
33+
stream
34+
.write_all(b"hello there")
35+
.wrap_err(eyre!("Failed to send message to {destination}"))?;
36+
37+
Ok(())
38+
}
39+
40+
pub fn send_udp(_opt: &Opt, destination: SocketAddr) -> Result<(), eyre::Error> {
41+
let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);
42+
43+
eprintln!("Leaking UDP packets to {destination}");
44+
45+
let family = match &destination {
46+
SocketAddr::V4(_) => socket2::Domain::IPV4,
47+
SocketAddr::V6(_) => socket2::Domain::IPV6,
48+
};
49+
let sock = socket2::Socket::new(family, socket2::Type::DGRAM, Some(socket2::Protocol::UDP))
50+
.wrap_err("Failed to create UDP socket")?;
51+
52+
sock.bind(&socket2::SockAddr::from(bind_addr))
53+
.wrap_err(eyre!("Failed to bind UDP socket to {bind_addr}"))?;
54+
55+
//log::debug!("Send message from {bind_addr} to {destination}/UDP");
56+
57+
let std_socket = std::net::UdpSocket::from(sock);
58+
std_socket
59+
.send_to(b"Hello there!", destination)
60+
.wrap_err(eyre!("Failed to send message to {destination}"))?;
61+
62+
Ok(())
63+
}
64+
65+
pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> {
66+
eprintln!("Leaking IMCP packets to {destination}");
67+
68+
ping::ping(
69+
destination,
70+
Some(Duration::from_millis(opt.leak_timeout)),
71+
None,
72+
None,
73+
None,
74+
None,
75+
)?;
76+
77+
Ok(())
78+
}

0 commit comments

Comments
 (0)