From 3a2ca13f530c6cd5a1e199f0661dc71331c380f8 Mon Sep 17 00:00:00 2001 From: Markus Pettersson Date: Wed, 20 Mar 2024 17:30:02 +0100 Subject: [PATCH] Implement test for audit ticket `MUL-02-002 WP2` --- test/connection-checker/src/net.rs | 6 +- test/test-manager/src/network_monitor.rs | 10 ++- test/test-manager/src/tests/install.rs | 1 + test/test-manager/src/tests/tunnel.rs | 90 +++++++++++++++++++++++- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/test/connection-checker/src/net.rs b/test/connection-checker/src/net.rs index 6634be41b0c8..b927ffa5413f 100644 --- a/test/connection-checker/src/net.rs +++ b/test/connection-checker/src/net.rs @@ -7,6 +7,8 @@ use std::{ use crate::cli::Opt; +const PAYLOAD: &[u8] = b"Hello there!"; + pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> { let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0); @@ -31,7 +33,7 @@ pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> { let mut stream = std::net::TcpStream::from(sock); stream - .write_all(b"hello there") + .write_all(PAYLOAD) .wrap_err(eyre!("Failed to send message to {destination}"))?; Ok(()) @@ -56,7 +58,7 @@ pub fn send_udp(_opt: &Opt, destination: SocketAddr) -> Result<(), eyre::Error> let std_socket = std::net::UdpSocket::from(sock); std_socket - .send_to(b"Hello there!", destination) + .send_to(PAYLOAD, destination) .wrap_err(eyre!("Failed to send message to {destination}"))?; Ok(()) diff --git a/test/test-manager/src/network_monitor.rs b/test/test-manager/src/network_monitor.rs index c48a660ba070..c07716c9d5f9 100644 --- a/test/test-manager/src/network_monitor.rs +++ b/test/test-manager/src/network_monitor.rs @@ -26,6 +26,7 @@ pub struct ParsedPacket { pub source: SocketAddr, pub destination: SocketAddr, pub protocol: IpNextHeaderProtocol, + pub payload: Vec, } impl PacketCodec for Codec { @@ -75,9 +76,9 @@ impl Codec { let mut source = SocketAddr::new(IpAddr::V4(packet.get_source()), 0); let mut destination = SocketAddr::new(IpAddr::V4(packet.get_destination()), 0); + let mut payload = vec![]; let protocol = packet.get_next_level_protocol(); - match protocol { IpHeaderProtocols::Tcp => { let seg = TcpPacket::new(packet.payload()).or_else(|| { @@ -86,6 +87,7 @@ impl Codec { })?; source.set_port(seg.get_source()); destination.set_port(seg.get_destination()); + payload = seg.payload().to_vec(); } IpHeaderProtocols::Udp => { let seg = UdpPacket::new(packet.payload()).or_else(|| { @@ -94,6 +96,7 @@ impl Codec { })?; source.set_port(seg.get_source()); destination.set_port(seg.get_destination()); + payload = seg.payload().to_vec(); } IpHeaderProtocols::Icmp => {} proto => log::debug!("ignoring v4 packet, transport/protocol type {proto}"), @@ -103,6 +106,7 @@ impl Codec { source, destination, protocol, + payload, }) } @@ -114,6 +118,7 @@ impl Codec { let mut source = SocketAddr::new(IpAddr::V6(packet.get_source()), 0); let mut destination = SocketAddr::new(IpAddr::V6(packet.get_destination()), 0); + let mut payload = vec![]; let protocol = packet.get_next_header(); match protocol { @@ -124,6 +129,7 @@ impl Codec { })?; source.set_port(seg.get_source()); destination.set_port(seg.get_destination()); + payload = seg.payload().to_vec(); } IpHeaderProtocols::Udp => { let seg = UdpPacket::new(packet.payload()).or_else(|| { @@ -132,6 +138,7 @@ impl Codec { })?; source.set_port(seg.get_source()); destination.set_port(seg.get_destination()); + payload = seg.payload().to_vec(); } IpHeaderProtocols::Icmpv6 => {} proto => log::debug!("ignoring v6 packet, transport/protocol type {proto}"), @@ -141,6 +148,7 @@ impl Codec { source, destination, protocol, + payload, }) } } diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index 7dbf092f5c49..a42030dc9f4e 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -287,6 +287,7 @@ pub async fn test_installation_idempotency( // Connect to any relay. This forces the daemon to enter a secured target state connect_and_wait(&mut mullvad_client) .await + .map(|_| ()) // Discard the new tunnel state .or_else(|error| match error { Error::UnexpectedErrorState(_) => Ok(()), err => Err(err), diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs index f93bc549b70f..b37b4018a684 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -2,11 +2,13 @@ use super::helpers::{ self, connect_and_wait, disconnect_and_wait, set_bridge_settings, set_relay_settings, }; use super::{config::TEST_CONFIG, Error, TestContext}; -use crate::network_monitor::{start_packet_monitor, MonitorOptions}; +use crate::network_monitor::{start_packet_monitor, MonitorOptions, ParsedPacket}; use crate::tests::helpers::{login_with_retries, ConnChecker}; +use anyhow::{bail, ensure}; use mullvad_management_interface::MullvadProxyClient; use mullvad_relay_selector::query::builder::RelayQueryBuilder; +use mullvad_types::states::TunnelState; use mullvad_types::{ constraints::Constraint, relay_constraints::{ @@ -802,3 +804,89 @@ 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, +) -> Result<(), anyhow::Error> { + // Step 0 - Disconnect from any active tunnel connection + helpers::disconnect_and_wait(&mut mullvad_client).await?; + // Step 1 - Choose a relay + let relay_constraints = RelayQueryBuilder::new() + .openvpn() + .transport_protocol(TransportProtocol::Tcp) + .port(443) + .into_constraint(); + + set_relay_settings( + &mut mullvad_client, + RelaySettings::Normal(relay_constraints), + ) + .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 gateway = endpoint.endpoint.address; + + // Step 2 - Start a network monitor snooping the outbound network interface for some + // identifiable payload + // FIXME: This needs to be kept in sync with the magic payload string defined in `connection_cheker::net`. + // An easy fix would be to make the payload for `ConnCheck` configurable. + let unique_identifier = b"Hello there!"; + let identify_rogue_packet = move |packet: &ParsedPacket| { + packet + .payload + .windows(unique_identifier.len()) + .any(|window| window == unique_identifier) + }; + let rogue_packet_monitor = + start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await; + + // Step 3 - Start the rogue program which will try to leak traffic to the chosen relay endpoint + let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), gateway); + 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.unwrap(); + + 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" + ); + + // 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.unwrap(); + + 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(()) +}