feat: add native firewalld support (#1388)

## Summary

- Add native firewalld support for RHEL/Fedora/CentOS systems
- When firewalld is active, use `firewall-cmd --permanent` instead of
raw iptables
- Rules persist across `firewall-cmd --reload`
- Fall back to iptables when firewalld is not active
- Add `After=firewalld.service` to iptables systemd unit for safety

## Changes

**Install:** Detect firewalld, use `firewall-cmd` to add port,
masquerade, and rich rules. Fall back to iptables if inactive.

**Uninstall:** Detect which method was used and clean up accordingly.

**Tests:** Add `fedora-42-firewalld` CI test with firewalld enabled.

---

Closes https://github.com/angristan/openvpn-install/issues/356
Closes https://github.com/angristan/openvpn-install/pull/1200
This commit is contained in:
Stanislas
2025-12-13 20:49:40 +01:00
committed by GitHub
parent 9175c2c221
commit d8aa625639
6 changed files with 148 additions and 56 deletions

View File

@@ -89,6 +89,15 @@ jobs:
name: tls-auth name: tls-auth
sig: "3" sig: "3"
key_file: tls-auth.key key_file: tls-auth.key
# Test firewalld support on Fedora
- os:
name: fedora-42-firewalld
image: fedora:42
enable_firewalld: true
tls:
name: tls-crypt-v2
sig: "1"
key_file: tls-crypt-v2.key
name: ${{ matrix.os.name }} name: ${{ matrix.os.name }}
steps: steps:
@@ -103,6 +112,7 @@ jobs:
run: | run: |
docker build \ docker build \
--build-arg BASE_IMAGE=${{ matrix.os.image }} \ --build-arg BASE_IMAGE=${{ matrix.os.image }} \
--build-arg ENABLE_FIREWALLD=${{ matrix.os.enable_firewalld && 'y' || 'n' }} \
-t openvpn-server \ -t openvpn-server \
-f test/Dockerfile.server . -f test/Dockerfile.server .

4
FAQ.md
View File

@@ -87,9 +87,9 @@ If your client is <2.3.3, remove `tls-version-min 1.2` from your `/etc/openvpn/s
--- ---
**Q:** What syctl and iptables changes are made by the script? **Q:** What sysctl and firewall changes are made by the script?
**A:** Iptables rules are saved at `/etc/iptables/add-openvpn-rules.sh` and `/etc/iptables/rm-openvpn-rules.sh`. They are managed by the service `/etc/systemd/system/iptables-openvpn.service` **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`.
Sysctl options are at `/etc/sysctl.d/99-openvpn.conf` Sysctl options are at `/etc/sysctl.d/99-openvpn.conf`

View File

@@ -149,8 +149,8 @@ export CLIENTNUMBER="1" # Revokes the first client in the list
- Installs and configures a ready-to-use OpenVPN server - Installs and configures a ready-to-use OpenVPN server
- Certificate renewal for both client and server certificates - Certificate renewal for both client and server certificates
- 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
- Iptables rules and forwarding managed in a seamless way - Firewall rules and forwarding managed seamlessly (native firewalld support, iptables fallback)
- If needed, the script can cleanly remove OpenVPN, including configuration and iptables rules - If needed, the script can cleanly remove OpenVPN, including configuration and firewall rules
- Customisable encryption settings, enhanced default settings (see [Security and Encryption](#security-and-encryption) below) - Customisable encryption settings, enhanced default settings (see [Security and Encryption](#security-and-encryption) below)
- OpenVPN 2.4 features, mainly encryption improvements (see [Security and Encryption](#security-and-encryption) below) - OpenVPN 2.4 features, mainly encryption improvements (see [Security and Encryption](#security-and-encryption) below)
- Variety of DNS resolvers to be pushed to the clients - Variety of DNS resolvers to be pushed to the clients

View File

@@ -1419,48 +1419,66 @@ verb 3" >>/etc/openvpn/server/server.conf
installUnbound installUnbound
fi fi
# Add iptables rules in two scripts # Configure firewall rules
log_info "Configuring firewall rules..." log_info "Configuring firewall rules..."
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules if systemctl is-active --quiet firewalld; then
echo "#!/bin/sh # Use firewalld native commands for systems with firewalld active
log_info "firewalld detected, using firewall-cmd..."
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
# Add rich rules for VPN traffic (source-based rules work reliably with dynamic tun0 interface)
run_cmd "Adding VPN subnet rule" firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.8.0.0/24" accept'
if [[ $IPV6_SUPPORT == 'y' ]]; then
run_cmd "Adding IPv6 source rule" firewall-cmd --permanent --add-rich-rule='rule family="ipv6" source address="fd42:42:42:42::/112" accept'
fi
run_cmd "Reloading firewalld" firewall-cmd --reload
else
# Use iptables for systems without firewalld
run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables
# Script to add rules
echo "#!/bin/sh
iptables -t nat -I POSTROUTING 1 -s 10.8.0.0/24 -o $NIC -j MASQUERADE iptables -t nat -I POSTROUTING 1 -s 10.8.0.0/24 -o $NIC -j MASQUERADE
iptables -I INPUT 1 -i tun0 -j ACCEPT iptables -I INPUT 1 -i tun0 -j ACCEPT
iptables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT iptables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT
iptables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT iptables -I FORWARD 1 -i tun0 -o $NIC -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
if [[ $IPV6_SUPPORT == 'y' ]]; then if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
ip6tables -I INPUT 1 -i tun0 -j ACCEPT ip6tables -I INPUT 1 -i tun0 -j ACCEPT
ip6tables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT ip6tables -I FORWARD 1 -i $NIC -o tun0 -j ACCEPT
ip6tables -I FORWARD 1 -i tun0 -o $NIC -j ACCEPT ip6tables -I FORWARD 1 -i tun0 -o $NIC -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
# Script to remove rules # Script to remove rules
echo "#!/bin/sh echo "#!/bin/sh
iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o $NIC -j MASQUERADE iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o $NIC -j MASQUERADE
iptables -D INPUT -i tun0 -j ACCEPT iptables -D INPUT -i tun0 -j ACCEPT
iptables -D FORWARD -i $NIC -o tun0 -j ACCEPT iptables -D FORWARD -i $NIC -o tun0 -j ACCEPT
iptables -D FORWARD -i tun0 -o $NIC -j ACCEPT iptables -D FORWARD -i tun0 -o $NIC -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
if [[ $IPV6_SUPPORT == 'y' ]]; then if [[ $IPV6_SUPPORT == 'y' ]]; then
echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE
ip6tables -D INPUT -i tun0 -j ACCEPT ip6tables -D INPUT -i tun0 -j ACCEPT
ip6tables -D FORWARD -i $NIC -o tun0 -j ACCEPT ip6tables -D FORWARD -i $NIC -o tun0 -j ACCEPT
ip6tables -D FORWARD -i tun0 -o $NIC -j ACCEPT ip6tables -D FORWARD -i tun0 -o $NIC -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
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
# Handle the rules via a systemd script # Handle the rules via a systemd script
echo "[Unit] echo "[Unit]
Description=iptables rules for OpenVPN Description=iptables rules for OpenVPN
After=firewalld.service
Before=network-online.target Before=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -1473,10 +1491,11 @@ RemainAfterExit=yes
[Install] [Install]
WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service 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 "Starting iptables service" systemctl start iptables-openvpn
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
if [[ $ENDPOINT != "" ]]; then if [[ $ENDPOINT != "" ]]; then
@@ -2057,15 +2076,24 @@ function removeOpenVPN() {
# Remove customised service # Remove customised service
run_cmd "Removing service file" rm -f /etc/systemd/system/openvpn-server@.service run_cmd "Removing service file" rm -f /etc/systemd/system/openvpn-server@.service
# Remove the iptables rules related to the script # Remove firewall rules
log_info "Removing iptables rules..." log_info "Removing firewall rules..."
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn if systemctl is-active --quiet firewalld && firewall-cmd --list-ports | grep -q "$PORT/$PROTOCOL"; then
# Cleanup # firewalld was used
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port="$PORT/$PROTOCOL"
run_cmd "Removing iptables service file" rm /etc/systemd/system/iptables-openvpn.service run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade
run_cmd "Reloading systemd" systemctl daemon-reload run_cmd "Removing VPN subnet rule" firewall-cmd --permanent --remove-rich-rule='rule family="ipv4" source address="10.8.0.0/24" accept' 2>/dev/null || true
run_cmd "Removing iptables add script" rm /etc/iptables/add-openvpn-rules.sh run_cmd "Removing IPv6 source rule" firewall-cmd --permanent --remove-rich-rule='rule family="ipv6" source address="fd42:42:42:42::/112" accept' 2>/dev/null || true
run_cmd "Removing iptables rm script" rm /etc/iptables/rm-openvpn-rules.sh run_cmd "Reloading firewalld" firewall-cmd --reload
elif [[ -f /etc/systemd/system/iptables-openvpn.service ]]; then
# iptables was used
run_cmd "Stopping iptables service" systemctl stop iptables-openvpn
run_cmd "Disabling iptables service" systemctl disable iptables-openvpn
run_cmd "Removing iptables service file" rm /etc/systemd/system/iptables-openvpn.service
run_cmd "Reloading systemd" systemctl daemon-reload
run_cmd "Removing iptables add script" rm -f /etc/iptables/add-openvpn-rules.sh
run_cmd "Removing iptables rm script" rm -f /etc/iptables/rm-openvpn-rules.sh
fi
# SELinux # SELinux
if hash sestatus 2>/dev/null; then if hash sestatus 2>/dev/null; then

View File

@@ -5,7 +5,10 @@ ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE} FROM ${BASE_IMAGE}
ARG BASE_IMAGE ARG BASE_IMAGE
# Set to "y" to install and enable firewalld for testing
ARG ENABLE_FIREWALLD=n
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV ENABLE_FIREWALLD=${ENABLE_FIREWALLD}
# Install basic dependencies based on the OS # Install basic dependencies based on the OS
# dnsutils/bind-utils provides dig for DNS testing with Unbound # dnsutils/bind-utils provides dig for DNS testing with Unbound
@@ -16,10 +19,12 @@ RUN if command -v apt-get >/dev/null; then \
elif command -v dnf >/dev/null; then \ elif command -v dnf >/dev/null; then \
dnf install -y --allowerasing \ dnf install -y --allowerasing \
iproute iptables curl procps-ng systemd tar gzip bind-utils \ iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& if [ "$ENABLE_FIREWALLD" = "y" ]; then dnf install -y firewalld; fi \
&& dnf clean all; \ && dnf clean all; \
elif command -v yum >/dev/null; then \ elif command -v yum >/dev/null; then \
yum install -y \ yum install -y \
iproute iptables curl procps-ng systemd tar gzip bind-utils \ iproute iptables curl procps-ng systemd tar gzip bind-utils \
&& if [ "$ENABLE_FIREWALLD" = "y" ]; then yum install -y firewalld; fi \
&& yum clean all; \ && yum clean all; \
elif command -v pacman >/dev/null; then \ elif command -v pacman >/dev/null; then \
pacman -Syu --noconfirm \ pacman -Syu --noconfirm \
@@ -31,6 +36,11 @@ RUN if command -v apt-get >/dev/null; then \
&& zypper clean -a; \ && zypper clean -a; \
fi fi
# Enable firewalld if requested (must be done after systemd is available)
RUN if [ "$ENABLE_FIREWALLD" = "y" ] && command -v firewall-cmd >/dev/null; then \
systemctl enable firewalld; \
fi
# Create TUN device (will be mounted at runtime) # Create TUN device (will be mounted at runtime)
RUN mkdir -p /dev/net RUN mkdir -p /dev/net

View File

@@ -77,15 +77,22 @@ fi
# Verify all expected files were created # Verify all expected files were created
echo "Verifying installation..." echo "Verifying installation..."
MISSING_FILES=0 MISSING_FILES=0
for f in \ # Build list of required files
/etc/openvpn/server/server.conf \ REQUIRED_FILES=(
/etc/openvpn/server/ca.crt \ /etc/openvpn/server/server.conf
/etc/openvpn/server/ca.key \ /etc/openvpn/server/ca.crt
"/etc/openvpn/server/$TLS_KEY_FILE" \ /etc/openvpn/server/ca.key
/etc/openvpn/server/crl.pem \ "/etc/openvpn/server/$TLS_KEY_FILE"
/etc/openvpn/server/easy-rsa/pki/ca.crt \ /etc/openvpn/server/crl.pem
/etc/iptables/add-openvpn-rules.sh \ /etc/openvpn/server/easy-rsa/pki/ca.crt
/root/testclient.ovpn; do /root/testclient.ovpn
)
# Only check for iptables script if firewalld is not active
if ! systemctl is-active --quiet firewalld; then
REQUIRED_FILES+=(/etc/iptables/add-openvpn-rules.sh)
fi
for f in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "ERROR: Missing file: $f" echo "ERROR: Missing file: $f"
MISSING_FILES=$((MISSING_FILES + 1)) MISSING_FILES=$((MISSING_FILES + 1))
@@ -380,21 +387,58 @@ echo ""
# 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..."
# Verify iptables NAT rules exist (applied by iptables-openvpn service) # Verify firewall rules exist
echo "Verifying iptables NAT rules..." echo "Verifying firewall rules..."
for _ in $(seq 1 10); do if systemctl is-active --quiet firewalld; then
if iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then # firewalld is active - verify masquerade is enabled
echo "PASS: NAT POSTROUTING rule for 10.8.0.0/24 exists" echo "firewalld detected, checking masquerade..."
break 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
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
echo "FAIL: OpenVPN port not found in firewalld"
firewall-cmd --list-ports
exit 1
fi
# Verify VPN subnet rich rule exists
if firewall-cmd --list-rich-rules | grep -q 'source address="10.8.0.0/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
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 "10.8.0.0"; then
echo "PASS: NAT POSTROUTING rule for 10.8.0.0/24 exists"
break
fi
sleep 1
done
if ! iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "FAIL: NAT POSTROUTING rule for 10.8.0.0/24 not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true
exit 1
fi fi
sleep 1
done
if ! iptables -t nat -L POSTROUTING -n | grep -q "10.8.0.0"; then
echo "FAIL: NAT POSTROUTING rule for 10.8.0.0/24 not found"
echo "Current NAT rules:"
iptables -t nat -L POSTROUTING -n -v
systemctl status iptables-openvpn 2>&1 || true
exit 1
fi fi
# Verify IP forwarding is enabled # Verify IP forwarding is enabled