diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-09-15 11:20:53 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-09-15 11:20:53 +0200 |
| commit | 23fccfb8f7fc901405bbc141d9ea0472a1f08ce2 (patch) | |
| tree | fedf61e730dd31a3b768a5a8af47960541c80ba6 | |
| parent | 12bb6552e49e19510b3272f8d3d1f0c6277236ff (diff) | |
| parent | 2aedc93c417ee8af682a8f5bae09ee42552fbfcb (diff) | |
| download | mullvadvpn-23fccfb8f7fc901405bbc141d9ea0472a1f08ce2.tar.xz mullvadvpn-23fccfb8f7fc901405bbc141d9ea0472a1f08ce2.zip | |
Merge branch 'add-test-that-ipv6-in-the-tunnel-works-as-expected-on-des-1088'
| -rw-r--r-- | Cargo.lock | 8 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 2 | ||||
| -rw-r--r-- | talpid-tunnel/src/tun_provider/android/ipnetwork_sub.rs | 4 | ||||
| -rw-r--r-- | talpid-types/Cargo.toml | 2 | ||||
| -rw-r--r-- | test/Cargo.lock | 83 | ||||
| -rw-r--r-- | test/connection-checker/Cargo.toml | 3 | ||||
| -rw-r--r-- | test/connection-checker/src/main.rs | 54 | ||||
| -rw-r--r-- | test/connection-checker/src/net.rs | 58 | ||||
| -rw-r--r-- | test/test-manager/Cargo.toml | 5 | ||||
| -rw-r--r-- | test/test-manager/src/container.rs | 10 | ||||
| -rw-r--r-- | test/test-manager/src/network_monitor.rs | 37 | ||||
| -rw-r--r-- | test/test-manager/src/tests/helpers.rs | 87 | ||||
| -rw-r--r-- | test/test-manager/src/tests/relay_ip_overrides.rs | 8 | ||||
| -rw-r--r-- | test/test-manager/src/tests/tunnel.rs | 121 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/linux.rs | 88 | ||||
| -rw-r--r-- | test/test-runner/src/net.rs | 2 |
17 files changed, 366 insertions, 208 deletions
diff --git a/Cargo.lock b/Cargo.lock index fed1ebf2a6..6e651f17c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2494,9 +2494,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "ipnetwork" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" dependencies = [ "serde", ] @@ -4124,9 +4124,9 @@ dependencies = [ [[package]] name = "pfctl" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44e65c0d3523afa79a600a3964c3ac0fabdabe2d7c68da624b2bb0b441b9d61" +checksum = "944d2c073758b6bda57f517cff54cf69d74eae3593fe1e9aa9918666543456a9" dependencies = [ "derive_builder", "ioctl-sys 0.8.0", diff --git a/Cargo.toml b/Cargo.toml index 5ade871d11..e33522942f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ strum = { version = "0.27" } # Networking pnet_packet = "0.35.0" -ipnetwork = "0.20" +ipnetwork = "0.21.1" tun = { version = "0.5.5", features = ["async"] } socket2 = "0.5.7" reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] } diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index f7a22ee346..d7303d9d7c 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -51,7 +51,7 @@ duct = "0.13" [target.'cfg(target_os = "macos")'.dependencies] async-trait = "0.1" -pfctl = "0.6.1" +pfctl = "0.7.0" system-configuration = "0.5.1" hickory-proto = { workspace = true } hickory-server = { workspace = true, features = ["resolver"] } diff --git a/talpid-tunnel/src/tun_provider/android/ipnetwork_sub.rs b/talpid-tunnel/src/tun_provider/android/ipnetwork_sub.rs index 44f9160ef1..272a28a911 100644 --- a/talpid-tunnel/src/tun_provider/android/ipnetwork_sub.rs +++ b/talpid-tunnel/src/tun_provider/android/ipnetwork_sub.rs @@ -40,11 +40,11 @@ impl AbstractIpNetwork for Ipv4Network { } fn mask(self) -> Self::Representation { - Ipv4Network::mask(self).into() + Ipv4Network::mask(&self).into() } fn network(self) -> Self::Representation { - Ipv4Network::network(self).into() + Ipv4Network::network(&self).into() } fn prefix(self) -> u8 { diff --git a/talpid-types/Cargo.toml b/talpid-types/Cargo.toml index 3933e9ecf0..188ea92237 100644 --- a/talpid-types/Cargo.toml +++ b/talpid-types/Cargo.toml @@ -12,7 +12,7 @@ workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } -ipnetwork = { workspace = true } +ipnetwork = { workspace = true, features = ["serde"] } base64 = "0.22.0" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "zeroize", "getrandom"] } thiserror = { workspace = true } diff --git a/test/Cargo.lock b/test/Cargo.lock index fe46d24b8c..263e29b763 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -494,33 +494,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] -name = "color-eyre" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -550,9 +523,8 @@ dependencies = [ name = "connection-checker" version = "0.0.0" dependencies = [ + "anyhow", "clap", - "color-eyre", - "eyre", "ping", "reqwest", "serde", @@ -749,6 +721,12 @@ dependencies = [ ] [[package]] +name = "duplicate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" + +[[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -906,16 +884,6 @@ dependencies = [ ] [[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] name = "fast-socks5" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1625,12 +1593,6 @@ dependencies = [ ] [[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - -[[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1730,9 +1692,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "ipnetwork" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" dependencies = [ "serde", ] @@ -2379,6 +2341,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "openssl-src" +version = "300.5.2+3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +dependencies = [ + "cc", +] + +[[package]] name = "openssl-sys" version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2386,6 +2357,7 @@ checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2416,12 +2388,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "owo-colors" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" - -[[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3759,6 +3725,7 @@ dependencies = [ "colored", "data-encoding-macro", "dirs", + "duplicate", "env_logger", "futures", "glob", @@ -4284,16 +4251,6 @@ dependencies = [ ] [[package]] -name = "tracing-error" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] name = "tracing-opentelemetry" version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/test/connection-checker/Cargo.toml b/test/connection-checker/Cargo.toml index a0b5e7fbae..16ecdc3059 100644 --- a/test/connection-checker/Cargo.toml +++ b/test/connection-checker/Cargo.toml @@ -11,9 +11,8 @@ rust-version.workspace = true workspace = true [dependencies] +anyhow = "1.0.99" clap = { workspace = true, features = ["derive"] } -color-eyre = "0.6.2" -eyre = "0.6.12" ping = "0.5.2" reqwest = { version = "0.12.23", default-features = false, features = ["blocking", "rustls-tls", "json"] } serde = { workspace = true, features = ["derive"] } diff --git a/test/connection-checker/src/main.rs b/test/connection-checker/src/main.rs index 3c8a3c1c0c..19e2eddbec 100644 --- a/test/connection-checker/src/main.rs +++ b/test/connection-checker/src/main.rs @@ -1,5 +1,5 @@ +use anyhow::{Context, anyhow}; use clap::Parser; -use eyre::{Context, eyre}; use reqwest::blocking::Client; use serde::Deserialize; use std::{io::stdin, time::Duration}; @@ -9,24 +9,23 @@ use connection_checker::{ net::{send_ping, send_tcp, send_udp}, }; -fn main() -> eyre::Result<()> { +fn main() { 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)?; + if line.is_err() { + break; + }; + test_connection(&opt); } } else { - test_connection(&opt)?; + test_connection(&opt); } - - Ok(()) } -fn test_connection(opt: &Opt) -> eyre::Result<bool> { +fn test_connection(opt: &Opt) { if let Some(destination) = opt.leak { if opt.leak_tcp { let _ = send_tcp(opt, destination); @@ -38,11 +37,11 @@ fn test_connection(opt: &Opt) -> eyre::Result<bool> { let _ = send_ping(opt, destination.ip()); } } - am_i_mullvad(opt) + am_i_mullvad(opt); } /// Check if connected to Mullvad and print the result to stdout -fn am_i_mullvad(opt: &Opt) -> eyre::Result<bool> { +fn am_i_mullvad(opt: &Opt) { #[derive(Debug, Deserialize)] struct Response { ip: String, @@ -52,24 +51,29 @@ fn am_i_mullvad(opt: &Opt) -> eyre::Result<bool> { let url = &opt.url; let client = Client::new(); - let response: Response = client + let result: Result<Response, _> = client .get(url) .timeout(Duration::from_secs(opt.timeout)) .send() .and_then(|r| r.json()) - .wrap_err_with(|| eyre!("Failed to GET {url}"))?; + .with_context(|| anyhow!("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) + match result { + Ok(response) => { + if let Some(server) = &response.mullvad_exit_ip_hostname { + println!( + "You are connected to Mullvad (server {}). Your IP address is {}", + server, response.ip + ); + } else { + println!( + "You are not connected to Mullvad. Your IP address is {}", + response.ip + ); + } + } + Err(e) => { + println!("Error: {e}"); + } } } diff --git a/test/connection-checker/src/net.rs b/test/connection-checker/src/net.rs index 8b8136846f..e087ad50b5 100644 --- a/test/connection-checker/src/net.rs +++ b/test/connection-checker/src/net.rs @@ -1,67 +1,67 @@ -use eyre::{Context, eyre}; +use anyhow::{Context, anyhow}; use std::{ io::Write, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, 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); +pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> anyhow::Result<()> { + eprintln!("Leaking TCP packets to {destination}"); - let family = match &destination { - SocketAddr::V4(_) => socket2::Domain::IPV4, - SocketAddr::V6(_) => socket2::Domain::IPV6, + let (family, bind_address) = match &destination { + SocketAddr::V4(_) => (socket2::Domain::IPV4, IpAddr::from(Ipv4Addr::UNSPECIFIED)), + SocketAddr::V6(_) => (socket2::Domain::IPV6, IpAddr::from(Ipv6Addr::UNSPECIFIED)), }; - let sock = socket2::Socket::new(family, socket2::Type::STREAM, Some(socket2::Protocol::TCP)) - .wrap_err(eyre!("Failed to create TCP socket"))?; + let bind_address: SocketAddr = SocketAddr::new(bind_address, 0); - eprintln!("Leaking TCP packets to {destination}"); + let sock = socket2::Socket::new(family, socket2::Type::STREAM, Some(socket2::Protocol::TCP)) + .context(anyhow!("Failed to create TCP socket"))?; - sock.bind(&socket2::SockAddr::from(bind_addr)) - .wrap_err(eyre!("Failed to bind TCP socket to {bind_addr}"))?; + sock.bind(&socket2::SockAddr::from(bind_address)) + .context(anyhow!("Failed to bind TCP socket to {bind_address}"))?; let timeout = Duration::from_secs(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}"))?; + .context(anyhow!("Failed to connect to {destination}"))?; let mut stream = std::net::TcpStream::from(sock); stream .write_all(opt.payload.as_bytes()) - .wrap_err(eyre!("Failed to send message to {destination}"))?; + .context(anyhow!("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); - +pub fn send_udp(opt: &Opt, destination: SocketAddr) -> Result<(), anyhow::Error> { eprintln!("Leaking UDP packets to {destination}"); - let family = match &destination { - SocketAddr::V4(_) => socket2::Domain::IPV4, - SocketAddr::V6(_) => socket2::Domain::IPV6, + let (family, bind_address) = match &destination { + SocketAddr::V4(_) => (socket2::Domain::IPV4, IpAddr::from(Ipv4Addr::UNSPECIFIED)), + SocketAddr::V6(_) => (socket2::Domain::IPV6, IpAddr::from(Ipv6Addr::UNSPECIFIED)), }; + let bind_address: SocketAddr = SocketAddr::new(bind_address, 0); + let sock = socket2::Socket::new(family, socket2::Type::DGRAM, Some(socket2::Protocol::UDP)) - .wrap_err("Failed to create UDP socket")?; + .context("Failed to create UDP socket")?; - sock.bind(&socket2::SockAddr::from(bind_addr)) - .wrap_err(eyre!("Failed to bind UDP socket to {bind_addr}"))?; + sock.bind(&socket2::SockAddr::from(bind_address)) + .context(anyhow!("Failed to bind UDP socket to {bind_address}"))?; let std_socket = std::net::UdpSocket::from(sock); std_socket .send_to(opt.payload.as_bytes(), destination) - .wrap_err(eyre!("Failed to send message to {destination}"))?; + .context(anyhow!("Failed to send message to {destination}"))?; Ok(()) } #[cfg(target_os = "windows")] -pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { +pub fn send_ping(opt: &Opt, destination: IpAddr) -> anyhow::Result<()> { eprintln!("Leaking ICMP packets to {destination}"); ping::ping( @@ -77,7 +77,7 @@ pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { } #[cfg(target_os = "macos")] -pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { +pub fn send_ping(opt: &Opt, destination: IpAddr) -> anyhow::Result<()> { eprintln!("Leaking ICMP packets to {destination}"); // On macOS, use dgramsock (SOCK_DGRAM) instead of the default sock type (SOCK_RAW), @@ -98,7 +98,7 @@ pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { // SOCK_DGRAM sockets. We use the ping command (which has capabilities/setuid set) to get around // that. #[cfg(target_os = "linux")] -pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { +pub fn send_ping(opt: &Opt, destination: IpAddr) -> anyhow::Result<()> { eprintln!("Leaking ICMP packets to {destination}"); let mut cmd = std::process::Command::new("ping"); @@ -107,7 +107,7 @@ pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { cmd.args(["-c", "1", "-W", &timeout_sec, &destination.to_string()]); - let output = cmd.output().wrap_err(eyre!( + let output = cmd.output().context(anyhow!( "Failed to execute ping for destination {destination}" ))?; @@ -121,7 +121,7 @@ pub fn send_ping(opt: &Opt, destination: IpAddr) -> eyre::Result<()> { std::str::from_utf8(&output.stderr).unwrap_or("invalid utf8") ); - return Err(eyre!("ping for destination {destination} failed")); + return Err(anyhow!("ping for destination {destination} failed")); } Ok(()) diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml index 2225f46ad9..bc7ab9515d 100644 --- a/test/test-manager/Cargo.toml +++ b/test/test-manager/Cargo.toml @@ -21,7 +21,7 @@ tokio-serial = { workspace = true } thiserror = { workspace = true } bytes = { workspace = true } test_macro = { path = "./test_macro" } -ipnetwork = "0.20" +ipnetwork = "0.21.1" inventory = "0.3" data-encoding-macro = "0.1.12" itertools = "0.10.5" @@ -61,10 +61,11 @@ mullvad-types = { path = "../../mullvad-types" } mullvad-version = { path = "../../mullvad-version" } talpid-types = { path = "../../talpid-types" } -ssh2 = "0.9.5" +ssh2 = { version = "0.9.5", features = ["vendored-openssl"] } nix = { workspace = true } socket2 = { workspace = true } +duplicate = { version = "2.0.0", default-features = false } [target.'cfg(target_os = "macos")'.dependencies] tun = "0.5.1" diff --git a/test/test-manager/src/container.rs b/test/test-manager/src/container.rs index 88ba7d9a6b..9560f48b3e 100644 --- a/test/test-manager/src/container.rs +++ b/test/test-manager/src/container.rs @@ -11,7 +11,15 @@ pub async fn relaunch_with_rootlesskit(vnc_port: Option<u16>) { } let mut cmd = Command::new("rootlesskit"); - cmd.args(["--net", "slirp4netns", "--copy-up=/etc"]); + cmd.args([ + "--net", + "slirp4netns", + "--ipv6", + // A higher MTU breaks IPv6 + "--mtu", + "1500", + "--copy-up=/etc", + ]); if let Some(port) = vnc_port { log::debug!("VNC port: {port} -> 5901/tcp"); diff --git a/test/test-manager/src/network_monitor.rs b/test/test-manager/src/network_monitor.rs index e89c33fb34..64e1414b5c 100644 --- a/test/test-manager/src/network_monitor.rs +++ b/test/test-manager/src/network_monitor.rs @@ -284,29 +284,26 @@ async fn start_packet_monitor_for_interface( break Ok(monitor_result); } maybe_next_packet = next_packet => { - match maybe_next_packet { - Some(Ok(packet))=> { - if let Some(packet) = packet { - if !filter_fn(&packet) { - log::trace!("{interface} \"{packet:?}\" does not match closure conditions"); - monitor_result.discarded_packets = - monitor_result.discarded_packets.saturating_add(1); - } else { - log::trace!("{interface} \"{packet:?}\" matches closure conditions"); + let Some(Ok(packet)) = maybe_next_packet else { + log::error!("lost packet stream"); + break Err(MonitorUnexpectedlyStopped); + }; - let should_continue = should_continue_fn(&packet); + let Some(packet) = packet else { continue }; - monitor_result.packets.push(packet); + if !filter_fn(&packet) { + log::trace!("{interface} \"{packet:?}\" does not match closure conditions"); + monitor_result.discarded_packets = + monitor_result.discarded_packets.saturating_add(1); + } else { + log::trace!("{interface} \"{packet:?}\" matches closure conditions"); - if !should_continue { - break Ok(monitor_result); - } - } - } - } - _ => { - log::error!("lost packet stream"); - break Err(MonitorUnexpectedlyStopped); + let should_continue = should_continue_fn(&packet); + + monitor_result.packets.push(packet); + + if !should_continue { + break Ok(monitor_result); } } } diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index 55b7b2384e..ac3a16fc9a 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -1025,7 +1025,7 @@ pub struct ConnCheckerHandle<'a> { pub struct ConnectionStatus { /// True if <https://am.i.mullvad.net/> reported we are connected. - am_i_mullvad: bool, + am_i_mullvad: anyhow::Result<bool>, /// True if we sniffed TCP packets going outside the tunnel. leaked_tcp: bool, @@ -1041,7 +1041,7 @@ impl ConnChecker { pub fn new( rpc: ServiceClient, mullvad_client: MullvadProxyClient, - leak_destination: SocketAddr, + leak_destination: impl Into<SocketAddr>, ) -> Self { let artifacts_dir = &TEST_CONFIG.artifacts_dir; let executable_path = match TEST_CONFIG.os { @@ -1052,7 +1052,7 @@ impl ConnChecker { Self { rpc, mullvad_client, - leak_destination, + leak_destination: leak_destination.into(), split: false, executable_path, payload: None, @@ -1072,6 +1072,11 @@ impl ConnChecker { log::debug!("spawning connection checker"); let opts = { + let ipvx = match self.leak_destination { + SocketAddr::V4(..) => "ipv4", + SocketAddr::V6(..) => "ipv6", + }; + let mut args = [ "--interactive", "--timeout", @@ -1085,7 +1090,7 @@ impl ConnChecker { "--leak-udp", "--leak-icmp", "--url", - &format!("https://am.i.{}/json", TEST_CONFIG.mullvad_host), + &format!("https://{ipvx}.am.i.{}/json", TEST_CONFIG.mullvad_host), ] .map(String::from) .to_vec(); @@ -1178,28 +1183,64 @@ impl ConnCheckerHandle<'_> { self.checker.unsplit().await } + /// Assert that traffic is blocked and that no packets are leaked. + pub async fn assert_blocked(&mut self) -> anyhow::Result<()> { + log::info!("checking that connection is blocked"); + async { + let status = self.check_connection().await?; + ensure!(status.am_i_mullvad.is_err()); + ensure!(!status.leaked_tcp); + ensure!(!status.leaked_udp); + ensure!(!status.leaked_icmp); + Ok(()) + } + .await + .with_context(|| { + anyhow!( + "assert_secure failed (leak_destination={})", + self.checker.leak_destination, + ) + }) + } + /// Assert that traffic is flowing through the Mullvad tunnel and that no packets are leaked. pub async fn assert_secure(&mut self) -> anyhow::Result<()> { log::info!("checking that connection is secure"); - let status = self.check_connection().await?; - ensure!(status.am_i_mullvad); - ensure!(!status.leaked_tcp); - ensure!(!status.leaked_udp); - ensure!(!status.leaked_icmp); - - Ok(()) + async { + let status = self.check_connection().await?; + ensure!(status.am_i_mullvad?); + ensure!(!status.leaked_tcp); + ensure!(!status.leaked_udp); + ensure!(!status.leaked_icmp); + Ok(()) + } + .await + .with_context(|| { + anyhow!( + "assert_secure failed (leak_destination={})", + self.checker.leak_destination, + ) + }) } /// Assert that traffic is NOT flowing through the Mullvad tunnel and that packets ARE leaked. pub async fn assert_insecure(&mut self) -> anyhow::Result<()> { log::info!("checking that connection is not secure"); - let status = self.check_connection().await?; - ensure!(!status.am_i_mullvad); - ensure!(status.leaked_tcp); - ensure!(status.leaked_udp); - ensure!(status.leaked_icmp); - - Ok(()) + async { + let status = self.check_connection().await?; + ensure!(!status.am_i_mullvad?); + ensure!(status.leaked_tcp); + ensure!(status.leaked_udp); + ensure!(status.leaked_icmp); + Ok(()) + } + .await + .with_context(|| { + anyhow!( + "assert_secure failed (leak_destination={})", + self.checker.leak_destination, + ) + }) } pub async fn check_connection(&mut self) -> anyhow::Result<ConnectionStatus> { @@ -1228,8 +1269,10 @@ impl ConnCheckerHandle<'_> { .await .map_err(|_e| anyhow!("Packet monitor unexpectedly stopped"))?; + let leak_destination = self.checker.leak_destination; + Ok(ConnectionStatus { - am_i_mullvad: parse_am_i_mullvad(line)?, + am_i_mullvad: parse_am_i_mullvad(line), leaked_tcp: (monitor_result.packets.iter()) .any(|pkt| pkt.protocol == IpNextHeaderProtocols::Tcp), @@ -1237,8 +1280,10 @@ impl ConnCheckerHandle<'_> { leaked_udp: (monitor_result.packets.iter()) .any(|pkt| pkt.protocol == IpNextHeaderProtocols::Udp), - leaked_icmp: (monitor_result.packets.iter()) - .any(|pkt| pkt.protocol == IpNextHeaderProtocols::Icmp), + leaked_icmp: (monitor_result.packets.iter()).any(|pkt| match leak_destination { + SocketAddr::V4(..) => pkt.protocol == IpNextHeaderProtocols::Icmp, + SocketAddr::V6(..) => pkt.protocol == IpNextHeaderProtocols::Icmpv6, + }), }) } diff --git a/test/test-manager/src/tests/relay_ip_overrides.rs b/test/test-manager/src/tests/relay_ip_overrides.rs index 36a9c828f8..786060bf23 100644 --- a/test/test-manager/src/tests/relay_ip_overrides.rs +++ b/test/test-manager/src/tests/relay_ip_overrides.rs @@ -6,7 +6,7 @@ use super::{ }; use crate::{ tests::config::TEST_CONFIG, - vm::{self, network::linux::TEST_SUBNET}, + vm::{self, network::linux::TEST_SUBNET_IPV4}, }; use anyhow::{Context, anyhow, bail, ensure}; use futures::FutureExt; @@ -330,9 +330,10 @@ async fn pick_a_relay( /// Spawn a TCP socket that forwards packets between `destination` and anyone that connects to it. /// +/// The proxy socket will be bound to [TEST_SUBNET_V4]. /// Returns a handle that will stop the proxy when dropped. async fn spawn_tcp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<AbortOnDrop<()>> { - let socket = TcpListener::bind((TEST_SUBNET.ip(), port)).await?; + let socket = TcpListener::bind((TEST_SUBNET_IPV4.ip(), port)).await?; log::info!("started TCP proxy to {destination} on port {port}"); async fn client_task(destination: SocketAddr, mut client: TcpStream) -> anyhow::Result<()> { @@ -383,9 +384,10 @@ async fn spawn_tcp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<A /// /// NOTE: Doesn't work with multiple concurrent clients. /// +/// The proxy socket will be bound to [TEST_SUBNET_V4]. /// Returns a handle that will stop the proxy when dropped. async fn spawn_udp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<AbortOnDrop<()>> { - let socket = UdpSocket::bind((TEST_SUBNET.ip(), port)).await?; + let socket = UdpSocket::bind((TEST_SUBNET_IPV4.ip(), port)).await?; log::info!("started UDP proxy to {destination} on port {port}"); async fn proxy_task(destination: SocketAddr, socket: UdpSocket) -> anyhow::Result<()> { diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index 5e7e1eb383..828ced73a8 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -5,10 +5,13 @@ use super::{ }; use crate::{ network_monitor::{MonitorOptions, start_packet_monitor}, - tests::helpers::{geoip_lookup_with_retries, login_with_retries, update_relay_constraints}, + tests::helpers::{ + ConnChecker, geoip_lookup_with_retries, login_with_retries, update_relay_constraints, + }, }; use anyhow::{Context, ensure}; +use duplicate::duplicate_item; use mullvad_management_interface::MullvadProxyClient; use mullvad_relay_selector::query::builder::RelayQueryBuilder; use mullvad_types::{ @@ -18,9 +21,12 @@ use mullvad_types::{ }, wireguard, }; -use std::net::SocketAddr; +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + str::FromStr, +}; use talpid_types::net::{ - TransportProtocol, TunnelType, + IpVersion, TransportProtocol, TunnelType, proxy::{CustomProxy, Socks5Local, Socks5Remote}, }; use test_macro::test_function; @@ -82,21 +88,29 @@ pub async fn test_openvpn_tunnel( /// Set up a WireGuard tunnel. /// This test fails if a working tunnel cannot be set up. /// WARNING: This test will fail if host has something bound to port 53 such as a connected Mullvad +#[duplicate_item( + VX test_wireguard_tunnel_ipvx; + [ V4 ] [ test_wireguard_tunnel_ipv4 ]; + [ V6 ] [ test_wireguard_tunnel_ipv6 ]; +)] #[test_function] -pub async fn test_wireguard_tunnel( +pub async fn test_wireguard_tunnel_ipvx( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, ) -> Result<(), Error> { // TODO: observe UDP traffic on the expected destination/port (only) - // TODO: IPv6 + let ip_version = IpVersion::VX; const PORTS: [(u16, bool); 3] = [(53, true), (51820, true), (1, false)]; for (port, should_succeed) in PORTS { log::info!("Connect to WireGuard endpoint on port {port}"); - let query = RelayQueryBuilder::wireguard().port(port).build(); + let query = RelayQueryBuilder::wireguard() + .port(port) + .ip_version(ip_version) + .build(); apply_settings_from_relay_query(&mut mullvad_client, query) .await @@ -122,6 +136,67 @@ pub async fn test_wireguard_tunnel( Ok(()) } +/// Set up a WireGuard tunnel and check whether in-tunnel IPv6 works. +/// WARNING: This test will fail if host has something bound to port 53 such as a connected Mullvad +#[duplicate_item( + VX test_wireguard_ipv6_in_ipvx; + [ V4 ] [ test_wireguard_ipv6_in_ipv4 ]; + [ V6 ] [ test_wireguard_ipv6_in_ipv6 ]; +)] +#[test_function] +pub async fn test_wireguard_ipv6_in_ipvx( + _: TestContext, + rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> Result<(), Error> { + let ip_version = IpVersion::VX; + + let mut conn_checker_v4 = ConnChecker::new( + rpc.clone(), + mullvad_client.clone(), + (Ipv4Addr::new(1, 1, 1, 1), 53), + ); + + let mut conn_checker_v6 = ConnChecker::new( + rpc.clone(), + mullvad_client.clone(), + (Ipv6Addr::from_str("2606:4700:4700::1111").unwrap(), 53), + ); + + let mut conn_checker_v4 = conn_checker_v4.spawn().await?; + let mut conn_checker_v6 = conn_checker_v6.spawn().await?; + + conn_checker_v4.assert_insecure().await?; + conn_checker_v6.assert_insecure().await?; + + log::info!("Connect to WireGuard endpoint"); + + let query = RelayQueryBuilder::wireguard() + .ip_version(ip_version) + .build(); + apply_settings_from_relay_query(&mut mullvad_client, query) + .await + .unwrap(); + + // Test with in-tunnel IPv6 enabled + mullvad_client.set_enable_ipv6(true).await?; + let connection_result = connect_and_wait(&mut mullvad_client).await; + assert!(connection_result.is_ok()); + conn_checker_v4.assert_secure().await?; + conn_checker_v6.assert_secure().await?; + + // Test with in-tunnel IPv6 disabled + mullvad_client.set_enable_ipv6(false).await?; + let connection_result = connect_and_wait(&mut mullvad_client).await; + assert!(connection_result.is_ok()); + conn_checker_v4.assert_secure().await?; + conn_checker_v6.assert_blocked().await?; // ipv6 mustnt leak + + disconnect_and_wait(&mut mullvad_client).await?; + + Ok(()) +} + /// Use udp2tcp obfuscation. This test connects to a WireGuard relay over TCP. It fails if no /// outgoing TCP traffic to the relay is observed on the expected port. #[test_function] @@ -202,14 +277,24 @@ pub async fn test_wireguard_over_shadowsocks( /// Use QUIC obfuscation. This tests whether the daemon can establish a QUIC connection. /// Note that this doesn't verify that the outgoing traffic looks like http traffic (even though it /// doesn't sound too difficult to do?). +#[duplicate_item( + VX test_wireguard_over_quic_ipvx; + [ V4 ] [ test_wireguard_over_quic_ipv4 ]; + [ V6 ] [ test_wireguard_over_quic_ipv6 ]; +)] #[test_function] -pub async fn test_wireguard_over_quic( +pub async fn test_wireguard_over_quic_ipvx( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, ) -> anyhow::Result<()> { + let ip_version = IpVersion::VX; + log::info!("Enable QUIC as obfuscation method"); - let query = RelayQueryBuilder::wireguard().quic().build(); + let query = RelayQueryBuilder::wireguard() + .ip_version(ip_version) + .quic() + .build(); apply_settings_from_relay_query(&mut mullvad_client, query).await?; log::info!("Connect to WireGuard via QUIC endpoint"); @@ -553,18 +638,26 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel( /// /// This is not testing any of the individual components, just whether the daemon can connect when /// all of these features are combined. +#[duplicate_item( + VX test_quantum_resistant_multihop_shadowsocks_tunnel_ipvx; + [ V4 ] [ test_quantum_resistant_multihop_shadowsocks_tunnel_ipv4 ]; + [ V6 ] [ test_quantum_resistant_multihop_shadowsocks_tunnel_ipv6 ]; +)] #[test_function] -pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel( +pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel_ipvx( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, ) -> anyhow::Result<()> { + let ip_version = IpVersion::VX; + mullvad_client .set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On) .await .context("Failed to enable PQ tunnels")?; let query = RelayQueryBuilder::wireguard() + .ip_version(ip_version) .multihop() .shadowsocks() .build(); @@ -587,12 +680,19 @@ pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel( /// /// This is not testing any of the individual components, just whether the daemon can connect when /// all of these features are combined. +#[duplicate_item( + VX test_quantum_resistant_multihop_quic_tunnel_ipvx; + [ V4 ] [ test_quantum_resistant_multihop_quic_tunnel_ipv4 ]; + [ V6 ] [ test_quantum_resistant_multihop_quic_tunnel_ipv6 ]; +)] #[test_function] -pub async fn test_quantum_resistant_multihop_quic_tunnel( +pub async fn test_quantum_resistant_multihop_quic_tunnel_ipvx( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, ) -> anyhow::Result<()> { + let ip_version = IpVersion::VX; + mullvad_client // TODO: Why is this needed, exactly? .set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On) @@ -600,6 +700,7 @@ pub async fn test_quantum_resistant_multihop_quic_tunnel( .context("Failed to enable PQ tunnels")?; let query = RelayQueryBuilder::wireguard() + .ip_version(ip_version) .quantum_resistant() .multihop() .quic() diff --git a/test/test-manager/src/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs index ef7eebeb2a..b6df187dcc 100644 --- a/test/test-manager/src/vm/network/linux.rs +++ b/test/test-manager/src/vm/network/linux.rs @@ -1,24 +1,31 @@ -use ipnetwork::Ipv4Network; +use ipnetwork::{Ipv4Network, Ipv6Network}; use std::{ ffi::OsStr, io, - net::{IpAddr, Ipv4Addr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::RangeInclusive, process::Stdio, str::FromStr, - sync::LazyLock, }; use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::{Child, Command}, }; -/// (Contained) test subnet for the test runner: 172.29.1.1/24 -pub static TEST_SUBNET: LazyLock<Ipv4Network> = - LazyLock::new(|| Ipv4Network::new(Ipv4Addr::new(172, 29, 1, 1), 24).unwrap()); -/// Range of IPs returned by the DNS server: TEST_SUBNET_DHCP_FIRST to TEST_SUBNET_DHCP_LAST -pub const TEST_SUBNET_DHCP_FIRST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 2); -/// Range of IPs returned by the DNS server: TEST_SUBNET_DHCP_FIRST to TEST_SUBNET_DHCP_LAST -pub const TEST_SUBNET_DHCP_LAST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 128); +/// (Contained) IPv4 subnet for the test runner: 172.29.1.1/24 +pub const TEST_SUBNET_IPV4: Ipv4Network = + Ipv4Network::new_checked(Ipv4Addr::new(172, 29, 1, 1), 24).unwrap(); + +/// IPv4 range returned by the DHCP server. +pub const TEST_SUBNET_IPV4_DHCP: RangeInclusive<Ipv4Addr> = + Ipv4Addr::new(172, 29, 1, 2)..=Ipv4Addr::new(172, 29, 1, 128); + +/// IPv6 subnet for the test runner. "0xfd multest" +pub const TEST_SUBNET_IPV6: Ipv6Network = Ipv6Network::new_checked( + Ipv6Addr::new(0xfd6d, 0x756c, 0x7465, 0x7374, 0, 0, 0, 1), + 64, +) +.unwrap(); /// Bridge interface on the host pub(crate) const BRIDGE_NAME: &str = "br-mullvadtest"; @@ -97,26 +104,44 @@ struct DhcpProcHandle { _pid_file: async_tempfile::TempFile, } +/// IPv6-support in `rootlesskit` is experimental, and addresses are not automatically assigned. +/// This function will assigned an IPv6 address to the TAP interface, and set up routes. +async fn fix_ipv6() -> Result<()> { + let tap = "tap0"; // TAP-device that connects to slirp2netns + let addr = "fd00::1337/64"; // our address within the slirp2netns subnet + let gateway = "fd00::2"; // slirp2netns gateway + let _dns = "fd00::3"; // slirp2netns dns + + run_ip_cmd(["-6", "addr", "add", addr, "dev", tap]).await?; + run_ip_cmd(["-6", "route", "add", "default", "via", gateway, "dev", tap]).await?; + Ok(()) +} + /// Create a bridge network and hosts pub async fn setup_test_network() -> Result<NetworkHandle> { + fix_ipv6().await?; + enable_forwarding().await?; - let test_subnet = TEST_SUBNET.to_string(); + let test_subnet_v4 = TEST_SUBNET_IPV4.to_string(); + let test_subnet_v6 = TEST_SUBNET_IPV6.to_string(); - log::debug!("Create bridge network: dev {BRIDGE_NAME}, net {test_subnet}"); + log::debug!("Create bridge network: dev {BRIDGE_NAME}, net {test_subnet_v4}"); run_ip_cmd(["link", "add", BRIDGE_NAME, "type", "bridge"]).await?; - run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet]).await?; + run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet_v4]).await?; + run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet_v6]).await?; run_ip_cmd(["link", "set", "dev", BRIDGE_NAME, "up"]).await?; log::debug!("Masquerade traffic from bridge to internet"); run_nft(&format!( " -table ip mullvad_test_nat {{ +table inet mullvad_test_nat {{ chain POSTROUTING {{ type nat hook postrouting priority srcnat; policy accept; - ip saddr {test_subnet} ip daddr != {test_subnet} counter masquerade + ip saddr {test_subnet_v4} ip daddr != {test_subnet_v4} counter masquerade + ip6 saddr {test_subnet_v6} ip6 daddr != {test_subnet_v6} counter masquerade }} }}" )) @@ -189,8 +214,11 @@ impl NetworkHandle { } } +/// Run dnsmasq as a DHCP server. +/// +/// dnsmasq will serve IPv4 addresses within the range [TEST_SUBNET_IPV4_DHCP] using regular DHCP. +/// It will also advertise SLAAC for IPv6 within [TEST_SUBNET_IPV6]. async fn start_dnsmasq() -> Result<DhcpProcHandle> { - // dnsmasq -i BRIDGE_NAME -F TEST_SUBNET_DHCP_FIRST,TEST_SUBNET_DHCP_LAST ... let mut cmd = Command::new("dnsmasq"); cmd.kill_on_drop(true); @@ -198,13 +226,21 @@ async fn start_dnsmasq() -> Result<DhcpProcHandle> { cmd.stderr(Stdio::piped()); cmd.args([ + "--conf-file=/dev/null", "--bind-interfaces", - "-C", - "/dev/null", - "-i", - BRIDGE_NAME, - "-F", - &format!("{TEST_SUBNET_DHCP_FIRST},{TEST_SUBNET_DHCP_LAST}"), + &format!("--interface={BRIDGE_NAME}"), + // IPv4 + &format!( + "--dhcp-range={},{}", + TEST_SUBNET_IPV4_DHCP.start(), + TEST_SUBNET_IPV4_DHCP.end(), + ), + // IPv6 + &format!( + "--dhcp-range={prefix},slaac,{prefix_len}", + prefix = TEST_SUBNET_IPV6.ip(), + prefix_len = TEST_SUBNET_IPV6.prefix() + ), "--no-hosts", "--keep-in-foreground", "--log-facility=-", @@ -343,5 +379,13 @@ async fn enable_forwarding() -> Result<()> { if !output.status.success() { return Err(Error::SysctlFailed(output.status.code().unwrap())); } + + let mut cmd = Command::new("sysctl"); + cmd.arg("net.ipv6.conf.all.forwarding=1"); + let output = cmd.output().await.map_err(Error::SysctlStart)?; + if !output.status.success() { + return Err(Error::SysctlFailed(output.status.code().unwrap())); + } + Ok(()) } diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index 967d3c8c32..fe100f22c7 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -308,7 +308,7 @@ pub fn get_interface_mtu(interface_name: &str) -> Result<u16, test_rpc::Error> { // TODO: define SIOCGIFMTU for macos // SAFETY: SIOCGIFMTU expects an ifreq, and the socket is valid - if unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFMTU, &mut ifr) } < 0 { + if unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFMTU as libc::Ioctl, &mut ifr) } < 0 { let e = std::io::Error::last_os_error(); log::error!("{}", e); |
