RFC 8805 Geofeed Parser Script (Python)

This Python script parses and validates an RFC 8805 self-published geofeed CSV file — the prefix,country[,region[,city[,postal]]] format network operators publish to declare where their IP blocks are actually used — aggregates prefix counts by country, and reports every syntax error with its exact line number. It makes no network request: checking a geofeed file you already downloaded is pure local parsing.

How to run this script

placeholder

  1. Download geofeed-parse.py below.
  2. Get a geofeed CSV — either your own, or a published one (network operators link theirs from their RDAP records).
  3. Run: python3 geofeed-parse.py geofeed.csv
  4. Read the syntax-error list (if any) and the per-country prefix counts.

Full source code

geofeed-parse.py Download geofeed-parse.py
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""geofeed-parse.py -- parse and validate an RFC 8805 self-published geofeed
CSV, aggregating prefix counts by country and reporting any syntax errors
with line numbers.

This recipe does NOT call the ip.xakki.pro API -- validating a geofeed file
you already downloaded is pure local parsing (stdlib csv + ipaddress), the
same honest "no API needed here" case as subnet-membership.js and
cidr-aggregate.py in this section. (Our own service ingests geofeeds as ONE
input among several for its own geo data -- see /docs/ -- but reading and
checking a geofeed file's syntax is not itself an IP lookup.)

RFC 8805 defines an unquoted CSV, one prefix per line, in this exact column
order: prefix,country[,region[,city[,postal code]]] -- only "prefix" and
"country" are required; region/city/postal may be blank or omitted
entirely. This script validates:
  - prefix parses as a valid IPv4 or IPv6 CIDR block.
  - country is exactly 2 letters (ISO 3166-1 alpha-2 shape; NOT checked
    against the real country-code list, since RFC 8805 itself doesn't
    require that and new codes do get assigned).
  - the line has between 2 and 5 comma-separated fields.

Zero dependencies -- stdlib only (csv, ipaddress, argparse, re). Python 3.8+.

Usage:
    python3 geofeed-parse.py geofeed.csv
"""

import argparse
import csv
import ipaddress
import re
from collections import Counter

COUNTRY_RE = re.compile(r"^[A-Za-z]{2}$")


def parse_geofeed(path):
    """Returns (rows, errors). rows is a list of dicts
    {prefix, country, region, city, postal, line}; errors is a list of
    "line N: message" strings for every row that failed validation."""
    rows, errors = [], []
    with open(path, newline="", encoding="utf-8") as f:
        for line_no, fields in enumerate(csv.reader(f), start=1):
            if not fields or (len(fields) == 1 and fields[0].strip() == ""):
                continue  # blank line, not a syntax error
            if not (2 <= len(fields) <= 5):
                errors.append("line {}: expected 2-5 fields (prefix,country[,region[,city[,postal]]]), got {}".format(
                    line_no, len(fields)))
                continue

            prefix_raw, country_raw = fields[0].strip(), fields[1].strip()
            try:
                prefix = ipaddress.ip_network(prefix_raw, strict=False)
            except ValueError as e:
                errors.append("line {}: invalid prefix {!r} -- {}".format(line_no, prefix_raw, e))
                continue
            if not COUNTRY_RE.match(country_raw):
                errors.append("line {}: invalid country code {!r} (expected 2 letters)".format(line_no, country_raw))
                continue

            rows.append({
                "prefix": str(prefix),
                "country": country_raw.upper(),
                "region": fields[2].strip() if len(fields) > 2 else "",
                "city": fields[3].strip() if len(fields) > 3 else "",
                "postal": fields[4].strip() if len(fields) > 4 else "",
                "line": line_no,
            })
    return rows, errors


def main():
    parser = argparse.ArgumentParser(description="Parse and validate an RFC 8805 geofeed CSV.")
    parser.add_argument("input_file", help="Path to the geofeed CSV file.")
    parser.add_argument("--top", type=int, default=20, help="How many countries to print (default: 20).")
    args = parser.parse_args()

    rows, errors = parse_geofeed(args.input_file)

    print("{} valid row(s), {} error(s)".format(len(rows), len(errors)))
    if errors:
        print("\nSyntax errors:")
        for err in errors:
            print("  " + err)

    per_country = Counter(row["country"] for row in rows)
    print("\nTop {} countries by prefix count:".format(args.top))
    for country, count in per_country.most_common(args.top):
        print("  {:6d}  {}".format(count, country))

    addresses_total = sum(ipaddress.ip_network(row["prefix"]).num_addresses for row in rows)
    print("\n{} prefixes covering {} addresses total (no overlap collapsing -- see "
          "cidr-aggregate.py in this section for that)".format(len(rows), addresses_total))


if __name__ == "__main__":
    main()

This script is MIT-licensed (SPDX-License-Identifier: MIT header at the top of the file) — copy, modify and reuse it freely in your own project, no attribution required.

RFC 8805 Geofeed Parser Script (Python)

RFC 8805 defines an unquoted CSV, exactly 2 to 5 fields per line, in a fixed column order — only "prefix" and "country" are required, region/city/postal may be blank or omitted entirely from the right. This script validates each field independently: prefix must parse as a real IPv4 or IPv6 CIDR block (ipaddress.ip_network, strict=False, so a prefix with host bits set is normalized rather than rejected — the same tolerance cidr-aggregate.py in this section applies), and country must be exactly 2 letters.

The country field is checked for SHAPE only (exactly 2 letters), never against the real ISO 3166-1 list of assigned codes — RFC 8805 itself doesn't require that, and new codes do get assigned over time, so hardcoding a country whitelist would eventually reject a legitimately new code. A 3-letter or numeric "country" value is still rejected, since that shape genuinely can't be alpha-2.

Every error is reported with its 1-based line number (matching what you'd see opening the file in a text editor), and a malformed line never aborts the run — every other valid line is still parsed and counted. This mirrors cidr-aggregate.py's same error-collection philosophy: a single bad row in a large real-world export shouldn't cost you the rest of the file's results.

Glossary

Geofeed
A self-published CSV file (RFC 8805) that a network operator uses to declare which country/region/city each of their IP prefixes is actually used in — one input among several our own service's geo data ingests, see /docs/.
RFC 8805
The IETF standard defining the geofeed CSV format this script validates against — "prefix,country[,region[,city[,postal]]]", 2 to 5 comma-separated fields.
ISO 3166-1 alpha-2
The 2-letter country-code standard ("US", "DE", "RU") a geofeed's country column follows in shape — this script checks the shape only, not membership in the current assigned list.
strict=False
The ipaddress.ip_network option that accepts a prefix with host bits set and normalizes it, instead of raising an error — the same tolerance this script and cidr-aggregate.py both apply.
RDAP geofeed link
The mechanism (an IANA-registered RDAP "remarks" or a dedicated link relation) a network operator uses to point at their own published geofeed file for a given prefix.
csv module
Python's standard-library CSV reader/writer — the only non-builtin-type dependency this script needs, used here purely for robust comma/quote splitting, never for writing output.

API equivalent

This script makes no network request and has no API equivalent — the whole task is local arithmetic/parsing over data you already have, and there is nothing for our (or any) API to answer here. If you also need live IP facts (geolocation, ASN, threat/fraud signals), that's what the full API docs cover:

Read the full API docs

Frequently asked questions

Does this script check the geofeed against the real country-code list?
No — it checks that the country field is 2 letters (the correct SHAPE), not that the specific code is currently assigned. RFC 8805 doesn't require the stricter check, and hardcoding the assigned-code list would eventually go stale as new codes are added.
What's the difference between this and cidr-aggregate.py in this section?
cidr-aggregate.py collapses/merges a plain CIDR list; this script validates and aggregates a geofeed's prefix,country,... rows specifically — different input shape, different task, though both are pure local ipaddress-module logic with no API call.
Can a geofeed row have more than 5 fields?
Not per RFC 8805 — this script flags anything outside the 2-5 field range as a syntax error with its line number, rather than silently ignoring the extra fields.
Does our own ip.xakki.pro service use geofeeds like this?
Yes — geofeed data (imported via `make geofeed`, per the project's own data pipeline) is one of several sources our geo resolution ranks by trust and specificity; see /docs/ for how the winning geo source is chosen.
What happens with a blank line or a comment in the file?
A fully blank line is silently skipped, not reported as an error — RFC 8805 doesn't define a comment syntax, so anything else on a line is parsed as real data and validated normally.

Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →