diff --git a/csf.pl b/csf.pl index bcc323c..c4d94e1 100644 --- a/csf.pl +++ b/csf.pl @@ -944,6 +944,7 @@ sub dostart { &getethdev; &modprobe; + &checkstatemodule; $noowner = 0; if ($config{VPS} and $config{SMTP_BLOCK}) { @@ -5783,6 +5784,58 @@ sub modprobe { } # end modprobe ############################################################################### +# start checkstatemodule +# The SPI ruleset depends on the kernel state/conntrack match for the +# ESTABLISHED/RELATED accept rules that allow reply and outbound traffic. If +# the match is unusable (e.g. EL10-family minimal installs without +# kernel-modules-extra), applying the ruleset would set DROP policies without +# those accepts and silently block all outbound TCP, or leave a half-applied +# firewall. Verify the match works before any rules or policies are applied, +# and fail safe via error() (policies reset to ACCEPT, exit 1) with an +# actionable message if it does not. This guards both the FASTSTART and the +# rule-by-rule startup paths +sub checkstatemodule { + unless ($config{LF_SPI} or ($config{IPV6} and $config{IPV6_SPI})) {return} + + my @checks; + if ($config{LF_SPI}) {push @checks, $config{IPTABLES}} + if ($config{IPV6} and $config{IPV6_SPI}) {push @checks, $config{IP6TABLES}} + + foreach my $iptables (@checks) { + my ($childin, $childout); + my $cmdpid = open3($childin, $childout, $childout, "$iptables $config{IPTABLESWAIT} -I OUTPUT -p tcp --dport 9999 $statemodule ESTABLISHED -j ACCEPT"); + my @output = <$childout>; + waitpid ($cmdpid, 0); + my $status = $? >> 8; + chomp @output; + if ($output[0] =~ /# Warning: iptables-legacy tables present/) {shift @output} + + my ($childdin, $childdout); + my $delpid = open3($childdin, $childdout, $childdout, "$iptables $config{IPTABLESWAIT} -D OUTPUT -p tcp --dport 9999 $statemodule ESTABLISHED -j ACCEPT"); + my @deloutput = <$childdout>; + waitpid ($delpid, 0); + + if ($status != 0) { + my $reason = $output[0]; + if ($reason eq "") {$reason = "exit status $status"} + my $hint = ""; + my $el = 0; + if (open (my $OSREL, "<", "/etc/os-release")) { + while (my $osline = <$OSREL>) { + if ($osline =~ /^PLATFORM_ID=["']?platform:el(\d+)/) {$el = $1} + } + close ($OSREL); + } + if ($el >= 10) { + $hint = " On EL${el} minimal installs the xt_* netfilter kernel modules are shipped in the separate kernel-modules-extra package - run: dnf install kernel-modules-extra-\$(uname -r), reboot if the modules still fail to load, then try again. If this is a container, the host kernel must provide these modules."; + } + &error(__LINE__,"*Error* The iptables state module [$statemodule] is not usable with the running kernel via [$iptables] [$reason]. csf cannot create the stateful rules that allow reply/outbound traffic, so starting the firewall would block all connectivity (e.g. all outbound TCP). The firewall has NOT been started and has been reset to ACCEPT.$hint Run 'perl /usr/local/csf/bin/csftest.pl' for a full report"); + } + } + return; +} +# end checkstatemodule +############################################################################### # start faststart sub faststart { my $text = shift; diff --git a/csftest.pl b/csftest.pl index 1b2807e..937a059 100644 --- a/csftest.pl +++ b/csftest.pl @@ -24,10 +24,17 @@ umask(0177); -our ($return, $fatal, $error); +our ($return, $fatal, $error, $modulefail); $fatal = 0; $error = 0; +$modulefail = 0; + +# Each test inserts a probe rule and then always attempts to delete it, even +# when the insert is treated as failed. On some backends (e.g. iptables-nft on +# EL10 without kernel-modules-extra) an insert can succeed while still printing +# a warning, and skipping the delete in that case leaks the probe rule into the +# live ruleset #my @modules = ("ip_tables","ipt_state","ipt_multiport","iptable_filter","ipt_limit","ipt_LOG","ipt_REJECT","ipt_conntrack","ip_conntrack","ip_conntrack_ftp","iptable_mangle","ip_tables","xt_state","xt_multiport","iptable_filter","xt_limit","ipt_LOG","ipt_REJECT","ip_conntrack_ftp","iptable_mangle","xt_conntrack"); #push @modules,"ipt_owner"; @@ -39,118 +46,144 @@ print "Testing ip_tables/iptable_filter..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -j ACCEPT"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j ACCEPT"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j ACCEPT"); } print "Testing ipt_LOG..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -j LOG"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j LOG"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j LOG"); } print "Testing ipt_multiport/xt_multiport..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp -m multiport --dports 9998,9999 -j LOG"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp -m multiport --dports 9998,9999 -j LOG"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp -m multiport --dports 9998,9999 -j LOG"); } print "Testing ipt_REJECT..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -j REJECT"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j REJECT"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -j REJECT"); } print "Testing ipt_state/xt_state..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -m state --state NEW -j LOG"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m state --state NEW -j LOG"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m state --state NEW -j LOG"); } print "Testing ipt_limit/xt_limit..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -m limit --limit 30/m --limit-burst 5 -j LOG"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m limit --limit 30/m --limit-burst 5 -j LOG"); if ($return ne "") { print "FAILED [FATAL Error: $return] - Required for csf to function\n"; $fatal++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m limit --limit 30/m --limit-burst 5 -j LOG"); } print "Testing ipt_recent..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -m recent --set"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m recent --set"); if ($return ne "") { print "FAILED [Error: $return] - Required for PORTFLOOD and PORTKNOCKING features\n"; $error++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m recent --set"); } print "Testing xt_connlimit..."; $return = &testiptables("/sbin/iptables -I INPUT -p tcp --dport 9999 -m connlimit --connlimit-above 100 -j REJECT --reject-with tcp-reset"); +&testiptables("/sbin/iptables -D INPUT -p tcp --dport 9999 -m connlimit --connlimit-above 100 -j REJECT --reject-with tcp-reset"); if ($return ne "") { print "FAILED [Error: $return] - Required for CONNLIMIT feature\n"; $error++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D INPUT -p tcp --dport 9999 -m connlimit --connlimit-above 100 -j REJECT --reject-with tcp-reset"); } print "Testing ipt_owner/xt_owner..."; $return = &testiptables("/sbin/iptables -I OUTPUT -p tcp --dport 9999 -m owner --uid-owner 0 -j LOG"); +&testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m owner --uid-owner 0 -j LOG"); if ($return ne "") { print "FAILED [Error: $return] - Required for SMTP_BLOCK and UID/GID blocking features\n"; $error++; } else { print "OK\n"; - &testiptables("/sbin/iptables -D OUTPUT -p tcp --dport 9999 -m owner --uid-owner 0 -j LOG"); } print "Testing iptable_nat/ipt_REDIRECT..."; $return = &testiptables("/sbin/iptables -t nat -I OUTPUT -p tcp --dport 9999 -j REDIRECT --to-ports 9900"); +&testiptables("/sbin/iptables -t nat -D OUTPUT -p tcp --dport 9999 -j REDIRECT --to-ports 9900"); if ($return ne "") { print "FAILED [Error: $return] - Required for MESSENGER feature\n"; $error++; } else { print "OK\n"; - &testiptables("/sbin/iptables -t nat -D OUTPUT -p tcp --dport 9999 -j REDIRECT --to-ports 9900"); } print "Testing iptable_nat/ipt_DNAT..."; $return = &testiptables("/sbin/iptables -t nat -I PREROUTING -p tcp --dport 9999 -j DNAT --to-destination 192.168.254.1"); +&testiptables("/sbin/iptables -t nat -D PREROUTING -p tcp --dport 9999 -j DNAT --to-destination 192.168.254.1"); if ($return ne "") { print "FAILED [Error: $return] - Required for csf.redirect feature\n"; $error++; } else { print "OK\n"; - &testiptables("/sbin/iptables -t nat -D PREROUTING -p tcp --dport 9999 -j DNAT --to-destination 192.168.254.1"); } if ($fatal) {print "\nRESULT: csf will not function on this server due to FATAL errors from missing modules [$fatal]\n"} elsif ($error) {print "\nRESULT: csf will function on this server but some features will not work due to some missing iptables modules [$error]\n"} else {print "\nRESULT: csf should function on this server\n"} +if (($fatal or $error) and $modulefail) { + my $el = 0; + if (open (my $OSREL, "<", "/etc/os-release")) { + while (my $line = <$OSREL>) { + if ($line =~ /^PLATFORM_ID=["']?platform:el(\d+)/) {$el = $1} + } + close ($OSREL); + } + if ($el >= 10) { + print <; waitpid ($cmdpid, 0); chomp @ipdata; + if ($ipdata[0] =~ /not supported|missing kernel module|No chain\/target\/match|Couldn.t load match|Couldn.t find target/i) {$modulefail = 1} return $ipdata[0]; } diff --git a/install.sh b/install.sh index c64c9d6..f66cd71 100644 --- a/install.sh +++ b/install.sh @@ -18,6 +18,86 @@ # this program; if not, see . ############################################################################### +# The stateful (SPI) ruleset csf builds depends on the kernel xt_* netfilter +# extension modules. EL10-family minimal installs (AlmaLinux/Rocky/RHEL 10+) +# ship these in the separate kernel-modules-extra package, and without them +# enabling csf can leave the server with DROP policies but no conntrack +# accepts, blocking all outbound TCP. Probe for a working state/conntrack +# match before installing; on EL10-family try to install the missing package +# and abort with a clear message if the kernel still cannot use it. +state_match_works() { + iptables_probe_ok=0 + if iptables -I OUTPUT -p tcp --dport 9999 -m conntrack --ctstate ESTABLISHED -j ACCEPT >/dev/null 2>&1; then + iptables_probe_ok=1 + fi + # Always remove the probe rule - some backends insert successfully while + # still emitting warnings, and a failed delete is harmless + iptables -D OUTPUT -p tcp --dport 9999 -m conntrack --ctstate ESTABLISHED -j ACCEPT >/dev/null 2>&1 + if [ "$iptables_probe_ok" = "1" ]; then + return 0 + fi + if iptables -I OUTPUT -p tcp --dport 9999 -m state --state ESTABLISHED -j ACCEPT >/dev/null 2>&1; then + iptables_probe_ok=1 + fi + iptables -D OUTPUT -p tcp --dport 9999 -m state --state ESTABLISHED -j ACCEPT >/dev/null 2>&1 + [ "$iptables_probe_ok" = "1" ] +} + +check_kernel_modules() { + # iptables may not be installed yet - the panel installers pull it in as a + # dependency, and csf itself refuses to start if the modules are unusable + command -v iptables >/dev/null 2>&1 || return 0 + modprobe xt_conntrack >/dev/null 2>&1 + + if state_match_works; then + return 0 + fi + + el_major="" + if [ -r /etc/os-release ]; then + el_major=$(sed -n 's/^PLATFORM_ID="*platform:el\([0-9]*\).*/\1/p' /etc/os-release) + fi + + if [ -n "$el_major" ] && [ "$el_major" -ge 10 ] 2>/dev/null && command -v dnf >/dev/null 2>&1; then + echo + echo "The iptables state/conntrack match is not usable with the running kernel." + echo "EL${el_major} minimal installs ship the xt_* netfilter kernel modules in the" + echo "separate kernel-modules-extra package. Attempting to install it..." + echo + dnf -y install "kernel-modules-extra-$(uname -r)" || dnf -y install kernel-modules-extra + modprobe xt_conntrack >/dev/null 2>&1 + if state_match_works; then + echo + echo "kernel-modules-extra installed and the netfilter modules now work - continuing" + echo + return 0 + fi + echo + echo "ERROR: The xt_* netfilter kernel modules are still not usable, so csf cannot" + echo "create the stateful firewall rules that allow reply/outbound traffic. Enabling" + echo "csf in this state would block all outbound TCP connections, so the installation" + echo "has been aborted." + echo + echo "If kernel-modules-extra was installed for a newer kernel than the one running," + echo "reboot into that kernel and re-run this installer:" + echo + echo " dnf install kernel-modules-extra-\$(uname -r)" + echo " reboot" + echo + echo "If this is a container, the host kernel must provide these modules." + exit 1 + fi + + echo + echo "WARNING: The iptables state/conntrack match does not appear to be usable with" + echo "the running kernel. csf may not be able to create its stateful firewall rules." + echo "After installation, run 'perl /usr/local/csf/bin/csftest.pl' and resolve any" + echo "FATAL errors before enabling csf." + echo +} + +check_kernel_modules + echo echo "Selecting installer..." echo