mirror of
https://github.com/angristan/openvpn-install.git
synced 2025-12-16 17:07:02 +01:00
## Summary Comprehensive IPv4/IPv6 overhaul that decouples server endpoint addressing from client tunnel addressing, supporting all combinations with automatic leak prevention. ### Supported Configurations | Endpoint | Client Mode | Description | |----------|-------------|-------------| | IPv4 | IPv4-only | Traditional setup (4→4) | | IPv4 | Dual-stack | IPv4 endpoint, clients get both (4→4/6) | | IPv4 | IPv6-only | IPv4 endpoint, clients get IPv6 only (4→6) | | IPv6 | IPv4-only | IPv6 endpoint, clients get IPv4 only (6→4) | | IPv6 | Dual-stack | IPv6 endpoint, clients get both (6→4/6) | | IPv6 | IPv6-only | Full IPv6 setup (6→6) | ### Leak Prevention - **IPv4-only mode**: Pushes `block-ipv6` to clients, blocking all IPv6 traffic - **IPv6-only mode**: Assigns IPv4 addresses and pushes `redirect-gateway def1` to capture IPv4 traffic, which is then dropped (no IPv4 NAT configured) - **Dual-stack mode**: Both protocols tunneled normally ### New CLI Options ``` Network Options: --endpoint-type <4|6> Endpoint IP version (default: 4) --client-ipv4 Enable IPv4 for VPN clients (default: enabled) --no-client-ipv4 Disable IPv4 for VPN clients --client-ipv6 Enable IPv6 for VPN clients (default: disabled) --no-client-ipv6 Disable IPv6 for VPN clients --subnet-ipv4 <x.x.x.0> IPv4 VPN subnet (default: 10.8.0.0) --subnet-ipv6 <prefix> IPv6 VPN subnet (default: fd42:42:42:42::) ``` ### Usage Examples ```bash # Dual-stack clients (IPv4 + IPv6) ./openvpn-install.sh install --client-ipv4 --client-ipv6 # IPv6-only clients (IPv4 traffic blocked) ./openvpn-install.sh install --no-client-ipv4 --client-ipv6 # IPv4-only clients (IPv6 traffic blocked) - default behavior ./openvpn-install.sh install --client-ipv4 --no-client-ipv6 # IPv6 server endpoint ./openvpn-install.sh install --endpoint-type 6 --endpoint 2001:db8::1 # Custom subnets ./openvpn-install.sh install --client-ipv6 --subnet-ipv4 10.9.0.0 --subnet-ipv6 fd00🔢5678:: ``` ### Implementation Details **Core changes:** - New `ENDPOINT_TYPE` variable (4 or 6) controls server listening protocol - New `CLIENT_IPV4`/`CLIENT_IPV6` variables control client tunnel addressing - Renamed `VPN_SUBNET` → `VPN_SUBNET_IPV4`, added `VPN_SUBNET_IPV6` - Separate `resolvePublicIPv4()` and `resolvePublicIPv6()` functions - New `validate_subnet_ipv6()` for ULA (fd00::/8) validation **Protocol handling:** - Uses `proto udp6`/`tcp6` when endpoint type is IPv6 - Firewall and SELinux commands handle both protocol variants **Firewall updates:** - firewalld: Conditional IPv6 masquerade and forwarding - nftables: Separate ip/ip6 tables for NAT based on client config - iptables: ip6tables rules only when IPv6 clients enabled **DNS configuration:** - Unbound listens on IPv4/IPv6 gateway addresses as needed - All third-party DNS providers now include IPv6 servers: - Cloudflare: 2606:4700:4700::1111 - Quad9: 2620:fe::fe - Google: 2001:4860:4860::8888 - OpenDNS: 2620:119:35::35 - AdGuard: 2a10:50c0::ad1:ff **CI/Testing:** - Added `ubuntu-24.04-dual-stack` test matrix entry - Docker test container enables IPv6 forwarding - `CLIENT_IPV6` environment variable passed to test container ## Test Plan - [x] Shellcheck passes - [x] CI Docker tests pass (including new dual-stack test) - [x] Manual testing: IPv4-only mode blocks IPv6 traffic - [x] Manual testing: IPv6-only mode blocks IPv4 traffic - [x] Manual testing: Dual-stack mode tunnels both protocols --- Closes #1317 Closes #1288 Closes #1084 Closes #701 Closes #350
309 lines
11 KiB
YAML
309 lines
11 KiB
YAML
---
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
name: Docker Test
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
docker-test:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os:
|
|
- name: ubuntu-18.04
|
|
image: ubuntu:18.04
|
|
- name: ubuntu-20.04
|
|
image: ubuntu:20.04
|
|
- name: ubuntu-22.04
|
|
image: ubuntu:22.04
|
|
- name: ubuntu-24.04
|
|
image: ubuntu:24.04
|
|
- name: ubuntu-25.10
|
|
image: ubuntu:25.10
|
|
- name: debian-11
|
|
image: debian:11
|
|
- name: debian-12
|
|
image: debian:12
|
|
- name: centos-stream-9
|
|
image: quay.io/centos/centos:stream9
|
|
- name: centos-stream-10
|
|
image: quay.io/centos/centos:stream10
|
|
- name: fedora-42
|
|
image: fedora:42
|
|
- name: fedora-43
|
|
image: fedora:43
|
|
- name: rocky-8
|
|
image: rockylinux/rockylinux:8
|
|
- name: rocky-9
|
|
image: rockylinux/rockylinux:9
|
|
- name: rocky-10
|
|
image: rockylinux/rockylinux:10
|
|
- name: almalinux-8
|
|
image: almalinux:8
|
|
- name: almalinux-9
|
|
image: almalinux:9
|
|
- name: almalinux-10
|
|
image: almalinux:10
|
|
- name: archlinux
|
|
image: archlinux:latest
|
|
- name: opensuse-leap-16.0
|
|
image: opensuse/leap:16.0
|
|
- name: opensuse-tumbleweed
|
|
image: opensuse/tumbleweed
|
|
- name: oraclelinux-8
|
|
image: oraclelinux:8
|
|
- name: oraclelinux-9
|
|
image: oraclelinux:9
|
|
- name: oraclelinux-10
|
|
image: oraclelinux:10
|
|
- name: amazonlinux-2023
|
|
image: amazonlinux:2023
|
|
# Default TLS settings (tls-crypt-v2)
|
|
tls:
|
|
- name: tls-crypt-v2
|
|
sig: crypt-v2
|
|
key_file: tls-crypt-v2.key
|
|
# Additional TLS types tested on Ubuntu 24.04 only
|
|
include:
|
|
- os:
|
|
name: ubuntu-24.04-tls-crypt
|
|
image: ubuntu:24.04
|
|
tls:
|
|
name: tls-crypt
|
|
sig: crypt
|
|
key_file: tls-crypt.key
|
|
- os:
|
|
name: ubuntu-24.04-tls-auth
|
|
image: ubuntu:24.04
|
|
tls:
|
|
name: tls-auth
|
|
sig: auth
|
|
key_file: tls-auth.key
|
|
# Test firewalld support on Fedora
|
|
- os:
|
|
name: fedora-42-firewalld
|
|
image: fedora:42
|
|
enable_firewalld: true
|
|
tls:
|
|
name: tls-crypt-v2
|
|
sig: crypt-v2
|
|
key_file: tls-crypt-v2.key
|
|
# Test nftables support on Debian
|
|
- os:
|
|
name: debian-12-nftables
|
|
image: debian:12
|
|
enable_nftables: true
|
|
tls:
|
|
name: tls-crypt-v2
|
|
sig: crypt-v2
|
|
key_file: tls-crypt-v2.key
|
|
# Test IPv6 dual-stack support on Ubuntu
|
|
- os:
|
|
name: ubuntu-24.04-dual-stack
|
|
image: ubuntu:24.04
|
|
client_ipv6: true
|
|
tls:
|
|
name: tls-crypt-v2
|
|
sig: crypt-v2
|
|
key_file: tls-crypt-v2.key
|
|
|
|
name: ${{ matrix.os.name }}
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
|
|
|
- name: Build server image
|
|
run: |
|
|
docker build \
|
|
--build-arg BASE_IMAGE=${{ matrix.os.image }} \
|
|
--build-arg ENABLE_FIREWALLD=${{ matrix.os.enable_firewalld && 'y' || 'n' }} \
|
|
--build-arg ENABLE_NFTABLES=${{ matrix.os.enable_nftables && 'y' || 'n' }} \
|
|
-t openvpn-server \
|
|
-f test/Dockerfile.server .
|
|
|
|
- name: Build client image
|
|
run: docker build -t openvpn-client -f test/Dockerfile.client .
|
|
|
|
- name: Create Docker network
|
|
run: docker network create --subnet=172.28.0.0/24 vpn-test
|
|
|
|
- name: Create shared volume
|
|
run: docker volume create shared-config
|
|
|
|
- name: Start OpenVPN server
|
|
run: |
|
|
docker run -d \
|
|
--name openvpn-server \
|
|
--hostname openvpn-server \
|
|
--privileged \
|
|
--cgroupns=host \
|
|
--device=/dev/net/tun:/dev/net/tun \
|
|
--sysctl net.ipv4.ip_forward=1 \
|
|
--sysctl net.ipv6.conf.all.forwarding=1 \
|
|
--network vpn-test \
|
|
--ip 172.28.0.10 \
|
|
-v shared-config:/shared \
|
|
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
|
--tmpfs /run \
|
|
--tmpfs /run/lock \
|
|
--stop-signal SIGRTMIN+3 \
|
|
-e TLS_SIG=${{ matrix.tls.sig }} \
|
|
-e TLS_KEY_FILE=${{ matrix.tls.key_file }} \
|
|
-e CLIENT_IPV6=${{ matrix.os.client_ipv6 && 'y' || 'n' }} \
|
|
openvpn-server
|
|
|
|
- name: Wait for server installation and startup
|
|
run: |
|
|
echo "Waiting for OpenVPN server to install and client config to be ready..."
|
|
for i in {1..90}; do
|
|
# Get service status (properly handle non-zero exit codes)
|
|
# systemctl is-active returns exit code 3 for "inactive"/"failed", so capture output without checking exit code
|
|
SERVICE_STATUS="$(docker exec openvpn-server systemctl is-active openvpn-test.service 2>/dev/null)" || true
|
|
[ -z "$SERVICE_STATUS" ] && SERVICE_STATUS="unknown"
|
|
|
|
# Fail fast if service failed
|
|
if [ "$SERVICE_STATUS" = "failed" ]; then
|
|
echo "ERROR: openvpn-test.service failed during installation"
|
|
docker exec openvpn-server systemctl status openvpn-test.service 2>&1 || true
|
|
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
|
|
exit 1
|
|
fi
|
|
|
|
# Check if OpenVPN server is running and client config exists
|
|
# The service will be "activating" while waiting for client tests - that's expected
|
|
OPENVPN_RUNNING=false
|
|
CONFIG_EXISTS=false
|
|
|
|
if docker exec openvpn-server pgrep -f "openvpn.*server.conf" > /dev/null 2>&1; then
|
|
OPENVPN_RUNNING=true
|
|
fi
|
|
|
|
if docker exec openvpn-server test -f /shared/client.ovpn 2>/dev/null; then
|
|
CONFIG_EXISTS=true
|
|
fi
|
|
|
|
if [ "$OPENVPN_RUNNING" = true ] && [ "$CONFIG_EXISTS" = true ]; then
|
|
echo "OpenVPN server is running and client config is ready!"
|
|
break
|
|
fi
|
|
|
|
echo "Waiting... ($i/90) - Service: $SERVICE_STATUS, OpenVPN running: $OPENVPN_RUNNING, Config exists: $CONFIG_EXISTS"
|
|
sleep 5
|
|
done
|
|
|
|
# Final verification with retry (handles race condition during cert renewal restart)
|
|
OPENVPN_STARTED=false
|
|
for retry in {1..5}; do
|
|
if docker exec openvpn-server pgrep -f "openvpn.*server.conf" > /dev/null 2>&1; then
|
|
OPENVPN_STARTED=true
|
|
break
|
|
fi
|
|
echo "Waiting for OpenVPN process... (retry $retry/5)"
|
|
sleep 2
|
|
done
|
|
|
|
if [ "$OPENVPN_STARTED" = false ]; then
|
|
echo "ERROR: OpenVPN server failed to start"
|
|
docker exec openvpn-server systemctl status openvpn-server@server 2>&1 || true
|
|
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker exec openvpn-server test -f /shared/client.ovpn 2>/dev/null; then
|
|
echo "ERROR: Client config not generated"
|
|
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
|
|
exit 1
|
|
fi
|
|
|
|
echo "Server ready for client connection!"
|
|
|
|
- name: Verify client config was generated
|
|
run: |
|
|
docker run --rm -v shared-config:/shared alpine \
|
|
ls -la /shared/
|
|
docker run --rm -v shared-config:/shared alpine \
|
|
cat /shared/client.ovpn
|
|
|
|
- name: Start OpenVPN client and run tests
|
|
run: |
|
|
docker run \
|
|
--name openvpn-client \
|
|
--hostname openvpn-client \
|
|
--cap-add=NET_ADMIN \
|
|
--device=/dev/net/tun:/dev/net/tun \
|
|
--network vpn-test \
|
|
--ip 172.28.0.20 \
|
|
-v shared-config:/shared \
|
|
openvpn-client &
|
|
|
|
# Wait for tests to complete (look for success message)
|
|
# Extended timeout for revocation e2e tests
|
|
for i in {1..180}; do
|
|
if docker logs openvpn-client 2>&1 | grep -q "ALL TESTS PASSED"
|
|
then
|
|
echo "Tests passed!"
|
|
exit 0
|
|
fi
|
|
if docker logs openvpn-client 2>&1 | grep -q "FAIL:"; then
|
|
echo "Tests failed!"
|
|
docker logs openvpn-client
|
|
exit 1
|
|
fi
|
|
echo "Waiting for tests... ($i/180)"
|
|
sleep 2
|
|
done
|
|
|
|
echo "Timeout waiting for tests"
|
|
docker logs openvpn-client
|
|
exit 1
|
|
|
|
- name: Show server logs
|
|
if: always()
|
|
run: docker logs openvpn-server 2>&1 || true
|
|
|
|
- name: Show systemd journal logs
|
|
if: always()
|
|
run: |
|
|
echo "=== openvpn-test.service status ==="
|
|
docker exec openvpn-server systemctl status openvpn-test.service 2>&1 || true
|
|
echo ""
|
|
echo "=== openvpn-test.service journal ==="
|
|
docker exec openvpn-server journalctl -u openvpn-test.service --no-pager -n 100 2>&1 || true
|
|
echo ""
|
|
echo "=== openvpn-server@server.service journal ==="
|
|
docker exec openvpn-server journalctl -u openvpn-server@server.service --no-pager -n 50 2>&1 || true
|
|
|
|
- name: Show install script log
|
|
if: always()
|
|
run: |
|
|
docker cp openvpn-server:/root/openvpn-install.log /tmp/openvpn-install.log 2>/dev/null && \
|
|
cat /tmp/openvpn-install.log || echo "No install log found"
|
|
|
|
- name: Show client logs
|
|
if: always()
|
|
run: docker logs openvpn-client 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 volume rm shared-config 2>/dev/null || true
|