mmdbio
Overview
mmdbio is a command-line tool for MMDB files. Use it to look up IP addresses, export data, compare two files, check a file, make a file smaller, and build your own MMDB file from CSV or JSON.
It is one program with nothing else to install, for Linux, macOS, Windows and the BSDs. It replaces the older Go version of mmdbio, and all the old commands and options still work.
Install
| System | Command |
|---|---|
| Linux or macOS | curl -fsSL https://raw.githubusercontent.com/IPGeolocation/mmdbio/main/install.sh | sh |
| Windows (PowerShell) | irm https://raw.githubusercontent.com/IPGeolocation/mmdbio/main/install.ps1 | iex |
| Debian or Ubuntu | sudo apt install ./mmdbio_<version>_amd64.deb (from the releases page) |
| Fedora, RHEL or SUSE | sudo rpm -i mmdbio-<version>-1.x86_64.rpm (from the releases page) |
mmdbio --versionTo build from source (use gmake on the BSDs and illumos):
git clone https://github.com/IPGeolocation/mmdbio.git
cd mmdbio
make
sudo make installCommands
| Command | What It Does |
|---|---|
| import | Build an MMDB file from a CSV or JSON file. |
| read | Look up IP addresses. |
| export | Write out the data, or a list of IP blocks. |
| shrink | Make a file smaller without changing any answer. |
| metadata, stats, inspect, verify | Show information about a file, or check it. |
| diff | Compare two files. |
| completion | Set up Tab completion in your shell. |
mmdbio <command> --help lists every option of a command. Files and
addresses can also be given without their flags, for example
mmdbio read 8.8.8.8 ips.mmdb.
1. import
Build an MMDB file from a CSV, TSV, JSON or NDJSON file, gzipped or not.
The examples in this README use this ips.csv:
network,country,city,asn,is_vpn,score
8.8.8.0/24,US,Mountain View,15169,false,5
1.1.1.0/24,AU,Sydney,13335,false,3
2.56.0.0/16,DE,Berlin,64496,true,80
2001:db8::/32,NL,Amsterdam,64500,true,90mmdbio import --in ips.csv1input: CSV (comma-delimited, with a header); network column "network"; 5 fields
2rows: 4 read, 4 inserted, 0 skipped
3tree: 4 unique records, 177 search-tree nodes, record size 24, 1,455 bytes
4✅ Successfully wrote 4 entries to ips.mmdbThe input: line shows the column mmdbio used for the IPs. Every other
column becomes a text field.
mmdbio import --in ips.csv --out city.mmdb # pick the output name (default: ips.mmdb)
mmdbio import --in ips.csv.gz # gzipped input
mmdbio import --in data.json # JSON or NDJSON
mmdbio import --in ips.csv --title City --description "My city data"
mmdbio import --config build.json # settings from a config fileTo keep the settings in a file, see Config file.
I. The IP column
mmdbio finds the IP column by these names (case, _, - and spaces don't
matter, so Start IP works):
| Your File Has | Names |
|---|---|
| one IP column | network, cidr, prefix, subnet, ip_range, range, ip_network, net, block, netblock, ip, ip_address, address, ips, ip_range_cidr |
| a first address | start_ip, ip_start, start, from, ip_from, from_ip, first_ip, ip_first, range_start, start_address, begin_ip, network_start_ip, start_ip_address, low_ip, ip_low, min_ip |
| a last address | end_ip, ip_end, end, to, ip_to, to_ip, last_ip, ip_last, range_end, end_address, network_last_ip, end_ip_address, high_ip, ip_high, max_ip, stop_ip |
The values can be 8.8.8.0/24, 8.8.8.8, 8.8.8.0-8.8.8.255, a number
like 134744064, or IPv6.
| Your File Starts With | Command |
|---|---|
network, (or cidr, ip, start_ip,, ...) | mmdbio import --in file.csv |
addr, | mmdbio import --in file.csv --network-column addr |
first, | mmdbio import --in file.csv --start-column first --end-column last |
cidr, | mmdbio import --in file.csv --exclude cidr (it uses start_ip and end_ip) |
8.8.8.0/24, (no header) | mmdbio import --in file.csv --columns network, |
8.8.8.0, (no header) | mmdbio import --in file.csv --columns start_ip, |
US, (no header) | mmdbio import --in file.csv --columns country, |
Without --columns, a file with no header gets fields named column2,
column3, and so on. Comma, tab, semicolon and pipe are found on their own;
for anything else use --delimiter.
II. Types
mmdbio import --in ips.csv --out typed.mmdb --type asn=uint32 --type is_vpn=boolean --type score=uint16
mmdbio read --db typed.mmdb --ip 8.8.8.8 -f jsonl1{"asn":15169,"city":"Mountain View","country":"US","ip":"8.8.8.8","is_vpn":false,"score":5}Types: string (default), uint16, uint32, uint64, uint128, int32,
double, float, boolean, array and auto. --infer-types guesses
every column. --type tags=array reads [dns, as a list
(--array-separator ';' changes the separator). A value that doesn't fit its
type skips the row with a warning.
III. Fields
mmdbio import --in ips.csv --include country,city # only these columns
mmdbio import --in ips.csv --exclude asn,score # leave these out
mmdbio import --in ips.csv --rename country=country_code # store a column under another name
mmdbio import --in ips.csv --nest-keys # location.city becomes {"location": {"city": ...}}
mmdbio import --in ips.csv --omit-empty # leave out empty values
mmdbio import --in ips.csv --empty-if country=- # treat "-" as empty
mmdbio import --in ips.csv --network-field # add "network" to each record
mmdbio import --in list.txt --columns ip --no-fields # a plain IP list, no fieldsIV. Overlaps and bad rows
mmdbio import --in file.csv # rows that overlap: the later row wins
mmdbio import --in file.csv --merge keep # the earlier row wins
mmdbio import --in file.csv --merge toplevel # combine both records (recurse: nested parts too)
mmdbio import --in file.csv --strict # stop at the first bad row (default: skip it with a warning)V. JSON
| File | Command |
|---|---|
{"8.8.8.0/24": {"country": "US", | mmdbio import --in data.json |
[{"network": "8.8.8.0/24", | mmdbio import --in data.json |
{"start_ip": "8.8.8.0",, one per line | mmdbio import --in data.jsonl |
JSON values keep their types. --type location.latitude=double sets the type
of a nested field.
VI. Details from a second file
1ranges.csv: start_ip,end_ip,asn_id
2asn-details.csv: id,asn,name,countrymmdbio import --in ranges.csv --out asn.mmdb --lookup asn-details.csv --lookup-key id --lookup-on asn_idVII. Settings of the file you build
| Option | Sets | Default |
|---|---|---|
--out file | output file (- for standard output) | the input name with .mmdb |
--title | database type | the input file's name |
--description | description | Custom IP Intelligence Database |
--languages en, | languages | en |
--ip 4 or 6 | IPv4 only, or IPv4 and IPv6 | 6 |
--size 24, 28, 32 | size of each link | the smallest that fits |
--alias-6to4 | IPv6 forms of an IPv4 address give the IPv4 answer | off |
--disallow-reserved | drop private and special-use blocks | off |
--shrink | shrink and check the file after building it | off |
--memory MB | memory for sorting rows | 512 |
2. read
mmdbio read --db ips.mmdb --ip 8.8.8.81{
2 "asn": "15169",
3 "city": "Mountain View",
4 "country": "US",
5 "is_vpn": "false",
6 "score": "5"
7}ips.txt has 8.8.8.8, 1.1.1.1 and 9.9.9.9, one per line:
mmdbio read --db ips.mmdb --input ips.txt1{
2 "1.1.1.1": { "asn": "13335", "city": "Sydney", ... },
3 "8.8.8.8": { "asn": "15169", "city": "Mountain View", ... },
4 "9.9.9.9": null
5}mmdbio read --db ips.mmdb --range 2.56.0.0/30 -f csv1ip,asn,city,country,is_vpn,score
22.56.0.0,64496,Berlin,DE,true,80
32.56.0.1,64496,Berlin,DE,true,80
42.56.0.2,64496,Berlin,DE,true,80
52.56.0.3,64496,Berlin,DE,true,80mmdbio read --db ips.mmdb --range 1.1.1.0/29 # every address of a CIDR
cat ips.txt | mmdbio read --db ips.mmdb --input - # addresses from standard input
mmdbio read --db ips.mmdb --ip 8.8.8.8 --fields city,country
mmdbio read --db ips.mmdb --input ips.txt -f jsonl --out result.jsonl| Option | What It Does |
|---|---|
--db file | The MMDB file. |
--ip | One address. |
--range | Every address of a CIDR. |
--input file | A file with one address per line (- for standard input). |
-f | json (default), jsonl, json-pretty, csv or tsv |
--fields a, | Only these fields. Nested fields: location.city.name. |
--flatten | CSV/TSV: one column per nested field. |
--no-header | CSV/TSV without the header line. |
--out file | Write to a file (one address in JSON always prints on the screen). |
3. export
mmdbio export --db ips.mmdb --out out.csv1Exported 4 records to out.csvrange,asn,city,country,is_vpn,score
1.1.1.0/24,13335,Sydney,AU,false,3
2.56.0.0/16,64496,Berlin,DE,true,80
8.8.8.0/24,15169,Mountain View,US,false,5
2001:db8::/32,64500,Amsterdam,NL,true,90The file ending picks the format: .json, .jsonl, .csv or .tsv.
Without --out, JSON goes to the screen.
A list of IP blocks, for example for a firewall:
mmdbio export --db ips.mmdb --where is_vpn=true -f cidr12.56.0.0/16
22001:db8::/32
3Exported 2 networks as 2 CIDRs to stdoutmmdbio export --db ips.mmdb -f jsonl --where 'score>=50' # quote conditions that have > or <
mmdbio export --db ips.mmdb --where country=DE,FR # DE or FR
mmdbio export --db ips.mmdb --where country!=US # not US
mmdbio export --db ips.mmdb --where is_vpn=true --where 'score>50' # both must match
mmdbio export --db ips.mmdb --fields city,score # only these fields
mmdbio export --db ips.mmdb --family 4 # only IPv4 networks
mmdbio export --db ips.mmdb --range 8.8.0.0/16 # only networks inside this block| Option | What It Does |
|---|---|
--db file | The MMDB file. |
--out file | The output file (- or none: the screen). |
-f | json, jsonl, csv, tsv, or cidr (only the blocks, with neighbours joined; --aggregate=false lists them as stored) |
--where | field=a,, field!=a,, field>N (also >=, <, <=). Text ignores case. |
--fields a, | Only these fields. |
--family 4 or 6 | Only IPv4 or only IPv6 networks. |
--range a, | Only networks inside these blocks. |
--flatten, --no-header | As in read. |
4. shrink
mmdbio shrink --db city.mmdb # writes city.shrunk.mmdb
mmdbio shrink --db city.mmdb --out small.mmdb # pick the output name
mmdbio shrink --db city.mmdb --dry-run # only show the savings
mmdbio shrink --db city.mmdb --record-size 24 # also use smaller links (24, 28 or 32)ips32.mmdb is ips.csv built with --size 32:
mmdbio shrink --db ips32.mmdb --record-size 24 --dry-run1input: ips32.mmdb
2output: (dry run, nothing written)
3
4 size: 1.81 KB -> 1.46 KB (saved 354 B, -19.57%)
5 nodes: 177 -> 177 (+0.00%)
6 tree: 1.42 KB -> 1.06 KB
7 data: 163 B -> 163 B (unchanged)
8 record size: 32 -> 24 bitsBefore writing, shrink checks that both files give the same answer for 41
edge addresses and for 1,000,000 random IPv4 and IPv6 addresses. If any answer
differs, it writes nothing and exits with status 2 (4 if the metadata
differs).
| Option | What It Does |
|---|---|
--overwrite | Replace the output file if it exists. |
--full-check | Also compare every network of both files. |
--lookups N | Random addresses to check (default 1,000,000). |
--check=false | Skip the checks. |
--bench=false | Skip timing lookups on both files. |
-v | Show each step and its time. |
5. metadata, stats, inspect, verify
mmdbio metadata --db ips.mmdb1MMDB Metadata
2-----------------------------
3Database Type: ips
4IP Version: 6
5Record Size: 24 bits
6Node Count: 177
7Build Date: Tue, 10 Nov 2026 21:05:55 PKT
8Languages: [en]
9Description: {
10 "en": "Custom IP Intelligence Database"
11 }mmdbio metadata --db ips.mmdb --json # as JSON
mmdbio metadata --db ips.mmdb --layout # also the size of each part of the file
mmdbio stats --db ips.mmdb # the metadata, plus "IPv4 Count: 3", "IPv6 Count: 1", "Networks: 4"
mmdbio inspect --db ips.mmdb --sample-ip 8.8.8.8 # the fields of one record and their types ("asn : string", ...)
mmdbio verify --db ips.mmdb # "valid", or "invalid: <reason>" with exit status 16. diff
ips-new.mmdb is a newer build: Sydney is now Melbourne, 2.56.0.0/16 is
cut to 2.56.0.0/17, 9.9.9.0/24 is added and 2001:db8::/32 is removed.
mmdbio diff --old ips.mmdb --new ips-new.mmdb1Added: 2
2Removed: 2
3Modified: 1
4
5Added:
6 2.56.0.0/17
7 9.9.9.0/24
8Removed:
9 2.56.0.0/16
10 2001:db8::/32
11Modified:
12 1.1.1.0/24mmdbio diff --old ips.mmdb --new ips-new.mmdb -s -r1Subnets changed: 3
2 2.56.0.0/16 -> 2.56.0.0/17
3 9.0.0.0/8 (no data) -> 9.9.9.0/24
4 2001:db8::/32 -> 2000::/3 (no data)
5
6Records changed: 1
7 1.1.1.0/24
8 - {"asn":"13335","city":"Sydney","country":"AU","is_vpn":"false","score":"3"}
9 + {"asn":"13335","city":"Melbourne","country":"AU","is_vpn":"false","score":"3"}mmdbio diff --old ips.mmdb --new ips-new.mmdb --summary # Added: 2 | Removed: 2 | Modified: 1
mmdbio diff --old ips.mmdb --new ips-new.mmdb --json # with the full records7. completion
mmdbio completion install # bash, zsh, fish or PowerShell; open a new terminal after
mmdbio completion uninstallConfig file
1{
2 "input": "ips.csv",
3 "output": "typed.mmdb",
4 "metadata": {"database_type": "Demo-City", "description": {"en": "Demo city database"}},
5 "fields": {"types": {"asn": "uint32", "is_vpn": "boolean", "score": "uint16"}}
6}mmdbio import --config build.json
mmdbio import --config build.json --size 32 # an option on the command line wins${name} in a path comes from vars, and --var changes it for one run:
1{"vars": {"root": "~/data"}, "input": "${root}/ranges.csv", "output": "${root}/ranges.mmdb"}mmdbio import --config build.json --var root=/data/samplesPaths are relative to the config file. Keys starting with _ are notes.
examples/import-config.json shows most keys.
| Key | Same As |
|---|---|
input, or input.path, .format, .delimiter, .header, .columns | --in, --format, --delimiter, --header, --columns |
output | --out |
network, or network.column, .start_column, .end_column | --network-column, --start-column, --end-column |
lookup, or lookup.path, .key_column, .input_column | --lookup, --lookup-key, --lookup-on |
metadata.database_type, .description, .languages, .ip_version, .record_size | --title, --description, --languages, --ip, --size |
tree.alias_ipv4, .include_reserved, .fill_gaps | --alias-6to4, the opposite of --disallow-reserved, --fill-gaps |
fields.include, .exclude, .types, .infer_types, .nest_keys, .rename, .empty_if, .array_separator, .network_field, .no_fields | the import options of the same name (types is --type) |
fields.empty: "omit" | --omit-empty |
merge, order, on_error: "fail" | --merge, --order, --strict |
shrink, temp_dir, memory_mb | --shrink, --tmp-dir, --memory |
record | a record template |
Record templates
A record template builds nested records from several CSV files. It goes under
record in a config file.
1ranges.csv: start_ip,end_ip,country_id,city_name_id,latitude,radius,time_zone
2countries.csv: id,iso_code,name_id
3names.csv: id,name_en,name_de1{
2 "input": "${root}/ranges.csv",
3 "output": "${root}/city.mmdb",
4 "vars": {"root": "~/data"},
5 "order": "ipv6-first",
6 "record": {
7 "tables": {
8 "countries": "${root}/countries.csv",
9 "names": {"path": "${root}/names.csv", "key": "id", "trim": false}
10 },
11 "trim": true,
12 "types": {"latitude": "double", "accuracy_radius": "uint16"},
13 "define": {"localized": {"en": "name_en", "de": "name_de"}},
14 "skip_if": [{"value": {"$table": "countries", "$on": "country_id", "$col": "iso_code"}, "in": ["", "ZZ"]}],
15 "shape": {
16 "country": {"$table": "countries", "$on": "country_id",
17 "iso_code": "iso_code",
18 "names": {"$use": "localized", "$table": "names", "$on": "name_id"}},
19 "city": {"names": {"$use": "localized", "$table": "names", "$on": "city_name_id"}},
20 "location": {"latitude": "latitude", "accuracy_radius": {"$col": "radius", "$ceil": true}},
21 "time_zone": "time_zone"
22 }
23 }
24}A row of ranges.csv becomes:
1{"country": {"iso_code": "DE", "names": {"en": "Germany", "de": "Deutschland"}},
2 "city": {"names": {"en": "Munich", "de": "München"}},
3 "location": {"latitude": 48.137, "accuracy_radius": 20},
4 "time_zone": "Europe/Berlin"}| In Record | What It Does |
|---|---|
shape | The record. |
tables | Extra CSV files, by name. key is the id column (default: the first). |
define | Named parts, used with $use. |
types | The type of every value stored under a name. |
trim | true: remove spaces around every value. |
empty | keep (default) or omit empty values. |
invalid | When a check fails: bad (skip the row with a warning), omit (drop the value) or skip (skip the row). |
skip_if | Skip rows whose value is in the list. |
skip_empty | true: skip rows whose record is empty. |
| In Shape | What It Does |
|---|---|
"column" | That column ("column?" if it may be missing). |
{...}, [...] | A map, a list. |
12, true, {"$const": ...} | A fixed value. |
{"$table": "t", | Read the inside from table t, at the row whose id is in column. "$missing": empty, omit or skip. |
{"$use": "name"} | A part from define. |
{"$first": [...]} | The first value that isn't empty. |
{"$col": "column", | A column, with any of these steps, run in this order: $trim, $case (upper/lower), $empty_if (["0",), $strip_prefix ({"equals": "country_code",), $one_of (["a",), $chars ("A-Z0-9"), $length ([2,), $number ([-90,), $ceil, $default ("unknown"), $parse (literal or list), $type ("uint16"). |
C library
make lib # build/libmmdbio.a and include/mmdbio.h#include "mmdbio.h"
mmdbio_db *db;
if (mmdbio_open("city.mmdb", &db) == 0) {
int found;
char *json, *network;
if (mmdbio_lookup(db, "8.8.8.8", &found, &json, &network) == 0 && found)
printf("%s %s\n", network, json); /* free() both when done */
mmdbio_close(db);
}cc -Iinclude my.c build/libmmdbio.a -lm # add -lpsapi on Windowsinclude/mmdbio.h lists every function, and examples/lib_example.c is a
working example.
Upgrading from the Go version
All commands and options of the Go version (1.x) still work. What changed:
importreads CSV, TSV, JSON lists and NDJSON (gzipped or not), and takes a config file.importfollows file order: the later row wins. Whole numbers in JSON stay whole numbers.--sizedefaults to the smallest that fits (was 32), and--titleto the input file's name.exportlists networks in order and works on very large files; without an output file it writes to the screen.diffandstatscount each network once and show IPv4 networks as IPv4.- New:
verify,shrink,completion install, CSV/TSV/JSON Lines output, and files as plain arguments.
Development
make test # run the tests (needs python3)
./build-release.sh # build, test and pack every release file into dist/build-release.sh also shows how to cross-compile for 32-bit, ARM and
Windows. Pushing a tag like v2.0.0 publishes a release; a tag with a hyphen,
like v2.0.0-rc.1, is a pre-release.
License
Apache License 2.0; see LICENSE. The included zlib is under the zlib
license (third_party/zlib/LICENSE).