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;
}
}
}
このリストにはIPv6も記載されているし、日本以外のアジア諸国もすべて入っている。特定の国のIPだけ弾きたい、といったニーズならばそれに応じたフィルタを書けば同じように対応できる。 ↩
あるときA国で使われていたIPが、気がついたら何時の間にかB国に再割当てされていたということは割とある。 ↩
差分更新処理は端折っているため、実行すると各IPブロックのカウンターはすべてゼロ初期化される。 ↩
GeoIPモジュール http://dev.maxmind.com/geoip/ を使ったほうが手間は少ないし処理も高速だし設定もスッキリするが、DBファイルをアップデートしたらサービスリロードが必要になるのは変わらない。 ↩