summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-09-08 10:32:40 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-09-15 11:07:07 +0200
commite7e41250e6543b3d67e6c86d8194d401790d4905 (patch)
tree34cf84bdb73babd0624ddf7607cb699f13a36f23
parent12bb6552e49e19510b3272f8d3d1f0c6277236ff (diff)
downloadmullvadvpn-e7e41250e6543b3d67e6c86d8194d401790d4905.tar.xz
mullvadvpn-e7e41250e6543b3d67e6c86d8194d401790d4905.zip
Add IPv6 to test-manager linux network
-rw-r--r--test/test-manager/src/container.rs10
-rw-r--r--test/test-manager/src/tests/relay_ip_overrides.rs8
-rw-r--r--test/test-manager/src/vm/network/linux.rs82
3 files changed, 76 insertions, 24 deletions
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/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/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs
index ef7eebeb2a..561eb91d27 100644
--- a/test/test-manager/src/vm/network/linux.rs
+++ b/test/test-manager/src/vm/network/linux.rs
@@ -1,8 +1,9 @@
-use ipnetwork::Ipv4Network;
+use ipnetwork::{Ipv4Network, Ipv6Network};
use std::{
ffi::OsStr,
io,
net::{IpAddr, Ipv4Addr},
+ ops::RangeInclusive,
process::Stdio,
str::FromStr,
sync::LazyLock,
@@ -12,13 +13,17 @@ use tokio::{
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 static TEST_SUBNET_IPV4: LazyLock<Ipv4Network> =
+ LazyLock::new(|| "172.29.1.1/24".parse().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 static TEST_SUBNET_IPV6: LazyLock<Ipv6Network> =
+ LazyLock::new(|| "fd6d:756c:7465:7374::1/64".parse().unwrap());
/// Bridge interface on the host
pub(crate) const BRIDGE_NAME: &str = "br-mullvadtest";
@@ -97,26 +102,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 +212,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 +224,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 +377,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(())
}