From d8aa6256398d19474ed4b6aab4ee421060ccd7ab Mon Sep 17 00:00:00 2001 From: Stanislas Date: Sat, 13 Dec 2025 20:49:40 +0100 Subject: [PATCH] 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 --- .github/workflows/docker-test.yml | 10 ++++ FAQ.md | 4 +- README.md | 4 +- openvpn-install.sh | 86 +++++++++++++++++++---------- test/Dockerfile.server | 10 ++++ test/server-entrypoint.sh | 90 +++++++++++++++++++++++-------- 6 files changed, 148 insertions(+), 56 deletions(-) diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 8908876..94c90eb 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -89,6 +89,15 @@ jobs: name: tls-auth sig: "3" 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 }} steps: @@ -103,6 +112,7 @@ jobs: run: | docker build \ --build-arg BASE_IMAGE=${{ matrix.os.image }} \ + --build-arg ENABLE_FIREWALLD=${{ matrix.os.enable_firewalld && 'y' || 'n' }} \ -t openvpn-server \ -f test/Dockerfile.server . diff --git a/FAQ.md b/FAQ.md index f05bfdb..ef94482 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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` diff --git a/README.md b/README.md index b3b34e3..f3f9675 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ export CLIENTNUMBER="1" # Revokes the first client in the list - Installs and configures a ready-to-use OpenVPN server - 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 -- Iptables rules and forwarding managed in a seamless way -- If needed, the script can cleanly remove OpenVPN, including configuration and iptables rules +- Firewall rules and forwarding managed seamlessly (native firewalld support, iptables fallback) +- 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) - 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 diff --git a/openvpn-install.sh b/openvpn-install.sh index b3c9aa0..97fe45b 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1419,48 +1419,66 @@ verb 3" >>/etc/openvpn/server/server.conf installUnbound fi - # Add iptables rules in two scripts + # Configure firewall rules log_info "Configuring firewall rules..." - run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables - # Script to add rules - echo "#!/bin/sh + if systemctl is-active --quiet firewalld; then + # 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 -I INPUT 1 -i 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 INPUT 1 -i $NIC -p $PROTOCOL --dport $PORT -j ACCEPT" >/etc/iptables/add-openvpn-rules.sh - if [[ $IPV6_SUPPORT == 'y' ]]; then - echo "ip6tables -t nat -I POSTROUTING 1 -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE + if [[ $IPV6_SUPPORT == 'y' ]]; then + 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 FORWARD 1 -i $NIC -o tun0 -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 - fi + fi - # Script to remove rules - echo "#!/bin/sh + # Script to remove rules + echo "#!/bin/sh iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o $NIC -j MASQUERADE iptables -D INPUT -i tun0 -j ACCEPT iptables -D FORWARD -i $NIC -o tun0 -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 - if [[ $IPV6_SUPPORT == 'y' ]]; then - echo "ip6tables -t nat -D POSTROUTING -s fd42:42:42:42::/112 -o $NIC -j MASQUERADE + if [[ $IPV6_SUPPORT == 'y' ]]; then + 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 FORWARD -i $NIC -o tun0 -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 - fi + fi - 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 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 - # Handle the rules via a systemd script - echo "[Unit] + # Handle the rules via a systemd script + echo "[Unit] Description=iptables rules for OpenVPN +After=firewalld.service Before=network-online.target Wants=network-online.target @@ -1473,10 +1491,11 @@ RemainAfterExit=yes [Install] WantedBy=multi-user.target" >/etc/systemd/system/iptables-openvpn.service - # Enable service and apply rules - run_cmd "Reloading systemd" systemctl daemon-reload - run_cmd "Enabling iptables service" systemctl enable iptables-openvpn - run_cmd "Starting iptables service" systemctl start iptables-openvpn + # Enable service and apply rules + run_cmd "Reloading systemd" systemctl daemon-reload + run_cmd "Enabling iptables service" systemctl enable 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 [[ $ENDPOINT != "" ]]; then @@ -2057,15 +2076,24 @@ function removeOpenVPN() { # Remove customised service run_cmd "Removing service file" rm -f /etc/systemd/system/openvpn-server@.service - # Remove the iptables rules related to the script - log_info "Removing iptables rules..." - run_cmd "Stopping iptables service" systemctl stop iptables-openvpn - # Cleanup - 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 /etc/iptables/add-openvpn-rules.sh - run_cmd "Removing iptables rm script" rm /etc/iptables/rm-openvpn-rules.sh + # Remove firewall rules + log_info "Removing firewall rules..." + if systemctl is-active --quiet firewalld && firewall-cmd --list-ports | grep -q "$PORT/$PROTOCOL"; then + # firewalld was used + run_cmd "Removing OpenVPN port from firewalld" firewall-cmd --permanent --remove-port="$PORT/$PROTOCOL" + run_cmd "Removing masquerade from firewalld" firewall-cmd --permanent --remove-masquerade + 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 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 "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 if hash sestatus 2>/dev/null; then diff --git a/test/Dockerfile.server b/test/Dockerfile.server index 6f9438c..e27a3c8 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -5,7 +5,10 @@ ARG BASE_IMAGE=ubuntu:24.04 FROM ${BASE_IMAGE} ARG BASE_IMAGE +# Set to "y" to install and enable firewalld for testing +ARG ENABLE_FIREWALLD=n ENV DEBIAN_FRONTEND=noninteractive +ENV ENABLE_FIREWALLD=${ENABLE_FIREWALLD} # Install basic dependencies based on the OS # 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 \ dnf install -y --allowerasing \ iproute iptables curl procps-ng systemd tar gzip bind-utils \ + && if [ "$ENABLE_FIREWALLD" = "y" ]; then dnf install -y firewalld; fi \ && dnf clean all; \ elif command -v yum >/dev/null; then \ yum install -y \ iproute iptables curl procps-ng systemd tar gzip bind-utils \ + && if [ "$ENABLE_FIREWALLD" = "y" ]; then yum install -y firewalld; fi \ && yum clean all; \ elif command -v pacman >/dev/null; then \ pacman -Syu --noconfirm \ @@ -31,6 +36,11 @@ RUN if command -v apt-get >/dev/null; then \ && zypper clean -a; \ 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) RUN mkdir -p /dev/net diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index a909836..4ea0bfc 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -77,15 +77,22 @@ fi # Verify all expected files were created echo "Verifying installation..." MISSING_FILES=0 -for f in \ - /etc/openvpn/server/server.conf \ - /etc/openvpn/server/ca.crt \ - /etc/openvpn/server/ca.key \ - "/etc/openvpn/server/$TLS_KEY_FILE" \ - /etc/openvpn/server/crl.pem \ - /etc/openvpn/server/easy-rsa/pki/ca.crt \ - /etc/iptables/add-openvpn-rules.sh \ - /root/testclient.ovpn; do +# Build list of required files +REQUIRED_FILES=( + /etc/openvpn/server/server.conf + /etc/openvpn/server/ca.crt + /etc/openvpn/server/ca.key + "/etc/openvpn/server/$TLS_KEY_FILE" + /etc/openvpn/server/crl.pem + /etc/openvpn/server/easy-rsa/pki/ca.crt + /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 echo "ERROR: Missing file: $f" MISSING_FILES=$((MISSING_FILES + 1)) @@ -380,21 +387,58 @@ echo "" # Verify OpenVPN server (started by systemd via install script) echo "Verifying OpenVPN server..." -# Verify iptables NAT rules exist (applied by iptables-openvpn service) -echo "Verifying iptables 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 +# Verify firewall rules exist +echo "Verifying firewall rules..." +if systemctl is-active --quiet firewalld; then + # firewalld is active - verify masquerade is enabled + echo "firewalld detected, checking masquerade..." + for _ in $(seq 1 10); do + if firewall-cmd --query-masquerade 2>/dev/null; then + echo "PASS: firewalld masquerade is enabled" + break + fi + sleep 1 + done + if ! firewall-cmd --query-masquerade 2>/dev/null; then + echo "FAIL: firewalld masquerade is not enabled" + echo "Current firewalld config:" + firewall-cmd --list-all 2>&1 || true + 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 - 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 # Verify IP forwarding is enabled