Files
algo/.github/workflows/integration-tests.yml
T
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

302 lines
10 KiB
YAML

---
name: Integration Tests
'on':
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'main.yml'
- 'roles/**'
- 'playbooks/**'
- 'library/**'
workflow_dispatch:
schedule:
- cron: '0 2 * * 1' # Weekly on Monday at 2 AM
permissions:
contents: read
jobs:
localhost-deployment:
name: Localhost VPN Deployment Test
runs-on: ubuntu-22.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
vpn_type: ['wireguard', 'ipsec', 'both']
steps:
- uses: actions/checkout@c2d88d3ecc89a9ef08eebf45d9637801dcee7eb5 # v5.0.1
with:
persist-credentials: false
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: '3.11'
# Note: No pip cache - we use uv for dependency management
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
wireguard \
wireguard-tools \
strongswan \
libstrongswan-standard-plugins \
dnsmasq \
qrencode \
openssl \
linux-headers-$(uname -r) \
libxml2-utils \
dnsutils
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install Python dependencies
run: uv sync
- name: Create test configuration
run: |
cat > integration-test.cfg << EOF
users:
- alice
- bob
cloud_providers:
local:
server: localhost
endpoint: 127.0.0.1
wireguard_enabled: ${{ matrix.vpn_type == 'wireguard' || matrix.vpn_type == 'both' }}
ipsec_enabled: ${{ matrix.vpn_type == 'ipsec' || matrix.vpn_type == 'both' }}
dns_adblocking: true
ssh_tunneling: false
store_pki: true
algo_provider: local
algo_server_name: github-ci-test
server: localhost
algo_ssh_port: 22
CA_password: "test-ca-password-${{ github.run_id }}"
p12_export_password: "test-p12-password-${{ github.run_id }}"
tests: true
no_log: false
ansible_connection: local
dns_encryption: true
algo_dns_adblocking: true
algo_ssh_tunneling: false
BetweenClients_DROP: true
block_smb: true
block_netbios: true
pki_in_tmpfs: true
endpoint: 127.0.0.1
ssh_port: 4160
local_service_ip: 172.16.0.1
local_service_ipv6: "fd00::1"
EOF
- name: Run Algo deployment
run: |
# Run ansible-playbook via uv - become: true in playbook handles root
# GitHub runners have passwordless sudo for become escalation
uv run ansible-playbook main.yml \
-i "localhost," \
-c local \
-e @integration-test.cfg \
-e "provider=local" \
-vv
- name: Verify services are running
run: |
# Check WireGuard
if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
echo "Checking WireGuard..."
sudo wg show
if ! sudo systemctl is-active --quiet wg-quick@wg0; then
echo "✗ WireGuard service not running"
exit 1
fi
echo "✓ WireGuard is running"
fi
# Check StrongSwan (service name is strongswan-starter on Ubuntu 20.04+)
if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
echo "Checking StrongSwan..."
sudo ipsec statusall
if ! sudo systemctl is-active --quiet strongswan-starter; then
echo "✗ StrongSwan service not running"
exit 1
fi
echo "✓ StrongSwan is running"
fi
# Check dnsmasq
if ! sudo systemctl is-active --quiet dnsmasq; then
echo "⚠️ dnsmasq not running (may be expected)"
else
echo "✓ dnsmasq is running"
fi
# Check dnscrypt-proxy
if sudo systemctl is-active --quiet dnscrypt-proxy; then
echo "✓ dnscrypt-proxy is running"
else
echo "⚠️ dnscrypt-proxy not running"
fi
# DNS health check - verify DNS resolution works
echo "Testing DNS resolution via local_service_ip (172.16.0.1)..."
if dig @172.16.0.1 google.com +short +timeout=5 | grep -q .; then
echo "✓ DNS resolution working"
else
echo "⚠️ DNS resolution failed (service may still be starting)"
fi
- name: Verify generated configs
run: |
echo "Checking generated configuration files..."
# WireGuard configs
if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
for user in alice bob; do
if [ ! -f "configs/localhost/wireguard/${user}.conf" ]; then
echo "✗ Missing WireGuard config for ${user}"
exit 1
fi
if [ ! -f "configs/localhost/wireguard/${user}.png" ]; then
echo "✗ Missing WireGuard QR code for ${user}"
exit 1
fi
done
echo "✓ All WireGuard configs generated"
fi
# IPsec configs (p12 in manual/, mobileconfig in apple/)
if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
for user in alice bob; do
if [ ! -f "configs/localhost/ipsec/manual/${user}.p12" ]; then
echo "✗ Missing IPsec certificate for ${user}"
exit 1
fi
if [ ! -f "configs/localhost/ipsec/apple/${user}.mobileconfig" ]; then
echo "✗ Missing IPsec mobile config for ${user}"
exit 1
fi
done
echo "✓ All IPsec configs generated"
fi
- name: Test VPN connectivity
run: |
echo "Testing basic VPN connectivity..."
# Test WireGuard
if [[ "${{ matrix.vpn_type }}" == "wireguard" || "${{ matrix.vpn_type }}" == "both" ]]; then
# Get server's WireGuard public key
SERVER_PUBKEY=$(sudo wg show wg0 public-key)
echo "Server public key: $SERVER_PUBKEY"
# Check if interface has peers
PEER_COUNT=$(sudo wg show wg0 peers | wc -l)
echo "✓ WireGuard has $PEER_COUNT peer(s) configured"
fi
# Test StrongSwan
if [[ "${{ matrix.vpn_type }}" == "ipsec" || "${{ matrix.vpn_type }}" == "both" ]]; then
# Check IPsec policies
sudo ipsec statusall | grep -E "INSTALLED|ESTABLISHED" || echo "No active IPsec connections (expected)"
fi
- name: Run E2E VPN connectivity tests
env:
VPN_TYPE: ${{ matrix.vpn_type }}
run: |
chmod +x tests/e2e/test-vpn-connectivity.sh
sudo tests/e2e/test-vpn-connectivity.sh "${VPN_TYPE}"
- name: Collect E2E debug info on failure
if: failure()
run: |
echo "=== E2E Test Debug Information ==="
echo "=== Network Namespaces ==="
ip netns list || true
echo "=== WireGuard Config (alice) ==="
cat configs/localhost/wireguard/alice.conf 2>/dev/null || echo "Not found"
echo "=== IPsec Certificates ==="
ls -la configs/localhost/ipsec/.pki/certs/ 2>/dev/null || echo "Not found"
echo "=== iptables NAT ==="
sudo iptables -t nat -L -n -v || true
- name: Upload configs as artifacts
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: vpn-configs-${{ matrix.vpn_type }}-${{ github.run_id }}
path: configs/
retention-days: 7
- name: Upload logs on failure
if: failure()
run: |
echo "=== Network Interfaces ==="
ip addr || true
echo "=== Listening Ports ==="
sudo ss -tulnp || true
echo "=== WireGuard Status ==="
sudo wg show || true
echo "=== IPsec Status ==="
sudo ipsec statusall || true
echo "=== DNS Services ==="
sudo systemctl status dnscrypt-proxy dnscrypt-proxy.socket dnsmasq --no-pager || true
echo "=== WireGuard Log ==="
sudo journalctl -u wg-quick@wg0 -n 50 --no-pager || true
echo "=== StrongSwan Log ==="
sudo journalctl -u strongswan -n 50 --no-pager || true
echo "=== dnscrypt-proxy Log ==="
sudo journalctl -u dnscrypt-proxy -n 50 --no-pager || true
echo "=== System Log (last 100 lines) ==="
sudo journalctl -n 100 --no-pager || true
docker-build-test:
name: Docker Image Build Test
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@c2d88d3ecc89a9ef08eebf45d9637801dcee7eb5 # v5.0.1
with:
persist-credentials: false
- name: Build Algo Docker image
run: |
docker build -t algo:ci-test .
- name: Test Docker image
run: |
# Test that the image can run and show help
docker run --rm --entrypoint /bin/sh algo:ci-test -c "cd /algo && ./algo --help" || true
# Test that required binaries exist in the virtual environment
docker run --rm --entrypoint /bin/sh algo:ci-test -c "cd /algo && uv run which ansible"
docker run --rm --entrypoint /bin/sh algo:ci-test -c "which python3"
docker run --rm --entrypoint /bin/sh algo:ci-test -c "which rsync"
- name: Test Docker config validation
run: |
# Create a minimal valid config
mkdir -p test-data
cat > test-data/config.cfg << 'EOF'
users:
- test-user
cloud_providers:
ec2:
size: t3.micro
region: us-east-1
wireguard_enabled: true
ipsec_enabled: false
dns_encryption: true
algo_provider: ec2
EOF
# Test that config is readable
docker run --rm --entrypoint cat -v $(pwd)/test-data:/data algo:ci-test /data/config.cfg
echo "✓ Docker image built and basic tests passed"