managementnode/lib/VCL/Module/OS/Linux/firewall/firewalld.pm (708 lines of code) (raw):

#!/usr/bin/perl -w ############################################################################### # $Id$ ############################################################################### # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################### =head1 NAME VCL::Module::OS::Linux::firewall::firewalld.pm =head1 DESCRIPTION This module provides VCL support for firewalld-based firewalls. =cut ############################################################################### package VCL::Module::OS::Linux::firewall::firewalld; # Specify the lib path using FindBin use FindBin; use lib "$FindBin::Bin/../../../../.."; # Configure inheritance use base qw(VCL::Module::OS::Linux::firewall::iptables); # Specify the version of this module our $VERSION = '2.5.1'; our @ISA; # Specify the version of Perl to use use 5.008000; use strict; use warnings; use diagnostics; use VCL::utils; ############################################################################### =head1 OBJECT METHODS =cut #////////////////////////////////////////////////////////////////////////////// =head2 initialize Parameters : none Returns : boolean Description : =cut sub initialize { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $arguments = shift || {}; my $computer_name = $self->data->get_computer_hostname(); notify($ERRORS{'DEBUG'}, 0, "initializing " . ref($self) . " object to control $computer_name"); if (!$self->os->service_exists('firewalld')) { notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewalld service does not exist"); return 0; } if (!$self->os->is_service_enabled('firewalld')) { notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewalld service is not enabled"); return 0; } if (!$self->os->command_exists('firewall-cmd')) { notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, firewall-cmd command does not exist"); return 0; } notify($ERRORS{'DEBUG'}, 0, ref($self) . " object initialized to control $computer_name"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 process_post_load Parameters : none Returns : boolean Description : Performs the initial iptables firewall configuration after an image is loaded: * Performs all of the tasks done by iptables.pm::process_post_load except the pre-VCL 2.5 legacy cleanup tasks * Removes the ssh protocol from the public zone =cut sub process_post_load { my $self = shift; if (ref($self) !~ /VCL::Module::OS::Linux::firewall/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $computer_name = $self->data->get_computer_short_name(); notify($ERRORS{'DEBUG'}, 0, "beginning firewalld post-load configuration on $computer_name"); # Call subroutine in iptables.pm return unless $self->SUPER::process_post_load(); # Remove ssh from public zone return unless $self->remove_service('public', 'ssh'); $self->save_configuration(); notify($ERRORS{'DEBUG'}, 0, "completed firewalld post-load configuration on $computer_name"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 get_all_direct_rules Parameters : none Returns : array Description : Calls 'firewall-cmd --permanent --direct --get-all-rules' and returns an array of strings. =cut sub get_all_direct_rules { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --direct --get-all-rules"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve all firewalld direct rules on $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to retrieve all firewalld direct rules on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return; } # Rules should be in the format: # ipv4 filter vcl-pre_capture 0 --jump ACCEPT --protocol tcp --match comment --comment 'VCL: Allow traffic to SSH port 22 from any IP address (2017-04-07 17:19:21)' --match tcp --destination-port 22 # ipv4 filter INPUT 0 --jump vcl-pre_capture --match comment --comment 'VCL: jump to rules added during the pre-capture stage (2017-04-07 17:19:21)' my @rules = grep(/^(ipv4|ipv6|eb)/, @$output); notify($ERRORS{'DEBUG'}, 0, "retrieved all firewalld direct rules defined on $computer_name:\n" . join("\n", @rules)); return @rules; } #////////////////////////////////////////////////////////////////////////////// =head2 get_direct_chain_rules Parameters : $table_name, $chain_name Returns : array Description : Calls 'firewall-cmd --permanent --direct --get-rules' and returns an array of strings. =cut sub get_direct_chain_rules { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name) = @_; if (!$table_name) { notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } elsif (!$chain_name) { notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --direct --get-rules ipv4 $table_name $chain_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to retrieve firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to retrieve firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return; } # All rule lines should begin with an integer: # 0 --jump ACCEPT --source 10.25.7.2 --match comment --comment 'VCL: Allow traffic from management node (2017-04-07 15:36:24)' # 1 --jump ACCEPT --source 10.25.7.2 my @rules = grep(/^\d+/, @$output); notify($ERRORS{'DEBUG'}, 0, "retrieved firewalld direct rules defined for '$chain_name' chain in '$table_name' table on $computer_name:\n" . join("\n", @rules)); return @rules; } #////////////////////////////////////////////////////////////////////////////// =head2 save_configuration Parameters : none Returns : boolean Description : Calls 'firewall-cmd --reload'. =cut sub save_configuration { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --reload"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to reload firewalld configuration on $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to reload firewalld configuration on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } else { notify($ERRORS{'OK'}, 0, "reloaded firewalld configuration on $computer_name"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 create_chain Parameters : $table_name, $chain_name Returns : boolean Description : Creates a new chain. Returns true if the chain was successfully created or already exists. =cut sub create_chain { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name) = @_; if (!defined($table_name)) { notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } elsif (!defined($chain_name)) { notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --direct --add-chain ipv4 $table_name $chain_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); return; } elsif (grep(/ALREADY_ENABLED/i, @$output)) { notify($ERRORS{'OK'}, 0, "'$chain_name' chain in '$table_name' table already exists on $computer_name"); return 1; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to create '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } elsif (!grep(/success/, @$output)) { notify($ERRORS{'WARNING'}, 0, "potentially failed to create '$chain_name' chain in '$table_name' table on $computer_name, output does not contain 'success', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } else { notify($ERRORS{'OK'}, 0, "created '$chain_name' chain in '$table_name' table on $computer_name"); #$self->save_configuration(); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 remove_direct_chain_rules Parameters : $table_name, $chain_name Returns : boolean Description : Flushes (deletes) rules from the specified chain. =cut sub remove_direct_chain_rules { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name) = @_; if (!defined($table_name)) { notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } elsif (!defined($chain_name)) { notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } my $computer_name = $self->data->get_computer_hostname(); # !!! WARNING !!! # DON'T USE --remove-rules # With firewall-cmd version 0.4.3.2, this option removes rules from ALL direct chains, not just the one specified #my $command = "firewall-cmd --permanent --direct --remove-rules ipv4 $table_name $chain_name"; my @rules = $self->get_direct_chain_rules($table_name, $chain_name); for my $rule (@rules) { # [--permanent] --direct --remove-rule { ipv4 | ipv6 | eb } table chain priority args my $command = "firewall-cmd --permanent --direct --remove-rule ipv4 $table_name $chain_name $rule"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to remove rule from '$chain_name' chain in '$table_name' table on $computer_name: '$rule', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } else { notify($ERRORS{'OK'}, 0, "removed direct rule from '$chain_name' chain in '$table_name' table on $computer_name: '$rule'"); } } notify($ERRORS{'OK'}, 0, "removed all direct rules from '$chain_name' chain in '$table_name' table on $computer_name"); return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 delete_chain Parameters : $table_name, $chain_name Returns : boolean Description : Deletes an existing chain. Returns true if the chain was successfully deleted or doesn't exist. =cut sub delete_chain { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name_argument) = @_; if (!defined($table_name)) { notify($ERRORS{'WARNING'}, 0, "table name argument was not specified"); return; } elsif (!defined($chain_name_argument)) { notify($ERRORS{'WARNING'}, 0, "chain name argument was not specified"); return; } my $computer_name = $self->data->get_computer_hostname(); my @chains_deleted; my @chain_names = $self->get_table_chain_names($table_name); for my $chain_name (@chain_names) { if ($chain_name !~ /^$chain_name_argument$/) { next; } # Delete all rules which reference the chain being deleted or else the chain can't be deleted # Do this BEFORE checking if the chain exists to clean up leftover references in direct.xml if (!$self->delete_chain_references($table_name, $chain_name)) { notify($ERRORS{'WARNING'}, 0, "unable to delete '$chain_name' chain from '$table_name' table on $computer_name, failed to delete all rules which reference the chain prior to deletion"); return; } $self->remove_direct_chain_rules($table_name, $chain_name) || return; my $command = "firewall-cmd --permanent --direct --remove-chain ipv4 $table_name $chain_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command"); return; } elsif (grep(/NOT_ENABLED/i, @$output)) { notify($ERRORS{'OK'}, 0, "'$chain_name' chain in '$table_name' does not exist on $computer_name"); } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to delete '$chain_name' chain in '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } elsif (!grep(/success/, @$output)) { notify($ERRORS{'WARNING'}, 0, "potentially failed to delete '$chain_name' chain in '$table_name' table on $computer_name, output does not contain 'success', exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); } else { notify($ERRORS{'OK'}, 0, "deleted '$chain_name' chain in '$table_name' table on $computer_name"); #$self->save_configuration(); } if (!$self->clean_direct_xml($table_name . '.*jump\s+' . $chain_name)) { return; } notify($ERRORS{'OK'}, 0, "deleted '$chain_name' chain from '$table_name' table on $computer_name"); push @chains_deleted, $chain_name; } if (!@chains_deleted) { notify($ERRORS{'DEBUG'}, 0, "no chains exist in '$table_name' table on $computer_name matching argument: '$chain_name_argument'"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 clean_direct_xml Parameters : $regex_pattern Returns : boolean Description : =cut sub clean_direct_xml { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my $regex_pattern = shift; if (!defined($regex_pattern)) { notify($ERRORS{'WARNING'}, 0, "regex pattern argument was not supplied"); return; } $self->os->firewall->save_configuration(); my @keep_lines; my @prune_lines; my $file_path = '/etc/firewalld/direct.xml'; my @lines = $self->os->get_file_contents($file_path); for my $line (@lines) { if ($line =~ /$regex_pattern/i) { push @prune_lines, $line; } else { push @keep_lines, $line; } } if (@prune_lines) { my $updated_contents = join("\n", @keep_lines); notify($ERRORS{'DEBUG'}, 0, "pruning the following lines from $file_path matching pattern: '$regex_pattern'\n" . join("\n", @prune_lines) . "\nnew file contents:\n$updated_contents"); return $self->os->create_text_file($file_path, $updated_contents); } else { notify($ERRORS{'DEBUG'}, 0, "no lines were pruned from $file_path matching pattern: '$regex_pattern'"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 _insert_rule Parameters : $table_name, $chain_name, $argument_string Returns : boolean Description : Executes the command to insert a firewalld direct rule. This is a helper subroutine and should only be called by iptable.pm::insert_rule. =cut sub _insert_rule { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name, $argument_string) = @_; my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --direct --add-rule ipv4 $table_name $chain_name 0 $argument_string"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to add direct firewalld rule to $chain_name chain in $table_name table on $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to add direct firewalld rule to $chain_name chain in $table_name table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return 0; } else { notify($ERRORS{'OK'}, 0, "added direct firewalld rule to $chain_name chain in $table_name table on $computer_name, command: $command, output:\n" . join("\n", @$output)); #$self->save_configuration(); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 _delete_rule Parameters : $table_name, $chain_name, $rule_specification_string Returns : boolean Description : Deletes a firewalld direct rule. This should only used as a helper subroutine. =cut sub _delete_rule { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($table_name, $chain_name, $rule_specification_string) = @_; my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --direct --remove-rule ipv4 $table_name $chain_name 0 $rule_specification_string"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete firewalld direct rule on $computer_name: $command"); return; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to delete firewalld direct rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output)); return; } else { notify($ERRORS{'OK'}, 0, "deleted firewalld direct rule on $computer_name, command: '$command', output:\n" . join("\n", @$output)); #$self->save_configuration(); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 remove_service Parameters : $zone_name, $service Returns : boolean Description : =cut sub remove_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $service) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($service)) { notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); return; } $service = 'tcp' unless $service; my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --remove-service=serviceid[-serviceid]/service # Remove the service from zone. If zone is omitted, default zone will be used. This option can be specified # multiple times. my $command = "firewall-cmd --permanent --zone=$zone_name --remove-service=$service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove $service service from '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to remove $service service from '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/NOT_ENABLED/, @$output)) { notify($ERRORS{'OK'}, 0, "$service service has not been added to '$zone_name' zone on $computer_name"); } else { notify($ERRORS{'OK'}, 0, "removed $service service from '$zone_name' zone on $computer_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 create_zone Parameters : $zone_name Returns : boolean Description : Creates a new firewalld zone on the computer. =cut sub create_zone { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name) = @_; my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --new-zone=$zone_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to create '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if (grep(/NAME_CONFLICT/, @$output)) { # Error: NAME_CONFLICT: new_zone(): 'vcl-test' notify($ERRORS{'OK'}, 0, "'$zone_name' zone already exists on $computer_name"); return 1; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to create '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } else { notify($ERRORS{'OK'}, 0, "created '$zone_name' zone on $computer_name"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 delete_zone Parameters : $zone_name Returns : boolean Description : Deletes a firewalld zone from the computer. =cut sub delete_zone { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name) = @_; my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --permanent --delete-zone=$zone_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if (grep(/INVALID_ZONE/, @$output)) { # Error: INVALID_ZONE: vcl-test notify($ERRORS{'OK'}, 0, "'$zone_name' zone does not exist on $computer_name"); return 1; } elsif ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to delete '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } else { notify($ERRORS{'OK'}, 0, "deleted '$zone_name' zone on $computer_name"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 get_zone_info Parameters : $zone_name Returns : hash reference Description : Retrieves information about a firewalld zone from the computer and constructs a hash reference: { "forward-ports" => "", "icmp-block-inversion" => "no", "icmp-blocks" => "", "interfaces" => "", "masquerade" => "no", "ports" => "", "protocols" => "", "rich rules" => "", "services" => "", "sourceports" => "", "sources" => "", "target" => "default" } =cut sub get_zone_info { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name) = @_; my $computer_name = $self->data->get_computer_hostname(); my $command = "firewall-cmd --info-zone $zone_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to delete '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0' || grep(/Error:/, @$output)) { notify($ERRORS{'WARNING'}, 0, "failed to retrieve info for '$zone_name' zone from $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } # vcl-test # target: default # icmp-block-inversion: no # interfaces: # sources: # services: # ports: # protocols: # masquerade: no # forward-ports: # sourceports: # icmp-blocks: # rich rules: my $zone_info = {}; for my $line (@$output) { my ($property, $value) = $line =~ /\s*(\S[^:]+)\s*:\s*(.*)/g; if (!defined($property)) { notify($ERRORS{'DEBUG'}, 0, "ignoring line: '$line'") if ($line !~ /^$zone_name/); next; } $zone_info->{$property} = $value; } notify($ERRORS{'OK'}, 0, "retrieved info for '$zone_name' zone on $computer_name:\n" . format_data($zone_info)); return $zone_info; } #////////////////////////////////////////////////////////////////////////////// =head2 set_zone_target Parameters : $zone_name, $target Returns : boolean Description : Sets the target for a firewalld zone. =cut sub set_zone_target { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $target) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($target)) { notify($ERRORS{'WARNING'}, 0, "target argument was not supplied"); return; } elsif ($target !~ /^(ACCEPT|DROP|REJECT)$/i) { notify($ERRORS{'WARNING'}, 0, "target argument is not valid: $target, it must be 'ACCEPT', 'DROP', or 'REJECT'"); return; } $target = uc($target); my $computer_name = $self->data->get_computer_hostname(); # --permanent [--zone=zone] --set-target=target # Set the target of a permanent zone. target is one of: default, ACCEPT, DROP, REJECT my $command = "firewall-cmd --permanent --zone=$zone_name --set-target=$target"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to set target of '$zone_name' zone to '$target' on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to set target of '$zone_name' zone to '$target' on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } else { notify($ERRORS{'OK'}, 0, "set target of '$zone_name' zone to '$target' on $computer_name"); return 1; } } #////////////////////////////////////////////////////////////////////////////// =head2 add_source Parameters : $zone_name, $source Returns : boolean Description : =cut sub add_source { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $source) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($source)) { notify($ERRORS{'WARNING'}, 0, "source argument was not supplied"); return; } my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --add-source=source[/mask]|MAC|ipset:ipset my $command = "firewall-cmd --permanent --zone=$zone_name --add-source=$source"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to add '$source' source to '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to add source to '$zone_name' zone on $computer_name: $source, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/ALREADY_ENABLED/, @$output)) { # Warning: ALREADY_ENABLED: 10.1.2.3 notify($ERRORS{'OK'}, 0, "source was previously added to '$zone_name' zone on $computer_name: $source"); } else { notify($ERRORS{'OK'}, 0, "added source to '$zone_name' zone on $computer_name: $source"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 remove_source Parameters : $zone_name, $source Returns : boolean Description : =cut sub remove_source { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $source) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($source)) { notify($ERRORS{'WARNING'}, 0, "source argument was not supplied"); return; } my $computer_name = $self->data->get_computer_hostname(); # [--permanent] --remove-source=source[/mask]|MAC|ipset:ipset my $command = "firewall-cmd --permanent --zone=$zone_name --remove-source=$source"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove '$source' source from '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to remove source from '$zone_name' zone on $computer_name: $source, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/NOT_ENABLED/, @$output)) { notify($ERRORS{'OK'}, 0, "source is not specified in '$zone_name' zone on $computer_name: $source"); } else { notify($ERRORS{'OK'}, 0, "removed source from '$zone_name' zone on $computer_name: $source"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 add_interface Parameters : $zone_name, $interface_name Returns : boolean Description : =cut sub add_interface { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $interface_name) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($interface_name)) { notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); return; } my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --add-interface=interface my $command = "firewall-cmd --permanent --zone=$zone_name --add-interface=$interface_name"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to add '$interface_name' interface to '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to add interface to '$zone_name' zone on $computer_name: $interface_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/already bound/, @$output)) { # The interface is under control of NetworkManager and already bound to 'public' notify($ERRORS{'OK'}, 0, "interface is already bound to '$zone_name' zone on $computer_name: $interface_name"); } else { notify($ERRORS{'OK'}, 0, "bound interface to '$zone_name' zone on $computer_name: $interface_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 add_port Parameters : $zone_name, $port, $protocol (optional) Returns : boolean Description : =cut sub add_port { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $port, $protocol) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($port)) { notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); return; } $protocol = 'tcp' unless $protocol; my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --add-port=portid[-portid]/protocol [--timeout=timeval] my $command = "firewall-cmd --permanent --zone=$zone_name --add-port=$port/$protocol"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to add port $port/$protocol to '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to add port $port/$protocol to '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/ALREADY_ENABLED/, @$output)) { notify($ERRORS{'OK'}, 0, "port $port/$protocol was previously added to '$zone_name' zone on $computer_name"); } else { notify($ERRORS{'OK'}, 0, "added port $port/$protocol to '$zone_name' zone on $computer_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 remove_port Parameters : $zone_name, $port, $protocol (optional) Returns : boolean Description : =cut sub remove_port { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $port, $protocol) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($port)) { notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); return; } $protocol = 'tcp' unless $protocol; my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --remove-port=portid[-portid]/protocol my $command = "firewall-cmd --permanent --zone=$zone_name --remove-port=$port/$protocol"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to remove port $port/$protocol from '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to remove port $port/$protocol from '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/NOT_ENABLED/, @$output)) { notify($ERRORS{'OK'}, 0, "port $port/$protocol has not been added from '$zone_name' zone on $computer_name"); } else { notify($ERRORS{'OK'}, 0, "removed port $port/$protocol from '$zone_name' zone on $computer_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// =head2 add_service Parameters : $zone_name, $service Returns : boolean Description : =cut sub add_service { my $self = shift; if (ref($self) !~ /VCL::Module/i) { notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method"); return 0; } my ($zone_name, $service) = @_; if (!defined($zone_name)) { notify($ERRORS{'WARNING'}, 0, "zone name argument was not supplied"); return; } elsif (!defined($service)) { notify($ERRORS{'WARNING'}, 0, "interface name argument was not supplied"); return; } $service = 'tcp' unless $service; my $computer_name = $self->data->get_computer_hostname(); # [--permanent] [--zone=zone] --add-service=service [--timeout=timeval] my $command = "firewall-cmd --permanent --zone=$zone_name --add-service=$service"; my ($exit_status, $output) = $self->os->execute($command, 0); if (!defined($output)) { notify($ERRORS{'WARNING'}, 0, "failed to execute command to add $service service to '$zone_name' zone on $computer_name: $command"); return; } # Remove color controls (my $output_string = join("\n", @$output)) =~ s/\e\[\d+m//g; if ($exit_status ne '0') { notify($ERRORS{'WARNING'}, 0, "failed to add $service service to '$zone_name' zone on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n$output_string"); return; } elsif (grep(/ALREADY_ENABLED/, @$output)) { notify($ERRORS{'OK'}, 0, "$service service was previously added to '$zone_name' zone on $computer_name"); } else { notify($ERRORS{'OK'}, 0, "added $service service to '$zone_name' zone on $computer_name"); } return 1; } #////////////////////////////////////////////////////////////////////////////// 1; __END__ =head1 SEE ALSO L<http://cwiki.apache.org/VCL/> =cut