From c2b1b6999dc03b07849410e90f856993194a1d74 Mon Sep 17 00:00:00 2001 From: Stanislas Lange Date: Tue, 4 Aug 2026 11:55:40 +0200 Subject: [PATCH] Fix firewall backend installation --- openvpn-install.sh | 48 ++++++++++++++++--------- test/Dockerfile.server | 5 +-- test/interactive-install-flow.sh | 60 ++++++++++++++++++++++++++++++++ test/server-entrypoint.sh | 16 +++++++++ 4 files changed, 110 insertions(+), 19 deletions(-) create mode 100755 test/interactive-install-flow.sh diff --git a/openvpn-install.sh b/openvpn-install.sh index 6b98872..3c7dd3b 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -1854,8 +1854,9 @@ cmd_interactive() { if isOpenVPNInstalled; then manageMenu else - installQuestions - installOpenVPN + # Reuse the install command so interactive installs receive the same + # validation and derived network configuration as non-interactive installs. + cmd_install --interactive fi } @@ -2429,6 +2430,17 @@ function detect_server_ips() { fi } +# Select the active firewall manager, with iptables as the fallback. +function detect_firewall_backend() { + if systemctl is-active --quiet firewalld; then + echo firewalld + elif systemctl is-active --quiet nftables; then + echo nftables + else + echo iptables + fi +} + # Calculate derived network configuration values function prepare_network_config() { # Calculate IPv4 gateway (always needed for leak prevention) @@ -3047,6 +3059,10 @@ function installOpenVPN() { fi fi + # Select the firewall backend before installing dependencies so native + # firewalld and nftables systems do not need the iptables package. + FIREWALL_BACKEND=$(detect_firewall_backend) + # If OpenVPN isn't installed yet, install it. This script is more-or-less # idempotent on multiple runs, but will only install OpenVPN from upstream # the first time. @@ -3057,21 +3073,26 @@ function installOpenVPN() { installOpenVPNRepo log_info "Installing OpenVPN and dependencies..." - # socat is used for communicating with the OpenVPN management interface (client disconnect on revoke) + # iptables is only required by the fallback backend. socat communicates + # with the OpenVPN management interface for client disconnect on revoke. + local -a firewall_packages=() + if [[ $FIREWALL_BACKEND == 'iptables' ]]; then + firewall_packages+=(iptables) + fi if [[ $OS =~ (debian|ubuntu) ]]; then - run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils socat + run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn "${firewall_packages[@]}" openssl curl ca-certificates tar dnsutils socat elif [[ $OS == 'centos' ]]; then - run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat 'policycoreutils-python*' + run_cmd_fatal "Installing OpenVPN" yum install -y openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind-utils socat 'policycoreutils-python*' elif [[ $OS == 'oracle' ]]; then - run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils + run_cmd_fatal "Installing OpenVPN" yum install -y openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils elif [[ $OS == 'amzn2023' ]]; then - run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat + run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind-utils socat elif [[ $OS == 'fedora' ]]; then - run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils + run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils elif [[ $OS == 'opensuse' ]]; then - run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat + run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind-utils socat elif [[ $OS == 'arch' ]]; then - run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind socat + run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn "${firewall_packages[@]}" openssl ca-certificates curl tar bind socat fi # Verify ChaCha20-Poly1305 compatibility if selected @@ -3493,13 +3514,6 @@ verb 3" } >>/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" diff --git a/test/Dockerfile.server b/test/Dockerfile.server index 2886b18..4442a49 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -18,7 +18,7 @@ ENV ENABLE_NFTABLES=${ENABLE_NFTABLES} # Note: socat is installed by openvpn-install.sh during OpenVPN installation 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 jq \ + iproute2 curl procps systemd systemd-sysv dnsutils jq \ && 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 \ @@ -69,7 +69,8 @@ RUN chmod +x /opt/openvpn-install.sh COPY test/server-entrypoint.sh /entrypoint.sh COPY test/validate-output.sh /opt/test/validate-output.sh COPY test/local-network-detection.sh /opt/test/local-network-detection.sh -RUN chmod +x /entrypoint.sh /opt/test/validate-output.sh /opt/test/local-network-detection.sh +COPY test/interactive-install-flow.sh /opt/test/interactive-install-flow.sh +RUN chmod +x /entrypoint.sh /opt/test/validate-output.sh /opt/test/local-network-detection.sh /opt/test/interactive-install-flow.sh # Create systemd service for the test script # PassEnvironment passes Docker env vars (-e) from PID 1 to the service diff --git a/test/interactive-install-flow.sh b/test/interactive-install-flow.sh new file mode 100755 index 0000000..56e0efe --- /dev/null +++ b/test/interactive-install-flow.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# shellcheck disable=SC1091,SC2034 +# SC1091: The installer path is provided by the test environment. +# SC2034: Configuration globals are consumed by sourced installer functions. +set -euo pipefail + +INSTALLER=${1:-/opt/openvpn-install.sh} +export FORCE_COLOR=0 LOG_FILE="" NON_INTERACTIVE_INSTALL=n OUTPUT_FORMAT=table +# shellcheck source=../openvpn-install.sh +source "$INSTALLER" + +questions_called=n +validation_called=n +install_called=n + +isOpenVPNInstalled() { + return 1 +} + +requireNoOpenVPN() { + return 0 +} + +installQuestions() { + questions_called=y + VPN_SUBNET_IPV4=10.23.0.0 + VPN_SUBNET_IPV6=fd42:23:: + CLIENT_IPV6=n + ROUTE_INTERNET=y +} + +validate_configuration() { + validation_called=y +} + +installOpenVPN() { + install_called=y + if [[ ${VPN_GATEWAY_IPV4:-} != "10.23.0.1" ]]; then + echo "FAIL: Interactive install did not prepare the IPv4 VPN gateway" >&2 + exit 1 + fi + if [[ ${IPV6_SUPPORT:-} != "n" ]]; then + echo "FAIL: Interactive install did not prepare legacy IPv6 state" >&2 + exit 1 + fi +} + +cmd_interactive + +for state in \ + "questions_called:$questions_called" \ + "validation_called:$validation_called" \ + "install_called:$install_called"; do + if [[ ${state#*:} != "y" ]]; then + echo "FAIL: Interactive install skipped ${state%%:*}" >&2 + exit 1 + fi +done + +echo "PASS: Interactive install prepares derived network configuration" diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 25aa0a9..af0f893 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -4,6 +4,7 @@ set -e echo "=== OpenVPN Server Container ===" /opt/test/local-network-detection.sh /opt/openvpn-install.sh +/opt/test/interactive-install-flow.sh /opt/openvpn-install.sh # Create TUN device if it doesn't exist if [ ! -c /dev/net/tun ]; then @@ -185,6 +186,21 @@ if [ "$INSTALL_EXIT_CODE" -ne 0 ]; then exit 1 fi +# Native firewall backends must not require the iptables package. The fallback +# backend must install it because the test image does not include it. +if systemctl is-active --quiet firewalld || systemctl is-active --quiet nftables; then + if command -v dpkg-query >/dev/null && dpkg-query -W -f='${db:Status-Abbrev}' iptables 2>/dev/null | grep -q '^ii'; then + echo "FAIL: iptables was installed with a native firewall backend" + exit 1 + fi +else + if ! command -v iptables >/dev/null; then + echo "FAIL: iptables fallback dependency was not installed" + exit 1 + fi +fi +echo "PASS: Firewall dependencies match the selected backend" + # Verify all expected files were created echo "Verifying installation..." MISSING_FILES=0