├── .gitignore ├── BlockCountries ├── LICENSE ├── README.bcinstall ├── README.md ├── bcinstall └── config └── BlockCountries /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | -------------------------------------------------------------------------------- /BlockCountries: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # BlockCountries Block IP traffic from specified countries 4 | # 5 | # chkconfig: 2345 10 92 6 | # description: Blocks IP traffic from IP addresses assigned to specific countries 7 | # 8 | 9 | # iptables/ip6tables are dependencies, but aren't always registered as a service 10 | 11 | ### BEGIN INIT INFO 12 | # Provides: BlockCountries 13 | # Required-Start: $network $local_fs $remote_fs 14 | # Required-Stop: $network $local_fs $remote_fs 15 | # Default-Start: 2 3 4 5 16 | # Default-Stop: 0 1 6 17 | # Should-Start: $syslog $named ip6tables 18 | # Should-Stop: $syslog $named ip6tables 19 | # Short-Description: start and stop BlockCountries 20 | # Description: Blocks IP traffic from IP addresses assigned to specific countries 21 | # using iptables/ip6tables. Updates/queries database when not run 22 | # as an init script. 23 | ### END INIT INFO 24 | 25 | use strict; 26 | use warnings; 27 | 28 | # my $VERSION = see below; 29 | 30 | # Copyright (c) 2010, 2012, 2015, 2016 Timothe Litt, litt__at__acm_dot_org 31 | # All rights reserved. 32 | # 33 | # This software is licensed under the terms of the Perl 34 | # Artistic License (see http://dev.perl.org/licenses/artistic.html). 35 | # 36 | # This is free software - it works for me, and it may (or may not) 37 | # work for you. No warranty or support is provided. 38 | # 39 | # 40 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 43 | # THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 44 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 45 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | # 47 | # Except as contained in this notice, the name of the author shall not be 48 | # used in advertising or otherwise to promote the sale, use or other dealings 49 | # in this Software without prior written authorization from the author. 50 | # 51 | # Consider carefully whether you want to use this software 52 | # and the full consequences to your site and/or business. 53 | # 54 | # This is written as a technical means to assist in implementing 55 | # your policy. The author expressly disclaims any responsibility 56 | # for the consequences of using this software. 57 | # 58 | # The above copyright notice, cautions and this permission notice shall 59 | # be included in all copies or substantial portions of the Software. 60 | 61 | ### Block all traffic from specified countries. ### 62 | # 63 | # See Usage() for documentation 64 | # 65 | 66 | # Local configuration. First file in @CFGFILE that exists is used. 67 | # The environment variable BlockCountriesCFG is consulted before this 68 | # list of files. Therefore, it should NOT be necessary to edit this 69 | # script for any distribution. 70 | 71 | my @CFGFILE = qw{ ./BlockCountries 72 | ~/BlockCountries 73 | /etc/sysconfig/BlockCountries 74 | /etc/default/BlockCountries 75 | /etc/BlockCountries 76 | }; 77 | 78 | # The following were previously available for customization. 79 | # They should be set with the corresponding variables in 80 | # the configuration script instead. 81 | 82 | # my $CFGFILE = '/etc/sysconfig/BlockCountries'; 83 | 84 | # These can (and should) be specified in the configuration file 85 | # They can NOT be specified on the command line 86 | 87 | my $ZONEDIR = '/root/blockips'; # zonedir 88 | 89 | my $LOGPFX = '[Blocked CC]: '; # logpfx 90 | my $LOG = '/var/log/messages*'; # logfile Note: This is a wildcard to handle log rotation. .gz files will decompressed on the 91 | # fly and processed. 92 | my $LOGPGM = 'kernel'; # logpgm 93 | my $LOGLEVEL = ''; # loglevel To force a non-default logging priority, specify here (e.g. NOTICE). 94 | # Can be a number or name. Leave as '' for the default. (Recommended) 95 | 96 | my %config = ( # Parameters that can vary between IPV4 and IPV6 97 | 4 => { 98 | IPT => '$path/iptables', # $path can be changed from config file with -path 99 | IPTR => '$path/iptables-restore', 100 | IPTS => '$path/iptables-save', 101 | IHOOK => 'INPUT-HOOK', # Note: if this table is not found, INPUT will be used 102 | OHOOK => 'OUTPUT-HOOK', # Note: if this table is not found, OUTPUT will be used 103 | FHOOK => 'FORWARD-HOOK', # Note: if this table is not found, FORWARD will be used 104 | table_names => '/proc/net/ip_tables_names', 105 | table_targets => '/proc/net/ip_tables_targets', 106 | table_matches => '/proc/net/ip_tables_matches', 107 | }, 108 | 6 => { 109 | IPT => '$path/ip6tables', 110 | IPTR => '$path/ip6tables-restore', 111 | IPTS => '$path/ip6tables-save', 112 | IHOOK => 'INPUT-HOOK', # Note: if this table is not found, INPUT will be used 113 | OHOOK => 'OUTPUT-HOOK', # Note: if this table is not found, OUTPUT will be used 114 | FHOOK => 'FORWARD-HOOK', # Note: if this table is not found, FORWARD will be used 115 | table_names => '/proc/net/ip6_tables_names', 116 | table_targets => '/proc/net/ip6_tables_targets', 117 | table_matches => '/proc/net/ip6_tables_matches', 118 | }, 119 | ); 120 | 121 | # List of country codes blocked by default - specify yours in the config file 122 | 123 | my @DEFAULT_ISO = qw /cn kr kp kz ru/; 124 | 125 | my $path = '/sbin'; # Path to iptables 126 | 127 | # ### End of configuration 128 | 129 | my %RIRS = ( # Should be stable; http: URIs would be prefered for IF_MODIFIED support 130 | arin => 'ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', 131 | ripe => 'ftp://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest', 132 | afrinic => 'ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest', 133 | apnic => 'ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-extended-latest', 134 | lacnic => 'ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest', 135 | ); 136 | 137 | # The following are either part of base perl, or available on CPAN 138 | # Commented-out modules are used, but not read until a command requires them. 139 | 140 | use 5.8.8; 141 | use Errno; 142 | use File::Basename; 143 | use Carp; 144 | use Cwd qw/realpath/; 145 | use File::Path; 146 | #use File::Temp; 147 | #use IO::Compress::Zip; 148 | #use IO::Uncompress::Gunzip; 149 | use Locale::Country; 150 | #use LWP::Simple; 151 | use Fcntl qw(:seek); 152 | use Net::IP; 153 | use NetAddr::IP qw(:lower); 154 | #use Net::Domain; 155 | #use Parse::Syslog; 156 | #use POSIX; 157 | use Regexp::IPv6 qw( $IPv6_re ); 158 | use Socket qw( :addrinfo SOCK_RAW ); 159 | use Storable qw( lock_store lock_retrieve ); 160 | #use Symbol 161 | use Sys::Syslog; 162 | use Text::ParseWords; 163 | #use Text::Wrap; 164 | 165 | # Changelog (Also update revision above & README!) 166 | # 1.0 Initial development 167 | # 1.1 Add support for hook tables 168 | # 1.2 By unpopular demand, add -permitonly 169 | # Add logging rate limit 170 | # Add -dip (deny IP) 171 | # 1.3 Add output filtering (-blockout) 172 | # 1.4 Output filters need to match on destination port. 173 | # Separate output port overrides as trojans like to ride on well-known ports 174 | # 1.5 Consolidate redundant code in parser, put config file arguments BEFORE command line. 175 | # E.g. start -nolog now overrides config file -log. 176 | # Update help. 177 | # 2.0 Obtain data directly from registries, support IPV6, inline short chains, more branching, optimize rule deletion. 178 | # Remove need for iptables file. Store country data in binary. 179 | # N.B. If your are running an older version of Perl, you may need to update Socket from cpan. 180 | # 2.1 Update for extended statistics file format defined by 181 | # https://www.arin.net/knowledge/statistics/nro_extended_stats_format.pdf 182 | # 2.2 Switch to conntrack --ctstate to avoid warnings. 183 | # 2.3 Add support for passive mode FTP to avoid requring FTP_PASSIVE.environment variable. Make conntrack use optional 184 | # under -newstate 185 | # 2.4 Clarify -permitonly and rule placement/priorities in help 186 | # 2.5 Fix uninitialized variable $cmd with no arguments. 187 | # 2.6 Add --version commmand, syntax cleanup for print & printf. Remove help switches due to uninitialized configuration 188 | # variables. 189 | # 2.7 Add LSB init block. 190 | # 2.8 Add LOGLEVEL configuration option. 191 | # 2.9 Simplify configuration. 192 | # 2.10 Add Default-Start and Default-Stop to LSB block, remove iptables because it's not always considered a service. 193 | # 2.11 Debug logging improvement: Append to STDERR when starting second filter. 194 | # 2.12 Rewrite debug logging and gather system data to assist diagnosing problems. 195 | # 2.13 Add zip compression to debug log when possible. Log start/stop to syslog. 196 | # 2.14 Cosmetic logging improvements. 197 | # 2.15 Include ipt-save format data in -d to ease reproducing issues 198 | # 2.16 Don't inspect a protocol's configuration if it's not enabled. Add -modules for debugging. 199 | # 2.17 Some future-proofing for -modules. Add -noprotect for obscure configurations. Check all close() errors. 200 | # Verify that perlcritic's complaints are invalid. 201 | # 2.18 Minor cleanup & debug determinisim changes. 202 | # 2.19 Include iptables version in debug configuration. Reformat some long lines of debug output. Fix 2.18's handling 203 | # of IPv6. 204 | # 2.20 Include information on the zone data in debug log. 205 | # 2.21 Provide additional detail when unable to get data from RIR 206 | # 2.22 Corect script permissions 207 | 208 | my $VERSION = '2.22'; 209 | 210 | # Make perlcritic happy: 211 | # 1) The special things that IO::Interactive checks for aren't worth another module as they don't apply 212 | # 2) Prototype *is* exactly what we want; is_tty is replacing the unary operator -t 213 | # This centralizes all the tests for -t so if IO::Interactive::is_interactive is ever needed, it can go here. 214 | 215 | sub is_tty( * ) { return -t shift; } ## no critic 216 | 217 | my $IPv4_re = qr/((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))/; 218 | 219 | umask 0137; 220 | 221 | my $prog = basename $0; 222 | 223 | my $stderr = \*STDERR; 224 | my $zipavail; 225 | 226 | # Command is never in config file 227 | 228 | my $cmd = shift; 229 | $cmd = '' unless( defined $cmd ); 230 | 231 | my $argc = @ARGV; 232 | 233 | my $currentCFGFILE; 234 | 235 | my %cfg = ( # switches only allowed in config file 236 | -zonedir => \$ZONEDIR, 237 | -logpfx => \$LOGPFX, 238 | -logfile => \$LOG, 239 | -logpgm => \$LOGPGM, 240 | -loglevel => \$LOGLEVEL, 241 | -path => \$path, 242 | ); 243 | 244 | foreach my $CFGFILE (($ENV{BlockCountriesCFG}, @CFGFILE)) { 245 | if( defined $CFGFILE && -e $CFGFILE && !-x $CFGFILE ) { 246 | open( my $fh, '<', $CFGFILE ) or die( "Can't open $CFGFILE: $!" ); 247 | my @cfg; 248 | while( <$fh> ) { 249 | s/\s*#.*$//; 250 | s/^\s+//; 251 | s/\s+$//; 252 | next unless length; 253 | push @cfg, parse_line( '\s+', 0, $_ ); 254 | } 255 | close( $fh ) or die( sprintf( "%s: %s\n", $CFGFILE, $! ) ); 256 | $currentCFGFILE = realpath( $CFGFILE ); 257 | 258 | # Check for immediate configuration variables and set them here 259 | # These are not allowed on the command line and are set to avoid 260 | # the necessity of editing this script to fit the local environment. 261 | 262 | for( my $i = 0; $i < @cfg; ) { 263 | my $arg = $cfg[$i]; 264 | 265 | if( exists $cfg{ $arg } && $i < $#cfg ) { 266 | ${$cfg{$arg}} = $cfg[$i+1]; 267 | splice( @cfg, $i, 2 ); 268 | } else { 269 | $i++; 270 | } 271 | } 272 | 273 | # Prepend config file arguments (so command line overrides if conflict) 274 | 275 | unshift @ARGV, @cfg; 276 | last; 277 | } 278 | } 279 | 280 | sub config { 281 | my $fh = shift; 282 | 283 | unless( defined $currentCFGFILE ) { 284 | print $fh ( "No configuration file found\n" ); 285 | return 1; 286 | } 287 | print $fh ( "Configured from $currentCFGFILE\n" ); 288 | my $mlen = 0; 289 | foreach my $item (keys %cfg) { 290 | $mlen = length $item if( length $item > $mlen ); 291 | } 292 | foreach my $item (sort keys %cfg) { 293 | printf $fh ( " %-*s: \"%s\"\n", $mlen, $item, ${$cfg{$item}} ); 294 | } 295 | return 0; 296 | } 297 | 298 | if( $cmd eq 'config' ) { 299 | exit( config( \*STDOUT ) ); 300 | } 301 | 302 | # Collect all arguments here, even though they are mostly for start 303 | # This allows detailed status 304 | 305 | my( $debug, $verbose, $modules, $protect, $usezip, $syslog, @iso, %iso, $update, $log, $days, $host, $permitonly, $outrules, 306 | $ipv4, $ipv6, $conntrack, @loglimits, @atports, @auports, @atportso, @auportso, @aips, @dips ); 307 | 308 | @loglimits = ( '1/minute', 10 ); 309 | $syslog = 1; 310 | $protect = 1; 311 | 312 | # Parse port number or name 313 | 314 | sub gport { 315 | my( $arg, $prot ) = @_; 316 | 317 | return $arg if( $arg =~ /^\d+$/ ); 318 | 319 | my $val = getservbyname( $arg, $prot ); 320 | return $val if( defined $val ); 321 | 322 | print( "Invalid $prot port $arg\n" ); 323 | exit 1; 324 | } 325 | 326 | # Parse ip address, ip subnet, or hostname 327 | 328 | sub gip { 329 | my $arg = shift; 330 | 331 | if( $arg =~ m,^(?:$IPv4_re|$IPv6_re)(?:/\d+)?$, ) { 332 | return NetAddr::IP->new( $arg ); 333 | } 334 | 335 | # Try hostname - supports both IPv4 and IPv6 336 | 337 | my ( $err, @res ) = getaddrinfo( $arg, "", { socktype => SOCK_RAW } ); 338 | if( $err ) { 339 | print( "Unrecognized $arg: $err\n" ); 340 | exit 1; 341 | } 342 | 343 | # Turn list of binary addresses back to (numeric) text. 344 | 345 | my @ips; 346 | 347 | while( my $ai = shift @res ) { 348 | my ( $err, $ipaddr ) = getnameinfo( $ai->{addr}, NI_NUMERICHOST, NIx_NOSERV ); 349 | if( $err ) { 350 | print( "Unrecognized $arg: $err\n" ); 351 | exit 1; 352 | } 353 | push @ips, NetAddr::IP->new( $ipaddr ); 354 | } 355 | 356 | return @ips; 357 | } 358 | 359 | # Loop over $ARGV 360 | my @argv = @ARGV; 361 | 362 | while( (my $arg = shift) ) { 363 | if( $arg =~ /^-/ ) { 364 | if( $arg eq '-blockout' ) { 365 | $outrules = 1; 366 | next; 367 | } 368 | if( $arg eq '-d' ) { 369 | $debug = 1; 370 | next; 371 | } 372 | if( $arg eq '-z' ) { 373 | $usezip = 1; 374 | next; 375 | } 376 | if( $arg eq '-modules' ) { 377 | $modules = 1; 378 | next; 379 | } 380 | if( $arg eq '-nomodules' ) { 381 | $modules = 0; 382 | next; 383 | } 384 | if( $arg eq '-protect' ) { 385 | $protect = 1; 386 | next; 387 | } 388 | if( $arg eq '-noprotect' ) { 389 | $protect = 0; 390 | next; 391 | } 392 | if( $arg eq '-noz' ) { 393 | $usezip = 0; 394 | next; 395 | } 396 | if( $arg eq '-syslog' ) { 397 | $syslog = 1; 398 | next; 399 | } 400 | if( $arg eq '-nosyslog' ) { 401 | $syslog = 0; 402 | next; 403 | } 404 | if( $arg eq '-limit' && $ARGV[0] ) { 405 | unless( $ARGV[0] =~ m#(\d+/(?:second|minute|hour|day))(?::(\d+))?# ) { 406 | print( "Syntax error in -limit $ARGV[0]\n" ); 407 | exit 1; 408 | } 409 | shift; 410 | $loglimits[0] = $1; 411 | $loglimits[1] = $2 if( defined $2 ); 412 | next; 413 | } 414 | if( $arg eq '-nolimit' ) { 415 | @loglimits = (); 416 | next; 417 | } 418 | if( $arg eq '-log' ) { 419 | $log = 1; 420 | next; 421 | } 422 | if( $arg eq '-nolog' ) { 423 | $log = 0; 424 | next; 425 | } 426 | if( $arg eq '-permitonly' ) { 427 | $permitonly = 1; 428 | next; 429 | } 430 | if( $arg eq '-update' ) { 431 | $update = 1; 432 | next; 433 | } 434 | if( $arg eq '-v' ) { 435 | $verbose = 1; 436 | next; 437 | } 438 | if( $arg eq '-days' && $ARGV[0] && $ARGV[0] =~ /^\d+$/ ){ 439 | $days = shift; 440 | next; 441 | } 442 | if( $arg eq '-host' && $ARGV[0] ){ 443 | $host = shift; 444 | next; 445 | } 446 | if( $arg eq '-passive' ){ 447 | $ENV{FTP_PASSIVE} = 1; 448 | next; 449 | } 450 | if( $arg eq '-nopassive' ){ 451 | delete $ENV{FTP_PASSIVE}; 452 | next; 453 | } 454 | if( $arg eq '-path' && $ARGV[0] ){ 455 | $path = shift; 456 | next; 457 | } 458 | if( $arg eq '-atport' && $ARGV[0] ){ 459 | push @atports, gport( shift, 'tcp' ); 460 | next; 461 | } 462 | if( $arg eq '-auport' && $ARGV[0] ){ 463 | push @auports, gport( shift, 'udp' ); 464 | next; 465 | } 466 | if( $arg eq '-atporto' && $ARGV[0] ){ 467 | push @atportso, gport( shift, 'tcp' ); 468 | next; 469 | } 470 | if( $arg eq '-auporto' && $ARGV[0] ){ 471 | push @auportso, gport( shift, 'udp' ); 472 | next; 473 | } 474 | if( $arg eq '-aip' && $ARGV[0] ) { 475 | push @aips, gip( shift ); 476 | next; 477 | } 478 | if( $arg eq '-dip' && $ARGV[0] ) { 479 | push @dips, gip( shift ); 480 | next; 481 | } 482 | if( $arg eq '-ipv4' || $arg eq '-4' ) { 483 | $ipv4 = 1; 484 | next; 485 | } 486 | if( $arg eq '-noipv4' || $arg eq '-no4' ) { 487 | $ipv4 = 0; 488 | next; 489 | } 490 | if( $arg eq '-ipv6' || $arg eq '-6' ) { 491 | $ipv6 = 1; 492 | next; 493 | } 494 | if( $arg eq '-conntrack' ) { 495 | $conntrack = 1; 496 | next; 497 | } 498 | if( $arg eq '-noipv6' || $arg eq '-no6' ) { 499 | $ipv6 = 0; 500 | next; 501 | } 502 | # Usage can't be supplied here since configuration isn't set up 503 | print( "Unknown switch $arg" ); 504 | print( ' ', $ARGV[0] ) if( defined $ARGV[0] && $ARGV[0] !~ /^-/ ); 505 | print( "\n" ); 506 | exit 1; 507 | } 508 | if( defined code2country( $arg ) ) { 509 | $iso{lc $arg} = 1; 510 | } else { 511 | my $cc = country2code( $arg ); 512 | if( defined $cc ) { 513 | $iso{lc $cc} = 1; 514 | } else { 515 | print( "Unrecognized country/country code: $arg\n" ); 516 | exit 1; 517 | } 518 | } 519 | } 520 | 521 | @argv = map { # Quote arguments for unambigous logging 522 | my $t = $_; 523 | 524 | my $qq = $t =~ s/(["\$`\\])/\\$1/g || $t =~ /\s/; 525 | $qq ||= $t =~ s/([[:^print:]])/'\\c' . chr( ord( 'A' ) - ord( "\cA" ) + ord( $1 ) )/ge; 526 | $qq? qq("$t") : $t 527 | } 528 | ( $0, ( splice( @argv, 0, @ARGV - $argc ), $cmd, splice( @argv, -$argc ) ) ); 529 | 530 | # Analyze a pipe'd handle error return and return a suitable text string 531 | sub pCloseError { 532 | my $reason = ''; 533 | 534 | if( $! ) { 535 | $reason = " $!"; 536 | } else { 537 | if( $? == -1 ) { 538 | $reason .= " Unknown reason"; 539 | } else { 540 | $reason .= sprintf( " killed by signal %u", $? & 127 ) if( $? & 127 ); 541 | $reason .= sprintf( " exit code %d", $? >> 8 ) if( $? >> 8 ); 542 | $reason .= " core dumped" if( $? & 128 ); 543 | } 544 | } 545 | 546 | return $reason; 547 | } 548 | 549 | if( $debug ) { 550 | require POSIX; 551 | require File::Temp; 552 | require Text::Wrap; 553 | 554 | $zipavail = eval { require IO::Compress::Zip }; 555 | 556 | $usezip = $zipavail unless( defined $usezip || is_tty $stderr ); 557 | 558 | # Attach STDERR to a tempfile so iptables output can be captured. 559 | # The real STDERR is saved and the output from it and the BC::IO logging copied to it 560 | # in the right sequence. 561 | 562 | my $errf = File::Temp->new( UNLINK => 1 ); 563 | open( my $std, ">&STDERR" ) or die( "Can't save STDERR: $!\n" ); 564 | $stderr = $std; 565 | close( STDERR ); # Required to re-open in update mode 566 | 567 | open( STDERR, "+>&" . fileno( $errf ) ) or ## no critic 568 | die( "Can't redirect STDERR: $!\n" ); 569 | 570 | if( $usezip ) { 571 | if( $zipavail && ! is_tty $stderr ) { 572 | print( "Debug log will be written as a .zip archive\n" ); 573 | } elsif( !$zipavail ) { 574 | print( "IO::Compress::Zip must be installed to use zip, debug log will be written as text\n" ); 575 | } else { 576 | print( "Debug log will be written as text: won't zip to a terminal\n" ); 577 | } 578 | } 579 | 580 | my $label = 'Executing'; 581 | printf STDERR ( "Version $VERSION invoked on %s\n%s", 582 | POSIX::strftime( '%A %e-%b-%Y %T %Z', localtime( time ) ), $label ); 583 | my $pos = length $label; 584 | $label = ' ' x length( $label ); 585 | 586 | foreach my $arg (@argv) { 587 | $pos += 1 + length $arg; 588 | if( $pos > 80 ) { 589 | $pos = length( $label ) + 1 + length $arg; 590 | printf STDERR ( "\n%s", $label ); 591 | } 592 | print STDERR ( ' ', $arg ); 593 | } 594 | print STDERR ( "\n\n" ), 595 | } 596 | 597 | if( ($modules || $cmd eq 'needs' || ($debug && !defined $modules)) && seek( DATA, 0, SEEK_SET ) ) { 598 | # Report any module used by any command 599 | # Caution: may mask or change some bugs, so -d -nomodules if it does... 600 | 601 | my %modules = ( 'IO::Compress::Zip' => 1 ); # Not detected by regexp 602 | my %pragma = ( map { $_ => 1 } (qw/attributes autodie autouse base bigint bignum bigrat/, 603 | qw/blib bytes charnames constant/, 604 | qw/diagnostics encoding feature fields filetest if integer/, 605 | qw/less lib locale mro/, 606 | qw/open ops overload overloading parent re sigtrap sort/, 607 | qw/strict vars subs threads thread::shared utr8 vars vmsish/, 608 | qw/warnings warnings::register/ ) ); 609 | my $max = 0; 610 | 611 | while( defined( my $line = ) ) { 612 | last if( $line =~ /^__END__$/ ); 613 | next unless( $line =~ m/^\s*(?:require|use)\s+([^\s(;]+)/ ); 614 | my $mod = $1; 615 | next if( $pragma{$mod} || $mod =~ /^\d+\.\d+\.\d+$/ ); 616 | $modules{$mod} = 1; 617 | $max = length( $mod ) if( length $mod > $max ); 618 | } 619 | my $needs = $cmd eq 'needs'? 'needs ': ''; # Generate bcinstall function calls 620 | 621 | printf STDERR ( "Modules used by $prog\n----------------%s\n", 622 | '-' x length $prog ) unless( $needs ); 623 | printf STDERR ( "$needs%-*s% vd\n", $max, ($needs? '' : 'Perl'), $^V ); 624 | 625 | foreach my $mod (sort keys %modules) { 626 | eval "require $mod"; ## no critic 627 | printf STDERR ( "$needs%-*s ", $max, $mod ); 628 | if( $@ ) { 629 | print STDERR ( "Not found\n" ); 630 | } else { 631 | my $ver = eval "\$${mod}::VERSION"; ## no critic 632 | printf STDERR ( "%s\n", ($@ || !defined $ver)? "undef": $ver ); 633 | } 634 | } 635 | print STDERR ( "\n" ); 636 | exit if( $needs ); 637 | } 638 | 639 | 640 | sub finish { 641 | if( $debug ) { 642 | local $!; 643 | local $?; 644 | 645 | seek( STDERR, 0, SEEK_CUR ) or croak( "seek: $!\n" ); 646 | 647 | printf STDERR ( "\n---------------------------------------------\nComplete on %s\n", 648 | POSIX::strftime( '%A %e-%b-%Y %T %Z', localtime( time ) ) ); 649 | 650 | # Copy spooled output for STDERR to real STDERR 651 | 652 | seek( STDERR, 0, SEEK_SET ) or die( "seek: $!\n" ); 653 | 654 | my $fh = $stderr; 655 | if( $debug && $zipavail && $usezip && ! is_tty $stderr ) { 656 | no warnings 'once'; 657 | $fh = IO::Compress::Zip->new( $stderr, 658 | AutoClose => 0, 659 | Name => sprintf( "$prog-%s.log", POSIX::strftime( '%Y-%m-%d-%H-%M-%S', 660 | gmtime( time ) ) ), 661 | CanonicalName => 1, 662 | TextFlag => 1 ) or 663 | die( "Can't create zipfile: $IO::Compress::Zip::ZipError\n" ); 664 | } 665 | print $fh ( $_ ) while( ); 666 | close( $fh ) if( $fh->isa( 'IO::Compress::Zip' ) ); 667 | } 668 | } 669 | 670 | END { 671 | 672 | finish(); 673 | } 674 | 675 | $SIG{PIPE} = 'IGNORE'; 676 | 677 | $SIG{__DIE__} = sub { 678 | die( @_ ) if( $^S ); 679 | 680 | print STDERR ( @_ ); 681 | 682 | finish(); 683 | 684 | exit( $! ) if( $! ); 685 | exit( $? >> 8 ) if( $? >> 8 ); 686 | exit 255; 687 | }; 688 | 689 | my( $ifcfgDone, $unameDone, $lsDone ); 690 | 691 | if( !defined $ipv4 && !defined $ipv6 ) { 692 | $ipv4 = 1; 693 | } 694 | 695 | foreach my $ipv (qw(4 6 )) { 696 | foreach my $key (qw(IPT IPTR IPTS)) { 697 | $config{$ipv}{$key} =~ s/\$path/$path/g; 698 | } 699 | } 700 | 701 | # Merge configuration with iptables state 702 | 703 | my %IP = ( 704 | 4 => { 705 | VERSION => 4, 706 | ENABLED => $ipv4, 707 | %{$config{4}}, 708 | }, 709 | 6 => { 710 | VERSION => 6, 711 | ENABLED => $ipv6, 712 | %{$config{6}}, 713 | }, 714 | ); 715 | { 716 | for my $v (keys %IP) { 717 | my $ipv = $IP{$v}; 718 | 719 | next unless( $ipv->{ENABLED} ); 720 | 721 | # New Old Default 722 | my @chains = ( 'BLOCKCC0', 'BLOCKCC1' ); 723 | 724 | open( my $ipt, '-|', $ipv->{IPT} . " -n -L 2>/dev/null" ) or ( print( $ipv->{IPT} . ": $!\n" ), return ); 725 | 726 | my( $i, $o, $f ) = ( $ipv->{IHOOK}, $ipv->{OHOOK}, $ipv->{FHOOK}, ); 727 | my $HOOK = qr/^($i|$o|$f|INPUT|OUTPUT|FORWARD)$/; 728 | my $chain; 729 | 730 | while( <$ipt> ) { 731 | my $line = $_; 732 | if( $line =~ /^Chain\s+([^\s(]+)\s+\(/ ) { 733 | $chain = $1; 734 | if( $chain =~ $HOOK ) { 735 | if( $chain eq $i ) { 736 | $ipv->{ihookvalid} = 1; 737 | } elsif( $chain eq $o ) { 738 | $ipv->{ohookvalid} = 1; 739 | } elsif( $chain eq $f ) { 740 | $ipv->{fhookvalid} = 1; 741 | } 742 | } 743 | } elsif( $line =~ /^(BLOCKCC0-[^\s]+)\s/ ) { 744 | die( "Bad table format at $.\n" ) unless( defined $chain ); 745 | if( $chain =~ $HOOK ) { 746 | @chains = ( 'BLOCKCC1', 'BLOCKCC0' ); # CC0 is active, make 1 new 747 | } 748 | } 749 | } 750 | close( $ipt ) or ( printf( "%s: %s\n", $ipv->{IPT}, pCloseError() ), return ); 751 | $ipv->{IHOOK} = 'INPUT' unless( $ipv->{ihookvalid} ); 752 | $ipv->{OHOOK} = 'OUTPUT' unless( $ipv->{ohookvalid} ); 753 | $ipv->{FHOOK} = 'FORWARD' unless( $ipv->{fhookvalid} ); 754 | $ipv->{NEWCHAIN} = $chains[0]; 755 | $ipv->{OLDCHAIN} = $chains[1]; 756 | } 757 | } 758 | 759 | @iso = sort keys %iso; 760 | @iso = @DEFAULT_ISO unless( @iso ); 761 | 762 | # Primary chains to generate (I is input, O is output; forward shares output) 763 | 764 | my @genlist = $outrules? qw/I O/ : 'I'; 765 | 766 | sub Usage { 767 | print( << "HELP" ); 768 | IP filter manager for country filters version $VERSION 769 | 770 | Usage: $prog command args 771 | config Display active configuration file 772 | status [-v] Display filter status. 773 | -v provides configuration from config file 774 | and command file - NOT iptables. 775 | list List available country names/codes. 776 | Contacts server for list. 777 | intercepts [-host name] [-days n] 778 | List today\'s intercepts by host from $LOG. 779 | Requires -log to start. 780 | stop args Stop filtering 781 | restart args Synonym for start (reloads with no open window) 782 | condrestart args Restarts only if already running 783 | start args Starts filter 784 | 785 | Start uses tables of IP blocks assigned to country codes that are 786 | stored in $ZONEDIR, which will be created if necessary. The country 787 | files are binary, and are only created for country codes that are 788 | filtered. The data is obtained from the Regional Internet Registries 789 | when absent, or when start -update is specified. 790 | 791 | iptables filters are generated and installed by start. The filters are 792 | optimized and generally will not look identical to the input data. However 793 | they will match the same address (no more and no fewer.) 794 | 795 | Arguments for start-class commands are: 796 | -update Get latest IP allocation data from regional registries. 797 | Otherwise, only gets data if no local file exists for a registry. 798 | -4 -ipv4 Install filter for IPv4 addresses \\ Default is -4 override with -no4 -no6 799 | -6 -ipv6 Install filter for IPv6 addresses / For both, use -4 -6 800 | -conntrack Use -m conntrack rather than -m state to avoid warnings from newer iptables 801 | -log Install a logging rule to log rejected packets. 802 | -nolog Don\'t install a logging rule. (default) 803 | -nolimit Do not limit logging (can generate huge log files if under attack; not advised) 804 | -limit spec Limit logging, default = $loglimits[0]:$loglimits[1] (see man iptables "limit") 805 | -atport n Allow connections to TCP port n even FROM banned addresses. 806 | May specify any number of times. May use a service name. 807 | -auport n Allow connections to UDP port n even FROM banned addresses. 808 | May specify any number of times. May use a service name. 809 | -atporto n Same as -atport, but for connections TO banned addresses. 810 | -auporto n Same as -auport, but for connections TO banned addresses. 811 | -aip ip(\/mask) Allow connection from an otherwise banned IP address. 812 | For a block, specify a netlength or mask. A hostname may 813 | also be specified. May specify any number of times. 814 | -dip ip(\/mask) Deny connections from an otherwise allowed IP address. 815 | Same syntax as -aip. 816 | -passive When using FTP for updates, use passive mode (traverse firewalls) 817 | -path /sbin Path for iptables utilities (use in the configuration file) 818 | -permitonly Listed countries will be permited, all others denied. 819 | Put in -permitonly in the configuration file so status and start are consistent. 820 | -protect Protect private (RFC1918) addresses from filtering (default) 821 | -noprotect Don\'t 822 | -syslog Log start/stop to syslog (default) 823 | -nosyslog Don\'t log to syslog 824 | -blockout Also generate rules to block output & forwarded-output. 825 | This is probably not required for most applications, and 826 | will roughly double the memory requirements. 827 | Caution: If you use -blockout for start, you must also use it for stop. 828 | This will not be a problem if it\'s in the configuration file. 829 | -d Output random debugging messages. Must redirect stderr to disk for some 830 | commands. Can be several MB. 831 | -z Write -d output to a zip archive on stderr. TTY not allowed. Default if possible. 832 | -noz Don\'t 833 | -v Output extended status/statistics 834 | CC ISO Country code or name to ban (or permit if -permitonly). 835 | Specify as many as you like. 836 | Default list: 837 | HELP 838 | for my $cc (sort @DEFAULT_ISO ) { 839 | print( " $cc - ", code2country($cc), "\n" ); 840 | } 841 | my $IPT4 = $IP{4}{IPT}; 842 | my $IPT6 = $IP{6}{IPT}; 843 | 844 | my $cfglist = join( "\n", @CFGFILE ); 845 | 846 | $currentCFGFILE = "none found" unless( defined $currentCFGFILE ); 847 | 848 | print( << "HELP" ); 849 | 850 | Arguments may also be obtained from the configuration file, currently 851 | $currentCFGFILE. The configuration file must be readable but not 852 | executable. It is searched for in the following places. The first 853 | file found is used: 854 | 855 | The file specified by the environment variable BlockCountriesCFG 856 | $cfglist 857 | 858 | The system configuration is specified by the variables: 859 | -logfile "/var/log/messages*" # Wildcard should include rotated/archived/.gz compressed files 860 | # containing iptables log messages 861 | -loglevel number or name # syslog message priority. Normally omitted. System-dependent. 862 | # Usually 0-6 (or as names, EMERGENCY, ALERT, CRITICAL,ERROR, 863 | # WARNING,NOTICE,INFORMATIONAL, or DEBUG) 864 | -logpfx '[Blocked CC]:' # Prefix used for log messages from BlockedCountries 865 | -logpgm 'kernel' # Program name used by netfilter. Always 'kernel'. 866 | -path '/sbin' # Path for iptables components 867 | -zonedir '/root/blockips' # Directory used to hold IP mapping data 868 | 869 | The defaults are indicated above. These variables can NOT be specified on the command line. 870 | 871 | Anything else (except comments) contained in it is prepended to every command 872 | line\'s arguments. 873 | 874 | Use single or double-quoted strings for country names containining spaces. 875 | 876 | This script is designed to run as a service; depending on your distribution 877 | chkconfig, update-rc.d, or "system-ctl enable" will link it into /etc/rcN.d/. 878 | Be sure to put all configuration in the configuration file, since startup 879 | scripts only get "start" (or "stop") as an argument. 880 | 881 | This script should also be run with start -update from a cron job - weekly 882 | is suggested - to obtain the latest IP address databases. If the CRONJOB 883 | environment variable is set, only errors and zone updates will be reported. 884 | This is recomended to minimize e-mail from cron. 885 | 886 | To prevent communications from banned IP addresses during updates, 887 | start will install new rules before removing active rules. For 888 | this to be effective, you should not stop the service. 889 | 890 | The status -v command will report the current configuration, although the 891 | actual implementation in iptables will be different due to optimizations. 892 | 893 | Rules are applied to the INPUT, OUTPUT or FORWARD chains after any existing 894 | IPTABLES rules. Typically, this means that related and established connections 895 | are accepted before BlockCountries rules are applied, and icmp packets are 896 | allowed. (This varies by distribution and local option.) To control 897 | placement, define the corresponding -HOOK chain.(e.g. INPUT-HOOK) and call it 898 | it from the main chain(s). BlockCountries rules will be installed in 899 | the -HOOK chain instead of the main chain. 900 | 901 | When manual configuration directives are used, the rules are applied in the 902 | following order (first match determines result): 903 | -atports (or -atportso) are accepted 904 | -auports (or -auportso) are accepted 905 | -aips are accepted 906 | -dips are denied 907 | Country constraints deny or accept (per -permitonly) 908 | IPTABLES rules later in the caller\'s chain 909 | If a BlockCountries rule denies a connection, it will be dropped (and logged). 910 | If a BlockCountries rule "accepts" a connection, it is still subject to any 911 | IPTABLES rules that follow it. 912 | 913 | To view the actual iptables configuration, use 914 | $IPT4 -nvL --line-number | less for IPV4 or 915 | $IPT6 -nvL --line-number | less for IPV6 916 | 917 | start -v will provide some statistics for the optimizations and generated rules. 918 | 919 | The intercepts command will parse $LOG and summarize intercepts by IP address. 920 | It will break down the dropped packets by protocol(s) and port(s). Of course, 921 | logging must be on for this to work. -days specifys how many days back 922 | (from the current time) to read. -host specifies the hostname to match. 923 | Default -days is 1, hostname is current host. 924 | 925 | Bug reports: 926 | Please raise bug reports or suggestions at http://github.com/tlhackque/BlockCountries/issues. 927 | Always include BlockCountries version, BlockCountries config, and perl --version. 928 | Praise is also welcome. 929 | 930 | Credits: 931 | Some ideas came from http://www.cyberciti.biz/faq/block-entier-country-using-iptables/. 932 | 933 | This version of the script merges all the IP address blocks; this saves over 1,000 934 | rules for the default banned address list. It\'s also somewhat faster than a shell 935 | script, and contains a more complete and polished user and system interface. It is 936 | not a complete superset; at this time it does not implement file archiving (better 937 | left to your backup solution) nor does it implement interface selection (better done 938 | with your own firewall rules and placement of the *-HOOK chains.) 939 | 940 | Issues: 941 | Consider carefully whether you want to use this software and the full consequences 942 | to your site and\/or business. By necessity, it will block potential customers and 943 | 'good' connections along with villains. You must consider the costs and benefits 944 | to your operation - the author does not endorse any specific policy. In particular, 945 | the defaults should be viewed as examples, not value judgements. 946 | 947 | Very large numbers of exception IP blocks might benefit from implementing a subchain 948 | structure - but that would be a rather different use model. The known use cases 949 | would probably be penalized - so one would want to make a dynamic choice. I\'d 950 | want to see actual data before implementing this. 951 | 952 | The iptables-restore format is undocumented, though used by others. It may be 953 | fragile. 954 | 955 | This code should use IPTables::IPv4 - but it doesn\'t currently work on my x64 system. 956 | It may be re-written to do so at some point. 957 | 958 | --tlhackque 1-Aug-2010, 8-Nov-2010, 3-Oct-2012, 4-Sep-2013, 17-Dec-2015, 5-Feb-2016 959 | HELP 960 | } 961 | 962 | # Success and failure return true and false, and 963 | # print boot-compatible strings (color and aligned 964 | # to column 60 if on a terminal) 965 | # CHA (...G) 966 | # SGR codes used (...m) 967 | # 0 = default; 1 = bold; 31 = green, 32 = red; 1 = bold; 39 = "default" (boot requires) 968 | 969 | sub success { 970 | eval { 971 | openlog( $prog, 'pid', 'daemon' ); 972 | syslog( "info", "succeeded" ); 973 | closelog; 974 | } if( $syslog ); 975 | return 1 if( $ENV{CRONJOB} ); 976 | 977 | if( is_tty STDOUT ) { 978 | print( "\033[60G[\033[1;32m OK \033[0;39m]\n" ); 979 | } else { 980 | print( " [ OK ]\n" ); 981 | } 982 | return 1; 983 | } 984 | 985 | sub failure { 986 | eval { 987 | openlog( $prog, 'pid', 'daemon' ); 988 | syslog( "info", "failed" ); 989 | closelog; 990 | } if( $syslog ); 991 | return 0 if( $ENV{CRONJOB} ); 992 | 993 | if( is_tty STDOUT ) { 994 | print( "\033[60G[\033[1;31m FAILED \033[0;39m]\n" ); 995 | } else { 996 | print( " [ FAILED ]\n" ); 997 | } 998 | return 0; 999 | } 1000 | 1001 | sub running { 1002 | my %run; 1003 | 1004 | foreach my $ipv (sort values %IP) { 1005 | next unless( $ipv->{ENABLED} ); 1006 | 1007 | open( my $ipt, '-|', $ipv->{IPT} . " -n -L 2>/dev/null" ) or ( print( $ipv->{IPT} . ": $!\n" ), return ); 1008 | 1009 | my( $i, $o, $f ) = ( $ipv->{IHOOK}, $ipv->{OHOOK}, $ipv->{FHOOK}, ); 1010 | my %txt = ( 1011 | $i => 'Input', 1012 | $o => 'Output', 1013 | $f => 'Forwarding', 1014 | ); 1015 | 1016 | my $HOOK = qr/^($i|$o|$f)$/; 1017 | my $CHAIN = $ipv->{OLDCHAIN}; 1018 | my $chain; 1019 | 1020 | while( <$ipt> ) { 1021 | my $line = $_; 1022 | if( $line =~ /^Chain\s+([^\s(]+)\s+\(/ ) { 1023 | $chain = $1; 1024 | } elsif( $line =~ /^($CHAIN-[^\s]+)\s/ ) { 1025 | die( "Bad table format at $.\n" ) unless( defined $chain ); 1026 | if( $chain =~ $HOOK ) { 1027 | $run{$ipv->{VERSION}}{$txt{$1}} = 1; 1028 | } 1029 | } 1030 | } 1031 | close( $ipt ) or ( printf( "%s: %s\n", $ipv->{IPT}, pCloseError() ), return ); 1032 | } 1033 | 1034 | if( $ipv4 && $ipv6 ) { 1035 | return keys %{$run{4}} && keys %{$run{6}} && 1036 | "IPV4(" .join( ", ", sort keys %{$run{4}}) . ") & IPV6(" . 1037 | join( ", ", sort keys %{$run{6}}) . ")"; 1038 | } elsif( $ipv4 ) { 1039 | return keys %{$run{4}} && "IPV4(" .join( ", ", sort keys %{$run{4}}) . ")"; 1040 | } 1041 | return keys %{$run{6}} && "IPV6(" . join( ", ", sort keys %{$run{6}}) . ")"; 1042 | } 1043 | 1044 | sub pport { 1045 | my $p = shift; 1046 | my @p = @_; 1047 | 1048 | printf( " %5u", $p ); 1049 | if( @p ) { 1050 | print( " $p[0]" ); 1051 | if( length $p[1] ) { 1052 | print( " (", join( '/', split( ' ', $p[1] ) ), ")" ); 1053 | } 1054 | } 1055 | print( "\n" ); 1056 | } 1057 | 1058 | sub status { 1059 | if( (my $r = running) ) { 1060 | print( "Blocked countries $r filter is running" ); 1061 | if( $verbose ) { 1062 | printf( " and configured to %s:\n", ($permitonly? 'permit only' : 'block') ); 1063 | for my $cc (sort @iso ) { 1064 | print( " $cc - ", code2country($cc), "\n" ); 1065 | } 1066 | if( (my $n = @atports + @auports + @atportso + @auportso + @aips + @dips) ) { 1067 | printf( "However, the following exception%s:\n", ($n == 1)? ' exists' : 's exist' ); 1068 | if( @atports ) { 1069 | printf( " TCP port%s permitted (input):\n", ((@atports == 1)? '' : 's') ); 1070 | for my $p (@atports) { 1071 | pport( $p, getservbyport($p, 'tcp') ); 1072 | } 1073 | } 1074 | if( @atportso ) { 1075 | printf( " TCP port%s permitted (output):\n", ((@atportso == 1)? '' : 's') ); 1076 | for my $p (@atportso) { 1077 | pport( $p, getservbyport($p, 'tcp') ); 1078 | } 1079 | } 1080 | if( @auports ) { 1081 | printf( " UDP port%s permitted (input):\n", ((@auports == 1)? '' : 's') ); 1082 | for my $p (@auports) { 1083 | pport( $p, getservbyport($p, 'udp') ); 1084 | } 1085 | } 1086 | if( @auportso ) { 1087 | printf( " UDP port%s permitted (output):\n", ((@auportso == 1)? '' : 's') ); 1088 | for my $p (@auportso) { 1089 | pport( $p, getservbyport($p, 'udp') ); 1090 | } 1091 | } 1092 | if( @aips ) { 1093 | printf( " IP %s permitted:\n", ((@aips == 1)? 'address/network' : 'addresses/networks') ); 1094 | for my $ip (@aips) { 1095 | printf( " $ip\n" ); 1096 | } 1097 | } 1098 | if( @dips ) { 1099 | printf( " IP %s blocked:\n", ((@dips == 1)? 'address/network' : 'addresses/networks') ); 1100 | for my $ip (@dips) { 1101 | printf( " $ip\n" ); 1102 | } 1103 | } 1104 | } 1105 | } else { 1106 | print( "\n" ); 1107 | } 1108 | return 1; 1109 | } 1110 | print( "Blocked countries IP filter is stopped\n" ); 1111 | return 0; 1112 | } 1113 | 1114 | sub list { 1115 | my $lh; 1116 | 1117 | unless( open( $lh, '<', "$ZONEDIR/cclist.txt" ) ) { 1118 | unless( -e $ZONEDIR ) { 1119 | print( "$ZONEDIR does not exist. Please run $prog start -update\n" ); 1120 | return 0; 1121 | } 1122 | if( $!{ENOENT} ) { 1123 | print( "No data from registries found. Run $prog start -update to initialize the zone list\n" ); 1124 | return 0; 1125 | } 1126 | print( "Can't open index: $!\nHas $prog start -update been run?\n" ); 1127 | return 0; 1128 | } 1129 | 1130 | my %ccs; 1131 | while( <$lh> ) { 1132 | chomp; 1133 | $ccs{$_} = 1; 1134 | } 1135 | close( $lh ) or print( "$ZONEDIR/cclist.txt: $!\n" ); 1136 | 1137 | print( "Recognized country codes:\n" ); 1138 | 1139 | for my $cc (sort keys %ccs) { 1140 | my $cn = code2country($cc); 1141 | next unless( defined $cn ); # There seem to be some undocumented zones 1142 | printf( " $cc - %s\n", $cn ); 1143 | } 1144 | return 1; 1145 | } 1146 | 1147 | sub startUpdate { 1148 | my $ipv = shift; 1149 | 1150 | my $iptr = $ipv->{IPTR}; 1151 | 1152 | tie( my $fh, 'BC::IO', $debug, '|-', $iptr, '-n' ) or ( print( "Unable to run $iptr: $!\n" ), return ); 1153 | 1154 | if( $debug ) { 1155 | if( $ENV{PATH} && !( $ifcfgDone && $unameDone ) ) { 1156 | foreach my $dir (split( /:/, $ENV{PATH} )) { 1157 | my $ifp; 1158 | if( !$ifcfgDone && ( -x ($ifp = "$dir/ifconfig") || -x ($ifp = "$dir/ip") ) ) { 1159 | $ifcfgDone = 1; 1160 | $ifp .= ' a' if( $ifp =~ m,/ip$, ); 1161 | $fh->log( "%s\n%s\n", $ifp, '-' x length $ifp ); 1162 | if( open( my $ifc, '-|', "$ifp 2>&1" ) ) { 1163 | while( <$ifc> ) { 1164 | next unless( $ifp =~ m,/ip a$, || m/ (?:HW)?addr[: ]/ ); 1165 | $fh->log( "%s", $_ ); 1166 | } 1167 | close( $ifc ) or $fh->log( "%s: %s\n", $ifp, pCloseError() ); 1168 | } else { 1169 | $fh->log( "Unable to run $ifp: $!\n" ); 1170 | } 1171 | $fh->log( "\n" ); 1172 | } 1173 | if( !$unameDone && -x "$dir/uname" ) { 1174 | $unameDone = 1; 1175 | $fh->log( "$dir/uname -a\n--------\n" ); 1176 | if( open( my $unm, '-|', "$dir/uname -a 2>&1" ) ) { 1177 | while( <$unm> ) { 1178 | $fh->log( "%s", $_ ); 1179 | } 1180 | close( $unm ) or $fh->log( "%s: %s\n", "$dir/uname", pCloseError() ); 1181 | } else { 1182 | $fh->log( "Unable to run $dir/uname: $!\n" ); 1183 | } 1184 | $fh->log( "\n" ); 1185 | } 1186 | } 1187 | } 1188 | 1189 | unless( $lsDone ) { 1190 | $lsDone = 1; 1191 | 1192 | $fh->log( "\nZone data directory %s\n--------------------%s\n", $ZONEDIR, '-' x length( $ZONEDIR ) ); 1193 | my $lscmd = "ls -ld $ZONEDIR && ls -l $ZONEDIR"; 1194 | 1195 | if( open( my $ls, '-|', "2>&1 $lscmd" ) ) { 1196 | while( <$ls> ) { 1197 | $fh->log( "%s", $_ ); 1198 | } 1199 | close( $ls ) or $fh->log( "%s: %s\n", "$lscmd" , pCloseError() ); 1200 | } else { 1201 | $fh->log( "Unable to run $lscmd: $!\n" ); 1202 | } 1203 | $fh->log( "\n" ); 1204 | if( open( my $cc, '<', "$ZONEDIR/cclist.txt" ) ) { 1205 | { 1206 | local $/; 1207 | $_ = <$cc>; 1208 | } 1209 | s/\n/ /g; 1210 | 1211 | local $Text::Wrap::columns = 80; 1212 | local $Text::Wrap::unexpand = 0; 1213 | local $Text::Wrap::huge = 'wrap'; 1214 | 1215 | my $label = 'cclist.txt: '; 1216 | $fh->log( "%s\n", Text::Wrap::wrap( $label, ' ' x length( $label ), $_ ) ); 1217 | close( $cc ) or $fh->log( "%s: %s\n", "$ZONEDIR/cclist.txt", $! ); 1218 | } else { 1219 | $fh->log( "%s: %s\n", "$ZONEDIR/cclist.txt", $! ); 1220 | } 1221 | } 1222 | $fh->log( "\n" ); 1223 | 1224 | $fh->log( "\nCurrent IPv%u rules\n------------------\n\n", $ipv->{VERSION} ); 1225 | open( my $ipt, '-|', $ipv->{IPT} . " -n -L 2>&1" ) or 1226 | ( die( $ipv->{IPT} . ": $!\n" ) ); 1227 | 1228 | while( <$ipt> ) { 1229 | $fh->log( "%s", $_ ); 1230 | } 1231 | close( $ipt ) or $fh->log( "%s: %s\n", $ipv->{IPT}, pCloseError() ); 1232 | 1233 | $fh->log( "-------- $ipv->{IPTS} --------\n" ); 1234 | undef $ipt; 1235 | open( $ipt, '-|', $ipv->{IPTS} . " 2>&1" ) or 1236 | ( die( $ipv->{IPTS} . ": $!\n" ) ); 1237 | 1238 | while( <$ipt> ) { 1239 | $fh->log( "%s", $_ ); 1240 | } 1241 | close( $ipt ) or $fh->log( "%s: %s\n", $ipv->{IPTS}, pCloseError() ); 1242 | 1243 | $fh->log( "\nGenerating rules for %s\n---------------------%s\n", 1244 | $iptr, '-' x length( $iptr ) ); 1245 | } 1246 | 1247 | print $fh ( "*filter\n" ); 1248 | return $fh; 1249 | } 1250 | 1251 | sub commitUpdate { 1252 | my( $fh, $ipv ) = @_; 1253 | 1254 | print $fh ( "COMMIT\n" ); 1255 | unless( close $fh ) { 1256 | my $reason = 'Rules update failed:' . pCloseError() . sprintf( " at line %u\n", $fh->line ); 1257 | 1258 | print STDOUT ( $reason ) if( $debug ); 1259 | die( $reason ); 1260 | 1261 | return 0; 1262 | } 1263 | 1264 | if( $debug ) { 1265 | $fh->log( "\n----------------------\nRules update succeeded\n----------------------\n\n" . 1266 | "New IPv%u rules\n--------------\n\n", $ipv->{VERSION} ); 1267 | open( my $ipt, '-|', $ipv->{IPT} . " -n -L 2>&1" ) or ( die( $ipv->{IPT} . ": $!\n" ) ); 1268 | while( <$ipt> ) { 1269 | $fh->log( "%s", $_ ); 1270 | } 1271 | close( $ipt ) or $fh->log( "%s: %s\n", $ipv->{IPT}, pCloseError() ); 1272 | } 1273 | 1274 | return 1; 1275 | } 1276 | 1277 | sub defchain { 1278 | my( $fh, $declared, $chain, $remove ) = @_; 1279 | 1280 | unless( $declared->{$chain} ) { 1281 | # Chains must be declared (even if we're deleting them) 1282 | # Syntax differs for built-in chains. 1283 | 1284 | if( $chain =~ /^(INPUT|OUTPUT|FORWARD)$/ ) { 1285 | print $fh ( ":$chain ACCEPT [0:0]\n" ); 1286 | } else { 1287 | print $fh ( ":$chain - [0:0]\n" ); 1288 | } 1289 | } 1290 | $declared->{$chain} = ($remove? 2 : 1); 1291 | } 1292 | 1293 | # Delete all our rules from a chain 1294 | 1295 | sub deleterules { 1296 | my( $ipv, $declared, $MAIN, $fh ) = @_; 1297 | 1298 | my $chain; 1299 | 1300 | open( my $ipt, '-|', $ipv->{IPT} . " -n -L 2>/dev/null" ) or ( die( $ipv->{IPT} . ": $!\n" ) ); 1301 | 1302 | my( $i, $o, $f ) = ( $ipv->{IHOOK}, $ipv->{OHOOK}, $ipv->{FHOOK}, ); 1303 | my $HOOK = qr/^(?:$i|$o|$f)$/; 1304 | my $CHAIN = $ipv->{$MAIN}; 1305 | my @actions; 1306 | 1307 | while( <$ipt> ) { 1308 | my $line = $_; # Chain INPUT (policy ACCEPT) 1309 | if( $line =~ /^Chain\s+([^\s(]+)\s+\(/ ) { # Chain BLOCKCC0-I-103-239 (1 references) 1310 | $chain = $1; 1311 | if( $chain =~ /^${CHAIN}-/ ) { # A (sub)chain of ours, flush contents, queue deletion 1312 | defchain( $fh, $declared, $chain, 1 ); 1313 | push @actions, "-F $chain\n"; 1314 | } 1315 | } elsif( $line =~ /^(${CHAIN}-\S+)\s/ ) { # BLOCKCC1-I all -- 0.0.0.0/0 0.0.0.0/0 1316 | die( "Bad table format at $.\n" ) unless( defined $chain ); 1317 | my $target = $1; 1318 | if( $chain =~ $HOOK ) { # Remove each rule in every hook that calls our chain 1319 | # Must not defchain a hook as it will (mysteriously) empty it, 1320 | # causing -D to fail (and other chains using the hook to be lost as well.) 1321 | defchain( $fh, $declared, $target, 1 ); 1322 | push @actions, "-D $chain -j $target\n"; 1323 | } 1324 | } 1325 | } 1326 | close $ipt or $fh->log( "%s: %s\n", $ipv->{IPT}, pCloseError() ); 1327 | 1328 | # The declarations are done. Now do the actions. 1329 | # It should be OK to do the actions in-line with the declarations, 1330 | # but iptables does it this way... 1331 | 1332 | print $fh ( shift @actions ) while( @actions ); 1333 | 1334 | # All our chains are empty and the hook rule is gone. 1335 | # It's now legal to delete the (sub)chain since refcounts are zero 1336 | 1337 | # The sort shouldn't be necessary, but it allows deterministic debug. 1338 | 1339 | foreach my $chain (sort keys %$declared ) { 1340 | if( $declared->{$chain} == 2 ) { 1341 | print $fh ( "-X $chain\n" ); 1342 | delete $declared->{$chain}; # Allow redeclaration 1343 | } 1344 | } 1345 | } 1346 | 1347 | # Sort function for IP addresses for installation into filter chains 1348 | # The whole chain must be processed if we miss, so there's nothing we can do. 1349 | # But on a hit, we can improve the expected time somewhat by checking the 1350 | # largest blocks first. This corresponds to the smallest mask length. 1351 | # It is possible to do better if the traffic pattern is known, but there 1352 | # isn't a good way (short of active feedback) to determine it. 1353 | # In any case, we reduce the search length by hashing on the first 1354 | # octet of the address, so this is a secondary effect. 1355 | 1356 | sub ipcmp { 1357 | my $x = $a->version <=> $b->version; 1358 | return $x if( $x ); 1359 | $x = $a->masklen <=> $b->masklen; 1360 | return $x if( $x ); 1361 | return $a <=> $b; 1362 | } 1363 | 1364 | # Sort function for (sub)chain keys 1365 | # For deterministic debug - and while we're at it, looks. 1366 | 1367 | sub ip4kcmp { 1368 | my @a = split( /\./, $a ); 1369 | my @b = split( /\./, $b ); 1370 | 1371 | while( @a && @b ) { 1372 | my $a0 = shift @a; 1373 | my $b0 = shift @b; 1374 | my $x = $a0 <=> $b0; 1375 | return $x if( $x ); 1376 | } 1377 | return @a <=> @b; 1378 | } 1379 | 1380 | sub ip6kcmp { 1381 | my @a = split( /:/, $a ); 1382 | my @b = split( /:/, $b ); 1383 | 1384 | while( @a && @b ) { 1385 | my $a0 = hex( shift @a ); 1386 | my $b0 = hex( shift @b ); 1387 | my $x = $a0 <=> $b0; 1388 | return $x if( $x ); 1389 | } 1390 | return @a <=> @b; 1391 | } 1392 | 1393 | # Start command 1394 | 1395 | sub start { 1396 | print( "Starting blocked countries IP filter: " ) unless( $ENV{CRONJOB} ); 1397 | 1398 | File::Path::make_path( $ZONEDIR, { mode => oct( 771 ) } ) unless( -d $ZONEDIR ); 1399 | 1400 | # Local subnets - external firewalls prevent them from showing up here, but 1401 | # a bogus zone file could do damage. 1402 | unshift( @aips, ( NetAddr::IP->new( '192.168.0.0/16' ), 1403 | NetAddr::IP->new( '172.16.0.0/12' ), 1404 | NetAddr::IP->new( '10.0.0.0/8' ) ) ) 1405 | if( $protect ); 1406 | 1407 | # Optimize IP lists 1408 | @aips = sort ipcmp NetAddr::IP::Compact( @aips ); 1409 | @dips = sort ipcmp NetAddr::IP::Compact( @dips ); 1410 | 1411 | # Make sure we have a data file from each registry 1412 | # Generate a new one if -update or we don't have one 1413 | # If we fetch, only transfer the file if it's different from (usu. newer than) our copy. 1414 | 1415 | # Make sure we have RIR data, refresh it if needed 1416 | 1417 | my $newdata; 1418 | for my $rir (keys %RIRS) { 1419 | my $db = "$ZONEDIR/$rir.rdb"; 1420 | 1421 | # Fetch if updating or have no data from RIR 1422 | 1423 | next unless( $update || ! -f $db || -z $db ); 1424 | 1425 | require HTTP::Status; 1426 | HTTP::Status->import; 1427 | 1428 | require LWP::UserAgent; 1429 | 1430 | my $ua = LWP::UserAgent->new; 1431 | $ua->agent( "$prog/$VERSION" ); 1432 | $ua->env_proxy; 1433 | 1434 | print( "\nChecking $rir for new data" ) if( $debug ); 1435 | my $response = $ua->mirror( $RIRS{$rir}, $db ); 1436 | my $rc = $response->code; 1437 | if( is_success( $rc ) ) { 1438 | 1439 | print( "\nUpdated IP zone data from $rir" ) if( $debug || $update || $ENV{CRONJOB} ); 1440 | # Shouldn't ever get an empty file, but may as well check 1441 | unless( -f $db && -s $db ) { 1442 | print( "\nUpdated zone data from $rir is empty!" ); 1443 | unlink $db; 1444 | return failure; 1445 | } 1446 | $newdata = 1; 1447 | } else { 1448 | if( $rc == RC_NOT_MODIFIED() ) { # Can only happen if file exists 1449 | print( "\nNo new IP data available from $rir " ) if( $debug || ($update && !$ENV{CRONJOB}) ); 1450 | } else { 1451 | print( "\nUnable to fetch IP zone data from $rir: $rc - ", $response->message, " " ); 1452 | print( "\n - Attempted: $RIRS{$rir} " ); 1453 | } 1454 | next; 1455 | } 1456 | unless( -f $db && -s $db ) { 1457 | # No data - don't replace current country files or filter 1458 | print( "\nNo IP zone data available from $rir " ); 1459 | if( $debug ) { 1460 | next; 1461 | } 1462 | return failure; 1463 | } 1464 | } 1465 | 1466 | # new data from any registry invalidates all saved country data 1467 | 1468 | if( $newdata ) { 1469 | foreach my $cdb (glob "$ZONEDIR/*.cdb") { 1470 | unlink $cdb; 1471 | } 1472 | } 1473 | 1474 | # Only extract data from RIRS if at least one RIR has new data or we don't have 1475 | # country data cached for a CC we're using 1476 | # The data for a given country code can come from more than one RIR (!), so if 1477 | # one changes, we have to (re)process all the RIRs. 1478 | 1479 | my %ccneeded = map { my $cdb = -f "$ZONEDIR/$_.cdb"; $newdata ||= !$cdb; ($_ => !$cdb) } @iso; 1480 | my %ccavail; 1481 | my %cips; 1482 | if( $newdata ) { 1483 | for my $rir (keys %RIRS) { 1484 | my $db = "$ZONEDIR/$rir.rdb"; 1485 | 1486 | # Extract data into country files 1487 | 1488 | my $rirfile; 1489 | open( $rirfile, "<", $db ) or ( print( "\nCan't open $db: $! " ), return failure ); 1490 | my( $n, $r4, $r6 ) = (0) x 3; 1491 | my( $version, $reg, $serial, $records, $startdate, $enddate, $utc ); 1492 | while( <$rirfile> ) { 1493 | next if( /^\s*#/ ); 1494 | next if( /^\s*$/ ); 1495 | 1496 | chomp; 1497 | 1498 | unless( defined $version ) { 1499 | ( $version, $reg, $serial, $records, $startdate, $enddate, $utc ) = split( m/\|/, $_, 7 ); 1500 | unless( defined $version && $version =~ /^2(?:\.3)?$/ ) { 1501 | print( "\nUnknown version $version for $db" ); 1502 | return failure; 1503 | } 1504 | next; 1505 | } 1506 | # Summary lines are "registry | * | type | count |'summary' 1507 | # types are asn, ipv4, ipv6 1508 | # regid is a unique code for the resource holder 1509 | # status is one of: 1510 | # available => unallocated 1511 | # allocated => by this registry 1512 | # assigned => by this registry 1513 | # reserved => for growth of a specific consumer, experiments, etc 1514 | 1515 | my( $registry, $cc, $type, $start, $value, $date, $status, $regid, $extensions ) = split( m/\|/, $_, 9 ); 1516 | 1517 | next unless( $type =~ /^ipv[46]$/ && defined $cc && length $cc && $cc ne '*' ); 1518 | 1519 | # N.B. $registry 'iana' is CC 'ZZ' 1520 | next if (defined $status && $status eq 'available'); 1521 | 1522 | $cc =~ /^(\w\w)$/ && $value > 0 or ( print( "\nInvalid record $_ in $db at line $. "), next ); 1523 | $cc = lc $1; 1524 | $ccavail{$cc} = 1; 1525 | next unless( $ccneeded{$cc} ); 1526 | 1527 | if( $type eq 'ipv4' ) { 1528 | my $ip = Net::IP->new( "$start + " . ($value-1) ); 1529 | my @prefixes = $ip->find_prefixes; 1530 | 1531 | foreach my $pfx (@prefixes) { 1532 | push @{$cips{$cc}{4}}, NetAddr::IP->new( $pfx ); 1533 | $r4++; 1534 | } 1535 | } else { 1536 | push @{$cips{$cc}{6}}, NetAddr::IP->new( "$start/$value" ); 1537 | $r6++; 1538 | } 1539 | $n++; 1540 | } 1541 | close( $rirfile ) or ( print( "\nClose $db: $!" ), return failure ); 1542 | print( "\nExtracted ", $r4+$r6, " ranges ($r4 IPV4, $r6 IPV6) from $n records from $db " ) if( $debug ); 1543 | } 1544 | 1545 | # Index of all country codes (for LIST, so the RIR files don't need to be scanned.) 1546 | 1547 | open( my $idx, '>', "$ZONEDIR/cclist.txt" ) or ( print( "\nCan't open index: $!\n" ), return 0 ); 1548 | for my $cc (sort keys %ccavail) { 1549 | print $idx ( "$cc\n" ); 1550 | } 1551 | close( $idx ) or ( print( "\nClose $ZONEDIR/cclist.txt: $!" ), return failure ); 1552 | 1553 | # Compress each CC's data & save as a binary file 1554 | 1555 | for my $cc (keys %cips) { 1556 | for my $ipv (keys %{$cips{$cc}}) { 1557 | my $inc = @{$cips{$cc}{$ipv}}; 1558 | $cips{$cc}{$ipv} = [ NetAddr::IP::Compact(@{$cips{$cc}{$ipv}}) ]; 1559 | if( $verbose && $inc ) { 1560 | my $outc = @{$cips{$cc}{$ipv}}; 1561 | print( "\nExtracted $inc IPV$ipv ranges", ($inc == $outc? '' : "; compressed to $outc"), " for $cc " ); 1562 | } 1563 | } 1564 | lock_store( $cips{$cc}, "$ZONEDIR/$cc.cdb" ) or ( print( "\nUnable to write $ZONEDIR/$cc.cdb: $! "), return failure ); 1565 | print( "\nWrote $ZONEDIR/$cc.cdb" ) if( $debug ); 1566 | } 1567 | } 1568 | 1569 | # Get any CC IP data that didn't just come from the RIR from stored files. 1570 | 1571 | for my $c (@iso) { 1572 | my $db = "$ZONEDIR/$c.cdb"; 1573 | my $cn = code2country( $c ); 1574 | $cn = " ($cn)" if( defined $cn ); 1575 | 1576 | unless( -f $db && -s $db ) { 1577 | # No data - don't replace current filter 1578 | $cn ||= ''; 1579 | print( "\nNo IP zone data available for $c$cn " ); 1580 | if( $debug ) { 1581 | next; 1582 | } 1583 | return failure; 1584 | } 1585 | unless( $cips{$c} ) { 1586 | $cips{$c} = lock_retrieve( $db ); 1587 | unless( $cips{$c} ) { 1588 | print( "\nUnable to read $db: $!\n" ); 1589 | return failure; 1590 | } 1591 | print( "\nRead $ZONEDIR/$c.cdb" ) if( $debug ); 1592 | } 1593 | } 1594 | 1595 | print( "\n" ) if( $debug ); 1596 | 1597 | # Generate the tables for each protocol 1598 | 1599 | for my $ipv (sort values %IP) { 1600 | next unless( $ipv->{ENABLED} ); 1601 | 1602 | my $version = $ipv->{VERSION}; 1603 | my $NEWCHAIN = $ipv->{NEWCHAIN}; 1604 | 1605 | my @addresses; 1606 | 1607 | # Addresses from each country 1608 | 1609 | foreach my $cc (@iso) { 1610 | push @addresses, @{$cips{$cc}{$ipv->{VERSION}}} if( defined $cips{$cc}{$version} ); 1611 | delete $cips{$cc}{$version}; 1612 | } 1613 | 1614 | # Compact the blocks into the minimal covering set 1615 | my $inaddrs = @addresses; 1616 | 1617 | next unless( $inaddrs ); 1618 | 1619 | @addresses = sort ipcmp NetAddr::IP::Compact(@addresses); 1620 | 1621 | # Generate iptables-restore data 1622 | 1623 | my $fh = startUpdate( $ipv ) or return failure; 1624 | my %declared; 1625 | 1626 | # Delete any lingering references / parts of new chain 1627 | 1628 | deleterules( $ipv, \%declared, 'NEWCHAIN', $fh ); 1629 | 1630 | 1631 | # (Log & ) drop chains 1632 | 1633 | defchain( $fh, \%declared, "${NEWCHAIN}-DLOG", 0 ); 1634 | 1635 | if( $log ) { 1636 | # Note that we can not provide a per-country log prefix due to compaction. 1637 | # However, the intercepts report will map IPs back to their (alleged) country of origin 1638 | # To determine what countries are causing intercepts, the logs must be post-processed to 1639 | # lookup each IP. 1640 | my $limits = ''; 1641 | $limits = "-m limit --limit $loglimits[0] --limit-burst $loglimits[1] " if( @loglimits ); 1642 | my $loglevel = ''; 1643 | $loglevel = " --log-level $LOGLEVEL" if( $LOGLEVEL ne '' ); 1644 | print $fh ( "-A ${NEWCHAIN}-DLOG $limits-j LOG --log-prefix \"$LOGPFX\"$loglevel\n" ); 1645 | } 1646 | print $fh ( "-A ${NEWCHAIN}-DLOG -j DROP\n" ); 1647 | 1648 | my( $exceptions, $xrules ) = (0,0); 1649 | foreach my $pchain ( @genlist ) { # Direction codes: I or I O 1650 | defchain( $fh, \%declared, "${NEWCHAIN}-${pchain}", 0 ); 1651 | if( $conntrack ) { 1652 | print $fh ( "-A ${NEWCHAIN}-$pchain -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN\n" ); 1653 | } else { 1654 | print $fh ( "-A ${NEWCHAIN}-$pchain -m state --state RELATED,ESTABLISHED -j RETURN\n" ); 1655 | } 1656 | 1657 | # List any allowed ports - first since they have a netmask of 0 1658 | 1659 | # Allowed TCP ports - no more than 15 per rule (limit of multiport) 1660 | 1661 | $exceptions += @aips + @dips; 1662 | 1663 | my @ports = ($pchain eq 'I')? @atports : @atportso; 1664 | $exceptions += @ports; 1665 | while( @ports ) { 1666 | my $n = @ports; 1667 | $n = 15 if( $n > 15 ); 1668 | print $fh ( "-A ${NEWCHAIN}-$pchain -p tcp -m multiport --dports " . join( ',', @ports[0..$n-1] ) . " -j RETURN\n" ); 1669 | splice( @ports, 0, $n ); 1670 | $xrules++; 1671 | } 1672 | 1673 | # Allowed UDP ports 1674 | 1675 | @ports = ($pchain eq 'I')? @auports : @auportso; 1676 | $exceptions += @ports; 1677 | 1678 | while( @ports ) { 1679 | my $n = @ports; 1680 | $n = 15 if( $n > 15 ); 1681 | print $fh ( "-A ${NEWCHAIN}-$pchain -p udp -m multiport --dports " . join( ',', @ports[0..$n-1] ) . " -j RETURN\n" ); 1682 | splice( @ports, 0, $n ); 1683 | $xrules++; 1684 | } 1685 | 1686 | # Allowed IPs (with optional masklen/netmask); largest size first 1687 | # 1688 | 1689 | my $match = ($pchain eq 'I' ? 's' : 'd'); 1690 | 1691 | my @ips = @aips; 1692 | while( @ips ) { 1693 | my $ip = shift @ips; 1694 | next unless( $ip->version == $ipv->{VERSION} ); 1695 | $xrules++; 1696 | print $fh ( "-A ${NEWCHAIN}-$pchain -$match $ip -j RETURN\n" ); 1697 | } 1698 | 1699 | # Explicitly blocked IPs 1700 | 1701 | @ips = @dips; 1702 | while( @ips ) { 1703 | my $ip = shift @ips; 1704 | next unless( $ip->version == $ipv->{VERSION} ); 1705 | $xrules++; 1706 | print $fh ( "-A ${NEWCHAIN}-$pchain -$match $ip -j ${NEWCHAIN}-DLOG\n" ); 1707 | } 1708 | 1709 | 1710 | my %subchains; 1711 | 1712 | # Note: Do not include hook table declaration as this will clear it 1713 | # If we're hooking a system table, the declaration is required. 1714 | # Hook tables are guaranteed to exist because we checked earlier 1715 | 1716 | my $hook = $ipv->{$pchain.'HOOK'}; 1717 | defchain( $fh, \%declared, $hook, 0 ) if( $hook =~ /^(?:INPUT|OUTPUT|FORWARD)$/ ); 1718 | my $mrules = 0; 1719 | 1720 | foreach my $ipblock (@addresses) { 1721 | my $v = $ipblock->version; 1722 | next unless( $v == $ipv->{VERSION} ); 1723 | 1724 | my $subchain; 1725 | if( $v == 4 ) { 1726 | ($subchain) = ($ipblock =~ /^(\d+)\./); 1727 | unless( $subchains{$subchain} ) { 1728 | $subchains{$subchain}{define} = # Chain - branch on 1st octet to subchain 1729 | "-A ${NEWCHAIN}-${pchain} -$match ${subchain}.0.0.0/8 -g ${NEWCHAIN}-${pchain}-${subchain}\n"; 1730 | } 1731 | } else { 1732 | my $pfx = $ipblock->masklen; 1733 | if( $pfx < 24 ) { 1734 | # This net is bigger than our subchains, so a subchain won't work. 1735 | # We could split the net, but that isn't worthwhile, as all paths 1736 | # lead to the same result and it costs memory. So no subchains in those cases. 1737 | if( $permitonly ) { 1738 | print $fh ( "-A ${NEWCHAIN}-${pchain} -$match $ipblock -j RETURN\n" ); 1739 | } else { 1740 | print $fh ( "-A ${NEWCHAIN}-${pchain} -$match $ipblock -j ${NEWCHAIN}-DLOG\n" ); 1741 | } 1742 | $mrules++; 1743 | next; 1744 | } 1745 | my $ipb = $ipblock->full; 1746 | ($subchain) = ($ipb =~ /^([[:xdigit:]]{4}:[[:xdigit:]]{2})[[:xdigit:]]{2}:/); 1747 | unless( $subchains{$subchain} ) { 1748 | my $submask = $subchain; 1749 | $submask =~ m/:([[:xdigit:]]+)$/; 1750 | $submask .= '0' x (4-length($1)); # Left-justify mask 1751 | $subchains{$subchain}{define} = # Chain - branch on 1st 32 bits to subchain 1752 | "-A ${NEWCHAIN}-${pchain} -$match ${submask}::0/24 -g ${NEWCHAIN}-${pchain}-${subchain}\n"; 1753 | } 1754 | } 1755 | $subchains{$subchain}{length}++; 1756 | push @{$subchains{$subchain}{blocks}}, $ipblock; 1757 | } 1758 | 1759 | # Evaluate the subchains, and inline any that turned out to be length 1 1760 | # move the inlined chains to the end? 1761 | 1762 | my $kidchains = 0; 1763 | my $inlined = 0; 1764 | foreach my $subchain (sort { $ipv->{VERSION} == 4? ip4kcmp( $a, $b ): ip6kcmp( $a, $b ) } keys %subchains) { 1765 | if( $subchains{$subchain}{length} == 1 ) { 1766 | # Inline this subchain, as it is less expensive than the small chain 1767 | print $fh ( "-A ${NEWCHAIN}-${pchain} -$match " . (@{$subchains{$subchain}{blocks}})[0] . " -j ", 1768 | ($permitonly? "RETURN\n" : "${NEWCHAIN}-DLOG\n") ); 1769 | $inlined++; 1770 | $mrules++; 1771 | next; 1772 | } 1773 | # Either for data or dispatch to child chains (or both), we need this subchain 1774 | 1775 | defchain( $fh, \%declared, "${NEWCHAIN}-${pchain}-$subchain", 0 ); 1776 | print $fh ( $subchains{$subchain}{define} ); 1777 | 1778 | if( $subchains{$subchain}{length} > 50 ) { # Empirical threshold. 1779 | if( $ipv->{VERSION} == 4 ) { 1780 | my %kids; 1781 | my @subips = @{$subchains{$subchain}{blocks}}; 1782 | $subchains{$subchain}{blocks} = []; 1783 | $subchains{$subchain}{length} -= @subips; 1784 | while( my $ipblock = shift @subips ) { 1785 | if( $ipblock->masklen < 16 ) { # Too big for a child chain 1786 | push @{$subchains{$subchain}{blocks}}, $ipblock; 1787 | $subchains{$subchain}{length}++; 1788 | next; 1789 | } 1790 | my( $kid ) = ($ipblock =~ /^\d+\.(\d+)\./); 1791 | push @{$kids{$kid}}, $ipblock; 1792 | } 1793 | # Evaluate the child chains. Inline short ones by pushing them back on the subchain. 1794 | foreach my $kid (sort { $ipv->{VERSION} == 4? ip4kcmp( $a, $b ): ip6kcmp( $a, $b ) } keys %kids) { 1795 | if( @{$kids{$kid}} == 1 ) { 1796 | push @{$subchains{$subchain}{blocks}}, (@{$kids{$kid}})[0]; 1797 | $subchains{$subchain}{length}++; 1798 | $kids{$kid} = []; 1799 | next; 1800 | } 1801 | defchain( $fh, \%declared, "${NEWCHAIN}-${pchain}-${subchain}-${kid}", 0 ); 1802 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain} -$match ${subchain}.${kid}.0.0/16 " . 1803 | "-g ${NEWCHAIN}-${pchain}-${subchain}-${kid}\n" ); 1804 | $subchains{$subchain}{length}++; 1805 | foreach my $ipblock (@{$kids{$kid}}) { 1806 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain}-${kid} -$match $ipblock -j ", 1807 | ($permitonly? "RETURN\n" : "${NEWCHAIN}-DLOG\n") ); 1808 | } 1809 | if( $permitonly ) { 1810 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain}-${kid} -j ${NEWCHAIN}-DLOG\n" ); 1811 | } 1812 | $kidchains++; 1813 | } 1814 | $subchains{$subchain}{blocks} = [ sort ipcmp @{$subchains{$subchain}{blocks}} ]; 1815 | } else { 1816 | my %kids; 1817 | my @subips = @{$subchains{$subchain}{blocks}}; 1818 | $subchains{$subchain}{blocks} = []; 1819 | $subchains{$subchain}{length} -= @subips; 1820 | while( my $ipblock = shift @subips ) { # 24 sub + 16 child? 1821 | if( $ipblock->masklen < 40 ) { # Too big for a child chain 1822 | push @{$subchains{$subchain}{blocks}}, $ipblock; 1823 | $subchains{$subchain}{length}++; 1824 | next; 1825 | } 1826 | my( $kid ) = ($ipblock->full =~ 1827 | /^[[:xdigit:]]{4}:[[:xdigit:]]{2}([[:xdigit:]]{2}:[[:xdigit:]]{2})[[:xdigit:]]{2}:/); 1828 | push @{$kids{$kid}}, $ipblock; 1829 | } 1830 | # Evaluate the child chains. Inline short ones by pushing them back on the subchain. 1831 | foreach my $kid (sort keys %kids) { 1832 | if( @{$kids{$kid}} == 1 ) { 1833 | push @{$subchains{$subchain}{blocks}}, (@{$kids{$kid}})[0]; 1834 | $subchains{$subchain}{length}++; 1835 | $kids{$kid} = []; 1836 | next; 1837 | } 1838 | defchain( $fh, \%declared, "${NEWCHAIN}-${pchain}-${subchain}-${kid}", 0 ); 1839 | my $submask = "${subchain}${kid}"; 1840 | $submask =~ tr/://d; 1841 | $submask .= '0' x (4-(length($submask) % 4)) if( length($submask) %4 ); # Left-justify mask 1842 | $submask =~ s/([[:xdigit:]]{4})/$1:/g; 1843 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain} -$match $submask:0/40 " . 1844 | "-g ${NEWCHAIN}-${pchain}-${subchain}-${kid}\n" ); 1845 | $subchains{$subchain}{length}++; 1846 | foreach my $ipblock (@{$kids{$kid}}) { 1847 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain}-${kid} -$match $ipblock -j ", 1848 | ($permitonly? "RETURN\n" : "${NEWCHAIN}-DLOG\n") ); 1849 | } 1850 | if( $permitonly ) { 1851 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain}-${kid} -j ${NEWCHAIN}-DLOG\n" ); 1852 | } 1853 | $kidchains++; 1854 | } 1855 | $subchains{$subchain}{blocks} = [ sort ipcmp @{$subchains{$subchain}{blocks}} ]; 1856 | } 1857 | } 1858 | 1859 | foreach my $ipblock (@{$subchains{$subchain}{blocks}}) { 1860 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain} -$match $ipblock -j ", 1861 | ($permitonly? "RETURN\n" : "${NEWCHAIN}-DLOG\n") ); 1862 | } 1863 | if( $permitonly ) { 1864 | print $fh ( "-A ${NEWCHAIN}-${pchain}-${subchain} -j ${NEWCHAIN}-DLOG\n" ); 1865 | } 1866 | } 1867 | 1868 | if( $verbose && $pchain eq 'I' ) { 1869 | # Statistics are identical for each chain 1870 | my( $minlen, $maxlen ); 1871 | for my $s (map { $_->{length} } values %subchains) { 1872 | $minlen = $s if( !defined $minlen || $s < $minlen ); 1873 | $maxlen = $s if( !defined $maxlen || $s > $maxlen ); 1874 | } 1875 | print( "\n", 1876 | $inaddrs . ($permitonly? ' permitted' : ' blocked') . " IPV" . $ipv->{VERSION} . " address ranges generated ", 1877 | (scalar @addresses), " rules, using ", 1878 | (scalar keys %subchains), " sub-chains. Savings: ", 1879 | ($inaddrs - scalar @addresses), " rules (", 1880 | sprintf( "%.2f", 100*(1- ((scalar @addresses))/$inaddrs)), "%).\nMinimum chain length: $minlen", 1881 | ", Maximum: $maxlen, Inlined: $inlined; Split: $kidchains; Primary rules: $mrules\n" ); 1882 | } 1883 | 1884 | # Link hook'd chain to new chain 1885 | print $fh ( "-I $hook -j ${NEWCHAIN}-${pchain}\n" ); 1886 | 1887 | # Forward shares chain with output 1888 | print $fh ( "-I " . $ipv->{'FHOOK'} . " -j ${NEWCHAIN}-${pchain}\n" ) if( $pchain eq 'O' ); 1889 | } 1890 | 1891 | if( $verbose ) { 1892 | # Provide some statistics, mostly for debugging. 1893 | 1894 | # Exception statistics 1895 | print( "\n", 1896 | "$exceptions exceptions generated $xrules rules." ); 1897 | } 1898 | 1899 | # Remove old rules 1900 | deleterules( $ipv, \%declared, 'OLDCHAIN', $fh ); 1901 | 1902 | commitUpdate( $fh, $ipv ); 1903 | 1904 | $ipv->{OLDCHAIN} = $ipv->{NEWCHAIN}; 1905 | } 1906 | 1907 | return success if( running ); 1908 | 1909 | return failure; 1910 | } 1911 | 1912 | sub stop { 1913 | return 1 if( !running ); 1914 | 1915 | print( "Removing blocked countries IP filter" ); 1916 | 1917 | for my $ipv (sort values %IP) { 1918 | next unless( $ipv->{ENABLED} ); 1919 | 1920 | my $fh = startUpdate( $ipv ) or return failure; 1921 | my %declared; 1922 | 1923 | deleterules( $ipv, \%declared, 'OLDCHAIN', $fh ); 1924 | 1925 | commitUpdate( $fh, $ipv ); 1926 | } 1927 | 1928 | if( !running ) { 1929 | success; 1930 | return 1; 1931 | } 1932 | failure; 1933 | return 0; 1934 | } 1935 | 1936 | sub restart { 1937 | # Don't stop since start will keep the current table alive until 1938 | # the new one is active. 1939 | return start(); 1940 | } 1941 | 1942 | # List intercepted IPs for today 1943 | # This can be run in a cron job just before midnight to get a list of 1944 | # IPs to report. Or, you can use -days n to get the last n days worth 1945 | # of intercepts 1946 | # Only works if logging is on 1947 | 1948 | sub intercepts { 1949 | my( $fh, %ips ); 1950 | 1951 | require IO::Uncompress::Gunzip; 1952 | require Net::Domain; 1953 | require Parse::Syslog; 1954 | 1955 | $days ||= 1; 1956 | 1957 | my $start = time() - ( $days * 24*60*60 ); 1958 | $host ||= Net::Domain::hostname(); 1959 | 1960 | foreach my $logfile (glob $LOG) { 1961 | my $lh = IO::Uncompress::Gunzip->new( $logfile, MultiStream => 1, Transparent => 1 ); 1962 | unless( $lh ) { 1963 | print( "Skipping system log file: $IO::Uncompress::Gunzip::GunzipError\n" ); 1964 | my $x = $IO::Uncompress::Gunzip::GunzipError; # 2nd use (because we require rather than use) 1965 | next; 1966 | } 1967 | my $sl = Parse::Syslog->new( $lh, arrayref => 1 ); 1968 | 1969 | # Record # intercepts for each ip => protocol => port 1970 | 1971 | while( my $l = $sl->next ) { 1972 | next if( $l->[0] < $start ); 1973 | next unless $l->[2] eq $LOGPGM && $l->[1] =~ /$host/i; 1974 | # [timestamp] [logprefix] $2 = ipv4, $3-6 = ipv4 octects 1975 | if( $l->[4] =~ /^(?:\[[\d.]+\]\s+)?\Q$LOGPFX\E.*?\bSRC=($IPv4_re|$IPv6_re).*?\bPROTO=(ICMP(?:v?6)?)\b.*?\bTYPE=(\d+)/ ) { 1976 | $ips{NetAddr::IP->new($1)->addr}{lc $7}{$8}++; 1977 | } elsif( $l->[4] =~ /^(?:\[[\d.]+\]\s+)?\Q$LOGPFX\E.*?\bSRC=($IPv4_re|$IPv6_re).*?\bPROTO=(\w+).*?\bDPT=(\d+)/ ) { 1978 | $ips{NetAddr::IP->new($1)->addr}{lc $7}{$8}++; 1979 | } 1980 | } 1981 | 1982 | close( $lh ) or printf( "%s: %s\n", $logfile, $! ); 1983 | } 1984 | 1985 | return 0 unless %ips; 1986 | 1987 | # List each intercepted IP, its country, the protocols, ports and number of packets for each 1988 | 1989 | print( "Intercepts by host IP:\n" ); 1990 | 1991 | my( %ccip, %ccn ); 1992 | foreach (glob "$ZONEDIR/*.cdb") { 1993 | m,$ZONEDIR/(.*).cdb$,; 1994 | my $cc = $1; 1995 | 1996 | $ccip{$cc} = lock_retrieve( $_ ); 1997 | unless( $ccip{$cc} ) { 1998 | print( "\nUnable to read $_: $!\n" ); 1999 | return 0; 2000 | } 2001 | print( "\nRead $_" ) if( $debug ); 2002 | } 2003 | print( "\n" ) if( $debug ); 2004 | 2005 | for my $ip (sort ipcmp map {NetAddr::IP->new($_)} keys %ips) { 2006 | my $ccn; 2007 | CCSEARCH: 2008 | foreach my $cc (keys %ccip) { 2009 | my $cips = $ccip{$cc}{$ip->version}; 2010 | foreach my $cip (@$cips) { 2011 | if( $cip->contains($ip) ) { 2012 | print( "$cc: " ); 2013 | $ccn = $cc; 2014 | last CCSEARCH; 2015 | } 2016 | } 2017 | } 2018 | unless( $ccn ) { # Old log entries from country no longer blocked, or have a -dip from an unblocked country 2019 | $ccn = '??'; 2020 | print( '??: ' ); 2021 | } 2022 | print( ($ip->version == 6? $ip->short : $ip->addr) ); 2023 | my @plist = sort keys %{$ips{$ip->addr}}; # Protocol 2024 | for my $p (@plist) { 2025 | my @rlist = sort keys %{$ips{$ip->addr}{$p}}; # Ports 2026 | print( ' ', join( ' ', map { my $n = $ips{$ip->addr}{$p}{$_}; $ccn{$ccn} += $n; "$p-$_($n)" } @rlist ) ); 2027 | } 2028 | print( "\n" ); 2029 | } 2030 | 2031 | print( "Intercepts by country:\n" ); 2032 | for my $cc (sort {$ccn{$b} <=> $ccn{$a} } keys %ccn) { 2033 | my $cn = code2country($cc); 2034 | if( defined $cn ) { 2035 | $cn = "$cc ($cn)"; 2036 | } else { 2037 | $cn = $cc; 2038 | } 2039 | printf( "%10u %s\n", $ccn{$cc}, $cn ); 2040 | } 2041 | return 1; 2042 | } 2043 | 2044 | if( $debug ) { 2045 | if( config( \*STDERR ) == 0 ) { 2046 | if( open( my $fh, '<', $currentCFGFILE ) ) { 2047 | print STDERR ( "\nConfiguration file\n------------------\n" ); 2048 | while( <$fh> ) { 2049 | print STDERR ( " $_" ); 2050 | } 2051 | close( $fh ) or printf STDERR ( "%s: %s\n", $currentCFGFILE, $! ); 2052 | print STDERR ( '-' x 80, "\n" ); 2053 | } 2054 | } 2055 | 2056 | foreach my $ipv (sort values %IP) { 2057 | next unless( $ipv->{ENABLED} ); 2058 | 2059 | printf STDERR ( "\nIPv%s configuration:\n------------------\n", $ipv->{VERSION} ); 2060 | 2061 | my $iw = 0; 2062 | $iw = length > $iw? length: $iw foreach keys %$ipv; 2063 | 2064 | foreach my $item (sort keys %$ipv) { 2065 | next if( $item eq 'VERSION' ); 2066 | printf STDERR ( " %-*s: %s", $iw, $item, 2067 | (defined( $ipv->{$item} )? $ipv->{$item}: 'undef') ); 2068 | if( $item =~ /^table_/ ) { 2069 | if( -r $ipv->{$item} && open( my $f, '<', $ipv->{$item} ) ) { 2070 | local $/; 2071 | my %h = map { $_ => 1 } split( /\s+/, <$f> ); 2072 | local $Text::Wrap::columns = 80; 2073 | local $Text::Wrap::unexpand = 0; 2074 | local $Text::Wrap::huge = 'wrap'; 2075 | 2076 | my $leader = ' ' x (2+$iw+2 + length( (defined( $ipv->{$item} )? $ipv->{$item}: 'undef' ) ) + 4); 2077 | my $wrapped = Text::Wrap::wrap( $leader, $leader, join( ', ', sort keys %h ) ); 2078 | $wrapped =~ s/\A$leader/ => /; 2079 | print STDERR ( $wrapped ); 2080 | close( $f ) or ( printf STDERR ( "%s: %s\n", $ipv->{$item}, $! ) ); 2081 | } 2082 | } elsif( $item eq 'IPT' ) { 2083 | if( open( my $ipt, '-|', $ipv->{IPT} . " --version 2>&1" ) ) { 2084 | { 2085 | local $/; 2086 | $_ = <$ipt>; 2087 | } 2088 | if( defined ) { 2089 | chomp; 2090 | printf STDERR ( " (%s)", $_ ); 2091 | } 2092 | close( $ipt ) or printf STDERR ( "%s: %s", $ipv->{IPT}, pCloseError() ); 2093 | } else { 2094 | printf STDERR ( " %s", $! ); 2095 | } 2096 | } 2097 | print STDERR ( "\n" ); 2098 | } 2099 | } 2100 | printf STDERR ( "\n" ); 2101 | } 2102 | 2103 | my $rc = eval { 2104 | if( $cmd eq 'status' ) { 2105 | return !status(); 2106 | } 2107 | if( $cmd eq 'list' ) { 2108 | return !list(); 2109 | } 2110 | if( $cmd eq 'intercepts' ) { 2111 | return !intercepts(); 2112 | } 2113 | if( $cmd =~ /^(?:--)?help$/ ) { 2114 | Usage(); 2115 | return 0; 2116 | } 2117 | if( $cmd =~ /^(?:--)?version$/ ) { 2118 | print( "$prog version $VERSION\n" ); 2119 | return 0; 2120 | } 2121 | 2122 | if( $debug && is_tty $stderr ) { 2123 | printf( "**** COMMAND WILL NOT BE EXECUTED ****\nLarge output is expected, please add%s %u>file.%s to capture\ ****\n", 2124 | ($zipavail? ' -z': ''), fileno( STDERR ), ($zipavail? 'zip' : 'log') ); 2125 | $debug = 0; 2126 | return 1; 2127 | } 2128 | 2129 | eval { 2130 | openlog( $prog, 'pid', 'daemon' ); 2131 | my $pos = 0; 2132 | my $line = ''; 2133 | foreach my $arg (@argv) { 2134 | $pos += ($pos? 1:0) + length $arg; 2135 | if( $pos > 80 ) { 2136 | syslog( "info", $line ) if( length $line ); 2137 | $pos = length $arg; 2138 | $line = ''; 2139 | } 2140 | $line .= ' ' . $arg; 2141 | } 2142 | syslog( "info", $line ) if( length $line ); 2143 | closelog; 2144 | } if( $syslog ); 2145 | 2146 | if( $cmd eq 'start' ) { 2147 | return !start(); 2148 | } 2149 | if( $cmd eq 'stop' ) { 2150 | return !stop(); 2151 | } 2152 | if( $cmd eq 'restart' ) { 2153 | return !restart(); 2154 | } 2155 | if( $cmd eq 'condrestart' ) { 2156 | return !(running && restart()); 2157 | } 2158 | print( "Usage: $prog (start|stop|restart|condrestart|status|list|intercepts|config|help|version)\n" ); 2159 | return 1; 2160 | }; if( $@ ) { 2161 | print STDERR ( $@ ); 2162 | failure; 2163 | } 2164 | exit( $rc || 0 ); 2165 | 2166 | # IO Package to enable logging 2167 | # 2168 | # tie( my $fh, 'BC::IO', $debug, openmode, @openargs ) or die $!; 2169 | # 2170 | # If $debug is true, everything read or written is logged to a temp file, which is 2171 | # printed on STDERR when $fh is closed or destroyed. 2172 | 2173 | package BC::IO; 2174 | 2175 | use warnings; 2176 | use strict; 2177 | 2178 | use overload( q("") => \&path ); 2179 | 2180 | use Carp; 2181 | 2182 | sub TIESCALAR { 2183 | my $class = shift; 2184 | 2185 | require Symbol; 2186 | 2187 | my $self = Symbol::gensym(); 2188 | 2189 | bless( $self, $class ); 2190 | 2191 | $class .= '::Handle'; 2192 | 2193 | $$$self = tie( *$$self, $class, @_ ) or return; 2194 | 2195 | return $self; 2196 | } 2197 | 2198 | sub path { 2199 | my $self = shift; 2200 | 2201 | return $$$self->path; 2202 | } 2203 | 2204 | sub line { 2205 | my $self = shift; 2206 | 2207 | return $$$self->line( @_ ); 2208 | } 2209 | 2210 | sub log { 2211 | my $self = shift; 2212 | 2213 | return $$$self->log( @_ ); 2214 | } 2215 | 2216 | sub FETCH { 2217 | my $self = shift; 2218 | 2219 | return \*$$self; 2220 | } 2221 | 2222 | sub STORE { 2223 | croak( "Store into read-only handle\n" ); 2224 | ${ $_[0] } = $_[1]; 2225 | } 2226 | 2227 | package BC::IO::Handle; 2228 | 2229 | use warnings; 2230 | use strict; 2231 | 2232 | use overload( q("") => \&path ); 2233 | 2234 | use Carp; 2235 | use Fcntl qw(:seek); 2236 | 2237 | sub TIEHANDLE { 2238 | my $class = shift; 2239 | 2240 | my $debug = shift; 2241 | my $mode = shift; 2242 | 2243 | require Symbol; 2244 | 2245 | my $self = Symbol::gensym(); 2246 | 2247 | bless( $self, $class ); 2248 | 2249 | if( $debug ) { 2250 | require File::Temp; 2251 | 2252 | $$self->{tmp} = File::Temp->new( UNLINK => 1 ); 2253 | 2254 | seek( STDERR, 0, SEEK_CUR ) or croak( "stderr seek: $!\n" ); 2255 | $$self->{stdp} = tell( STDERR ); 2256 | } 2257 | 2258 | open( $self, $mode, @_ ) or return; 2259 | 2260 | $$self->{writing} = 1 if( $mode =~ />|(?:^|)/ ); 2261 | 2262 | $mode =~ s/^(?:\+?[<>]{1,2})|(?:\|-?)//; 2263 | $mode =~ s/^\s+//; 2264 | $mode =~ s/\s+(?:\|-?)?$//; 2265 | $mode = $_[0] unless( length $mode ); 2266 | $$self->{file} = $mode; 2267 | 2268 | return $self; 2269 | } 2270 | 2271 | sub path { 2272 | my $self = shift; 2273 | 2274 | return $$self->{file}; 2275 | } 2276 | 2277 | sub OPEN { 2278 | my $self = shift; 2279 | 2280 | my $mode = shift; 2281 | 2282 | $self->CLOSE; 2283 | 2284 | if( exists $$self->{tmp} ) { 2285 | seek( STDERR, 0, SEEK_CUR ) or croak( "stderr seek: $!\n" ); 2286 | } 2287 | 2288 | open( $self, $mode, @_ ) or return; 2289 | 2290 | $$self->{writing} = 1 if( $mode =~ />|(?:^|)/ ); 2291 | 2292 | $mode =~ s/^(?:\+?[<>]{1,2})|(?:\|-?)//; 2293 | $mode =~ s/^\s+//; 2294 | $mode =~ s/\s+(?:\|-?)?$//; 2295 | $mode = $_[0] unless( length $mode ); 2296 | $$self->{file} = $mode; 2297 | 2298 | return 1; 2299 | } 2300 | 2301 | sub BINMODE { 2302 | my $self = shift; 2303 | my $io = shift || ':raw'; 2304 | 2305 | binmode $$self->{tmp}, $io if( exists $$self->{tmp} ); 2306 | 2307 | return binmode( $self, $io ); 2308 | } 2309 | 2310 | sub FILENO { 2311 | my $self = shift; 2312 | 2313 | return fileno( $self ); 2314 | } 2315 | 2316 | sub PRINT { 2317 | my $self = shift; 2318 | 2319 | croak( "Write to read-only handle\n" ) unless( $$self->{writing} ); 2320 | 2321 | print { $$self->{tmp} } ( @_ ) if( exists $$self->{tmp} ); 2322 | 2323 | $$self->{'.'} += join( '', @_ ) =~ tr/\n/\n/; 2324 | return 1 if( print $self ( @_ ) ); 2325 | 2326 | $$self->{'e.'} = $$self->{'.'} unless( $$self->{'e.'} ); 2327 | return; 2328 | } 2329 | 2330 | sub line { 2331 | my $self = shift; 2332 | 2333 | return (\*$$self)->input_line_number unless( $$self->{writing} ); 2334 | 2335 | return $$self->{'e.'} || 0; 2336 | } 2337 | 2338 | sub log { 2339 | my $self = shift; 2340 | my $fmt = shift; 2341 | 2342 | return printf { $$self->{tmp} } ( $fmt, @_ ) if( exists $$self->{tmp} ); 2343 | 2344 | return 1; 2345 | } 2346 | 2347 | sub PRINTF { 2348 | my $self = shift; 2349 | my $fmt = shift; 2350 | 2351 | return $self->PRINT( sprintf( $fmt, @_ ) ); 2352 | } 2353 | 2354 | sub WRITE { 2355 | my $self = shift; 2356 | 2357 | croak( "Write to read-only handle\n" ) unless( $$self->{writing} ); 2358 | 2359 | $$self->{'.'} += $_[0] =~ tr/\n/\n/; 2360 | 2361 | my $rc = syswrite( $self, @_ ); # buf, len, offset 2362 | return $rc if( $rc ); 2363 | 2364 | $$self->{'e.'} = $$self->{'.'} unless( $$self->{'e.'} ); 2365 | return $rc; 2366 | } 2367 | 2368 | sub READ { 2369 | my( $self, undef, $length, $offset ) = @_; 2370 | 2371 | croak( "Can't read from " . ref $self . "\n" ) if( $$self->{writing} ); 2372 | 2373 | my $buf = \$_[1]; 2374 | my $n = read( $self, $$buf, $length, $offset ); 2375 | 2376 | print { $$self->{tmp} } ( $$buf ) if( exists $$self->{tmp} ); 2377 | return $n; 2378 | } 2379 | 2380 | sub READLINE { 2381 | my $self = shift; 2382 | 2383 | croak( "Can't read from " . ref $self . "\n" ) if( $$self->{writing} ); 2384 | return <$self>; 2385 | } 2386 | 2387 | sub GETC { 2388 | my $self = shift; 2389 | 2390 | croak( "Can't read from " . ref $self . "\n" ) if( $$self->{writing} ); 2391 | my $c = getc( $self ); 2392 | print { $$self->{tmp} } ( $c ) if( exists $$self->{tmp} ); 2393 | return $c; 2394 | } 2395 | 2396 | sub SEEK { 2397 | my $self = shift; 2398 | my( $offset, $whence ) = @_; 2399 | 2400 | croak( "Can't seek on " . ref $self . "\n" ); 2401 | return seek( $self, $offset, $whence ); 2402 | } 2403 | 2404 | sub TELL { 2405 | my $self = shift; 2406 | 2407 | return tell( $self ); 2408 | } 2409 | 2410 | sub EOF { 2411 | my $self = shift; 2412 | 2413 | return eof( $self ); 2414 | } 2415 | 2416 | sub CLOSE { 2417 | my $self = shift; 2418 | 2419 | my $rc = close( $self ); 2420 | 2421 | if( exists $$self->{tmp} ) { 2422 | local $!; 2423 | local $?; 2424 | 2425 | my $output; 2426 | 2427 | seek( STDERR, 0, SEEK_CUR ) or croak( "stderr seek: $!\n" ); 2428 | if( (my $stp = $$self->{stdp}) != tell( STDERR ) ) { # subprocess wrote to STDERR, collect output 2429 | seek( STDERR, $stp, SEEK_SET ) or croak( "stderr seek: $!\n" ); 2430 | local $/; 2431 | $output = ; 2432 | seek( STDERR, $stp, SEEK_SET ) or croak( "stderr seek: $!\n" ); 2433 | } 2434 | my $tmp = $$self->{tmp}; # Copy spooled output to STDERR spool file 2435 | seek( $tmp, 0, SEEK_SET ); 2436 | 2437 | print STDERR ( $_ ) while( <$tmp> ); 2438 | 2439 | seek( $tmp, 0, SEEK_SET ); 2440 | truncate( $tmp, 0 ) or croak( "Failed to truncate tmpfile: $!\n" ); 2441 | 2442 | if( defined $output ) { # Program output follows 2443 | print $output; 2444 | printf STDERR ( "===== %s =====\n%s=====%s=====\n", 2445 | $self->path, $output, '=' x (2 + length( $self->path )) ); 2446 | } 2447 | 2448 | seek( STDERR, 0, SEEK_CUR ) or croak( "seek: $!\n" ); 2449 | $$self->{stdp} = tell( STDERR ); 2450 | } 2451 | return $rc; 2452 | } 2453 | 2454 | sub DESTROY { 2455 | my $self = shift; 2456 | 2457 | $self->CLOSE; 2458 | } 2459 | 2460 | __END__ 2461 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010,2012 Timothe Litt, litt__at__acm_dot_org 2 | All rights reserved. 3 | 4 | This software is licensed under the terms of the Perl 5 | Artistic License (see http://dev.perl.org/licenses/artistic.html). 6 | 7 | This is free software - it works for me, and it may (or may not) 8 | work for you. No warranty or support is provided. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 13 | THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 14 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 15 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | Except as contained in this notice, the name of the author shall not be 18 | used in advertising or otherwise to promote the sale, use or other dealings 19 | in this Software without prior written authorization from the author. 20 | 21 | Consider carefully whether you want to use this software 22 | and the full consequences to your site and/or business. 23 | 24 | This is written as a technical means to assist in implementing 25 | your policy. The author expressly disclaims any responsibility 26 | for the consequences of using this software. 27 | 28 | The above copyright notice, cautions and this permission notice shall 29 | be included in all copies or substantial portions of the Software. 30 | 31 | -------------------------------------------------------------------------------- /README.bcinstall: -------------------------------------------------------------------------------- 1 | bcinstall is an installation aid for BlockCountries. 2 | 3 | To run: 4 | bcinstall 5 | 6 | This script will determine if perl is installed on your system 7 | and will determine whether all the required library modules are installed. 8 | 9 | Simply download the script, make it executable, and run it. 10 | (No parameters are required.) 11 | 12 | If it says 'All checks completed successfully', BlockCountries 13 | should run. 14 | 15 | To see the installed versions of the modules that it checks for, use 16 | bcinstall -v 17 | 18 | If you are not going to filter IPv4: 19 | bcinstall -F 20 | 21 | If you are not going to filter IPv6 22 | bcinstall -S 23 | 24 | If it says that you need to install perl, it should be packaged for your 25 | distribution (apt-get, yum, etc.) If you can’t find a current perl version 26 | for your distribution (which would be very, very surprising) you can find 27 | it at http://www.perl.org. 28 | 29 | If it says that you need to install a perl module, here is how to do this: 30 | 31 | Most distributions provide all of the modules as packages, using yum, apt-get, etc. 32 | 33 | If yours doesn't provide modules or doesn't have one that this needs, use 34 | cpan. bcinstall will provide guidance. 35 | 36 | Install the module(s), then run bcinstall again. Repeat until bcinstall reports 37 | that you have all the needed modules. Usually, this is all that is 38 | necessary. 39 | 40 | Note that some of these modules are shipped with the Perl core distribution. 41 | The others are on CPAN, though it's better to use the ones packaged by your 42 | distribution, as multiple update managers can confuse each-other. 43 | 44 | The library modules may depend on certain versions of Perl. This is handled 45 | by the distribution update managers. With CPAN, if you are running an older 46 | version of Perl, the latest version of a module may fail to install. In 47 | this case, you can either have CPAN install an older compatible version of 48 | the module, or update Perl. 49 | 50 | Versions of Perl and library modules: 51 | ===================================== 52 | 53 | bcinstall checks the version of Perl installed on your system. 54 | 55 | BlockCountries is known to run on Perl as early as 5.8.8, and as recent 56 | as 5.12.4. It should run on more recent releases; file bugs at 57 | http://github.com/tlhackque/BlockCountries/issues if not. 58 | 59 | 60 | bcinstall also checks the version of each library module. The 61 | minimums are fairly conservative. However, in many cases earlier 62 | versions have bugs that break BlockCountries. 63 | 64 | As newer versions often fix bugs, updating is encouraged. 65 | If you determine that a newer version of a library module is required by 66 | or breaks BlockCountries, please file a bug report. 67 | 68 | Caveat: 69 | ====== 70 | 71 | If Perl is used by other software on your system, that software may 72 | depend on specific versions of Perl or Perl library modules. Although 73 | BlockCountries should not impose incompatible dependencies, managing 74 | these is beyond the scope of this document. 75 | 76 | iptables 77 | ======== 78 | 79 | bcinstall also checks for a minimum version of iptables. 80 | 81 | BlockCountries requires at least a version that supports the -g [goto] 82 | action, but there are many bugs in versions later than that. 83 | 84 | bcinstall does several checks, but only reports its conclusion. 85 | 86 | use -v to see the detail. 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BlockCountries 2 | ================ 3 | 4 | iptables manager to block by country 5 | 6 | Copyright (C) 2010, 2012, 2013, 2015, 2016 Timothe Litt 7 | 8 | BlockCountries generates iptables and ipv6tables 9 | that allow blocking IP traffic based on the country 10 | that the IP address is assigned to by the issuing NIC. 11 | 12 | This may or may not be where the traffic actually originates 13 | (Tunnels, proxies, etc), but it ususally is. It has proven 14 | effective for me. 15 | 16 | It supports both IPv4 and IPv6 addresses. It is highly 17 | configurable, allowing you to block a country but allow 18 | access to some ports, or to only block all countries except 19 | those that you wish to permit. There are other options. 20 | 21 | BlockCountries obtains the IP address data from the NICs. 22 | 23 | The generated iptables are optimized for chain length, 24 | generating subchains when it is advantageous. 25 | 26 | BlockCountries can (and should) be run as a cron job to get 27 | updates to the IP address allocations, and as an initscript 28 | at system startup. 29 | 30 | If configured to log rejected connections, it will produce 31 | some simple reports of the intercepts by host IP, showing the 32 | country, protocols, ports and number of attempts. 33 | 34 | It is a Perl script. 35 | 36 | #Latest updates: 37 | 38 | V2.22 Correct script permissions in git 39 | 40 | V2.21 Provide additional detail when unable to get data from RIR 41 | 42 | V2.20 Include zone data summary in debug log. 43 | 44 | V2.19 Log iptables version in debug data. Correct IPv6 error in 2.18. 45 | 46 | V2.18 Debug determinisim to make comparing logs easier. 47 | 48 | V2.17 Add -noprotect & some more debugging improvements. 49 | 50 | V2.16 more bcinstall improvements. Only inspect protocol config if enabled. 51 | 52 | V2.15 bcinstall improvements. iptables-restore format debug data. 53 | 54 | V2.14 Cosmetic logging improvements 55 | 56 | V2.13 Add Zip compression to debug log. Log start and stop to syslog. 57 | 58 | V2.12 Rewrite debug logging and gather more data for debugging. 59 | 60 | V2.11 Improve debug logging. `bcinstall` reports all problems at once, provides more detail. 61 | 62 | V2.10 Add Default-Start and Default-Stop to LSB block 63 | 64 | V2.9 Simplify configuration so it's not necessary to modify the script. 65 | Improve documentation and packaging. `bcinstall` checks for minimum 66 | versions and iptables configuration. 67 | 68 | V2.8 Add $LOGLEVEL to control syslog priority 69 | 70 | V2.7 Add LSB init block 71 | 72 | V2.6 Add version command. Syntax cleanup. Corner case fix for help. 73 | 74 | V2.5 Fix uninitialized variable when no command arguments specified. 75 | 76 | V2.4 Documentation updates, no functional changes. 77 | * Clarify -permitonly and rules placement/priorities 78 | 79 | V2.3 Make conntrack optional - older netfilters are still around 80 | * Use -conntrack if you see warnings (or errors) 81 | * Add -passive to use passive FTP connection when getting registry data. This usually traverses firewalls more easily. 82 | 83 | V2.2 Support conntrack instead of state 84 | 85 | V2.1 Support new statistics file used by registries 86 | 87 | #Dependencies: 88 | The script uses a number of Perl library modules, available 89 | from CPAN (or in the base Perl distribution). The 90 | `bcinstall` bash script should be used whenever a new version of 91 | `BlockCountries` is installed to determine whether they (and Perl) 92 | are installed. 93 | 94 | `bcinstall` will also verify that the minimum required version of each 95 | module is installed. Later versions should be used if available. 96 | 97 | There are known bugs that impact BlockCountries in earlier versions 98 | of several modules. Don't try to use any version less than the 99 | minimum that bcinstall checks for. 100 | 101 | #Cautions: 102 | 103 | Consider carefully whether you want to use this software and the full consequences 104 | to your site and/or business. By necessity, it will block potential customers and 105 | 'good' connections along with villains. You must consider the costs and benefits 106 | to your operation - the author does not endorse any specific policy. In particular, 107 | the defaults should be viewed as examples, not value judgements. 108 | 109 | The IPv6 optimizations have not been tuned; it is not clear what the right 110 | subchain structure is. 111 | 112 | #Installation: 113 | 114 | Unpack the `tar.gz` or `zip` file that you downloaded from github. 115 | (`tar.gz` files are available from https://github.com/tlhackque/BlockCountries/releases) 116 | 117 | This will create a subdirectory named BlockCountries-. 118 | 119 | cd to that directory. 120 | 121 | Run `bcinstall` (see its README) to check that Perl and its 122 | dependencies are installed and meet the minimum version requirements. 123 | 124 | Copy BlockCountries to `/etc/init.d` (or your distributions startup directory) 125 | run `chkconfig`, `systemctl enable`, `update-rc.d` (or equivalent) to 126 | include it in the automatic system startup. 127 | 128 | If Perl is not installed in `/usr/bin`, create a softlink to it there, 129 | `bcinstall` will provide guidance. 130 | 131 | On Unix-like systems, 132 | 133 | which perl 134 | 135 | will usually reveal the location. 136 | 137 | The file `config/BlockCountries` should be copied to your distribution's 138 | configuration directory, such as `/etc/sysconfig` or `/etc/default`. 139 | 140 | BlockCountries looks for the configuration file in several common directories, 141 | Use BlockCountries help for a list. 142 | 143 | If you must place the configuration file in an alternate location, define 144 | the environment variable `BlockCountriesCFG` in all the environments where 145 | BlockCountries will be run. (cron, startup, and interactive) 146 | This is the first file attempted. 147 | 148 | Inspect the first few lines of the configuration file and modify the variables 149 | in Section 1 for your environment. 150 | 151 | Verify that BlockCountries finds your file by using `BlockCountries config` 152 | 153 | Add a crontab entry similar to: 154 | ```` 155 | CRONJOB=1 156 | 11 23 * * Sun /etc/init.d/BlockCountries start -update 157 | ```` 158 | Please pick a different time. Note that IP address allocations are fairly 159 | stable; updating more than once a week is rarely productive. 160 | 161 | 162 | Run 163 | ```` 164 | BlockCountries start -update 165 | ```` 166 | 167 | The filter should download the latest IP country assignments and be in place. 168 | 169 | 170 | #Usage: 171 | 172 | The usage as of V2.4 follows (the words prefixed by $ are filled 173 | in at runtime). Later versions differ - the definitive version, 174 | which is customized for your system, is available from: 175 | ```` 176 | $prog help 177 | ```` 178 | 179 | The following usage provides an introduction to BlockCountries. 180 | 181 | Please do not use this version of the usage to configure BlockCountries 182 | for production; use the version from the `help` command, which is 183 | always up-to-date with the latest functions and clarifications. 184 | 185 | ```` 186 | IP filter manager for country filters 187 | 188 | Usage: $prog command args 189 | config Display active configuration file 190 | status [-v] Display filter status. 191 | -v provides configuration from config file 192 | and command file - NOT iptables. 193 | list List available country names/codes. 194 | Contacts server for list. 195 | intercepts [-host name] [-days n] 196 | List today's intercepts by host from $LOG. 197 | Requires -log to start. 198 | stop args Stop filtering 199 | restart args Synonym for start (reloads with no open window) 200 | condrestart args Restarts only if already running 201 | start args Starts filter 202 | 203 | Start uses tables of IP blocks assigned to country codes that are 204 | stored in $ZONEDIR, which will be created if necessary. The country 205 | files are binary, and are only created for country codes that are 206 | filtered. The data is obtained from the Regional Internet Registries 207 | when absent, or when start -update is specified. 208 | 209 | iptables filters are generated and installed by start. The filters are 210 | optimized and generally will not look identical to the input data. However 211 | they will match the same address (no more and no fewer.) 212 | 213 | Arguments for start-class commands are: 214 | -update Get latest IP allocation data from regional registries. 215 | Otherwise, only gets data if no local file exists for a registry. 216 | -4 -ipv4 Install filter for IPv4 addresses \ Default is -4 override with -no4 -no6 217 | -6 -ipv6 Install filter for IPv6 addresses / For both, use -4 -6 218 | -conntrack Use -m conntrack rather than -m state to avoid warnings from newer iptables 219 | -log Install a logging rule to log rejected packets. 220 | -nolog Don't install a logging rule. (default) 221 | -nolimit Do not limit logging (can generate huge log files if under attack; not advised) 222 | -limit spec Limit logging, default = $loglimits[0]:$loglimits[1] (see man iptables "limit") 223 | -atport n Allow connections to TCP port n even FROM banned addresses. 224 | May specify any number of times. May use a service name. 225 | -auport n Allow connections to UDP port n even FROM banned addresses. 226 | May specify any number of times. May use a service name. 227 | -atporto n Same as -atport, but for connections TO banned addresses. 228 | -auporto n Same as -auport, but for connections TO banned addresses. 229 | -aip ip(/mask) Allow connection from an otherwise banned IP address. 230 | For a block, specify a netlength or mask. A hostname may 231 | also be specified. May specify any number of times. 232 | -dip ip(/mask) Deny connections from an otherwise allowed IP address. 233 | Same syntax as -aip. 234 | -passive When using FTP for updates, use passive mode (traverse firewalls) 235 | -permitonly Listed countries will be permited, all others denied. 236 | Put in -permitonly in the configuration file so status and start are consistent. 237 | -blockout Also generate rules to block output & forwarded-output. 238 | This is probably not required for most applications, and 239 | will roughly double the memory requirements. 240 | Caution: If you use -blockout for start, you must also use it for stop. 241 | This will not be a problem if it's in the configuration file. 242 | -d Output random debugging messages 243 | -v Output extended status/statistics 244 | CC ISO Country code or name to ban (or permit if -permitonly). 245 | Specify as many as you like. 246 | Default list: 247 | 248 | Arguments may also be obtained from the configuration file, 249 | The configuration file must be readable but not 250 | executable. It is searched for in a number of places. See 251 | BlockCountries help for a list. The first file found is used: 252 | 253 | The system configuration is specified by the variables: 254 | -logfile "/var/log/messages*" # Wildcard should include rotated/archived/.gz compressed files 255 | # containing iptables log messages 256 | -loglevel number or name # syslog message priority. Normally omitted. System-dependent. 257 | # Usually 0-6 (or as names, EMERGENCY, ALERT, CRITICAL,ERROR, 258 | # WARNING,NOTICE,INFORMATIONAL, or DEBUG) 259 | -logpfx '[Blocked CC]:' # Prefix used for log messages from BlockedCountries 260 | -logpgm 'kernel' # Program name used by netfilter. Always 'kernel'. 261 | -path '/sbin' # Path for iptables components 262 | -zonedir '/root/blockips' # Directory used to hold IP mapping data 263 | 264 | The defaults are indicated above. These variables can NOT be specified on the command line. 265 | 266 | Anything else (except comments) contained in it is prepended to every command 267 | line's arguments. 268 | 269 | Use single or double-quoted strings for country names containining spaces. 270 | 271 | This script is designed to run as a service; depending on your distribution 272 | `chkconfig`, `update-rc.d`, or `system-ctl enable` will link it into 273 | `/etc/rcN.d/.` Be sure to put all configuration in the configuration file, 274 | since startup scripts only get "start" (or "stop") as an argument. 275 | 276 | This script should also be run with start -update from a cron job - weekly 277 | is suggested - to obtain the latest IP address databases. If the CRONJOB 278 | environment variable is set, only errors and zone updates will be reported. 279 | This is recomended to minimize e-mail from cron. 280 | 281 | To prevent communications from banned IP addresses during updates, 282 | start will install new rules before removing active rules. For 283 | this to be effective, you should not stop the service. 284 | 285 | The status -v command will report the current configuration, although the 286 | actual implementation in iptables will be different due to optimizations. 287 | 288 | Rules are applied to the INPUT, OUTPUT or FORWARD chains after any existing 289 | IPTABLES rules. Typically, this means that related and established connections 290 | are accepted before BlockCountries rules are applied, and icmp packets are 291 | allowed. (This varies by distribution and local option.) To control 292 | placement, define the corresponding -HOOK chain.(e.g. INPUT-HOOK) and call it 293 | it from the main chain(s). BlockCountries rules will be installed in 294 | the -HOOK chain instead of the main chain. 295 | 296 | When manual configuration directives are used, the rules are applied in the 297 | following order (first match determines result): 298 | -atports (or -atportso) are accepted 299 | -auports (or -auportso) are accepted 300 | -aips are accepted 301 | -dips are denied 302 | Country constraints deny or accept (per -permitonly) 303 | IPTABLES rules later in the caller's chain 304 | If a BlockCountries rule denies a connection, it will be dropped (and logged). 305 | If a BlockCountries rule "accepts" a connection, it is still subject to any 306 | IPTABLES rules that follow it. 307 | 308 | To view the actual iptables configuration, use 309 | $IPT4 -nvL --line-number | less for IPV4 or 310 | $IPT6 -nvL --line-number | less for IPV6 311 | 312 | start -v will provide some statistics for the optimizations and generated rules. 313 | 314 | The intercepts command will parse $LOG and summarize intercepts by IP address. 315 | It will break down the dropped packets by protocol(s) and port(s). Of course, 316 | logging must be on for this to work. -days specifys how many days back 317 | (from the current time) to read. -host specifies the hostname to match. 318 | Default -days is 1, hostname is current host. 319 | 320 | Credits: 321 | Some ideas came from http://www.cyberciti.biz/faq/block-entier-country-using-iptables/. 322 | 323 | This version of the script merges all the IP address blocks; this saves over 1,000 324 | rules for the default banned address list. It's also somewhat faster than a shell 325 | script, and contains a more complete and polished user and system interface. It is 326 | not a complete superset; at this time it does not implement file archiving (better 327 | left to your backup solution) nor does it implement interface selection (better done 328 | with your own firewall rules and placement of the *-HOOK chains.) 329 | 330 | Issues: 331 | Consider carefully whether you want to use this software and the full consequences 332 | to your site and/or business. By necessity, it will block potential customers and 333 | 'good' connections along with villains. You must consider the costs and benefits 334 | to your operation - the author does not endorse any specific policy. In particular, 335 | the defaults should be viewed as examples, not value judgements. 336 | 337 | Very large numbers of exception IP blocks might benefit from implementing a subchain 338 | structure - but that would be a rather different use model. The known use cases 339 | would probably be penalized - so one would want to make a dynamic choice. I'd 340 | want to see actual data before implementing this. 341 | 342 | The iptables-restore format is undocumented, though used by others. It may be 343 | fragile. 344 | 345 | --tlhackque 1-Aug-2010, 8-Nov-2010, 3-Oct-2012, 4-Sep-2013, 17-Dec-2015, 5-Feb-2016 346 | ```` 347 | 348 | #Bug reports and suggestions 349 | 350 | Please raise bug reports or suggestions at http://github.com/tlhackque/BlockCountries/issues. 351 | 352 | Always include `BlockCountries version` `BlockCountries config`, and `perl --version`. 353 | If there is any error or warning message, include the full terminal session. 354 | 355 | Suggestions and/or praise are also welcome. 356 | -------------------------------------------------------------------------------- /bcinstall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (C) 2016 Timothe Litt litt _at_ acm _ddot_ org 4 | 5 | BCVERSION="2.97" 6 | 7 | # Minimum version of iptables 8 | # 9 | IPTV=1.4.7 10 | 11 | # Width 12 | W=25 13 | 14 | prog="`basename $0`" 15 | export V= 16 | IP6=1 17 | IP4=1 18 | while getopts ":hHFSv" opt; do 19 | case $opt in 20 | v) 21 | V=1 22 | ;; 23 | F) 24 | IP4= 25 | ;; 26 | S) 27 | IP6= 28 | ;; 29 | h|H) 30 | cat >&2 <&2 72 | echo "Usage: $prog -h for usage" 73 | exit 1 74 | ;; 75 | esac 76 | done 77 | 78 | echo "$prog version $BCVERSION" 79 | echo "" 80 | 81 | if ! which perl > /dev/null 2>&1 ; then 82 | echo "You need to install Perl, either from your distribution or see https://www.perl.org" 83 | echo "After installing Perl, run $prog again" 84 | exit 1 85 | fi 86 | 87 | ppath="`which perl`" 88 | export ppath="`dirname $ppath`" 89 | perl -e'printf "Perl version %vd was found in %s\n", $^V, $ENV{ppath}' 90 | 91 | function needs { 92 | local m="$1" 93 | local v="$2" 94 | local VW=8 95 | 96 | if perl -M"$m $v" -e'exit;' >/dev/null 2>&1 ; then 97 | if [ -n "$v" ] && [ -n "$V" ]; then 98 | perl -M"$m" -e"printf \" %-${W}s v%-${VW}s installed\n\", \"$m\", \$$m::VERSION;exit;" 99 | fi 100 | else 101 | if [ -z "$v" ]; then 102 | printf " %-${W}s v%-${VW}s *OUTDATED Must upgrade to v$m or higher\n" "Perl" "$m" 103 | OK= 104 | NEWPERL="$m" 105 | return 1 106 | fi 107 | if perl -M"$m" -e'exit;' >/dev/null 2>&1 ; then 108 | perl -M$m -e"printf \" %-${W}s v%-${VW}s *OUTDATED Must upgrade to v$v or higher\n\", \"$m\", \$$m::VERSION;exit;" 109 | OK= 110 | CPAN="$CPAN $m" 111 | return 1 112 | fi 113 | printf " %-${W}s %-${VW}s *NOT INSTALLED Must install v$v or higher\n" $m "" 114 | OK= 115 | CPAN="$CPAN $m" 116 | return 1 117 | fi 118 | return 0 119 | } 120 | 121 | OK=1 122 | CPAN= 123 | NEWPERL= 124 | 125 | echo "" 126 | echo " Checking Perl module status" 127 | echo "" 128 | 129 | # Update with BlockCountries needs -modules 130 | 131 | needs 5.8.8 # Perl version 132 | needs Carp 1.3301 133 | needs Cwd 3.33 134 | needs Errno 1.0901 135 | needs Fcntl 1.05 136 | needs File::Basename 2.74 137 | needs File::Path 2.08 138 | needs File::Temp 0.2304 139 | needs IO::Compress::Zip 2.069 140 | needs IO::Uncompress::Gunzip 2.064 141 | needs LWP::Simple 6.00 142 | needs Locale::Country 3.37 143 | needs Net::Domain 2.23 144 | needs Net::IP 1.25 145 | needs NetAddr::IP 4.044 146 | needs POSIX 1.09 147 | needs Parse::Syslog 1.10 148 | needs Regexp::IPv6 0.03 149 | needs Socket 2.006 150 | needs Storable 2.30 151 | needs Symbol 1.06 152 | needs Sys::Syslog 0.29 153 | needs Text::ParseWords 3.27 154 | needs Text::Wrap 2005.082401 155 | 156 | if [ -n "$V" ]; then 157 | echo 158 | fi 159 | 160 | if [ -n "$OK" ]; then 161 | echo "All needed perl modules are installed and meet minimum version requirements" 162 | else 163 | echo "" 164 | if [ -n "$NEWPERL" ]; then 165 | echo "Perl $NEWPERL must be installed from your distribution, or https://www.perl.org" 166 | echo "After upgrading Perl, run $prog again." 167 | exit 1 168 | fi 169 | echo " If Perl was installed as a package, you should get the *modules from your package distribution if possible." 170 | echo " For Linux distributions, you would use apt-get, yum install or equivalent" 171 | echo " If Perl was installed from source, or your package distribution doesn't have these modules, get them from CPAN." 172 | if [ -n"$CPAN" ]; then 173 | if which cpan > /dev/null 2>&1 ; then 174 | echo " If you need to get them from CPAN, the cpan install command is:" 175 | echo "" 176 | echo " cpan install$CPAN" 177 | echo "" 178 | echo " You can also visit https://search.cpan.org to download them manually." 179 | else 180 | echo " The cpan command is not installed. Visit https://search.cpan.org to download them manually." 181 | fi 182 | fi 183 | fi 184 | 185 | if [ "`readlink -f $ppath/perl`" != "`readlink -f /usr/bin/perl`" ]; then 186 | echo "" 187 | echo "You must create a symbolic link from /usr/bin/perl to `readlink -f $ppath/perl`" 188 | fi 189 | 190 | echo "" 191 | 192 | W=17 193 | VW=13 194 | 195 | IOK=1 196 | 197 | function checkver { 198 | local PGM="$1" 199 | local VER="$2" 200 | local REQ="$3" 201 | local DIR="$4" 202 | 203 | [ "$VF"="Unknown" ] && VF="$VER" 204 | if [ "$VER" != "$VF" ]; then 205 | echo "$PGM version $VER doesn't match $VF" 206 | IOK= 207 | return 1; 208 | fi 209 | perl - < \$r; 222 | next unless( \$c ); 223 | last; 224 | } 225 | if( \$c > 0 || \$c == 0 && @ver >= @req ) { 226 | printf " %-${W}s %-${VW}s in %-*s\n", \$ARGV[0], \$ARGV[1], \$ENV{PW}, \$ARGV[3] if( \$ENV{V} ); 227 | exit 0; 228 | } 229 | printf " %-${W}s %-${VW}s in %-*s *OUTDATED Must upgrade to v%s or higher\n", \$ARGV[0], \$ARGV[1], \$ENV{PW}, \$ARGV[3], \$ARGV[2]; 230 | exit 1; 231 | EOF 232 | IOK= 233 | return 1; 234 | } 235 | 236 | path= 237 | patha= 238 | pgma= 239 | VF="Unknown" 240 | export PW=20 241 | ip4tables="iptables iptables-restore iptables-save" 242 | ip6tables="ip6tables ip6tables-restore ip6tables-save" 243 | if [ -z "$IP6" ]; then 244 | ip6tables= 245 | fi 246 | if [ -z "$IP4" ]; then 247 | ip4tables= 248 | fi 249 | 250 | [ -n "$IP4$IP6" ] && echo "Checking iptables installation" 251 | 252 | for ipt in $ip4tables $ip6tables; do 253 | 254 | if ! which $ipt >/dev/null 2>&1 ; then 255 | printf " %-${W}s %-${VW}s %-${PW}s *NOT INSTALLED\n" $ipt "" "" 256 | IOK= 257 | else 258 | path="`which $ipt`" 259 | path="`dirname $path`" 260 | if [ -z "$patha" ]; then 261 | patha="$path" 262 | PW=${#patha} 263 | pgma="$ipt" 264 | fi 265 | if [[ "$ipt" =~ ip.?tables-save ]]; then # ip*tables-save has no --version, but writes in data 266 | checkver `$ipt | perl -e'$_=<>; s/^#\s+Generated\sby\s(\S+)\s+(\S*).*$/$1 $2/; print;'` $IPTV $path 267 | else 268 | if [[ "$ipt" =~ ip.?tables-restore ]]; then # ip*tables-restore doesn't have --version. Can't extract. 269 | [ -n "$V" ] && printf " %-${W}s %-${VW}s in %-${PW}s\n" $ipt "" $path 270 | else 271 | checkver `$ipt --version` $IPTV $path 272 | fi 273 | fi 274 | if [ "$path" != "$patha" ]; then 275 | echo "$ipt must be in the same directory ($patha) as $pgma" 276 | IOK= 277 | fi 278 | fi 279 | done 280 | 281 | echo "" 282 | if [ -z "$IOK" ]; then 283 | echo "iptables must be installed or upgraded. Minimum version is $IPTV" 284 | fi 285 | 286 | [ -z "$IP4$IP6" ] && echo "*** iptables installation not checked ***" || ( [ -n "$IOK" ] && echo "iptables version $VF installation validated" ) 287 | 288 | if [ -n "$patha" ] && [ "$patha" != '/sbin' ]; then 289 | echo "" 290 | echo "*** Be sure to include \"-path $patha\" in your configuration file" 291 | fi 292 | 293 | [ -z "$IOK" ] && OK= 294 | 295 | # Extract config search list from BlockCountries & report 296 | 297 | BC="`dirname $0`/BlockCountries" 298 | if [ -f "$BC" ]; then 299 | echo "" 300 | echo "Inspecting configuration of $BC" 301 | perl < \$maxd? length dirname \$_ : \$maxd) foreach( 'Directory/x', 'xENVxBlockCountriesCFGx/x', @CFGFILE ); 313 | my \$maxf = 0; 314 | \$maxf = ((length basename \$_) > \$maxf? length basename \$_ : \$maxf) foreach( 'x/File', @CFGFILE ); 315 | \$maxf = length( \$ENV{BlockCountriesCFG} ) if( defined \$ENV{BlockCountriesCFG} && length( \$ENV{BlockCountriesCFG} ) > \$maxf ); 316 | print ("\n Configuration file search list:\n"); 317 | 318 | printf( " %-*s %-*s Status\n", \$maxd, "Directory", \$maxf, "File" ); 319 | printf( " %-*s %-*s %s\n", \$maxd, '\$ENV{BlockCountriesCFG}', \$maxf, (\$ENV{BlockCountriesCFG} || ''), 320 | (\$ENV{BlockCountriesCFG} && -f \$ENV{BlockCountriesCFG})? '[Exists]': 321 | (\$ENV{BlockCountriesCFG} && -d dirname( \$ENV{BlockCountriesCFG} )? '[Directory exists]':'[Not present]' ) ); 322 | 323 | foreach my \$file (@CFGFILE) { 324 | my \$dir = dirname \$file; 325 | my \$rt = ''; 326 | printf(" %-*s ", \$maxd, \$dir ); 327 | if( \$dir =~ /^[.]/ ) { 328 | printf( "%-*s [Not checked, runtime value unknown]\n", \$maxf, '' ); 329 | next; 330 | } 331 | if( \$dir =~ /^[~]/ ) { 332 | \$dir =~ s,^[~],\$ENV{HOME} || (getpwuid(\$<))[7],e; 333 | \$file = "\$dir/" . basename \$file; 334 | \$rt = ", assuming runtime \$dir" 335 | 336 | } 337 | if( -f \$file && ! -x \$file ) { 338 | printf( "%-*s [Exists\$rt]\n", \$maxf, basename( \$file ) ); 339 | } elsif( -d \$dir ) { 340 | printf( "%-*s [Directory exists\$rt]\n", \$maxf, '' ); 341 | } else { 342 | printf( "%-*s [Not present\$rt]\n", \$maxf, '' ); 343 | } 344 | } 345 | print( "\n The first file found will be used at runtime.\n Use BlockCountries config to determine which file is selected.\n" ); 346 | exit; 347 | EOF 348 | else 349 | echo "BlockCountries not found in `dirname $0`" 350 | fi 351 | 352 | echo "" 353 | if [ -n "$OK" ]; then 354 | echo "All checks completed successfully by $prog v$BCVERSION" 355 | 356 | exit 0 357 | fi 358 | echo "*** $prog: BlockCountries requirements v$BCVERSION are **not** satisfied" 359 | exit 1 360 | -------------------------------------------------------------------------------- /config/BlockCountries: -------------------------------------------------------------------------------- 1 | # Configuration for BlockCountries service 2 | # 3 | # This file is a sample configuration provided to illustrate syntax. 4 | # 5 | # It is not a recommendation for which countries to block. 6 | 7 | # ******** Section 1 ******** 8 | # System configuration variables 9 | # Note: These are not valid on the command line 10 | # 11 | # Sylog file where log messages can be found. This does NOT control where they are 12 | # placed - see the man page for syslog, or for more powerful placement options, rsyslogd 13 | # 14 | # This is a wildcard specification that should include any rotated/archived logfiles. 15 | # .gz files can be processed. 16 | # 17 | -logfile "/var/log/messages*" 18 | 19 | # Directory where country/zone data is stored. Must be writable, will be created if missing. 20 | # 21 | -zonedir '/root/blockips' 22 | 23 | # *** Remainder of this section rarely needs to be changed *** 24 | 25 | # Path to iptables command 26 | 27 | -path /sbin 28 | 29 | # Prefix used for log messages from BlockCountries 30 | # Can be used to filter log file with other tools 31 | # 32 | -logpfx '[Blocked CC]:' 33 | 34 | # Log level (priority) - should be left blank, but some people attempt to filter syslog 35 | # by priority. The values are system-dependent, but usually: 0-6 (or as names, 36 | # EMERGENCY, ALERT, CRITICAL,ERROR,WARNING,NOTICE,INFORMATIONAL, or DEBUG) 37 | 38 | -loglevel '' 39 | 40 | # Program name used by netfilter to write log messages. In all known cases, this is 'kernel'. 41 | 42 | -logpgm 'kernel' 43 | 44 | # ******** Section 2 ******** 45 | 46 | # Countries 47 | # 48 | # The list of country codes and names can be obtained from BlockCountries with: 49 | # 50 | # BlockCountries list 51 | # 52 | # This lists both ISO code and name for documentation (and as insurance against changes in the name) 53 | # It is not necessary to specify both. The following are equivalent: 54 | # 55 | # lt Lithunia 56 | # Lithunia 57 | # lt 58 | 59 | lt Lithuania 60 | md "Moldova, Republic of" 61 | 62 | # ******** Section 3 ******** 63 | 64 | # Exceptions 65 | 66 | # Allow inbound mail, which requires DNS 67 | # 68 | 69 | -atport smtp -atport submission -atport smtps -atport domain 70 | -auport domain 71 | 72 | # ******** Section 4 ******** 73 | 74 | # Miscellaneous 75 | 76 | # Filter both IPV4 and IPV6 77 | 78 | -ipv4 -ipv6 79 | 80 | # Enable logging 81 | 82 | -log 83 | 84 | # Use passive mode FTP when retrieving data 85 | -passive 86 | 87 | # Log start/stop to syslog 88 | -syslog 89 | --------------------------------------------------------------------------------