summaryrefslogtreecommitdiffhomepage
path: root/test/test-manager
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-09-15 11:20:53 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-09-15 11:20:53 +0200
commit23fccfb8f7fc901405bbc141d9ea0472a1f08ce2 (patch)
treefedf61e730dd31a3b768a5a8af47960541c80ba6 /test/test-manager
parent12bb6552e49e19510b3272f8d3d1f0c6277236ff (diff)
parent2aedc93c417ee8af682a8f5bae09ee42552fbfcb (diff)
downloadmullvadvpn-23fccfb8f7fc901405bbc141d9ea0472a1f08ce2.tar.xz
mullvadvpn-23fccfb8f7fc901405bbc141d9ea0472a1f08ce2.zip
Merge branch 'add-test-that-ipv6-in-the-tunnel-works-as-expected-on-des-1088'
Diffstat (limited to 'test/test-manager')
-rw-r--r--test/test-manager/Cargo.toml5
-rw-r--r--test/test-manager/src/container.rs10
-rw-r--r--test/test-manager/src/network_monitor.rs37
-rw-r--r--test/test-manager/src/tests/helpers.rs87
-rw-r--r--test/test-manager/src/tests/relay_ip_overrides.rs8
-rw-r--r--test/test-manager/src/tests/tunnel.rs121
-rw-r--r--test/test-manager/src/vm/network/linux.rs88
7 files changed, 277 insertions, 79 deletions
diff --git a/test/test-manager/Cargo.toml b/test/test-manager/Cargo.toml
index 2225f46ad9..bc7ab9515d 100644
--- a/test/test-manager/Cargo.toml
+++ b/test/test-manager/Cargo.toml
@@ -21,7 +21,7 @@ tokio-serial = { workspace = true }
thiserror = { workspace = true }
bytes = { workspace = true }
test_macro = { path = "./test_macro" }
-ipnetwork = "0.20"
+ipnetwork = "0.21.1"
inventory = "0.3"
data-encoding-macro = "0.1.12"
itertools = "0.10.5"
@@ -61,10 +61,11 @@ mullvad-types = { path = "../../mullvad-types" }
mullvad-version = { path = "../../mullvad-version" }
talpid-types = { path = "../../talpid-types" }
-ssh2 = "0.9.5"
+ssh2 = { version = "0.9.5", features = ["vendored-openssl"] }
nix = { workspace = true }
socket2 = { workspace = true }
+duplicate = { version = "2.0.0", default-features = false }
[target.'cfg(target_os = "macos")'.dependencies]
tun = "0.5.1"
diff --git a/test/test-manager/src/container.rs b/test/test-manager/src/container.rs
index 88ba7d9a6b..9560f48b3e 100644
--- a/test/test-manager/src/container.rs
+++ b/test/test-manager/src/container.rs
@@ -11,7 +11,15 @@ pub async fn relaunch_with_rootlesskit(vnc_port: Option<u16>) {
}
let mut cmd = Command::new("rootlesskit");
- cmd.args(["--net", "slirp4netns", "--copy-up=/etc"]);
+ cmd.args([
+ "--net",
+ "slirp4netns",
+ "--ipv6",
+ // A higher MTU breaks IPv6
+ "--mtu",
+ "1500",
+ "--copy-up=/etc",
+ ]);
if let Some(port) = vnc_port {
log::debug!("VNC port: {port} -> 5901/tcp");
diff --git a/test/test-manager/src/network_monitor.rs b/test/test-manager/src/network_monitor.rs
index e89c33fb34..64e1414b5c 100644
--- a/test/test-manager/src/network_monitor.rs
+++ b/test/test-manager/src/network_monitor.rs
@@ -284,29 +284,26 @@ async fn start_packet_monitor_for_interface(
break Ok(monitor_result);
}
maybe_next_packet = next_packet => {
- match maybe_next_packet {
- Some(Ok(packet))=> {
- if let Some(packet) = packet {
- if !filter_fn(&packet) {
- log::trace!("{interface} \"{packet:?}\" does not match closure conditions");
- monitor_result.discarded_packets =
- monitor_result.discarded_packets.saturating_add(1);
- } else {
- log::trace!("{interface} \"{packet:?}\" matches closure conditions");
+ let Some(Ok(packet)) = maybe_next_packet else {
+ log::error!("lost packet stream");
+ break Err(MonitorUnexpectedlyStopped);
+ };
- let should_continue = should_continue_fn(&packet);
+ let Some(packet) = packet else { continue };
- monitor_result.packets.push(packet);
+ if !filter_fn(&packet) {
+ log::trace!("{interface} \"{packet:?}\" does not match closure conditions");
+ monitor_result.discarded_packets =
+ monitor_result.discarded_packets.saturating_add(1);
+ } else {
+ log::trace!("{interface} \"{packet:?}\" matches closure conditions");
- if !should_continue {
- break Ok(monitor_result);
- }
- }
- }
- }
- _ => {
- log::error!("lost packet stream");
- break Err(MonitorUnexpectedlyStopped);
+ let should_continue = should_continue_fn(&packet);
+
+ monitor_result.packets.push(packet);
+
+ if !should_continue {
+ break Ok(monitor_result);
}
}
}
diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs
index 55b7b2384e..ac3a16fc9a 100644
--- a/test/test-manager/src/tests/helpers.rs
+++ b/test/test-manager/src/tests/helpers.rs
@@ -1025,7 +1025,7 @@ pub struct ConnCheckerHandle<'a> {
pub struct ConnectionStatus {
/// True if <https://am.i.mullvad.net/> reported we are connected.
- am_i_mullvad: bool,
+ am_i_mullvad: anyhow::Result<bool>,
/// True if we sniffed TCP packets going outside the tunnel.
leaked_tcp: bool,
@@ -1041,7 +1041,7 @@ impl ConnChecker {
pub fn new(
rpc: ServiceClient,
mullvad_client: MullvadProxyClient,
- leak_destination: SocketAddr,
+ leak_destination: impl Into<SocketAddr>,
) -> Self {
let artifacts_dir = &TEST_CONFIG.artifacts_dir;
let executable_path = match TEST_CONFIG.os {
@@ -1052,7 +1052,7 @@ impl ConnChecker {
Self {
rpc,
mullvad_client,
- leak_destination,
+ leak_destination: leak_destination.into(),
split: false,
executable_path,
payload: None,
@@ -1072,6 +1072,11 @@ impl ConnChecker {
log::debug!("spawning connection checker");
let opts = {
+ let ipvx = match self.leak_destination {
+ SocketAddr::V4(..) => "ipv4",
+ SocketAddr::V6(..) => "ipv6",
+ };
+
let mut args = [
"--interactive",
"--timeout",
@@ -1085,7 +1090,7 @@ impl ConnChecker {
"--leak-udp",
"--leak-icmp",
"--url",
- &format!("https://am.i.{}/json", TEST_CONFIG.mullvad_host),
+ &format!("https://{ipvx}.am.i.{}/json", TEST_CONFIG.mullvad_host),
]
.map(String::from)
.to_vec();
@@ -1178,28 +1183,64 @@ impl ConnCheckerHandle<'_> {
self.checker.unsplit().await
}
+ /// Assert that traffic is blocked and that no packets are leaked.
+ pub async fn assert_blocked(&mut self) -> anyhow::Result<()> {
+ log::info!("checking that connection is blocked");
+ async {
+ let status = self.check_connection().await?;
+ ensure!(status.am_i_mullvad.is_err());
+ ensure!(!status.leaked_tcp);
+ ensure!(!status.leaked_udp);
+ ensure!(!status.leaked_icmp);
+ Ok(())
+ }
+ .await
+ .with_context(|| {
+ anyhow!(
+ "assert_secure failed (leak_destination={})",
+ self.checker.leak_destination,
+ )
+ })
+ }
+
/// Assert that traffic is flowing through the Mullvad tunnel and that no packets are leaked.
pub async fn assert_secure(&mut self) -> anyhow::Result<()> {
log::info!("checking that connection is secure");
- let status = self.check_connection().await?;
- ensure!(status.am_i_mullvad);
- ensure!(!status.leaked_tcp);
- ensure!(!status.leaked_udp);
- ensure!(!status.leaked_icmp);
-
- Ok(())
+ async {
+ let status = self.check_connection().await?;
+ ensure!(status.am_i_mullvad?);
+ ensure!(!status.leaked_tcp);
+ ensure!(!status.leaked_udp);
+ ensure!(!status.leaked_icmp);
+ Ok(())
+ }
+ .await
+ .with_context(|| {
+ anyhow!(
+ "assert_secure failed (leak_destination={})",
+ self.checker.leak_destination,
+ )
+ })
}
/// Assert that traffic is NOT flowing through the Mullvad tunnel and that packets ARE leaked.
pub async fn assert_insecure(&mut self) -> anyhow::Result<()> {
log::info!("checking that connection is not secure");
- let status = self.check_connection().await?;
- ensure!(!status.am_i_mullvad);
- ensure!(status.leaked_tcp);
- ensure!(status.leaked_udp);
- ensure!(status.leaked_icmp);
-
- Ok(())
+ async {
+ let status = self.check_connection().await?;
+ ensure!(!status.am_i_mullvad?);
+ ensure!(status.leaked_tcp);
+ ensure!(status.leaked_udp);
+ ensure!(status.leaked_icmp);
+ Ok(())
+ }
+ .await
+ .with_context(|| {
+ anyhow!(
+ "assert_secure failed (leak_destination={})",
+ self.checker.leak_destination,
+ )
+ })
}
pub async fn check_connection(&mut self) -> anyhow::Result<ConnectionStatus> {
@@ -1228,8 +1269,10 @@ impl ConnCheckerHandle<'_> {
.await
.map_err(|_e| anyhow!("Packet monitor unexpectedly stopped"))?;
+ let leak_destination = self.checker.leak_destination;
+
Ok(ConnectionStatus {
- am_i_mullvad: parse_am_i_mullvad(line)?,
+ am_i_mullvad: parse_am_i_mullvad(line),
leaked_tcp: (monitor_result.packets.iter())
.any(|pkt| pkt.protocol == IpNextHeaderProtocols::Tcp),
@@ -1237,8 +1280,10 @@ impl ConnCheckerHandle<'_> {
leaked_udp: (monitor_result.packets.iter())
.any(|pkt| pkt.protocol == IpNextHeaderProtocols::Udp),
- leaked_icmp: (monitor_result.packets.iter())
- .any(|pkt| pkt.protocol == IpNextHeaderProtocols::Icmp),
+ leaked_icmp: (monitor_result.packets.iter()).any(|pkt| match leak_destination {
+ SocketAddr::V4(..) => pkt.protocol == IpNextHeaderProtocols::Icmp,
+ SocketAddr::V6(..) => pkt.protocol == IpNextHeaderProtocols::Icmpv6,
+ }),
})
}
diff --git a/test/test-manager/src/tests/relay_ip_overrides.rs b/test/test-manager/src/tests/relay_ip_overrides.rs
index 36a9c828f8..786060bf23 100644
--- a/test/test-manager/src/tests/relay_ip_overrides.rs
+++ b/test/test-manager/src/tests/relay_ip_overrides.rs
@@ -6,7 +6,7 @@ use super::{
};
use crate::{
tests::config::TEST_CONFIG,
- vm::{self, network::linux::TEST_SUBNET},
+ vm::{self, network::linux::TEST_SUBNET_IPV4},
};
use anyhow::{Context, anyhow, bail, ensure};
use futures::FutureExt;
@@ -330,9 +330,10 @@ async fn pick_a_relay(
/// Spawn a TCP socket that forwards packets between `destination` and anyone that connects to it.
///
+/// The proxy socket will be bound to [TEST_SUBNET_V4].
/// Returns a handle that will stop the proxy when dropped.
async fn spawn_tcp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<AbortOnDrop<()>> {
- let socket = TcpListener::bind((TEST_SUBNET.ip(), port)).await?;
+ let socket = TcpListener::bind((TEST_SUBNET_IPV4.ip(), port)).await?;
log::info!("started TCP proxy to {destination} on port {port}");
async fn client_task(destination: SocketAddr, mut client: TcpStream) -> anyhow::Result<()> {
@@ -383,9 +384,10 @@ async fn spawn_tcp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<A
///
/// NOTE: Doesn't work with multiple concurrent clients.
///
+/// The proxy socket will be bound to [TEST_SUBNET_V4].
/// Returns a handle that will stop the proxy when dropped.
async fn spawn_udp_proxy(destination: SocketAddr, port: u16) -> anyhow::Result<AbortOnDrop<()>> {
- let socket = UdpSocket::bind((TEST_SUBNET.ip(), port)).await?;
+ let socket = UdpSocket::bind((TEST_SUBNET_IPV4.ip(), port)).await?;
log::info!("started UDP proxy to {destination} on port {port}");
async fn proxy_task(destination: SocketAddr, socket: UdpSocket) -> anyhow::Result<()> {
diff --git a/test/test-manager/src/tests/tunnel.rs b/test/test-manager/src/tests/tunnel.rs
index 5e7e1eb383..828ced73a8 100644
--- a/test/test-manager/src/tests/tunnel.rs
+++ b/test/test-manager/src/tests/tunnel.rs
@@ -5,10 +5,13 @@ use super::{
};
use crate::{
network_monitor::{MonitorOptions, start_packet_monitor},
- tests::helpers::{geoip_lookup_with_retries, login_with_retries, update_relay_constraints},
+ tests::helpers::{
+ ConnChecker, geoip_lookup_with_retries, login_with_retries, update_relay_constraints,
+ },
};
use anyhow::{Context, ensure};
+use duplicate::duplicate_item;
use mullvad_management_interface::MullvadProxyClient;
use mullvad_relay_selector::query::builder::RelayQueryBuilder;
use mullvad_types::{
@@ -18,9 +21,12 @@ use mullvad_types::{
},
wireguard,
};
-use std::net::SocketAddr;
+use std::{
+ net::{Ipv4Addr, Ipv6Addr, SocketAddr},
+ str::FromStr,
+};
use talpid_types::net::{
- TransportProtocol, TunnelType,
+ IpVersion, TransportProtocol, TunnelType,
proxy::{CustomProxy, Socks5Local, Socks5Remote},
};
use test_macro::test_function;
@@ -82,21 +88,29 @@ pub async fn test_openvpn_tunnel(
/// Set up a WireGuard tunnel.
/// This test fails if a working tunnel cannot be set up.
/// WARNING: This test will fail if host has something bound to port 53 such as a connected Mullvad
+#[duplicate_item(
+ VX test_wireguard_tunnel_ipvx;
+ [ V4 ] [ test_wireguard_tunnel_ipv4 ];
+ [ V6 ] [ test_wireguard_tunnel_ipv6 ];
+)]
#[test_function]
-pub async fn test_wireguard_tunnel(
+pub async fn test_wireguard_tunnel_ipvx(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> Result<(), Error> {
// TODO: observe UDP traffic on the expected destination/port (only)
- // TODO: IPv6
+ let ip_version = IpVersion::VX;
const PORTS: [(u16, bool); 3] = [(53, true), (51820, true), (1, false)];
for (port, should_succeed) in PORTS {
log::info!("Connect to WireGuard endpoint on port {port}");
- let query = RelayQueryBuilder::wireguard().port(port).build();
+ let query = RelayQueryBuilder::wireguard()
+ .port(port)
+ .ip_version(ip_version)
+ .build();
apply_settings_from_relay_query(&mut mullvad_client, query)
.await
@@ -122,6 +136,67 @@ pub async fn test_wireguard_tunnel(
Ok(())
}
+/// Set up a WireGuard tunnel and check whether in-tunnel IPv6 works.
+/// WARNING: This test will fail if host has something bound to port 53 such as a connected Mullvad
+#[duplicate_item(
+ VX test_wireguard_ipv6_in_ipvx;
+ [ V4 ] [ test_wireguard_ipv6_in_ipv4 ];
+ [ V6 ] [ test_wireguard_ipv6_in_ipv6 ];
+)]
+#[test_function]
+pub async fn test_wireguard_ipv6_in_ipvx(
+ _: TestContext,
+ rpc: ServiceClient,
+ mut mullvad_client: MullvadProxyClient,
+) -> Result<(), Error> {
+ let ip_version = IpVersion::VX;
+
+ let mut conn_checker_v4 = ConnChecker::new(
+ rpc.clone(),
+ mullvad_client.clone(),
+ (Ipv4Addr::new(1, 1, 1, 1), 53),
+ );
+
+ let mut conn_checker_v6 = ConnChecker::new(
+ rpc.clone(),
+ mullvad_client.clone(),
+ (Ipv6Addr::from_str("2606:4700:4700::1111").unwrap(), 53),
+ );
+
+ let mut conn_checker_v4 = conn_checker_v4.spawn().await?;
+ let mut conn_checker_v6 = conn_checker_v6.spawn().await?;
+
+ conn_checker_v4.assert_insecure().await?;
+ conn_checker_v6.assert_insecure().await?;
+
+ log::info!("Connect to WireGuard endpoint");
+
+ let query = RelayQueryBuilder::wireguard()
+ .ip_version(ip_version)
+ .build();
+ apply_settings_from_relay_query(&mut mullvad_client, query)
+ .await
+ .unwrap();
+
+ // Test with in-tunnel IPv6 enabled
+ mullvad_client.set_enable_ipv6(true).await?;
+ let connection_result = connect_and_wait(&mut mullvad_client).await;
+ assert!(connection_result.is_ok());
+ conn_checker_v4.assert_secure().await?;
+ conn_checker_v6.assert_secure().await?;
+
+ // Test with in-tunnel IPv6 disabled
+ mullvad_client.set_enable_ipv6(false).await?;
+ let connection_result = connect_and_wait(&mut mullvad_client).await;
+ assert!(connection_result.is_ok());
+ conn_checker_v4.assert_secure().await?;
+ conn_checker_v6.assert_blocked().await?; // ipv6 mustnt leak
+
+ disconnect_and_wait(&mut mullvad_client).await?;
+
+ Ok(())
+}
+
/// Use udp2tcp obfuscation. This test connects to a WireGuard relay over TCP. It fails if no
/// outgoing TCP traffic to the relay is observed on the expected port.
#[test_function]
@@ -202,14 +277,24 @@ pub async fn test_wireguard_over_shadowsocks(
/// Use QUIC obfuscation. This tests whether the daemon can establish a QUIC connection.
/// Note that this doesn't verify that the outgoing traffic looks like http traffic (even though it
/// doesn't sound too difficult to do?).
+#[duplicate_item(
+ VX test_wireguard_over_quic_ipvx;
+ [ V4 ] [ test_wireguard_over_quic_ipv4 ];
+ [ V6 ] [ test_wireguard_over_quic_ipv6 ];
+)]
#[test_function]
-pub async fn test_wireguard_over_quic(
+pub async fn test_wireguard_over_quic_ipvx(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
+ let ip_version = IpVersion::VX;
+
log::info!("Enable QUIC as obfuscation method");
- let query = RelayQueryBuilder::wireguard().quic().build();
+ let query = RelayQueryBuilder::wireguard()
+ .ip_version(ip_version)
+ .quic()
+ .build();
apply_settings_from_relay_query(&mut mullvad_client, query).await?;
log::info!("Connect to WireGuard via QUIC endpoint");
@@ -553,18 +638,26 @@ pub async fn test_quantum_resistant_multihop_udp2tcp_tunnel(
///
/// This is not testing any of the individual components, just whether the daemon can connect when
/// all of these features are combined.
+#[duplicate_item(
+ VX test_quantum_resistant_multihop_shadowsocks_tunnel_ipvx;
+ [ V4 ] [ test_quantum_resistant_multihop_shadowsocks_tunnel_ipv4 ];
+ [ V6 ] [ test_quantum_resistant_multihop_shadowsocks_tunnel_ipv6 ];
+)]
#[test_function]
-pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel(
+pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel_ipvx(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
+ let ip_version = IpVersion::VX;
+
mullvad_client
.set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On)
.await
.context("Failed to enable PQ tunnels")?;
let query = RelayQueryBuilder::wireguard()
+ .ip_version(ip_version)
.multihop()
.shadowsocks()
.build();
@@ -587,12 +680,19 @@ pub async fn test_quantum_resistant_multihop_shadowsocks_tunnel(
///
/// This is not testing any of the individual components, just whether the daemon can connect when
/// all of these features are combined.
+#[duplicate_item(
+ VX test_quantum_resistant_multihop_quic_tunnel_ipvx;
+ [ V4 ] [ test_quantum_resistant_multihop_quic_tunnel_ipv4 ];
+ [ V6 ] [ test_quantum_resistant_multihop_quic_tunnel_ipv6 ];
+)]
#[test_function]
-pub async fn test_quantum_resistant_multihop_quic_tunnel(
+pub async fn test_quantum_resistant_multihop_quic_tunnel_ipvx(
_: TestContext,
rpc: ServiceClient,
mut mullvad_client: MullvadProxyClient,
) -> anyhow::Result<()> {
+ let ip_version = IpVersion::VX;
+
mullvad_client
// TODO: Why is this needed, exactly?
.set_quantum_resistant_tunnel(wireguard::QuantumResistantState::On)
@@ -600,6 +700,7 @@ pub async fn test_quantum_resistant_multihop_quic_tunnel(
.context("Failed to enable PQ tunnels")?;
let query = RelayQueryBuilder::wireguard()
+ .ip_version(ip_version)
.quantum_resistant()
.multihop()
.quic()
diff --git a/test/test-manager/src/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs
index ef7eebeb2a..b6df187dcc 100644
--- a/test/test-manager/src/vm/network/linux.rs
+++ b/test/test-manager/src/vm/network/linux.rs
@@ -1,24 +1,31 @@
-use ipnetwork::Ipv4Network;
+use ipnetwork::{Ipv4Network, Ipv6Network};
use std::{
ffi::OsStr,
io,
- net::{IpAddr, Ipv4Addr},
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+ ops::RangeInclusive,
process::Stdio,
str::FromStr,
- sync::LazyLock,
};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
};
-/// (Contained) test subnet for the test runner: 172.29.1.1/24
-pub static TEST_SUBNET: LazyLock<Ipv4Network> =
- LazyLock::new(|| Ipv4Network::new(Ipv4Addr::new(172, 29, 1, 1), 24).unwrap());
-/// Range of IPs returned by the DNS server: TEST_SUBNET_DHCP_FIRST to TEST_SUBNET_DHCP_LAST
-pub const TEST_SUBNET_DHCP_FIRST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 2);
-/// Range of IPs returned by the DNS server: TEST_SUBNET_DHCP_FIRST to TEST_SUBNET_DHCP_LAST
-pub const TEST_SUBNET_DHCP_LAST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 128);
+/// (Contained) IPv4 subnet for the test runner: 172.29.1.1/24
+pub const TEST_SUBNET_IPV4: Ipv4Network =
+ Ipv4Network::new_checked(Ipv4Addr::new(172, 29, 1, 1), 24).unwrap();
+
+/// IPv4 range returned by the DHCP server.
+pub const TEST_SUBNET_IPV4_DHCP: RangeInclusive<Ipv4Addr> =
+ Ipv4Addr::new(172, 29, 1, 2)..=Ipv4Addr::new(172, 29, 1, 128);
+
+/// IPv6 subnet for the test runner. "0xfd multest"
+pub const TEST_SUBNET_IPV6: Ipv6Network = Ipv6Network::new_checked(
+ Ipv6Addr::new(0xfd6d, 0x756c, 0x7465, 0x7374, 0, 0, 0, 1),
+ 64,
+)
+.unwrap();
/// Bridge interface on the host
pub(crate) const BRIDGE_NAME: &str = "br-mullvadtest";
@@ -97,26 +104,44 @@ struct DhcpProcHandle {
_pid_file: async_tempfile::TempFile,
}
+/// IPv6-support in `rootlesskit` is experimental, and addresses are not automatically assigned.
+/// This function will assigned an IPv6 address to the TAP interface, and set up routes.
+async fn fix_ipv6() -> Result<()> {
+ let tap = "tap0"; // TAP-device that connects to slirp2netns
+ let addr = "fd00::1337/64"; // our address within the slirp2netns subnet
+ let gateway = "fd00::2"; // slirp2netns gateway
+ let _dns = "fd00::3"; // slirp2netns dns
+
+ run_ip_cmd(["-6", "addr", "add", addr, "dev", tap]).await?;
+ run_ip_cmd(["-6", "route", "add", "default", "via", gateway, "dev", tap]).await?;
+ Ok(())
+}
+
/// Create a bridge network and hosts
pub async fn setup_test_network() -> Result<NetworkHandle> {
+ fix_ipv6().await?;
+
enable_forwarding().await?;
- let test_subnet = TEST_SUBNET.to_string();
+ let test_subnet_v4 = TEST_SUBNET_IPV4.to_string();
+ let test_subnet_v6 = TEST_SUBNET_IPV6.to_string();
- log::debug!("Create bridge network: dev {BRIDGE_NAME}, net {test_subnet}");
+ log::debug!("Create bridge network: dev {BRIDGE_NAME}, net {test_subnet_v4}");
run_ip_cmd(["link", "add", BRIDGE_NAME, "type", "bridge"]).await?;
- run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet]).await?;
+ run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet_v4]).await?;
+ run_ip_cmd(["addr", "add", "dev", BRIDGE_NAME, &test_subnet_v6]).await?;
run_ip_cmd(["link", "set", "dev", BRIDGE_NAME, "up"]).await?;
log::debug!("Masquerade traffic from bridge to internet");
run_nft(&format!(
"
-table ip mullvad_test_nat {{
+table inet mullvad_test_nat {{
chain POSTROUTING {{
type nat hook postrouting priority srcnat; policy accept;
- ip saddr {test_subnet} ip daddr != {test_subnet} counter masquerade
+ ip saddr {test_subnet_v4} ip daddr != {test_subnet_v4} counter masquerade
+ ip6 saddr {test_subnet_v6} ip6 daddr != {test_subnet_v6} counter masquerade
}}
}}"
))
@@ -189,8 +214,11 @@ impl NetworkHandle {
}
}
+/// Run dnsmasq as a DHCP server.
+///
+/// dnsmasq will serve IPv4 addresses within the range [TEST_SUBNET_IPV4_DHCP] using regular DHCP.
+/// It will also advertise SLAAC for IPv6 within [TEST_SUBNET_IPV6].
async fn start_dnsmasq() -> Result<DhcpProcHandle> {
- // dnsmasq -i BRIDGE_NAME -F TEST_SUBNET_DHCP_FIRST,TEST_SUBNET_DHCP_LAST ...
let mut cmd = Command::new("dnsmasq");
cmd.kill_on_drop(true);
@@ -198,13 +226,21 @@ async fn start_dnsmasq() -> Result<DhcpProcHandle> {
cmd.stderr(Stdio::piped());
cmd.args([
+ "--conf-file=/dev/null",
"--bind-interfaces",
- "-C",
- "/dev/null",
- "-i",
- BRIDGE_NAME,
- "-F",
- &format!("{TEST_SUBNET_DHCP_FIRST},{TEST_SUBNET_DHCP_LAST}"),
+ &format!("--interface={BRIDGE_NAME}"),
+ // IPv4
+ &format!(
+ "--dhcp-range={},{}",
+ TEST_SUBNET_IPV4_DHCP.start(),
+ TEST_SUBNET_IPV4_DHCP.end(),
+ ),
+ // IPv6
+ &format!(
+ "--dhcp-range={prefix},slaac,{prefix_len}",
+ prefix = TEST_SUBNET_IPV6.ip(),
+ prefix_len = TEST_SUBNET_IPV6.prefix()
+ ),
"--no-hosts",
"--keep-in-foreground",
"--log-facility=-",
@@ -343,5 +379,13 @@ async fn enable_forwarding() -> Result<()> {
if !output.status.success() {
return Err(Error::SysctlFailed(output.status.code().unwrap()));
}
+
+ let mut cmd = Command::new("sysctl");
+ cmd.arg("net.ipv6.conf.all.forwarding=1");
+ let output = cmd.output().await.map_err(Error::SysctlStart)?;
+ if !output.status.success() {
+ return Err(Error::SysctlFailed(output.status.code().unwrap()));
+ }
+
Ok(())
}