Skip to content

Commit 3a2ca13

Browse files
Implement test for audit ticket MUL-02-002 WP2
1 parent 6c58ebc commit 3a2ca13

File tree

4 files changed

+103
-4
lines changed

4 files changed

+103
-4
lines changed

test/connection-checker/src/net.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::{
77

88
use crate::cli::Opt;
99

10+
const PAYLOAD: &[u8] = b"Hello there!";
11+
1012
pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> {
1113
let bind_addr: SocketAddr = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0);
1214

@@ -31,7 +33,7 @@ pub fn send_tcp(opt: &Opt, destination: SocketAddr) -> eyre::Result<()> {
3133

3234
let mut stream = std::net::TcpStream::from(sock);
3335
stream
34-
.write_all(b"hello there")
36+
.write_all(PAYLOAD)
3537
.wrap_err(eyre!("Failed to send message to {destination}"))?;
3638

3739
Ok(())
@@ -56,7 +58,7 @@ pub fn send_udp(_opt: &Opt, destination: SocketAddr) -> Result<(), eyre::Error>
5658

5759
let std_socket = std::net::UdpSocket::from(sock);
5860
std_socket
59-
.send_to(b"Hello there!", destination)
61+
.send_to(PAYLOAD, destination)
6062
.wrap_err(eyre!("Failed to send message to {destination}"))?;
6163

6264
Ok(())

test/test-manager/src/network_monitor.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct ParsedPacket {
2626
pub source: SocketAddr,
2727
pub destination: SocketAddr,
2828
pub protocol: IpNextHeaderProtocol,
29+
pub payload: Vec<u8>,
2930
}
3031

3132
impl PacketCodec for Codec {
@@ -75,9 +76,9 @@ impl Codec {
7576

7677
let mut source = SocketAddr::new(IpAddr::V4(packet.get_source()), 0);
7778
let mut destination = SocketAddr::new(IpAddr::V4(packet.get_destination()), 0);
79+
let mut payload = vec![];
7880

7981
let protocol = packet.get_next_level_protocol();
80-
8182
match protocol {
8283
IpHeaderProtocols::Tcp => {
8384
let seg = TcpPacket::new(packet.payload()).or_else(|| {
@@ -86,6 +87,7 @@ impl Codec {
8687
})?;
8788
source.set_port(seg.get_source());
8889
destination.set_port(seg.get_destination());
90+
payload = seg.payload().to_vec();
8991
}
9092
IpHeaderProtocols::Udp => {
9193
let seg = UdpPacket::new(packet.payload()).or_else(|| {
@@ -94,6 +96,7 @@ impl Codec {
9496
})?;
9597
source.set_port(seg.get_source());
9698
destination.set_port(seg.get_destination());
99+
payload = seg.payload().to_vec();
97100
}
98101
IpHeaderProtocols::Icmp => {}
99102
proto => log::debug!("ignoring v4 packet, transport/protocol type {proto}"),
@@ -103,6 +106,7 @@ impl Codec {
103106
source,
104107
destination,
105108
protocol,
109+
payload,
106110
})
107111
}
108112

@@ -114,6 +118,7 @@ impl Codec {
114118

115119
let mut source = SocketAddr::new(IpAddr::V6(packet.get_source()), 0);
116120
let mut destination = SocketAddr::new(IpAddr::V6(packet.get_destination()), 0);
121+
let mut payload = vec![];
117122

118123
let protocol = packet.get_next_header();
119124
match protocol {
@@ -124,6 +129,7 @@ impl Codec {
124129
})?;
125130
source.set_port(seg.get_source());
126131
destination.set_port(seg.get_destination());
132+
payload = seg.payload().to_vec();
127133
}
128134
IpHeaderProtocols::Udp => {
129135
let seg = UdpPacket::new(packet.payload()).or_else(|| {
@@ -132,6 +138,7 @@ impl Codec {
132138
})?;
133139
source.set_port(seg.get_source());
134140
destination.set_port(seg.get_destination());
141+
payload = seg.payload().to_vec();
135142
}
136143
IpHeaderProtocols::Icmpv6 => {}
137144
proto => log::debug!("ignoring v6 packet, transport/protocol type {proto}"),
@@ -141,6 +148,7 @@ impl Codec {
141148
source,
142149
destination,
143150
protocol,
151+
payload,
144152
})
145153
}
146154
}

test/test-manager/src/tests/install.rs

+1
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ pub async fn test_installation_idempotency(
287287
// Connect to any relay. This forces the daemon to enter a secured target state
288288
connect_and_wait(&mut mullvad_client)
289289
.await
290+
.map(|_| ()) // Discard the new tunnel state
290291
.or_else(|error| match error {
291292
Error::UnexpectedErrorState(_) => Ok(()),
292293
err => Err(err),

test/test-manager/src/tests/tunnel.rs

+89-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use super::helpers::{
22
self, connect_and_wait, disconnect_and_wait, set_bridge_settings, set_relay_settings,
33
};
44
use super::{config::TEST_CONFIG, Error, TestContext};
5-
use crate::network_monitor::{start_packet_monitor, MonitorOptions};
5+
use crate::network_monitor::{start_packet_monitor, MonitorOptions, ParsedPacket};
66
use crate::tests::helpers::{login_with_retries, ConnChecker};
77

8+
use anyhow::{bail, ensure};
89
use mullvad_management_interface::MullvadProxyClient;
910
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
11+
use mullvad_types::states::TunnelState;
1012
use mullvad_types::{
1113
constraints::Constraint,
1214
relay_constraints::{
@@ -802,3 +804,89 @@ pub async fn test_establish_tunnel_without_api(
802804
// Profit
803805
Ok(())
804806
}
807+
808+
/// Fail to leak traffic to verify that mitigation for MUL-02-002-WP2
809+
/// ("Firewall allows deanonymization by eavesdropper") works.
810+
///
811+
/// # Vulnerability
812+
/// 1. Connect to a relay on port 443. Record this relay's IP address (the new gateway of the client)
813+
/// 2. Start listening for unencrypted traffic on the outbound network interface
814+
/// (Choose some human-readable, identifiable payload to look for in the outgoing TCP packets)
815+
/// 3. Start a rogue program which performs a GET request* containing the payload defined in step 2
816+
/// 4. The network snooper started in step 2 should now be able to observe the network request
817+
/// containing the identifiable payload being sent unencrypted over the wire
818+
///
819+
/// * or something similiar, as long as it generates some traffic containing UDP and/or TCP packets
820+
/// with the correct payload.
821+
#[test_function]
822+
pub async fn test_mul_02_002(
823+
_: TestContext,
824+
rpc: ServiceClient,
825+
mut mullvad_client: MullvadProxyClient,
826+
) -> Result<(), anyhow::Error> {
827+
// Step 0 - Disconnect from any active tunnel connection
828+
helpers::disconnect_and_wait(&mut mullvad_client).await?;
829+
// Step 1 - Choose a relay
830+
let relay_constraints = RelayQueryBuilder::new()
831+
.openvpn()
832+
.transport_protocol(TransportProtocol::Tcp)
833+
.port(443)
834+
.into_constraint();
835+
836+
set_relay_settings(
837+
&mut mullvad_client,
838+
RelaySettings::Normal(relay_constraints),
839+
)
840+
.await?;
841+
842+
// Step 1.5 - Temporarily connect to the relay to get the target endpoint
843+
let tunnel_state = helpers::connect_and_wait(&mut mullvad_client).await?;
844+
let TunnelState::Connected { endpoint, .. } = tunnel_state else {
845+
bail!("Expected tunnel state to be `Connected` - instead it was {tunnel_state:?}");
846+
};
847+
helpers::disconnect_and_wait(&mut mullvad_client).await?;
848+
let gateway = endpoint.endpoint.address;
849+
850+
// Step 2 - Start a network monitor snooping the outbound network interface for some
851+
// identifiable payload
852+
// FIXME: This needs to be kept in sync with the magic payload string defined in `connection_cheker::net`.
853+
// An easy fix would be to make the payload for `ConnCheck` configurable.
854+
let unique_identifier = b"Hello there!";
855+
let identify_rogue_packet = move |packet: &ParsedPacket| {
856+
packet
857+
.payload
858+
.windows(unique_identifier.len())
859+
.any(|window| window == unique_identifier)
860+
};
861+
let rogue_packet_monitor =
862+
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;
863+
864+
// Step 3 - Start the rogue program which will try to leak traffic to the chosen relay endpoint
865+
let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), gateway);
866+
let mut conn_artist = checker.spawn().await?;
867+
// Before proceeding, assert that the method of detecting identifiable packets work.
868+
conn_artist.check_connection().await?;
869+
let monitor_result = rogue_packet_monitor.into_result().await.unwrap();
870+
871+
log::info!("Checking that the identifiable payload was detectable without encryption");
872+
ensure!(
873+
!monitor_result.packets.is_empty(),
874+
"Did not observe rogue packets! The method seems to be broken"
875+
);
876+
877+
// Step 4 - Finally, connect to a tunnel and assert that no outgoing traffic contains the
878+
// payload in plain text.
879+
helpers::connect_and_wait(&mut mullvad_client).await?;
880+
let rogue_packet_monitor =
881+
start_packet_monitor(identify_rogue_packet, MonitorOptions::default()).await;
882+
conn_artist.check_connection().await?;
883+
let monitor_result = rogue_packet_monitor.into_result().await.unwrap();
884+
885+
log::info!("Checking that the identifiable payload was not detected");
886+
ensure!(
887+
monitor_result.packets.is_empty(),
888+
"Observed rogue packets! The tunnel seems to be leaking traffic"
889+
);
890+
891+
Ok(())
892+
}

0 commit comments

Comments
 (0)