mirror of
https://github.com/trailofbits/algo.git
synced 2026-08-30 19:59:42 +02:00
* 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>
302 lines
10 KiB
YAML
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"
|