2012年11月22日

Python(Scapy)を使って'DHCP DISCOVER' STORMERを作ってみる

皆さんがお持ちの端末(PC、スマートフォン、モデムなど)がIPネットワーク上でオンラインになるまでには、幾つかの連続した手順(シーケンス)が必要で、IPアドレスをリースするDHCPサーバーもそのシーケンスの重要な一部です。

数十~数百の小さなLANで使う分には何を使っても良いのですが、大規模なネットワークを支えるDHCPサーバーには性能が求められます。

何故かと言えば、大規模なネットワークで障害や切り替えが発生した場合、復旧時には、まるで ”STORM” のように、一斉に複数の端末でオンライン・シーケンスが始まることがあるからです。
 
storm_ID-10074985

僕も大規模ネットワークに関わる仕事をしていて、DHCPサーバーの評価・検証・構築に関わる事があるんですが、無料で柔軟性のあるDHCPサーバーの性能評価ツールが少なくて困っていたんです。

しかし、よく考えるとScapyで作れば良いんじゃないの? と思い立ち、試しに”DHCP DISCOVER” STORMERを実装して見ることにしました。

Scapyのインストール


まずはネットワーク・パケット、各種プロコトルを簡易に生成・操作できるScapy(Python向けライブラリ)をインストールします。

ちなみに、僕はXPS 13(最上位モデル), Sputnik(Ubuntu 12.04ベース), Python 2.7を使っています。

$ 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

これで準備はおしまい。


DHCP DISCOVERメッセージをScapyでシンプルに書くと・・・


Scapyを使ってDHCP DISCOVERメッセージをシンプルに書いてみます。

#!/usr/bin/env python
 
from scapy.all import *

mac = "00:00:00:00:00:01"
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=68,dport=67)/
    BOOTP(chaddr=chaddr,xid=0x10000000)/
    DHCP(options=[('message-type','discover'),('end')])
    )
 
# debug
#discover.show()
 
sendp(discover,iface="eth0",verbose=2)

MACアドレスを00:00:00:00:00:01として、EthernetパケットのペイロードにIPパケット、またそのペイロードにUDPパケット・・・というように各レイヤー毎に簡潔に記述することができます。

BOOTPに設定するchaddr値はちょっと複雑でMACアドレスを8ビット単位に分割・10進数に変換後、chr()で文字化・再結合しています。

最後にsendp()で、OSI参照モデルでいう第二層(データリンク層)に、eth0からデータを送信します。
※第三層であればsend()

尚、sendp()は非同期なメソッドなのでこの時点で応答を受け取ることはできません。

discover.show()を実行すると、パケット内部のデータ構造を詳しく出力・確認することができます。

###[ Ethernet ]###
  dst       = ff:ff:ff:ff:ff:ff
  src       = 00:00:00:00:00:01
  type      = 0x800
###[ IP ]###
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = None
     src       = 0.0.0.0
     dst       = 255.255.255.255
     \options   \
###[ UDP ]###
        sport     = bootpc
        dport     = bootps
        len       = None
        chksum    = None
###[ BOOTP ]###
           op        = BOOTREQUEST
           htype     = 1
           hlen      = 6
           hops      = 0
           xid       = 268435456
           secs      = 0
           flags     = 
           ciaddr    = 0.0.0.0
           yiaddr    = 0.0.0.0
           siaddr    = 0.0.0.0
           giaddr    = 0.0.0.0
           chaddr    = '\x00\x00\x00\x00\x00\x01'
           sname     = ''
           file      = ''
           options   = 'c\x82Sc'
###[ DHCP options ]###
              options   = [message-type='discover' end]

じゃあ、早速これを改良してストームしてみましょう。


'DHCP DISCOVER' STORMER


こちらがSTORMERのコード。

#!/usr/bin/env python

from scapy.all import *
from time import sleep
import sys

conf.iface = "eth0"

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])

print "Starting DHCP DISCOVER storm... "

discovers = []
for i in range(num_clients):
  mac = str(RandMAC())
  chaddr = ''.join([chr(int(x,16)) for x in mac.split(':')])
  #print mac, caddr
  discover = (
      Ether(src=mac,dst="ff:ff:ff:ff:ff:ff")/
      IP(src="0.0.0.0",dst="255.255.255.255")/
      UDP(sport=68,dport=67)/
      BOOTP(chaddr=chaddr,xid=0x10000000)/
      DHCP(options=[('message-type','discover'),('end')])
      )
  discovers.append(discover)
  print "  client",i,mac

print "Total clients:",num_clients

print "Storming..."

start = time.time()

for discover in discovers:
  # debug
  #eframe.show()
  sendp(discover, verbose=0) # Send packet at layer 2

end = time.time()

print end - start

このコードでは、引数で指定された数(デフォルト10)だけダミーのDISCOVERパケットを作り出し、sendp()で非同期(一斉)に送信しています。

クライアントのMACアドレスが同じだと、DHCP OFFERされるIPも常に同じになってしまい都合が悪いので、RandMac()でランダムなMACアドレスを自動生成しています(Scapyはホント便利だなぁ)。
※先に全てのEtherオブジェクトをインスタンス化しておくのは、送信時の処理を出来るだけ少なくしてバースト性を上げるため。

後もうひとつ、DHCP OFFERメッセージが届いていることを確認する為にリスナーも用意してきます。

#!/usr/bin/env python

from scapy.all import *

conf.iface = "eth0"
MESSAGE_TYPE_OFFER = 2

count = 0;
def callback(packet):
  global count
  if DHCP in packet and packet[DHCP].options[0][1] == MESSAGE_TYPE_OFFER:
    count = count + 1
    dst_hwaddr =  packet.dst
    offered_addr = packet[BOOTP].yiaddr
    # debug
    #packet.show()
    print '%s DHCP OFFER: %s for %s from %s' % (count, offered_addr, dst_hwaddr, packet[IP].src)

print "Listening dhcp packet..."

sniff(prn=callback, filter="udp and (port 67 or 68)", store=0)

準備が出来たら先にリスナーを起動します。

$ sudo python listen.py
Listening dhcp packet... 

この状態でSTORMERを使って3つのDHCP DISCOVERメッセージをネットワークに流してみます。

$ sudo python dhcp-storm.py 3
Starting DHCP DISCOVER storm... 
  client 0 5c:78:34:66:5c:78
  client 1 5c:78:66:64:5c:78
  client 2 5c:78:63:37:5c:78
Total clients: 3
Storming...
0.106115818024

3つのクライアント(MACアドレス)になりすまし、約100ミリ秒で全てのリクエストを送信しました。

目を移してリスナー側の出力と見ると・・・

$ sudo python listen.py 
Listening dhcp packet...
1 DHCP OFFER: 10.0.0.33 for 5c:78:34:66:5c:78 from 10.0.0.1
2 DHCP OFFER: 10.0.0.34 for 5c:78:66:64:5c:78 from 10.0.0.1
3 DHCP OFFER: 10.0.0.35 for 5c:78:63:37:5c:78 from 10.0.0.1

お、確かにDHCP OFFERを受信してますね。 これで無事STORMERの動作確認できました。

念の為、DHCPサーバー側を見ると、こちらもオッケーですね。
(ここではDHCPサーバーにはISC-DHCP-SERVERを使っています)

[isc-dhcp-server]# tail -f /var/log/messages | grep -e DHCPDISCOVER -e DHCPOFFER
23:07:18 isc-dhcp-server dhcpd: DHCPDISCOVER from 5c:78:34:66:5c:78 via eth0
23:07:18 isc-dhcp-server dhcpd: DHCPDISCOVER from 5c:78:66:64:5c:78 via eth0
23:07:18 isc-dhcp-server dhcpd: DHCPDISCOVER from 5c:78:63:37:5c:78 via eth0
23:07:19 isc-dhcp-server dhcpd: DHCPOFFER on 10.0.0.33 to 5c:78:34:66:5c:78 via eth0
23:07:19 isc-dhcp-server dhcpd: DHCPOFFER on 10.0.0.34 to 5c:78:66:64:5c:78 via eth0
23:07:19 isc-dhcp-server dhcpd: DHCPOFFER on 10.0.0.35 to 5c:78:63:37:5c:78 via eth0

さあ、ここからが本番、クライアント数を1000にして、本当にSTORMERなのか確認してみます。
(注意:あくまでテスト環境で実行すること。一般ユーザが利用しているネットワーク上で実行するとDHCPサーバー側のリースIPプールが枯渇する可能性があります)

今回はリスナーは止め、STORMだけにリソースを集中させます。

$ sudo python dhcp-storm.py 1000
Starting DHCP DISCOVER storm... 
  client 0 a8:a5:1a:6f:19:cf
  - snip -
  client 999 ed:73:b1:02:8e:24
Total clients: 1000
Storming...
6.97294998169

お、おう・・・少し残念な結果に・・・。 1000クライアントに約7秒要しているので 143 discover/sec 程度のスピードしかありませんでした。
念の為、スレッド化もしてみましたが、逆に遅くなっちゃいました。

本当は 500 discover/sec ぐらいの性能が欲しかったので、これでは小型台風という感じですが、それでも色々と使えそうです。


DHCPオプションの改変(なりすまし)、DHCPACKまでの1トランザクション処理の話は、また今度!



DHCP―ホスト設定サーバの設定・運用・管理
江面 敦 矢吹 道郎 
テクノプレス 
売り上げランキング: 640984

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


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

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

コメントする

名前
URL
 
  絵文字