IP Info : géolocaliser une adresse IP
Géolocaliser une IP sans quota ni API externe : mon service maison lit la base MaxMind chez toi, en Docker ou en Lambda.
Savoir d’où vient une adresse IP, c’est un besoin qui revient sur presque tous mes projets. Et c’est à chaque fois le même casse-tête. J’ai fini par écrire mon propre service : la base de données est chez moi, et il n’y a plus de compteur.
Le besoin revient, la solution jamais
Ça fait des années que je tombe dessus. Sur ce blog, pour les statistiques : savoir de quel pays viennent les visiteurs, c’est une des rares informations vraiment utiles quand tu ne veux poser aucun cookie. Sur Teambudd.io, mon jeu, dont je vous parlerai une autre fois. Et sur à peu près tout ce que j’ai bricolé entre les deux.
À chaque fois, même réflexe : je cherche une API en ligne. Et à chaque fois, même mur. Ces services existent, ils marchent très bien, mais ils sont construits sur des quotas. Mille requêtes par jour, parfois moins, et au-delà tu passes à la caisse. Pour un site qui a un peu de trafic, mille requêtes c’est une matinée. Sans compter le détail qui me gêne le plus : tu envoies l’adresse de tes visiteurs à quelqu’un d’autre, et ce quelqu’un d’autre s’en fait une idée assez précise de qui vient chez toi.
Un seul appel externe, et il n’a lieu qu’une fois par jour
Alors j’ai fabriqué IP Info. L’idée tient dans une bascule toute simple : au lieu d’interroger un service à chaque adresse, tu télécharges la base une fois et tu la lis toi-même.
La base, c’est GeoLite2, publiée par MaxMind. Tu crées un compte, tu récupères une clé de licence, c’est gratuit. Tu poses la clé dans une variable d’environnement et le service fait le reste : il télécharge le fichier au démarrage, le range dans son volume, et va vérifier une fois par jour s’il y a une version plus fraîche. C’est ça, l’unique dépendance externe. Tout le reste tourne chez toi.
Un fichier .mmdb, c’est un arbre de recherche binaire. Le service le parcourt bit par bit et te répond en quelques microsecondes, sans serveur de base de données, sans réseau, sans rien. Autant dire que la notion de quota n’a plus de sens : tu peux lui envoyer un million de requêtes, c’est ta machine qui décide, pas une grille tarifaire.
Ce qu’il te répond
Et il ne te rend pas que le pays. C’est justement là que ça devient intéressant pour des statistiques :
curl https://ip.exemple.fr/81.2.69.200
{
"ip": "81.2.69.200",
"version": 4,
"type": "public",
"hostname": null,
"continent": "EU",
"continent_name": "Europe",
"country": "GB",
"country_name": "Royaume-Uni",
"country_is_eu": false,
"region": "England",
"region_code": "ENG",
"city": "Londres",
"postal": "OX1",
"latitude": 51.5142,
"longitude": -0.0931,
"accuracy_radius": 10,
"timezone": "Europe/London",
"network": "81.2.69.192/26",
"asn": 12345,
"as_name": "Test ISP Ltd",
"as_network": "81.2.69.0/24"
}
Le pays, la région, la ville, le fuseau horaire, les coordonnées, et même l’opérateur derrière l’adresse. Tous les champs sont toujours présents : une valeur inconnue vaut null, jamais une clé qui disparaît. Ça évite d’écrire du code défensif partout.
Quand tu n’as besoin que d’une seule information, tu la demandes seule et tu la reçois en texte brut. C’est le mode que j’utilise pour les statistiques du blog :
curl https://ip.exemple.fr/8.8.8.8/country
# US
curl 'https://ip.exemple.fr/81.2.69.200?fields=country,city&lang=fr'
# {"country":"GB","city":"Londres"}
Deux champs méritent qu’on s’y arrête. accuracy_radius donne le rayon en kilomètres dans lequel MaxMind situe l’adresse : sans lui, quatre décimales de latitude ressemblent à une adresse postale, ce qu’elles ne sont absolument pas. Et type dit ce que l’adresse raconte d’elle-même, public, private, loopback et compagnie. Une adresse privée n’est dans aucune base de géolocalisation, et le service te le dit au lieu de te rendre un objet vide à interpréter.
Une adresse toute seule, sans rien préciser, te renvoie celle de l’appelant. Pratique pour un « quelle est mon IP » maison.
Deux façons de le faire tourner
Comme pour Image Resizer, il y a deux chemins, et c’est le même code derrière.
Le premier, c’est une fonction Lambda serverless. Aucun serveur, aucune infra à surveiller, la facture suit strictement l’usage. Chaque version publie une archive zip prête à déposer, et une seule fonction suffit : pas de seconde fonction pour tenir la base à jour, pas de tâche planifiée. Le démarrage à froid télécharge le fichier, et chaque invocation vérifie son âge au passage. Le LAMBDA.md du dépôt déroule la marche à suivre.
Le second, c’est Docker, et donc Portainer. La configuration tient en quelques lignes :
services:
ip-info:
image: smeagolworms4/ip-info:latest
restart: unless-stopped
user: "${PUID:-1000}:${PGID:-1000}"
environment:
MAXMIND_LICENSE_KEY: "ta-clé-de-licence"
TRUST_PROXY: "true"
DEFAULT_LANGUAGE: "fr"
ports:
- "3000:3000"
volumes:
- ${DATA_DIR}/ip-info:/data
Le volume sur /data n’est pas décoratif : c’est là que les bases sont rangées. Sans lui, chaque redémarrage retélécharge 70 Mo, et MaxMind les décompte de ta clé.
Et si tu veux le joindre depuis l’extérieur, c’est le duo habituel : un nom de domaine avec Duck DNS, et un reverse proxy avec Nginx Proxy Manager devant. Une seule chose à ne pas oublier dans ce cas : TRUST_PROXY. Sans elle, le service voit l’adresse du proxy et te la renvoie à toi comme aux autres.
Les images couvrent amd64, arm64 et arm/v7, donc le même tag marche sur ton PC, ton NAS ou ton Raspberry Pi.
Une clé secrète, pour que les petits malins aillent voir ailleurs
Le jour où ton service est exposé sur Internet, rien n’empêche quelqu’un de s’en servir gratuitement à ta place. Là encore c’est le même mécanisme que sur Image Resizer : tu poses une clé secrète, et le service ne répond plus qu’aux adresses correctement signées.
SIGNATURE_KEY=ta-clé-secrète
Une URL signée porte alors deux paramètres, un horodatage et un HMAC :
/8.8.8.8?d=1770000000&s=cc5849a839193e59
La signature se génère côté serveur, évidemment, avec la clé secrète que tu es seul à connaître. La règle tient en une ligne :
const s = createHmac('sha256', KEY).update(`8.8.8.8?d=${d}`).digest('hex').slice(0, 16);
L’horodatage fait partie de ce qui est signé, donc il ne peut pas être déplacé, et il borne la durée de vie de l’adresse : une heure par défaut. C’est toute la différence entre une URL qui a fuité par accident et une clé volée. Le SIGNATURE.md du dépôt contient la spécification complète, des valeurs de référence pour vérifier ton implémentation, et les mêmes trois lignes écrites en JavaScript, PHP, Python, C# et Java.
Et si tu sais ce que tu fais, réseau privé ou proxy déjà authentifié, tu laisses la clé vide : le service répond à tout le monde, et c’est très bien comme ça.
Ce que ça vaut vraiment
Je ne vais pas te vendre ça comme indispensable. Si tu as besoin de dix géolocalisations par mois, une API externe toute faite te coûtera moins de temps que de monter un conteneur. Et il faut être honnête sur la précision : GeoLite2 est la version gratuite d’une estimation. La ville affichée est souvent celle de l’opérateur, pas celle du visiteur, et un VPN déplace la réponse dans un autre pays. Décider quoi que ce soit d’important là-dessus, un paiement, un droit, une identité, c’est une erreur, quelle que soit la base.
Mais pour ce à quoi ça sert vraiment, remplir la colonne « pays » de tes statistiques, adapter une langue par défaut, repérer d’où arrive un pic de trafic, c’est parfait. Pas de quota, pas de facture qui grimpe avec le trafic, et surtout l’adresse de tes visiteurs qui ne sort jamais de ta machine. C’est exactement pour ça que je m’en sers ici.
Le tout est sur GitHub, en licence MIT. Comme d’habitude, je suis preneur de tout retour, et les pull requests sont les bienvenues.
Image d’en-tête générée par IA.
SmeagolWorms4
Commentaires