# originet > Map IP addresses to origin ASNs with CAIDA's RouteViews prefix-to-AS snapshots originet maps IP addresses to the Autonomous System that originates them, using CAIDA's daily RouteViews prefix-to-AS snapshots. One Rust binary and a Python package, with no C library to install. # Documentation # ๐ŸŒ originet Map IP addresses to the Autonomous System that originates them, using CAIDA's daily RouteViews prefix-to-AS snapshots. (Formerly `map-ip-to-asn`, a Python tool on pyipmeta.) One binary, no C library: it picks the snapshot for the day you ask for, downloads it once, builds a longest-prefix-match trie and answers a million addresses in about two seconds. A Python package exposes the same table for bulk lookups. [llms.txt](https://estcarisimo.github.io/originet/llms.txt) is the documentation index for AI agents. ## โœจ Features - ๐Ÿ” **Longest-prefix match**: every answer carries the most specific prefix that covers the address, its origin ASN and, for multi-origin prefixes and AS sets, all origins in CAIDA's notation - ๐Ÿ“… **Any day since 2005**: `--date` resolves the newest snapshot on or before that day through CAIDA's index and monthly listings; the default is the newest snapshot - ๐ŸŒ **IPv4 and IPv6 in one run**: each family's snapshot is loaded only when the input needs it - ๐Ÿ“ฆ **Batch by default**: addresses from arguments, a file or standard input; a colored table on the terminal (`--color never` for plain), JSON, JSON Lines, CSV, TSV or Parquet out - ๐Ÿ’พ **Cached and offline-capable**: snapshots are downloaded once (atomically, with a progress bar) into a cache; `--fetch` warms it, `--cache` lists and prunes it, `--offline` never touches the network; `--pfx2as` uses your own file - ๐Ÿ **Python bindings**: `Table.from_caida()` / `Table.from_files()` and `lookup_many()` with the GIL released - ๐Ÿฆ€ **Single binary, no C stack**: no libipmeta, no pyipmeta, no scraping; `unsafe_code = "forbid"` - ๐Ÿณ **Docker image**: multi-stage build, non-root, cache volume ## ๐Ÿš€ Quick Start ### CLI From [crates.io](https://crates.io/crates/originet), with a Rust toolchain (1.88 or newer): ``` cargo install originet --locked originet 8.8.8.8 2001:4860::8888 ``` The same from the repository's `main` instead of the latest release: ``` cargo install --git https://github.com/estcarisimo/originet --locked ``` Or from source: ``` git clone https://github.com/estcarisimo/originet.git && cd originet cargo build --release --locked # target/release/originet ``` Prebuilt binaries for Linux (x86_64, aarch64, static musl), macOS (Intel, Apple Silicon) and Windows are attached to every [tagged release](https://github.com/estcarisimo/originet/releases), with SHA-256 checksums. ### Python From [PyPI](https://pypi.org/project/originet/), with prebuilt wheels for Linux (x86_64, aarch64; glibc and musl), macOS (Intel, Apple Silicon) and Windows on Python 3.10 or newer: ``` uv pip install originet # or: pip install originet ``` Or the repository's `main` (builds the extension, so it needs Rust 1.88+): ``` uv pip install "originet @ git+https://github.com/estcarisimo/originet#subdirectory=python" ``` ### Docker ``` docker build -t originet . docker run --rm -v originet-cache:/cache originet 8.8.8.8 ``` ## ๐Ÿ“– Usage ``` # One or more addresses: a table on the terminal (the JSON report when piped), summary on stderr originet 8.8.8.8 1.1.1.1 2606:4700::1111 originet --format json 8.8.8.8 # A file (one address per line, # comments allowed) to CSV originet --file ips.txt --format csv --output results.csv # Standard input, JSON Lines out: the shape for pipelines cat ips.txt | originet -f - -F jsonl > results.jsonl # The routing table of a given day (newest snapshot on or before it) originet --date 2015-01-01 --file ips.txt -F parquet -o ips-2015.parquet # Your own snapshot files (plain or gzip), no download originet --pfx2as routeviews-rv2-20261021-0800.pfx2as.gz --pfx2as6 routeviews-rv6-20261021-1200.pfx2as.gz -f ips.txt # Never touch the network; fail unless everything needed is cached originet --offline --date 2024-02-29 8.8.8.8 # Fail (exit 1) if any input is not an IP address; show what is fetched originet --strict -v -f ips.txt ``` Every output format has the same six columns: `ip`, `prefix`, `asn`, `origins`, `snapshot`, `error`. `asn` is the first ASN of `origins`; `origins` is `15169`, `149511_138421` (several ASes announce the prefix) or `64496,64497` (an AS set). An address no prefix covers gets null `prefix` and `asn`; an input that is not an address gets an `error`. The JSON report also lists the snapshot files used, their source and how many prefixes each had. Details: [output formats](https://estcarisimo.github.io/originet/output/), [the data](https://estcarisimo.github.io/originet/data/). ### ๐Ÿ Python ``` from originet import Table table = Table.from_caida("2024-01-01") # newest snapshot on or before that day, cached table = Table.from_files(ipv4="routeviews-rv2-20261021-0800.pfx2as.gz") # or your own file table.lookup("8.8.8.8") # Match(ip='8.8.8.8', prefix='8.8.8.0/24', asn=15169, origins='15169') table.lookup_many(["1.1.1.1", "10.0.0.1", "2001:4860::8888"]) # GIL released # [Match(ip='1.1.1.1', prefix='1.1.1.0/24', asn=13335, origins='13335'), None, Match(...)] table.lookup_asns(["1.1.1.1", "bogus"], strict=False) # just the ASNs # [13335, None] table.snapshots # [{'family': 'ipv4', 'source': 'caida', 'date': '2024-01-01', ...}] ``` ## ๐Ÿ”ง Configuration | Setting | Effect | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--cache-dir DIR` / `ORIGINET_CACHE_DIR` | Where indexes, listings and snapshots are kept. Default: the platform cache directory (`~/.cache/originet` on Linux, `~/Library/Caches/originet` on macOS, `/cache` in the image) | | `--offline` | Never download; error if the index, listing or snapshot is not cached | | `--refresh` | Fetch the snapshot index again even if the cached copy is less than six hours old | | `--cache list\|size\|prune` | Show what the cache holds and its size, or prune it: `--keep-days N`, `--keep-last N`, `--dry-run` | | `-v`, `-vv`, `RUST_LOG` | Log what is fetched or read from the cache | The index and the current month's listing are refreshed after six hours; snapshot files and past months never change and are cached forever. ## ๐Ÿ—๏ธ Architecture ``` src/ โ”œโ”€โ”€ main.rs # CLI (clap): arguments, orchestration, stderr summary, exit codes โ”œโ”€โ”€ pfx2as.rs # Parser of CAIDA's pfx2as format; Origins (A, A_B, A,B); gzip by magic bytes โ”œโ”€โ”€ table.rs # Longest-prefix-match table (one prefix-trie per family) โ”œโ”€โ”€ caida.rs # Dataset layout, rolling index and monthly listings, date selection โ”œโ”€โ”€ fetch.rs # Cached, atomic downloads; index freshness; offline mode โ”œโ”€โ”€ cache.rs # Inventory of the cache; pruning by age or count โ”œโ”€โ”€ load.rs # Local file or CAIDA snapshot into a table, with provenance โ”œโ”€โ”€ input.rs # Address parsing (/32, /128, IPv4-mapped IPv6, comments) โ””โ”€โ”€ output.rs # Records, summary, report; JSON, JSON Lines, CSV, TSV, Parquet python/ โ”œโ”€โ”€ src/lib.rs # PyO3 module originet._native: Table, Match โ”œโ”€โ”€ originet/ # Package, typed stub, py.typed โ””โ”€โ”€ tests/ # pytest suite ``` The crate is also a library (`originet`); see [Rust library](https://estcarisimo.github.io/originet/library/). ## ๐Ÿงช Development ``` cargo build && cargo test --workspace --all-features cargo fmt --all && cargo clippy --workspace --all-targets --all-features -- -D warnings cargo deny check # cargo install cargo-deny cd python && uv sync --locked && uv run pytest -q && uv run ruff check . && uv run mypy uvx --with-requirements docs/requirements.txt mkdocs build --strict ``` Tests never touch the network: they run against `tests/fixtures/` and a seeded cache. `CONTRIBUTING.md` has the full checklist and the release procedure. ## ๐Ÿ“Š Example Output ``` $ originet -F csv 8.8.8.8 1.1.1.1 1.1.8.9 10.0.0.1 ip,prefix,asn,origins,snapshot,error 8.8.8.8,8.8.8.0/24,15169,15169,2026-10-21, 1.1.1.1,1.1.1.0/24,13335,13335,2026-10-21, 1.1.8.9,1.1.8.0/24,149511,149511_138421,2026-10-21, 10.0.0.1,,,,2026-10-21, 4 addresses: 3 found, 1 not found, 0 invalid (ipv4 2026-10-21; 407.8ms) $ originet --date 2005-06-01 -F csv 8.8.8.8 ip,prefix,asn,origins,snapshot,error 8.8.8.8,8.0.0.0/8,3356,3356,2005-06-01, 1 addresses: 1 found, 0 not found, 0 invalid (ipv4 2005-06-01; 2.0s) $ originet -f ips-1M.txt -F jsonl -o out.jsonl 1000000 addresses: 836003 found, 163997 not found, 0 invalid (ipv4 2026-10-21; 1.7s) ``` The first run for a day downloads the snapshot (about 4 MB for IPv4, 1 MB for IPv6); after that the time is loading 1.1 M prefixes into the trie plus the lookups. The 1 M figure is one million random IPv4 addresses on a 2023 laptop, snapshot already cached. ## ๐Ÿค Contributing Contributions are welcome. Read [CONTRIBUTING.md](https://github.com/estcarisimo/originet/blob/main/CONTRIBUTING.md) first, then: 1. Fork the repository 1. Create a branch (`git checkout -b feat/my-change`) 1. Commit with tests and a `CHANGELOG.md` entry 1. Push the branch 1. Open a pull request (every PR is reviewed per `.github/REVIEW.md`) ## ๐Ÿ“„ License MIT, see [LICENSE](https://github.com/estcarisimo/originet/blob/main/LICENSE). The data is CAIDA's and keeps [its own terms](https://www.caida.org/catalog/datasets/routeviews-prefix2as/). ## ๐Ÿ”— Related Resources - [Documentation site](https://estcarisimo.github.io/originet/) - [llms.txt](https://estcarisimo.github.io/originet/llms.txt) for AI agents ([full text](https://estcarisimo.github.io/originet/llms-full.txt)) - [CAIDA Routeviews Prefix to AS mappings Dataset (pfx2as)](https://www.caida.org/catalog/datasets/routeviews-prefix2as/) - [RouteViews](https://www.routeviews.org/) - [CAIDA libipmeta](https://github.com/CAIDA/libipmeta), the C library the 0.x Python version wrapped ## ๐Ÿ™ Acknowledgements This tool reads **The CAIDA UCSD Routeviews Prefix to AS mappings Dataset (pfx2as) for IPv4 and IPv6**, https://www.caida.org/catalog/datasets/routeviews-prefix2as/, derived from RouteViews BGP data. Please cite the dataset in work that uses this tool; `CITATION.cff` has the entries for both. # Command line ``` $ originet --help Map IP addresses to origin ASNs with CAIDA's RouteViews prefix-to-AS snapshots. Addresses come from the command line, from a file (`--file`, one per line) or from standard input (`--file -`). The matching snapshot is downloaded from CAIDA on first use and cached; `--date` picks the newest snapshot on or before a day, and `--pfx2as`/`--pfx2as6` use local files instead of downloading. `--fetch` only fills the cache; `--cache list|size|prune` inspects or prunes it. Usage: originet [OPTIONS] [IP]... Arguments: [IP]... IP addresses to look up (IPv4 or IPv6) Options: -f, --file Read addresses from a file, one per line (`-` for standard input) -d, --date Use the newest snapshot taken on or before this day (YYYY-MM-DD). Default: the newest --pfx2as Local IPv4 pfx2as file (plain or gzip) instead of downloading from CAIDA --pfx2as6 Local IPv6 pfx2as file (plain or gzip) instead of downloading from CAIDA -F, --format Output format. Default: `table` on a terminal, `json` when piped or with `--output` Possible values: - table: Aligned columns for reading in a terminal, results only; `asn` lists every origin - json: One JSON document with metadata, summary and results - jsonl: One JSON object per line, results only - csv: Comma-separated values with a header, results only - tsv: Tab-separated values with a header, results only - parquet: Apache Parquet, results only -o, --output Write the output to this file instead of standard output --color Color the table, the summary and the progress bar: `auto` only on a terminal (and not with `NO_COLOR`), `always` even into a pipe or file, `never` for plain text [default: auto] [possible values: auto, always, never] --cache-dir Directory for downloaded indexes and snapshots [env: ORIGINET_CACHE_DIR=] --offline Never download; fail unless the index and snapshot are already cached --refresh Re-download the snapshot index even if the cached copy is fresh --fetch Download the IPv4 and IPv6 snapshots for the day (`--date`, default the newest) into the cache and exit without looking anything up; print one line per snapshot with its date, whether it was already cached, and its path. With `--offline` it only checks that they are cached --cache Inspect or prune the cache and exit without looking anything up. `list` prints one line per cached snapshot (family, date, size, path); `size` prints the snapshot count and size per family and in total; `prune` removes the snapshots that `--keep-days` and `--keep-last` do not keep (one of them is required) and then the month and year directories that are left empty. Indexes and anything not placed there by originet are never touched Possible values: - list: Every cached snapshot with its family, date, size and path, then a summary - size: The number of snapshots and the size per family, and in total - prune: Remove the snapshots `--keep-days`/`--keep-last` do not keep (`--dry-run` only reports) --keep-days With `--cache prune`: remove the snapshots taken more than N days ago (by the day in the file name; `0` keeps only today's) --keep-last With `--cache prune`: keep only the N newest snapshots of each family --dry-run With `--cache prune`: print what would be removed and remove nothing --strict Exit with status 1 when any input is not an IP address -q, --quiet Do not print the summary to standard error -v, --verbose... More detail on standard error (`-v` what is fetched, `-vv` debugging) -h, --help Print help (see a summary with '-h') -V, --version Print version Examples: originet 8.8.8.8 2001:4860::8888 originet --format json 8.8.8.8 2001:4860::8888 originet --file ips.txt --format csv --output results.csv originet --date 2019-06-01 --file ips.txt --format parquet -o june-2019.parquet originet --pfx2as routeviews-rv2-20261021-0800.pfx2as.gz --file ips.txt cat ips.txt | originet -f - -F jsonl originet --fetch --date 2019-06-01 originet --cache list originet --cache prune --keep-days 30 --dry-run ``` ## Inputs Addresses can be given as arguments and, at the same time, from `--file` (one per line; blank lines and `#` comments are ignored, also at the end of a line; `-` reads standard input). Each one is an IPv4 or IPv6 address, optionally with a `/32` or `/128` host suffix; an IPv4-mapped IPv6 address (`::ffff:8.8.8.8`) is looked up as IPv4. Anything else is kept in the output as a row with an `error` and counted as *invalid*; `--strict` turns that into exit status 1. Order and duplicates are preserved: the output has exactly one row per input, in the same order. ## Which snapshot | Option | Snapshot used | | --------------------------- | ---------------------------------------------------------------------------------------------------- | | none | the newest one CAIDA has published | | `--date 2024-02-29` | the newest one taken on or before 2024-02-29 (a warning names it when the day itself has none) | | `--pfx2as FILE` | that file for IPv4 (plain or gzip); the date comes from the file name when it follows CAIDA's naming | | `--pfx2as6 FILE` | that file for IPv6 | | `--fetch` | downloads the day's IPv4 and IPv6 snapshots into the cache and exits, looking nothing up | | `--cache list\|size\|prune` | none: inspects or prunes the cache and exits (see [The cache](#the-cache)) | Only the families present in the input are loaded: an IPv4-only input never fetches the IPv6 snapshot. `--date` applies to the families fetched from CAIDA; a family read from `--pfx2as` or `--pfx2as6` keeps the file's own date, and the tool warns when both are given. See [The data](https://estcarisimo.github.io/originet/data/index.md) for how dates are resolved and cached. `--fetch` fills the cache without a lookup: one line per snapshot on standard output with its date, `downloaded` or `cached`, and its path. Run it once (or in a cron job, an image build, a CI step) and later lookups for the same day need no network; with `--offline` it only checks that the snapshots are there. Addresses, `--file`, `--pfx2as`, `--format`, `--output` and `--strict` do not combine with it. ``` $ originet --fetch --date 2026-10-19 --cache-dir ./cache ipv4 2026-10-19 downloaded ./cache/routeviews-prefix2as/2026/10/routeviews-rv2-20261019-1200.pfx2as.gz ipv6 2026-10-19 downloaded ./cache/routeviews6-prefix2as/2026/10/routeviews-rv6-20261019-1200.pfx2as.gz 2 snapshots ready in ./cache (ipv4 2026-10-19 downloaded, ipv6 2026-10-19 downloaded; 6.1s) ``` ## The cache Snapshots never change, so every day you ask for stays in the cache (about 5 MB per day for both families) until you prune it. `--cache` does that without a lookup and exits; it takes no addresses and never touches the network. - `--cache list` prints one line per cached snapshot (family, date, size, path) on standard output and a summary on standard error. - `--cache size` prints the number of snapshots and the size per family and in total; the size counts every file under the dataset directories, so the indexes and the monthly listings (a few KB) are included. The per-family figures in the `list` and `prune` summaries are the same whole-directory sizes, while a prune's `kept (โ€ฆ)` is the kept snapshots' own size. - `--cache prune` removes the snapshots that `--keep-days N` (taken more than N days ago, by the day in the file name, counted from today in UTC) and `--keep-last N` (all but the N newest of each family) do not keep. One of the two is required; with both, a snapshot stays only when both rules keep it. `--dry-run` prints what would go and removes nothing. A month directory left with nothing but its listing is removed with the listing, then the emptied year directory; the index files and anything originet did not put there (a file of yours, a download in progress) are never touched. A snapshot is recognized only by CAIDA's exact file name for its family (`routeviews-rv2-YYYYMMDD-HHMM.pfx2as.gz`, `routeviews-rv6-...`) in the directory of its own month. A symbolic link standing in for a whole dataset directory is followed. ``` $ originet --cache list --cache-dir ./cache ipv4 2019-06-01 2.9 MB ./cache/routeviews-prefix2as/2019/06/routeviews-rv2-20190601-0600.pfx2as.gz ipv4 2026-10-19 3.9 MB ./cache/routeviews-prefix2as/2026/10/routeviews-rv2-20261019-1200.pfx2as.gz ipv4 2026-10-21 3.9 MB ./cache/routeviews-prefix2as/2026/10/routeviews-rv2-20261021-0800.pfx2as.gz ipv6 2019-06-01 289.2 KB ./cache/routeviews6-prefix2as/2019/06/routeviews-rv6-20190601-1200.pfx2as.gz ipv6 2026-10-19 907.9 KB ./cache/routeviews6-prefix2as/2026/10/routeviews-rv6-20261019-1200.pfx2as.gz ipv6 2026-10-21 907.9 KB ./cache/routeviews6-prefix2as/2026/10/routeviews-rv6-20261021-1200.pfx2as.gz 6 snapshots, 12.8 MB in ./cache (ipv4 3 snapshots 10.7 MB, ipv6 3 snapshots 2.1 MB; 2.6ms) $ originet --cache size --cache-dir ./cache ipv4 3 snapshots 10.7 MB ipv6 3 snapshots 2.1 MB total 6 snapshots 12.8 MB ./cache $ originet --cache prune --keep-days 30 --dry-run --cache-dir ./cache ipv4 2019-06-01 2.9 MB would remove ./cache/routeviews-prefix2as/2019/06/routeviews-rv2-20190601-0600.pfx2as.gz ipv6 2019-06-01 289.2 KB would remove ./cache/routeviews6-prefix2as/2019/06/routeviews-rv6-20190601-1200.pfx2as.gz 2 snapshots would be removed (3.2 MB), 4 kept (9.6 MB) in ./cache (ipv4 3 snapshots 10.7 MB, ipv6 3 snapshots 2.1 MB; 1.7ms) $ originet --cache prune --keep-days 30 --cache-dir ./cache ipv4 2019-06-01 2.9 MB removed ./cache/routeviews-prefix2as/2019/06/routeviews-rv2-20190601-0600.pfx2as.gz ipv6 2019-06-01 289.2 KB removed ./cache/routeviews6-prefix2as/2019/06/routeviews-rv6-20190601-1200.pfx2as.gz 2 snapshots removed (3.2 MB), 4 kept (9.6 MB) in ./cache (ipv4 2 snapshots 7.8 MB, ipv6 2 snapshots 1.8 MB; 1.6ms) ``` The summary lines go to standard error (`-q` drops them); the per-snapshot lines are `family date size [status] path`, so `awk` can read them. On a terminal the paths are dimmed and the status colored; `--color` applies. Passing a prune flag without `--cache prune`, or `--cache prune` without `--keep-days` or `--keep-last`, is a usage error (exit status 2). ## Outputs `-F/--format` is `table`, `json`, `jsonl`, `csv`, `tsv` or `parquet`. Without it, a terminal gets the `table` and a pipe or `-o/--output` gets `json`, so `originet 8.8.8.8` reads well at the prompt and `originet 8.8.8.8 | jq` keeps working; pass `-F` to choose regardless. Parquet is refused on a terminal. The columns are the same in every format; see [Output formats](https://estcarisimo.github.io/originet/output/index.md). The table has five columns: `asn` lists every origin of the prefix (`149511, 138421` for a multi-origin prefix, `{8035, 13979}` for an AS set) in place of the separate `asn` and `origins` of the other formats. On a terminal it is colored (bold header, cyan prefix, bold green ASN, yellow multi-origin lists, dim dates and dashes, red invalid inputs), as are the counts of the summary line. `--color never` gives plain text, `--color always` keeps the escapes even into a pipe or `--output`, and `auto` (the default) also honors `NO_COLOR`, `CLICOLOR_FORCE` and `TERM=dumb`. Only the table is ever colored. Standard error gets a one-line summary, warnings, and with `-v` the downloads and cache hits. When it is a terminal, a progress bar shows a snapshot downloading (size, speed, time left) and a spinner shows it loading; `-q` silences the summary and the bars. ## Exit status `0` done (invalid inputs are rows, not failures); `1` an error (no input, unreadable file, snapshot not available, `--strict` with invalid inputs); `2` a usage error. ## Examples ``` # Enrich a log: JSON Lines in, JSON Lines out, joined on ip cut -d' ' -f1 access.log | sort -u | originet -f - -F jsonl -q > ip2asn.jsonl # Compare today's and 2019's origin for the same addresses originet -F csv -f ips.txt -o now.csv originet -F csv -f ips.txt --date 2019-06-01 -o 2019.csv # Keep a cache in the project and never download during a run originet --cache-dir ./cache --fetch # warms the cache originet --cache-dir ./cache --offline -f ips.txt -F parquet -o out.parquet # See what the cache holds and keep only the last ten days originet --cache size originet --cache prune --keep-days 10 --dry-run # what would go originet --cache prune --keep-days 10 ``` # Python The `originet` package wraps the Rust table with [PyO3](https://pyo3.rs). It has no Python dependencies and ships type stubs (`py.typed`). ## Install From [PyPI](https://pypi.org/project/originet/): ``` uv pip install originet # or: pip install originet ``` Wheels are published for Linux x86_64 (manylinux2014) and aarch64 (manylinux_2_28), Linux with musl, such as Alpine (musllinux_1_2, x86_64 and aarch64), macOS (Intel, Apple Silicon) and Windows, for CPython 3.10 and newer (abi3), and are also attached to each [GitHub release](https://github.com/estcarisimo/originet/releases). Anything else falls back to the sdist, which compiles the extension and needs a Rust toolchain 1.88+. The repository's `main` installs the same way: ``` uv pip install "originet @ git+https://github.com/estcarisimo/originet#subdirectory=python" ``` ## Loading a table ``` from originet import Table table = Table.from_caida() # newest IPv4 + IPv6 snapshots table = Table.from_caida("2019-06-01", ipv6=False) # newest on or before that day, IPv4 only table = Table.from_caida(cache_dir="/data/pfx2as", offline=True) # only what is cached table = Table.from_files(ipv4="routeviews-rv2-20261021-0800.pfx2as.gz", ipv6="routeviews-rv6-20261021-1200.pfx2as.gz") ``` `from_caida` takes `date` (`"YYYY-MM-DD"` or `None`), and keyword-only `cache_dir`, `ipv4`, `ipv6`, `offline`, `refresh`. It raises `ValueError` for a bad date or a date before the dataset, `OSError` when offline and something is not cached or a file cannot be read, and `RuntimeError` for a failed download. Loading the IPv4 snapshot (1.1 M prefixes) takes about a second; the table is immutable afterwards and safe to share between threads. `table.snapshots` lists what was loaded: ``` >>> table.snapshots [{'family': 'ipv4', 'source': 'caida', 'date': '2026-10-21', 'file': 'routeviews-rv2-20261021-0800.pfx2as.gz', 'location': 'https://publicdata.caida.org/datasets/routing/routeviews-prefix2as/2026/10/routeviews-rv2-20261021-0800.pfx2as.gz', 'prefixes': 1129523}, ...] >>> table.len_v4, table.len_v6, len(table) (1129523, 265790, 1395313) ``` ## Looking up ``` >>> m = table.lookup("1.1.8.9") >>> m Match(ip='1.1.8.9', prefix='1.1.8.0/24', asn=149511, origins='149511_138421') >>> m.asn, m.prefix, m.origins, m.asns(), m.is_multi_origin, m.is_as_set (149511, '1.1.8.0/24', '149511_138421', [149511, 138421], True, False) >>> table.lookup("10.0.0.1") is None True >>> table.lookup("not an ip") ValueError: "not an ip": invalid IP address syntax ``` For many addresses use the bulk methods; they parse and look up in Rust with the GIL released, so other threads keep running: ``` hits = table.lookup_many(ips) # list[Match | None], same order as ips asns = table.lookup_asns(ips) # list[int | None], cheaper when only the ASN matters asns = table.lookup_asns(ips, strict=False) # invalid inputs become None instead of raising ``` `Match` objects are immutable, hashable and compare by `ip`, `prefix` and `origins`. ### With pandas ``` import pandas as pd from originet import Table df = pd.read_csv("flows.csv") table = Table.from_caida("2024-01-01") hits = table.lookup_many(df["dst_ip"].astype(str).tolist(), strict=False) df["asn"] = [h.asn if h else None for h in hits] df["prefix"] = [h.prefix if h else None for h in hits] ``` ## Development ``` cd python uv sync --locked # builds the extension in editable mode uv run pytest -q uv run ruff check . && uv run ruff format --check . && uv run mypy ``` The tests use the fixtures in `../tests/fixtures/` and never touch the network. On an Apple Silicon Mac the Rust toolchain must be the arm64 one (rustup); an x86_64 Homebrew toolchain cannot build the extension for an arm64 Python. # The data ## Where the snapshots come from CAIDA derives a prefix-to-origin-AS mapping from RouteViews BGP tables once a day and publishes it as the *Routeviews Prefix to AS mappings Dataset (pfx2as)*: | Family | Directory on `publicdata.caida.org` | File names | | ------ | ------------------------------------------------- | ---------------------------------------- | | IPv4 | `datasets/routing/routeviews-prefix2as/YYYY/MM/` | `routeviews-rv2-YYYYMMDD-HHMM.pfx2as.gz` | | IPv6 | `datasets/routing/routeviews6-prefix2as/YYYY/MM/` | `routeviews-rv6-YYYYMMDD-HHMM.pfx2as.gz` | Each dataset directory also has an index, `pfx2as-creation.log`, listing the files published in roughly the last thirty days (`seqnum`, Unix timestamp, path). For older days originet reads the monthly directory listings (`YYYY/MM/`), which exist back to May 2005 for IPv4 and 2007 for IPv6 (the oldest files are named `routeviews-oix-...`). ## Snapshot selection - No `--date`: the newest snapshot in the index. - `--date YYYY-MM-DD` inside the index window: the newest snapshot taken **on or before** that day. If the day itself has no snapshot the closest earlier one is used and a warning names it. - `--date` older than the index: the listing of that month, then the previous months, up to twelve, until one has a snapshot on or before the day. A date before 2005 is an error that says so; a gap longer than twelve months is an error that says how far back it looked. The index is cached and reused for six hours, then fetched again (`--refresh` forces it). A month's listing is final once it was fetched after that month ended (UTC) and is then kept forever; one fetched while the month was still running follows the six-hour rule. Snapshot files never change once published, so they are cached forever. ## The `pfx2as` format One line per announced prefix, tab-separated: ``` 1.0.0.0 24 13335 1.1.8.0 24 149511_138421 64.0.0.0 8 64496,64497 2001:4:112:: 48 112 ``` The third field is one origin ASN, several joined with `_` (a **multi-origin** prefix, MOAS: several ASes announce it) or several joined with `,` (an **AS set**). originet keeps the field verbatim in `origins` and reports the first ASN as `asn`. Nothing in the dataset ranks the origins of a MOAS prefix, so treat `asn` as "one of them" and look at `origins` (or `Match.asns()` in Python) when it matters. ## Longest-prefix match Every lookup returns the most specific prefix in the snapshot that covers the address, the same rule routers apply. `prefix` in the output is that prefix. An address that no prefix covers (private space, unrouted space, or space not seen by RouteViews that day) gets `prefix` and `asn` set to null. An IPv4-mapped IPv6 address (`::ffff:8.8.8.8`) is looked up as IPv4. A `/32` or `/128` host suffix is accepted; any other prefix length is rejected, because the tool maps addresses, not prefixes. ## Cache | Platform | Default directory | | ------------ | -------------------------------- | | Linux | `~/.cache/originet/` | | macOS | `~/Library/Caches/originet/` | | Windows | `%LOCALAPPDATA%\originet\cache\` | | Docker image | `/cache` | Override it with `--cache-dir` or `ORIGINET_CACHE_DIR`. Inside, files mirror CAIDA's layout: `routeviews-prefix2as/pfx2as-creation.log`, `routeviews-prefix2as/2026/10/routeviews-rv2-20261021-0800.pfx2as.gz`, a parsed listing per month visited (`routeviews-prefix2as/2015/01/.listing`), and the same under `routeviews6-prefix2as/`. A snapshot is about 4 MB (IPv4) and 1 MB (IPv6) compressed. Downloads go to a temporary file and are renamed into place, so an interrupted run never leaves a truncated snapshot behind. Nothing expires on its own: every day you ask for stays, about 5 MB per day for both families, and the same day is never downloaded twice. `--cache list` and `--cache size` show what is there; `--cache prune --keep-days N` or `--keep-last N` (with `--dry-run` to look first) drops the old snapshots and the month and year directories they leave empty, and never touches the indexes or anything else (see [The cache](https://estcarisimo.github.io/originet/cli/#the-cache)). Deleting by hand works too: a month directory (`routeviews-prefix2as/2019/06/`) drops its snapshots, the whole cache directory starts over. `--fetch --offline --date DAY` tells whether a day is cached without downloading. ## Terms of use The dataset is CAIDA's; its [terms of use](https://www.caida.org/catalog/datasets/routeviews-prefix2as/) apply to anything you derive from it, and CAIDA asks to be cited (see [Citing](https://estcarisimo.github.io/originet/citing/index.md)). # Output formats Every format carries the same six columns per input, in input order: | Column | Meaning | | ---------- | ------------------------------------------------------------------------------------------------ | | `ip` | The address, normalized (`::ffff:8.8.8.8` becomes `8.8.8.8`; invalid inputs are kept as typed) | | `prefix` | The longest matching prefix, or null when none covers the address | | `asn` | The origin ASN: the first ASN of the `origins` field, or null | | `origins` | All origins in CAIDA's notation: `15169`, `149511_138421` (multi-origin), `64496,64497` (AS set) | | `snapshot` | The date of the snapshot that answered (`YYYY-MM-DD`), when known | | `error` | Why the input could not be looked up; only set for inputs that are not IP addresses | Null is `null` in JSON, an empty field in CSV/TSV, `-` in the table and a real null in Parquet. The table is the one exception to the six columns: being for people, it folds `origins` into `asn` and shows every origin there. Without `-F/--format`, the `table` is used when standard output is a terminal and `json` when it is a pipe or `--output` names a file. ## `table` Aligned columns for reading at the prompt, results only (the snapshots and totals go to standard error as the summary line). Five columns: `ip`, `prefix`, `asn`, `snapshot`, `error`. Instead of the first origin, `asn` lists every origin of the prefix, so a multi-origin prefix (`149511_138421` in CAIDA's notation) reads `149511, 138421` and an AS set (`8035,13979`) reads `{8035, 13979}`, BGP's own notation for a set; the full `origins` string is in the other formats. A missing value is `-`; the `error` column is empty unless the input was invalid. On a terminal it is colored (multi-origin lists in yellow); `--color never` makes it plain and `--color always` keeps the colors in a pipe or a file (see [Command line](https://estcarisimo.github.io/originet/cli/#outputs)). ``` $ originet 8.8.8.8 2001:4860::8888 1.1.8.9 15.0.64.1 10.0.0.1 not-an-ip ip prefix asn snapshot error 8.8.8.8 8.8.8.0/24 15169 2026-10-21 2001:4860::8888 2001:4860::/32 15169 2026-10-21 1.1.8.9 1.1.8.0/24 149511, 138421 2026-10-21 15.0.64.1 15.0.64.0/18 {8035, 13979} 2026-10-21 10.0.0.1 - - 2026-10-21 not-an-ip - - - invalid IP address syntax 6 addresses: 4 found, 1 not found, 1 invalid (ipv4 2026-10-21, ipv6 2026-10-21; 1.2s) ``` ## `json` One document with the tool version, the snapshots used, the totals and the results; the default for pipes and files. ``` { "tool": { "name": "originet", "version": "1.1.0" }, "generated_at": "2026-10-29T13:47:29Z", "snapshots": [ { "family": "ipv4", "source": "caida", "date": "2026-10-21", "file": "routeviews-rv2-20261021-0800.pfx2as.gz", "location": "https://publicdata.caida.org/datasets/routing/routeviews-prefix2as/2026/10/routeviews-rv2-20261021-0800.pfx2as.gz", "prefixes": 1129523 } ], "summary": { "total": 2, "found": 1, "not_found": 1, "invalid": 0 }, "results": [ { "ip": "8.8.8.8", "prefix": "8.8.8.0/24", "asn": 15169, "origins": "15169", "snapshot": "2026-10-21", "error": null }, { "ip": "10.0.0.1", "prefix": null, "asn": null, "origins": null, "snapshot": "2026-10-21", "error": null } ] } ``` `source` is `caida` for a downloaded (or cached) snapshot and `local` for a `--pfx2as` file; `location` is then the URL or the path. ## `jsonl` One result object per line, nothing else. The natural format for pipes and for very large batches. ``` {"ip":"8.8.8.8","prefix":"8.8.8.0/24","asn":15169,"origins":"15169","snapshot":"2026-10-21","error":null} ``` ## `csv` and `tsv` A header line and one row per result; minimal quoting. ``` ip,prefix,asn,origins,snapshot,error 8.8.8.8,8.8.8.0/24,15169,15169,2026-10-21, 10.0.0.1,,,,2026-10-21, bogus,,,,,invalid IP address syntax ``` ## `parquet` The same six columns (`ip`, `prefix`, `origins`, `snapshot`, `error` as UTF-8 strings, `asn` as nullable `UInt32`) with Snappy compression; the file's `created_by` names the tool version. The writer refuses to stream Parquet to a terminal: pass `--output` or redirect standard output. Parquet is a default Cargo feature; build with `--no-default-features` to drop it (and the arrow dependency). ## Exit status and standard error | Status | Meaning | | ------ | ----------------------------------------------------------------------------------------------------------------------- | | 0 | Done (invalid inputs are reported per row, not as a failure) | | 1 | An error: no input, a file that could not be read, a snapshot that could not be fetched, `--strict` with invalid inputs | | 2 | Usage error (clap) | Standard error gets a one-line summary unless `--quiet`, warnings (for example a snapshot date that had to be moved earlier), and with `-v` what is fetched or read from the cache. `RUST_LOG` is honored too. On a terminal it also shows a progress bar while a snapshot downloads and a spinner while it loads, cleared when done; `--quiet` turns them off. # Docker The image builds the CLI from source in a `rust` stage and ships only the binary on Debian slim with `ca-certificates`, as a non-root user. Downloads persist in the `/cache` volume (`ORIGINET_CACHE_DIR`); `/data` is the working directory for input and output files. ``` docker build -t originet . docker run --rm -v originet-cache:/cache originet 8.8.8.8 2001:4860::8888 docker run --rm -v originet-cache:/cache -v "$PWD/data:/data" originet \ --file /data/ips.txt --format csv --output /data/results.csv ``` With Compose (`./data` on the host is `/data` in the container): ``` docker compose run --rm originet --file /data/ips.txt --date 2024-01-01 -F jsonl ``` Tagged releases are also published as `ghcr.io/estcarisimo/originet:` (linux/amd64). # Rust library The binary is a thin layer over the `originet` crate, so the same pieces are available to Rust programs: `cargo add originet` and read the API documentation on [docs.rs](https://docs.rs/originet). ``` use originet::caida::Family; use originet::fetch::Fetcher; use originet::load; use originet::table::Table; fn main() -> anyhow::Result<()> { let fetcher = Fetcher::new("/tmp/pfx2as-cache".into()); let mut table = Table::new(); let info = load::load_caida(&mut table, &fetcher, Family::V4, None)?; println!("{} prefixes from {}", info.prefixes, info.file); if let Some(hit) = table.lookup("8.8.8.8".parse()?) { println!("{} -> {} (origins {})", hit.prefix, hit.origins.primary(), hit.origins); } Ok(()) } ``` `Fetcher::offline`, `refresh_index` and `progress` are builder methods; the last takes an implementation of `fetch::Progress`, which is told when a snapshot starts downloading (with its size, when known), how many bytes arrived, and when it starts and finishes loading. The binary's progress bars are one such implementation; a library user may log, or pass nothing and hear nothing. `load::fetch_snapshot` resolves the snapshot for a date and puts it in the cache without loading it (what `--fetch` does). Modules: | Module | What it does | | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pfx2as` | Parses the `pfx2as` format line by line, gzip or plain; `Origins` keeps the field verbatim and knows whether it is multi-origin or an AS set | | `table` | `Table` holds one longest-prefix-match trie per family; `lookup` returns the prefix and a reference to its origins | | `caida` | Dataset URLs, `Index::parse` of the rolling `pfx2as-creation.log`, `parse_listing` of a month's directory page, `Index::on_or_before` | | `fetch` | `Fetcher`: cached index (six-hour freshness), cached snapshots, atomic downloads, offline mode | | `cache` | `Inventory::scan` lists the cached snapshots with their sizes; `Retention` (`keep_days`, `keep_last`) and `prune` remove the old ones and the directories they empty, with a dry run; `format_bytes` | | `load` | `select_snapshot` resolves a date through the index and the monthly listings; `load_local` and `load_caida` fill a table and return a `SnapshotInfo` with the provenance | | `input` | `Item::parse` and `read_items` for addresses from text | | `output` | `Record`, `Report`, `Summary`, and `write` for every format | The crate has one default feature, `parquet`, which pulls in `arrow` and `parquet`. Disable it with `default-features = false` when you do not need Parquet output. # Changelog All notable changes to this project are documented in this file. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [SemVer](https://semver.org). ## [Unreleased] ## [1.1.0] - 2026-10-29 Python wheels for Linux with musl, so `pip install originet` works on Alpine without a Rust toolchain, and every pull request now builds the Linux wheels and installs the x86_64 ones, so a wheel that stops building fails at the pull request, not at the tag. ### Added - Python wheels for Linux with musl (`musllinux_1_2`, x86_64 and aarch64), so `pip install originet` works on Alpine without a Rust toolchain. Before, pip found no wheel there and fell back to the sdist, which needs Rust 1.88+ to compile. - `context7.json` so Context7 indexes the user documentation in `docs/` and passes short usage rules to coding agents. ## [1.0.1] - 2026-10-23 The crates.io 1.0.0 was uploaded by hand from a commit that predates the cache commands, so `cargo install originet` lacked `--cache` while the release binaries and the Python wheels had it. 1.0.1 brings every channel to the same code, and it is the first version published to crates.io by the release workflow through Trusted Publishing (PyPI 1.0.0 already was). ### Added - The documentation site serves `/llms.txt` and `/llms-full.txt` for AI agents. The README links to them. ### Changed - README and library docs: install from crates.io (`cargo install originet`), link docs.rs. ### Fixed - Release workflow: the PyPI publish and the release attach download only the wheel and sdist artifacts; the image job's `.dockerbuild` record made twine fail the upload. - Release workflow: the linux-aarch64 wheel builds again (manylinux_2_28 cross image and `__ARM_ARCH` for ring), so the wheels get attached and published. That wheel now targets glibc 2.28 (manylinux_2_28); the x86_64 wheel stays on manylinux2014. ## [1.0.0] - 2026-10-23 A rewrite in Rust, renamed from `map-ip-to-asn` to **originet**. The tool no longer depends on CAIDA's libipmeta/pyipmeta C stack: it reads the `pfx2as` snapshots itself and ships as one binary with no C library to install. ### Added - Single-binary CLI: addresses from arguments, a file or standard input; IPv4 and IPv6 in one run; JSON, JSON Lines, CSV, TSV and Parquet output; `--strict`, `--quiet`, `-v`. - Snapshots are resolved through CAIDA's `pfx2as-creation.log` index for recent days and the monthly directory listings for older ones: `--date` picks the newest snapshot on or before any day since the dataset began (2005), and the default is the newest one. - Local cache of indexes and snapshots (`--cache-dir`, `ORIGINET_CACHE_DIR`), atomic downloads, `--offline` and `--refresh`. - `--pfx2as` / `--pfx2as6` to use local snapshot files (plain or gzip) instead of downloading. - Every answer carries the longest matching prefix and all origins in CAIDA's notation (`A`, `A_B` multi-origin, `A,B` AS set), plus the snapshot date; the JSON report also records the snapshot files, their source and the counts. - Rust library (`originet`) exposing the parser, the prefix table and the fetcher. - Python bindings (`python/`, PyO3): `Table.from_files`, `Table.from_caida`, `lookup`, `lookup_many` and `lookup_asns` (bulk, GIL released), typed stubs. - Multi-stage Dockerfile (non-root, cache volume) and Compose file. - CI on Linux, macOS and Windows; MSRV check; clippy pedantic; rustdoc; cargo-deny; CodeQL (Rust, Python, Actions); zizmor; pip-audit; Dependabot for Cargo, uv, pip, Actions and Docker; release workflow (binaries, wheels, GHCR image, optional PyPI and crates.io Trusted Publishing); MkDocs site on GitHub Pages. - `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `CITATION.cff`, `AGENTS.md`, issue forms, PR template, `CODEOWNERS`, review brief. - `--format table`: aligned columns for reading in a terminal, with `-` for missing values and one `asn` column that lists every origin (`149511, 138421` for a multi-origin prefix, `{8035, 13979}` for an AS set). It is the default when standard output is a terminal; `json` stays the default for pipes and `--output`. - `--color auto|always|never`: on a terminal the table, the summary counts and the progress bar are colored (`NO_COLOR` honored); `never` gives plain text, `always` keeps the escapes in a pipe or a file. Library: `output::Theme` and `write_table`. - A progress bar on standard error while a snapshot downloads (size, speed, time left) and a spinner while it loads, when standard error is a terminal; `--quiet` turns them off. - `--fetch`: download the day's IPv4 and IPv6 snapshots into the cache and exit, printing each one's date, `downloaded`/`cached` and path; with `--offline` it checks the cache. - Library: `fetch::Progress` and `Fetcher::progress` to observe downloads and loads; `load::fetch_snapshot` resolves and caches a snapshot without loading it. - `--cache list|size|prune`: inspect and prune the cache without deleting by hand. `list` prints every cached snapshot with its family, date, size and path, `size` the count and size per family and in total, and `prune` removes the snapshots that `--keep-days N` (older than N days) and `--keep-last N` (all but the N newest per family) do not keep, then the month and year directories left empty; `--dry-run` only reports. Indexes and files originet did not create are never touched. Library: the `cache` module (`Inventory`, `Retention`, `prune`, `format_bytes`). ### Changed - Project renamed to `originet` (repository `estcarisimo/originet`; the old URL redirects). The binary, the crate, the Python package and the cache directory follow the new name; the environment variable is `ORIGINET_CACHE_DIR`. - `--format` is now `-F/--format` (`-f` is `--file`); `--ip` is gone, addresses are positional. `--date` no longer fails for dates without a snapshot in the previous six months; it uses the newest earlier snapshot and says so. - The `asn` field is `null` (empty in CSV/TSV) when no prefix covers an address, instead of `0`. Invalid inputs are reported per row with an `error` field instead of aborting. - Output records are flat (`ip`, `prefix`, `asn`, `origins`, `snapshot`, `error`) across every format; the per-row `timestamp` and `provider` fields are gone. - At a terminal the default output is the table instead of the JSON report; pipes and files are unchanged. - Release workflow: a version already on PyPI or crates.io is skipped instead of failing the run, so the first crates.io upload can be done by hand and the tag still works. - Crate tarball no longer carries `AGENTS.md`, `CLAUDE.md` and `.dockerignore`. ### Removed - The Python implementation (pydantic models, pandas serializers, pyipmeta provider, BeautifulSoup scraping), `run-docker.sh`, the Docker Hub publishing job and the multi-service Compose setup. ## [0.1.0] - 2025-08-05 ### Added - Initial Python CLI on CAIDA's pyipmeta with JSON, CSV and Parquet output and a Docker image. # Contributing Thanks for helping with originet. Bugs and proposals go through [issues](https://github.com/estcarisimo/originet/issues); code through pull requests. ## Setup ``` git clone git@github.com:estcarisimo/originet.git && cd originet cargo build # the CLI: target/debug/originet cargo test --workspace --all-features cd python && uv sync --locked # builds the extension in editable mode (needs Rust) uv run pytest ``` Requirements: Rust 1.88 or newer (`rust-version` in `Cargo.toml`), and for the Python bindings [uv](https://docs.astral.sh/uv/) and Python 3.10 or newer. Build the extension with `uv sync` (maturin), not with `cargo build --workspace`: a pyo3 `extension-module` cannot be linked by cargo alone (`cargo clippy --workspace` and `cargo test --workspace` are fine, they do not link it). On an Apple Silicon Mac the Rust toolchain must be the arm64 one (rustup); an x86_64 Homebrew toolchain cannot build the extension for an arm64 Python. Checks that CI runs, in the same order: ``` cargo fmt --all --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo clippy -p originet --all-targets --no-default-features -- -D warnings RUSTDOCFLAGS="-D warnings" cargo doc -p originet --no-deps --all-features cargo deny check # cargo install cargo-deny cargo test --workspace --all-features --locked cargo test -p originet --no-default-features --locked (cd python && uv run ruff check . && uv run ruff format --check . && uv run mypy && uv run pytest -q) docker build -t originet . uvx --with-requirements docs/requirements.txt mkdocs build --strict ``` Tests never touch the network: they use the fixtures in `tests/fixtures/` and a seeded cache directory. Keep it that way; a test that needs CAIDA is an `#[ignore]` test. ## Pull requests - Branch from `main`; never push to `main` directly (the `protect-main` ruleset enforces a pull request with green checks). - Every PR gets an independent review following `.github/REVIEW.md` and is merged only after `VERDICT: APPROVE` on its final commit with CI green. - Add a bullet under `[Unreleased]` in `CHANGELOG.md` for user-visible changes. - American English in identifiers, comments, docs and the changelog. - Every documented command is run before it is written down; example output is a real capture. - A change to the Python API updates `python/originet/_native.pyi` and `python/tests/` too. - Dependencies: pinned to SHAs in workflows; new crates must pass `cargo deny check` (license allow-list in `deny.toml`). ## Releases 1. Move `[Unreleased]` in `CHANGELOG.md` to `## [X.Y.Z] - YYYY-MM-DD`. 1. Set `version` to `X.Y.Z` in `Cargo.toml`, `python/Cargo.toml`, `python/pyproject.toml` and `CITATION.cff` (with `date-released`); run `cargo update --workspace` and `(cd python && uv lock)` so the lockfiles follow. 1. Merge that PR, then tag `vX.Y.Z` on `main` and push the tag. The release workflow checks that the tag, the four versions and the changelog agree and that the tag is on `main`; then it creates the GitHub release with the changelog section, attaches binaries (Linux x86_64/aarch64 musl, macOS x86_64/arm64, Windows x86_64), builds the wheels and the sdist, and pushes `ghcr.io/estcarisimo/originet`. Publishing to PyPI and crates.io is gated by the repository variables `PYPI_PUBLISH` and `CRATES_IO_PUBLISH` (`true` to publish) and uses OIDC Trusted Publishing, no stored tokens. One-time setup per registry: - PyPI: a *pending publisher* for `originet` (owner `estcarisimo`, repository `originet`, workflow `release.yml`, environment `pypi`) can be created before the first upload, so the workflow can publish the first version. - crates.io has no pending publisher: the first version is uploaded by hand (`cargo login` with a token scoped to `publish-new` for `originet`, then `cargo publish -p originet --locked` from `main`), the trusted publisher is added in the crate's settings (owner `estcarisimo`, repository `originet`, workflow `release.yml`, environment `crates-io`), and the token is revoked. The workflow skips a version that is already on the registry, so tagging that same version afterwards is safe. # Security Policy ## Scope In scope: the `originet` CLI and Rust library, the Python bindings under `python/`, the Dockerfile, the Compose file and the GitHub Actions workflows. The data comes from CAIDA's public RouteViews prefix-to-AS dataset; problems with the data itself belong to [CAIDA](https://www.caida.org/catalog/datasets/routeviews-prefix2as/). ## Supported versions The latest release on `main`. There are no maintained older branches. ## Reporting a vulnerability Use GitHub's private vulnerability reporting: https://github.com/estcarisimo/originet/security/advisories/new. Please include what is affected, how to reproduce it, the impact, and whether it is already public. You will get an acknowledgment within 7 days and an assessment within 14; fixes ship as a release. Please allow up to 90 days before public disclosure. If private reporting is unavailable, contact the maintainer listed in `.github/CODEOWNERS` through GitHub with the subject `[originet security]`. ## What the tool does with the network and the file system - It downloads only from `https://publicdata.caida.org` (HTTPS, rustls), with an identifying `User-Agent`, and only when the needed index or snapshot is not cached. `--offline` disables downloads entirely. - Downloads go to a temporary file in the cache directory and are renamed into place, so an interrupted download never leaves a truncated file behind. - The cache directory is the platform cache dir (`~/.cache/originet` on Linux, `~/Library/Caches/originet` on macOS), or `--cache-dir` / `ORIGINET_CACHE_DIR`. - Input files are read as text; gzip is detected by magic bytes and decompressed in memory in a streaming fashion. The tool never executes anything from its inputs. - No telemetry, no other endpoints. ## What is in place - `unsafe_code = "forbid"` in every crate; clippy `pedantic` with `-D warnings`. - Supply chain: `Cargo.lock` and `python/uv.lock` committed and enforced (`--locked`), `cargo deny` (advisories, license allow-list, registry allow-list) on every PR and weekly, `pip-audit` on the bindings' dev tools, Dependabot for Cargo, uv, pip, Actions and Docker base images, actions pinned to commit SHAs, `zizmor` on the workflows, `permissions: contents: read` by default, CodeQL for Rust, Python and Actions. - Releases: tag must match the versions and sit on `main`; binaries with SHA-256 checksums; registries only through OIDC Trusted Publishing, no stored tokens. - Container: multi-stage build, Debian slim runtime with only `ca-certificates`, non-root user, no shell entrypoint. - Process: no direct pushes to `main` (`protect-main` ruleset), independent review of every PR (`.github/REVIEW.md`), CI green before merge. ## Out of scope Resource exhaustion by feeding the tool enormous input files on purpose (it streams but holds the whole result set in memory before writing), and findings in CAIDA's infrastructure. # Citing GitHub renders the citation from `CITATION.cff` ("Cite this repository" on the repository page). The file: ``` cff-version: 1.2.0 message: "If you use originet in your work, please cite it and CAIDA's prefix-to-AS dataset." title: "originet" type: software version: 1.1.0 date-released: 2026-10-29 repository-code: "https://github.com/estcarisimo/originet" url: "https://estcarisimo.github.io/originet/" license: MIT abstract: "Map IP addresses to origin ASNs with CAIDA's RouteViews prefix-to-AS snapshots, fetched on the fly or read from a local file. Rust CLI and library with Python bindings." keywords: - asn - bgp - caida - routeviews - prefix-to-as - longest-prefix-match authors: - family-names: Carisimo given-names: Esteban references: - type: data title: "The CAIDA UCSD Routeviews Prefix to AS mappings Dataset (pfx2as) for IPv4 and IPv6" authors: - name: "Center for Applied Internet Data Analysis (CAIDA)" url: "https://www.caida.org/catalog/datasets/routeviews-prefix2as/" ``` Please also cite the dataset: *The CAIDA UCSD Routeviews Prefix to AS mappings Dataset (pfx2as) for IPv4 and IPv6*, https://www.caida.org/catalog/datasets/routeviews-prefix2as/.