#!/usr/bin/perl -w
###############################################################################
# $Id: iptables.pm 1801653 2017-07-11 20:50:48Z arkurth $
###############################################################################
# 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
