# The LearningOnline Network with CAPA # Firewall configuration to allow internal LON-CAPA communication between servers # # $Id: Firewall.pm,v 1.24 2021/12/21 13:57:47 raeburn Exp $ # # The LearningOnline Network with CAPA # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # Startup script for the LON-CAPA network processes # package LONCAPA::Firewall; use strict; use lib '/home/httpd/perl/lib'; use LONCAPA::Configuration; use LONCAPA; sub uses_firewalld { my ($distro) = @_; if ($distro eq '') { $distro = &get_distro(); } my ($inuse,$checkfirewalld); if ($distro =~ /^(suse|sles)([\d\.]+)$/) { if (($1 eq 'sles') && ($2 >= 15)) { $checkfirewalld = 1; } } elsif ($distro =~ /^fedora(\d+)$/) { if ($1 >= 18) { $checkfirewalld = 1; } } elsif ($distro =~ /^(?:centos|rhes|scientific|oracle|rocky|alma)(\d+)/) { if ($1 >= 7) { $checkfirewalld = 1; } } if ($checkfirewalld) { my ($loaded,$active); if (open(PIPE,"systemctl status firewalld 2>&1 |")) { while () { chomp(); if (/^\s*Loaded:\s+(\w+)/) { $loaded = $1; } if (/^\s*Active\s+(\w+)/) { $active = $1; } } close(PIPE); } if (($loaded eq 'loaded') || ($active eq 'active')) { $inuse = 1; } } return $inuse; } sub firewall_open_port { my ($iptables,$fw_chains,$lond_port,$iphost,$ports,$firewalld) = @_; return 'inactive firewall' if (!&firewall_is_active()); return 'port number unknown' if !$lond_port; return 'invalid firewall chain' unless (ref($fw_chains) eq 'ARRAY'); my (@opened,@okchains,$zone); if ($firewalld) { $zone = &get_default_zone(); return 'invalid zone' if ($zone eq ''); } else { my @badchains; foreach my $chain (@{$fw_chains}) { if ($chain =~ /^([\w\-]+)$/) { push(@okchains,$1); } else { push(@badchains,$chain); } } if (!@okchains) { return 'None of the chain names has the expected format.'."\n"; } } if (ref($ports) ne 'ARRAY') { return 'List of ports to open needed.'; } foreach my $portnum (@{$ports}) { my $port = ''; if ($portnum =~ /^(\d+)$/) { $port = $1; } else { print "Skipped non-numeric port: $portnum.\n"; next; } print "Opening firewall access on port $port.\n"; my $result; if ($port eq $lond_port) { # For lond port, restrict the servers allowed to attempt to communicate # to include only source IPs in the LON-CAPA cluster. my (@port_error,%command_error,@lond_port_open, @lond_port_curropen); if (ref($iphost) eq 'HASH') { if (keys(%{$iphost}) > 0) { my $count = scalar(keys(%{$iphost})); if ($count > 1) { print "Please be patient. Checking $count IPs.\n"; } my %curropen; if ($firewalld) { &firewall_close_anywhere($iptables,$zone,$port,$firewalld); my $current = &firewall_is_port_open($iptables,$zone,$port, $lond_port,$iphost,\%curropen, $firewalld); } else { foreach my $fw_chain (@okchains) { &firewall_close_anywhere($iptables,$fw_chain,$port); my $current = &firewall_is_port_open($iptables,$fw_chain,$port, $lond_port,$iphost,\%curropen); } } my $countok = 0; foreach my $key (keys(%{$iphost})) { my $ip = ''; if ($key =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) { if (($1<=255) && ($2<=255) && ($3<=255) && ($4<=255)) { $ip = "$1.$2.$3.$4"; } else { print "IP address: $key does not have expected format.\n"; next; } } else { print "IP address: $key does not have expected format.\n"; next; } if ($curropen{$ip}) { push(@lond_port_curropen,$ip); } else { if ($firewalld) { my $cmd = 'firewall-cmd --zone='.$zone.' --add-rich-rule \'rule family="ipv4" source address="'.$ip.'/32" port port="'.$port.'" protocol="tcp" accept\''; if (open(PIPE,"$cmd |")) { my $result = ; chomp($result); close(PIPE); if ($result eq 'success') { push(@lond_port_open,$ip); } else { push(@port_error,$ip); } } } else { foreach my $fw_chain (@okchains) { my $firewall_command = "$iptables -I $fw_chain -p tcp -s $ip -d 0/0 --dport $port -j ACCEPT"; system($firewall_command); my $return_status = $?>>8; if ($return_status == 1) { unless(grep(/^\Q$ip\E$/,@port_error)) { push(@port_error,$ip); } } elsif ($return_status == 2) { push(@{$command_error{$fw_chain}},$ip); } elsif ($return_status == 0) { push(@lond_port_open,$ip); last; } } } } if ($count > 1) { $countok ++; print '.'; if ($countok%40 == 0) { print "\n"; } } } if ($count > 1) { if ($countok%40) { print "\n"; } } } else { print "no key found in \$iphost hash ref.\n". "Domain Name Service (DNS) may not be available.\n". "If this LON-CAPA node is standalone, then you can fix this issue by modifying /etc/hosts.\n". "Use a text editor to add: IPaddress Hostname\n"; } } else { print "\$iphost is not a reference to a hash\n"; } if (@lond_port_curropen) { unless (grep(/^\Q$port\E$/,@opened)) { push(@opened,$port); } print "Port already open for ".scalar(@lond_port_curropen)." IP addresses.\n"; } if (@lond_port_open) { unless (grep(/^\Q$port\E$/,@opened)) { push(@opened,$port); } print "Port opened for ".scalar(@lond_port_open)." IP addresses.\n"; } if (@port_error) { print "Error opening port for following IP addresses: ".join(', ',@port_error)."\n"; } if (keys(%command_error) > 0) { foreach my $chain (sort(keys(%command_error))) { if (ref($command_error{$chain}) eq 'ARRAY') { if (@{$command_error{$chain}}) { print "Bad command error opening port for following IP addresses: ". join(', ',@{$command_error{$chain}})."\n". 'Command was: "'."$iptables -I $chain -p tcp -s ".'$ip'." -d 0/0 --dport $port -j ACCEPT".'", where $ip is IP address'."\n"; } } } } } else { if ($firewalld) { my ($port_error); my $cmd = 'firewall-cmd --zone='.$zone.' --add-rich-rule \'rule family="ipv4" port port="'.$port.'" protocol="tcp" accept\''; if (open(PIPE,"$cmd |")) { my $result = ; chomp($result); close(PIPE); if ($result eq 'success') { push(@opened,$port); } else { $port_error = $port; } } else { $port_error = $port; } if ($port_error) { print "Error opening port: $port\n"; } } else { my (@port_errors,%command_errors); foreach my $fw_chain (@okchains) { my $firewall_command = "$iptables -I $fw_chain -p tcp -d 0/0 --dport $port -j ACCEPT"; system($firewall_command); my $return_status = $?>>8; if ($return_status == 1) { push(@port_errors,$fw_chain); } elsif ($return_status == 2) { $command_errors{$fw_chain} = $firewall_command; } elsif ($return_status == 0) { push(@opened,$port); last; } } unless (grep(/^\Q$port\E$/,@opened)) { if (@port_errors) { print "Error opening port for chains: ". join(', ',@port_errors).".\n"; } if (keys(%command_errors)) { foreach my $fw_chain (sort(keys(%command_errors))) { print "Bad command error opening port for chain: $fw_chain. Command was\n". " ".$command_errors{$fw_chain}."\n"; } } } } } } foreach my $port (@{$ports}) { if (!grep(/^\Q$port\E$/,@opened)) { return 'Required port not open: '.$port."\n"; } } return 'ok'; } sub firewall_is_port_open { my ($iptables,$fw_chain,$port,$lond_port,$iphost,$curropen,$firewalld) = @_; # for lond port returns number of source IPs for which firewall port is open # for other ports returns 1 if the firewall port is open, 0 if not. # if firewalld is in use, checks for rich rules only. my $count = 0; # check if firewall is active or installed return $count if (! &firewall_is_active()); if ($firewalld) { my $zone = &get_default_zone(); return $count if ($zone eq ''); if ($port eq $lond_port) { if (open(PIPE,"firewall-cmd --zone=$zone --list-rich-rules |")) { while() { chomp(); if (/\Qrule family="ipv4" source address="\E([\d.]+)\Q\/32" port port="$port" protocol="tcp" accept\E/) { my $ip = $1; if ($iphost->{$ip}) { $count ++; if (ref($curropen) eq 'HASH') { $curropen->{$ip} ++; } } } } close(PIPE); } } else { if (open(PIPE,"firewall-cmd --zone=$zone --list-rich-rules |")) { while() { if (/\Qrule family="ipv4" port port="$port" protocol="tcp" accept\E/) { $count ++; last; } } close(PIPE); } } } elsif (($fw_chain =~ /^[\w-]+$/) && (open(PIPE,"$iptables -L $fw_chain -n |"))) { while() { if ($port eq $lond_port) { if (ref($iphost) eq 'HASH') { if (/^ACCEPT\s+tcp\s+\-{2}\s+(\S+)\s+\S+\s+tcp\s+dpt\:\Q$port\E/) { my $ip = $1; if ($iphost->{$ip}) { $count ++; if (ref($curropen) eq 'HASH') { $curropen->{$ip} ++; } } } } } elsif (/tcp dpt\:\Q$port\E/) { $count ++; last; } } close(PIPE); } return $count; } sub firewall_is_active { my $status = 0; if (-e '/proc/net/ip_tables_names') { if (open(PIPE,'cat /proc/net/ip_tables_names |')) { while() { chomp(); if (/^filter$/) { $status = 1; last; } } close(PIPE); } } unless ($status) { $status = &uses_firewalld(); } return $status; } sub firewall_close_port { my ($iptables,$fw_chains,$lond_port,$iphost,$ports,$firewalld) = @_; return 'inactive firewall' if (!&firewall_is_active()); return 'port number unknown' if !$lond_port; return 'invalid firewall chain' unless (ref($fw_chains) eq 'ARRAY'); my (@okchains,$zone); if ($firewalld) { $zone = &get_default_zone(); return 'no default zone' if ($zone eq ''); } else { my @badchains; foreach my $chain (@{$fw_chains}) { if ($chain =~ /^([\w\-]+)$/) { push(@okchains,$1); } else { push(@badchains,$chain); } } if (!@okchains) { return 'None of the chain names has the expected format.'."\n"; } } if (ref($ports) ne 'ARRAY') { return 'List of ports to close needed.'; } foreach my $portnum (@{$ports}) { my $port = ''; if ($portnum =~ /^(\d+)$/) { $port = $1; } else { print "Skipped non-numeric port: $portnum\n"; next; } print "Closing firewall access on port $port.\n"; if (($port ne '') && ($port eq $lond_port)) { my $output; if ($firewalld) { my (%to_close,@port_error,@lond_port_close); my $cmd = 'firewall-cmd --list-rich-rules'; if (open(PIPE,"$cmd |")) { while() { if (/\Qrule family="ipv4" source address="\E([\d.]+)\Q\/32" port port="$port" protocol="tcp" accept\E/) { my $ip = $1; my $keepopen = 0; if (ref($iphost) eq 'HASH') { if (exists($iphost->{$ip})) { $keepopen = 1; } } unless ($keepopen) { $to_close{$ip} = $port; } } } close(PIPE); } if (keys(%to_close) > 0) { foreach my $ip (sort(keys(%to_close))) { my $cmd = 'firewall-cmd --zone='.$zone.' --remove-rich-rule \'rule family="ipv4" source address="'.$ip.'/32" port port="'.$port.'" protocol="tcp" accept\''; if (open(PIPE,"$cmd |")) { my $result = ; chomp($result); close(PIPE); if ($result eq 'success') { push(@lond_port_close,$ip); } else { push(@port_error,$ip); } } else { push(@port_error,$ip); } } } if (@lond_port_close) { $output .= "Port closed for ".scalar(@lond_port_close)." IP addresses.\n"; } if (@port_error) { $output .= "Error closing port for following IP addresses: ".join(', ',@port_error)."\n"; } } else { foreach my $fw_chain (@okchains) { my (%to_close,@port_error,@command_error,@lond_port_close); if (open(PIPE, "$iptables -n -L $fw_chain |")) { while () { chomp(); next unless (/dpt:\Q$port\E/); if (/^ACCEPT\s+tcp\s+\-{2}\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+/) { my $ip = $1; my $keepopen = 0; if (ref($iphost) eq 'HASH') { if (exists($iphost->{$ip})) { $keepopen = 1; } } unless ($keepopen) { $to_close{$ip} = $port; } } } close(PIPE); } if (keys(%to_close) > 0) { foreach my $ip (keys(%to_close)) { my $firewall_command = "$iptables -D $fw_chain -p tcp -s $ip -d 0/0 --dport $port -j ACCEPT"; system($firewall_command); my $return_status = $?>>8; if ($return_status == 1) { push(@port_error,$ip); } elsif ($return_status == 2) { push(@command_error,$ip); } elsif ($return_status == 0) { push(@lond_port_close,$ip); } } } if (@lond_port_close) { $output .= "Port closed for ".scalar(@lond_port_close)." IP addresses.\n"; } if (@port_error) { $output .= "Error closing port for following IP addresses: ".join(', ',@port_error)."\n"; } if (@command_error) { $output .= "Bad command error opening port for following IP addresses: ". join(', ',@command_error)."\n". 'Command was: "'."$iptables -D $fw_chain -p tcp -s ".'$ip'." -d 0/0 --dport $port -j ACCEPT".'", where $ip is IP address'."\n"; } } } if ($output) { print $output; } else { print "No IP addresses required discontinuation of access.\n"; } } else { if ($firewalld) { my $to_close; if (open(PIPE,"firewall-cmd --list-rich-rules |")) { while() { next unless (/\Qrule family="ipv4" port port="$port" protocol="tcp" accept\E/); $to_close = 1; last; } close(PIPE); } if ($to_close) { my $cmd = 'firewall-cmd --zone='.$zone.' --remove-rich-rule \'rule family="ipv4" port port="'.$port.'" protocol="tcp" accept\''; if (open(PIPE,"$cmd|")) { my $result = ; chomp($result); close(PIPE); if ($result eq 'success') { print "Port: $port closed in zone: $zone.\n"; } else { print "Error closing port: $port in zone: $zone.\n"; } } else { print "Error closing port: $port in zone: $zone.\n"; } } } else { foreach my $fw_chain (@okchains) { my (@port_error,@command_error,@lond_port_close); my $to_close; if (open(PIPE, "$iptables -n -L $fw_chain |")) { while () { chomp(); next unless (/dpt:\Q$port\E/); $to_close = 1; last; } close(PIPE); } if ($to_close) { my $firewall_command = "$iptables -D $fw_chain -p tcp -d 0/0 --dport $port -j ACCEPT"; system($firewall_command); my $return_status = $?>>8; if ($return_status == 1) { # Error print "Error closing port: $port for chain: $fw_chain.\n"; } elsif ($return_status == 2) { # Bad command print "Bad command error closing port. Command was\n". " ".$firewall_command."\n"; } else { print "Port closed for chain $fw_chain.\n"; } } } } } } return; } sub firewall_close_anywhere { my ($iptables,$fw_chain,$port,$firewalld) = @_; my $zone; if ($firewalld) { $zone = &get_default_zone(); if ($zone eq '') { print 'no default zone'; return; } } else { unless ($fw_chain =~ /^([\w\-]+)$/) { print 'invalid chain'; return; } } if ($firewalld) { my $to_close; my $cmd = 'firewall-cmd --list-ports'; if (open(PIPE,"$cmd |")) { my $currports = ; close(PIPE); chomp($currports); if (grep(/^\Q$port\E\/tcp/,split(/\s+/,$currports))) { $to_close = 1; } } if ($to_close) { my $cmd = 'firewall-cmd --zone='.$zone.' --remove-port='.$port.'/tcp'; if (open(PIPE,"$cmd |")) { my $result = ; chomp($result); close(PIPE); if ($result eq 'success') { print 'Port '.$port.' closed for source "anywhere"'."\n"; } else { print 'Error closing port '.$port.' for source "anywhere".'."\n"; } } else { print 'Error closing port '.$port.' for source "anywhere".'."\n"; } } } elsif (open(PIPE, "$iptables --line-numbers -n -L $fw_chain |")) { while () { next unless (/dpt:\Q$port\E/); chomp(); if (/^(\d+)\s+ACCEPT\s+tcp\s+\-{2}\s+0\.0\.0\.0\/0\s+0\.0\.0\.0\/0/) { my $firewall_command = "$iptables -D $fw_chain $1"; system($firewall_command); my $return_status = $?>>8; if ($return_status == 1) { print 'Error closing port '.$port.' for source "anywhere".'."\n"; } elsif ($return_status == 2) { print 'Bad command error closing port '.$port.' for source "anywhere". Command was'."\n". ' '.$firewall_command."\n"; } else { print 'Port '.$port.' closed for source "anywhere"'."\n"; } } } close(PIPE); } } sub get_lond_port { my $perlvarref=&LONCAPA::Configuration::read_conf(); my $lond_port; if (ref($perlvarref) eq 'HASH') { if (defined($perlvarref->{'londPort'})) { $lond_port = $perlvarref->{'londPort'}; } } if (!$lond_port) { print("Unable to determine lond port number from LON-CAPA configuration.\n"); } return $lond_port; } sub get_fw_chains { my ($iptables,$distro) = @_; if ($distro eq '') { $distro = &get_distro(); } my @fw_chains; my $suse_config = "/etc/sysconfig/SuSEfirewall2"; my $ubuntu_config = "/etc/ufw/ufw.conf"; my $firewalld = &uses_firewalld($distro); if ($firewalld) { my ($dist,$version) = ($distro =~ /^([\D]+)(\d+)(?:|\-stream)$/); if (((($dist eq 'rhes') || ($dist eq 'centos') || ($dist eq 'rocky') || ($dist eq 'alma')) && ($version >= 8)) || (($dist eq 'oracle') && ($version >= 7))) { push(@fw_chains,'INPUT'); } else { my $zone = &get_default_zone(); if ($zone ne '') { push(@fw_chains,'IN_'.$zone.'_allow'); } else { push(@fw_chains,'IN_public_allow'); } } } elsif (-e $suse_config) { push(@fw_chains,'input_ext'); } else { my @posschains; if (-e $ubuntu_config) { @posschains = ('ufw-user-input','INPUT'); } else { if ($distro =~ /^(debian|ubuntu|suse|sles)/) { @posschains = ('INPUT'); } elsif ($distro =~ /^(fedora|rhes|centos|scientific|oracle|rocky|alma)(\d+)(?:|\-stream)$/) { if ((($1 eq 'fedora') && ($2 > 15)) || (($1 ne 'fedora') && ($2 >= 7))) { @posschains = ('INPUT'); } else { @posschains = ('RH-Firewall-1-INPUT','INPUT'); } } if (!-e '/etc/sysconfig/iptables') { if (!-e '/var/lib/iptables') { unless ($distro =~ /^(debian|ubuntu)/) { print("Unable to find iptables file containing static definitions.\n"); } } if ($distro =~ /^(fedora|rhes|centos|scientific|oracle|rocky|alma)(\d+){?:|\-stream)$/) { unless ((($1 eq 'fedora') && ($2 > 15)) || (($1 ne 'fedora') && ($2 >= 7))) { push(@fw_chains,'RH-Firewall-1-INPUT'); } } } } if ($iptables eq '') { $iptables = &get_pathto_iptables(); } my %counts; if (open(PIPE,"$iptables -L -n |")) { while() { foreach my $chain (@posschains) { if (/(\Q$chain\E)/) { $counts{$1} ++; } } } close(PIPE); } foreach my $fw_chain (@posschains) { if ($counts{$fw_chain}) { unless(grep(/^\Q$fw_chain\E$/,@fw_chains)) { push(@fw_chains,$fw_chain); } } } } return @fw_chains; } sub get_default_zone { my $cmd = 'firewall-cmd --get-default-zone'; my $zone; if (open(PIPE,"$cmd |")) { my $result = ; chomp($result); close(PIPE); ($zone) = ($result =~ /^(\w+)$/); } return $zone; } sub get_pathto_iptables { my $iptables; if (-e '/sbin/iptables') { $iptables = '/sbin/iptables'; } elsif (-e '/usr/sbin/iptables') { $iptables = '/usr/sbin/iptables'; } else { print("Unable to find iptables command.\n"); } return $iptables; } sub get_distro { my $distro; if (open(PIPE,"/home/httpd/perl/distprobe |")) { $distro = ; close(PIPE); } return $distro; } 1; __END__ =pod =head1 NAME B - dynamic opening/closing of firewall ports =head1 SYNOPSIS use lib '/home/httpd/lib/perl/'; use LONCAPA::Firewall; LONCAPA::Firewall::uses_firewalld(); LONCAPA::Firewall::firewall_open_port(); LONCAPA::Firewall::firewall_close_port(); LONCAPA::Firewall::firewall_is_port_open(); LONCAPA::Firewall::firewall_is_active(); LONCAPA::Firewall::firewall_close_anywhere(); =head1 DESCRIPTION The scripts: /etc/init.d/loncontrol, used to stop or start LON-CAPA services, as well as the setuid script /home/httpd/perl/lciptables, called by loncron for housekeeping tasks, make use of the methods provided by this module to open and close firewall ports (currently the default port: 5663), used for socket-based communication between LON-CAPA servers in the cluster of networked servers to which the server belongs. The following methods are available: =over 4 =item LONCAPA::Firewall::uses_firewalld( $distro ); =back =over 4 =item LONCAPA::Firewall::firewall_open_port( $iptables,$fw_chains,$lond_port,$iphost,$ports,$firewalld ); =back =over 4 =item LONCAPA::Firewall::firewall_close_port( $iptables,$fw_chains,$lond_port,$iphost,$ports,$firewalld ); =back =over 4 =item LONCAPA::Firewall::firewall_is_port_open( $iptables,$fw_chain,$port,$lond_port,$iphost,$curropen,$firewalld ); =back =over 4 =item LONCAPA::Firewall::firewall_is_active(); =back =over 4 =item LONCAPA::Firewall::firewall_close_anywhere( $iptables,$fw_chain,$port,$firewalld ); =back =over 4 =item LONCAPA::Firewall::get_lond_port(); =back =over 4 =item LONCAPA::Firewall::get_fw_chains( $iptables,$distro ); =back =over 4 =item LONCAPA::Firewall::get_pathto_iptables(); =back =over 4 =item LONCAPA::Firewall::get_distro(); =back =head1 AUTHORS This library is free software; you can redistribute it and/or modify it under the same terms as LON-CAPA itself. =cut