nginx: doing ip geolocation right in nginx

nginx: doing ip geolocation right in nginx

ยท

6 min read

knowing the geolocation of your site's users is handy thing. maybe you want to force your canadian users into a degraded, second-rate version of your ecommerce site, or maybe you want to redirect people from brazil to a frontend you ran through google translate, or maybe you just want to block the netherlands because you hate the dutch. there are reasons.

traditionally, this gets done by calling a third-party geolocation api. you gotta fiddle with api keys and manage rate limits and write a bunch of code. or... we could just let nginx do it all for us.

in this post we're going to go over how to do ip geolocation for country and city in nginx and get that data into our web app where we can use it. all of this was written for ubuntu-like systems runing nginx 1.18.0.

geordi approves

doing geolocation in your httpd

test if have the necessary nginx module

geopip lookup in nginx is done by the http-geoip2 module. on ubuntu-like systems this module usually comes pre-installed, although ymmv.

to test if our nginx has http-geoip2 installed, we can run:

nginx -V 2>&1 | sed -n "/http-geoip2/p"

this just takes the output of nginx's version data and tests if http-geoip2 is listed in it. if our nginx has the module, we will see output that looks something like.

nginx version: nginx/1.18.0 (Ubuntu)
built with OpenSSL 3.0.2 15 Mar 2022
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 <snip> --with-threads --add-dynamic-module=/build/nginx-d8gVax/nginx-1.18.0/debian/modules/http-geoip2 <snip>

if we don't have the module, the output will be blank.

get the maxmind databases

maxmind is technically a purveyor of anti-fraud software, but the thing they're really known for is compiling and distributing ip-to-location databases. these are made available as either plain ol' csvs or in a 'custom but open' binary format that has an associated c library for speedy searches. we're going to use the binary format.

let's install the c library (and associated executable) first. on ubuntu-like systems this is just an apt away:

sudo add-apt-repository ppa:maxmind/ppa
sudo apt update
sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin

on non-ubuntu-based distros, we'll be stuck with compiling it from source like it's the nineties.

getting the databases themselves is actually more work. first, we have to sign up for an account on maxmind and jump through the email verification and 2fa login hoops.

once we have our account and have signed in, on our 'account summary' page there will be a text link that reads 'download databases'. clicking that leads to a list of downloadables. the ones we want are:

  • GeoLite2 City

  • GeoLite2 Country

make sure you get the format 'GeoIP2 Binary (.mmdb)'. these are tarballs. download them to the server where you are running nginx.

untarring the archives is straightforward:

sudo tar -zxvf GeoLite2-City_20240604.tar.gz
sudo tar -zxvf GeoLite2-Country_20240604.tar.gz

next, we will make a directory to store the mmdb files. i like to put these things in /var, which is maybe contentious since the os doesn't write to them, but they are, technically, databases. and databases go in /var.

sudo mkdir /var/maxmind

we then put just the mmdb files in there.

sudo mv ./GeoLite2-City_20240604/GeoLite2-City.mmdb /var/maxmind
sudo mv ./GeoLite2-Country_20240604/GeoLite2-Country.mmdb /var/maxmind

we now have our geolocation databases and the library to query it.

fun fact: you can lookup ip locations on the command line

along with the c library for mmdb, which nginx needs, we also installed mmdblookup, an executable that can query the geoip database. it takes two arguments: the --file where the database lives, and the --ip we want to lookup.

mmdblookup --file /var/maxmind/GeoLite2-City.mmdb --ip <some ip address>

that should spit out a small moutain of information about the ip location in a format that looks like json, but is not actually json.

if we want to narrow our output, we can specify a path to the desired data in the command. for instance, looking at the not-quite-json output, we see there is a city object which contains a names object which has a value keyed at en for the english name of the city. we can get just that english city name by adding the path.

mmdblookup --file /var/maxmind/GeoLite2-City.mmdb --ip <some ip address> city names en

configure nginx.conf to do the actual lookup

nginx will do our geolocation lookup automatically, but first we need to configure it so it knows things like where we stored those mmdb databases and what data we want to extract from them. we'll do this in the http block of our nginx.conf file.

##
# GeoIp country
##
geoip2 /var/maxmind/GeoLite2-Country.mmdb {
    $geoip2_data_country_name country names en;
    $geoip2_data_country_code country iso_code;
}

##
# GeoIp city
##
geoip2 /var/maxmind/GeoLite2-City.mmdb {
    $geoip2_data_city_name city names en;
}

here, we have the geoip2 directive entered twice for two different databases: the country one and the city one. both of these directives will be run.

the body of each of those blocks, the stuff in the curly braces, is variable assignments. if we recall, when we were using mmdblookup in the 'fun fact' section above, we could pass a path to the value we wanted in the record. here we're taking a variable name, ie $geoip2_data_city_name and assigning it to the value at the path city names en for the record for the current ip address. of course we can use any variable name and any valid path we want here, but city name and, country name and iso code are probably the most useful.

this is all that's required for nginx to extract geolocation data for an ip.

note: the complete nginx.conf file is at the bottom of this post, because providing snippets without context is Not Cool.

configure our virtual host to make our geodata available

of course extracting geolocation data is only useful if we can get it into our script.

we're going to do that by taking the variables geoip2 made in our nginx.conf file and setting them as fastcgi_params in the location block of our virtual host. here's an example

location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;

    ###
    # geoip2 variables
    fastcgi_param COUNTRY_CODE $geoip2_data_country_code;
    fastcgi_param COUNTRY_NAME $geoip2_data_country_name;
    fastcgi_param CITY_NAME $geoip2_data_city_name;

    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_intercept_errors off;
    fastcgi_buffer_size 16k;
    fastcgi_buffers 4 16k;
    fastcgi_connect_timeout 300;
    fastcgi_send_timeout 300;
    fastcgi_read_timeout 300;
}

if you've ever looked at an nginx configuration for a php site before, this location block should look somewhat familiar. the important addition here, of course, is where we call fastcgi_param and pass it two arguments: first, the name of the fastcgi paramater we want to declare and, second, the name of the geoip2 variable we set in nginx.conf.

get location data in one line of php

here's a fun fact about fastcgi_params: they're available in php's $_SERVER array.

with that in mind, let's write a one line php script (not including the opening tag) that will display the country name of the user:

<?php
print $_SERVER['COUNTRY_NAME'];

if we serve this script on our geolocation-enabled nginx, behold: we will see the country name associated with the user's ip address.

addendum 1: the complete nginx.conf

the complete nginx.conf used in the examples above is available as a gist.

addendum 2: the complete virutal host configuration

the virtual hosts file used for the examples with the following elements removed

  • server_name

  • root

  • php version

  • access log name

  • error log name

is available as a gist.

๐Ÿ”Ž this post was originally written in the grant horwood technical blog

ย