mirror of
https://github.com/angristan/openvpn-install.git
synced 2026-08-11 00:58:13 +02:00
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:
@@ -30,6 +30,7 @@ jobs:
|
||||
image: ubuntu:22.04
|
||||
- name: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
policy_e2e: deny
|
||||
- name: ubuntu-25.10
|
||||
image: ubuntu:25.10
|
||||
- name: debian-11
|
||||
@@ -98,6 +99,7 @@ jobs:
|
||||
name: fedora-42-firewalld
|
||||
image: fedora:42
|
||||
enable_firewalld: true
|
||||
policy_e2e: deny
|
||||
tls:
|
||||
name: tls-crypt-v2
|
||||
sig: crypt-v2
|
||||
@@ -107,6 +109,7 @@ jobs:
|
||||
name: debian-12-nftables
|
||||
image: debian:12
|
||||
enable_nftables: true
|
||||
policy_e2e: deny
|
||||
tls:
|
||||
name: tls-crypt-v2
|
||||
sig: crypt-v2
|
||||
@@ -129,6 +132,42 @@ jobs:
|
||||
name: tls-crypt-v2
|
||||
sig: crypt-v2
|
||||
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 }}
|
||||
steps:
|
||||
@@ -154,12 +193,24 @@ jobs:
|
||||
- name: Create Docker network
|
||||
run: docker network create --subnet=172.28.0.0/24 vpn-test
|
||||
|
||||
- name: Create policy test LAN
|
||||
if: matrix.os.policy_e2e != ''
|
||||
run: |
|
||||
docker network create --subnet=10.55.0.0/24 policy-lan
|
||||
docker run -d \
|
||||
--name policy-lan-target \
|
||||
--network policy-lan \
|
||||
--ip 10.55.0.20 \
|
||||
--entrypoint sleep \
|
||||
openvpn-client infinity
|
||||
|
||||
- name: Create shared volume
|
||||
run: docker volume create shared-config
|
||||
|
||||
- name: Start OpenVPN server
|
||||
run: |
|
||||
docker run -d \
|
||||
POLICY_E2E=${{ matrix.os.policy_e2e || '' }}
|
||||
docker create \
|
||||
--name openvpn-server \
|
||||
--hostname openvpn-server \
|
||||
--privileged \
|
||||
@@ -178,8 +229,17 @@ jobs:
|
||||
-e TLS_KEY_FILE=${{ matrix.tls.key_file }} \
|
||||
-e CLIENT_IPV6=${{ matrix.os.client_ipv6 && 'y' || 'n' }} \
|
||||
-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
|
||||
|
||||
if [ -n "$POLICY_E2E" ]; then
|
||||
docker network connect --ip 10.55.0.10 --gw-priority -1 policy-lan openvpn-server
|
||||
fi
|
||||
docker start openvpn-server
|
||||
|
||||
- name: Wait for server installation and startup
|
||||
run: |
|
||||
echo "Waiting for OpenVPN server to install and client config to be ready..."
|
||||
@@ -252,6 +312,21 @@ jobs:
|
||||
docker run --rm -v shared-config:/shared alpine \
|
||||
cat /shared/client.ovpn
|
||||
|
||||
- name: Start policy test peer
|
||||
if: matrix.os.policy_e2e != ''
|
||||
run: |
|
||||
docker exec policy-lan-target ping -c 3 -W 2 10.55.0.10
|
||||
docker run -d \
|
||||
--name policy-peer \
|
||||
--hostname policy-peer \
|
||||
--cap-add=NET_ADMIN \
|
||||
--device=/dev/net/tun:/dev/net/tun \
|
||||
--network vpn-test \
|
||||
--ip 172.28.0.30 \
|
||||
-v shared-config:/shared \
|
||||
--entrypoint /policy-peer-entrypoint.sh \
|
||||
openvpn-client
|
||||
|
||||
- name: Start OpenVPN client and run tests
|
||||
run: |
|
||||
docker run \
|
||||
@@ -262,6 +337,8 @@ jobs:
|
||||
--network vpn-test \
|
||||
--ip 172.28.0.20 \
|
||||
-v shared-config:/shared \
|
||||
-e POLICY_E2E=${{ matrix.os.policy_e2e || '' }} \
|
||||
-e POLICY_LAN_IP=10.55.0.20 \
|
||||
openvpn-client &
|
||||
|
||||
# Wait for tests to complete (look for success message)
|
||||
@@ -309,12 +386,14 @@ jobs:
|
||||
|
||||
- name: Show client logs
|
||||
if: always()
|
||||
run: docker logs openvpn-client 2>&1 || true
|
||||
run: |
|
||||
docker logs openvpn-client 2>&1 || true
|
||||
docker logs policy-peer 2>&1 || true
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker stop openvpn-server openvpn-client 2>/dev/null || true
|
||||
docker rm openvpn-server openvpn-client 2>/dev/null || true
|
||||
docker network rm vpn-test 2>/dev/null || true
|
||||
docker stop openvpn-server openvpn-client policy-peer policy-lan-target 2>/dev/null || true
|
||||
docker rm openvpn-server openvpn-client policy-peer policy-lan-target 2>/dev/null || true
|
||||
docker network rm vpn-test policy-lan 2>/dev/null || true
|
||||
docker volume rm shared-config 2>/dev/null || true
|
||||
|
||||
@@ -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?
|
||||
|
||||
**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`
|
||||
|
||||
@@ -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?
|
||||
|
||||
**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?
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
```
|
||||
push "route 192.168.1.0 255.255.255.0"
|
||||
```
|
||||
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.
|
||||
|
||||
Replace `192.168.1.0/24` with your actual LAN subnet.
|
||||
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.
|
||||
|
||||
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)
|
||||
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.
|
||||
|
||||
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`
|
||||
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
|
||||
route-nopull
|
||||
route 10.0.0.0 255.0.0.0
|
||||
```bash
|
||||
./openvpn-install.sh install \
|
||||
--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
|
||||
```
|
||||
|
||||
```
|
||||
#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`
|
||||
The installer does not push internet routes, leak-blocking directives, or VPN DNS in this mode.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
graph LR
|
||||
flowchart LR
|
||||
A[Phone] -->|Encrypted| VPN
|
||||
B[Laptop] -->|Encrypted| VPN
|
||||
C[Computer] -->|Encrypted| VPN
|
||||
|
||||
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?
|
||||
|
||||
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)
|
||||
- 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)
|
||||
- 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 tunnel MTU (default: `1500`)
|
||||
- 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
|
||||
./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
|
||||
./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
|
||||
- `--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::`)
|
||||
- `--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-random` - Use random port (49152-65535)
|
||||
- `--protocol <udp|tcp>` - Protocol (default: `udp`)
|
||||
- `--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 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-primary <ip>` - Custom primary DNS (requires `--dns custom`)
|
||||
- `--dns-secondary <ip>` - Custom secondary DNS (requires `--dns custom`)
|
||||
|
||||
@@ -14,6 +14,10 @@ services:
|
||||
cgroupns: host
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
ROUTE_INTERNET: ${ROUTE_INTERNET:-y}
|
||||
CLIENT_TO_CLIENT: ${CLIENT_TO_CLIENT:-n}
|
||||
LOCAL_NETWORKS: ${LOCAL_NETWORKS:-}
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
volumes:
|
||||
|
||||
+870
-280
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@ RUN mkdir -p /dev/net
|
||||
|
||||
# Copy test scripts
|
||||
COPY test/client-entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
COPY test/policy-peer-entrypoint.sh /policy-peer-entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh /policy-peer-entrypoint.sh
|
||||
|
||||
WORKDIR /etc/openvpn
|
||||
|
||||
|
||||
@@ -68,7 +68,8 @@ RUN chmod +x /opt/openvpn-install.sh
|
||||
# Copy test scripts
|
||||
COPY test/server-entrypoint.sh /entrypoint.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
|
||||
# PassEnvironment passes Docker env vars (-e) from PID 1 to the service
|
||||
@@ -80,7 +81,7 @@ RUN printf '%s\n' \
|
||||
'[Service]' \
|
||||
'Type=oneshot' \
|
||||
'Environment=HOME=/root' \
|
||||
'PassEnvironment=AUTH_MODE TLS_SIG TLS_KEY_FILE TLS_VERSION_MIN TLS13_CIPHERSUITES CLIENT_IPV6 VPN_SUBNET_IPV6 WAIT_TIMEOUT_SIGNAL WAIT_TIMEOUT_CONNECT WAIT_TIMEOUT_REVOKE' \
|
||||
'PassEnvironment=AUTH_MODE TLS_SIG TLS_KEY_FILE TLS_VERSION_MIN TLS13_CIPHERSUITES CLIENT_IPV6 VPN_SUBNET_IPV6 ROUTE_INTERNET CLIENT_TO_CLIENT LOCAL_NETWORKS POLICY_E2E WAIT_TIMEOUT_SIGNAL WAIT_TIMEOUT_CONNECT WAIT_TIMEOUT_REVOKE' \
|
||||
'WorkingDirectory=/root' \
|
||||
'ExecStart=/entrypoint.sh' \
|
||||
'RemainAfterExit=yes' \
|
||||
|
||||
+102
-9
@@ -131,10 +131,13 @@ wait_for_revoked_reconnect_rejected() {
|
||||
|
||||
test_dns_resolution() {
|
||||
local label="$1"
|
||||
local test_name="github.com"
|
||||
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)..."
|
||||
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
|
||||
success=true
|
||||
break
|
||||
@@ -147,7 +150,7 @@ test_dns_resolution() {
|
||||
echo "PASS: DNS resolution through Unbound works"
|
||||
else
|
||||
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
|
||||
fi
|
||||
}
|
||||
@@ -220,13 +223,42 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 2: Ping VPN gateway (IPv4)
|
||||
echo "Test 2: Pinging VPN gateway (IPv4) ($VPN_GATEWAY)..."
|
||||
# Test 2: Verify pushed routes match the access policy.
|
||||
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)"
|
||||
|
||||
# Test 2b: Ping VPN gateway (IPv6, if enabled)
|
||||
# Test 3b: Ping VPN gateway (IPv6, if enabled)
|
||||
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
|
||||
echo "PASS: Can ping VPN gateway (IPv6)"
|
||||
else
|
||||
@@ -235,8 +267,67 @@ if [ "${CLIENT_IPV6:-n}" = "y" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 3: DNS resolution through Unbound
|
||||
test_dns_resolution "Test 3"
|
||||
# Packet-level access policy tests use a second VPN client and a LAN-only host.
|
||||
if [ -n "${POLICY_E2E:-}" ]; then
|
||||
echo "Test 4: Checking packet-level access policy..."
|
||||
wait_for_file /shared/policy-peer-ip "policy peer VPN address"
|
||||
POLICY_PEER_IP=$(cat /shared/policy-peer-ip)
|
||||
POLICY_LAN_IP="${POLICY_LAN_IP:-10.55.0.20}"
|
||||
|
||||
if [ "$POLICY_E2E" = "allow" ]; then
|
||||
if ping -c 3 -W 2 "$POLICY_PEER_IP" >/dev/null; then
|
||||
echo "PASS: Client-to-client packets are allowed"
|
||||
else
|
||||
echo "FAIL: Cannot reach allowed VPN peer $POLICY_PEER_IP"
|
||||
exit 1
|
||||
fi
|
||||
if ping -c 3 -W 2 "$POLICY_LAN_IP" >/dev/null; then
|
||||
echo "PASS: LAN packets and destination-scoped NAT work"
|
||||
else
|
||||
echo "FAIL: Cannot reach allowed LAN host $POLICY_LAN_IP"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$POLICY_E2E" = "deny" ]; then
|
||||
if ping -c 1 -W 2 "$POLICY_PEER_IP" >/dev/null; then
|
||||
echo "FAIL: Isolated VPN peer $POLICY_PEER_IP is reachable"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Client-to-client packets are blocked"
|
||||
if ping -c 1 -W 2 "$POLICY_LAN_IP" >/dev/null; then
|
||||
echo "FAIL: Unexposed LAN host $POLICY_LAN_IP is reachable"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Unexposed LAN packets are blocked"
|
||||
else
|
||||
echo "FAIL: Unknown POLICY_E2E value: $POLICY_E2E"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
|
||||
PUBLIC_DNS_OUTPUT=""
|
||||
for _ in $(seq 1 5); do
|
||||
PUBLIC_DNS_OUTPUT=$(dig @1.1.1.1 example.com +short +tcp +time=5 +tries=1 2>&1) || true
|
||||
if grep -qE '^[0-9]+(\.[0-9]+){3}$' <<<"$PUBLIC_DNS_OUTPUT"; then
|
||||
break
|
||||
fi
|
||||
PUBLIC_DNS_OUTPUT=""
|
||||
sleep 2
|
||||
done
|
||||
if [ -n "$PUBLIC_DNS_OUTPUT" ]; then
|
||||
echo "PASS: Direct internet packets traverse VPN forwarding and NAT"
|
||||
else
|
||||
echo "FAIL: Direct public DNS query through the VPN failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 5: DNS resolution through Unbound in full-tunnel mode.
|
||||
if [ "${ROUTE_INTERNET:-y}" = "y" ]; then
|
||||
test_dns_resolution "Test 5"
|
||||
else
|
||||
echo "Test 5: SKIP: VPN DNS is disabled in split-tunnel mode"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Initial connectivity tests PASSED ==="
|
||||
@@ -269,7 +360,9 @@ sleep 5
|
||||
echo "Test: Pinging VPN gateway after renewal ($VPN_GATEWAY)..."
|
||||
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 "=== Post-renewal connectivity tests PASSED ==="
|
||||
|
||||
Executable
+92
@@ -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"
|
||||
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
|
||||
+251
-109
@@ -3,6 +3,8 @@ set -e
|
||||
|
||||
echo "=== OpenVPN Server Container ==="
|
||||
|
||||
/opt/test/local-network-detection.sh /opt/openvpn-install.sh
|
||||
|
||||
# Create TUN device if it doesn't exist
|
||||
if [ ! -c /dev/net/tun ]; then
|
||||
mkdir -p /dev/net
|
||||
@@ -57,10 +59,28 @@ if grep -q $'\033' "$NO_COLOR_OUTPUT"; then
|
||||
fi
|
||||
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)
|
||||
VPN_GATEWAY="${VPN_SUBNET_IPV4%.*}.1"
|
||||
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)
|
||||
# CLIENT_IPV6: y/n to enable IPv6 for VPN clients
|
||||
# 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+=(--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
|
||||
if [ "$CLIENT_IPV6" = "y" ]; then
|
||||
INSTALL_CMD+=(--client-ipv6)
|
||||
@@ -197,6 +229,65 @@ fi
|
||||
|
||||
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
|
||||
# =====================================================
|
||||
@@ -253,6 +344,17 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$POLICY_E2E" ]; then
|
||||
echo "Creating second VPN client for packet-level policy tests..."
|
||||
bash /opt/openvpn-install.sh client add policy-peer --cert-days 3650
|
||||
if [ ! -f /root/policy-peer.ovpn ]; then
|
||||
echo "FAIL: Policy peer client configuration was not generated"
|
||||
exit 1
|
||||
fi
|
||||
cp /root/policy-peer.ovpn /shared/policy-peer.ovpn
|
||||
sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/policy-peer.ovpn
|
||||
fi
|
||||
|
||||
# Copy client config to shared volume for initial connectivity tests
|
||||
cp /root/testclient.ovpn /shared/client.ovpn
|
||||
sed -i 's/^remote .*/remote openvpn-server 1194/' /shared/client.ovpn
|
||||
@@ -264,6 +366,9 @@ echo "Client config copied to /shared/client.ovpn"
|
||||
echo "VPN_GATEWAY=$VPN_GATEWAY"
|
||||
echo "CLIENT_IPV6=$CLIENT_IPV6"
|
||||
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
|
||||
echo "VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6"
|
||||
echo "VPN_GATEWAY_IPV6=$VPN_GATEWAY_IPV6"
|
||||
@@ -599,87 +704,117 @@ echo "Post-renewal client tests passed"
|
||||
# =====================================================
|
||||
# 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
|
||||
# Verify Unbound is running (started by systemctl in install script)
|
||||
echo "Checking Unbound service status..."
|
||||
for _ in $(seq 1 30); do
|
||||
if pgrep -x unbound >/dev/null; then
|
||||
echo "PASS: Unbound is running"
|
||||
break
|
||||
if [ -f /etc/unbound/unbound.conf ]; then
|
||||
# Verify Unbound is running (started by systemctl in install script)
|
||||
echo "Checking Unbound service status..."
|
||||
for _ in $(seq 1 30); do
|
||||
if pgrep -x unbound >/dev/null; then
|
||||
echo "PASS: Unbound is running"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! pgrep -x unbound >/dev/null; then
|
||||
echo "FAIL: Unbound is not running"
|
||||
systemctl status unbound 2>&1 || true
|
||||
journalctl -u unbound --no-pager -n 50 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! pgrep -x unbound >/dev/null; then
|
||||
echo "FAIL: Unbound is not running"
|
||||
systemctl status unbound 2>&1 || true
|
||||
journalctl -u unbound --no-pager -n 50 2>&1 || true
|
||||
else
|
||||
echo "FAIL: /etc/unbound/unbound.conf not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Verifying Unbound Installation ==="
|
||||
|
||||
# Verify Unbound config exists in conf.d directory
|
||||
UNBOUND_OPENVPN_CONF="/etc/unbound/unbound.conf.d/openvpn.conf"
|
||||
if [ -f "$UNBOUND_OPENVPN_CONF" ]; then
|
||||
echo "PASS: Found Unbound config at $UNBOUND_OPENVPN_CONF"
|
||||
else
|
||||
echo "FAIL: OpenVPN Unbound config not found at $UNBOUND_OPENVPN_CONF"
|
||||
echo "Contents of /etc/unbound/:"
|
||||
ls -la /etc/unbound/
|
||||
ls -la /etc/unbound/unbound.conf.d/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify Unbound listens on VPN gateway
|
||||
if grep -q "interface: $VPN_GATEWAY" "$UNBOUND_OPENVPN_CONF"; then
|
||||
echo "PASS: Unbound configured to listen on $VPN_GATEWAY"
|
||||
else
|
||||
echo "FAIL: Unbound not configured for $VPN_GATEWAY"
|
||||
cat "$UNBOUND_OPENVPN_CONF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify OpenVPN pushes correct DNS
|
||||
if grep -q "push \"dhcp-option DNS $VPN_GATEWAY\"" /etc/openvpn/server/server.conf; then
|
||||
echo "PASS: OpenVPN configured to push Unbound DNS"
|
||||
else
|
||||
echo "FAIL: OpenVPN not configured to push Unbound DNS"
|
||||
grep "dhcp-option DNS" /etc/openvpn/server/server.conf || echo "No DNS push found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Unbound Installation Verified ==="
|
||||
echo ""
|
||||
else
|
||||
echo "FAIL: /etc/unbound/unbound.conf not found"
|
||||
exit 1
|
||||
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
|
||||
|
||||
echo ""
|
||||
echo "=== Verifying Unbound Installation ==="
|
||||
|
||||
# Verify Unbound config exists in conf.d directory
|
||||
UNBOUND_OPENVPN_CONF="/etc/unbound/unbound.conf.d/openvpn.conf"
|
||||
if [ -f "$UNBOUND_OPENVPN_CONF" ]; then
|
||||
echo "PASS: Found Unbound config at $UNBOUND_OPENVPN_CONF"
|
||||
else
|
||||
echo "FAIL: OpenVPN Unbound config not found at $UNBOUND_OPENVPN_CONF"
|
||||
echo "Contents of /etc/unbound/:"
|
||||
ls -la /etc/unbound/
|
||||
ls -la /etc/unbound/unbound.conf.d/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify Unbound listens on VPN gateway
|
||||
if grep -q "interface: $VPN_GATEWAY" "$UNBOUND_OPENVPN_CONF"; then
|
||||
echo "PASS: Unbound configured to listen on $VPN_GATEWAY"
|
||||
else
|
||||
echo "FAIL: Unbound not configured for $VPN_GATEWAY"
|
||||
cat "$UNBOUND_OPENVPN_CONF"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify OpenVPN pushes correct DNS
|
||||
if grep -q "push \"dhcp-option DNS $VPN_GATEWAY\"" /etc/openvpn/server/server.conf; then
|
||||
echo "PASS: OpenVPN configured to push Unbound DNS"
|
||||
else
|
||||
echo "FAIL: OpenVPN not configured to push Unbound DNS"
|
||||
grep "dhcp-option DNS" /etc/openvpn/server/server.conf || echo "No DNS push found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Unbound Installation Verified ==="
|
||||
echo ""
|
||||
|
||||
# Verify OpenVPN server (started by systemd via install script)
|
||||
echo "Verifying OpenVPN server..."
|
||||
|
||||
# Verify firewall rules exist
|
||||
echo "Verifying firewall rules..."
|
||||
if systemctl is-active --quiet firewalld; then
|
||||
# firewalld is active - verify masquerade is enabled
|
||||
echo "firewalld detected, checking masquerade..."
|
||||
for _ in $(seq 1 10); do
|
||||
if firewall-cmd --query-masquerade 2>/dev/null; then
|
||||
echo "PASS: firewalld masquerade is enabled"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if ! firewall-cmd --query-masquerade 2>/dev/null; then
|
||||
echo "FAIL: firewalld masquerade is not enabled"
|
||||
echo "Current firewalld config:"
|
||||
firewall-cmd --list-all 2>&1 || true
|
||||
echo "firewalld detected, checking scoped policy rules..."
|
||||
if ! firewall-cmd --get-policies | grep -qw openvpn-egress; then
|
||||
echo "FAIL: firewalld OpenVPN policy is missing"
|
||||
exit 1
|
||||
fi
|
||||
if ! firewall-cmd --zone=openvpn-install --query-source="$VPN_SUBNET_IPV4/24"; then
|
||||
echo "FAIL: firewalld OpenVPN source zone is missing"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(firewall-cmd --permanent --policy=openvpn-egress --get-target)" != "DROP" ]; then
|
||||
echo "FAIL: firewalld OpenVPN policy does not default to DROP"
|
||||
exit 1
|
||||
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
|
||||
fi
|
||||
# Verify port is open
|
||||
if firewall-cmd --list-ports | grep -q "1194/udp"; then
|
||||
echo "PASS: OpenVPN port is open in firewalld"
|
||||
else
|
||||
@@ -687,15 +822,6 @@ if systemctl is-active --quiet firewalld; then
|
||||
firewall-cmd --list-ports
|
||||
exit 1
|
||||
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
|
||||
# nftables mode - verify OpenVPN tables exist
|
||||
echo "nftables detected, checking OpenVPN tables..."
|
||||
@@ -712,20 +838,25 @@ elif systemctl is-active --quiet nftables; then
|
||||
nft list ruleset 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
# Verify NAT table exists
|
||||
if nft list table ip openvpn-nat >/dev/null 2>&1; then
|
||||
echo "PASS: nftables 'ip openvpn-nat' table exists"
|
||||
else
|
||||
echo "FAIL: nftables 'ip openvpn-nat' table not found"
|
||||
nft list ruleset 2>&1 || true
|
||||
exit 1
|
||||
if [ "$ROUTE_INTERNET" = "y" ] || [ -n "$LOCAL_NETWORKS" ]; 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 scoped NAT is configured"
|
||||
else
|
||||
echo "FAIL: nftables scoped NAT is missing"
|
||||
nft list ruleset 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Verify masquerade rule exists
|
||||
if nft list table ip openvpn-nat | grep -q "masquerade"; then
|
||||
echo "PASS: nftables masquerade rule exists"
|
||||
else
|
||||
echo "FAIL: nftables masquerade rule not found"
|
||||
nft list table ip openvpn-nat 2>&1 || true
|
||||
if [ "$ROUTE_INTERNET" = "y" ]; then
|
||||
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
|
||||
echo "FAIL: nftables private-network isolation is missing"
|
||||
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
|
||||
fi
|
||||
# Verify include in nftables.conf
|
||||
@@ -737,20 +868,32 @@ elif systemctl is-active --quiet nftables; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# iptables mode - verify NAT rules
|
||||
echo "iptables mode, checking NAT rules..."
|
||||
for _ in $(seq 1 10); do
|
||||
if iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4"; then
|
||||
echo "PASS: NAT POSTROUTING rule for $VPN_SUBNET_IPV4/24 exists"
|
||||
break
|
||||
echo "iptables mode, checking policy rules..."
|
||||
if [ "$ROUTE_INTERNET" = "y" ] || [ -n "$LOCAL_NETWORKS" ]; then
|
||||
for _ in $(seq 1 10); do
|
||||
iptables -t nat -L POSTROUTING -n | grep -q "$VPN_SUBNET_IPV4" && break
|
||||
sleep 1
|
||||
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
|
||||
sleep 1
|
||||
done
|
||||
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 "Current NAT rules:"
|
||||
iptables -t nat -L POSTROUTING -n -v
|
||||
systemctl status iptables-openvpn 2>&1 || true
|
||||
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
|
||||
fi
|
||||
fi
|
||||
@@ -930,10 +1073,8 @@ echo "=== Certificate Revocation Tests PASSED ==="
|
||||
echo ""
|
||||
echo "=== Testing List Client Certificates ==="
|
||||
|
||||
# At this point we have 3 client certificates:
|
||||
# - testclient (Valid) - the renewed certificate
|
||||
# - testclient (Revoked) - the old certificate revoked during renewal
|
||||
# - revoketest (Revoked) - the revoked certificate
|
||||
# At this point PKI mode has three lifecycle-test certificates, plus the
|
||||
# optional policy peer used by packet-level access tests.
|
||||
LIST_OUTPUT="/tmp/list-clients-output.log"
|
||||
(bash /opt/openvpn-install.sh client list) 2>&1 | tee "$LIST_OUTPUT" || true
|
||||
|
||||
@@ -956,8 +1097,9 @@ fi
|
||||
|
||||
# Verify certificate count (varies by auth mode)
|
||||
if [ "$AUTH_MODE" = "pki" ]; then
|
||||
# PKI mode: 3 certs (testclient valid, testclient revoked from renewal, revoketest revoked)
|
||||
if grep -q "Found 3 client certificate(s)" "$LIST_OUTPUT"; then
|
||||
EXPECTED_CLIENT_COUNT=3
|
||||
[ -n "$POLICY_E2E" ] && EXPECTED_CLIENT_COUNT=4
|
||||
if grep -q "Found $EXPECTED_CLIENT_COUNT client certificate(s)" "$LIST_OUTPUT"; then
|
||||
echo "PASS: List shows correct certificate count"
|
||||
else
|
||||
echo "FAIL: List does not show correct certificate count"
|
||||
@@ -993,10 +1135,10 @@ fi
|
||||
# Verify client count in JSON (varies by auth mode)
|
||||
JSON_CLIENT_COUNT=$(jq '.clients | length' "$LIST_JSON_OUTPUT")
|
||||
if [ "$AUTH_MODE" = "pki" ]; then
|
||||
if [ "$JSON_CLIENT_COUNT" -eq 3 ]; then
|
||||
if [ "$JSON_CLIENT_COUNT" -eq "$EXPECTED_CLIENT_COUNT" ]; then
|
||||
echo "PASS: Client list JSON has correct count ($JSON_CLIENT_COUNT)"
|
||||
else
|
||||
echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected 3)"
|
||||
echo "FAIL: Client list JSON has wrong count: $JSON_CLIENT_COUNT (expected $EXPECTED_CLIENT_COUNT)"
|
||||
cat "$LIST_JSON_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user