diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-03-20 17:30:02 +0100 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-04-09 14:58:56 +0200 |
| commit | 3ca257e84e23232943966233f93fa9d0fbc101b7 (patch) | |
| tree | 20fd23b7f89656d8de7c9da57c249d7726e5f3bd /test | |
| parent | 47091d9f5c456a827e64fc8493c62059f1503a1d (diff) | |
| download | mullvadvpn-3ca257e84e23232943966233f93fa9d0fbc101b7.tar.xz mullvadvpn-3ca257e84e23232943966233f93fa9d0fbc101b7.zip | |
Implement test for audit ticket `MUL-02-002 WP2`
Diffstat (limited to 'test')
| -rw-r--r-- | test/connection-checker/src/net.rs | 4 | ||||
| -rw-r--r-- | test/test-manager/src/network_monitor.rs | 17 | ||||
| -rw-r--r-- | test/test-manager/src/tests/helpers.rs | 76 | ||||
| -rw-r--r-- | test/test-manager/src/tests/install.rs | 1 | ||||
| -rw-r--r-- | test/test-manager/src/tests/tunnel.rs | 89 | ||||
| -rw-r--r-- | test/test-manager/src/tests/tunnel_state.rs | 33 | ||||
| -rw-r--r-- | test/test-manager/src/tests/ui.rs | 28 |
7 files changed, 166 insertions, 82 deletions
diff --git a/test/connection-checker/src/net.rs b/test/connection-checker/src/net.rs index 2e17423933..ff45993408 100644 --- a/test/connection-checker/src/net.rs +++ b/test/connection-checker/src/net.rs @@ -31,7 +31,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 +56,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 87a8193e29..ddd35edc7e 100644 --- a/test/test-manager/src/network_monitor.rs +++ b/test/test-manager/src/network_monitor.rs @@ -25,6 +25,7 @@ pub struct ParsedPacket { pub source: SocketAddr, pub destination: SocketAddr, pub protocol: IpNextHeaderProtocol, + pub payload: Vec<u8>, } impl PacketCodec for Codec { @@ -74,9 +75,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(|| { @@ -85,6 +86,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(|| { @@ -93,6 +95,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}"), @@ -102,6 +105,7 @@ impl Codec { source, destination, protocol, + payload, }) } @@ -113,6 +117,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 { @@ -123,6 +128,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(|| { @@ -131,6 +137,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}"), @@ -140,12 +147,14 @@ impl Codec { source, destination, protocol, + payload, }) } } -#[derive(Debug)] -pub struct MonitorUnexpectedlyStopped(()); +#[derive(Debug, thiserror::Error)] +#[error("Packet monitor stopped unexpectedly")] +pub struct MonitorUnexpectedlyStopped; pub struct PacketMonitor { handle: tokio::task::JoinHandle<Result<MonitorResult, MonitorUnexpectedlyStopped>>, @@ -297,7 +306,7 @@ async fn start_packet_monitor_for_interface( } _ => { log::error!("lost packet stream"); - break Err(MonitorUnexpectedlyStopped(())); + break Err(MonitorUnexpectedlyStopped); } } } diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index 3652d1f15a..2dff6eccdd 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -5,13 +5,17 @@ use crate::network_monitor::{ use anyhow::{anyhow, bail, ensure, Context}; use futures::StreamExt; use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; +use mullvad_relay_selector::{ + query::RelayQuery, GetRelay, RelaySelector, SelectorConfig, WireguardConfig, +}; use mullvad_types::{ constraints::Constraint, location::Location, relay_constraints::{ - BridgeSettings, GeographicLocationConstraint, LocationConstraint, RelaySettings, + BridgeSettings, GeographicLocationConstraint, LocationConstraint, RelayConstraints, + RelaySettings, }, - relay_list::{Relay, RelayList}, + relay_list::Relay, states::TunnelState, }; use pcap::Direction; @@ -499,27 +503,49 @@ pub fn get_app_env() -> HashMap<String, String> { ]) } -/// Return a filtered version of the daemon's relay list. +/// Constrain the daemon to only select the relay selected with `query` when establishing all +/// future tunnels (until relay settings are updated, see [`set_relay_settings`]). Returns the +/// selected [`Relay`] for future reference. /// -/// * `mullvad_client` - An interface to the Mullvad daemon. -/// * `critera` - A function used to determine which relays to return. -pub async fn filter_relays<Filter>( +/// # Note +/// This function does not handle bridges and multihop configurations (currently). There is no +/// particular reason for this other than it not being needed at the time, so feel free to extend this +/// function :). +pub async fn constrain_to_relay( mullvad_client: &mut MullvadProxyClient, - criteria: Filter, -) -> Result<Vec<Relay>, Error> -where - Filter: Fn(&Relay) -> bool, -{ - let relay_list: RelayList = mullvad_client - .get_relay_locations() - .await - .map_err(|error| Error::Daemon(format!("Failed to obtain relay list: {}", error)))?; + query: RelayQuery, +) -> anyhow::Result<Relay> { + /// Convert the result of invoking the relay selector to a relay constraint. + fn convert_to_relay_constraints( + selected_relay: GetRelay, + ) -> anyhow::Result<(Relay, RelayConstraints)> { + match selected_relay { + GetRelay::Wireguard { + inner: WireguardConfig::Singlehop { exit }, + .. + } + | GetRelay::OpenVpn { exit, .. } => { + let location = into_constraint(&exit)?; + let relay_constraints = RelayConstraints { + location, + ..Default::default() + }; + Ok((exit, relay_constraints)) + } + unsupported => bail!("Can not constrain to a {unsupported:?}"), + } + } + + // Construct a relay selector with up-to-date information from the runnin daemon's relay list + let relay_list = mullvad_client.get_relay_locations().await?; + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list); + // Select an(y) appropriate relay for the given query and constrain the daemon to only connect + // to that specific relay (when connecting). + let relay = relay_selector.get_relay_by_query(query)?; + let (exit, relay_constraints) = convert_to_relay_constraints(relay)?; + set_relay_settings(mullvad_client, RelaySettings::Normal(relay_constraints)).await?; - Ok(relay_list - .relays() - .filter(|relay| criteria(relay)) - .cloned() - .collect()) + Ok(exit) } /// Convenience function for constructing a constraint from a given [`Relay`]. @@ -527,7 +553,7 @@ where /// # Panics /// /// The relay must have a location set. -pub fn into_constraint(relay: &Relay) -> Constraint<LocationConstraint> { +pub fn into_constraint(relay: &Relay) -> anyhow::Result<Constraint<LocationConstraint>> { relay .location .as_ref() @@ -537,16 +563,12 @@ pub fn into_constraint(relay: &Relay) -> Constraint<LocationConstraint> { city_code, .. }| { - GeographicLocationConstraint::Hostname( - country_code.to_string(), - city_code.to_string(), - relay.hostname.to_string(), - ) + GeographicLocationConstraint::hostname(country_code, city_code, &relay.hostname) }, ) .map(LocationConstraint::Location) .map(Constraint::Only) - .expect("relay is missing location") + .ok_or(anyhow!("relay is missing location")) } /// Ping monitoring made easy! diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index cb1ddf58c5..f0e0942af7 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -281,6 +281,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 a0acbf2114..594c930ef0 100644 --- a/test/test-manager/src/tests/tunnel.rs +++ b/test/test-manager/src/tests/tunnel.rs @@ -6,10 +6,11 @@ use super::{ Error, TestContext, }; use crate::{ - network_monitor::{start_packet_monitor, MonitorOptions}, - tests::helpers::login_with_retries, + network_monitor::{start_packet_monitor, MonitorOptions, ParsedPacket}, + 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::{ @@ -19,6 +20,7 @@ use mullvad_types::{ RelaySettings, SelectedObfuscation, TransportPort, Udp2TcpObfuscationSettings, WireguardConstraints, }, + states::TunnelState, wireguard, }; use std::net::SocketAddr; @@ -787,3 +789,86 @@ 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 + // 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(), target_endpoint); + 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?; + + 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(()) +} diff --git a/test/test-manager/src/tests/tunnel_state.rs b/test/test-manager/src/tests/tunnel_state.rs index 5e69b33b7a..edbe595092 100644 --- a/test/test-manager/src/tests/tunnel_state.rs +++ b/test/test-manager/src/tests/tunnel_state.rs @@ -11,12 +11,12 @@ use crate::{ }; use mullvad_management_interface::MullvadProxyClient; +use mullvad_relay_selector::query::builder::RelayQueryBuilder; use mullvad_types::{ constraints::Constraint, relay_constraints::{ GeographicLocationConstraint, LocationConstraint, RelayConstraints, RelaySettings, }, - relay_list::{Relay, RelayEndpointData}, states::TunnelState, CustomTunnelEndpoint, }; @@ -340,40 +340,21 @@ pub async fn test_connected_state( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, -) -> Result<(), Error> { +) -> anyhow::Result<()> { let inet_destination = "1.1.1.1:1337".parse().unwrap(); // Set relay to use - // - log::info!("Select relay"); - - let relay_filter = |relay: &Relay| { - relay.active && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) - }; - - let relay = helpers::filter_relays(&mut mullvad_client, relay_filter) - .await? - .pop() - .unwrap(); - - let relay_settings = RelaySettings::Normal(RelayConstraints { - location: helpers::into_constraint(&relay), - ..Default::default() - }); - - set_relay_settings(&mut mullvad_client, relay_settings) - .await - .expect("failed to update relay settings"); + let relay = helpers::constrain_to_relay( + &mut mullvad_client, + RelayQueryBuilder::new().wireguard().build(), + ) + .await?; // Connect - // - connect_and_wait(&mut mullvad_client).await?; // Verify that endpoint was selected - // - match mullvad_client.get_tunnel_state().await? { TunnelState::Connected { endpoint: diff --git a/test/test-manager/src/tests/ui.rs b/test/test-manager/src/tests/ui.rs index ebd6fb3ee9..cce7cdd990 100644 --- a/test/test-manager/src/tests/ui.rs +++ b/test/test-manager/src/tests/ui.rs @@ -1,9 +1,6 @@ use super::{config::TEST_CONFIG, helpers, Error, TestContext}; use mullvad_management_interface::MullvadProxyClient; -use mullvad_types::{ - relay_constraints::{RelayConstraints, RelaySettings}, - relay_list::{Relay, RelayEndpointData}, -}; +use mullvad_relay_selector::query::builder::RelayQueryBuilder; use std::{ collections::BTreeMap, fmt::Debug, @@ -85,25 +82,14 @@ pub async fn test_ui_tunnel_settings( _: TestContext, rpc: ServiceClient, mut mullvad_client: MullvadProxyClient, -) -> Result<(), Error> { +) -> anyhow::Result<()> { // tunnel-state.spec precondition: a single WireGuard relay should be selected log::info!("Select WireGuard relay"); - let entry = helpers::filter_relays(&mut mullvad_client, |relay: &Relay| { - relay.active && matches!(relay.endpoint_data, RelayEndpointData::Wireguard(_)) - }) - .await? - .pop() - .unwrap(); - - // The test expects us to be disconnected and logged in but to have a specific relay selected - let relay_settings = RelaySettings::Normal(RelayConstraints { - location: helpers::into_constraint(&entry), - ..Default::default() - }); - - helpers::set_relay_settings(&mut mullvad_client, relay_settings) - .await - .expect("failed to update relay settings"); + let entry = helpers::constrain_to_relay( + &mut mullvad_client, + RelayQueryBuilder::new().wireguard().build(), + ) + .await?; let ui_result = run_test_env( &rpc, |
