Add configurable VPN access policies (#1505)

## Summary

- add independent install options for internet routing, client-to-client
access, and explicit server-side networks
- enforce the selected policy across firewalld, nftables, and iptables,
including DCO traffic
- use destination-scoped NAT for home LAN access and preserve client
routes and DNS in split-tunnel mode
- document the new defaults and add focused Docker policy coverage

Defaults remain internet access enabled, client-to-client access
disabled, and server-side network access disabled.


Related: #1496 #443 #385 #624 #547 #1436 #1103 #1126 #575 #1434 #1213
#147
This commit is contained in:
Stanislas
2026-08-02 22:57:05 +02:00
committed by GitHub
parent 25476a7143
commit d2fc6d444b
11 changed files with 1498 additions and 476 deletions
+84 -5
View File
@@ -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,6 +132,42 @@ 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 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
key_file: tls-crypt-v2.key
name: ${{ matrix.os.name }} name: ${{ matrix.os.name }}
steps: steps:
@@ -154,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 \
@@ -178,8 +229,17 @@ jobs:
-e TLS_KEY_FILE=${{ matrix.tls.key_file }} \ -e TLS_KEY_FILE=${{ matrix.tls.key_file }} \
-e CLIENT_IPV6=${{ matrix.os.client_ipv6 && 'y' || 'n' }} \ -e CLIENT_IPV6=${{ matrix.os.client_ipv6 && 'y' || 'n' }} \
-e AUTH_MODE=${{ matrix.os.auth_mode || 'pki' }} \ -e AUTH_MODE=${{ matrix.os.auth_mode || 'pki' }} \
-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 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..."
@@ -252,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 \
@@ -262,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)
@@ -309,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
+31 -66
View File
@@ -86,7 +86,7 @@ down /usr/share/openvpn/contrib/pull-resolv-conf/client.down
**Q:** What sysctl and firewall changes are made by the script? **Q:** What sysctl and firewall changes are made by the script?
**A:** If firewalld is active, the script uses `firewall-cmd --permanent` to configure port, masquerade, and rich rules. Otherwise, iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`, managed by `/etc/systemd/system/iptables-openvpn.service`. **A:** If firewalld is active, the script uses `firewall-cmd --permanent` to configure scoped rich rules. If nftables is active, it creates `/etc/nftables/openvpn.nft`. Otherwise, iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`, managed by `/etc/systemd/system/iptables-openvpn.service`.
Sysctl options are at `/etc/sysctl.d/99-openvpn.conf` Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`
@@ -94,7 +94,13 @@ Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`
**Q:** How can I access other clients connected to the same OpenVPN server? **Q:** How can I access other clients connected to the same OpenVPN server?
**A:** Add `client-to-client` to your `server.conf` **A:** Enable client-to-client access during installation:
```bash
./openvpn-install.sh install --client-to-client
```
It is disabled by default. The installer configures both OpenVPN and the firewall so the policy also applies when Data Channel Offload (DCO) is active.
--- ---
@@ -110,36 +116,19 @@ Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`
**Q:** How can I access computers on the OpenVPN server's LAN? **Q:** How can I access computers on the OpenVPN server's LAN?
**A:** Two steps are required: **A:** Specify the LAN during installation:
1. **Push a route to clients** - Add the LAN subnet to `/etc/openvpn/server/server.conf`:
```
push "route 192.168.1.0 255.255.255.0"
```
Replace `192.168.1.0/24` with your actual LAN subnet.
2. **Enable routing back to VPN clients** - Choose one of these options:
- **Option A: Add a static route on your router** (recommended when you can configure your router)
On your LAN router, add a route for the VPN subnet (default `10.8.0.0/24`) pointing to the OpenVPN server's LAN IP. This allows LAN devices to reply to VPN clients without NAT.
- **Option B: Masquerade VPN traffic to LAN**
If you can't modify your router, add a masquerade rule so VPN traffic appears to come from the server:
```bash ```bash
# iptables ./openvpn-install.sh install --local-network 192.168.1.0/24
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -d 192.168.1.0/24 -j MASQUERADE
# or nftables
nft add rule ip nat postrouting ip saddr 10.8.0.0/24 ip daddr 192.168.1.0/24 masquerade
``` ```
Make this persistent by adding it to your firewall scripts. Repeat `--local-network` to expose more than one server-side network. Using `--local-network` alone keeps the default full-tunnel internet routing enabled. Add `--no-route-internet` if only the selected server-side networks should use the VPN.
Restart OpenVPN after making changes: `systemctl restart openvpn-server@server` This feature is mainly for OpenVPN servers installed at home. During interactive installation, enabling LAN access shows directly connected private networks as one editable, comma-separated list. Review the list because it can include cloud VPC or container networks. LAN access remains disabled by default, and non-interactive installation never detects networks automatically.
The installer pushes the route, permits only the selected destination, and adds destination-scoped NAT. LAN computers therefore see the connection as coming from the OpenVPN server and do not need a return route to the VPN subnet.
Do not use a LAN CIDR that overlaps the VPN subnet. An overlap with the client's current LAN can also prevent the route from working.
--- ---
@@ -180,56 +169,32 @@ To add password-protected clients:
--- ---
**Q:** For my clients - I want to set my internal network to pass through the VPN and the rest to go through my internet? **Q:** For my clients, how can I route only an internal network through the VPN?
**A:** You would need to edit the `.ovpn` file. You can edit the template out of which those files are created by editing `/etc/openvpn/server/client-template.txt` file and adding **A:** Disable internet routing and specify the server-side network during installation:
```sh ```bash
route-nopull ./openvpn-install.sh install \
route 10.0.0.0 255.0.0.0 --no-route-internet \
--local-network 10.0.0.0/8
``` ```
So for example - here it would route all traffic of `10.0.0.0/8` to the VPN. And the rest through the internet. The client's normal internet route and DNS remain unchanged.
--- ---
**Q:** How do I configure split-tunnel mode on the server (route only specific networks through VPN for all clients)? **Q:** How do I configure split-tunnel mode on the server?
**A:** By default, the script configures full-tunnel mode where all client traffic goes through the VPN. To configure split-tunnel (only specific networks routed through VPN), edit `/etc/openvpn/server/server.conf`: **A:** Use `--no-route-internet`. Add each server-side network that should use the tunnel:
1. Remove or comment out the redirect-gateway line: ```bash
./openvpn-install.sh install \
``` --no-route-internet \
#push "redirect-gateway def1 bypass-dhcp" --local-network 10.0.0.0/8 \
--local-network 192.168.1.0/24
``` ```
2. Add routes for the networks you want to tunnel: The installer does not push internet routes, leak-blocking directives, or VPN DNS in this mode.
```
push "route 10.0.0.0 255.0.0.0"
push "route 192.168.1.0 255.255.255.0"
```
3. Optionally remove DNS push directives if you don't want VPN DNS:
```
#push "dhcp-option DNS 1.1.1.1"
```
4. For IPv6, remove or comment out:
```
#push "route-ipv6 2000::/3"
#push "redirect-gateway ipv6"
```
Or add specific IPv6 routes:
```
push "route-ipv6 2001:db8::/32"
```
5. Restart OpenVPN: `systemctl restart openvpn-server@server`
--- ---
+23 -3
View File
@@ -12,19 +12,23 @@ This script is meant to be run on your own server, whether it's a VPS or a dedic
Once set up, you will be able to generate client configuration files for every device you want to connect. Once set up, you will be able to generate client configuration files for every device you want to connect.
Each client will be able to route its internet traffic through the server, fully encrypted. Internet routing, access between VPN clients, and access to selected server-side networks can be configured independently. By default, internet routing is enabled and the other paths are disabled.
```mermaid ```mermaid
graph LR flowchart LR
A[Phone] -->|Encrypted| VPN A[Phone] -->|Encrypted| VPN
B[Laptop] -->|Encrypted| VPN B[Laptop] -->|Encrypted| VPN
C[Computer] -->|Encrypted| VPN C[Computer] -->|Encrypted| VPN
VPN[OpenVPN Server] VPN[OpenVPN Server]
VPN --> I[Internet] VPN -->|Internet routing<br/>Default: enabled| I[Internet]
VPN -.->|Explicit CIDRs only<br/>Default: disabled| LAN[Home LAN or cloud VPC]
VPN -.->|Client-to-client access<br/>Default: disabled| PEERS[Other VPN clients]
``` ```
The solid destination path is enabled by default. Dashed destination paths are opt-in.
## Why OpenVPN? ## Why OpenVPN?
OpenVPN was the de facto standard for open-source VPNs when this script was created. WireGuard came later and is simpler and faster for most use cases. Check out [wireguard-install](https://github.com/angristan/wireguard-install). OpenVPN was the de facto standard for open-source VPNs when this script was created. WireGuard came later and is simpler and faster for most use cases. Check out [wireguard-install](https://github.com/angristan/wireguard-install).
@@ -44,6 +48,7 @@ That said, OpenVPN still makes sense when you need:
- Immediate client disconnect on certificate revocation (via management interface) - Immediate client disconnect on certificate revocation (via management interface)
- Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases - Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases
- Firewall rules and forwarding managed seamlessly (native firewalld and nftables support, iptables fallback) - Firewall rules and forwarding managed seamlessly (native firewalld and nftables support, iptables fallback)
- Independent access policies for internet routing, communication between VPN clients, and selected server-side networks
- Configurable VPN subnets (IPv4: default `10.8.0.0/24`, IPv6: default `fd42:42:42:42::/112`) - Configurable VPN subnets (IPv4: default `10.8.0.0/24`, IPv6: default `fd42:42:42:42::/112`)
- Configurable tunnel MTU (default: `1500`) - Configurable tunnel MTU (default: `1500`)
- If needed, the script can cleanly remove OpenVPN, including configuration and firewall rules - If needed, the script can cleanly remove OpenVPN, including configuration and firewall rules
@@ -263,6 +268,12 @@ The `install` command supports many options for customization:
# Custom VPN subnet # Custom VPN subnet
./openvpn-install.sh install --subnet-ipv4 10.9.0.0 ./openvpn-install.sh install --subnet-ipv4 10.9.0.0
# Home VPN: access the home LAN without routing internet through the VPN
./openvpn-install.sh install --no-route-internet --local-network 192.168.1.0/24
# Allow VPN clients to access each other
./openvpn-install.sh install --client-to-client
# Enable dual-stack (IPv4 + IPv6) for clients # Enable dual-stack (IPv4 + IPv6) for clients
./openvpn-install.sh install --client-ipv4 --client-ipv6 ./openvpn-install.sh install --client-ipv4 --client-ipv6
@@ -299,13 +310,22 @@ The `install` command supports many options for customization:
- `--no-client-ipv6` - Disable IPv6 for VPN clients - `--no-client-ipv6` - Disable IPv6 for VPN clients
- `--subnet-ipv4 <x.x.x.0>` - IPv4 VPN subnet (default: `10.8.0.0`) - `--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::`) - `--subnet-ipv6 <prefix>` - IPv6 VPN subnet (default: `fd42:42:42:42::`)
- `--route-internet` / `--no-route-internet` - Enable or disable routing client internet traffic through the VPN (default: enabled)
- `--client-to-client` / `--no-client-to-client` - Allow or isolate communication between VPN clients (default: isolated)
- `--local-network <CIDR>` - Allow access to a server-side network and add destination-scoped NAT. Repeat for multiple networks (default: none)
- `--port <num>` - OpenVPN port (default: `1194`) - `--port <num>` - OpenVPN port (default: `1194`)
- `--port-random` - Use random port (49152-65535) - `--port-random` - Use random port (49152-65535)
- `--protocol <udp|tcp>` - Protocol (default: `udp`) - `--protocol <udp|tcp>` - Protocol (default: `udp`)
- `--mtu <size>` - Tunnel MTU (default: `1500`) - `--mtu <size>` - Tunnel MTU (default: `1500`)
Server-side network access is mainly intended for VPN servers installed at home. In interactive mode, the installer suggests directly connected private IPv4 and IPv6 networks as one editable, comma-separated list after you enable LAN access. Review and confirm the list because it can include cloud VPC or container networks. No network is exposed unless you opt in. Non-interactive installs require an explicit `--local-network` for each network. LAN devices see connections as coming from the VPN server because destination-scoped NAT is enabled.
Local networks must use network-aligned IPv4 or IPv6 CIDRs and must not overlap the VPN pools. Client-side and server-side LANs that overlap can still cause routing conflicts.
**DNS Options:** **DNS Options:**
DNS settings are pushed only when internet routing through the VPN is enabled.
- `--dns <provider>` - DNS provider (default: `cloudflare`). Options: `system`, `unbound`, `cloudflare`, `quad9`, `quad9-uncensored`, `fdn`, `dnswatch`, `opendns`, `google`, `yandex`, `adguard`, `nextdns`, `custom` - `--dns <provider>` - DNS provider (default: `cloudflare`). Options: `system`, `unbound`, `cloudflare`, `quad9`, `quad9-uncensored`, `fdn`, `dnswatch`, `opendns`, `google`, `yandex`, `adguard`, `nextdns`, `custom`
- `--dns-primary <ip>` - Custom primary DNS (requires `--dns custom`) - `--dns-primary <ip>` - Custom primary DNS (requires `--dns custom`)
- `--dns-secondary <ip>` - Custom secondary DNS (requires `--dns custom`) - `--dns-secondary <ip>` - Custom secondary DNS (requires `--dns custom`)
+4
View File
@@ -14,6 +14,10 @@ services:
cgroupns: host cgroupns: host
devices: devices:
- /dev/net/tun:/dev/net/tun - /dev/net/tun:/dev/net/tun
environment:
ROUTE_INTERNET: ${ROUTE_INTERNET:-y}
CLIENT_TO_CLIENT: ${CLIENT_TO_CLIENT:-n}
LOCAL_NETWORKS: ${LOCAL_NETWORKS:-}
sysctls: sysctls:
- net.ipv4.ip_forward=1 - net.ipv4.ip_forward=1
volumes: volumes:
+684 -94
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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
+3 -2
View File
@@ -68,7 +68,8 @@ RUN chmod +x /opt/openvpn-install.sh
# Copy test scripts # Copy test scripts
COPY test/server-entrypoint.sh /entrypoint.sh COPY test/server-entrypoint.sh /entrypoint.sh
COPY test/validate-output.sh /opt/test/validate-output.sh COPY test/validate-output.sh /opt/test/validate-output.sh
RUN chmod +x /entrypoint.sh /opt/test/validate-output.sh COPY test/local-network-detection.sh /opt/test/local-network-detection.sh
RUN chmod +x /entrypoint.sh /opt/test/validate-output.sh /opt/test/local-network-detection.sh
# Create systemd service for the test script # Create systemd service for the test script
# PassEnvironment passes Docker env vars (-e) from PID 1 to the service # PassEnvironment passes Docker env vars (-e) from PID 1 to the service
@@ -80,7 +81,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 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' \
+101 -8
View File
@@ -131,10 +131,13 @@ wait_for_revoked_reconnect_rejected() {
test_dns_resolution() { test_dns_resolution() {
local label="$1" local label="$1"
local test_name="github.com"
local success=false local success=false
# This verifies recursive DNS connectivity. Use an unsigned zone so the test
# does not depend on DNSSEC key retrieval over GitHub runner networks.
echo "$label: Testing DNS resolution via Unbound ($VPN_GATEWAY)..." echo "$label: Testing DNS resolution via Unbound ($VPN_GATEWAY)..."
for i in $(seq 1 10); do for i in $(seq 1 10); do
DIG_OUTPUT=$(dig @"$VPN_GATEWAY" example.com +short +time=5 2>&1) DIG_OUTPUT=$(dig @"$VPN_GATEWAY" "$test_name" +short +time=5 2>&1)
if [ -n "$DIG_OUTPUT" ] && ! echo "$DIG_OUTPUT" | grep -qi "timed out\|SERVFAIL\|connection refused"; then if [ -n "$DIG_OUTPUT" ] && ! echo "$DIG_OUTPUT" | grep -qi "timed out\|SERVFAIL\|connection refused"; then
success=true success=true
break break
@@ -147,7 +150,7 @@ test_dns_resolution() {
echo "PASS: DNS resolution through Unbound works" echo "PASS: DNS resolution through Unbound works"
else else
echo "FAIL: DNS resolution through Unbound failed after 10 attempts" echo "FAIL: DNS resolution through Unbound failed after 10 attempts"
dig @"$VPN_GATEWAY" example.com +time=5 || true dig @"$VPN_GATEWAY" "$test_name" +time=5 || true
exit 1 exit 1
fi fi
} }
@@ -220,13 +223,42 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
fi fi
fi fi
# Test 2: Ping VPN gateway (IPv4) # Test 2: Verify pushed routes match the access policy.
echo "Test 2: Pinging VPN gateway (IPv4) ($VPN_GATEWAY)..." echo "Test 2: Checking access policy routes..."
if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
if ip route show | grep -q '^0.0.0.0/1 .* tun0' && ip route show | grep -q '^128.0.0.0/1 .* tun0'; then
echo "PASS: Internet routes use the VPN"
else
echo "FAIL: VPN internet routes are missing"
ip route show
exit 1
fi
else
if ip route show | grep -qE '^(0\.0\.0\.0/1|128\.0\.0\.0/1) .* tun0'; then
echo "FAIL: Internet route uses the VPN in split-tunnel mode"
ip route show
exit 1
fi
echo "PASS: Internet routes remain outside the VPN"
fi
if [ -n "${LOCAL_NETWORKS:-}" ]; then
while IFS= read -r local_network; do
if [[ $local_network == *.* ]] && ! ip route show "$local_network" | grep -q 'tun0'; then
echo "FAIL: Local network route is missing for $local_network"
ip route show
exit 1
fi
done < <(tr ',' '\n' <<<"$LOCAL_NETWORKS")
fi
# Test 3: Ping VPN gateway (IPv4)
echo "Test 3: Pinging VPN gateway (IPv4) ($VPN_GATEWAY)..."
wait_for_gateway_ping "VPN gateway (IPv4)" wait_for_gateway_ping "VPN gateway (IPv4)"
# Test 2b: Ping VPN gateway (IPv6, if enabled) # Test 3b: Ping VPN gateway (IPv6, if enabled)
if [ "${CLIENT_IPV6:-n}" = "y" ]; then if [ "${CLIENT_IPV6:-n}" = "y" ]; then
echo "Test 2b: Pinging VPN gateway (IPv6) ($VPN_GATEWAY_IPV6)..." echo "Test 3b: Pinging VPN gateway (IPv6) ($VPN_GATEWAY_IPV6)..."
if ping6 -c 5 "$VPN_GATEWAY_IPV6"; then if ping6 -c 5 "$VPN_GATEWAY_IPV6"; then
echo "PASS: Can ping VPN gateway (IPv6)" echo "PASS: Can ping VPN gateway (IPv6)"
else else
@@ -235,8 +267,67 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
fi fi
fi fi
# Test 3: DNS resolution through Unbound # Packet-level access policy tests use a second VPN client and a LAN-only host.
test_dns_resolution "Test 3" 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 5"
else
echo "Test 5: SKIP: VPN DNS is disabled in split-tunnel mode"
fi
echo "" echo ""
echo "=== Initial connectivity tests PASSED ===" echo "=== Initial connectivity tests PASSED ==="
@@ -269,7 +360,9 @@ sleep 5
echo "Test: Pinging VPN gateway after renewal ($VPN_GATEWAY)..." echo "Test: Pinging VPN gateway after renewal ($VPN_GATEWAY)..."
wait_for_gateway_ping "VPN gateway after renewal" wait_for_gateway_ping "VPN gateway after renewal"
if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
test_dns_resolution "Test: Post-renewal DNS" test_dns_resolution "Test: Post-renewal DNS"
fi
echo "" echo ""
echo "=== Post-renewal connectivity tests PASSED ===" echo "=== Post-renewal connectivity tests PASSED ==="
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# shellcheck disable=SC1091,SC2034
# SC1091: The installer path is provided by the test environment.
# SC2034: VPN subnet globals are consumed by sourced installer functions.
set -euo pipefail
INSTALLER=${1:-/opt/openvpn-install.sh}
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
cat >"$TEMP_DIR/ip" <<'EOF'
#!/bin/bash
case "$*" in
"-4 -o route show type unicast")
cat <<'ROUTES'
default via 167.172.176.1 dev public0
10.8.0.0/24 dev tun-test proto kernel scope link
10.19.0.0/16 dev public0 proto kernel scope link src 10.19.0.5
10.135.0.0/16 dev eth1 proto kernel scope link src 10.135.0.2
10.135.0.0/16 dev eth1 proto kernel scope link src 10.135.0.2
10.200.0.0/16 via 10.135.0.1 dev eth1
100.64.0.0/10 dev tailscale0 proto kernel scope link
169.254.0.0/16 dev eth1 proto kernel scope link
172.20.0.0/16 dev docker0 proto kernel scope link
192.168.50.0/24 dev lan0 proto kernel scope link
203.0.113.0/24 dev public0 proto kernel scope link
ROUTES
;;
"-6 -o route show type unicast")
cat <<'ROUTES'
default via fe80::1 dev public0
fc00:1::/64 via fd12:3456::1 dev lan0
fd12:3456::/64 dev lan0 proto kernel metric 256
fe80::/64 dev public0 proto kernel metric 256
2001:db8::/64 dev public0 proto kernel metric 256
ROUTES
;;
"-4 -o address show dev public0 scope global")
cat <<'ADDRESSES'
2: public0 inet 203.0.113.10/24 brd 203.0.113.255 scope global public0
2: public0 inet 10.19.0.5/16 brd 10.19.255.255 scope global public0
ADDRESSES
;;
"-4 -o address show dev eth1 scope global")
echo "3: eth1 inet 10.135.0.2/16 brd 10.135.255.255 scope global eth1"
;;
esac
EOF
chmod +x "$TEMP_DIR/ip"
export FORCE_COLOR=0 LOG_FILE="" NON_INTERACTIVE_INSTALL=n OUTPUT_FORMAT=table
# shellcheck source=../openvpn-install.sh
source "$INSTALLER"
PATH="$TEMP_DIR:$PATH"
VPN_SUBNET_IPV4=10.8.0.0
VPN_SUBNET_IPV6=fd42:42:42:42::
assert_equal() {
local expected="$1" actual="$2" description="$3"
if [[ $actual != "$expected" ]]; then
echo "FAIL: $description" >&2
echo "Expected: $expected" >&2
echo "Actual: $actual" >&2
exit 1
fi
}
assert_equal \
"10.135.0.0/16,172.20.0.0/16,192.168.50.0/24,fd12:3456::/64" \
"$(detect_private_local_networks y y)" \
"detects unique, directly connected RFC1918 and ULA networks"
assert_equal \
"10.135.0.0/16,172.20.0.0/16,192.168.50.0/24" \
"$(detect_private_local_networks y n)" \
"honors IPv4-only client configuration"
assert_equal \
"fd12:3456::/64" \
"$(detect_private_local_networks n y)" \
"honors IPv6-only client configuration"
assert_equal "" "$(detect_private_local_networks n n)" "returns an empty list when both families are disabled"
if is_private_ipv4_network 10.0.0.0/7; then
echo "FAIL: IPv4 network broader than RFC1918 space was accepted" >&2
exit 1
fi
if is_private_ipv6_network fc00::/6; then
echo "FAIL: IPv6 network broader than ULA space was accepted" >&2
exit 1
fi
echo "PASS: Local network candidate detection"
+35
View File
@@ -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
+191 -49
View File
@@ -3,6 +3,8 @@ set -e
echo "=== OpenVPN Server Container ===" echo "=== OpenVPN Server Container ==="
/opt/test/local-network-detection.sh /opt/openvpn-install.sh
# Create TUN device if it doesn't exist # Create TUN device if it doesn't exist
if [ ! -c /dev/net/tun ]; then if [ ! -c /dev/net/tun ]; then
mkdir -p /dev/net mkdir -p /dev/net
@@ -57,10 +59,28 @@ if grep -q $'\033' "$NO_COLOR_OUTPUT"; then
fi fi
echo "PASS: --no-color help output has no ANSI escape sequences" echo "PASS: --no-color help output has no ANSI escape sequences"
INVALID_NETWORK_OUTPUT="/tmp/invalid-local-network.log"
if /opt/openvpn-install.sh install --local-network 192.168.1.1/24 >"$INVALID_NETWORK_OUTPUT" 2>&1; then
echo "FAIL: Host-address CIDR was accepted as a local network"
exit 1
elif grep -q "Invalid local network" "$INVALID_NETWORK_OUTPUT"; then
echo "PASS: Invalid local network CIDR is rejected"
else
echo "FAIL: Expected local network validation error"
cat "$INVALID_NETWORK_OUTPUT"
exit 1
fi
# Calculate VPN gateway from subnet (first usable IP) # Calculate VPN gateway from subnet (first usable IP)
VPN_GATEWAY="${VPN_SUBNET_IPV4%.*}.1" VPN_GATEWAY="${VPN_SUBNET_IPV4%.*}.1"
export VPN_GATEWAY export VPN_GATEWAY
# Access policy configuration
ROUTE_INTERNET="${ROUTE_INTERNET:-y}"
CLIENT_TO_CLIENT="${CLIENT_TO_CLIENT:-n}"
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
# VPN_SUBNET_IPV6: IPv6 subnet (ULA prefix, e.g., fd42:42:42:42::) # VPN_SUBNET_IPV6: IPv6 subnet (ULA prefix, e.g., fd42:42:42:42::)
@@ -95,6 +115,18 @@ INSTALL_CMD+=(--subnet-ipv4 "$VPN_SUBNET_IPV4")
INSTALL_CMD+=(--mtu 1400) INSTALL_CMD+=(--mtu 1400)
INSTALL_CMD+=(--client testclient) INSTALL_CMD+=(--client testclient)
if [ "$ROUTE_INTERNET" = "n" ]; then
INSTALL_CMD+=(--no-route-internet)
fi
if [ "$CLIENT_TO_CLIENT" = "y" ]; then
INSTALL_CMD+=(--client-to-client)
fi
if [ -n "$LOCAL_NETWORKS" ]; then
while IFS= read -r local_network; do
INSTALL_CMD+=(--local-network "$local_network")
done < <(tr ',' '\n' <<<"$LOCAL_NETWORKS")
fi
# Add IPv6 client support if enabled # Add IPv6 client support if enabled
if [ "$CLIENT_IPV6" = "y" ]; then if [ "$CLIENT_IPV6" = "y" ]; then
INSTALL_CMD+=(--client-ipv6) INSTALL_CMD+=(--client-ipv6)
@@ -197,6 +229,65 @@ fi
echo "All required files present" echo "All required files present"
# =====================================================
# Verify access policy configuration
# =====================================================
echo ""
echo "=== Verifying Access Policy Configuration ==="
if [ "$ROUTE_INTERNET" = "y" ]; then
if grep -q '^push "redirect-gateway def1 bypass-dhcp"' /etc/openvpn/server/server.conf; then
echo "PASS: Internet default route is pushed"
else
echo "FAIL: Internet default route is missing"
exit 1
fi
else
if grep -q 'redirect-gateway\|block-ipv6' /etc/openvpn/server/server.conf; then
echo "FAIL: Internet or leak-blocking routes exist in split-tunnel mode"
exit 1
fi
echo "PASS: Client internet routes remain outside the VPN"
fi
if [ "$CLIENT_TO_CLIENT" = "y" ]; then
grep -q '^client-to-client$' /etc/openvpn/server/server.conf || {
echo "FAIL: client-to-client directive is missing"
exit 1
}
else
if grep -q '^client-to-client$' /etc/openvpn/server/server.conf; then
echo "FAIL: client-to-client is enabled by default"
exit 1
fi
fi
if [ -n "$LOCAL_NETWORKS" ]; then
while IFS= read -r local_network; do
if [[ $local_network == *.* ]]; then
local_address="${local_network%/*}"
grep -q "^push \"route $local_address " /etc/openvpn/server/server.conf || {
echo "FAIL: Local IPv4 route for $local_network is missing"
exit 1
}
else
grep -q "^push \"route-ipv6 $local_network\"" /etc/openvpn/server/server.conf || {
echo "FAIL: Local IPv6 route for $local_network is missing"
exit 1
}
fi
done < <(tr ',' '\n' <<<"$LOCAL_NETWORKS")
fi
for setting in "ROUTE_INTERNET=$ROUTE_INTERNET" "CLIENT_TO_CLIENT=$CLIENT_TO_CLIENT" "LOCAL_NETWORKS=$LOCAL_NETWORKS"; do
grep -Fxq "$setting" /etc/openvpn/server/openvpn-install.conf || {
echo "FAIL: Policy manifest is missing $setting"
exit 1
}
done
echo "PASS: Access policy configuration is correct"
# ===================================================== # =====================================================
# Verify management interface configuration # Verify management interface configuration
# ===================================================== # =====================================================
@@ -253,6 +344,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
@@ -264,6 +366,9 @@ echo "Client config copied to /shared/client.ovpn"
echo "VPN_GATEWAY=$VPN_GATEWAY" echo "VPN_GATEWAY=$VPN_GATEWAY"
echo "CLIENT_IPV6=$CLIENT_IPV6" echo "CLIENT_IPV6=$CLIENT_IPV6"
echo "AUTH_MODE=$AUTH_MODE" echo "AUTH_MODE=$AUTH_MODE"
echo "ROUTE_INTERNET=$ROUTE_INTERNET"
echo "CLIENT_TO_CLIENT=$CLIENT_TO_CLIENT"
echo "LOCAL_NETWORKS=$LOCAL_NETWORKS"
if [ "$CLIENT_IPV6" = "y" ]; then if [ "$CLIENT_IPV6" = "y" ]; then
echo "VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6" echo "VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6"
echo "VPN_GATEWAY_IPV6=$VPN_GATEWAY_IPV6" echo "VPN_GATEWAY_IPV6=$VPN_GATEWAY_IPV6"
@@ -599,6 +704,7 @@ echo "Post-renewal client tests passed"
# ===================================================== # =====================================================
# Verify Unbound DNS resolver (started by systemd via install script) # Verify Unbound DNS resolver (started by systemd via install script)
# ===================================================== # =====================================================
if [ "$ROUTE_INTERNET" = "y" ]; then
echo "=== Verifying Unbound DNS Resolver ===" echo "=== Verifying Unbound DNS Resolver ==="
if [ -f /etc/unbound/unbound.conf ]; then if [ -f /etc/unbound/unbound.conf ]; then
@@ -657,6 +763,13 @@ fi
echo "=== Unbound Installation Verified ===" echo "=== Unbound Installation Verified ==="
echo "" echo ""
else
if grep -q '^push "dhcp-option DNS ' /etc/openvpn/server/server.conf; then
echo "FAIL: DNS is pushed while internet routing is disabled"
exit 1
fi
echo "PASS: VPN DNS setup is skipped in split-tunnel mode"
fi
# Verify OpenVPN server (started by systemd via install script) # Verify OpenVPN server (started by systemd via install script)
echo "Verifying OpenVPN server..." echo "Verifying OpenVPN server..."
@@ -664,22 +777,44 @@ echo "Verifying OpenVPN server..."
# Verify firewall rules exist # Verify firewall rules exist
echo "Verifying firewall rules..." echo "Verifying firewall rules..."
if systemctl is-active --quiet firewalld; then if systemctl is-active --quiet firewalld; then
# firewalld is active - verify masquerade is enabled echo "firewalld detected, checking scoped policy rules..."
echo "firewalld detected, checking masquerade..." if ! firewall-cmd --get-policies | grep -qw openvpn-egress; then
for _ in $(seq 1 10); do echo "FAIL: firewalld OpenVPN policy is missing"
if firewall-cmd --query-masquerade 2>/dev/null; then exit 1
echo "PASS: firewalld masquerade is enabled" fi
break if ! firewall-cmd --zone=openvpn-install --query-source="$VPN_SUBNET_IPV4/24"; then
fi echo "FAIL: firewalld OpenVPN source zone is missing"
sleep 1 exit 1
done fi
if ! firewall-cmd --query-masquerade 2>/dev/null; then if [ "$(firewall-cmd --permanent --policy=openvpn-egress --get-target)" != "DROP" ]; then
echo "FAIL: firewalld masquerade is not enabled" echo "FAIL: firewalld OpenVPN policy does not default to DROP"
echo "Current firewalld config:" exit 1
firewall-cmd --list-all 2>&1 || true fi
FIREWALLD_POLICY_RULES=$(firewall-cmd --policy=openvpn-egress --list-rich-rules)
if [ "$ROUTE_INTERNET" = "y" ]; then
if grep -q 'family="ipv4" masquerade' <<<"$FIREWALLD_POLICY_RULES"; then
echo "PASS: firewalld has policy-scoped internet NAT"
else
echo "FAIL: firewalld policy-scoped internet NAT is missing"
printf '%s\n' "$FIREWALLD_POLICY_RULES"
exit 1
fi
if grep -q 'destination address="10.0.0.0/8" reject' <<<"$FIREWALLD_POLICY_RULES"; then
echo "PASS: firewalld private-network isolation is configured"
else
echo "FAIL: firewalld private-network isolation is missing"
printf '%s\n' "$FIREWALLD_POLICY_RULES"
exit 1
fi
fi
if [ "$CLIENT_TO_CLIENT" = "y" ] && ! firewall-cmd --zone=openvpn-install --query-forward; then
echo "FAIL: firewalld client-to-client forwarding is missing"
exit 1
fi
if firewall-cmd --query-masquerade 2>/dev/null; then
echo "FAIL: firewalld zone-wide masquerade should not be enabled"
exit 1 exit 1
fi fi
# Verify port is open
if firewall-cmd --list-ports | grep -q "1194/udp"; then if firewall-cmd --list-ports | grep -q "1194/udp"; then
echo "PASS: OpenVPN port is open in firewalld" echo "PASS: OpenVPN port is open in firewalld"
else else
@@ -687,15 +822,6 @@ if systemctl is-active --quiet firewalld; then
firewall-cmd --list-ports firewall-cmd --list-ports
exit 1 exit 1
fi fi
# Verify VPN subnet rich rule exists (source-based rules work reliably across firewalld backends)
if firewall-cmd --list-rich-rules | grep -q "source address=\"$VPN_SUBNET_IPV4/24\""; then
echo "PASS: VPN subnet rich rule is configured"
else
echo "FAIL: VPN subnet rich rule not found in firewalld"
echo "Current rich rules:"
firewall-cmd --list-rich-rules
exit 1
fi
elif systemctl is-active --quiet nftables; then elif systemctl is-active --quiet nftables; then
# nftables mode - verify OpenVPN tables exist # nftables mode - verify OpenVPN tables exist
echo "nftables detected, checking OpenVPN tables..." echo "nftables detected, checking OpenVPN tables..."
@@ -712,20 +838,25 @@ elif systemctl is-active --quiet nftables; then
nft list ruleset 2>&1 || true nft list ruleset 2>&1 || true
exit 1 exit 1
fi fi
# Verify NAT table exists if [ "$ROUTE_INTERNET" = "y" ] || [ -n "$LOCAL_NETWORKS" ]; then
if nft list table ip openvpn-nat >/dev/null 2>&1; then if nft list table ip openvpn-nat >/dev/null 2>&1 && nft list table ip openvpn-nat | grep -q "masquerade"; then
echo "PASS: nftables 'ip openvpn-nat' table exists" echo "PASS: nftables scoped NAT is configured"
else else
echo "FAIL: nftables 'ip openvpn-nat' table not found" echo "FAIL: nftables scoped NAT is missing"
nft list ruleset 2>&1 || true nft list ruleset 2>&1 || true
exit 1 exit 1
fi fi
# Verify masquerade rule exists fi
if nft list table ip openvpn-nat | grep -q "masquerade"; then if [ "$ROUTE_INTERNET" = "y" ]; then
echo "PASS: nftables masquerade rule exists" if nft list table inet openvpn | grep -q "ip daddr 10.0.0.0/8 drop"; then
echo "PASS: nftables private-network isolation is configured"
else else
echo "FAIL: nftables masquerade rule not found" echo "FAIL: nftables private-network isolation is missing"
nft list table ip openvpn-nat 2>&1 || true nft list table inet openvpn
exit 1
fi
elif ! nft list table inet openvpn | grep -q "ip saddr $VPN_SUBNET_IPV4/24 drop"; then
echo "FAIL: nftables split-tunnel default drop is missing"
exit 1 exit 1
fi fi
# Verify include in nftables.conf # Verify include in nftables.conf
@@ -737,20 +868,32 @@ elif systemctl is-active --quiet nftables; then
exit 1 exit 1
fi fi
else else
# iptables mode - verify NAT rules echo "iptables mode, checking policy rules..."
echo "iptables mode, checking NAT rules..." if [ "$ROUTE_INTERNET" = "y" ] || [ -n "$LOCAL_NETWORKS" ]; then
for _ in $(seq 1 10); do for _ in $(seq 1 10); do
if iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4" && break
echo "PASS: NAT POSTROUTING rule for $VPN_SUBNET_IPV4/24 exists"
break
fi
sleep 1 sleep 1
done done
if ! iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then if ! iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then
echo "FAIL: NAT POSTROUTING rule for $VPN_SUBNET_IPV4/24 not found" echo "FAIL: Expected scoped NAT rule was not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true exit 1
fi
fi
if [ "$ROUTE_INTERNET" = "y" ]; then
if iptables -S OPENVPN_INSTALL_FORWARD | grep -q -- "-d 10.0.0.0/8 -j REJECT"; then
echo "PASS: iptables private-network isolation is configured"
else
echo "FAIL: iptables private-network isolation is missing"
iptables -S OPENVPN_INSTALL_FORWARD
exit 1
fi
elif ! iptables -S OPENVPN_INSTALL_FORWARD | grep -q -- "-A OPENVPN_INSTALL_FORWARD -j REJECT"; then
echo "FAIL: iptables split-tunnel default reject is missing"
exit 1
fi
if [ "$CLIENT_TO_CLIENT" = "y" ] && ! iptables -S OPENVPN_INSTALL_FORWARD | grep -q -- "-d $VPN_SUBNET_IPV4/24 -j ACCEPT"; then
echo "FAIL: iptables client-to-client allow rule is missing"
exit 1 exit 1
fi fi
fi fi
@@ -930,10 +1073,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
@@ -956,8 +1097,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"
@@ -993,10 +1135,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