Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement leak checker in daemon #7344

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
469 changes: 380 additions & 89 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ members = [
"tunnel-obfuscation",
"wireguard-go-rs",
"windows-installer",
"leak-checker",
]

# The default members may exclude packages that cannot be built for all targets, or that do not always
# need to be built
default-members = [
Expand Down Expand Up @@ -113,6 +115,7 @@ hickory-server = { version = "0.24.2", features = ["resolver"] }
tokio = { version = "1.8" }
parity-tokio-ipc = "0.9"
futures = "0.3.15"

# Tonic and related crates
tonic = "0.12.3"
tonic-build = { version = "0.10.0", default-features = false }
Expand All @@ -137,6 +140,7 @@ serde = "1.0.204"
serde_json = "1.0.122"

ipnetwork = "0.20"
socket2 = "0.5.7"

# Test dependencies
proptest = "1.4"
Expand Down
34 changes: 34 additions & 0 deletions leak-checker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "leak-checker"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true

[dependencies]
log.workspace = true
eyre = "0.6.12"
socket2 = { version = "0.5.7", features = ["all"] }
match_cfg = "0.1.0"
pnet_packet = "0.35.0"
pretty_env_logger = "0.5.0"
tokio = { workspace = true, features = ["macros", "time", "rt", "sync", "net"] }
futures.workspace = true
serde = { workspace = true, features = ["derive"] }
reqwest = { version = "0.12.9", default-features = false, features = ["json", "rustls-tls"] }
clap = { version = "*", features = ["derive"] }

[dev-dependencies]
tokio = { workspace = true, features = ["full"] }

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", features = ["net", "socket", "uio"] }

[target.'cfg(windows)'.dependencies]
windows-sys.workspace = true
talpid-windows = { path = "../talpid-windows" }

[lints]
workspace = true
36 changes: 36 additions & 0 deletions leak-checker/examples/leaker-cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use clap::{Parser, Subcommand};
use leak_checker::{am_i_mullvad::AmIMullvadOpt, traceroute::TracerouteOpt};

#[derive(Parser)]
pub struct Opt {
#[clap(subcommand)]
pub method: LeakMethod,
}

#[derive(Subcommand, Clone)]
pub enum LeakMethod {
/// Check for leaks by binding to a non-tunnel interface and probing for reachable nodes.
Traceroute(#[clap(flatten)] TracerouteOpt),

/// Ask `am.i.mullvad.net` whether you are leaking.
AmIMullvad(#[clap(flatten)] AmIMullvadOpt),
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
pretty_env_logger::formatted_builder()
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();

let opt = Opt::parse();

let leak_status = match &opt.method {
LeakMethod::Traceroute(opt) => leak_checker::traceroute::run_leak_test(opt).await,
LeakMethod::AmIMullvad(opt) => leak_checker::am_i_mullvad::run_leak_test(opt).await,
};

log::info!("Leak status: {leak_status:#?}");

Ok(())
}
16 changes: 16 additions & 0 deletions leak-checker/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Apple notes

The first packet is always dropped when a connection is routed and NATed


The NAT rules do not match up with the firewall rules in regards to the relay


```
# NAT-rule
no nat inet from any to 185.213.154.68

# FW-rule
pass out quick inet proto udp from any to 185.213.154.68 port = 49020 user = 0 keep state
```

90 changes: 90 additions & 0 deletions leak-checker/src/am_i_mullvad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use eyre::{eyre, Context};
use futures::TryFutureExt;
use match_cfg::match_cfg;
use reqwest::{Client, ClientBuilder};
use serde::Deserialize;

use crate::{LeakInfo, LeakStatus};

#[derive(Clone, clap::Args)]
pub struct AmIMullvadOpt {
/// Try to bind to a specific interface
#[clap(short, long)]
interface: Option<String>,
}

const AM_I_MULLVAD_URL: &str = "https://am.i.mullvad.net/json";

/// [try_run_leak_test], but on an error, assume we aren't leaking.
pub async fn run_leak_test(opt: &AmIMullvadOpt) -> LeakStatus {
try_run_leak_test(opt)
.await
.inspect_err(|e| log::debug!("Leak test errored, assuming no leak. {e:?}"))
.unwrap_or(LeakStatus::NoLeak)
}

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

let mut client = Client::builder();

if let Some(interface) = &opt.interface {
client = bind_client_to_interface(client, interface)?;
}

let client = client.build().wrap_err("Failed to create HTTP client")?;
let response: Response = client
.get(AM_I_MULLVAD_URL)
//.timeout(Duration::from_secs(opt.timeout))
.send()
.and_then(|r| r.json())
.await
.wrap_err_with(|| eyre!("Failed to GET {AM_I_MULLVAD_URL}"))?;

if let Some(server) = &response.mullvad_exit_ip_hostname {
log::debug!(
"You are connected to Mullvad (server {}). Your IP address is {}",
server,
response.ip
);
Ok(LeakStatus::NoLeak)
} else {
log::debug!(
"You are not connected to Mullvad. Your IP address is {}",
response.ip
);
Ok(LeakStatus::LeakDetected(LeakInfo::AmIMullvad {
ip: response.ip.parse().wrap_err("Malformed IP")?,
}))
}
}

match_cfg! {
#[cfg(target_os = "linux")] => {
fn bind_client_to_interface(
builder: ClientBuilder,
interface: &str
) -> eyre::Result<ClientBuilder> {
log::debug!("Binding HTTP client to {interface}");
Ok(builder.interface(interface))
}
}
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "android"))] => {
fn bind_client_to_interface(
builder: ClientBuilder,
interface: &str
) -> eyre::Result<ClientBuilder> {
use crate::util::get_interface_ip;

let ip = get_interface_ip(interface)?;

log::debug!("Binding HTTP client to {ip} ({interface})");
Ok(builder.local_address(ip))
}
}
}
24 changes: 24 additions & 0 deletions leak-checker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::net::IpAddr;

pub mod am_i_mullvad;
pub mod traceroute;
mod util;

#[derive(Clone, Debug)]
pub enum LeakStatus {
NoLeak,
LeakDetected(LeakInfo),
}

/// Details about how a leak happened
#[derive(Clone, Debug)]
pub enum LeakInfo {
/// Managed to reach another network node on the physical interface, bypassing firewall rules.
NodeReachableOnInterface {
reachable_nodes: Vec<IpAddr>,
interface: String,
},

/// Queried a <https://am.i.mullvad.net>, and was not mullvad.
AmIMullvad { ip: IpAddr },
}
Loading
Loading