geoip-country-lists-package¶
Packages
Package the ipverse/rir-ip per-country IP range lists as a pinned, reproducible Nix derivation, so firewall / fail2ban / nginx-geo rules can read country IPv4/IPv6 ranges from the Nix store instead of fetching them live at runtime.
The problem¶
You want to allow or block traffic by country at the packet filter (or feed
per-country CIDR sets into fail2ban, nftables sets, an nginx geo map, etc.).
The RIRs (ARIN, RIPE, APNIC, LACNIC, AFRINIC) publish delegation statistics that
map IP allocations to countries; ipverse/rir-ip aggregates those into
per-ISO-3166 files.
The naive approach is to curl those lists at boot or on a timer. That has three
failure modes:
- Silent drift. Your firewall's behaviour changes whenever upstream (or a mirror, or a DNS answer) changes. A country's ranges shift and you only find out when something is wrongly (un)blocked. Nothing in your config history records what the rules were.
- Boot / network race. Rules meant to police the network depend on the network being up to fetch them. On a cold boot or an offline host the fetch fails and you either fail open (no rules) or fail closed (locked out).
- Not auditable. There's no diff to review when the rule set changes.
The insight¶
Country IP ranges are data, and data can be pinned like source. Wrapping the
list repo in a derivation with fetchFromGitHub { rev; sha256; } gives you:
- Reproducibility — a rebuild produces byte-identical rules; the ranges only
change when you deliberately bump
rev, which is a reviewable diff. - Boot-safety — the store path exists before any interface comes up; no fetch races the firewall it configures.
- Auditability — updating the range set is a rev+hash change in version control, not invisible runtime state.
Store layout¶
After building, the derivation exposes the upstream country/ tree under a
stable prefix:
<out>/share/geoip-country-lists/<cc>/ipv4.txt
<out>/share/geoip-country-lists/<cc>/ipv6.txt
<out>/share/geoip-country-lists/<cc>/ipv4-aggregated.txt
<out>/share/geoip-country-lists/<cc>/ipv6-aggregated.txt
<cc> is the lowercase ISO-3166 alpha-2 code (de, us, cn, …). Prefer
the *-aggregated.txt files — they merge adjacent CIDRs into the smallest set,
which means fewer rules / smaller nftables sets. Lines beginning with # are
comments and must be stripped before feeding the ranges to a rule engine.
Usage¶
As an overlay¶
# overlays/geoip-country-lists.nix
final: _: {
geoip-country-lists = final.callPackage ./geoip-country-lists-package { };
}
Consuming it (example: build an nftables/iptables allow set)¶
{ pkgs, lib, ... }:
let
lists = pkgs.geoip-country-lists;
countries = [ "de" "us" ]; # lowercase ISO codes
rangesFor = cc:
"${lists}/share/geoip-country-lists/${cc}/ipv4-aggregated.txt";
in
{
# e.g. in a service ExecStart, read + strip comments:
# grep -v '^#' ${rangesFor "de"} | while read cidr; do
# nft add element inet filter allowed_v4 "{ $cidr }"
# done
environment.etc."geoip/de-v4.txt".source = rangesFor "de";
}
The key move in any consumer: interpolate the store path, grep -v '^#' to
drop comment lines, then load each CIDR into your firewall's set. Because the
path is a store path, the ranges are fixed at build time.
Updating the pinned data¶
- Pick a new commit from
ipverse/rir-ip. - Set
revto it andsha256 = lib.fakeSha256;. - Build once; Nix reports the real hash in the
got:line — paste it back intosha256. - Nothing else to touch:
versionisfinalAttrs.src.rev, so it follows the pin automatically. - Commit. The diff is your audit record of what the range set changed to.
Caveats¶
- Coarse geolocation. This is allocation-level, country-granularity data. It
is fine for "block/allow an entire country at layer 3". It is not city- or
ASN-level and should not be used for analytics-grade geolocation — use a
MaxMind GeoLite2
.mmdbfor that. - Country geolocation is inherently imprecise. VPNs, CDNs, cloud providers, and re-allocated blocks mean a per-country filter will have false positives and negatives. Treat it as a blunt instrument, not an identity check.
- You are responsible for freshness. Pinning is the whole point — it will not
auto-update. Bump the
revon whatever cadence your threat model needs. - License. The upstream repo is MIT-licensed; the underlying RIR delegation data has its own terms. Review both before redistributing.
Files¶
default.nix— the derivation (call withcallPackage).
Source¶
packages/geoip-country-lists-package/default.nix
{
lib,
stdenv,
fetchFromGitHub,
}:
# Package the ipverse/rir-ip country IP-range lists as a pinned, reproducible
# Nix derivation. Firewall / fail2ban / nginx geo rules can then reference
# <out>/share/geoip-country-lists/<cc>/ipv4-aggregated.txt
# <out>/share/geoip-country-lists/<cc>/ipv6-aggregated.txt
# from the store instead of fetching the lists live at runtime.
#
# Why a derivation and not a runtime curl:
# - Reproducible: the ranges are pinned by `rev` + `sha256`, so a rebuild
# yields byte-identical rules. A live fetch means your firewall silently
# changes whenever upstream (or a mirror, or DNS) changes — untracked drift
# that is invisible until it wrongly (un)blocks traffic.
# - Offline / boot-safe: firewall rules that depend on a network fetch race
# the very network they are meant to police. Store paths are present before
# the interface comes up.
# - Auditable: bumping the range set becomes a reviewable rev+hash change.
#
# The RIRs (ARIN, RIPE, APNIC, LACNIC, AFRINIC) publish delegation stats; the
# ipverse/rir-ip repo aggregates them per ISO-3166 alpha-2 country code. This
# is coarse, allocation-level geolocation — fine for "block/allow a whole
# country at the packet filter", NOT for city/ASN lookup (use a MaxMind mmdb
# for that).
stdenv.mkDerivation (finalAttrs: {
pname = "geoip-country-lists";
# Derived from the pinned `rev` via `finalAttrs`, so it can never drift out of
# sync with the data: bumping the rev below bumps the version for free.
version = finalAttrs.src.rev;
src = fetchFromGitHub {
owner = "ipverse";
repo = "rir-ip";
# Pin to a specific commit, not a branch/tag. To bump: pick a new rev, set
# sha256 = lib.fakeSha256, build once, paste the "got:" hash back here.
rev = "7c8ed361db346baac03fcaa0d2965c1a12050d8e";
sha256 = "sha256-jG9FVzTGgo7WSq/Dk+pqQiwu5c2UttS6TBrovTF56bU=";
};
# Pure data — nothing to compile.
dontBuild = true;
dontConfigure = true;
# The repo lays the lists out as country/<cc>/{ipv4,ipv6,ipv4-aggregated,
# ipv6-aggregated}.txt. We expose the whole `country/` tree under a stable
# share/ prefix so consumers can build the path from a lowercased ISO code.
installPhase = ''
runHook preInstall
mkdir -p "$out/share/geoip-country-lists"
cp -r country/* "$out/share/geoip-country-lists/"
runHook postInstall
'';
meta = with lib; {
description = "Per-country IPv4/IPv6 allocation lists (ipverse/rir-ip) for firewall rules";
homepage = "https://github.com/ipverse/rir-ip";
license = licenses.mit;
platforms = platforms.all;
maintainers = [ ];
};
})