diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-03-08 16:36:06 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-03-08 16:36:06 +0100 |
| commit | 9c035eedaea92216b6b5be4bf2fe0ca25fe9f257 (patch) | |
| tree | ad1059e76cc50061001d068b916a9631459d9dc7 | |
| parent | cd5f33503bfe579f418359ebd3a0c335bb16dca1 (diff) | |
| parent | 301a12319e4b4812fe829243106e6884c0f2d8ee (diff) | |
| download | mullvadvpn-9c035eedaea92216b6b5be4bf2fe0ca25fe9f257.tar.xz mullvadvpn-9c035eedaea92216b6b5be4bf2fe0ca25fe9f257.zip | |
Merge branch 'mtu-detection-tests-nft' into main
| -rw-r--r-- | test/Cargo.lock | 77 | ||||
| -rw-r--r-- | test/Cargo.toml | 20 | ||||
| -rw-r--r-- | test/test-manager/Cargo.toml | 1 | ||||
| -rw-r--r-- | test/test-manager/src/tests/helpers.rs | 24 | ||||
| -rw-r--r-- | test/test-manager/src/tests/mod.rs | 4 | ||||
| -rw-r--r-- | test/test-manager/src/tests/tunnel_state.rs | 152 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/linux.rs | 2 | ||||
| -rw-r--r-- | test/test-rpc/src/client.rs | 20 | ||||
| -rw-r--r-- | test/test-rpc/src/lib.rs | 30 | ||||
| -rw-r--r-- | test/test-runner/Cargo.toml | 14 | ||||
| -rw-r--r-- | test/test-runner/src/main.rs | 20 | ||||
| -rw-r--r-- | test/test-runner/src/net.rs | 146 |
12 files changed, 367 insertions, 143 deletions
diff --git a/test/Cargo.lock b/test/Cargo.lock index f05e2dc9dd..2579a6a005 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -1097,6 +1097,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] name = "hkdf" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2166,6 +2172,15 @@ dependencies = [ ] [[package]] +name = "pnet_base" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" +dependencies = [ + "no-std-net", +] + +[[package]] name = "pnet_macros" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2178,12 +2193,33 @@ dependencies = [ ] [[package]] +name = "pnet_macros" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", +] + +[[package]] name = "pnet_macros_support" version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89de095dc7739349559913aed1ef6a11e73ceade4897dadc77c5e09de6740750" dependencies = [ - "pnet_base", + "pnet_base 0.31.0", +] + +[[package]] +name = "pnet_macros_support" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" +dependencies = [ + "pnet_base 0.33.0", ] [[package]] @@ -2193,9 +2229,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3b5111e697c39c8b9795b9fdccbc301ab696699e88b9ea5a4e4628978f495f" dependencies = [ "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", + "pnet_base 0.31.0", + "pnet_macros 0.31.0", + "pnet_macros_support 0.31.0", +] + +[[package]] +name = "pnet_packet" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" +dependencies = [ + "glob", + "pnet_base 0.33.0", + "pnet_macros 0.33.0", + "pnet_macros_support 0.33.0", ] [[package]] @@ -2952,6 +3000,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] +name = "surge-ping" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af341b2be485d647b5dc4cfb2da99efac35b5c95748a08fb7233480fedc5ead3" +dependencies = [ + "hex", + "parking_lot 0.12.1", + "pnet_packet 0.33.0", + "rand 0.8.5", + "socket2 0.5.4", + "thiserror", + "tokio", + "tracing", +] + +[[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3116,8 +3180,9 @@ dependencies = [ "nix 0.25.1", "once_cell", "pcap", - "pnet_packet", + "pnet_packet 0.31.0", "regex", + "scopeguard", "serde", "serde_json", "socks-server", @@ -3174,10 +3239,12 @@ dependencies = [ "once_cell", "parity-tokio-ipc", "plist", + "rand 0.8.5", "rs-release", "serde", "serde_json", "socket2 0.5.4", + "surge-ping", "talpid-platform-metadata", "talpid-windows", "tarpc", diff --git a/test/Cargo.toml b/test/Cargo.toml index 44cc016d66..977f9082d8 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -7,12 +7,7 @@ rust-version = "1.75.0" [workspace] resolver = "2" -members = [ - "test-manager", - "test-runner", - "test-rpc", - "socks-server", -] +members = ["test-manager", "test-runner", "test-rpc", "socks-server"] [workspace.lints.rust] rust_2018_idioms = "deny" @@ -22,7 +17,15 @@ unused_async = "deny" [workspace.dependencies] futures = "0.3" -tokio = { version = "1.8", features = ["macros", "rt", "process", "time", "fs", "io-util", "rt-multi-thread"] } +tokio = { version = "1.8", features = [ + "macros", + "rt", + "process", + "time", + "fs", + "io-util", + "rt-multi-thread", +] } tokio-serial = "5.4.1" # Serde and related crates serde = "1.0" @@ -46,8 +49,9 @@ shadowsocks-service = { version = "1.16" } windows-sys = "0.48.0" -chrono = { version = "0.4.26", default-features = false} +chrono = { version = "0.4.26", default-features = false } clap = { version = "4.2.7", features = ["cargo", "derive"] } once_cell = "1.16.0" bytes = "1.3.0" async-trait = "0.1.58" +surge-ping = "0.8" diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml index 66ef6f0025..f0e6cb9235 100644 --- a/test/test-manager/Cargo.toml +++ b/test/test-manager/Cargo.toml @@ -32,6 +32,7 @@ async-tempfile = "0.2" async-trait = { workspace = true } uuid = "1.3" dirs = "5.0.1" +scopeguard = "1.2" serde = { workspace = true } serde_json = { workspace = true } diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index c3e0e59481..108da125f6 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -1,4 +1,4 @@ -use super::{config::TEST_CONFIG, Error, PING_TIMEOUT, WAIT_FOR_TUNNEL_STATE_TIMEOUT}; +use super::{config::TEST_CONFIG, Error, WAIT_FOR_TUNNEL_STATE_TIMEOUT}; use crate::network_monitor::{ self, start_packet_monitor, MonitorOptions, MonitorUnexpectedlyStopped, PacketMonitor, }; @@ -21,7 +21,6 @@ use std::{ }; use talpid_types::net::wireguard::{PeerConfig, PrivateKey, TunnelConfig}; use test_rpc::{package::Package, AmIMullvad, ServiceClient}; -use tokio::time::timeout; #[macro_export] macro_rules! assert_tunnel_state { @@ -182,9 +181,21 @@ pub async fn ping_with_timeout( dest: IpAddr, interface: Option<String>, ) -> Result<(), Error> { - timeout(PING_TIMEOUT, rpc.send_ping(interface, dest)) + const DEFAULT_PING_SIZE: usize = 64; + + rpc.send_ping(dest, interface, DEFAULT_PING_SIZE) + .await + .map_err(Error::Rpc) +} + +pub async fn ping_sized_with_timeout( + rpc: &ServiceClient, + dest: IpAddr, + interface: Option<String>, + size: usize, +) -> Result<(), Error> { + rpc.send_ping(dest, interface, size) .await - .map_err(|_| Error::PingTimeout)? .map_err(Error::Rpc) } @@ -506,8 +517,9 @@ impl Pinger { log::debug!("Monitoring outgoing traffic"); let monitor = start_packet_monitor( move |packet| { - // NOTE: Many packets will likely be observed for API traffic. Rather than filtering all - // of those specifically, simply fail if our probes are observed. + // NOTE: Many packets will likely be observed for API traffic. Rather than filtering + // all of those specifically, simply fail if our probes are + // observed. packet.source.ip() == guest_ip && packet.destination.ip() == builder.destination.ip() }, diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index 0e3e73a2f6..b104dbe9e8 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -21,7 +21,6 @@ use futures::future::BoxFuture; use mullvad_management_interface::MullvadProxyClient; use std::time::Duration; -const PING_TIMEOUT: Duration = Duration::from_secs(3); const WAIT_FOR_TUNNEL_STATE_TIMEOUT: Duration = Duration::from_secs(40); #[derive(Clone)] @@ -42,9 +41,6 @@ pub enum Error { #[error("RPC call failed")] Rpc(#[from] test_rpc::Error), - #[error("Timeout waiting for ping")] - PingTimeout, - #[error("geoip lookup failed")] GeoipLookup(test_rpc::Error), diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index 7abc505939..407328e029 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -1,24 +1,138 @@ -use super::helpers::{ - self, connect_and_wait, send_guest_probes, set_relay_settings, unreachable_wireguard_tunnel, - wait_for_tunnel_state, +use super::{ + helpers::{ + self, connect_and_wait, send_guest_probes, set_relay_settings, + unreachable_wireguard_tunnel, wait_for_tunnel_state, + }, + ui, Error, TestContext, +}; +use crate::{ + assert_tunnel_state, tests::helpers::ping_sized_with_timeout, + vm::network::DUMMY_LAN_INTERFACE_IP, }; -use super::{ui, Error, TestContext}; -use crate::assert_tunnel_state; -use crate::vm::network::DUMMY_LAN_INTERFACE_IP; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::relay_constraints::GeographicLocationConstraint; -use mullvad_types::relay_list::{Relay, RelayEndpointData}; -use mullvad_types::CustomTunnelEndpoint; use mullvad_types::{ - relay_constraints::{Constraint, LocationConstraint, RelayConstraints, RelaySettings}, + relay_constraints::{ + Constraint, GeographicLocationConstraint, LocationConstraint, RelayConstraints, + RelaySettings, + }, + relay_list::{Relay, RelayEndpointData}, states::TunnelState, + CustomTunnelEndpoint, +}; +use std::{ + net::{IpAddr, SocketAddr}, + time::Duration, }; -use std::net::{IpAddr, SocketAddr}; use talpid_types::net::{Endpoint, TransportProtocol, TunnelEndpoint, TunnelType}; use test_macro::test_function; use test_rpc::ServiceClient; +#[cfg(target_os = "linux")] +async fn setup_nftables_drop_pings_rule( + max_packet_size: u16, +) -> scopeguard::ScopeGuard<(), impl FnOnce(())> { + fn log_ruleset() { + let output = std::process::Command::new("nft") + .args(["list", "ruleset"]) + .output() + .unwrap(); + + log::debug!( + "Set NF-tables ruleset to:\n{}", + String::from_utf8(output.stdout).unwrap() + ); + + let exit_status = output.status; + assert_eq!(exit_status.code(), Some(0)); + } + // Set nftables ruleset + crate::vm::network::linux::run_nft( + &(format!( + "table inet DropPings {{ + chain postrouting {{ + type filter hook postrouting priority 0; policy accept; + ip length > {max_packet_size} drop; + }} + }}" + )), + ) + .await + .unwrap(); + log_ruleset(); + + scopeguard::guard((), |()| { + let mut cmd = std::process::Command::new("nft"); + cmd.args(["delete", "table", "inet", "DropPings"]); + let output = cmd.output().unwrap(); + if !output.status.success() { + panic!("{}", std::str::from_utf8(&output.stderr).unwrap()); + } + log_ruleset(); + }) +} + +#[test_function(target_os = "windows")] +pub async fn test_mtu_detection_windows( + _: TestContext, + rpc: ServiceClient, + mullvad_client: MullvadProxyClient, +) -> Result<(), Error> { + test_mtu_detection(rpc, mullvad_client).await +} + +#[test_function(target_os = "linux")] +pub async fn test_mtu_detection_linux( + _: TestContext, + rpc: ServiceClient, + mullvad_client: MullvadProxyClient, +) -> Result<(), Error> { + test_mtu_detection(rpc, mullvad_client).await +} + +async fn test_mtu_detection( + rpc: ServiceClient, + mut mullvad_client: MullvadProxyClient, +) -> Result<(), Error> { + const MAX_PACKET_SIZE: u16 = 800; + const MARGIN: u16 = 200; + let large_ping_size: usize = (MAX_PACKET_SIZE + MARGIN).into(); + + log::info!("Verify tunnel state: disconnected"); + assert_tunnel_state!(&mut mullvad_client, TunnelState::Disconnected { .. }); + + // mullvad.net address + let inet_destination = "45.83.223.209".parse().unwrap(); + + log::info!("Setting up nftables firewall rules"); + #[cfg(target_os = "linux")] + let _nft_guard = setup_nftables_drop_pings_rule(MAX_PACKET_SIZE).await; + + // Test that the firewall rule works + log::info!("Sending large ping outside tunnel"); + ping_sized_with_timeout(&rpc, inet_destination, None, large_ping_size) + .await + .expect_err("Ping larger than the filter should time out"); + + connect_and_wait(&mut mullvad_client).await.unwrap(); + let tunnel_iface = helpers::get_tunnel_interface(&mut mullvad_client) + .await + .expect("failed to find tunnel interface"); + + log::info!("Waiting for MTU detection"); + for _ in 0..10 { + let mtu = rpc.get_interface_mtu(tunnel_iface.clone()).await?; + if mtu < MAX_PACKET_SIZE { + println!( + "Tunnel MTU after dropping packets larger than {MAX_PACKET_SIZE} bytes: {mtu}" + ); + return Ok(()); + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + panic!("MTU detection test failed") +} + /// Verify that outgoing TCP, UDP, and ICMP packets can be observed /// in the disconnected state. The purpose is mostly to rule prevent /// false negatives in other tests. @@ -51,7 +165,6 @@ pub async fn test_disconnected_state( "did not see (all) outgoing packets to destination: {detected_probes:?}", ); - // // Test UI view // @@ -118,7 +231,6 @@ pub async fn test_connecting_state( new_state ); - // // Leak test // @@ -197,7 +309,6 @@ pub async fn test_error_state( let _ = connect_and_wait(&mut mullvad_client).await; assert_tunnel_state!(&mut mullvad_client, TunnelState::Error { .. }); - // // Leak test // @@ -235,12 +346,11 @@ pub async fn test_error_state( } /// Connect to a single relay and verify that: -/// * Traffic can be sent and received in the tunnel. -/// This is done by pinging a single public IP address -/// and failing if there is no response. +/// * Traffic can be sent and received in the tunnel. This is done by pinging a single public IP +/// address and failing if there is no response. /// * The correct relay is used. -/// * Leaks outside the tunnel are blocked. Refer to the -/// `test_connecting_state` documentation for details. +/// * Leaks outside the tunnel are blocked. Refer to the `test_connecting_state` documentation for +/// details. #[test_function] pub async fn test_connected_state( _: TestContext, @@ -249,7 +359,6 @@ pub async fn test_connected_state( ) -> Result<(), Error> { let inet_destination = "1.1.1.1:1337".parse().unwrap(); - // // Set relay to use // @@ -273,13 +382,11 @@ pub async fn test_connected_state( .await .expect("failed to update relay settings"); - // // Connect // connect_and_wait(&mut mullvad_client).await?; - // // Verify that endpoint was selected // @@ -307,7 +414,6 @@ pub async fn test_connected_state( actual => panic!("unexpected tunnel state: {:?}", actual), } - // // Ping outside of tunnel while connected // diff --git a/test/test-manager/src/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs index d375fc2eb5..f54d218b2f 100644 --- a/test/test-manager/src/vm/network/linux.rs +++ b/test/test-manager/src/vm/network/linux.rs @@ -334,7 +334,7 @@ where Ok(()) } -async fn run_nft(input: &str) -> Result<()> { +pub async fn run_nft(input: &str) -> Result<()> { let mut cmd = Command::new("nft"); cmd.args(["-f", "-"]); diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs index 6b3c2c4a0c..b4fb67f5c0 100644 --- a/test/test-rpc/src/client.rs +++ b/test/test-rpc/src/client.rs @@ -109,8 +109,8 @@ impl ServiceClient { .map_err(Error::Tarpc) } - /// Wait for the Mullvad service to enter a specified state. The state is inferred from the presence - /// of a named pipe or UDS, not the actual system service state. + /// Wait for the Mullvad service to enter a specified state. The state is inferred from the + /// presence of a named pipe or UDS, not the actual system service state. pub async fn mullvad_daemon_wait_for_state( &self, accept_state_fn: impl Fn(ServiceStatus) -> bool, @@ -178,11 +178,12 @@ impl ServiceClient { /// Send ICMP pub async fn send_ping( &self, - interface: Option<String>, destination: IpAddr, + interface: Option<String>, + size: usize, ) -> Result<(), Error> { self.client - .send_ping(tarpc::context::current(), interface, destination) + .send_ping(tarpc::context::current(), destination, interface, size) .await? } @@ -200,6 +201,13 @@ impl ServiceClient { .await? } + /// Returns the MTU of the given interface. + pub async fn get_interface_mtu(&self, interface: String) -> Result<u16, Error> { + self.client + .get_interface_mtu(tarpc::context::current(), interface) + .await? + } + /// Returns the name of the default non-tunnel interface pub async fn get_default_interface(&self) -> Result<String, Error> { self.client @@ -213,8 +221,8 @@ impl ServiceClient { .await? } - /// Start forwarding TCP from a server listening on `bind_addr` to the given address, and return a handle that closes the - /// server when dropped + /// Start forwarding TCP from a server listening on `bind_addr` to the given address, and return + /// a handle that closes the server when dropped pub async fn start_tcp_forward( &self, bind_addr: SocketAddr, diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 166a00d9a7..d151520601 100644 --- a/test/test-rpc/src/lib.rs +++ b/test/test-rpc/src/lib.rs @@ -19,9 +19,11 @@ pub enum Error { Tarpc(#[from] tarpc::client::RpcError), #[error("Syscall failed")] Syscall, + #[error("Internal IO error occurred: {0}")] + Io(String), #[error("Interface not found")] InterfaceNotFound, - #[error("HTTP request failed")] + #[error("HTTP request failed: {0}")] HttpRequest(String), #[error("Failed to deserialize HTTP body")] DeserializeBody, @@ -37,17 +39,17 @@ pub enum Error { SendUdp, #[error("Failed to send TCP segment")] SendTcp, - #[error("Failed to send ping")] - Ping, - #[error("Failed to get or set registry value")] + #[error("Failed to send ping: {0}")] + Ping(String), + #[error("Failed to get or set registry value: {0}")] Registry(String), - #[error("Failed to change the service")] + #[error("Failed to change the service: {0}")] Service(String), - #[error("Could not read from or write to the file system")] + #[error("Could not read from or write to the file system: {0}")] FileSystem(String), - #[error("Could not serialize or deserialize file")] + #[error("Could not serialize or deserialize file: {0}")] FileSerialization(String), - #[error("User must be logged in but is not")] + #[error("User must be logged in but is not: {0}")] UserNotLoggedIn(String), #[error("Invalid URL")] InvalidUrl, @@ -136,7 +138,11 @@ mod service { ) -> Result<(), Error>; /// Send ICMP - async fn send_ping(interface: Option<String>, destination: IpAddr) -> Result<(), Error>; + async fn send_ping( + destination: IpAddr, + interface: Option<String>, + size: usize, + ) -> Result<(), Error>; /// Fetch the current location. async fn geoip_lookup(mullvad_host: String) -> Result<AmIMullvad, Error>; @@ -144,6 +150,9 @@ mod service { /// Returns the IP of the given interface. async fn get_interface_ip(interface: String) -> Result<IpAddr, Error>; + /// Returns the MTU of the given interface. + async fn get_interface_mtu(interface: String) -> Result<u16, Error>; + /// Returns the name of the default interface. async fn get_default_interface() -> Result<String, Error>; @@ -175,7 +184,8 @@ mod service { verbosity_level: mullvad_daemon::Verbosity, ) -> Result<(), Error>; - /// Set environment variables for the daemon service. This will restart the daemon system service. + /// Set environment variables for the daemon service. This will restart the daemon system + /// service. async fn set_daemon_environment(env: HashMap<String, String>) -> Result<(), Error>; /// Copy a file from `src` to `dest` on the test runner. diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml index dbe57c938e..8e2ae8cbf6 100644 --- a/test/test-runner/Cargo.toml +++ b/test/test-runner/Cargo.toml @@ -23,6 +23,8 @@ bytes = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio-serde = { workspace = true } +surge-ping = { workspace = true } +rand = "0.8" libc = "0.2" chrono = { workspace = true, features = ["serde"] } @@ -42,12 +44,12 @@ winreg = "0.50" [target.'cfg(windows)'.dependencies.windows-sys] version = "0.45.0" features = [ - "Win32_Foundation", - "Win32_Security", - "Win32_System_Shutdown", - "Win32_System_SystemServices", - "Win32_System_Threading", - "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_Security", + "Win32_System_Shutdown", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_UI_WindowsAndMessaging", ] [dependencies.tokio-util] 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()) } |
