#!/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 => 3, 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); }