# ip-rir

IP address → RIR delegation record lookup using RIR delegated stats.

Returns the country code, issuing registry, allocation status, date, and the full allocated IP range — no API key, no account, no external service at runtime. Works offline after an initial download.

## Why ip-rir?

Most GeoIP libraries require a MaxMind account or a paid license. The five Regional Internet Registries (APNIC, ARIN, RIPE NCC, LACNIC, AFRINIC) publish their delegation data for free as plain text files. `ip-rir` downloads those files and builds an in-memory table that can answer millions of lookups per second.

## Quick start

### 1. Download the RIR data files

Build and run the bundled `ip-rir-fetch` CLI (requires the `fetch` feature):

```sh
cargo build --release --features fetch --bin ip-rir-fetch
./target/release/ip-rir-fetch --out /var/lib/ip-rir/
```

This downloads the five delegation files (~28 MB total) and saves them under the output directory as `delegated-<rir>-latest`.

Recommended: schedule a weekly cron job to keep the data fresh (RIR files are updated weekly to monthly):

```cron
0 3 * * 0  /usr/local/bin/ip-rir-fetch --out /var/lib/ip-rir/ >> /var/log/ip-rir-update.log 2>&1
```

### 2. Add as a dependency

```toml
# Cargo.toml

[dependencies]
ip-rir = { path = "../ip-rir" }           # local path
# ip-rir = "0.1"                          # crates.io (once published)
```

### 3. Look up an IP address

```rust
use ip_rir::Database;
use std::net::IpAddr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load all delegated-* files found in the directory (call once at startup)
    let db = Database::init("/var/lib/ip-rir")?;

    let ip: IpAddr = "203.0.113.1".parse()?;
    match db.lookup(ip) {
        Some(rec) => println!(
            "{ip} → cc={} registry={:?} status={:?} date={} range={}..{}",
            rec.cc(), rec.registry, rec.status, rec.date,
            rec.range.start, rec.range.end,
        ),
        None => println!("{ip} → (unallocated)"),
    }
    Ok(())
}
```

## API

### `Database`

```rust
pub struct Database { /* opaque */ }

impl Database {
    /// Load all `delegated-*` files in `data_dir` and build the lookup table.
    /// Call once at startup; wrap in Arc to share across threads.
    pub fn init(data_dir: impl AsRef<Path>) -> Result<Self, Error>;

    /// Load a single RIR file.
    pub fn init_from_file(path: impl AsRef<Path>) -> Result<Self, Error>;

    /// Load an explicit list of RIR files.
    pub fn init_from_files(paths: &[impl AsRef<Path>]) -> Result<Self, Error>;

    /// Look up the RIR delegation record for `ip`.
    /// Returns `None` for addresses not present in any RIR record
    /// (unallocated, private, loopback, etc.).
    pub fn lookup(&self, ip: IpAddr) -> Option<Record>;

    /// Total number of IP range entries loaded (IPv4 + IPv6, all RIRs combined).
    pub fn len(&self) -> usize;
    pub fn is_empty(&self) -> bool;
}
```

### `Record`

```rust
pub struct Record {
    pub registry: Registry,
    pub status:   Status,
    /// Allocation date as YYYYMMDD (e.g. 20110811). 0 means not recorded.
    pub date:     u32,
    /// The full allocated range that contains the queried IP.
    pub range:    IpRange,
}

impl Record {
    /// ISO 3166-1 alpha-2 country code (e.g. "JP", "US").
    pub fn cc(&self) -> &str;
}
```

### `Registry`

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Registry {
    Apnic,
    Arin,
    RipeNcc,
    Lacnic,
    Afrinic,
}
```

### `Status`

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
    Allocated,
    Assigned,
    Available,
    Reserved,
    /// Any status string not recognised by this crate (e.g. ARIN extended values).
    Other,
}
```

### `IpRange`

```rust
pub struct IpRange {
    pub start: IpAddr,   // inclusive
    pub end:   IpAddr,   // inclusive
}
```

### Notes on return values

- Returns `None` for unallocated / private / reserved addresses.
- `rec.cc()` returns `"ZZ"` when the RIR file uses that code (unknown economy); it is not normalised to `None`.
- `Record` is owned (no lifetime parameter), safe to store and send across threads.

## Performance

| Metric | Value |
|---|---|
| Memory footprint | < 5 MB (all 5 RIRs, IPv4 + IPv6) |
| Lookup algorithm | Binary search (`partition_point`) — O(log n) |
| Entries loaded (typical) | ~326,000 (as of 2026-06) |
| Init time | < 200 ms on a modern laptop |

## Data sources

| RIR | File URL |
|---|---|
| APNIC | `https://ftp.apnic.net/stats/apnic/delegated-apnic-latest` |
| ARIN | `https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest` |
| RIPE NCC | `https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest` |
| LACNIC | `https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest` |
| AFRINIC | `https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest` |

File format (one record per line):

```
registry|cc|type|start|value|date|status
apnic|JP|ipv4|1.0.0.0|256|20110811|allocated
```

- `ipv4`: `value` is the host count (not prefix length); `ip-rir` converts internally.
- `ipv6`: `value` is the prefix length.
- Summary lines (`cc == "*"`) and comment lines are skipped automatically.

## Integrating into another crate (example: vigily)

```toml
# vigily/Cargo.toml
[dependencies]
ip-rir = { path = "../ip-rir" }
```

```rust
// vigily/src/lib.rs or startup code
use std::sync::Arc;
use ip_rir::Database;

pub struct App {
    pub rir_db: Arc<Database>,
    // ...
}

impl App {
    pub fn new(rir_dir: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let rir_db = Arc::new(Database::init(rir_dir)?);
        Ok(Self { rir_db })
    }
}
```

```rust
// Geographic consistency check (e.g. Check 2):
fn check_geo(db: &Database, ip: std::net::IpAddr, expected_cc: &str) -> bool {
    match db.lookup(ip) {
        Some(rec) => rec.cc() == expected_cc,
        None      => false,   // unallocated — treat as mismatch or skip
    }
}
```

`Database` is `Send + Sync`, so a single `Arc<Database>` can be shared across async tasks or threads without cloning the data.

## Configuring the data directory

The data directory can come from:

- A CLI flag: `--rir-dir /var/lib/ip-rir`
- An environment variable: `RIR_DIR=/var/lib/ip-rir`
- A config file field

`Database::init()` reads the directory at the time it is invoked and holds the data in memory from that point on. To reload updated files, create a new `Database` and swap the `Arc`.

## Feature flags

| Feature | Default | Description |
|---|---|---|
| `fetch` | off | Enables the `ip-rir-fetch` download CLI; adds `reqwest` (blocking) |

## License

MIT
