ある地域のIPアドレス割り振り・割り当て一覧を作る


# はじめに

インターネットサービスをやっていると、アクセスしてくるホストの地域[^host-user]を判別したいってことが時折あります。

たとえば日本では「IPアドレス逆引きすると`*.hkd.mesh.ad.jp`だったので北海道からのアクセスっぽい」という判別ができたりします[^maxmind]が、今回はそこまで細かいことはしません。地域という表現ですが国ぐらいの粒度[^region]で考えます。

[^maxmind]: この方向は[MaxMind](https://dev.maxmind.com/geoip/geoip2/geolite2/)あたりの領分になってきます。

[^host-user]: 本当に欲しいのはアクセスしてくるユーザの居住地域とかな気もしますが、アクセスしてくるホストのIPアドレスしか分からないという前提。現実問題としてアクセス元のIPアドレスから分かるのは分配された組織の所在する地域でしかなく実際にホストやユーザがどこにいるかは別の話になります(VPNありますし)。


## IPアドレスと地域の関連付け
IPアドレスはIANAから各[RIR](https://ja.wikipedia.org/wiki/%E5%9C%B0%E5%9F%9F%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%83%AC%E3%82%B8%E3%82%B9%E3%83%88%E3%83%AA)に割り振られ、それを受けたRIRが各地域に割り振り・割り当てています。

なので、各RIRが提供する「どの地域へ分配したか」リストを処理すれば、各地域への分配IPアドレス一覧をつくることができます。

[^region]: 雑に国って言うと、「アメリカとは別にグアムへの割り振りがあるやん」とかなってくる。
## 割り振りと割り当てについて

記事タイトルに「割り振り・割り当て」と書いたように、分配には２つの表現があります。また、RIRの出しているリストにも「allocated」と「assigned」のふたつが存在しています。

これはJPNICが出しているドキュメント[割り振り(Allocation)、割り当て(Assignment)とは](https://www.nic.ad.jp/ja/basics/terms/allocation-assignment.html)にある通りなのですが、分配されたアドレス領域を受け取った側が自分で使うかどうかの違いがあります。

- allocate(割り振り)
    - 管理組織へ分配(傘下メンバーに分配するところに配る)
- assign(割り当て)
    - エンドユーザへの分配(分配されたアドレス領域を実際に使うところに配る)

この記事では、これら２つを区別せずに使うとき「分配」と書いています。

## 先達
このニーズは昔から存在するので、インターネットには既に上記の考えのもと作成されたリストがいくつか存在します。

- IPv4
    - [世界の国別 IPv4 アドレス割り当てリスト](http://nami.jp/ipv4bycc/)
    - [国/地域別IPアドレス割当数一覧](https://ipv4.fetus.jp/)
    - [IPアドレスの国別(国コード)割当て状況（グローバルIPアドレス）](https://www.ipvx.info/country/)

- IPv6
    - [国別 IPv6アドレス割当リスト](https://foxmaster.jp/ipv6bycc/)

# せっかくなので自分で作りたい
リストあるやん、で終わったら面白くないので、自分でも作ることにしました。ネットに正解とみなせるデータが存在しているので、生成結果をそれと比較すれば正しさの評価ができそう。

# リストを作る上での問題

※この項は先達の記事を適当にサマライズしています。

RIRのリストフォーマットはAPNICなどが[公開](https://www.apnic.net/about-apnic/corporate-documents/documents/resource-guidelines/rir-statistics-exchange-format/#RecordFormat)しています。これを読んで作れば良いのですが、いくつか問題があります。

## IPv4のときCIDR表記じゃない

IPv4は「開始アドレス＋個数」という表記[^ipv4-value]をしています。プログラムでIPアドレスから地域判定するときはCIDRで考えることが多いのでCIDRに変換した方が便利です。また、1レコードにCIDR表記1つで表記しきれないブロック[^non-cidr]も存在します[^historical-noncidr]。

IPv6の場合はCIDR表記[^ipv6-value]なので、この問題はありません。

[^ipv4-value]: 原文では`value`の項に「In the case of IPv4 address the count of hosts for this range. This count does not have to represent a CIDR range.」と書かれています。CIDRじゃないよって書いてありますね。

[^ipv6-value]: 原文では「In the case of an IPv6 address the value will be the CIDR prefix length from the ‘first address’ value of <start>.」とあります。

[^non-cidr]: 例えば、US割り当てのレコード「ripencc|US|ipv4|13.116.0.0|524288|19860425|assigned|61f026d4-8289-4155-b117-b70688eb33ff」を見ると、プレフィクス長は/13(=32-log2(524288))っぽく見えます。だけど、実際に13.116.0.0/13するとホストアドレスが0になりません。例えば[ネットワーク計算ツール](https://www.softel.co.jp/labs/tools/network/)で確認できます。

[^historical-noncidr]: CIDRの概念が発生する前から存在するフォーマットだろうからなぁ。

## CIDRにしてもブロックが小分けのときがある
これはRIRのドキュメントには書いてありません。先達の記事にはその旨記述があるし自分で実装しても確かにリストが縮みます[^historical-noncidr]。

リストが冗長になって良いことは特にないので、短くできるに越したことはない。

CIDRの結合については[CIDR＋CIDRはどんな処理をするの？](https://www.ipvx.info/how-to-use/multi_cidr2_merge/how-merge/)という記事がおすすめ。

# Pythonで作ってみる
CIDRの生成と結合をやるわけですが、PythonでCIDR結合などの操作はライブラリ[netaddr](https://netaddr.readthedocs.io/en/latest/)があります。

## CIDRレコードの生成
- [Arbitrary IP address ranges](https://netaddr.readthedocs.io/en/latest/api.html#arbitrary-ip-address-ranges)
    - `IPRange`コンストラクタの引数にIPアドレスの最初と最後を食わせ、[cidrs()](https://netaddr.readthedocs.io/en/latest/api.html#netaddr.IPRange.cidrs)を叩くとCIDR(`IPNetwork`オブジェクト)の配列[^cidr-array]が得られます。

[^cidr-array]: これは前述したとおり、IPアドレスレンジが1つのCIDRで表せるとは限らないからです。
```python
from netaddr import IPRange,IPAddress

# `start`と`value` は RIRのレコード情報
start_ip = IPAddress(start, version = 4)
end_ip = IPAddress(int(start_ip) + value - 1) # `value` は個数なので-1する
cidr_list = IPRange(start, end_ip).cidrs()
```

## CIDRブロックの結合

- [IP Set](https://netaddr.readthedocs.io/en/latest/api.html#ip-sets)
    - CIDR情報(`IPNetwork`オブジェクト)のリストを食わせると、適当に結合してくれます。
    - 結合されたCIDRは[iter_cidrs()](https://netaddr.readthedocs.io/en/latest/api.html#netaddr.IPSet.iter_cidrs)で取り出せます。

```python
from netaddr import IPSet

v4set = IPSet(v4_cider_list)
for cidr in v4set.iter_cidrs():
    print(cidr)
```

## つくってみたもの
色々書いてきましたが、シュッと書けてしまいました。ライブラリすごい。

大したものでは有りませんが、Gistに放り込んでおきました。

https://gist.github.com/walkure/d1d87d8b4aad3c692edef1cce0f69aab

# 他言語ではどうなのか

Perlの場合は[Net::CIDR::Lite](https://metacpan.org/pod/Net::CIDR::Lite)ライブラリ[^perl-lib]でCIDR結合ができそうです。実際に書いてみようと思ったんですが、`cpan`叩いたら「Free to wrong pool 1f7d20 not 89034600d957d249 at C:\Perl64\site\lib/IO/Socket/SSL.pm line 2739.」とか言って落ちてしまう。調べると、Windowsでのみ生じる既知かつ未解決のの問題[^community-activestate]の模様で断念。

Goの場合は[cidrman](https://godoc.org/github.com/ewbankkit/go-cidrman)というnetaddrにインスパイアされた[^cidrman-readme]ライブラリがありますが、現状IPv4のみ実装されています[^cidrman-v6issue]。

PHPの場合は、[自前の国別IPv6、IPv4アドレス割当リストを作成しよう](https://oxynotes.com/?p=10783)という記事が存在します。

[^community-activestate]: [Free to wrong pool in SSL.pm](https://community.activestate.com/t/free-to-wrong-pool-in-ssl-pm/2160)
[^perl-lib]: [Challenge Problem: Merging Network Addresses](https://www.perlmonks.org/?node_id=118346)あたりも参照。
[^cidrman-readme]: 「Inspired by the Python netaddr library」と[README](https://github.com/EvilSuperstars/go-cidrman/blob/master/README.md)に書いてある。
[^cidrman-v6issue]: [Merging IPv6 CIDR ranges unimplemented](https://github.com/EvilSuperstars/go-cidrman/issues/3)というissueが経っていますが、1年ほど動きがない。