日本国内限定IPv4アクセス制限の仕込み方

Webサービスとかやってると世界中と繋がっているのだなと思うことは多々ある。だがサービス内容によってはその範囲を狭めたいこともあるし、それ以前にお断りということもある。ではどうやって日本国内からのIPアクセスとそれ以外を区別しよう?


    IPアドレスリストをAPNICから入手する

    世界中に分配されているIPv4アドレスは総元締めのIANAから、世界を分割支配する5つの団体(ARIN/APNIC/LACNIC/AfriNIC/RIPE NCC)に分配され、そこからさらに各国別の元締め(日本ならJPNIC)に再分配されている。このIP分配リストは各団体のFTPサイト等で公開されているので、これを入手して日本にアサインされたIPリストだけを抽出すればACL(アクセス制限リスト)の元を作ることが出来る。今回の場合はJPNICに割り当てられたIPリストを得るために、APNICのFTPサイトから更新リストを入手する。

    この更新リスト1はほぼ毎日アップデートされている。ファイルサイズは1.8MBほどで約4万行弱ある。また世界中からダウンロードに来ることもあって、日に何回もアクセスにゆくと回数制限に引っ掛かって数時間ブロックされるということもある。故に基本的には1日1回、cronを使用してダウンロードすることになる。

    一方で毎日更新されているとはいえ、日本にアサインされたIPが日々こまめに変化しているというわけでもない。だいたい週に1〜2回程度、僅かな追加と削除2が発生している程度だ。そこでダウンロードと共に前回のリストと比較して変化があったらメール通知を行い、さらにACLの更新処理を行うという仕組みを作ることにする。

    なお以下の仕掛けはすべてrootユーザが行い、メインのワークエリアとしては/etc/rc.dを使用するものとする。

    #!/bin/sh
    
    ### /etc/rc.d/apnic.list_update
    ###
    ### delegated-apnic-latest を取得して変化があれば
    ### メール通知およびrc.iptablesの再実行を行うスクリプト
    ###
    ### /etc/cron.daily からsymlinkを張るか、そこに配置すること
    ###
    
    new=`mktemp`  
    errors=`mktemp`  
    DIR="/etc/rc.d"  
    list="$DIR/delegated-apnic-latest"
    
    test -f $list || touch $list  
    wget -q -O - "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest" > $new 2> $errors
    
    if [ $? -eq 0 ]; then  
        sort_new=`mktemp`
        sort_old=`mktemp`
        diff_out=`mktemp`
    
        # 日本国内IPについてのみ比較する
        /bin/grep '^apnic|JP|ipv4|' $new > $sort_new
        /bin/grep '^apnic|JP|ipv4|' $list > $sort_old
        diff --ignore-matching-lines="^#" $sort_new $sort_old > $diff_out
        if [ $? -ne 0 ]; then
            (
             echo '-------------------- old delegated-apnic-latest --------------------'
             grep -P '^\d' $list
             echo
             echo '-------------------- new delegated-apnic-latest --------------------'
             grep -P '^\d' $new
             echo '---------------------- difference ----------------------'
             cat $diff_out
    
             # メールは rootにだす( /etc/aliases に従う)
            ) | mail -s "delegated-apnic-latest updated $(hostname)" root
            cp -f $new $list
    
            # iptables の -s に渡せる形式に変換
            $DIR/jponly.pl < $list > $DIR/jponly.list
    
    #        # rc.iptables を再実行する
    #        \time $DIR/rc.iptables
    
             # iptables-save/restoreで更新する
             $DIR/update_mangle.pl
        fi
        rm -f $sort_new $sort_old $diff_out
    else  
        cat $errors | mail -s "delegated-apnic-latest update check error $(hostname)" root
    fi  
    rm -f $new $errors  
    

    このスクリプトは1日1回、APNICにお伺いを立てて(日本に割り当てられた)IPv4アドレスリストが変化していたら/etc/rc.d/delegated-apnic-latestファイルを更新する。通知メールはrootに向けて投げるため、/etc/aliasesに実際の通知先メールアドレスを書いておこう。

    取得したリストの中身は概ね以下のような行の羅列である。

    apnic|CN|ipv4|223.214.0.0|131072|20100803|allocated  
    apnic|JP|ipv4|223.216.0.0|262144|20100712|allocated  
    apnic|CN|ipv4|223.220.0.0|131072|20100723|allocated  
    apnic|KR|ipv4|223.222.0.0|65536|20100721|allocated  
    apnic|JP|ipv4|223.223.0.0|32768|20100803|allocated  
    apnic|IN|ipv4|223.223.128.0|8192|20100809|allocated  
    

    バーチカルラインで区切られた各カラムは次の意味を持つ。

    • IP管理組織
    • 2文字の国コード
    • IP種別
    • 割り当てブロック先頭のIPアドレス
    • 割り当てブロックに含まれるIP数(2の冪乗に一致)
    • 割り当てられた日付
    • 現在のステータス

    注意するのは5カラム目のIP数がIPマスクでもCIDRでもないという点で、ACLとして使用するにはこれをCIDRに変換する必要がある。これを行っているのが43行目のjponly.plだ。


    日本国内IPのCIDRを抽出する

    このフィルタスクリプトは、国コードがJPかつステータスがallocatedの行を抽出し、そのIP数からCIDRを計算して出力する。

    #!/usr/bin/perl
    use strict;  
    use utf8;
    
    ###
    ### /etc/rc.d/jponly.pl
    ###
    ### delegated-apnic-latest から日本国内IPのCIDRを抽出する
    ###
    
    my %CIDR;  
    for my $bit ( 0..32 ) {  
        my $len = 2 ** ( 32 - $bit );
        $CIDR{$len} = $bit;
    }
    while ( <> ) {  
        chomp;
        if ( m{^apnic\|JP\|ipv4\|(\d+(?:\.\d+){3})\|(\d+)\|\d+\|allocated} ) {
            my $addr = $1;
            my $cidr = $CIDR{ $2 || 32 };
            printf "%s/%u\n", $addr, $cidr;
        }
    }
    exit 0;  
    __END__  
    

    これを通して生成した/etc/rc.d/jponly.listは1行1ブロックのIP/CIDRリストとなる。

    223.216.0.0/14  
    223.223.0.0/17  
    

    iptablesでのACL

    これを用いて例えばiptablesのmangleテーブル・Jponlyチェイン(フィルタ)に書き込むには次のようにする。

    #!/bin/bash
    JPONLY=/etc/rc.d/jponly.list
    
    # mangleテーブルでフィルタを作る
    iptables -t mangle -N Jponly  
    cat $JPONLY | while read; do  
      iptables -t mangle -A Jponly -s $REPLY -j MARK --set-mark 2/2
    done
    
    # 上記のフィルタをINPUT/FORWARDに適用してパケットにmarkビットを立てる
    iptables -t mangle -A INPUT -j Jponly  
    iptables -t mangle -A FORWARD -j Jponly
    
    # 他のチェインの中で、markビットが立っているならACCEPT、なければDROPする例
    iptables -A Accept -m mark --mark 2/2 -j ACCEPT  
    iptables -A Accept -j DROP  
    

    mangleとmarkビットマスクを使うことで、このJponlyフィルタを容易に再利用できるようにしている。この他にホワイトリストチェイン・ブラックリストチェインを併用する場合も、markビットマスクを利用することで柔軟なACLフィルタを作り出すことが出来る。


    フィルタチェインの高速更新

    apnic.list_updateの49行目で呼び出しているupdate_mangle.plは、iptablesで設定した既存のJponlyチェインをアップデートするスクリプトだ。これはiptables-save・iptables-restoreコマンドを使用して高速に更新処理を行う。初回こそ前項のようにiptablesコマンドで丁寧に設定する必要があるが、これは数秒〜数十秒の処理時間が掛かってしまう。そこで2回目以降は該当チェインだけを書き換えることでダウンタイムを実用上問題ない時間(ミリ秒オーダー)まで切り詰める。3

    #!/usr/bin/perl
    use strict;
    
    my $JPONLY  = '/etc/rc.d/jponly.list';  
    my $UPDATE  = '/etc/rc.d/iptables.bak';  
    my $SAVE    = '/sbin/iptables-save -c';  
    my @RESTORE = ('/sbin/iptables-restore', '-c');
    
    my $SEARCH   = qr{-A Jponly -s};  
    my $TEMPLATE = "[0:0] -A Jponly -s %s -j MARK --set-xmark 0x2/0x2 \n";
    
    my $BEFORE;  
    my $AFTER = [];  
    my @INSERT;
    
    my $PIPE;  
    open $PIPE, '<', $JPONLY or die;  
    while (<$PIPE>) {  
        chomp;
        push @INSERT, sprintf $TEMPLATE, $_;
    }
    
    close $PIPE;  
    open $PIPE, '-|', $SAVE or die;  
    while (<$PIPE>) {  
        unless ($BEFORE) {
            if ($_ =~ $SEARCH) {
                $BEFORE = $AFTER;
                push @$BEFORE, @INSERT;
                next;
            }
        }
        elsif ($_ =~ $SEARCH) {
            next;
        }
        push @$AFTER, $_;
    }
    close $PIPE;
    
    open $PIPE, '>', $UPDATE or die;  
    print $PIPE @$BEFORE;  
    print $PIPE @$AFTER;  
    close $PIPE;
    
    system @RESTORE, $UPDATE;
    
    1;  
    __END__  
    

    ApacheでのACL

    前述のjponly.listからApache用のACLを作り出すには次のようにする。

    #!/bin/bash
    JPONLY=/etc/rc.d/jponly.list
    
    ( cat $JPONLY | while read; do
      echo "Allow from $REPLY"
    done ) > /etc/httpd/conf/jponly.conf  
    

    こうして生成したACLを、必要な場所でIncludeディレクティブを使用して読み込む。なおリストが更新されたらhttpdプロセスをreload(あるいはgraceful)しなければ実際には反映されない。4

    Order Allow,Deny  
    Allow from localhost  
    Allow from 192.168.0.0/16  
    Include conf/jponly.conf  
    Deny from All  
    

    nginxでのACL

    nginxでACLを掛けるには、geoモジュールを使用する。まずApacheの場合と同様にInclude可能なファイルを作成する。

    #!/bin/bash
    JPONLY=/etc/rc.d/jponly.list
    
    ( cat $JPONLY | while read; do
      echo "$REPLY 2;"
    done ) > /etc/nginx/jponly.conf  
    

    これをhttpブロックのgeoモジュールで読み込み、$acl変数に反映させる。これをlocationブロック内で参照して条件分岐する。なおリスト更新後はnginxプロセスをreloadしなければ実際には反映されない。

    http {  
        geo $acl {
            default        0;
            127.0.0.1      1;
            192.168.0.0/16 1;
            include jponly.conf;
        }
        server {
            location /jponly/ {
                root /var/www/html_jponly;
                if ($acl = 0) {
                    return 403;
                }
            }
            location / {
                root /var/www/html_public;
            }
        }
    }
    

    1. このリストにはIPv6も記載されているし、日本以外のアジア諸国もすべて入っている。特定の国のIPだけ弾きたい、といったニーズならばそれに応じたフィルタを書けば同じように対応できる。

    2. あるときA国で使われていたIPが、気がついたら何時の間にかB国に再割当てされていたということは割とある。

    3. 差分更新処理は端折っているため、実行すると各IPブロックのカウンターはすべてゼロ初期化される。

    4. GeoIPモジュール http://dev.maxmind.com/geoip/ を使ったほうが手間は少ないし処理も高速だし設定もスッキリするが、DBファイルをアップデートしたらサービスリロードが必要になるのは変わらない。

    RECENT LINKS