#!/usr/bin/perl -w

###
# Project:     pflogstats
# Module:      pflogstats-extensions-networking.pm
# Description: Extensions for networking
# Copyright:   Dr. Peter Bieringer <pbieringer at aerasec dot de>
#               AERAsec GmbH <http://www.aerasec.de/> 
# License:     GNU GPL v2
# CVS:         $Id: pflogstats-extensions-networking.pm,v 1.12 2005/04/26 16:05:02 peter Exp $
###

###
# ChangeLog:
#	0.01
#	 - split-off from pflogstats-common-support
#	0.02
#	 - fix display of network exclude list
#       0.03
#        - make Perl 5.0 compatible
#	0.04
#	 - minimal review
#	0.05
#	 - skip_ipv6 didn't work if no other IP network was given
#	 - table width now 75
#	0.06
#	 - check function got an optional flag: returnonerror
#	0.07
#	 - implement caching in check_ip_notaccounted (speed-up: x2)
#	0.08
#	 - fix detection of not valid addresses in returnonerror mode
#	0.09
#	 - use option print-max-width
#	0.10
#	 - remove optional address prefix token
#	0.11
#	 - reorganize hook names
###


use strict;

use Net::IP; # required for IP network calculation


package pflogstats::extensions::networking;


## Local constants
my $module_type = "extensions";
my $module_name = $module_type . "-networking";
my $module_version = "0.11";

## Export module info
$main::moduleinfo{$module_name}->{'version'} = $module_version;
$main::moduleinfo{$module_name}->{'type'} = $module_type;
$main::moduleinfo{$module_name}->{'name'} = $module_name;

## Local prototyping


## Global variables


## Local variables
my @net_excluded;
my @opt_net_excluded;
my %cache;
my %cache_statistics;
$cache_statistics{'new'} = 0;
$cache_statistics{'hit'} = 0;


## Register options
$main::options{'skip_net=s'} = \@opt_net_excluded;
$main::options{'skip_ipv6'} = \$main::opts{'skip_ipv6'};


## Register calling hooks
$main::hooks{'beforemainloopstarts'}->{$module_name} = \&beforemainloopstarts;
$main::hooks{'help'}->{$module_name} = \&help;
$main::hooks{'checkoptions'}->{$module_name} = \&checkoptions;
$main::hooks{'testipaddress'}->{$module_name} = \&check_ip_notaccounted;
$main::hooks{'print_additional_statistics'}->{$module_name} = \&printstatistics;


## Global callable functions
sub help() {
	my $temp;
	my $helpstring = "
    General:
    [--skip_net <network>]   IPv4/v6 network in CIDR notation skipped for accounting
                              (e.g. internal or DMZ, more than one time allowed)
    [--skip_ipv6]            All IPv6 addresses
";


	return $helpstring;
};

# Check options
sub checkoptions() {
	## Excluded networks
	# Fill excluded networks	
	foreach my $net (@opt_net_excluded) {
		print STDERR "INFO: convert net: " . $net .  "\n" if ($main::opts{'debug'} & 0x0020);
		my $ip = new Net::IP ($net) or die (Net::IP::Error());
		print STDERR "INFO: converted to: " . $ip->ip . "/" . $ip->mask .  "\n" if ($main::opts{'debug'} & 0x0020);
		push @net_excluded, $ip;

		if ($ip->version() == 6 ) {
			# nothing more to do
			next;
		};

		# Also as IPv6 compatbile address
		$net =~ /^([^\/]+)\/?.*$/;
		$net = "::ffff:" . $1 . "/" . ($ip->prefixlen + 96);
		print STDERR "INFO: convert net (for IPv6): " . $net .  "\n" if ($main::opts{'debug'} & 0x0020);
		$ip = new Net::IP ($net) or die (Net::IP::Error());
		print STDERR "INFO: converted to: " . $ip->ip . "/" . $ip->mask .  "\n" if ($main::opts{'debug'} & 0x0020);
		push @net_excluded, $ip;
	};

};

sub beforemainloopstarts() {
	if ( scalar (@net_excluded) > 0 || $main::opts{'skip_ipv6'}) {
		print '=' x $main::opts{'print-max-width'} . "\n";
		print "Network exclude list\n";
		print '-' x $main::opts{'print-max-width'} . "\n";
		foreach my $ip (sort {$a->version() <=> $b->version()} @net_excluded) {
			if ($ip->version() == 6) {
				if ($main::opts{'skip_ipv6'}) {
					next;
				};
			};
			print $ip->ip . "/" . $ip->mask .  "\n";
		};
		if ($main::opts{'skip_ipv6'} ) {
			print "*** All IPv6 addresses ***\n";
		};
		print '=' x $main::opts{'print-max-width'} . "\n";
	};
};


# Check IP address for in not-account list
# Arguments:
#  $1: IP address
#  $2 (optional): change behavior in case of $1 is no IP address
#	'returnonerror': don't die, return 0 instead
# Return value: 
#  0=no
#  1=yes
sub check_ip_notaccounted($;$) {
	my $ip_string = $_[0];
	my $flag = $_[1];

	if (! defined $ip_string) { die "ERROR(check_ip_notaccounted): Missing argument (arg1)"; };

	if (defined $flag) {
		if ($flag eq "returnonerror") {
			# ok
			$flag = 1;
		} else {
			die "ERROR(check_ip_notaccounted): Unsupported flag (arg2): $flag";
		};
	} else {
		$flag = 0;
	};

	printf STDERR "DEBUG: Got string: " . $ip_string . "\n" if ($main::opts{'debug'} & 0x2000);

	$ip_string =~ s/^IPv6://ig;
	$ip_string =~ s/^IPv4://ig;

	# Cache lookup
	if (defined $cache{$ip_string}) {
		$cache_statistics{'hit'}++;
		if ($cache{$ip_string} == 2) {
			# cached return on error
			if ($flag == 1) {
				return 0;
			};
		} else {
			return $cache{$ip_string};
		};
	} else {
		$cache_statistics{'new'}++;
	};

	my $ip;

	if ($flag == 1) {
		$ip = new Net::IP ($ip_string);
		if ($? != 0) {
			$cache{$ip_string} = 2;
			return 0;
		};
		printf STDERR "Debug: successful creation of ip object\n" if ($main::opts{'debug'} & 0x2000);

		eval {
			# Test for version
			$ip->version();
		};
		if ($@) {
			printf STDERR "Debug: skip\n" if ($main::opts{'debug'} & 0x2000);
			$cache{$ip_string} = 2;
			return 0;
		};
	} else {
		$ip = new Net::IP ($ip_string) or die (Net::IP::Error() . " '" . $ip_string . "'");
	};

	printf STDERR "Debug: check IP: %s (Version: %s)\n", $ip->ip(), $ip->version() if ($main::opts{'debug'} & 0x2000);
	if ($main::opts{'skip_ipv6'} && ($ip->version() == 6) ) {
		# skip IPv6

		# Fill cache
		$cache{$ip_string} = 1;

		return 1;
	};

	foreach my $net (@net_excluded) {
		if ( $ip->version() != $net->version() ) {
			# Skip incompatible types
			printf STDERR "Debug: skip check against %s/%s (incompatible)\n", $net->ip(), $net->prefixlen() if ($main::opts{'debug'} & 0x2000);
			next;
		};

		printf STDERR "Debug: check against IP: %s/%s (Version: %s)\n", $net->ip(), $net->prefixlen(), $net->version() if ($main::opts{'debug'} & 0x2000);

		my $result = $ip->overlaps($net);
		if ( $result == $Net::IP::IP_A_IN_B_OVERLAP || $result == $Net::IP::IP_IDENTICAL) {
			# Fill cache
			$cache{$ip_string} = 1;

			return 1;
		};
	};

	# Fill cache
	$cache{$ip_string} = 0;

	return 0;
};


# Print some statistics
sub printstatistics() {
	# About cache usage
	my $requests = $cache_statistics{'new'} + $cache_statistics{'hit'};

	if ($requests == 0) { return; };	

	my $ratehit_percent = ($cache_statistics{'hit'} / $requests) * 100;

	print "# module '" . $module_name . "' internal cache usage:\n";
	print "#        requests=" . $requests . " hits=" . $cache_statistics{'hit'};
	printf " (%2.1f%%)\n", $ratehit_percent;
};



## End of module
return 1;


syntax highlighted by Code2HTML, v. 0.9.1