blob: 9a526ab9d15c99696d5fdd20951b11c9fe264419 [file] [log] [blame]
#!/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::iptables.pm
=head1 DESCRIPTION
This module provides support for configuring iptables-based firewalls.
=cut
###############################################################################
package VCL::Module::OS::Linux::firewall::iptables;
# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../../../..";
# Configure inheritance
use base qw(VCL::Module::OS::Linux::firewall);
# Specify the version of this module
our $VERSION = '2.5';
our @ISA;
# Specify the version of Perl to use
use 5.008000;
use strict;
use warnings;
use diagnostics;
use English '-no_match_vars';
use VCL::utils;
###############################################################################
=head1 OBJECT METHODS
=cut
#//////////////////////////////////////////////////////////////////////////////
=head2 initialize
Parameters : none
Returns : boolean
Description : Returns true if the iptables command exists on the computer.
Returns false if the command does not exist.
=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->command_exists('iptables')) {
notify($ERRORS{'DEBUG'}, 0, ref($self) . " object not initialized to control $computer_name, iptables 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:
* A vcl-post_load chain is created in the filter table with a
rule is added to this chain to allow traffic on any port from
the management node's IP address.
* All existing rules explicitly allowing traffic to TCP/22 are
deleted.
* All other chains in the filter table named vcl-* are deleted to
clean up any possible remnants.
=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 firewall post-load configuration on $computer_name");
my $timestamp = makedatestring();
my $post_load_chain_name = $self->get_post_load_chain_name();
# Try to determine the IP address the management node uses to connect to remote hosts
# managementnode.IPaddress is not necessarily the private IP used to connect to computers being loaded
my @mn_ip_addresses = $self->os->get_management_node_connected_ip_address();
# If unable to determine the connecting IP, open up access to all MN IP's
if (!@mn_ip_addresses) {
# Get all of the IP addresses in use on the management node
@mn_ip_addresses = $self->mn_os->get_ip_addresses();
if (!@mn_ip_addresses) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, management node IP addresses could not be determined");
return;
}
}
# Create a chain and add a jump rule to INPUT chain
$self->create_chain('filter', $post_load_chain_name);
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $post_load_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump to rules added during the post-load stage ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, failed to create rule in INPUT chain to jump to '$post_load_chain_name' chain");
return;
}
# Allow traffic from any of the management node IP addresses
if (!$self->insert_rule('filter', $post_load_chain_name,
{
'parameters' => {
'source' => join(',', @mn_ip_addresses),
'jump' => 'ACCEPT',
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: allow traffic from management node ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall post-load configuration on $computer_name, failed to add rule allowing traffic from management node IP addresses to $post_load_chain_name chain");
return;
}
# Delete other vcl-* chains added by vcld
my $table_info = $self->get_table_info();
for my $chain_name (keys %$table_info) {
if ($chain_name ne $post_load_chain_name && $chain_name =~ /^vcl-/) {
$self->delete_chain('filter', $chain_name);
}
}
if (!$self->isa('VCL::Module::OS::Linux::firewall::firewalld')) {
# Legacy code may have been used previously for a reservation, before an upgrade
# Clean up old connect method rules from the INPUT chain
# Delete all rules from INPUT chain matching connect method protocols and ports
$self->delete_connect_method_rules();
# Delete all TCP/22 rules
# Images captured prior to VCL 2.5 are saved with an expicit TCP/22 allow rule from any address
$self->delete_rules('filter', 'INPUT',
{
"match_extensions" => {
"tcp" => {
"dport" => 22,
},
},
"parameters" => {
"jump" => "ACCEPT",
},
}
);
$self->save_configuration();
}
notify($ERRORS{'DEBUG'}, 0, "completed firewall post-load configuration on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 process_reserved
Parameters : none
Returns : boolean
Description : Configures the iptables firewall for the reserved state:
* A vcl-reserved chain is created with rules allowing traffic to
the connect method ports from any IP address.
=cut
sub process_reserved {
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 $reservation_id = $self->data->get_reservation_id();
my $computer_name = $self->data->get_computer_short_name();
# Make sure the post-load steps were done
if (!$self->chain_exists('filter', $self->get_post_load_chain_name())) {
$self->process_post_load();
}
my $timestamp = makedatestring();
notify($ERRORS{'DEBUG'}, 0, "beginning firewall configuration on $computer_name for reserved state");
my $reserved_chain_name = $self->get_reserved_chain_name();
# Delete existing chain if one exists to prevent inconsistent results
# Create a chain and add a jump rule to INPUT chain
$self->create_chain('filter', $reserved_chain_name);
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $reserved_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump to rules added during the reserved stage of reservation $reservation_id ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall reserved configuration on $computer_name, failed to create rule in INPUT chain to jump to '$reserved_chain_name' chain");
return;
}
my @protocol_ports = $self->data->get_connect_method_protocol_port_array();
for my $protocol_port (@protocol_ports) {
my ($protocol, $port) = @$protocol_port;
if (!$self->insert_rule('filter', $reserved_chain_name,
{
'parameters' => {
'protocol' => $protocol,
'jump' => 'ACCEPT',
},
'match_extensions' => {
$protocol => {
'dport' => $port,
},
'comment' => {
'comment' => "VCL: allow traffic from any IP address to connect method ports during reserved stage of reservation $reservation_id ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall reserved configuration on $computer_name, failed to add rule to allow traffic to '$reserved_chain_name' chain, protocol: $protocol, port: $port");
return;
}
}
$self->save_configuration();
notify($ERRORS{'DEBUG'}, 0, "completed firewall reserved configuration on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 process_inuse
Parameters : $remote_ip_address (optional)
Returns : boolean
Description : Configures the iptables firewall for the inuse state:
* A vcl-inuse chain is created if it does not already exist.
* Rules are added to the vcl-inuse chain allowing to allow
traffic to the connect method ports from the end user's
specific IP address.
* The vcl-reserved chain is deleted if it exists.
This subroutine can be called over and over again. It will not
remove rules previously added to the vcl-inuse chain. If a user's
remote IP address changes, this subroutine will add a new rule to
the vcl-inuse chain.
=cut
sub process_inuse {
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 $reservation_id = $self->data->get_reservation_id();
my $computer_name = $self->data->get_computer_short_name();
# Make sure the post-load steps were done
if (!$self->chain_exists('filter', $self->get_post_load_chain_name())) {
$self->process_post_load();
}
my $timestamp = makedatestring();
my $remote_ip_address = shift || $self->data->get_reservation_remote_ip();
if (!$remote_ip_address) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, remote IP could not be retrieved for reservation");
return;
}
notify($ERRORS{'DEBUG'}, 0, "beginning firewall configuration on $computer_name for inuse state");
my $inuse_chain_name = $self->get_inuse_chain_name();
my $reserved_chain_name = $self->get_reserved_chain_name();
# Delete existing chain if one exists to prevent inconsistent results
# Create a chain and add a jump rule to INPUT chain
$self->create_chain('filter', $inuse_chain_name);
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $inuse_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump to rules added during the inuse stage of reservation $reservation_id ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, failed to create rule in INPUT chain to jump to '$inuse_chain_name' chain");
return;
}
my @protocol_ports = $self->data->get_connect_method_protocol_port_array();
for my $protocol_port (@protocol_ports) {
my ($protocol, $port) = @$protocol_port;
if (!$self->insert_rule('filter', $inuse_chain_name,
{
'parameters' => {
'protocol' => $protocol,
'source' => "$remote_ip_address",
'jump' => 'ACCEPT',
},
'match_extensions' => {
$protocol => {
'dport' => $port,
},
'comment' => {
'comment' => "VCL: allow traffic from $remote_ip_address to $protocol/$port during the inuse stage of reservation $reservation_id ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall inuse configuration on $computer_name, failed to add rule to allow traffic to '$inuse_chain_name' chain, protocol: $protocol, port: $port");
return;
}
}
# Delete the reserved chain which allows traffic from any address
$self->delete_chain('filter', $reserved_chain_name);
$self->save_configuration();
notify($ERRORS{'DEBUG'}, 0, "completed firewall inuse configuration on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 process_sanitize
Parameters : none
Returns : boolean
Description : Performs the same iptables firewall configuration steps as
process_post_load.
=cut
sub process_sanitize {
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;
}
return $self->process_post_load();
}
#//////////////////////////////////////////////////////////////////////////////
=head2 process_pre_capture
Parameters : none
Returns : boolean
Description : Performs the iptables firewall configuration prior to capturing
an image:
* A vcl-pre_capture chain is added to the filter table
with a rule allowing TCP/22 traffic from any IP address.
* Rules matching any of the management node's IP addresses are
deleted.
* Any other chains named vcl-* are flushed and deleted.
=cut
sub process_pre_capture {
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 $timestamp = makedatestring();
my $computer_name = $self->data->get_computer_short_name();
notify($ERRORS{'DEBUG'}, 0, "beginning firewall pre-capture configuration on $computer_name");
my $pre_capture_chain_name = $self->get_pre_capture_chain_name();
# Create a chain and add a jump rule to INPUT chain
if (!$self->create_chain('filter', $pre_capture_chain_name)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to create '$pre_capture_chain_name' chain");
return;
}
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $pre_capture_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump to rules added during the pre-capture stage ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to create rule in INPUT chain to jump to '$pre_capture_chain_name' chain");
return;
}
# Allow unrestricted SSH traffic
if (!$self->insert_rule('filter', $pre_capture_chain_name,
{
'parameters' => {
'jump' => 'ACCEPT',
'protocol' => 'tcp',
},
'match_extensions' => {
'tcp' => {
'destination-port' => 22,
},
'comment' => {
'comment' => "VCL: allow traffic to SSH port 22 from any IP address ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall pre-capture configuration on $computer_name, failed to add rule to allow traffic on port 22 to $pre_capture_chain_name chain");
return;
}
if (!$self->isa('VCL::Module::OS::Linux::firewall::firewalld')) {
# Delete all rules explicitly defined for any of the management node IP addresses
# Legacy firewall code would add rules directly to the filter/INPUT table for each management node address
my @mn_ip_addresses = $self->mn_os->get_ip_addresses();
for my $mn_ip_address (@mn_ip_addresses) {
$self->delete_rules('filter', 'INPUT',
{
'parameters' => {
'source' => $mn_ip_address,
},
}
);
}
# Legacy code may have been used previously for a reservation, before an upgrade
# Clean up old connect method rules from the INPUT chain
# Delete all rules from INPUT chain matching connect method protocols and ports
$self->delete_connect_method_rules();
}
# Delete other vcl-* chains added by vcld
my $table_info = $self->get_table_info();
for my $chain_name (keys %$table_info) {
if ($chain_name ne $pre_capture_chain_name && $chain_name =~ /^vcl-/) {
$self->delete_chain('filter', $chain_name);
}
}
$self->save_configuration();
notify($ERRORS{'DEBUG'}, 0, "completed firewall pre-capture configuration on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 process_cluster
Parameters : none
Returns : boolean
Description : Performs the iptables firewall configuration to allow all traffic
from other computers assigned to a cluster request.
=cut
sub process_cluster {
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 $timestamp = makedatestring();
my $request_id = $self->data->get_request_id();
my $computer_name = $self->data->get_computer_short_name();
notify($ERRORS{'DEBUG'}, 0, "beginning firewall cluster configuration on $computer_name");
my $cluster_chain_name = $self->get_cluster_chain_name();
my @cluster_computer_public_ip_addresses = $self->data->get_other_cluster_computer_public_ip_addresses();
# Delete existing chain or else duplicate rules will be added
# This subroutine really should only need to be called once
$self->delete_chain('filter', $cluster_chain_name);
# Create a chain and add a jump rule to INPUT chain
if (!$self->create_chain('filter', $cluster_chain_name)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall cluster configuration on $computer_name, failed to create '$cluster_chain_name' chain");
return;
}
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $cluster_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump to rules added during for cluster reservation ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall cluster configuration on $computer_name, failed to create rule in INPUT chain to jump to '$cluster_chain_name' chain");
return;
}
# Allow all traffic from other cluster computer public IP addresses
if (!$self->insert_rule('filter', $cluster_chain_name,
{
'parameters' => {
'source' => join(',', @cluster_computer_public_ip_addresses),
'jump' => 'ACCEPT',
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: allow all traffic from other computers assigned to cluster request $request_id ($timestamp)",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to complete firewall cluster configuration on $computer_name, failed to add rule allowing traffic from cluster computer public IP addresses to $cluster_chain_name chain");
return;
}
$self->save_configuration();
notify($ERRORS{'DEBUG'}, 0, "completed firewall cluster configuration on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_iptables_semaphore
Parameters : none
Returns : true or VCL::Semaphore object reference
Description : Obtains and returns a VCL::Semaphore object if called from a
subroutine containing 'nat' in the name. This should always be
called prior to executing iptables commands on a host this could
potentially be controlled by multiple vcld processes at the same
time. If multiple iptables commands are attempted at the same
time, the following error is generated:
iptables: Resource temporarily unavailable.
=cut
sub get_iptables_semaphore {
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;
}
# Check if the calling subroutine contains 'nat'
my $calling_subroutine = get_calling_subroutine();
if ($calling_subroutine !~ /(nat)/) {
return 1;
}
my $computer_id = $self->data->get_computer_id();
return $self->get_semaphore("iptables-$computer_id", 120, 1);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 insert_rule
Parameters : $table_name, $chain_name, $rule_specification_hashref
Returns : boolean
Description : Inserts an iptables rule. The argument must be a properly
constructed hash reference. Supported top-level hash keys are:
* {parameters} => {<hash reference>} (optional)
Allows any of the options under the iptables man page
"PARAMETERS" section to be specified. Full parameter names
should be used such as "protocol" instead of "-p".
Parameters can be negated by adding an exclaimation point
before the parameter name.
* {match_extensions} => {<hash reference>} (optional)
Allows any of the options under the iptables man page
"MATCH EXTENSIONS" section to be specified. Each key should
be a match extension module name such as "state". The value
should be a hash reference whose key names should be the
names of the supported options for that match extension
module.
* {target_extensions} => {<hash reference>} (optional)
Allows any of the options under the iptables man page
"TARGET EXTENSIONS" section to be specified. Each key should
be a target extension module name such as "DNAT". The value
should be a hash reference whose key names should be the
names of the supported options for that target extension
module.
Example:
$self->os->firewall->create_chain('nat', 'test');
$self->os->firewall->insert_rule('nat', 'test',
{
'parameters' => {
'protocol' => 'tcp',
'in-interface' => 'eth1',
},
'match_extensions' => {
'comment' => {
'comment' => "forward: eth1:50443 --> 10.1.2.3:443 (tcp)",
},
'tcp' => {
'destination-port' => 50443,
},
},
'target_extensions' => {
'DNAT' => {
'to-destination' => "10.1.2.3:443",
},
},
}
);
=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, $rule_specification_hashref, $check_already_exists) = @_;
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;
}
elsif (!$rule_specification_hashref) {
notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified");
return;
}
elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') {
notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref));
return;
}
elsif (!scalar(keys(%$rule_specification_hashref))) {
notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys");
return;
}
my $computer_name = $self->data->get_computer_hostname();
# Avoid duplicate/redundant rules
my @matching_rules = $self->get_matching_rules($table_name, $chain_name, $rule_specification_hashref);
if (@matching_rules) {
my @specification_strings = map { $_->{"rule_specification"} } @matching_rules;
notify($ERRORS{'OK'}, 0, "$chain_name chain rule in $table_name table already exists on $computer_name:\n" . join("\n", @specification_strings));
return 1;
}
# Convert the specification into valid iptables command arguments
my $argument_string = $self->get_insert_rule_argument_string($rule_specification_hashref);
if (!$argument_string) {
notify($ERRORS{'WARNING'}, 0, "failed to add iptables rule to $chain_name chain in $table_name table on $computer_name, rule specification hash reference could not be converted into an iptables command argument string:\n" . format_data($rule_specification_hashref));
return;
}
my $semaphore = $self->get_iptables_semaphore();
return $self->_insert_rule($table_name, $chain_name, $argument_string);
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _insert_rule
Parameters : $table_name, $chain_name, $argument_string
Returns : boolean
Description : Executes the command to insert a rule. This is a helper
subroutine and should only be called by 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 = "/sbin/iptables --insert $chain_name --table $table_name $argument_string";
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command");
return;
}
elsif ($exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to add iptables 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 iptables rule to $chain_name chain in $table_name table on $computer_name, command: $command");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_insert_rule_argument_string
Parameters : $rule_specification_hashref
Returns : string
Description :
=cut
sub get_insert_rule_argument_string {
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 ($rule_specification_hashref) = @_;
if (!$rule_specification_hashref) {
notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified");
return;
}
elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') {
notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref));
return;
}
elsif (!scalar(keys(%$rule_specification_hashref))) {
notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys");
return;
}
my $argument_string;
# Add the parameters to the arguments string
for my $parameter (sort keys %{$rule_specification_hashref->{parameters}}) {
my $value = $rule_specification_hashref->{parameters}{$parameter};
if ($parameter =~ /^\!/) {
$argument_string .= "! ";
$parameter =~ s/^\!//;
}
$argument_string .= "--$parameter $value ";
}
# Add the match extension to the arguments string
for my $match_extension (sort keys %{$rule_specification_hashref->{match_extensions}}) {
$argument_string .= "--match $match_extension ";
for my $option (sort keys %{$rule_specification_hashref->{match_extensions}{$match_extension}}) {
my $value = $rule_specification_hashref->{match_extensions}{$match_extension}{$option};
if ($option =~ /(comment)/) {
$value = "\"$value\"";
}
if ($option =~ /^\!/) {
$argument_string .= "! ";
$option =~ s/^\!//;
}
$argument_string .= "--$option " if $option;
$argument_string .= "$value ";
}
}
# Add the target extensions to the arguments string
for my $target_extension (sort keys %{$rule_specification_hashref->{target_extensions}}) {
$argument_string .= "--jump $target_extension ";
for my $option (sort keys %{$rule_specification_hashref->{target_extensions}{$target_extension}}) {
my $value = $rule_specification_hashref->{target_extensions}{$target_extension}{$option};
$argument_string .= "--$option " if $option;
$argument_string .= "$value ";
}
}
$argument_string =~ s/\s+$//g;
return $argument_string;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_matching_rules
Parameters : $table_name, $chain_name, $rule_specification_hashref
Returns : array
Description : Checks the chain for any rules that match all parameters
specified in the $rule_specification_hashref argument. For
example, to find all TCP/22 rules:
$self->os->firewall->get_matching_rules('filter', 'INPUT',
{
'parameters' => {
'protocol' => 'tcp',
},
'match_extensions' => {
'tcp' => {
'dport' => 22,
},
},
}
);
=cut
sub get_matching_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, $rule_specification_hashref) = @_;
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;
}
elsif (!$rule_specification_hashref) {
notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified");
return;
}
elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') {
notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref));
return;
}
elsif (!scalar(keys(%$rule_specification_hashref))) {
notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys");
return;
}
my $computer_name = $self->data->get_computer_hostname();
my @matching_rules;
my $table_info = $self->get_table_info($table_name) || return;
if (!defined($table_info->{$chain_name})) {
notify($ERRORS{'DEBUG'}, 0, "no rules match on $computer_name, $table_name table does not contain a '$chain_name' chain");
return @matching_rules;
}
elsif (!defined($table_info->{$chain_name}{rules})) {
notify($ERRORS{'DEBUG'}, 0, "no rules match on $computer_name, $chain_name chain in $table_name table contains no rules") if ($self->{debug});
return @matching_rules;
}
# This sub was designed to accept a hash reference argument to match other
# parts of this module. However, we need to compare the hash reference
# argument to the hash reference which contains current rule info. Comparing
# the two as-is is extremely difficult and would require complex recursion.
# Instead, get_collapsed_hash_reference takes the input multi-level hash
# reference, finds all of the keys which contain a scalar value, and
# constucts concatenated key names containing the values. The key names can
# used in an eval statement to compare another hash reference.
my $collapsed_specification = get_collapsed_hash_reference($rule_specification_hashref);
if (!$collapsed_specification) {
notify($ERRORS{'WARNING'}, 0, "failed to determine if any rules match on $computer_name, failed to parse rule specification hash reference argument:\n" . format_data($rule_specification_hashref));
return;
}
elsif (!scalar keys(%$collapsed_specification)) {
notify($ERRORS{'WARNING'}, 0, "failed to determine if any rules match on $computer_name, attempt to collapse the rule specification hash reference argument produced a result with no keys:\n" . format_data($rule_specification_hashref));
return;
}
notify($ERRORS{'DEBUG'}, 0, "checking if $chain_name chain in $table_name table on $computer_name has any rules matching specifications:\n" . format_data($collapsed_specification)) if ($self->{debug});
# Some iptables options may take multiple forms
# Attempt to try all forms
my $alternate_option_names = {
'destination-port' => 'dport',
'source-port' => 'sport',
};
RULE: for my $rule (@{$table_info->{$chain_name}{rules}}) {
my $rule_specification = $rule->{rule_specification};
for my $specification_key (keys %$collapsed_specification) {
# Ignore comments when comparing
if ($specification_key =~ /(comment)/i) {
next;
}
my $specification_value = $collapsed_specification->{$specification_key};
# Check if matches known alternate ('source-port' <--> 'sport')
my $alternate_specification_key;
for my $original_name (keys %$alternate_option_names) {
if ($specification_key =~ /$original_name/i) {
my $alternate_name = $alternate_option_names->{$original_name};
$alternate_specification_key = $specification_key;
$alternate_specification_key =~ s/$original_name/$alternate_name/i;
}
}
# $specification_key will contain a string such as:
# "{'match_extensions'}{'tcp'}{'dport'}"
# Use this in an eval block to check if the current rule has a matching key and the same value
my $rule_value;
my $eval_string;
if ($alternate_specification_key) {
$eval_string = "\$rule_value = (\$rule->$specification_key || \$rule->$alternate_specification_key)";
}
else {
$eval_string = "\$rule_value = \$rule->$specification_key";
}
eval($eval_string);
if ($EVAL_ERROR) {
notify($ERRORS{'WARNING'}, 0, "failed to determine value of $specification_key key from rule on $computer_name, code evaluated: '$eval_string', error: $EVAL_ERROR, rule:\n" . format_data($rule));
return;
}
elsif (!defined($rule_value)) {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name, it does not contain a $specification_key value, rule specification: '$rule_specification'") if ($self->{debug});
next RULE;
}
if ($rule_value ne $specification_value && $rule_value !~ /^$specification_value(\/32)?$/i) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name:\n" .
# "rule_specification : '$rule_specification'\n" .
# "specification key : '$specification_key'\n" .
# "argument value : '$specification_value'\n" .
# "rule value : '$rule_value'"
#);
next RULE;
}
}
notify($ERRORS{'DEBUG'}, 0, "rule matches: $rule_specification");
push @matching_rules, $rule;
}
my $matching_rule_count = scalar(@matching_rules);
#notify($ERRORS{'DEBUG'}, 0, "found $matching_rule_count matching rule" . ($matching_rule_count == 1 ? '' : 's')) if $matching_rule_count;
return @matching_rules;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_rules
Parameters : $table_name, $chain_name, $rule_specification_hashref
Returns : boolean
Description : Deletes all rules matching the table, chain, and specification
hash reference. The hash must be in the same format that is
returned by get_table_info, such as:
{
"match_extensions" => {
"tcp" => {
"dport" => 22,
},
},
"parameters" => {
"jump" => {
"target" => "ACCEPT",
},
"protocol" => "tcp",
},
}
An existing rule will be deleted if and only if it contains
exactly all of the keys defined in the argument, case sensitive.
The actual value must match but is checked case insensitive.
=cut
sub delete_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, $rule_specification_hashref) = @_;
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;
}
elsif (!$rule_specification_hashref) {
notify($ERRORS{'WARNING'}, 0, "rule specification hash reference argument was not specified");
return;
}
elsif (!ref($rule_specification_hashref) || ref($rule_specification_hashref) ne 'HASH') {
notify($ERRORS{'WARNING'}, 0, "rule specification argument is not a hash reference:\n" . format_data($rule_specification_hashref));
return;
}
elsif (!scalar(keys(%$rule_specification_hashref))) {
notify($ERRORS{'WARNING'}, 0, "rule specification argument does not contain any keys");
return;
}
my $computer_name = $self->data->get_computer_hostname();
my @matching_rules = $self->get_matching_rules($table_name, $chain_name, $rule_specification_hashref);
for my $rule (@matching_rules) {
# Make sure rule has a 'rule_specification' value or else it can't be deleted
my $rule_specification_string = $rule->{rule_specification};
if (!$rule_specification_string) {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule on $computer_name because it does not contain a 'rule_specification' key:\n" . format_data($rule));
next RULE;
}
notify($ERRORS{'DEBUG'}, 0, "attempting to delete rule on $computer_name: $rule_specification_string");
my $semaphore = $self->get_iptables_semaphore();
$self->_delete_rule($table_name, $chain_name, $rule_specification_string) || return;
}
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _delete_rule
Parameters : $table_name, $chain_name, $rule_specification_string
Returns : boolean
Description : Executes the command to delete a rule. This is a helper
subroutine and should only be called by delete_rules.
=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 = "/sbin/iptables --delete $chain_name -t $table_name $rule_specification_string";
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command on $computer_name: $command");
return;
}
elsif ($exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to delete rule on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
else {
notify($ERRORS{'OK'}, 0, "deleted rule on $computer_name with specification: '$rule_specification_string'");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_connect_method_rules
Parameters : none
Returns : boolean
Description : Deletes all rules from the INPUT chain in the filter table
matching any connect method ports.
=cut
sub delete_connect_method_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 @protocol_ports = $self->data->get_connect_method_protocol_port_array();
for my $protocol_port (@protocol_ports) {
my ($protocol, $port) = @$protocol_port;
$self->delete_rules('filter', 'INPUT',
{
'parameters' => {
'protocol' => $protocol,
},
'match_extensions' => {
$protocol => {
'dport' => $port,
},
},
}
);
}
notify($ERRORS{'DEBUG'}, 0, "deleted explicit rules from INPUT chain in filter table for all connect method ports");
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 $semaphore = $self->get_iptables_semaphore();
my $command = "/sbin/iptables --new-chain $chain_name --table $table_name";
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
return;
}
elsif (grep(/already exists/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;
}
else {
notify($ERRORS{'OK'}, 0, "created '$chain_name' chain in '$table_name' table on $computer_name");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 delete_chain
Parameters : $table_name, $chain_name
Returns : boolean
Description : Deletes the specified chain from the table. All rules
which exist in the chain or reference the chain are deleted prior
to deletion of the chain.
=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;
}
# Flush the chain first - delete will fail if the chain still contains rules
if (!$self->flush_chain($table_name, $chain_name)) {
notify($ERRORS{'WARNING'}, 0, "unable to delete '$chain_name' chain from '$table_name' table on $computer_name, failed to flush chain prior to deletion");
return;
}
# Delete all rules which reference the chain being deleted or else the chain can't be deleted
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;
}
my $command = "/sbin/iptables --delete-chain $chain_name --table $table_name";
my $semaphore = $self->get_iptables_semaphore();
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
return;
}
elsif (grep(/Too many links/i, @$output)) {
notify($ERRORS{'WARNING'}, 0, "unable to delete '$chain_name' chain from '$table_name' table on $computer_name, the chain is referenced by another rule");
return;
}
elsif ($exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to delete '$chain_name' chain from '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return;
}
else {
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 delete_chain_references
Parameters : $table_name, $chain_name
Returns : boolean
Description : Checks all chains in the specified table for references to the
$chain_name argument. If found, the referencing rules are
deleted.
=cut
sub delete_chain_references {
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 $table_info = $self->get_table_info($table_name);
for my $referencing_chain_name (keys %$table_info) {
$self->delete_rules($table_name, $referencing_chain_name,
{
"parameters" => {
"jump" => $chain_name,
},
}
);
}
notify($ERRORS{'DEBUG'}, 0, "deleted all rules in '$table_name' table referencing '$chain_name' chain on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 chain_exists
Parameters : $table_name, $chain_name
Returns : boolean
Description : Determines if an iptables chain exists in the table specified.
=cut
sub chain_exists {
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 $table_info = $self->get_table_info($table_name) || return;
if (defined($table_info->{$chain_name})) {
notify($ERRORS{'DEBUG'}, 0, "$chain_name chain exists in $table_name table on $computer_name");
return 1;
}
else {
notify($ERRORS{'DEBUG'}, 0, "'$chain_name' chain does NOT exist in '$table_name' table on $computer_name");
return 0;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_table_chain_names
Parameters : $table_name
Returns : array
Description : Returns an array containing the chain names defined for a table.
=cut
sub get_table_chain_names {
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) = @_;
if (!defined($table_name)) {
notify($ERRORS{'WARNING'}, 0, "table name argument was not specified");
return;
}
my $computer_name = $self->data->get_computer_hostname();
my $table_info = $self->get_table_info($table_name, 1) || return;
my @table_chain_names = sort keys %$table_info;
notify($ERRORS{'DEBUG'}, 0, "retrieved chain names defined in $table_name table on $computer_name: " . join(', ', @table_chain_names)) if ($self->{debug});
return @table_chain_names;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 nat_sanitize_reservation
Parameters : $reservation_id (optional)
Returns : boolean
Description : Deletes the chains created for a reservation on a NAT host. Saves
the iptables configuration.
=cut
sub nat_sanitize_reservation {
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 $reservation_id = shift || $self->data->get_reservation_id();
my $reservation_chain_name = $self->get_nat_reservation_chain_name($reservation_id);
if (!$self->delete_chain('nat', $reservation_chain_name)) {
return;
}
$self->save_configuration();
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 flush_chain
Parameters : $table_name, $chain_name
Returns : boolean
Description : Flushes (deletes) rules from the specified chain.
=cut
sub flush_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 = "/sbin/iptables --flush $chain_name --table $table_name";
my $semaphore = $self->get_iptables_semaphore();
my ($exit_status, $output) = $self->_execute_iptables($command);
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 flush '$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, "flushed '$chain_name' chain in '$table_name' table on $computer_name");
return 1;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_table_info
Parameters : $table_name
Returns : boolean
Description : Retrieves the configuration of an iptables table and constructs a
hash reference. Information from the 'filter' table is returned
if the $table_name argument is not specified. Example:
{
"OUTPUT" => {
"policy" => "ACCEPT"
},
"PREROUTING" => {
"policy" => "ACCEPT",
"rules" => [
{
"parameters" => {
"jump" => {
"target" => "vcld-3116"
}
},
"rule_specification" => "-j vcld-3116"
}
]
},
"vcld-3116" => {
"rules" => [
{
"match_extensions" => {
"comment" => {
"comment" => "forward: eth1:18892 --> 192.168.110.201:53 (tcp)"
},
"tcp" => {
"dport" => 18892
}
},
"parameters" => {
"in-interface" => "eth1",
"jump" => {
"target" => "DNAT",
"to-destination" => "192.168.110.201:53"
},
"protocol" => "tcp"
},
"rule_specification" => "-i eth1 -p tcp -m comment --comment \"forward: eth1:18892 --> 192.168.110.201:53 (tcp)\" -m tcp --dport 18892 -j DNAT --to-destination 192.168.110.201:53"
}
]
}
}
=cut
sub get_table_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 ($table_name) = @_;
$table_name = 'filter' unless $table_name;
$ENV{iptables_get_table_info_count}{$table_name}++;
my $computer_name = $self->data->get_computer_hostname();
my @lines;
my $command = "/sbin/iptables --list-rules --table $table_name";
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $command");
return;
}
elsif (grep(/Unknown arg/i, @$output)) {
# Older versions of iptables don't support --list-rules
# Error output:
# iptables v1.3.5: Unknown arg `--list-rules'
# Try iptables-save
notify($ERRORS{'DEBUG'}, 0, "version of iptables installed on $computer_name does NOT support the --list-rules option, trying iptables-save");
my $iptables_save_command = "/sbin/iptables-save";
my ($iptables_save_exit_status, $iptables_save_output) = $self->os->execute($iptables_save_command, 0);
if (!defined($iptables_save_output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command $computer_name: $iptables_save_command");
return;
}
elsif ($iptables_save_exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to list rules from '$table_name' table on $computer_name, iptables does not support the --list-rules option and iptables-save returned exit status: $iptables_save_exit_status, command:\n$iptables_save_command\noutput:\n" . join("\n", @$iptables_save_output));
return 0;
}
else {
# Extract lines like:
# -A INPUT -p tcp...
@lines = grep(/^-[A-Z]\s/, @$iptables_save_output);
#notify($ERRORS{'DEBUG'}, 0, "parsed iptables-save output for command lines, output:\n" . join("\n", @$iptables_save_output) . "\ncommand lines:\n" . join("\n", @lines));
}
}
elsif ($exit_status ne '0') {
notify($ERRORS{'WARNING'}, 0, "failed to list rules from '$table_name' table on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
else {
@lines = @$output;
}
if ($self->can('get_all_direct_rules')) {
# Convert:
# ipv4 filter vcl-pre_capture 0 --jump ACCEPT --protocol tcp --match comment --comment 'VCL: ...' --match tcp --destination-port 22
# ipv4 nat POSTROUTING 0 '!' --destination 10.0.0.0/20 --jump MASQUERADE --out-interface eth1 --match comment --comment 'blah... blah'
# To:
# -A vcl-pre_capture -p tcp -m comment --comment "VCL: ..." -m tcp --dport 22 -j ACCEPT
DIRECT_RULE: for my $direct_rule ($self->get_all_direct_rules()) {
my ($rule_protocol, $rule_table, $rule_chain, $rule_priority, $rule_specification) = $direct_rule =~
/^
(\S+)\s+
(\S+)\s+
(\S+)\s+
(\d+)\s+
(\S.*)
$/x
;
if (!defined($rule_specification)) {
notify($ERRORS{'WARNING'}, 0, "failed to parse firewalld direct rule: $direct_rule");
next DIRECT_RULE;
}
elsif ($rule_table ne $table_name) {
#notify($ERRORS{'DEBUG'}, 0, "ignoring rule, table does not match '$table_name': $direct_rule");
next DIRECT_RULE;
}
my $converted_rule = "-A $rule_chain $rule_specification";
#notify($ERRORS{'DEBUG'}, 0, "converted iptables direct rule to iptables format:\n" .
# "direct rule : $direct_rule\n" .
# "iptables format : $converted_rule"
#);
push @lines, $converted_rule;
}
}
my $table_info = {};
LINE: for my $line (@lines) {
# Split the rule, samples:
# -P OUTPUT ACCEPT
# -N vcld-3115
# -A PREROUTING -j vclark-3115
# -A POSTROUTING ! -d 192.168.96.0/20 -o eth1
# -A INPUT -d 192.168.96.0/32 -i eth1 -p udp -m multiport --dports 5700:6500,9696:9701,49152:65535 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
my ($iptables_command, $chain_name, $rule_specification_string) = $line =~
/
^
(--?[a-z\-]+) # command: -A, -N, etc
\s+ # space after command
([^ ]+) # chain name
\s* # space after chain name
(.*) # remainder of rule
\s* # trailing spaces
$
/ixg;
if (!defined($iptables_command)) {
notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, iptables command type (ex. '-A') could not be parsed from beginning of line:\n$line");
next LINE;
}
elsif (!defined($chain_name)) {
notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, iptables chain name could not be parsed from line:\n$line");
next LINE;
}
# Make sure the rule specification isn't null to avoid warnings
$rule_specification_string = '' unless defined($rule_specification_string);
# Remove spaces from end of rule specification
$rule_specification_string =~ s/\s+$//;
#notify($ERRORS{'DEBUG'}, 0, "split iptables line:\n" .
# "line : '$line'\n" .
# "command : '$iptables_command'\n" .
# "chain : '$chain_name'\n" .
# "specification : '$rule_specification_string'"
#);
if ($iptables_command =~ /^(-P|--policy)/) {
# -P, --policy chain target (Set the policy for the chain to the given target)
$table_info->{$chain_name}{policy} = $rule_specification_string;
}
elsif ($iptables_command =~ /^(-N|--new-chain)/) {
# -N, --new-chain chain
$table_info->{$chain_name} = {} unless defined($table_info->{$chain_name});
}
elsif ($iptables_command =~ /^(-A|--append chain)/) {
# -A, --append chain rule-specification
#notify($ERRORS{'DEBUG'}, 0, "parsing iptables append rule command:\n" .
# "iptables command: $line\n" .
# "iptables rule specification: $rule_specification_string"
#);
my $rule = {};
$rule->{rule_specification} = $rule_specification_string;
# Parse the rule parameters
# Be sure to check for ! enclosed in quotes:
# -A POSTROUTING '!' --destination 10.10.0.0/20 --jump MASQUERADE
my $parameters = {
'protocol' => '(?:\'?(\!?)\'?\s)?(-p|--protocol)\s+([^\s]+)',
'source' => '(?:\'?(\!?)\'?\s)?(-s|--source)\s+([\d\.\/]+)',
'destination' => '(?:\'?(\!?)\'?\s)?(-d|--destination)\s+([\d\.\/]+)',
'in-interface' => '(?:\'?(\!?)\'?\s)?(-i|--in-interface)\s+([^\s]+)',
'out-interface' => '(?:\'?(\!?)\'?\s)?(-o|--out-interface)\s+([^\s]+)',
'fragment' => '(?:\'?(\!?)\'?\s)?(-f|--fragment)',
};
PARAMETER: for my $parameter (keys %$parameters) {
my $pattern = $parameters->{$parameter};
my ($inverted, $parameter_match, $value) = $rule_specification_string =~ /$pattern/ig;
next PARAMETER unless $parameter_match;
if ($inverted) {
$rule->{parameters}{"!$parameter"} = $value;
}
else {
$rule->{parameters}{$parameter} = $value;
}
# Remove the matching pattern from the rule specification string
# This is done to make it easier to parse the match extension parts of the specification later on
my $rule_specification_string_before = $rule_specification_string;
$rule_specification_string =~ s/(^\s+|$pattern|\s+$)//igx;
#notify($ERRORS{'DEBUG'}, 0, "trimmed $parameter parameter:\n" .
# "before : '$rule_specification_string_before'\n" .
# "after : '$rule_specification_string'"
#);
}
# -j ACCEPT
# -j REJECT --reject-with icmp-host-prohibited
# -j LOG --log-prefix "[UFW BLOCK] "
my $target_section_regex = <<'EOF';
(
(-[jg]|--(?:jump|goto))
\s+
([^\s]+)
(
(?:
(?!\s+(?:-m|--match)\s+)
.
)*
)
)
EOF
my ($target_section_match, $target_parameter_match, $target, $target_extension_option_string) = $rule_specification_string =~ /$target_section_regex/ix;
if ($target_parameter_match) {
my $target_parameter_type = ($target_parameter_match =~ /j/ ? 'jump' : 'goto');
$rule->{parameters}{$target_parameter_type} = $target;
my $target_extension_option_name;
# Need to split line not just by spaces, but also find sections enclosed in quotes:
# -j REJECT --reject-with icmp-host-prohibited
# -j LOG --log-prefix "IN_public_DROP: "
my @target_extension_option_sections = $target_extension_option_string =~
/
(
['"][^'"]*['"]
|
[^\s]+
)
/gx;
TARGET_OPTION_SECTION: for my $target_extension_option_section (@target_extension_option_sections) {
# Check if this is the beginning of a target extension option
if ($target_extension_option_section =~ /^[-]+(\w[\w-]+)/) {
$target_extension_option_name = $1;
#notify($ERRORS{'DEBUG'}, 0, "located $target_parameter/$target target extension option: $target_extension_option_name");
$rule->{target_extensions}{$target}{$target_extension_option_name} = undef;
}
elsif (!$target_extension_option_name) {
# If here, the section should be a target extension option value
notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule on $computer_name, target extension option name was not detected before this section: '$target_extension_option_section'\n" .
"output line: $line\n" .
"target section: $target_section_match"
);
next LINE;
}
else {
# Found target extension option value
$rule->{target_extensions}{$target}{$target_extension_option_name} = $target_extension_option_section;
$target_extension_option_name = undef;
}
} # TARGET_OPTION_SECTION
my $rule_specification_string_before = $rule_specification_string;
$rule_specification_string =~ s/(^\s+|$target_section_regex|\s+$)//igx;
if ($rule_specification_string_before ne $rule_specification_string) {
#notify($ERRORS{'DEBUG'}, 0, "trimmed $target_parameter_type target section:\n" .
# "before : '$rule_specification_string_before'\n" .
# "after : '$rule_specification_string'"
#);
}
else {
notify($ERRORS{'WARNING'}, 0, "regex failed to remove target section from rule specification:\n" .
"line : $line\n" .
"remaining rule specification before : $rule_specification_string_before\n" .
"remaining rule specification after : $rule_specification_string\n" .
"target section regex:\n$target_section_regex"
);
}
}
else {
notify($ERRORS{'WARNING'}, 0, "target section was not found in rule specification: '$rule_specification_string', line: '$line'");
}
# The only text remaining in $rule_specification_string should be match extension information
# Make sure space exists between match extension module name (comment) and the option
# --match comment--comment 'my comment'
# --match tcp--destination-port
$rule_specification_string =~ s/(--match [^\s-]+)--/$1 --/g;
# Split the remaining string by spaces or sections enclosed in quotes
my @match_extension_sections = $rule_specification_string =~
/
(
['"][^'"]*['"]
|
[^\s]+
)
/gx;
# Match extensions will be in the form:
# -m,--match <module> [!] -<x>,--<option> <value> [[!] -<x>,--<option> <value>...]
my $match_extension_module_name;
my $match_extension_option;
my $match_extension_option_inverted = 0;
my $comment;
MATCH_EXTENSION_SECTION: for my $match_extension_section (@match_extension_sections) {
next MATCH_EXTENSION_SECTION if !$match_extension_section;
# Check if the section is the beginning of a match extension specification
if ($match_extension_section =~ /^(-m|--match)$/) {
$match_extension_module_name = undef;
$match_extension_option = undef;
$match_extension_option_inverted = 0;
next MATCH_EXTENSION_SECTION;
}
# Parse match extension module name
if (!$match_extension_module_name) {
# Haven't found module name for this match extension specification
# If section begins with a letter it should be the match extension module name
if ($match_extension_section =~ /^[a-z]/i) {
$match_extension_module_name = $match_extension_section;
#notify($ERRORS{'DEBUG'}, 0, "located match extension module name: $match_extension_module_name");
next MATCH_EXTENSION_SECTION;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule in $table_name table on $computer_name\n" .
"match extension module name was not detected before this section: '$match_extension_section'\n" .
"iptables rule specification: '$rule_specification_string'\n" .
"iptables command: '$line'"
);
next LINE;
}
}
# Check if this is the beginning of a match extension option
if ($match_extension_section =~ /^[-]+(\w[\w-]+)/) {
$match_extension_option = $1;
if ($match_extension_option_inverted) {
$match_extension_option = "!$match_extension_option";
$match_extension_option_inverted = 0;
}
#notify($ERRORS{'DEBUG'}, 0, "match extension module name: $match_extension_module_name, located match extension option: $match_extension_option");
next MATCH_EXTENSION_SECTION;
}
elsif ($match_extension_section =~ /^!/) {
$match_extension_option_inverted = 1;
next MATCH_EXTENSION_SECTION;
}
# If here, the section should be (part of) a match extension option value
if (!$match_extension_option) {
notify($ERRORS{'WARNING'}, 0, "failed to parse iptables rule, match extension option name was not detected before this section: '$match_extension_section'\n" .
"iptables command: $line\n" .
"iptables rule specification: $rule_specification_string\n" .
"preceeding match extension module name: $match_extension_module_name"
);
next LINE;
}
# Check if this is part of a comment
if ($match_extension_module_name =~ /(comment)/) {
$comment .= "$match_extension_section ";
next MATCH_EXTENSION_SECTION;
}
$rule->{match_extensions}{$match_extension_module_name}{$match_extension_option} = $match_extension_section;
}
if ($comment) {
# Remove quotes from beginning and end of comment
$comment =~ s/(^[\\\"]+|[\s\\\"]+$)//g;
$rule->{match_extensions}{comment}{comment} = $comment;
$comment = undef;
}
push @{$table_info->{$chain_name}{rules}}, $rule;
}
else {
notify($ERRORS{'WARNING'}, 0, "iptables '$iptables_command' command is not supported: $line");
next LINE;
}
}
#notify($ERRORS{'DEBUG'}, 0, "retrieved rules from iptables $table_name table from $computer_name:\n" . format_data($table_info));
return $table_info;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 nat_configure_host
Parameters : none
Returns : boolean
Description : Configures the iptables firewall to pass NAT traffic.
=cut
sub nat_configure_host {
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 $public_ip_address = $self->data->get_nathost_public_ip_address();
my $internal_ip_address = $self->data->get_nathost_internal_ip_address();
my $nat_host_chain_name = $self->get_nat_host_chain_name();
# Enable IP port forwarding
if (!$self->os->enable_ip_forwarding()) {
notify($ERRORS{'WARNING'}, 0, "unable to configure NAT host $computer_name, failed to enable IP forwarding");
return;
}
my $nat_table_info = $self->get_table_info('nat');
if (!$nat_table_info) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT on $computer_name, nat table info could not be retrieved");
return;
}
elsif (!defined($nat_table_info->{PREROUTING})) {
notify($ERRORS{'WARNING'}, 0, "unable to configure NAT on $computer_name, nat table does not contain a PREROUTING chain:\n" . format_data($nat_table_info));
return;
}
elsif (!defined($nat_table_info->{POSTROUTING})) {
notify($ERRORS{'WARNING'}, 0, "unable to configure NAT on $computer_name, nat table does not contain a POSTROUTING chain:\n" . format_data($nat_table_info));
return;
}
# Check if NAT has previously been configured
if (defined($nat_table_info->{$nat_host_chain_name})) {
notify($ERRORS{'DEBUG'}, 0, "NAT has already been configured on $computer_name, '$nat_host_chain_name' chain exists in nat table");
return 1;
}
else {
# Before VCL 2.5, dedicated NAT host chain wasn't created, check if MASQUERADE rule exists
for my $rule (@{$nat_table_info->{POSTROUTING}{rules}}) {
my $rule_specification_string = $rule->{rule_specification};
if ($rule_specification_string =~ /MASQUERADE/) {
notify($ERRORS{'DEBUG'}, 0, "POSTROUTING chain in nat table contains a MASQUERADE rule, assuming NAT has already been configured: $rule_specification_string");
return 1;
}
}
}
# Figure out the public and internal interface names
my $public_interface_name;
my $internal_interface_name;
my $public_subnet_mask;
my $internal_subnet_mask;
my $network_configuration = $self->os->get_network_configuration();
for my $interface_name (keys %$network_configuration) {
my @ip_addresses = keys %{$network_configuration->{$interface_name}{ip_address}};
# Check if the interface is assigned the nathost.publicIPaddress
if (grep { $_ eq $public_ip_address } @ip_addresses) {
$public_interface_name = $interface_name;
$public_subnet_mask = $network_configuration->{$interface_name}{ip_address}{$public_ip_address};
}
# If nathost.internalIPaddress is set, check if interface is assigned matching IP address
if (grep { $_ eq $internal_ip_address } @ip_addresses) {
$internal_interface_name = $interface_name;
$internal_subnet_mask = $network_configuration->{$interface_name}{ip_address}{$internal_ip_address};
}
}
if (!$public_interface_name) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name, no interface is assigned the public IP address configured in the nathost table: $public_ip_address\n" . format_data($network_configuration));
return;
}
if (!$internal_interface_name) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name, no interface is assigned the internal IP address configured in the nathost table: $internal_ip_address\n" . format_data($network_configuration));
return;
}
my ($public_network_address, $public_network_bits) = ip_address_to_network_address($public_ip_address, $public_subnet_mask);
my ($internal_network_address, $internal_network_bits) = ip_address_to_network_address($internal_ip_address, $internal_subnet_mask);
notify($ERRORS{'DEBUG'}, 0, "determined NAT host interfaces:\n" .
"public - interface: $public_interface_name, IP address: $public_ip_address/$public_subnet_mask, network: $public_network_address/$public_network_bits\n" .
"internal - interface: $internal_interface_name, IP address: $internal_ip_address/$internal_subnet_mask, network: $internal_network_address/$internal_network_bits"
);
my @natport_ranges = get_natport_ranges();
my $destination_ports = '';
for my $natport_range (@natport_ranges) {
my ($start_port, $end_port) = @$natport_range;
if (!defined($start_port)) {
notify($ERRORS{'WARNING'}, 0, "unable to parse NAT port range: '$natport_range'");
next;
}
$destination_ports .= "," if ($destination_ports);
$destination_ports .= "$start_port:$end_port";
}
$self->create_chain('filter', $nat_host_chain_name);
$self->create_chain('nat', $nat_host_chain_name);
if (!$self->insert_rule('filter', 'INPUT',
{
'parameters' => {
'jump' => $nat_host_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump from filter table INPUT chain to NAT host $nat_host_chain_name chain",
},
},
}
)) {
return;
}
if (!$self->insert_rule('filter', 'FORWARD',
{
'parameters' => {
'jump' => $nat_host_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump from filter table FORWARD chain to NAT host $nat_host_chain_name chain",
},
},
}
)) {
return;
}
if (!$self->insert_rule('nat', 'POSTROUTING',
{
'parameters' => {
'jump' => $nat_host_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump from nat table POSTROUTING chain to NAT host $nat_host_chain_name chain",
},
},
}
)) {
return;
}
if (!$self->insert_rule('nat', $nat_host_chain_name,
{
'parameters' => {
'out-interface' => $public_interface_name,
'!destination' => "$internal_network_address/$internal_network_bits",
'jump' => 'MASQUERADE',
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: change IP of outbound $public_interface_name packets to NAT host IP address $public_ip_address",
},
},
}
)) {
return;
}
if (!$self->insert_rule('filter', $nat_host_chain_name,
{
'parameters' => {
'in-interface' => $public_interface_name,
'destination' => $public_ip_address,
'jump' => 'ACCEPT',
'protocol' => 'tcp',
},
'match_extensions' => {
'state' => {
'state' => 'NEW,RELATED,ESTABLISHED',
},
'multiport' => {
'destination-ports' => $destination_ports,
},
'comment' => {
'comment' => "VCL: allow inbound TCP traffic on the NAT port ranges to public $public_interface_name",
},
},
}
)) {
return;
}
if (!$self->insert_rule('filter', $nat_host_chain_name,
{
'parameters' => {
'in-interface' => $public_interface_name,
'destination' => $public_ip_address,
'jump' => 'ACCEPT',
'protocol' => 'udp',
},
'match_extensions' => {
'state' => {
'state' => 'NEW,RELATED,ESTABLISHED',
},
'multiport' => {
'destination-ports' => $destination_ports,
},
'comment' => {
'comment' => "VCL: allow inbound UDP traffic on the NAT port ranges to public $public_interface_name",
},
},
}
)) {
return;
}
if (!$self->insert_rule('filter', $nat_host_chain_name,
{
'parameters' => {
'in-interface' => $public_interface_name,
'out-interface' => $internal_interface_name,
'jump' => 'ACCEPT',
},
'match_extensions' => {
'state' => {
'state' => 'NEW,RELATED,ESTABLISHED',
},
'comment' => {
'comment' => "VCL: forward inbound packets from public $public_interface_name to internal $internal_interface_name",
},
},
}
)) {
return;
}
if (!$self->insert_rule('filter', $nat_host_chain_name,
{
'parameters' => {
'in-interface' => $internal_interface_name,
'out-interface' => $public_interface_name,
'jump' => 'ACCEPT',
},
'match_extensions' => {
'state' => {
'state' => 'NEW,RELATED,ESTABLISHED',
},
'comment' => {
'comment' => "VCL: forward outbound packets from internal $internal_interface_name to public $public_interface_name",
},
},
}
)) {
return;
}
$self->save_configuration();
notify($ERRORS{'DEBUG'}, 0, "successfully configured NAT on $computer_name");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 nat_configure_reservation
Parameters : none
Returns : boolean
Description : Adds a chain named after the reservation ID to the nat table.
Adds a rule to the PREROUTING table to jump to the reservation
chain.
=cut
sub nat_configure_reservation {
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 $reservation_id = $self->data->get_reservation_id();
my $computer_name = $self->data->get_computer_hostname();
my $nat_table_info = $self->get_table_info('nat');
if (!$nat_table_info) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name for reservation, nat table information could not be retrieved");
return;
}
my $reservation_nat_chain_name = $self->get_nat_reservation_chain_name();
# Check if chain for reservation has already been created
if (defined($nat_table_info->{$reservation_nat_chain_name})) {
notify($ERRORS{'DEBUG'}, 0, "'$reservation_nat_chain_name' chain already exists in nat table on $computer_name");
}
elsif (!$self->create_chain('nat', $reservation_nat_chain_name)) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name for reservation, failed to add '$reservation_nat_chain_name' chain to nat table");
return;
}
# Check if rule to jump to reservation's chain already exists in the PREROUTING table
for my $rule (@{$nat_table_info->{PREROUTING}{rules}}) {
my $rule_specification_string = $rule->{rule_specification};
if ($rule_specification_string =~ /-j $reservation_nat_chain_name(\s|$)/) {
notify($ERRORS{'DEBUG'}, 0, "PREROUTING chain in nat table on $computer_name already contains a rule to jump to '$reservation_nat_chain_name' chain: $rule_specification_string");
return 1;;
}
}
# Add a rule to the nat PREROUTING chain
if (!$self->insert_rule('nat', 'PREROUTING',
{
'parameters' => {
'jump' => $reservation_nat_chain_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: jump from nat table PREROUTING chain to reservation NAT chain $reservation_nat_chain_name",
},
},
}
)) {
notify($ERRORS{'WARNING'}, 0, "failed to configure NAT host $computer_name for reservation, failed to create rule in PREROUTING chain in nat table to jump to '$reservation_nat_chain_name' chain");
return;
}
$self->save_configuration();
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 nat_add_port_forward
Parameters : $protocol, $source_port, $destination_ip_address, $destination_port
Returns : boolean
Description : Forwards a port via DNAT.
=cut
sub nat_add_port_forward {
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 ($protocol, $source_port, $destination_ip_address, $destination_port) = @_;
if (!defined($protocol)) {
notify($ERRORS{'WARNING'}, 0, "protocol argument was not provided");
return;
}
elsif (!defined($source_port)) {
notify($ERRORS{'WARNING'}, 0, "source port argument was not provided");
return;
}
elsif (!defined($destination_ip_address)) {
notify($ERRORS{'WARNING'}, 0, "destination IP address argument was not provided");
return;
}
elsif (!defined($destination_port)) {
notify($ERRORS{'WARNING'}, 0, "destination port argument was not provided");
return;
}
my $chain_name = $self->get_nat_reservation_chain_name();
$protocol = lc($protocol);
my $public_interface_name = $self->os->get_public_interface_name();
my $nat_table_info = $self->get_table_info('nat');
if (!$nat_table_info) {
notify($ERRORS{'WARNING'}, 0, "failed to add NAT port forward on $computer_name, nat table information could not be retrieved");
return;
}
# Check if rule has previously been added
for my $rule (@{$nat_table_info->{$chain_name}{rules}}) {
my $rule_target = $rule->{parameters}{jump} || '<not set>';
if ($rule_target ne 'DNAT') {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule, target is not DNAT: $rule_target");
next;
}
my $rule_protocol = $rule->{parameters}{protocol} || '<not set>';
if (lc($rule_protocol) ne $protocol) {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule, protocol '$rule_protocol' does not match protocol argument: '$protocol'");
next;
}
my $rule_source_port = $rule->{match_extensions}{$protocol}{dport} || '<not set>';
if ($rule_source_port ne $source_port) {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule, source port $rule_source_port does not match argument: $source_port");
next;
}
my $rule_destination = $rule->{target_extensions}{DNAT}{'to-destination'} || '<not set>';
if ($rule_destination ne "$destination_ip_address:$destination_port") {
notify($ERRORS{'DEBUG'}, 0, "ignoring rule, destination $rule_destination does not match argument: $destination_ip_address:$destination_port");
next;
}
my $rule_specification_string = $rule->{'rule_specification'};
notify($ERRORS{'DEBUG'}, 0, "NAT port forwared rule already exists, chain: $chain_name, protocol: $protocol, source port: $source_port, destination: $destination_ip_address:$destination_port\nrule specification:\n$rule_specification_string");
return 1;
}
if ($self->insert_rule('nat', $chain_name,
{
'parameters' => {
'protocol' => $protocol,
'in-interface' => $public_interface_name,
},
'match_extensions' => {
'comment' => {
'comment' => "VCL: forward $public_interface_name:$protocol/$source_port --> $destination_ip_address:$destination_port",
},
$protocol => {
'destination-port' => $source_port,
},
},
'target_extensions' => {
'DNAT' => {
'to-destination' => "$destination_ip_address:$destination_port",
},
},
}
)) {
notify($ERRORS{'OK'}, 0, "added NAT port forward on $computer_name: $public_interface_name:$source_port --> $destination_ip_address:$destination_port");
$self->save_configuration();
return 1;
}
else {
notify($ERRORS{'WARNING'}, 0, "failed to add NAT port forward on $computer_name: $public_interface_name:$source_port --> $destination_ip_address:$destination_port");
return;
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 save_configuration
Parameters : $file_path (optional)
Returns : boolean
Description : Saves the current iptables configuration by running
iptables-save. If no file path argument is provided, the output
is saved to /etc/sysconfig/iptables.
=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 $file_path = shift || '/etc/sysconfig/iptables';
my $computer_id = $self->data->get_computer_id();
my $computer_name = $self->data->get_computer_hostname();
# Get the output of iptables-save
# IMPORTANT: don't simply redirect the output to the file
# If iptables is stopped or else the previously saved configuration will be overwritten
my $command = '/sbin/iptables-save';
my ($exit_status, $output) = $self->_execute_iptables($command);
if (!defined($output)) {
notify($ERRORS{'WARNING'}, 0, "failed to execute command to save iptables configuration on $computer_name");
return;
}
elsif ($exit_status ne 0) {
notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, exit status: $exit_status, command:\n$command\noutput:\n" . join("\n", @$output));
return 0;
}
# Make sure output contains at least 1 line beginning with "-A"
# If the iptables service is stopped the output will be blank
# If the iptables service is stopped but "iptables -L" is executed the output may contain something like:
# #Generated by iptables-save v1.4.7 on Thu Mar 5 13:36:51 2015
# *filter
# :INPUT ACCEPT [40:4736]
# :FORWARD ACCEPT [0:0]
# :OUTPUT ACCEPT [8:1200]
# COMMIT
# #Completed on Thu Mar 5 13:36:51 2015
if (!grep(/\w/, @$output)) {
notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, iptables service may not be running, no output was returned from $command");
return 0;
}
elsif (!grep(/^-A/, @$output)) {
notify($ERRORS{'WARNING'}, 0, "iptables configuration not saved to $file_path on $computer_name for safety, iptables service may not be running, output of $command does not contain any lines beginning with '-A':\n" . join("\n", @$output));
return 0;
}
my $semaphore = $self->get_iptables_semaphore();
if (!$semaphore) {
notify($ERRORS{'WARNING'}, 0, "failed to save iptables configuration on $computer_name, $file_path already exists and semaphore could not be obtained to avoid multiple processes writing to the file at the same time");
return;
}
return $self->os->create_text_file($file_path, join("\n", @$output));
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_pre_capture_chain_name
Parameters : none
Returns : string
Description : Returns 'vcl-pre_capture'.
=cut
sub get_pre_capture_chain_name {
return 'vcl-pre_capture';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_post_load_chain_name
Parameters : none
Returns : string
Description : Returns 'vcl-post_load'.
=cut
sub get_post_load_chain_name {
return 'vcl-post_load';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_reserved_chain_name
Parameters : none
Returns : string
Description : Returns 'vcl-reserved'.
=cut
sub get_reserved_chain_name {
return 'vcl-reserved';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_nat_host_chain_name
Parameters : none
Returns : string
Description : Returns the name of the iptables chain on the NAT host containing
rules for NAT to function. Returns 'vcl-nat_host'.
=cut
sub get_nat_host_chain_name {
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;
}
return 'vcl-nat_host';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_nat_reservation_chain_name
Parameters : $reservation_id (optional)
Returns : string
Description : Returns the name of the iptables chain containing rules for a
VCL reservation: '<vcld process name>-<reservation ID>'
=cut
sub get_nat_reservation_chain_name {
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 $reservation_id = shift || $self->data->get_reservation_id();
return "$PROCESSNAME-$reservation_id";
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_inuse_chain_name
Parameters : $reservation_id (optional)
Returns : string
Description : Returns the name of the iptables chain containing rules added
during the inuse state for a VCL reservation.
=cut
sub get_inuse_chain_name {
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;
}
return 'vcl-inuse';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 get_cluster_chain_name
Parameters : none
Returns : string
Description : Returns 'vcl-cluster'.
=cut
sub get_cluster_chain_name {
return 'vcl-cluster';
}
#//////////////////////////////////////////////////////////////////////////////
=head2 nat_delete_orphaned_reservation_chains
Parameters : none
Returns : boolean
Description : Checks all of the chains that exist in the nat table on a NAT
host. Chains which don't begin with the vcld process name
followed by a hyphen (ex. 'vcld-') are ignored. Retrieves list of
all reservation IDs currently in the database. If a chain exists
but a corresponding reservation does not, the chain is deleted.
=cut
sub nat_delete_orphaned_reservation_chains {
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 @reservation_ids = get_all_reservation_ids();
if (!@reservation_ids) {
notify($ERRORS{'WARNING'}, 0, "not deleting orphaned reservation chains on $computer_name, failed to retrieve all reservation IDs from the database");
return;
}
my %reservation_id_hash = map { $_ => 1 } @reservation_ids;
my @chain_names = $self->get_table_chain_names('nat');
my @chains_ignored;
my @chains_with_reservation;
my @chains_deleted;
for my $chain_name (@chain_names) {
if ($chain_name !~ /^$PROCESSNAME-/) {
notify($ERRORS{'DEBUG'}, 0, "ignoring chain in nat table on $computer_name: $chain_name, name does not begin with '$PROCESSNAME-'");
push @chains_ignored, $chain_name;
next;
}
my ($chain_reservation_id) = $chain_name =~ /^$PROCESSNAME-(\d+)$/;
if (!defined($chain_reservation_id)) {
notify($ERRORS{'DEBUG'}, 0, "ignoring chain in nat table on $computer_name: $chain_name, reservation ID could not be determined from chain name, pattern: '$PROCESSNAME-<reservation ID>'");
push @chains_ignored, $chain_name;
next;
}
elsif (defined($reservation_id_hash{$chain_reservation_id})) {
notify($ERRORS{'DEBUG'}, 0, "ignoring chain in nat table on $computer_name: $chain_name, reservation $chain_reservation_id exists");
push @chains_with_reservation, $chain_name;
next;
}
notify($ERRORS{'OK'}, 0, "deleting orphaned chain in nat table on $computer_name: $chain_name, reservation $chain_reservation_id does NOT exist");
if ($self->delete_chain('nat', $chain_name)) {
push @chains_deleted, $chain_name;
}
else {
return;
}
}
if (scalar(@chains_deleted)) {
$self->save_configuration();
}
notify($ERRORS{'DEBUG'}, 0, "checked for orphaned reservation chains on NAT host $computer_name:\n" .
"chains ignored (" . scalar(@chains_ignored) . "): " . join(', ', @chains_ignored) . "\n" .
"chains with a current reservation (" . scalar(@chains_with_reservation) . "): " . join(', ', @chains_with_reservation) . "\n" .
"chains deleted (" . scalar(@chains_deleted) . "): " . join(', ', @chains_deleted)
);
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
=head2 _execute_iptables
Parameters : $iptables_command, $display_output (optional)
Returns : ($exit_status, $output)
Description : Wrapper subroutine to execute iptables commands. This executes an
iptables command and checks the output for the following error:
"Another app is currently holding the xtables lock."
If ancountered, up to 6 attempts is made to execute the iptables
command. A progressive delay occurs between each attempt. The
delay is 5 seconds longer for each attempt.
=cut
sub _execute_iptables {
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 ($iptables_command, $display_output) = @_;
if (!defined($iptables_command)) {
notify($ERRORS{'WARNING'}, 0, "iptables command argument was not supplied");
return;
}
$display_output = 0 unless defined($display_output);
my $computer_name = $self->data->get_computer_hostname();
my $attempt_limit = 6;
for (my $attempt = 1; $attempt <= $attempt_limit; $attempt++) {
my ($exit_status, $output) = $self->os->execute($iptables_command, $display_output);
if (defined($output) && $attempt < $attempt_limit) {
# Another app is currently holding the xtables lock. Perhaps you want to use the -w option?
if ($exit_status ne 0 && grep(/xtables lock/, @$output)) {
my $sleep_seconds = ($attempt * 5);
notify($ERRORS{'DEBUG'}, 0, "attempt $attempt/$attempt_limit: unable to execute iptables command on $computer_name becuase another process is holding an xtables lock, waiting for $sleep_seconds seconds before attempting command again, command: '$iptables_command', output:" . (scalar(@$output) > 1 ? "\n" . join("\n", @$output) : " '" . join("\n", @$output) . "'"));
sleep_uninterrupted($sleep_seconds);
next;
}
}
return ($exit_status, $output);
}
}
#//////////////////////////////////////////////////////////////////////////////
=head2 DESTROY
Parameters : none
Returns : true
Description : Prints the number of calls to get_table_info.
=cut
sub DESTROY {
my $self = shift || return;
my $address = sprintf('%x', $self);
my $table_count_string;
if ($ENV{iptables_get_table_info_count}) {
for my $table_name (keys %{$ENV{iptables_get_table_info_count}}) {
my $table_count = $ENV{iptables_get_table_info_count}{$table_name};
$table_count_string .= "$table_name: $table_count\n";
}
notify($ERRORS{'DEBUG'}, 0, "get_table_info calls ($address):\n$table_count_string");
}
# Check for an overridden destructor
$self->SUPER::DESTROY if $self->can("SUPER::DESTROY");
return 1;
}
#//////////////////////////////////////////////////////////////////////////////
1;
__END__
=head1 SEE ALSO
L<http://cwiki.apache.org/VCL/>
=cut