diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml index 94c90eb..c8cd01a 100644 --- a/.github/workflows/docker-test.yml +++ b/.github/workflows/docker-test.yml @@ -98,6 +98,15 @@ jobs: name: tls-crypt-v2 sig: "1" key_file: tls-crypt-v2.key + # Test nftables support on Debian + - os: + name: debian-12-nftables + image: debian:12 + enable_nftables: true + tls: + name: tls-crypt-v2 + sig: "1" + key_file: tls-crypt-v2.key name: ${{ matrix.os.name }} steps: @@ -113,6 +122,7 @@ jobs: docker build \ --build-arg BASE_IMAGE=${{ matrix.os.image }} \ --build-arg ENABLE_FIREWALLD=${{ matrix.os.enable_firewalld && 'y' || 'n' }} \ + --build-arg ENABLE_NFTABLES=${{ matrix.os.enable_nftables && 'y' || 'n' }} \ -t openvpn-server \ -f test/Dockerfile.server . @@ -269,7 +279,7 @@ jobs: - name: Show install script log if: always() run: | - docker cp openvpn-server:/opt/openvpn-install.log /tmp/openvpn-install.log 2>/dev/null && \ + docker cp openvpn-server:/root/openvpn-install.log /tmp/openvpn-install.log 2>/dev/null && \ cat /tmp/openvpn-install.log || echo "No install log found" - name: Show client logs diff --git a/README.md b/README.md index e9764f6..732dd16 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ 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 -- Firewall rules and forwarding managed seamlessly (native firewalld support, iptables fallback) +- Firewall rules and forwarding managed seamlessly (native firewalld and nftables 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) diff --git a/docker-compose.yml b/docker-compose.yml index 7b90969..f7f0411 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: dockerfile: test/Dockerfile.server args: BASE_IMAGE: ${BASE_IMAGE:-ubuntu:24.04} + ENABLE_FIREWALLD: ${ENABLE_FIREWALLD:-n} + ENABLE_NFTABLES: ${ENABLE_NFTABLES:-n} container_name: openvpn-server hostname: openvpn-server privileged: true diff --git a/openvpn-install.sh b/openvpn-install.sh index afbbf4f..b79ace5 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1436,8 +1436,52 @@ verb 3" >>/etc/openvpn/server/server.conf fi run_cmd "Reloading firewalld" firewall-cmd --reload + elif systemctl is-active --quiet nftables; then + # Use nftables native rules for systems with nftables active + log_info "nftables detected, configuring nftables rules..." + run_cmd_fatal "Creating nftables directory" mkdir -p /etc/nftables + + # Create nftables rules file + echo "table inet openvpn { + chain input { + type filter hook input priority 0; policy accept; + iifname \"tun0\" accept + iifname \"$NIC\" $PROTOCOL dport $PORT accept + } + + chain forward { + type filter hook forward priority 0; policy accept; + iifname \"$NIC\" oifname \"tun0\" accept + iifname \"tun0\" oifname \"$NIC\" accept + } +} + +table ip openvpn-nat { + chain postrouting { + type nat hook postrouting priority 100; policy accept; + ip saddr 10.8.0.0/24 oifname \"$NIC\" masquerade + } +}" >/etc/nftables/openvpn.nft + + if [[ $IPV6_SUPPORT == 'y' ]]; then + echo " +table ip6 openvpn-nat { + chain postrouting { + type nat hook postrouting priority 100; policy accept; + ip6 saddr fd42:42:42:42::/112 oifname \"$NIC\" masquerade + } +}" >>/etc/nftables/openvpn.nft + fi + + # Add include to nftables.conf if not already present + 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' + fi + + # Reload nftables to apply rules + run_cmd "Reloading nftables" systemctl reload nftables else - # Use iptables for systems without firewalld + # Use iptables for systems without firewalld or nftables run_cmd_fatal "Creating iptables directory" mkdir -p /etc/iptables # Script to add rules @@ -2143,6 +2187,14 @@ function removeOpenVPN() { 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/nftables/openvpn.nft ]]; then + # nftables was used + # Delete tables (suppress errors in case tables don't exist) + nft delete table inet openvpn 2>/dev/null || true + nft delete table ip 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 nftables rules file" rm -f /etc/nftables/openvpn.nft elif [[ -f /etc/systemd/system/iptables-openvpn.service ]]; then # iptables was used run_cmd "Stopping iptables service" systemctl stop iptables-openvpn diff --git a/test/Dockerfile.server b/test/Dockerfile.server index e27a3c8..7eae918 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -7,32 +7,40 @@ FROM ${BASE_IMAGE} ARG BASE_IMAGE # Set to "y" to install and enable firewalld for testing ARG ENABLE_FIREWALLD=n +# Set to "y" to install and enable nftables for testing +ARG ENABLE_NFTABLES=n ENV DEBIAN_FRONTEND=noninteractive ENV ENABLE_FIREWALLD=${ENABLE_FIREWALLD} +ENV ENABLE_NFTABLES=${ENABLE_NFTABLES} # Install basic dependencies based on the OS # dnsutils/bind-utils provides dig for DNS testing with Unbound RUN if command -v apt-get >/dev/null; then \ apt-get update && apt-get install -y --no-install-recommends \ iproute2 iptables curl procps systemd systemd-sysv dnsutils \ + && if [ "$ENABLE_NFTABLES" = "y" ]; then apt-get install -y --no-install-recommends nftables; fi \ && rm -rf /var/lib/apt/lists/*; \ 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 \ + && if [ "$ENABLE_NFTABLES" = "y" ]; then dnf install -y nftables; 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 \ + && if [ "$ENABLE_NFTABLES" = "y" ]; then yum install -y nftables; fi \ && yum clean all; \ elif command -v pacman >/dev/null; then \ pacman -Syu --noconfirm \ iproute2 iptables curl procps-ng bind \ + && if [ "$ENABLE_NFTABLES" = "y" ]; then pacman -S --noconfirm nftables; fi \ && pacman -Scc --noconfirm; \ elif command -v zypper >/dev/null; then \ zypper install -y \ iproute2 iptables curl procps systemd tar gzip bind-utils gawk \ + && if [ "$ENABLE_NFTABLES" = "y" ]; then zypper install -y nftables; fi \ && zypper clean -a; \ fi @@ -41,6 +49,14 @@ RUN if [ "$ENABLE_FIREWALLD" = "y" ] && command -v firewall-cmd >/dev/null; then systemctl enable firewalld; \ fi +# Enable nftables if requested (must be done after systemd is available) +# Use empty nftables.conf - do NOT flush ruleset as it removes Docker's networking rules +RUN if [ "$ENABLE_NFTABLES" = "y" ] && command -v nft >/dev/null; then \ + systemctl enable nftables; \ + mkdir -p /etc/nftables; \ + echo '#!/usr/sbin/nft -f' > /etc/nftables.conf; \ + 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 4ea0bfc..cb66425 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -87,9 +87,11 @@ REQUIRED_FILES=( /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 +# Only check for iptables script if firewalld and nftables are not active +if ! systemctl is-active --quiet firewalld && ! systemctl is-active --quiet nftables; then REQUIRED_FILES+=(/etc/iptables/add-openvpn-rules.sh) +elif systemctl is-active --quiet nftables; then + REQUIRED_FILES+=(/etc/nftables/openvpn.nft) fi for f in "${REQUIRED_FILES[@]}"; do @@ -422,6 +424,46 @@ if systemctl is-active --quiet firewalld; then firewall-cmd --list-rich-rules exit 1 fi +elif systemctl is-active --quiet nftables; then + # nftables mode - verify OpenVPN tables exist + echo "nftables detected, checking OpenVPN tables..." + for _ in $(seq 1 10); do + if nft list table inet openvpn >/dev/null 2>&1; then + echo "PASS: nftables 'inet openvpn' table exists" + break + fi + sleep 1 + done + if ! nft list table inet openvpn >/dev/null 2>&1; then + echo "FAIL: nftables 'inet openvpn' table not found" + echo "Current nftables ruleset:" + nft list ruleset 2>&1 || true + exit 1 + fi + # Verify NAT table exists + if nft list table ip openvpn-nat >/dev/null 2>&1; then + echo "PASS: nftables 'ip openvpn-nat' table exists" + else + echo "FAIL: nftables 'ip openvpn-nat' table not found" + nft list ruleset 2>&1 || true + exit 1 + fi + # Verify masquerade rule exists + if nft list table ip openvpn-nat | grep -q "masquerade"; then + echo "PASS: nftables masquerade rule exists" + else + echo "FAIL: nftables masquerade rule not found" + nft list table ip openvpn-nat 2>&1 || true + exit 1 + fi + # Verify include in nftables.conf + if grep -q 'include.*/etc/nftables/openvpn.nft' /etc/nftables.conf; then + echo "PASS: OpenVPN rules included in nftables.conf" + else + echo "FAIL: OpenVPN rules not included in nftables.conf" + cat /etc/nftables.conf 2>&1 || true + exit 1 + fi else # iptables mode - verify NAT rules echo "iptables mode, checking NAT rules..."