diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 7c4c1b1..94d0645 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -30,6 +30,7 @@ jobs: image: ubuntu:22.04 - name: ubuntu-24.04 image: ubuntu:24.04 + policy_e2e: deny - name: ubuntu-25.10 image: ubuntu:25.10 - name: debian-11 @@ -98,6 +99,7 @@ jobs: name: fedora-42-firewalld image: fedora:42 enable_firewalld: true + policy_e2e: deny tls: name: tls-crypt-v2 sig: crypt-v2 @@ -107,6 +109,7 @@ jobs: name: debian-12-nftables image: debian:12 enable_nftables: true + policy_e2e: deny tls: name: tls-crypt-v2 sig: crypt-v2 @@ -129,13 +132,38 @@ jobs: name: tls-crypt-v2 sig: crypt-v2 key_file: tls-crypt-v2.key - # Test split tunnel with peer and home-LAN access enabled + # Test split tunnel with packet-level peer and home-LAN access - os: name: ubuntu-24.04-access-policy image: ubuntu:24.04 route_internet: n client_to_client: y local_networks: 10.55.0.0/24 + policy_e2e: allow + tls: + name: tls-crypt-v2 + sig: crypt-v2 + key_file: tls-crypt-v2.key + - os: + name: fedora-42-firewalld-access-policy + image: fedora:42 + enable_firewalld: true + route_internet: n + client_to_client: y + local_networks: 10.55.0.0/24 + policy_e2e: allow + tls: + name: tls-crypt-v2 + sig: crypt-v2 + key_file: tls-crypt-v2.key + - os: + name: debian-12-nftables-access-policy + image: debian:12 + enable_nftables: true + route_internet: n + client_to_client: y + local_networks: 10.55.0.0/24 + policy_e2e: allow tls: name: tls-crypt-v2 sig: crypt-v2 @@ -165,12 +193,24 @@ jobs: - name: Create Docker network run: docker network create --subnet=172.28.0.0/24 vpn-test + - name: Create policy test LAN + if: matrix.os.policy_e2e != '' + run: | + docker network create --subnet=10.55.0.0/24 policy-lan + docker run -d \ + --name policy-lan-target \ + --network policy-lan \ + --ip 10.55.0.20 \ + --entrypoint sleep \ + openvpn-client infinity + - name: Create shared volume run: docker volume create shared-config - name: Start OpenVPN server run: | - docker run -d \ + POLICY_E2E=${{ matrix.os.policy_e2e || '' }} + docker create \ --name openvpn-server \ --hostname openvpn-server \ --privileged \ @@ -192,8 +232,14 @@ jobs: -e ROUTE_INTERNET=${{ matrix.os.route_internet || 'y' }} \ -e CLIENT_TO_CLIENT=${{ matrix.os.client_to_client || 'n' }} \ -e LOCAL_NETWORKS=${{ matrix.os.local_networks || '' }} \ + -e POLICY_E2E="$POLICY_E2E" \ openvpn-server + if [ -n "$POLICY_E2E" ]; then + docker network connect --ip 10.55.0.10 --gw-priority -1 policy-lan openvpn-server + fi + docker start openvpn-server + - name: Wait for server installation and startup run: | echo "Waiting for OpenVPN server to install and client config to be ready..." @@ -266,6 +312,21 @@ jobs: docker run --rm -v shared-config:/shared alpine \ cat /shared/client.ovpn + - name: Start policy test peer + if: matrix.os.policy_e2e != '' + run: | + docker exec policy-lan-target ping -c 3 -W 2 10.55.0.10 + docker run -d \ + --name policy-peer \ + --hostname policy-peer \ + --cap-add=NET_ADMIN \ + --device=/dev/net/tun:/dev/net/tun \ + --network vpn-test \ + --ip 172.28.0.30 \ + -v shared-config:/shared \ + --entrypoint /policy-peer-entrypoint.sh \ + openvpn-client + - name: Start OpenVPN client and run tests run: | docker run \ @@ -276,6 +337,8 @@ jobs: --network vpn-test \ --ip 172.28.0.20 \ -v shared-config:/shared \ + -e POLICY_E2E=${{ matrix.os.policy_e2e || '' }} \ + -e POLICY_LAN_IP=10.55.0.20 \ openvpn-client & # Wait for tests to complete (look for success message) @@ -323,12 +386,14 @@ jobs: - name: Show client logs if: always() - run: docker logs openvpn-client 2>&1 || true + run: | + docker logs openvpn-client 2>&1 || true + docker logs policy-peer 2>&1 || true - name: Cleanup if: always() run: | - docker stop openvpn-server openvpn-client 2>/dev/null || true - docker rm openvpn-server openvpn-client 2>/dev/null || true - docker network rm vpn-test 2>/dev/null || true + docker stop openvpn-server openvpn-client policy-peer policy-lan-target 2>/dev/null || true + docker rm openvpn-server openvpn-client policy-peer policy-lan-target 2>/dev/null || true + docker network rm vpn-test policy-lan 2>/dev/null || true docker volume rm shared-config 2>/dev/null || true diff --git a/test/Dockerfile.client b/test/Dockerfile.client index 51be779..3fb1bc3 100644 --- a/test/Dockerfile.client +++ b/test/Dockerfile.client @@ -19,7 +19,8 @@ RUN mkdir -p /dev/net # Copy test scripts COPY test/client-entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +COPY test/policy-peer-entrypoint.sh /policy-peer-entrypoint.sh +RUN chmod +x /entrypoint.sh /policy-peer-entrypoint.sh WORKDIR /etc/openvpn diff --git a/test/Dockerfile.server b/test/Dockerfile.server index b451526..032852c 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -80,7 +80,7 @@ RUN printf '%s\n' \ '[Service]' \ 'Type=oneshot' \ 'Environment=HOME=/root' \ - 'PassEnvironment=AUTH_MODE TLS_SIG TLS_KEY_FILE TLS_VERSION_MIN TLS13_CIPHERSUITES CLIENT_IPV6 VPN_SUBNET_IPV6 ROUTE_INTERNET CLIENT_TO_CLIENT LOCAL_NETWORKS WAIT_TIMEOUT_SIGNAL WAIT_TIMEOUT_CONNECT WAIT_TIMEOUT_REVOKE' \ + 'PassEnvironment=AUTH_MODE TLS_SIG TLS_KEY_FILE TLS_VERSION_MIN TLS13_CIPHERSUITES CLIENT_IPV6 VPN_SUBNET_IPV6 ROUTE_INTERNET CLIENT_TO_CLIENT LOCAL_NETWORKS POLICY_E2E WAIT_TIMEOUT_SIGNAL WAIT_TIMEOUT_CONNECT WAIT_TIMEOUT_REVOKE' \ 'WorkingDirectory=/root' \ 'ExecStart=/entrypoint.sh' \ 'RemainAfterExit=yes' \ diff --git a/test/client-entrypoint.sh b/test/client-entrypoint.sh index 19b3a2d..b43e21f 100755 --- a/test/client-entrypoint.sh +++ b/test/client-entrypoint.sh @@ -264,11 +264,66 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then fi fi -# Test 4: DNS resolution through Unbound in full-tunnel mode. +# Packet-level access policy tests use a second VPN client and a LAN-only host. +if [ -n "${POLICY_E2E:-}" ]; then + echo "Test 4: Checking packet-level access policy..." + wait_for_file /shared/policy-peer-ip "policy peer VPN address" + POLICY_PEER_IP=$(cat /shared/policy-peer-ip) + POLICY_LAN_IP="${POLICY_LAN_IP:-10.55.0.20}" + + if [ "$POLICY_E2E" = "allow" ]; then + if ping -c 3 -W 2 "$POLICY_PEER_IP" >/dev/null; then + echo "PASS: Client-to-client packets are allowed" + else + echo "FAIL: Cannot reach allowed VPN peer $POLICY_PEER_IP" + exit 1 + fi + if ping -c 3 -W 2 "$POLICY_LAN_IP" >/dev/null; then + echo "PASS: LAN packets and destination-scoped NAT work" + else + echo "FAIL: Cannot reach allowed LAN host $POLICY_LAN_IP" + exit 1 + fi + elif [ "$POLICY_E2E" = "deny" ]; then + if ping -c 1 -W 2 "$POLICY_PEER_IP" >/dev/null; then + echo "FAIL: Isolated VPN peer $POLICY_PEER_IP is reachable" + exit 1 + fi + echo "PASS: Client-to-client packets are blocked" + if ping -c 1 -W 2 "$POLICY_LAN_IP" >/dev/null; then + echo "FAIL: Unexposed LAN host $POLICY_LAN_IP is reachable" + exit 1 + fi + echo "PASS: Unexposed LAN packets are blocked" + else + echo "FAIL: Unknown POLICY_E2E value: $POLICY_E2E" + exit 1 + fi + + if [ "${ROUTE_INTERNET:-y}" = "y" ]; then + PUBLIC_DNS_OUTPUT="" + for _ in $(seq 1 5); do + PUBLIC_DNS_OUTPUT=$(dig @1.1.1.1 example.com +short +tcp +time=5 +tries=1 2>&1) || true + if grep -qE '^[0-9]+(\.[0-9]+){3}$' <<<"$PUBLIC_DNS_OUTPUT"; then + break + fi + PUBLIC_DNS_OUTPUT="" + sleep 2 + done + if [ -n "$PUBLIC_DNS_OUTPUT" ]; then + echo "PASS: Direct internet packets traverse VPN forwarding and NAT" + else + echo "FAIL: Direct public DNS query through the VPN failed" + exit 1 + fi + fi +fi + +# Test 5: DNS resolution through Unbound in full-tunnel mode. if [ "${ROUTE_INTERNET:-y}" = "y" ]; then - test_dns_resolution "Test 4" + test_dns_resolution "Test 5" else - echo "Test 4: SKIP: VPN DNS is disabled in split-tunnel mode" + echo "Test 5: SKIP: VPN DNS is disabled in split-tunnel mode" fi echo "" diff --git a/test/policy-peer-entrypoint.sh b/test/policy-peer-entrypoint.sh new file mode 100755 index 0000000..db4e932 --- /dev/null +++ b/test/policy-peer-entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" +elapsed=0 + +while [ ! -f /shared/policy-peer.ovpn ]; do + if [ "$elapsed" -ge "$WAIT_TIMEOUT" ]; then + echo "FAIL: Timed out waiting for peer client configuration" + exit 1 + fi + echo "Waiting for peer client configuration... (${elapsed}/${WAIT_TIMEOUT}s)" + sleep 2 + elapsed=$((elapsed + 2)) +done + +openvpn --config /shared/policy-peer.ovpn --daemon --log /var/log/openvpn-policy-peer.log + +elapsed=0 +until ip -4 addr show tun0 2>/dev/null | grep -q 'inet '; do + if [ "$elapsed" -ge "$WAIT_TIMEOUT" ]; then + echo "FAIL: Timed out waiting for peer VPN connection" + cat /var/log/openvpn-policy-peer.log 2>/dev/null || true + exit 1 + fi + echo "Waiting for peer VPN connection... (${elapsed}/${WAIT_TIMEOUT}s)" + sleep 2 + elapsed=$((elapsed + 2)) +done + +PEER_IP=$(ip -4 -o addr show tun0 | awk '{print $4}' | cut -d/ -f1) +printf '%s\n' "$PEER_IP" >/shared/policy-peer-ip +echo "Policy peer connected with VPN address $PEER_IP" + +exec tail -f /var/log/openvpn-policy-peer.log diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 565c77f..99a6f5e 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -77,6 +77,7 @@ export VPN_GATEWAY ROUTE_INTERNET="${ROUTE_INTERNET:-y}" CLIENT_TO_CLIENT="${CLIENT_TO_CLIENT:-n}" LOCAL_NETWORKS="${LOCAL_NETWORKS:-}" +POLICY_E2E="${POLICY_E2E:-}" # IPv6 configuration (optional) # CLIENT_IPV6: y/n to enable IPv6 for VPN clients @@ -341,6 +342,17 @@ else exit 1 fi +if [ -n "$POLICY_E2E" ]; then + echo "Creating second VPN client for packet-level policy tests..." + bash /opt/openvpn-install.sh client add policy-peer --cert-days 3650 + if [ ! -f /root/policy-peer.ovpn ]; then + echo "FAIL: Policy peer client configuration was not generated" + exit 1 + fi + cp /root/policy-peer.ovpn /shared/policy-peer.ovpn + sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/policy-peer.ovpn +fi + # Copy client config to shared volume for initial connectivity tests cp /root/testclient.ovpn /shared/client.ovpn sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/client.ovpn @@ -1049,10 +1061,8 @@ echo "=== Certificate Revocation Tests PASSED ===" echo "" echo "=== Testing List Client Certificates ===" -# At this point we have 3 client certificates: -# - testclient (Valid) - the renewed certificate -# - testclient (Revoked) - the old certificate revoked during renewal -# - revoketest (Revoked) - the revoked certificate +# At this point PKI mode has three lifecycle-test certificates, plus the +# optional policy peer used by packet-level access tests. LIST_OUTPUT="/tmp/list-clients-output.log" (bash /opt/openvpn-install.sh client list) 2>&1 | tee "$LIST_OUTPUT" || true @@ -1075,8 +1085,9 @@ fi # Verify certificate count (varies by auth mode) if [ "$AUTH_MODE" = "pki" ]; then - # PKI mode: 3 certs (testclient valid, testclient revoked from renewal, revoketest revoked) - if grep -q "Found 3 client certificate(s)" "$LIST_OUTPUT"; then + EXPECTED_CLIENT_COUNT=3 + [ -n "$POLICY_E2E" ] && EXPECTED_CLIENT_COUNT=4 + if grep -q "Found $EXPECTED_CLIENT_COUNT client certificate(s)" "$LIST_OUTPUT"; then echo "PASS: List shows correct certificate count" else echo "FAIL: List does not show correct certificate count" @@ -1112,10 +1123,10 @@ fi # Verify client count in JSON (varies by auth mode) JSON_CLIENT_COUNT=$(jq '.clients | length' "$LIST_JSON_OUTPUT") if [ "$AUTH_MODE" = "pki" ]; then - if [ "$JSON_CLIENT_COUNT" -eq 3 ]; then + if [ "$JSON_CLIENT_COUNT" -eq "$EXPECTED_CLIENT_COUNT" ]; then echo "PASS: Client list JSON has correct count ($JSON_CLIENT_COUNT)" else - echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 3)" + echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected $EXPECTED_CLIENT_COUNT)" cat "$LIST_JSON_OUTPUT" exit 1 fi