IPv6アドレスをRFC 5952で正規化 — ::の位置とIPv4射影
2001:db8:0:0:1:0:0:1 と 2001:DB8::1:0:0:1 は、どちらも同じ IPv6 アドレスである。ところが文字列としては一致しないので、アクセスログを grep したり、許可リストと突き合わせたり、設定ファイルの差分を取ったりすると、同じアドレスが別物として扱われる。
IPv6 アドレスの書き方を1つに絞るための規則が RFC 5952(A Recommendation for IPv6 Address Text Representation、2010年8月、RFC 4291 を更新)である。この記事では、ハシトシステムのIPv6アドレス展開・短縮ツールが実装している短縮処理を読みながら、
- RFC 5952 が決めている規則と、それが必要な理由
::をどこに置くかの判定を、実装でどう書くか- IPv4 射影アドレス(
::ffff:192.0.2.128)の表記が、実装ごとに分かれること
を、手元で動かしたコードで確かめる。確認した環境は Windows 11 / Node.js v24.11.1 / Python 3.11.9・3.12.10 である。
1つのアドレスに8通りの書き方がある
IPv6 アドレスは 16 ビットずつ8グループに区切り、16進で書く。RFC 4291 の section 2.2 は、各グループの先頭の0を省いてよいこと、0が続くグループを :: で1回だけ省略してよいこと、末尾32ビットを 192.0.2.1 のような10進で書いてよいことを定めている。どれも「書いてよい」なので、同じアドレスに複数の書き方が成立する。
RFC 5952 の section 2 は、その例として次の8通りを挙げている。すべて同じアドレスである。
1 | |
:: を前の 0:0 に使うか後ろの 0:0 に使うか、先頭の0を残すか、大文字で書くか。この自由度があるので、機械が出力した値と人が書いた値が一致しない。
RFC 5952 の規則
RFC 5952 の section 4 は、出力する側が守るべき規則を次のように定めている(いずれも MUST)。
| 節 | 規則 | 例 |
|---|---|---|
| 4.1 | 各グループの先頭の0は省く | 2001:0db8::0001 ではなく 2001:db8::1 |
| 4.2.1 | :: は省略できる限り使う |
2001:db8:0:0:0:0:2:1 ではなく 2001:db8::2:1 |
| 4.2.2 | 0が1グループだけのところに :: を使わない |
2001:db8:0:1:1:1:1:1 が正しく、2001:db8::1:1:1:1:1 は誤り |
| 4.2.3 | 候補が複数あれば最も長い0の連続を省く。長さが同じなら左側を省く | 冒頭の例は 2001:db8::1:0:0:1 になる |
| 4.3 | 16進の英字は小文字で書く | 2001:DB8::1 ではなく 2001:db8::1 |
4.2.2 は見落としやすい。:: は「0のグループが1つ以上」を表せるので、1グループだけの0に使っても読み込み側は正しく復元できる。それでも RFC 5952 は、書き方を1つに絞るためにこれを禁じている。
実装:最長の0連続を探す
ハシトシステムのIPv6アドレス展開・短縮ツールは、外部ライブラリを使わずにページ内の JavaScript で変換している。入力を8グループの数値配列にしたうえで、短縮は次の関数で行う(ツールのソースをそのまま引用)。
1 | |
RFC 5952 の規則は、この関数の3か所に対応している。
- 最長を選ぶ(4.2.3):
len>bestLenのときだけ最長を更新する - 同点は左(4.2.3):比較が
>で>=ではないので、同じ長さの連続が後から来ても更新されず、先に見つけた左側が残る - 1グループだけは省かない(4.2.2):
bestLen<2なら::を使わず、noPad(先頭の0を省いてコロンでつなぐだけ)を返す
先頭の0を省く(4.1)と小文字(4.3)は、n.toString(16) が両方を満たす。数値に直してから16進に戻すので、0DB8 は db8 になる。
ブラウザの URL パーサと同じ結果になるか
同じ規則は、WHATWG の URL Standard にも入っている。URL のホスト部分に [2001:DB8::1] のような IPv6 アドレスを書くと、URL パーサは IPv6 シリアライザでそれを書き直す。仕様の IPv6 シリアライザには、この処理が RFC 5952 の推奨に従うための手順である旨が注記されている。圧縮する位置を求める手順は、0の連続のうち最初に見つかった最長のものを選び、長さ1の連続は圧縮しない(longestSize の初期値が1で、それより長い場合だけ採用する)。
Node.js の URL も WHATWG URL の実装なので、ツールの関数と結果を突き合わせられる。ツールの HTML からこの関数を取り出して、new URL() の結果と比べたところ、次の入力はすべて同じ文字列になった。
| 入力 | ツールの short() |
new URL() のホスト |
|---|---|---|
2001:0DB8:0000:0000:0001:0000:0000:0001 |
2001:db8::1:0:0:1 |
2001:db8::1:0:0:1 |
2001:db8:0:1:1:1:1:1 |
2001:db8:0:1:1:1:1:1 |
2001:db8:0:1:1:1:1:1 |
2001:db8:0:0:1:0:0:0 |
2001:db8:0:0:1:: |
2001:db8:0:0:1:: |
0:f:0:0:f:f:0:0 |
0:f::f:f:0:0 |
0:f::f:f:0:0 |
1:0:0:2:0:0:0:3 |
1:0:0:2::3 |
1:0:0:2::3 |
1:2:3:4:5:6:7:: |
1:2:3:4:5:6:7:0 |
1:2:3:4:5:6:7:0 |
0:f:0:0:f:f:0:0 は、長さ2の0連続が2か所(3〜4番目と7〜8番目)にある。左側が選ばれて 0:f::f:f:0:0 になる。URL Standard 自身も、この入力で2番目の0(左側の連続の先頭)を指す例を載せている。
最後の 1:2:3:4:5:6:7:: は、入力としては正しい(:: が1グループの0を表す)。しかし正規化すると、4.2.2 により :: を使わない 1:2:3:4:5:6:7:0 に戻る。
IPv4 射影アドレスは実装で表記が分かれる
RFC 5952 の section 5 は、IPv4 アドレスを埋め込んだアドレスについて、よく知られたプレフィックスから埋め込みだと判別できる場合は、下位32ビットを10進で書く混在表記を推奨(RECOMMENDED)している。IPv4 射影アドレス(::ffff:0:0/96)なら ::ffff:192.0.2.1 の形である。section 4 の規則と違い、ここは MUST ではない。
実際に同じ ::ffff:192.0.2.128 を渡すと、結果は次のように分かれた。
| 実装 | 出力 |
|---|---|
Node.js v24.11.1 の new URL() |
::ffff:c000:280 |
| ハシトシステムのツール(推奨表記の欄) | ::ffff:c000:280 |
Python 3.11.9 / 3.12.10 の ipaddress.IPv6Address を str() |
::ffff:c000:280 |
Python 3.11.9 の socket.inet_ntop(Windows 11) |
::ffff:192.0.2.128 |
URL Standard の IPv6 シリアライザは、各グループを「最短の小文字16進」で出力する手順しか持たないので、IPv4 射影でも16進になる。ツールは推奨表記の欄を16進で出したうえで、別の欄に「埋め込まれたIPv4」として 192.0.2.128 を表示している。Python の ipaddress では、ipv4_mapped 属性で埋め込まれた IPv4 アドレスを取り出せる。
これが実害になるのは、Node.js のサーバが受け取ったアドレスである。:: で待ち受ける(IPv4 と IPv6 の両方を受ける)サーバに 127.0.0.1 から接続すると、socket.remoteAddress は ::ffff:127.0.0.1 と混在表記で返ってきた。これを new URL() で正規化すると ::ffff:7f00:1 になる。つまり、「受け取ったアドレス」と「正規化した許可リスト」を文字列で比べると、同じ接続元でも一致しない。IPv4 射影アドレスを扱うときは、どちらの表記に揃えるかを決めて、比べる両側を同じ関数に通す必要がある。
ゾーンIDは URL では書けない
fe80::1%en0 の %en0 は、リンクローカルアドレスがどのインターフェースのものかを示すゾーンIDである。Python の ipaddress.IPv6Address('fe80::1%en0') は受け付け、scope_id 属性で取り出せる(Python 3.5 で追加)。Node.js の net.isIPv6('fe80::1%en0') も true を返した。
一方、URL Standard は IPv6 のホスト構文について、ゾーンIDのサポートを意図的に省いていると明記している。Node.js v24.11.1 の new URL('http://[fe80::1%en0]/') は ERR_INVALID_URL で失敗した。ツールはゾーンIDを切り離してアドレス部分だけを変換し、表示のときに付け直している。自前で正規化するときも、% 以降を先に分けておくのが安全である。
実際に試す
前提:Node.js(筆者は v24.11.1 で確認。ES モジュールの .mjs と、グローバルの URL を使う)。外部パッケージは使わない。
1. 正規化の関数を書く
構文の検査は WHATWG URL のパーサに任せ、戻ってきた文字列を8グループに展開してから、RFC 5952 の規則で書き直す。IPv4 射影アドレスを混在表記で出す関数も用意する。
1 | |
bestLen の初期値を1にして > で比べているので、長さ1の0は選ばれず(4.2.2)、同じ長さなら左が残る(4.2.3)。URL Standard の手順と同じ考え方である。混在表記は、この記事では IPv4 射影アドレスだけを対象にしている。RFC 5952 の section 5 は ISATAP などほかの埋め込み形式も挙げているので、必要なら条件を足す。
2. RFC の8通りが1つにまとまるか確かめる
1 | |
1 | |
期待される出力:
1 | |
8通りの表記が、正規化後は 2001:db8::1:0:0:1 の1つにまとまる。:: を2回使った 1::2::3 は、どこに何グループ入るかが決まらないので URL パーサが拒否し、null になる。
3. サーバが受け取るアドレスの表記を見る
1 | |
1 | |
筆者の環境(Windows 11 / Node.js v24.11.1)での出力:
1 | |
同じ接続元が、受け取った時点では混在表記、URL パーサを通すと16進表記になる。許可リストやレート制限のキーにアドレスを使うなら、両側を formatMixed(parse(x).groups) のように同じ関数へ通してから比べる。OS の設定によっては :: で待ち受けても IPv4 を受けない場合があるので、そのときは接続が失敗する。
4. Python での確認
1 | |
筆者の環境(Python 3.11.9 と 3.12.10)での出力:
1 | |
str() は RFC 5952 の section 4 どおりの短縮形、exploded は4桁ゼロ埋めの完全形になる。IPv4 射影は、このバージョンでは16進で表示され、埋め込まれた IPv4 は ipv4_mapped で取り出せた。表記は実装やバージョンで変わりうるので、突き合わせに使う前に手元で1回出力を見ておくとよい。
ブラウザだけで確かめたい場合は、IPv6アドレス展開・短縮ツールに 2001:DB8:0:0:1::1 や ::ffff:192.0.2.128、fe80::1%en0 を入れると、推奨表記・完全形・ゼロ埋めなしの形と、埋め込まれた IPv4 やゾーンIDが並んで表示される。末尾に /64 のようなプレフィックス長を付ければ、ネットワークアドレスと範囲も出る。変換はブラウザ内で完結し、入力は送信されない。
文字列を比べる前に正規化が要る、という点はドメイン名でも同じである。日本語ドメインの xn-- 表記で大文字や全角が結果を変える話は、日本語ドメインのxn–をJSで作る記事で扱っている。
まとめ
- IPv6 アドレスは RFC 4291 の省略規則だけだと何通りにも書ける。RFC 5952 の section 4 は、先頭の0を省く、
::は最長の0連続に1回だけ(同点は左、長さ1には使わない)、英字は小文字、で1つに絞る - ハシトシステムのツールの短縮関数と Node.js の
new URL()は、試した入力ですべて同じ結果になった。WHATWG URL の IPv6 シリアライザも同じ規則で::の位置を決める - IPv4 射影アドレスの10進表記は RFC 5952 でも推奨(RECOMMENDED)にとどまり、Node.js の URL パーサと Python の
ipaddressは16進、Node.js のremoteAddressや Windows のinet_ntopは10進で返した。比べる両側を同じ関数に通す - ゾーンID(
%en0)は URL の構文では書けない。正規化の前に切り離しておく
参照した一次情報
- RFC 5952 - A Recommendation for IPv6 Address Text Representation(section 2 の表記の例、section 4 の規則、section 5 の混在表記)
- RFC 4291 - IP Version 6 Addressing Architecture(section 2.2 のテキスト表記)
- URL Standard(WHATWG)(IPv6 シリアライザ、圧縮位置の決め方、ゾーンIDを省いている旨)
- Python ipaddress ドキュメント(
ipv4_mapped、scope_id)