1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
use anyhow::Context;
use mullvad_management_interface::MullvadProxyClient;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use test_macro::test_function;
use test_rpc::{ServiceClient, meta::OsVersion};
use super::{
TestContext,
helpers::{self, ConnChecker},
ui,
};
const LEAK_DESTINATION: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 1337);
/// Test that split tunneling works by asserting the following:
/// - Splitting a process shouldn't do anything if tunnel is not connected.
/// - A split process should never push traffic through the tunnel.
/// - Splitting/unsplitting should work regardless if process is running.
#[test_function]
pub async fn test_split_tunnel(
_ctx: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// Skip test on macOS 12, since the feature is unsupported
if is_macos_12_or_lower(&rpc).await? {
return Ok(());
}
let mut checker = ConnChecker::new(rpc.clone(), mullvad_client.clone(), LEAK_DESTINATION);
// Test that program is behaving when we are disconnected
(checker.spawn().await?.assert_insecure().await)
.with_context(|| "Test disconnected and unsplit")?;
checker.split().await?;
(checker.spawn().await?.assert_insecure().await)
.with_context(|| "Test disconnected and split")?;
checker.unsplit().await?;
// Test that program is behaving being split/unsplit while running and we are disconnected
let mut handle = checker.spawn().await?;
handle.split().await?;
(handle.assert_insecure().await)
.with_context(|| "Test disconnected and being split while running")?;
handle.unsplit().await?;
(handle.assert_insecure().await)
.with_context(|| "Test disconnected and being unsplit while running")?;
drop(handle);
helpers::connect_and_wait(&mut mullvad_client).await?;
// Test running an unsplit program
checker
.spawn()
.await?
.assert_secure()
.await
.with_context(|| "Test connected and unsplit")?;
// Test running a split program
checker.split().await?;
checker
.spawn()
.await?
.assert_insecure()
.await
.with_context(|| "Test connected and split")?;
checker.unsplit().await?;
// Test splitting and unsplitting a program while it's running
let mut handle = checker.spawn().await?;
(handle.assert_secure().await).with_context(|| "Test connected and unsplit (again)")?;
handle.split().await?;
(handle.assert_insecure().await)
.with_context(|| "Test connected and being split while running")?;
handle.unsplit().await?;
(handle.assert_secure().await)
.with_context(|| "Test connected and being unsplit while running")?;
Ok(())
}
/// Test that split tunneling works by asserting the following:
/// - Splitting a process with the split tunneling (ST) feature enabled and an active tunnel
/// allow the split process to leak.
/// - Disabling ST forces the split program to route its traffic through the active tunnel.
/// - Enabling ST allows the split program to leak again.
/// The property we're testing for here is that toggling ST respects the list of split apps, and
/// vice-versa.
///
/// NOTE: This will not work with Linux split tunneling, since there is no persistant list of split
/// apps(yet!).
#[test_function(target_os = "macos", target_os = "windows")]
pub async fn test_split_tunnel_toggle(
_ctx: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
// I'm a gamer, so I want to split steam for maximum performance.
let mut steam = ConnChecker::new(rpc.clone(), mullvad_client.clone(), LEAK_DESTINATION);
// Enable the split tunneling feature in the daemon.
// No apps are split at this point.
//
// Note: ConnChecker::split already does this, but being explicit with the state the we expect
// the daemon to be in at any stage of the test is not harmful.
mullvad_client.set_split_tunnel_state(true).await?; // <- Split tunneling: on
// Connect.
helpers::connect_and_wait(&mut mullvad_client).await?;
// Assert that steam does not leak yet. We are yet to add it as a split app.
let mut steam = steam.spawn().await?;
steam.assert_secure().await?;
// Assert that splitting the process does indeed leak.
steam.split().await?; // <- SPLIT
steam.assert_insecure().await?;
// Disabling split-tunneling at the settings-level should force all traffic through the tunnel.
// HACK: ConnChecker::split tries to be clever and enables ST at the settings-level.
// Therefore we have to explicitly disable it *after* calling split.
mullvad_client.set_split_tunnel_state(false).await?; // <- Split tunneling: off
// Steam should now be forced to route traffic through the tunnel again.
steam.assert_secure().await?;
// Re-enabling split-tunneling will once again make the split program leak.
mullvad_client.set_split_tunnel_state(true).await?; // <- Split tunneling: on
steam.assert_insecure().await?;
Ok(())
}
/// Test that split tunneling works by asserting the following:
/// - Splitting a process shouldn't do anything if tunnel is not connected.
/// - A split process should never push traffic through the tunnel.
/// - Splitting/unsplitting should work regardless if process is running.
#[test_function(target_os = "macos", target_os = "windows")]
pub async fn test_split_tunnel_ui(
_ctx: TestContext,
rpc: ServiceClient,
_: MullvadProxyClient,
) -> anyhow::Result<()> {
// Skip test on macOS 12 and on Linux, since the feature is unsupported
if cfg!(target_os = "macos") && is_macos_12_or_lower(&rpc).await? {
return Ok(());
}
let ui_result = ui::run_test(&rpc, &["split-tunneling.spec"]).await.unwrap();
assert!(ui_result.success());
Ok(())
}
async fn is_macos_12_or_lower(rpc: &ServiceClient) -> anyhow::Result<bool> {
match rpc.get_os_version().await.context("Detect OS version")? {
OsVersion::Macos(version) if version.major <= 12 => Ok(true),
_ => Ok(false),
}
}
|