IP アドレスから国を判別する

スポンサーリンク

不正アクセスや迷惑メールの発信元を解析するために、IP アドレスがどこの国に割り当てられたものかを判別するスクリプトを作成した。

まずは下準備。世界各地域の 5 つの RIP から IP アドレスの割当リストを取得する。このリストのフォーマットは RIR statistics exchange format に書かれてある。

  • ftp://ftp.arin.net/pub/stats/arin/delegated-arin-latest
    ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-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 進数の数値に変換する。

$ vi conv.pl
#!/usr/bin/perl

use strict;

while (<STDIN>) {
    chomp;
    my ($registry, $cc, $type, $start, $value, undef, $status) = split(/\|/);

    next unless ($type eq 'ipv4' && ($status eq 'allocated' || $status eq 'assigned'));

    my $num;
    $num = ($num << 8) + $_ foreach (split(/\./, $start));

    print $num . "\t" . ($num + $value) . "\t" . $cc . "\n";
}

このリストをソートしてテキストファイルに保存する。

$ cat delegated-* | perl ./conv.pl | sort -n > ipv4.txt

ここで,連続したブロックが同じ国に割り当てられている場合,次の Perl スクリプト (minimize.pl) により連結するようにする。これにより,リストが半分以下に圧縮されることが確認できた。

$ vi minimize.pl
#!/usr/bin/perl

use strict;

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) で自動化できる。

$ vi prepare.sh
#!/bin/sh

DIR=$(cd $(dirname $0); pwd)
cd $DIR

rm -f ./delegated-*

wget ftp://ftp.arin.net/pub/stats/arin/delegated-arin-extended-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 ./minimize.pl > ipv4.txt

これで各国に割り当てられた IP アドレスと国別コードのデータベースファイル (ipv4.txt) が作成できた。

このデータベースファイルに対して、次の Perl スクリプト (ip2cc.pl) により、IP アドレスから国別コードを取得する。

$ vi ip2cc.pl
#!/usr/bin/perl

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 $0 <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 アドレスでテストすると、次のようになる。

$ perl ./ip2cc.pl 124.83.147.204
JP

Perl のサブルーチン化したら次のような感じか。

my @ipv4db = &file('ipv4.txt');

print &ip2cc(*ipv4db, '124.83.147.204') . "\n";

sub file {
    my $file = shift;

    open(FH, $file);
    my @lines = <FH>;
    close(FH);
    chomp @lines;

    return @lines;
}

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 回で済む。

$ vi ip2cc.pl
#!/usr/bin/perl

use strict;

my $file = 'ipv4.txt';

if (!defined $ARGV[0]) {
    print "Usage: perl $0 <addr>\n";
    exit;
}

my @ipv4db = &file($file);

foreach (@ARGV) {
    print &ip2cc(*ipv4db, $_) . "\n";
}

sub file {
    my $file = shift;

    open(FH, $file);
    my @lines = <FH>;
    close(FH);
    chomp @lines;

    return @lines;
}

sub ip2cc {
    *ipv4db = shift;
    my $addr = shift;

    return if ($addr !~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/);

    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 版を作った。

$file = './ipv4.txt';
$ipv4db = file($file);

echo ip2cc($ipv4db, $addr) . "\n";

function ip2cc($ipv4db, $addr) {
	if (!preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $addr))
		return false;

	$num = 0;
	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;
	}
}

コメント

  1. seraphis より:

    ぐぐっていて辿り着きました。
    とても助かりました。

    一個目のip2cc.plですが、
    $num = ($num << 8) + $_ foreach (split(/\./, $start));
    は、もしかして、
    $num = ($num << 8) + $_ foreach (split(/\./, $addr));
    ではないでしょうか…

  2. sho より:

    あ,本当ですね。修正しておきました。

    ご指摘ありがとうございました。

  3. ヤス より:

    こんにちは。
    とても助かっています。
    一つ教えて頂きたいのですが
    「46.229.168.79」を上記のツールで調べるとNL(オランダ)になるのですが、IP逆引きで調べるとアメリカのサイトのようです。これはIPの管理元はオランダだけどサーバーはアメリカにあるということなんでしょうか?

  4. 匿名 より:

    誰かすでに作ってくれてないかなぁとググったらここにたどり着きました。
    めっちゃ助かりました。ありがとうございました。

タイトルとURLをコピーしました