#!/usr/bin/perl -w
#
# blacklist - IP blacklisting utility (uses iptables)
# Apache Security, http://www.apachesecurity.net
# Copyright (C) 2004 Ivan Ristic <ivanr@webkreator.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

# This script maintains a list of IP addresses that are not
# to be allowed through the iptables firewall. To use the
# script to the following:
#
# 1) Configure the blacklist script with the proper data
#    path (variable $DATA).
#
# 2) Create a new iptables chain and configure iptables
#    to forward traffic to it selectively. The example
#    below sends port 80 traffic to the blaclist. The
#    chain will return the control back to the INPUT
#    chain.
#
#    iptables -N BLACKLIST
#    iptables -A INPUT -p tcp --dport 80 -j BLACKLIST
#   
# 2) Configure blacklist to be initialized at boot time:
#
#    blacklist start
#
# 3) Call blacklist to remove stale blocks from the
#    firewall every couple of minutes:
#
#    blacklist unblock_stale
#
# 4) To block an IP address use the block command together
#    with the duration (in seconds) of the block:
#
#    blacklist block 192.168.0.1 60
#
# 5) To unblock an IP address:
#
#    blacklist unblock 192.168.0.1
#
# 

# TODO Add feature to read addresses to block from a file
#
# TODO Add a whitelist
#
# TODO Allow a range of addresses to be blocked
#
# TODO Replace the "system" call with something safer


use Fcntl qw(:DEFAULT :flock);
use Sys::Syslog qw(:DEFAULT setlogsock);

$IPT = "/sbin/iptables";
$CHAIN = "BLACKLIST";
$DATA = "/etc/blacklist.dat";
$USAGE = "Usage: blacklist [start | stop | reload | status | clear | block <ip> <duration> | unblock <ip> | unblock_stale]\n";

# do we log to syslog
$LOG = 1;

sub load_data {
    %data = ();

    sysopen(FILE, $DATA, O_RDWR | O_CREAT) || die("Cannot open/create file $DATA");
    flock(FILE, LOCK_SH) || die("Cannot lock (shared) file $DATA");

    while(<FILE>) {
        chomp;
        my(@list) = split;
        if (@list == 3) {
            my($ip, $start_time, $duration) = @list;
            # print("ip=$ip, start_time=$start_time, duration=$duration\n");
            my(%iphash) = ();
            $iphash{"ip"} = $ip;
            $iphash{"start_time"} = $start_time;
            $iphash{"duration"} = $duration;
            $data{$ip} = \%iphash;
        }
    }
}

sub save_data {
    flock(FILE, LOCK_EX) || die("Cannot lock (exclusive) file $DATA");
    seek(FILE, 0, 0) || die("Cannot seek in file $DATA");
    truncate(FILE, 0) || die("Cannot truncate file $DATA");
    while(($ip, $ref) = each %data) {
        print FILE "$ip " . ${$ref}{"start_time"} . " " . ${$ref}{"duration"} . "\n";
    }
    close(FILE);
}

sub add_ip_to_data {
    my($ip, $duration) = @_;
    my($existed) = 0;

    if (exists $data{$ip}) {
        $existed = 1;
    }

    %iphash = ();
    $iphash{"ip"} = $ip;
    $iphash{"start_time"} = time();
    $iphash{"duration"} = $duration;
    $data{$ip} = \%iphash;

    return $existed;
}

sub delete_ip_from_data {
    my($ip) = @_;
    delete $data{$ip};
}

sub do_block {
    my($ip, $duration) = @_;

    if ($ip !~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
        die("$ip is not a valid IP address");
    }

    if ($duration !~ m/^\d+$/) {
        die("$duration is not a valid duration");
    }

    load_data();

    if (add_ip_to_data($ip, $duration) == 0) {
        save_data();

        # talk to iptables
        my($cmd) = "$IPT -I $CHAIN -s $ip -j DROP";
        # print("$cmd\n");
        system($cmd);
        if ($LOG) {
            syslog("info", "Blocking $ip for $duration seconds");
        }
    } else {
        # no need to save anything
        # just close the file
        close(FILE);
    }
}

sub do_unblock {
    my($ip) = @_;

    if ($ip !~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
        die("$ip is not a valid IP address");
    }

    load_data();
    delete_ip_from_data($ip);
    save_data();

    my($cmd) = "$IPT -D $CHAIN -s $ip -j DROP";
    # print("$cmd\n");
    system($cmd);

    if ($LOG) {
        syslog("info", "Unblocked $ip (manual)");
    }
}

sub do_unblock_stale {
    my($time_now) = time();
    %unblock_data = ();
    load_data();

    while(($ip, $ref) = each %data) {
        my($expire_time) = ${$ref}{"start_time"} + ${$ref}{"duration"};
        # print "time_now=$time_now, expire_time=$expire_time\n";
        if ($time_now > $expire_time) {
            delete_ip_from_data($ip);
            $unblock_data{$ip} = $ref;            
        }
    }
    save_data();

    while(($ip, $ref) = each %unblock_data) {
        my($cmd) = "$IPT -D $CHAIN -s $ip -j DROP";
        system($cmd);

        if ($LOG) {
            syslog("info", "Unblocked $ip (expired)");
        }
    }
}

sub do_start {
    my($time_now) = time();
    load_data();

    system("$IPT -F $CHAIN");
    system("$IPT -A $CHAIN -j RETURN");
    
    while(($ip, $ref) = each %data) {
        # print "$ip " . ${$ref}{"start_time"} . " " . ${$ref}{"duration"} . "\n";
        my($expire_time) = ${$ref}{"start_time"} + ${$ref}{"duration"};
        # print "time_now=$time_now, expire_time=$expire_time\n";
        if ($time_now > $expire_time) {
            delete_ip_from_data($ip);
        } else {
            my($cmd) = "$IPT -I $CHAIN -s $ip -j DROP";
            print "$cmd\n";
            system($cmd);
            if ($LOG) {
                my($duration) = $expire_time - $time_now;
                syslog("info", "Blocking $ip for $duration seconds");
            }
        }
    }    

    save_data();
}

sub do_stop {
    system("$IPT -F $CHAIN");
    system("$IPT -A $CHAIN -j RETURN");
}

sub do_status {
    my($time_now) = time();
    load_data();

    print "ip       \tblocking since\t\t\tduration\tremains\n";
    while(($ip, $ref) = each %data) {
        my($time_left) = (${$ref}{"start_time"} + ${$ref}{"duration"}) - $time_now;
        print "$ip\t" . localtime(${$ref}{"start_time"}) . "\t" . ${$ref}{"duration"} . "      \t" . $time_left . "\n";
    }
}

sub do_clear {
    # clear current firewall configuration
    do_stop();

    # clear persistent data
    load_data();
    %data = ();
    save_data();
}

# -- main ---------------------------------------------------

if (@ARGV == 0) {
    print $USAGE;
    exit();
}

if ($LOG) {
    openlog("blacklist", "cons,pid");
    setlogsock('unix');
}

my($command) = shift(@ARGV);

if (($command eq "start")||($command eq "reload")) {
    do_start();
} elsif ($command eq "status") {
    do_status();
} elsif ($command eq "stop") {
    do_stop();
} elsif ($command eq "clear") {
    do_clear();
} elsif ($command eq "unblock_stale") {
    do_unblock_stale();
} elsif ($command eq "block") {
    if (@ARGV == 2) {
        do_block(@ARGV);
    } else {
        print $USAGE;
    }
} elsif ($command eq "unblock") {
    if (@ARGV == 1) {
        do_unblock(@ARGV);
    } else {
        print $USAGE;
    }
} else {
    print $USAGE;
} 
