Contents

Compiling, Installing and Configuring the GeoIP2 module for NGINX

This is the second part of a series of 3 posts where I will be installing NGINX, in this section, we will install support for GeoIP2, via module ngx_http_geoip2_module.

This is the second of 3 posts where I will be:

Compiling and Installing

To install support for GeoIP2 to NGINX we have two options, installing the module directly from the repository if available or compiling and installing.

If you need to just the standard module from the repository and is available in your distro, do the following, update apt with sudo apt update then sudo apt install libnginx-mod-http-geoip2, and proceed to the confi

If you want to get the latest module, you will need to compile this from the sources and then add this to the modules folder; for that, you need to have a few things installed first, let’s do:

1
2
3
4
$ sudo apt update
$ sudo apt install build-essential
$ sudo apt install git wget libpcre3 libssl1.1 zlib1g
$ sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin

This should install the requirements libc6 (>= 2.28), libpcre3, libssl1.1 (>= 1.1.1), zlib1g (>= 1:1.1.4), lsb-base (>= 3.0-6).

The module we want to compile is available in GitHub from leev/ngx_http_geoip2_module, we can clone the repository using:

1
$ git clone https://github.com/leev/ngx_http_geoip2_module.git

Now you should have a directory named ngx_http_geoip2_module which contains the dynamic module code.

Then download the sources from the NGINX repository:

1
2
3
$ wget http://nginx.org/download/nginx-<VERSION>.tar.gz
$ tar zxvf nginx-<VERSION>.tar.gz
$ cd nginx-<VERSION>

<VERSION> should be your NGINX version, e.g. 1.20.1 (or whatever you get from sudo nginx -v)

The compilation phase will prepare the environment and check for the requirements, in our build we will generate also the stream version of the module. For that issue the following command:

1
$ ./configure --with-compat --add-dynamic-module=../ngx_http_geoip2_module --with-stream

Check for any errors or missing dependencies, you should have a summary like:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
checking for OS
 + Linux 4.19.0-18-amd64 x86_64
checking for C compiler ... found
 + using GNU C compiler
 + gcc version: 8.3.0 (Debian 8.3.0-6)
checking for gcc -pipe switch ... found
checking for -Wl,-E switch ... found
checking for gcc builtin atomic operations ... found
checking for C99 variadic macros ... found
checking for gcc variadic macros ... found
checking for gcc builtin 64 bit byteswap ... found
checking for unistd.h ... found
checking for inttypes.h ... found
checking for limits.h ... found
checking for sys/filio.h ... not found
checking for sys/param.h ... found
checking for sys/mount.h ... found
checking for sys/statvfs.h ... found
checking for crypt.h ... found
checking for Linux specific features
checking for epoll ... found
checking for EPOLLRDHUP ... found
checking for EPOLLEXCLUSIVE ... found
checking for eventfd() ... found
checking for O_PATH ... found
checking for sendfile() ... found
checking for sendfile64() ... found
checking for sys/prctl.h ... found
checking for prctl(PR_SET_DUMPABLE) ... found
checking for prctl(PR_SET_KEEPCAPS) ... found
checking for capabilities ... found
checking for crypt_r() ... found
checking for sys/vfs.h ... found
checking for nobody group ... not found
checking for nogroup group ... found
checking for poll() ... found
checking for /dev/poll ... not found
checking for kqueue ... not found
checking for crypt() ... not found
checking for crypt() in libcrypt ... found
checking for F_READAHEAD ... not found
checking for posix_fadvise() ... found
checking for O_DIRECT ... found
checking for F_NOCACHE ... not found
checking for directio() ... not found
checking for statfs() ... found
checking for statvfs() ... found
checking for dlopen() ... not found
checking for dlopen() in libdl ... found
checking for sched_yield() ... found
checking for sched_setaffinity() ... found
checking for SO_SETFIB ... not found
checking for SO_REUSEPORT ... found
checking for SO_ACCEPTFILTER ... not found
checking for SO_BINDANY ... not found
checking for IP_TRANSPARENT ... found
checking for IP_BINDANY ... not found
checking for IP_BIND_ADDRESS_NO_PORT ... found
checking for IP_RECVDSTADDR ... not found
checking for IP_SENDSRCADDR ... not found
checking for IP_PKTINFO ... found
checking for IPV6_RECVPKTINFO ... found
checking for TCP_DEFER_ACCEPT ... found
checking for TCP_KEEPIDLE ... found
checking for TCP_FASTOPEN ... found
checking for TCP_INFO ... found
checking for accept4() ... found
checking for int size ... 4 bytes
checking for long size ... 8 bytes
checking for long long size ... 8 bytes
checking for void * size ... 8 bytes
checking for uint32_t ... found
checking for uint64_t ... found
checking for sig_atomic_t ... found
checking for sig_atomic_t size ... 4 bytes
checking for socklen_t ... found
checking for in_addr_t ... found
checking for in_port_t ... found
checking for rlim_t ... found
checking for uintptr_t ... uintptr_t found
checking for system byte ordering ... little endian
checking for size_t size ... 8 bytes
checking for off_t size ... 8 bytes
checking for time_t size ... 8 bytes
checking for AF_INET6 ... found
checking for setproctitle() ... not found
checking for pread() ... found
checking for pwrite() ... found
checking for pwritev() ... found
checking for strerrordesc_np() ... not found
checking for sys_nerr ... found
checking for localtime_r() ... found
checking for clock_gettime(CLOCK_MONOTONIC) ... found
checking for posix_memalign() ... found
checking for memalign() ... found
checking for mmap(MAP_ANON|MAP_SHARED) ... found
checking for mmap("/dev/zero", MAP_SHARED) ... found
checking for System V shared memory ... found
checking for POSIX semaphores ... not found
checking for POSIX semaphores in libpthread ... found
checking for struct msghdr.msg_control ... found
checking for ioctl(FIONBIO) ... found
checking for ioctl(FIONREAD) ... found
checking for struct tm.tm_gmtoff ... found
checking for struct dirent.d_namlen ... not found
checking for struct dirent.d_type ... found
checking for sysconf(_SC_NPROCESSORS_ONLN) ... found
checking for sysconf(_SC_LEVEL1_DCACHE_LINESIZE) ... found
checking for openat(), fstatat() ... found
checking for getaddrinfo() ... found
configuring additional dynamic modules
adding module in ../ngx_http_geoip2_module
checking for MaxmindDB library ... found
 + ngx_geoip2_module was configured
checking for PCRE library ... found
checking for PCRE JIT support ... found
checking for zlib library ... found
creating objs/Makefile

Configuration summary
  + using system PCRE library
  + OpenSSL library is not used
  + using system zlib library

  nginx path prefix: "/usr/local/nginx"
  nginx binary file: "/usr/local/nginx/sbin/nginx"
  nginx modules path: "/usr/local/nginx/modules"
  nginx configuration prefix: "/usr/local/nginx/conf"
  nginx configuration file: "/usr/local/nginx/conf/nginx.conf"
  nginx pid file: "/usr/local/nginx/logs/nginx.pid"
  nginx error log file: "/usr/local/nginx/logs/error.log"
  nginx http access log file: "/usr/local/nginx/logs/access.log"
  nginx http client request body temporary files: "client_body_temp"
  nginx http proxy temporary files: "proxy_temp"
  nginx http fastcgi temporary files: "fastcgi_temp"
  nginx http uwsgi temporary files: "uwsgi_temp"
  nginx http scgi temporary files: "scgi_temp"

If no error or missing libraries, you are ready to build the module with:

1
$ make modules

It should finish quickly and your fresh backed modules should be at: objs/ngx_http_geoip2_module.so and objs/ngx_stream_geoip2_module.so. You can copy those files to your modules directory.

1
$ sudo cp objs/ngx_*.so /etc/nginx/modules

And let’s enable them by creating the .conf files in modules-enabled. First create a file named 50-mod-http-geoip2.conf as:

1
$ sudo vi /etc/nginx/modules-enabled/50-mod-http-geoip2.conf

and add the following line:

1
load_module modules/ngx_http_geoip2_module.so;

save it and let’s create another 50-mod-stream-geoip2.conf as

1
$ sudo vi /etc/nginx/modules-enabled/50-mod-stream-geoip2.conf

and add the following line:

1
load_module modules/ngx_stream_geoip2_module.so;

Check the config and restart NGINX if all is ok.

1
$ sudo nginx -tt && sudo systemctl restart nginx.service

Configuring

I found a bit cryptic the documentation to configure this module, but I will do my best to tell you how in an easy way, and for that let’s hold and try the CLI for MaxMind.

By now you should have your mmdb files, if not please register at Maxmind for a free account and download one of them, for example GeoLite2-City.mmdb. Look for the download link “Download GZIP”, you will download a file like GeoLite2-City_20211012.tar.gz, to extract the actual mmdb file use:

1
$ tar -xvf GeoLite2-City_20211012.tar.gz

It will create a directory with all the files like:

1
2
3
4
5
GeoLite2-City_20211012/
GeoLite2-City_20211012/README.txt
GeoLite2-City_20211012/COPYRIGHT.txt
GeoLite2-City_20211012/GeoLite2-City.mmdb
GeoLite2-City_20211012/LICENSE.txt

Let’s copy the database to the folder where NGINX can reach them:

1
2
$ sudo mkdir /etc/ningx/geolite2db
$ sudo cp GeoLite2-City_20211012/GeoLite2-City.mmdb /etc/ningx/geolite2db/

!!!! NOTE: If you want to download all databases you can request a license key and use a tool I have created B1gG/MaxmindDownloader to download all the files and check them at the same time.

Trying the CLI

The tool we will be using should have been installed during the installation of the package mmdb-bin, when we installed the dependencies to compile the module (you can check if is installed with mmdblookup --version, if not install the package)

Let’s try a basic command:

1
$ mmdblookup --file GeoLite2-City_20211026/GeoLite2-City.mmdb --ip 1.0.0.1

The output should be something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
  {
    "continent":
      {
        "code":
          "OC" <utf8_string>
        "geoname_id":
          6255151 <uint32>
        "names":
          {
            "de":
              "Ozeanien" <utf8_string>
            "en":
              "Oceania" <utf8_string>
            "es":
              "Oceanía" <utf8_string>
            "fr":
              "Océanie" <utf8_string>
            "ja":
              "オセアニア" <utf8_string>
            "pt-BR":
              "Oceania" <utf8_string>
            "ru":
              "Океания" <utf8_string>
            "zh-CN":
              "大洋洲" <utf8_string>
          }
      }
    "country":
      {
        "geoname_id":
          2077456 <uint32>
        "iso_code":
          "AU" <utf8_string>
        "names":
          {
            "de":
              "Australien" <utf8_string>
            "en":
              "Australia" <utf8_string>
            "es":
              "Australia" <utf8_string>
            "fr":
              "Australie" <utf8_string>
            "ja":
              "オーストラリア" <utf8_string>
            "pt-BR":
              "Austrália" <utf8_string>
            "ru":
              "Австралия" <utf8_string>
            "zh-CN":
              "澳大利亚" <utf8_string>
          }
      }
    "location":
      {
        "accuracy_radius":
          1000 <uint16>
        "latitude":
          -33.494000 <double>
        "longitude":
          143.210400 <double>
        "time_zone":
          "Australia/Sydney" <utf8_string>
      }
    "registered_country":
      {
        "geoname_id":
          2077456 <uint32>
        "iso_code":
          "AU" <utf8_string>
        "names":
          {
            "de":
              "Australien" <utf8_string>
            "en":
              "Australia" <utf8_string>
            "es":
              "Australia" <utf8_string>
            "fr":
              "Australie" <utf8_string>
            "ja":
              "オーストラリア" <utf8_string>
            "pt-BR":
              "Austrália" <utf8_string>
            "ru":
              "Австралия" <utf8_string>
            "zh-CN":
              "澳大利亚" <utf8_string>
          }
      }
  }

That output is the result of the query to the file GeoLite2-City.mmdb for the IP 1.0.0.1, that is all the information available in that DB for that IP. Try now with your IP:

1
mmdblookup --file GeoLite2-City_20211026/GeoLite2-City.mmdb --ip `curl -s https://icanhazip.com`

That will bring all the information in the mmdb file for your IP, but let’s say you only want the postal code, then you can use:

1
mmdblookup --file GeoLite2-City_20211026/GeoLite2-City.mmdb --ip `curl -s https://icanhazip.com` postal code

Basically, you need to indicate the names of the nodes up to the one with the information you want to retrieve, also it should be the full path. The command as well as variables can only hold one value, you can’t retrieve all values at once. If there is an array of values you need to indicate the index of what you need, let’s suppose your IP information has a field called subdivisions like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
...
    "subdivisions":
      [
        {
          "geoname_id":
            6269131 <uint32>
          "iso_code":
            "ENG" <utf8_string>
          "names":
            {
              "de":
                "England" <utf8_string>
              "en":
                "England" <utf8_string>
              "es":
                "Inglaterra" <utf8_string>
              "fr":
                "Angleterre" <utf8_string>
              "ja":
                "イングランド" <utf8_string>
              "pt-BR":
                "Inglaterra" <utf8_string>
              "ru":
                "Англия" <utf8_string>
              "zh-CN":
                "英格兰" <utf8_string>
            }
        }
        {
          "geoname_id":
            3333171 <uint32>
          "iso_code":
            "MRT" <utf8_string>
          "names":
            {
              "de":
                "London Borough of Merton" <utf8_string>
              "en":
                "Merton" <utf8_string>
              "fr":
                "Merton" <utf8_string>
            }
        }
      ]

And you want to retrieve the iso_code of the first occurrence (index 0, as in programming languages) then you can issue:

1
$ mmdblookup --file GeoLite2-City_20211026/GeoLite2-City.mmdb --ip `curl -s https://icanhazip.com` subdivisions 0 iso_code
Creating variables

With that in mind, now you can define the variables inside of nginx.conf as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    geoip2 /etc/nginx/geolite2db/GeoLite2-City.mmdb {
        #auto_reload 5m;
        #geoip2_proxy_recursive on;
        # Continent/Region Details
        $geoip2_city_continent_code continent code;
        $geoip2_city_continent_geoname_id continent geoname_id;
        $geoip2_city_continent_name continent names en;
        # Country Details
        $geoip2_city_country_geoname_id country geoname_id;
        $geoip2_city_country_code country iso_code;
        $geoip2_city_country_name country names en;
        # City Details
        $geoip2_city_ar location accuracy_radius;
        $geoip2_city_latitude location latitude;
        $geoip2_city_longitude location longitude;
        $geoip2_city_tz location time_zone;
        $geoip2_city_geoname_id city geoname_id;
        $geoip2_city_name city names en;
        $geoip2_city_postal_code postal code;
    }

This code could be included in the config file in the http declaration after the initial set-up, like in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# Generated by nginxconfig.io
# https://www.digitalocean.com/community/tools/nginx?domains.0.php.php=false&domains.0.routing.index=index.html&domains.0.routing.fallbackHtml=true

user                 www-data;
pid                  /run/nginx.pid;
worker_processes     auto;
worker_rlimit_nofile 65535;

# Load modules
include              /etc/nginx/modules-enabled/*.conf;

events {
    multi_accept       on;
    worker_connections 65535;
}

http {
    charset                utf-8;
    sendfile               on;
    tcp_nopush             on;
    tcp_nodelay            on;
    server_tokens          off;
    log_not_found          off;
    types_hash_max_size    2048;
    types_hash_bucket_size 64;
    client_max_body_size   16M;

    # GeoIP
    geoip2 /etc/nginx/geolite2db/GeoLite2-City.mmdb {
        auto_reload 5m;
        geoip2_proxy_recursive on;
        # Continent/Region Details
        $geoip2_city_continent_code continent code;
        $geoip2_city_continent_geoname_id continent geoname_id;
        $geoip2_city_continent_name continent names en;
        # Country Details
        $geoip2_city_country_geoname_id country geoname_id;
        $geoip2_city_country_code country iso_code;
        $geoip2_city_country_name country names en;
        # City Details
        $geoip2_city_ar location accuracy_radius;
        $geoip2_city_latitude location latitude;
        $geoip2_city_longitude location longitude;
        $geoip2_city_tz location time_zone;
        $geoip2_city_geoname_id city geoname_id;
        $geoip2_city_name city names en;
        $geoip2_city_postal_code postal code;
    }

    # MIME
    include                mime.types;
    default_type           application/octet-stream;

    # Logging
    access_log             /var/log/nginx/access.log;
    error_log              /var/log/nginx/error.log warn;

    # SSL
    ssl_session_timeout    1d;
    ssl_session_cache      shared:SSL:10m;
    ssl_session_tickets    off;

    # Diffie-Hellman parameter for DHE ciphersuites
    ssl_dhparam            /etc/nginx/dhparam.pem;

    # Mozilla Intermediate configuration
    ssl_protocols          TLSv1.2 TLSv1.3;
    ssl_ciphers            ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # OCSP Stapling
    ssl_stapling           on;
    ssl_stapling_verify    on;
    resolver               1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
    resolver_timeout       2s;

    # Load configs
    include                /etc/nginx/conf.d/*.conf;
    include                /etc/nginx/sites-enabled/*;
}

There are two directives you can use:

  • auto_reload, specify the intervals for NGINX to check for an updated DB, in case you a process that automatically downloads the file from Maxmind. In the example configuration, this will be 5 minutes.
  • geoip2_proxy_recursive, will indicate to NGINX to use the last address sent in “X-Forwarded-For” for the query to the DB.
Using the new variables

Those new variables can be used to add information to your logs or sent as parameters to your app when NGINX is acting as a proxy. Let’s try the log example; for that, you will need to declare a log format in the nginx.conf, change the Logging section from this:

1
2
3
    # Logging
    access_log             /var/log/nginx/access.log;
    error_log              /var/log/nginx/error.log warn;

to this:

1
2
3
4
    # Logging
    log_format log_with_geo_data '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $http_x_forwarded_for $http_x_forwarded_proto $http_true_client_ip  $geoip2_city_postal_code $geoip2_city_name $geoip2_city_country_name';
    access_log             /var/log/nginx/access.log log_with_geo_data;
    error_log              /var/log/nginx/error.log warn;

Check the config and restart NGINX if all is ok.

1
$ sudo nginx -tt && sudo systemctl restart nginx.service

Now you should be able to see details of each of your visitors in the access.log file.

Buy me a Coffee
Hope you find this useful, if you have any question please visit my twitter @bigg_blog and if you have a couple of pounds buy me a coffee.
G