summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim@hulthe.net>2024-07-05 16:25:32 +0200
committerJoakim Hulthe <joakim@hulthe.net>2024-07-16 15:46:33 +0200
commit71ef4709732d9b0ba052d957a6483c743d5746aa (patch)
tree14dc9da32f3e8ae0249a4b4109eb086b939b7a4e /test
parent28a3776b3a7a35c4e1f964e9dcb9c4698afd7a68 (diff)
downloadmullvadvpn-71ef4709732d9b0ba052d957a6483c743d5746aa.tar.xz
mullvadvpn-71ef4709732d9b0ba052d957a6483c743d5746aa.zip
Add E2E test of CVE-2019-14899 mitigation
Diffstat (limited to 'test')
-rw-r--r--test/Cargo.lock96
-rw-r--r--test/Cargo.toml6
-rw-r--r--test/test-manager/Cargo.toml6
-rw-r--r--test/test-manager/src/tests/cve_2019_14899.rs319
-rw-r--r--test/test-manager/src/tests/helpers.rs49
-rw-r--r--test/test-manager/src/tests/mod.rs1
-rw-r--r--test/test-rpc/src/client.rs7
-rw-r--r--test/test-rpc/src/lib.rs3
-rw-r--r--test/test-runner/Cargo.toml2
-rw-r--r--test/test-runner/src/main.rs8
-rw-r--r--test/test-runner/src/net.rs40
11 files changed, 469 insertions, 68 deletions
diff --git a/test/Cargo.lock b/test/Cargo.lock
index c889477eb6..8e9f42b6c3 100644
--- a/test/Cargo.lock
+++ b/test/Cargo.lock
@@ -395,6 +395,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1641,9 +1647,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.153"
+version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libdbus-sys"
@@ -1799,6 +1805,15 @@ dependencies = [
]
[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1963,29 +1978,28 @@ dependencies = [
[[package]]
name = "nix"
-version = "0.25.1"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
- "autocfg",
"bitflags 1.3.2",
"cfg-if",
"libc",
- "memoffset 0.6.5",
+ "memoffset 0.7.1",
"pin-utils",
]
[[package]]
name = "nix"
-version = "0.26.4"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.5.0",
"cfg-if",
+ "cfg_aliases",
"libc",
- "memoffset 0.7.1",
- "pin-utils",
+ "memoffset 0.9.1",
]
[[package]]
@@ -2319,15 +2333,6 @@ dependencies = [
[[package]]
name = "pnet_base"
-version = "0.31.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d3a993d49e5fd5d4d854d6999d4addca1f72d86c65adf224a36757161c02b6"
-dependencies = [
- "no-std-net",
-]
-
-[[package]]
-name = "pnet_base"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c"
@@ -2337,18 +2342,6 @@ dependencies = [
[[package]]
name = "pnet_macros"
-version = "0.31.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48dd52a5211fac27e7acb14cfc9f30ae16ae0e956b7b779c8214c74559cef4c3"
-dependencies = [
- "proc-macro2",
- "quote",
- "regex",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "pnet_macros"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804"
@@ -2361,32 +2354,11 @@ dependencies = [
[[package]]
name = "pnet_macros_support"
-version = "0.31.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89de095dc7739349559913aed1ef6a11e73ceade4897dadc77c5e09de6740750"
-dependencies = [
- "pnet_base 0.31.0",
-]
-
-[[package]]
-name = "pnet_macros_support"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56"
dependencies = [
- "pnet_base 0.34.0",
-]
-
-[[package]]
-name = "pnet_packet"
-version = "0.31.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc3b5111e697c39c8b9795b9fdccbc301ab696699e88b9ea5a4e4628978f495f"
-dependencies = [
- "glob",
- "pnet_base 0.31.0",
- "pnet_macros 0.31.0",
- "pnet_macros_support 0.31.0",
+ "pnet_base",
]
[[package]]
@@ -2396,9 +2368,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba"
dependencies = [
"glob",
- "pnet_base 0.34.0",
- "pnet_macros 0.34.0",
- "pnet_macros_support 0.34.0",
+ "pnet_base",
+ "pnet_macros",
+ "pnet_macros_support",
]
[[package]]
@@ -3196,7 +3168,7 @@ checksum = "efbf95ce4c7c5b311d2ce3f088af2b93edef0f09727fa50fbe03c7a979afce77"
dependencies = [
"hex",
"parking_lot 0.12.1",
- "pnet_packet 0.34.0",
+ "pnet_packet",
"rand 0.8.5",
"socket2 0.5.6",
"thiserror",
@@ -3378,14 +3350,16 @@ dependencies = [
"mullvad-management-interface",
"mullvad-relay-selector",
"mullvad-types",
- "nix 0.25.1",
+ "nix 0.29.0",
"once_cell",
"pcap",
- "pnet_packet 0.31.0",
+ "pnet_base",
+ "pnet_packet",
"regex",
"scopeguard",
"serde",
"serde_json",
+ "socket2 0.5.6",
"socks-server",
"ssh2",
"talpid-types",
@@ -3436,7 +3410,7 @@ dependencies = [
"libc",
"log",
"mullvad-paths",
- "nix 0.25.1",
+ "nix 0.29.0",
"once_cell",
"parity-tokio-ipc",
"plist",
diff --git a/test/Cargo.toml b/test/Cargo.toml
index c0939eac97..386bab87c4 100644
--- a/test/Cargo.toml
+++ b/test/Cargo.toml
@@ -47,10 +47,12 @@ tokio = { version = "1.8", features = [
"rt-multi-thread",
] }
tokio-serial = "5.4.1"
+
# Serde and related crates
serde = "1.0"
serde_json = "1.0"
tokio-serde = { version = "0.8.0", features = ["json"] }
+
# Tonic and related crates
tonic = "0.10.0"
tonic-build = { version = "0.10.0", default-features = false }
@@ -58,20 +60,22 @@ tower = "0.4"
prost = "0.12.0"
prost-types = "0.12.0"
tarpc = { version = "0.30", features = ["tokio1", "serde-transport", "serde1"] }
+
# Logging
env_logger = "0.11.0"
thiserror = "1.0.57"
log = "0.4"
colored = "2.0.0"
+
# Proxy protocols
shadowsocks = { version = "1.16" }
shadowsocks-service = { version = "1.16" }
windows-sys = "0.52.0"
-
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"
+nix = { version = "0.29", features = ["ioctl", "socket", "net"] }
diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml
index fa86db329e..7285a9e837 100644
--- a/test/test-manager/Cargo.toml
+++ b/test/test-manager/Cargo.toml
@@ -40,7 +40,8 @@ tokio-serde = { workspace = true }
log = { workspace = true }
pcap = { version = "1.3", features = ["capture-stream"] }
-pnet_packet = "0.31.0"
+pnet_packet = "0.34.0"
+pnet_base = "0.34.0"
test-rpc = { path = "../test-rpc" }
socks-server = { path = "../socks-server" }
@@ -59,7 +60,8 @@ talpid-types = { path = "../../talpid-types" }
ssh2 = "0.9.4"
-nix = { version = "0.25", features = ["net"] }
+nix = { workspace = true }
+socket2 = "0.5.6"
[target.'cfg(target_os = "macos")'.dependencies]
tun = "0.5.1"
diff --git a/test/test-manager/src/tests/cve_2019_14899.rs b/test/test-manager/src/tests/cve_2019_14899.rs
new file mode 100644
index 0000000000..d194451066
--- /dev/null
+++ b/test/test-manager/src/tests/cve_2019_14899.rs
@@ -0,0 +1,319 @@
+#![cfg(target_os = "linux")]
+
+use std::{
+ convert::Infallible,
+ ffi::{c_int, c_uint, c_void},
+ mem::size_of,
+ net::{IpAddr, Ipv4Addr},
+ os::fd::AsRawFd,
+ time::Duration,
+};
+
+use anyhow::{anyhow, bail, Context};
+use futures::{select, FutureExt};
+use mullvad_management_interface::MullvadProxyClient;
+use nix::{
+ errno::Errno,
+ sys::socket::{self, MsgFlags, SockProtocol},
+};
+use pnet_base::MacAddr;
+use pnet_packet::{
+ ethernet::{EtherTypes, EthernetPacket, MutableEthernetPacket},
+ ip::IpNextHeaderProtocols,
+ ipv4::{Ipv4Packet, MutableIpv4Packet},
+ tcp::{MutableTcpPacket, TcpFlags, TcpPacket},
+ MutablePacket, Packet,
+};
+use socket2::Socket;
+use test_macro::test_function;
+use test_rpc::ServiceClient;
+use tokio::{task::yield_now, time::sleep};
+
+use crate::{
+ tests::helpers,
+ vm::network::{linux::TAP_NAME, NON_TUN_GATEWAY},
+};
+
+use super::TestContext;
+
+/// The port number we set in the malicious packet.
+const MALICIOUS_PACKET_PORT: u16 = 12345;
+
+/// Test mitigation for cve-2019-14899.
+///
+/// The vulnerability allowed a malicious router to learn the victims private mullvad tunnel IP.
+/// It is performed by sending a TCP packet to the victim with SYN and ACK flags set.
+///
+/// If the destination_addr of the packet was the same as the private IP, the victims computer
+/// would respond to the packet with the RST flag set.
+///
+/// This test simply gets the private tunnel IP from the test runner and sends the SYN/ACK packet
+/// targeted to that address. If the guest does not respond, the test passes.
+///
+/// Note that only linux was susceptible to this vulnerability.
+#[test_function(target_os = "linux")]
+pub async fn test_cve_2019_14899_mitigation(
+ _: TestContext,
+ rpc: ServiceClient,
+ mut mullvad_client: MullvadProxyClient,
+) -> anyhow::Result<()> {
+ // The vulnerability required local network sharing to be enabled
+ mullvad_client
+ .set_allow_lan(true)
+ .await
+ .context("Failed to allow local network sharing")?;
+
+ helpers::connect_and_wait(&mut mullvad_client).await?;
+
+ let host_interface = TAP_NAME;
+ let victim_tunnel_interface = "wg0-mullvad";
+ let victim_gateway_ip = NON_TUN_GATEWAY;
+
+ // Create a raw socket which let's us send custom ethernet packets
+ log::info!("Creating raw socket");
+ let socket = Socket::new(
+ socket2::Domain::PACKET,
+ socket2::Type::RAW,
+ Some(socket2::Protocol::from(SockProtocol::EthAll as c_int)),
+ )
+ .with_context(|| "Failed to create raw socket")?;
+
+ log::info!("Binding raw socket to tap interface");
+ socket
+ .bind_device(Some(host_interface.as_bytes()))
+ .with_context(|| anyhow!("Failed to bind the socket to {host_interface:?}"))?;
+
+ // Get the private IP address of the victims VPN tunnel
+ let victim_tunnel_ip = rpc
+ .get_interface_ip(victim_tunnel_interface.to_string())
+ .await
+ .with_context(|| {
+ anyhow!("Failed to get ip of guest tunnel interface {victim_tunnel_interface:?}")
+ })?;
+
+ let IpAddr::V4(victim_tunnel_ip) = victim_tunnel_ip else {
+ bail!("I didn't ask for IPv6!");
+ };
+
+ let victim_default_interface = rpc
+ .get_default_interface()
+ .await
+ .context("failed to get guest default interface")?;
+
+ let victim_default_interface_mac = rpc
+ .get_interface_mac(victim_default_interface.clone())
+ .await
+ .with_context(|| {
+ anyhow!("Failed to get ip of guest default interface {victim_default_interface:?}")
+ })?
+ .ok_or(anyhow!(
+ "No mac address for guest default interface {victim_default_interface:?}"
+ ))?;
+
+ // Get the MAC address and index of the tap interface
+ let host_interface_index = helpers::get_interface_index(host_interface)?;
+ let host_interface_mac = helpers::get_interface_mac(host_interface)?.ok_or(anyhow!(
+ "No mac address for host interface {host_interface:?}"
+ ))?;
+
+ let malicious_packet = craft_malicious_packet(
+ MacAddr::from(host_interface_mac),
+ MacAddr::from(victim_default_interface_mac),
+ victim_gateway_ip,
+ victim_tunnel_ip,
+ );
+
+ let filter = |tcp: &TcpPacket<'_>| {
+ let reset_flag_set = (tcp.get_flags() & TcpFlags::RST) != 0;
+ let correct_source_port = tcp.get_source() == MALICIOUS_PACKET_PORT;
+ let correct_destination_port = tcp.get_destination() == MALICIOUS_PACKET_PORT;
+
+ reset_flag_set && correct_source_port && correct_destination_port
+ };
+
+ let rst_packet = select! {
+ result = filter_for_packet(&socket, filter, Duration::from_secs(5)).fuse() => result?,
+
+ result = spam_packet(&socket, host_interface_index, &malicious_packet).fuse() => match result {
+ Err(e) => return Err(e),
+ Ok(never) => match never {}, // I dream of ! being stabilized
+ },
+ };
+
+ if let Some(rst_packet) = rst_packet {
+ log::warn!("Victim responded with an RST packet: {rst_packet:?}");
+ bail!("Managed to leak private tunnel IP");
+ }
+
+ Ok(())
+}
+
+/// Read from the socket and return the first packet that passes the filter.
+/// Returns `None` if we don't see such a packet within the timeout.
+async fn filter_for_packet(
+ socket: &Socket,
+ filter: impl Fn(&TcpPacket<'_>) -> bool,
+ timeout: Duration,
+) -> anyhow::Result<Option<TcpPacket<'static>>> {
+ let mut buf = vec![0u8; usize::from(u16::MAX)];
+
+ let result = tokio::time::timeout(timeout, async {
+ loop {
+ let packet = poll_for_packet(socket, &mut buf).await?;
+ if filter(&packet) {
+ return anyhow::Ok(packet);
+ }
+ }
+ });
+
+ match result.await {
+ Ok(packet) => Ok(Some(packet?)),
+ Err(_timed_out) => Ok(None),
+ }
+}
+
+/// Repeatedly poll the raw socket until we receives an Ethernet/IPv4/TCP packet.
+/// Drops any non-TCP packets.
+///
+/// # Returns
+/// - `Err` if the `read` system call failed.
+/// - A single TCP packet otherwise.
+async fn poll_for_packet(socket: &Socket, buf: &mut [u8]) -> anyhow::Result<TcpPacket<'static>> {
+ loop {
+ // yield so we don't end up hogging the runtime while polling the socket
+ yield_now().await;
+
+ let result = socket::recv(socket.as_raw_fd(), &mut buf[..], MsgFlags::MSG_DONTWAIT);
+
+ let n = match result {
+ Ok(0) | Err(Errno::EWOULDBLOCK) => {
+ sleep(Duration::from_millis(10)).await;
+ continue;
+ }
+ Err(e) => return Err(e).context("Failed to read from socket"),
+ Ok(n) => n,
+ };
+
+ let packet = &buf[..n];
+
+ let Some(eth_packet) = EthernetPacket::new(packet) else {
+ continue;
+ };
+
+ if eth_packet.get_ethertype() != EtherTypes::Ipv4 {
+ continue;
+ }
+
+ let Some(ipv4_packet) = Ipv4Packet::new(eth_packet.payload()) else {
+ continue;
+ };
+
+ let valid_ip_version = ipv4_packet.get_version() == 4;
+ let protocol_is_tcp = ipv4_packet.get_next_level_protocol() == IpNextHeaderProtocols::Tcp;
+
+ if !valid_ip_version || !protocol_is_tcp {
+ continue;
+ }
+
+ if let Some(tcp_packet) = TcpPacket::owned(ipv4_packet.payload().to_vec()) {
+ return Ok(tcp_packet);
+ };
+ }
+}
+
+/// Send `packet` on the socket in a loop.
+// NOTE: Replace return type with ! if/when stable.
+async fn spam_packet(
+ socket: &Socket,
+ interface_index: c_uint,
+ packet: &EthernetPacket<'_>,
+) -> anyhow::Result<Infallible> {
+ loop {
+ send_packet(socket, interface_index, packet)?;
+ sleep(Duration::from_millis(50)).await;
+ }
+}
+
+/// Send an ethernet packet on the raw socket.
+fn send_packet(
+ socket: &Socket,
+ interface_index: c_uint,
+ packet: &EthernetPacket<'_>,
+) -> anyhow::Result<()> {
+ let result = {
+ let mut destination = libc::sockaddr_ll {
+ sll_family: 0,
+ sll_protocol: 0,
+ sll_ifindex: interface_index as c_int,
+ sll_hatype: 0,
+ sll_pkttype: 0,
+ sll_halen: size_of::<MacAddr>() as u8,
+ sll_addr: [0; 8],
+ };
+ destination.sll_addr[..6].copy_from_slice(&packet.get_destination().octets());
+ unsafe {
+ // NOTE: since you're reading this, consider using https://docs.rs/pnet_datalink
+ // instead of whatever you're planning...
+ libc::sendto(
+ socket.as_raw_fd(),
+ packet.packet().as_ptr() as *const c_void,
+ packet.packet().len(),
+ 0,
+ (&destination as *const libc::sockaddr_ll).cast(),
+ size_of::<libc::sockaddr_ll>() as u32,
+ )
+ }
+ };
+
+ if result < 0 {
+ let err = Errno::last();
+ bail!("Failed to send ethernet packet: {err}");
+ }
+
+ Ok(())
+}
+
+fn craft_malicious_packet(
+ source_mac: MacAddr,
+ destination_mac: MacAddr,
+ source_ip: Ipv4Addr,
+ destination_ip: Ipv4Addr,
+) -> EthernetPacket<'static> {
+ // length of the various parts of the malicious packet we'll be crafting.
+ const TCP_LEN: usize = 20; // a TCP packet is 20 bytes
+ const IPV4_LEN: usize = 20 + TCP_LEN; // an IPv4 packet is 20 bytes + payload
+ const ETH_LEN: usize = 14 + IPV4_LEN; // an ethernet packet is 14 bytes + payload
+
+ let mut eth_packet =
+ MutableEthernetPacket::owned(vec![0u8; ETH_LEN]).expect("ETH_LEN bytes is enough");
+ eth_packet.set_destination(destination_mac);
+ eth_packet.set_source(source_mac);
+ eth_packet.set_ethertype(EtherTypes::Ipv4);
+
+ let mut ipv4_packet =
+ MutableIpv4Packet::new(eth_packet.payload_mut()).expect("IPV4_LEN bytes is enough");
+ ipv4_packet.set_version(4);
+ ipv4_packet.set_header_length(5);
+ ipv4_packet.set_total_length(IPV4_LEN as u16);
+ ipv4_packet.set_identification(0x77);
+ ipv4_packet.set_ttl(0xff);
+ ipv4_packet.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
+ ipv4_packet.set_source(source_ip);
+ ipv4_packet.set_destination(destination_ip);
+ ipv4_packet.set_checksum(pnet_packet::ipv4::checksum(&ipv4_packet.to_immutable()));
+
+ let mut tcp_packet =
+ MutableTcpPacket::new(ipv4_packet.payload_mut()).expect("TCP_LEN bytes is enough");
+ tcp_packet.set_source(MALICIOUS_PACKET_PORT);
+ tcp_packet.set_destination(MALICIOUS_PACKET_PORT);
+ tcp_packet.set_data_offset(5); // 5 is smallest possible value
+ tcp_packet.set_window(0xff);
+ tcp_packet.set_flags(TcpFlags::SYN | TcpFlags::ACK);
+ tcp_packet.set_checksum(pnet_packet::tcp::ipv4_checksum(
+ &tcp_packet.to_immutable(),
+ &source_ip,
+ &destination_ip,
+ ));
+
+ eth_packet.consume_to_immutable()
+}
diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs
index d6a59bd33c..adefedcf98 100644
--- a/test/test-manager/src/tests/helpers.rs
+++ b/test/test-manager/src/tests/helpers.rs
@@ -253,6 +253,55 @@ pub async fn resolve_hostname_with_retries(
}
}
+/// Get the mac address (if any) of a network interface (on the test-manager machine).
+#[cfg(target_os = "linux")] // not used on macos
+pub fn get_interface_mac(interface: &str) -> anyhow::Result<Option<[u8; 6]>> {
+ let addrs = nix::ifaddrs::getifaddrs().map_err(|error| {
+ log::error!("Failed to obtain interfaces: {}", error);
+ test_rpc::Error::Syscall
+ })?;
+
+ let mut interface_exists = false;
+
+ let mac_addr = addrs
+ .filter(|addr| addr.interface_name == interface)
+ .find_map(|addr| {
+ // sadly, the only way of distinguishing between "iface doesn't exist" and
+ // "iface has no mac addr" is to check if the interface appears anywhere in the list.
+ interface_exists = true;
+
+ let addr = addr.address.as_ref()?;
+ let link_addr = addr.as_link_addr()?;
+ let mac_addr = link_addr.addr()?;
+ Some(mac_addr)
+ });
+
+ if interface_exists {
+ Ok(mac_addr)
+ } else {
+ bail!("Interface not found: {interface:?}")
+ }
+}
+
+/// Get the index of a network interface (on the test-manager machine).
+#[cfg(target_os = "linux")] // not used on macos
+pub fn get_interface_index(interface: &str) -> anyhow::Result<std::ffi::c_uint> {
+ use nix::errno::Errno;
+ use std::ffi::CString;
+
+ let interface = CString::new(interface).context(anyhow!(
+ "Failed to turn interface name {interface:?} into cstr"
+ ))?;
+
+ match unsafe { libc::if_nametoindex(interface.as_ptr()) } {
+ 0 => {
+ let err = Errno::last();
+ Err(err).context("Failed to get interface index")
+ }
+ i => Ok(i),
+ }
+}
+
/// Log in and retry if it fails due to throttling
pub async fn login_with_retries(
mullvad_client: &mut MullvadProxyClient,
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs
index 25811f7d45..255efb5941 100644
--- a/test/test-manager/src/tests/mod.rs
+++ b/test/test-manager/src/tests/mod.rs
@@ -1,6 +1,7 @@
mod access_methods;
mod account;
pub mod config;
+mod cve_2019_14899;
mod dns;
mod helpers;
mod install;
diff --git a/test/test-rpc/src/client.rs b/test/test-rpc/src/client.rs
index 84d923a826..4c56601d75 100644
--- a/test/test-rpc/src/client.rs
+++ b/test/test-rpc/src/client.rs
@@ -208,6 +208,13 @@ impl ServiceClient {
.await?
}
+ /// Returns the MAC address of the given interface.
+ pub async fn get_interface_mac(&self, interface: String) -> Result<Option<[u8; 6]>, Error> {
+ self.client
+ .get_interface_mac(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
diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs
index 82fed91541..cc263b845e 100644
--- a/test/test-rpc/src/lib.rs
+++ b/test/test-rpc/src/lib.rs
@@ -187,6 +187,9 @@ mod service {
/// Returns the MTU of the given interface.
async fn get_interface_mtu(interface: String) -> Result<u16, Error>;
+ /// Returns the MAC address of the given interface.
+ async fn get_interface_mac(interface: String) -> Result<Option<[u8; 6]>, Error>;
+
/// Returns the name of the default interface.
async fn get_default_interface() -> Result<String, Error>;
diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml
index 50f3ddda6a..31c817fb87 100644
--- a/test/test-runner/Cargo.toml
+++ b/test/test-runner/Cargo.toml
@@ -58,7 +58,7 @@ features = ["codec"]
default-features = false
[target.'cfg(unix)'.dependencies]
-nix = { version = "0.25", features = ["socket", "net"] }
+nix = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
rs-release = "0.1.7"
diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs
index 79e11e0506..3fe91fb723 100644
--- a/test/test-runner/src/main.rs
+++ b/test/test-runner/src/main.rs
@@ -233,6 +233,14 @@ impl Service for TestServer {
net::get_interface_mtu(&interface)
}
+ async fn get_interface_mac(
+ self,
+ _: context::Context,
+ interface: String,
+ ) -> Result<Option<[u8; 6]>, test_rpc::Error> {
+ net::get_interface_mac(&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 22de4da9c9..a12fa2776c 100644
--- a/test/test-runner/src/net.rs
+++ b/test/test-runner/src/net.rs
@@ -173,7 +173,6 @@ pub async fn send_ping(
#[cfg(unix)]
pub fn get_interface_ip(interface: &str) -> Result<IpAddr, test_rpc::Error> {
// TODO: IPv6
- use std::net::Ipv4Addr;
let addrs = nix::ifaddrs::getifaddrs().map_err(|error| {
log::error!("Failed to obtain interfaces: {}", error);
@@ -183,13 +182,13 @@ pub fn get_interface_ip(interface: &str) -> Result<IpAddr, test_rpc::Error> {
if addr.interface_name == interface {
if let Some(address) = addr.address {
if let Some(sockaddr) = address.as_sockaddr_in() {
- return Ok(IpAddr::V4(Ipv4Addr::from(sockaddr.ip())));
+ return Ok(IpAddr::V4(sockaddr.ip()));
}
}
}
}
- log::error!("Could not find tunnel interface");
+ log::error!("Could not find interface {interface:?}");
Err(test_rpc::Error::InterfaceNotFound)
}
@@ -215,6 +214,41 @@ fn get_interface_ip_for_family(
})
}
+#[cfg(target_os = "linux")]
+pub fn get_interface_mac(interface: &str) -> Result<Option<[u8; 6]>, test_rpc::Error> {
+ let addrs = nix::ifaddrs::getifaddrs().map_err(|error| {
+ log::error!("Failed to obtain interfaces: {}", error);
+ test_rpc::Error::Syscall
+ })?;
+
+ let mut interface_exists = false;
+
+ let mac_addr = addrs
+ .filter(|addr| addr.interface_name == interface)
+ .find_map(|addr| {
+ // sadly, the only way of distinguishing between "iface doesn't exist" and
+ // "iface has no mac addr" is to check if the interface appears anywhere in the list.
+ interface_exists = true;
+
+ let addr = addr.address?;
+ let link_addr = addr.as_link_addr()?;
+ let mac_addr = link_addr.addr()?;
+ Some(mac_addr)
+ });
+
+ if interface_exists {
+ Ok(mac_addr)
+ } else {
+ log::error!("Could not find interface {interface:?}");
+ Err(test_rpc::Error::InterfaceNotFound)
+ }
+}
+
+#[cfg(not(target_os = "linux"))]
+pub fn get_interface_mac(_interface: &str) -> Result<Option<[u8; 6]>, test_rpc::Error> {
+ unimplemented!("get_interface_mac")
+}
+
#[cfg(target_os = "windows")]
pub fn get_default_interface() -> &'static str {
use once_cell::sync::OnceCell;