test: add comprehensive VPN confinement test with DNS leak detection
Add a new test that validates VPN namespace isolation for Nixarr services. The test uses a 3-VM topology to ensure that transmission traffic is properly confined to the VPN tunnel and includes: - IPv4 and IPv6 traffic routing verification - DNS leak detection using tcpdump and static DNS entries - Traffic leak prevention when VPN fails - Port forwarding from external clients - Service recovery after VPN reconnection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,9 @@
|
||||
permissions-test = pkgs.callPackage ./tests/permissions-test.nix {
|
||||
inherit (self) nixosModules;
|
||||
};
|
||||
vpn-confinement-test = pkgs.callPackage ./tests/vpn-confinement-test.nix {
|
||||
inherit (self) nixosModules;
|
||||
};
|
||||
});
|
||||
|
||||
devShells = forAllSystems ({pkgs}: {
|
||||
|
||||
@@ -0,0 +1,767 @@
|
||||
/*
|
||||
VPN Confinement Integration Test
|
||||
|
||||
This test validates that Nixarr services are properly confined to a VPN namespace
|
||||
and cannot leak traffic when the VPN connection fails. It uses a 3-VM topology
|
||||
to simulate real-world network conditions.
|
||||
|
||||
Network Topology:
|
||||
┌──────────────┐ VLAN 2 ┌─────────────┐ VLAN 1 ┌─────────────┐
|
||||
│internetClient│ ◄──────────── │ gateway │ ◄──────────── │ nixarrHost │
|
||||
│ 10.0.1.2 │ │ 10.0.1.1 │ │192.168.1.2 │
|
||||
│ fd00:2::2 │ │192.168.1.1 │ │ fd00:1::2 │
|
||||
└──────────────┘ │ fd00:2::1 │ └─────────────┘
|
||||
│ fd00:1::1 │ │
|
||||
└─────────────┘ │
|
||||
│ │
|
||||
WireGuard tunnel │
|
||||
10.100.0.1 ◄────────────────────────┘
|
||||
fd00:100::1 VPN namespace
|
||||
(10.100.0.2, fd00:100::2)
|
||||
|
||||
Test Coverage:
|
||||
- VPN namespace isolation (transmission confined to wg namespace)
|
||||
- IPv4 and IPv6 traffic routing through VPN tunnel
|
||||
- Traffic leak prevention when VPN is down
|
||||
- Port forwarding from external clients through gateway to VPN services
|
||||
- DNS configuration in VPN namespace
|
||||
- Service recovery after VPN reconnection
|
||||
|
||||
The test ensures that:
|
||||
1. All transmission traffic goes through the VPN tunnel
|
||||
2. Source IP is preserved (shows VPN client IP: 10.100.0.2/fd00:100::2)
|
||||
3. No traffic leaks to host network when VPN fails
|
||||
4. External port forwarding works correctly
|
||||
5. Both IPv4 and IPv6 work identically through the tunnel
|
||||
*/
|
||||
{
|
||||
pkgs,
|
||||
nixosModules,
|
||||
lib ? pkgs.lib,
|
||||
}: let
|
||||
# WireGuard configuration for the VPN gateway
|
||||
wgGatewayPort = 51820;
|
||||
|
||||
# Generate real WireGuard keys
|
||||
wgGatewayPrivateKey = pkgs.runCommand "wg-gateway-private" {buildInputs = [pkgs.wireguard-tools];} ''
|
||||
wg genkey > $out
|
||||
'';
|
||||
wgGatewayPublicKey = pkgs.runCommand "wg-gateway-public" {buildInputs = [pkgs.wireguard-tools];} ''
|
||||
cat ${wgGatewayPrivateKey} | wg pubkey > $out
|
||||
'';
|
||||
|
||||
wgClientPrivateKey = pkgs.runCommand "wg-client-private" {buildInputs = [pkgs.wireguard-tools];} ''
|
||||
wg genkey > $out
|
||||
'';
|
||||
wgClientPublicKey = pkgs.runCommand "wg-client-public" {buildInputs = [pkgs.wireguard-tools];} ''
|
||||
cat ${wgClientPrivateKey} | wg pubkey > $out
|
||||
'';
|
||||
|
||||
# Network configuration
|
||||
wgGatewayAddr = "10.100.0.1";
|
||||
wgClientAddr = "10.100.0.2";
|
||||
wgSubnet = "10.100.0.0/24";
|
||||
|
||||
# Fixed VM IPs
|
||||
gatewayIP = "192.168.1.1";
|
||||
nixarrHostIP = "192.168.1.2";
|
||||
|
||||
# Internet client IPs
|
||||
internetClientIP = "10.0.1.2";
|
||||
internetGatewayIP = "10.0.1.1";
|
||||
|
||||
# IPv6 addresses
|
||||
gatewayIPv6 = "fd00:1::1";
|
||||
nixarrHostIPv6 = "fd00:1::2";
|
||||
internetClientIPv6 = "fd00:2::2";
|
||||
internetGatewayIPv6 = "fd00:2::1";
|
||||
wgGatewayAddrV6 = "fd00:100::1";
|
||||
wgClientAddrV6 = "fd00:100::2";
|
||||
|
||||
# Generate WireGuard config file for client
|
||||
wgClientConfig = pkgs.writeText "wg-client.conf" ''
|
||||
[Interface]
|
||||
PrivateKey = ${builtins.readFile wgClientPrivateKey}
|
||||
Address = ${wgClientAddr}/24, ${wgClientAddrV6}/64
|
||||
DNS = ${wgGatewayAddr}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${builtins.readFile wgGatewayPublicKey}
|
||||
Endpoint = ${gatewayIP}:${toString wgGatewayPort}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
PersistentKeepalive = 25
|
||||
'';
|
||||
in
|
||||
pkgs.nixosTest {
|
||||
name = "nixarr-vpn-confinement-test";
|
||||
|
||||
# Disable interactive mode to avoid hanging
|
||||
interactive = false;
|
||||
|
||||
nodes = {
|
||||
# Internet client VM - Simulates external services and clients
|
||||
internetClient = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
virtualisation.vlans = [2]; # Connect to VLAN 2 (Internet)
|
||||
|
||||
networking = {
|
||||
firewall.enable = false;
|
||||
};
|
||||
|
||||
# Add route to VPN subnet
|
||||
boot.kernel.sysctl."net.ipv4.ip_forward" = 0; # internetClient doesn't forward
|
||||
|
||||
# Enable systemd-networkd for proper route management
|
||||
systemd.network.enable = true;
|
||||
networking.useNetworkd = true;
|
||||
|
||||
# Configure static routes to VPN subnet using systemd-networkd
|
||||
systemd.network.networks."40-eth1" = {
|
||||
matchConfig.Name = "eth1";
|
||||
networkConfig = {
|
||||
DHCP = "no";
|
||||
};
|
||||
address = [
|
||||
"${internetClientIP}/24"
|
||||
"${internetClientIPv6}/64"
|
||||
];
|
||||
gateway = ["${internetGatewayIP}" "${internetGatewayIPv6}"];
|
||||
routes = [
|
||||
{
|
||||
Destination = "${wgSubnet}";
|
||||
Gateway = "${internetGatewayIP}";
|
||||
}
|
||||
{
|
||||
Destination = "fd00:100::/64";
|
||||
Gateway = "${internetGatewayIPv6}";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Web server that returns source IP for testing
|
||||
systemd.services.source-ip-server = {
|
||||
enable = true;
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = ["network.target"];
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
ExecStart = let
|
||||
server = pkgs.writeText "server.py" ''
|
||||
import http.server
|
||||
import socketserver
|
||||
import socket
|
||||
|
||||
class DualStackHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
address_family = socket.AF_INET6
|
||||
def server_bind(self):
|
||||
# Enable dual-stack support
|
||||
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
super().server_bind()
|
||||
|
||||
class MyHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(f"Source: {self.client_address[0]}".encode())
|
||||
|
||||
# Listen on all interfaces
|
||||
with DualStackHTTPServer(("::", 8080), MyHandler) as httpd:
|
||||
httpd.serve_forever()
|
||||
'';
|
||||
in "${pkgs.python3}/bin/python3 ${server}";
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
netcat-gnu
|
||||
curl
|
||||
python3
|
||||
];
|
||||
};
|
||||
|
||||
# VPN Gateway VM - Acts as WireGuard server and internet gateway
|
||||
gateway = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
virtualisation.vlans = [1 2]; # VLAN 1 for LAN, VLAN 2 for Internet
|
||||
|
||||
networking = {
|
||||
interfaces.eth1 = {
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = gatewayIP;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
ipv6.addresses = [
|
||||
{
|
||||
address = gatewayIPv6;
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
interfaces.eth2 = {
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = internetGatewayIP;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
ipv6.addresses = [
|
||||
{
|
||||
address = internetGatewayIPv6;
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
firewall = {
|
||||
enable = true;
|
||||
allowedUDPPorts = [wgGatewayPort 51413];
|
||||
allowedTCPPorts = [51413];
|
||||
};
|
||||
|
||||
wireguard.interfaces.wg0 = {
|
||||
ips = ["${wgGatewayAddr}/24" "${wgGatewayAddrV6}/64"];
|
||||
listenPort = wgGatewayPort;
|
||||
privateKeyFile = "${wgGatewayPrivateKey}";
|
||||
|
||||
peers = [
|
||||
{
|
||||
publicKey = builtins.readFile wgClientPublicKey;
|
||||
allowedIPs = ["${wgClientAddr}/32" "${wgClientAddrV6}/128"];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Enable IP forwarding
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv4.ip_forward" = 1;
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
};
|
||||
|
||||
# Port forwarding and firewall rules
|
||||
networking.firewall.extraCommands = ''
|
||||
# Allow WireGuard and testing traffic (IPv4)
|
||||
iptables -A INPUT -i eth1 -j ACCEPT
|
||||
iptables -A INPUT -i eth2 -j ACCEPT
|
||||
iptables -A INPUT -i wg0 -j ACCEPT
|
||||
|
||||
# Allow WireGuard and testing traffic (IPv6)
|
||||
ip6tables -A INPUT -i eth1 -j ACCEPT
|
||||
ip6tables -A INPUT -i eth2 -j ACCEPT
|
||||
ip6tables -A INPUT -i wg0 -j ACCEPT
|
||||
|
||||
# IPv6 forwarding rules - Allow forwarding between interfaces
|
||||
ip6tables -A FORWARD -i wg0 -o eth2 -j ACCEPT
|
||||
ip6tables -A FORWARD -i eth2 -o wg0 -j ACCEPT
|
||||
ip6tables -A FORWARD -i wg0 -o eth1 -j ACCEPT
|
||||
ip6tables -A FORWARD -i eth1 -o wg0 -j ACCEPT
|
||||
ip6tables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
|
||||
# Note: No masquerading - we want to preserve source IPs for testing
|
||||
|
||||
# Forward transmission peer port from internet to VPN client (this is the key test)
|
||||
iptables -t nat -A PREROUTING -i eth2 -p tcp --dport 51413 -j DNAT --to-destination ${wgClientAddr}:51413
|
||||
iptables -t nat -A PREROUTING -i eth2 -p udp --dport 51413 -j DNAT --to-destination ${wgClientAddr}:51413
|
||||
|
||||
# Allow forwarded traffic
|
||||
iptables -A FORWARD -p tcp --dport 51413 -d ${wgClientAddr} -j ACCEPT
|
||||
iptables -A FORWARD -p udp --dport 51413 -d ${wgClientAddr} -j ACCEPT
|
||||
|
||||
# Accept return traffic for established connections
|
||||
iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||
'';
|
||||
|
||||
# Simple DNS server for testing
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings = {
|
||||
interface = "wg0";
|
||||
bind-interfaces = true;
|
||||
listen-address = wgGatewayAddr;
|
||||
# Log DNS queries for leak detection
|
||||
log-queries = true;
|
||||
log-facility = "/var/log/dnsmasq-queries.log";
|
||||
# Static DNS entries for testing
|
||||
address = [
|
||||
"/test.vpn.local/${wgGatewayAddr}"
|
||||
"/leak.test.local/1.2.3.4"
|
||||
"/transmission.local/${wgClientAddr}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure dnsmasq starts after WireGuard
|
||||
systemd.services.dnsmasq = {
|
||||
after = ["wireguard-wg0.service"];
|
||||
wants = ["wireguard-wg0.service"];
|
||||
};
|
||||
|
||||
# No additional routing needed on gateway - it has direct interfaces to both networks
|
||||
|
||||
# Install test utilities
|
||||
environment.systemPackages = with pkgs; [
|
||||
iptables
|
||||
netcat-gnu
|
||||
python3
|
||||
iproute2 # for ss command
|
||||
tcpdump
|
||||
];
|
||||
};
|
||||
|
||||
# Nixarr host with VPN-confined transmission
|
||||
nixarrHost = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
imports = [nixosModules.default];
|
||||
|
||||
virtualisation.vlans = [1]; # Connect to VLAN 1
|
||||
|
||||
networking = {
|
||||
interfaces.eth1 = {
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = nixarrHostIP;
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
ipv6.addresses = [
|
||||
{
|
||||
address = nixarrHostIPv6;
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Disable firewall for testing
|
||||
firewall.enable = false;
|
||||
|
||||
# Add route to gateway
|
||||
defaultGateway = {
|
||||
address = gatewayIP;
|
||||
interface = "eth1";
|
||||
};
|
||||
defaultGateway6 = {
|
||||
address = gatewayIPv6;
|
||||
interface = "eth1";
|
||||
};
|
||||
};
|
||||
|
||||
# Copy WireGuard config to the expected location
|
||||
system.activationScripts.setupWgConfig = ''
|
||||
mkdir -p /etc/wireguard
|
||||
cp ${wgClientConfig} /etc/wireguard/wg0.conf
|
||||
chmod 600 /etc/wireguard/wg0.conf
|
||||
'';
|
||||
|
||||
# Minimal nixarr configuration with VPN
|
||||
nixarr = {
|
||||
enable = true;
|
||||
|
||||
# Required directories
|
||||
mediaDir = "/data/media";
|
||||
stateDir = "/data/.state/nixarr";
|
||||
|
||||
# Enable VPN
|
||||
vpn = {
|
||||
enable = true;
|
||||
wgConf = "/etc/wireguard/wg0.conf";
|
||||
};
|
||||
|
||||
# Enable transmission with VPN
|
||||
transmission = {
|
||||
enable = true;
|
||||
vpn.enable = true;
|
||||
# Use specific peer port for testing
|
||||
peerPort = 51413;
|
||||
# Disable firewall opening since we're in VPN
|
||||
openFirewall = false;
|
||||
};
|
||||
|
||||
# Disable all other services
|
||||
sonarr.enable = false;
|
||||
radarr.enable = false;
|
||||
lidarr.enable = false;
|
||||
readarr.enable = false;
|
||||
bazarr.enable = false;
|
||||
prowlarr.enable = false;
|
||||
jellyfin.enable = false;
|
||||
plex.enable = false;
|
||||
sabnzbd.enable = false;
|
||||
autobrr.enable = false;
|
||||
recyclarr.enable = false;
|
||||
jellyseerr.enable = false;
|
||||
};
|
||||
|
||||
# Add IPv6 route for VPN namespace to reach internetClient via WireGuard
|
||||
systemd.network.networks."10-eth1" = {
|
||||
matchConfig.Name = "eth1";
|
||||
routes = [
|
||||
{
|
||||
routeConfig = {
|
||||
Destination = "fd00:2::/64"; # Route to internetClient network
|
||||
Gateway = gatewayIPv6; # Via gateway IPv6
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Install test utilities
|
||||
environment.systemPackages = with pkgs; [
|
||||
wireguard-tools
|
||||
dig
|
||||
curl
|
||||
iproute2
|
||||
iptables
|
||||
netcat-gnu
|
||||
tcpdump
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
print("=== Waiting for VMs to boot ===")
|
||||
# Wait for all VMs to be ready
|
||||
internetClient.wait_for_unit("multi-user.target", timeout=60)
|
||||
gateway.wait_for_unit("multi-user.target", timeout=60)
|
||||
nixarrHost.wait_for_unit("multi-user.target", timeout=60)
|
||||
|
||||
# Wait for web server on internetClient
|
||||
internetClient.wait_for_unit("source-ip-server.service")
|
||||
internetClient.wait_for_open_port(8080)
|
||||
|
||||
# Wait for systemd-networkd to set up routes
|
||||
internetClient.wait_for_unit("systemd-networkd.service")
|
||||
|
||||
|
||||
print("=== Test 1: Basic connectivity between VMs ===")
|
||||
# First verify that nixarrHost can reach the gateway
|
||||
nixarrHost.succeed("ping -c 1 ${gatewayIP}")
|
||||
gateway.succeed("ping -c 1 ${nixarrHostIP}")
|
||||
|
||||
|
||||
print("=== Test 2: Check VPN namespace setup ===")
|
||||
# Check that wg namespace exists
|
||||
nixarrHost.succeed("ip netns list | grep -q wg")
|
||||
|
||||
# The VPN namespace service should be running
|
||||
nixarrHost.wait_for_unit("wg.service")
|
||||
|
||||
# Check if transmission is running
|
||||
nixarrHost.wait_for_unit("transmission.service", timeout=30)
|
||||
|
||||
|
||||
print("=== Test 3: Check WireGuard connectivity ===")
|
||||
# Check if WireGuard interface exists in namespace
|
||||
nixarrHost.succeed("ip netns exec wg ip link show wg0")
|
||||
|
||||
# Check WireGuard status
|
||||
nixarrHost.succeed("ip netns exec wg wg show")
|
||||
|
||||
# Test VPN tunnel connectivity
|
||||
nixarrHost.succeed("ip netns exec wg ping -c 3 ${wgGatewayAddr}")
|
||||
|
||||
|
||||
print("=== Test 4: Verify traffic routing through VPN ===")
|
||||
# Debug: See what the web server actually returns
|
||||
response = nixarrHost.succeed("ip netns exec wg curl -s http://${internetClientIP}:8080")
|
||||
print(f"Web server response: {response}")
|
||||
|
||||
# Test traffic through VPN tunnel to internetClient - should show VPN client IP
|
||||
nixarrHost.succeed("ip netns exec wg curl -s http://${internetClientIP}:8080 | grep -q '${wgClientAddr}'")
|
||||
|
||||
|
||||
print("=== Test 5: Verify traffic routing through VPN ===")
|
||||
# IPv4 traffic to host network should be blocked (specific route handling)
|
||||
nixarrHost.fail("ip netns exec wg curl -s --max-time 2 http://${gatewayIP}:8080")
|
||||
|
||||
# Debug IPv6 connectivity before main test
|
||||
print("=== Debug IPv6 connectivity ===")
|
||||
|
||||
# Check IPv6 addresses in VPN namespace
|
||||
ipv6_addrs = nixarrHost.succeed("ip netns exec wg ip -6 addr show")
|
||||
print(f"VPN namespace IPv6 addresses:\n{ipv6_addrs}")
|
||||
|
||||
# Check if VPN namespace can ping gateway IPv6
|
||||
try:
|
||||
nixarrHost.succeed("ip netns exec wg ping -6 -c 1 -W 3 ${wgGatewayAddrV6}")
|
||||
print("✓ VPN namespace can ping WireGuard gateway IPv6")
|
||||
except Exception as e:
|
||||
print(f"✗ VPN namespace cannot ping WireGuard gateway IPv6: {e}")
|
||||
|
||||
# Check if VPN namespace can reach internetClient IPv6 (simple connectivity)
|
||||
try:
|
||||
result = nixarrHost.succeed("ip netns exec wg curl -6 -s --max-time 5 http://[${internetClientIPv6}]:8080")
|
||||
print(f"✓ VPN namespace can reach internetClient IPv6: {result}")
|
||||
except Exception as e:
|
||||
print(f"✗ VPN namespace cannot reach internetClient IPv6: {e}")
|
||||
|
||||
# Check gateway IPv6 routes
|
||||
gw_routes = gateway.succeed("ip -6 route show")
|
||||
print(f"Gateway IPv6 routes:\n{gw_routes}")
|
||||
|
||||
# Check internetClient IPv6 routes
|
||||
client_routes = internetClient.succeed("ip -6 route show")
|
||||
print(f"InternetClient IPv6 routes:\n{client_routes}")
|
||||
|
||||
# IPv6 traffic should go through VPN tunnel (shows VPN client source)
|
||||
nixarrHost.succeed("ip netns exec wg curl -6 -s --max-time 2 http://[${internetClientIPv6}]:8080 | grep -q 'Source: fd00:100::2'")
|
||||
|
||||
|
||||
print("=== Test 6: Verify transmission is confined ===")
|
||||
# Check transmission is running and confined to VPN namespace
|
||||
nixarrHost.succeed("systemctl status transmission.service | grep -q 'Active: active'")
|
||||
|
||||
|
||||
print("=== Test 7: Interrupt VPN - Verify no connectivity ===")
|
||||
# Block WireGuard traffic on gateway using iptables
|
||||
gateway.succeed("iptables -I INPUT -p udp --dport ${toString wgGatewayPort} -j DROP")
|
||||
gateway.succeed("iptables -I OUTPUT -p udp --sport ${toString wgGatewayPort} -j DROP")
|
||||
|
||||
# All connectivity should fail - no leaks!
|
||||
nixarrHost.fail("ip netns exec wg ping -c 1 -W 2 ${wgGatewayAddr}")
|
||||
nixarrHost.fail("ip netns exec wg curl -s --max-time 2 http://${internetClientIP}:8080")
|
||||
|
||||
# DNS should also fail completely when VPN is down
|
||||
nixarrHost.fail("ip netns exec wg dig @${wgGatewayAddr} test.vpn.local +short +timeout=2")
|
||||
nixarrHost.fail("ip netns exec wg dig leak.test.local +short +timeout=2")
|
||||
print("✓ DNS queries fail when VPN is down - no fallback to host DNS")
|
||||
|
||||
# Verify no traffic leaks to host network
|
||||
nixarrHost.fail("ip netns exec wg curl -s --max-time 2 http://${gatewayIP}:8080")
|
||||
|
||||
|
||||
print("=== Test 8: Restore VPN - Verify recovery ===")
|
||||
# Remove iptables blocks
|
||||
gateway.succeed("iptables -D INPUT -p udp --dport ${toString wgGatewayPort} -j DROP")
|
||||
gateway.succeed("iptables -D OUTPUT -p udp --sport ${toString wgGatewayPort} -j DROP")
|
||||
|
||||
# Restart the wg namespace service to force reconnection
|
||||
nixarrHost.succeed("systemctl restart wg.service")
|
||||
nixarrHost.wait_for_unit("wg.service")
|
||||
|
||||
# Verify VPN connectivity is restored
|
||||
nixarrHost.succeed("ip netns exec wg ping -c 3 ${wgGatewayAddr}")
|
||||
|
||||
# Verify source IP is correct again - use internetClient since gateway has no web server
|
||||
nixarrHost.succeed("ip netns exec wg curl -s http://${internetClientIP}:8080 | grep -q '${wgClientAddr}'")
|
||||
|
||||
|
||||
print("=== Test 9: Verify DNS configuration ===")
|
||||
# Check that resolv.conf in namespace uses VPN DNS
|
||||
nixarrHost.succeed("ip netns exec wg cat /etc/resolv.conf | grep -q 'nameserver ${wgGatewayAddr}'")
|
||||
|
||||
# Verify no host DNS servers are present
|
||||
nixarrHost.fail("ip netns exec wg cat /etc/resolv.conf | grep -q 'nameserver 10.0.2.3'")
|
||||
|
||||
|
||||
print("=== Test 9b: DNS leak test ===")
|
||||
|
||||
# Debug: Check if dnsmasq is running on gateway
|
||||
gateway.succeed("systemctl status dnsmasq")
|
||||
gateway.succeed("ss -unpl | grep :53 || echo 'No DNS listener found'")
|
||||
|
||||
# Debug: Check connectivity to DNS server
|
||||
nixarrHost.succeed("ip netns exec wg ping -c 1 ${wgGatewayAddr}")
|
||||
|
||||
# Start tcpdump on host interface to detect DNS leaks
|
||||
nixarrHost.succeed("nohup tcpdump -i eth1 -n 'port 53' -w /tmp/dns-leak.pcap > /tmp/tcpdump-dns.log 2>&1 &")
|
||||
|
||||
# Clear dnsmasq query log
|
||||
gateway.succeed("echo > /var/log/dnsmasq-queries.log || true")
|
||||
|
||||
# Use dig instead of nslookup for more reliable DNS queries
|
||||
nixarrHost.succeed("ip netns exec wg dig @${wgGatewayAddr} test.vpn.local +short | grep -q ${wgGatewayAddr}")
|
||||
nixarrHost.succeed("ip netns exec wg dig @${wgGatewayAddr} leak.test.local +short | grep -q '1.2.3.4'")
|
||||
nixarrHost.succeed("ip netns exec wg dig @${wgGatewayAddr} transmission.local +short | grep -q ${wgClientAddr}")
|
||||
|
||||
# Also test without specifying server (uses resolv.conf)
|
||||
nixarrHost.succeed("ip netns exec wg dig test.vpn.local +short | grep -q ${wgGatewayAddr}")
|
||||
|
||||
# Wait for any potential leaked packets
|
||||
nixarrHost.succeed("pkill tcpdump || true")
|
||||
|
||||
# Check if any DNS packets were captured on host interface
|
||||
dns_packets = nixarrHost.succeed("tcpdump -r /tmp/dns-leak.pcap -nn 2>/dev/null | wc -l").strip()
|
||||
if int(dns_packets) > 0:
|
||||
# Show what was captured for debugging
|
||||
captured = nixarrHost.succeed("tcpdump -r /tmp/dns-leak.pcap -nn 2>/dev/null || echo 'No packets'")
|
||||
print("DNS leak detected! Captured " + dns_packets + " packets:")
|
||||
print(captured)
|
||||
nixarrHost.fail("DNS queries leaked to host network")
|
||||
|
||||
# Verify queries went through VPN by checking gateway's dnsmasq log
|
||||
gateway.succeed("grep -q 'test.vpn.local' /var/log/dnsmasq-queries.log")
|
||||
gateway.succeed("grep -q 'leak.test.local' /var/log/dnsmasq-queries.log")
|
||||
|
||||
print("✓ No DNS leaks detected - all queries confined to VPN")
|
||||
|
||||
# Clean up
|
||||
nixarrHost.succeed("rm -f /tmp/dns-leak.pcap /tmp/tcpdump-dns.log")
|
||||
|
||||
|
||||
print("=== Test 10: Port forwarding test ===")
|
||||
# Wait for transmission to be ready and listening
|
||||
nixarrHost.wait_for_open_port(9091) # Web UI port
|
||||
|
||||
# Check that transmission peer port is listening in namespace
|
||||
nixarrHost.succeed("ip netns exec wg ss -tlnp | grep -q ':51413'")
|
||||
nixarrHost.succeed("ip netns exec wg ss -ulnp | grep -q ':51413'")
|
||||
|
||||
# Ensure WireGuard tunnel is active
|
||||
nixarrHost.succeed("ip netns exec wg ping -c 1 ${wgGatewayAddr}")
|
||||
|
||||
# Debug: Print iptables rules and routing info
|
||||
print("=== Gateway NAT OUTPUT rules ===")
|
||||
output_rules = gateway.succeed("iptables -t nat -L OUTPUT -n -v")
|
||||
print(output_rules)
|
||||
|
||||
print("=== Gateway NAT PREROUTING rules ===")
|
||||
prerouting_rules = gateway.succeed("iptables -t nat -L PREROUTING -n -v")
|
||||
print(prerouting_rules)
|
||||
|
||||
print("=== Gateway routing table ===")
|
||||
routes = gateway.succeed("ip route show")
|
||||
print(routes)
|
||||
|
||||
print("=== WireGuard status on gateway ===")
|
||||
wg_gateway = gateway.succeed("wg show")
|
||||
print(wg_gateway)
|
||||
|
||||
print("=== WireGuard status on client ===")
|
||||
wg_client = nixarrHost.succeed("ip netns exec wg wg show")
|
||||
print(wg_client)
|
||||
|
||||
# Test port forwarding through gateway
|
||||
print("=== Testing port forwarding ===")
|
||||
# First verify the NAT rules were actually applied
|
||||
output_nat = gateway.succeed("iptables -t nat -L OUTPUT | grep 51413 || echo 'No OUTPUT NAT rules found'")
|
||||
prerouting_nat = gateway.succeed("iptables -t nat -L PREROUTING | grep 51413 || echo 'No PREROUTING NAT rules found'")
|
||||
print(f"OUTPUT NAT check: {output_nat}")
|
||||
print(f"PREROUTING NAT check: {prerouting_nat}")
|
||||
|
||||
# Debug connectivity and routing
|
||||
print("=== Testing connectivity from nixarrHost to gateway ===")
|
||||
nixarrHost.succeed("ping -c 1 ${gatewayIP}")
|
||||
|
||||
# Debug FORWARD chain
|
||||
forward_rules = gateway.succeed("iptables -L FORWARD -n -v")
|
||||
print(f"Gateway FORWARD rules:\n{forward_rules}")
|
||||
|
||||
# Check if gateway can reach VPN client (after handshake)
|
||||
gateway.succeed("wg show | grep -q 'latest handshake:'")
|
||||
|
||||
# Debug port forwarding connectivity
|
||||
print("=== Debugging port forwarding ===")
|
||||
|
||||
# First, ensure WireGuard tunnel is fully established
|
||||
gateway.succeed("wg") # Force handshake if needed
|
||||
nixarrHost.succeed("ip netns exec wg wg")
|
||||
|
||||
# Skip direct gateway->client test after restart (WireGuard asymmetry)
|
||||
print("=== Testing port forwarding ===")
|
||||
|
||||
# Test from nixarrHost (outside VPN) through gateway DNAT
|
||||
print("Test: External client -> Gateway -> VPN Client (via DNAT)")
|
||||
|
||||
# First, ensure client initiates some traffic to establish WireGuard state
|
||||
nixarrHost.succeed("ip netns exec wg ping -c 1 ${wgGatewayAddr}")
|
||||
|
||||
# Start tcpdump in background using nohup to properly detach it
|
||||
gateway.succeed("nohup tcpdump -i any -n 'port 51413 or host ${wgClientAddr}' -w /tmp/capture.pcap > /tmp/tcpdump.log 2>&1 &")
|
||||
|
||||
# Verify tcpdump is running
|
||||
gateway.succeed("pgrep tcpdump")
|
||||
print("Tcpdump started, now testing connection...")
|
||||
|
||||
# Now test the connection - this should succeed through DNAT!
|
||||
# Test from internetClient to gateway's internet IP - this simulates external traffic
|
||||
# The connection should be forwarded through the VPN to nixarrHost's transmission
|
||||
internetClient.succeed("timeout 5 nc -z -v ${internetGatewayIP} 51413")
|
||||
print("Success: Port forwarding works!")
|
||||
|
||||
# Stop tcpdump and analyze what happened
|
||||
gateway.succeed("pkill tcpdump")
|
||||
tcpdump_output = gateway.succeed("tcpdump -r /tmp/capture.pcap -nn 2>/dev/null || echo 'No packets captured'")
|
||||
print(f"Tcpdump results:\n{tcpdump_output}")
|
||||
|
||||
# Verify transmission can reach external services through VPN tunnel
|
||||
nixarrHost.succeed("ip netns exec wg curl -s http://${internetClientIP}:8080 | grep -q '${wgClientAddr}'")
|
||||
|
||||
# Verify port is NOT accessible from host network (outside VPN)
|
||||
nixarrHost.fail("timeout 2 nc -z -v localhost 51413")
|
||||
|
||||
|
||||
print("=== Test 11: IPv6 leak test ===")
|
||||
# Verify IPv6 connectivity between VMs
|
||||
nixarrHost.succeed("ping -6 -c 1 ${gatewayIPv6}")
|
||||
gateway.succeed("ping -6 -c 1 ${nixarrHostIPv6}")
|
||||
|
||||
# Check if IPv6 is enabled in VPN namespace
|
||||
nixarrHost.succeed("ip netns exec wg ip -6 addr show")
|
||||
|
||||
# Test IPv6 through VPN tunnel
|
||||
nixarrHost.succeed("ip netns exec wg ping -6 -c 1 ${wgGatewayAddrV6}")
|
||||
|
||||
# Test IPv6 traffic routing - should go through VPN tunnel to internetClient
|
||||
nixarrHost.succeed("ip netns exec wg curl -6 -s --max-time 2 http://[${internetClientIPv6}]:8080")
|
||||
nixarrHost.succeed("ip netns exec wg curl -6 -s http://[${internetClientIPv6}]:8080 | grep -q '${wgClientAddrV6}'")
|
||||
|
||||
|
||||
print("=== Test 12: IPv6 traffic test with VPN interruption ===")
|
||||
# Since WireGuard tunnel uses IPv4, blocking it affects both IPv4 and IPv6 traffic
|
||||
# The IPv6 traffic inside the tunnel should fail when we block the IPv4 WireGuard connection
|
||||
# This test verifies IPv6 behavior is tied to the VPN tunnel
|
||||
|
||||
# Verify WireGuard is listening (debug what ports are actually open)
|
||||
print("=== Gateway listening ports ===")
|
||||
listening_ports = gateway.succeed("ss -unp")
|
||||
print(listening_ports)
|
||||
|
||||
print("=== Looking for WireGuard port ${toString wgGatewayPort} ===")
|
||||
wg_port_check = gateway.succeed("ss -unp | grep :${toString wgGatewayPort} || echo 'WireGuard port not found'")
|
||||
print(wg_port_check)
|
||||
|
||||
# The previous test (Test 7) already blocked IPv4 WireGuard and verified it works
|
||||
# So IPv6 through the tunnel should also be blocked after IPv4 VPN disruption
|
||||
# Let's verify IPv6 still works before disruption
|
||||
nixarrHost.succeed("ip netns exec wg ping -6 -c 1 ${wgGatewayAddrV6}")
|
||||
|
||||
# Now use the same IPv4 blocking as Test 7
|
||||
gateway.succeed("iptables -I INPUT -p udp --dport ${toString wgGatewayPort} -j DROP")
|
||||
gateway.succeed("iptables -I OUTPUT -p udp --sport ${toString wgGatewayPort} -j DROP")
|
||||
|
||||
# Both IPv4 and IPv6 connectivity through VPN should fail
|
||||
nixarrHost.fail("ip netns exec wg ping -c 1 -W 2 ${wgGatewayAddr}")
|
||||
nixarrHost.fail("ip netns exec wg ping -6 -c 1 -W 2 ${wgGatewayAddrV6}")
|
||||
nixarrHost.fail("ip netns exec wg curl -6 -s --max-time 2 http://[${internetClientIPv6}]:8080")
|
||||
|
||||
|
||||
print("=== Test 13: IPv6 VPN recovery ===")
|
||||
# Remove iptables blocks (IPv4, since that's what WireGuard uses)
|
||||
gateway.succeed("iptables -D INPUT -p udp --dport ${toString wgGatewayPort} -j DROP")
|
||||
gateway.succeed("iptables -D OUTPUT -p udp --sport ${toString wgGatewayPort} -j DROP")
|
||||
|
||||
# Verify IPv6 VPN connectivity is restored
|
||||
nixarrHost.succeed("ip netns exec wg ping -6 -c 3 ${wgGatewayAddrV6}")
|
||||
|
||||
# Verify source IPv6 is correct again
|
||||
nixarrHost.succeed("ip netns exec wg curl -6 -s http://[${internetClientIPv6}]:8080 | grep -q '${wgClientAddrV6}'")
|
||||
|
||||
|
||||
print("=== All tests passed! ===")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user