Skip to content

Commit

Permalink
Merge branch 'add-e2e-tests-for-arp-ping-leak-des-1438'
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkusPettersson98 committed Nov 15, 2024
2 parents f56bae1 + 8bd6218 commit 4d7fb81
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
#![cfg(target_os = "linux")]
//! 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.
use std::{
convert::Infallible,
Expand Down Expand Up @@ -30,27 +42,13 @@ use test_rpc::ServiceClient;
use tokio::{task::yield_now, time::sleep};

use crate::{
tests::helpers,
tests::{helpers, TestContext},
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,
Expand Down
81 changes: 81 additions & 0 deletions test/test-manager/src/tests/audits/mllvd_cr_24_03.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#![cfg(target_os = "linux")]
//! Test mitigation for mllvd_cr_24_03
//!
//! By sending an ARP request for the in-tunnel IP address to any network interface on the device running Mullvad, it
//! will respond and confirm that it owns this address. This means someone on the LAN or similar can figure out the
//! device's in-tunnel IP, and potentially also make an educated guess that they are using Mullvad at all.
//!
//! # Setup
//!
//! Victim: test-runner
//!
//! Network adjacent attacker: test-manager
//!
//! # Procedure
//! Have test-runner connect to relay. Let test-manager know about the test-runner's private in-tunnel IP (such that
//! we don't have to enumerate all possible private IPs).
//!
//! Have test-manager invoke the `arping` command targeting the bridge network between test-manager <-> test-runner.
//! If `arping` times out without a reply, it will exit with a non-0 exit code. If it got a reply from test-runner, it
//! will exit with code 0.
//!
//! Note that only linux was susceptible to this vulnerability.
use std::ffi::OsStr;
use std::process::Output;

use anyhow::bail;
use mullvad_management_interface::MullvadProxyClient;
use test_macro::test_function;
use test_rpc::ServiceClient;

use crate::tests::helpers::*;
use crate::tests::TestContext;
use crate::vm::network::bridge;

#[test_function(target_os = "linux")]
pub async fn test_mllvd_cr_24_03(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// Get the bridge network between manager and runner. This will be used when invoking `arping`.
let bridge = bridge()?;
// Connect runner to a relay. After this point we will be able to acquire the runner's private in-tunnel IP.
connect_and_wait(&mut mullvad_client).await?;
// Get the private ip address
let in_tunnel_ip = {
let vpn_interface = get_tunnel_interface(&mut mullvad_client).await?;
rpc.get_interface_ip(vpn_interface).await?
};
// Invoke arping
let malicious_arping = arping([
"-w",
"5",
"-i",
"1",
"-I",
&bridge,
&in_tunnel_ip.to_string(),
])
.await?;
// If arping exited with code 0, it means the runner replied to the ARP request, implying the runner leaked its
// private in-tunnel IP!
if let Some(0) = malicious_arping.status.code() {
log::error!("{}", String::from_utf8(malicious_arping.stdout)?);
bail!("ARP leak detected")
}
// test runner did not respond to ARP request, leak mitigation seems to work!
Ok(())
}

/// Invoke `arping` on test-manager.
async fn arping<I, S>(args: I) -> std::io::Result<Output>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut arping = tokio::process::Command::new("arping");
arping.args(args);
arping.output().await
}
4 changes: 4 additions & 0 deletions test/test-manager/src/tests/audits/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! This module collects tests for old audit issues to prevent any potential regression.
pub mod cve_2019_14899;
pub mod mllvd_cr_24_03;
pub mod mul_02_002;
98 changes: 98 additions & 0 deletions test/test-manager/src/tests/audits/mul_02_002.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! Test mitigation for MUL-02-002-WP2
//!
//! Fail to leak traffic to verify that mitigation "Firewall allows deanonymization by eavesdropper" works.
//!
//! # Vulnerability
//! 1. Connect to a relay on port 443. Record this relay's IP address (the new gateway of the
//! client)
//! 2. Start listening for unencrypted traffic on the outbound network interface
//! (Choose some human-readable, identifiable payload to look for in the outgoing TCP packets)
//! 3. Start a rogue program which performs a GET request\* containing the payload defined in step 2
//! 4. The network snooper started in step 2 should now be able to observe the network request
//! containing the identifiable payload being sent unencrypted over the wire
//!
//! \* or something similar, as long as it generates some traffic containing UDP and/or TCP packets
//! with the correct payload.
use anyhow::{bail, ensure};
use mullvad_management_interface::MullvadProxyClient;
use mullvad_relay_selector::query::builder::{RelayQueryBuilder, TransportProtocol};
use mullvad_types::states::TunnelState;
use test_macro::test_function;
use test_rpc::ServiceClient;

use crate::network_monitor::{start_packet_monitor, MonitorOptions, ParsedPacket};
use crate::tests::helpers::{
connect_and_wait, constrain_to_relay, disconnect_and_wait, ConnChecker,
};
use crate::tests::TestContext;

#[test_function]
pub async fn test_mul_02_002(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// Step 1 - Choose a relay
constrain_to_relay(
&mut mullvad_client,
RelayQueryBuilder::new()
.openvpn()
.transport_protocol(TransportProtocol::Tcp)
.port(443)
.build(),
)
.await?;

// Step 1.5 - Temporarily connect to the relay to get the target endpoint
let tunnel_state = connect_and_wait(&mut mullvad_client).await?;
let TunnelState::Connected { endpoint, .. } = tunnel_state else {
bail!("Expected tunnel state to be `Connected` - instead it was {tunnel_state:?}");
};
disconnect_and_wait(&mut mullvad_client).await?;
let target_endpoint = endpoint.endpoint.address;

// Step 2 - Start a network monitor snooping the outbound network interface for some
// identifiable payload
let unique_identifier = "Hello there!";
let identify_rogue_packet = move |packet: &ParsedPacket| {
packet
.payload
.windows(unique_identifier.len())
.any(|window| window == unique_identifier.as_bytes())
};
let rogue_packet_monitor =
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;

// Step 3 - Start the rogue program which will try to leak the unique identifier payload
// to the chosen relay endpoint
let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), target_endpoint);
checker.payload(unique_identifier);
let mut conn_artist = checker.spawn().await?;
// Before proceeding, assert that the method of detecting identifiable packets work.
conn_artist.check_connection().await?;
let monitor_result = rogue_packet_monitor.into_result().await?;

log::info!("Checking that the identifiable payload was detectable without encryption");
ensure!(
!monitor_result.packets.is_empty(),
"Did not observe rogue packets! The method seems to be broken"
);
log::info!("The identifiable payload was detected! (that's good)");

// Step 4 - Finally, connect to a tunnel and assert that no outgoing traffic contains the
// payload in plain text.
connect_and_wait(&mut mullvad_client).await?;
let rogue_packet_monitor =
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;
conn_artist.check_connection().await?;
let monitor_result = rogue_packet_monitor.into_result().await?;

log::info!("Checking that the identifiable payload was not detected");
ensure!(
monitor_result.packets.is_empty(),
"Observed rogue packets! The tunnel seems to be leaking traffic"
);

Ok(())
}
11 changes: 7 additions & 4 deletions test/test-manager/src/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,15 @@ pub async fn using_mullvad_exit(rpc: &ServiceClient) -> bool {
}

/// Get VPN tunnel interface name
pub async fn get_tunnel_interface(client: &mut MullvadProxyClient) -> Option<String> {
match client.get_tunnel_state().await.ok()? {
pub async fn get_tunnel_interface(client: &mut MullvadProxyClient) -> anyhow::Result<String> {
match client.get_tunnel_state().await? {
TunnelState::Connecting { endpoint, .. } | TunnelState::Connected { endpoint, .. } => {
endpoint.tunnel_interface
let Some(tunnel_interface) = endpoint.tunnel_interface else {
bail!("Unknown tunnel interface");
};
Ok(tunnel_interface)
}
_ => None,
_ => bail!("Tunnel is not up"),
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/test-manager/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod access_methods;
mod account;
mod audits;
pub mod config;
mod cve_2019_14899;
mod daita;
mod dns;
mod helpers;
Expand Down
91 changes: 3 additions & 88 deletions test/test-manager/src/tests/tunnel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use super::{
Error, TestContext,
};
use crate::{
network_monitor::{start_packet_monitor, MonitorOptions, ParsedPacket},
tests::helpers::{login_with_retries, ConnChecker},
network_monitor::{start_packet_monitor, MonitorOptions},
tests::helpers::login_with_retries,
};

use anyhow::{bail, ensure, Context};
use anyhow::Context;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
use mullvad_types::{
Expand All @@ -20,7 +20,6 @@ use mullvad_types::{
self, BridgeConstraints, BridgeSettings, BridgeType, OpenVpnConstraints, RelayConstraints,
RelaySettings, TransportPort, WireguardConstraints,
},
states::TunnelState,
wireguard,
};
use std::net::SocketAddr;
Expand Down Expand Up @@ -833,87 +832,3 @@ pub async fn test_establish_tunnel_without_api(
// Profit
Ok(())
}

/// Fail to leak traffic to verify that mitigation for MUL-02-002-WP2
/// ("Firewall allows deanonymization by eavesdropper") works.
///
/// # Vulnerability
/// 1. Connect to a relay on port 443. Record this relay's IP address (the new gateway of the
/// client)
/// 2. Start listening for unencrypted traffic on the outbound network interface
/// (Choose some human-readable, identifiable payload to look for in the outgoing TCP packets)
/// 3. Start a rogue program which performs a GET request* containing the payload defined in step 2
/// 4. The network snooper started in step 2 should now be able to observe the network request
/// containing the identifiable payload being sent unencrypted over the wire
///
/// * or something similiar, as long as it generates some traffic containing UDP and/or TCP packets
/// with the correct payload.
#[test_function]
pub async fn test_mul_02_002(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// Step 1 - Choose a relay
helpers::constrain_to_relay(
&mut mullvad_client,
RelayQueryBuilder::new()
.openvpn()
.transport_protocol(TransportProtocol::Tcp)
.port(443)
.build(),
)
.await?;

// Step 1.5 - Temporarily connect to the relay to get the target endpoint
let tunnel_state = helpers::connect_and_wait(&mut mullvad_client).await?;
let TunnelState::Connected { endpoint, .. } = tunnel_state else {
bail!("Expected tunnel state to be `Connected` - instead it was {tunnel_state:?}");
};
helpers::disconnect_and_wait(&mut mullvad_client).await?;
let target_endpoint = endpoint.endpoint.address;

// Step 2 - Start a network monitor snooping the outbound network interface for some
// identifiable payload
let unique_identifier = "Hello there!";
let identify_rogue_packet = move |packet: &ParsedPacket| {
packet
.payload
.windows(unique_identifier.len())
.any(|window| window == unique_identifier.as_bytes())
};
let rogue_packet_monitor =
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;

// Step 3 - Start the rogue program which will try to leak the unique identifier payload
// to the chosen relay endpoint
let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), target_endpoint);
checker.payload(unique_identifier);
let mut conn_artist = checker.spawn().await?;
// Before proceeding, assert that the method of detecting identifiable packets work.
conn_artist.check_connection().await?;
let monitor_result = rogue_packet_monitor.into_result().await?;

log::info!("Checking that the identifiable payload was detectable without encryption");
ensure!(
!monitor_result.packets.is_empty(),
"Did not observe rogue packets! The method seems to be broken"
);
log::info!("The identifiable payload was detected! (that's good)");

// Step 4 - Finally, connect to a tunnel and assert that no outgoing traffic contains the
// payload in plain text.
helpers::connect_and_wait(&mut mullvad_client).await?;
let rogue_packet_monitor =
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;
conn_artist.check_connection().await?;
let monitor_result = rogue_packet_monitor.into_result().await?;

log::info!("Checking that the identifiable payload was not detected");
ensure!(
monitor_result.packets.is_empty(),
"Observed rogue packets! The tunnel seems to be leaking traffic"
);

Ok(())
}

0 comments on commit 4d7fb81

Please sign in to comment.