国名コードから国名を調べる

一昨日の続き。
IP アドレスから国名コードを調べることはできた。でも、JP とか US くらいならわかるけど、ほかは国名コードを出力されても、結局どこの国なんだかわからない。例えばおとといの cidr.txt の最後はこうなっている。

takatoh@nightschool $ tail cidr.txt
ZW	196.43.96.0/19
ZW	196.43.199.0/24
ZW	196.44.176.0/20
ZW	196.201.1.0/24
ZW	196.220.96.0/19
ZW	196.223.26.0/24
ZW	197.157.204.0/22
ZW	197.214.120.0/21
ZW	197.221.224.0/19
ZW	197.231.212.0/22

ZW ってどこの国だ?

国名コードと国名のデータベース

国名コードの一覧は例えば Wikipedia にもあるので、それを調べれば ZW がどこの国かはわかる。けど、やりたいのはそんなことじゃなくて、コマンド一発で国名を出力して欲しいんだ。
まずは国名コードと国名を対応付けるデータベースが必要だ。データベースと言ってもそんな大げさなものじゃなくて CSV ファイルでいいんだけども。とはいえ、Wikipedia のページでは表になってるので、それをスクレイピングするなんてめんどくさくてやってられない。ところが、「国名コード CSV」でググったら、あるじゃないか、CSV。Qiita で公開してくれている人がいた。ありがとう、いい人だ。

 cf. 国コード一覧CSV ISO 3166-1 – Qiita

というわけで、ここのページをエディタにコピペして、country_code.csv ファイルに保存した。

Rubyスクリプト

データさえ準備できれば、あとは検索するスクリプトさえ書けばいい。こんなの大して考えるまでもない。ちょちょいと書いた。

# coding: utf-8

require 'csv'

def main
  cc_table = load_country_code("country_code.csv")
  cc = ARGV.shift
  puts cc_table[cc]
end

def load_country_code(csv)
  table = {}
  File.open("country_code.csv", "r") do |f|
    f.gets
    f.each do |line|
      fields = line.parse_csv
      table[fields[4]] = fields[0]
    end
  end
  table
end

main

country_code.csv がスクリプトを実行するディレクトリにあることを前提としている。

さて、試してみよう。

takatoh@nightschool $ ruby cc_to_country.rb JP
日本
takatoh@nightschool $ ruby cc_to_country.rb ZW
ジンバブエ

というわけで、ZW はジンバブエでした。

IPアドレスがどこの国に割り当てられたものかを調べる

昨日の延長上の話。ある IP アドレスが、どのサブネットに属しているかがわかれば、あとはそのサブネットがどの国に割り当てられたものかがわかればいい。

cidr.txt

何はともあれ、IP アドレスの国別割り当て状況がわからないと話にならない。でも、世の中良くしたもんで、データを公開してくれている人がいた。ありがたい。

 cf. 世界の国別 IPv4 アドレス割り当てリスト

このページから、cidr.txt をダウンロードした。
CIDR ってのは Classless Inter-Domain Routing の略で、サブネットマスクを使って IP アドレスの割り当てとルーティングをする仕組みのこと。また、例えば 192.168.1.0/24 というサブネットの表記を CIDR表記というようだ。簡単な説明はここ。

 cf. CIDRとは – IT用語辞典 e-Words

cidr.txt は国名コードと割り当てられているサブネットの CIDR表記の一覧になっている。冒頭の10行はこうなっている。

AD 85.94.160.0/19
AD 91.187.64.0/19
AD 109.111.96.0/19
AD 185.4.52.0/22
AD 194.158.64.0/19
AE 2.48.0.0/14
AE 5.30.0.0/15
AE 5.32.0.0/17
AE 5.38.0.0/17
AE 5.53.96.0/21

国名コードと CIDR表記のサブネットはタブ文字で区切られている。
ちなみに AD はアンドラ、AE はアラブ首長国連邦だそうだ。

Ruby版

cidr.txt を読み込んでハッシュに保存しておき、入力された IP アドレスが属するサブネットを探して、見つかったらその国名コードを出力する。cidr.txt が実行ディレクトリにおいてあるのを前提としている。

# coding: utf-8

def main
  cidr = load_cidr("cidr.txt")
  addr = ARGV.shift
  cidr.each do |subnet, country|
    if in_subnet?(addr, subnet)
      puts country
      break
    end
  end
end

def load_cidr(file)
  cidr = {}
  File.open(file) do |f|
    f.each do |line|
      co, subnet = line.chomp.split("\t")
      cidr[subnet] = co
    end
  end
  cidr
end

def ip_to_b(addr)
  addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("")
end

def in_subnet?(addr, subnet)
  addr_b = ip_to_b(addr)
  subnet_addr, mask = subnet.split("/")
  subnet_addr_b = ip_to_b(subnet_addr)
  mask = mask.to_i
  addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask)
end


main

IP アドレスの属するサブネットの検索が力技だけど、まあいいか。
実行結果。

takatoh@nightschool $ ruby -rresolv -e 'puts Resolv.getaddress("blog.panicblanket.com")'
210.224.185.170
takatoh@nightschool $ ruby addr_to_co.rb 210.224.185.170
JP

というわけで、このブログの IP アドレスは日本のものだってことが分かった。

Python版

同じやり方。

# coding: utf-8

import sys

def main():
    cidr = load_cidr("cidr.txt")
    addr = sys.argv[1]
    for subnet, co in cidr.items():
        if is_in_subnet(addr, subnet):
            print co
            break

def load_cidr(file):
    cidr = {}
    f = open(file)
    for line in f:
        co, subnet = line.rstrip().split("\t")
        cidr[subnet] = co
    return cidr

def ip_to_b(addr):
    return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split(".")))

def is_in_subnet(addr, subnet):
    addr_b = ip_to_b(addr)
    subnet_addr, mask = subnet.split("/")
    subnet_addr_b = ip_to_b(subnet_addr)
    mask = int(mask)
    return addr_b[:mask] == subnet_addr_b[:mask]


main()
takatoh@nightschool $ python addr_to_co.py 210.224.185.170
JP

IPアドレスがサブネットに含まれるかどうかを判定する

Ruby版

# coding: utf-8

def ip_to_b(addr)
  addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("")
end

def in_subnet?(addr, subnet)
  addr_b = ip_to_b(addr)
  subnet_addr, mask = subnet.split("/")
  subnet_addr_b = ip_to_b(subnet_addr)
  mask = mask.to_i
  addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask)
end

addr = "192.168.1.5"
subnet = "192.168.1.0/24"

puts in_subnet?(addr, subnet)

addr2 = "127.0.0.1"

puts in_subnet?(addr2, subnet)
takatoh@nightschool $ ruby is_in_subnet.rb
true
false

ちょっとトリッキーなのは、ip_to_b メソッドの sprintf のところ。頭に 0 をつけて8桁にするために、2進数(の文字列)にしたものを1と0だけからなる10進数だとみなして、%08d というフォーマットに変換している。これで IPアドレスの . で区切られた4つの部分のそれぞれが8桁(言い換えると8ビット)になる。
結果は上に示したとおり。192.18.1.5 はサブネット 192.168.1.0/24 に含まれるけど、127.0.0.1 は含まれない。

Python版

やってることは Ruby 版と同じ。

# coding: utf-8

def ip_to_b(addr):
    return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split(".")))

def is_in_subnet(addr, subnet):
    addr_b = ip_to_b(addr)
    subnet_addr, mask = subnet.split("/")
    subnet_addr_b = ip_to_b(subnet_addr)
    mask = int(mask)
    return addr_b[:mask] == subnet_addr_b[:mask]

addr = "192.168.1.5"
subnet = "192.168.1.0/25"

print is_in_subnet(addr, subnet)

addr2 = "127.0.0.1"

print is_in_subnet(addr2, subnet)

当然結果も同じ。

takatoh@nightschool $ python is_in_subnet.py
True
False

10進数の数値から2進数表現の文字列に変換する

Ruby では Fixnum#to_s メソッドに 2 を引数として与えると、2進数表現の文字列になる。
こんな機能あったんだ、知らなかった。

2.1.1 :001 > 5.to_s
 => "5" 
2.1.1 :002 > 5.to_s(2)
 => "101"

引数は 2 じゃなくてもいい。整数 n を与えると n を基数とした数値表現になる。

2.1.1 :003 > 5.to_s(10)
 => "5" 
2.1.1 :004 > 15.to_s(16)
 => "f" 
2.1.1 :005 > 36.to_s(36)
 => "10" 
2.1.1 :006 > 36.to_s(37)
ArgumentError: invalid radix 37
	from (irb):6:in `to_s'
	from (irb):6
	from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

基数36までは OK で、37になるとダメみたいだ。

一方、Python では2進数に変換する bin 関数がある。

>>> bin(5)
'0b101'

頭に 0b がついてしまうので、とってやる。

>>> bin(5)[2:]
'101'

ちなみに、8進数にするには oct、16進数にするには hex 関数がある。

>>> oct(15)
'017'
>>> hex(15)
'0xf'

Nginxで同一IPからのコネクションを制限する

limit_conn_zone ディレクティブと limit_conn ディレクティブを使う。
ここのページが参考になった。

 cf. http://wiki.nginx.org/HttpLimitConnModuleJa

まずはゾーンの設定。これは制限の対象を何にするかの定義みたい。/etc/nginx/nginx.conf の http セクションの中で定義する。こんな感じ。

limit_conn_zone $binary_remote_addr zone=addr:2m;

$binary_remote_addr という変数は Nginx に標準で定義されている変数で、アクセスして来たクライアントのIPアドレス(バイナリ)が入っている。これをキーにしている、つまり制限の対象にしているわけだ。で、その設定に、addr という名前をつけている。2m と書いてあるのは、キーを保存するためのメモリーを確保しているみたい。

さて、次は実際に制限を設定するほう。例えば、vhost.example.com というバーチャルホストに対するアクセスを制限したければ、/etc/nginx/sites-enabled/vhost.exmaple.com の
server セクションに次のように設定する。

limit_conn addr 1;

これで、同一IPからのコネクションを同時に1つだけに制限できる。

なんか情報が少なくて自信がないけど、EC2 で動かしてるサーバーで試してみたら、ちゃんと制限されて2つ目のコネクションには 503 が帰ってきたので、多分あってる。