diff --git a/test/Cargo.lock b/test/Cargo.lock index c889477eb6b3..8e9f42b6c3eb 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -394,6 +394,12 @@ version = "1.0.0" 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" @@ -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" @@ -1798,6 +1804,15 @@ dependencies = [ "autocfg", ] +[[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" @@ -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]] @@ -2317,15 +2331,6 @@ dependencies = [ "time", ] -[[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" @@ -2335,18 +2340,6 @@ dependencies = [ "no-std-net", ] -[[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" @@ -2359,34 +2352,13 @@ dependencies = [ "syn 2.0.60", ] -[[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 c0939eac971d..386bab87c48d 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 fa86db329e2b..7285a9e837e5 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 000000000000..d19445106632 --- /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>> { + 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> { + 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 { + 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::() 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::() 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 d6a59bd33c1b..adefedcf98ef 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> { + 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 { + 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 25811f7d4520..255efb594197 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 84d923a826bc..4c56601d75b1 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, 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 { self.client diff --git a/test/test-rpc/src/lib.rs b/test/test-rpc/src/lib.rs index 82fed91541a9..cc263b845e1e 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; + /// Returns the MAC address of the given interface. + async fn get_interface_mac(interface: String) -> Result, Error>; + /// Returns the name of the default interface. async fn get_default_interface() -> Result; diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml index 50f3ddda6a91..31c817fb87c2 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 79e11e0506f5..3fe91fb72364 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, test_rpc::Error> { + net::get_interface_mac(&interface) + } + async fn get_default_interface(self, _: context::Context) -> Result { Ok(net::get_default_interface().to_owned()) } diff --git a/test/test-runner/src/net.rs b/test/test-runner/src/net.rs index 22de4da9c9f6..a12fa2776cb6 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 { // 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 { 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, 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, test_rpc::Error> { + unimplemented!("get_interface_mac") +} + #[cfg(target_os = "windows")] pub fn get_default_interface() -> &'static str { use once_cell::sync::OnceCell;