diff options
| author | Sebastian Holmin <sebastian.holmin@mullvad.net> | 2024-03-05 12:11:39 +0100 |
|---|---|---|
| committer | Sebastian Holmin <sebastian.holmin@mullvad.net> | 2024-03-08 14:13:02 +0100 |
| commit | bee0555bc6d0a6116574af949a17ce3fe73c7d27 (patch) | |
| tree | 5208586178567d35f9d6a51ad4ef069e519e7f02 /test/test-runner/src | |
| parent | cd5f33503bfe579f418359ebd3a0c335bb16dca1 (diff) | |
| download | mullvadvpn-bee0555bc6d0a6116574af949a17ce3fe73c7d27.tar.xz mullvadvpn-bee0555bc6d0a6116574af949a17ce3fe73c7d27.zip | |
Add MTU detection integration test for Linux and Windows
Add dependency `scopeguard` for cleaning up nftables ruleset.
Diffstat (limited to 'test/test-runner/src')
| -rw-r--r-- | test/test-runner/src/main.rs | 20 | ||||
| -rw-r--r-- | test/test-runner/src/net.rs | 146 |
2 files changed, 92 insertions, 74 deletions
diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index befeeb61e0..3511d78cec 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -6,8 +6,7 @@ use std::{ path::{Path, PathBuf}, }; -use tarpc::context; -use tarpc::server::Channel; +use tarpc::{context, server::Channel}; use test_rpc::{ mullvad_daemon::{ServiceStatus, SOCKET_PATH}, net::SockHandleId, @@ -15,10 +14,10 @@ use test_rpc::{ transport::GrpcForwarder, AppTrace, Service, }; -use tokio::sync::broadcast::error::TryRecvError; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, process::Command, + sync::broadcast::error::TryRecvError, }; use tokio_util::codec::{Decoder, LengthDelimitedCodec}; @@ -141,10 +140,13 @@ impl Service for TestServer { async fn send_ping( self, _: context::Context, - interface: Option<String>, destination: IpAddr, + interface: Option<String>, + size: usize, ) -> Result<(), test_rpc::Error> { - net::send_ping(interface.as_deref(), destination).await + net::send_ping(destination, interface.as_deref(), size) + .await + .map_err(|e| test_rpc::Error::Ping(e.to_string())) } async fn geoip_lookup( @@ -194,6 +196,14 @@ impl Service for TestServer { net::get_interface_ip(&interface) } + async fn get_interface_mtu( + self, + _: context::Context, + interface: String, + ) -> Result<u16, test_rpc::Error> { + net::get_interface_mtu(&interface) + } + async fn get_default_interface(self, _: context::Context) -> Result<String, test_rpc::Error> { Ok(net::get_default_interface().to_owned()) } diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index 1de728a744..22de4da9c9 100644 --- a/test/test-runner/src/net.rs +++ b/test/test-runner/src/net.rs @@ -4,9 +4,7 @@ use std::{ffi::CString, num::NonZeroU32}; use std::{ io::Write, net::{IpAddr, SocketAddr}, - process::Output, }; -use tokio::process::Command; pub async fn send_tcp( bind_interface: Option<String>, @@ -139,66 +137,37 @@ pub async fn send_udp( } pub async fn send_ping( - interface: Option<&str>, destination: IpAddr, + interface: Option<&str>, + size: usize, ) -> Result<(), test_rpc::Error> { - #[cfg(target_os = "windows")] - let mut source_ip = None; - #[cfg(target_os = "windows")] - if let Some(interface) = interface { - let family = match destination { - IpAddr::V4(_) => talpid_windows::net::AddressFamily::Ipv4, - IpAddr::V6(_) => talpid_windows::net::AddressFamily::Ipv6, - }; - source_ip = get_interface_ip_for_family(interface, family) - .map_err(|_error| test_rpc::Error::Syscall)?; - if source_ip.is_none() { - log::error!("Failed to obtain interface IP"); - return Err(test_rpc::Error::Ping); - } - } - - let mut cmd = Command::new("ping"); - cmd.arg(destination.to_string()); - - #[cfg(target_os = "windows")] - cmd.args(["-n", "1"]); - - #[cfg(not(target_os = "windows"))] - cmd.args(["-c", "1"]); - - match interface { - Some(interface) => { - log::info!("Pinging {destination} on interface {interface}"); - - #[cfg(target_os = "windows")] - if let Some(source_ip) = source_ip { - cmd.args(["-S", &source_ip.to_string()]); - } - - #[cfg(target_os = "linux")] - cmd.args(["-I", interface]); - - #[cfg(target_os = "macos")] - cmd.args(["-b", interface]); - } - None => log::info!("Pinging {destination}"), - } + use surge_ping::{Client, Config, PingIdentifier, PingSequence, ICMP}; - cmd.kill_on_drop(true); + const IPV4_HEADER_SIZE: usize = 20; + const ICMP_HEADER_SIZE: usize = 8; + let payload_size = size - IPV4_HEADER_SIZE - ICMP_HEADER_SIZE; + let payload: &[u8] = &vec![0; payload_size]; - cmd.spawn() - .map_err(|error| { - log::error!("Failed to spawn ping process: {error}"); - test_rpc::Error::Ping - })? - .wait_with_output() + let config = match destination { + IpAddr::V4(_) => Config::builder(), + IpAddr::V6(_) => Config::builder().kind(ICMP::V6), + }; + let config = if let Some(interface) = interface { + let interface_ip = get_interface_ip(interface)?; + config.interface(interface).bind((interface_ip, 0).into()) + } else { + config + }; + let client = Client::new(&config.build()).map_err(|e| test_rpc::Error::Ping(e.to_string()))?; + let mut pinger = client + .pinger(destination, PingIdentifier(rand::random())) + .await; + pinger + .ping(PingSequence(0), payload) .await - .map_err(|error| { - log::error!("Failed to wait on ping: {error}"); - test_rpc::Error::Ping - }) - .and_then(|output| result_from_output("ping", output, test_rpc::Error::Ping)) + .map_err(|e| test_rpc::Error::Ping(e.to_string()))?; + + Ok(()) } #[cfg(unix)] @@ -273,19 +242,58 @@ pub fn get_default_interface() -> &'static str { "en0" } -fn result_from_output<E>(action: &'static str, output: Output, err: E) -> Result<(), E> { - if output.status.success() { - return Ok(()); +#[cfg(target_os = "macos")] +pub fn get_interface_mtu(_interface_name: &str) -> Result<u16, test_rpc::Error> { + todo!("Implement setting MTU on macOS") +} + +#[cfg(target_os = "linux")] +pub fn get_interface_mtu(interface_name: &str) -> Result<u16, test_rpc::Error> { + use std::os::fd::AsRawFd; + + let sock = socket2::Socket::new( + socket2::Domain::IPV4, + socket2::Type::STREAM, + Some(socket2::Protocol::TCP), + ) + .map_err(|e| test_rpc::Error::Io(e.to_string()))?; + + let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() }; + if interface_name.len() >= ifr.ifr_name.len() { + panic!("Interface '{interface_name}' name too long") } - let stdout_str = std::str::from_utf8(&output.stdout).unwrap_or("non-utf8 string"); - let stderr_str = std::str::from_utf8(&output.stderr).unwrap_or("non-utf8 string"); + // SAFETY: interface_name is shorter than ifr.ifr_name + unsafe { + std::ptr::copy_nonoverlapping( + interface_name.as_ptr() as *const libc::c_char, + &mut ifr.ifr_name as *mut _, + interface_name.len(), + ) + }; + + // 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 { + let e = std::io::Error::last_os_error(); - log::error!( - "{action} failed:\n\ncode: {:?}\n\nstdout:\n\n{}\n\nstderr:\n\n{}", - output.status.code(), - stdout_str, - stderr_str - ); - Err(err) + log::error!("{}", e); + return Err(test_rpc::Error::Io(e.to_string())); + } + + // SAFETY: ifru_mtu is set since SIOGCIFMTU succeeded + Ok(unsafe { ifr.ifr_ifru.ifru_mtu } + .try_into() + .expect("MTU should fit in u16")) +} + +#[cfg(target_os = "windows")] +pub fn get_interface_mtu(interface: &str) -> Result<u16, test_rpc::Error> { + let luid = talpid_windows::net::luid_from_alias(interface).map_err(|error| { + log::error!("Failed to obtain interface LUID: {error}"); + test_rpc::Error::Syscall + })?; + talpid_windows::net::get_ip_interface_entry(talpid_windows::net::AddressFamily::Ipv4, &luid) + .map_err(|_error| test_rpc::Error::InterfaceNotFound) + .map(|row| row.NlMtu.try_into().unwrap()) } |
