digital 千里眼 @abp_jp

アナログな日常とデジタルの接点

国を指定して ipfilter.dat を作成する例1:apnic.net のデータを利用した場合

  • 「%03s」は間違い。正しくは「%03d

2010-08-06 修正

  • 数値をゼロ埋めしレコードを固定長に変更(こちらの書式が主流?)
  • 改行文字の修正(LF ⇒ CR+LF)を awk スクリプト中に含めた(Linux 等で使う場合は printf の \r を削ってください)

2009-07-05 修正

  • ネット上を探してみると、個人で作成されたIP範囲のデータもいろいろ見つかる。が、どうせなら自分の納得いくよう作成しようという企画
  • awk は初心者なのでバグが残っている可能性があります...指摘頂けると幸いです
  • エラー処理は自分用なのでいいかげんでよしとする
前提条件
  • Windows + CygwinLinux でも大きな違いはないハズ)
  • IPv4のみを対象(IPv6対応のソフト使ってないので)
  • 主に awk(gawk) を使用。sed, sort, uniq も併用する
  • バッチ処理を前提とする
  • 今回のサンプルでの抽出条件は中国、香港、台湾、日本のIPアドレス範囲とする。ご自分の好きなように書き換えて使ってください

1.元データをダウンロード

wget -nd -nc http://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest
オプション 意味
-nd ディレクトリを作成しない
-nc 既にファイルがある場合はダウンロードしない
  • このデータは毎日更新されている

2.データ概要

  • ファイルサイズは1メガ弱。2万行程度でパイプ(¦)区切り(psvファイル)
  • 各フィールドについての資料(英語)はここ参照のこと。以下では利用するフィールドのみ説明
  • 2番目のフィールドはCC=国コード
  • 3番目のフィールドでIPv4かどうか判定できる(asnはAS番号の意)
  • 4番目のフィールドはIPv4レコードの場合、開始IPアドレス
  • 5番目のフィールドは開始IPアドレスから終了IPアドレスまでの差を10進数化した値
  • IPアドレスでソート済み
    冒頭のコメント略
2|apnic|20090627|20105|gpan|20090626|+1000
apnic|*|asn|*|3283|summary
apnic|*|ipv4|*|16063|summary
apnic|*|ipv6|*|759|summary
apnic|JP|asn|173|1|20020801|allocated
apnic|NZ|asn|681|1|20020801|allocated
        中略
apnic|NZ|asn|2.115|1|20090623|allocated
apnic|HK|asn|2.116|1|20090625|allocated
apnic|JP|ipv4|58.0.0.0|131072|20050106|allocated
apnic|IN|ipv4|58.2.0.0|65536|20050110|allocated
        中略
apnic|KR|ipv4|222.251.128.0|32768|20051215|allocated
apnic|VN|ipv4|222.252.0.0|262144|20040518|allocated
apnic|CN|ipv6|240c::|28|20080721|allocated
apnic|JP|ipv6|2001:200:2000::|35|20030423|allocated
        中略
apnic|MY|ipv6|2407:f800::|32|20090609|allocated
apnic|JP|ipv6|2408::|22|20071102|allocated

3.ipfilter.dat 形式に変換

  • awk を使います。コードは以下の通り
  • ファイル名は"apnic2ipfilter.awk"とします
# Make ipfilter, targeting specific CC records from delegated-apnic-latest 
#
# Date      : 2009-07-05
# Author    : k2jp
#
BEGIN{
    FS = "|";
#    OFS = ",";
    CLS_A = 16777216;
    CLS_B = 65536;
    CLS_C = 256;
    match_counter = 0;
}

/^apnic\|CN\|ipv4\|/ || /^apnic\|HK\|ipv4\|/ || /^apnic\|TW\|ipv4\|/ || /^apnic\|JP\|ipv4\|/ {
    gsub(",", " ", $2);
    gsub(",", " ", $6);
    gsub(",", " ", $7);
    printf "%-s - %-s , %03d , [%s]%s_%s\r\n", FillByZero($4), IPrize( NUMrize($4) + int($5) - 1 ), 56, $2, $6, $7;
    ++match_counter;
}

END{
    if(match_counter > 0) printf "### The total number of matched records in the APNIC list: %d\r\n", match_counter;
}

############################## User-defined Functions ##############################
function NUMrize(ip){
    if( split(ip, IPs, ".") != 4 )
    {
        print "[IP Field Error]Total number of IP field is invalid";
        exit 4;
    }
    return int(IPs[1])*CLS_A + int(IPs[2])*CLS_B + int(IPs[3])*CLS_C + int(IPs[4]);
}

function IPrize(num){
    a = int(num/CLS_A);
    b = int( (num-a*CLS_A)/CLS_B );
    c = int( (num - a*CLS_A - b*CLS_B)/CLS_C );
    d = num - a*CLS_A - b*CLS_B - c*CLS_C;
    return sprintf("%03d.%03d.%03d.%03d", a, b, c, d );
}

function FillByZero(ip){
  gsub(" ", "", ip);
  if( split(ip, IPs, ".") != 4 ){
    print "[ERROR]Total number of IP field is invalid";
    exit 4;
  }
  return sprintf("%03d.%03d.%03d.%03d", int(IPs[1]), int(IPs[2]), int(IPs[3]), int(IPs[4]) );
}

流れとしては

  1. BEGIN ブロックで入力ファイルのセパレータ(FS)に ¦ を指定。定数定義とレコードカウンター(match_counter)を初期化
  2. /正規表現/ ブロックで正規表現にマッチする行(中国 ¦ 香港 ¦ 台湾 ¦ 日本)を処理対象に抽出し、終了IPアドレスを計算している。計算式は、10進数化した開始IPアドレス + 終了IPアドレスまでの差 - 1
  3. 終了時に処理した行数(match_counter)をコメント行として出力する
  • NUMrize関数はIPアドレスを引数に10進数化された数値を返す
  • IPrize関数は10進数の数を引数に見慣れたIPアドレス形式の文字列を返す

作成したawkスクリプトの実行は次のコマンドラインを実行するだけ

gawk -f apnic2ipfilter.awk delegated-apnic-latest

5000行弱が画面に出力されるハズ

058.000.000.000 - 058.001.255.255 , 056 , [JP]20050106_allocated
058.003.000.000 - 058.003.127.255 , 056 , [JP]20050304_allocated
058.003.128.000 - 058.003.255.255 , 056 , [JP]20060404_allocated
058.004.000.000 - 058.005.255.255 , 056 , [JP]20050118_allocated
058.012.000.000 - 058.013.255.255 , 056 , [JP]20050222_allocated
              中 略
222.249.176.000 - 222.249.191.255 , 056 , [CN]20050511_allocated
222.249.192.000 - 222.249.255.255 , 056 , [CN]20050511_allocated
222.250.000.000 - 222.250.255.255 , 056 , [TW]20040525_allocated
222.251.000.000 - 222.251.127.255 , 056 , [TW]20040525_allocated
### The total number of matched records in the APNIC list: 4769

4.IPアドレスの範囲が連続しているレコードを連結してファイルサイズを減らす

  • Part1 で出力されたデータを見てみると、連結可能なものが散見されます。例えば、終了IPアドレス 58.3.127.255 は次の開始IPアドレス 58.3.128.0 に連結してもよさそう(ファイルサイズも劇的に減ってエコです)。この処理をするのが次の awk スクリプト(ConnectRecords.awk
# Connect redundantly separated Records
#
# Date      : 2009-07-05
# Author    : k2jp
# Condition : Input records must be sorted in advance. There shuld NOT be overlaped IP ranges.
#
BEGIN{
    FS            = " , ";
#    OFS           = ",";
    CLS_A         = 16777216;
    CLS_B         = 65536;
    CLS_C         = 256;
    match_counter = 0;
    rec_counter   = 0;
    IP_start      = "";
    IP_end        = "";
    level         = 0;
    comment       = "";
}

/^[0-9]/ {
    ++match_counter;
    if( split($1, IPRange, " - ") != 2) {
        print "[ERROR]split error";
        exit 3;
    }

    # trim white space
    for(eachIP in IPRange) {
        gsub(" ", "", IPRange[eachIP]);
    }
    
    if(match_counter == 1){
        # The first record initialization
        IP_start     = IPRange[1];
        IP_end       = IPRange[2];
        level        = int($2);
        comment      = $3;
    }else if( NUMrize(IPRange[1]) != NUMrize(IP_end)+1 ){
        # This record is NOT connectable. Print previous.
        # 
        # Previous Record: IP_start|---------->IP_end
        # Current  Record:                       IPRange[1]|--------->IPRange[2]
        #
        ++rec_counter;
        printf "%-s - %-s , %03d , %s\r\n", IP_start, IP_end, level, comment;
        IP_start     = IPRange[1];
        IP_end       = IPRange[2];
        level        = int($2);
        comment      = $3;
    }else{
        # Connecting records
        # 
        # Previous Record: IP_start|----->IP_end
        # Current  Record:      IPRange[1]|---->IPRange[2]
        #
        IP_end       = IPRange[2];
        level = level < int($2) ? level : int($2);
        comment = sprintf("%s_%s", comment, $3);
    }
}

/^#+ *total number of matched records/ {
    # Check the number of match_counter. The same number is expected.
    if( match_counter != int(substr($0, index($0, ":")+1 )) ) {
        print "[ERROR]match_counter error", match_counter, int(substr($0, index($0, ":")+1 ));
        exit match_counter;
    }
}

END{
    if(match_counter > 0) {
        ++rec_counter;
        printf "%-s - %-s , %03d , %s\r\n", IP_start, IP_end, level, comment;
        # print statistics at final line.
        printf "### The total number of matched records: %d\tCompacted: %d\r\n", match_counter, rec_counter;
    }
}

############################## User-defined Functions ##############################
function NUMrize(ip){
    if( split(ip, IPs, ".") != 4 )
    {
        print "[ERROR]Total number of IP field is invalid";
        exit 4;
    }
    return int(IPs[1])*CLS_A + int(IPs[2])*CLS_B + int(IPs[3])*CLS_C + int(IPs[4]);
}

現在行以外に以前の行データを比較の為に保持するので、最初のスクリプトより面倒です

  1. BEGIN ブロックでは、セパレーター指定(FS)をカンマにして、定数定義及び短縮前(match_counter)短縮後(rec_counter)のカウンターを初期化。以前の行データを保持する IP_start 及び IP_end 変数の初期化等を行う
  2. /正規表現/ ブロックでは行の先頭が数字で始まる行に対し、
    1. 開始IP・終了IPを含む最初のフィールドを"-"で分割し、余白を削除
    2. マッチした最初の行はそのまま変数代入し保存
    3. マッチした2行目以降は連結可能かどうかを、以前の行の終了IPアドレスを10進数化して1加えた値と現在行の開始IPアドレスを10進数化した値とが一致するかどうかで判定しています。連結不可能なら以前の行を出力し、現在行のデータを変数に代入し初期化する。連結可能なら終了IPを現在行の終了IPに書き換え、第2フィールドの値をより小さな値にしたり、第3フィールドを連結したりしています
  3. 入力ファイルの最終行に入力行数がコメントされている場合、今回処理した行数と一致するか確認し、不一致なら何らかの問題が発生しているので処理をエラー終了
  4. END ブロックで、入力された有効行数(match_counter)と連結処理された行数(rec_counter)をコメント行にして追加

以上を Part 1 で処理した結果に適用したいのでパイプでつなぎ、結果をファイル(ipfilter.dat)に出力

gawk -f apnic2ipfilter.awk delegated-apnic-latest | gawk -f ConnectRecords.awk >ipfilter.dat

出力されたファイルの最終行を見てみると、4769行あった行数が1596行まで減ったので約65%削減できました

058.000.000.000 - 058.001.255.255 , 056 , [JP]20050106_allocated
058.003.000.000 - 058.005.255.255 , 056 , [JP]20050304_allocated_[JP]20060404_allocated_表示幅の都合で切捨
058.012.000.000 - 058.025.255.255 , 056 , [JP]20050222_allocated_[CN]20050224_allocated_表示幅の都合で切捨
058.030.000.000 - 058.063.255.255 , 056 , [CN]20050316_allocated_[CN]20050128_allocated_表示幅の都合で切捨
             中 略
222.166.000.000 - 222.229.079.255 , 056 , [HK]20040421_allocated_[HK]20041119_allocated_表示幅の都合で切捨
222.229.096.000 - 222.230.255.255 , 056 , [JP]20060915_allocated_[JP]20041130_allocated_表示幅の都合で切捨
222.231.064.000 - 222.231.255.255 , 056 , [JP]20051102_allocated_[JP]20051103_allocated
222.240.000.000 - 222.251.127.255 , 056 , [CN]20040326_allocated_[CN]20040427_allocated_表示幅の都合で切捨
### The total number of matched records: 4769 Compacted: 1596
5.使い方

作成した ipfilter.dat を対応アプリに読み込ませます

まとめ

2行で ipfilter.dat が作成できた

wget -nd -nc http://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest
gawk -f apnic2ipfilter.awk delegated-apnic-latest | gawk -f ConnectRecords.awk >ipfilter.dat
備考
  • 第3フィールドが長すぎるかもしれない。これはレコードを連結した際に内容をつなげるように処理しているため。awk で出力(printf)しているところで substr(comment, 1, 50) とすれば50文字以内に限定することができる