package Guardian::IPtables;
use strict;
use warnings;

use Exporter qw(import);

our @EXPORT = qw(DoBlock DoUnblock DoFlush);

# Array of supported block actions.
my @supported_actions = ("DROP", "REJECT");

# Path where the IPtables related binaries are stored.
my $bindir = "/usr/sbin/";

# Hash which contains the path to the IPtables binaries,
# based on the used IP protocol version. IPtables is
# designed to use different binaries for IPv4 and IPv6.
my %binaries = (
	"4" => "iptables",
	"6" => "ip6tables",
);

# The used firewall chain.
my $chain = "GUARDIAN";

#
## The DoBlock subroutine.
#
## This subroutine is called, when a given address should be locked by
## using IPtables.
#
## Guardian is using the "append" option from IPtables, which will add the new rule
## to the end of the chain to prevent from possible race-conditions when adding/deleting
## rules at the same time.
#
sub DoBlock (@) {
	my ($address, $action) = @_;

	# If no action has been given, default to "DROP".
	unless($action) {
		$action = "DROP";
	}

	# Check if the given action is supported.
	unless(&_check_action($action)) {
		# Abort and return an error message.
		return "Unsupported action: $action";
	}

	# Obtain which binary should be executed.
	my $iptables = &_omit_binary($address);

	# Abort if no suitable binary could be obtained.
	unless ($iptables) {
		return "No suitable binary found.";
	}

	# Call IPtables binary to block the given address.
	system("$iptables --wait -A $chain -s $address -j $action");
}

#
## The DoUnblock subroutine.
#
## This subroutine can be used to delete all firewall rules (unblock)
## of a previously blocked address.
#
## To do this a subroutine will be called, which is collecting all rule
## positions in the firewall chain for the given address, which returns
## them in reversed order. This list of rules will be deleted one-by-one
## so multiple entries (if present) for the address will be deleted.
#
## This approach also eliminates the exact rule argument processing again
## for dropping it. So it is not neccessary to know the additional arguments
## like firewall action (DROP, REJECT) etc. 
#
sub DoUnblock ($) {
	my ($address) = @_;

	# Obtain which binary should be executed.
	my $iptables = &_omit_binary($address);

	# Abort if no suitable binary could be obtained.
	unless($iptables) {
		return "No suitable binary found.";
	}

	# Get rulepositions for the specified address.
	my @rules = &_get_rules_positions_by_address($address, $iptables);

	# Loop through the rules array and drop the firewall rules.
	foreach my $rule (@rules) {
		# Call iptables to delete the rule.
		system("$iptables --wait -D $chain $rule");
	}
}

#
## The DoFlush subroutine.
#
## Call this subroutine to entirely flush the IPtables chains for each
## supported protocol version.
#
sub DoFlush () {
	# Loop through the binaries hash to flush 
	foreach my $key (keys %binaries) {
		# Grab binary from hash and generate the absolute path
		# to the binary.
		my $iptables = join ("/", $bindir,$binaries{$key});

		# Execute the binary if avail- and executeable.
		if (-x $iptables) {
			system("$iptables --wait -F $chain");
		}
	}
}

#
## Get rules subroutine.
#
## This subroutine is used to get the rule position of the active
## firewall rules for a given address. Those position will be collected
## and returned in reversed order. 
#
sub _get_rules_positions_by_address ($$) {
	my ($address, $iptables) = @_;

	# Array to store the rule positions.
	my @rules;

	# Call iptables and list all firewall rules.
	open(RULES, "$iptables --wait -L $chain -n -v --line-numbers |");

	# Read input line by line.
	foreach my $line (<RULES>) {
		# Skip descriptive line.
		next if ($line =~ /^Chain/);
		next if ($line =~ /^ pkts/);

		# Generate array, based on the line content
		# (seperator is a single or multiple space's)
		my @comps = split(/\s{1,}/, $line);
		my ($pos, $pkts, $bytes, $target, $prot, $opt, $in, $out, $source, $destination) = @comps;

		# Compare the current source address with the given one.
		# If they are equal, the rule position will be added to the
		# rules array.
		if ($address eq $source) {
			push(@rules, $pos);
		}
	}

	# Reverse the array.
	my @reversed_rules = reverse @rules;

	# Return the reversed array.
	return @reversed_rules;
}

#
## The _check_action function.
#
## This private function is used to check if the given action is supported by
## the firewall engine.
#
sub _check_action ($) {
	my $action = shift;

	# Check if the recieved action is part of the supported_actions array.
	foreach my $item (@supported_actions) {
		# Exit the loop and return "nothing" if we found a match.
		if($item eq $action) {
			# Return "1" (True).
			return 1;
		}
	}

	# If we got here, the given action is not part of the array of supported
	# actions. Return nothing (False).
	return;
}

#
## The _omit_binary function.
#
## This private function is responsible for selecting and returning the correct
## IPtables binary, based on which kind of IP address has been given.
#
sub _omit_binary ($) {
	my $address = shift;

	# Obtain used protocol version, based on the
	# given IP address.
	my $proto_version = &Guardian::Base::DetectIPProtocolVersion($address);

	# Abort if the protocol version could not proper be detected.
	unless ($proto_version) {
		# Return nothing (False).
		return;
	}

	# Obtain which binary is responsible, based on the detected protocol version and
	# generate the full path to it.
	my $binary = join("/",$bindir,$binaries{$proto_version});

	# Abort if the obtained binary is not avail- or executeable.
	unless (-x $binary) {
		# Return nothing (False).
		return;
	}

	# Return the detected and validated binary.
	return $binary;
}

1;
