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:
+619 -96
View File
@@ -213,6 +213,11 @@ show_install_help() {
--no-client-ipv6 Disable IPv6 for VPN clients (default) --no-client-ipv6 Disable IPv6 for VPN clients (default)
--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 Route client internet traffic through VPN (default)
--no-route-internet Keep client internet traffic outside VPN
--client-to-client Allow VPN clients to access each other
--no-client-to-client Isolate VPN clients from each other (default)
--local-network <CIDR> Allow access to a server-side network (repeatable)
--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 <proto> Protocol: udp or tcp (default: udp) --protocol <proto> Protocol: udp or tcp (default: udp)
@@ -486,6 +491,11 @@ readonly AUTH_MODES=("pki" "fingerprint")
# HMAC algorithms # HMAC algorithms
readonly HMAC_ALGS=("SHA256" "SHA384" "SHA512") readonly HMAC_ALGS=("SHA256" "SHA384" "SHA512")
# Networks that internet access must not implicitly expose. Explicit local
# networks are allowed before these deny rules are evaluated.
readonly PROTECTED_IPV4_NETWORKS=("10.0.0.0/8" "100.64.0.0/10" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12" "192.168.0.0/16")
readonly PROTECTED_IPV6_NETWORKS=("::1/128" "fc00::/7" "fe80::/10")
# TLS 1.3 cipher suite options # TLS 1.3 cipher suite options
readonly TLS13_OPTIONS=("all" "aes-256-only" "aes-128-only" "chacha20-only") readonly TLS13_OPTIONS=("all" "aes-256-only" "aes-128-only" "chacha20-only")
@@ -503,6 +513,9 @@ set_installation_defaults() {
CLIENT_IPV6="${CLIENT_IPV6:-n}" CLIENT_IPV6="${CLIENT_IPV6:-n}"
VPN_SUBNET_IPV4="${VPN_SUBNET_IPV4:-10.8.0.0}" VPN_SUBNET_IPV4="${VPN_SUBNET_IPV4:-10.8.0.0}"
VPN_SUBNET_IPV6="${VPN_SUBNET_IPV6:-fd42:42:42:42::}" VPN_SUBNET_IPV6="${VPN_SUBNET_IPV6:-fd42:42:42:42::}"
ROUTE_INTERNET="${ROUTE_INTERNET:-y}"
CLIENT_TO_CLIENT="${CLIENT_TO_CLIENT:-n}"
LOCAL_NETWORKS="${LOCAL_NETWORKS:-}"
PORT="${PORT:-1194}" PORT="${PORT:-1194}"
PROTOCOL="${PROTOCOL:-udp}" PROTOCOL="${PROTOCOL:-udp}"
@@ -597,6 +610,196 @@ validate_subnet_ipv6() {
fi fi
} }
is_valid_ipv4_cidr() {
local cidr="$1" address prefix extra
local -a octets
[[ $cidr == */* ]] || return 1
address="${cidr%/*}"
prefix="${cidr##*/}"
[[ $prefix =~ ^(0|[1-9][0-9]?)$ ]] || return 1
prefix=$((10#$prefix))
((prefix >= 1 && prefix <= 32)) || return 1
IFS='.' read -r -a octets <<<"$address"
[[ ${#octets[@]} -eq 4 ]] || return 1
for extra in "${octets[@]}"; do
[[ $extra =~ ^(0|[1-9][0-9]{0,2})$ ]] || return 1
extra=$((10#$extra))
((extra >= 0 && extra <= 255)) || return 1
done
local ip mask
ip=$(((10#${octets[0]} << 24) | (10#${octets[1]} << 16) | (10#${octets[2]} << 8) | 10#${octets[3]}))
if ((prefix == 0)); then
mask=0
else
mask=$(((0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF))
fi
(( (ip & mask) == ip ))
}
expand_ipv6_address() {
local address="$1"
local -n result_ref="$2"
local left right remainder part
local -a left_parts=() right_parts=()
[[ $address == *:* ]] || return 1
[[ $address =~ ^[0-9a-fA-F:]+$ ]] || return 1
if [[ $address == *::* ]]; then
remainder="${address#*::}"
[[ $remainder != *::* ]] || return 1
left="${address%%::*}"
right="${address#*::}"
[[ -z $left ]] || IFS=':' read -r -a left_parts <<<"$left"
[[ -z $right ]] || IFS=':' read -r -a right_parts <<<"$right"
((${#left_parts[@]} + ${#right_parts[@]} < 8)) || return 1
else
IFS=':' read -r -a left_parts <<<"$address"
[[ ${#left_parts[@]} -eq 8 ]] || return 1
fi
for part in "${left_parts[@]}" "${right_parts[@]}"; do
[[ $part =~ ^[0-9a-fA-F]{1,4}$ ]] || return 1
done
result_ref=()
for part in "${left_parts[@]}"; do
result_ref+=("$((16#$part))")
done
while ((${#result_ref[@]} + ${#right_parts[@]} < 8)); do
result_ref+=(0)
done
for part in "${right_parts[@]}"; do
result_ref+=("$((16#$part))")
done
[[ ${#result_ref[@]} -eq 8 ]]
}
is_valid_ipv6_cidr() {
local cidr="$1" address prefix_text prefix index remaining host_mask
local -a hextets
[[ $cidr == */* ]] || return 1
address="${cidr%/*}"
prefix_text="${cidr##*/}"
[[ $prefix_text =~ ^(0|[1-9][0-9]{0,2})$ ]] || return 1
prefix=$((10#$prefix_text))
((prefix >= 1 && prefix <= 128)) || return 1
expand_ipv6_address "$address" hextets || return 1
remaining=$prefix
for index in "${!hextets[@]}"; do
if ((remaining >= 16)); then
remaining=$((remaining - 16))
elif ((remaining <= 0)); then
((hextets[index] == 0)) || return 1
else
host_mask=$(((1 << (16 - remaining)) - 1))
(( (hextets[index] & host_mask) == 0 )) || return 1
remaining=0
fi
done
}
is_valid_local_network() {
is_valid_ipv4_cidr "$1" || is_valid_ipv6_cidr "$1"
}
add_local_network() {
local network="${1//[[:space:]]/}"
is_valid_local_network "$network" || log_fatal "Invalid local network: $1. Use a network CIDR such as 192.168.1.0/24 or fd00:1::/64."
if [[ -z $LOCAL_NETWORKS ]]; then
LOCAL_NETWORKS="$network"
elif [[ ",$LOCAL_NETWORKS," != *",$network,"* ]]; then
LOCAL_NETWORKS+=",$network"
fi
}
normalize_local_networks() {
local configured="${LOCAL_NETWORKS//[[:space:]]/}" network
LOCAL_NETWORKS=""
[[ -z $configured ]] && return
while IFS= read -r network; do
add_local_network "$network"
done < <(tr ',' '\n' <<<"$configured")
}
local_networks_for_family() {
local family="$1" network
[[ -z $LOCAL_NETWORKS ]] && return
while IFS= read -r network; do
if [[ $family == "4" && $network == *.* ]] || [[ $family == "6" && $network == *:* ]]; then
echo "$network"
fi
done < <(tr ',' '\n' <<<"$LOCAL_NETWORKS")
}
has_local_network_family() {
[[ -n $(local_networks_for_family "$1") ]]
}
ipv4_prefix_to_netmask() {
local prefix="$1" mask
if ((prefix == 0)); then
mask=0
else
mask=$(((0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF))
fi
printf '%d.%d.%d.%d\n' \
$(((mask >> 24) & 255)) \
$(((mask >> 16) & 255)) \
$(((mask >> 8) & 255)) \
$((mask & 255))
}
ipv4_cidrs_overlap() {
local first="$1" second="$2" first_address second_address first_prefix second_prefix prefix mask
local -a first_octets second_octets
first_address="${first%/*}"
second_address="${second%/*}"
first_prefix=$((10#${first##*/}))
second_prefix=$((10#${second##*/}))
prefix=$first_prefix
((second_prefix < prefix)) && prefix=$second_prefix
IFS='.' read -r -a first_octets <<<"$first_address"
IFS='.' read -r -a second_octets <<<"$second_address"
mask=$(((0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF))
local first_ip=$(((10#${first_octets[0]} << 24) | (10#${first_octets[1]} << 16) | (10#${first_octets[2]} << 8) | 10#${first_octets[3]}))
local second_ip=$(((10#${second_octets[0]} << 24) | (10#${second_octets[1]} << 16) | (10#${second_octets[2]} << 8) | 10#${second_octets[3]}))
(( (first_ip & mask) == (second_ip & mask) ))
}
ipv6_cidrs_overlap() {
local first="$1" second="$2" first_prefix second_prefix prefix index remaining mask
local -a first_hextets second_hextets
first_prefix=$((10#${first##*/}))
second_prefix=$((10#${second##*/}))
prefix=$first_prefix
((second_prefix < prefix)) && prefix=$second_prefix
expand_ipv6_address "${first%/*}" first_hextets || return 1
expand_ipv6_address "${second%/*}" second_hextets || return 1
remaining=$prefix
for index in "${!first_hextets[@]}"; do
((remaining <= 0)) && return 0
if ((remaining >= 16)); then
((first_hextets[index] == second_hextets[index])) || return 1
remaining=$((remaining - 16))
else
mask=$(((0xFFFF << (16 - remaining)) & 0xFFFF))
(( (first_hextets[index] & mask) == (second_hextets[index] & mask) ))
return
fi
done
return 0
}
validate_positive_int() { validate_positive_int() {
local value="$1" local value="$1"
local name="$2" local name="$2"
@@ -643,9 +846,12 @@ validate_configuration() {
*) log_fatal "Invalid protocol: $PROTOCOL. Must be 'udp' or 'tcp'." ;; *) log_fatal "Invalid protocol: $PROTOCOL. Must be 'udp' or 'tcp'." ;;
esac esac
# Validate DNS # Validate DNS. Split-tunnel installs do not push a DNS server.
case "$DNS" in case "$DNS" in
system | unbound | cloudflare | quad9 | quad9-uncensored | fdn | dnswatch | opendns | google | yandex | adguard | nextdns | custom) ;; system | unbound | cloudflare | quad9 | quad9-uncensored | fdn | dnswatch | opendns | google | yandex | adguard | nextdns | custom) ;;
"")
[[ $ROUTE_INTERNET == "n" ]] || log_fatal "A DNS provider is required when internet routing is enabled."
;;
*) log_fatal "Invalid DNS provider: $DNS. Valid providers: system, unbound, cloudflare, quad9, quad9-uncensored, fdn, dnswatch, opendns, google, yandex, adguard, nextdns, custom" ;; *) log_fatal "Invalid DNS provider: $DNS. Valid providers: system, unbound, cloudflare, quad9, quad9-uncensored, fdn, dnswatch, opendns, google, yandex, adguard, nextdns, custom" ;;
esac esac
@@ -686,6 +892,31 @@ validate_configuration() {
log_fatal "At least one of CLIENT_IPV4 or CLIENT_IPV6 must be 'y'" log_fatal "At least one of CLIENT_IPV4 or CLIENT_IPV6 must be 'y'"
fi fi
case "$ROUTE_INTERNET" in
y | n) ;;
*) log_fatal "Invalid ROUTE_INTERNET value: $ROUTE_INTERNET. Must be 'y' or 'n'." ;;
esac
case "$CLIENT_TO_CLIENT" in
y | n) ;;
*) log_fatal "Invalid CLIENT_TO_CLIENT value: $CLIENT_TO_CLIENT. Must be 'y' or 'n'." ;;
esac
normalize_local_networks
if has_local_network_family 4 && [[ $CLIENT_IPV4 != "y" ]]; then
log_fatal "IPv4 local networks require IPv4 for VPN clients. Use --client-ipv4 or remove the IPv4 local network."
fi
if has_local_network_family 6 && [[ $CLIENT_IPV6 != "y" ]]; then
log_fatal "IPv6 local networks require IPv6 for VPN clients. Use --client-ipv6 or remove the IPv6 local network."
fi
local local_network
while IFS= read -r local_network; do
if [[ $local_network == *.* ]] && ipv4_cidrs_overlap "$local_network" "$VPN_SUBNET_IPV4/24"; then
log_fatal "Local network $local_network overlaps the IPv4 VPN subnet $VPN_SUBNET_IPV4/24."
elif [[ $local_network == *:* ]] && ipv6_cidrs_overlap "$local_network" "${VPN_SUBNET_IPV6}/112"; then
log_fatal "Local network $local_network overlaps the IPv6 VPN subnet ${VPN_SUBNET_IPV6}/112."
fi
done < <(tr ',' '\n' <<<"$LOCAL_NETWORKS")
# Validate ENDPOINT_TYPE # Validate ENDPOINT_TYPE
case "$ENDPOINT_TYPE" in case "$ENDPOINT_TYPE" in
4 | 6) ;; 4 | 6) ;;
@@ -950,6 +1181,27 @@ cmd_install() {
VPN_SUBNET_IPV4="$2" VPN_SUBNET_IPV4="$2"
shift 2 shift 2
;; ;;
--route-internet)
ROUTE_INTERNET=y
shift
;;
--no-route-internet)
ROUTE_INTERNET=n
shift
;;
--client-to-client)
CLIENT_TO_CLIENT=y
shift
;;
--no-client-to-client)
CLIENT_TO_CLIENT=n
shift
;;
--local-network)
[[ -z "${2:-}" ]] && log_fatal "--local-network requires an argument"
add_local_network "$2"
shift 2
;;
--port) --port)
[[ -z "${2:-}" ]] && log_fatal "--port requires an argument" [[ -z "${2:-}" ]] && log_fatal "--port requires an argument"
validate_port "$2" validate_port "$2"
@@ -1157,13 +1409,18 @@ cmd_install() {
# Set all defaults for any unset values # Set all defaults for any unset values
set_installation_defaults set_installation_defaults
# Validate configuration values (catches invalid env vars)
validate_configuration
# Detect IPs and set up network config (interactive mode does this in installQuestions) # Detect IPs and set up network config (interactive mode does this in installQuestions)
detect_server_ips detect_server_ips
fi fi
# Split-tunnel installs leave the client's DNS configuration unchanged.
if [[ $ROUTE_INTERNET == "n" ]]; then
DNS=""
fi
# Validate both CLI and interactive configuration.
validate_configuration
# Prepare derived network configuration (gateways, etc.) # Prepare derived network configuration (gateways, etc.)
prepare_network_config prepare_network_config
@@ -2307,6 +2564,39 @@ function installQuestions() {
esac esac
fi fi
# ==========================================================================
# Step 7: Client access policy
# ==========================================================================
log_menu ""
log_prompt "What should VPN clients be allowed to access?"
prompt_yes_no "Route client internet traffic through the VPN?" "y" ROUTE_INTERNET
prompt_yes_no "Allow VPN clients to access each other?" "n" CLIENT_TO_CLIENT
local local_network_access
prompt_yes_no "Allow VPN clients to access the server's local network? (mainly for home servers)" "n" local_network_access
if [[ $local_network_access == "y" ]]; then
log_prompt "Enter the server-side networks clients may access."
log_prompt "Use comma-separated CIDRs, for example: 192.168.1.0/24,fd00:1::/64"
until [[ -n $LOCAL_NETWORKS ]]; do
local configured_networks network networks_valid=true
read -rp "Local networks: " -e configured_networks
while IFS= read -r network; do
network="${network//[[:space:]]/}"
if [[ -z $network ]] || ! is_valid_local_network "$network"; then
log_warn "Invalid network CIDR: ${network:-<empty>}"
networks_valid=false
break
fi
done < <(tr ',' '\n' <<<"$configured_networks")
if [[ $networks_valid == true ]]; then
LOCAL_NETWORKS="$configured_networks"
normalize_local_networks
fi
done
else
LOCAL_NETWORKS=""
fi
log_menu "" log_menu ""
log_prompt "What port do you want OpenVPN to listen to?" log_prompt "What port do you want OpenVPN to listen to?"
log_menu " 1) Default: 1194" log_menu " 1) Default: 1194"
@@ -2346,6 +2636,7 @@ function installQuestions() {
PROTOCOL="tcp" PROTOCOL="tcp"
;; ;;
esac esac
if [[ $ROUTE_INTERNET == "y" ]]; then
log_menu "" log_menu ""
log_prompt "What DNS resolvers do you want to use with the VPN?" log_prompt "What DNS resolvers do you want to use with the VPN?"
local dns_labels=("Current system resolvers (from /etc/resolv.conf)" "Self-hosted DNS Resolver (Unbound)" "Cloudflare (Anycast: worldwide)" "Quad9 (Anycast: worldwide)" "Quad9 uncensored (Anycast: worldwide)" "FDN (France)" "DNS.WATCH (Germany)" "OpenDNS (Anycast: worldwide)" "Google (Anycast: worldwide)" "Yandex Basic (Russia)" "AdGuard DNS (Anycast: worldwide)" "NextDNS (Anycast: worldwide)" "Custom") local dns_labels=("Current system resolvers (from /etc/resolv.conf)" "Self-hosted DNS Resolver (Unbound)" "Cloudflare (Anycast: worldwide)" "Quad9 (Anycast: worldwide)" "Quad9 uncensored (Anycast: worldwide)" "FDN (France)" "DNS.WATCH (Germany)" "OpenDNS (Anycast: worldwide)" "Google (Anycast: worldwide)" "Yandex Basic (Russia)" "AdGuard DNS (Anycast: worldwide)" "NextDNS (Anycast: worldwide)" "Custom")
@@ -2384,6 +2675,10 @@ function installQuestions() {
dns_valid=true dns_valid=true
fi fi
done done
else
DNS=""
log_info "VPN DNS is not configured because internet routing is disabled."
fi
log_menu "" log_menu ""
log_prompt "Do you want to allow a single .ovpn profile to be used on multiple devices simultaneously?" log_prompt "Do you want to allow a single .ovpn profile to be used on multiple devices simultaneously?"
log_prompt "Note: Enabling this disables persistent IP addresses for clients." log_prompt "Note: Enabling this disables persistent IP addresses for clients."
@@ -2629,6 +2924,9 @@ function installOpenVPN() {
log_info " CLIENT_IPV6=$CLIENT_IPV6" log_info " CLIENT_IPV6=$CLIENT_IPV6"
log_info " VPN_SUBNET_IPV4=$VPN_SUBNET_IPV4" log_info " VPN_SUBNET_IPV4=$VPN_SUBNET_IPV4"
log_info " VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6" log_info " VPN_SUBNET_IPV6=$VPN_SUBNET_IPV6"
log_info " ROUTE_INTERNET=$ROUTE_INTERNET"
log_info " CLIENT_TO_CLIENT=$CLIENT_TO_CLIENT"
log_info " LOCAL_NETWORKS=${LOCAL_NETWORKS:-none}"
log_info " PORT=$PORT" log_info " PORT=$PORT"
log_info " PROTOCOL=$PROTOCOL" log_info " PROTOCOL=$PROTOCOL"
log_info " DNS=$DNS" log_info " DNS=$DNS"
@@ -2862,6 +3160,9 @@ function installOpenVPN() {
if [[ $MULTI_CLIENT == "y" ]]; then if [[ $MULTI_CLIENT == "y" ]]; then
echo "duplicate-cn" >>/etc/openvpn/server/server.conf echo "duplicate-cn" >>/etc/openvpn/server/server.conf
fi fi
if [[ $CLIENT_TO_CLIENT == "y" ]]; then
echo "client-to-client" >>/etc/openvpn/server/server.conf
fi
echo "dev tun" >>/etc/openvpn/server/server.conf echo "dev tun" >>/etc/openvpn/server/server.conf
# Only add user/group if systemd doesn't handle it (avoids double privilege drop) # Only add user/group if systemd doesn't handle it (avoids double privilege drop)
@@ -2892,7 +3193,8 @@ topology subnet" >>/etc/openvpn/server/server.conf
echo "ifconfig-pool-persist ipp.txt" >>/etc/openvpn/server/server.conf echo "ifconfig-pool-persist ipp.txt" >>/etc/openvpn/server/server.conf
fi fi
# DNS resolvers # DNS resolvers are only pushed when the VPN carries internet traffic.
if [[ $ROUTE_INTERNET == "y" ]]; then
case $DNS in case $DNS in
system) system)
# Locate the proper resolv.conf # Locate the proper resolv.conf
@@ -3027,18 +3329,33 @@ topology subnet" >>/etc/openvpn/server/server.conf
fi fi
;; ;;
esac esac
fi
# Redirect gateway settings - always redirect both IPv4 and IPv6 to prevent leaks # Push explicit routes for server-side networks. These routes are independent
# For IPv4: redirect-gateway def1 routes all IPv4 through VPN (or drops it if IPv4 not configured) # from internet routing and are protected by matching firewall rules.
# For IPv6: route-ipv6 + redirect-gateway ipv6 routes all IPv6, or block-ipv6 drops it local local_network address prefix netmask
while IFS= read -r local_network; do
address="${local_network%/*}"
prefix="${local_network##*/}"
netmask=$(ipv4_prefix_to_netmask "$prefix")
echo "push \"route $address $netmask\"" >>/etc/openvpn/server/server.conf
done < <(local_networks_for_family 4)
while IFS= read -r local_network; do
echo "push \"route-ipv6 $local_network\"" >>/etc/openvpn/server/server.conf
done < <(local_networks_for_family 6)
# Full-tunnel mode redirects enabled address families and blocks leaks from
# disabled families. Split-tunnel mode leaves normal client internet routes intact.
if [[ $ROUTE_INTERNET == "y" ]]; then
echo 'push "redirect-gateway def1 bypass-dhcp"' >>/etc/openvpn/server/server.conf echo 'push "redirect-gateway def1 bypass-dhcp"' >>/etc/openvpn/server/server.conf
if [[ $CLIENT_IPV6 == "y" ]]; then if [[ $CLIENT_IPV6 == "y" ]]; then
echo 'push "route-ipv6 2000::/3"' >>/etc/openvpn/server/server.conf echo 'push "route-ipv6 2000::/3"' >>/etc/openvpn/server/server.conf
echo 'push "redirect-gateway ipv6"' >>/etc/openvpn/server/server.conf echo 'push "redirect-gateway ipv6"' >>/etc/openvpn/server/server.conf
else else
# Block IPv6 on clients to prevent IPv6 leaks when VPN only handles IPv4 # Block IPv6 on clients to prevent IPv6 leaks when VPN only handles IPv4.
echo 'push "block-ipv6"' >>/etc/openvpn/server/server.conf echo 'push "block-ipv6"' >>/etc/openvpn/server/server.conf
fi fi
fi
if [[ -n $MTU ]]; then if [[ -n $MTU ]]; then
echo "tun-mtu $MTU" >>/etc/openvpn/server/server.conf echo "tun-mtu $MTU" >>/etc/openvpn/server/server.conf
@@ -3084,6 +3401,24 @@ management /var/run/openvpn-server/server.sock unix
verb 3" verb 3"
} >>/etc/openvpn/server/server.conf } >>/etc/openvpn/server/server.conf
# Record installer-owned policy so firewall rules can be removed exactly.
if systemctl is-active --quiet firewalld; then
FIREWALL_BACKEND=firewalld
elif systemctl is-active --quiet nftables; then
FIREWALL_BACKEND=nftables
else
FIREWALL_BACKEND=iptables
fi
{
echo "FIREWALL_BACKEND=$FIREWALL_BACKEND"
echo "ROUTE_INTERNET=$ROUTE_INTERNET"
echo "CLIENT_TO_CLIENT=$CLIENT_TO_CLIENT"
echo "LOCAL_NETWORKS=$LOCAL_NETWORKS"
echo "CLIENT_IPV4=$CLIENT_IPV4"
echo "CLIENT_IPV6=$CLIENT_IPV6"
} >/etc/openvpn/server/openvpn-install.conf
chmod 600 /etc/openvpn/server/openvpn-install.conf
# Create client-config-dir dir # Create client-config-dir dir
run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd
# Create log dir # Create log dir
@@ -3096,19 +3431,22 @@ verb 3"
chown -R "$OPENVPN_USER:$OPENVPN_GROUP" /etc/openvpn/server chown -R "$OPENVPN_USER:$OPENVPN_GROUP" /etc/openvpn/server
chown "$OPENVPN_USER:$OPENVPN_GROUP" /var/log/openvpn chown "$OPENVPN_USER:$OPENVPN_GROUP" /var/log/openvpn
fi fi
chown root:root /etc/openvpn/server/openvpn-install.conf
chmod 600 /etc/openvpn/server/openvpn-install.conf
# Enable routing # Enable routing
log_info "Enabling IP forwarding..." log_info "Enabling IP forwarding..."
run_cmd_fatal "Creating sysctl.d directory" mkdir -p /etc/sysctl.d run_cmd_fatal "Creating sysctl.d directory" mkdir -p /etc/sysctl.d
# Enable IPv4 forwarding if clients get IPv4 # Forwarding is needed for internet or server-side network access. OpenVPN
if [[ $CLIENT_IPV4 == 'y' ]]; then # handles non-DCO client-to-client traffic internally, while DCO traffic is
# still constrained by the firewall rules below.
if [[ $CLIENT_IPV4 == 'y' ]] && { [[ $ROUTE_INTERNET == 'y' ]] || has_local_network_family 4 || [[ $CLIENT_TO_CLIENT == 'y' ]]; }; then
echo 'net.ipv4.ip_forward=1' >/etc/sysctl.d/99-openvpn.conf echo 'net.ipv4.ip_forward=1' >/etc/sysctl.d/99-openvpn.conf
else else
echo '# IPv4 forwarding not needed (no IPv4 clients)' >/etc/sysctl.d/99-openvpn.conf echo '# IPv4 forwarding not required by the selected access policy' >/etc/sysctl.d/99-openvpn.conf
fi fi
# Enable IPv6 forwarding if clients get IPv6 if [[ $CLIENT_IPV6 == 'y' ]] && { [[ $ROUTE_INTERNET == 'y' ]] || has_local_network_family 6 || [[ $CLIENT_TO_CLIENT == 'y' ]]; }; then
if [[ $CLIENT_IPV6 == 'y' ]]; then
echo 'net.ipv6.conf.all.forwarding=1' >>/etc/sysctl.d/99-openvpn.conf echo 'net.ipv6.conf.all.forwarding=1' >>/etc/sysctl.d/99-openvpn.conf
fi fi
# Apply sysctl rules # Apply sysctl rules
@@ -3186,7 +3524,7 @@ verb 3"
run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server run_cmd "Starting OpenVPN service" systemctl restart openvpn-server@server
fi fi
if [[ $DNS == "unbound" ]]; then if [[ $ROUTE_INTERNET == "y" && $DNS == "unbound" ]]; then
installUnbound installUnbound
fi fi
@@ -3194,34 +3532,62 @@ verb 3"
# Use source-based rules for VPN traffic (works reliably regardless of which tun interface OpenVPN uses) # Use source-based rules for VPN traffic (works reliably regardless of which tun interface OpenVPN uses)
log_info "Configuring firewall rules..." log_info "Configuring firewall rules..."
if systemctl is-active --quiet firewalld; then if [[ $FIREWALL_BACKEND == 'firewalld' ]]; then
# Use firewalld native commands for systems with firewalld active # Rich-rule priorities make explicit local and peer access win before the
# private-network deny rules, followed by the selected default policy.
log_info "firewalld detected, using firewall-cmd..." log_info "firewalld detected, using firewall-cmd..."
run_cmd "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port="$PORT/$PROTOCOL" run_cmd_fatal "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port="$PORT/$PROTOCOL"
run_cmd "Adding masquerade to firewalld" firewall-cmd --permanent --add-masquerade
# Add rich rules for VPN traffic (source-based only, as firewalld doesn't reliably if [[ -n $VPN_SUBNET_IPV4 ]]; then
# support interface patterns with direct rules when using nftables backend) run_cmd_fatal "Allowing the IPv4 VPN gateway" firewall-cmd --permanent --add-rich-rule="rule priority=\"-400\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$VPN_GATEWAY_IPV4/32\" accept"
if [[ $CLIENT_IPV4 == 'y' ]]; then while IFS= read -r local_network; do
run_cmd "Adding IPv4 VPN subnet rule" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept" run_cmd_fatal "Allowing local IPv4 network $local_network" firewall-cmd --permanent --add-rich-rule="rule priority=\"-300\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$local_network\" accept"
run_cmd_fatal "Adding NAT for local IPv4 network $local_network" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$local_network\" masquerade"
done < <(local_networks_for_family 4)
if [[ $CLIENT_IPV4 == 'y' && $CLIENT_TO_CLIENT == 'y' ]]; then
run_cmd_fatal "Allowing IPv4 client-to-client traffic" firewall-cmd --permanent --add-rich-rule="rule priority=\"-300\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$VPN_SUBNET_IPV4/24\" accept"
fi
if [[ $CLIENT_IPV4 == 'y' && $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV4_NETWORKS[@]}"; do
run_cmd_fatal "Protecting IPv4 network $protected_network" firewall-cmd --permanent --add-rich-rule="rule priority=\"-200\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$protected_network\" reject"
done
run_cmd_fatal "Allowing IPv4 internet access" firewall-cmd --permanent --add-rich-rule="rule priority=\"-100\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept"
run_cmd_fatal "Adding IPv4 internet NAT" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" masquerade"
else
run_cmd_fatal "Restricting other IPv4 access" firewall-cmd --permanent --add-rich-rule="rule priority=\"-100\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" reject"
fi
fi fi
if [[ $CLIENT_IPV6 == 'y' ]]; then if [[ $CLIENT_IPV6 == 'y' ]]; then
run_cmd "Adding IPv6 VPN subnet rule" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" accept" run_cmd_fatal "Allowing the IPv6 VPN gateway" firewall-cmd --permanent --add-rich-rule="rule priority=\"-400\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$VPN_GATEWAY_IPV6/128\" accept"
while IFS= read -r local_network; do
run_cmd_fatal "Allowing local IPv6 network $local_network" firewall-cmd --permanent --add-rich-rule="rule priority=\"-300\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$local_network\" accept"
run_cmd_fatal "Adding NAT for local IPv6 network $local_network" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$local_network\" masquerade"
done < <(local_networks_for_family 6)
if [[ $CLIENT_TO_CLIENT == 'y' ]]; then
run_cmd_fatal "Allowing IPv6 client-to-client traffic" firewall-cmd --permanent --add-rich-rule="rule priority=\"-300\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"${VPN_SUBNET_IPV6}/112\" accept"
fi
if [[ $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV6_NETWORKS[@]}"; do
run_cmd_fatal "Protecting IPv6 network $protected_network" firewall-cmd --permanent --add-rich-rule="rule priority=\"-200\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$protected_network\" reject"
done
run_cmd_fatal "Allowing IPv6 internet access" firewall-cmd --permanent --add-rich-rule="rule priority=\"-100\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" accept"
run_cmd_fatal "Adding IPv6 internet NAT" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" masquerade"
else
run_cmd_fatal "Restricting other IPv6 access" firewall-cmd --permanent --add-rich-rule="rule priority=\"-100\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" reject"
fi
fi fi
run_cmd "Reloading firewalld" firewall-cmd --reload run_cmd_fatal "Reloading firewalld" firewall-cmd --reload
elif systemctl is-active --quiet nftables; then elif [[ $FIREWALL_BACKEND == 'nftables' ]]; then
# Use nftables native rules for systems with nftables active
log_info "nftables detected, configuring nftables rules..." log_info "nftables detected, configuring nftables rules..."
run_cmd_fatal "Creating nftables directory" mkdir -p /etc/nftables run_cmd_fatal "Creating nftables directory" mkdir -p /etc/nftables
# Create nftables rules file
{ {
echo "table inet openvpn {" echo "table inet openvpn {"
echo " chain input {" echo " chain input {"
echo " type filter hook input priority 0; policy accept;" echo " type filter hook input priority 0; policy accept;"
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ -n $VPN_SUBNET_IPV4 ]]; then
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept" echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept"
fi fi
if [[ $CLIENT_IPV6 == 'y' ]]; then if [[ $CLIENT_IPV6 == 'y' ]]; then
@@ -3231,93 +3597,197 @@ verb 3"
echo " }" echo " }"
echo "" echo ""
echo " chain forward {" echo " chain forward {"
echo " type filter hook forward priority 0; policy accept;" echo " type filter hook forward priority -10; policy accept;"
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ -n $VPN_SUBNET_IPV4 ]]; then
echo " oifname \"tun*\" ip daddr $VPN_SUBNET_IPV4/24 ct state established,related accept"
while IFS= read -r local_network; do
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 ip daddr $local_network accept"
done < <(local_networks_for_family 4)
if [[ $CLIENT_IPV4 == 'y' && $CLIENT_TO_CLIENT == 'y' ]]; then
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 ip daddr $VPN_SUBNET_IPV4/24 accept"
fi
if [[ $CLIENT_IPV4 == 'y' && $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV4_NETWORKS[@]}"; do
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 ip daddr $protected_network drop"
done
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept" echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept"
echo " oifname \"tun*\" ip daddr $VPN_SUBNET_IPV4/24 accept" else
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 drop"
fi
fi fi
if [[ $CLIENT_IPV6 == 'y' ]]; then if [[ $CLIENT_IPV6 == 'y' ]]; then
echo " oifname \"tun*\" ip6 daddr ${VPN_SUBNET_IPV6}/112 ct state established,related accept"
while IFS= read -r local_network; do
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 ip6 daddr $local_network accept"
done < <(local_networks_for_family 6)
if [[ $CLIENT_TO_CLIENT == 'y' ]]; then
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 ip6 daddr ${VPN_SUBNET_IPV6}/112 accept"
fi
if [[ $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV6_NETWORKS[@]}"; do
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 ip6 daddr $protected_network drop"
done
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 accept" echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 accept"
echo " oifname \"tun*\" ip6 daddr ${VPN_SUBNET_IPV6}/112 accept" else
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 drop"
fi
fi fi
echo " }" echo " }"
echo "}" echo "}"
} >/etc/nftables/openvpn.nft } >/etc/nftables/openvpn.nft
# IPv4 NAT rules (only if clients get IPv4) if [[ $CLIENT_IPV4 == 'y' ]] && { [[ $ROUTE_INTERNET == 'y' ]] || has_local_network_family 4; }; then
if [[ $CLIENT_IPV4 == 'y' ]]; then {
echo " echo ""
table ip openvpn-nat { echo "table ip openvpn-nat {"
chain postrouting { echo " chain postrouting {"
type nat hook postrouting priority 100; policy accept; echo " type nat hook postrouting priority 100; policy accept;"
ip saddr $VPN_SUBNET_IPV4/24 oifname \"$NIC\" masquerade while IFS= read -r local_network; do
} echo " ip saddr $VPN_SUBNET_IPV4/24 ip daddr $local_network masquerade"
}" >>/etc/nftables/openvpn.nft done < <(local_networks_for_family 4)
if [[ $ROUTE_INTERNET == 'y' ]]; then
echo " ip saddr $VPN_SUBNET_IPV4/24 oifname \"$NIC\" masquerade"
fi
echo " }"
echo "}"
} >>/etc/nftables/openvpn.nft
fi fi
# IPv6 NAT rules (only if clients get IPv6) if [[ $CLIENT_IPV6 == 'y' ]] && { [[ $ROUTE_INTERNET == 'y' ]] || has_local_network_family 6; }; then
if [[ $CLIENT_IPV6 == 'y' ]]; then {
echo " echo ""
table ip6 openvpn-nat { echo "table ip6 openvpn-nat {"
chain postrouting { echo " chain postrouting {"
type nat hook postrouting priority 100; policy accept; echo " type nat hook postrouting priority 100; policy accept;"
ip6 saddr ${VPN_SUBNET_IPV6}/112 oifname \"$NIC\" masquerade while IFS= read -r local_network; do
} echo " ip6 saddr ${VPN_SUBNET_IPV6}/112 ip6 daddr $local_network masquerade"
}" >>/etc/nftables/openvpn.nft done < <(local_networks_for_family 6)
if [[ $ROUTE_INTERNET == 'y' ]]; then
echo " ip6 saddr ${VPN_SUBNET_IPV6}/112 oifname \"$NIC\" masquerade"
fi
echo " }"
echo "}"
} >>/etc/nftables/openvpn.nft
fi fi
# Add include to nftables.conf if not already present
if ! grep -q 'include.*/etc/nftables/openvpn.nft' /etc/nftables.conf; then if ! grep -q 'include.*/etc/nftables/openvpn.nft' /etc/nftables.conf; then
run_cmd "Adding include to nftables.conf" sh -c 'echo "include \"/etc/nftables/openvpn.nft\"" >> /etc/nftables.conf' run_cmd_fatal "Adding include to nftables.conf" sh -c 'echo "include \"/etc/nftables/openvpn.nft\"" >> /etc/nftables.conf'
fi fi
run_cmd_fatal "Reloading nftables" systemctl reload nftables
# Reload nftables to apply rules
run_cmd "Reloading nftables" systemctl reload nftables
else else
# Use iptables for systems without firewalld or nftables # Use iptables for systems without firewalld or nftables
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules # Dedicated chains enforce the same policy for userspace and DCO traffic.
echo "#!/bin/sh" >/etc/iptables/add-openvpn-rules.sh {
echo "#!/bin/sh"
# IPv4 rules (only if clients get IPv4) echo "set -eu"
if [[ $CLIENT_IPV4 == 'y' ]]; then echo "if iptables -nL OPENVPN_INSTALL_FORWARD >/dev/null 2>&1; then"
echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE echo " echo 'iptables chain OPENVPN_INSTALL_FORWARD already exists' >&2"
iptables -I INPUT 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT echo " exit 1"
iptables -I FORWARD 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT echo "fi"
iptables -I FORWARD 1 -o tun+ -d $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
fi
# IPv6 rules (only if clients get IPv6)
if [[ $CLIENT_IPV6 == 'y' ]]; then if [[ $CLIENT_IPV6 == 'y' ]]; then
echo "ip6tables -t nat -I POSTROUTING 1 -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE echo "if ip6tables -nL OPENVPN_INSTALL_FORWARD >/dev/null 2>&1; then"
ip6tables -I INPUT 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo " echo 'ip6tables chain OPENVPN_INSTALL_FORWARD already exists' >&2"
ip6tables -I FORWARD 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo " exit 1"
ip6tables -I FORWARD 1 -o tun+ -d ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo "fi"
ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh fi
echo "cleanup() { /etc/iptables/rm-openvpn-rules.sh >/dev/null 2>&1 || true; }"
echo "trap cleanup EXIT HUP INT TERM"
} >/etc/iptables/add-openvpn-rules.sh
{
echo "#!/bin/sh"
echo "set -u"
echo 'remove_rule() { "$@" 2>/dev/null || true; }'
} >/etc/iptables/rm-openvpn-rules.sh
if [[ $ENDPOINT_TYPE == '4' ]]; then
echo "iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
echo "remove_rule iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
else
echo "ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
echo "remove_rule ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
fi fi
# Script to remove rules if [[ -n $VPN_SUBNET_IPV4 ]]; then
echo "#!/bin/sh" >/etc/iptables/rm-openvpn-rules.sh {
echo "iptables -N OPENVPN_INSTALL_FORWARD"
echo "iptables -I INPUT 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT"
echo "iptables -I FORWARD 1 -o tun+ -d $VPN_SUBNET_IPV4/24 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
echo "iptables -I FORWARD 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j OPENVPN_INSTALL_FORWARD"
while IFS= read -r local_network; do
echo "iptables -A OPENVPN_INSTALL_FORWARD -d $local_network -j ACCEPT"
echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4/24 -d $local_network -j MASQUERADE"
done < <(local_networks_for_family 4)
if [[ $CLIENT_IPV4 == 'y' && $CLIENT_TO_CLIENT == 'y' ]]; then
echo "iptables -A OPENVPN_INSTALL_FORWARD -d $VPN_SUBNET_IPV4/24 -j ACCEPT"
fi
if [[ $CLIENT_IPV4 == 'y' && $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV4_NETWORKS[@]}"; do
echo "iptables -A OPENVPN_INSTALL_FORWARD -d $protected_network -j REJECT"
done
echo "iptables -A OPENVPN_INSTALL_FORWARD -j ACCEPT"
echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE"
else
echo "iptables -A OPENVPN_INSTALL_FORWARD -j REJECT"
fi
} >>/etc/iptables/add-openvpn-rules.sh
# IPv4 removal rules {
if [[ $CLIENT_IPV4 == 'y' ]]; then echo "remove_rule iptables -D FORWARD -i tun+ -s $VPN_SUBNET_IPV4/24 -j OPENVPN_INSTALL_FORWARD"
echo "iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE echo "remove_rule iptables -D FORWARD -o tun+ -d $VPN_SUBNET_IPV4/24 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
iptables -D INPUT -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT echo "remove_rule iptables -D INPUT -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT"
iptables -D FORWARD -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT if [[ $CLIENT_IPV4 == 'y' && $ROUTE_INTERNET == 'y' ]]; then
iptables -D FORWARD -o tun+ -d $VPN_SUBNET_IPV4/24 -j ACCEPT echo "remove_rule iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE"
iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh fi
while IFS= read -r local_network; do
echo "remove_rule iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4/24 -d $local_network -j MASQUERADE"
done < <(local_networks_for_family 4)
echo "remove_rule iptables -F OPENVPN_INSTALL_FORWARD"
echo "remove_rule iptables -X OPENVPN_INSTALL_FORWARD"
} >>/etc/iptables/rm-openvpn-rules.sh
fi fi
# IPv6 removal rules
if [[ $CLIENT_IPV6 == 'y' ]]; then if [[ $CLIENT_IPV6 == 'y' ]]; then
echo "ip6tables -t nat -D POSTROUTING -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE {
ip6tables -D INPUT -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo "ip6tables -N OPENVPN_INSTALL_FORWARD"
ip6tables -D FORWARD -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo "ip6tables -I INPUT 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT"
ip6tables -D FORWARD -o tun+ -d ${VPN_SUBNET_IPV6}/112 -j ACCEPT echo "ip6tables -I FORWARD 1 -o tun+ -d ${VPN_SUBNET_IPV6}/112 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh echo "ip6tables -I FORWARD 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j OPENVPN_INSTALL_FORWARD"
while IFS= read -r local_network; do
echo "ip6tables -A OPENVPN_INSTALL_FORWARD -d $local_network -j ACCEPT"
echo "ip6tables -t nat -I POSTROUTING 1 -s ${VPN_SUBNET_IPV6}/112 -d $local_network -j MASQUERADE"
done < <(local_networks_for_family 6)
if [[ $CLIENT_TO_CLIENT == 'y' ]]; then
echo "ip6tables -A OPENVPN_INSTALL_FORWARD -d ${VPN_SUBNET_IPV6}/112 -j ACCEPT"
fi fi
if [[ $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV6_NETWORKS[@]}"; do
echo "ip6tables -A OPENVPN_INSTALL_FORWARD -d $protected_network -j REJECT"
done
echo "ip6tables -A OPENVPN_INSTALL_FORWARD -j ACCEPT"
echo "ip6tables -t nat -I POSTROUTING 1 -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE"
else
echo "ip6tables -A OPENVPN_INSTALL_FORWARD -j REJECT"
fi
} >>/etc/iptables/add-openvpn-rules.sh
{
echo "remove_rule ip6tables -D FORWARD -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j OPENVPN_INSTALL_FORWARD"
echo "remove_rule ip6tables -D FORWARD -o tun+ -d ${VPN_SUBNET_IPV6}/112 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT"
echo "remove_rule ip6tables -D INPUT -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT"
if [[ $ROUTE_INTERNET == 'y' ]]; then
echo "remove_rule ip6tables -t nat -D POSTROUTING -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE"
fi
while IFS= read -r local_network; do
echo "remove_rule ip6tables -t nat -D POSTROUTING -s ${VPN_SUBNET_IPV6}/112 -d $local_network -j MASQUERADE"
done < <(local_networks_for_family 6)
echo "remove_rule ip6tables -F OPENVPN_INSTALL_FORWARD"
echo "remove_rule ip6tables -X OPENVPN_INSTALL_FORWARD"
} >>/etc/iptables/rm-openvpn-rules.sh
fi
echo "trap - EXIT HUP INT TERM" >>/etc/iptables/add-openvpn-rules.sh
run_cmd "Making add-openvpn-rules.sh executable" chmod +x /etc/iptables/add-openvpn-rules.sh run_cmd "Making add-openvpn-rules.sh executable" chmod +x /etc/iptables/add-openvpn-rules.sh
run_cmd "Making rm-openvpn-rules.sh executable" chmod +x /etc/iptables/rm-openvpn-rules.sh run_cmd "Making rm-openvpn-rules.sh executable" chmod +x /etc/iptables/rm-openvpn-rules.sh
@@ -3341,7 +3811,7 @@ WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service
# Enable service and apply rules # Enable service and apply rules
run_cmd "Reloading systemd" systemctl daemon-reload run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Enabling iptables service" systemctl enable iptables-openvpn run_cmd "Enabling iptables service" systemctl enable iptables-openvpn
run_cmd "Starting iptables service" systemctl start iptables-openvpn run_cmd_fatal "Starting iptables service" systemctl start iptables-openvpn
fi fi
# If the server is behind a NAT, use the correct IP address for the clients to connect to # If the server is behind a NAT, use the correct IP address for the clients to connect to
@@ -4446,6 +4916,20 @@ function removeOpenVPN() {
# Extract IPv6 subnet (may be empty if IPv6 not enabled) # Extract IPv6 subnet (may be empty if IPv6 not enabled)
VPN_SUBNET_IPV6=$(grep '^server-ipv6 ' /etc/openvpn/server/server.conf | cut -d " " -f 2 | sed 's|/.*||') VPN_SUBNET_IPV6=$(grep '^server-ipv6 ' /etc/openvpn/server/server.conf | cut -d " " -f 2 | sed 's|/.*||')
local install_config=/etc/openvpn/server/openvpn-install.conf
local has_policy_manifest=n
if [[ -f $install_config ]]; then
has_policy_manifest=y
FIREWALL_BACKEND=$(grep '^FIREWALL_BACKEND=' "$install_config" | cut -d= -f2-)
ROUTE_INTERNET=$(grep '^ROUTE_INTERNET=' "$install_config" | cut -d= -f2-)
CLIENT_TO_CLIENT=$(grep '^CLIENT_TO_CLIENT=' "$install_config" | cut -d= -f2-)
LOCAL_NETWORKS=$(grep '^LOCAL_NETWORKS=' "$install_config" | cut -d= -f2-)
CLIENT_IPV4=$(grep '^CLIENT_IPV4=' "$install_config" | cut -d= -f2-)
CLIENT_IPV6=$(grep '^CLIENT_IPV6=' "$install_config" | cut -d= -f2-)
VPN_GATEWAY_IPV4="${VPN_SUBNET_IPV4%.*}.1"
VPN_GATEWAY_IPV6="${VPN_SUBNET_IPV6}1"
fi
# Stop OpenVPN # Stop OpenVPN
log_info "Stopping OpenVPN service..." log_info "Stopping OpenVPN service..."
run_cmd "Disabling OpenVPN service" systemctl disable openvpn-server@server run_cmd "Disabling OpenVPN service" systemctl disable openvpn-server@server
@@ -4455,20 +4939,59 @@ function removeOpenVPN() {
# Remove firewall rules # Remove firewall rules
log_info "Removing firewall rules..." log_info "Removing firewall rules..."
if systemctl is-active --quiet firewalld && firewall-cmd --list-ports | grep -q "$PORT/$PROTOCOL_BASE"; then if systemctl is-active --quiet firewalld && { [[ $has_policy_manifest == 'y' && $FIREWALL_BACKEND == 'firewalld' ]] || { [[ $has_policy_manifest == 'n' ]] && firewall-cmd --list-ports | grep -q "$PORT/$PROTOCOL_BASE"; }; }; then
# firewalld was used
run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port="$PORT/$PROTOCOL_BASE" run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port="$PORT/$PROTOCOL_BASE"
if [[ $has_policy_manifest == 'y' ]]; then
if [[ -n $VPN_SUBNET_IPV4 ]]; then
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-400\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$VPN_GATEWAY_IPV4/32\" accept" 2>/dev/null || true
while IFS= read -r local_network; do
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-300\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$local_network\" accept" 2>/dev/null || true
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$local_network\" masquerade" 2>/dev/null || true
done < <(local_networks_for_family 4)
if [[ $CLIENT_IPV4 == 'y' && $CLIENT_TO_CLIENT == 'y' ]]; then
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-300\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$VPN_SUBNET_IPV4/24\" accept" 2>/dev/null || true
fi
if [[ $CLIENT_IPV4 == 'y' && $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV4_NETWORKS[@]}"; do
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-200\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" destination address=\"$protected_network\" reject" 2>/dev/null || true
done
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-100\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept" 2>/dev/null || true
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" masquerade" 2>/dev/null || true
else
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-100\" family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" reject" 2>/dev/null || true
fi
fi
if [[ $CLIENT_IPV6 == 'y' ]]; then
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-400\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$VPN_GATEWAY_IPV6/128\" accept" 2>/dev/null || true
while IFS= read -r local_network; do
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-300\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$local_network\" accept" 2>/dev/null || true
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$local_network\" masquerade" 2>/dev/null || true
done < <(local_networks_for_family 6)
if [[ $CLIENT_TO_CLIENT == 'y' ]]; then
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-300\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"${VPN_SUBNET_IPV6}/112\" accept" 2>/dev/null || true
fi
if [[ $ROUTE_INTERNET == 'y' ]]; then
for protected_network in "${PROTECTED_IPV6_NETWORKS[@]}"; do
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-200\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" destination address=\"$protected_network\" reject" 2>/dev/null || true
done
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-100\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" accept" 2>/dev/null || true
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" masquerade" 2>/dev/null || true
else
firewall-cmd --permanent --remove-rich-rule="rule priority=\"-100\" family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" reject" 2>/dev/null || true
fi
fi
else
# Compatibility with installations created before policy manifests.
run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade
# Remove IPv4 rich rule if configured
if [[ -n $VPN_SUBNET_IPV4 ]]; then if [[ -n $VPN_SUBNET_IPV4 ]]; then
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept" 2>/dev/null || true firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept" 2>/dev/null || true
fi fi
# Remove IPv6 rich rule if configured
if [[ -n $VPN_SUBNET_IPV6 ]]; then if [[ -n $VPN_SUBNET_IPV6 ]]; then
firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" accept" 2>/dev/null || true firewall-cmd --permanent --remove-rich-rule="rule family=\"ipv6\" source address=\"${VPN_SUBNET_IPV6}/112\" accept" 2>/dev/null || true
fi fi
fi
run_cmd "Reloading firewalld" firewall-cmd --reload run_cmd "Reloading firewalld" firewall-cmd --reload
elif [[ -f /etc/nftables/openvpn.nft ]]; then elif [[ $has_policy_manifest == 'y' && $FIREWALL_BACKEND == 'nftables' && -f /etc/nftables/openvpn.nft ]] || [[ $has_policy_manifest == 'n' && -f /etc/nftables/openvpn.nft ]]; then
# nftables was used # nftables was used
# Delete tables (suppress errors in case tables don't exist) # Delete tables (suppress errors in case tables don't exist)
nft delete table inet openvpn 2>/dev/null || true nft delete table inet openvpn 2>/dev/null || true
@@ -4476,7 +4999,7 @@ function removeOpenVPN() {
nft delete table ip6 openvpn-nat 2>/dev/null || true nft delete table ip6 openvpn-nat 2>/dev/null || true
run_cmd "Removing include from nftables.conf" sed -i '/include.*openvpn\.nft/d' /etc/nftables.conf run_cmd "Removing include from nftables.conf" sed -i '/include.*openvpn\.nft/d' /etc/nftables.conf
run_cmd "Removing nftables rules file" rm -f /etc/nftables/openvpn.nft run_cmd "Removing nftables rules file" rm -f /etc/nftables/openvpn.nft
elif [[ -f /etc/systemd/system/iptables-openvpn.service ]]; then elif [[ $has_policy_manifest == 'y' && $FIREWALL_BACKEND == 'iptables' && -f /etc/systemd/system/iptables-openvpn.service ]] || [[ $has_policy_manifest == 'n' && -f /etc/systemd/system/iptables-openvpn.service ]]; then
# iptables was used # iptables was used
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn run_cmd "Stopping iptables service" systemctl stop iptables-openvpn
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn run_cmd "Disabling iptables service" systemctl disable iptables-openvpn
+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 ==="
+157 -38
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,15 +785,19 @@ 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
echo "PASS: firewalld private-network isolation is configured"
else else
echo "FAIL: VPN subnet rich rule not found in firewalld" echo "FAIL: firewalld private-network isolation is missing"
echo "Current rich rules:"
firewall-cmd --list-rich-rules firewall-cmd --list-rich-rules
exit 1 exit 1
fi 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
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 +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
# 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 +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"
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