不正アクセスや迷惑メールの発信元を解析するために、IP アドレスからどこの国かを判別するスクリプトを作成した。
まずは下準備。世界各地域の 5 つの RIP から IP アドレスの割当リストを取得する。このリストのフォーマットは RIR statistics exchange format に書かれてある。
- ftp://ftp.arin.net/pub/stats/arin/delegated-arin-latest
- ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest
- ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest
- ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest
- ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest
取得したリストの IP アドレスのリストに対して、次の Perl スクリプト (conv.pl) により 10 進数の数値に変換する。
use strict;
while (<STDIN>) {
chomp;
my ($registry, $cc, $type, $start, $value, undef, $status) = split(/\|/);
unless ($type eq 'ipv4' && ($status eq 'allocated' || $status eq 'assigned')) { next; }
my ($num);
$num = ($num << 8) + $_ foreach (split(/\./, $start));
print $num . "\t" . ($num + $value) . "\t" . $cc . "\n";
}
このリストをソートしてテキストファイルに保存する。
ここで,連続したブロックが同じ国に割り当てられている場合,次の Perl スクリプト (mini.pl) により連結するようにする。これにより,リストが半分以下に圧縮されることが確認できた。
my ($flag, $START, $END, $CC);
$flag = 0;
while (<STDIN>) {
chomp;
my ($start, $end, $cc) = split(/\t/);
if ($cc ne $CC || $start != $END) {
if ($flag != 0) { print $START . "\t" . $END . "\t" . $CC . "\n"; }
else { $flag = 1; }
$START = $start;
}
$END = $end;
$CC = $cc;
}
print $START . "\t" . $END . "\t" . $CC . "\n";
以上の処理は次のシェルスクリプト (prepare.sh) で自動化できる。
rm -f ./delegated-*
wget ftp://ftp.arin.net/pub/stats/arin/delegated-arin-latest
wget ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest
wget ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest
wget ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest
wget ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest
cat delegated-* | perl ./conv.pl | sort -n | perl ./mini.pl > ipv4.txt
これで各国に割り当てられた IP アドレスと国別コードのデータベースファイル (ipv4.txt) が作成できた。
このデータベースファイルに対して、次の Perl スクリプト (ip2cc.pl) により、IP アドレスから国別コードを取得する。
use strict;
my ($file, $addr);
$file = 'ipv4.txt';
$addr = $ARGV[0];
if ($addr !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) {
print "Usage: perl ./ip2cc.pl <addr>\n";
exit;
}
my ($num);
$num = ($num << 8) + $_ foreach (split(/\./, $addr));
open(FH, $file);
while (<FH>) {
chomp;
my ($start, $end, $cc) = split(/\t/);
if ($start <= $num && $num < $end) {
print $cc . "\n";
last;
}
}
close(FH);
試しに Yahoo! JAPAN の IP アドレスでテストしたところ、次のようになった。
JP
Perl のサブルーチン化したら次のような感じか。
print &ip2cc(*ipv4db, '124.83.147.204') . "\n";
sub ip2cc_prepare {
my ($file) = 'ipv4.txt';
my (@ipv4db);
open(FH, $file);
@ipv4db = <FH>;
close(FH);
chomp @ipv4db;
return @ipv4db;
}
sub ip2cc {
*ipv4db = shift;
my ($addr) = shift;
if ($addr !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) { return; }
my ($num);
$num = ($num << 8) + $_ foreach (split(/\./, $addr));
foreach (@ipv4db) {
my ($start, $end, $cc) = split(/\t/);
if ($start <= $num && $num < $end) {
return $cc;
}
}
return;
}
ただしこれだと,大量の IP アドレスを調べようとするとかなり効率が悪いので,二分探索するなどの改良の余地はある。
生成したデータを用いて,サンプルの Web アプリを作成してみた。
参考ページ
(2010/06/01 追記)
サブルーチン ip2cc を二分探索するように書き直したら劇的に速くなった。計算量は O(n) → O(log2n) に改善された。現時点でのデータベースファイルの行数は 45,000 行程度なので,探索回数は高々 16 回で済む。
*ipv4db = shift;
my ($addr) = shift;
if ($addr !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) { return; }
my ($num);
$num = ($num << 8) + $_ foreach (split(/\./, $addr));
my ($l, $r, $m);
$l = 0;
$r = $#ipv4db;
while ($l <= $r) {
$m = int(($l + $r) / 2);
my ($start, $end, $cc) = split(/\t/, $ipv4db[$m]);
if ($start <= $num && $num < $end) {
return $cc;
} elsif ($end < $num) {
$l = $m + 1;
} elsif ($start >= $num) {
$r = $m - 1;
} else {
return;
}
}
}
(2011/02/05 追記)
ip2cc の PHP 版を作った。
if (!preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $addr))
return false;
foreach (explode('.', $addr) as $val)
$num = (@$num * 256) + $val;
$l = 0;
$r = count($ipv4db);
while ($l <= $r) {
$m = floor(($l + $r) / 2);
list($start, $end, $cc) = explode("\t", rtrim($ipv4db[$m]));
if ($start <= $num && $num < $end)
return $cc;
elseif ($end < $num)
$l = $m + 1;
elseif ($start >= $num)
$r = $m - 1;
else
return false;
}
}
あ,本当ですね。修正しておきました。
ご指摘ありがとうございました。
ぐぐっていて辿り着きました。
とても助かりました。
一個目のip2cc.plですが、
$num = ($num << 8) + $_ foreach (split(/\./, $start));
は、もしかして、
$num = ($num << 8) + $_ foreach (split(/\./, $addr));
ではないでしょうか…