Add configurable VPN access policies

This commit is contained in:
Stanislas Lange
2026-08-02 12:43:31 +02:00
parent 25476a7143
commit 7357079bb9
8 changed files with 953 additions and 279 deletions
+14
View File
@@ -129,6 +129,17 @@ 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
- 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
tls:
name: tls-crypt-v2
sig: crypt-v2
key_file: tls-crypt-v2.key
name: ${{ matrix.os.name }} name: ${{ matrix.os.name }}
steps: steps:
@@ -178,6 +189,9 @@ 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 || '' }} \
openvpn-server openvpn-server
- name: Wait for server installation and startup - name: Wait for server installation and startup
+31 -68
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,17 @@ 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`: ```bash
./openvpn-install.sh install --local-network 192.168.1.0/24
```
``` Repeat `--local-network` to expose more than one server-side network. This feature is mainly for OpenVPN servers installed at home. It is disabled by default so cloud VPC, container, and management networks are not exposed automatically.
push "route 192.168.1.0 255.255.255.0"
```
Replace `192.168.1.0/24` with your actual LAN subnet. 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.
2. **Enable routing back to VPN clients** - Choose one of these options: 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.
- **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
# iptables
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.
Restart OpenVPN after making changes: `systemctl restart openvpn-server@server`
--- ---
@@ -180,56 +167,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 \
--local-network 10.0.0.0/8 \
--local-network 192.168.1.0/24
```
``` The installer does not push internet routes, leak-blocking directives, or VPN DNS in this mode.
#push "redirect-gateway def1 bypass-dhcp"
```
2. Add routes for the networks you want to tunnel:
```
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`
--- ---
+17 -1
View File
@@ -12,7 +12,7 @@ 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. By default, each client routes its internet traffic through the server, fully encrypted. You can instead keep internet traffic outside the VPN and allow only selected server-side networks.
```mermaid ```mermaid
graph LR graph LR
@@ -44,6 +44,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 +264,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 +306,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. Specify each LAN explicitly. The installer does not automatically expose connected cloud, container, or management networks. 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:
+677 -154
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 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 WAIT_TIMEOUT_SIGNAL WAIT_TIMEOUT_CONNECT WAIT_TIMEOUT_REVOKE' \
'WorkingDirectory=/root' \ 'WorkingDirectory=/root' \
'ExecStart=/entrypoint.sh' \ 'ExecStart=/entrypoint.sh' \
'RemainAfterExit=yes' \ 'RemainAfterExit=yes' \
+42 -7
View File
@@ -220,13 +220,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 +264,12 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
fi fi
fi fi
# Test 3: DNS resolution through Unbound # Test 4: DNS resolution through Unbound in full-tunnel mode.
test_dns_resolution "Test 3" if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
test_dns_resolution "Test 4"
else
echo "Test 4: SKIP: VPN DNS is disabled in split-tunnel mode"
fi
echo "" echo ""
echo "=== Initial connectivity tests PASSED ===" echo "=== Initial connectivity tests PASSED ==="
@@ -269,7 +302,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"
test_dns_resolution "Test: Post-renewal DNS" if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
test_dns_resolution "Test: Post-renewal DNS"
fi
echo "" echo ""
echo "=== Post-renewal connectivity tests PASSED ===" echo "=== Post-renewal connectivity tests PASSED ==="
+167 -48
View File
@@ -57,10 +57,27 @@ 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:-}"
# 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 +112,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 +226,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
# ===================================================== # =====================================================
@@ -264,6 +352,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,7 +690,8 @@ 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)
# ===================================================== # =====================================================
echo "=== Verifying Unbound DNS Resolver ===" if [ "$ROUTE_INTERNET" = "y" ]; then
echo "=== Verifying Unbound DNS Resolver ==="
if [ -f /etc/unbound/unbound.conf ]; then if [ -f /etc/unbound/unbound.conf ]; then
# Verify Unbound is running (started by systemctl in install script) # Verify Unbound is running (started by systemctl in install script)
@@ -655,8 +747,15 @@ else
exit 1 exit 1
fi 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,19 +763,18 @@ 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 [ "$ROUTE_INTERNET" = "y" ]; then
for _ in $(seq 1 10); do if firewall-cmd --list-rich-rules | grep -q "source address=\"$VPN_SUBNET_IPV4/24\" masquerade"; then
if firewall-cmd --query-masquerade 2>/dev/null; then echo "PASS: firewalld has source-scoped internet NAT"
echo "PASS: firewalld masquerade is enabled" else
break echo "FAIL: firewalld source-scoped internet NAT is missing"
firewall-cmd --list-rich-rules
exit 1
fi fi
sleep 1 fi
done if firewall-cmd --query-masquerade 2>/dev/null; then
if ! firewall-cmd --query-masquerade 2>/dev/null; then echo "FAIL: firewalld zone-wide masquerade should not be enabled"
echo "FAIL: firewalld masquerade is not enabled"
echo "Current firewalld config:"
firewall-cmd --list-all 2>&1 || true
exit 1 exit 1
fi fi
# Verify port is open # Verify port is open
@@ -687,13 +785,17 @@ 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 [ "$ROUTE_INTERNET" = "y" ]; then
if firewall-cmd --list-rich-rules | grep -q "source address=\"$VPN_SUBNET_IPV4/24\""; then # Private destinations, including the VPN pool, stay isolated unless explicitly allowed.
echo "PASS: VPN subnet rich rule is configured" if firewall-cmd --list-rich-rules | grep -q "destination address=\"10.0.0.0/8\" reject"; then
else echo "PASS: firewalld private-network isolation is configured"
echo "FAIL: VPN subnet rich rule not found in firewalld" else
echo "Current rich rules:" echo "FAIL: firewalld private-network isolation is missing"
firewall-cmd --list-rich-rules firewall-cmd --list-rich-rules
exit 1
fi
elif ! firewall-cmd --list-rich-rules | grep -q "source address=\"$VPN_SUBNET_IPV4/24\" reject"; then
echo "FAIL: firewalld split-tunnel default reject is missing"
exit 1 exit 1
fi fi
elif systemctl is-active --quiet nftables; then elif systemctl is-active --quiet nftables; then
@@ -712,20 +814,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 fi
# Verify masquerade rule exists if [ "$ROUTE_INTERNET" = "y" ]; then
if nft list table ip openvpn-nat | grep -q "masquerade"; then if nft list table inet openvpn | grep -q "ip daddr 10.0.0.0/8 drop"; then
echo "PASS: nftables masquerade rule exists" 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 +844,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" sleep 1
break done
if ! iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then
echo "FAIL: Expected scoped NAT rule was not found"
iptables -t nat -L POSTROUTING -n -v
exit 1
fi fi
sleep 1 fi
done if [ "$ROUTE_INTERNET" = "y" ]; then
if ! iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then if iptables -S OPENVPN_INSTALL_FORWARD | grep -q -- "-d 10.0.0.0/8 -j REJECT"; then
echo "FAIL: NAT POSTROUTING rule for $VPN_SUBNET_IPV4/24 not found" echo "PASS: iptables private-network isolation is configured"
echo "Current NAT rules:" else
iptables -t nat -L POSTROUTING -n -v echo "FAIL: iptables private-network isolation is missing"
systemctl status iptables-openvpn 2>&1 || true 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