Géolocalisation, construire ses fichiers DAT et MMDB

tbowan

14 Avril 2020

Parce que, parfois, on préfèrerait utiliser ses propres bases de géolocalisation, mais qu’il n’y pas vraiment d’outils tout prêts pour ça, les arsouyes vous partagent leurs scripts.

Lorsqu’on utilise des applications ou des librairies toutes faites, on est tributaire des choix de leurs développeurs. La géolocalisation n’y fait pas exception et si vous voulez que ces outils fonctionnent, vous devez leur fournir les bases de données dans le bon format.

Ce marché est en fait dominé par une seule société, Maxmind, et ses deux formats de fichier qu’elle a mis au point, l’ancien dat et le nouveau mmdb. Si l’outil que vous utilisez fait de la géolocalisation, il aura besoin de fichier dans un de ces deux formats.

Le problème, c’est que pour produire les fichiers les plus précis possibles, Maxmind et ses concurrents ne peuvent se contenter des données publiques. Ils doivent donc collecter et corréler des données personnelles. Bien sûr, ils ne communiquent pas sur leurs méthodes mais lorsqu’une application demande votre position, vous avez maintenant une idée des clients potentiels.

Si, comme nous, vous avez configuré un bloqueur de publicité DNS, leur domaine est en liste noire. Pour accéder à leurs sites webs, vous devez donc d’ajouter une exception à votre pare-feu.

Si vous ne voulez pas être impliqué dans cette aspiration de données, vous devez donc vous passer de leurs services et construire vos propres fichiers. Vous perdrez en précision (en obtenant le pays et plus la ville) mais vous gagnerez notre respect. Tout bien considéré, vous y gagnez.

Illustration de PIRO4D

Après vous avoir montré que ces données sont publiques puis fourni un script en PHP qui les récupère et fourni une première base sqlite, on va voir aujourd’hui comment produire les fichiers dans ces deux formats classiques.

Note aux devops. Vous pourrez aussi utiliser ces méthodes pour produire des fichiers avec des données artificielles (i.e. des adresses privée ou carrément des pays n’existant pas). Fichiers utiles en tests ou en pré-production.

Format DAT

Ou plutôt GeoIP legacy, il s’agit du premier format de fichier pour stocker les données de géolocalisation mis au point par MaxMind. Du fait de son grand âge, ce format est disponible nativement dans plein de langages (C, Java, Perl, PHP, … cf. liste officielle). L’inconvénient c’est que les différentes données (i.e. pays, villes, fournisseurs d’accès) sont dans des fichiers séparés.

Techniquement, ce format modélise l’ensemble des données comme un immense arbre binaire, dont chaque nœud correspond à un réseau et les deux fils, aux deux sous-réseaux immédiats. Les feuilles stockant l’information de géolocalisation (pays, ville, fournisseur d’accès, …) suivant les cas.

Arbre binaire, illustration de MagicTree

Pour ce format, Maxmind a fourni des outils tout prêts dont certains en ligne de commande, pour construire ses propres base et faire des recherches. Ils sont disponibles dans n’importe quelle distribution Linux via le paquet geoip-bin. Pour Ubuntu et Debian, la commande suivante vous installera ce dont vous avez besoin.

sudo apt-get install geoip-bin

Construire un CSV

Les données mises à disposition par les Registres Internet Regionnaux sont déjà au format CSV mais les champs ne sont pas compatibles avec les outils de Maxmind. Il faut donc construire un nouveau fichier avec les bons champs, dans le bon ordre…

Cette information n’est pas documentée mais vous pouvez la reconstruire. Soit en regardant les CSV publiés par Maxmind, soit en regardant le code source des binaires correspondants (il y a un enum dont les éléments ont des noms évocateurs).

Si vous voulez faire votre propre fichier, voici l’ordre des champs des fichiers CSV, seuls trois d’entre eux seront utiles lors de la conversion :

  1. Première adresse du réseau,
  2. Dernière adresse du réseau,
  3. Première adresse (en entier 32 bits) du réseau, inutilisé
  4. Dernière adresse (en entier 32 bits) du réseau, inutilisé
  5. Code du pays,
  6. Nom du pays, inutilisé.

Notes aux devops : Si vous voulez utiliser les scripts suivants pour construires vos bases de tests, vous pouvez partir de fichiers csv remplis avec vos jeux de tests.

Si vous utilisez notre code PHP, vous pouvez ajouter la fonction suivante à votre classe pour exporter les réseaux IP version 4 :

public function toCSV4() {
    $st = $this->pdo->prepare("select * from IPv4") ;
    $st->execute() ;

    foreach ($st as $row) {
        echo long2ip($row["start"]) . "," ;
        echo long2ip($row["end"]) . "," ;
        echo "," ;
        echo "," ;
        echo $row["country"] . "," ;
        echo "\n" ;
    }
}

La version 6 d’IP est similaire mais comme on va le voir, le format dat ne le gère pas bien. Je vous montrerai donc comment faire un csv pour IP version 6 plus loin dans l’article.

Pour créer le CSV, vous pouvez maintenant appeler la fonction dans un script autonome de ce genre qui ouvre la base passée en premier argument et écrit, sur la sortie standard, le contenu au format CSV :

#!/usr/bin/env php
<?php

include "Database.php" ;

$db = new Database($argv[1]) ;
$db->toCSV4() ;

Construire un DAT

Maintenant qu’on a un CSV dans le bon ordre, on va pouvoir le passer au convertisseur de Maxmind. Il s’agit d’un des binaires du paquet qu’on a installé précédement. Comme, par défaut, il n’est pas dans le PATH, vous devrez l’appeler avec son adresse absolue (ou modifier le PATH mais c’est moins bien) :

/usr/lib/geoip/geoip-generator -4 -v -o geoip_database.dat geoip_database.csv 

Pour des adresse IP version 6, il faudrait changer l’option -4 en -6. L’option -v vous donne un peu d’informations sur le déroulement, facultative donc mais je la trouve utile.

Tourisme : ce format étant conçu pour être efficace dans la recherche et le stockage, il prend bien moins de place que la version CSV ; 2 Mo pour le dat contre 11 Mo pour le csv.

Vérifier et utiliser

Pour vérifier le contenu de la base, ou l’utiliser en ligne de commande, vous pouvez utiliser un autre des binaires disponibles, geoiplookup qui effectue une recherche dans la base de votre choix. Ce binaire, par contre, est dans le PATH et vous pouvez l’invoquer par son nom.

$ geoiplookup -f geoip_database.dat 188.165.53.185
GeoIP Country Edition: FR, France

Vous remarquerez que la recherche a trouvé toute seule que le code FR correspond à la France sans que j’ai besoin d’utiliser une table de correspondance.

IP version 6

Il semble que ce format ne soit pas adapté à IP Version 6… Vous pouvez produire un fichier csv avec des données IP version 6 (y compris des IP version 4 dans les sous réseaux dédiés) puis convertir le tout avec geoip-generator (et l’option -6). Le problème viendra lors de la recherche…

D’un côté l’outil geoiplookup a un bogue que personne ne compte corriger (l’outil considère les adresses en version 6 comme des noms d’hôtes et n’arrive pas à les résoudre).

De l’autre, la librarie PHP officielle n’arrive pas à faire des recherche dans ces fichiers. Les IP Version 4 ne trouvent rien. Les IP version 6 génèrent une erreur fatale suivante :

GeoIP API: Error traversing database - perhaps it is corrupt?

Si le support d’IP en version 6 est important, vous devriez donc passer au format mmdb.

Format MMDB

Ou plutôt GeoIP2, il s’agit du deuxième format mis au point par MaxMind pour combler les lacunes de son prédécesseur. Ce format permet ainsi de stocker n’importe quelle donnée structurée correspondant à un réseau. Maxmind diffuse ses fichiers avec ses données de géolocalisation mais le format peut en fait accueillir vos propres structures.

Cette fois, Maxmind n’a pas pris la peine de développer toute une ribambelle de librairies et d’outils. Les spécifications son disponibles et seuls les langages les plus courants disposent d’un lecteur. Charge aux développeurs d’ajouter leurs propres librairies pour leurs langages favoris.

La mode ayant évolué, Maxmind met également à disposition des web services. Vous n’avez alors plus besoin de mettre à jours vos fichiers mais il faudrait payer un abonnement.

La mode change, logo de Maxmind

CSV pour IP version 6

Si vous n’utilisez que des adresses IP en version 4, le csv précédent fera l’affaire. Sinon, voici comment exporter le contenu des bases IP version 4 et version 6 dans un csv unique que vous pourrez ensuite transformer en mmdb

Encore une fois, nous allons compléter la classe avec quelques fonctions.

La première fonction nous permet de convertir les entiers stockés dans la table en adresse IP version 6 lisible.

public static function bigInt2Ip6($ip) {
    $bin = pack("J2", $ip, 0) ;
    return inet_ntop($bin) ;
}

La seconde va écrire les lignes csv pour les adresses IP version 6. Je ne remplis que les champs utiles, les autres sont donc volontairement laissés vides.

public function toCSV6() {
    $st = $this->pdo->prepare("select * from IPv6") ;
    $st->execute() ;

    foreach ($st as $row) {
        echo self::bigInt2Ip6($row["start"])  . "," ;
        echo self::bigInt2Ip6($row["end"])    . "," ;
        echo "," ;
        echo "," ;
        echo $row["country"] . "," ;
        echo "\n" ;
    }
}

On pourrait s’arrêter là mais pour produire une base contenant la géolocalisation de toutes les adresses IP dans un seul fichier, on va devoir traduire les vieilles adresses IP version 4 dans le format des IP version 6. Et il y a justement des sous réseaux prévus pour ça.

  1. ::0:0/96 qui est le sous réseau compatible IPv4. C’est à dire des adresses qui ont la même valeur entière dans les deux réseaux. Ce sous-réseau est obsolète et devrait être remplacé par le suivant (mais comme certaines libraries l’utilisent encore, on est parfois obligé de le gérer aussi).
  2. ::ffff:0:0/96 qui est le sous réseau officiel pour représenter les adresses IP version 4. Ces adresses peuvent être manipulées dans les logiciels mais ne sont pas routables.
public function toCSV4as6() {
    $st = $this->pdo->prepare("select * from IPv4") ;
    $st->execute() ;

    foreach ($st as $row) {
        echo "::" . long2ip($row["start"])  . "," ;
        echo "::" . long2ip($row["end"])    . "," ;
        echo "," ;
        echo "," ;
        echo $row["country"] . "," ;
        echo "\n" ;
        
        echo "::ffff:" . long2ip($row["start"])  . "," ;
        echo "::ffff:" . long2ip($row["end"])    . "," ;
        echo "," ;
        echo "," ;
        echo $row["country"] . "," ;
        echo "\n" ;
    }
}

Tourisme : Normalement, les adresses IP en version 6 s’écrivent en hexadécimal. Mais par soucis de lisibilité, il est autorisé d’écrire les 32 derniers bits comme on le fait avec les adresses IP version 4 (des nombres entre 0 et 255 séparés par des points). J’utilise cette convention ici pour simplifier l’écriture.

Avec ces trois nouvelles méthodes, on peut écrire un script pour convertir la base sqlite en csv :

#!/usr/bin/env php
<?php

include "Database.php" ;

$db = new Database($argv[1]) ;
$db->toCSV4as6() ;
$db->toCSV6() ;

Construire un mmdb

La seule librairie disponible pour écrire vos propres fichiers étant en Perl, c’est donc vers ce langage qu’on va maintenant se tourner. Afin d’utiliser des libraries tierces, on initialise cpan.

cpan

Comme je n’ai pas de particularité, je laisse tout les choix par défaut. Une fois le processus terminé, il faut relancer votre terminal pour que tout ait été pris en compte (c’est d’ailleurs explicitement dit à la fin).

Pour créer un fichier mmdb, vous avez besoin de la librairie développée par Maxmind, Maxmind::DB::Writer. Elle s’installe via cpan via la ligne suivante :

cpan install MaxMind::DB::Writer

Plutôt que de vous mettre le script entier d’un coup, je vais vous le présenter au fur et à mesure. On commence donc par les inclusions classiques dans tout code :

#!/usr/bin/env perl
use strict;
use warnings;
use MaxMind::DB::Writer::Tree;

Il est ensuite nécessaire de déclarer les types de données qui vont être insérées dans l’arbre de recherche. Vous pouvez voir ça un peut comme un schéma dans une base NoSQL. Je reviendrai plus tard sur ces champs et leurs signification.

my %types = (
    country  => 'map',
    iso_code => 'utf8_string',
    names    => 'map',
    fr       => 'utf8_string',
    is_in_european_union => 'boolean',
);

On peut alors créer un objet qui stockera l’arbre de recherche et les données. Ici, il ne s’agit que de méta informations qui spécialise en quelque sorte la base.

my $tree = MaxMind::DB::Writer::Tree->new(
    database_type => 'Country',
    languages => ['fr'],
    description =>
        { fr => 'Geolocalisation IPv4', },
    ip_version => 6,
    record_size => 24,
    # Typage
    map_key_type_callback => sub { $types{ $_[0] } },
);

Une fois l’objet créé, on peut s’occuper du CSV, son parcours et l’insertion des données dans l’arbre. C’est ici que la liste des champs et le typage prend son sens. Techniquement, vous pouvez stocker les structures que vous voulez dans l’arbre. Le format mmdb est d’ailleurs conçu pour ça et vos lecteur devront simplement être compatibles avec votre base.

Comme le but est de faire une base compatible avec les outils, il faut que nos données soient formatées comme les bases officielles. Ici, pas de documentation non plus, j’ai du rétro concevoir les libraries (en PHP si vous voulez savoir) :

my $csv = $ARGV[0] or die "Need 2 arguments" ;
open(my $data, '<', $csv) or die "Could not open file $csv\n" ;

while (my $line = <$data>) {
    chomp $line ;
    my @fields = split "," , $line;

    my $record = {
        country => {
            iso_code => $fields[4],
            is_in_european_union => 1,
            names => {
                fr => $fields[4],
            },
        },
    } ;
    $tree->insert_range($fields[0], $fields[1], $record ) ;
}

Une fois tous les enregistrements insérés, il ne reste plus qu’à sauvegarder le fichier, l’étape la plus facile.

my $filename = $ARGV[1] or die "Need 2 arguments" ;

# Write the database to disk.
open my $fh, '>:raw', $filename;
$tree->write_tree( $fh );
close $fh;

Avec ce script, on peut récupérer la base csv exportée précédement et la convertir en mmdb :

csv2mmdb.pl geoip_database.csv geoip_database.mmdb

Vérifier en Perl

Comme nous n’avons pas d’outil tout prêt, on va continuer avec des scripts perl. Cette fois, la librairie nécessaire est différence, MaxMind::DB::Reader.

Notez que si vous avez géjà installé le Writer, le Reader est en fait déjà disponible.

cpan install MaxMind::DB::Reader

Cette fois, le code est bien plus simple, il suffit d’importer les modules, ouvrir le fichier, faire une recherche et afficher les données.

#!/usr/bin/env perl

use strict;
use warnings;

use MaxMind::DB::Reader;

my $filename = shift @ARGV or die "Usage: $0 <file> [ip_address]";
my $reader = MaxMind::DB::Reader->new( file => $filename );

foreach my $ip (@ARGV) {
    my $record = $reader->record_for_address( $ip );
    print $ip . " => " . $record->{country}->{iso_code} . "\n" ;
}

On peut alors faire les recherche en ligne de commande :

./search.pl geoip.mmdb 188.165.53.185 ::ffff:188.165.53.185 2001:41d0:301::21
188.165.53.185 => FR
::ffff:188.165.53.185 => FR
2001:41d0:301::21 => FR

Vérifier en PHP

L’intérêt étant de créer des bases compatibles, je vous montre ici un autre script de recherche, en PHP, utilisant GeoIP2, l’api officielle de Maxmind.

Pour aller au plus simple, j’utilise cette API via composer et la ligne de commande suivante :

composer require geoip2/geoip2:~2.0

Je peux alors utiliser l’autoloader et les classes fournies par Maxmind. L’exemple suivant est inspiré de l’exemple officiel :

#!/usr/bin/env php
<?php

require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;

$reader = new Reader($argv[1], ['fr']);
for ($i = 2; $i < $argc; $i++) {
    $ip = $argv[$i] ;

    $record = $reader->country($ip);
    echo "$ip => " . $record->country->isoCode . "\n" ;
}

Cet exemple en PHP m’a permis de découvrir quelques subtilités du format mmdb et son utilisation pourla géolocaliation…

La méthode country utilisée en PHP pour chercher un enregistrement vérifie que le database_type renseigné en perl lors de la création de l’arbre contient la chaîne Country quelque part. Sinon, l’API PHP lance une exception car, d’après elle, il n’y a pas de donnée de pays dans votre fichier…

throw new \BadMethodCallException(
    "The $method method cannot be used to open a {$this->dbType} database"
);

Le nommage des champs change en fonction de l’API. En Perl (et à l’intérieur du fichier) leur nom est en snake_case avec des soulignés entre les mots. En PHP, leur nom est en camelCase, avec des majuscules à chaque mot… L’explication se trouve dans le code officiel :

    public function __get($attr)
    {
        // XXX - kind of ugly but greatly reduces boilerplate code
        $key = $this->attributeToKey($attr);
        // [...]
    }

    private function attributeToKey($attr)
    {
        return strtolower(preg_replace('/([A-Z])/', '_\1', $attr));
    }

Ces problèmes étant résolus, on peut utiliser le script pour obtenir le code du pays pour des adresses IP :

$ ./search.php geoip.mmdb 188.165.53.185 ::ffff:188.165.53.185 2001:41d0:301::21
188.165.53.185 => FR
::ffff:188.165.53.185 => FR
2001:41d0:301::21 => FR

Et après ?

Avec ces scripts, vous pouvez maintenant produire vos propres bases de données compatibles avec tous les outils qui font de la géolocalisation. Soit avec des données artificielles pour les phases de tests ou la préproduction pour vérifier que l’intégration s’est bien passée. Soit avec les données publiques pour géolocaliser vos utilisateurs tout en respectant leur vie privée.

Pour continuer sur la thématique, ces articles pourraient vous intéresser.

Anonymiser les adresses IP pour faire des statistiques

23 Mars 2020 Je ne sais pas vous, mais moi, j’adore faire des statistiques. Le problème, c’est lorsqu’on traite de données personnelles. Aujourd’hui, je vais vous expliquer pourquoi et comment anonymiser ces adresses.

RGPD, retour d’expérience
14 Octobre 2019 A force de passer des commandes ou de s’inscrire sur des sites web, nous sommes tous à la tête d’un nombre incalculable de comptes en ligne. Mais ça se passe comment lorsque l’on veut les supprimer ?
Statistiques web éthiques avec Goaccess

20 Avril 2020 Ce n’est pas parce qu’on veut mesurer son audience qu’on doit forcément utiliser des méthodes intrusives et peu respectueuse des visiteurs. Aujourd’hui on vous montre une solution avec goaccess.