#!/usr/bin/perl
# $Id: ipfw_compare.pl,v 1.3 2007/05/22 06:55:15 pkuijv Exp $
#
# *BSD ipfw compare script by Petje and Vo
# Please email your remarks and suggestions to < ipfw_compare at painfullscratch.nl >
# Tested on FreeBSD (4 .. 6)
#
# Compares 'ipfw list'-output and ipfw's config (set in rc.conf) and output's the difference (if any)
#
# Needs /usr/ports/net-mgmt/p5-NetAddr-IP, /usr/ports/devel/p5-Algorithm-Diff and Perl :)
#

my $rcconf              = '/etc/rc.conf';
my $ipfwlist            = '/sbin/ipfw list';

#
#
#
#

use lib './lib';        # for those who install Algorithm::Diff and NetAddr::IP in '.'
use strict;
use Algorithm::Diff;
use NetAddr::IP;
my $VERSION = '0.8.3';

my $ipfw_log_limit      = undef; # This "singleton" is set by get_ipfw_log_limit().

sub read_file {
        my ($file) = @_;
        local($/) = wantarray ? $/ : undef;
        local(*F);
        my $r; my (@r);
        open(F, "<$file") || die "error opening $file: $!";
        @r = <F>;
        close(F) || die "error closing $file: $!";
        return $r[0] unless wantarray;
        return @r;
}

# rcconf configfilename as input parameter
sub get_ipfw_configfilename {
        my $rcconf = shift;
        my $content = main::read_file($rcconf);
        unless($content =~ /^firewall_enable=["']{1}YES["']{1}\s*$/m) {
                die "The firewall_enable option in '$rcconf' is not set to enabled!";
        }
        unless($content =~ /^firewall_type=["']{1}(\/.+)["']{1}\s*$/m) {
                die "The firewall_type option in '$rcconf' makes no sense or isn't present!";
        }
        return $1;
}

sub get_ipfw_log_limit {
        unless(defined($ipfw_log_limit)) {
                # Get active ipfw_log_limit from kernel and set global var.
                $ipfw_log_limit = `/sbin/sysctl -n net.inet.ip.fw.verbose_limit 2> /dev/null`;
                chomp($ipfw_log_limit);
        }
        return $ipfw_log_limit;
}

sub collect_active_array {
        my @activerules;
        my @ipfwlist_output = `$ipfwlist`;
        for my $line (@ipfwlist_output) {
                chomp($line);
                push(@activerules,main::normalize_rule($line));
        }
        # ignore the implicit deny rule at the end
        if($activerules[$#activerules] eq 'deny ip from any to any') {
                pop(@activerules);
        }
        return @activerules;
}

sub collect_config_array {
        my $ipfwconf = shift;
        my @configlines = main::read_file($ipfwconf);
        my @configrules;
        foreach my $line (@configlines) {
                chomp($line);
                # only the 'add'-lines ..
                # todo: makes no sense, one can have a 'flush' somewhere in the middle ...
                if($line =~ /^\s*add\s*(.*?)$/) {
                        push(@configrules,main::normalize_rule($1));
                }
        }
        return @configrules;
}

# the ever changing 'normalize_rule'-sub ..
sub normalize_rule {
        my $rule = shift;
        # strip linenumbers and whitespace at begin and end of line
        $rule =~ s/^\s*?\d*\s*(.*?)\s*$/$1/;
        # strip double whitespace
        $rule =~ s/\s{1,}/ /g;
        # normalize 'allow|accept|pass|permit' and deny|drop
        my $ipfw_allow_regexp = '^'.join('|',qw { allow accept pass permit }).'(.*)$';
        my $ipfw_deny_regexp = '^'.join('|',qw { deny drop }).'(.*)$';
        $rule =~ s/${ipfw_allow_regexp}/allow${1}/;
        $rule =~ s/${ipfw_deny_regexp}/deny${1}/;

        # todo , make the to|from while normalizing ipaddr nicer :)

        # normalize 'ip|all'
        $rule =~ s/^(allow|deny)\s+(log\s+)*(ip|all)(\s+from.*)$/${1} ${2}ip${4}/g;
        # normalize ipaddresses and subnets
        $_ = $rule;
        if(/^(.*to)\s+((?:(?:\d{1,3}\.){3}\d{1,3})(?:\/\d{1,2}|\:(?:\d{1,3}\.){3}\d{1,3})*)\s+(.*)$/) {
                $rule = $1.' '.main::normalize_ip($2).' '.$3;
        }
        $_ = $rule;
        if(/^(.*from)\s+((?:(?:\d{1,3}\.){3}\d{1,3})(?:\/\d{1,2}|\:(?:\d{1,3}\.){3}\d{1,3})*)\s+(.*)$/) {
                $rule = $1.' '.main::normalize_ip($2).' '.$3;
        }
        # normalize optional dst-port|src-port
        $rule =~ s/^(.*to)\s+((?:(?:\d{1,3}\.){3}\d{1,3})(?:\/\d{1,2}|\:(?:\d{1,3}\.){3}\d{1,3})*)\s+(\d{1,5}.*)$/$1 $2 dst-port $3/;
        $rule =~ s/^(.*from)\s+((?:(?:\d{1,3}\.){3}\d{1,3})(?:\/\d{1,2}|\:(?:\d{1,3}\.){3}\d{1,3})*)\s+(\d{1,5}.*)$/$1 $2 src-port $3/;
        # 'via any' is trivial
        $rule =~ s/^(.*?)\s*via any$/${1}/;
        # normalizing keep-state and established
        $rule =~ s/^(.*)\s+(keep-state|established) via (.+)$/${1} via ${3} ${2}/;
        # ipfw is nice to 'in via' and 'out via' rules
        $rule =~ s/^(.*\s+)in via (.{3,5})$/${1}in recv ${2}/;
        $rule =~ s/^(.*\s+)out via (.{3,5})$/${1}out xmit ${2}/;
        # ipoptions -> ipopt
        $rule =~ s/ipoptions/ipopt/g;
        # ignore logamount <digit>. Which is set with net.inet.ip.fw.verbose_limit kernel parameter.
        my $loglimit = main::get_ipfw_log_limit();
        $rule =~ s/logamount\s+${loglimit}\s+//g;
        # ipfw on fbsd 4.x changes syntax on a rule from 'in via <NIC> keep-state' to 'keep-state in recv <NIC>'
        $rule =~ s/^(.*\s+)in via (.{3,5})\s+keep-state/${1}keep-state in recv ${2}/;
        # ipfw on fbsd 4.x changes syntax on a rule from 'out via <NIC> keep-state' to 'keep-state out xmit <NIC>'
        $rule =~ s/^(.*\s+)out via (.{3,5})\s+keep-state/${1}keep-state out xmit ${2}/;
        # ipfw is flexible on syntax 'via em0 setup limit {src-addr|src-port|dst-addr|dst-port} N' to
        #                            'limit {src-addr|src-port|dst-addr|dst-port} N via em0 setup'
        $rule =~ s/^(.*\s+)via (.{3,5}) setup limit (src-addr|src-port|dst-addr|dst-port) ([0-9]*)/${1}limit ${3} ${4} via ${2} setup/;
        return $rule;
}

sub normalize_ip {
        my $ip = shift;
        $ip =~ s/^((?:\d{1,3}\.){3}\d{1,3}):((?:\d{1,3}\.){3}\d{1,3})$/$1\/$2/;
        my $nip = new NetAddr::IP($ip);
        return $nip->cidr();
}

die('You must be root to run this program') unless($> == 0);
my $reqversion = 1.19; # OO interface available since 1.19
if($Algorithm::Diff::VERSION < $reqversion) {
        die("I need at least version $reqversion of Algorithm::Diff");
}

if ( $ARGV[0] eq "-v" ) {
        print $main::VERSION."\n";
        exit(0);
}

my $ipfwconf = main::get_ipfw_configfilename($rcconf);
my @activerules = main::collect_active_array();
my @configrules = main::collect_config_array($ipfwconf);

my $diff = Algorithm::Diff->new( \@activerules, \@configrules );
$diff->Base(1);
my @output;
while(  $diff->Next()  ) {
        next   if  $diff->Same();
        if(  ! $diff->Items(2)  ) {
            push(@output,sprintf "%d,%dd%d", $diff->Get(qw( Min1 Max1 Max2 )));
        } elsif(  ! $diff->Items(1)  ) {
            push(@output,sprintf "%da%d,%d", $diff->Get(qw( Max1 Min2 Max2 )));
        } else {
            push(@output,sprintf "%d,%dc%d,%d", $diff->Get(qw( Min1 Max1 Min2 Max2 )));
        }
        push(@output,"+ ${_}") for $diff->Items(1);
        push(@output,"- ${_}") for $diff->Items(2);
}

if(scalar(@output) > 0) {
        print '+++ active ('.$ipfwlist.")\n";
        print '--- config ('.$ipfwconf.")\n";
        print join("\n",@output);
        print "\n";
};

exit(0);
