2012年12月13日
Python(Scapy)を使ってDHCP PERFECT(?) STORMERを作ってみる
先日作ったDHCP DISCOVER STORMER。 実はこれ、DHCPの性能測定という意味ではPERFECTとは言えません。 だって、 DISCOVERだけでACK(IPアドレス・リース成立)までの1トランザクション性能を測定することは出来ませんからね。
Python+Scapyで実装する以上、送受信性能に限界があるのは承知の上で、ACKまで計測できるPERFECT(?)なDHCP STORMERを完成させてみます。
Python+Scapyで実装する以上、送受信性能に限界があるのは承知の上で、ACKまで計測できるPERFECT(?)なDHCP STORMERを完成させてみます。
DHCP RELAY AGENTをエミュレートして少し便利なSTORMに
DHCPリレーエージェント、つまりDHCPメッセージ中に giaddr を設定できるとツールとして少し便利になります。
え、何故かって? 、普通、多数のDHCPクライアントをエミュレートするようなツールを動かす場合には、実際に運用・利用しているネットワークとは別に、物理的に分断されたブロードキャスト・ドメインを用意します。
面倒ですが、DHCPアドレス・プールを使い果たして他のクライアントに迷惑を掛けたらマズイですからね。
例えば、次のようなネットワークがあった場合、192.168.1.0/24 ネットワーク上のクライアントをエミュレートしてしまうと、最大でも250個程度しかないアドレスを一気に使い果たしてしまい問題になります。
しかし、もし、ツールがDHCPメッセージで giaddr (この例では 10.0.0.0/16 系のアドレス)を指定する事が出来るのであれば、実際に運用中で他のリアル・クライアントが存在するネットワーク上でも、別のDHCPアドレス・プールを使って気軽に試すことが出来るんです。
今回、DHCPサーバー側(ISC-DHCPDを利用)は、次のようにshared-networkブロックを使わず2つのサブネットを定義しておきます。
subnet 10.0.0.0 netmask 255.255.0.0 {
option routers 10.0.255.254;
option subnet-mask 255.255.0.0;
--- snip ---
range 10.0.0.10 10.0.255.250;
}
subnet 192.168.1.0 netmask 255.255.255.0 {
option routers 192.168.1.1;
option subnet-mask 255.255.255.0;
--- snip ---
range dynamic-bootp 192.168.1.11 192.168.1.100;
}
2つの subnet を shared-network ブロックで括ってしまうと、192.168.1.0/24 からもリースされてしまうので、注意して下さいね。
Scapyネットワーク・ライブラリのインストール
本家サイトから最新版をダウロードしてインストールする場合には次のコマンドを実行します。
$ wget http://www.secdev.org/projects/scapy/files/scapy-latest.tar.gz
$ tar xzf scapy-latest.tar.gz
$ cd scapy-.x.x/
$ sudo python setup.py install
もし、Ubuntuを利用しているのであれば、apt-getコマンドを使ってインストールすることも可能。
$ sudo apt-get install python-scapy
これで準備は出来ました。
(ちなみにPythonは2.7.3を使ってます)
DHCP PERFECT STORMER in Python, Scapy
実際のところ、性能面ではPERFECTでは無いんですが、用意したコードは次の通り。
#!/usr/bin/env python from scapy.all import * import threading import time import binascii import datetime import fpformat MESSAGE_TYPE_DISCOVER = 1 MESSAGE_TYPE_OFFER = 2 MESSAGE_TYPE_REQUEST = 3 MESSAGE_TYPE_ACK = 5 MESSAGE_TYPE_NAK = 6 MESSAGE_TYPE_RELEASE = 7 argv = sys.argv num_argvs = len(argv) NUM_MAX_CLIENT = 10 num_clients = NUM_MAX_CLIENT if num_argvs > 1: num_clients = int(argv[1]) conf.iface = "eth0" giaddr = "10.0.255.254" my_hwaddr = get_if_hwaddr(conf.iface) # option 82 = circuit-id:03 + remote-id(my hardware addr) opt82 = "0101030206"+my_hwaddr.replace(':','') num_discovers = 0; num_offers = 0; num_acks = 0; num_naks = 0; num_unkowns = 0; class DHCPDHandler(threading.Thread): def __init__(self): threading.Thread.__init__(self) def callbak(self, pkt): global num_offers global num_acks global num_naks global num_unkowns global giaddr global opt82 if DHCP in pkt: mtype = pkt[DHCP].options[0][1] yipaddr = pkt[BOOTP].yiaddr myhwaddr = pkt.dst if mtype == MESSAGE_TYPE_OFFER: num_offers = num_offers + 1 #print '%s DHCP OFFER(xid:%s): %s from %s' % (num_offers,pkt[BOOTP].xid,yipaddr,pkt[IP].src) request = ( Ether(src=myhwaddr,dst="ff:ff:ff:ff:ff:ff")/ IP(src="0.0.0.0",dst="255.255.255.255")/ UDP(sport=67,dport=67)/ BOOTP(giaddr=giaddr,chaddr=pkt[BOOTP].chaddr,xid=pkt[BOOTP].xid)/ DHCP( options=[ ('message-type','request'), ('requested_addr',yipaddr), ('relay_agent_Information',binascii.a2b_hex(opt82)), ('end')]) ) sendp(request,verbose=0) elif mtype == MESSAGE_TYPE_ACK: num_acks = num_acks + 1 #print '%s DHCP ACK(xid:%s): %s from %s' % (num_acks,pkt[BOOTP].xid,yipaddr,pkt[IP].src) elif mtype == MESSAGE_TYPE_NAK: num_naks = num_naks + 1 #print '%s DHCP NAK(xid:%s): %s from %s' % (num_acks,pkt[BOOTP].xid,yipaddr,pkt[IP].src) elif mtype != MESSAGE_TYPE_DISCOVER and mtype != MESSAGE_TYPE_REQUEST: num_unkowns = num_unkowns + 1 #print '%s DHCP UNKOWN(xid:%s): %s' % (num_unkowns,pkt[BOOTP].xid,mtype) def run(self): sniff(prn=self.callbak, filter="udp and port 67", store=0) class DHCPStormer(threading.Thread): def __init__(self, discovers): threading.Thread.__init__(self) self.discovers = discovers def run(self): global start global num_discovers start = time.time() global num_discovers for discover in self.discovers: sendp(discover, verbose=0) # Send packet at layer 2 num_discovers = num_discovers + 1 #time.sleep(0.1) # discover(as dhcp relay) packets discovers = [] xid = 1; for i in range(num_clients): mac = str(RandMAC()) chaddr = ''.join([chr(int(x,16)) for x in mac.split(':')]) discover = ( Ether(src=mac,dst="ff:ff:ff:ff:ff:ff")/ IP(src="0.0.0.0",dst="255.255.255.255")/ UDP(sport=67,dport=67)/ BOOTP(giaddr=giaddr,chaddr=chaddr,xid=xid)/ DHCP( options=[ ('message-type','discover'), ('relay_agent_Information',binascii.a2b_hex(opt82)), ('end')]) ) discovers.append(discover) print " client-%s %s" % (xid, mac) xid = xid + 1 print "total clients:",num_clients dh = DHCPDHandler() dh.daemon = True dh.start() time.sleep(1) ds = DHCPStormer(discovers) ds.daemon = True ds.start() print "%s\t%s\t%s\t%s\t%s" % ("time","disc","offer","ack","nak") while 1: cur = time.time() global start diff = cur - start print "%s\t%s\t%s\t%s\t%s" % (fpformat.fix(diff,1),num_discovers,num_offers,num_acks,num_naks) time.sleep(1)
じゃあ、早速コードの説明を。
まず、conf.ifaceでDHCPメッセージを送信するインタフェースに eth0 、giaddr には10.0.255.254を指定し、10.0.0.0/16上でIPアドレスには10.0.255.254が設定されているDHCPリレーエージェントをエミュレートしています。
opt82 とあるのは、DHCPメッセージに含めるリレーエージェント・インフォメーション(DHCPオプション82番)。 データ形式はサブ・オプション番号+データ部のバイト長+データの繰り返しで、ここでは最低限circuit-idに03、remote-idにMACアドレスを指定(0101+03+0206+MACアドレス6バイト)しています。
# option 82 = circuit-id:03 + remote-id(my hardware addr)
opt82 = "0101030206"+my_hwaddr.replace(':','')
本当は eth0 のMACアドレスでは無く、別のインタフェース(10.0.255.254)のMACアドレスが良いんですが、このあたりは適当です。
ここからは2つ(DHCPDHandler、DHCPStormer)のクラス定義が続きます。
DHCPDHandlerクラスは受信したパケットのDHCPメッセージ・タイプがOFFERであれば、REQUESTメッセージを送り返し、ACK、NAKであればグローバル変数をカウントアップしています。
DHCPStormerクラスは前と同じでインスタンス生成時に渡されたDISCOVERパケットを一斉に送信しています。
ちなみに、forインデント内でコメントアウトされているsleep部分を変更すると送信スピードを調整できますよ。
for discover in self.discovers:
sendp(discover, verbose=0) # Send packet at layer 2
num_discovers = num_discovers + 1
#time.sleep(0.1)
最後のwhileループでは1秒間間隔で統計値を出力しています。
早速、動かしてみましょうか。
まずは、プログラムを動かすPCにリレーエージェントのIPアドレスを設定(又は、エイリアスを追加)します。
$ sudo ifconfig eth0:0 10.0.255.254 netmask 255.255.0.0
続いて、DHCPサーバー側に10.0.0.0/16ネットワークへのルートを追加します。
route add -net 10.0.0.0 netmask 255.255.0.0 gw 192.168.1.100
※ここで指定した192.168.1.100はプログラムが動作するPCの192.168.1.0/16側IPアドレス。
この状態でコードをdhcp-perfect-storm.pyという名前で保存し、次のように実行してみます。
$ sudo python dhcp-perfect-storm.py 100
実行結果はこんな感じ。
$ sudo python dhcp-storm.py 100
client-1 96:86:34:08:45:b8
--- snip ---
client-100 05:34:4e:2c:f0:96
total clients: 100
time disc offer ack nak
0.0 0 0 0 0
1.0 10 1 1 0
2.0 18 11 11 0
3.0 26 19 19 0
4.0 34 27 27 0
5.0 42 35 35 0
6.0 51 45 45 0
7.0 60 51 51 0
8.0 68 61 61 0
9.0 76 71 71 0
10.0 86 79 79 0
11.0 95 90 89 0
12.0 100 98 98 0
13.0 100 100 100 0
自動生成されたクライアントのMACアドレスが出力された後、経過時間(秒)、DHCP DISCOVER、OFFER、ACK、NAK数が1秒毎に出力されます。
100クライアント分のDHCP ACKメッセージ受信までに約13秒ですから、7.7 トランザクション/秒の性能でした。
DISCOVERメッセージの送信がこれより多少速いぐらいですから、7.7 トランザクション/秒がISC-DHCPDの性能限界かというと、このプログラム・僕のPC性能も関係していそうで、もう少し時間を掛けて慎重に判断した方が良さそうです。
もちろん、DHCPサーバー側のログも確認しておきましょう。
# tail -f /var/log/messages(出力先ファイルは設定による。ここではCentOS/ISC-DHCPD標準設定の場合)
dhcpd: DHCPDISCOVER from 5c:78:64:66:5c:78 via 10.0.255.254
dhcpd: DHCPDISCOVER from 5c:78:61:39:5c:78 via 10.0.255.254
dhcpd: DHCPDISCOVER from 5c:78:32:34:5c:78 via 10.0.255.254
dhcpd: DHCPOFFER on 10.0.0.109 to 5c:78:64:66:5c:78 via 10.0.255.254
dhcpd: DHCPOFFER on 10.0.0.42 to 5c:78:61:39:5c:78 via 10.0.255.254
dhcpd: DHCPOFFER on 10.0.0.101 to 5c:78:32:34:5c:78 via 10.0.255.254
dhcpd: DHCPREQUEST for 10.0.0.109 from 5c:78:64:66:5c:78 via 10.0.255.254
dhcpd: DHCPACK on 10.0.0.109 to 5c:78:64:66:5c:78 via 10.0.255.254
dhcpd: DHCPREQUEST for 10.0.0.42 from 5c:78:61:39:5c:78 via 10.0.255.254
dhcpd: DHCPACK on 10.0.0.42 to 5c:78:61:39:5c:78 via 10.0.255.254
dhcpd: DHCPREQUEST for 10.0.0.101 from 5c:78:32:34:5c:78 via 10.0.255.254
dhcpd: DHCPACK on 10.0.0.101 to 5c:78:32:34:5c:78 via 10.0.255.254
それでは、また今度!
Pythonプロフェッショナルプログラミング
posted with amazlet at 12.12.13
ビープラウド
秀和システム
売り上げランキング: 81549
秀和システム
売り上げランキング: 81549
アジャイルサムライ-達人開発者への道-
posted with amazlet at 12.12.13
Jonathan Rasmusson
オーム社
売り上げランキング: 3031
オーム社
売り上げランキング: 3031