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.
placeholder
#!/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 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.
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:
Like this tool? Our API gives free accounts 1,000 requests/day.See the API docs →