Documentation exists for the old MaxMind GeoIP databases but I found myself needing to add some RFC1918 addresses into the DB for a logstash configuration. This was a bit of a pain so I figured I’d share for others who want to do the same. This script is setup to use the MaxMind::DB::Reader::XS and MaxMind::DB::Writer::Tree perl modules. (I gave up perl 20 years ago. Every time you think you’re out, they pull you back in!) Anyway, the XS module requires you to build the libmaxminddb library on your machine but you can probably get away with the standard reader. Anything you put into the custom_ranges hash will get inserted or updated in the default DB. This script takes a while to run (23min on my machine) but so far so good. Consider it beta. YMMV.

 

#!/usr/bin/env perl

use strict;
use warnings;
use feature qw( say );
use local::lib 'local';

use MaxMind::DB::Reader::XS;
use MaxMind::DB::Writer::Tree;
use Net::Works::Network;
use Data::Dumper;
use LWP::Simple;
use Archive::Tar;

my $download = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz';
my $local = 'GeoLite2-City.mmdb';
my $output = '/etc/logstash/GeoLite2-Custom.mmdb';
my $tar = Archive::Tar->new;

# Check if local downlaod is fresh
if(-e $local) { my @lstat = stat($local); }
if((! -e $local) || ($lstat[9] + (60*60*24*6) < time))
{
  say "Getting updated database file...";
  getstore($download, $local . ".tgz");
  $tar->read($local . '.tgz');
  for my $target (grep(/GeoLite2-City.mmdb$/, $tar->list_files()))
  {
    $tar->extract_file( $target, $local );
  }


}

my $reader   = MaxMind::DB::Reader->new( file => $local);

my %types = (
    names                  => 'map',
    city                   => 'map',
    continent              => 'map',
    registered_country     => 'map',
    represented_country    => 'map',
    country                => 'map',
    location               => 'map',
    postal                 => 'map',
    traits                 => 'map',

    geoname_id             => 'uint32',

    type                   => 'utf8_string',
    en                     => 'utf8_string',
    de                     => 'utf8_string',
    es                     => 'utf8_string',
    fr                     => 'utf8_string',
    ja                     => 'utf8_string',
    'pt-BR'                => 'utf8_string',
    ru                     => 'utf8_string',
    'zh-CN'                => 'utf8_string',

    locales                => [ 'array', 'utf8_string' ],
    code                   => 'utf8_string',
    geoname_id             => 'uint32',
    ip_address             => 'utf8_string',
    subdivisions           => [ 'array' , 'map' ],
    iso_code               => 'utf8_string',
    environments           => [ 'array', 'utf8_string' ],
    expires                => 'uint32',
    name                   => 'utf8_string',
    time_zone              => 'utf8_string',
    accuracy_radius        => 'uint32',
    latitude               => 'float',
    longitude              => 'float',
    metro_code             => 'uint32',
    time_zone              => 'utf8_string',
    is_in_european_union   => 'utf8_string',
    is_satellite_provider   => 'utf8_string',
    is_anonymous_proxy     => 'utf8_string',
);


my $tree = MaxMind::DB::Writer::Tree->new(

    database_type => 'GeoLite2-City',
    description => { en => 'GeoLite2 City database' },
    ip_version => 4,
    map_key_type_callback => sub { $types{ $_[0] } },
    merge_strategy => 'recurse',
    record_size => 28,
    remove_reserved_networks => 0,
);

$reader->iterate_search_tree(
  sub {
        my $ip_as_integer = shift;
        my $mask_length   = shift;
        my $data          = shift;
        my $net_address;

        if ($ip_as_integer > 2**32-1) {
          return;
        }

        my $address = Net::Works::Address->new_from_integer( integer => $ip_as_integer );
        $net_address = join '/', $address->as_ipv4_string, $mask_length - 96;

        if($mask_length > 127) { return; }

        #say join '/', $address->as_ipv4_string, $mask_length - 96;
        #say Dumper($data);

        $tree->insert_network( $net_address, $data );
  }
);


my %custom_ranges = (

    '172.16.0.0/12' => {
      city => {
        geoname_id => 4704482,
        names => {
          en => "Dallas",
        },
      },
      continent => {
        code => "NA",
        geoname_id => 6255149,
        names => {
          en => "North America",
        },
      },
      country => {
        geoname_id => 6252001,
        iso_code => "US",
        names => {
          en => "United States",
        },
      },
      location => {
        accuracy_radius => 2,
        latitude => 32.776706,
        longitude => -96.805047,
        metro_code => 623,
        time_zone => "America/Chicago",
      },
      postal => {
        code => 75202,
      },
      registered_country => {
        geoname_id => 6252001,
        iso_code => "US",
        names => {
          en => "United States",
        },
      },
      subdivisions => [ {
        geoname_id => 4736286,
        iso_code => "TX",
        names => {
          en => "Texas",
        },
      } ],
    },

);


for my $range ( keys %custom_ranges ) {
  my $metadata = $custom_ranges{$range};
  my $network = Net::Works::Network->new_from_string ( string => $range );
  $tree->insert_network($network, $metadata);
}


open my $fh, '>:raw', $output;
$tree->write_tree( $fh );
close $fh;