# ip-rir

RIR（地域インターネットレジストリ）の公開データを使い、IPアドレスから RIR 割り当て情報を引くための Rust クレートです。

国コード・発行元 RIR・割り当てステータス・日付・割り当て IP レンジをまとめて返します。APIキー不要、アカウント不要、実行時に外部サービスへのアクセス不要。初回ダウンロード後はオフラインで動作します。

## なぜ ip-rir か

MaxMind GeoLite2 などの主要なジオ IP ライブラリは MaxMind アカウントや有料ライセンスが必要です。世界5つの地域インターネットレジストリ（APNIC、ARIN、RIPE NCC、LACNIC、AFRINIC）は、それぞれの割り当てデータをプレーンテキストファイルとして無償公開しています。`ip-rir` はこれらのファイルをダウンロードしてメモリ上のテーブルを構築し、毎秒数百万件規模のルックアップに応答します。

## クイックスタート

### 1. RIR データファイルのダウンロード

同梱の `ip-rir-fetch` CLI をビルドして実行します（`fetch` フィーチャーが必要）。

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

5機関のファイル（合計約28 MB）を `delegated-<rir>-latest` という名前で出力ディレクトリに保存します。

RIR ファイルは週次〜月次で更新されるため、cron による定期取得を推奨します。

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

### 2. 依存関係の追加

```toml
# Cargo.toml

[dependencies]
ip-rir = { path = "../ip-rir" }           # ローカルパス
# ip-rir = "0.1"                          # crates.io 公開後はこちら
```

### 3. IPアドレスの引き方

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ディレクトリ内の delegated-* ファイルをすべて読み込む（起動時に一度だけ呼ぶ）
    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} → (未割り当て)"),
    }
    Ok(())
}
```

## API

### `Database`

```rust
pub struct Database { /* 非公開フィールド */ }

impl Database {
    /// data_dir にある delegated-* ファイルをすべて読み込んでテーブルを構築する。
    /// 起動時に一度だけ呼ぶ。Arc でラップすれば複数スレッドで共有可能。
    pub fn init(data_dir: impl AsRef<Path>) -> Result<Self, Error>;

    /// 単一の RIR ファイルを読み込む。
    pub fn init_from_file(path: impl AsRef<Path>) -> Result<Self, Error>;

    /// 複数の RIR ファイルを読み込む。
    pub fn init_from_files(paths: &[impl AsRef<Path>]) -> Result<Self, Error>;

    /// ip に対応する RIR 割り当てレコードを返す。
    /// RIR レコードに存在しないアドレス（未割り当て、プライベート、ループバック等）は None。
    pub fn lookup(&self, ip: IpAddr) -> Option<Record>;

    /// ロード済みエントリ数の合計（IPv4 + IPv6、全 RIR）。
    pub fn len(&self) -> usize;
    pub fn is_empty(&self) -> bool;
}
```

### `Record`

```rust
pub struct Record {
    pub registry: Registry,
    pub status:   Status,
    /// 割り当て日（YYYYMMDD 形式。例: 20110811）。不明な場合は 0。
    pub date:     u32,
    /// 問い合わせた IP を含む割り当てブロック全体（両端を含む）。
    pub range:    IpRange,
}

impl Record {
    /// ISO 3166-1 alpha-2 国コード（例: "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,
    /// このクレートが認識しない status 文字列（ARIN 拡張フォーマットの独自値など）。
    Other,
}
```

### `IpRange`

```rust
pub struct IpRange {
    pub start: IpAddr,   // 範囲の先頭（含む）
    pub end:   IpAddr,   // 範囲の末尾（含む）
}
```

### 返り値の補足

- 未割り当て・プライベート・予約済みアドレスは `None`。
- RIR ファイルに `ZZ`（不明経済圏）として記録されているアドレスは `rec.cc()` が `"ZZ"` を返します。`None` には正規化しません。
- `Record` は所有型でライフタイムパラメータなし。スレッド間でそのまま送受信可能です。

## パフォーマンス

| 指標 | 値 |
|---|---|
| メモリ使用量 | 5 MB 未満（全5 RIR、IPv4 + IPv6） |
| ルックアップアルゴリズム | 二分探索（`partition_point`）— O(log n) |
| 読み込みエントリ数（目安） | 約 326,000 件（2026年6月時点） |
| init 所要時間 | 現代的なラップトップで 200 ms 未満 |

## データソース

| RIR | ファイル 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` |

ファイル形式（1行1レコード）:

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

- `ipv4`: `value` はホスト数（CIDR prefix 長ではない）— 内部で変換済み。
- `ipv6`: `value` は prefix 長。
- サマリー行（`cc == "*"`）とコメント行は自動的にスキップ。

## 別クレートへの組み込み方法（vigily を例に）

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

```rust
// vigily/src/lib.rs など、アプリ起動処理
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
// 地理的整合性チェック（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,   // 未割り当て → 不一致扱い or スキップ
    }
}
```

`Database` は `Send + Sync` を実装しているため、`Arc<Database>` を非同期タスクやスレッド間で共有してもデータのクローンは発生しません。

## データディレクトリの指定方法

以下のいずれかで渡せます。

- CLI フラグ: `--rir-dir /var/lib/ip-rir`
- 環境変数: `RIR_DIR=/var/lib/ip-rir`
- 設定ファイルのフィールド

`Database::init()` は呼び出し時にディレクトリを読み込み、以降はメモリ上で保持します。ファイルを更新した後に再読み込みするには、新しい `Database` を生成して `Arc` をスワップしてください。

## フィーチャーフラグ

| フィーチャー | デフォルト | 内容 |
|---|---|---|
| `fetch` | off | `ip-rir-fetch` ダウンロード CLI を有効化。`reqwest`（blocking）を追加 |

## ライセンス

MIT
