summaryrefslogtreecommitdiffhomepage
path: root/test/test-runner/src
diff options
context:
space:
mode:
authorSebastian Holmin <sebastian.holmin@mullvad.net>2024-03-05 12:11:39 +0100
committerSebastian Holmin <sebastian.holmin@mullvad.net>2024-03-08 14:13:02 +0100
commitbee0555bc6d0a6116574af949a17ce3fe73c7d27 (patch)
tree5208586178567d35f9d6a51ad4ef069e519e7f02 /test/test-runner/src
parentcd5f33503bfe579f418359ebd3a0c335bb16dca1 (diff)
downloadmullvadvpn-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.rs20
-rw-r--r--test/test-runner/src/net.rs146
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())
}