mirror of
https://github.com/angristan/openvpn-install.git
synced 2026-08-03 05:08:13 +02:00
Add packet-level access policy tests
This commit is contained in:
@@ -30,6 +30,7 @@ jobs:
|
|||||||
image: ubuntu:22.04
|
image: ubuntu:22.04
|
||||||
- name: ubuntu-24.04
|
- name: ubuntu-24.04
|
||||||
image: ubuntu:24.04
|
image: ubuntu:24.04
|
||||||
|
policy_e2e: deny
|
||||||
- name: ubuntu-25.10
|
- name: ubuntu-25.10
|
||||||
image: ubuntu:25.10
|
image: ubuntu:25.10
|
||||||
- name: debian-11
|
- name: debian-11
|
||||||
@@ -98,6 +99,7 @@ jobs:
|
|||||||
name: fedora-42-firewalld
|
name: fedora-42-firewalld
|
||||||
image: fedora:42
|
image: fedora:42
|
||||||
enable_firewalld: true
|
enable_firewalld: true
|
||||||
|
policy_e2e: deny
|
||||||
tls:
|
tls:
|
||||||
name: tls-crypt-v2
|
name: tls-crypt-v2
|
||||||
sig: crypt-v2
|
sig: crypt-v2
|
||||||
@@ -107,6 +109,7 @@ jobs:
|
|||||||
name: debian-12-nftables
|
name: debian-12-nftables
|
||||||
image: debian:12
|
image: debian:12
|
||||||
enable_nftables: true
|
enable_nftables: true
|
||||||
|
policy_e2e: deny
|
||||||
tls:
|
tls:
|
||||||
name: tls-crypt-v2
|
name: tls-crypt-v2
|
||||||
sig: crypt-v2
|
sig: crypt-v2
|
||||||
@@ -129,13 +132,38 @@ jobs:
|
|||||||
name: tls-crypt-v2
|
name: tls-crypt-v2
|
||||||
sig: crypt-v2
|
sig: crypt-v2
|
||||||
key_file: tls-crypt-v2.key
|
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:
|
- os:
|
||||||
name: ubuntu-24.04-access-policy
|
name: ubuntu-24.04-access-policy
|
||||||
image: ubuntu:24.04
|
image: ubuntu:24.04
|
||||||
route_internet: n
|
route_internet: n
|
||||||
client_to_client: y
|
client_to_client: y
|
||||||
local_networks: 10.55.0.0/24
|
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:
|
tls:
|
||||||
name: tls-crypt-v2
|
name: tls-crypt-v2
|
||||||
sig: crypt-v2
|
sig: crypt-v2
|
||||||
@@ -165,12 +193,24 @@ jobs:
|
|||||||
- name: Create Docker network
|
- name: Create Docker network
|
||||||
run: docker network create --subnet=172.28.0.0/24 vpn-test
|
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
|
- name: Create shared volume
|
||||||
run: docker volume create shared-config
|
run: docker volume create shared-config
|
||||||
|
|
||||||
- name: Start OpenVPN server
|
- name: Start OpenVPN server
|
||||||
run: |
|
run: |
|
||||||
docker run -d \
|
POLICY_E2E=${{ matrix.os.policy_e2e || '' }}
|
||||||
|
docker create \
|
||||||
--name openvpn-server \
|
--name openvpn-server \
|
||||||
--hostname openvpn-server \
|
--hostname openvpn-server \
|
||||||
--privileged \
|
--privileged \
|
||||||
@@ -192,8 +232,14 @@ jobs:
|
|||||||
-e ROUTE_INTERNET=${{ matrix.os.route_internet || 'y' }} \
|
-e ROUTE_INTERNET=${{ matrix.os.route_internet || 'y' }} \
|
||||||
-e CLIENT_TO_CLIENT=${{ matrix.os.client_to_client || 'n' }} \
|
-e CLIENT_TO_CLIENT=${{ matrix.os.client_to_client || 'n' }} \
|
||||||
-e LOCAL_NETWORKS=${{ matrix.os.local_networks || '' }} \
|
-e LOCAL_NETWORKS=${{ matrix.os.local_networks || '' }} \
|
||||||
|
-e POLICY_E2E="$POLICY_E2E" \
|
||||||
openvpn-server
|
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
|
- name: Wait for server installation and startup
|
||||||
run: |
|
run: |
|
||||||
echo "Waiting for OpenVPN server to install and client config to be ready..."
|
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 \
|
docker run --rm -v shared-config:/shared alpine \
|
||||||
cat /shared/client.ovpn
|
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
|
- name: Start OpenVPN client and run tests
|
||||||
run: |
|
run: |
|
||||||
docker run \
|
docker run \
|
||||||
@@ -276,6 +337,8 @@ jobs:
|
|||||||
--network vpn-test \
|
--network vpn-test \
|
||||||
--ip 172.28.0.20 \
|
--ip 172.28.0.20 \
|
||||||
-v shared-config:/shared \
|
-v shared-config:/shared \
|
||||||
|
-e POLICY_E2E=${{ matrix.os.policy_e2e || '' }} \
|
||||||
|
-e POLICY_LAN_IP=10.55.0.20 \
|
||||||
openvpn-client &
|
openvpn-client &
|
||||||
|
|
||||||
# Wait for tests to complete (look for success message)
|
# Wait for tests to complete (look for success message)
|
||||||
@@ -323,12 +386,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Show client logs
|
- name: Show client logs
|
||||||
if: always()
|
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
|
- name: Cleanup
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
docker stop openvpn-server openvpn-client 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 2>/dev/null || true
|
docker rm openvpn-server openvpn-client policy-peer policy-lan-target 2>/dev/null || true
|
||||||
docker network rm vpn-test 2>/dev/null || true
|
docker network rm vpn-test policy-lan 2>/dev/null || true
|
||||||
docker volume rm shared-config 2>/dev/null || true
|
docker volume rm shared-config 2>/dev/null || true
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ RUN mkdir -p /dev/net
|
|||||||
|
|
||||||
# Copy test scripts
|
# Copy test scripts
|
||||||
COPY test/client-entrypoint.sh /entrypoint.sh
|
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
|
WORKDIR /etc/openvpn
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ RUN printf '%s\n' \
|
|||||||
'[Service]' \
|
'[Service]' \
|
||||||
'Type=oneshot' \
|
'Type=oneshot' \
|
||||||
'Environment=HOME=/root' \
|
'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' \
|
'WorkingDirectory=/root' \
|
||||||
'ExecStart=/entrypoint.sh' \
|
'ExecStart=/entrypoint.sh' \
|
||||||
'RemainAfterExit=yes' \
|
'RemainAfterExit=yes' \
|
||||||
|
|||||||
@@ -264,11 +264,66 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
|
|||||||
fi
|
fi
|
||||||
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
|
if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
|
||||||
test_dns_resolution "Test 4"
|
test_dns_resolution "Test 5"
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Executable
+35
@@ -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
|
||||||
@@ -77,6 +77,7 @@ export VPN_GATEWAY
|
|||||||
ROUTE_INTERNET="${ROUTE_INTERNET:-y}"
|
ROUTE_INTERNET="${ROUTE_INTERNET:-y}"
|
||||||
CLIENT_TO_CLIENT="${CLIENT_TO_CLIENT:-n}"
|
CLIENT_TO_CLIENT="${CLIENT_TO_CLIENT:-n}"
|
||||||
LOCAL_NETWORKS="${LOCAL_NETWORKS:-}"
|
LOCAL_NETWORKS="${LOCAL_NETWORKS:-}"
|
||||||
|
POLICY_E2E="${POLICY_E2E:-}"
|
||||||
|
|
||||||
# IPv6 configuration (optional)
|
# IPv6 configuration (optional)
|
||||||
# CLIENT_IPV6: y/n to enable IPv6 for VPN clients
|
# CLIENT_IPV6: y/n to enable IPv6 for VPN clients
|
||||||
@@ -341,6 +342,17 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# Copy client config to shared volume for initial connectivity tests
|
||||||
cp /root/testclient.ovpn /shared/client.ovpn
|
cp /root/testclient.ovpn /shared/client.ovpn
|
||||||
sed -i 's/^remote .*/remote openvpn-server 1194/' /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 ""
|
||||||
echo "=== Testing List Client Certificates ==="
|
echo "=== Testing List Client Certificates ==="
|
||||||
|
|
||||||
# At this point we have 3 client certificates:
|
# At this point PKI mode has three lifecycle-test certificates, plus the
|
||||||
# - testclient (Valid) - the renewed certificate
|
# optional policy peer used by packet-level access tests.
|
||||||
# - testclient (Revoked) - the old certificate revoked during renewal
|
|
||||||
# - revoketest (Revoked) - the revoked certificate
|
|
||||||
LIST_OUTPUT="/tmp/list-clients-output.log"
|
LIST_OUTPUT="/tmp/list-clients-output.log"
|
||||||
(bash /opt/openvpn-install.sh client list) 2>&1 | tee "$LIST_OUTPUT" || true
|
(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)
|
# Verify certificate count (varies by auth mode)
|
||||||
if [ "$AUTH_MODE" = "pki" ]; then
|
if [ "$AUTH_MODE" = "pki" ]; then
|
||||||
# PKI mode: 3 certs (testclient valid, testclient revoked from renewal, revoketest revoked)
|
EXPECTED_CLIENT_COUNT=3
|
||||||
if grep -q "Found 3 client certificate(s)" "$LIST_OUTPUT"; then
|
[ -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"
|
echo "PASS: List shows correct certificate count"
|
||||||
else
|
else
|
||||||
echo "FAIL: List does not show correct certificate count"
|
echo "FAIL: List does not show correct certificate count"
|
||||||
@@ -1112,10 +1123,10 @@ fi
|
|||||||
# Verify client count in JSON (varies by auth mode)
|
# Verify client count in JSON (varies by auth mode)
|
||||||
JSON_CLIENT_COUNT=$(jq '.clients | length' "$LIST_JSON_OUTPUT")
|
JSON_CLIENT_COUNT=$(jq '.clients | length' "$LIST_JSON_OUTPUT")
|
||||||
if [ "$AUTH_MODE" = "pki" ]; then
|
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)"
|
echo "PASS: Client list JSON has correct count ($JSON_CLIENT_COUNT)"
|
||||||
else
|
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"
|
cat "$LIST_JSON_OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user