#!/usr/bin/perl

# iptablesdiff -- compare two iptables-save rulesets
# call as   $0  ruleset1 ruleset2
# depends: libyaml-syck-perl libalgorithm-diff-perl

# Copyright (c) 2010 Uli Martens <uli@youam.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use strict;
use warnings;
use YAML::Syck;
use Algorithm::Diff;

sub parse_ruleset {
	my $file = shift;

	open my $FILE, '<', $file
		or die "$0: can't open file $file: $!";

	my $ruleset;
	my $table;
	while (<$FILE>) {
		next if m/^#/;
		next if m/^COMMIT/;
		s/ *$//;
		next if m/^$/;
		s/ --source / -s /;
		s/ --destination / -d /;
		s/ --out-interface / -o /;
		s/ --in-interface / -i /;
		s/ --jump / -j /;
		s/ --protocol / -p /;
		s/ --match / -m /;
		s# --state ESTABLISHED,RELATED # --state RELATED,ESTABLISHED #;

		s#-o ([a-z0-9]+) -d ([0-9./]+) #-d $2 -o $1 #;
		s#-p (tcp|udp) --dport #-p $1 -m $1 --dport #;

		if ( m/^\*([a-zA-Z0-9_-]+)$/ ) {
			# this is a table
			$ruleset->{$1} = {};
			$table = $1;
			next;
		}
		if ( m/^:([a-zA-Z0-9]+) (ACCEPT|DROP) \[[0-9]+:[0-9]+\]$/ ) {
			my $chain = $1;
			my $policy = $2;
			$ruleset->{$table}->{$chain}->{policy}=$policy;
			$ruleset->{$table}->{$chain}->{rules} = [];
			next;
		}
		if ( m/^-A ([a-zA-Z0-9]+) (.*)$/ ) {
			my $chain = $1;
			my $rule  = $2;
			push @{$ruleset->{$table}->{$chain}->{rules}}, $rule;
			next;
		}
		print STDERR "ignored: $_";
	}
	return $ruleset;
	close $FILE;
}

sub compare_chains {
	my $table = shift;
	my $chain = shift;
	my $c1 = shift;
	my $c2 = shift;


	if ( $c1->{policy} ne $c2->{policy} ) {
		print "; iptables -t $table -P $chain $c2->{policy}\n";
	}
	my $diff = Algorithm::Diff->new( $c1->{rules}, $c2->{rules} );
	my $same=1;
	while ( $diff->Next() ) {
		if ( !$diff->Same() ) {
			$same=0;
			last;
		}
	}
	$diff->Reset();
	if ( $same ) {
		#print "# rules are unchanged\n";
	} else {
		print "# rules diff for table $table, chain $chain\n";
		while(  $diff->Next()  ) {
			if ( $diff->Same() ) {
				print "  $_\n" for $diff->Items(1);
				next;
			}
			print "- $_\n"   for  $diff->Items(1);
			print "+ $_\n"   for  $diff->Items(2);
		}
		$diff->Reset();
		my $p = 1;
		while(  $diff->Next()  ) {
			if ( $diff->Same() ) {
				$p += scalar $diff->Items(1);
				next;
			}
			print "; iptables -t $table -D $chain $p # $_\n" for $diff->Items(1);
			print "; iptables -t $table -I $chain ",$p++," $_\n" for $diff->Items(2);
		}

	}



}

sub compare_tables {
	my $table = shift;
	my $t1 = shift;
	my $t2 = shift;

	my %all_chains;
	$all_chains{$_}++ for keys %{$t1};
	$all_chains{$_}++ for keys %{$t2};

	for my $chain ( sort keys %all_chains ) {
		compare_chains( $table, $chain, $t1->{$chain}, $t2->{$chain} );
	}
}

sub compare_rulesets {
	my $r1 = shift;
	my $r2 = shift;

	my %all_tables;
	$all_tables{$_}++ for keys %{$r1};
	$all_tables{$_}++ for keys %{$r2};

	for my $table ( sort keys %all_tables ) {
		compare_tables( $table, $r1->{$table}, $r2->{$table} );
	}
}

my $r1 = parse_ruleset( $ARGV[0] );
my $r2 = parse_ruleset( $ARGV[1] );

compare_rulesets( $r1, $r2 );
