Länder blocken mit GeoIP und iptables unter Ubuntu
Blocking countries using GeoIP and iptables on Ubuntu
Um meine Firewall Logs ein wenig zu filtern (und da die meisten Attacken nicht
aus Europa kommen) habe ich auf meinem Ubuntu mal die xtables addons installiert,
um nach geoip einen Vorfilter definieren zu können. Leider war es mit
aptitude install xtables-addons-common
nicht getan, denn die Lookup-Dateien
haben noch gefehlt. Nach ein wenig Googlen (Referenzen weiter unten angegeben)
ergab sich dieses Updatescript (falls es jemand brauchen kann):
Setup
Die zu kopierenden Skripte sind weiter unten gelistet.
To filter my firewall logs a bit (and as the most attacks don't come from European countries in my case) I used the xtables extension for iptables filtering on my Ubuntu 11.10 home server. Unfortunately the setup was not done by just installing xtables-addons-common with aptitude, as the country IP database was missing. After some googling (references given below) I implemented a quick and dirty shell script to download and update it:
Setup
The files to copy are listed below.
sudo aptitude install xtables-addons-common libtext-csv-xs-perl
sudo mkdir /usr/share/xt_geoip
sudo cp update.sh /usr/share/xt_geoip
sudo cp xt_geoip_build.pl /usr/share/xt_geoip
sudo . /usr/share/xt_geoip/update.sh
xt_geoip
├── BE
│ ├── A1.iv4
. . ...
│ ├── ZW.iv4
│ └── ZW.iv6
├── LE
│ ├── A1.iv4
. . ...
│ └── ZW.iv6
├── dl
│ ├── GeoIPCountryCSV.zip
│ └── GeoIPv6.csv.gz
├── update.sh
└── xt_geoip_build.pl
3 directories, 1466 files
Beispiel
Hier werden einfach alle Anfragen (alle Ports), die nicht aus Deutschland, Frankreich oder Großbritannien kommen, ignoriert. Damit es keine Probleme im LAN gibt (sollte eigentlich nicht sein), schieben wir noch ein accept fürs LAN an erste Stelle. Die UFW ist dadurch nicht beeinflusst, außer dass bereits vorgefilterte Anfragen reinkommen.
Example
Block all incoming traffic not coming from Germany, France or Great Britain. To prevent trouble during the setup, accept incoming from class C networks. We insert these rules at position 1 and 2, so that ufw filters are processed later on.
iptables -I INPUT 1 -s 192.168.0.0/24 -j ACCEPT
iptables -I INPUT 2 -m state --state NEW -m geoip ! --source-country DE,GB,FR -j DROP
Dateien
Der Pfad des xtables Addons ist hardcoded (/usr/share/xt_geoip
). Die Lookup-Dateien
müssen in den Unterverzeichnissen LE (little endian) und BE (big endian) stehen.
Also habe ich das Update-Script und ein Hilfsscript (perl) in /usr/share/xt_geoip
platziert (damit alles beisammen ist). Update.sh legt LE und BE an falls diese noch
nicht existieren. Auch ein Download-Verzeichnis dl
wird erstellt. Darin werden
die komprimierten CSV IP Tabellen gespeichert. Diese sollte man auch nicht löschen,
weil wget -N
die Dateien nur herunterläd falls sie sich geändert haben, das spart
den Jungs von www.maxmind.com Traffic. Das Perlsscript ist die Originaldatei aus
den xtables-addons Quellen und hier nur als Deko nochmals gelistet.
Files
The paths for the xtables addon are hardcoded to /usr/share/xt_geoip
. The lookup
files have to be in the directories /usr/share/xt_geoip/BE
(big endian) and
/usr/share/xt_geoip/LE
(little endian). So I placed the update script and an
auxiliary build script in the base directory. The update script will generate a
subdirectory dl
, and the output directories LE and BE if not existing yet.
The perl script is the original conversion script of the xtables-addons source
and just listed here for convenience.
update.sh
#/bin/sh
cd $(dirname $0) || exit 1;
BASE_DIR=$(pwd);
DL_DIR=$BASE_DIR/dl
mkdir -p $DL_DIR >/dev/null 2>&1
cd $DL_DIR || exit 1;
# Get data (only if modified)
wget -N http://geolite.maxmind.com/download/geoip/database/GeoIPv6.csv.gz || exit 1;
wget -N http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip || exit 1;
if [ -f GeoIPv6.csv.gz ]; then
rm GeoIPv6.csv >/dev/null 2>&1
gzip -dc GeoIPv6.csv.gz > GeoIPv6.csv || exit 1;
fi
if [ -f GeoIPCountryCSV.zip ]; then
rm GeoIPCountryWhois.csv >/dev/null 2>&1
unzip GeoIPCountryCSV.zip >/dev/null || exit 1;
fi
echo "Building ..."
cat *.csv | perl $BASE_DIR/xt_geoip_build.pl -D $DL_DIR || exit 1;
rm *.csv
echo "Copying to $BASE_DIR ..."
rm -rf $BASE_DIR/LE && mv -f $DL_DIR/LE $BASE_DIR/
rm -rf $BASE_DIR/BE && mv -f $DL_DIR/BE $BASE_DIR/
echo "Ready."
xt_geoip_build.pl
#!/usr/bin/perl
#
# Converter for MaxMind CSV database to binary, for xt_geoip
# Copyright © Jan Engelhardt <jengelh@medozas.de>, 2008-2011
#
# Use -b argument to create big-endian tables.
#
use Getopt::Long;
use IO::Handle;
use Text::CSV_XS; # or trade for Text::CSV
use strict;
my $csv = Text::CSV_XS->new({
allow_whitespace => 1,
binary => 1,
eol => $/,
}); # or Text::CSV
my $target_dir = ".";
&Getopt::Long::Configure(qw(bundling));
&GetOptions(
"D=s" => \$target_dir,
);
if (!-d $target_dir) {
print STDERR "Target directory $target_dir does not exist.\n";
exit 1;
}
foreach (qw(LE BE)) {
my $dir = "$target_dir/$_";
if (!-e $dir && !mkdir($dir)) {
print STDERR "Could not mkdir $dir: $!\n";
exit 1;
}
}
&dump(&collect());
sub collect
{
my %country;
while (my $row = $csv->getline(*ARGV)) {
if (!defined($country{$row->[4]})) {
$country{$row->[4]} = {
name => $row->[5],
pool_v4 => [],
pool_v6 => [],
};
}
my $c = $country{$row->[4]};
if ($row->[0] =~ /:/) {
push(@{$c->{pool_v6}},
[&ip6_pack($row->[0]), &ip6_pack($row->[1])]);
} else {
push(@{$c->{pool_v4}}, [$row->[2], $row->[3]]);
}
if ($. % 4096 == 0) {
print STDERR "\r\e[2K$. entries";
}
}
print STDERR "\r\e[2K$. entries total\n";
return \%country;
}
sub dump
{
my $country = shift @_;
foreach my $iso_code (sort keys %$country) {
&dump_one($iso_code, $country->{$iso_code});
}
}
sub dump_one
{
my($iso_code, $country) = @_;
my($file, $fh_le, $fh_be);
printf "%5u IPv6 ranges for %s %s\n",
scalar(@{$country->{pool_v6}}),
$iso_code, $country->{name};
$file = "$target_dir/LE/".uc($iso_code).".iv6";
if (!open($fh_le, "> $file")) {
print STDERR "Error opening $file: $!\n";
exit 1;
}
$file = "$target_dir/BE/".uc($iso_code).".iv6";
if (!open($fh_be, "> $file")) {
print STDERR "Error opening $file: $!\n";
exit 1;
}
foreach my $range (@{$country->{pool_v6}}) {
print $fh_be $range->[0], $range->[1];
print $fh_le &ip6_swap($range->[0]), &ip6_swap($range->[1]);
}
close $fh_le;
close $fh_be;
printf "%5u IPv4 ranges for %s %s\n",
scalar(@{$country->{pool_v4}}),
$iso_code, $country->{name};
$file = "$target_dir/LE/".uc($iso_code).".iv4";
if (!open($fh_le, "> $file")) {
print STDERR "Error opening $file: $!\n";
exit 1;
}
$file = "$target_dir/BE/".uc($iso_code).".iv4";
if (!open($fh_be, "> $file")) {
print STDERR "Error opening $file: $!\n";
exit 1;
}
foreach my $range (@{$country->{pool_v4}}) {
print $fh_le pack("VV", $range->[0], $range->[1]);
print $fh_be pack("NN", $range->[0], $range->[1]);
}
close $fh_le;
close $fh_be;
}
sub ip6_pack
{
my $addr = shift @_;
$addr =~ s{::}{:!:};
my @addr = split(/:/, $addr);
my @e = (0) x 8;
foreach (@addr) {
if ($_ eq "!") {
$_ = join(':', @e[0..(8-scalar(@addr))]);
}
}
@addr = split(/:/, join(':', @addr));
$_ = hex($_) foreach @addr;
return pack("n*", @addr);
}
sub ip6_swap
{
return pack("V*", unpack("N*", shift @_));
}