2012年12月13日

Python(Scapy)を使ってDHCP PERFECT(?) STORMERを作ってみる

先日作ったDHCP DISCOVER STORMER。 実はこれ、DHCPの性能測定という意味ではPERFECTとは言えません。 だって、 DISCOVERだけでACK(IPアドレス・リース成立)までの1トランザクション性能を測定することは出来ませんからね。

 perfect_storm

Python+Scapyで実装する以上、送受信性能に限界があるのは承知の上で、ACKまで計測できるPERFECT(?)なDHCP STORMERを完成させてみます。

DHCP RELAY AGENTをエミュレートして少し便利なSTORMに


DHCPリレーエージェント、つまりDHCPメッセージ中に giaddr を設定できるとツールとして少し便利になります。

え、何故かって? 、普通、多数のDHCPクライアントをエミュレートするようなツールを動かす場合には、実際に運用・利用しているネットワークとは別に、物理的に分断されたブロードキャスト・ドメインを用意します。

面倒ですが、DHCPアドレス・プールを使い果たして他のクライアントに迷惑を掛けたらマズイですからね。

例えば、次のようなネットワークがあった場合、192.168.1.0/24 ネットワーク上のクライアントをエミュレートしてしまうと、最大でも250個程度しかないアドレスを一気に使い果たしてしまい問題になります。


dhcp-storm-network1

しかし、もし、ツールが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プロフェッショナルプログラミング
ビープラウド 
秀和システム 
売り上げランキング: 81549

アジャイルサムライ-達人開発者への道-
Jonathan Rasmusson 
オーム社 
売り上げランキング: 3031

Posted by netbuffalo at 18:30│Comments(0)TrackBack(0)プログラミング | ネットワーク


この記事へのトラックバックURL

http://trackback.blogsys.jp/livedoor/netbuffalo/4303319

コメントする

名前
URL
 
  絵文字