Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/perl
- #
- # BlockCountries Block IP traffic from specified countries
- #
- # chkconfig: 2345 10 92
- # description: Blocks IP traffic from IP addresses assigned to specific countries
- #
- use strict;
- use warnings;
- # Version 1.4
- #
- # Copyright (c) 2010 Timothe Litt, litt__at__acm_dot_org
- # All rights reserved.
- #
- # This software is licensed under the terms of the Perl
- # Artistic License (see http://dev.perl.org/licenses/artistic.html).
- #
- # This is free software - it works for me, and it may (or may not)
- # work for you. No warranty or support is provided.
- #
- # Consider carefully whether you want to use this software
- # and the full consequences to your site and/or business.
- #
- # This is written as a technical means to assist in implementing
- # your policy. The author expressly disclaims any responsibility
- # for the consequences of using this software.
- ### Block all traffic from specified countries. ###
- #
- # See Usage() for documentation
- #
- # List of country codes - specify yours in the config file
- my @DEFAULT_ISO = qw /cn kr kp kz ru/;
- # Local configuration
- my $IPT = '/sbin/iptables';
- my $IPTR = '/sbin/iptables-restore';
- my $GREP = '/bin/grep';
- my $CFGFILE = '/etc/sysconfig/BlockCountries';
- my $ZONEDIR = '/root/blockips';
- my $ZONETBL = "$ZONEDIR/tables.ipt";
- my $BLOCKURL = 'http://www.ipdeny.com/ipblocks/data/countries';
- my $LOGPFX = '[Blocked CC]: ';
- my $LOG = '/var/log/messages*'; # Note: This is a wildcard to handle log rotation. .gz files will decompressed on the fly and processed.
- my $LOGPGM = 'kernel';
- my $IHOOK = 'INPUT-HOOK'; # Note: if this table is not found, INPUT will be used
- my $OHOOK = 'OUTPUT-HOOK'; # Note: if this table is not found, OUTPUT will be used
- my $FHOOK = 'FORWARD-HOOK'; # Note: if this table is not found, FORWARD will be used
- # ### End of configuration
- # The following are either part of base perl, or available on CPAN
- use File::Basename;
- use File::Path;
- use IO::Uncompress::Gunzip;
- use Locale::Country;
- use LWP::Simple;
- use NetAddr::IP;
- use Net::Domain;
- use Parse::Syslog;
- use POSIX;
- use Text::ParseWords;
- # Changelog (Also update revision above!)
- # 1.0 Initial development
- # 1.1 Add support for hook tables
- # 1.2 By unpopular demand, add -permitonly
- # Add logging rate limit
- # Add -dip (deny IP)
- # 1.3 Add output filtering (-blockout)
- # 1.4 Output filters need to match on destination port.
- # Separate output port overrides as trojans like to ride on well-known ports
- umask 0137;
- my $prog = basename $0;
- # new, old
- my @IPCHAINS = ( 'BLOCKCC0', 'BLOCKCC1' );
- @IPCHAINS = reverse( @IPCHAINS ) if( system( "$IPT -n -L $IPCHAINS[0]-I >/dev/null 2>&1" ) == 0 );
- my $IPNEWCHAIN = $IPCHAINS[0];
- my $IPOLDCHAIN = $IPCHAINS[1];
- $IHOOK = 'INPUT' unless( system( "$IPT -n -L $IHOOK >/dev/null 2>&1" ) == 0 );
- $OHOOK = 'OUTPUT' unless( system( "$IPT -n -L $OHOOK >/dev/null 2>&1" ) == 0 );
- $FHOOK = 'FORWARD' unless( system( "$IPT -n -L $FHOOK >/dev/null 2>&1" ) == 0 );
- if( -e $CFGFILE ) {
- open( my $fh, '<', $CFGFILE ) or die( "Can't open $CFGFILE:$!" );
- while( <$fh> ) {
- s/\s*#.*$//;
- s/^\s+//;
- s/\s+$//;
- next unless length;
- push @ARGV, parse_line( '\s+', 0, $_ );
- }
- close $fh;
- }
- my $cmd = shift;
- # Collect all arguments here, even though they are mostly for start
- # This allows detailed status
- my( $debug, $verbose, @iso, %iso, $update, $log, $days, $host, $permitonly, $outrules, @loglimits, @atports, @auports, @atportso, @auportso, @aips, @dips );
- @loglimits = ( '1/minute', 10 );
- while( (my $arg = shift) ) {
- if( $arg =~ /^-/ ) {
- if( $arg eq '-update' ) {
- $update = 1;
- next;
- }
- if( $arg eq '-log' ) {
- $log = 1;
- next;
- }
- if( $arg eq '-limit' && $ARGV[0] ) {
- unless( $ARGV[0] =~ m#(\d+/(?:second|minute|hour|day))(?::(\d+))?# ) {
- print "Syntax error in -limit\n";
- exit 1;
- }
- shift;
- $loglimits[0] = $1;
- $loglimits[1] = $2 if( defined $2 );
- next;
- }
- if( $arg eq '-nolimit' ) {
- @loglimits = ();
- next;
- }
- if( $arg eq '-nolog' ) {
- $log = 0;
- next;
- }
- if( $arg eq '-permitonly' ) {
- $permitonly = 1;
- next;
- }
- if( $arg eq '-d' ) {
- $debug = 1;
- next;
- }
- if( $arg eq '-blockout' ) {
- $outrules = 1;
- next;
- }
- if( $arg eq '-v' ) {
- $verbose = 1;
- next;
- }
- if( $arg eq '-days' && $ARGV[0] && $ARGV[0] =~ /^\d+$/ ){
- $days = shift;
- next;
- }
- if( $arg eq '-host' && $ARGV[0] ){
- $host = shift;
- next;
- }
- if( $arg eq '-atport' && $ARGV[0] ){
- $arg = shift;
- if( $arg =~ /^(?:\d+)$/ ) {
- push @atports, $arg;
- next;
- }
- my $val = getservbyname( $arg, 'tcp' );
- if( defined $val ) {
- push @atports, $val;
- next;
- }
- print "Invalid port $arg\n";
- exit 1;
- }
- if( $arg eq '-auport' && $ARGV[0] ){
- $arg = shift;
- if( $arg =~ /^(?:\d+)$/ ) {
- push @auports, $arg;
- next;
- }
- my $val = getservbyname( $arg, 'udp' );
- if( defined $val ) {
- push @auports, $val;
- next;
- }
- print "Invalid port $arg\n";
- exit 1;
- }
- if( $arg eq '-atporto' && $ARGV[0] ){
- $arg = shift;
- if( $arg =~ /^(?:\d+)$/ ) {
- push @atportso, $arg;
- next;
- }
- my $val = getservbyname( $arg, 'tcp' );
- if( defined $val ) {
- push @atportso, $val;
- next;
- }
- print "Invalid port $arg\n";
- exit 1;
- }
- if( $arg eq '-auporto' && $ARGV[0] ){
- $arg = shift;
- if( $arg =~ /^(?:\d+)$/ ) {
- push @auportso, $arg;
- next;
- }
- my $val = getservbyname( $arg, 'udp' );
- if( defined $val ) {
- push @auportso, $val;
- next;
- }
- print "Invalid port $arg\n";
- exit 1;
- }
- if( $arg eq '-aip' && $ARGV[0] ) {
- if( $ARGV[0] =~ /^\d{1,3}(?:\.\d{1,3}){0,3}(?:\/(?:\d+|(?:\d{1,3}(?:\.\d{1,3}){3})))?$/ ){
- push @aips, NetAddr::IP->new( shift );
- next;
- }
- my @h = gethostbyname( $ARGV[0] );
- unless( @h) {
- print "Unknown host $ARGV[0]\n";
- exit 1;
- }
- unless( $h[3] == 4 && $#h >= 4 ) {
- print "$ARGV[0] : not an IPV4 address\n";
- exit 1;
- }
- for my $a (@h[4..$#h]) {
- push @aips, NetAddr::IP->new( sprintf( "%vd", $a ) );
- }
- shift;
- next;
- }
- if( $arg eq '-dip' && $ARGV[0] ) {
- if( $ARGV[0] =~ /^\d{1,3}(?:\.\d{1,3}){0,3}(?:\/(?:\d+|(?:\d{1,3}(?:\.\d{1,3}){3})))?$/ ){
- push @dips, NetAddr::IP->new( shift );
- next;
- }
- my @h = gethostbyname( $ARGV[0] );
- unless( @h) {
- print "Unknown host $ARGV[0]\n";
- exit 1;
- }
- unless( $h[3] == 4 && $#h >= 4 ) {
- print "$ARGV[0] : not an IPV4 address\n";
- exit 1;
- }
- for my $a (@h[4..$#h]) {
- push @dips, NetAddr::IP->new( sprintf( "%vd", $a ) );
- }
- shift;
- next;
- }
- if( $arg eq '-h' || $arg eq '--help' ) {
- Usage();
- exit 0;
- }
- print "Unknown switch $arg";
- print ' ', $ARGV[0] if( defined $ARGV[0] );
- print "\n";
- exit 1;
- }
- if( defined code2country( $arg ) ) {
- $iso{lc $arg} = 1;
- } else {
- my $cc = country2code( $arg );
- if( defined $cc ) {
- $iso{lc $cc} = 1;
- } else {
- print "Unrecognized country/country code: $arg\n";
- exit 1;
- }
- }
- }
- @iso = sort keys %iso;
- @iso = @DEFAULT_ISO unless( @iso );
- my @genlist = $outrules? qw/I O/ : 'I';
- sub Usage {
- print << "HELP";
- IP filter manager for country filters
- Usage: $prog command args
- status [-v] Display filter status
- -v provides configuration from config file
- and command file - NOT iptables.
- list List available country names/codes
- Contacts server for list.
- intercepts [-host name] [-days n]
- List today\'s intercepts by host (from $LOG)
- Requires -log
- stop Stop filtering
- restart args Synonym for start (reloads with no open window)
- condrestart args Restarts only if already running
- start args Starts filter
- Start uses tables of IP blocks assigned to country codes that are
- stored in $ZONEDIR, which will be created if necessary. The data
- is obtained from $BLOCKURL when needed,
- or when start -update is specified.
- iptables filters are generated and installed by start. The filters are
- optimized and generally will not look identical to the input data. However
- they will match the same address (no more and no fewer.)
- Arguments for start-class commands are:
- -update Get latest data for active country codes.
- Otherwise, only gets data if no local file exists for a CC.
- -log Install a logging rule to log rejected packets.
- -nolog Don\'t install a logging rule (default)
- -nolimit Do not limit logging (can generate huge log files if under attack; not advised)
- -limit spec Limit logging, default = $loglimits[0]:$loglimits[1] (see man iptables "limit")
- -atport n Allow connections to TCP port n even FROM banned addresses.
- May specify any number of times. May use a service name.
- -auport n Allow connections to UDP port n even FROM banned addresses.
- May specify any number of times. May use a service name.
- -atporto n Same as -atport, but for connections TO banned addresses.
- -auporto n Same as -auport, but for connections TO banned addresses.
- -aip ip(\/mask) Allow connection from an otherwise banned IP address.
- For a block, specify a netlength or mask. A hostname may
- also be specified.
- -dip ip(\/mask) Deny connections from an otherwise allowed IP address.
- Same syntax as -aip
- -permitonly Listed countries will be permited, all others denied
- -blockout Also generate rules to block output & forwarded-output
- This is probably not required for most applications, and
- will roughly double the memory requirements.
- Caution: If you use -blockout for start, you must also use it for stop.
- This will not be a problem if it\'s in the config file.
- -d Output random debugging messages
- -v Output extended status/statistics
- CC ISO Country code or name to ban (as many as you like)
- Default list:
- HELP
- for my $cc (sort @DEFAULT_ISO ) {
- print " $cc - ", code2country($cc), "\n";
- }
- print << "HELP";
- Arguments may also be obtained from $CFGFILE.
- Anything (except comments) contained in it is appended to every command line.
- Use single or double-quoted strings for country names including spaces.
- This script is designed to run as a service; chkconfig will link it into
- /etc/rcn.d/.
- This script should also be run with start -update from a cron job - weekly
- is suggested - to obtain the latest IP address databases. If the CRONJOB
- environment variable is set, only errors and zone updates will be reported.
- This minimizes e-mail from cron.
- To prevent communications from banned IP addresses during updates,
- start will install new rules before removing active rules. For
- this to be effective, you should not stop the service.
- The status -v command will report the current configuration, although the
- actual implementation in iptables will be different due to optimizations.
- start -v will provide some statistics for the optimizations and generated rules.
- The intercepts command will parse $LOG and summarize intercepts by IP address.
- It will break down the dropped packets by protocol(s) and port(s). Of course,
- logging must be on for this to work. -days specifys how many days back
- (from the current time) to read. -host specifies the hostname to match.
- Default -days is 1, hostname is current host.
- Credits:
- Some ideas came from http://www.cyberciti.biz/faq/block-entier-country-using-iptables/.
- This version of the script merges all the IP address blocks; this saves over 1,000
- rules for the default banned address list. It\'s also somewhat faster than a shell
- script, and contains a more complete and polished user and system interface.
- Issues:
- Consider carefully whether you want to use this software and the full consequences
- to your site and\/or business. By necessity, it will block potential customers and
- 'good' connections along with villains. You must consider the costs and benefits
- to your operation - the author does not endorse any specific policy. In particular,
- the defaults should be viewed as examples, not value judgements.
- If you use selinux (and you should), you may have to create a policy allowing
- $IPTR to access $ZONETBL.
- Very large numbers of exception IP blocks might benefit from implementing a subchain
- structure - but that would be a rather different use model. The known use cases
- would probably be penalized - so one would want to make a dynamic choice. I\'d
- want to see actual data before implementing this.
- The iptables-restore format is undocumented, though used by others. It may be
- fragile.
- This code should use IPTables::IPv4 - but it doesn\'t currently work on my x64 system.
- It may be re-written to do so at some point.
- --tlhackque 1-Aug-2010
- HELP
- }
- # Success and failure return true and false, and
- # print boot-compatible strings (color and aligned
- # to column 60 if on a terminal)
- # CHA (...G)
- # SGR codes used (...m)
- # 0 = default; 1 = bold; 31 = green, 32 = red; 1 = bold; 39 = "default" (boot requires)
- sub success {
- return 1 if( $ENV{CRONJOB} );
- if( -t STDOUT ) {
- print "\033[60G[\033[1;32m OK \033[0;39m]\n";
- } else {
- print " [ OK ]\n";
- }
- return 1;
- }
- sub failure {
- return 0 if( $ENV{CRONJOB} );
- if( -t STDOUT ) {
- print "\033[60G[\033[1;31m FAILED \033[0;39m]\n";
- } else {
- print " [ FAILED ]\n";
- }
- return 0;
- }
- sub running {
- return system( "$IPT -n -L $IHOOK | $GREP -q -P '^$IPOLDCHAIN-\[IO\]\\s+'" ) == 0;
- }
- sub pport {
- my $p = shift;
- my @p = @_;
- printf " %5u", $p;
- if( @p ) {
- print " $p[0]";
- if( length $p[1] ) {
- print " (", join( '/', split( ' ', $p[1] ) ), ")";
- }
- }
- print "\n";
- }
- sub status {
- if( running ) {
- print "Blocked countries IP filter is running";
- if( $verbose ) {
- printf " and configured to %s:\n", ($permitonly? 'permit only' : 'block');
- for my $cc (sort @iso ) {
- print " $cc - ", code2country($cc), "\n";
- }
- if( (my $n = @atports + @auports + @atportso + @auportso + @aips + @dips) ) {
- printf "However, the following exception%s exist:\n", ($n == 1)? ' is' : 's are';
- if( @atports ) {
- printf " TCP port%s permitted (input):\n", ((@atports == 1)? '' : 's');
- for my $p (@atports) {
- pport( $p, getservbyport($p, 'tcp') );
- }
- }
- if( @atportso ) {
- printf " TCP port%s permitted (output):\n", ((@atportso == 1)? '' : 's');
- for my $p (@atportso) {
- pport( $p, getservbyport($p, 'tcp') );
- }
- }
- if( @auports ) {
- printf " UDP port%s permitted (input):\n", ((@auports == 1)? '' : 's');
- for my $p (@auports) {
- pport( $p, getservbyport($p, 'udp') );
- }
- }
- if( @auportso ) {
- printf " UDP port%s permitted (output):\n", ((@auportso == 1)? '' : 's');
- for my $p (@auportso) {
- pport( $p, getservbyport($p, 'udp') );
- }
- }
- if( @aips ) {
- printf " IP %s permitted:\n", ((@aips == 1)? 'address/network' : 'addresses/networks');
- for my $ip (@aips) {
- printf " $ip\n";
- }
- }
- if( @dips ) {
- printf " IP %s blocked:\n", ((@dips == 1)? 'address/network' : 'addresses/networks');
- for my $ip (@dips) {
- printf " $ip\n";
- }
- }
- }
- } else {
- print "\n";
- }
- return 1;
- }
- print "Blocked countries IP filter is stopped\n";
- return 0;
- }
- sub list {
- my $list = get( $BLOCKURL );
- unless( defined $list ) {
- print "Unable to contact $BLOCKURL for listing\n";
- return 0;
- }
- print "Recognized country codes:\n";
- my @ccs = $list =~ /href=['"](..)\.zone["']/gm;
- for my $cc (sort @ccs) {
- my $cn = code2country($cc);
- next unless( defined $cn ); # There seem to be some undocumented zones
- printf " $cc - %s\n", $cn
- }
- return 1;
- }
- sub delsubchains {
- my $chain = shift;
- # List sub-chains of the form NAME-[IO]-nnn
- my @schains = map { (m/^($chain-\d+)\s/ ? ( $1, ) : ()) } split( /\n/, `$IPT -n -L $chain 2>/dev/null`);
- # Delete each one
- # First, delete the rule in the main chain that reads '-s 1st octet, goto subchain'
- # Then empty and delete the subchain
- for my $schain (@schains) {
- $schain =~ m/-([IO])-(\d+)$/;
- system( "$IPT -D $chain -" . ($1 eq 'I'? 's' : 'd' ) . " $2.0.0.0/8 -g $schain" );
- system( "$IPT -F $schain" );
- system( "$IPT -X $schain" );
- }
- }
- sub delchainref {
- my $main = shift; # e.g. INPUT
- my $chain = shift; # e.g. BLOCKchain
- my @crefs = map { (m/^$chain\s/ ? ( $chain, ) : ()) } split( /\n/, `$IPT -n -L $main 2>/dev/null`);
- for my $cref (@crefs) { # should be only one
- system( "$IPT -D $main -j $cref" );
- }
- }
- # Sort function for IP addresses for installation into filter chains
- # The whole chain must be processed if we miss, so there's nothing we can do.
- # But on a hit, we can improve the expected time somewhat by checking the
- # largest blocks first. This corresponds to the smallest mask length.
- # It is possible to do better if the traffic pattern is known, but there
- # isn't a good way (short of active feedback) to determine it.
- # In any case, we reduce the search length by hashing on the first
- # octet of the address, so this is a secondary effect.
- sub ipcmp {
- my $x = $a->masklen <=> $b->masklen;
- return $x if( $x );
- return $a <=> $b;
- }
- sub start {
- print "Starting blocked countries IP filter: " unless( $ENV{CRONJOB} );
- File::Path::make_path( $ZONEDIR, { mode => 0771 } ) unless( -d $ZONEDIR );
- # Delete any lingering references / parts of new chain
- delchainref( $IHOOK, "$IPNEWCHAIN-I" );
- delchainref( $OHOOK, "$IPNEWCHAIN-O" );
- delchainref( $FHOOK, "$IPNEWCHAIN-O" );
- delsubchains( "$IPNEWCHAIN-I" );
- delsubchains( "$IPNEWCHAIN-O" );
- foreach my $pchain ( @genlist ) {
- system( "$IPT -F $IPNEWCHAIN-$pchain >/dev/null 2>&1" );
- system( "$IPT -X $IPNEWCHAIN-$pchain >/dev/null 2>&1" );
- }
- # (Log & ) drop chain
- system( "$IPT -F $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
- system( "$IPT -X $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
- return failure unless( system( "$IPT -N $IPNEWCHAIN-DLOG" ) == 0 );
- if( $log ) {
- # Note that we can not provide a per-country log prefix due to compaction.
- # However, the intercepts report will map IPs back to their (alleged) country of origin
- # To determine what countries are causing intercepts, the logs must be post-processed to
- # lookup each IP.
- my $limits = "";
- $limits = "-m limit --limit $loglimits[0] --limit-burst $loglimits[1] " if( @loglimits );
- return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG $limits-j LOG --log-prefix \"$LOGPFX\"" ) == 0 );
- }
- return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG -j DROP" ) == 0 );
- # Local subnets - external firewalls prevent them from showing up here, but
- # a bogus zone file could do damage.
- unshift @aips, NetAddr::IP->new( '192.168.0.0/16' ), NetAddr::IP->new( '172.16.0.0/12' ), NetAddr::IP->new( '10.0.0.0/8' );
- # Optimize IP lists
- @aips = sort ipcmp NetAddr::IP::Compact( @aips );
- @dips = sort ipcmp NetAddr::IP::Compact( @dips );
- # Make sure we have a zone file for each country code
- # Fetch a new one if -update or we don't have one
- # If we fetch, only transfer the file if it's different from (usu. newer than) our copy.
- my @files;
- for my $c (@iso) {
- my $db = "$ZONEDIR/$c.zone";
- my $cn = code2country( $c );
- $cn = " ($cn)" if( defined $cn );
- # Fetch if updating or have no data
- if( $update || ! -f $db || -z $db ) {
- my $rc = mirror( "$BLOCKURL/$c.zone", $db );
- if( is_success( $rc ) ) {
- print "\nUpdated IP zone data for $c$cn", if( $debug || $update || $ENV{CRONJOB} );
- # Shouldn't ever get an empty file, but may as well check
- unless( -f $db && -s $db ) {
- print "\nUpdated zone data for $c$cn is empty!";
- unlink $db;
- return failure;
- }
- } else {
- if( $rc == RC_NOT_MODIFIED ) { # Can only happen if file exists
- print "\nNo new IP data available for $c$cn " if( $debug || ($update && !$ENV{CRONJOB}) );
- } else {
- print "\nUnable to fetch IP zone data for $c$cn: $rc - ", status_message($rc), " ";
- }
- unless( -f $db && -s $db ) {
- # No data - don't replace current filter
- print "\nNo IP zone data available for $c$cn ";
- if( $debug ) {
- next;
- }
- return failure;
- }
- # Failed, but have old file, continue since other zones may be updated
- }
- }
- push @files, $db;
- }
- return failure unless( @files );
- # Parse the zone files and create a list of IP blocks
- my @addresses = ();
- for my $if (@files) {
- open( my $ifh, '<', $if ) or die( "Can't open $if: $!" );
- while( <$ifh> ) {
- s/\s*#.*$//;
- next unless( length );
- push @addresses, NetAddr::IP->new( $_ );
- }
- close $ifh;
- }
- return failure unless( @addresses );
- # Compact the blocks into the minimal covering set
- my $inaddrs = @addresses;
- @addresses = sort ipcmp NetAddr::IP::Compact(@addresses);
- open( my $fh, '>', $ZONETBL ) or die( "Can't open $ZONETBL: $!" );
- print $fh "# Generated by $prog on ", (scalar localtime), "\n",
- "*filter\n"; # table
- my( $exceptions, $xrules ) = (0,0);
- foreach my $pchain ( @genlist ) {
- return failure unless( system( "$IPT -N $IPNEWCHAIN-$pchain" ) == 0 );
- return failure unless( system( "$IPT -A $IPNEWCHAIN-$pchain -m state --state RELATED,ESTABLISHED -j RETURN" ) == 0 );
- # List any allowed ports - first since they have a netmask of 0
- # Allowed TCP ports - no more than 15 per rule (limit of multiport)
- $exceptions += @aips + @dips;
- my @ports = ($pchain eq 'I')? @atports : @atportso;
- $exceptions += @ports;
- while( @ports ) {
- my $n = @ports;
- $n = 15 if( $n > 15 );
- return failure unless( system( "$IPT -A $IPNEWCHAIN-$pchain -p tcp -m multiport --dports " . join( ',', @ports[0..$n-1] ) . ' -j RETURN' ) == 0 );
- splice( @ports, 0, $n );
- $xrules++;
- }
- # Allowed UDP ports
- @ports = ($pchain eq 'I')? @auports : @auportso;
- $exceptions += @ports;
- while( @ports ) {
- my $n = @ports;
- $n = 15 if( $n > 15 );
- return failure unless( system( "$IPT -A $IPNEWCHAIN-$pchain -p udp -m multiport --dports " . join( ',', @ports[0..$n-1] ) . ' -j RETURN' ) == 0 );
- splice( @ports, 0, $n );
- $xrules++;
- }
- # Allowed IPs (with optional masklen/netmask); largest size first
- #
- $xrules += @aips;
- my $match = ($pchain eq 'I' ? 's' : 'd');
- my @ips = @aips;
- while( @ips ) {
- return failure unless( system( "$IPT -A $IPNEWCHAIN-$pchain -$match " . shift( @ips ) . ' -j RETURN' ) == 0 );
- }
- # Explicitly blocked IPs
- $xrules += @dips;
- @ips = @dips;
- while( @ips ) {
- return failure unless( system( "$IPT -A $IPNEWCHAIN-$pchain -$match " . shift( @ips ) . " -j $IPNEWCHAIN-DLOG" ) == 0 );
- }
- # Generate iptables-restore data
- my %subchains;
- # Note: Do not include input hook table declaration as this will clear it
- # It is guaranteed to exist because we checked earlier
- if( $pchain eq 'I' ) {
- if( $IHOOK eq 'INPUT' ) {
- print $fh ":INPUT ACCEPT [0:0]\n"; # built-in chain, policy, counters
- }
- } elsif( $pchain eq 'O' ) {
- if( $OHOOK eq 'OUTPUT' ) {
- print $fh ":OUTPUT ACCEPT [0:0]\n"; # built-in chain, policy, counters
- }
- if( $FHOOK eq 'FORWARD' ) {
- print $fh ":FORWARD ACCEPT [0:0]\n"; # built-in chain, policy, counters
- }
- }
- foreach my $ipblock (@addresses) {
- $ipblock =~ /^(\d+)\./;
- unless( $subchains{$1} ) {
- print $fh ":$IPNEWCHAIN-$pchain-$1 - [0:0]\n", # subchain, no policy, zero counters
- "-A $IPNEWCHAIN-$pchain -$match $1.0.0.0/8 -g $IPNEWCHAIN-$pchain-$1\n"; # Chain - branch on 1st octet to subchain
- }
- $subchains{$1}++;
- if( $permitonly ) {
- print $fh "-A $IPNEWCHAIN-$pchain-$1 -$match $ipblock -j RETURN\n"; # Subchain, accept
- } else {
- print $fh "-A $IPNEWCHAIN-$pchain-$1 -$match $ipblock -j $IPNEWCHAIN-DLOG\n"; # Subchain, branch on match to log & drop
- }
- }
- if( $permitonly ) {
- foreach my $subchain (keys %subchains) {
- print $fh "-A $IPNEWCHAIN-$pchain-$subchain -j $IPNEWCHAIN-DLOG\n";
- }
- }
- if( $verbose && $pchain eq 'I' ) {
- # Statistics are identical for each chain
- my( $minlen, $maxlen );
- $minlen = $maxlen = $subchains{(keys %subchains)[0]};
- for my $s (values %subchains) {
- $minlen = $s if( $s < $minlen );
- $maxlen = $s if( $s > $maxlen );
- }
- print( "\n",
- $inaddrs . ($permitonly? ' permitted' : ' blocked') . " address ranges generated ",
- (scalar @addresses), " rules, using ",
- (scalar keys %subchains), " sub-chains. Savings: ",
- ($inaddrs - scalar @addresses), " rules (",
- sprintf( "%.2f", 100*(1- ((scalar @addresses))/$inaddrs)), "%). Minimum chain length: $minlen",
- ", Maximum: $maxlen\n" );
- }
- }
- print $fh "COMMIT\n",
- "# Completed on ", (scalar localtime), "\n";
- close $fh or die "Close failed for $ZONETBL: $!\n";
- if( $verbose ) {
- # Provide some statistics, mostly for debugging.
- # Exception statistics
- print( "\n",
- "$exceptions exceptions generated $xrules rules." );
- }
- # Install new ruleset
- # -- Mass-install new Chain, subchains & rules
- return failure unless( system( "$IPTR -n $ZONETBL" ) == 0 );
- # -- Link INPUT to the new chain
- return failure unless( system( "$IPT -I $IHOOK -j $IPNEWCHAIN-I" ) == 0 );
- # -- Link OUTPUT and FORWARD to the new chains if we generated them.
- if( $outrules ) {
- return failure unless( system( "$IPT -I $OHOOK -j $IPNEWCHAIN-O" ) == 0 );
- return failure unless( system( "$IPT -I $FHOOK -j $IPNEWCHAIN-O" ) == 0 );
- }
- # Remove old rules
- delchainref( $IHOOK, "$IPOLDCHAIN-I" );
- delchainref( $OHOOK, "$IPOLDCHAIN-O" );
- delchainref( $FHOOK, "$IPOLDCHAIN-O" );
- delsubchains( "$IPOLDCHAIN-I" );
- delsubchains( "$IPOLDCHAIN-O" );
- foreach my $pchain ( @genlist ) {
- system( "$IPT -F $IPOLDCHAIN-$pchain >/dev/null 2>&1" );
- system( "$IPT -X $IPOLDCHAIN-$pchain >/dev/null 2>&1" );
- }
- system( "$IPT -F $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
- system( "$IPT -X $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
- $IPOLDCHAIN = $IPNEWCHAIN;
- return success if( running );
- return failure;
- }
- sub stop {
- return 1 if( !running );
- print "Removing blocked countries IP filter";
- delchainref( $IHOOK, "$IPOLDCHAIN-I" );
- delchainref( $OHOOK, "$IPOLDCHAIN-O" );
- delchainref( $FHOOK, "$IPOLDCHAIN-O" );
- delsubchains( "$IPOLDCHAIN-I" );
- delsubchains( "$IPOLDCHAIN-O" );
- foreach my $pchain ( @genlist ) {
- system( "$IPT -F $IPOLDCHAIN-$pchain" );
- system( "$IPT -X $IPOLDCHAIN-$pchain" );
- }
- system( "$IPT -F $IPOLDCHAIN-DLOG" );
- system( "$IPT -X $IPOLDCHAIN-DLOG" );
- if( !running ) {
- success;
- return 1;
- }
- failure;
- return 0;
- }
- sub restart {
- # Don't stop since start will keep the current table alive until
- # the new one is active.
- return start();
- }
- # List intercepted IPs for today
- # This can be run in a cron job just before midnight to get a list of
- # IPs to report. Or, you can use -days n to get the last n days worth
- # of intercepts
- # Only works if logging is on
- sub intercepts {
- my( $fh, %ips );
- $days ||= 1;
- my $start = time() - ( $days * 24*60*60 );
- $host ||= Net::Domain::hostname();
- foreach my $logfile (glob $LOG) {
- my $lh = IO::Uncompress::Gunzip->new( $logfile, MultiStream => 1, Transparent => 1 );
- unless( $lh ) {
- print "Skipping system log file: $IO::Uncompress::Gunzip::GunzipError\n";
- next;
- }
- my $sl = Parse::Syslog->new( $lh, arrayref => 1 );
- # Record # intercepts for each ip => protocol => port
- while( my $l = $sl->next ) {
- next if( $l->[0] < $start );
- next unless $l->[2] eq $LOGPGM && $l->[1] =~ /$host/i;
- if( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(ICMP)\b.*?\bTYPE=(\d+)/ ) {
- $ips{$1}{lc $2}{$3}++;
- } elsif( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(\w+).*?\bDPT=(\d+)/ ) {
- $ips{$1}{lc $2}{$3}++;
- }
- }
- close $lh;
- }
- return 0 unless %ips;
- # List each intercepted IP, its country, the protocols, ports and number of packets for each
- print "Intercepts by host IP:\n";
- my( %ccip, %ccn );
- foreach (glob "$ZONEDIR/*.zone") {
- /$ZONEDIR\/(.*).zone/;
- my $cc = $1;
- next unless( defined code2country( $cc ) ); # Skip undocumented zone files
- open( my $ifh, '<', $_ ) or next;
- while( <$ifh> ) {
- s/\s*#.*$//;
- next unless( length );
- push @{$ccip{$cc}}, NetAddr::IP->new( $_ );
- }
- close $ifh;
- }
- for my $ip (sort map {NetAddr::IP->new($_)} keys %ips) {
- my $ccn;
- CCSEARCH:
- foreach my $cc (keys %ccip) {
- foreach my $cip (@{$ccip{$cc}}) {
- if( $cip->contains($ip) ) {
- print "$cc: ";
- $ccn = $cc;
- last CCSEARCH;
- }
- }
- }
- unless( $ccn ) { # Possible if we've stopped blocking a country but have old log entries
- $ccn = '??';
- print '??: ';
- }
- print $ip->addr;
- my @plist = sort keys %{$ips{$ip->addr}}; # Protocol
- for my $p (@plist) {
- my @rlist = sort keys %{$ips{$ip->addr}{$p}}; # Ports
- print ' ', join( ' ', map { my $n = $ips{$ip->addr}{$p}{$_}; $ccn{$ccn} += $n; "$p-$_($n)" } @rlist );
- }
- print "\n";
- }
- print "Intercepts by country:\n";
- for my $cc (sort {$ccn{$b} <=> $ccn{$a} } keys %ccn) {
- my $cn = code2country($cc);
- if( defined $cn ) {
- $cn = "$cc ($cn)";
- } else {
- $cn = $cc;
- }
- printf "%10u %s\n", $ccn{$cc}, $cn;
- }
- return 1;
- }
- if( $cmd eq 'start' ) {
- exit !start();
- }
- if( $cmd eq 'stop' ) {
- exit !stop();
- }
- if( $cmd eq 'restart' ) {
- exit !restart();
- }
- if( $cmd eq 'condrestart' ) {
- exit !(running && restart());
- }
- if( $cmd eq 'status' ) {
- exit !status();
- }
- if( $cmd eq 'list' ) {
- exit !list();
- }
- if( $cmd eq 'intercepts' ) {
- exit !intercepts();
- }
- if( $cmd eq 'help' ) {
- Usage();
- exit;
- }
- print "Usage: $prog (start|stop|restart|condrestart|status|list|intercepts|help)\n";
- exit 1;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement