Skip to content

Commit 18ac9a4

Browse files
Implement test for audit ticket MUL-02-002 WP2
1 parent 36a4dc3 commit 18ac9a4

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
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

+7
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 {
@@ -99,10 +100,13 @@ impl Codec {
99100
proto => log::debug!("ignoring v4 packet, transport/protocol type {proto}"),
100101
}
101102

103+
let payload = packet.payload().to_vec();
104+
102105
Some(ParsedPacket {
103106
source,
104107
destination,
105108
protocol,
109+
payload,
106110
})
107111
}
108112

@@ -137,10 +141,13 @@ impl Codec {
137141
proto => log::debug!("ignoring v6 packet, transport/protocol type {proto}"),
138142
}
139143

144+
let payload = packet.payload().to_vec();
145+
140146
Some(ParsedPacket {
141147
source,
142148
destination,
143149
protocol,
150+
payload,
144151
})
145152
}
146153
}

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

+71-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ use super::helpers::{
33
};
44
use super::{config::TEST_CONFIG, Error, TestContext};
55
use crate::network_monitor::{start_packet_monitor, MonitorOptions};
6-
use crate::tests::helpers::login_with_retries;
6+
use crate::tests::helpers::ConnChecker;
77

88
use mullvad_management_interface::MullvadProxyClient;
99
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
10+
use mullvad_types::states::TunnelState;
1011
use mullvad_types::{
1112
constraints::Constraint,
1213
relay_constraints::{
@@ -802,3 +803,72 @@ pub async fn test_establish_tunnel_without_api(
802803
// Profit
803804
Ok(())
804805
}
806+
807+
/// Fail to leak traffic to verify that mitigation for MUL-02-002-WP2
808+
/// ("Firewall allows deanonymization by eavesdropper") works.
809+
///
810+
/// # Vulnerability
811+
/// 1. Connect to a relay on port 443. Record this relay's IP address (the new gateway of the client)
812+
/// 2. Start listening for unencrypted traffic on the outbound network interface
813+
/// (Choose some human-readable, identifiable payload to look for in the outgoing TCP packets)
814+
/// 3. Start a rogue program which performs a GET request* containing the payload defined in step 2
815+
/// 4. The network snooper started in step 2 should now be able to observe the network request
816+
/// containing the identifiable payload being sent unencrypted over the wire
817+
///
818+
/// * or something similiar, as long as it fetches some remote resources
819+
#[test_function]
820+
pub async fn test_mul_02_002(
821+
_: TestContext,
822+
rpc: ServiceClient,
823+
mut mullvad_client: MullvadProxyClient,
824+
) -> Result<(), anyhow::Error> {
825+
// Step 0 - Disconnect from any active tunnel connection
826+
helpers::disconnect_and_wait(&mut mullvad_client).await?;
827+
// Step 1 - Choose a relay
828+
let relay_constraints = RelayQueryBuilder::new()
829+
.openvpn()
830+
.transport_protocol(TransportProtocol::Tcp)
831+
.port(443)
832+
.into_constraint();
833+
// Step 1.5 - Connect to the relay
834+
set_relay_settings(
835+
&mut mullvad_client,
836+
RelaySettings::Normal(relay_constraints),
837+
)
838+
.await?;
839+
840+
let tunnel_state = helpers::connect_and_wait(&mut mullvad_client).await?;
841+
let TunnelState::Connected { endpoint, .. } = tunnel_state else {
842+
panic!("Expected tunnel state to be `Connected` - instead it was {tunnel_state:?}");
843+
};
844+
let gateway = endpoint.endpoint.address;
845+
// Step 2 - Choose a payload
846+
// FIXME: This needs to be kept in sync with the magic payload string defined in `connection_cheker::net`.
847+
// The payload for `ConnChecherk` could also be made configurable.
848+
let unique_identifier = b"Hello there!";
849+
// Step 2.5 - Start a network monitor snooping the outbound network interface for payload
850+
let monitor = start_packet_monitor(
851+
move |packet| {
852+
packet.destination.ip() == gateway.ip()
853+
&& packet.protocol == IpNextHeaderProtocols::Tcp
854+
&& packet
855+
.payload
856+
.windows(unique_identifier.len())
857+
.any(|window| window == unique_identifier)
858+
},
859+
MonitorOptions::default(),
860+
)
861+
.await;
862+
863+
// Step 3 - Start the rogue program using payload + relay info
864+
let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), gateway);
865+
checker.spawn().await?.check_connection().await?;
866+
// Step 4 - Assert that no outgoing traffic contains the payload in plain text.
867+
let monitor_result = monitor.into_result().await.unwrap();
868+
assert!(
869+
monitor_result.packets.is_empty(),
870+
"Observed rogue packets! The tunnel seems to be leaking traffic"
871+
);
872+
// Step 5 - Profit
873+
Ok(())
874+
}

0 commit comments

Comments
 (0)