geoip-country-lists¶
Overlays
Pin a public RIR IP-allocation dataset as a build-time, content-hashed Nix derivation so that "firewall by country" allow/block rulesets are reproducible — instead of fetching the IP lists at runtime.
The problem¶
If you want to allow or block traffic per country (with ipset, nftables,
fail2ban, etc.), you need the set of IP ranges assigned to each country. The
common approaches all fetch that data at runtime:
- a boot/activation script that
curls a country-IP list off some website, - a cron job that regenerates ipsets from a downloaded file,
- a GeoIP database that a daemon reloads periodically.
Every one of those makes your firewall depend on a network fetch that:
- can fail or hang at boot (the firewall comes up wrong, or not at all),
- can silently change under you (the upstream file updates, your rules shift),
- differs machine-to-machine (each host downloads at a different moment),
- isn't captured by your config's hash, so "the same config" ≠ "the same rules".
The insight¶
The ipverse/rir-ip repo already
publishes per-country IPv4/IPv6 CIDR lists, aggregated from the Regional
Internet Registries, as plain text files in a country/<cc>/ tree. So you can
just fetchFromGitHub it at build time and pin the commit + hash.
That flips every downside above:
- No network access at boot — the lists are already in the Nix store.
- The exact data is pinned by
rev+sha256; nothing changes until you deliberately bump it. - Every machine importing this derivation gets byte-identical lists.
- The list content is part of your config's closure hash — reproducible.
Updating the data becomes an explicit, reviewable change (bump the rev), which is exactly what you want for something that decides who can reach your box.
Output layout¶
The derivation installs the country tree under a stable share path:
$out/share/geoip-country-lists/<cc>/ipv4-aggregated.txt
$out/share/geoip-country-lists/<cc>/ipv6-aggregated.txt
<cc>is a lowercase ISO 3166-1 alpha-2 code (us,de,fr, …).- The
*-aggregated.txtfiles are route-summarized — far fewer, larger CIDRs, which keeps the resulting ipset/nftables sets small and fast to load. The upstream tree also has non-aggregated variants if you need raw prefixes. - Each file has
#comment lines; strip them (grep -v '^#') before feeding the CIDRs into a tool.
Usage¶
As a package:
As an overlay (adds pkgs.geoip-country-lists):
Example: build ipsets from the lists¶
A minimal sketch of consuming the store path in a firewall script. This reads the pinned files and loads one ipset per country — no runtime download:
{ pkgs, ... }:
let
lists = pkgs.geoip-country-lists;
countries = [ "us" "de" "fr" ]; # lowercase = the on-disk dir names
base = "${lists}/share/geoip-country-lists";
in
{
networking.firewall.extraCommands = ''
${pkgs.lib.concatMapStrings (cc: ''
${pkgs.ipset}/bin/ipset create -exist country_${cc} hash:net family inet
${pkgs.ipset}/bin/ipset flush country_${cc}
if [ -f "${base}/${cc}/ipv4-aggregated.txt" ]; then
${pkgs.gnugrep}/bin/grep -v '^#' "${base}/${cc}/ipv4-aggregated.txt" \
| while read -r cidr; do
${pkgs.ipset}/bin/ipset add -exist country_${cc} "$cidr"
done
fi
'') countries}
# ... then match with: iptables -m set --match-set country_us src -j ACCEPT
'';
}
Arguments¶
Every part of the pin is an overridable callPackage argument.
| Argument | Default | Purpose |
|---|---|---|
owner / repo |
ipverse / rir-ip |
Upstream repo; point at a mirror or fork with the same country/ layout. |
rev |
a pinned commit | The snapshot. Bump this to move the data forward. |
sha256 |
hash of that commit | Must be recomputed whenever rev changes. |
version |
"2026-03-08" |
Informational label only; use the upstream snapshot date. |
Updating the pinned snapshot¶
- Set
revto a newer commit ofipverse/rir-ip. - Set
sha256 = pkgs.lib.fakeHash;(or override thesha256arg to that). - Build once; Nix errors with the real hash — paste it back into
sha256. - Bump
versionto the snapshot date for clarity.
Because the hash is baked in, this is the only moment the rules can change — which is the guarantee the whole recipe exists to give you.
Caveats¶
- RIR data is coarse and drifts. Country → IP mapping is best-effort:
ranges get reallocated, transferred between regions, and routed elsewhere by
the holder. Treat it as a broad filter, not an authority. Combine
allow/block-by-country with connection-state rules (accept
ESTABLISHED,RELATED) and explicit exceptions for your own loopback/private/VPN ranges so you don't lock yourself out. - Country codes are case-sensitive on disk. The directories are lowercase;
normalize user input (
toLower) before building the store path. - Aggregated ≠ complete-per-IP. Aggregation merges adjacent prefixes; it's the right default for firewalls but don't treat a single CIDR as an exact organizational boundary.
- The upstream data is MIT-licensed; this recipe just repackages it.
Source¶
overlays/geoip-country-lists/default.nix
# geoip-country-lists
#
# Pin a public RIR (Regional Internet Registry) IP-allocation repo as a
# build-time, content-hashed Nix derivation. The output is a directory of
# per-country IPv4/IPv6 CIDR lists that firewall / fail2ban / ipset configs
# can read from the store, so a "block/allow by country" ruleset is fully
# reproducible instead of depending on a runtime download.
#
# Usage as a package:
#
# pkgs.callPackage ./geoip-country-lists { }
#
# Usage as an overlay (see ./overlay.nix for the tiny wrapper):
#
# nixpkgs.overlays = [ (import ./geoip-country-lists/overlay.nix) ];
# # then reference pkgs.geoip-country-lists anywhere
#
# The build output layout is:
#
# $out/share/geoip-country-lists/<cc>/ipv4-aggregated.txt
# $out/share/geoip-country-lists/<cc>/ipv6-aggregated.txt
#
# where <cc> is a lowercase ISO 3166-1 alpha-2 country code. The
# "-aggregated" files are route-summarized (fewer, larger CIDRs) which keeps
# ipset/nftables sets small; there are also non-aggregated variants in the
# upstream tree if you want the raw prefixes.
#
# To update: bump `rev` to a newer commit of ipverse/rir-ip, set `sha256` to
# lib.fakeHash, build once, and copy the real hash Nix prints back in. Because
# the hash is baked into the derivation, every machine that imports this gets
# byte-identical lists — the whole point of doing it at build time.
{
lib,
stdenv,
fetchFromGitHub,
# Pin of the upstream RIR IP data repo. Override these to move to a newer
# snapshot (or to point at a mirror/fork with the same `country/` layout).
owner ? "ipverse",
repo ? "rir-ip",
rev ? "7c8ed361db346baac03fcaa0d2965c1a12050d8e",
sha256 ? "sha256-jG9FVzTGgo7WSq/Dk+pqQiwu5c2UttS6TBrovTF56bU=",
# Version label — informational only; use the upstream snapshot date.
version ? "2026-03-08",
}:
stdenv.mkDerivation {
pname = "geoip-country-lists";
inherit version;
src = fetchFromGitHub {
inherit
owner
repo
rev
sha256
;
};
# It's pure data — nothing to compile.
dontBuild = true;
dontConfigure = true;
installPhase = ''
runHook preInstall
mkdir -p $out/share/geoip-country-lists
cp -r country/* $out/share/geoip-country-lists/
runHook postInstall
'';
meta = with lib; {
description = "Country-specific IP address lists for firewall configurations";
longDescription = ''
Per-country IPv4/IPv6 CIDR allocation lists derived from the Regional
Internet Registries, packaged as a build-time Nix derivation so that
firewall-by-country rulesets are reproducible and content-addressed
rather than fetched at runtime.
'';
homepage = "https://github.com/ipverse/rir-ip";
license = licenses.mit;
platforms = platforms.all;
maintainers = [ ];
};
}