Géolocalisation, construire ses fichiers DAT et MMDB
Divulgâchage : La plupart des logiciels lisant des données de géolocalisations ne sont compatibles qu’avec le format DAT ou MMDB ce qui vous impose de vous fournir chez l’éditeur du format (maxmind). Pour sortir du monopole, les arsouyes vous partagent leurs scripts de conversion pour produire vos propres fichiers.
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.
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.
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 :
- Première adresse du réseau,
- Dernière adresse du réseau,
- Première adresse (en entier 32 bits) du réseau, inutilisé
- Dernière adresse (en entier 32 bits) du réseau, inutilisé
- Code du pays,
- 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 uncsv
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 ledat
contre 11 Mo pour lecsv
.
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.
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.
::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).::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 = (
'map',
country => 'utf8_string',
iso_code => 'map',
names => 'utf8_string',
fr => 'boolean',
is_in_european_union => );
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.
- database_type : est une chaîne qui dit ce qu’il y a
dans la base. Elle doit contenir
Country
(respectez la casse) pour pouvoir ensuite l’utiliser dans les libraries tierces (on y reviendra plus loin), - languages : donne la liste des langues supportées pour les noms des pays (parce qu’on peut insérer les noms traduits dans plusieurs langues), ça n’est utile que pour les lecteurs qui vérifieraient que le fichier est compatible.
- description : permet d’en dire plus sur la base, notez que c’est internationnalisé,
- ip_version : 4 ou 6 suivant le format de vos adresses,
- record_size : taille des enregistrements en bits, 24 ou 32, la doc n’est pas très claire : « vous pouvez mettre 128 mais les lecteurs n’acceptent que 24 ou 32 »
- mak_key_type_callback : la fonction qui déterminer comment stocker la données.
my $tree = MaxMind::DB::Writer::Tree->new(
'Country',
database_type => 'fr'],
languages => [
description =>'Geolocalisation IPv4', },
{ fr => 6,
ip_version => 24,
record_size => # Typage
sub { $types{ $_[0] } },
map_key_type_callback => );
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) :
country
contient les données de géolocalisation au niveau Pays.iso_code
est le code du pays à deux lettres, ISO 3166-1 alpha-2,is_in_european_union
n’est présent (et vaut vrai) que si le pays est dans l’Union Européenne. Ici, je met oui tout le temps pour vous montrer la syntaxe mais pour une vraie base, un petitif
serait nécessaire,names
contient une table où le nom du pays est traduit dans les langues, ici je n’ai mis que le français (fr
) et j’y ai mis le code pays, pour montrer la syntaxe,geoname_id
peut être utilisée pour référencer le pays avec son code dans la base geonames, mais pour rester simple, je ne l’ai pas géré du tout.
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 => {$fields[4],
iso_code => 1,
is_in_european_union =>
names => {$fields[4],
fr =>
},
},
} ;$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
, leReader
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
:188.165.53.185 => FR
::ffff2001: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.