Files
9268a78028 Add end-to-end VPN connectivity tests using network namespaces (#14914)
* Add end-to-end VPN connectivity tests using network namespaces

Addresses #14912

Current integration tests verify that VPN services start, but don't verify
they actually work. This adds true E2E tests using Linux network namespaces
to simulate a client connecting to the server.

New tests verify:
- WireGuard handshake completes and tunnel is functional
- IPsec/StrongSwan service is configured and listening
- DNS resolution works through VPN (172.16.0.1)
- mobileconfig XML files are valid
- CA certificate chain is correct

Changes:
- Add tests/e2e/test-vpn-connectivity.sh - main E2E test script
- Add tests/e2e/README.md - documentation for running tests
- Update integration-tests.yml to run E2E tests after deployment
- Delete tests/legacy-lxd/ - replaced by new E2E tests
- Update .ansible-lint to remove legacy-lxd from excludes
- Rewrite tests/README.md for clarity

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix WireGuard handshake timeout by allowing VPN traffic on veth

The namespace test was timing out because the firewall was blocking
UDP traffic on the veth interface. This adds explicit INPUT rules
to allow WireGuard (51820) and IPsec (500, 4500) traffic.

Also refines the MASQUERADE rule to not apply to bridge-local traffic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Use -I instead of -A for iptables rules; add debug output

The firewall rules were being appended (-A) after existing DROP rules
and never matched. Changed to -I to insert at beginning of chain.

Also added debug output to show:
- Server WireGuard peers before client connects
- Server port listening status
- iptables INPUT chain on timeout (to verify rules)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Work around deployment bug where WireGuard handlers don't fire

The async role execution in server.yml causes handlers not to fire
properly. This workaround restarts WireGuard if no peers are found,
ensuring the peer configuration is loaded.

Root cause: import_role with async: 300, poll: 0 breaks handler
notification flow. The 'restart wireguard' handler is notified but
never executed because the async context loses track of handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add packet capture and rp_filter diagnostics to debug WireGuard handshake

- Disable reverse path filtering on veth interface (can drop packets)
- Add tcpdump capture to see if UDP packets are arriving
- Show host and namespace routing tables
- Add route debugging to error output

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add PersistentKeepalive to trigger WireGuard handshake

WireGuard only initiates a handshake when there's outgoing traffic or
a keepalive timer fires. Without PersistentKeepalive, the test was
waiting forever because no traffic was being sent through the tunnel
(Table=off prevents route creation).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Clean up verbose debug output from WireGuard tests

Remove routing table and rp_filter debug output that was printed on every
run. Keep the packet capture and detailed error diagnostics that are only
shown on failure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Document configuration assumptions in E2E test README

Add explicit documentation about the hardcoded IP addresses and test user
requirements as suggested in code review. This helps users understand what
default values are expected and why tests might fail on custom configurations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove unused pip cache from integration tests workflow

We use uv for dependency management, not pip, so the pip cache setting
was causing warnings about missing cache folders.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-29 02:16:33 -05:00
..

End-to-End VPN Connectivity Tests

This directory contains end-to-end tests that verify actual VPN connectivity using Linux network namespaces.

Architecture

+---------------------------+     veth pair      +---------------------------+
|   Main Namespace          |                    |   Client Namespace        |
|   (VPN Server)            |                    |   (algo-client)           |
|                           |                    |                           |
|   wg0: 10.49.0.1         |   veth-algo-srv    |   veth-algo-cli           |
|   strongswan listening    |<----------------->|   10.99.0.2/24            |
|   dns: 172.16.0.1        |   10.99.0.1/24     |                           |
|                           |                    |   wg0 (after connection)  |
+---------------------------+                    +---------------------------+

The test creates a network namespace that simulates a VPN client. Traffic from the namespace routes through a veth pair to the host, which NATs it to allow the client to connect to the VPN server running on localhost.

Running Locally (Linux)

These tests require Linux (network namespaces are a Linux kernel feature).

# Deploy Algo first
ansible-playbook main.yml -e "provider=local"

# Run all connectivity tests
sudo tests/e2e/test-vpn-connectivity.sh both

# Run only WireGuard tests
sudo tests/e2e/test-vpn-connectivity.sh wireguard

# Run only IPsec tests
sudo tests/e2e/test-vpn-connectivity.sh ipsec

Running on macOS (via Multipass)

Use Multipass to run an Ubuntu VM:

# Launch and mount algo directory
multipass launch 22.04 --name algo-test --cpus 2 --memory 4G --disk 20G
multipass mount ~/path/to/algo algo-test:/home/ubuntu/algo
multipass shell algo-test

# Inside VM: install dependencies and deploy
sudo apt-get update
sudo apt-get install -y python3-pip wireguard-tools strongswan libxml2-utils dnsutils
curl -LsSf https://astral.sh/uv/install.sh | sh && source ~/.bashrc
cd ~/algo && uv sync
uv run ansible-playbook main.yml -e "provider=local"

# Run tests
sudo tests/e2e/test-vpn-connectivity.sh both

# Cleanup (from macOS)
multipass delete algo-test && multipass purge

Requirements

  • Root access (for network namespace operations)
  • Linux (network namespaces are a kernel feature)
  • Deployed Algo VPN on localhost (configs in configs/localhost/)
  • Required tools:
    • iproute2 (ip netns)
    • wireguard-tools (wg, wg-quick)
    • strongswan (ipsec, swanctl)
    • libxml2-utils (xmllint)
    • openssl
    • dnsutils (host)

Configuration Assumptions

The tests assume Algo's default network configuration:

Setting Default Value Environment Variable
Test user alice TEST_USER=username
WireGuard server IP 10.49.0.1 (hardcoded)
DNS service IP 172.16.0.1 (hardcoded)

If you've customized wireguard_network_ipv4 or local_service_ip in your deployment, the tests will fail. The CI workflow creates users alice and bob specifically for testing.

What Gets Tested

Validation Tests (No Namespace Required)

  • mobileconfig XML syntax validation (xmllint)
  • CA certificate chain verification (openssl verify)

WireGuard Tests

  1. Client config file exists and is parseable
  2. WireGuard interface comes up in namespace
  3. Cryptographic handshake completes (checks latest handshake)
  4. Ping to server VPN IP (10.49.0.1) succeeds
  5. DNS resolution through VPN (172.16.0.1) works

IPsec Tests

  1. Certificate and key files exist
  2. Certificate chain validates
  3. IPsec service is running and listening
  4. IPsec ports (500, 4500) are reachable
  5. DNS service is responding

Test Flow

  1. Setup: Create algo-client network namespace with veth pair
  2. Validate: Check mobileconfig XML and certificates
  3. WireGuard: Start wg-quick in namespace, verify handshake and connectivity
  4. IPsec: Verify certificates and service status
  5. Cleanup: Remove namespace, NAT rules, and temp files

Troubleshooting

Common Issues

Namespace already exists

sudo ip netns del algo-client

WireGuard handshake timeout

  • Check firewall allows UDP 51820
  • Verify wg0 interface exists on host: sudo wg show
  • Check server public key matches config

IPsec connection failed

  • Verify strongswan service: sudo systemctl status strongswan-starter
  • Check certificates: openssl verify -CAfile cacert.pem user.crt
  • Review logs: sudo journalctl -u strongswan -n 50

DNS resolution failed

  • Check dnscrypt-proxy: sudo systemctl status dnscrypt-proxy
  • Verify DNS IP is routed: ip route get 172.16.0.1
  • Test from host: host google.com 172.16.0.1

Debug Mode

If tests fail, debug information is automatically collected including:

  • Network interfaces and routes
  • WireGuard and IPsec status
  • iptables NAT rules
  • DNS service status
  • Recent system logs

CI Integration

These tests run automatically in GitHub Actions after Algo deployment:

- name: Run E2E VPN connectivity tests
  run: sudo tests/e2e/test-vpn-connectivity.sh "${{ matrix.vpn_type }}"

The tests are matrix-aware and run for wireguard, ipsec, or both configurations.