Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,4 @@ dmypy.json
# Cython debug symbols
cython_debug/

poetry.lock
226 changes: 226 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,229 @@ The output format is an array of JSON object (to support the ability to serve mu
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
```

# Fork additions (bikramj/mmdb-server)

This fork adds two things to upstream mmdb-server:

- **ipinfo databases** — serve the daily-refreshed
[ipinfo free country + ASN database](https://ipinfo.io/products/free-ip-database)
(or any other MMDB file) instead of the GeoOpen files committed in `db/`.
- **`/cidr` export endpoints** — all networks of a country or an ASN as a
collapsed CIDR list, ready to use as an HAProxy or Apache ACL.

## Serving the ipinfo database

`etc/server.conf` takes one or more MMDB files (comma-separated, relative to
the working directory or absolute); each file contributes one element to the
JSON array, in the order listed:

~~~ini
[global]
mmdb_file = /opt/ipinfo-db/country_asn.mmdb
country_file = db/country.json
lookup_pubsub = no
port = 8000
~~~

ipinfo rebuilds the file daily. Download it and restart the server so the
new file is loaded (a restart also clears the `/cidr` cache):

~~~bash
curl -sSL --fail --retry 3 -o /opt/ipinfo-db/country_asn.mmdb.tmp \
"https://ipinfo.io/data/free/country_asn.mmdb?token=$IPINFO_TOKEN" \
&& mv /opt/ipinfo-db/country_asn.mmdb.tmp /opt/ipinfo-db/country_asn.mmdb \
&& systemctl restart mmdb-server
~~~

## Usage examples

All examples assume the server listens on `127.0.0.1:8000`.

### IP lookup — `GET /geolookup/{ip}`

~~~bash
curl -s http://127.0.0.1:8000/geolookup/128.100.100.128 | jq .
~~~

With the ipinfo database the record is flat (`country`, `country_name`,
`continent`, `continent_name`, `asn`, `as_name`, `as_domain`):

~~~json
[
{
"as_domain": "utoronto.ca",
"as_name": "University of Toronto",
"asn": "AS239",
"continent": "NA",
"continent_name": "North America",
"country": "CA",
"country_name": "Canada",
"meta": {
"description": { "en": "ipinfo generic_country_free_country_asn.mmdb" },
"build_db": "2026-09-09 04:04:41",
"db_source": "ipinfo generic_country_free_country_asn.mmdb",
"nb_nodes": 2049115
},
"ip": "128.100.100.128",
"country_info": {
"Country": "Canada",
"Alpha-2 code": "CA",
"Alpha-3 code": "CAN",
"Numeric code": "124",
"Latitude (average)": "60",
"Longitude (average)": "-95"
}
}
]
~~~

IPv6 works the same way: `curl -s http://127.0.0.1:8000/geolookup/2606:fa00::1`

Pick out fields with `jq`:

~~~bash
# "Canada | University of Toronto"
curl -s http://127.0.0.1:8000/geolookup/128.100.100.128 \
| jq -r '.[0] | "\(.country_name) | \(.as_name)"'

# country code only
curl -s http://127.0.0.1:8000/geolookup/8.8.8.8 | jq -r '.[0].country'
~~~

(With the GeoOpen databases the same values live under `.country.iso_code`
and `.country.AutonomousSystemNumber` — see the upstream examples above.)

A malformed address is a `422`:

~~~
$ curl -s -w ' -> %{http_code}\n' http://127.0.0.1:8000/geolookup/not-an-ip
"IPv4 or IPv6 address is in an incorrect format. Dotted decimal for IPv4 or textual representation for IPv6 are required." -> 422
~~~

### The caller's own address — `GET /` and `GET /raw`

~~~bash
curl -s http://127.0.0.1:8000/ | jq . # full lookup of the caller's IP
curl -s http://127.0.0.1:8000/raw # just the IP, as text
curl -sI http://127.0.0.1:8000/raw | grep -i x-ip # HEAD: the IP in an X-IP header
~~~

Behind a reverse proxy the caller's address is taken from `X-Forwarded-For`
(Falcon's `access_route`), so treat these two endpoints as informational.

### All networks of a country — `GET /cidr/country/{cc}`

~~~bash
curl -s --retry 10 --retry-delay 5 http://127.0.0.1:8000/cidr/country/CA
~~~

~~~
# country CA - 8174 IPv4 ranges - GeoOpen-Country-ASN (build 2025-12-03 04:21:34)
2.22.72.0/22
5.44.16.0/20
...
~~~

The first request for a key answers `503` with `Retry-After: 5` while the
database is scanned in the background (a few seconds); `curl --retry` honours
that header and returns the real answer. Later requests for the same key are
served from cache until the server restarts.

IPv4 only by default. `?family=6` gives the IPv6 prefixes and `?family=all`
both; the family is applied when the response is built, so switching is free
once the key is cached:

~~~bash
curl -s "http://127.0.0.1:8000/cidr/country/CA?family=6"
# country CA - 1358 IPv6 ranges - GeoOpen-Country-ASN (build 2025-12-03 04:21:34)
# 2001:410::/32
# ...
curl -s "http://127.0.0.1:8000/cidr/country/CA?family=all"
~~~

The same list in the other formats (`family` combines with `format`):

~~~bash
# Apache: one "Require ip" line per network
curl -s --retry 10 --retry-delay 5 "http://127.0.0.1:8000/cidr/country/CA?format=apache"

# JSON: {query, family, count, source, cidrs[]}
curl -s --retry 10 --retry-delay 5 "http://127.0.0.1:8000/cidr/country/CA?format=json&family=all" \
| jq '{family, count, source, first: .cidrs[0]}'
~~~

Country codes are case-insensitive (`/cidr/country/ca`). A code that is not
in `country.json` is a `404` right away; anything that is not two letters is
a `422`.

### All networks of an ASN — `GET /cidr/asn/{asn}`

~~~bash
curl -s --retry 10 --retry-delay 5 http://127.0.0.1:8000/cidr/asn/AS239 # or /cidr/asn/239
~~~

~~~
# AS239 - 8 IPv4 ranges - GeoOpen-Country-ASN (build 2025-12-03 04:21:34)
128.100.0.0/16
138.51.0.0/16
142.1.0.0/16
...
~~~

`?family=` and `?format=` work here too. A non-numeric ASN is a `422`; an
ASN with no networks in the requested family is a `404` (after the scan).

### Feeding an HAProxy ACL

~~~bash
#!/bin/bash
# refresh-ca-list.sh — run from cron after the daily database refresh
set -e
new=$(mktemp)
# IPv4 only by default; add ?family=all for a dual-stack frontend
curl -sf --retry 10 --retry-delay 5 -o "$new" http://127.0.0.1:8000/cidr/country/CA
# refuse a suspiciously short list and keep the last known-good file
[ "$(grep -c / "$new")" -ge 5000 ] || { echo "CA list too short, keeping the old one"; exit 1; }
install -m 0644 "$new" /etc/haproxy/ca.lst
systemctl reload haproxy
~~~

~~~
# haproxy.cfg — '#' comment lines in the file are fine
acl from_canada src -f /etc/haproxy/ca.lst
http-request deny if { path_beg /canada-only } !from_canada
~~~

### Feeding an Apache access rule

~~~bash
curl -sf --retry 10 --retry-delay 5 -o /etc/apache2/ca-require.conf \
"http://127.0.0.1:8000/cidr/country/CA?format=apache"
~~~

~~~apache
<Location /canada-only>
<RequireAny>
Include /etc/apache2/ca-require.conf
</RequireAny>
</Location>
~~~

### Health check

~~~bash
curl -sf http://127.0.0.1:8000/geolookup/8.8.8.8 > /dev/null && echo up
~~~

## How `/cidr` works

Nothing is indexed at startup. The first request for a country or ASN scans
the database in a background thread (a few seconds; requires
`maxminddb >= 2.5`) and answers `503 Retry-After` — retry, e.g.
`curl --retry 10 --retry-delay 5 ...` — and the result is cached until the
service restarts. Only the requested key's networks are kept, so memory
stays flat and the lookup endpoints are never blocked. IPv4 is returned by
default (`?family=6` for IPv6, `?family=all` for both); adjacent prefixes
are collapsed. Consumers fetching ACL files should sanity-check the result
(e.g. minimum line count) before deploying it.
1 change: 1 addition & 0 deletions etc/server.conf.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[global]
mmdb_file = db/GeoOpen-Country.mmdb,db/GeoOpen-Country-ASN.mmdb
#mmdb_file = db/ipinfo_country_asn.mmdb,db/ipinfo_country.mmdb
country_file = db/country.json
lookup_pubsub = no
port = 8000
Loading