diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-09-08 10:32:40 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-09-15 11:07:07 +0200 |
| commit | e7e41250e6543b3d67e6c86d8194d401790d4905 (patch) | |
| tree | 34cf84bdb73babd0624ddf7607cb699f13a36f23 | |
| parent | 12bb6552e49e19510b3272f8d3d1f0c6277236ff (diff) | |
| download | mullvadvpn-e7e41250e6543b3d67e6c86d8194d401790d4905.tar.xz mullvadvpn-e7e41250e6543b3d67e6c86d8194d401790d4905.zip | |
Add IPv6 to test-manager linux network
| -rw-r--r-- | test/test-manager/src/container.rs | 10 | ||||
| -rw-r--r-- | test/test-manager/src/tests/relay_ip_overrides.rs | 8 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/linux.rs | 82 |
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(()) } |
