fix: use source-based firewall rules with interface wildcard matching (#1426)

## Summary

- Fixes firewall rules that hardcode `tun0` interface, which fails when
OpenVPN uses `tun1`, `tun2`, etc. because another service already
occupies `tun0`
- Uses a defense-in-depth approach combining interface wildcard matching
with source-based rules to prevent IP spoofing

Fixes #1298

## Changes

| Backend | Before | After |
|---------|--------|-------|
| **iptables** | `-i tun0` | `-i tun+ -s $VPN_SUBNET` |
| **nftables** | `iifname "tun0"` | `iifname "tun*" ip saddr
$VPN_SUBNET` |
| **firewalld** | rich rules (source-based) | no change needed |

## Implementation Details

- **iptables/nftables**: Combined interface wildcard (`tun+`/`tun*`)
with source matching provides defense in depth - traffic must come from
both a tun interface AND the VPN subnet
- **firewalld**: Already used source-based rich rules, so no changes
required (rich rules work reliably across both iptables and nftables
backends)
This commit is contained in:
Stanislas
2025-12-16 09:58:30 +01:00
committed by GitHub
parent f7436ef2c1
commit e273a77dcd
2 changed files with 47 additions and 32 deletions

View File

@@ -2929,6 +2929,7 @@ verb 3" >>/etc/openvpn/server/server.conf
fi fi
# Configure firewall rules # Configure firewall rules
# 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 systemctl is-active --quiet firewalld; then
@@ -2937,7 +2938,8 @@ verb 3" >>/etc/openvpn/server/server.conf
run_cmd "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port="$PORT/$PROTOCOL" run_cmd "Adding OpenVPN port to firewalld" firewall-cmd --permanent --add-port="$PORT/$PROTOCOL"
run_cmd "Adding masquerade to firewalld" firewall-cmd --permanent --add-masquerade run_cmd "Adding masquerade to firewalld" firewall-cmd --permanent --add-masquerade
# Add rich rules for VPN traffic (source-based rules work reliably with dynamic tun0 interface) # Add rich rules for VPN traffic (source-based only, as firewalld doesn't reliably
# support interface patterns with direct rules when using nftables backend)
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ $CLIENT_IPV4 == 'y' ]]; then
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 "Adding IPv4 VPN subnet rule" firewall-cmd --permanent --add-rich-rule="rule family=\"ipv4\" source address=\"$VPN_SUBNET_IPV4/24\" accept"
fi fi
@@ -2952,20 +2954,33 @@ verb 3" >>/etc/openvpn/server/server.conf
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 - base filter rules # Create nftables rules file
echo "table inet openvpn { {
chain input { echo "table inet openvpn {"
type filter hook input priority 0; policy accept; echo " chain input {"
iifname \"tun0\" accept echo " type filter hook input priority 0; policy accept;"
iifname \"$NIC\" $PROTOCOL dport $PORT accept if [[ $CLIENT_IPV4 == 'y' ]]; then
} echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept"
fi
chain forward { if [[ $CLIENT_IPV6 == 'y' ]]; then
type filter hook forward priority 0; policy accept; echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 accept"
iifname \"$NIC\" oifname \"tun0\" accept fi
iifname \"tun0\" oifname \"$NIC\" accept echo " iifname \"$NIC\" $PROTOCOL dport $PORT accept"
} echo " }"
}" >/etc/nftables/openvpn.nft echo ""
echo " chain forward {"
echo " type filter hook forward priority 0; policy accept;"
if [[ $CLIENT_IPV4 == 'y' ]]; then
echo " iifname \"tun*\" ip saddr $VPN_SUBNET_IPV4/24 accept"
echo " oifname \"tun*\" ip daddr $VPN_SUBNET_IPV4/24 accept"
fi
if [[ $CLIENT_IPV6 == 'y' ]]; then
echo " iifname \"tun*\" ip6 saddr ${VPN_SUBNET_IPV6}/112 accept"
echo " oifname \"tun*\" ip6 daddr ${VPN_SUBNET_IPV6}/112 accept"
fi
echo " }"
echo "}"
} >/etc/nftables/openvpn.nft
# IPv4 NAT rules (only if clients get IPv4) # IPv4 NAT rules (only if clients get IPv4)
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ $CLIENT_IPV4 == 'y' ]]; then
@@ -3006,18 +3021,18 @@ table ip6 openvpn-nat {
# IPv4 rules (only if clients get IPv4) # IPv4 rules (only if clients get IPv4)
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ $CLIENT_IPV4 == 'y' ]]; then
echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE echo "iptables -t nat -I POSTROUTING 1 -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE
iptables -I INPUT 1 -i tun0 -j ACCEPT iptables -I INPUT 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT iptables -I FORWARD 1 -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT 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 iptables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
fi fi
# IPv6 rules (only if clients get IPv6) # 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 "ip6tables -t nat -I POSTROUTING 1 -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE
ip6tables -I INPUT 1 -i tun0 -j ACCEPT ip6tables -I INPUT 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT ip6tables -I FORWARD 1 -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT ip6tables -I FORWARD 1 -o tun+ -d ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/add-openvpn-rules.sh
fi fi
@@ -3027,18 +3042,18 @@ ip6tables -I INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptabl
# IPv4 removal rules # IPv4 removal rules
if [[ $CLIENT_IPV4 == 'y' ]]; then if [[ $CLIENT_IPV4 == 'y' ]]; then
echo "iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE echo "iptables -t nat -D POSTROUTING -s $VPN_SUBNET_IPV4/24 -o $NIC -j MASQUERADE
iptables -D INPUT -i tun0 -j ACCEPT iptables -D INPUT -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -D FORWARD -i $NIC -o tun0 -j ACCEPT iptables -D FORWARD -i tun+ -s $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -D FORWARD -i tun0 -o $NIC -j ACCEPT iptables -D FORWARD -o tun+ -d $VPN_SUBNET_IPV4/24 -j ACCEPT
iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh iptables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
fi fi
# IPv6 removal rules # 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 echo "ip6tables -t nat -D POSTROUTING -s ${VPN_SUBNET_IPV6}/112 -o $NIC -j MASQUERADE
ip6tables -D INPUT -i tun0 -j ACCEPT ip6tables -D INPUT -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -D FORWARD -i $NIC -o tun0 -j ACCEPT ip6tables -D FORWARD -i tun+ -s ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -D FORWARD -i tun0 -o $NIC -j ACCEPT ip6tables -D FORWARD -o tun+ -d ${VPN_SUBNET_IPV6}/112 -j ACCEPT
ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh ip6tables -D INPUT -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >>/etc/iptables/rm-openvpn-rules.sh
fi fi
@@ -3854,13 +3869,13 @@ function removeOpenVPN() {
# firewalld was used # 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"
run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade
# Remove IPv4 rule if it was configured # Remove IPv4 rich rule if configured
if [[ -n $VPN_SUBNET_IPV4 ]]; then if [[ -n $VPN_SUBNET_IPV4 ]]; then
run_cmd "Removing IPv4 VPN subnet rule" 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 rule if it was configured # Remove IPv6 rich rule if configured
if [[ -n $VPN_SUBNET_IPV6 ]]; then if [[ -n $VPN_SUBNET_IPV6 ]]; then
run_cmd "Removing IPv6 VPN subnet rule" 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
run_cmd "Reloading firewalld" firewall-cmd --reload run_cmd "Reloading firewalld" firewall-cmd --reload
elif [[ -f /etc/nftables/openvpn.nft ]]; then elif [[ -f /etc/nftables/openvpn.nft ]]; then

View File

@@ -571,7 +571,7 @@ 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 # 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 if firewall-cmd --list-rich-rules | grep -q "source address=\"$VPN_SUBNET_IPV4/24\""; then
echo "PASS: VPN subnet rich rule is configured" echo "PASS: VPN subnet rich rule is configured"
else else