PHPでIPv6から適切なwhoisサーバーへ接続し情報を取得するサンプル
IPv6への対応、完了していますか?
IPが枯渇する、する、というわりに日本では実はけっこう余っているとか、ついこの前、アメリカでも突如1億7500万個ものIPアドレスが放出されたとか聞くし、そもそもIPv6になったからといってWebアプリ側で特に気を付けることなんてないのではないか、そんなふうに思っていました。
…CloudFlareを導入するまでは。
このブログはWordPressを使って構築していますが、これが、とても、重い。
たぶん一度でもWordPressを使ったことがある方ならある程度共感してくれると思うのですが、窓から投げ捨てたくなるほどに重いので、なんとか高速化しようと、キャッシュ用のプラグインを入れたり、JavaScriptやCSSをダイエットするプラグインを入れたり、画像をコンパクトにするプラグインを入れたり、とにかくプラグイン…プラグイン…プラグイン…とプラグインだらけになります。
WordPressはプラグインが豊富だからすごい、みたいなこと書かれていることもあるけれど、導入しているプラグインのほとんどが高速化関連ではないのか?最初から軽量なシステムならほとんどのプラグインが不要ではないのか?と問い詰めたい。
まぁ、それはともかく、こういった経緯からCloudFlareの導入に踏み切るケースも多いと思うのです。うちの場合はLiteSpeed Web Serverで動いているレンタルサーバーへ移転してからだいぶ快適になりましたが、セキュリティ面でCloudFlareのファイアウォール機能を使いたくなり、CDNの導入に踏み切りました。
CloudFlareを導入したおかげで、表示スピードも高速化され、ファイアウォールによってかなりの数の攻撃的なbotを拒否することができ、その効果にはとても満足しているのですが、いかんせん強制的にIPv6へ変更される、という点が悩ましい。
IPv6にするとホスト情報を取れないことが多い
ぼくと同じ、インターネット老人会の人ならよくご存知であろう「確認くん」。
うちの環境からアクセスするとホスト情報は「ほにゃらら.v4.enabler.ne.jp」のように表示されます。
このホスト情報にはプロバイダのFQDNが割り当てられていることが多いため、BBExciteなら .dy.bbexcite.ne.jp、OCNなら .ocn.ne.jp 等の名称が入り、おおよそのプロバイダー情報がわかります。上の例の v4.enabler.ne.jp はプロバイダーのFQDNではありませんが、V6プラスのサービスを使っていることはわかります。
このホスト情報、IPアドレスを基に取得することができ、逆引きと呼ばれるのですが、PHPなら gethostbyaddr関数で簡単に取得できるため、たとえば掲示板システムを作ったとしたら、投稿者の名前と一緒にホスト名もデータベースに保存しておく、みたいな作り方をするんですね。
悪質ないたずら行為などがあった場合、ホスト名の一部で判断して、BBExciteからのアクセスだけは一時的に遮断する!みたいな使い方もできるし、通報先の判断にも使えます。
ところが、IPv6を導入するとこのホスト名が取れないことがとても増えるのです。ほとんど取れないと言って良いくらい。
というのも、IPアドレスからホスト名の逆引きをするためには逆引き用のレコードの登録が必要であり、IPv4ではプロバイダーが接続時に自動でレコード登録していましたが、IPv6だと膨大な数になるため、逆引きレコードを登録しないのだそうです。
IT系のニュースサイトで読んだだけなので本当かどうかは知りません。でも実体験としてIPv6の逆引きに成功したことがありません。
ホスト情報がわからないならwhoisに聞けば良いじゃない
whoisとはIPアドレス/ドメイン名の登録者情報を持つ、インターネット上のデータベースです。
とはいえ、世界中のIPアドレスとドメインの情報を1か所に集約していては回線がパンクしてしまうため、地域ごとに5つに分かれています。
もっと詳しく知りたい場合はJPNIC等で解説されているのでそちらをご覧ください。
https://www.nic.ad.jp/ja/basics/terms/rir.html
5つの地域をまとめるとこんな感じ。
名称 | 略称 | whoisサーバー |
---|---|---|
北米地域 | ARIN | whois.arin.net |
欧州地域 | RIPE | whois.ripe.net |
中南米地域 | LACNIC | whois.lacnic.net |
アジア太平洋地域 | APNIC | whois.apnic.net |
アフリカ地域 | LACNIC | whois.afrinic.net |
日本はアジア太平洋地域なので、うちのIPv6アドレスを whois.apnic.net へ問い合わせる(=telnet接続する)と下記のような回答が返ってきます。
全文ではなく一部ですが、これでもJPNICの割り当て範囲、つまり日本からのアクセスだということはわかりますよね。この後、更に whois.nic.ad.jp へ問い合わせると回線事業者の詳細までわかりますが、おおよそで良いならnetnameの項目だけでもプロバイダの判別ができます。
上述の例ではnetnameの先頭がJPNE-で始まっていることからJapan Network Enabler、つまりV6プラスだと判別できますし、Biglobeの場合は BIGLOBE-JPNIC-JP-ほにゃらら、楽天モバイルなら RMNI-JP みたいな名前が付いているのでプロバイダの判別には十分です。
今回は自分が日本からのアクセスだとわかりきっていたので、whois.apnic.netへ接続しましたが、Webサイトのアクセスログから回線情報を調べるためにはどのwhoisサーバーへ問い合わせるべきなのか知る必要があります。
ではIPアドレスから問い合わせ先のwhoisサーバーを知るためにはどうするか。
地域ごとに割り当てられているIPv6アドレスの範囲は決まっている
IANAのこちらのページ。
https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml
ここでIPv6の範囲ごとにどの地域に割り振られているのか一覧が公開されていました。
うーん、5つの地域+それ以外の特別な範囲=IANAということで、6個で済むのかと思いきや、アドレス範囲が飛び地になっていてちょっとだけ複雑ですね…。
まぁでもこのリストを連想配列にでもしてプログラム内に持って判断すれば良いでしょう。
以前作ったドメインの登録情報を取得するサンプルではwhoisサーバーを200以上リストアップしていたので、それよりはだいぶマシです。
参考記事:https://blog.ver001.com/php-whois/
PHPでIPv6から適切なwhoisサーバーへ接続し情報を取得するサンプル
というわけで、ようやく、本題のPHPサンプルです。
<?php
//https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml
$whoisArray['2001:0000::/23'] = 'whois.iana.org';
$whoisArray['2001:0200::/23'] = 'whois.apnic.net';
$whoisArray['2001:0400::/23'] = 'whois.arin.net';
$whoisArray['2001:0600::/23'] = 'whois.ripe.net';
$whoisArray['2001:0800::/22'] = 'whois.ripe.net';
$whoisArray['2001:0c00::/23'] = 'whois.apnic.net';
$whoisArray['2001:0e00::/23'] = 'whois.apnic.net';
$whoisArray['2001:1200::/23'] = 'whois.lacnic.net';
$whoisArray['2001:1400::/22'] = 'whois.ripe.net';
$whoisArray['2001:1800::/23'] = 'whois.arin.net';
$whoisArray['2001:1a00::/23'] = 'whois.ripe.net';
$whoisArray['2001:1c00::/22'] = 'whois.ripe.net';
$whoisArray['2001:2000::/19'] = 'whois.ripe.net';
$whoisArray['2001:4000::/23'] = 'whois.ripe.net';
$whoisArray['2001:4200::/23'] = 'whois.afrinic.net';
$whoisArray['2001:4400::/23'] = 'whois.apnic.net';
$whoisArray['2001:4600::/23'] = 'whois.ripe.net';
$whoisArray['2001:4800::/23'] = 'whois.arin.net';
$whoisArray['2001:4a00::/23'] = 'whois.ripe.net';
$whoisArray['2001:4c00::/23'] = 'whois.ripe.net';
$whoisArray['2001:5000::/20'] = 'whois.ripe.net';
$whoisArray['2001:8000::/19'] = 'whois.apnic.net';
$whoisArray['2001:a000::/20'] = 'whois.apnic.net';
$whoisArray['2001:b000::/20'] = 'whois.apnic.net';
$whoisArray['2002:0000::/16'] = '';
$whoisArray['2003:0000::/18'] = 'whois.ripe.net';
$whoisArray['2400:0000::/12'] = 'whois.apnic.net';
$whoisArray['2600:0000::/12'] = 'whois.arin.net';
$whoisArray['2610:0000::/23'] = 'whois.arin.net';
$whoisArray['2620:0000::/23'] = 'whois.arin.net';
$whoisArray['2630:0000::/12'] = 'whois.arin.net';
$whoisArray['2800:0000::/12'] = 'whois.lacnic.net';
$whoisArray['2a00:0000::/12'] = 'whois.ripe.net';
$whoisArray['2a10:0000::/12'] = 'whois.ripe.net';
$whoisArray['2c00:0000::/12'] = 'whois.afrinic.net';
$whoisArray['2d00:0000::/8'] = '';
$whoisArray['2e00:0000::/7'] = '';
$whoisArray['3000:0000::/4'] = '';
$whoisArray['3ffe::/16'] = '';
$whoisArray['5f00::/8'] = '';
$ipv6 = $_SERVER['REMOTE_ADDR'];
$whois_server = '';
foreach ($whoisArray as $iprange => $fqdn) {
if (ip_in_range($ipv6, $iprange)) {
$whois_server = $fqdn;
break;
}
}
echo '<textarea>'.whois($whois_server, $ipv6).'</textarea>';
function whois($whois_server, $ip)
{
$errno = 0;
$errstr = '';
$body = '';
$fp = fsockopen($whois_server, 43, $errno, $errstr);
if (!$fp) { //接続失敗
echo "$errstr ($errno)<br />\n";
} else {
fputs($fp, $ip."\r\n" );
while (!feof($fp)) {
$body .= fgets($fp);
}
fclose($fp);
}
return $body;
}
function mask_ip($ip, $subnet)
{
$addr = inet_pton($ip);
$len = 8 * strlen($addr);
$mask = str_repeat('f', $subnet >> 2);
switch ($subnet & 3) {
case 3:
$mask .= 'e';
break;
case 2:
$mask .= 'c';
break;
case 1:
$mask .= '8';
break;
default:
break;
}
$mask = str_pad($mask, $len >> 2, '0');
$mask = pack('H*', $mask);
return ($addr & $mask);
}
function ip_in_range($ip, $iprange)
{
list($range_base, $range_mask) = explode('/', $iprange);
$m1 = mask_ip($range_base, $range_mask);
$m2 = mask_ip($ip, $range_mask);
return ($m1 === $m2);
}
?>
解説
流れとしては下記のとおり単純なものです。
- IPv6の割り当て範囲と対応whoisサーバーのアドレスを $whoisArray という連装配列に保存しておく
- 渡されたIPアドレスで連装配列を検索し、接続先のwhoisサーバーを判別する
- whoisサーバーの43ポートへ接続し、IPアドレスを渡す
- 返された内容を表示
連想配列についてはianaで公開されている表をそのままコピペしただけ。
特定のIPアドレスが指定範囲 (例:2400:0000::/12)に含まれているかどうか、これを判断する部分が唯一複雑なところですが、これも実はPHPのページにサンプルがあるんです。
https://www.php.net/manual/ja/function.inet-pton.php
PHPにはinet_ptonという「人間が読める形式のIPアドレスを、パックされた in_addr 形式に変換する」という関数があります。…説明がわかりづらいw
IPv4だと 192.168.0.1 というような0~255までの数字を4つ組み合わせたのがIPアドレスでしたよね?
0~255、つまり8ビット。8ビットが4つで32ビット。32ビットはlong値で表せるのでip2longなどで簡単に数値化できましたが、IPv6は128ビットあるので数値型の変数では表現できません。128ビット整数値型とかあると良いんだけどなー。
というわけで、128ビットの文字列をバイナリデータに変換してくれるのがinet_ptonという関数。
/12などの数字はネットマスクと呼ばれ、IPアドレスの先頭から12ビット分が共通範囲だよ、という意味。この範囲内に指定されたIPアドレスが収まっているかどうかチェックしているのが、上のサンプルのip_in_range関数で、その中で呼んでいるmask_ip関数ではPHPマニュアルのサンプルをそのままコピペしてIPアドレスをバイナリ化してマスク処理しています。
まとめ
本題より前振りのほうが長くなってしまった気もしますが、ともあれ、これでIPv6から適切なwhoisサーバーへ接続し、whois情報を取得することが出来るようになりました。
ただ、こんな仕組みをアクセスログの解析などで使ったらすっげー迷惑だと思うんですよ…。うちの個人サイトですら月数十万件、一意のIPアドレスに絞っても数万件あるのに、それを全部whoisサーバーに問い合わせるとか、どんなスパムだよっていう。
なので、興味本位でプログラムは作りましたが、これを自動化処理などに載せるつもりはありません。せいぜい管理ページで、気になるIPアドレスをポチっと押したらwhois情報を表示する、とかそんな使い方をするかなぁ。
ログに記録されたIPv6のアドレスからISP(プロバイダ)情報を取得する手段については、db-ipが公開しているCSVと突き合わせるのが良いかなぁと思って現在検討中です。
https://db-ip.com/db/download/ip-to-asn-lite