You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
16 KiB
366 lines
16 KiB
#!/usr/bin/perl |
|
# |
|
# This keeps an eye on the network configuration and ensures the firewall is configured appropriately. What |
|
# exactly that means depends on why kind of machine the local host is. |
|
# |
|
# Exit codes; |
|
# 0 = Normal exit. |
|
# 1 = Failed to unlink an unneeded file. |
|
# 2 = Failed to write or update a file. |
|
# |
|
# TODO: |
|
# - TEMP: During development, firewalling is disabled. |
|
# - Add support for enabling/disabling MASQ'ing the BCN |
|
# - Add support for listening for NTP queries based on /etc/chrony.conf's Server entries (map them to networks / zones). |
|
# |
|
# # Allow routing/masq'ing through the IFN1 (provide net access to the BCN) |
|
# firewall-cmd --zone=IFN1 --add-masquerade |
|
# # Check |
|
# firewall-cmd --zone=IFN1 --query-masquerade |
|
# #[yes|no] |
|
# # Disable |
|
# # NOTE: Doesn't break existing connections |
|
# firewall-cmd --zone=IFN1 --remove-masquerade |
|
# |
|
|
|
use strict; |
|
use warnings; |
|
use Anvil::Tools; |
|
use Data::Dumper; |
|
use Text::Diff; |
|
|
|
|
|
# Disable buffering |
|
$| = 1; |
|
|
|
my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0]; |
|
my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0]; |
|
if (($running_directory =~ /^\./) && ($ENV{PWD})) |
|
{ |
|
$running_directory =~ s/^\./$ENV{PWD}/; |
|
} |
|
|
|
my $anvil = Anvil::Tools->new(); |
|
|
|
# If the user has disabled auto-management of the firewall, exit. |
|
if (not $anvil->data->{sys}{manage}{firewall}) |
|
{ |
|
# Do nothing. |
|
$anvil->nice_exit({exit_code => 0}); |
|
} |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 2, secure => 0, key => "log_0115", variables => { program => $THIS_FILE }}); |
|
|
|
# Read switches |
|
$anvil->data->{switches}{'y'} = ""; |
|
$anvil->Get->switches; |
|
|
|
# For now, we just disable the firewall, if it is enabled. |
|
my $firewall_running = $anvil->System->check_daemon({daemon => "firewalld", debug => 3}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { firewall_running => $firewall_running }}); |
|
if ($firewall_running eq "1") |
|
{ |
|
# Disable it. |
|
$anvil->System->stop_daemon({daemon => "firewalld", debug => 2}); |
|
$anvil->System->disable_daemon({daemon => "firewalld", debug => 2}); |
|
} |
|
$anvil->nice_exit({exit_code => 0}); |
|
|
|
|
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 2, key => "message_0134"}); |
|
check_initial_setup($anvil); |
|
|
|
# Restart, if needed. |
|
if ($anvil->data->{firewall}{reload}) |
|
{ |
|
restart_firewall($anvil); |
|
} |
|
|
|
# We're done |
|
$anvil->nice_exit({exit_code => 0}); |
|
|
|
|
|
############################################################################################################# |
|
# Private functions. # |
|
############################################################################################################# |
|
|
|
sub check_initial_setup |
|
{ |
|
my ($anvil) = @_; |
|
|
|
# See what we've found... We'll look at what 'check_firewall' finds later to know if any unused zones |
|
# need to be removed. |
|
my $needed_zones = []; |
|
|
|
# This will get set if we need to restart the firewalld daemon. |
|
$anvil->data->{firewall}{reload} = 0; |
|
|
|
# Get a list of networks. |
|
$anvil->Network->get_ips({debug => 3}); |
|
|
|
# Get the list of existing zones from iptables/firewalld. |
|
$anvil->System->check_firewall({debug => 3}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "firewall::default_zone" => $anvil->data->{firewall}{default_zone} }}); |
|
|
|
my $internet_zone = ""; |
|
my $local_host = $anvil->Get->short_host_name(); |
|
foreach my $interface (sort {$a cmp $b} keys %{$anvil->data->{network}{$local_host}{interface}}) |
|
{ |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { interface => $interface }}); |
|
if ($interface =~ /^((bcn|ifn|sn)\d+)_/) |
|
{ |
|
# We'll use the start of the string (network type) as the zone, though it should |
|
# always be overridden by the ZONE="" variable in each interface's config. |
|
my $zone = $1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { zone => $zone }}); |
|
if ((exists $anvil->data->{network}{$local_host}{interface}{$interface}{variable}{ZONE}) && ($anvil->data->{network}{$local_host}{interface}{$interface}{variable}{ZONE})) |
|
{ |
|
$zone = $anvil->data->{network}{$local_host}{interface}{$interface}{variable}{ZONE}; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { zone => $zone }}); |
|
} |
|
push @{$needed_zones}, $zone; |
|
|
|
$anvil->data->{firewall}{zone}{$zone}{interface}{$interface}{ip} = $anvil->data->{network}{$local_host}{interface}{$interface}{ip}; |
|
$anvil->data->{firewall}{zone}{$zone}{interface}{$interface}{subnet_mask} = $anvil->data->{network}{$local_host}{interface}{$interface}{subnet_mask}; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
"firewall::zone::${zone}::interface::${interface}::ip" => $anvil->data->{firewall}{zone}{$zone}{interface}{$interface}{ip}, |
|
"firewall::zone::${zone}::interface::${interface}::subnet_mask" => $anvil->data->{firewall}{zone}{$zone}{interface}{$interface}{subnet_mask}, |
|
"network::${local_host}::interface::${interface}::default_gateway" => $anvil->data->{network}{$local_host}{interface}{$interface}{default_gateway}, |
|
}}); |
|
|
|
if ($anvil->data->{network}{$local_host}{interface}{$interface}{default_gateway}) |
|
{ |
|
$internet_zone = $zone; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { internet_zone => $internet_zone }}); |
|
|
|
if ((not $anvil->data->{firewall}{default_zone}) or ($anvil->data->{firewall}{default_zone} eq "public")) |
|
{ |
|
$anvil->data->{firewall}{default_zone} = $zone; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "firewall::default_zone" => $anvil->data->{firewall}{default_zone} }}); |
|
} |
|
|
|
} |
|
} |
|
} |
|
|
|
# Process the list of existing zones from iptables/firewalld. |
|
foreach my $zone (sort {$a cmp $b} keys %{$anvil->data->{firewall}{zone}}) |
|
{ |
|
my $file = exists $anvil->data->{firewall}{zone}{$zone}{file} ? $anvil->data->{firewall}{zone}{$zone}{file} : $anvil->data->{path}{directories}{firewalld_zones}."/".$zone.".xml"; |
|
my $user_file = $anvil->data->{path}{directories}{firewalld_zones_etc}."/".$zone.".xml"; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
"s1:zone" => $zone, |
|
"s2:file" => $file, |
|
"s3:user_file" => $user_file, |
|
}}); |
|
|
|
### NOTE: This is probably overkill. |
|
# Is this a zone I want/need? |
|
my $wanted = 0; |
|
foreach my $needed_zone (sort {$a cmp $b} @{$needed_zones}) |
|
{ |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
"s1:zone" => $zone, |
|
"s2:needed_zone" => $needed_zone, |
|
}}); |
|
if ($needed_zone eq $zone) |
|
{ |
|
$wanted = 1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { wanted => $wanted }}); |
|
last; |
|
} |
|
} |
|
|
|
# Skip if this is a zone I don't care about. |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { wanted => $wanted }}); |
|
next if not $wanted; |
|
|
|
# Now, skip if the user-land file exists. |
|
if (-e $user_file) |
|
{ |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0143", variables => { zone => $zone, file => $user_file }}); |
|
next; |
|
} |
|
|
|
# Create or update the zone file, if needed. |
|
my $template = ""; |
|
my $description = ""; |
|
if ($zone =~ /bcn(\d+)/i) |
|
{ |
|
my $number = $1; |
|
$template = "bcn_zone"; |
|
$description = $anvil->Words->string({key => "message_0131", variables => { number => $number }}); |
|
} |
|
elsif ($zone =~ /sn(\d+)/i) |
|
{ |
|
my $number = $1; |
|
$template = "sn_zone"; |
|
$description = $anvil->Words->string({key => "message_0132", variables => { number => $number }}); |
|
} |
|
elsif ($zone =~ /ifn(\d+)/i) |
|
{ |
|
my $number = $1; |
|
$template = "ifn_zone"; |
|
$description = $anvil->Words->string({key => "message_0133", variables => { number => $number }}); |
|
} |
|
else |
|
{ |
|
# This should never be hit, but it's a fail-safe in we're in a zone we don't manage. |
|
next; |
|
} |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
"s1:template" => $template, |
|
"s2:description" => $description, |
|
}}); |
|
|
|
my $new_zone_body = $anvil->Template->get({debug => 3, file => "firewall.txt", show_name => 0, name => $template, variables => { |
|
zone => $zone, |
|
description => $description, |
|
}}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { new_zone_body => $new_zone_body }}); |
|
|
|
# This is another fail safe, don't edit unless we have a new file body. |
|
if (not $new_zone_body) |
|
{ |
|
next; |
|
} |
|
|
|
# If there isn't a body, see if the file exists. If it doesn't, create it. If it does, read it. |
|
my $update_file = 0; |
|
my $old_zone_body = exists $anvil->data->{firewall}{zone}{$zone}{body} ? $anvil->data->{firewall}{zone}{$zone}{body} : ""; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { old_zone_body => $old_zone_body }}); |
|
if (-e $file) |
|
{ |
|
# Has it changed? |
|
my $diff = diff \$old_zone_body, \$new_zone_body, { STYLE => 'Unified' }; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { diff => $diff }}); |
|
if ($diff) |
|
{ |
|
# Update it |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0136", variables => { zone => $zone, file => $file }}); |
|
$update_file = 1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { update_file => $update_file }}); |
|
} |
|
} |
|
else |
|
{ |
|
# Create it |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0137", variables => { zone => $zone, file => $file }}); |
|
$update_file = 1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { update_file => $update_file }}); |
|
} |
|
|
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { update_file => $update_file }}); |
|
if ($update_file) |
|
{ |
|
my $error = $anvil->Storage->write_file({ |
|
file => $file, |
|
body => $new_zone_body, |
|
group => "root", |
|
user => "root", |
|
mode => "0644", |
|
overwrite => 1, |
|
}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { error => $error }}); |
|
|
|
if ($error) |
|
{ |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 0, priority => "err", key => "error_0043", variables => { file => $file }}); |
|
$anvil->nice_exit({exit_code => 2}); |
|
} |
|
else |
|
{ |
|
# We need an immediate reload to pick up the new file. |
|
restart_firewall($anvil); |
|
} |
|
} |
|
|
|
# Make sure the appropriate interfaces are in this zone. |
|
foreach my $interface (sort {$a cmp $b} keys %{$anvil->data->{firewall}{zone}{$zone}{interface}}) |
|
{ |
|
my $in_zone = exists $anvil->data->{firewall}{interface}{$interface}{zone} ? $anvil->data->{firewall}{interface}{$interface}{zone} : ""; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
"s1:interface" => $interface, |
|
"s2:in_zone" => $in_zone, |
|
"s3:zone" => $zone, |
|
}}); |
|
|
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { in_zone => $in_zone, zone => $zone }}); |
|
if ((not $in_zone) or ($zone ne $in_zone)) |
|
{ |
|
# Add it |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0138", variables => { |
|
interface => $interface, |
|
zone => $zone, |
|
}}); |
|
|
|
my $shell_call = $anvil->data->{path}{exe}{'firewall-cmd'}." --zone=".$zone." --change-interface=".$interface." --permanent"; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }}); |
|
my ($output, $return_code) = $anvil->System->call({debug => 2, shell_call => $shell_call}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { output => $output, return_code => $return_code }}); |
|
|
|
$shell_call = $anvil->data->{path}{exe}{'firewall-cmd'}." --zone=".$zone." --change-interface=".$interface; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }}); |
|
($output, $return_code) = $anvil->System->call({debug => 2, shell_call => $shell_call}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { output => $output, return_code => $return_code }}); |
|
|
|
$anvil->data->{firewall}{reload} = 1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "firewall::reload" => $anvil->data->{firewall}{reload} }}); |
|
} |
|
|
|
# Delete it so we know this one has been processed. |
|
delete $anvil->data->{firewall}{interface}{$interface}; |
|
} |
|
} |
|
|
|
# Do we need to update the default zone? |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { |
|
internet_zone => $internet_zone, |
|
"firewall::default_zone" => $anvil->data->{firewall}{default_zone}, |
|
}}); |
|
if ($anvil->data->{firewall}{default_zone}) |
|
{ |
|
# What's the current default zone? |
|
my $shell_call = $anvil->data->{path}{exe}{'firewall-cmd'}." --get-default-zone"; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }}); |
|
my ($default_zone, $return_code) = $anvil->System->call({debug => 3, shell_call => $shell_call}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { default_zone => $default_zone, return_code => $return_code }}); |
|
|
|
if ($default_zone ne $anvil->data->{firewall}{default_zone}) |
|
{ |
|
# Update. |
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0141", variables => { zone => $internet_zone }}); |
|
my $shell_call = $anvil->data->{path}{exe}{'firewall-cmd'}." --set-default-zone=".$anvil->data->{firewall}{default_zone}; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }}); |
|
my ($output, $return_code) = $anvil->System->call({debug => 2, shell_call => $shell_call}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { output => $output, return_code => $return_code }}); |
|
|
|
$anvil->data->{firewall}{reload} = 1; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "firewall::reload" => $anvil->data->{firewall}{reload} }}); |
|
} |
|
} |
|
|
|
# NOTE: We may want to do machine-specific stuff down the road. |
|
my $type = $anvil->Get->host_type(); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { type => $type }}); |
|
|
|
return(0); |
|
} |
|
|
|
sub restart_firewall |
|
{ |
|
my ($anvil) = @_; |
|
|
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0139"}); |
|
my $shell_call = $anvil->data->{path}{exe}{'firewall-cmd'}." --complete-reload"; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }}); |
|
my ($output, $return_code) = $anvil->System->call({debug => 3, shell_call => $shell_call}); |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { output => $output, return_code => $return_code }}); |
|
|
|
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 2, key => "message_0140"}); |
|
$anvil->System->restart_daemon({debug => 3, daemon => "firewalld"}); |
|
|
|
$anvil->data->{firewall}{reload} = 0; |
|
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "firewall::reload" => $anvil->data->{firewall}{reload} }}); |
|
|
|
return(0); |
|
}
|
|
|