#!/usr/bin/perl
# Author: ROTTLER Tamas <tom@bitfit.hu>
# 2014-12-05 - release
# 2014-12-11 - looking for config in FindBin::RealBin
#            - cleanup: detect tables with policy
#            - cleanup: no ip6 nat table for kernels < 3.7
# 2015-02-17 - debian package
#            - configuration now in /etc/pf6.conf
use strict; use warnings;
use Getopt::Long;

sub usage {
    print STDERR "Usage: $0 [OPTIONS..] ACTION\n".
        " OPTIONS:\n".
        "  --verbose|-v   print commands\n".
        "  --dry-run      only show ip(6)tables commands\n".
        "  --ipv4|-4      IPv4 only\n".
        "  --ipv6|-6      IPv6 only\n".
        " ACTION:\n".
        "  start|RULESET          load given ruleset\n".
        "  stop                   unload all rules, set ACCEPT policies\n".
        "  restart|force-reload   (re)load the 'start' ruleset\n".
        "  status                 print status (always running)\n";
    exit 1;
}

my $self = {
    config_fn => "/etc/pf6.conf",
    config => '',
    iptables => '/sbin/iptables',
    ip6tables => '/sbin/ip6tables',
    cleanup_tables => [qw(filter nat mangle raw)],
    verbose => 0,
    dryrun => 0
    };

sub iptables {
    my %opts = @_;

    $opts{ip} = '46' if !$opts{ip} or $opts{ip} !~ /[46]/;
    my @ipts;
    if ($opts{ip} =~ /4/) {
        push @ipts, $self->{iptables};
    }
    if ($opts{ip} =~ /6/) {
        push @ipts, $self->{ip6tables};
    }
    my @output;
    for my $ipt (@ipts) {
        my $cline = "$ipt $opts{cmd}";
        if (!$opts{query}) {
            print "pf6 CMD: $cline\n" if $self->{verbose};
            next if $self->{dryrun};
        }
        @output = `$cline 2>&1`;
        if ($? > 0) {
            my $ec = $? >> 8;
            print STDERR "pf6 ERR: exit code $ec on:\n$cline\n" .
                join('', @output);
            return 0, @output if $opts{output};
            return 0;
        }
    }
    return 1, @output if $opts{output};
    return 1;
}

sub kernel_older {
    my ($vmin) = @_;

    my $output = qx(uname -sr);
    if ($output !~ /^Linux\s+(\d+)\.(\d+)(\.(\d+))?/) {
        return 0; # unexpexted output
    }
    my @v = ($1, $2, $4);
    my @m = split /\./, $vmin;

    for (my $i = 0; $i <= $#v and $i <= $#m; $i++) {
        if ($v[$i] < $m[$i]) {
            return 1;
        }
    }
    return 0;
}

sub grepchains {
    my %opts = @_;

    my @output = iptables(
        cmd => "-n -t $opts{table} -L",
        query => 1,
        output => 1,
        ip => $opts{ip},
        );

    my @chains;
    for my $line (@output) {
        next unless $line =~ /Chain (\S+) \((policy)?/;
        push @chains, {
            name => $1,
            policy => $2 ? 1 : 0
            };
    }
    return @chains;
}

sub cleanup {
    my %opts = @_;

    if ($opts{ip} ne '4' and $opts{ip} ne '6') {
        print "pf6: cleanup: invalid ip protocol\n";
        return;
    }

    print "pf6: cleanup ip$opts{ip}..\n";
    my $error = 0;
    my $tables = $self->{cleanup_tables};
    for my $table (@$tables) {
        if ($opts{ip} eq '6' and kernel_older('3.7') and $table eq 'nat') {
            # no ip6 nat table before linux 3.7
            next;
        }
        my @chains = grepchains(
            table => $table,
            ip => $opts{ip}
            );
        # clean up first
        for my $chain (@chains) {
            iptables(
                cmd => "-t $table -F $chain->{name}",
                ip => $opts{ip}
                );
            if ($chain->{policy}) {
                iptables(
                    cmd => "-t $table -P $chain->{name} ACCEPT",
                    ip => $opts{ip}
                    );
            }
        }
        # now there are no references to the chains
        for my $chain (@chains) {
            next if $chain->{policy};
            my $success = iptables(
                cmd => "-t $table -X $chain->{name}",
                ip => $opts{ip}
                );
            $error++ if !$success;
        }
    }
}

sub loadconf {
    open(CF, '<', $self->{config_fn})
        or die "Could not open $self->{config_fn}: $!\n";
    my @cf;
    while (my $line = <CF>) {
        chomp $line;
        next if $line !~ /\S+/;
        next if $line =~ /^\s*#/;
        if ($line =~ /^\s+(.*)/) {
            # lines beginning with \s continue last line
            push @cf, '' if !scalar @cf;
            $cf[-1] .= $1;
        } else {
            push @cf, $line;
        }
    }
    close CF;
    $self->{config} = \@cf;
}

sub find_ruleset {
    my ($ruleset) = @_;
    my $in_ruleset = 0;
    my @rules;
    for (my $i = 0; $i < scalar @{$self->{config}}; $i++) {
        my $line = $self->{config}->[$i];
        if (!$in_ruleset) {
            if ($line =~ /^R>\s*$ruleset\s*$/) {
                $in_ruleset = 1;
            }
        } else {
            if ($line =~ /^R>/) {
                $in_ruleset = 0;
                $i--; # rerun line, perhaps it's us again
            } else {
                push @rules, $line;
            }
        }
    }
    return @rules;
}

sub activate_rules {
    my %opts = @_;

    my ($good, $bad) = (0, 0);
    for my $r (@{$opts{rules}}) {
        if ($r =~ s/^PRINT>(.*)//) {
            print "$1\n";
            next;
        }
        my $ip = '46';
        if ($r =~ s/^([46])>\s*//) {
            $ip = $1;
        }
        $ip =~ s/6// if $opts{ip} eq '4';
        $ip =~ s/4// if $opts{ip} eq '6';
        next if $ip eq '';
        my $success = iptables(
            cmd => $r,
            ip => $ip
            );
        if ($success) {
            $good++;
        } else {
            $bad++;
        }
    }
    return $good, $bad;
}


my ($ipv4, $ipv6) = (0, 0);
GetOptions(
    "verbose|v" => \$self->{verbose},
    "dry-run" => \$self->{dryrun},
    "ipv4|4" => \$ipv4,
    "ipv6|6" => \$ipv6,
    ) or usage;
if (!$ipv4 and !$ipv6) {
    ($ipv4, $ipv6) = (1, 1);
}
if ($self->{dryrun}) {
    print "pf6: DRY RUN!\n";
    $self->{verbose} = 1;
}

loadconf;

my $action = $ARGV[0] or usage;
# these are the same for us:
$action = 'start' if $action eq 'restart' or $action eq 'force-reload';
if ($action eq 'stop') {
    cleanup(ip => '4') if $ipv4;
    cleanup(ip => '6') if $ipv6;
    exit 0;
} if ($action eq 'status') {
    print "pf6 status is meaningless.\n";
    exit 0;
}

my @rules = find_ruleset($action);
if (!scalar @rules) {
    print STDERR "Ruleset '$action' not defined.\n";
    exit 1;
}
cleanup(ip => '4') if $ipv4;
cleanup(ip => '6') if $ipv6;
print "pf6: loading ruleset '$action'..\n";
my ($good, $bad) = activate_rules(
    rules => \@rules,
    ip => ($ipv4 ? '4' : '') . ($ipv6 ? '6' : '')
    );
if ($bad) {
    print "pf6: $good rules ok, $bad rules FAILED.\n";
    exit 1;
}
print "pf6: $good rules ok.\n";
exit 0;
__END__
# vim: set tabstop=4 shiftwidth=4 expandtab smarttab:
